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:
@@ -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,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,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) {
|
||||
|
||||
Reference in New Issue
Block a user