Scope presentation associations to PresentationMount and retain navigation queries

Generate checked navigation query pairs from closed React contracts before
package checking. Retain per-type subscriptions under the logical
PresentationMount and resolve cold component metadata independently of code.

Keep scoped occurrence membership and idempotent local preparation in the
presentation library; copy directly through state contracts without reading
other PresentationMounts' source associations. Preserve normal transaction
creation, pending references, portal synchronization and detach.

Exercise automatic object refs in Reader queries, imported fragments and
structured/aggregate results. Add heterogeneous owner-index, live dependency,
cold metadata, browser ownership and latency coverage. Document measured
improvements, supported query limits and qualification evidence.
This commit is contained in:
Timothy J. Aveni
2026-09-26 12:32:47 -07:00
parent 3728612e8c
commit ad64ba532f
8 changed files with 260 additions and 24 deletions
+83
View File
@@ -0,0 +1,83 @@
import { createHash } from "node:crypto";
import type { InterfaceRevision, PackageRevision, ValueType } from "../capability-model/types.js";
import { defaultQueryBudgets, type QueryDeclaration } from "../query/types.js";
/** React contract tooling generates ordinary queries before normal checking.
* Neither the query language nor its execution planner knows presentation IDs. */
export function presentationQueries(pkg: PackageRevision, interfaces: readonly InterfaceRevision[]) {
const documents = new Map<string, string>();
const generated: QueryDeclaration[] = [];
const contracts = new Map(interfaces.map((contract) => [contract.revisionId, contract]));
const navigation = new Set<string>();
for (const module of pkg.modules ?? []) {
const contract = contracts.get(module.interfaceRevisionId);
const member = contract?.members.find((member) => member.id === module.memberId);
if (member?.kind !== "module" || member.contract !== "org.quixos.react.component/2") continue;
const argument = contract?.application?.arguments[1];
if (argument?.kind !== "object" || argument.target.kind !== "interface")
throw Error("Stateful React presentations require a closed navigation interface");
navigation.add(argument.target.interfaceRevisionId);
}
const valueSelection = (type: ValueType): string => {
if (type.kind === "optional" || type.kind === "list") return valueSelection(type.value);
return type.kind === "record"
? `{${Object.entries(type.fields)
.map(([name, type]) => `${name} ${valueSelection(type)}`)
.join(" ")}}`
: "";
};
for (const id of [...navigation].sort()) {
const contract = contracts.get(id as InterfaceRevision["revisionId"]);
if (!contract || contract.template) throw Error(`Missing closed presentation navigation ${id}`);
const fields = contract.members.flatMap((member) => {
if ((member.kind !== "value" && member.kind !== "relationship") || !member.queryRead) return [];
if (member.kind === "value") return [`${member.displayName} ${valueSelection(member.valueType)}`];
if (member.target.kind !== "interface")
throw Error(`Navigation relationship ${member.displayName} must declare an interface view`);
return [
member.cardinality === "many" || member.cardinality === "many-unique"
? `${member.displayName}(all:512) {entries {node {_camino {ref}}}}`
: `${member.displayName} {_camino {ref}}`,
];
});
const stem = `PresentationNavigation${createHash("sha256").update(id).digest("hex").slice(0, 12)}`;
const inputs = `presentation:navigation:${id}:inputs`;
for (const local of [false, true]) {
const name = `${stem}${local ? "View" : "Inputs"}`;
const document = `generated/${name}.graphql`;
documents.set(
document,
`query ${name}($presentationMount: QxRef_PresentationMount!) {
root { ${local ? "availableObjects" : "objects"}(all:512, where:{_camino:{as:{PresentationState:{presentationMount:{eq:$presentationMount}}}}}) {
entries { node { ${fields.join("\n")}
... on PresentationState {
_presentationOccurrence: occurrence
_presentationSource: source {_camino {ref}}
_presentationParent: parent {_camino {ref}}
_presentationMount: presentationMount {_camino {ref}}
}
} }
} }
}`,
);
generated.push({
id: local ? `presentation:navigation:${id}:view` : inputs,
displayName: name,
root: { kind: "workspace-interface", interface: contract.revisionId },
document,
operation: name,
fragments: [],
views: [],
allowances: [],
budgets: { ...defaultQueryBudgets, rows: 5000, candidates: 5000, resultBytes: 8388608 },
watch: local,
placement: local ? { kind: "local", sourceQueryId: inputs } : { kind: "replica" },
});
}
}
for (const query of generated)
if (pkg.queries?.some((existing) => existing.id === query.id || existing.displayName === query.displayName))
throw Error(`Presentation query conflicts with an authored query: ${query.id}`);
pkg.queries = [...(pkg.queries ?? []), ...generated];
return documents;
}
+20 -14
View File
@@ -1,7 +1,8 @@
import { readFile, realpath } from "node:fs/promises"; import { readFile, realpath } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { loadQxSources, resolveQxSources } from "./source-loader.js"; import { loadQxSources, resolveQxSources } from "./source-loader.js";
import { compileRepositoryQuery } from "../query/compile.js"; import { compileQuery, compileRepositoryQuery } from "../query/compile.js";
import { presentationQueries } from "../bindings/presentation-queries.js";
import { checkQueryTemplate } from "../query/templates.js"; import { checkQueryTemplate } from "../query/templates.js";
import { readRepositorySource } from "./source-loader.js"; import { readRepositorySource } from "./source-loader.js";
import { parseControllerCatalog } from "../capability-model/controllers.js"; import { parseControllerCatalog } from "../capability-model/controllers.js";
@@ -272,6 +273,7 @@ const createResourceGraphResolver = (quixosCommit: string, resolveResource: Capa
...(environment.interfaceClosure ?? []), ...(environment.interfaceClosure ?? []),
...(compiled.resource.specializations ?? []), ...(compiled.resource.specializations ?? []),
]; ];
const presentationDocuments = presentationQueries(compiled.resource.revision, queryInterfaces);
for (const template of compiled.resource.revision.queryTemplates ?? []) for (const template of compiled.resource.revision.queryTemplates ?? [])
await checkQueryTemplate(template, queryInterfaces, (name) => await checkQueryTemplate(template, queryInterfaces, (name) =>
readRepositorySource(snapshot.directory, name), readRepositorySource(snapshot.directory, name),
@@ -288,19 +290,23 @@ const createResourceGraphResolver = (quixosCommit: string, resolveResource: Capa
if (previous) return previous; if (previous) return previous;
const query = pkg.queries!.find((query) => query.id === id); const query = pkg.queries!.find((query) => query.id === id);
if (!query) throw new Error(`Unknown fragment ${pkg.revisionId}/${id}`); if (!query) throw new Error(`Unknown fragment ${pkg.revisionId}/${id}`);
const result = await compileRepositoryQuery( const generatedDocument = presentationDocuments.get(query.document);
snapshot.directory, const result =
query, generatedDocument !== undefined
queryInterfaces, ? await compileQuery(query, queryInterfaces, async () => generatedDocument)
async (packageId, fragmentId) => { : await compileRepositoryQuery(
if (packageId === pkg.revisionId) return compile(fragmentId, [...parents, id]); snapshot.directory,
const imported = environment.packageClosure query,
?.find((other) => other.revisionId === packageId) queryInterfaces,
?.checkedQueries?.find((query) => query.declaration.id === fragmentId); async (packageId, fragmentId) => {
if (!imported) throw new Error(`Unchecked imported fragment ${packageId}/${fragmentId}`); if (packageId === pkg.revisionId) return compile(fragmentId, [...parents, id]);
return imported; const imported = environment.packageClosure
}, ?.find((other) => other.revisionId === packageId)
); ?.checkedQueries?.find((query) => query.declaration.id === fragmentId);
if (!imported) throw new Error(`Unchecked imported fragment ${packageId}/${fragmentId}`);
return imported;
},
);
checked.set(id, result); checked.set(id, result);
return result; return result;
}; };
+6
View File
@@ -171,6 +171,12 @@ export async function compileQuery(
visit( visit(
document, document,
visitWithTypeInfo(projectionTypes, { visitWithTypeInfo(projectionTypes, {
Field(node) {
const parent = projectionTypes.getParentType();
const member =
parent && generated.byName.get(parent.name)?.members.find((member) => member.displayName === node.name.value);
if (member?.kind === "relationship") generated.target(member);
},
InlineFragment(node) { InlineFragment(node) {
if (!node.typeCondition) return; if (!node.typeCondition) return;
const from = projectionTypes.getParentType(); const from = projectionTypes.getParentType();
+4 -1
View File
@@ -44,6 +44,7 @@ export type SchemaRowField = { kind: "leaf"; type: ValueType } | { kind: "row";
export interface RelationalSchemaEnvironment { export interface RelationalSchemaEnvironment {
contracts: Map<InterfaceRevisionId, InterfaceRevision>; contracts: Map<InterfaceRevisionId, InterfaceRevision>;
target(member: RelationshipInterfaceMember): InterfaceRevisionId; target(member: RelationshipInterfaceMember): InterfaceRevisionId;
viewable(member: RelationshipInterfaceMember): boolean;
name(id: InterfaceRevisionId): string; name(id: InterfaceRevisionId): string;
scalar(type: ValueType): GraphQLInputType & GraphQLOutputType; scalar(type: ValueType): GraphQLInputType & GraphQLOutputType;
comparison(type: ValueType): GraphQLInputObjectType; comparison(type: ValueType): GraphQLInputObjectType;
@@ -83,7 +84,8 @@ export function relationalSchema(env: RelationalSchemaEnvironment, document?: Do
? env.contracts ? env.contracts
.get(row.interfaceRevisionId)! .get(row.interfaceRevisionId)!
.members.filter( .members.filter(
(member): member is RelationshipInterfaceMember => member.kind === "relationship" && !!member.queryRead, (member): member is RelationshipInterfaceMember =>
member.kind === "relationship" && !!member.queryRead && env.viewable(member),
) )
: []; : [];
const fields = (row: QueryRow): Map<string, SchemaRowField> => { const fields = (row: QueryRow): Map<string, SchemaRowField> => {
@@ -96,6 +98,7 @@ export function relationalSchema(env: RelationalSchemaEnvironment, document?: Do
env.contracts.get(row.interfaceRevisionId)!.members.flatMap((member): [string, SchemaRowField][] => { env.contracts.get(row.interfaceRevisionId)!.members.flatMap((member): [string, SchemaRowField][] => {
if (member.kind === "operation" || member.kind === "module" || !member.queryRead) return []; if (member.kind === "operation" || member.kind === "module" || !member.queryRead) return [];
if (member.kind === "value") return [[member.displayName, { kind: "leaf", type: member.valueType }]]; if (member.kind === "value") return [[member.displayName, { kind: "leaf", type: member.valueType }]];
if (!env.viewable(member)) return [];
if (many(member)) return []; if (many(member)) return [];
return [ return [
[ [
+7
View File
@@ -150,11 +150,15 @@ export function querySchema(
return result; return result;
}; };
const structured = structuredQueryTypes(scalar, scalarComparison, direction); const structured = structuredQueryTypes(scalar, scalarComparison, direction);
const viewable = (member: RelationshipInterfaceMember) =>
member.target.kind === "interface" ||
declaration.views.some((view) => member.target.kind === "atom" && view.atomId === member.target.atomId);
const comparison = (type: ValueType) => structured.input(type) as GraphQLInputObjectType; const comparison = (type: ValueType) => structured.input(type) as GraphQLInputObjectType;
const relational = relationalSchema( const relational = relationalSchema(
{ {
contracts, contracts,
target, target,
viewable,
name, name,
scalar, scalar,
comparison, comparison,
@@ -205,6 +209,9 @@ export function querySchema(
if (member.kind === "value") { if (member.kind === "value") {
fields[member.displayName] = { type: structured.output(member.valueType) }; fields[member.displayName] = { type: structured.output(member.valueType) };
} else { } else {
// Reference-only projections need no views for unselected outgoing
// atom relationships. Selecting one still requires its explicit view.
if (!viewable(member)) continue;
const targetId = target(member), const targetId = target(member),
node = object(targetId); node = object(targetId);
if (member.cardinality === "exactly-one" || member.cardinality === "optional-one") if (member.cardinality === "exactly-one" || member.cardinality === "optional-one")
+129 -4
View File
@@ -226,18 +226,33 @@ test("retired live syntax fails with migration guidance", async () => {
test("implicit references reject aliases which occupy the canonical metadata path", async () => { test("implicit references reject aliases which occupy the canonical metadata path", async () => {
for (const invalid of ["_camino: title", "_camino {ref: __typename}"]) for (const invalid of ["_camino: title", "_camino {ref: __typename}"])
await assert.rejects(compile(`query Upcoming {root {items(first: 3) {entries {node {...Row}}}}}`, `fragment Row on TaskFacts {${invalid}}`), /QUERY_VALIDATION|Introspection/); await assert.rejects(
compile(
`query Upcoming {root {items(first: 3) {entries {node {...Row}}}}}`,
`fragment Row on TaskFacts {${invalid}}`,
),
/QUERY_VALIDATION|Introspection/,
);
}); });
test("implicit reference merges with conditional aggregate metadata", async () => { test("implicit reference merges with conditional aggregate metadata", async () => {
const checked = await compileQuery({ ...fixture(""), fragments: [] }, [collection, facts], async () => const checked = await compileQuery(
`query Upcoming($show: Boolean!) {root {_camino @include(if: $show) {relations {items {aggregate {count}}}}}}`); { ...fixture(""), fragments: [] },
[collection, facts],
async () =>
`query Upcoming($show: Boolean!) {root {_camino @include(if: $show) {relations {items {aggregate {count}}}}}}`,
);
const metadata = checked.selection[0]!.selection.filter((field) => field.key === "_camino"); const metadata = checked.selection[0]!.selection.filter((field) => field.key === "_camino");
assert.equal(metadata.length, 1); assert.equal(metadata.length, 1);
assert.deepEqual(metadata[0]!.conditions, []); assert.deepEqual(metadata[0]!.conditions, []);
assert.equal(metadata[0]!.selection.find((field) => field.name === "relations")!.conditions.length, 1); assert.equal(metadata[0]!.selection.find((field) => field.name === "relations")!.conditions.length, 1);
assert.deepEqual(metadata[0]!.selection.find((field) => field.name === "ref")!.conditions, []); assert.deepEqual(metadata[0]!.selection.find((field) => field.name === "ref")!.conditions, []);
assert.equal(metadata[0]!.selection.find((field) => field.name === "relations")!.selection[0]!.selection[0]!.selection.some((field) => field.name === "_camino"), false); assert.equal(
metadata[0]!.selection
.find((field) => field.name === "relations")!
.selection[0]!.selection[0]!.selection.some((field) => field.name === "_camino"),
false,
);
}); });
test("qualified interface predicates and guaranteed inline projections retain nominal reads", async () => { test("qualified interface predicates and guaranteed inline projections retain nominal reads", async () => {
@@ -280,6 +295,21 @@ test("qualified interface predicates and guaranteed inline projections retain no
const node = objects.selection[0]!.selection[0]!; const node = objects.selection[0]!.selection[0]!;
assert.equal(node.selection.find((s) => s.key === "ownerLabel")!.interfaceRevisionId, owned.revisionId); assert.equal(node.selection.find((s) => s.key === "ownerLabel")!.interfaceRevisionId, owned.revisionId);
assert.equal(node.selection.filter((s) => s.key === "_camino").length, 1); assert.equal(node.selection.filter((s) => s.key === "_camino").length, 1);
await assert.rejects(
compileQuery(declaration, [item, owned, owner], async () =>
document.replace("ownerLabel: label", "ownerLabel: label _camino {ref}"),
),
/QUERY_VALIDATION/,
);
const explicitProjectionRef = await compileQuery(declaration, [item, owned, owner], async () =>
document.replace("ownerLabel: label", "ownerLabel: label ownedIdentity: _camino {ref}"),
);
const projectedNode = explicitProjectionRef.selection[0]!.selection[0]!.selection[0]!.selection[0]!;
assert.equal(
projectedNode.selection.find((field) => field.key === "_camino")!.selection.find((field) => field.key === "ref")!
.targetInterfaceRevisionId,
item.revisionId,
);
await assert.rejects( await assert.rejects(
compileQuery(declaration, [item, owned, owner], async () => document.replace("ownerLabel: label", "label")), compileQuery(declaration, [item, owned, owner], async () => document.replace("ownerLabel: label", "label")),
/QUERY_PROJECTION_CONFLICT|QUERY_VALIDATION/, /QUERY_PROJECTION_CONFLICT|QUERY_VALIDATION/,
@@ -369,3 +399,98 @@ test("query allowlist and two-sided RPC effects fail during compilation", async
await rejects(compile(document.replace("...Row", "__typename ...Row")), "QUERY_UNSUPPORTED_FEATURE"); await rejects(compile(document.replace("...Row", "__typename ...Row")), "QUERY_UNSUPPORTED_FEATURE");
await rejects(compile(document, "fragment Row on TaskFacts { ...Row }"), "QUERY_VALIDATION"); await rejects(compile(document, "fragment Row on TaskFacts { ...Row }"), "QUERY_VALIDATION");
}); });
test("unselected atom relationships do not require views on reference-only targets", async () => {
const target = iface(`external atom Detail id "detail";
interface Referenced id "referenced" revision "referenced@1" {
queryable relation details id "details" : many atom Detail {resolve id "details:resolve";}
}`);
const subject = iface(
`external atom Detail id "detail"; import interface Referenced;
interface Subject id "subject" revision "subject@1" {
queryable relation selected id "selected" : optional-one interface Referenced {resolve id "selected:resolve";}
}`,
[target],
);
const declaration = {
...fixture(""),
root: { kind: "object" as const, interface: subject.revisionId },
fragments: [],
};
const document = "query Upcoming {root {selected {_camino {ref}}}}";
const checked = await compileQuery(declaration, [subject, target], async () => document);
assert.match(JSON.stringify(checked.output), /referenced@1/);
await assert.rejects(
compileQuery(
declaration,
[subject, target],
async () => "query Upcoming {root {selected {details(all: 3) {entries {node {_camino {ref}}}}}}}",
),
/QUERY_VIEW_REQUIRED/,
);
});
test("React tooling generates checked owner queries from imported closed navigation fields", async () => {
const { presentationQueries } = await import("../src/bindings/presentation-queries.js");
const mount = iface('interface PresentationMount id "mount" revision "mount@1" {}');
const subject = iface('interface Subject id "subject" revision "subject@1" {}');
const state = iface(
`import interface PresentationMount; import interface Subject;
interface PresentationState id "state" revision "state@1" {
queryable value occurrence id "occurrence" : string {get id "occurrence:get" execution snapshot targets [browser,server];}
queryable relation presentationMount id "mount" : optional-one interface PresentationMount {resolve id "mount:resolve" execution snapshot targets [browser,server];}
queryable relation parent id "parent" : optional-one interface PresentationState {resolve id "parent:resolve" execution snapshot targets [browser,server];}
queryable relation source id "source" : exactly-one interface Subject {resolve id "source:resolve" execution snapshot targets [browser,server];}
}`,
[mount, subject],
);
const navigation = iface(
`import interface PresentationState;
interface Navigation<value T> id "navigation" revision "navigation@1" requires PresentationState {
queryable value custom id "custom" : T {get id "custom:get" execution snapshot targets [browser,server];}
}`,
[mount, subject, state],
);
const react = iface(
`import interface PresentationState;
interface ReactComponent<object Subject, object Navigation implements PresentationState> id "react" revision "react@1" {
module render id "render" contract "org.quixos.react.component/2" subject object Subject;
}`,
[mount, subject, state],
);
const dependencies = [mount, subject, state, navigation, react];
const parsed = compileCapabilityResourceSource(
`import interface ReactComponent; import interface Navigation; import interface Subject;
package Example id "example" revision "example@1" {
module Screen id "screen" implements ReactComponent<interface Subject,interface Navigation<record {label:string;count:int32;}>>.render;
}`,
{
source,
environment: {
interfaces: new Map(dependencies.map((entry) => [entry.displayName, entry])),
interfaceClosure: dependencies,
},
},
);
assert.ok(parsed.ok, JSON.stringify(parsed.diagnostics));
if (parsed.resource.kind !== "package") throw Error("package required");
const contracts = [...dependencies, ...(parsed.resource.specializations ?? [])];
const documents = presentationQueries(parsed.resource.revision, contracts);
assert.equal(documents.size, 2);
const checked = await Promise.all(
parsed.resource.revision.queries!.map((query) =>
compileQuery(query, contracts, async (name) => documents.get(name)!),
),
);
assert.deepEqual(
checked.map((query) => query.declaration.placement?.kind),
["replica", "local"],
);
assert.match(JSON.stringify(checked[0]!.output), /"custom":\{"kind":"record","fields":\{"label"/);
assert.match(JSON.stringify(checked[0]!.selection), /"kind":"as"/);
assert(
checked.every((query) =>
query.effects.some((effect) => effect.memberId === "mount" && effect.uses.includes("predicate")),
),
);
});
+3 -3
View File
@@ -34,7 +34,7 @@ test("exported fragments generate local projection and direct admission", async
assert.equal(p.queries!.length, 2); assert.equal(p.queries!.length, 2);
const local = p.queries!.find((q) => !q.fragment?.admission)!; const local = p.queries!.find((q) => !q.fragment?.admission)!;
assert.equal(local.placement?.kind, "local"); assert.equal(local.placement?.kind, "local");
const checked = await compileQuery(local, [item], async () => "fragment Row on Item {_camino {ref} title}"); const checked = await compileQuery(local, [item], async () => "fragment Row on Item {title}");
assert.equal(checked.output.kind, "record"); assert.equal(checked.output.kind, "record");
assert.match(checked.document, /query QuixosFragment/); assert.match(checked.document, /query QuixosFragment/);
await assert.rejects( await assert.rejects(
@@ -47,7 +47,7 @@ test("imported fragments retain checked occurrences and require exact roots", as
const checked = await compileQuery( const checked = await compileQuery(
provider.queries!.find((q) => !q.fragment?.admission)!, provider.queries!.find((q) => !q.fragment?.admission)!,
[item], [item],
async () => "fragment Row on Item {_camino {ref} title}", async () => "fragment Row on Item {title}",
); );
const consumer = pkg( const consumer = pkg(
'query Screen id "screen" root Item document "screen.graphql" operation "Screen" {replica; fragments Provider.Row;}', 'query Screen id "screen" root Item document "screen.graphql" operation "Screen" {replica; fragments Provider.Row;}',
@@ -145,7 +145,7 @@ test("independent fragment package survives execution lowering and typed client
compileQuery( compileQuery(
q, q,
workspace.interfaceImports, workspace.interfaceImports,
async () => "fragment CommentRow on CommentData {_camino {ref} content {text tags}}", async () => "fragment CommentRow on CommentData {content {text tags}}",
), ),
), ),
); );
+8 -2
View File
@@ -70,8 +70,14 @@ test("record/list query values preserve nested projections and leaf predicates",
} }
}; };
assertValuesOnly(node.selection.filter((field) => field.key !== "_camino")); assertValuesOnly(node.selection.filter((field) => field.key !== "_camino"));
assert.equal(checked.selection[0]!.selection.some((field) => field.key === "_camino"), false); assert.equal(
assert.equal(extent.selection.some((field) => field.key === "_camino"), false); checked.selection[0]!.selection.some((field) => field.key === "_camino"),
false,
);
assert.equal(
extent.selection.some((field) => field.key === "_camino"),
false,
);
assert.match(raw, /"begin"/); assert.match(raw, /"begin"/);
assert.match(raw, /"tags":\{"kind":"list"/); assert.match(raw, /"tags":\{"kind":"list"/);
assert.equal(checked.effects.filter((e) => e.memberId === "range").length, 1); assert.equal(checked.effects.filter((e) => e.memberId === "range").length, 1);