From ad72185a6cb200bb80a60157e2d8c9797eb56085 Mon Sep 17 00:00:00 2001 From: "Timothy J. Aveni" Date: Sun, 27 Sep 2026 19:21:05 -0700 Subject: [PATCH] Implement independent presentation readiness boundaries --- src/execution-world/client-contracts.ts | 4 +-- src/query/compile.ts | 31 ++++++++++++++-- src/query/fragments.ts | 18 ++++++++-- src/query/types.ts | 4 +++ src/react-build/declarations.ts | 1 - src/react-build/expansion.ts | 31 ++++++++++------ test/prepared-presentations.test.ts | 34 +++++++++++------- test/query-fragments.test.ts | 48 +++++++++++++++++++++++++ 8 files changed, 140 insertions(+), 31 deletions(-) diff --git a/src/execution-world/client-contracts.ts b/src/execution-world/client-contracts.ts index cd0b676..6977abe 100644 --- a/src/execution-world/client-contracts.ts +++ b/src/execution-world/client-contracts.ts @@ -251,7 +251,7 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi .join("\n"); return ( interfaces + - '\nimport {createObjectRenderer,definePackageQuery,definePackageFragment,type ComponentType,type ComponentProps,type PreparedPresentation} from "@quixos/camino-react";\n' + + '\nimport {createObjectRenderer,definePackageQuery,definePackageFragment,type ComponentType,type ComponentProps,type PresentationHandle} from "@quixos/camino-react";\n' + subjectBindings + "\n" + queries + @@ -288,7 +288,7 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi )!; extra = props(iface.revisionId, member.id); } - const handle = `PreparedPresentation<${JSON.stringify(child.subject)}, ${extra}>`; + const handle = `PresentationHandle<${JSON.stringify(child.subject)}, ${extra}>`; return `readonly ${JSON.stringify(child.name)}: ${child.each ? `readonly ${handle}[]` : child.with || child.when ? `${handle} | null` : handle}`; }); const input = `${tsType(composition.output)} & {readonly children: {${children.join(";")}}}`; diff --git a/src/query/compile.ts b/src/query/compile.ts index 29e83d5..1a38c24 100644 --- a/src/query/compile.ts +++ b/src/query/compile.ts @@ -4,6 +4,8 @@ import { validate, specifiedRules, PossibleFragmentSpreadsRule, + NoUnusedVariablesRule, + GraphQLError, print, printSchema, visit, @@ -201,10 +203,29 @@ export async function compileQuery( }, }), ); + const bindingVariables = new Set( + [...fragmentContracts.values()] + .filter((entry) => entry.mode === "reference") + .flatMap((entry) => Object.values(entry.variables)), + ); const errors = validate( generated.schema, document, - specifiedRules.filter((rule) => rule !== PossibleFragmentSpreadsRule), + [ + ...specifiedRules.filter((rule) => rule !== PossibleFragmentSpreadsRule && rule !== NoUnusedVariablesRule), + (context) => ({ + OperationDefinition: { + leave(operation) { + const used = new Set(context.getRecursiveVariableUsages(operation).map(({ node }) => node.name.value)); + for (const variable of operation.variableDefinitions ?? []) { + const name = variable.variable.name.value; + if (!used.has(name) && !bindingVariables.has(name)) + context.reportError(new GraphQLError(`Variable "$${name}" is never used.`, { nodes: variable })); + } + }, + }, + }), + ], { maxErrors: 20 }, ); if (errors.length) { @@ -399,6 +420,7 @@ export async function compileQuery( for (const selection of set.selections) { if (++expandedFields > 10000) fail("QUERY_WORK_LIMIT", "Expanded query exceeds 10000 fields", selection); if (selection.kind === "FragmentSpread") { + if (fragmentContracts.get(selection.name.value)?.mode === "reference") continue; add( fragments.get(selection.name.value)!.selectionSet, conditional || !!selection.directives?.length, @@ -530,7 +552,9 @@ export async function compileQuery( }, ] : []), - ...selection(fragments.get(node.name.value)!.selectionSet, parent, inheritedConditions), + ...(contract?.mode === "reference" + ? [] + : selection(fragments.get(node.name.value)!.selectionSet, parent, inheritedConditions)), ]; } if (node.kind !== "Field") return []; @@ -610,9 +634,10 @@ export async function compileQuery( const definitionDigest = createHash("sha256") .update( canonicalJson({ - semantics: 4, + semantics: 5, declaration, normalized, + fragmentContracts: Object.fromEntries(fragmentContracts), // Source provenance belongs to the checked installation. Identical public // contracts keep their query identity when retained in another repository. // Closed generic identity remains explicit in revisionId/application. diff --git a/src/query/fragments.ts b/src/query/fragments.ts index 02e76f5..3fe3867 100644 --- a/src/query/fragments.ts +++ b/src/query/fragments.ts @@ -11,6 +11,8 @@ export async function fragmentSources( ) { const markers = new Map>(); for (const imported of declaration.importedFragments ?? []) { + if (imported.mode !== undefined && imported.mode !== "reference") + throw new QueryCompileError("FRAGMENT_IMPORT_MODE", "Unknown fragment import mode"); if (!resolve) throw new QueryCompileError( "FRAGMENT_RESOLVER_REQUIRED", @@ -57,7 +59,16 @@ export async function fragmentSources( node.name.value === exported.name ? exported.name : `${exported.name}__${node.name.value}`, ]), ); - for (const fragment of fragments) { + // A reference import binds the checked export, rather than spreading its + // fields into this operation. Keep a nominal stub for GraphQL root checking; + // the independently checked export supplies the actual projection contract. + const referenced = parse("fragment Binding on Placeholder { _camino { ref } }") + .definitions[0] as FragmentDefinitionNode; + for (const sourceFragment of imported.mode === "reference" + ? fragments.filter((entry) => entry.name.value === exported.name) + : fragments) { + const fragment = + imported.mode === "reference" ? { ...sourceFragment, selectionSet: referenced.selectionSet } : sourceFragment; definitions.push( visit(fragment, { FragmentDefinition(node) { @@ -73,13 +84,16 @@ export async function fragmentSources( ); } markers.set(exported.name, { + ...(imported.mode ? { mode: imported.mode } : {}), package: imported.packageRevisionId, fragment: imported.fragmentId, digest: checked.definitionDigest, root: checked.declaration.root.interface, variables: imported.variables, }); - for (const [name, contract] of Object.entries(checked.fragmentContracts ?? {})) { + for (const [name, contract] of Object.entries( + imported.mode === "reference" ? {} : (checked.fragmentContracts ?? {}), + )) { markers.set(privateNames.get(name) ?? name, { ...contract, variables: Object.fromEntries( diff --git a/src/query/types.ts b/src/query/types.ts index 41279c6..2afa27e 100644 --- a/src/query/types.ts +++ b/src/query/types.ts @@ -38,6 +38,9 @@ export interface QueryDeclaration { /** Exported local projection; compilation also creates its direct admission. */ fragment?: { name: string; operation?: string; admission?: boolean }; importedFragments?: { + /** Bind an independently observed fragment without including its fields in + * this projection. Its acquisition is still an explicit replica source. */ + mode?: "reference"; packageRevisionId: string; packageAlias?: string; fragmentId: string; @@ -113,6 +116,7 @@ export type QueryArgument = export interface QuerySelection { /** A checked occurrence marker, not an output field or storage capability. */ fragmentSpread?: { + mode?: "reference"; package: string; fragment: string; digest: string; diff --git a/src/react-build/declarations.ts b/src/react-build/declarations.ts index 42fd8ab..72635ae 100644 --- a/src/react-build/declarations.ts +++ b/src/react-build/declarations.ts @@ -17,7 +17,6 @@ export interface ReactChildDeclaration { export interface ReactModulePreparation { module: string; composition: string; - discoveryComposition?: string; children: ReactChildDeclaration[]; } export interface ReactPreparationArtifact { diff --git a/src/react-build/expansion.ts b/src/react-build/expansion.ts index 823b74b..177d8ba 100644 --- a/src/react-build/expansion.ts +++ b/src/react-build/expansion.ts @@ -147,9 +147,6 @@ export async function expandPresentationQueries(workspace: WorkspaceRevision): P for (const child of children) for (const branch of child.branches) await expandModule(identity(branch.packageRevision, branch.id)); const composition = pkg.queryCompositions!.find((entry) => entry.id === preparation.composition)!; - const discovery = { ...composition, id: composition.id + ":discovery" }; - preparation.discoveryComposition = discovery.id; - pkg.queryCompositions!.push(discovery); const steps = structuredClone(composition.steps); const exported: { step: string; query: CheckedQuery }[] = []; for (const step of steps) { @@ -187,7 +184,12 @@ export async function expandPresentationQueries(workspace: WorkspaceRevision): P (entry) => entry.packageRevisionId === branch.packageRevision && entry.fragmentId === declaration.id, ) ) { - imports.push({ packageRevisionId: branch.packageRevision, fragmentId: declaration.id, variables: {} }); + imports.push({ + packageRevisionId: branch.packageRevision, + fragmentId: declaration.id, + variables: {}, + mode: "reference", + }); } selection = insert(selection, path, declaration.fragment!.name); const admission = admissionFragments.get(identity(branch.packageRevision, declaration.id))!; @@ -203,6 +205,18 @@ export async function expandPresentationQueries(workspace: WorkspaceRevision): P variables: {}, }); admissionSelection = insert(admissionSelection, path, admission.declaration.fragment!.name); + if ( + !admissionImports.some( + (entry) => entry.packageRevisionId === branch.packageRevision && entry.fragmentId === declaration.id, + ) + ) + admissionImports.push({ + packageRevisionId: branch.packageRevision, + fragmentId: declaration.id, + variables: {}, + mode: "reference", + }); + admissionSelection = insert(admissionSelection, path, declaration.fragment!.name); } } } @@ -337,13 +351,7 @@ export async function expandPresentationQueries(workspace: WorkspaceRevision): P { id: composition.id, input: composition.input, steps, maxQueries: composition.maxQueries }, available, ); - const checkedDiscovery = compileQueryComposition( - { id: discovery.id, input: discovery.input, steps: discovery.steps, maxQueries: discovery.maxQueries }, - available, - ); - pkg.queryCompositions = pkg.queryCompositions!.map((entry) => - entry.id === composition.id ? checked : entry.id === discovery.id ? checkedDiscovery : entry, - ); + pkg.queryCompositions = pkg.queryCompositions!.map((entry) => (entry.id === composition.id ? checked : entry)); eager.set(key, exported); done.add(key); } @@ -411,6 +419,7 @@ export async function expandPresentationQueries(workspace: WorkspaceRevision): P packageRevisionId: input.pkg.revisionId, fragmentId: input.query.declaration.id, variables: {}, + ...(original.declaration.placement?.kind === "local" ? { mode: "reference" as const } : {}), })), }, print({ ...document, definitions }), diff --git a/test/prepared-presentations.test.ts b/test/prepared-presentations.test.ts index ae901cc..d9afea5 100644 --- a/test/prepared-presentations.test.ts +++ b/test/prepared-presentations.test.ts @@ -124,19 +124,20 @@ async function fixture(replacement = "First", change?: (artifact: ReactPreparati test("workspace expands distinct providers' private inputs without parent field declarations", async () => { const { pkg } = await fixture(); const parent = pkg.checkedQueries!.find((query) => query.declaration.id === "react:input:library-card:data")!; - assert.match(parent.document, /title/); - assert.match(parent.document, /detail/); + const acquisition = pkg.checkedQueries!.find( + (query) => + query.declaration.id === + (parent.declaration.placement?.kind === "local" ? parent.declaration.placement.sourceQueryId : ""), + )!; + assert.match(acquisition.document, /title/); + assert.match(acquisition.document, /detail/); const plan = pkg.queryCompositions!.find((composition) => composition.id === "react:inputs:library-card")!; assert.equal(plan.steps[0]!.query, parent.declaration.id); assert.equal(Object.keys(parent.fragmentContracts ?? {}).length, 2); - const discovery = pkg.queryCompositions!.find( - (composition) => composition.id === "react:inputs:library-card:discovery", - )!; - assert.equal( - discovery.steps[0]!.query, - "LibraryData", - "Cold child data cannot block discovering its selected reference", - ); + assert(!pkg.queryCompositions!.some((composition) => composition.id.endsWith(":discovery"))); + const original = pkg.checkedQueries!.find((query) => query.declaration.id === "LibraryData")!; + assert.deepEqual(parent.output, original.output, "Acquisition-only child fields are not parent requirements"); + assert(Object.values(parent.fragmentContracts!).every((entry) => entry.mode === "reference")); }); test("eager cycles and wrongly typed child bindings fail before runtime", async () => { @@ -167,7 +168,16 @@ test("eager cycles and wrongly typed child bindings fail before runtime", async test("workspace replacement changes acquisition requirements without changing the parent", async () => { const { pkg } = await fixture("Second"); const parent = pkg.checkedQueries!.find((query) => query.declaration.id === "react:input:library-card:data")!; - assert.doesNotMatch(parent.document, /\btitle\b/); - assert.match(parent.document, /detail/); + const acquisition = pkg.checkedQueries!.find( + (query) => + query.declaration.id === + (parent.declaration.placement?.kind === "local" ? parent.declaration.placement.sourceQueryId : ""), + )!; + assert.doesNotMatch(acquisition.document, /\btitle\b/); + assert.match(acquisition.document, /detail/); assert.equal(Object.keys(parent.fragmentContracts ?? {}).length, 1); + const previous = (await fixture()).pkg.checkedQueries!.find( + (query) => query.declaration.id === parent.declaration.id, + )!; + assert.deepEqual(parent.output, previous.output); }); diff --git a/test/query-fragments.test.ts b/test/query-fragments.test.ts index 880f0e9..c4e38a2 100644 --- a/test/query-fragments.test.ts +++ b/test/query-fragments.test.ts @@ -62,6 +62,21 @@ test("imported fragments retain checked occurrences and require exact roots", as assert.equal(result.selection[0]!.selection[0]!.fragmentSpread?.digest, checked.definitionDigest); assert.equal(result.selection[0]!.selection[0]!.fragmentSpread?.root, item.revisionId); assert.ok(result.effects.some((e) => e.memberId === "title")); + const bound = await compileQuery( + { + ...consumer.queries![0]!, + importedFragments: consumer.queries![0]!.importedFragments!.map((entry) => ({ ...entry, mode: "reference" })), + }, + [item], + async () => "query Screen {root {...Row}}", + async () => checked, + ); + assert.equal(bound.selection[0]!.selection.find((entry) => entry.fragmentSpread)?.fragmentSpread?.mode, "reference"); + assert( + !bound.selection[0]!.selection.some((entry) => entry.memberId === "title"), + "A fragment binding does not evaluate its fields in the parent", + ); + assert(!JSON.stringify(bound.output).includes('"title"'), "Parent output excludes separately observed fields"); const wrong = { ...checked, declaration: { @@ -113,6 +128,39 @@ test("fragment variable mappings are explicit and checked", async () => { async () => checked, ); assert.deepEqual(result.selection[0]!.selection[0]!.fragmentSpread?.variables, { show: "visible" }); + const referenceDeclaration = build("with {show to visible;}"); + referenceDeclaration.importedFragments = referenceDeclaration.importedFragments!.map((entry) => ({ + ...entry, + mode: "reference", + })); + const binding = await compileQuery( + referenceDeclaration, + [item], + async () => document, + async () => checked, + ); + assert.deepEqual(binding.selection[0]!.selection[0]!.fragmentSpread?.variables, { show: "visible" }); + assert.notEqual(binding.definitionDigest, result.definitionDigest); + await assert.rejects( + compileQuery( + referenceDeclaration, + [item], + async () => document.replace("$visible: Boolean!", "$visible: Boolean!, $unused: Boolean!"), + async () => checked, + ), + /never used/, + ); + const changedDigest = await compileQuery( + referenceDeclaration, + [item], + async () => document, + async () => ({ ...checked, definitionDigest: "b".repeat(64) }), + ); + assert.notEqual( + changedDigest.definitionDigest, + binding.definitionDigest, + "A bound fragment's identity remains in the parent contract digest", + ); await assert.rejects( compileQuery( build("with {show to visible;}"),