Add checked query composition foundations for prepared presentations

Move React navigation query generation behind an explicit build frontend. Add checked result-dependent query bindings and pure composition evaluation, including one-snapshot server execution. Preserve the existing rendering path while the React manifest, worker preparation, and replica admission integration remain pending; see docs/PREPARED_PRESENTATIONS_PROGRESS.md.
This commit is contained in:
Timothy J. Aveni
2026-09-27 16:28:33 -07:00
parent ad64ba532f
commit fdb7cd3f6f
14 changed files with 595 additions and 23 deletions
+78 -22
View File
@@ -2,7 +2,8 @@ import { readFile, realpath } from "node:fs/promises";
import path from "node:path";
import { loadQxSources, resolveQxSources } from "./source-loader.js";
import { compileQuery, compileRepositoryQuery } from "../query/compile.js";
import { presentationQueries } from "../bindings/presentation-queries.js";
import type { QueryDeclaration } from "../query/types.js";
import { compileQueryComposition, type QueryComposition } from "../query/composition.js";
import { checkQueryTemplate } from "../query/templates.js";
import { readRepositorySource } from "./source-loader.js";
import { parseControllerCatalog } from "../capability-model/controllers.js";
@@ -42,6 +43,19 @@ export type CapabilityRepositoryResolver = (
kind: LockedResource["kind"],
) => Promise<CapabilityRepositorySnapshot>;
/** An explicitly selected build frontend supplies ordinary query sources. The
* assembler never chooses a frontend by inspecting an interface/contract ID.
* Generated sources go through the same query and imported-fragment checks. */
export type PackageQueryGenerator = (input: {
readonly package: Readonly<PackageRevision>;
readonly interfaces: readonly InterfaceRevision[];
readonly readSource: (name: string) => Promise<string>;
}) => Promise<{
readonly queries: readonly QueryDeclaration[];
readonly documents: ReadonlyMap<string, string>;
readonly compositions?: readonly QueryComposition[];
}>;
export type ResolvedCapabilityResource = {
key: string;
kind: LockedResource["kind"];
@@ -198,7 +212,11 @@ const environmentFor = (
};
};
const createResourceGraphResolver = (quixosCommit: string, resolveResource: CapabilityRepositoryResolver) => {
const createResourceGraphResolver = (
quixosCommit: string,
resolveResource: CapabilityRepositoryResolver,
generateQueries?: PackageQueryGenerator,
) => {
const resolved = new Map<string, Promise<ResolvedCapabilityResource>>();
const active: string[] = [];
@@ -273,7 +291,22 @@ const createResourceGraphResolver = (quixosCommit: string, resolveResource: Capa
...(environment.interfaceClosure ?? []),
...(compiled.resource.specializations ?? []),
];
const presentationDocuments = presentationQueries(compiled.resource.revision, queryInterfaces);
const generated = await generateQueries?.({
package: structuredClone(compiled.resource.revision),
interfaces: structuredClone(queryInterfaces),
readSource: (name) => readRepositorySource(snapshot.directory, name),
});
if (generated) {
const queries = [...(compiled.resource.revision.queries ?? [])];
for (const query of generated.queries) {
if (queries.some((entry) => entry.id === query.id || entry.displayName === query.displayName))
throw new Error(`Generated query conflicts with an authored query: ${query.id}`);
if (!generated.documents.has(query.document))
throw new Error(`Generated query has no source: ${query.document}`);
queries.push(structuredClone(query));
}
compiled.resource.revision.queries = queries;
}
for (const template of compiled.resource.revision.queryTemplates ?? [])
await checkQueryTemplate(template, queryInterfaces, (name) =>
readRepositorySource(snapshot.directory, name),
@@ -290,29 +323,42 @@ const createResourceGraphResolver = (quixosCommit: string, resolveResource: Capa
if (previous) return previous;
const query = pkg.queries!.find((query) => query.id === id);
if (!query) throw new Error(`Unknown fragment ${pkg.revisionId}/${id}`);
const generatedDocument = presentationDocuments.get(query.document);
const result =
generatedDocument !== undefined
? await compileQuery(query, queryInterfaces, async () => generatedDocument)
: 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;
},
);
const resolveFragment = async (packageId: string, fragmentId: string) => {
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;
};
const result = generated?.documents.has(query.document)
? await compileQuery(
query,
queryInterfaces,
async (name) =>
generated.documents.get(name) ?? (await readRepositorySource(snapshot.directory, name)),
resolveFragment,
)
: await compileRepositoryQuery(snapshot.directory, query, queryInterfaces, resolveFragment);
checked.set(id, result);
return result;
};
pkg.checkedQueries = [];
for (const query of pkg.queries!) pkg.checkedQueries.push(await compile(query.id));
}
if (generated?.compositions?.length) {
const pkg = compiled.resource.revision;
const queries = new Map((pkg.checkedQueries ?? []).map((query) => [query.declaration.id, query]));
for (const dependency of environment.packageClosure ?? [])
for (const query of dependency.checkedQueries ?? [])
queries.set(JSON.stringify([dependency.revisionId, query.declaration.id]), query);
const ids = new Set<string>();
pkg.queryCompositions = generated.compositions.map((composition) => {
if (ids.has(composition.id)) throw new Error(`Duplicate query composition: ${composition.id}`);
ids.add(composition.id);
return compileQueryComposition(composition, queries);
});
}
let controllerText: string | undefined;
try {
controllerText = await readRepositorySource(snapshot.directory, "quixos.controllers.json");
@@ -377,12 +423,17 @@ export const compileCapabilityResourceRepository = async (options: {
kind: LockedResource["kind"];
source: GitSource;
resolveResource: CapabilityRepositoryResolver;
generateQueries?: PackageQueryGenerator;
}): Promise<CompiledCapabilityResourceRepository> => {
const lockResult = await loadQuixosLock(path.join(options.rootDirectory, "quixos.lock"));
if (!lockResult.ok) {
throw new Error(diagnosticsMessage("Resource lock", lockResult.diagnostics));
}
const resolver = createResourceGraphResolver(lockResult.lock.quixos.commit, options.resolveResource);
const resolver = createResourceGraphResolver(
lockResult.lock.quixos.commit,
options.resolveResource,
options.generateQueries,
);
const root = await resolver.visit(
{
kind: options.kind,
@@ -408,13 +459,18 @@ export const compileWorkspaceRepository = async (options: {
sourceRootCommit?: string;
/** An editor's proposed source snapshot; locks and dependency revisions remain exact. */
readSource?: (name: string) => Promise<string>;
generateQueries?: PackageQueryGenerator;
}): Promise<CompiledWorkspaceRepository> => {
const rootLockResult = await loadQuixosLock(path.join(options.rootDirectory, "quixos.lock"));
if (!rootLockResult.ok) {
throw new Error(diagnosticsMessage("Workspace lock", rootLockResult.diagnostics));
}
const rootLock = rootLockResult.lock;
const resolver = createResourceGraphResolver(rootLock.quixos.commit, options.resolveResource);
const resolver = createResourceGraphResolver(
rootLock.quixos.commit,
options.resolveResource,
options.generateQueries,
);
const directPairs: Array<[LockedResource, ResolvedCapabilityResource]> = [];
for (const locked of rootLock.resources) {
@@ -55,12 +55,17 @@ export async function scaffoldComponent(
const insertion = ` ${nixLiteral(spec.id)} = { entry = ${nixLiteral(`src/components/${spec.name}.tsx`)}; export = ${nixLiteral(spec.name)}; };\n`;
const after = before.replace(/\n}(\s*)$/, (_match, trailing) => "\n" + insertion + "}" + trailing);
const parentID = parent.children.find((node) => node.kind === "stringLiteral")!;
const configFile = prefix + "quixos.check.json";
const configBefore = await fs.readFile(path.join(root, configFile), "utf8");
const config = JSON.parse(configBefore);
config.queryFrontends = [...new Set([...(config.queryFrontends ?? []), "react"])];
return {
kind: "package",
source: spec.source,
resourceRoot: spec.directory,
validation: "syntax",
files: [
{ file: configFile, expected: configBefore, replace: JSON.stringify(config, null, 2) + "\n" },
{
file: prefix + "package.qx",
edits: [
@@ -0,0 +1,13 @@
import path from "node:path";
import { pathToFileURL } from "node:url";
import type { PackageQueryGenerator } from "./assembly.js";
/** The build invoker selects this executable explicitly. Never resolve a
* generator by a capability name or execute a module discovered in user QX. */
export async function loadQueryGenerator(filename?: string): Promise<PackageQueryGenerator | undefined> {
if (!filename) return undefined;
const module = await import(pathToFileURL(path.resolve(filename)).href);
if (typeof module.generatePackageQueries !== "function")
throw new Error("Query generator must export generatePackageQueries");
return module.generatePackageQueries;
}
+5
View File
@@ -5,10 +5,12 @@ import process from "node:process";
import { bindingSchema } from "../bindings/index.js";
import { compileCapabilityResourceRepository, type ResolvedCapabilityResource } from "./assembly.js";
import { createGitCapabilityResolver } from "./git-resolver.js";
import { loadQueryGenerator } from "./query-generator.js";
const usage = `usage: quixos-resource-compile --root DIRECTORY --kind interface|package|build
--repository URL --commit GIT_REV --checkout-root DIRECTORY
[--snapshot-map PATH] [--snapshot-only true] [--graph-out PATH] [--schema-out PATH]
[--query-generator BUILD_MODULE_PATH]
Resolves a standalone resource's recursive dependency-lock graph, validates
its interface.qx or package.qx manifest, and emits the checked resource as JSON.`;
@@ -30,6 +32,7 @@ const parseArgs = (args: string[]) => {
"--snapshot-only",
"--graph-out",
"--schema-out",
"--query-generator",
].includes(key) ||
values.has(key)
)
@@ -65,6 +68,7 @@ const parseArgs = (args: string[]) => {
snapshotOnly: values.get("--snapshot-only") === "true",
graphOut: values.get("--graph-out"),
schemaOut: values.get("--schema-out"),
queryGenerator: values.get("--query-generator"),
} as const;
};
@@ -106,6 +110,7 @@ const main = async () => {
commit: options.commit,
},
resolveResource,
generateQueries: await loadQueryGenerator(options.queryGenerator),
});
if (options.graphOut) {
await writeFile(
+4
View File
@@ -4,6 +4,7 @@ import { readFile, writeFile } from "node:fs/promises";
import process from "node:process";
import { compileWorkspaceRepository } from "./assembly.js";
import { createGitCapabilityResolver } from "./git-resolver.js";
import { loadQueryGenerator } from "./query-generator.js";
import { compileExecutionPlan } from "../execution-world/index.js";
import { bindingSchema, specializeBindingSchema } from "../bindings/index.js";
import {
@@ -17,6 +18,7 @@ const usage = `usage: quixos-workspace-compile --root DIRECTORY --checkout-root
[--snapshot-map PATH] [--graph-out PATH] [--workspace-id ID] [--workspace-revision-id ID]
[--source-root-commit GIT_REV] [--baseline PLAN_JSON] [--evolution-out PATH]
[--reviews REVIEW_JSON] [--schemas-out PATH] [--execution-plan-out PATH]
[--query-generator BUILD_MODULE_PATH]
Resolves a workspace's recursive resource-lock graph, clones every exact
resource revision, validates standalone interface/package manifests, and emits
@@ -46,6 +48,7 @@ const parseArgs = (args: string[]) => {
baseline: values.get("--baseline"),
evolutionOut: values.get("--evolution-out"),
reviews: values.get("--reviews"),
queryGenerator: values.get("--query-generator"),
};
};
@@ -65,6 +68,7 @@ const main = async () => {
workspaceRevisionId: options.workspaceRevisionId,
sourceRootCommit: options.sourceRootCommit,
resolveResource,
generateQueries: await loadQueryGenerator(options.queryGenerator),
});
if (options.graphOut) {
+1
View File
@@ -330,6 +330,7 @@ export interface PackageRevision {
controllers?: PackageController[];
queries?: import("../query/types.js").QueryDeclaration[];
checkedQueries?: import("../query/types.js").CheckedQuery[];
queryCompositions?: import("../query/composition.js").CheckedQueryComposition[];
queryTemplates?: import("../query/types.js").QueryTemplate[];
genericExports?: import("./generic-packages.js").GenericPackageExport[];
argumentRequirements?: { target: ObjectExpectation; required: InterfaceRevisionId }[];
+32
View File
@@ -3,6 +3,7 @@ import { compileInspectionTemplates } from "./inspection.js";
import { portableValueSchema } from "../bindings/portable.js";
import { ExecutionPlanError, hexDigest, type ExecutionPlan } from "./plan.js";
import type { ValueType } from "../capability-model/types.js";
import { compileQueryComposition } from "../query/composition.js";
const lowerTypes = (value: unknown): unknown =>
JSON.parse(
@@ -25,9 +26,40 @@ export function compileQueryProgram(plan: ExecutionPlan): unknown {
if (hexDigest(plan.definition) !== plan.digest)
throw new ExecutionPlanError("INVALID_PLAN", "queries", "Plan digest mismatch");
const interfaces = plan.definition.workspace.interfaceImports;
const allQueries = new Map(plan.definition.queries.map(({ id, query }) => [id, query.checked]));
const compositions = plan.definition.workspace.packageImports.flatMap((pkg) =>
(pkg.queryCompositions ?? []).map((composition) => {
const scoped = new Map(allQueries);
for (const query of pkg.checkedQueries ?? []) scoped.set(query.declaration.id, query);
const checked = compileQueryComposition(
{
id: composition.id,
input: composition.input,
steps: composition.steps,
maxQueries: composition.maxQueries,
},
scoped,
);
if (checked.definitionDigest !== composition.definitionDigest)
throw new ExecutionPlanError("INVALID_QUERY_COMPOSITION", composition.id, "Checked dependencies changed");
return {
id: JSON.stringify([pkg.revisionId, composition.id]),
input: portableValueSchema(checked.input),
output: portableValueSchema(checked.output),
maxQueries: checked.maxQueries,
steps: checked.steps.map((step) => ({
...step,
query: (pkg.checkedQueries ?? []).some((query) => query.declaration.id === step.query)
? JSON.stringify([pkg.revisionId, step.query])
: step.query,
})),
};
}),
);
return {
codec: "camino-query-program-v1",
plan: (compileKernelProgram(plan) as { plan: string }).plan,
...(compositions.length ? { compositions } : {}),
queries: [
...compileInspectionTemplates(plan).queries,
...plan.definition.queries.map(({ id, query, admittedOperations }) => {
+139
View File
@@ -0,0 +1,139 @@
import { createHash } from "node:crypto";
import type { ValueType } from "../capability-model/types.js";
import { canonicalJson } from "../capability-model/evolution.js";
import { QueryCompileError, type CheckedQuery } from "./types.js";
/** Bindings refer to checked values, never executable callbacks. A path cannot
* silently cross a nullable value or expand an unbounded collection. */
export type QueryBinding =
| { literal: string | boolean }
| { equal: [QueryBinding, QueryBinding] }
| { from: "input"; path: string[] }
| { from: "item"; path: string[] }
| { from: "result"; name: string; path: string[] }
| { record: Record<string, QueryBinding> };
export interface QueryCompositionStep {
name: string;
query: string;
root: QueryBinding | null;
arguments: QueryBinding;
/** Null roots skip this step and produce null, rather than querying an
* arbitrary object or treating unavailable query inputs as absence. */
optional?: boolean;
when?: QueryBinding;
/** An optional object is unwrapped as item; absence skips the query. */
with?: QueryBinding;
each?: { source: QueryBinding; limit: number };
}
export interface QueryComposition {
id: string;
input: ValueType;
steps: QueryCompositionStep[];
maxQueries: number;
}
export interface CheckedQueryComposition extends QueryComposition {
output: ValueType;
definitionDigest: string;
dependencies: { query: string; digest: string }[];
}
/** Checks a finite dependency graph. Query identities are resolved by the
* caller's package/workspace scope; no presentation concepts participate. */
export function compileQueryComposition(
source: QueryComposition,
queries: ReadonlyMap<string, CheckedQuery>,
): CheckedQueryComposition {
const fail = (message: string): never => {
throw new QueryCompileError("QUERY_COMPOSITION", `${source.id}: ${message}`);
};
if (!Number.isSafeInteger(source.maxQueries) || source.maxQueries < 1) fail("maxQueries must be a positive integer");
if (source.input.kind !== "record") fail("input must be a record");
const outputs = new Map<string, ValueType>();
const dependencies = new Map<string, string>();
let count = 0;
function binding(expression: QueryBinding, item?: ValueType): ValueType {
if ("literal" in expression)
return { kind: "scalar", name: typeof expression.literal === "string" ? "string" : "bool" };
if ("equal" in expression) {
const left = binding(expression.equal[0], item);
const right = binding(expression.equal[1], item);
if (left.kind !== "scalar" || !equal(left, right)) fail("equal requires matching scalar operands");
return { kind: "scalar", name: "bool" };
}
if ("record" in expression)
return {
kind: "record",
fields: Object.fromEntries(
Object.entries(expression.record).map(([key, value]) => [key, binding(value, item)]),
),
};
let type =
expression.from === "input" ? source.input : expression.from === "item" ? item : outputs.get(expression.name);
if (!type)
return fail(
expression.from === "result"
? `result ${expression.name} must name an earlier step (forward references and cycles are unsupported)`
: "item is only available within a bounded each step",
);
for (const key of expression.path) {
if (type.kind !== "record" || !Object.hasOwn(type.fields, key))
return fail(`cannot select ${key} from ${type.kind}; nulls and collections need an explicit boundary`);
type = type.fields[key]!;
}
return type;
}
const equal = (a: ValueType, b: ValueType) => canonicalJson(a) === canonicalJson(b);
for (const step of source.steps) {
if (!step.name || outputs.has(step.name)) fail(`duplicate or empty step name ${step.name}`);
const query = queries.get(step.query);
if (!query) return fail(`unknown query ${step.query}`);
let item: ValueType | undefined;
if (step.with) {
if (step.each) fail(`${step.name} cannot combine with and each`);
const value = binding(step.with);
item = value.kind === "optional" ? value.value : value;
}
if (step.each) {
const collection = binding(step.each.source);
if (collection.kind !== "list") return fail(`${step.name}.each requires a list`);
if (!Number.isSafeInteger(step.each.limit) || step.each.limit < 1)
fail(`${step.name}.each.limit must be a positive integer`);
item = collection.value;
}
count += step.each?.limit ?? 1;
if (!Number.isSafeInteger(count) || count > source.maxQueries) fail("declared steps exceed maxQueries");
if (step.when && !equal(binding(step.when, item), { kind: "scalar", name: "bool" }))
fail(`${step.name}.when requires a boolean`);
if (query.declaration.root.kind === "workspace-interface") {
if (step.root !== null) fail(`${step.name} workspace query takes no object root`);
} else {
if (step.root === null) return fail(`${step.name} requires an object root`);
let root = binding(step.root, item);
if (root.kind === "optional" && step.optional) root = root.value;
const expected: ValueType = {
kind: "object-ref",
expectation: {
kind: "interface",
interfaceRevisionId: query.declaration.root.interface,
},
};
if (!equal(root, expected)) fail(`${step.name} root must be a reference to ${query.declaration.root.interface}`);
}
if (!equal(binding(step.arguments, item), query.variables))
fail(`${step.name} arguments do not match ${step.query}`);
let output = query.output;
if (step.optional || step.with || step.when) output = { kind: "optional", value: output };
if (step.each) output = { kind: "list", value: output };
outputs.set(step.name, output);
dependencies.set(step.query, query.definitionDigest);
}
const result = {
...structuredClone(source),
output: { kind: "record" as const, fields: Object.fromEntries(outputs) },
dependencies: [...dependencies].map(([query, digest]) => ({ query, digest })),
};
return { ...result, definitionDigest: createHash("sha256").update(canonicalJson(result)).digest("hex") };
}
+6 -1
View File
@@ -197,7 +197,12 @@ export function querySchema(
const metadata = new GraphQLObjectType({
name: `QxMetadata_${name(id)}`,
fields: {
...(id === workspaceQueryRootId ? {} : { ref: { type: new GraphQLNonNull(relational.refs(id)) } }),
...(id === workspaceQueryRootId
? {}
: {
ref: { type: new GraphQLNonNull(relational.refs(id)) },
atom: { type: new GraphQLNonNull(GraphQLString) },
}),
...(helpers ? { relations: { type: new GraphQLNonNull(helpers) } } : {}),
},
});
+11
View File
@@ -0,0 +1,11 @@
import type { PackageQueryGenerator } from "../capability-language/assembly.js";
import { presentationQueries } from "../bindings/presentation-queries.js";
/** Selected explicitly by the React build. Its output is checked as ordinary
* Camino queries; no React identifiers are interpreted by package assembly. */
export const generatePackageQueries: PackageQueryGenerator = async (input) => {
const pkg = structuredClone(input.package);
const before = new Set(pkg.queries?.map((query) => query.id));
const documents = presentationQueries(pkg, input.interfaces);
return { queries: (pkg.queries ?? []).filter((query) => !before.has(query.id)), documents };
};