feat(tree): close rust family shell cutover

This commit is contained in:
lix-2026
2026-04-28 16:30:51 +08:00
parent 4ab36a9386
commit 7965c6c107
75 changed files with 9721 additions and 1174 deletions
+436 -91
View File
@@ -294,6 +294,8 @@ pub struct RuntimeCommandExecutionPlan {
pub struct RuntimeCommandArtifactPlan {
pub command_log: RuntimeCommandLogArtifactPlan,
pub domain_event: Option<RuntimeDomainEventArtifactPlan>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub domain_events: Vec<RuntimeDomainEventArtifactPlan>,
}
#[derive(Debug, Clone, Serialize, PartialEq)]
@@ -627,7 +629,8 @@ struct DocumentEmbedCommandPayload {
document_id: String,
workspace_id: Option<String>,
revision: Option<u64>,
content: Value,
#[serde(default)]
content: Option<Value>,
conflict_detection_key: Option<String>,
source_document_id: String,
target_document_id: String,
@@ -2247,6 +2250,32 @@ fn materialize_tree_domain_event_plan(
result: &Value,
) -> Option<(String, Value)> {
let event_plan = plan.args_json.get("domainEventPlan")?;
materialize_tree_domain_event_value(event_plan, plan, result)
}
fn materialize_tree_domain_event_plans(
plan: &RuntimeCommandExecutionPlan,
result: &Value,
) -> Vec<(String, Value)> {
plan.args_json
.get("domainEventPlans")
.and_then(Value::as_array)
.map(|event_plans| {
event_plans
.iter()
.filter_map(|event_plan| materialize_tree_domain_event_value(event_plan, plan, result))
.collect()
})
.filter(|event_plans: &Vec<(String, Value)>| !event_plans.is_empty())
.or_else(|| materialize_tree_domain_event_plan(plan, result).map(|event_plan| vec![event_plan]))
.unwrap_or_default()
}
fn materialize_tree_domain_event_value(
event_plan: &Value,
plan: &RuntimeCommandExecutionPlan,
result: &Value,
) -> Option<(String, Value)> {
if event_plan.get("family").and_then(Value::as_str) != Some("tree")
|| event_plan.get("schema").and_then(Value::as_str) != Some("mnote.tree.domain_event")
|| event_plan.get("schemaVersion").and_then(Value::as_i64) != Some(1)
@@ -2354,8 +2383,8 @@ pub fn build_runtime_command_artifact_plan(
.and_then(|target| target.block_id.as_ref())
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
let materialized_event_plan = materialize_tree_domain_event_plan(plan, result);
let command_payload = if let Some((_, domain_event_plan)) = &materialized_event_plan {
let materialized_event_plans = materialize_tree_domain_event_plans(plan, result);
let command_payload = if let Some((_, domain_event_plan)) = materialized_event_plans.first() {
if let Some(stream_delta) = domain_event_plan.get("streamDelta") {
let mut payload = command.payload.clone();
if let Some(map) = payload.as_object_mut() {
@@ -2374,6 +2403,18 @@ pub fn build_runtime_command_artifact_plan(
command.payload.clone()
};
let aggregate_type = if target_block_id.is_some() {
"block"
} else if target_page_id.is_some() {
"page"
} else {
"workspace"
};
let aggregate_id = target_block_id
.as_deref()
.or(target_page_id.as_deref())
.unwrap_or(workspace_id);
let command_log = RuntimeCommandLogArtifactPlan {
workspace_id: workspace_id.into(),
id: command_log_id.clone(),
@@ -2400,25 +2441,22 @@ pub fn build_runtime_command_artifact_plan(
finished_at: Some(now.into()),
};
let domain_event = materialized_event_plan.map(|(event_type, domain_event_plan)| {
let aggregate_type = if target_block_id.is_some() {
"block"
} else if target_page_id.is_some() {
"page"
} else {
"workspace"
};
let aggregate_id = target_block_id
.as_deref()
.or(target_page_id.as_deref())
.unwrap_or(workspace_id);
let domain_events: Vec<RuntimeDomainEventArtifactPlan> = materialized_event_plans
.iter()
.enumerate()
.map(|(index, (event_type, domain_event_plan))| {
RuntimeDomainEventArtifactPlan {
workspace_id: workspace_id.into(),
id: format!("evt_{}", command.command_id),
id: tree_domain_event_artifact_id(
&command.command_id,
event_type,
index,
materialized_event_plans.len(),
),
request_id: context.request_id.clone(),
trace_id: context.trace_id.clone(),
command_id: command.command_id.clone(),
command_log_id,
command_log_id: command_log_id.clone(),
event_type: event_type.clone(),
aggregate_type: aggregate_type.into(),
aggregate_id: aggregate_id.into(),
@@ -2435,14 +2473,110 @@ pub fn build_runtime_command_artifact_plan(
),
created_at: now.into(),
}
});
})
.collect();
let domain_event = domain_events.first().cloned();
Some(RuntimeCommandArtifactPlan {
command_log,
domain_event,
domain_events,
})
}
fn tree_domain_event_artifact_id(command_id: &str, event_type: &str, index: usize, total: usize) -> String {
if total <= 1 || index == 0 {
return format!("evt_{command_id}");
}
let suffix: String = event_type
.chars()
.map(|ch| {
if ch.is_ascii_alphanumeric() {
ch.to_ascii_lowercase()
} else {
'_'
}
})
.collect();
format!("evt_{command_id}_{:02}_{suffix}", index + 1)
}
fn read_preflight_field<'a>(command: &'a RuntimeCommandEnvelopeWire, field: &str) -> Option<&'a Value> {
command
.preflight_data
.as_ref()
.and_then(|preflight| preflight.get(field))
}
fn compose_content_with_blocks(content: &Value, blocks: Vec<Value>) -> Value {
if content.is_array() {
return Value::Array(blocks);
}
if let Some(map) = content.as_object() {
let mut next = map.clone();
next.insert("blocks".into(), Value::Array(blocks));
return Value::Object(next);
}
json!({ "blocks": blocks })
}
fn build_page_aggregate_embed_plan(
command_wire: &RuntimeCommandEnvelopeWire,
payload: &DocumentEmbedCommandPayload,
) -> Result<Option<Value>, BridgeError> {
let Some(input) = read_preflight_field(command_wire, "pageAggregateEmbed") else {
return Ok(None);
};
let target_content = input
.get("targetContent")
.ok_or_else(|| BridgeError::validation("pageAggregateEmbed 缺少 targetContent"))?;
let source_title = read_trimmed_str_field(input, "sourceTitle").unwrap_or("无标题");
let anchor_block_id = payload
.anchor_block_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.or_else(|| read_trimmed_str_field(input, "anchorBlockId"));
let block_id = read_trimmed_str_field(input, "blockId")
.map(ToOwned::to_owned)
.unwrap_or_else(|| format!("page_ref_{}", payload.source_document_id));
let current_blocks = normalize_blocks_from_value(target_content);
let insert_index = anchor_block_id
.and_then(|anchor_id| {
current_blocks.iter().position(|block| {
read_trimmed_str_field(block, "id")
.map(|block_id| block_id == anchor_id)
.unwrap_or(false)
})
})
.map(|index| index + 1)
.unwrap_or(current_blocks.len());
let page_reference_block = json!({
"id": block_id,
"type": "pageReference",
"props": {
"pageId": payload.source_document_id,
"title": source_title,
},
});
let mut next_blocks = Vec::with_capacity(current_blocks.len() + 1);
next_blocks.extend(current_blocks.iter().take(insert_index).cloned());
next_blocks.push(page_reference_block.clone());
next_blocks.extend(current_blocks.iter().skip(insert_index).cloned());
let next_content = compose_content_with_blocks(target_content, next_blocks);
Ok(Some(json!({
"schema": "mnote.page_aggregate.embed_plan",
"schemaVersion": 1,
"sourceDocumentId": payload.source_document_id,
"targetDocumentId": payload.target_document_id,
"anchorBlockId": anchor_block_id,
"insertIndex": insert_index,
"block": page_reference_block,
"content": next_content,
"blockCount": normalize_blocks_from_value(&next_content).len(),
})))
}
#[derive(Debug, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct RuntimeBlockSummary {
@@ -6288,6 +6422,8 @@ fn build_file_tree_projection_result(
filters: &KernelProjectionFilter,
) -> KernelProjectionResult {
let (assets_by_doc, asset_by_id, child_assets_by_parent) = build_file_tree_assets(data);
let requested_query = normalize_projection_query(filters.query.as_deref());
let requested_max_results = filters.max_results;
let page_ids = subtree
.nodes
.iter()
@@ -6595,6 +6731,8 @@ fn build_file_tree_projection_result(
}
apply_file_tree_projection_search_filter(&mut items, &mut edges, filters);
let visible_rows = items.len();
let visible_edges = edges.len();
KernelProjectionResult {
projection_id: format!(
@@ -6605,6 +6743,31 @@ fn build_file_tree_projection_result(
root_node_id: root_node_id.map(ToOwned::to_owned),
items,
edges,
meta: BTreeMap::from([(
"search".into(),
json!({
"query": requested_query.clone(),
"maxResults": requested_max_results,
"maxResultsRule": "matches_only_before_ancestor_completion",
"ancestorCompletion": "include_all_ancestors_after_match_truncation",
"ordering": "kernel_file_tree_preorder",
"indexingVisibility": {
"schema": "mnote.file_tree.indexing_visibility",
"schemaVersion": 1,
"source": "kernel.project_view",
"status": "visible",
"requestKey": requested_query
.as_ref()
.map(|query| format!("{}:{query}", root_node_id.unwrap_or("root"))),
"indexedResourceKinds": ["document", "index", "asset", "asset_folder", "mindmap", "book", "pdf"],
"visibleResourceKinds": ["document", "index", "asset", "asset_folder", "mindmap", "book", "pdf"],
"metrics": {
"visibleRows": visible_rows,
"visibleEdges": visible_edges,
},
},
}),
)]),
}
}
@@ -6883,6 +7046,7 @@ fn build_kernel_projection_result(
root_node_id: root_node_id.map(ToOwned::to_owned),
items,
edges: subtree.edges,
meta: BTreeMap::new(),
})
}
@@ -7677,11 +7841,24 @@ fn execute_command(
"documentId": payload.document_id,
}),
);
let domain_event_payload = document_save_domain_event_payload(
let page_body_event_payload = document_save_page_body_domain_event_payload(
&payload,
&editor_document,
);
let snapshot_event_payload = document_save_snapshot_domain_event_payload(
&payload,
&canonical_content,
)?;
let page_body_event_plan = tree_domain_event_plan_with_payload(
"page.body.saved",
page_body_event_payload,
stream_delta_hint.clone(),
);
let snapshot_event_plan = tree_domain_event_plan_with_payload(
"document.snapshot.saved",
snapshot_event_payload,
stream_delta_hint.clone(),
);
json!({
"id": payload.document_id,
"content": canonical_content,
@@ -7691,11 +7868,8 @@ fn execute_command(
"conflictDetectionKey": payload.conflict_detection_key,
"streamDeltaHint": stream_delta_hint,
"domainEventHint": tree_domain_event_hint("page.body.saved"),
"domainEventPlan": tree_domain_event_plan_with_payload(
"page.body.saved",
domain_event_payload,
stream_delta_hint,
),
"domainEventPlan": page_body_event_plan.clone(),
"domainEventPlans": [page_body_event_plan, snapshot_event_plan],
})
},
}))
@@ -7707,6 +7881,17 @@ fn execute_command(
} else {
"documents.embed"
};
let page_aggregate_embed_plan = build_page_aggregate_embed_plan(&command_wire, &payload)?;
let embed_content = page_aggregate_embed_plan
.as_ref()
.and_then(|plan| plan.get("content"))
.cloned()
.or_else(|| payload.content.clone())
.ok_or_else(|| {
BridgeError::validation(format!(
"{command_name} 缺少 content 或 pageAggregateEmbed preflight"
))
})?;
let command = CommandEnvelope {
name: command_name.into(),
command_id: command_wire.command_id.clone(),
@@ -7718,7 +7903,7 @@ fn execute_command(
page_id: payload.document_id.clone(),
workspace_id: payload.workspace_id.clone(),
revision: payload.revision,
content_json: serde_json::to_string(&payload.content).map_err(|error| {
content_json: serde_json::to_string(&embed_content).map_err(|error| {
BridgeError::validation(format!(
"{command_name} content 序列化失败: {error}"
))
@@ -7743,12 +7928,13 @@ fn execute_command(
payload_json: request.payload_json,
args_json: json!({
"id": payload.document_id,
"content": payload.content,
"content": embed_content,
"expectedRevision": payload.revision,
"conflictDetectionKey": payload.conflict_detection_key,
"sourceDocumentId": payload.source_document_id,
"targetDocumentId": payload.target_document_id,
"anchorBlockId": payload.anchor_block_id,
"pageAggregateEmbedPlan": page_aggregate_embed_plan,
"streamDeltaHint": tree_stream_delta_hint("noop", json!({})),
"domainEventHint": tree_domain_event_hint("tree.node.embedded"),
"domainEventPlan": tree_domain_event_plan(
@@ -7979,8 +8165,12 @@ fn execute_command(
"documents.move" | "tree.subtree.move" => {
let payload: DocumentMoveCommandPayload = parse_payload(command_wire.payload.clone())?;
validate_document_move_legality(&payload, command_wire.preflight_data.as_ref())?;
let normalized_move =
let move_order_plan =
resolve_document_move_order_plan(&payload, command_wire.preflight_data.as_ref())?;
let tree_write_operation = document_move_write_operation(
context.workspace_id.as_deref(),
move_order_plan.as_ref(),
);
let command_name = if command_wire.name == "tree.subtree.move" {
"tree.subtree.move"
} else {
@@ -8014,11 +8204,7 @@ fn execute_command(
"id": payload.document_id,
"parentId": payload.parent_id,
"sortOrder": payload.sort_order,
"normalizedMove": normalized_move,
"treeWriteOperation": document_move_write_operation(
context.workspace_id.as_deref(),
normalized_move.as_ref(),
),
"treeWriteOperation": tree_write_operation,
"streamDeltaHint": tree_stream_delta_hint("move_document", json!({
"documentId": payload.document_id,
"parentId": payload.parent_id,
@@ -8792,9 +8978,24 @@ fn editor_document_block_ids(document: &EditorBlockDocument) -> Vec<String> {
ids
}
fn document_save_domain_event_payload(
fn document_save_page_body_domain_event_payload(
payload: &DocumentSaveCommandPayload,
editor_document: &EditorBlockDocument,
) -> Value {
json!({
"page": {
"id": payload.document_id.clone(),
"workspaceId": payload.workspace_id.clone(),
},
"blocks": {
"ids": editor_document_block_ids(editor_document),
"count": editor_document.blocks.len(),
},
})
}
fn document_save_snapshot_domain_event_payload(
payload: &DocumentSaveCommandPayload,
canonical_content: &Value,
) -> Result<Value, BridgeError> {
Ok(json!({
@@ -8807,10 +9008,6 @@ fn document_save_domain_event_payload(
"contentHash": stable_json_content_hash(canonical_content)?,
"updatedAt": Value::Null,
},
"blocks": {
"ids": editor_document_block_ids(editor_document),
"count": editor_document.blocks.len(),
},
}))
}
@@ -9023,26 +9220,47 @@ mod tests {
"id": "block_1",
"type": "paragraph",
},
"streamDeltaHint": {
"family": "tree",
"kind": "noop",
"args": {}
},
"domainEventHint": {
"family": "tree",
"eventType": "tree.block.patched"
},
"domainEventPlan": {
"family": "tree",
"schema": "mnote.tree.domain_event",
"schemaVersion": 1,
"eventType": "tree.block.patched",
"streamDeltaHint": {
"family": "tree",
"kind": "noop",
"args": {}
}
}
"streamDeltaHint": {
"family": "tree",
"kind": "resync_required",
"args": {
"reason": "blocks.patch",
"documentId": "doc_1",
"blockId": "block_1"
}
},
"domainEventHint": {
"family": "tree",
"eventType": "block.patched"
},
"domainEventPlan": {
"family": "tree",
"schema": "mnote.tree.domain_event",
"schemaVersion": 1,
"eventType": "block.patched",
"payload": {
"document": {
"id": "doc_1",
"workspaceId": "ws_1"
},
"block": {
"id": "block_1"
},
"patch": {
"summary": "replace_block",
"nextType": "paragraph"
}
},
"streamDeltaHint": {
"family": "tree",
"kind": "resync_required",
"args": {
"reason": "blocks.patch",
"documentId": "doc_1",
"blockId": "block_1"
}
}
}
})
);
}
@@ -10197,11 +10415,6 @@ mod tests {
"id": "doc_1",
"workspaceId": "ws_1"
},
"snapshot": {
"version": 8,
"contentHash": "fnv1a64:d039f8f3496411e8",
"updatedAt": null
},
"blocks": {
"ids": ["legacy_content_1"],
"count": 1
@@ -10218,6 +10431,62 @@ mod tests {
}
})
);
assert_eq!(
plan.args_json["domainEventPlans"],
json!([
{
"family": "tree",
"schema": "mnote.tree.domain_event",
"schemaVersion": 1,
"eventType": "page.body.saved",
"payload": {
"page": {
"id": "doc_1",
"workspaceId": "ws_1"
},
"blocks": {
"ids": ["legacy_content_1"],
"count": 1
}
},
"streamDeltaHint": {
"family": "tree",
"kind": "resync_required",
"args": {
"reason": "page_body_saved",
"pageId": "doc_1",
"documentId": "doc_1"
}
}
},
{
"family": "tree",
"schema": "mnote.tree.domain_event",
"schemaVersion": 1,
"eventType": "document.snapshot.saved",
"payload": {
"page": {
"id": "doc_1",
"workspaceId": "ws_1"
},
"snapshot": {
"version": 8,
"contentHash": "fnv1a64:d039f8f3496411e8",
"updatedAt": null
}
},
"streamDeltaHint": {
"family": "tree",
"kind": "resync_required",
"args": {
"reason": "page_body_saved",
"pageId": "doc_1",
"documentId": "doc_1"
}
}
}
])
);
}
#[test]
@@ -10610,10 +10879,11 @@ mod tests {
"content": [{ "id": "block_1", "type": "pageReference" }],
"expectedRevision": 5,
"conflictDetectionKey": "conflict_5",
"sourceDocumentId": "doc_1",
"targetDocumentId": "doc_2",
"anchorBlockId": "anchor_1",
"streamDeltaHint": {
"sourceDocumentId": "doc_1",
"targetDocumentId": "doc_2",
"anchorBlockId": "anchor_1",
"pageAggregateEmbedPlan": null,
"streamDeltaHint": {
"family": "tree",
"kind": "noop",
"args": {}
@@ -11177,7 +11447,7 @@ mod tests {
}
#[test]
fn tree_subtree_move_command_includes_normalized_move_plan_from_snapshot() {
fn tree_subtree_move_command_does_not_emit_legacy_normalized_move_fallback() {
let plan = execute_runtime_input(RuntimeInput::Command {
context: demo_context(),
command: RuntimeCommandEnvelopeWire {
@@ -11226,9 +11496,15 @@ mod tests {
assert_eq!(plan.command_name, "tree.subtree.move");
assert_eq!(plan.function_name, "documents:move");
assert_eq!(plan.args_json["sortOrder"], json!(-2));
assert!(plan.args_json.get("normalizedMove").is_none());
assert_eq!(
plan.args_json["normalizedMove"],
plan.args_json["treeWriteOperation"],
json!({
"family": "tree",
"schema": "mnote.tree.write_operation",
"schemaVersion": 1,
"operation": "tree.subtree.move.write",
"workspaceId": "ws_1",
"documentId": "doc_b",
"fromParentId": "source",
"toParentId": "target",
@@ -11711,20 +11987,33 @@ mod tests {
page_id: Some("doc_2".into()),
block_id: None,
}),
payload: json!({
"documentId": "doc_2",
"workspaceId": "ws_1",
"revision": 5,
"content": [{ "id": "block_1", "type": "pageReference" }],
"conflictDetectionKey": "conflict_5",
"sourceDocumentId": "doc_1",
"targetDocumentId": "doc_2",
"anchorBlockId": "anchor_1",
}),
preflight_data: None,
reason: Some("树命令嵌入页面".into()),
refs: vec![],
dry_run: false,
payload: json!({
"documentId": "doc_2",
"workspaceId": "ws_1",
"revision": 5,
"conflictDetectionKey": "conflict_5",
"sourceDocumentId": "doc_1",
"targetDocumentId": "doc_2",
"anchorBlockId": "anchor_1",
}),
preflight_data: Some(json!({
"pageAggregateEmbed": {
"sourceDocumentId": "doc_1",
"sourceTitle": "来源页面",
"targetDocumentId": "doc_2",
"targetContent": {
"blocks": [
{ "id": "anchor_1", "type": "paragraph" }
],
"format": "editor"
},
"anchorBlockId": "anchor_1",
"blockId": "page_ref_doc_1"
}
})),
reason: Some("树命令嵌入页面".into()),
refs: vec![],
dry_run: false,
validate_only: false,
},
})
@@ -11736,15 +12025,59 @@ mod tests {
assert_eq!(plan.command_name, "tree.node.embed");
assert_eq!(
plan.args_json,
json!({
"id": "doc_2",
"content": [{ "id": "block_1", "type": "pageReference" }],
"expectedRevision": 5,
"conflictDetectionKey": "conflict_5",
"sourceDocumentId": "doc_1",
"targetDocumentId": "doc_2",
"anchorBlockId": "anchor_1",
"streamDeltaHint": {
json!({
"id": "doc_2",
"content": {
"blocks": [
{ "id": "anchor_1", "type": "paragraph" },
{
"id": "page_ref_doc_1",
"type": "pageReference",
"props": {
"pageId": "doc_1",
"title": "来源页面"
}
}
],
"format": "editor"
},
"expectedRevision": 5,
"conflictDetectionKey": "conflict_5",
"sourceDocumentId": "doc_1",
"targetDocumentId": "doc_2",
"anchorBlockId": "anchor_1",
"pageAggregateEmbedPlan": {
"schema": "mnote.page_aggregate.embed_plan",
"schemaVersion": 1,
"sourceDocumentId": "doc_1",
"targetDocumentId": "doc_2",
"anchorBlockId": "anchor_1",
"insertIndex": 1,
"block": {
"id": "page_ref_doc_1",
"type": "pageReference",
"props": {
"pageId": "doc_1",
"title": "来源页面"
}
},
"content": {
"blocks": [
{ "id": "anchor_1", "type": "paragraph" },
{
"id": "page_ref_doc_1",
"type": "pageReference",
"props": {
"pageId": "doc_1",
"title": "来源页面"
}
}
],
"format": "editor"
},
"blockCount": 2
},
"streamDeltaHint": {
"family": "tree",
"kind": "noop",
"args": {}
@@ -13181,6 +13514,18 @@ mod tests {
result["projectionId"],
json!("kernel_projection:file_tree:page_root")
);
assert_eq!(
result["meta"]["search"]["indexingVisibility"]["schema"],
json!("mnote.file_tree.indexing_visibility")
);
assert_eq!(
result["meta"]["search"]["indexingVisibility"]["status"],
json!("visible")
);
assert_eq!(
result["meta"]["search"]["indexingVisibility"]["metrics"]["visibleRows"],
json!(items.len())
);
assert_eq!(
item_by_row_id["doc:page_root"]["rowKind"],
json!("document")