52803dda05
Add kinded parameters, capability bounds, Self, aliases and closed application identities. Check generic implementations universally and build candidate-specific codecs and descriptors from immutable schemas. Preserve lexical aliases and exact dispatch identities in package and host bindings. Add an imperative CRUD+index domain scaffold with explicit soft-deletion semantics, source/codegen regression coverage, installed CLI tests and an authoring guide. Existing Web Studio opaque props and class-level create-menu migration are separate from the implemented language core.
400 lines
14 KiB
TypeScript
400 lines
14 KiB
TypeScript
import { readFile, realpath } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { loadQxSources, resolveQxSources } from "./source-loader.js";
|
|
import {
|
|
capabilityId,
|
|
compileWorkspaceRevision,
|
|
validateMigrationCatalog,
|
|
contentDigest,
|
|
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>;
|
|
|
|
export type ResolvedCapabilityResource = {
|
|
key: string;
|
|
kind: LockedResource["kind"];
|
|
source: GitSource;
|
|
directory: string;
|
|
lock: QuixosRepositoryLock;
|
|
resource: CapabilityResource;
|
|
dependencies: ReadonlyMap<string, ResolvedCapabilityResource>;
|
|
};
|
|
|
|
export type CompiledWorkspaceRepository = {
|
|
workspace: WorkspaceRevision;
|
|
plan: CompiledWorkspaceRevision;
|
|
lock: QuixosRepositoryLock;
|
|
resources: ResolvedCapabilityResource[];
|
|
directResources: ReadonlyMap<string, ResolvedCapabilityResource>;
|
|
};
|
|
|
|
export type CompiledCapabilityResourceRepository = {
|
|
resource: CapabilityResource;
|
|
lock: QuixosRepositoryLock;
|
|
resources: ResolvedCapabilityResource[];
|
|
directResources: ReadonlyMap<string, ResolvedCapabilityResource>;
|
|
};
|
|
|
|
const sourceKey = (kind: LockedResource["kind"], source: GitSource) =>
|
|
`${kind}\0${source.repository}\0${source.commit.toLowerCase()}`;
|
|
|
|
const bindingKey = (kind: LockedResource["kind"], binding: string) => `${kind}\0${binding}`;
|
|
|
|
const importsKey = (entry: CapabilityResourceImport | LockedResource) => bindingKey(entry.kind, entry.binding);
|
|
|
|
const assertImportsMatchLock = (
|
|
label: string,
|
|
imports: readonly CapabilityResourceImport[],
|
|
resources: readonly LockedResource[],
|
|
) => {
|
|
const authored = [...imports].map(importsKey).sort();
|
|
const locked = [...resources].map(importsKey).sort();
|
|
if (authored.length !== locked.length || authored.some((entry, index) => entry !== locked[index])) {
|
|
throw new Error(
|
|
`${label} imports do not match quixos.lock:\n` +
|
|
`authored: ${authored.join(", ") || "none"}\n` +
|
|
`locked: ${locked.join(", ") || "none"}`,
|
|
);
|
|
}
|
|
};
|
|
|
|
const exactRevisions = <
|
|
Revision extends {
|
|
revisionId: string;
|
|
source: SourceRevision;
|
|
},
|
|
>(
|
|
revisions: readonly Revision[],
|
|
) => {
|
|
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;
|
|
}[],
|
|
) =>
|
|
`${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")}`;
|
|
|
|
const revisionFor = (node: ResolvedCapabilityResource) => node.resource.revision;
|
|
|
|
const resourceClosure = (roots: readonly ResolvedCapabilityResource[]): ResolvedCapabilityResource[] => {
|
|
const result: ResolvedCapabilityResource[] = [];
|
|
const seen = new Set<string>();
|
|
const visit = (node: ResolvedCapabilityResource) => {
|
|
if (seen.has(node.key)) return;
|
|
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[],
|
|
): CapabilityImportEnvironment => ({
|
|
interfaces: new Map(
|
|
direct.flatMap(([locked, node]) =>
|
|
node.resource.kind === "interface" ? [[locked.binding, node.resource.revision] as const] : [],
|
|
),
|
|
),
|
|
packages: new Map(
|
|
direct.flatMap(([locked, node]) =>
|
|
node.resource.kind === "package" ? [[locked.binding, node.resource.revision] as const] : [],
|
|
),
|
|
),
|
|
interfaceClosure: exactRevisions(
|
|
closure.flatMap((node) => [
|
|
...(node.resource.kind === "interface" ? [node.resource.revision] : []),
|
|
...(node.resource.specializations ?? []),
|
|
]),
|
|
),
|
|
packageClosure: exactRevisions(
|
|
closure.flatMap((node) => (node.resource.kind === "package" ? [node.resource.revision] : [])),
|
|
),
|
|
externalAtoms: exactAtoms(closure.flatMap((node) => node.resource.externalAtoms)),
|
|
});
|
|
|
|
const createResourceGraphResolver = (quixosCommit: string, resolveResource: CapabilityRepositoryResolver) => {
|
|
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 {
|
|
const snapshot = suppliedSnapshot ?? (await resolveResource(locked.source, locked.kind));
|
|
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 ` +
|
|
`${lockResult.lock.quixos.policy}; resource locks may only declare their exact authored-against commit`,
|
|
);
|
|
}
|
|
if (lockResult.lock.quixos.commit.toLowerCase() !== quixosCommit.toLowerCase()) {
|
|
throw new Error(
|
|
`${locked.kind} ${locked.binding} selects Quixos ${lockResult.lock.quixos.commit}, ` +
|
|
`but the repository graph selects ${quixosCommit}`,
|
|
);
|
|
}
|
|
const directPairs: Array<[LockedResource, ResolvedCapabilityResource]> = [];
|
|
for (const dependency of lockResult.lock.resources) {
|
|
directPairs.push([dependency, await visit(dependency)]);
|
|
}
|
|
const dependencyClosure = resourceClosure(directPairs.map(([, node]) => node));
|
|
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) {
|
|
throw new Error(`${manifestPath} declares ${compiled.resource.kind}, not ${locked.kind}`);
|
|
}
|
|
assertImportsMatchLock(manifestPath, compiled.resource.imports, lockResult.lock.resources);
|
|
if (compiled.resource.kind === "package") {
|
|
let catalogText: string | undefined;
|
|
try {
|
|
catalogText = await readFile(path.join(snapshot.directory, "quixos.migrations.json"), "utf8");
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
}
|
|
if (catalogText !== undefined) {
|
|
const catalog = validateMigrationCatalog(
|
|
JSON.parse(catalogText),
|
|
new Set(compiled.resource.revision.exports.map((entry) => entry.id)),
|
|
);
|
|
const root = await realpath(snapshot.directory);
|
|
for (const migration of catalog.migrations) {
|
|
const implementation = await realpath(path.join(root, migration.implementation.file));
|
|
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}`);
|
|
}
|
|
compiled.resource.revision.migrationCatalog = catalog;
|
|
}
|
|
}
|
|
return {
|
|
key,
|
|
kind: locked.kind,
|
|
source: locked.source,
|
|
directory: snapshot.directory,
|
|
lock: lockResult.lock,
|
|
resource: compiled.resource,
|
|
dependencies: new Map(
|
|
directPairs.map(([dependency, node]) => [bindingKey(dependency.kind, dependency.binding), node]),
|
|
),
|
|
};
|
|
} finally {
|
|
active.pop();
|
|
}
|
|
})();
|
|
resolved.set(key, pending);
|
|
return await pending;
|
|
};
|
|
|
|
return {
|
|
visit,
|
|
nodes: async () => await Promise.all(resolved.values()),
|
|
};
|
|
};
|
|
|
|
export const compileCapabilityResourceRepository = async (options: {
|
|
rootDirectory: string;
|
|
kind: LockedResource["kind"];
|
|
source: GitSource;
|
|
resolveResource: CapabilityRepositoryResolver;
|
|
}): 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 root = await resolver.visit(
|
|
{
|
|
kind: options.kind,
|
|
binding: "<root>",
|
|
source: options.source,
|
|
},
|
|
{ directory: options.rootDirectory },
|
|
);
|
|
return {
|
|
resource: root.resource,
|
|
lock: root.lock,
|
|
resources: await resolver.nodes(),
|
|
directResources: root.dependencies,
|
|
};
|
|
};
|
|
|
|
export const compileWorkspaceRepository = async (options: {
|
|
rootDirectory: string;
|
|
resolveResource: CapabilityRepositoryResolver;
|
|
workspaceId?: string;
|
|
workspaceRevisionId?: string;
|
|
sourceRootCommit?: string;
|
|
/** An editor's proposed source snapshot; locks and dependency revisions remain exact. */
|
|
readSource?: (name: string) => Promise<string>;
|
|
}): 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 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");
|
|
const sources = options.readSource
|
|
? await resolveQxSources(options.readSource)
|
|
: await loadQxSources(options.rootDirectory);
|
|
const compiled = compileCapabilitySource(sources.source, workspacePath, environment);
|
|
if (!compiled.ok) {
|
|
throw new Error(
|
|
diagnosticsMessage(
|
|
"Workspace",
|
|
compiled.diagnostics.map((diagnostic) =>
|
|
diagnostic.line > 0
|
|
? { ...diagnostic, ...sources.originalPosition(diagnostic.line, diagnostic.column) }
|
|
: diagnostic,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
assertImportsMatchLock(workspacePath, compiled.imports, rootLock.resources);
|
|
const availableInterfaceIds = new Set(
|
|
nodes.flatMap((node) => (node.resource.kind === "interface" ? [node.resource.revision.revisionId] : [])),
|
|
);
|
|
const availableAtomIds = new Set(compiled.workspace.atoms.map((atom) => atom.id));
|
|
for (const node of nodes) {
|
|
for (const requirement of node.resource.externalInterfaces) {
|
|
if (!availableInterfaceIds.has(requirement.revisionId)) {
|
|
throw new Error(
|
|
`${node.kind} ${node.resource.revision.displayName} requires external interface ` +
|
|
`${requirement.binding} (${requirement.revisionId}), but the workspace resource graph does not provide it`,
|
|
);
|
|
}
|
|
}
|
|
for (const atom of node.resource.externalAtoms) {
|
|
if (!availableAtomIds.has(atom.id)) {
|
|
throw new Error(
|
|
`${node.kind} ${node.resource.revision.displayName} requires external atom ` +
|
|
`${atom.displayName} (${atom.id}), but the workspace does not define it`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
const workspace: WorkspaceRevision = {
|
|
...compiled.workspace,
|
|
...(options.workspaceId ? { workspaceId: capabilityId.workspace(options.workspaceId) } : {}),
|
|
...(options.workspaceRevisionId
|
|
? { id: capabilityId.workspaceRevision(options.workspaceRevisionId) }
|
|
: options.sourceRootCommit
|
|
? {
|
|
id: capabilityId.workspaceRevision(
|
|
`workspace-revision:${options.workspaceId ?? compiled.workspace.workspaceId}:${options.sourceRootCommit}`,
|
|
),
|
|
}
|
|
: {}),
|
|
...(options.sourceRootCommit ? { sourceRootCommit: options.sourceRootCommit } : {}),
|
|
};
|
|
const checked = compileWorkspaceRevision(workspace);
|
|
if (!checked.ok) {
|
|
throw new Error(
|
|
`Instantiated workspace did not compile:\n${checked.issues
|
|
.map((entry) => `${entry.path}: ${entry.message}`)
|
|
.join("\n")}`,
|
|
);
|
|
}
|
|
return {
|
|
workspace,
|
|
plan: checked.plan,
|
|
lock: rootLock,
|
|
resources: nodes,
|
|
directResources: new Map(directPairs.map(([locked, node]) => [bindingKey(locked.kind, locked.binding), node])),
|
|
};
|
|
};
|