收敛树命令与投影切换

This commit is contained in:
lix-2026
2026-04-30 06:58:17 +08:00
parent 3cc090ba5e
commit 8c895b3dc0
16 changed files with 980 additions and 281 deletions
+236 -191
View File
@@ -771,7 +771,7 @@ struct DocumentMoveSnapshotDocument {
#[serde(default, alias = "parent_id")]
parent_id: Option<String>,
#[serde(default, alias = "sort_order")]
sort_order: Option<i64>,
sort_order: Option<f64>,
#[serde(default, alias = "created_at")]
created_at: Option<String>,
}
@@ -904,12 +904,18 @@ fn normalize_parent_id(value: &Option<String>) -> Option<String> {
fn document_move_sort_key(document: &DocumentMoveSnapshotDocument) -> (i64, String, String) {
(
document.sort_order.unwrap_or(i64::MAX),
normalize_document_move_sort_order(document.sort_order).unwrap_or(i64::MAX),
document.created_at.clone().unwrap_or_default(),
document.id.clone(),
)
}
fn normalize_document_move_sort_order(value: Option<f64>) -> Option<i64> {
value
.filter(|number| number.is_finite())
.map(|number| number.floor() as i64)
}
fn clamp_document_move_index(sort_order: i64, max: usize) -> usize {
if sort_order < 0 {
return 0;
@@ -927,7 +933,7 @@ fn append_document_move_order_patches(
let sort_order = index as i64;
let moved = document.id == moved_document_id;
if normalize_parent_id(&document.parent_id) == parent_id
&& document.sort_order == Some(sort_order)
&& normalize_document_move_sort_order(document.sort_order) == Some(sort_order)
&& !moved
{
continue;
@@ -2286,11 +2292,15 @@ fn materialize_tree_domain_event_plans(
.map(|event_plans| {
event_plans
.iter()
.filter_map(|event_plan| materialize_tree_domain_event_value(event_plan, plan, result))
.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]))
.or_else(|| {
materialize_tree_domain_event_plan(plan, result).map(|event_plan| vec![event_plan])
})
.unwrap_or_default()
}
@@ -2467,36 +2477,36 @@ pub fn build_runtime_command_artifact_plan(
let domain_events: Vec<RuntimeDomainEventArtifactPlan> = materialized_event_plans
.iter()
.enumerate()
.map(|(index, (event_type, domain_event_plan))| {
RuntimeDomainEventArtifactPlan {
workspace_id: workspace_id.into(),
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.clone(),
event_type: event_type.clone(),
aggregate_type: aggregate_type.into(),
aggregate_id: aggregate_id.into(),
event_version: 1,
status: "committed".into(),
actor_type: context.actor.actor_type.clone(),
payload: tree_artifact_payload(
context,
command,
&event_type,
aggregate_type,
aggregate_id,
&domain_event_plan,
),
created_at: now.into(),
}
})
.map(
|(index, (event_type, domain_event_plan))| RuntimeDomainEventArtifactPlan {
workspace_id: workspace_id.into(),
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.clone(),
event_type: event_type.clone(),
aggregate_type: aggregate_type.into(),
aggregate_id: aggregate_id.into(),
event_version: 1,
status: "committed".into(),
actor_type: context.actor.actor_type.clone(),
payload: tree_artifact_payload(
context,
command,
&event_type,
aggregate_type,
aggregate_id,
&domain_event_plan,
),
created_at: now.into(),
},
)
.collect();
let domain_event = domain_events.first().cloned();
@@ -2507,7 +2517,12 @@ pub fn build_runtime_command_artifact_plan(
})
}
fn tree_domain_event_artifact_id(command_id: &str, event_type: &str, index: usize, total: usize) -> String {
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}");
}
@@ -2524,7 +2539,10 @@ fn tree_domain_event_artifact_id(command_id: &str, event_type: &str, index: usiz
format!("evt_{command_id}_{:02}_{suffix}", index + 1)
}
fn read_preflight_field<'a>(command: &'a RuntimeCommandEnvelopeWire, field: &str) -> Option<&'a Value> {
fn read_preflight_field<'a>(
command: &'a RuntimeCommandEnvelopeWire,
field: &str,
) -> Option<&'a Value> {
command
.preflight_data
.as_ref()
@@ -5168,7 +5186,9 @@ fn build_page_aggregate_projection_result(
source: PageAggregateSource,
) -> Result<PageAggregateProjection, BridgeError> {
if data.is_null() {
return Err(BridgeError::not_found("page.aggregate.get 未返回页面聚合数据"));
return Err(BridgeError::not_found(
"page.aggregate.get 未返回页面聚合数据",
));
}
let meta = data.get("meta").unwrap_or(data);
@@ -5248,9 +5268,8 @@ fn build_page_aggregate_projection_result(
.cloned()
.unwrap_or(Value::Null),
};
let layout_options = serde_json::to_value(&page_options).map_err(|error| {
BridgeError::transport(format!("PageOptions 序列化失败: {error}"))
})?;
let layout_options = serde_json::to_value(&page_options)
.map_err(|error| BridgeError::transport(format!("PageOptions 序列化失败: {error}")))?;
let revision_ref = revision
.as_u64()
.map(|value| format!("{resolved_document_id}:{value}"));
@@ -5275,7 +5294,9 @@ fn build_page_aggregate_projection_result(
title,
updated_at: updated_at.map(Value::String).unwrap_or(Value::Null),
permissions: PagePermissions {
read_only: bool_field(meta, "can_edit").map(|can_edit| !can_edit).unwrap_or(false),
read_only: bool_field(meta, "can_edit")
.map(|can_edit| !can_edit)
.unwrap_or(false),
disable_download: bool_field(meta, "disable_download")
.or_else(|| bool_field(meta, "disableDownload"))
.unwrap_or(false),
@@ -5353,11 +5374,7 @@ fn build_mindmap_projection_result(
.filter(|value| value.get("data").is_some() || value.get("children").is_some())
.unwrap_or(data);
let tree = normalize_mindmap_from_value(tree_input)?;
let root_node = tree
.data
.uid
.clone()
.unwrap_or_else(|| "root".into());
let root_node = tree.data.uid.clone().unwrap_or_else(|| "root".into());
let mut nodes = Vec::new();
let mut edges = Vec::new();
collect_mindmap_projection_rows(&tree, None, &mut nodes, &mut edges);
@@ -5414,10 +5431,12 @@ fn collect_mindmap_projection_rows(
}
fn search_documents_canonical_projection(evaluation: SearchDocumentsEvaluation) -> Value {
let mut payload = serde_json::to_value(evaluation).unwrap_or_else(|_| json!({
"enqueueAssetIds": [],
"results": []
}));
let mut payload = serde_json::to_value(evaluation).unwrap_or_else(|_| {
json!({
"enqueueAssetIds": [],
"results": []
})
});
if let Some(map) = payload.as_object_mut() {
map.insert("projectionOwner".into(), json!("rust-kernel"));
if let Some(results) = map.get_mut("results").and_then(Value::as_array_mut) {
@@ -7483,9 +7502,7 @@ fn execute_query_result(
let payload: MindmapGetQueryPayload = parse_payload(query_wire.payload)?;
let result = build_mindmap_projection_result(&data, &payload.mindmap_id)?;
serde_json::to_value(result).map_err(|error| {
BridgeError::transport(format!(
"mindmap.projection.get result 序列化失败: {error}"
))
BridgeError::transport(format!("mindmap.projection.get result 序列化失败: {error}"))
})
}
"search.documents" | "search.documents.query" => {
@@ -8167,14 +8184,10 @@ fn execute_command(
"documentId": payload.document_id,
}),
);
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_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,
@@ -8207,7 +8220,8 @@ fn execute_command(
} else {
"documents.embed"
};
let page_aggregate_embed_plan = build_page_aggregate_embed_plan(&command_wire, &payload)?;
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"))
@@ -9575,47 +9589,47 @@ mod tests {
"id": "block_1",
"type": "paragraph",
},
"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"
}
}
}
"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"
}
}
}
})
);
}
@@ -9775,7 +9789,10 @@ mod tests {
.expect("canonical search query result should build");
assert_eq!(result["projectionOwner"], json!("rust-kernel"));
assert_eq!(result["results"][0]["projectionOwner"], json!("rust-kernel"));
assert_eq!(
result["results"][0]["projectionOwner"],
json!("rust-kernel")
);
assert_eq!(result["results"][0]["id"], json!("page_1"));
}
@@ -9848,7 +9865,10 @@ mod tests {
RuntimeExecutionPlan::Command(plan) => {
assert_eq!(plan.command_name, "mindmap.command.apply");
assert_eq!(plan.function_name, "mindmaps:applyCommand");
assert_eq!(plan.args_json["canonicalCommand"], json!("mindmap.command.apply"));
assert_eq!(
plan.args_json["canonicalCommand"],
json!("mindmap.command.apply")
);
}
RuntimeExecutionPlan::Query(_) | RuntimeExecutionPlan::Tool(_) => {
panic!("expected command plan")
@@ -11514,11 +11534,11 @@ mod tests {
"content": [{ "id": "block_1", "type": "pageReference" }],
"expectedRevision": 5,
"conflictDetectionKey": "conflict_5",
"sourceDocumentId": "doc_1",
"targetDocumentId": "doc_2",
"anchorBlockId": "anchor_1",
"pageAggregateEmbedPlan": null,
"streamDeltaHint": {
"sourceDocumentId": "doc_1",
"targetDocumentId": "doc_2",
"anchorBlockId": "anchor_1",
"pageAggregateEmbedPlan": null,
"streamDeltaHint": {
"family": "tree",
"kind": "noop",
"args": {}
@@ -12009,6 +12029,31 @@ mod tests {
);
}
#[test]
fn document_move_order_plan_accepts_convex_float_sort_order_snapshot() {
let payload = DocumentMoveCommandPayload {
document_id: "doc_b".into(),
parent_id: None,
sort_order: 0,
};
let snapshot = serde_json::from_value::<DocumentMoveSnapshotPayload>(json!({
"documents": [
{ "id": "doc_a", "workspace_id": "ws_1", "parent_id": null, "sort_order": 0.0, "created_at": "2026-04-25T00:00:01Z" },
{ "id": "doc_b", "workspace_id": "ws_1", "parent_id": null, "sort_order": 1.0, "created_at": "2026-04-25T00:00:02Z" }
]
}))
.expect("snapshot with float sort_order");
let plan = build_document_move_order_plan_from_snapshot(&payload, &snapshot)
.expect("move order plan");
assert_eq!(plan.normalized_sort_order, 0);
assert!(plan
.patches
.iter()
.any(|patch| patch.document_id == "doc_b" && patch.sort_order == 0 && patch.moved));
}
#[test]
fn document_move_order_plan_normalizes_cross_parent_and_clamps_index() {
let payload = DocumentMoveCommandPayload {
@@ -12598,33 +12643,33 @@ mod tests {
page_id: Some("doc_2".into()),
block_id: None,
}),
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,
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,
},
})
@@ -12636,59 +12681,59 @@ mod tests {
assert_eq!(plan.command_name, "tree.node.embed");
assert_eq!(
plan.args_json,
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": {
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": {}