Files
quixos-protocol/src/capability-language/candidate-check.ts
T
Timothy J. Aveni 52803dda05 Implement capability generics, checked package specializations and CRUD scaffolding
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.
2026-09-16 10:42:12 -07:00

265 lines
11 KiB
TypeScript

import fs from "node:fs/promises";
import path from "node:path";
import os from "node:os";
import { execFile as execFileCallback } from "node:child_process";
import { promisify } from "node:util";
import { createHash } from "node:crypto";
import { compileWorkspaceRepository, compileCapabilityResourceRepository } from "./assembly.js";
import { createGitCapabilityResolver } from "./git-resolver.js";
import {
contentDigest,
planEvolution,
type EvolutionReview,
type WorkspaceRevision,
} from "../capability-model/index.js";
import { bindingSchema, specializeBindingSchema } from "../bindings/index.js";
import { snapshotCommit, checkoutCommit, buildCheckedPackage } from "./checked-build.js";
const execFile = promisify(execFileCallback);
const bytesDigest = (value: Uint8Array) => `sha256:${createHash("sha256").update(value).digest("hex")}`;
export const localResourceSnapshots = async (
root: string,
filename?: string,
): Promise<{ resources: { kind: string; repository: string; commit: string; directory: string }[] }> => {
if (filename) {
const document = JSON.parse(await fs.readFile(filename, "utf8"));
return {
resources: document.resources.map((entry: { directory: string }) => ({
...entry,
directory: path.resolve(path.dirname(filename), entry.directory),
})),
};
}
let directory = await fs.realpath(root);
for (;;) {
let graphText: string | undefined;
try {
graphText = await fs.readFile(path.join(directory, ".quixos/resource-graph.json"), "utf8");
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
if (graphText !== undefined) {
const graph = JSON.parse(graphText);
const resources = [];
for (const entry of graph.resources) {
const location = await fs.realpath(path.resolve(directory, entry.directory));
if (!location.startsWith(`${directory}/resources/`))
throw new Error("Workbench resource escapes managed directory");
resources.push({ kind: entry.kind, ...entry.source, directory: location });
}
return { resources };
}
const parent = path.dirname(directory);
if (parent === directory) return { resources: [] };
directory = parent;
}
};
/** Copy actual authoring files without snapshotting jj or creating a Git commit. */
export const snapshotRepository = async (source: string, destination: string) => {
const root = await fs.realpath(source);
const files = async () =>
(
await execFile("git", ["-C", root, "ls-files", "--cached", "--others", "--exclude-standard", "-z"], {
maxBuffer: 16 * 1024 * 1024,
})
).stdout
.split("\0")
.filter(Boolean)
.sort();
const names = [...new Set(await files())];
if (names.length > 50_000) throw new Error("Candidate source exceeds 50000 files");
const contents: { name: string; digest: string; mode: number }[] = [];
let bytes = 0;
await fs.mkdir(destination, { recursive: true, mode: 0o700 });
for (const name of names) {
if (path.isAbsolute(name) || name.split(/[\\/]/).some((part) => part === ".." || part === ".git" || part === ".jj"))
throw new Error("Invalid candidate source path");
const file = path.join(root, name);
let metadata;
try {
metadata = await fs.lstat(file);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
throw error;
}
if (!metadata.isFile() || metadata.isSymbolicLink() || !(await fs.realpath(file)).startsWith(`${root}${path.sep}`))
throw new Error(`Candidate source must be a regular file: ${name}`);
const data = await fs.readFile(file);
bytes += data.length;
if (bytes > 128 * 1024 * 1024) throw new Error("Candidate source exceeds 128 MiB");
contents.push({ name, digest: bytesDigest(data), mode: metadata.mode & 0o777 });
await fs.mkdir(path.dirname(path.join(destination, name)), { recursive: true });
await fs.writeFile(path.join(destination, name), data, { flag: "wx", mode: metadata.mode & 0o777 });
}
if (JSON.stringify([...new Set(await files())]) !== JSON.stringify(names))
throw new Error("Source files changed during candidate snapshot");
for (const entry of contents)
if (bytesDigest(await fs.readFile(path.join(root, entry.name))) !== entry.digest)
throw new Error(`Source changed during candidate snapshot: ${entry.name}`);
return { source: root, directory: destination, treeDigest: contentDigest(contents), files: contents };
};
// Local mirrors accelerate resolution, but only their committed locked trees
// may stand in for published dependencies. Never relabel dirty files as a pin.
async function committedResolver(root: string, temporary: string, filename?: string, publishedOnly = false) {
const map = publishedOnly ? { resources: [] } : await localResourceSnapshots(root, filename);
const resources = [];
for (const [index, entry] of map.resources.entries()) {
const directory = path.join(temporary, `dependency-${index}`);
await checkoutCommit(entry.directory, entry.commit, directory);
resources.push({ ...entry, directory });
}
const snapshotMap = path.join(temporary, "snapshots.json");
await fs.writeFile(snapshotMap, JSON.stringify({ resources }));
return createGitCapabilityResolver({ checkoutRoot: path.join(temporary, "resolved"), snapshotMap });
}
export const checkResourceCandidate = async (options: {
root: string;
output: string;
kind: "package" | "interface";
source: { repository: string; commit: string };
snapshotMap?: string;
publishedOnly?: boolean;
}) => {
await fs.mkdir(options.output, { mode: 0o700 });
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "qx-resource-check-"));
const blockers: string[] = [];
let treeDigest: string | undefined, commit: string | undefined, artifactPath: string | undefined;
try {
commit = await snapshotCommit(options.root);
treeDigest = (await snapshotRepository(options.root, path.join(temporary, "observed"))).treeDigest;
const root = path.join(temporary, "source");
await checkoutCommit(options.root, commit, root);
const resolveResource = await committedResolver(
options.root,
temporary,
options.snapshotMap,
options.publishedOnly,
);
const compiled = await compileCapabilityResourceRepository({
rootDirectory: root,
kind: options.kind,
source: { resolver: "git", repository: options.source.repository, commit },
resolveResource,
});
if (compiled.resource.kind === "package") {
const schema = path.join(temporary, "bindings.json");
await fs.writeFile(schema, JSON.stringify(bindingSchema(compiled)));
artifactPath = await buildCheckedPackage(root, schema, compiled.resource.revision.revisionId);
}
if ((await snapshotCommit(options.root)) !== commit)
throw new Error("Source changed during verification; run the check again");
await fs.writeFile(path.join(options.output, "candidate.json"), JSON.stringify(compiled.resource, null, 2));
} catch (error) {
blockers.push(error instanceof Error ? error.message : String(error));
} finally {
await fs.rm(temporary, { recursive: true, force: true });
}
const result = {
candidateOnly: true,
activationEvidence: false,
commit,
treeDigest,
artifactPath,
blockers,
note: "Checked immutable candidate; cutover independently checks current migration/review requirements. No publication or activation performed.",
};
await fs.writeFile(path.join(options.output, "report.json"), JSON.stringify(result, null, 2));
return result;
};
export const checkWorkspaceCandidate = async (options: {
root: string;
output: string;
snapshotMap?: string;
baseline?: string;
reviews?: string;
}) => {
await fs.mkdir(options.output, { mode: 0o700 });
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "qx-workspace-check-"));
const blockers: string[] = [],
checks: { packageRevisionId: string; artifactPath: string }[] = [];
let commit: string | undefined;
try {
commit = await snapshotCommit(options.root);
for (const entry of (await localResourceSnapshots(options.root, options.snapshotMap)).resources) {
const current = await snapshotCommit(entry.directory);
const tree = async (revision: string) =>
(await execFile("git", ["rev-parse", `${revision}^{tree}`], { cwd: entry.directory })).stdout.trim();
if ((await tree(current)) !== (await tree(entry.commit)))
throw new Error(
`Edited resource is not in the root's locked candidate: ${entry.directory}. Check that resource, then run qx-workspace resource upgrade --publish to propagate its revision.`,
);
}
const root = path.join(temporary, "source");
await checkoutCommit(options.root, commit, root);
const resolveResource = await committedResolver(options.root, temporary, options.snapshotMap);
const compiled = await compileWorkspaceRepository({
rootDirectory: root,
sourceRootCommit: commit,
resolveResource,
});
const baseline = options.baseline
? (JSON.parse(await fs.readFile(options.baseline, "utf8")) as WorkspaceRevision)
: null;
const reviews = options.reviews
? (JSON.parse(await fs.readFile(options.reviews, "utf8")) as EvolutionReview[])
: [];
const evolution = planEvolution(baseline, compiled.workspace, { reviews });
blockers.push(...evolution.blockers);
for (const resource of compiled.resources.filter((entry) => entry.kind === "package")) {
// Per-package recursive schema, identical to host activation, not unrelated
// workspace declarations that would unnecessarily invalidate build caches.
const candidate = await compileCapabilityResourceRepository({
rootDirectory: resource.directory,
kind: "package",
source: resource.source,
resolveResource,
});
const schema = path.join(temporary, "bindings.json");
await fs.writeFile(
schema,
JSON.stringify(
specializeBindingSchema(bindingSchema(candidate), compiled.workspace, candidate.resource.revision.revisionId),
),
);
const artifactPath = await buildCheckedPackage(
resource.directory,
schema,
candidate.resource.revision.revisionId,
);
checks.push({ packageRevisionId: candidate.resource.revision.revisionId, artifactPath });
}
if ((await snapshotCommit(options.root)) !== commit)
throw new Error("Source changed during verification; run the check again");
const result = {
schemaVersion: 1,
candidateOnly: true,
activationEvidence: false,
commit,
evolution,
checks,
blockers,
note: "Checks the committed root and its exact locked dependencies. Resource edits must be verified and repinned before they enter this candidate. No publication or activation performed.",
};
await fs.writeFile(path.join(options.output, "candidate.json"), JSON.stringify(compiled.workspace, null, 2));
await fs.writeFile(path.join(options.output, "report.json"), JSON.stringify(result, null, 2));
return result;
} catch (error) {
const result = {
schemaVersion: 1,
candidateOnly: true,
activationEvidence: false,
commit,
checks,
blockers: [...blockers, error instanceof Error ? error.message : String(error)],
};
await fs.writeFile(path.join(options.output, "report.json"), JSON.stringify(result, null, 2));
return result;
} finally {
await fs.rm(temporary, { recursive: true, force: true });
}
};