Evaluate coherent local queries over explicit replica scopes

This commit is contained in:
Timothy J. Aveni
2026-09-20 04:04:35 -07:00
parent 3f1976815f
commit c75888833b
5 changed files with 39 additions and 8 deletions
+11 -2
View File
@@ -49,13 +49,22 @@ export function generatePortableClientBindings(
atoms.set(atom.displayName, list); atoms.set(atom.displayName, list);
} }
const queries = new Map<string, string[]>(); const queries = new Map<string, string[]>();
const views = new Map<string, string[]>();
for (const query of plan.definition.queries) { for (const query of plan.definition.queries) {
const declaration = query.query.checked.declaration; 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 pkg = workspace.packageImports.find((p) => p.revisionId === query.query.packageRevisionId)!;
const alias = JSON.stringify([pkg.displayName, declaration.displayName]); const alias = JSON.stringify([pkg.displayName, declaration.displayName]);
if (seen.has(`query:${alias}`)) throw new PortableBindingError(alias, "Ambiguous query alias"); if (seen.has(`query:${alias}`)) throw new PortableBindingError(alias, "Ambiguous query alias");
seen.add(`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) ?? []; const list = queries.get(pkg.displayName) ?? [];
list.push( 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)}>`, `${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<string, string[]>) => const groups = (entries: Map<string, string[]>) =>
[...entries].map(([name, values]) => `${JSON.stringify(name)}: {${values.join(",\n")}}`).join(",\n"); [...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`;
} }
+15 -3
View File
@@ -312,14 +312,19 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
"Local and replica queries must use the same root contract", "Local and replica queries must use the same root contract",
); );
const traversals = (linked: LinkedQuery) => { const traversals = (linked: LinkedQuery) => {
const paths = new Set<string>(); const paths = new Map<string, boolean>();
const visit = (selections: LinkedQuery["checked"]["selection"], parents: string[]) => { const visit = (selections: LinkedQuery["checked"]["selection"], parents: string[]) => {
for (const selection of selections) { for (const selection of selections) {
const member = selection.interfaceRevisionId const member = selection.interfaceRevisionId
? interfaces.get(selection.interfaceRevisionId)?.members.find((m) => m.id === selection.memberId) ? interfaces.get(selection.interfaceRevisionId)?.members.find((m) => m.id === selection.memberId)
: undefined; : undefined;
const path = member?.kind === "relationship" ? [...parents, member.id] : parents; 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); visit(selection.selection, path);
} }
}; };
@@ -327,13 +332,20 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
return paths; return paths;
}; };
const admittedTraversals = traversals(source.query); const admittedTraversals = traversals(source.query);
for (const traversal of traversals(query.query)) for (const traversal of traversals(query.query).keys()) {
if (!admittedTraversals.has(traversal)) if (!admittedTraversals.has(traversal))
throw new ExecutionPlanError( throw new ExecutionPlanError(
"REPLICA_TRAVERSAL_MISSING", "REPLICA_TRAVERSAL_MISSING",
query.id, query.id,
`Replica source does not admit relationship path ${traversal}`, `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. // The admission executor follows these canonical dependencies on the admitted roots.
// They do not change source candidate membership or make local take/filters authoritative. // They do not change source candidate membership or make local take/filters authoritative.
source.admittedOperations.push(...query.operations); source.admittedOperations.push(...query.operations);
+9
View File
@@ -173,6 +173,15 @@ test("local queries cannot silently expand the replica relationship traversal",
assert.throws(() => compileExecutionPlan(workspace), /REPLICA_TRAVERSAL_MISSING/); 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 () => { test("closed generic applications retain distinct identities and codec shapes", async () => {
const { compileCapabilityResourceSource, compileCapabilitySource } = const { compileCapabilityResourceSource, compileCapabilitySource } =
await import("../src/capability-language/index.js"); await import("../src/capability-language/index.js");
+1
View File
@@ -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 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 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 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 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; } 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; }
} }
+3 -3
View File
@@ -3,13 +3,13 @@ import { test } from "node:test";
import { compileQuery } from "../src/query/compile.js"; import { compileQuery } from "../src/query/compile.js";
import { linkQueries } from "../src/query/link.js"; import { linkQueries } from "../src/query/link.js";
import { loadReaderWorkspace as load, readReaderFixture as read } from "./reader-fixture.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(); const workspace = await load();
assert.equal(workspace.constructors.length, 0); assert.equal(workspace.constructors.length, 0);
assert.equal(workspace.atoms.length, 3); assert.equal(workspace.atoms.length, 3);
const linked = linkQueries(workspace); const linked = linkQueries(workspace);
assert.equal(linked.length, 7); assert.equal(linked.length, 8);
assert.equal(linked.filter((q) => q.checked.declaration.placement?.kind === "local").length, 3); assert.equal(linked.filter((q) => q.checked.declaration.placement?.kind === "local").length, 4);
const screen = linked.find((q) => q.checked.declaration.displayName === "ReaderScreen")!; 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 === "highlight:excerpt" && f.binding.kind === "package"));
assert.ok(screen.fields.some((f) => f.memberId === "comment:content" && f.binding.kind === "state")); assert.ok(screen.fields.some((f) => f.memberId === "comment:content" && f.binding.kind === "state"));