Add exported query fragments and scoped coverage observations

This commit is contained in:
Timothy J. Aveni
2026-09-22 16:18:57 -07:00
parent eba85ba158
commit a8869e4545
13 changed files with 2503 additions and 1715 deletions
+31 -6
View File
@@ -276,12 +276,37 @@ const createResourceGraphResolver = (quixosCommit: string, resolveResource: Capa
await checkQueryTemplate(template, queryInterfaces, (name) =>
readRepositorySource(snapshot.directory, name),
);
if (compiled.resource.revision.queries?.length)
compiled.resource.revision.checkedQueries = await Promise.all(
compiled.resource.revision.queries.map((query) =>
compileRepositoryQuery(snapshot.directory, query, queryInterfaces),
),
);
if (compiled.resource.revision.queries?.length) {
const pkg = compiled.resource.revision;
const checked = new Map<string, import("../query/types.js").CheckedQuery>();
const compile = async (
id: string,
parents: readonly string[] = [],
): Promise<import("../query/types.js").CheckedQuery> => {
if (parents.includes(id)) throw new Error(`Query fragment cycle: ${[...parents, id].join(" -> ")}`);
const previous = checked.get(id);
if (previous) return previous;
const query = pkg.queries!.find((query) => query.id === id);
if (!query) throw new Error(`Unknown fragment ${pkg.revisionId}/${id}`);
const result = await compileRepositoryQuery(
snapshot.directory,
query,
queryInterfaces,
async (packageId, fragmentId) => {
if (packageId === pkg.revisionId) return compile(fragmentId, [...parents, id]);
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);
return result;
};
pkg.checkedQueries = [];
for (const query of pkg.queries!) pkg.checkedQueries.push(await compile(query.id));
}
let controllerText: string | undefined;
try {
controllerText = await readRepositorySource(snapshot.directory, "quixos.controllers.json");
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -35,6 +35,7 @@ import { PresentationDeclContext } from "./QuixosCapabilityParser.js";
import { PackageQueryContext } from "./QuixosCapabilityParser.js";
import { PackageQuerySpecializationContext } from "./QuixosCapabilityParser.js";
import { QueryClauseContext } from "./QuixosCapabilityParser.js";
import { FragmentVariableContext } from "./QuixosCapabilityParser.js";
import { PackageOperationExportContext } from "./QuixosCapabilityParser.js";
import { PackageFunctionExportContext } from "./QuixosCapabilityParser.js";
import { AcceptanceClauseContext } from "./QuixosCapabilityParser.js";
@@ -291,6 +292,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result
*/
visitQueryClause?: (ctx: QueryClauseContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.fragmentVariable`.
* @param ctx the parse tree
* @return the visitor result
*/
visitFragmentVariable?: (ctx: FragmentVariableContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.packageOperationExport`.
* @param ctx the parse tree
+64 -3
View File
@@ -1380,6 +1380,14 @@ const lowerPackage = (
if (exportContext.packageQuerySpecialization()) return [];
const query = exportContext.packageQuery();
if (query) {
const isFragment = query.FRAGMENT().length > 0;
if (isFragment && (query.FRAGMENT().length !== 2 || query.typeParameters() || query.WORKSPACE()))
loweringIssue(
state,
query,
"invalid-query-fragment",
"Export a fragment on an exact object interface; generic fragments and workspace fragment roots are not supported",
);
const previousParameters = state.types.parameters;
state.types.parameters = new Map();
try {
@@ -1403,7 +1411,19 @@ const lowerPackage = (
displayName: identifier(query.identifier()),
root: { kind: query.WORKSPACE() ? "workspace-interface" : "object", interface: close(query.interfaceType()) },
document: stringValue(query.stringLiteral(1)),
operation: stringValue(query.stringLiteral(2)),
operation: isFragment
? query.stringLiteral(3)
? stringValue(query.stringLiteral(3))
: "QuixosFragment"
: stringValue(query.stringLiteral(2)),
...(isFragment
? {
fragment: {
name: stringValue(query.stringLiteral(2)),
...(query.stringLiteral(3) ? { operation: stringValue(query.stringLiteral(3)) } : {}),
},
}
: {}),
fragments: [],
views: [],
allowances: [],
@@ -1436,8 +1456,32 @@ const lowerPackage = (
);
if (sourceQueryId) declaration.placement = { kind: "local", sourceQueryId };
}
} else if (clause.FRAGMENTS()) declaration.fragments.push(stringValue(clause.stringLiteral()!));
else if (clause.VIEW()) {
} else if (clause.FRAGMENTS()) {
if (clause.stringLiteral()) declaration.fragments.push(stringValue(clause.stringLiteral()!));
else {
const imported = lowerQueryReference(
state,
identifier(clause.identifier(0)),
identifier(clause.identifier(1)),
clause,
);
if (imported) {
const variables: Record<string, string> = {};
for (const mapping of clause.fragmentVariable()) {
const name = identifier(mapping.identifier(0));
if (Object.hasOwn(variables, name))
loweringIssue(state, mapping, "duplicate-fragment-variable", `Repeated fragment variable ${name}`);
variables[name] = identifier(mapping.identifier(1));
}
(declaration.importedFragments ??= []).push({
packageRevisionId: imported.packageRevisionId,
packageAlias: identifier(clause.identifier(0)),
fragmentId: imported.queryId,
variables,
});
}
}
} else if (clause.VIEW()) {
const name = identifier(clause.identifier(0));
if (parameters.length) {
const parameter = state.types.parameters.get(name);
@@ -1548,6 +1592,23 @@ const lowerPackage = (
views: templateViews,
allowances: templateAllowances,
});
} else if (isFragment) {
if (declaration.placement || declaration.polling || declaration.allowances.length)
loweringIssue(
state,
query,
"invalid-query-fragment",
"Fragments are local projections with generated admission; remote allowances, polling and explicit placement are not supported",
);
const sourceQueryId = `${declaration.id}:fragment-inputs`;
queries.push({
...structuredClone(declaration),
id: sourceQueryId,
displayName: `${declaration.displayName}$inputs`,
placement: { kind: "replica" },
fragment: { ...declaration.fragment!, admission: true },
});
queries.push({ ...declaration, placement: { kind: "local", sourceQueryId } });
} else queries.push(declaration);
return [];
} finally {
+63 -7
View File
@@ -124,12 +124,64 @@ export function generateInterfaceClientBindings(
export function generatePackageQueryClientBindings(
pkg: PackageRevision,
runtimeModule = "@quixos/camino-replica-core",
packages: readonly PackageRevision[] = [pkg],
): string {
const queries = (pkg.checkedQueries ?? []).map(
(query) =>
`${JSON.stringify(query.declaration.displayName)}: definePackageQuery<${tsType(query.variables)}, ${tsType(query.output)}, ${JSON.stringify(publicQueryContract(pkg, query.declaration.id).placement)}>(${JSON.stringify(publicQueryContract(pkg, query.declaration.id))})`,
);
return `// Generated from checked package query contracts. Regenerate; do not edit.\nimport { definePackageQuery, type ManagedReference } from ${JSON.stringify(runtimeModule)};\nexport const queries = {${queries.join(",\n")}} as const;\n`;
const queries = (pkg.checkedQueries ?? [])
.filter((query) => !query.declaration.fragment)
.map(
(query) =>
`${JSON.stringify(query.declaration.displayName)}: definePackageQuery<${tsType(query.variables)}, ${tsType(query.output)}, ${JSON.stringify(publicQueryContract(pkg, query.declaration.id).placement)}>(${JSON.stringify(publicQueryContract(pkg, query.declaration.id))})`,
);
const emitFragment = (provider: PackageRevision, id: string) => {
const query = provider.checkedQueries?.find((query) => query.declaration.id === id);
if (
!query?.declaration.fragment ||
query.declaration.fragment.admission ||
query.declaration.placement?.kind !== "local" ||
query.output.kind !== "record" ||
!query.output.fields.root
)
throw new ExecutionPlanError("UNCHECKED_FRAGMENT", id, "Expected a checked local fragment export");
const output = query.output.fields.root;
const descriptor = {
package: provider.revisionId,
fragment: id,
digest: query.definitionDigest,
subject: query.declaration.root.interface,
input: portableValueSchema(query.variables),
output: portableValueSchema(output),
admission: publicQueryContract(provider, query.declaration.placement.sourceQueryId),
view: publicQueryContract(provider, id),
};
return `definePackageFragment<${tsType(query.variables)}, ${tsType(output)}, ${JSON.stringify(query.declaration.root.interface)}>(${JSON.stringify(descriptor)})`;
};
const fragments = (pkg.checkedQueries ?? [])
.filter((query) => query.declaration.fragment && !query.declaration.fragment.admission)
.map((query) => `${JSON.stringify(query.declaration.displayName)}: ${emitFragment(pkg, query.declaration.id)}`);
const imports = new Map<string, Map<string, string>>();
for (const query of pkg.queries ?? [])
for (const imported of query.importedFragments ?? []) {
if (imported.packageRevisionId === pkg.revisionId) continue;
const provider = packages.find((provider) => provider.revisionId === imported.packageRevisionId);
const name = provider?.queries?.find((query) => query.id === imported.fragmentId)?.displayName;
if (!provider || !name)
throw new ExecutionPlanError("UNCHECKED_FRAGMENT", imported.fragmentId, "Missing imported fragment provider");
const alias = imported.packageAlias ?? provider.displayName;
if (pkg.queries?.some((query) => query.fragment && query.displayName === alias))
throw new ExecutionPlanError(
"FRAGMENT_ALIAS_COLLISION",
alias,
"Imported fragment namespace conflicts with a local fragment export",
);
const entries = imports.get(alias) ?? new Map<string, string>();
entries.set(name, emitFragment(provider, imported.fragmentId));
imports.set(alias, entries);
}
for (const [alias, entries] of imports)
fragments.push(
`${JSON.stringify(alias)}: {${[...entries].map(([name, value]) => `${JSON.stringify(name)}: ${value}`).join(",")}}`,
);
return `// Generated from checked package query contracts. Regenerate; do not edit.\nimport { definePackageQuery, definePackageFragment, type ManagedReference } from ${JSON.stringify(runtimeModule)};\nexport const queries = {${queries.join(",\n")}} as const;\nexport const fragments = {${fragments.join(",\n")}} as const;\n`;
}
/** Receipt-covered bindings expose the package's imports, component subjects and
@@ -157,13 +209,17 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi
.filter((line) => !line.startsWith("import "))
.join("\n")
.replace("export const interfaces =", "export const subjects =");
const queries = generatePackageQueryClientBindings(pkg, "@quixos/camino-react")
const queries = generatePackageQueryClientBindings(
pkg,
"@quixos/camino-react",
plan.definition.workspace.packageImports,
)
.split("\n")
.filter((line) => !line.startsWith("import "))
.join("\n");
return (
interfaces +
'\nimport {definePackageQuery,type ComponentType,type ComponentProps} from "@quixos/camino-react";\n' +
'\nimport {definePackageQuery,definePackageFragment,type ComponentType,type ComponentProps} from "@quixos/camino-react";\n' +
subjectBindings +
"\n" +
queries +
+9
View File
@@ -30,6 +30,15 @@ export function compileQueryProgram(plan: ExecutionPlan): unknown {
const checked = query.checked;
return {
id,
...(checked.declaration.fragment && !checked.declaration.fragment.admission
? {
fragment: {
package: query.packageRevisionId,
id: checked.declaration.id,
digest: checked.definitionDigest,
},
}
: {}),
placement: checked.declaration.placement?.kind ?? "remote",
source:
checked.declaration.placement?.kind === "local"
+30 -7
View File
@@ -37,6 +37,7 @@ import { querySchema } from "./schema.js";
import { objectRow } from "./relational-schema.js";
import { workspaceQueryRootId } from "./root.js";
import { relationalCompiler } from "./relational-compile.js";
import { fragmentSources, type FragmentResolver } from "./fragments.js";
import {
QueryCompileError,
type QueryDeclaration,
@@ -67,6 +68,7 @@ export async function compileQuery(
declaration: QueryDeclaration,
interfaces: readonly InterfaceRevision[],
read: (name: string) => Promise<string>,
resolveFragment?: FragmentResolver,
): Promise<CheckedQuery> {
const paths = [declaration.document, ...declaration.fragments];
if (paths.length > 128 || new Set(paths).size !== paths.length)
@@ -86,7 +88,10 @@ export async function compileQuery(
const document = parse(new Source(text, path), { maxTokens: 10000 });
definitions.push(...document.definitions);
}
const fragmentContracts = await fragmentSources(declaration, definitions, resolveFragment);
const document: DocumentNode = { kind: "Document" as DocumentNode["kind"], definitions };
if (definitions.length > 256 || Buffer.byteLength(print(document)) > 262144)
fail("QUERY_SOURCE_LIMIT", "Expanded fragment sources exceed the query source budget");
visit(document, {
Directive(node) {
if (node.name.value === "live")
@@ -112,7 +117,7 @@ export async function compileQuery(
if (node.name.value.startsWith("__")) fail("QUERY_UNSUPPORTED_FEATURE", "Introspection is not supported", node);
},
Directive(node) {
if (!["include", "skip", "live"].includes(node.name.value))
if (!["include", "skip"].includes(node.name.value))
fail("QUERY_UNSUPPORTED_FEATURE", `Unsupported directive ${node.name.value}`, node);
},
InlineFragment(node) {
@@ -400,11 +405,27 @@ export async function compileQuery(
inherited: QuerySelection["conditions"] = [],
): QuerySelection[] =>
set.selections.flatMap((node): QuerySelection[] => {
if (node.kind === "FragmentSpread")
return selection(fragments.get(node.name.value)!.selectionSet, parent, [
...inherited,
...conditions(node.directives),
]);
if (node.kind === "FragmentSpread") {
const inheritedConditions = [...inherited, ...conditions(node.directives)];
const contract = fragmentContracts.get(node.name.value);
if (contract && generated.byName.get(parent.name)?.revisionId !== contract.root)
fail("FRAGMENT_ROOT_MISMATCH", "Imported fragment requires its exact nominal root interface", node);
return [
...(contract
? [
{
name: "$fragment",
key: "",
selection: [],
arguments: {},
conditions: inheritedConditions,
fragmentSpread: contract,
},
]
: []),
...selection(fragments.get(node.name.value)!.selectionSet, parent, inheritedConditions),
];
}
if (node.kind !== "Field") return [];
const contract = generated.byName.get(parent.name);
const member = contract?.members.find((entry) => entry.displayName === node.name.value);
@@ -471,6 +492,7 @@ export async function compileQuery(
.map((entry) => [entry.variable.name.value, argument(entry.defaultValue!)]),
),
sourceFiles: paths,
...(fragmentContracts.size ? { fragmentContracts: Object.fromEntries(fragmentContracts) } : {}),
};
}
@@ -478,10 +500,11 @@ export const compileRepositoryQuery = async (
root: string,
declaration: QueryDeclaration,
interfaces: readonly InterfaceRevision[],
resolveFragment?: FragmentResolver,
) => {
const started = performance.now();
try {
return await compileQuery(declaration, interfaces, (name) => readRepositorySource(root, name));
return await compileQuery(declaration, interfaces, (name) => readRepositorySource(root, name), resolveFragment);
} finally {
// Timing belongs in check logs, never in immutable artifact identities.
console.error(
+120
View File
@@ -0,0 +1,120 @@
import { parse, print, visit, type DocumentNode, type FragmentDefinitionNode } from "graphql";
import { QueryCompileError, type CheckedQuery, type QueryDeclaration, type QuerySelection } from "./types.js";
export type FragmentResolver = (packageRevisionId: string, fragmentId: string) => Promise<CheckedQuery>;
/** Expand only explicitly imported exports. Each variable mapping is checked by
* GraphQL against the consuming operation, rather than silently inheriting names. */
export async function fragmentSources(
declaration: QueryDeclaration,
definitions: DocumentNode["definitions"][number][],
resolve?: FragmentResolver,
) {
const markers = new Map<string, NonNullable<QuerySelection["fragmentSpread"]>>();
for (const imported of declaration.importedFragments ?? []) {
if (!resolve)
throw new QueryCompileError(
"FRAGMENT_RESOLVER_REQUIRED",
"Resolve imported fragments through package dependencies",
);
const checked = await resolve(imported.packageRevisionId, imported.fragmentId);
const exported = checked.declaration.fragment;
if (!exported || exported.admission) throw new QueryCompileError("NOT_EXPORTED_FRAGMENT", imported.fragmentId);
const variables = checked.variables.kind === "record" ? Object.keys(checked.variables.fields) : [];
if (JSON.stringify(Object.keys(imported.variables).sort()) !== JSON.stringify(variables.sort()))
throw new QueryCompileError(
"FRAGMENT_VARIABLE_MAPPING",
`Map every variable of ${checked.declaration.displayName} explicitly`,
);
const source = parse(checked.document);
const sourceOperation = source.definitions.find(
(node) => node.kind === "OperationDefinition" && node.name?.value === checked.declaration.operation,
);
const consumingOperation = definitions.find(
(node) => node.kind === "OperationDefinition" && node.name?.value === declaration.operation,
);
for (const variable of sourceOperation?.kind === "OperationDefinition"
? (sourceOperation.variableDefinitions ?? [])
: []) {
const name = imported.variables[variable.variable.name.value];
const target =
consumingOperation?.kind === "OperationDefinition"
? consumingOperation.variableDefinitions?.find((v) => v.variable.name.value === name)
: undefined;
if (!target || print(target.type) !== print(variable.type))
throw new QueryCompileError(
"FRAGMENT_VARIABLE_TYPE",
`Mapped variable ${name} must have type ${print(variable.type)}`,
);
}
const fragments = source.definitions.filter(
(node): node is FragmentDefinitionNode => node.kind === "FragmentDefinition",
);
if (definitions.some((node) => "name" in node && node.name?.value === exported.name))
throw new QueryCompileError("FRAGMENT_NAME_COLLISION", exported.name);
const privateNames = new Map(
fragments.map((node) => [
node.name.value,
node.name.value === exported.name ? exported.name : `${exported.name}__${node.name.value}`,
]),
);
for (const fragment of fragments) {
definitions.push(
visit(fragment, {
FragmentDefinition(node) {
return { ...node, name: { ...node.name, value: privateNames.get(node.name.value)! } };
},
FragmentSpread(node) {
return { ...node, name: { ...node.name, value: privateNames.get(node.name.value) ?? node.name.value } };
},
Variable(node) {
return { ...node, name: { ...node.name, value: imported.variables[node.name.value] ?? node.name.value } };
},
}),
);
}
markers.set(exported.name, {
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 ?? {})) {
markers.set(privateNames.get(name) ?? name, {
...contract,
variables: Object.fromEntries(
Object.entries(contract.variables).map(([inner, outer]) => [inner, imported.variables[outer] ?? outer]),
),
});
}
}
if (declaration.fragment) {
const exported = declaration.fragment;
if (!definitions.some((node) => node.kind === "FragmentDefinition" && node.name.value === exported.name))
throw new QueryCompileError("FRAGMENT_MISSING", exported.name);
const operations = definitions.filter((node) => node.kind === "OperationDefinition");
if (!exported.operation) {
if (operations.length)
throw new QueryCompileError(
"FRAGMENT_OPERATION_REQUIRED",
"Name the standalone operation explicitly when a fragment document includes variables or an operation",
);
definitions.push(...parse(`query ${declaration.operation} { root { ...${exported.name} } }`).definitions);
} else {
const operation = operations.find(
(node) => node.kind === "OperationDefinition" && node.name?.value === exported.operation,
);
if (!operation || operation.kind !== "OperationDefinition")
throw new QueryCompileError("FRAGMENT_OPERATION_MISSING", exported.operation);
// The standalone operation specifies variables only. Its result must be the
// exact exported fragment, not additional fields or a different window.
const expected = parse(`query ${declaration.operation} { root { ...${exported.name} } }`).definitions[0]!;
if (expected.kind !== "OperationDefinition" || print(operation.selectionSet) !== print(expected.selectionSet))
throw new QueryCompileError(
"FRAGMENT_STANDALONE_SHAPE",
"Standalone operation must select root { ...ExportedFragment }",
);
}
}
return markers;
}
+17
View File
@@ -35,6 +35,14 @@ export type QueryRoot<Interface = InterfaceRevisionId> =
| { kind: "object"; interface: Interface }
| { kind: "workspace-interface"; interface: Interface };
export interface QueryDeclaration {
/** Exported local projection; compilation also creates its direct admission. */
fragment?: { name: string; operation?: string; admission?: boolean };
importedFragments?: {
packageRevisionId: string;
packageAlias?: string;
fragmentId: string;
variables: Record<string, string>;
}[];
placement?: { kind: "replica" } | { kind: "local"; sourceQueryId: string };
id: string;
displayName: string;
@@ -83,6 +91,7 @@ export class QueryCompileError extends Error {
}
}
export interface CheckedQuery {
fragmentContracts?: Record<string, NonNullable<QuerySelection["fragmentSpread"]>>;
declaration: QueryDeclaration;
definitionDigest: string;
/** Fixed, validated document: never supplied by runtime callers. */
@@ -102,6 +111,14 @@ export type QueryArgument =
| { kind: "list"; values: QueryArgument[] }
| { kind: "object"; fields: Record<string, QueryArgument> };
export interface QuerySelection {
/** A checked occurrence marker, not an output field or storage capability. */
fragmentSpread?: {
package: string;
fragment: string;
digest: string;
root: InterfaceRevisionId;
variables: Record<string, string>;
};
name: string;
key: string;
interfaceRevisionId?: InterfaceRevisionId;