From c75888833bf5455cb4deb87173ba07f1ffd5a3ad Mon Sep 17 00:00:00 2001 From: "Timothy J. Aveni" Date: Sun, 20 Sep 2026 03:44:49 -0700 Subject: [PATCH] Evaluate coherent local queries over explicit replica scopes --- src/bindings/portable-client.ts | 13 +++++++++++-- src/execution-world/plan.ts | 18 +++++++++++++++--- test/execution-world.test.ts | 9 +++++++++ .../local-first-reader/ReaderRuntime.qx | 1 + test/local-first-reader.test.ts | 6 +++--- 5 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/bindings/portable-client.ts b/src/bindings/portable-client.ts index b9be48e..a9d1aa3 100644 --- a/src/bindings/portable-client.ts +++ b/src/bindings/portable-client.ts @@ -49,13 +49,22 @@ export function generatePortableClientBindings( atoms.set(atom.displayName, list); } const queries = new Map(); + const views = new Map(); for (const query of plan.definition.queries) { const declaration = query.query.checked.declaration; - if (declaration.placement?.kind !== "replica") continue; + if (!declaration.placement) continue; const pkg = workspace.packageImports.find((p) => p.revisionId === query.query.packageRevisionId)!; const alias = JSON.stringify([pkg.displayName, declaration.displayName]); if (seen.has(`query:${alias}`)) throw new PortableBindingError(alias, "Ambiguous query alias"); seen.add(`query:${alias}`); + if (declaration.placement.kind === "local") { + const list = views.get(pkg.displayName) ?? []; + list.push( + `${JSON.stringify(declaration.displayName)}: ${JSON.stringify({ id: query.id, plan: plan.digest, source: JSON.stringify([query.query.packageRevisionId, declaration.placement.sourceQueryId]) })} as const satisfies LocalQueryDescriptor`, + ); + views.set(pkg.displayName, list); + continue; + } const list = queries.get(pkg.displayName) ?? []; list.push( `${JSON.stringify(declaration.displayName)}: { id: ${JSON.stringify(query.id)}, plan: ${JSON.stringify(plan.digest)}, root: ${JSON.stringify(declaration.root.interface)}, rootKind: ${JSON.stringify(declaration.root.kind)} } as const satisfies ReplicaProjection<${JSON.stringify(declaration.root.interface)}, ${JSON.stringify(declaration.root.kind)}>`, @@ -64,5 +73,5 @@ export function generatePortableClientBindings( } const groups = (entries: Map) => [...entries].map(([name, values]) => `${JSON.stringify(name)}: {${values.join(",\n")}}`).join(",\n"); - return `// Generated application descriptors. Regenerate; do not edit.\nimport { defineOperationDescriptor, type ManagedReference, type ReplicaProjection } from ${JSON.stringify(options.runtimeModule ?? "@quixos/camino-replica-core")};\nexport const planDigest = ${JSON.stringify(plan.digest)};\nexport const operations = {${groups(atoms)}} as const;\nexport const queries = {${groups(queries)}} as const;\nexport const unavailable = ${JSON.stringify(unavailable)} as const;\n`; + return `// Generated application descriptors. Regenerate; do not edit.\nimport { defineOperationDescriptor, type ManagedReference, type ReplicaProjection, type LocalQueryDescriptor } from ${JSON.stringify(options.runtimeModule ?? "@quixos/camino-replica-core")};\nexport const planDigest = ${JSON.stringify(plan.digest)};\nexport const operations = {${groups(atoms)}} as const;\nexport const queries = {${groups(queries)}} as const;\nexport const views = {${groups(views)}} as const;\nexport const unavailable = ${JSON.stringify(unavailable)} as const;\n`; } diff --git a/src/execution-world/plan.ts b/src/execution-world/plan.ts index f411da7..1372ee7 100644 --- a/src/execution-world/plan.ts +++ b/src/execution-world/plan.ts @@ -312,14 +312,19 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan { "Local and replica queries must use the same root contract", ); const traversals = (linked: LinkedQuery) => { - const paths = new Set(); + const paths = new Map(); const visit = (selections: LinkedQuery["checked"]["selection"], parents: string[]) => { for (const selection of selections) { const member = selection.interfaceRevisionId ? interfaces.get(selection.interfaceRevisionId)?.members.find((m) => m.id === selection.memberId) : undefined; const path = member?.kind === "relationship" ? [...parents, member.id] : parents; - if (member?.kind === "relationship") paths.add(JSON.stringify(path)); + if (member?.kind === "relationship") { + const many = member.cardinality === "many" || member.cardinality === "many-unique"; + const complete = !selection.predicate && (!many || !!selection.arguments.all); + const key = JSON.stringify(path); + paths.set(key, complete || paths.get(key) === true); + } visit(selection.selection, path); } }; @@ -327,13 +332,20 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan { return paths; }; const admittedTraversals = traversals(source.query); - for (const traversal of traversals(query.query)) + for (const traversal of traversals(query.query).keys()) { if (!admittedTraversals.has(traversal)) throw new ExecutionPlanError( "REPLICA_TRAVERSAL_MISSING", query.id, `Replica source does not admit relationship path ${traversal}`, ); + if (!admittedTraversals.get(traversal)) + throw new ExecutionPlanError( + "REPLICA_RELATION_INCOMPLETE", + query.id, + `Local relationship ${traversal} needs an unfiltered bounded all selection in its replica source; a filtered/windowed source cannot supply unseen matches`, + ); + } // The admission executor follows these canonical dependencies on the admitted roots. // They do not change source candidate membership or make local take/filters authoritative. source.admittedOperations.push(...query.operations); diff --git a/test/execution-world.test.ts b/test/execution-world.test.ts index 3b3afe0..a4ad112 100644 --- a/test/execution-world.test.ts +++ b/test/execution-world.test.ts @@ -173,6 +173,15 @@ test("local queries cannot silently expand the replica relationship traversal", assert.throws(() => compileExecutionPlan(workspace), /REPLICA_TRAVERSAL_MISSING/); }); +test("local relationships require complete source candidates even when a filter initially matches", async () => { + for (const selection of ["highlights(first: 10)", 'highlights(all: 1000, where: {color: {eq: "yellow"}})']) { + const workspace = await loadReaderWorkspace((name, text) => + name === "queries/replica-inputs.graphql" ? text.replace("highlights(all: 1000)", selection) : text, + ); + assert.throws(() => compileExecutionPlan(workspace), /REPLICA_RELATION_INCOMPLETE/); + } +}); + test("closed generic applications retain distinct identities and codec shapes", async () => { const { compileCapabilityResourceSource, compileCapabilitySource } = await import("../src/capability-language/index.js"); diff --git a/test/fixtures/local-first-reader/ReaderRuntime.qx b/test/fixtures/local-first-reader/ReaderRuntime.qx index 9067cbb..f31385b 100644 --- a/test/fixtures/local-first-reader/ReaderRuntime.qx +++ b/test/fixtures/local-first-reader/ReaderRuntime.qx @@ -79,6 +79,7 @@ revision "package:reader-runtime@1" { query HighlightScope id "reader:highlight-scope" root workspace HighlightData document "queries/highlight-scope.graphql" operation "HighlightScope" { view Comment as CommentData; replica; max rows 200; } query HighlightWindow id "reader:highlight-window" root workspace HighlightData document "queries/highlight-window.graphql" operation "HighlightWindow" { view Comment as CommentData; replica; max rows 200; } query HighlightList id "reader:highlight-list" root workspace HighlightData document "queries/highlight-list.graphql" operation "HighlightList" { view Comment as CommentData; local from HighlightScope; max rows 200; watch; } + query HighlightWindowList id "reader:highlight-window-list" root workspace HighlightData document "queries/highlight-list.graphql" operation "HighlightList" { view Comment as CommentData; local from HighlightWindow; max rows 200; watch; } query HighlightEditorInputs id "reader:editor-inputs" root HighlightData document "queries/editor-inputs.graphql" operation "HighlightEditorInputs" { view Comment as CommentData; replica; max rows 1; } query HighlightEditorView id "reader:editor-view" root HighlightData document "queries/editor-view.graphql" operation "HighlightEditorView" { view Comment as CommentData; local from HighlightEditorInputs; max rows 1; watch; } } diff --git a/test/local-first-reader.test.ts b/test/local-first-reader.test.ts index 4c04d5f..29b5b65 100644 --- a/test/local-first-reader.test.ts +++ b/test/local-first-reader.test.ts @@ -3,13 +3,13 @@ import { test } from "node:test"; import { compileQuery } from "../src/query/compile.js"; import { linkQueries } from "../src/query/link.js"; import { loadReaderWorkspace as load, readReaderFixture as read } from "./reader-fixture.js"; -test("complete reader contracts compose factories, portable selectors, CRDT edits, and six query scopes", async () => { +test("complete reader contracts compose factories, portable selectors, CRDT edits, and bounded query scopes", async () => { const workspace = await load(); assert.equal(workspace.constructors.length, 0); assert.equal(workspace.atoms.length, 3); const linked = linkQueries(workspace); - assert.equal(linked.length, 7); - assert.equal(linked.filter((q) => q.checked.declaration.placement?.kind === "local").length, 3); + assert.equal(linked.length, 8); + assert.equal(linked.filter((q) => q.checked.declaration.placement?.kind === "local").length, 4); const screen = linked.find((q) => q.checked.declaration.displayName === "ReaderScreen")!; assert.ok(screen.fields.some((f) => f.memberId === "highlight:excerpt" && f.binding.kind === "package")); assert.ok(screen.fields.some((f) => f.memberId === "comment:content" && f.binding.kind === "state"));