Build composable Camino packages with explicit type and source dependencies

This commit is contained in:
Timothy J. Aveni
2026-09-22 14:42:54 -07:00
parent 7173751f37
commit 82a4542746
30 changed files with 736 additions and 420 deletions
+69 -30
View File
@@ -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])),
};
};
+2 -2
View File
@@ -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)
) {
+1 -1
View File
@@ -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,
),
)
+2 -1
View File
@@ -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,
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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);
+14 -10
View File
@@ -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,
+15 -5
View File
@@ -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,
+4 -2
View File
@@ -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();
+3 -3
View File
@@ -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,
});
+52 -22
View File
@@ -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({
+12 -7
View File
@@ -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,