Complete prepared presentation build and runtime integration

This commit is contained in:
Timothy J. Aveni
2026-09-27 18:51:05 -07:00
parent fdb7cd3f6f
commit b499298299
13 changed files with 1013 additions and 38 deletions
+127
View File
@@ -0,0 +1,127 @@
import type { InterfaceRevision, PackageRevision, ValueType } from "../capability-model/types.js";
import type { QueryBinding, QueryComposition, QueryCompositionStep } from "../query/composition.js";
/** This is an artifact of the explicitly selected React frontend, not QX IR. */
export const preparationArtifact = "org.quixos.react.preparation/1";
export interface ReactChildDeclaration {
name: string;
subject: string;
root: QueryBinding;
slot: string;
with?: QueryBinding;
each?: { source: QueryBinding; limit: number };
when?: QueryBinding;
role?: { interface: string; member: string };
provider?: { package: string; module: string };
}
export interface ReactModulePreparation {
module: string;
composition: string;
discoveryComposition?: string;
children: ReactChildDeclaration[];
}
export interface ReactPreparationArtifact {
modules: ReactModulePreparation[];
}
/** Names are package imports/module declarations. All query bindings are passed
* to Camino's ordinary composition checker after generated queries are checked. */
export function presentationDeclarations(
text: string,
pkg: PackageRevision,
interfaces: readonly InterfaceRevision[],
): { compositions: QueryComposition[]; artifact: ReactPreparationArtifact } {
const source: unknown = JSON.parse(text);
const object = (value: unknown, label: string): Record<string, unknown> => {
if (!value || typeof value !== "object" || Array.isArray(value)) throw Error(`${label}: expected an object`);
return value as Record<string, unknown>;
};
const top = object(source, "quixos.react.json");
if (Object.keys(top).some((key) => key !== "modules")) throw Error("quixos.react.json: unknown field");
const modules = object(top.modules, "modules");
const compositions: QueryComposition[] = [];
const artifact: ReactPreparationArtifact = { modules: [] };
const imported = (name: string) => {
const id = pkg.clientInterfaces?.[name];
if (!id) throw Error(`React declaration: unknown imported interface ${name}`);
return id;
};
for (const [name, value] of Object.entries(modules)) {
const declaration = object(value, `module ${name}`);
if (Object.keys(declaration).some((key) => !["inputs", "children"].includes(key)))
throw Error(`${name}: unknown preparation field`);
const module = pkg.modules?.find((entry) => entry.displayName === name);
if (!module) throw Error(`React declaration: unknown module ${name}`);
const contract = interfaces.find((entry) => entry.revisionId === module.interfaceRevisionId);
const member = contract?.members.find((entry) => entry.id === module.memberId);
if (
member?.kind !== "module" ||
member.subject?.kind !== "interface" ||
!["org.quixos.react.component/1", "org.quixos.react.component/2", "org.quixos.react.subject-only/1"].includes(
member.contract,
)
)
throw Error(`${name}: preparation requires a React module contract`);
const fields: Record<string, ValueType> = {
subject: {
kind: "object-ref",
expectation: { kind: "interface", interfaceRevisionId: member.subject.interfaceRevisionId },
},
};
if (member.contract === "org.quixos.react.component/2") {
const state = contract?.application?.arguments[1];
if (state?.kind !== "object" || state.target.kind !== "interface")
throw Error(`${name}: expected a closed state interface`);
fields.navigation = {
kind: "object-ref",
expectation: { kind: "interface", interfaceRevisionId: state.target.interfaceRevisionId },
};
}
if (!Array.isArray(declaration.inputs)) throw Error(`${name}: inputs must be an array of query bindings`);
const steps = declaration.inputs.map((value) => {
const step = object(value, `${name} input`) as unknown as QueryCompositionStep;
if (step.name === "children") throw Error(`${name}: children is reserved for prepared child inputs`);
const query = pkg.queries?.find((entry) => entry.displayName === step.query);
if (!query) throw Error(`${name}.${step.name}: unknown query ${step.query}`);
if (query.placement?.kind !== "local" && !query.fragment)
throw Error(`${name}.${step.name}: requires a local projection or fragment`);
return { ...step, query: query.id };
});
const composition = `react:inputs:${module.id}`;
compositions.push({
id: composition,
input: { kind: "record", fields },
steps,
maxQueries: steps.reduce((count, step) => count + (step.each?.limit ?? 1), 0) || 1,
});
const children = declaration.children ?? [];
if (!Array.isArray(children)) throw Error(`${name}: children must be an array`);
const names = new Set<string>();
const parsed = children.map((value) => {
const child = object(value, `${name} child`) as unknown as ReactChildDeclaration;
if (!child.name || names.has(child.name) || !child.slot || !child.root)
throw Error(`${name}: child requires a unique name, slot and root`);
names.add(child.name);
if (child.each && child.with) throw Error(`${name}.${child.name}: cannot combine each and with`);
if (child.each && (!Number.isSafeInteger(child.each.limit) || child.each.limit < 1))
throw Error(`${name}.${child.name}: finite positive collection limit required`);
return {
...child,
subject: imported(child.subject),
...(child.role ? { role: { ...child.role, interface: imported(child.role.interface) } } : {}),
};
});
artifact.modules.push({ module: module.id, composition, children: parsed });
}
for (const module of pkg.modules ?? []) {
const contract = interfaces.find((entry) => entry.revisionId === module.interfaceRevisionId);
const member = contract?.members.find((entry) => entry.id === module.memberId);
if (
member?.kind === "module" &&
member.contract.startsWith("org.quixos.react.") &&
!artifact.modules.some((entry) => entry.module === module.id)
)
throw Error(`React module ${module.displayName} has no preparation declaration`);
}
return { compositions, artifact };
}
+420
View File
@@ -0,0 +1,420 @@
import { createHash } from "node:crypto";
import {
parse,
print,
visit,
Kind,
type SelectionSetNode,
type FragmentDefinitionNode,
type SelectionNode,
type DocumentNode,
} from "graphql";
import type { PackageRevision, WorkspaceRevision } from "../capability-model/types.js";
import { compileQuery } from "../query/compile.js";
import { compileQueryComposition, type QueryBinding } from "../query/composition.js";
import type { CheckedQuery, QueryDeclaration } from "../query/types.js";
import { preparationArtifact, type ReactModulePreparation, type ReactPreparationArtifact } from "./declarations.js";
interface Branch {
packageRevision: string;
id: string;
}
type Child = ReactModulePreparation["children"][number] & { branches: Branch[] };
interface Module {
pkg: PackageRevision;
preparation: ReactModulePreparation;
}
const identity = (pkg: string, id: string) => JSON.stringify([pkg, id]);
const suffix = (value: string) => createHash("sha256").update(value).digest("hex").slice(0, 20);
/** Lower bounded, statically bound subject projections into ordinary fragments.
* The generated query still passes the normal GraphQL/QX checker. Stateful
* lookups remain independent ordinary root-scoped queries owned by the adapter. */
export async function expandPresentationQueries(workspace: WorkspaceRevision): Promise<void> {
const modules = new Map<string, Module>();
for (const pkg of workspace.packageImports) {
const artifact = pkg.buildArtifacts?.[preparationArtifact] as ReactPreparationArtifact | undefined;
for (const preparation of artifact?.modules ?? [])
modules.set(identity(pkg.revisionId, preparation.module), { pkg, preparation });
}
const query = (pkg: string, id: string) => {
const result = workspace.packageImports
.find((entry) => entry.revisionId === pkg)
?.checkedQueries?.find((entry) => entry.declaration.id === id);
if (!result) throw Error(`Missing checked preparation query ${pkg}/${id}`);
return result;
};
const resolver = async (pkg: string, id: string) => query(pkg, id);
const install = async (pkg: PackageRevision, declaration: QueryDeclaration, document: string) => {
const views = new Map(declaration.views.map((view) => [view.atomId, view]));
const allowances = [...declaration.allowances];
for (const imported of declaration.importedFragments ?? []) {
const dependency = query(imported.packageRevisionId, imported.fragmentId).declaration;
for (const view of dependency.views) {
const previous = views.get(view.atomId);
if (previous && previous.interfaceRevisionId !== view.interfaceRevisionId)
throw Error(`${declaration.displayName}: conflicting interface views for ${view.atomId}`);
views.set(view.atomId, view);
}
for (const allowance of dependency.allowances)
if (!allowances.some((entry) => JSON.stringify(entry) === JSON.stringify(allowance)))
allowances.push(allowance);
}
declaration = { ...declaration, views: [...views.values()], allowances };
const checked = await compileQuery(declaration, workspace.interfaceImports, async () => document, resolver);
pkg.queries = [...(pkg.queries ?? []).filter((entry) => entry.id !== declaration.id), declaration];
pkg.checkedQueries = [
...(pkg.checkedQueries ?? []).filter((entry) => entry.declaration.id !== declaration.id),
checked,
];
return checked;
};
function expanded(source: CheckedQuery): SelectionSetNode {
const document = parse(source.document);
const fragments = new Map(
document.definitions
.filter((entry): entry is FragmentDefinitionNode => entry.kind === Kind.FRAGMENT_DEFINITION)
.map((entry) => [entry.name.value, entry]),
);
const operation = document.definitions.find((entry) => entry.kind === Kind.OPERATION_DEFINITION);
if (!operation || operation.kind !== Kind.OPERATION_DEFINITION)
throw Error(`Missing query operation ${source.declaration.id}`);
const expand = (selection: SelectionSetNode, parents: string[] = []): SelectionSetNode => ({
...selection,
selections: selection.selections.flatMap((entry): SelectionNode[] => {
if (entry.kind === Kind.FRAGMENT_SPREAD) {
if (parents.includes(entry.name.value)) throw Error("Cyclic query fragment");
const fragment = fragments.get(entry.name.value);
if (!fragment) throw Error(`Missing query fragment ${entry.name.value}`);
return [
{
kind: Kind.INLINE_FRAGMENT,
typeCondition: fragment.typeCondition,
directives: entry.directives ?? [],
selectionSet: expand(fragment.selectionSet, [...parents, entry.name.value]),
},
];
}
return [{ ...entry, ...(entry.selectionSet ? { selectionSet: expand(entry.selectionSet, parents) } : {}) }];
}),
});
return expand(operation.selectionSet);
}
function insert(selection: SelectionSetNode, path: readonly string[], spread: string): SelectionSetNode {
if (!path.length)
return {
...selection,
selections: [...selection.selections, { kind: Kind.FRAGMENT_SPREAD, name: { kind: Kind.NAME, value: spread } }],
};
let found = false;
const selections = selection.selections.map((entry): SelectionNode => {
if (entry.kind === Kind.INLINE_FRAGMENT) {
try {
const nested = insert(entry.selectionSet, path, spread);
found = true;
return { ...entry, selectionSet: nested };
} catch (error) {
if (!(error instanceof Error) || error.message !== "REACT_SELECTION_PATH") throw error;
}
}
if (entry.kind === Kind.FIELD && (entry.alias?.value ?? entry.name.value) === path[0] && entry.selectionSet) {
found = true;
return { ...entry, selectionSet: insert(entry.selectionSet, path.slice(1), spread) };
}
return entry;
});
if (!found) throw Error("REACT_SELECTION_PATH");
return { ...selection, selections };
}
function location(binding: QueryBinding): { name: string; path: string[] } | undefined {
if ("from" in binding && binding.from === "result") return { name: binding.name, path: [...binding.path] };
if ("flatMap" in binding) {
const root = location(binding.flatMap.source);
return root ? { name: root.name, path: [...root.path, ...binding.flatMap.path] } : undefined;
}
return undefined;
}
const done = new Set<string>();
const eager = new Map<string, { step: string; query: CheckedQuery }[]>();
const admissionFragments = new Map<string, CheckedQuery>();
const stateInputs = new Map<string, { pkg: PackageRevision; query: CheckedQuery }[]>();
async function expandModule(key: string): Promise<void> {
if (done.has(key)) return;
const module = modules.get(key);
if (!module) throw Error(`Missing preparation module ${key}`);
const { pkg, preparation } = module;
const children = preparation.children as Child[];
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) {
const original = query(pkg.revisionId, step.query);
// Variable roots/arguments remain checked dependent steps. Constants can
// be substituted without requesting new runtime parameters from a parent.
if (!("record" in step.arguments) || Object.values(step.arguments.record).some((value) => !("literal" in value)))
continue;
const constants = step.arguments.record;
let selection = expanded(original);
if (original.declaration.placement?.kind !== "local")
throw Error(key + "." + step.name + ": expected checked local input");
const source = query(pkg.revisionId, original.declaration.placement.sourceQueryId);
let admissionSelection = expanded(source);
const admissionImports: NonNullable<QueryDeclaration["importedFragments"]> = [];
const imports: NonNullable<QueryDeclaration["importedFragments"]> = [];
for (const child of children) {
const selected = child.with
? location(child.with)
: child.each
? location(child.each.source)
: location(child.root);
if (!selected || selected.name !== step.name) continue;
const rootPath = "from" in child.root && child.root.from === "item" ? child.root.path : [];
const path = [...selected.path, ...rootPath];
if (path.slice(-2).join(".") !== "_camino.ref") {
if (!rootPath.length && (child.with || child.each))
throw Error(`${key}.${child.name}: child root must select the item's reference`);
} else path.splice(-2);
for (const branch of child.branches) {
for (const input of eager.get(identity(branch.packageRevision, branch.id)) ?? []) {
const declaration = input.query.declaration;
if (
!imports.some(
(entry) => entry.packageRevisionId === branch.packageRevision && entry.fragmentId === declaration.id,
)
) {
imports.push({ packageRevisionId: branch.packageRevision, fragmentId: declaration.id, variables: {} });
}
selection = insert(selection, path, declaration.fragment!.name);
const admission = admissionFragments.get(identity(branch.packageRevision, declaration.id))!;
if (
!admissionImports.some(
(entry) =>
entry.packageRevisionId === branch.packageRevision && entry.fragmentId === admission.declaration.id,
)
)
admissionImports.push({
packageRevisionId: branch.packageRevision,
fragmentId: admission.declaration.id,
variables: {},
});
admissionSelection = insert(admissionSelection, path, admission.declaration.fragment!.name);
}
}
}
const token = suffix(identity(pkg.revisionId, `${preparation.module}/${step.name}`));
const name = `ReactInput${token}`,
id = `react:input:${preparation.module}:${step.name}`;
const base: QueryDeclaration = {
...original.declaration,
id,
displayName: name,
document: `generated/react/${name}.graphql`,
operation: name,
fragments: [],
importedFragments: imports,
watch: true,
budgets: {
...original.declaration.budgets,
depth: Math.max(original.declaration.budgets.depth, 32),
rows: Math.max(original.declaration.budgets.rows, 5000),
},
};
delete base.fragment;
function documentFor(selection: SelectionSetNode, fragmentName: string): DocumentNode {
const substituted = visit(selection, {
Variable(node) {
const constant = constants[node.name.value];
if (!constant || !("literal" in constant))
throw Error(key + "." + step.name + ": unbound query variable " + node.name.value);
return typeof constant.literal === "boolean"
? { kind: Kind.BOOLEAN, value: constant.literal }
: { kind: Kind.STRING, value: constant.literal };
},
});
if (original.declaration.root.kind === "object") {
const root = substituted.selections.find((entry) => entry.kind === Kind.FIELD && entry.name.value === "root");
if (!root || root.kind !== Kind.FIELD || !root.selectionSet)
throw Error(key + ": expected object-rooted query");
const contract = workspace.interfaceImports.find(
(entry) => entry.revisionId === original.declaration.root.interface,
)!;
return {
kind: Kind.DOCUMENT,
definitions: [
{
kind: Kind.FRAGMENT_DEFINITION,
name: { kind: Kind.NAME, value: fragmentName },
typeCondition: { kind: Kind.NAMED_TYPE, name: { kind: Kind.NAME, value: contract.displayName } },
selectionSet: root.selectionSet,
},
],
};
}
return {
kind: Kind.DOCUMENT,
definitions: [
{
kind: Kind.OPERATION_DEFINITION,
operation: "query" as import("graphql").OperationTypeNode,
name: { kind: Kind.NAME, value: name },
selectionSet: substituted,
},
],
};
}
if (original.declaration.root.kind === "object") base.fragment = { name };
const sourceId = id + ":fragment-inputs";
const admissionName = name + "Admission";
const admissionBase: QueryDeclaration = {
...base,
views: source.declaration.views,
allowances: source.declaration.allowances,
importedFragments: admissionImports,
...(base.fragment ? { fragment: { name: admissionName } } : {}),
};
const admissionDocument = documentFor(admissionSelection, admissionName);
await install(
pkg,
{
...admissionBase,
id: sourceId,
displayName: name + "$inputs",
placement: { kind: "replica" },
watch: false,
...(base.fragment ? { fragment: { name: admissionName, admission: true } } : {}),
},
print(admissionDocument),
);
const admissionFragment = await install(
pkg,
{
...admissionBase,
id: id + ":admission-projection",
displayName: name + "$admission",
placement: { kind: "local", sourceQueryId: sourceId },
},
print(admissionDocument),
);
const local = await install(
pkg,
{ ...base, placement: { kind: "local", sourceQueryId: sourceId } },
print(documentFor(selection, name)),
);
admissionFragments.set(identity(pkg.revisionId, local.declaration.id), admissionFragment);
step.query = local.declaration.id;
step.arguments = { record: {} };
if (
local.declaration.fragment &&
step.root &&
"from" in step.root &&
step.root.from === "input" &&
step.root.path.join(".") === "subject"
)
exported.push({ step: step.name, query: local });
if (
local.declaration.fragment &&
step.root &&
"from" in step.root &&
step.root.from === "input" &&
step.root.path.join(".") === "navigation"
) {
const iface = local.declaration.root.interface;
stateInputs.set(iface, [...(stateInputs.get(iface) ?? []), { pkg, query: local }]);
}
}
const available = new Map(
workspace.packageImports.flatMap((pkg) =>
(pkg.checkedQueries ?? []).map((query) => [identity(pkg.revisionId, query.declaration.id), query] as const),
),
);
for (const query of pkg.checkedQueries ?? []) available.set(query.declaration.id, query);
const checked = compileQueryComposition(
{ 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,
);
eager.set(key, exported);
done.add(key);
}
for (const key of modules.keys()) await expandModule(key);
// Retention queries are ordinary checked queries too. They carry the declared
// state projections, including any subject fragments reached through state.
// This is specific to this frontend; no navigation operator enters Camino.
for (const pkg of workspace.packageImports) {
for (const original of [...(pkg.checkedQueries ?? [])]) {
if (!original.declaration.id.startsWith("presentation:navigation:")) continue;
const inputs = (stateInputs.get(original.declaration.root.interface) ?? []).map((input) => ({
...input,
query:
original.declaration.placement?.kind === "replica"
? admissionFragments.get(identity(input.pkg.revisionId, input.query.declaration.id))!
: input.query,
}));
if (!inputs.length) continue;
const document = parse(original.document);
const collection = original.declaration.placement?.kind === "local" ? "availableObjects" : "objects";
const definitions = document.definitions.map((definition) => {
if (definition.kind !== Kind.OPERATION_DEFINITION) return definition;
return visit(definition, {
Field(node) {
if (node.name.value !== "root" || !node.selectionSet) return;
const source = node.selectionSet.selections.find(
(entry) => entry.kind === Kind.FIELD && entry.name.value === collection,
);
if (!source || source.kind !== Kind.FIELD) throw Error("Missing navigation collection");
return {
...node,
selectionSet: {
...node.selectionSet,
selections: [
...node.selectionSet.selections,
...inputs.map((input) => ({
...source,
alias: {
kind: Kind.NAME as const,
value: `prepared${suffix(identity(input.pkg.revisionId, input.query.declaration.id))}`,
},
selectionSet: parse(
`{ entries { node { ...${input.query.declaration.fragment!.name} } } }`,
).definitions.flatMap((entry) =>
entry.kind === Kind.OPERATION_DEFINITION ? [entry.selectionSet] : [],
)[0]!,
})),
],
},
};
},
});
});
await install(
pkg,
{
...original.declaration,
...(original.declaration.placement?.kind === "local"
? {
id: original.declaration.id.replace(/:view$/, ":prepared-view"),
displayName: original.declaration.displayName + "Prepared",
}
: {}),
importedFragments: inputs.map((input) => ({
packageRevisionId: input.pkg.revisionId,
fragmentId: input.query.declaration.id,
variables: {},
})),
},
print({ ...document, definitions }),
);
}
}
}
+20 -1
View File
@@ -1,5 +1,7 @@
import type { PackageQueryGenerator } from "../capability-language/assembly.js";
import { presentationQueries } from "../bindings/presentation-queries.js";
import { presentationDeclarations, preparationArtifact } from "./declarations.js";
export { completeWorkspace } from "./workspace.js";
/** Selected explicitly by the React build. Its output is checked as ordinary
* Camino queries; no React identifiers are interpreted by package assembly. */
@@ -7,5 +9,22 @@ 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 };
const reactModules = (pkg.modules ?? []).filter((module) =>
input.interfaces
.find((entry) => entry.revisionId === module.interfaceRevisionId)
?.members.some(
(entry) =>
entry.id === module.memberId && entry.kind === "module" && entry.contract.startsWith("org.quixos.react."),
),
);
const declarations = reactModules.length
? presentationDeclarations(await input.readSource("quixos.react.json"), pkg, input.interfaces)
: undefined;
return {
queries: (pkg.queries ?? []).filter((query) => !before.has(query.id)),
documents,
...(declarations
? { compositions: declarations.compositions, artifacts: { [preparationArtifact]: declarations.artifact } }
: {}),
};
};
+150
View File
@@ -0,0 +1,150 @@
import type { WorkspaceRevision, ValueType } from "../capability-model/types.js";
import { moduleContract } from "../capability-model/modules.js";
import { canonicalJson } from "../capability-model/evolution.js";
import { queryBindingType } from "../query/composition.js";
import { moduleTypeContext } from "../execution-world/module-contracts.js";
import { preparationArtifact, type ReactPreparationArtifact } from "./declarations.js";
import { expandPresentationQueries } from "./expansion.js";
/** Explicit frontend stage. Resolve finite provider branches after workspace
* bindings exist; the generic workspace compiler does not invoke this itself. */
export async function completeWorkspace(workspace: WorkspaceRevision): Promise<void> {
const modules = workspace.packageImports.flatMap((pkg) => {
const artifact = pkg.buildArtifacts?.[preparationArtifact] as ReactPreparationArtifact | undefined;
return (artifact?.modules ?? []).map((preparation) => {
const module = pkg.modules?.find((entry) => entry.id === preparation.module);
if (!module) throw Error(`React preparation has no module ${preparation.module}`);
const contract = moduleContract(workspace.interfaceImports, module)!;
return { pkg, module, contract, preparation };
});
});
const graph = new Map<string, string[]>();
const key = (pkg: string, id: string) => JSON.stringify([pkg, id]);
for (const parent of modules) {
const composition = parent.pkg.queryCompositions?.find((entry) => entry.id === parent.preparation.composition);
if (!composition || composition.output.kind !== "record")
throw Error(`Missing checked inputs for ${parent.module.displayName}`);
const outputs = new Map(Object.entries(composition.output.fields));
const dependencies: string[] = [];
const children = parent.preparation.children.map((child) => {
const at = `${parent.module.displayName}.${child.name}`;
let item: ValueType | undefined;
if (child.with) {
const type = queryBindingType(child.with, composition.input, outputs);
item = type.kind === "optional" ? type.value : type;
}
if (child.each) {
const type = queryBindingType(child.each.source, composition.input, outputs);
if (type.kind !== "list") throw Error(`${at}: each requires a list`);
item = type.value;
}
const root = queryBindingType(child.root, composition.input, outputs, item);
if (
root.kind !== "object-ref" ||
root.expectation.kind !== "interface" ||
root.expectation.interfaceRevisionId !== child.subject
)
throw Error(`${at}: child root must be a ${child.subject} reference; unwrap nulls and lists explicitly`);
if (
child.when &&
canonicalJson(queryBindingType(child.when, composition.input, outputs, item)) !==
canonicalJson({ kind: "scalar", name: "bool" })
)
throw Error(`${at}: when requires a boolean`);
const atoms = workspace.atoms.filter((atom) =>
workspace.conformances.some((entry) => entry.atomId === atom.id && entry.interfaceRevisionId === child.subject),
);
if (!atoms.length) throw Error(`${at}: no installed subject atoms`);
const branches = atoms.map((atom) => {
let candidates = modules.filter(
(entry) =>
entry.contract.subject?.kind === "interface" &&
entry.contract.subject.interfaceRevisionId === child.subject,
);
if (child.role) {
const iface = workspace.interfaceImports.find((entry) => entry.revisionId === child.role!.interface);
const member = iface?.members.find(
(entry) => entry.displayName === child.role!.member || entry.id === child.role!.member,
);
if (member?.kind !== "module") throw Error(`${at}: unknown rendering role`);
if (
!workspace.conformances.some(
(entry) => entry.atomId === atom.id && entry.interfaceRevisionId === iface!.revisionId,
)
)
throw Error(`${at}: ${atom.id} does not implement the requested role`);
candidates = candidates.filter(
(entry) => entry.module.interfaceRevisionId === iface!.revisionId && entry.module.memberId === member.id,
);
} else {
candidates = candidates.filter((entry) =>
["org.quixos.react.subject-only/1", "org.quixos.react.component/2"].includes(entry.contract.contract),
);
}
if (child.provider) {
const pkg = parent.pkg.clientPackages?.[child.provider.package];
if (!pkg) throw Error(`${at}: provider package is not imported`);
candidates = candidates.filter(
(entry) => entry.pkg.revisionId === pkg && entry.module.displayName === child.provider!.module,
);
} else {
const bound = candidates.filter((entry) =>
workspace.conformances.some(
(conformance) =>
conformance.atomId === atom.id &&
conformance.interfaceRevisionId === entry.module.interfaceRevisionId &&
conformance.moduleBindings?.some(
(binding) =>
binding.memberId === entry.module.memberId &&
binding.packageRevisionId === entry.pkg.revisionId &&
binding.moduleId === entry.module.id,
),
),
);
candidates = bound.length
? bound
: child.role
? []
: candidates.filter((entry) =>
workspace.presentations?.some(
(presentation) =>
presentation.subject === child.subject &&
presentation.packageRevisionId === entry.pkg.revisionId &&
presentation.componentId === entry.module.id,
),
);
}
if (candidates.length !== 1)
throw Error(`${at}: ${atom.id} has ${candidates.length} compatible providers; select one explicitly`);
const selected = candidates[0]!;
dependencies.push(key(selected.pkg.revisionId, selected.module.id));
return {
atom: atom.id,
packageRevision: selected.pkg.revisionId,
id: selected.module.id,
role: {
interface: selected.module.interfaceRevisionId,
member: selected.module.memberId,
subject: child.subject,
contract: selected.contract.contract,
...moduleTypeContext(
workspace.interfaceImports.find((entry) => entry.revisionId === selected.module.interfaceRevisionId)!,
),
},
};
});
return { ...child, branches };
});
graph.set(key(parent.pkg.revisionId, parent.module.id), dependencies);
Object.assign(parent.preparation, { children });
}
const visited = new Set<string>();
const visit = (node: string, ancestors: string[]) => {
if (ancestors.includes(node)) throw Error(`Eager React preparation cycle: ${[...ancestors, node].join(" -> ")}`);
if (visited.has(node)) return;
for (const child of graph.get(node) ?? []) visit(child, [...ancestors, node]);
visited.add(node);
};
for (const node of graph.keys()) visit(node, []);
await expandPresentationQueries(workspace);
}