2026-09-10 18:27:41 -07:00
|
|
|
import { readFile, realpath } from "node:fs/promises";
|
2026-09-05 12:33:52 -07:00
|
|
|
import path from "node:path";
|
2026-10-01 01:37:46 -07:00
|
|
|
import { profile } from "./profile.js";
|
2026-09-08 14:27:24 -07:00
|
|
|
import { loadQxSources, resolveQxSources } from "./source-loader.js";
|
2026-09-26 11:44:50 -07:00
|
|
|
import { compileQuery, compileRepositoryQuery } from "../query/compile.js";
|
2026-10-09 20:57:55 -07:00
|
|
|
import { queryTreeProjections } from "../query/tree.js";
|
2026-09-27 15:53:34 -07:00
|
|
|
import type { QueryDeclaration } from "../query/types.js";
|
|
|
|
|
import { compileQueryComposition, type QueryComposition } from "../query/composition.js";
|
2026-09-17 14:34:16 -07:00
|
|
|
import { checkQueryTemplate } from "../query/templates.js";
|
|
|
|
|
import { readRepositorySource } from "./source-loader.js";
|
2026-09-21 11:23:26 -07:00
|
|
|
import { parseControllerCatalog } from "../capability-model/controllers.js";
|
2026-09-17 14:34:16 -07:00
|
|
|
import { linkQueries } from "../query/link.js";
|
2026-09-05 12:33:52 -07:00
|
|
|
import {
|
|
|
|
|
capabilityId,
|
|
|
|
|
compileWorkspaceRevision,
|
2026-09-10 18:27:41 -07:00
|
|
|
validateMigrationCatalog,
|
|
|
|
|
contentDigest,
|
2026-09-05 12:33:52 -07:00
|
|
|
type AtomDefinition,
|
|
|
|
|
type CompiledWorkspaceRevision,
|
|
|
|
|
type InterfaceRevision,
|
|
|
|
|
type PackageRevision,
|
|
|
|
|
type SourceRevision,
|
|
|
|
|
type WorkspaceRevision,
|
|
|
|
|
} from "../capability-model/index.js";
|
|
|
|
|
import {
|
|
|
|
|
loadQuixosLock,
|
|
|
|
|
type GitSource,
|
|
|
|
|
type LockedResource,
|
|
|
|
|
type QuixosRepositoryLock,
|
|
|
|
|
} from "../resource-lock/index.js";
|
|
|
|
|
import {
|
|
|
|
|
compileCapabilityResourceSource,
|
|
|
|
|
compileCapabilitySource,
|
|
|
|
|
type CapabilityImportEnvironment,
|
|
|
|
|
type CapabilityResource,
|
|
|
|
|
type CapabilityResourceImport,
|
|
|
|
|
} from "./parser.js";
|
|
|
|
|
|
|
|
|
|
export type CapabilityRepositorySnapshot = {
|
|
|
|
|
directory: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type CapabilityRepositoryResolver = (
|
|
|
|
|
source: GitSource,
|
|
|
|
|
kind: LockedResource["kind"],
|
|
|
|
|
) => Promise<CapabilityRepositorySnapshot>;
|
|
|
|
|
|
2026-10-01 01:37:46 -07:00
|
|
|
/** Already checked outputs of this compiler, composed by the Nix build DAG.
|
|
|
|
|
* Source capture/check reports never grant this authority. */
|
|
|
|
|
export type CheckedResourceResolver = (
|
|
|
|
|
kind: LockedResource["kind"],
|
|
|
|
|
source: GitSource,
|
|
|
|
|
lock: QuixosRepositoryLock,
|
|
|
|
|
dependencies: ReadonlyMap<string, ResolvedCapabilityResource>,
|
|
|
|
|
) => Promise<CapabilityResource | undefined>;
|
|
|
|
|
|
2026-09-27 15:53:34 -07:00
|
|
|
/** 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. */
|
2026-09-27 16:34:53 -07:00
|
|
|
export type PackageQueryGenerator = ((input: {
|
2026-09-27 15:53:34 -07:00
|
|
|
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[];
|
2026-09-27 16:34:53 -07:00
|
|
|
readonly artifacts?: Readonly<Record<string, unknown>>;
|
|
|
|
|
}>) & {
|
|
|
|
|
/** Optional explicit frontend stage after workspace binding resolution.
|
|
|
|
|
* Its output still passes normal workspace/query validation. */
|
|
|
|
|
completeWorkspace?: (workspace: WorkspaceRevision) => Promise<void>;
|
|
|
|
|
};
|
2026-09-27 15:53:34 -07:00
|
|
|
|
2026-09-05 12:33:52 -07:00
|
|
|
export type ResolvedCapabilityResource = {
|
|
|
|
|
key: string;
|
|
|
|
|
kind: LockedResource["kind"];
|
|
|
|
|
source: GitSource;
|
|
|
|
|
directory: string;
|
|
|
|
|
lock: QuixosRepositoryLock;
|
2026-09-22 13:51:32 -07:00
|
|
|
resource: CapabilityResource | { kind: "build" };
|
2026-09-05 12:33:52 -07:00
|
|
|
dependencies: ReadonlyMap<string, ResolvedCapabilityResource>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type CompiledWorkspaceRepository = {
|
|
|
|
|
workspace: WorkspaceRevision;
|
|
|
|
|
plan: CompiledWorkspaceRevision;
|
|
|
|
|
lock: QuixosRepositoryLock;
|
|
|
|
|
resources: ResolvedCapabilityResource[];
|
2026-09-22 13:51:32 -07:00
|
|
|
capabilityResources: ResolvedCapabilityResource[];
|
2026-09-05 12:33:52 -07:00
|
|
|
directResources: ReadonlyMap<string, ResolvedCapabilityResource>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type CompiledCapabilityResourceRepository = {
|
2026-09-22 13:51:32 -07:00
|
|
|
resource: CapabilityResource | { kind: "build" };
|
2026-09-05 12:33:52 -07:00
|
|
|
lock: QuixosRepositoryLock;
|
|
|
|
|
resources: ResolvedCapabilityResource[];
|
2026-09-22 13:51:32 -07:00
|
|
|
capabilityResources: ResolvedCapabilityResource[];
|
2026-09-05 12:33:52 -07:00
|
|
|
directResources: ReadonlyMap<string, ResolvedCapabilityResource>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const sourceKey = (kind: LockedResource["kind"], source: GitSource) =>
|
|
|
|
|
`${kind}\0${source.repository}\0${source.commit.toLowerCase()}`;
|
|
|
|
|
|
2026-09-15 15:23:24 -07:00
|
|
|
const bindingKey = (kind: LockedResource["kind"], binding: string) => `${kind}\0${binding}`;
|
2026-09-05 12:33:52 -07:00
|
|
|
|
2026-09-15 15:23:24 -07:00
|
|
|
const importsKey = (entry: CapabilityResourceImport | LockedResource) => bindingKey(entry.kind, entry.binding);
|
2026-09-05 12:33:52 -07:00
|
|
|
|
|
|
|
|
const assertImportsMatchLock = (
|
|
|
|
|
label: string,
|
|
|
|
|
imports: readonly CapabilityResourceImport[],
|
|
|
|
|
resources: readonly LockedResource[],
|
|
|
|
|
) => {
|
|
|
|
|
const authored = [...imports].map(importsKey).sort();
|
2026-09-22 13:51:32 -07:00
|
|
|
const locked = resources
|
|
|
|
|
.filter((entry) => entry.kind !== "build")
|
|
|
|
|
.map(importsKey)
|
|
|
|
|
.sort();
|
2026-09-15 15:23:24 -07:00
|
|
|
if (authored.length !== locked.length || authored.some((entry, index) => entry !== locked[index])) {
|
2026-09-05 12:33:52 -07:00
|
|
|
throw new Error(
|
|
|
|
|
`${label} imports do not match quixos.lock:\n` +
|
2026-09-15 15:23:24 -07:00
|
|
|
`authored: ${authored.join(", ") || "none"}\n` +
|
|
|
|
|
`locked: ${locked.join(", ") || "none"}`,
|
2026-09-05 12:33:52 -07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-15 15:23:24 -07:00
|
|
|
const exactRevisions = <
|
|
|
|
|
Revision extends {
|
|
|
|
|
revisionId: string;
|
|
|
|
|
source: SourceRevision;
|
|
|
|
|
},
|
|
|
|
|
>(
|
|
|
|
|
revisions: readonly Revision[],
|
|
|
|
|
) => {
|
2026-09-05 12:33:52 -07:00
|
|
|
const seen = new Set<string>();
|
|
|
|
|
return revisions.filter((revision) => {
|
|
|
|
|
const key = `${revision.revisionId}\0${revision.source.repository}\0${revision.source.commit}`;
|
|
|
|
|
if (seen.has(key)) return false;
|
|
|
|
|
seen.add(key);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const exactAtoms = (atoms: readonly AtomDefinition[]) => {
|
|
|
|
|
const seen = new Set<string>();
|
|
|
|
|
return atoms.filter((atom) => {
|
|
|
|
|
if (seen.has(atom.id)) return false;
|
|
|
|
|
seen.add(atom.id);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const diagnosticsMessage = (
|
|
|
|
|
label: string,
|
|
|
|
|
diagnostics: readonly {
|
|
|
|
|
fileName: string;
|
|
|
|
|
line: number;
|
|
|
|
|
column: number;
|
|
|
|
|
phase: string;
|
|
|
|
|
code: string;
|
|
|
|
|
message: string;
|
|
|
|
|
path?: string;
|
|
|
|
|
}[],
|
2026-09-15 15:23:24 -07:00
|
|
|
) =>
|
|
|
|
|
`${label} did not compile:\n${diagnostics
|
|
|
|
|
.map((entry) => {
|
|
|
|
|
const location =
|
|
|
|
|
entry.line > 0
|
|
|
|
|
? `${entry.fileName}:${entry.line}:${entry.column + 1}`
|
|
|
|
|
: `${entry.fileName}${entry.path ? `:${entry.path}` : ""}`;
|
|
|
|
|
return `${location}: ${entry.phase} ${entry.code}: ${entry.message}`;
|
|
|
|
|
})
|
|
|
|
|
.join("\n")}`;
|
2026-09-05 12:33:52 -07:00
|
|
|
|
2026-09-22 13:51:32 -07:00
|
|
|
const revisionFor = (node: ResolvedCapabilityResource) => {
|
|
|
|
|
if (node.resource.kind === "build") throw Error("Build sources have no capability revision");
|
|
|
|
|
return node.resource.revision;
|
|
|
|
|
};
|
2026-09-05 12:33:52 -07:00
|
|
|
|
2026-09-22 13:51:32 -07:00
|
|
|
const resourceClosure = (
|
|
|
|
|
roots: readonly ResolvedCapabilityResource[],
|
|
|
|
|
capabilityOnly = false,
|
|
|
|
|
): ResolvedCapabilityResource[] => {
|
2026-09-05 12:33:52 -07:00
|
|
|
const result: ResolvedCapabilityResource[] = [];
|
|
|
|
|
const seen = new Set<string>();
|
|
|
|
|
const visit = (node: ResolvedCapabilityResource) => {
|
2026-09-22 13:51:32 -07:00
|
|
|
if (seen.has(node.key) || (capabilityOnly && node.kind === "build")) return;
|
2026-09-05 12:33:52 -07:00
|
|
|
seen.add(node.key);
|
|
|
|
|
for (const dependency of node.dependencies.values()) visit(dependency);
|
|
|
|
|
result.push(node);
|
|
|
|
|
};
|
|
|
|
|
for (const root of roots) visit(root);
|
|
|
|
|
return result;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const environmentFor = (
|
|
|
|
|
direct: readonly [LockedResource, ResolvedCapabilityResource][],
|
|
|
|
|
closure: readonly ResolvedCapabilityResource[],
|
2026-09-22 13:51:32 -07:00
|
|
|
): CapabilityImportEnvironment => {
|
|
|
|
|
closure = resourceClosure(
|
|
|
|
|
direct.map(([, node]) => node),
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
return {
|
|
|
|
|
interfaces: new Map(
|
|
|
|
|
direct.flatMap(([locked, node]) =>
|
|
|
|
|
node.resource.kind === "interface" ? [[locked.binding, node.resource.revision] as const] : [],
|
|
|
|
|
),
|
2026-09-15 15:23:24 -07:00
|
|
|
),
|
2026-09-22 13:51:32 -07:00
|
|
|
packages: new Map(
|
|
|
|
|
direct.flatMap(([locked, node]) =>
|
|
|
|
|
node.resource.kind === "package" ? [[locked.binding, node.resource.revision] as const] : [],
|
|
|
|
|
),
|
2026-09-15 15:23:24 -07:00
|
|
|
),
|
2026-09-22 13:51:32 -07:00
|
|
|
interfaceClosure: exactRevisions(
|
|
|
|
|
closure.flatMap((node) => [
|
|
|
|
|
...(node.resource.kind === "interface" ? [node.resource.revision] : []),
|
|
|
|
|
...(node.resource.kind === "build" ? [] : (node.resource.specializations ?? [])),
|
|
|
|
|
]),
|
|
|
|
|
),
|
|
|
|
|
packageClosure: exactRevisions(
|
|
|
|
|
closure.flatMap((node) => (node.resource.kind === "package" ? [node.resource.revision] : [])),
|
|
|
|
|
),
|
|
|
|
|
externalAtoms: exactAtoms(
|
|
|
|
|
closure.flatMap((node) => (node.resource.kind === "build" ? [] : node.resource.externalAtoms)),
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
};
|
2026-09-05 12:33:52 -07:00
|
|
|
|
2026-09-27 15:53:34 -07:00
|
|
|
const createResourceGraphResolver = (
|
|
|
|
|
quixosCommit: string,
|
|
|
|
|
resolveResource: CapabilityRepositoryResolver,
|
|
|
|
|
generateQueries?: PackageQueryGenerator,
|
2026-10-01 01:37:46 -07:00
|
|
|
checkedResource?: CheckedResourceResolver,
|
2026-09-27 15:53:34 -07:00
|
|
|
) => {
|
2026-10-01 01:37:46 -07:00
|
|
|
const graphStarted = performance.now();
|
|
|
|
|
let checkedResources = 0,
|
|
|
|
|
reusedResources = 0,
|
|
|
|
|
checkedQueries = 0;
|
2026-09-05 12:33:52 -07:00
|
|
|
const resolved = new Map<string, Promise<ResolvedCapabilityResource>>();
|
|
|
|
|
const active: string[] = [];
|
|
|
|
|
|
|
|
|
|
const visit = async (
|
|
|
|
|
locked: LockedResource,
|
|
|
|
|
suppliedSnapshot?: CapabilityRepositorySnapshot,
|
|
|
|
|
): Promise<ResolvedCapabilityResource> => {
|
|
|
|
|
const key = sourceKey(locked.kind, locked.source);
|
|
|
|
|
const cached = resolved.get(key);
|
|
|
|
|
if (cached) {
|
|
|
|
|
if (active.includes(key)) {
|
|
|
|
|
throw new Error(`Resource dependency cycle: ${[...active, key].join(" -> ")}`);
|
|
|
|
|
}
|
|
|
|
|
return await cached;
|
|
|
|
|
}
|
|
|
|
|
const pending = (async () => {
|
|
|
|
|
active.push(key);
|
|
|
|
|
try {
|
2026-09-15 15:23:24 -07:00
|
|
|
const snapshot = suppliedSnapshot ?? (await resolveResource(locked.source, locked.kind));
|
2026-09-05 12:33:52 -07:00
|
|
|
const lockResult = await loadQuixosLock(path.join(snapshot.directory, "quixos.lock"));
|
|
|
|
|
if (!lockResult.ok) {
|
|
|
|
|
throw new Error(diagnosticsMessage(`${locked.kind} ${locked.binding} lock`, lockResult.diagnostics));
|
|
|
|
|
}
|
|
|
|
|
if (lockResult.lock.quixos.policy) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`${locked.kind} ${locked.binding} lock declares workspace Quixos policy ` +
|
2026-09-15 15:23:24 -07:00
|
|
|
`${lockResult.lock.quixos.policy}; resource locks may only declare their exact authored-against commit`,
|
2026-09-05 12:33:52 -07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (lockResult.lock.quixos.commit.toLowerCase() !== quixosCommit.toLowerCase()) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`${locked.kind} ${locked.binding} selects Quixos ${lockResult.lock.quixos.commit}, ` +
|
2026-09-15 15:23:24 -07:00
|
|
|
`but the repository graph selects ${quixosCommit}`,
|
2026-09-05 12:33:52 -07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
const directPairs: Array<[LockedResource, ResolvedCapabilityResource]> = [];
|
|
|
|
|
for (const dependency of lockResult.lock.resources) {
|
|
|
|
|
directPairs.push([dependency, await visit(dependency)]);
|
|
|
|
|
}
|
2026-09-22 13:51:32 -07:00
|
|
|
if (locked.kind === "build") {
|
|
|
|
|
return {
|
|
|
|
|
key,
|
|
|
|
|
kind: locked.kind,
|
|
|
|
|
source: locked.source,
|
|
|
|
|
directory: snapshot.directory,
|
|
|
|
|
lock: lockResult.lock,
|
|
|
|
|
resource: { kind: "build" as const },
|
|
|
|
|
dependencies: new Map(
|
|
|
|
|
directPairs.map(([dependency, node]) => [bindingKey(dependency.kind, dependency.binding), node]),
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-09-15 15:23:24 -07:00
|
|
|
const dependencyClosure = resourceClosure(directPairs.map(([, node]) => node));
|
2026-10-01 01:37:46 -07:00
|
|
|
const dependencies = new Map(
|
|
|
|
|
directPairs.map(([dependency, node]) => [bindingKey(dependency.kind, dependency.binding), node]),
|
|
|
|
|
);
|
|
|
|
|
const checked = await checkedResource?.(locked.kind, locked.source, lockResult.lock, dependencies);
|
|
|
|
|
if (checked) {
|
|
|
|
|
if (
|
|
|
|
|
checked.kind !== locked.kind ||
|
|
|
|
|
checked.revision.source.repository !== locked.source.repository ||
|
|
|
|
|
checked.revision.source.commit !== locked.source.commit
|
|
|
|
|
)
|
|
|
|
|
throw new Error("Checked resource identity mismatch");
|
|
|
|
|
assertImportsMatchLock("Checked resource", checked.imports, lockResult.lock.resources);
|
|
|
|
|
reusedResources++;
|
|
|
|
|
return {
|
|
|
|
|
key,
|
|
|
|
|
kind: locked.kind,
|
|
|
|
|
source: locked.source,
|
|
|
|
|
directory: snapshot.directory,
|
|
|
|
|
lock: lockResult.lock,
|
|
|
|
|
resource: structuredClone(checked),
|
|
|
|
|
dependencies,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
const checkStarted = performance.now();
|
2026-09-05 12:33:52 -07:00
|
|
|
const environment = environmentFor(directPairs, dependencyClosure);
|
|
|
|
|
const manifestName = locked.kind === "interface" ? "interface.qx" : "package.qx";
|
|
|
|
|
const manifestPath = path.join(snapshot.directory, manifestName);
|
|
|
|
|
const sourceText = await readFile(manifestPath, "utf8");
|
|
|
|
|
const compiled = compileCapabilityResourceSource(sourceText, {
|
|
|
|
|
source: locked.source,
|
|
|
|
|
fileName: manifestPath,
|
|
|
|
|
environment,
|
|
|
|
|
});
|
|
|
|
|
if (!compiled.ok) {
|
|
|
|
|
throw new Error(diagnosticsMessage(`${locked.kind} ${locked.binding}`, compiled.diagnostics));
|
|
|
|
|
}
|
|
|
|
|
if (compiled.resource.kind !== locked.kind) {
|
2026-09-15 15:23:24 -07:00
|
|
|
throw new Error(`${manifestPath} declares ${compiled.resource.kind}, not ${locked.kind}`);
|
2026-09-05 12:33:52 -07:00
|
|
|
}
|
|
|
|
|
assertImportsMatchLock(manifestPath, compiled.resource.imports, lockResult.lock.resources);
|
2026-09-10 18:27:41 -07:00
|
|
|
if (compiled.resource.kind === "package") {
|
2026-09-17 14:34:16 -07:00
|
|
|
const queryInterfaces = [
|
|
|
|
|
...(environment.interfaceClosure ?? []),
|
|
|
|
|
...(compiled.resource.specializations ?? []),
|
|
|
|
|
];
|
2026-09-27 15:53:34 -07:00
|
|
|
const generated = await generateQueries?.({
|
|
|
|
|
package: structuredClone(compiled.resource.revision),
|
|
|
|
|
interfaces: structuredClone(queryInterfaces),
|
|
|
|
|
readSource: (name) => readRepositorySource(snapshot.directory, name),
|
|
|
|
|
});
|
|
|
|
|
if (generated) {
|
2026-09-27 16:34:53 -07:00
|
|
|
if (generated.artifacts) compiled.resource.revision.buildArtifacts = structuredClone(generated.artifacts);
|
2026-09-27 15:53:34 -07:00
|
|
|
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;
|
|
|
|
|
}
|
2026-09-17 14:34:16 -07:00
|
|
|
for (const template of compiled.resource.revision.queryTemplates ?? [])
|
|
|
|
|
await checkQueryTemplate(template, queryInterfaces, (name) =>
|
|
|
|
|
readRepositorySource(snapshot.directory, name),
|
|
|
|
|
);
|
2026-09-22 15:41:27 -07:00
|
|
|
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}`);
|
2026-09-27 15:53:34 -07:00
|
|
|
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);
|
2026-09-22 15:41:27 -07:00
|
|
|
checked.set(id, result);
|
|
|
|
|
return result;
|
|
|
|
|
};
|
|
|
|
|
pkg.checkedQueries = [];
|
|
|
|
|
for (const query of pkg.queries!) pkg.checkedQueries.push(await compile(query.id));
|
2026-10-09 20:57:55 -07:00
|
|
|
const projections = pkg.checkedQueries.flatMap(queryTreeProjections);
|
|
|
|
|
for (const projection of projections) {
|
|
|
|
|
if (pkg.queries!.some((query) => query.id === projection.declaration.id))
|
|
|
|
|
throw new Error(`Generated Query Tree projection conflicts with ${projection.declaration.id}`);
|
|
|
|
|
pkg.queries!.push(projection.declaration);
|
|
|
|
|
pkg.checkedQueries.push(projection);
|
|
|
|
|
}
|
2026-09-22 15:41:27 -07:00
|
|
|
}
|
2026-09-27 15:53:34 -07:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-09-21 11:23:26 -07:00
|
|
|
let controllerText: string | undefined;
|
|
|
|
|
try {
|
|
|
|
|
controllerText = await readRepositorySource(snapshot.directory, "quixos.controllers.json");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
|
|
|
}
|
|
|
|
|
if (controllerText !== undefined)
|
2026-09-21 17:38:45 -07:00
|
|
|
compiled.resource.revision.controllers = parseControllerCatalog(
|
|
|
|
|
controllerText,
|
|
|
|
|
queryInterfaces,
|
|
|
|
|
compiled.resource.revision.checkedQueries,
|
|
|
|
|
);
|
2026-09-10 18:27:41 -07:00
|
|
|
let catalogText: string | undefined;
|
2026-09-15 15:23:24 -07:00
|
|
|
try {
|
|
|
|
|
catalogText = await readFile(path.join(snapshot.directory, "quixos.migrations.json"), "utf8");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
|
|
|
}
|
2026-09-10 18:27:41 -07:00
|
|
|
if (catalogText !== undefined) {
|
2026-09-15 15:23:24 -07:00
|
|
|
const catalog = validateMigrationCatalog(
|
|
|
|
|
JSON.parse(catalogText),
|
|
|
|
|
new Set(compiled.resource.revision.exports.map((entry) => entry.id)),
|
|
|
|
|
);
|
2026-09-10 18:27:41 -07:00
|
|
|
const root = await realpath(snapshot.directory);
|
|
|
|
|
for (const migration of catalog.migrations) {
|
|
|
|
|
const implementation = await realpath(path.join(root, migration.implementation.file));
|
2026-09-15 15:23:24 -07:00
|
|
|
if (!implementation.startsWith(`${root}${path.sep}`))
|
|
|
|
|
throw new Error("Migration implementation escapes its package");
|
|
|
|
|
if (contentDigest(await readFile(implementation, "utf8")) !== migration.implementation.digest)
|
|
|
|
|
throw new Error(`Migration implementation digest mismatch: ${migration.id}`);
|
2026-09-10 18:27:41 -07:00
|
|
|
}
|
|
|
|
|
compiled.resource.revision.migrationCatalog = catalog;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-01 01:37:46 -07:00
|
|
|
checkedResources++;
|
|
|
|
|
const queries =
|
|
|
|
|
compiled.resource.kind === "package" ? (compiled.resource.revision.checkedQueries?.length ?? 0) : 0;
|
|
|
|
|
checkedQueries += queries;
|
|
|
|
|
profile("resource-check", checkStarted, {
|
|
|
|
|
kind: locked.kind,
|
|
|
|
|
resource: compiled.resource.revision.displayName,
|
|
|
|
|
queries,
|
|
|
|
|
});
|
2026-09-05 12:33:52 -07:00
|
|
|
return {
|
|
|
|
|
key,
|
|
|
|
|
kind: locked.kind,
|
|
|
|
|
source: locked.source,
|
|
|
|
|
directory: snapshot.directory,
|
|
|
|
|
lock: lockResult.lock,
|
|
|
|
|
resource: compiled.resource,
|
2026-09-15 15:23:24 -07:00
|
|
|
dependencies: new Map(
|
|
|
|
|
directPairs.map(([dependency, node]) => [bindingKey(dependency.kind, dependency.binding), node]),
|
|
|
|
|
),
|
2026-09-05 12:33:52 -07:00
|
|
|
};
|
|
|
|
|
} finally {
|
|
|
|
|
active.pop();
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
resolved.set(key, pending);
|
|
|
|
|
return await pending;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
visit,
|
2026-10-01 01:37:46 -07:00
|
|
|
nodes: async () => {
|
|
|
|
|
const nodes = await Promise.all(resolved.values());
|
|
|
|
|
profile("resource-graph", graphStarted, { checkedResources, reusedResources, checkedQueries });
|
|
|
|
|
return nodes;
|
|
|
|
|
},
|
2026-09-05 12:33:52 -07:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const compileCapabilityResourceRepository = async (options: {
|
|
|
|
|
rootDirectory: string;
|
|
|
|
|
kind: LockedResource["kind"];
|
|
|
|
|
source: GitSource;
|
|
|
|
|
resolveResource: CapabilityRepositoryResolver;
|
2026-09-27 15:53:34 -07:00
|
|
|
generateQueries?: PackageQueryGenerator;
|
2026-10-01 01:37:46 -07:00
|
|
|
checkedResource?: CheckedResourceResolver;
|
2026-09-05 12:33:52 -07:00
|
|
|
}): Promise<CompiledCapabilityResourceRepository> => {
|
|
|
|
|
const lockResult = await loadQuixosLock(path.join(options.rootDirectory, "quixos.lock"));
|
|
|
|
|
if (!lockResult.ok) {
|
|
|
|
|
throw new Error(diagnosticsMessage("Resource lock", lockResult.diagnostics));
|
|
|
|
|
}
|
2026-09-27 15:53:34 -07:00
|
|
|
const resolver = createResourceGraphResolver(
|
|
|
|
|
lockResult.lock.quixos.commit,
|
|
|
|
|
options.resolveResource,
|
|
|
|
|
options.generateQueries,
|
2026-10-01 01:37:46 -07:00
|
|
|
options.checkedResource,
|
2026-09-27 15:53:34 -07:00
|
|
|
);
|
2026-09-15 15:23:24 -07:00
|
|
|
const root = await resolver.visit(
|
|
|
|
|
{
|
|
|
|
|
kind: options.kind,
|
|
|
|
|
binding: "<root>",
|
|
|
|
|
source: options.source,
|
|
|
|
|
},
|
|
|
|
|
{ directory: options.rootDirectory },
|
2026-09-05 12:33:52 -07:00
|
|
|
);
|
|
|
|
|
return {
|
|
|
|
|
resource: root.resource,
|
|
|
|
|
lock: root.lock,
|
|
|
|
|
resources: await resolver.nodes(),
|
2026-09-22 13:51:32 -07:00
|
|
|
capabilityResources: resourceClosure([root], true),
|
2026-09-05 12:33:52 -07:00
|
|
|
directResources: root.dependencies,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const compileWorkspaceRepository = async (options: {
|
|
|
|
|
rootDirectory: string;
|
|
|
|
|
resolveResource: CapabilityRepositoryResolver;
|
|
|
|
|
workspaceId?: string;
|
|
|
|
|
workspaceRevisionId?: string;
|
|
|
|
|
sourceRootCommit?: string;
|
2026-09-08 14:27:24 -07:00
|
|
|
/** An editor's proposed source snapshot; locks and dependency revisions remain exact. */
|
|
|
|
|
readSource?: (name: string) => Promise<string>;
|
2026-09-27 15:53:34 -07:00
|
|
|
generateQueries?: PackageQueryGenerator;
|
2026-10-01 01:37:46 -07:00
|
|
|
checkedResource?: CheckedResourceResolver;
|
2026-09-05 12:33:52 -07:00
|
|
|
}): Promise<CompiledWorkspaceRepository> => {
|
2026-09-15 15:23:24 -07:00
|
|
|
const rootLockResult = await loadQuixosLock(path.join(options.rootDirectory, "quixos.lock"));
|
2026-09-05 12:33:52 -07:00
|
|
|
if (!rootLockResult.ok) {
|
|
|
|
|
throw new Error(diagnosticsMessage("Workspace lock", rootLockResult.diagnostics));
|
|
|
|
|
}
|
|
|
|
|
const rootLock = rootLockResult.lock;
|
2026-09-27 15:53:34 -07:00
|
|
|
const resolver = createResourceGraphResolver(
|
|
|
|
|
rootLock.quixos.commit,
|
|
|
|
|
options.resolveResource,
|
|
|
|
|
options.generateQueries,
|
2026-10-01 01:37:46 -07:00
|
|
|
options.checkedResource,
|
2026-09-27 15:53:34 -07:00
|
|
|
);
|
2026-09-05 12:33:52 -07:00
|
|
|
|
|
|
|
|
const directPairs: Array<[LockedResource, ResolvedCapabilityResource]> = [];
|
|
|
|
|
for (const locked of rootLock.resources) {
|
|
|
|
|
directPairs.push([locked, await resolver.visit(locked)]);
|
|
|
|
|
}
|
|
|
|
|
const nodes = await resolver.nodes();
|
|
|
|
|
const environment = environmentFor(directPairs, nodes);
|
|
|
|
|
const workspacePath = path.join(options.rootDirectory, "workspace.qx");
|
2026-09-15 15:23:24 -07:00
|
|
|
const sources = options.readSource
|
|
|
|
|
? await resolveQxSources(options.readSource)
|
|
|
|
|
: await loadQxSources(options.rootDirectory);
|
2026-09-08 14:27:24 -07:00
|
|
|
const compiled = compileCapabilitySource(sources.source, workspacePath, environment);
|
2026-09-05 12:33:52 -07:00
|
|
|
if (!compiled.ok) {
|
2026-09-15 15:23:24 -07:00
|
|
|
throw new Error(
|
|
|
|
|
diagnosticsMessage(
|
|
|
|
|
"Workspace",
|
|
|
|
|
compiled.diagnostics.map((diagnostic) =>
|
|
|
|
|
diagnostic.line > 0
|
|
|
|
|
? { ...diagnostic, ...sources.originalPosition(diagnostic.line, diagnostic.column) }
|
|
|
|
|
: diagnostic,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-09-05 12:33:52 -07:00
|
|
|
}
|
|
|
|
|
assertImportsMatchLock(workspacePath, compiled.imports, rootLock.resources);
|
2026-09-22 13:51:32 -07:00
|
|
|
const availableInterfaceIds = new Set((environment.interfaceClosure ?? []).map((iface) => iface.revisionId));
|
2026-09-05 12:33:52 -07:00
|
|
|
const availableAtomIds = new Set(compiled.workspace.atoms.map((atom) => atom.id));
|
2026-09-22 13:51:32 -07:00
|
|
|
for (const node of resourceClosure(
|
|
|
|
|
directPairs.map(([, node]) => node),
|
|
|
|
|
true,
|
|
|
|
|
)) {
|
|
|
|
|
if (node.resource.kind === "build") continue;
|
2026-09-05 12:33:52 -07:00
|
|
|
for (const requirement of node.resource.externalInterfaces) {
|
|
|
|
|
if (!availableInterfaceIds.has(requirement.revisionId)) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`${node.kind} ${node.resource.revision.displayName} requires external interface ` +
|
2026-09-15 15:23:24 -07:00
|
|
|
`${requirement.binding} (${requirement.revisionId}), but the workspace resource graph does not provide it`,
|
2026-09-05 12:33:52 -07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (const atom of node.resource.externalAtoms) {
|
|
|
|
|
if (!availableAtomIds.has(atom.id)) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`${node.kind} ${node.resource.revision.displayName} requires external atom ` +
|
2026-09-15 15:23:24 -07:00
|
|
|
`${atom.displayName} (${atom.id}), but the workspace does not define it`,
|
2026-09-05 12:33:52 -07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const workspace: WorkspaceRevision = {
|
|
|
|
|
...compiled.workspace,
|
2026-09-15 15:23:24 -07:00
|
|
|
...(options.workspaceId ? { workspaceId: capabilityId.workspace(options.workspaceId) } : {}),
|
2026-09-05 12:33:52 -07:00
|
|
|
...(options.workspaceRevisionId
|
|
|
|
|
? { id: capabilityId.workspaceRevision(options.workspaceRevisionId) }
|
2026-09-15 15:23:24 -07:00
|
|
|
: options.sourceRootCommit
|
|
|
|
|
? {
|
|
|
|
|
id: capabilityId.workspaceRevision(
|
|
|
|
|
`workspace-revision:${options.workspaceId ?? compiled.workspace.workspaceId}:${options.sourceRootCommit}`,
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
: {}),
|
|
|
|
|
...(options.sourceRootCommit ? { sourceRootCommit: options.sourceRootCommit } : {}),
|
2026-09-05 12:33:52 -07:00
|
|
|
};
|
2026-09-27 16:34:53 -07:00
|
|
|
await options.generateQueries?.completeWorkspace?.(workspace);
|
2026-09-05 12:33:52 -07:00
|
|
|
const checked = compileWorkspaceRevision(workspace);
|
|
|
|
|
if (!checked.ok) {
|
|
|
|
|
throw new Error(
|
2026-09-15 15:23:24 -07:00
|
|
|
`Instantiated workspace did not compile:\n${checked.issues
|
|
|
|
|
.map((entry) => `${entry.path}: ${entry.message}`)
|
|
|
|
|
.join("\n")}`,
|
2026-09-05 12:33:52 -07:00
|
|
|
);
|
|
|
|
|
}
|
2026-09-17 14:34:16 -07:00
|
|
|
const linkedQueries = linkQueries(workspace);
|
|
|
|
|
if (linkedQueries.length) {
|
|
|
|
|
workspace.linkedQueries = linkedQueries;
|
|
|
|
|
checked.plan.source.linkedQueries = structuredClone(linkedQueries);
|
|
|
|
|
}
|
2026-09-05 12:33:52 -07:00
|
|
|
return {
|
|
|
|
|
workspace,
|
|
|
|
|
plan: checked.plan,
|
|
|
|
|
lock: rootLock,
|
|
|
|
|
resources: nodes,
|
2026-09-22 13:51:32 -07:00
|
|
|
capabilityResources: resourceClosure(
|
|
|
|
|
directPairs.map(([, node]) => node),
|
|
|
|
|
true,
|
|
|
|
|
),
|
2026-09-15 15:23:24 -07:00
|
|
|
directResources: new Map(directPairs.map(([locked, node]) => [bindingKey(locked.kind, locked.binding), node])),
|
2026-09-05 12:33:52 -07:00
|
|
|
};
|
|
|
|
|
};
|