Build composable Camino packages with explicit type and source dependencies
This commit is contained in:
@@ -47,7 +47,7 @@ export type ResolvedCapabilityResource = {
|
||||
source: GitSource;
|
||||
directory: string;
|
||||
lock: QuixosRepositoryLock;
|
||||
resource: CapabilityResource;
|
||||
resource: CapabilityResource | { kind: "build" };
|
||||
dependencies: ReadonlyMap<string, ResolvedCapabilityResource>;
|
||||
};
|
||||
|
||||
@@ -56,13 +56,15 @@ export type CompiledWorkspaceRepository = {
|
||||
plan: CompiledWorkspaceRevision;
|
||||
lock: QuixosRepositoryLock;
|
||||
resources: ResolvedCapabilityResource[];
|
||||
capabilityResources: ResolvedCapabilityResource[];
|
||||
directResources: ReadonlyMap<string, ResolvedCapabilityResource>;
|
||||
};
|
||||
|
||||
export type CompiledCapabilityResourceRepository = {
|
||||
resource: CapabilityResource;
|
||||
resource: CapabilityResource | { kind: "build" };
|
||||
lock: QuixosRepositoryLock;
|
||||
resources: ResolvedCapabilityResource[];
|
||||
capabilityResources: ResolvedCapabilityResource[];
|
||||
directResources: ReadonlyMap<string, ResolvedCapabilityResource>;
|
||||
};
|
||||
|
||||
@@ -79,7 +81,10 @@ const assertImportsMatchLock = (
|
||||
resources: readonly LockedResource[],
|
||||
) => {
|
||||
const authored = [...imports].map(importsKey).sort();
|
||||
const locked = [...resources].map(importsKey).sort();
|
||||
const locked = resources
|
||||
.filter((entry) => entry.kind !== "build")
|
||||
.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` +
|
||||
@@ -137,13 +142,19 @@ const diagnosticsMessage = (
|
||||
})
|
||||
.join("\n")}`;
|
||||
|
||||
const revisionFor = (node: ResolvedCapabilityResource) => node.resource.revision;
|
||||
const revisionFor = (node: ResolvedCapabilityResource) => {
|
||||
if (node.resource.kind === "build") throw Error("Build sources have no capability revision");
|
||||
return node.resource.revision;
|
||||
};
|
||||
|
||||
const resourceClosure = (roots: readonly ResolvedCapabilityResource[]): ResolvedCapabilityResource[] => {
|
||||
const resourceClosure = (
|
||||
roots: readonly ResolvedCapabilityResource[],
|
||||
capabilityOnly = false,
|
||||
): ResolvedCapabilityResource[] => {
|
||||
const result: ResolvedCapabilityResource[] = [];
|
||||
const seen = new Set<string>();
|
||||
const visit = (node: ResolvedCapabilityResource) => {
|
||||
if (seen.has(node.key)) return;
|
||||
if (seen.has(node.key) || (capabilityOnly && node.kind === "build")) return;
|
||||
seen.add(node.key);
|
||||
for (const dependency of node.dependencies.values()) visit(dependency);
|
||||
result.push(node);
|
||||
@@ -155,28 +166,36 @@ const resourceClosure = (roots: readonly ResolvedCapabilityResource[]): Resolved
|
||||
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] : [],
|
||||
): 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] : [],
|
||||
),
|
||||
),
|
||||
),
|
||||
packages: new Map(
|
||||
direct.flatMap(([locked, node]) =>
|
||||
node.resource.kind === "package" ? [[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)),
|
||||
});
|
||||
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)),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const createResourceGraphResolver = (quixosCommit: string, resolveResource: CapabilityRepositoryResolver) => {
|
||||
const resolved = new Map<string, Promise<ResolvedCapabilityResource>>();
|
||||
@@ -218,6 +237,19 @@ const createResourceGraphResolver = (quixosCommit: string, resolveResource: Capa
|
||||
for (const dependency of lockResult.lock.resources) {
|
||||
directPairs.push([dependency, await visit(dependency)]);
|
||||
}
|
||||
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]),
|
||||
),
|
||||
};
|
||||
}
|
||||
const dependencyClosure = resourceClosure(directPairs.map(([, node]) => node));
|
||||
const environment = environmentFor(directPairs, dependencyClosure);
|
||||
const manifestName = locked.kind === "interface" ? "interface.qx" : "package.qx";
|
||||
@@ -332,6 +364,7 @@ export const compileCapabilityResourceRepository = async (options: {
|
||||
resource: root.resource,
|
||||
lock: root.lock,
|
||||
resources: await resolver.nodes(),
|
||||
capabilityResources: resourceClosure([root], true),
|
||||
directResources: root.dependencies,
|
||||
};
|
||||
};
|
||||
@@ -376,11 +409,13 @@ export const compileWorkspaceRepository = async (options: {
|
||||
);
|
||||
}
|
||||
assertImportsMatchLock(workspacePath, compiled.imports, rootLock.resources);
|
||||
const availableInterfaceIds = new Set(
|
||||
nodes.flatMap((node) => (node.resource.kind === "interface" ? [node.resource.revision.revisionId] : [])),
|
||||
);
|
||||
const availableInterfaceIds = new Set((environment.interfaceClosure ?? []).map((iface) => iface.revisionId));
|
||||
const availableAtomIds = new Set(compiled.workspace.atoms.map((atom) => atom.id));
|
||||
for (const node of nodes) {
|
||||
for (const node of resourceClosure(
|
||||
directPairs.map(([, node]) => node),
|
||||
true,
|
||||
)) {
|
||||
if (node.resource.kind === "build") continue;
|
||||
for (const requirement of node.resource.externalInterfaces) {
|
||||
if (!availableInterfaceIds.has(requirement.revisionId)) {
|
||||
throw new Error(
|
||||
@@ -430,6 +465,10 @@ export const compileWorkspaceRepository = async (options: {
|
||||
plan: checked.plan,
|
||||
lock: rootLock,
|
||||
resources: nodes,
|
||||
capabilityResources: resourceClosure(
|
||||
directPairs.map(([, node]) => node),
|
||||
true,
|
||||
),
|
||||
directResources: new Map(directPairs.map(([locked, node]) => [bindingKey(locked.kind, locked.binding), node])),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from "node:path";
|
||||
import { loadQuixosLock, type GitSource } from "../resource-lock/index.js";
|
||||
|
||||
export type AuthoringResource = {
|
||||
kind: "workspace" | "interface" | "package";
|
||||
kind: "workspace" | "interface" | "package" | "build";
|
||||
directory: string;
|
||||
resourceId?: string;
|
||||
source?: GitSource;
|
||||
@@ -31,7 +31,7 @@ export async function authoringContext(start: string) {
|
||||
if (typeof entry.directory === "string" && path.isAbsolute(entry.directory))
|
||||
entry.directory = path.relative(workbench, entry.directory);
|
||||
if (
|
||||
!["interface", "package"].includes(entry.kind) ||
|
||||
!["interface", "package", "build"].includes(entry.kind) ||
|
||||
!entry.source ||
|
||||
!/^resources\/[A-Za-z0-9][A-Za-z0-9._-]*$/.test(entry.directory)
|
||||
) {
|
||||
|
||||
@@ -69,7 +69,7 @@ export async function inspectAuthoringRepository(root: string, historyLimit = 10
|
||||
declarations: syntax
|
||||
? [...walkSyntax(syntax.root)]
|
||||
.filter((node) =>
|
||||
/^(?:interface|package|atom|state|edge|method|function|event|conformance|component|presentation)\w*Decl$/.test(
|
||||
/^(?:(?:interface|package|atom|state|edge|method|function|event|conformance|presentation)\w*Decl|module(?:Member|Export))$/.test(
|
||||
node.kind,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -209,7 +209,7 @@ export const checkWorkspaceCandidate = async (options: {
|
||||
: [];
|
||||
const evolution = planEvolution(baseline, compiled.workspace, { reviews });
|
||||
blockers.push(...evolution.blockers);
|
||||
for (const resource of compiled.resources.filter((entry) => entry.kind === "package")) {
|
||||
for (const resource of compiled.capabilityResources.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({
|
||||
@@ -218,6 +218,7 @@ export const checkWorkspaceCandidate = async (options: {
|
||||
source: resource.source,
|
||||
resolveResource,
|
||||
});
|
||||
if (candidate.resource.kind !== "package") throw Error("Expected checked package contract");
|
||||
const schema = path.join(temporary, "bindings.json");
|
||||
await fs.writeFile(
|
||||
schema,
|
||||
|
||||
@@ -107,7 +107,7 @@ export async function buildCheckedPackage(source: string, schema: string, packag
|
||||
* nested fetches. Keep build noise in a named log, with a bounded failure tail. */
|
||||
export async function buildImmutableCandidate(
|
||||
source: { repository: string; commit: string },
|
||||
kind: "workspace" | "interface" | "package",
|
||||
kind: "workspace" | "interface" | "package" | "build",
|
||||
logFile: string,
|
||||
contractOnly = false,
|
||||
): Promise<string> {
|
||||
|
||||
@@ -56,7 +56,7 @@ export async function executionScaffold(
|
||||
flake-utils.lib.eachDefaultSystem (system: let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
in {
|
||||
quixosPackages.checkedArtifacts = { plan, generator, core, sharedRuntime, typescript, packageRevisionId, react ? null }:
|
||||
quixosPackages.checkedArtifacts = { plan, generator, core, sharedRuntime, typescript, packageRevisionId, react ? null, ... }:
|
||||
(import (helpers + "/artifacts.nix")) {
|
||||
inherit pkgs plan core sharedRuntime typescript packageRevisionId;
|
||||
protocol = generator;
|
||||
|
||||
@@ -38,7 +38,7 @@ export const createGitCapabilityResolver = async (options: {
|
||||
}>;
|
||||
};
|
||||
for (const entry of document.resources ?? []) {
|
||||
if (entry.kind !== "interface" && entry.kind !== "package") {
|
||||
if (entry.kind !== "interface" && entry.kind !== "package" && entry.kind !== "build") {
|
||||
throw new Error(`Snapshot map has unsupported resource kind ${entry.kind}`);
|
||||
}
|
||||
const key = sourceKey(entry.kind, entry.repository, entry.commit);
|
||||
|
||||
@@ -19,7 +19,7 @@ import { planEvolution, type WorkspaceRevision, type EvolutionReview } from "../
|
||||
import { withFileLock } from "./file-lock.js";
|
||||
const execFile = promisify(callback);
|
||||
type Source = { repository: string; commit: string };
|
||||
export type UpgradeNode = { kind: "workspace" | "package" | "interface"; directory: string; source: Source };
|
||||
export type UpgradeNode = { kind: "workspace" | "package" | "interface" | "build"; directory: string; source: Source };
|
||||
export type UpgradeSpec = {
|
||||
nodes: UpgradeNode[];
|
||||
quixos?: Source;
|
||||
@@ -105,7 +105,7 @@ export const discoverUpgradeSpec = async (workbench: string): Promise<UpgradeSpe
|
||||
commit: await command(root, "jj", ["--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id"]),
|
||||
},
|
||||
},
|
||||
...graph.resources.map((entry: { kind: "interface" | "package"; directory: string; source: Source }) => ({
|
||||
...graph.resources.map((entry: { kind: "interface" | "package" | "build"; directory: string; source: Source }) => ({
|
||||
kind: entry.kind,
|
||||
source: entry.source,
|
||||
directory: path.relative(workbench, path.resolve(workbench, entry.directory)),
|
||||
@@ -139,7 +139,7 @@ export const planPinUpgrades = async (workbenchPath: string, spec: UpgradeSpec):
|
||||
const keys = new Map<string, string>();
|
||||
for (const node of spec.nodes) {
|
||||
validSource(node.source);
|
||||
if (!["workspace", "package", "interface"].includes(node.kind) || keys.has(sourceKey(node)))
|
||||
if (!["workspace", "package", "interface", "build"].includes(node.kind) || keys.has(sourceKey(node)))
|
||||
throw new Error("Duplicate/invalid upgrade resource identity");
|
||||
keys.set(sourceKey(node), node.directory);
|
||||
}
|
||||
@@ -423,14 +423,14 @@ export const applyPinUpgrades = async (
|
||||
const directory = await location(plan.workbench, node.directory);
|
||||
const standalone = await compileCapabilityResourceRepository({
|
||||
rootDirectory: directory,
|
||||
kind: node.kind as "package" | "interface",
|
||||
kind: node.kind as "package" | "interface" | "build",
|
||||
source,
|
||||
resolveResource,
|
||||
});
|
||||
for (const dependency of standalone.resources) resources.set(dependency.key, dependency);
|
||||
resources.set(key, {
|
||||
key,
|
||||
kind: node.kind as "package" | "interface",
|
||||
kind: node.kind as "package" | "interface" | "build",
|
||||
source,
|
||||
directory,
|
||||
lock: standalone.lock,
|
||||
@@ -450,11 +450,15 @@ export const applyPinUpgrades = async (
|
||||
kind: node.kind,
|
||||
source: node.source,
|
||||
directory: node.directory,
|
||||
resourceId:
|
||||
node.resource.kind === "interface"
|
||||
? node.resource.revision.interfaceId
|
||||
: node.resource.revision.packageId,
|
||||
revisionId: node.resource.revision.revisionId,
|
||||
...(node.resource.kind === "build"
|
||||
? {}
|
||||
: {
|
||||
resourceId:
|
||||
node.resource.kind === "interface"
|
||||
? node.resource.revision.interfaceId
|
||||
: node.resource.revision.packageId,
|
||||
revisionId: node.resource.revision.revisionId,
|
||||
}),
|
||||
dependencies: [...node.dependencies.entries()].map(([binding, dependency]) => ({
|
||||
binding,
|
||||
resourceKey: dependency.key,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { bindingSchema } from "../bindings/index.js";
|
||||
import { compileCapabilityResourceRepository, type ResolvedCapabilityResource } from "./assembly.js";
|
||||
import { createGitCapabilityResolver } from "./git-resolver.js";
|
||||
|
||||
const usage = `usage: quixos-resource-compile --root DIRECTORY --kind interface|package
|
||||
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]
|
||||
|
||||
@@ -41,7 +41,13 @@ const parseArgs = (args: string[]) => {
|
||||
const repository = values.get("--repository");
|
||||
const commit = values.get("--commit")?.toLowerCase();
|
||||
const checkoutRoot = values.get("--checkout-root");
|
||||
if (!rootDirectory || !repository || !commit || !checkoutRoot || (kind !== "interface" && kind !== "package")) {
|
||||
if (
|
||||
!rootDirectory ||
|
||||
!repository ||
|
||||
!commit ||
|
||||
!checkoutRoot ||
|
||||
(kind !== "interface" && kind !== "package" && kind !== "build")
|
||||
) {
|
||||
throw new Error(usage);
|
||||
}
|
||||
if (!/^([0-9a-f]{40}|[0-9a-f]{64})$/.test(commit)) {
|
||||
@@ -67,9 +73,13 @@ const graphEntry = (node: ResolvedCapabilityResource) => ({
|
||||
kind: node.kind,
|
||||
source: node.source,
|
||||
directory: node.directory,
|
||||
resourceId:
|
||||
node.resource.kind === "interface" ? node.resource.revision.interfaceId : node.resource.revision.packageId,
|
||||
revisionId: node.resource.revision.revisionId,
|
||||
...(node.resource.kind === "build"
|
||||
? {}
|
||||
: {
|
||||
resourceId:
|
||||
node.resource.kind === "interface" ? node.resource.revision.interfaceId : node.resource.revision.packageId,
|
||||
revisionId: node.resource.revision.revisionId,
|
||||
}),
|
||||
dependencies: [...node.dependencies.entries()].map(([binding, dependency]) => ({
|
||||
binding,
|
||||
resourceKey: dependency.key,
|
||||
|
||||
@@ -15,7 +15,7 @@ export type StructuralEdit =
|
||||
| { operation: "quixos-pin"; source: { repository: string; commit: string } }
|
||||
| {
|
||||
operation: "dependency";
|
||||
kind: "interface" | "package";
|
||||
kind: "interface" | "package" | "build";
|
||||
name: string;
|
||||
source: { repository: string; commit: string } | null;
|
||||
};
|
||||
@@ -29,6 +29,8 @@ const selectable = new Set([
|
||||
"packageResourceDecl",
|
||||
"atomDecl",
|
||||
"valueMember",
|
||||
"moduleMember",
|
||||
"moduleExport",
|
||||
"relationshipMember",
|
||||
"operationMember",
|
||||
"packageOperationExport",
|
||||
@@ -155,7 +157,7 @@ export const editStructure = (source: string, edit: StructuralEdit): string => {
|
||||
if (edit.operation === "dependency") {
|
||||
const parsed = parseQuixosLockDocument(source);
|
||||
if (!parsed.ok) throw new Error("Cannot scaffold an invalid lockfile");
|
||||
if (!["interface", "package"].includes(edit.kind) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(edit.name))
|
||||
if (!["interface", "package", "build"].includes(edit.kind) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(edit.name))
|
||||
throw new Error("Invalid dependency selector");
|
||||
const parser = new QuixosLockParser(new CommonTokenStream(new QuixosLockLexer(CharStream.fromString(source))));
|
||||
const tree = parser.document();
|
||||
|
||||
@@ -13,7 +13,7 @@ import { parseQuixosLockDocument } from "../resource-lock/index.js";
|
||||
import { withFileLock } from "./file-lock.js";
|
||||
|
||||
export type StructuralRequest = {
|
||||
kind: "workspace" | "interface" | "package";
|
||||
kind: "workspace" | "interface" | "package" | "build";
|
||||
source?: { repository: string; commit: string };
|
||||
resourceRoot?: string;
|
||||
validation?: "syntax" | "resource-graph";
|
||||
@@ -159,10 +159,10 @@ export const planStructure = async (rootPath: string, request: StructuralRequest
|
||||
const resourceRoot = path.join(snapshot.directory, request.resourceRoot ?? "");
|
||||
if (request.kind === "workspace")
|
||||
await compileWorkspaceRepository({ rootDirectory: resourceRoot, resolveResource });
|
||||
else if (["package", "interface"].includes(request.kind) && request.source) {
|
||||
else if (["package", "interface", "build"].includes(request.kind) && request.source) {
|
||||
const compiled = await compileCapabilityResourceRepository({
|
||||
rootDirectory: resourceRoot,
|
||||
kind: request.kind as "package" | "interface",
|
||||
kind: request.kind as "package" | "interface" | "build",
|
||||
source: { resolver: "git", ...request.source },
|
||||
resolveResource,
|
||||
});
|
||||
|
||||
@@ -192,16 +192,16 @@ const main = async () => {
|
||||
}
|
||||
if (command === "check-committed" && !args.includes("--help")) {
|
||||
const [kind, repository, commit, log, ...extra] = args;
|
||||
if (!["workspace", "interface", "package"].includes(kind) || !log || extra.length)
|
||||
throw new Error("usage: quixos-qx check-committed workspace|interface|package REPOSITORY COMMIT LOG_FILE");
|
||||
if (!["workspace", "interface", "package", "build"].includes(kind) || !log || extra.length)
|
||||
throw new Error("usage: quixos-qx check-committed workspace|interface|package|build REPOSITORY COMMIT LOG_FILE");
|
||||
process.stdout.write(
|
||||
`${await buildImmutableCandidate({ repository, commit }, kind as "workspace" | "interface" | "package", log)}\n`,
|
||||
`${await buildImmutableCandidate({ repository, commit }, kind as "workspace" | "interface" | "package" | "build", log)}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!command || command === "--help" || args.includes("--help")) {
|
||||
process.stdout.write(
|
||||
"quixos-qx: author-check, author-contract, converge, worklist, inspect, resources, check-committed, source-baseline, scaffold-package, scaffold-interface, scaffold-function, scaffold-query, scaffold-dependency, scaffold-structure, scaffold-resume, pin-upgrade, parse, lint, format\n" +
|
||||
"quixos-qx: author-check, author-contract, converge, worklist, inspect, resources, check-committed, source-baseline, scaffold-package, scaffold-interface, scaffold-build, scaffold-function, scaffold-query, scaffold-dependency, scaffold-structure, scaffold-resume, pin-upgrade, parse, lint, format\n" +
|
||||
"inspect WORKBENCH [RESOURCE] shows provisional contracts, with explicit historical fallback; never verification evidence.\n" +
|
||||
"resources WORKBENCH lists registered editable repositories. Use qx-workspace for the workspace authoring workflow.\n",
|
||||
);
|
||||
@@ -226,7 +226,7 @@ const main = async () => {
|
||||
if (flags.length && !flags[0].startsWith("--")) {
|
||||
repository = flags.shift();
|
||||
commit = flags.shift();
|
||||
} else if (root && ["interface", "package"].includes(kind) && name) {
|
||||
} else if (root && ["interface", "package", "build"].includes(kind) && name) {
|
||||
const context = await authoringContext(root);
|
||||
const alias = await realpath(path.join(context.workbench, `${kind}s`, name)).catch(() => null);
|
||||
const matches = context.resources.filter(
|
||||
@@ -256,15 +256,17 @@ const main = async () => {
|
||||
}
|
||||
if (
|
||||
!root ||
|
||||
!["interface", "package"].includes(kind) ||
|
||||
!["interface", "package", "build"].includes(kind) ||
|
||||
!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name ?? "") ||
|
||||
!repository ||
|
||||
!commit ||
|
||||
flags.some((flag) => flag !== "--write")
|
||||
)
|
||||
throw new Error("usage: quixos-qx scaffold-dependency ROOT interface|package NAME REPOSITORY COMMIT [--write]");
|
||||
const resourceKind = kind as "package" | "interface";
|
||||
let entrypoint: "workspace" | "package" | "interface" | undefined;
|
||||
throw new Error(
|
||||
"usage: quixos-qx scaffold-dependency ROOT interface|package|build NAME REPOSITORY COMMIT [--write]",
|
||||
);
|
||||
const resourceKind = kind as "package" | "interface" | "build";
|
||||
let entrypoint: "workspace" | "package" | "interface" | "build" | undefined;
|
||||
for (const candidate of ["workspace", "package", "interface"] as const) {
|
||||
try {
|
||||
await readFile(path.join(root, `${candidate}.qx`));
|
||||
@@ -274,7 +276,14 @@ const main = async () => {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||
}
|
||||
}
|
||||
if (!entrypoint) throw new Error("No QX repository entrypoint");
|
||||
if (!entrypoint) {
|
||||
const context = await authoringContext(root);
|
||||
const selected = context.resources.find(
|
||||
(entry) => path.join(context.workbench, entry.directory) === path.resolve(root),
|
||||
);
|
||||
if (selected?.kind !== "build") throw new Error("No QX repository entrypoint");
|
||||
entrypoint = "build";
|
||||
}
|
||||
const lock = await loadQuixosLock(path.join(root, "quixos.lock"));
|
||||
if (!lock.ok) throw new Error("Invalid resource lock");
|
||||
let target = "quixos.lock";
|
||||
@@ -288,7 +297,9 @@ const main = async () => {
|
||||
source: await authorSource(root),
|
||||
validation: "syntax",
|
||||
files: [
|
||||
{ file: `${entrypoint}.qx`, edits: [{ operation: "import", kind: resourceKind, name }] },
|
||||
...(entrypoint !== "build" && resourceKind !== "build"
|
||||
? [{ file: `${entrypoint}.qx`, edits: [{ operation: "import" as const, kind: resourceKind, name }] }]
|
||||
: []),
|
||||
{
|
||||
file: target,
|
||||
edits: [{ operation: "dependency", kind: resourceKind, name, source: { repository, commit } }],
|
||||
@@ -301,26 +312,45 @@ const main = async () => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (command === "scaffold-interface") {
|
||||
if (command === "scaffold-interface" || command === "scaffold-build") {
|
||||
const isBuild = command === "scaffold-build";
|
||||
const [root, specFile, ...flags] = args;
|
||||
if (!root || !specFile || flags.some((flag) => flag !== "--write"))
|
||||
throw new Error("usage: quixos-qx scaffold-interface ROOT SPEC_JSON [--write]");
|
||||
throw new Error("usage: quixos-qx scaffold-interface|scaffold-build ROOT SPEC_JSON [--write]");
|
||||
const spec = (await readSpec(specFile)) as ScaffoldRecipe;
|
||||
if (!spec.name || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.name) || !spec.id || !spec.revision || !spec.tools?.quixos)
|
||||
throw new Error("Interface scaffold requires name, id, revision and Quixos toolchain source");
|
||||
if (
|
||||
!spec.name ||
|
||||
!/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.name) ||
|
||||
(!isBuild && (!spec.id || !spec.revision)) ||
|
||||
!spec.tools?.quixos
|
||||
)
|
||||
throw new Error(
|
||||
isBuild
|
||||
? "Build scaffold requires name and Quixos toolchain source"
|
||||
: "Interface scaffold requires name, id, revision and Quixos toolchain source",
|
||||
);
|
||||
const request: StructuralRequest = {
|
||||
kind: "interface",
|
||||
kind: isBuild ? "build" : "interface",
|
||||
// Like package scaffolding, declaration creation is provisional. Imports
|
||||
// can be attached next; the normal check verifies the complete graph.
|
||||
validation: "syntax",
|
||||
source: spec.source,
|
||||
files: [
|
||||
{
|
||||
file: "interface.qx",
|
||||
create:
|
||||
spec.declaration ??
|
||||
`interface ${spec.name} id ${JSON.stringify(spec.id)} revision ${JSON.stringify(spec.revision)} {\n}\n`,
|
||||
},
|
||||
...(!isBuild
|
||||
? [
|
||||
{
|
||||
file: "interface.qx",
|
||||
create:
|
||||
spec.declaration ??
|
||||
`interface ${spec.name} id ${JSON.stringify(spec.id)} revision ${JSON.stringify(spec.revision)} {\n}\n`,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
file: "README.md",
|
||||
create: `# ${spec.name}\n\nManaged build dependency. Consumers select sources or Nix outputs through context.dependencies.\n`,
|
||||
},
|
||||
]),
|
||||
{
|
||||
file: "quixos.lock",
|
||||
create: formatQuixosLock({
|
||||
|
||||
@@ -83,11 +83,15 @@ const main = async () => {
|
||||
kind: node.kind,
|
||||
source: node.source,
|
||||
directory: node.directory,
|
||||
resourceId:
|
||||
node.resource.kind === "interface"
|
||||
? node.resource.revision.interfaceId
|
||||
: node.resource.revision.packageId,
|
||||
revisionId: node.resource.revision.revisionId,
|
||||
...(node.resource.kind === "build"
|
||||
? {}
|
||||
: {
|
||||
resourceId:
|
||||
node.resource.kind === "interface"
|
||||
? node.resource.revision.interfaceId
|
||||
: node.resource.revision.packageId,
|
||||
revisionId: node.resource.revision.revisionId,
|
||||
}),
|
||||
dependencies: [...node.dependencies.entries()].map(([binding, dependency]) => ({
|
||||
binding,
|
||||
resourceKey: dependency.key,
|
||||
@@ -108,14 +112,15 @@ const main = async () => {
|
||||
}
|
||||
if (options.schemasOut) {
|
||||
const schemas: Record<string, unknown> = {};
|
||||
for (const node of assembled.resources.filter((entry) => entry.kind === "package")) {
|
||||
for (const node of assembled.capabilityResources.filter((entry) => entry.kind === "package")) {
|
||||
const closure = new Map<string, typeof node>();
|
||||
const visit = (entry: typeof node) => {
|
||||
if (closure.has(entry.key)) return;
|
||||
if (closure.has(entry.key) || entry.kind === "build") return;
|
||||
closure.set(entry.key, entry);
|
||||
entry.dependencies.forEach(visit);
|
||||
};
|
||||
visit(node);
|
||||
if (node.resource.kind !== "package") continue;
|
||||
schemas[node.resource.revision.revisionId] = specializeBindingSchema(
|
||||
bindingSchema({ resources: [...closure.values()] }),
|
||||
candidate,
|
||||
|
||||
Reference in New Issue
Block a user