From 47a3fe9eae90c156f70227985d1022d9beaeafc2 Mon Sep 17 00:00:00 2001 From: "Timothy J. Aveni" Date: Sun, 20 Sep 2026 23:07:09 -0700 Subject: [PATCH] Preserve nested CRDT recovery and serialize compound editor submissions --- src/execution-world/kernel.ts | 10 ++++++++++ test/compound-crdt.test.ts | 12 +++++++++++- test/fixtures/compound-crdt/EditRuntime.qx | 9 +++++++++ test/fixtures/compound-crdt/NoteData.qx | 3 +++ test/fixtures/compound-crdt/workspace.qx | 4 ++++ 5 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/execution-world/kernel.ts b/src/execution-world/kernel.ts index 126aae6..87cc5dc 100644 --- a/src/execution-world/kernel.ts +++ b/src/execution-world/kernel.ts @@ -8,6 +8,14 @@ import type { } from "../capability-model/types.js"; import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js"; import { portableValueSchema } from "../bindings/portable.js"; +function preparedInputPaths(type: ValueType, path: (string | null)[] = []): (string | null)[][] { + if (type.kind === "change") return [path]; + if (type.kind === "optional") return preparedInputPaths(type.value, path); + if (type.kind === "list") return preparedInputPaths(type.value, [...path, null]); + if (type.kind === "record") + return Object.entries(type.fields).flatMap(([key, value]) => preparedInputPaths(value, [...path, key])); + return []; +} /** Small data-only interpreter input; no compiler, parser, Node module or SQL dependency reaches the browser. */ export function compileKernelProgram(plan: ExecutionPlan): unknown { if (hexDigest(plan.definition) !== plan.digest) @@ -172,6 +180,7 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown { : {}), }; } + const inputChanges = preparedInputPaths(operation.input); const input = schema(operation.input), output = schema(operation.output); return { @@ -183,6 +192,7 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown { effect: operation.execution.kind, targets: operation.execution.kind === "remote" ? [] : operation.execution.targets, input, + ...(inputChanges.length ? { inputChanges } : {}), output, unsupported: unsupported ?? (input && output ? null : "UNSUPPORTED_OPERATION_CODEC"), binding: target, diff --git a/test/compound-crdt.test.ts b/test/compound-crdt.test.ts index 30855cb..4f773b6 100644 --- a/test/compound-crdt.test.ts +++ b/test/compound-crdt.test.ts @@ -9,10 +9,20 @@ const source = { repository: "https://example.test/change.git", commit: "a".repe test("compound captured changes survive planning, transport lowering and typed edit ports", async () => { const plan = await compoundCrdtPlan(); const kernel = compileKernelProgram(plan) as { - operations: { operation: string; input: { fields: { change: { kind: string } } } }[]; + operations: { + operation: string; + inputChanges?: (string | null)[][]; + input: { fields: { change: { kind: string } } }; + }[]; }; const operation = kernel.operations.find((op) => op.operation === "compound:update:call")!; assert.equal(operation.input.fields.change.kind, "record"); + const paths = kernel.operations as unknown as { operation: string; inputChanges?: (string | null)[][] }[]; + assert.deepEqual(paths.find((op) => op.operation === "compound:update:call")!.inputChanges, [["change"]]); + assert.deepEqual(paths.find((op) => op.operation === "compound:batch:call")!.inputChanges, [ + ["edits", null, "change"], + ]); + assert.deepEqual(paths.find((op) => op.operation === "compound:content:edit")!.inputChanges, [[]]); const code = generatePortableTypeScriptBindings(plan.definition.bindingSchema, "compound:runtime@1", { target: "browser", }); diff --git a/test/fixtures/compound-crdt/EditRuntime.qx b/test/fixtures/compound-crdt/EditRuntime.qx index 7921851..be4f938 100644 --- a/test/fixtures/compound-crdt/EditRuntime.qx +++ b/test/fixtures/compound-crdt/EditRuntime.qx @@ -2,6 +2,15 @@ external atom Note id "compound:note"; type Content = record { text: string; tags: list; }; type Edit = record { change: change; resolved: bool; }; package EditRuntime id "compound:runtime" revision "compound:runtime@1" { + operation batch id "compound:runtime:batch" : record { edits: list; } -> unit + mode call execution transaction targets [browser, server] receiver atom Note requires { + state content id "compound:batch:content" : Content [edit]; + state resolved id "compound:batch:resolved" : bool [write]; + } accept using checkBatch; + operation checkBatch id "compound:runtime:check-batch" : record { edits: list; } -> unit + mode call execution snapshot targets [server] receiver atom Note requires { + state allowed id "compound:batch:allowed" : bool [read]; + }; operation update id "compound:runtime:update" : Edit -> unit mode call execution transaction targets [browser, server] receiver atom Note requires { state content id "compound:edit:content" : Content [edit]; diff --git a/test/fixtures/compound-crdt/NoteData.qx b/test/fixtures/compound-crdt/NoteData.qx index 7834319..f0e30e1 100644 --- a/test/fixtures/compound-crdt/NoteData.qx +++ b/test/fixtures/compound-crdt/NoteData.qx @@ -1,6 +1,9 @@ type Content = record { text: string; tags: list; }; type Edit = record { change: change; resolved: bool; }; interface NoteData id "compound:data" revision "compound:data@1" { + operation batch id "compound:batch" : record { edits: list; } -> unit { + call id "compound:batch:call" execution transaction targets [browser, server]; + } operation update id "compound:update" : Edit -> unit { call id "compound:update:call" execution transaction targets [browser, server]; } diff --git a/test/fixtures/compound-crdt/workspace.qx b/test/fixtures/compound-crdt/workspace.qx index d3eb453..dbe4d80 100644 --- a/test/fixtures/compound-crdt/workspace.qx +++ b/test/fixtures/compound-crdt/workspace.qx @@ -14,6 +14,10 @@ workspace Compound id "workspace:compound" revision "workspace:compound@1" commi bind resolved.get to state Resolved.read; bind title.get to state Title.read; bind title.set to state Title.write; + bind batch.call to package EditRuntime.batch with { + content to state Content; + resolved to state Resolved; + } accept with { allowed to state Allowed; }; bind update.call to package EditRuntime.update with { content to state Content; resolved to state Resolved;