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
@@ -0,0 +1,99 @@
import { describe, expect, it } from "vitest";
import {
assertDocumentMoveWriteOperationMatches,
buildDocumentMoveOrderPlanFromDocuments,
} from "../../../convex/_utils/documentMoveOrder";
describe("documentMoveOrder Convex helper", () => {
it("assertDocumentMoveWriteOperationMatches 应只接受 Rust tree write operation,并返回内部 move plan", () => {
const actual = buildDocumentMoveOrderPlanFromDocuments({
documents: [
{
id: "doc_a",
parent_id: "source",
sort_order: 0,
created_at: "2026-04-25T00:00:01Z",
},
{
id: "doc_b",
parent_id: "source",
sort_order: 1,
created_at: "2026-04-25T00:00:02Z",
},
],
documentId: "doc_b",
parentId: null,
sortOrder: 0,
});
const normalized = assertDocumentMoveWriteOperationMatches(
{
family: "tree",
schema: "mnote.tree.write_operation",
schemaVersion: 1,
operation: "tree.subtree.move.write",
workspaceId: "ws_1",
documentId: "doc_b",
fromParentId: "source",
toParentId: null,
requestedSortOrder: 0,
normalizedSortOrder: 0,
patches: [
{
documentId: "doc_b",
parentId: null,
sortOrder: 0,
moved: true,
},
],
},
actual,
);
expect(normalized).toEqual({
documentId: "doc_b",
fromParentId: "source",
toParentId: null,
requestedSortOrder: 0,
normalizedSortOrder: 0,
patches: [
{
documentId: "doc_b",
parentId: null,
sortOrder: 0,
moved: true,
},
],
});
expect(() => assertDocumentMoveWriteOperationMatches({ ...actual, operation: "documents.move" }, actual)).toThrow(
"Rust move write operation 与 Convex 当前排序状态不一致",
);
});
it("assertDocumentMoveWriteOperationMatches 应拒绝缺少正式 write operation 外壳的旧 move plan", () => {
const actual = buildDocumentMoveOrderPlanFromDocuments({
documents: [
{
id: "doc_a",
parent_id: "source",
sort_order: 0,
created_at: "2026-04-25T00:00:01Z",
},
{
id: "doc_b",
parent_id: "source",
sort_order: 1,
created_at: "2026-04-25T00:00:02Z",
},
],
documentId: "doc_b",
parentId: null,
sortOrder: 0,
});
expect(() => assertDocumentMoveWriteOperationMatches(actual, actual)).toThrow(
"Rust move write operation 与 Convex 当前排序状态不一致",
);
});
});
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import {
assertDocumentMoveOrderPlanMatches,
assertDocumentMoveWriteOperationMatches,
buildDocumentMoveOrderPlanFromDocuments,
} from "../../../convex/_utils/documentMoveOrder";
@@ -61,7 +61,7 @@ describe("documentMoveOrder", () => {
});
});
it("normalizedMove 与当前排序状态不一致时拒绝执行", () => {
it("treeWriteOperation 与当前排序状态不一致时拒绝执行", () => {
const actual = buildDocumentMoveOrderPlanFromDocuments({
documents: [
{
@@ -89,8 +89,13 @@ describe("documentMoveOrder", () => {
});
expect(() =>
assertDocumentMoveOrderPlanMatches(
assertDocumentMoveWriteOperationMatches(
{
family: "tree",
schema: "mnote.tree.write_operation",
schemaVersion: 1,
operation: "tree.subtree.move.write",
workspaceId: "ws_1",
...actual,
patches: actual.patches.map((patch) =>
patch.documentId === "doc_c" ? { ...patch, sortOrder: 9 } : patch,
@@ -98,6 +103,6 @@ describe("documentMoveOrder", () => {
},
actual,
),
).toThrow("Rust move plan 与 Convex 当前排序状态不一致");
).toThrow("Rust move write operation 与 Convex 当前排序状态不一致");
});
});
@@ -15,13 +15,10 @@ import {
recordBridgeCommandFailureArtifacts,
recordRustBridgeCommandArtifacts,
} from "@/lib/documents/bridge-log";
import { composeContentWithBlocks, extractBlocksFromContent } from "@/lib/document-content";
import {
executeRustBridgeMutationTransport,
resolveRustBridgeCommandPlan,
} from "@/lib/documents/rust-runtime";
import { buildDocumentSavePayload } from "@/lib/documents/save-contract";
import type { Json } from "@/types/supabase";
export type DocumentCreatePayload = {
documentId: string;
@@ -82,10 +79,7 @@ export type DocumentEmbedPayload = {
documentId: string;
workspaceId: string | null;
revision: number | null;
content: Json;
conflictDetectionKey: string | null;
snapshotCapturedAt: string | null;
blockCount: number | null;
sourceDocumentId: string;
targetDocumentId: string;
anchorBlockId: string | null;
@@ -275,47 +269,20 @@ export async function executeDocumentEmbedBridgeCommand(request: Request): Promi
}
const targetMeta = await client.query(api.documents.getMeta, { id: normalizedTargetId });
const currentBlocks = extractBlocksFromContent(targetContent.content);
const anchorId = trimOrNull((targetMeta as { embed_default_block_id?: string | null } | null)?.embed_default_block_id);
const anchorIndex =
anchorId
? currentBlocks.findIndex(
(block) => typeof block === "object" && block !== null && String((block as { id?: string }).id ?? "") === anchorId,
)
: -1;
const insertIndex = anchorIndex >= 0 ? anchorIndex + 1 : currentBlocks.length;
const nextBlocks: Json[] = [
...currentBlocks.slice(0, insertIndex),
{
id: safeRandomId(),
type: "pageReference",
props: {
pageId: normalizedSourceId,
title: sourceDoc.title ?? "无标题",
},
},
...currentBlocks.slice(insertIndex),
];
const payload: Json = composeContentWithBlocks(targetContent.content, nextBlocks);
const workspaceId = normalizeWorkspaceId(sourceDoc.workspace_id) ?? normalizeWorkspaceId((targetMeta as { workspace_id?: string | null } | null)?.workspace_id);
const context = await buildRuntimeContext(request, workspaceId);
const embedPayload: DocumentEmbedPayload = {
...buildDocumentSavePayload({
documentId: normalizedTargetId,
workspaceId,
revision:
typeof targetContent.revision === "number" && Number.isInteger(targetContent.revision)
? targetContent.revision
: null,
content: payload,
conflictDetectionKey:
typeof targetContent.conflict_detection_key === "string"
? targetContent.conflict_detection_key
: null,
blockCount: nextBlocks.length,
}),
documentId: normalizedTargetId,
workspaceId,
revision:
typeof targetContent.revision === "number" && Number.isInteger(targetContent.revision)
? targetContent.revision
: null,
conflictDetectionKey:
typeof targetContent.conflict_detection_key === "string"
? targetContent.conflict_detection_key
: null,
sourceDocumentId: normalizedSourceId,
targetDocumentId: normalizedTargetId,
anchorBlockId: anchorId,
@@ -324,6 +291,16 @@ export async function executeDocumentEmbedBridgeCommand(request: Request): Promi
name: "documents.embed",
payload: embedPayload,
context,
preflightData: {
pageAggregateEmbed: {
sourceDocumentId: normalizedSourceId,
sourceTitle: sourceDoc.title ?? "无标题",
targetDocumentId: normalizedTargetId,
targetContent: targetContent.content,
anchorBlockId: anchorId,
blockId: safeRandomId(),
},
},
target: {
workspaceId,
pageId: normalizedTargetId,
@@ -373,14 +373,14 @@ export async function handleDocumentCreateRequest(request: Request): Promise<Nex
export async function handleDocumentMoveRequest(request: Request): Promise<NextResponse> {
assertServerEnvironment();
const requestClone = request.clone();
let normalizedMove: NormalizedDocumentMovePayload | null = null;
let movePayload: NormalizedDocumentMovePayload | null = null;
let failureClient: Awaited<ReturnType<typeof getAuthedConvexClient>>["client"] | null = null;
let failureAuthUserId: string | null = null;
let failureSourceDocument: MovePreflightDocument | null = null;
try {
const payload = (await request.json()) as MovePayload;
normalizedMove = normalizeDocumentMovePayload(payload);
const documentId = normalizedMove.documentId;
movePayload = normalizeDocumentMovePayload(payload);
const documentId = movePayload.documentId;
if (!documentId) {
return NextResponse.json({ error: "缺少 documentId" }, { status: 400 });
}
@@ -402,14 +402,14 @@ export async function handleDocumentMoveRequest(request: Request): Promise<NextR
const movePreflight = await buildMovePreflight({
client,
sourceDocument: failureSourceDocument,
targetParentId: normalizedMove.parentId,
targetParentId: movePayload.parentId,
});
const envelope = buildDocumentCommandEnvelope({
name: "documents.move",
payload: {
documentId,
parentId: normalizedMove.parentId,
sortOrder: normalizedMove.sortOrder,
parentId: movePayload.parentId,
sortOrder: movePayload.sortOrder,
movePreflight,
},
preflightData: movePreflight,
@@ -438,7 +438,7 @@ export async function handleDocumentMoveRequest(request: Request): Promise<NextR
return NextResponse.json({ ok: true });
} catch (error) {
try {
const fallbackMove = normalizedMove
const fallbackMove = movePayload
?? normalizeDocumentMovePayload(
(await requestClone.json().catch(() => ({}))) as MovePayload,
);
@@ -5,6 +5,7 @@ import {
executeRustBridgeMutationTransport,
materializeRustTreeStreamDelta,
readRustTreeDomainEventPlan,
readRustTreeDomainEventPlans,
readRustTreeDomainEventType,
type RustBridgeCommandPlan,
} from "./rust-runtime";
@@ -83,7 +84,7 @@ describe("shouldUseBuiltBridgeRuntimeBinary", () => {
describe("executeRustBridgeMutationTransport", () => {
it("documents.move 应把 Rust treeWriteOperation 透传给 Convex 写执行器", async () => {
const normalizedMove = {
const movePlan = {
documentId: "doc_b",
fromParentId: "source",
toParentId: "target",
@@ -104,7 +105,7 @@ describe("executeRustBridgeMutationTransport", () => {
schemaVersion: 1,
operation: "tree.subtree.move.write",
workspaceId: "ws_1",
...normalizedMove,
...movePlan,
};
const mutation = vi.fn().mockResolvedValue({ ok: true });
const plan: RustBridgeCommandPlan = {
@@ -122,7 +123,6 @@ describe("executeRustBridgeMutationTransport", () => {
id: "doc_b",
parentId: "target",
sortOrder: 0,
normalizedMove,
treeWriteOperation,
},
};
@@ -137,7 +137,6 @@ describe("executeRustBridgeMutationTransport", () => {
id: "doc_b",
parentId: "target",
sortOrder: 0,
normalizedMove,
treeWriteOperation,
});
});
@@ -645,7 +644,7 @@ describe("readRustTreeDomainEventType", () => {
});
});
it("应保留 Rust formal domainEventPlan payload schema", () => {
it("应保留 Rust formal domainEventPlan payload schema,并拆出 snapshot 独立事件", () => {
const plan: RustBridgeCommandPlan = {
kind: "command",
commandName: "page.body.save",
@@ -668,11 +667,6 @@ describe("readRustTreeDomainEventType", () => {
id: "doc_1",
workspaceId: "ws_1",
},
snapshot: {
version: 8,
contentHash: "fnv1a64:d039f8f3496411e8",
updatedAt: null,
},
blocks: {
ids: ["block_1"],
count: 1,
@@ -687,6 +681,57 @@ describe("readRustTreeDomainEventType", () => {
},
},
},
domainEventPlans: [
{
family: "tree",
schema: "mnote.tree.domain_event",
schemaVersion: 1,
eventType: "page.body.saved",
payload: {
page: {
id: "doc_1",
workspaceId: "ws_1",
},
blocks: {
ids: ["block_1"],
count: 1,
},
},
streamDeltaHint: {
family: "tree",
kind: "resync_required",
args: {
reason: "page_body_saved",
pageId: "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",
},
},
},
],
},
};
@@ -700,11 +745,6 @@ describe("readRustTreeDomainEventType", () => {
id: "doc_1",
workspaceId: "ws_1",
},
snapshot: {
version: 8,
contentHash: "fnv1a64:d039f8f3496411e8",
updatedAt: null,
},
blocks: {
ids: ["block_1"],
count: 1,
@@ -719,6 +759,22 @@ describe("readRustTreeDomainEventType", () => {
},
},
});
expect(readRustTreeDomainEventPlans(plan).map((eventPlan) => eventPlan.eventType)).toEqual([
"page.body.saved",
"document.snapshot.saved",
]);
expect(readRustTreeDomainEventPlans(plan)[0]?.payload).not.toHaveProperty("snapshot");
expect(readRustTreeDomainEventPlans(plan)[1]?.payload).toMatchObject({
page: {
id: "doc_1",
workspaceId: "ws_1",
},
snapshot: {
version: 8,
contentHash: "fnv1a64:d039f8f3496411e8",
updatedAt: null,
},
});
});
});
@@ -796,11 +852,6 @@ describe("buildRustBridgeCommandArtifactPlan", () => {
id: "doc_1",
workspaceId: "ws_1",
},
snapshot: {
version: 8,
contentHash: "fnv1a64:d039f8f3496411e8",
updatedAt: null,
},
blocks: {
ids: ["block_1"],
count: 1,
@@ -815,6 +866,57 @@ describe("buildRustBridgeCommandArtifactPlan", () => {
},
},
},
domainEventPlans: [
{
family: "tree",
schema: "mnote.tree.domain_event",
schemaVersion: 1,
eventType: "page.body.saved",
payload: {
page: {
id: "doc_1",
workspaceId: "ws_1",
},
blocks: {
ids: ["block_1"],
count: 1,
},
},
streamDeltaHint: {
family: "tree",
kind: "resync_required",
args: {
reason: "page_body_saved",
pageId: "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",
},
},
},
],
},
},
result: {
@@ -842,6 +944,10 @@ describe("buildRustBridgeCommandArtifactPlan", () => {
});
expect(artifactPlan?.commandLog.commandId).toBe("cmd_artifact_1");
expect(artifactPlan?.domainEvent?.commandId).toBe("cmd_artifact_1");
expect(artifactPlan?.domainEvents?.map((event) => event.eventType)).toEqual([
"page.body.saved",
"document.snapshot.saved",
]);
expect(artifactPlan?.domainEvent).toMatchObject({
id: "evt_cmd_artifact_1",
eventType: "page.body.saved",
@@ -854,11 +960,6 @@ describe("buildRustBridgeCommandArtifactPlan", () => {
id: "doc_1",
workspaceId: "ws_1",
},
snapshot: {
version: 8,
contentHash: "fnv1a64:d039f8f3496411e8",
updatedAt: null,
},
blocks: {
ids: ["block_1"],
count: 1,
@@ -870,5 +971,30 @@ describe("buildRustBridgeCommandArtifactPlan", () => {
},
},
});
expect(artifactPlan?.domainEvent?.payload).not.toHaveProperty("snapshot");
expect(artifactPlan?.domainEvents?.[1]).toMatchObject({
id: "evt_cmd_artifact_1_02_document_snapshot_saved",
eventType: "document.snapshot.saved",
payload: {
schema: "mnote.tree.domain_event",
schemaVersion: 1,
eventType: "document.snapshot.saved",
command_id: "cmd_artifact_1",
page: {
id: "doc_1",
workspaceId: "ws_1",
},
snapshot: {
version: 8,
contentHash: "fnv1a64:d039f8f3496411e8",
updatedAt: null,
},
streamDelta: {
op: "resync_required",
reason: "page_body_saved",
pageId: "doc_1",
},
},
});
}, 30_000);
});
@@ -161,6 +161,7 @@ export type RustBridgeDomainEventArtifactPlan = {
export type RustBridgeCommandArtifactPlan = {
commandLog: RustBridgeCommandLogArtifactPlan;
domainEvent: RustBridgeDomainEventArtifactPlan | null;
domainEvents?: RustBridgeDomainEventArtifactPlan[];
};
export type RustBridgeToolPlanStep = {
@@ -555,6 +556,14 @@ function readOptionalRecordArg(argsJson: Record<string, unknown>, field: string)
return isRecord(value) ? value : null;
}
function readRequiredRecordArg(argsJson: Record<string, unknown>, field: string) {
const value = argsJson[field];
if (isRecord(value)) {
return value;
}
throw new DocumentBridgeError(`Rust runtime 缺少 ${field}`, 500, "TRANSPORT_ERROR");
}
function readOptionalBooleanField(source: Record<string, unknown>, field: string) {
const value = source[field];
return typeof value === "boolean" ? value : undefined;
@@ -670,6 +679,24 @@ export function readRustTreeDomainEventType(plan: RustBridgeCommandPlan): string
export function readRustTreeDomainEventPlan(plan: RustBridgeCommandPlan): RustTreeDomainEventPlan | null {
const eventPlan = plan.argsJson.domainEventPlan;
return normalizeRustTreeDomainEventPlan(eventPlan);
}
export function readRustTreeDomainEventPlans(plan: RustBridgeCommandPlan): RustTreeDomainEventPlan[] {
const eventPlans = plan.argsJson.domainEventPlans;
if (Array.isArray(eventPlans)) {
const normalized = eventPlans
.map((eventPlan) => normalizeRustTreeDomainEventPlan(eventPlan))
.filter((eventPlan): eventPlan is RustTreeDomainEventPlan => Boolean(eventPlan));
if (normalized.length > 0) {
return normalized;
}
}
const single = readRustTreeDomainEventPlan(plan);
return single ? [single] : [];
}
function normalizeRustTreeDomainEventPlan(eventPlan: unknown): RustTreeDomainEventPlan | null {
if (!isRecord(eventPlan) || eventPlan.family !== "tree") {
return null;
}
@@ -715,6 +742,24 @@ export function materializeRustTreeDomainEventPlan(input: {
};
}
export function materializeRustTreeDomainEventPlans(input: {
plan: RustBridgeCommandPlan;
result: unknown;
streamDelta?: RustTreeStreamDelta | null;
}): RustTreeDomainEventPlan[] {
const eventPlans = readRustTreeDomainEventPlans(input.plan);
const streamDelta =
input.streamDelta ??
materializeRustTreeStreamDelta({
plan: input.plan,
result: input.result,
});
return eventPlans.map((eventPlan) => ({
...eventPlan,
...(streamDelta ? { streamDelta } : {}),
}));
}
export function materializeRustTreeStreamDelta(input: {
plan: RustBridgeCommandPlan;
result: unknown;
@@ -882,8 +927,14 @@ export async function persistRustBridgeCommandArtifacts(input: {
) => Promise<unknown>;
await mutation(bridgeLogsApi.bridgeLogs.recordCommandLog, artifacts.commandLog as unknown as Record<string, unknown>);
if (artifacts.domainEvent) {
await mutation(bridgeLogsApi.bridgeLogs.recordDomainEvent, artifacts.domainEvent as unknown as Record<string, unknown>);
const domainEvents =
artifacts.domainEvents && artifacts.domainEvents.length > 0
? artifacts.domainEvents
: artifacts.domainEvent
? [artifacts.domainEvent]
: [];
for (const domainEvent of domainEvents) {
await mutation(bridgeLogsApi.bridgeLogs.recordDomainEvent, domainEvent as unknown as Record<string, unknown>);
}
}
@@ -1151,12 +1202,7 @@ export async function executeRustBridgeMutationTransport<TResult>(input: {
id: assertStringArg(input.plan.argsJson, "id"),
parentId: readOptionalStringArg(input.plan.argsJson, "parentId"),
sortOrder: readRequiredNumberArg(input.plan.argsJson, "sortOrder"),
...("normalizedMove" in input.plan.argsJson
? { normalizedMove: input.plan.argsJson.normalizedMove }
: {}),
...("treeWriteOperation" in input.plan.argsJson
? { treeWriteOperation: input.plan.argsJson.treeWriteOperation }
: {}),
treeWriteOperation: readRequiredRecordArg(input.plan.argsJson, "treeWriteOperation"),
});
case "documents:softDelete":
return mutation(api.documents.softDelete, {
@@ -21,6 +21,23 @@ describe("fetchKernelFileTreeProjection", () => {
rootNodeId: "page_root",
items: [{ rowId: "asset:table_1" }],
edges: [],
meta: {
search: {
indexingVisibility: {
schema: "mnote.file_tree.indexing_visibility",
schemaVersion: 1,
source: "kernel.project_view",
status: "visible",
requestKey: "page_root:预算",
indexedResourceKinds: ["document", "index", "asset"],
visibleResourceKinds: ["document", "index", "asset"],
metrics: {
visibleRows: 1,
visibleEdges: 0,
},
},
},
},
},
}),
{ status: 200 },
@@ -44,6 +61,11 @@ describe("fetchKernelFileTreeProjection", () => {
}),
);
expect(result.items.map((item) => item.rowId)).toEqual(["asset:table_1"]);
expect(result.meta?.search?.indexingVisibility).toMatchObject({
schema: "mnote.file_tree.indexing_visibility",
status: "visible",
requestKey: "page_root:预算",
});
});
it("固定 file_tree 搜索语义边界:命中数先截断,祖先补全不计入 maxResults", () => {
@@ -29,12 +29,36 @@ export type KernelFileTreeProjectionEdge = {
toNodeId: string;
};
export type KernelFileTreeIndexingVisibility = {
schema: "mnote.file_tree.indexing_visibility";
schemaVersion: 1;
source: "kernel.project_view";
status: "visible" | "stale" | "refreshing" | "unknown";
requestKey: string | null;
indexedResourceKinds: string[];
visibleResourceKinds: string[];
metrics: {
visibleRows: number;
visibleEdges: number;
};
};
export type KernelFileTreeProjection = {
projectionId: string;
projection: "file_tree";
rootNodeId: string | null;
items: KernelFileTreeProjectionItem[];
edges: KernelFileTreeProjectionEdge[];
meta?: {
search?: {
query?: string | null;
maxResults?: number | null;
maxResultsRule?: "matches_only_before_ancestor_completion";
ancestorCompletion?: "include_all_ancestors_after_match_truncation";
ordering?: "kernel_file_tree_preorder";
indexingVisibility?: KernelFileTreeIndexingVisibility;
};
};
};
type BuildKernelFileTreeProjectionInput = {
@@ -574,5 +598,27 @@ export function buildKernelFileTreeProjection(
rootNodeId,
items,
edges,
meta: {
search: {
query: null,
maxResults: null,
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: null,
indexedResourceKinds: ["document", "index", "asset", "asset_folder", "mindmap", "book", "pdf"],
visibleResourceKinds: ["document", "index", "asset", "asset_folder", "mindmap", "book", "pdf"],
metrics: {
visibleRows: items.length,
visibleEdges: edges.length,
},
},
},
},
};
}
@@ -0,0 +1,40 @@
import { describe, expect, it } from "vitest";
import { TREE_3000_ROUTE_BOUNDARY_MANIFEST } from "./tree-route-boundary";
describe("TREE_3000_ROUTE_BOUNDARY_MANIFEST", () => {
it("固定 3000 route 的 thin proxy 与 compat pending 边界", () => {
expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST).toMatchObject({
schema: "mnote.tree.3000_route_boundary",
schemaVersion: 1,
publicEntry: "3000",
});
expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST.routes).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: "tree.commands",
role: "next-thin-proxy",
route: "/api/tree/commands",
}),
expect.objectContaining({
id: "tree.stream",
role: "next-thin-proxy",
route: "/api/tree/stream",
}),
expect.objectContaining({
id: "tree.shell.debug",
role: "compat-pending",
route: "/api/tree/shell",
}),
]),
);
expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST.rustOwnedSemantics).toContain("tree.subtree.move");
expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST.nextThinProxyDuties).toContain(
"Rust command result 回包整形",
);
expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST.browserSubstrateDuties).toContain(
"新页面 scaffold 文件创建",
);
expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST.forbiddenNextSemantics).toContain("树排序 canonical plan");
});
});
@@ -0,0 +1,77 @@
export type TreeRouteBoundaryRole =
| "rust-owned"
| "next-thin-proxy"
| "browser-substrate"
| "compat-pending";
export type TreeRouteBoundaryItem = {
id: string;
role: TreeRouteBoundaryRole;
route: string;
owner: "rust-runtime" | "next-3000" | "convex-substrate";
description: string;
};
export const TREE_3000_ROUTE_BOUNDARY_MANIFEST = {
schema: "mnote.tree.3000_route_boundary",
schemaVersion: 1,
publicEntry: "3000",
routes: [
{
id: "tree.commands",
role: "next-thin-proxy",
route: "/api/tree/commands",
owner: "next-3000",
description:
"浏览器公开树命令入口,只负责认证、payload envelope、Rust command transport、artifact writer 调用与必要副作用调度。",
},
{
id: "tree.stream",
role: "next-thin-proxy",
route: "/api/tree/stream",
owner: "next-3000",
description:
"浏览器 SSE/polling 入口,只转发 bridge log/domain event cursor 与 Rust 产出的 streamDelta,缺少稳定 delta 时保守 resync。",
},
{
id: "tree.shell.debug",
role: "compat-pending",
route: "/api/tree/shell",
owner: "next-3000",
description:
"仅服务显式 debug/internal runtime 验证;3000 主路径使用 same-origin inline host,不应默认请求 mnote-web:3104。",
},
] satisfies TreeRouteBoundaryItem[],
rustOwnedSemantics: [
"tree.node.create",
"tree.node.rename",
"tree.subtree.move",
"tree.node.archive",
"tree.node.restore",
"tree.node.purge",
"tree.subtree.copy",
"tree.node.embed",
],
nextThinProxyDuties: [
"cookie/auth 读取与 Convex client 获取",
"workspace bootstrap",
"CommandEnvelope 构造与 Rust runtime transport",
"Rust command result 回包整形",
"Rust artifact writer 调用",
"tree.subtree.move 的 sidebar snapshot preflight 数据采集",
],
browserSubstrateDuties: [
"新页面 scaffold 文件创建",
"复制页面后的 mindmap 文件复制",
"页面嵌入前读取目标内容、源页面标题与 anchor block,作为 Rust Page Aggregate embed plan 的 preflight substrate",
"文件字节读取、upload URL、cookie/auth 等浏览器入口能力",
],
compatPending: [] satisfies TreeRouteBoundaryItem[],
forbiddenNextSemantics: [
"树合法性判断",
"树排序 canonical plan",
"长期 streamDelta 主语义拼装",
"tree shell renderer runtime",
"tree.node.embed 的 pageReference block 结构与插入位置语义",
],
} as const;