Build Camino packages from explicit contract inputs

Project source-free capability contracts and keep exact source provenance in package assembly. Reuse compiled groups across unrelated graph and dependency implementation edits. Keep React public inputs separate from workspace query lowering and retain explicit type-companion dependencies.

Allow independent execution-package checks, scope resource compilation to declared dependencies, and memoize source closure traversal. Document generic-specialization limits and measured Reader rebuild behavior.

Verified protocol, artifact, package, React, and managed activation checks; retained-Git independent/workspace artifact equality and incompatible type dependency rejection; nix fmt -- --check and lint. One-off Reader measurements recompiled 1 of 13 groups for a child edit and 0 for a workspace QX default edit.
This commit is contained in:
Timothy J. Aveni
2026-09-30 22:55:07 -07:00
parent b95cd149c6
commit e8d19d2bb7
23 changed files with 726 additions and 227 deletions
+3
View File
@@ -80,6 +80,9 @@
mkdir -p "$out" mkdir -p "$out"
install -Dm644 nix/checked-package.nix "$out/share/checked-package.nix" install -Dm644 nix/checked-package.nix "$out/share/checked-package.nix"
install -Dm644 nix/checked-artifacts.nix "$out/share/checked-artifacts.nix" install -Dm644 nix/checked-artifacts.nix "$out/share/checked-artifacts.nix"
install -Dm644 nix/package-context.nix "$out/share/package-context.nix"
install -Dm644 nix/package-context.mjs "$out/share/package-context.mjs"
install -Dm644 nix/check-package.mjs "$out/share/check-package.mjs"
install -Dm644 nix/check-execution.mjs "$out/share/check-execution.mjs" install -Dm644 nix/check-execution.mjs "$out/share/check-execution.mjs"
substitute nix/checked-candidate.nix "$out/share/checked-candidate.nix" \ substitute nix/checked-candidate.nix "$out/share/checked-candidate.nix" \
--replace-fail '@nixpkgs@' '${pkgs.path}' --replace-fail '@nixpkgs@' '${pkgs.path}'
+41
View File
@@ -0,0 +1,41 @@
// An independent package check witnesses its contract and bytes, without
// inventing a workspace or granting installation/activation authority.
import { readFile, writeFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import { pathToFileURL } from "node:url";
import { join } from "node:path";
const [compilerPath, schemaPath, checksPath, output] = process.argv.slice(2);
const compiler = await import(pathToFileURL(compilerPath));
const schema = JSON.parse(await readFile(schemaPath, "utf8"));
const checks = JSON.parse(await readFile(checksPath, "utf8"));
const generator = createHash("sha256")
.update(await readFile(compilerPath))
.digest("hex");
const evidence = [];
for (const check of checks) {
const pkg = schema.packages.find((pkg) => pkg.revisionId === check.packageRevisionId);
if (!pkg) throw Error("Unknown checked package");
const contract = compiler.packageBuildContract(schema, pkg.revisionId);
const sourceDigest = compiler.hexDigest({ revision: pkg.revisionId, source: pkg.source });
const manifest = compiler.parsePackageArtifacts(
JSON.parse(await readFile(join(check.artifactPath, "share/quixos/package-artifacts.json"), "utf8")),
);
compiler.checkPackageCoverage(
{ ...contract.definition.package, bindingDigest: contract.digest, sourceDigest },
manifest,
);
const receipt = JSON.parse(await readFile(join(check.artifactPath, "share/quixos/build-receipt.json"), "utf8"));
const expected = compiler.expectedPackageReceipt(contract, manifest, sourceDigest, generator);
if (compiler.hexDigest(receipt) !== compiler.hexDigest(expected.receipt))
throw Error("Invalid package build receipt");
const read = await compiler.artifactFileReader({ [pkg.revisionId]: check.artifactPath });
await compiler.checkArtifactFiles(manifest, read);
for (const [file, bytes] of expected.sources)
if (!Buffer.from(bytes).equals(await read(pkg.revisionId, file, bytes.length)))
throw Error("Modified generated binding " + file);
evidence.push({ ...check, contract: contract.digest, sourceDigest, receipt });
}
await writeFile(
join(output, "package-checks.json"),
JSON.stringify({ codec: "camino-package-checks-v1", generator, packages: evidence }),
);
+15 -7
View File
@@ -24,18 +24,26 @@ let
package = builtins.getFlake ( package = builtins.getFlake (
"path:" + builtins.unsafeDiscardStringContext (toString packageSource) "path:" + builtins.unsafeDiscardStringContext (toString packageSource)
); );
checked = pkgs = import package.inputs.nixpkgs { inherit system; };
package.quixosPackages.${system}.checkedArtifacts buildContext = import ./package-context.nix {
or (throw "Package ${packageRevisionId} lacks checkedArtifacts for the portable execution world."); inherit pkgs packageRevisionId;
in protocol = builtins.storePath generator;
checked {
inherit packageRevisionId;
plan = builtins.path { plan = builtins.path {
path = /. + plan; path = /. + plan;
name = "candidate-execution-plan.json"; name = "candidate-execution-plan.json";
}; };
};
checked =
package.quixosPackages.${system}.checkedArtifacts
or (throw "Package ${packageRevisionId} lacks checkedArtifacts for the portable execution world.");
in
checked (
buildContext
// {
inherit packageRevisionId;
generator = builtins.storePath generator; generator = builtins.storePath generator;
core = builtins.storePath core; core = builtins.storePath core;
sharedRuntime = builtins.storePath sharedRuntime; sharedRuntime = builtins.storePath sharedRuntime;
typescript = builtins.storePath typescript; typescript = builtins.storePath typescript;
} }
)
+111 -55
View File
@@ -20,26 +20,45 @@ let
ref = "refs/tags/quixos-reachability/${source.commit}"; ref = "refs/tags/quixos-reachability/${source.commit}";
shallow = true; shallow = true;
}; };
load = # Memoize by exact source identity. A diamond-shaped dependency graph must
ancestors: source: # not recursively evaluate every path through the same imported subtree.
if builtins.elem (sourceKey source) ancestors then loadSource =
throw "Source dependency cycle at ${sourceKey source}" source:
else if builtins.length ancestors >= 100 then
throw "Source dependency depth exceeds 100"
else
let let
directory = fetch source; directory = fetch source;
lockFile = pkgs.runCommand "qx-source-lock.json" { } '' lockFile = pkgs.runCommand "qx-source-lock.json" { } ''
${protocol}/bin/quixos-lock-check ${directory}/quixos.lock > "$out" ${protocol}/bin/quixos-lock-check ${directory}/quixos.lock > "$out"
''; '';
lock = builtins.fromJSON (builtins.readFile lockFile); lock = builtins.fromJSON (builtins.readFile lockFile);
children = map (
entry:
load (ancestors ++ [ (sourceKey source) ]) (entry.source // { inherit (entry) kind binding; })
) lock.resources;
in in
source // { inherit directory children lock; }; source // { inherit directory lock; };
root = load [ ] { inherit kind repository commit; }; sourceEntry = source: {
key = sourceKey source;
value = loadSource source;
};
sourceGraph = builtins.genericClosure {
startSet = [ (sourceEntry { inherit kind repository commit; }) ];
operator =
entry:
map (
resource: sourceEntry (resource.source // { inherit (resource) kind; })
) entry.value.lock.resources;
};
nodeTable = builtins.listToAttrs (
map (entry: {
name = entry.key;
value = entry.value // {
children = map (
resource:
nodeTable.${sourceKey (resource.source // { inherit (resource) kind; })}
// {
inherit (resource) binding;
}
) entry.value.lock.resources;
};
}) sourceGraph
);
root = nodeTable.${sourceKey { inherit kind repository commit; }};
configuration = configuration =
if builtins.pathExists (root.directory + "/quixos.check.json") then if builtins.pathExists (root.directory + "/quixos.check.json") then
builtins.fromJSON (builtins.readFile (root.directory + "/quixos.check.json")) builtins.fromJSON (builtins.readFile (root.directory + "/quixos.check.json"))
@@ -50,24 +69,28 @@ let
"git+${root.lock.quixos.repository}?rev=${root.lock.quixos.commit}&ref=refs/tags/quixos-reachability/${root.lock.quixos.commit}" "git+${root.lock.quixos.repository}?rev=${root.lock.quixos.commit}&ref=refs/tags/quixos-reachability/${root.lock.quixos.commit}"
); );
runtimePackages = runtime.packages.${system}; runtimePackages = runtime.packages.${system};
flatten = node: [ node ] ++ pkgs.lib.concatMap flatten node.children; resourceClosure =
capabilityFlatten = includeBuild: node:
node: map (entry: entry.value) (
if node.kind == "build" then builtins.genericClosure {
[ ] startSet = pkgs.lib.optional (includeBuild || node.kind != "build") {
else key = sourceKey node;
[ node ] ++ pkgs.lib.concatMap capabilityFlatten node.children;
nodes = builtins.attrValues (
builtins.listToAttrs (
map (node: {
name = sourceKey node;
value = node; value = node;
}) (flatten root) };
) operator =
entry:
map (child: {
key = sourceKey child;
value = child;
}) (builtins.filter (child: includeBuild || child.kind != "build") entry.value.children);
}
); );
capabilityFlatten = resourceClosure false;
# Frontends are an explicit build choice. The capability assembler itself # Frontends are an explicit build choice. The capability assembler itself
# neither discovers React contracts nor loads executable package code. # neither discovers React contracts nor loads executable package code.
queryFrontends = pkgs.lib.unique ( queryFrontends =
node:
pkgs.lib.unique (
pkgs.lib.concatMap ( pkgs.lib.concatMap (
node: node:
let let
@@ -77,16 +100,19 @@ let
(builtins.fromJSON (builtins.readFile config)).queryFrontends or [ ] (builtins.fromJSON (builtins.readFile config)).queryFrontends or [ ]
else else
[ ] [ ]
) (capabilityFlatten root) ) (capabilityFlatten node)
); );
queryGeneratorFlags = queryGeneratorFlags =
if queryFrontends == [ ] then node:
if queryFrontends node == [ ] then
"" ""
else if queryFrontends == [ "react" ] then else if queryFrontends node == [ "react" ] then
"--query-generator ${protocol}/libexec/quixos-protocol/react-query-generator.mjs" "--query-generator ${protocol}/libexec/quixos-protocol/react-query-generator.mjs"
else else
throw "Unsupported query frontend selection: ${builtins.toJSON queryFrontends}"; throw "Unsupported query frontend selection: ${builtins.toJSON (queryFrontends node)}";
snapshots = pkgs.writeText "qx-nix-source-graph.json" ( snapshots =
node:
pkgs.writeText "qx-nix-source-graph.json" (
builtins.toJSON { builtins.toJSON {
resources = map (node: { resources = map (node: {
inherit (node) inherit (node)
@@ -95,7 +121,7 @@ let
commit commit
directory directory
; ;
}) (builtins.filter (node: node.kind != "workspace") nodes); }) (builtins.filter (entry: entry.kind != "workspace") (resourceClosure true node));
} }
); );
compile = compile =
@@ -106,19 +132,22 @@ let
if node.kind == "workspace" then if node.kind == "workspace" then
'' ''
${protocol}/bin/quixos-workspace-compile --root ${node.directory} \ ${protocol}/bin/quixos-workspace-compile --root ${node.directory} \
${queryGeneratorFlags} \ ${queryGeneratorFlags node} \
--source-root-commit ${pkgs.lib.escapeShellArg node.commit} \ --source-root-commit ${pkgs.lib.escapeShellArg node.commit} \
--checkout-root "$TMPDIR/checkouts" --snapshot-map ${snapshots} \ --checkout-root "$TMPDIR/checkouts" --snapshot-map ${snapshots node} \
--graph-out "$out/graph.json" --schemas-out "$out/package-schemas.json" \ --graph-out "$out/graph.json" --schemas-out "$out/package-schemas.json" \
${pkgs.lib.optionalString execution ''--execution-plan-out "$out/execution-plan.json"''} > "$out/candidate.json" ${
pkgs.lib.optionalString (execution && kind == "workspace")
''--execution-plan-out "$out/execution-plan.json" --build-contexts-out "$out/build-contexts.json"''
} > "$out/candidate.json"
'' ''
else else
'' ''
${protocol}/bin/quixos-resource-compile --root ${node.directory} \ ${protocol}/bin/quixos-resource-compile --root ${node.directory} \
${queryGeneratorFlags} \ ${queryGeneratorFlags node} \
--kind ${node.kind} --repository ${pkgs.lib.escapeShellArg node.repository} \ --kind ${node.kind} --repository ${pkgs.lib.escapeShellArg node.repository} \
--commit ${pkgs.lib.escapeShellArg node.commit} \ --commit ${pkgs.lib.escapeShellArg node.commit} \
--checkout-root "$TMPDIR/checkouts" --snapshot-map ${snapshots} --snapshot-only true \ --checkout-root "$TMPDIR/checkouts" --snapshot-map ${snapshots node} --snapshot-only true \
--graph-out "$out/graph.json" --schema-out "$out/bindings.json" > "$out/candidate.json" --graph-out "$out/graph.json" --schema-out "$out/bindings.json" > "$out/candidate.json"
'' ''
} }
@@ -137,7 +166,7 @@ let
}) (builtins.filter (dependency: dependency.kind == "build") node.children) }) (builtins.filter (dependency: dependency.kind == "build") node.children)
); );
reactContracts = reactContracts =
node: node: moduleInterfaces:
let let
interfaces = builtins.attrValues ( interfaces = builtins.attrValues (
builtins.listToAttrs ( builtins.listToAttrs (
@@ -159,6 +188,17 @@ let
in in
if build == null then if build == null then
[ ] [ ]
else if
!(builtins.any (
entry:
let
resource = builtins.fromJSON (builtins.readFile "${compile dependency}/candidate.json");
in
entry.revisionId == resource.revision.revisionId
|| (entry.definitionId or null) == resource.revision.revisionId
) moduleInterfaces)
then
[ ]
else else
[ [
(build { (build {
@@ -174,15 +214,26 @@ let
node: node:
let let
compiled = compile node; compiled = compile node;
candidate = builtins.fromJSON (builtins.readFile "${compiled}/candidate.json"); candidate =
componentEntries = if execution && kind == "workspace" then
if execution then {
(pkgs.lib.findFirst ( revision =
entry: entry.revision == candidate.revision.revisionId pkgs.lib.findFirst
) { } (builtins.fromJSON (builtins.readFile "${contract}/execution-plan.json")).definition.packages) (pkg: pkg.source.repository == node.repository && pkg.source.commit == node.commit)
.components or [ ] (throw "Source package missing from workspace compilation: ${node.repository}")
(builtins.fromJSON (builtins.readFile "${contract}/execution-plan.json"))
.definition.bindingSchema.packages;
}
else else
[ ]; builtins.fromJSON (builtins.readFile "${compiled}/candidate.json");
buildContext = import ./package-context.nix {
inherit pkgs protocol;
contexts = if kind == "workspace" then "${contract}/build-contexts.json" else null;
plan = "${compiled}/bindings.json";
packageRevisionId = candidate.revision.revisionId;
};
buildContract = builtins.fromJSON (builtins.readFile buildContext.buildContract);
componentEntries = if execution then buildContract.definition.package.components or [ ] else [ ];
package = builtins.getFlake ( package = builtins.getFlake (
"git+${node.repository}?rev=${node.commit}&ref=refs/tags/quixos-reachability/${node.commit}" "git+${node.repository}?rev=${node.commit}&ref=refs/tags/quixos-reachability/${node.commit}"
); );
@@ -195,8 +246,8 @@ let
or (throw "Package ${node.repository} lacks checkedServer; use the supported package scaffold."); or (throw "Package ${node.repository} lacks checkedServer; use the supported package scaffold.");
artifact = checked ( artifact = checked (
if execution then if execution then
{ buildContext
plan = "${contract}/execution-plan.json"; // {
generator = protocol; generator = protocol;
core = runtimePackages.camino-replica-core; core = runtimePackages.camino-replica-core;
sharedRuntime = runtimePackages.camino-shared-runtime; sharedRuntime = runtimePackages.camino-shared-runtime;
@@ -208,7 +259,7 @@ let
react = { react = {
sdk = runtimePackages.camino-react; sdk = runtimePackages.camino-react;
runtime = runtimePackages.camino-react-runtime; runtime = runtimePackages.camino-react-runtime;
contracts = reactContracts node; contracts = reactContracts node buildContract.definition.moduleInterfaces;
}; };
} }
else else
@@ -236,7 +287,9 @@ let
[ ] [ ]
else else
map checkPackage ( map checkPackage (
builtins.filter (node: node.kind == "package") ( builtins.filter
(node: node.kind == "package" && (kind == "workspace" || sourceKey node == sourceKey root))
(
builtins.attrValues ( builtins.attrValues (
builtins.listToAttrs ( builtins.listToAttrs (
map (node: { map (node: {
@@ -254,8 +307,6 @@ let
) )
); );
in in
assert pkgs.lib.assertMsg (!execution || kind == "workspace")
"Execution packages must be checked in their complete workspace; run qx-workspace check from the workspace root.";
pkgs.runCommand (if contractOnly then "qx-contract" else "qx-checked-candidate") { } '' pkgs.runCommand (if contractOnly then "qx-contract" else "qx-checked-candidate") { } ''
mkdir -p "$out" mkdir -p "$out"
cp ${ cp ${
@@ -264,7 +315,7 @@ pkgs.runCommand (if contractOnly then "qx-contract" else "qx-checked-candidate")
cp ${contract}/candidate.json "$out/candidate.json" cp ${contract}/candidate.json "$out/candidate.json"
cp ${contract}/graph.json "$out/graph.json" cp ${contract}/graph.json "$out/graph.json"
cp ${manifest} "$out/checks.json" cp ${manifest} "$out/checks.json"
${pkgs.lib.optionalString execution '' ${pkgs.lib.optionalString (execution && kind == "workspace") ''
${ ${
if builtins.pathExists "${root.directory}/quixos.entry.json" then if builtins.pathExists "${root.directory}/quixos.entry.json" then
'' ''
@@ -291,7 +342,12 @@ pkgs.runCommand (if contractOnly then "qx-contract" else "qx-checked-candidate")
${pkgs.lib.optionalString ( ${pkgs.lib.optionalString (
kind != "workspace" kind != "workspace"
) ''cp ${contract}/bindings.json "$out/bindings.json"''} ) ''cp ${contract}/bindings.json "$out/bindings.json"''}
${pkgs.lib.optionalString (kind == "package") '' ${pkgs.lib.optionalString (execution && kind == "package" && !contractOnly) ''
${pkgs.nodejs_24}/bin/node ${./check-package.mjs} \
${protocol}/libexec/quixos-protocol/execution-world.mjs ${contract}/bindings.json \
"$out/checks.json" "$out"
''}
${pkgs.lib.optionalString (kind == "package" && !execution) ''
${protocol}/bin/quixos-codegen-ts ${contract}/bindings.json \ ${protocol}/bin/quixos-codegen-ts ${contract}/bindings.json \
${pkgs.lib.escapeShellArg (builtins.fromJSON (builtins.readFile "${contract}/candidate.json")).revision.revisionId} \ ${pkgs.lib.escapeShellArg (builtins.fromJSON (builtins.readFile "${contract}/candidate.json")).revision.revisionId} \
"$out/bindings.ts" ${bindingOptions} "$out/bindings.ts" ${bindingOptions}
+15
View File
@@ -0,0 +1,15 @@
import fs from "node:fs/promises";
import { pathToFileURL } from "node:url";
const compiler = await import(pathToFileURL(process.argv[2]));
const input = JSON.parse(await fs.readFile(process.argv[3], "utf8"));
const revision = process.argv[4];
const schema = input.definition?.bindingSchema ?? input;
if (input.definition && compiler.hexDigest(input.definition) !== input.digest) throw Error("Invalid execution plan");
const pkg = schema.packages.find((entry) => entry.revisionId === revision);
if (!pkg) throw Error("Unknown build package " + revision);
process.stdout.write(
JSON.stringify({
contract: compiler.packageBuildContract(schema, revision),
sourceDigest: compiler.hexDigest({ revision, source: pkg.source }),
}),
);
+30
View File
@@ -0,0 +1,30 @@
# Workspace composition may reevaluate this projection after any graph edit.
# Only the canonical, source-free value becomes a package compilation input.
{
pkgs,
protocol,
plan ? null,
contexts ? null,
packageRevisionId,
}:
let
projected = pkgs.runCommand "camino-package-context.json" { } ''
${pkgs.nodejs_24}/bin/node ${./package-context.mjs} \
${protocol}/libexec/quixos-protocol/execution-world.mjs ${plan} \
${pkgs.lib.escapeShellArg packageRevisionId} > "$out"
'';
context =
if contexts == null then
builtins.fromJSON (builtins.readFile projected)
else
(builtins.fromJSON (builtins.readFile contexts)).${packageRevisionId};
in
{
# This is typed semantic data (nominal IDs, types and generated source), never
# Nix artifact paths. Remove the projection derivation's string context, not
# any code/toolchain dependency. Those are separate explicit helper arguments.
buildContract = builtins.toFile "camino-package-contract.json" (
builtins.unsafeDiscardStringContext (builtins.toJSON context.contract)
);
sourceDigest = builtins.unsafeDiscardStringContext context.sourceDigest;
}
+8
View File
@@ -9,6 +9,14 @@ import type { CompiledCapabilityResourceRepository } from "../capability-languag
import { genericImplementationType } from "./generics.js"; import { genericImplementationType } from "./generics.js";
import { capabilityId } from "../capability-model/types.js"; import { capabilityId } from "../capability-model/types.js";
/** Source-free package compilation inputs. Nominal references do not import
* implementations or concrete workspace conformances. */
export type BindingContractSchema = {
format: "quixos-bindings";
version: 1;
interfaces: Omit<InterfaceRevision, "source">[];
packages: Pick<PackageRevision, "revisionId" | "exports" | "genericExports">[];
};
/** Portable generator input. New backends consume this instead of the parser or TS runtime. */ /** Portable generator input. New backends consume this instead of the parser or TS runtime. */
export type BindingSchema = { export type BindingSchema = {
format: "quixos-bindings"; format: "quixos-bindings";
+5 -5
View File
@@ -1,5 +1,5 @@
import { portableReflectionType } from "../execution-world/reflection.js"; import { portableReflectionType } from "../execution-world/reflection.js";
import type { BindingSchema } from "./index.js"; import type { BindingContractSchema } from "./index.js";
import type { import type {
ValueType, ValueType,
ObjectExpectation, ObjectExpectation,
@@ -121,7 +121,7 @@ interface Method {
explicitInput?: boolean; explicitInput?: boolean;
} }
function methods( function methods(
schema: BindingSchema, schema: BindingContractSchema,
entry: PackageExport, entry: PackageExport,
port: DependencyPort, port: DependencyPort,
target: string, target: string,
@@ -247,21 +247,21 @@ export interface PortableBindingOptions {
} }
/** Generate one target's handlers. The same source signatures are used in browser/server builds. */ /** Generate one target's handlers. The same source signatures are used in browser/server builds. */
export function generatePortableTypeScriptBindings( export function generatePortableTypeScriptBindings(
schema: BindingSchema, schema: BindingContractSchema,
revision: string, revision: string,
options: PortableBindingOptions, options: PortableBindingOptions,
): string { ): string {
return generateBindings(schema, revision, options, false); return generateBindings(schema, revision, options, false);
} }
export function generateRemoteTypeScriptBindings( export function generateRemoteTypeScriptBindings(
schema: BindingSchema, schema: BindingContractSchema,
revision: string, revision: string,
options: { runtimeModule?: string } = {}, options: { runtimeModule?: string } = {},
): string { ): string {
return generateBindings(schema, revision, { ...options, target: "server" }, true); return generateBindings(schema, revision, { ...options, target: "server" }, true);
} }
function generateBindings( function generateBindings(
schema: BindingSchema, schema: BindingContractSchema,
revision: string, revision: string,
options: PortableBindingOptions, options: PortableBindingOptions,
remote: boolean, remote: boolean,
@@ -80,9 +80,8 @@ export async function executionScaffold(
devShells.default = (import (helpers + "/quixos-package-helpers.nix")).mkCaminoYarnShell { inherit pkgs; }; devShells.default = (import (helpers + "/quixos-package-helpers.nix")).mkCaminoYarnShell { inherit pkgs; };
quixosPackages.checkedArtifacts = context: quixosPackages.checkedArtifacts = context:
let let
plan = builtins.fromJSON (builtins.readFile context.plan); contract = builtins.fromJSON (builtins.readFile context.buildContract);
package = pkgs.lib.findFirst (entry: entry.revision == context.packageRevisionId) package = contract.definition.package;
(throw "Package missing from checked plan") plan.definition.packages;
portable = builtins.filter (entry: entry.execution.kind != "remote") package.exports; portable = builtins.filter (entry: entry.execution.kind != "remote") package.exports;
remote = builtins.filter (entry: entry.execution.kind == "remote") package.exports; remote = builtins.filter (entry: entry.execution.kind == "remote") package.exports;
modules = import ./components.nix; modules = import ./components.nix;
@@ -107,7 +106,7 @@ in camino.assembleCaminoPackage {
); );
create( create(
"README.md", "README.md",
`# ${spec.name}\n\nThis execution package exports checked browser/server modules and a process service.\nUse scaffold function with an explicit execution contract to add a typed handler.\nSnapshot/transaction targets choose browser.ts and/or server.ts; remote handlers\nuse remote.ts. Shared helpers are ordinary imports. Generated bindings are build\noutputs, not authored source. The coordinator supplies the checked workspace plan\nand runtime dependencies to checkedArtifacts.\n\nRun qx-workspace check from the workspace root to compile exact source and build\nchecked artifacts. Activate the resulting report separately through the workspace\nhost. A source-only structural check is not runtime qualification.\n`, `# ${spec.name}\n\nThis execution package exports checked browser/server modules and a process service.\nUse scaffold function with an explicit execution contract to add a typed handler.\nSnapshot/transaction targets choose browser.ts and/or server.ts; remote handlers\nuse remote.ts. Shared helpers are ordinary imports. Generated bindings are build\noutputs, not authored source. The Nix composition supplies the canonical package contract\nand explicit runtime dependencies to checkedArtifacts.\n\nRun qx-workspace check from this package to build it independently, or from a\nworkspace to also check installed bindings. Activate the resulting report separately through the workspace\nhost. A source-only structural check is not runtime qualification.\n`,
); );
} else if (command === "function") { } else if (command === "function") {
if (!spec.declaration) if (!spec.declaration)
+2 -2
View File
@@ -168,8 +168,8 @@ export const planStructure = async (rootPath: string, request: StructuralRequest
}); });
if (compiled.resource.kind === "package") { if (compiled.resource.kind === "package") {
const configuration = JSON.parse(await fs.readFile(path.join(resourceRoot, "quixos.check.json"), "utf8")); const configuration = JSON.parse(await fs.readFile(path.join(resourceRoot, "quixos.check.json"), "utf8"));
// Execution bindings require the complete workspace plan and are owned // Execution bindings are immutable build outputs owned by checkedArtifacts,
// by checkedArtifacts, not a package-only structural preview. // not source edits produced by a structural preview.
const artifacts = const artifacts =
configuration.backend === "execution" configuration.backend === "execution"
? [] ? []
+8 -7
View File
@@ -5,7 +5,7 @@ import process from "node:process";
import { compileWorkspaceRepository } from "./assembly.js"; import { compileWorkspaceRepository } from "./assembly.js";
import { createGitCapabilityResolver } from "./git-resolver.js"; import { createGitCapabilityResolver } from "./git-resolver.js";
import { loadQueryGenerator } from "./query-generator.js"; import { loadQueryGenerator } from "./query-generator.js";
import { compileExecutionPlan } from "../execution-world/index.js"; import { compileExecutionPlan, packageBuildContexts } from "../execution-world/index.js";
import { bindingSchema, specializeBindingSchema } from "../bindings/index.js"; import { bindingSchema, specializeBindingSchema } from "../bindings/index.js";
import { import {
planEvolution, planEvolution,
@@ -17,7 +17,7 @@ import {
const usage = `usage: quixos-workspace-compile --root DIRECTORY --checkout-root DIRECTORY const usage = `usage: quixos-workspace-compile --root DIRECTORY --checkout-root DIRECTORY
[--snapshot-map PATH] [--graph-out PATH] [--workspace-id ID] [--workspace-revision-id ID] [--snapshot-map PATH] [--graph-out PATH] [--workspace-id ID] [--workspace-revision-id ID]
[--source-root-commit GIT_REV] [--baseline PLAN_JSON] [--evolution-out PATH] [--source-root-commit GIT_REV] [--baseline PLAN_JSON] [--evolution-out PATH]
[--reviews REVIEW_JSON] [--schemas-out PATH] [--execution-plan-out PATH] [--reviews REVIEW_JSON] [--schemas-out PATH] [--execution-plan-out PATH] [--build-contexts-out PATH]
[--query-generator BUILD_MODULE_PATH] [--query-generator BUILD_MODULE_PATH]
Resolves a workspace's recursive resource-lock graph, clones every exact Resolves a workspace's recursive resource-lock graph, clones every exact
@@ -41,6 +41,7 @@ const parseArgs = (args: string[]) => {
graphOut: values.get("--graph-out"), graphOut: values.get("--graph-out"),
schemasOut: values.get("--schemas-out"), schemasOut: values.get("--schemas-out"),
executionPlanOut: values.get("--execution-plan-out"), executionPlanOut: values.get("--execution-plan-out"),
buildContextsOut: values.get("--build-contexts-out"),
snapshotMap: values.get("--snapshot-map"), snapshotMap: values.get("--snapshot-map"),
workspaceId: values.get("--workspace-id"), workspaceId: values.get("--workspace-id"),
workspaceRevisionId: values.get("--workspace-revision-id"), workspaceRevisionId: values.get("--workspace-revision-id"),
@@ -108,11 +109,11 @@ const main = async () => {
); );
} }
const candidate = { ...assembled.workspace, executionContracts: runtimeContracts(assembled.workspace) }; const candidate = { ...assembled.workspace, executionContracts: runtimeContracts(assembled.workspace) };
if (options.executionPlanOut) { if (options.executionPlanOut || options.buildContextsOut) {
await writeFile( const plan = compileExecutionPlan(assembled.workspace);
options.executionPlanOut, if (options.executionPlanOut) await writeFile(options.executionPlanOut, `${JSON.stringify(plan, null, 2)}\n`);
`${JSON.stringify(compileExecutionPlan(assembled.workspace), null, 2)}\n`, if (options.buildContextsOut)
); await writeFile(options.buildContextsOut, JSON.stringify(packageBuildContexts(plan.definition.bindingSchema)));
} }
if (options.schemasOut) { if (options.schemasOut) {
const schemas: Record<string, unknown> = {}; const schemas: Record<string, unknown> = {};
+33 -25
View File
@@ -20,15 +20,19 @@ export interface ArtifactImplementation {
files: ArtifactFile[]; files: ArtifactFile[];
contentDigest: string; contentDigest: string;
} }
export interface PackageArtifacts { export interface ArtifactGroup {
schemaVersion: 1; schemaVersion: 1;
packageRevisionId: string; packageRevisionId: string;
bindingDigest: string; bindingDigest: string;
sourceDigest: string;
versions: ExecutionPlan["definition"]["versions"]; versions: ExecutionPlan["definition"]["versions"];
implementations: ArtifactImplementation[]; implementations: ArtifactImplementation[];
components?: ComponentArtifact[]; components?: ComponentArtifact[];
} }
/** Installation evidence binds a reusable group to the selected exact source.
* Compilation receipts never claim that a cache hit was built at a new pin. */
export interface PackageArtifacts extends ArtifactGroup {
sourceDigest: string;
}
export const componentSharedModules = [ export const componentSharedModules = [
"react", "react",
"react-dom", "react-dom",
@@ -199,11 +203,11 @@ function parseComponent(value: unknown, index: number): ComponentArtifact {
if (componentArtifactDigest(body) !== contentDigest) fail(path, "Component metadata digest mismatch"); if (componentArtifactDigest(body) !== contentDigest) fail(path, "Component metadata digest mismatch");
return component; return component;
} }
export function parsePackageArtifacts(value: unknown): PackageArtifacts { export function parseArtifactGroup(value: unknown): ArtifactGroup {
const record = object(value, "manifest"); const record = object(value, "manifest");
keys( keys(
record, record,
["schemaVersion", "packageRevisionId", "bindingDigest", "sourceDigest", "versions", "implementations"], ["schemaVersion", "packageRevisionId", "bindingDigest", "versions", "implementations"],
["components"], ["components"],
"manifest", "manifest",
); );
@@ -266,13 +270,19 @@ export function parsePackageArtifacts(value: unknown): PackageArtifacts {
schemaVersion: 1, schemaVersion: 1,
packageRevisionId: text(record.packageRevisionId, "packageRevisionId"), packageRevisionId: text(record.packageRevisionId, "packageRevisionId"),
bindingDigest: digest(record.bindingDigest, "bindingDigest"), bindingDigest: digest(record.bindingDigest, "bindingDigest"),
sourceDigest: digest(record.sourceDigest, "sourceDigest"),
versions: versions as unknown as PackageArtifacts["versions"], versions: versions as unknown as PackageArtifacts["versions"],
implementations, implementations,
...(components ? { components } : {}), ...(components ? { components } : {}),
}; };
} }
function checkCoverage(expected: PlanPackage, manifest: PackageArtifacts): void { export function parsePackageArtifacts(value: unknown): PackageArtifacts {
const { sourceDigest, ...group } = object(value, "manifest");
return { ...parseArtifactGroup(group), sourceDigest: digest(sourceDigest, "sourceDigest") };
}
export function checkPackageCoverage(
expected: Pick<PlanPackage, "revision" | "modules" | "components" | "exports" | "bindingDigest" | "sourceDigest">,
manifest: PackageArtifacts,
): void {
for (const module of expected.modules ?? []) for (const module of expected.modules ?? [])
if (!expected.components?.some((component) => component.id === module.id)) if (!expected.components?.some((component) => component.id === module.id))
fail(module.id, `No artifact consumer for module contract ${module.contract} and its subject`); fail(module.id, `No artifact consumer for module contract ${module.contract} and its subject`);
@@ -315,6 +325,21 @@ function checkCoverage(expected: PlanPackage, manifest: PackageArtifacts): void
} }
} }
export type ArtifactReader = (packageRevisionId: string, path: string, maxBytes: number) => Promise<Uint8Array>; export type ArtifactReader = (packageRevisionId: string, path: string, maxBytes: number) => Promise<Uint8Array>;
/** Shared by standalone package checks and complete world installation. */
export async function checkArtifactFiles(manifest: ArtifactGroup, read: ArtifactReader): Promise<void> {
const files = new Map<string, ArtifactFile>();
for (const artifact of [...manifest.implementations, ...(manifest.components ?? [])])
for (const file of artifact.files) {
const existing = files.get(file.path);
if (existing && canonicalJson(existing) !== canonicalJson(file)) fail(file.path, "Conflicting file descriptors");
files.set(file.path, file);
}
for (const file of files.values()) {
const bytes = await read(manifest.packageRevisionId, file.path, file.bytes);
if (bytes.byteLength !== file.bytes || createHash("sha256").update(bytes).digest("hex") !== file.digest)
fail(file.path, "Artifact bytes do not match the checked content digest");
}
}
/** Caller supplies a reader confined to the resolved immutable package output, including symlink checks. */ /** Caller supplies a reader confined to the resolved immutable package output, including symlink checks. */
export async function checkExecutionWorld( export async function checkExecutionWorld(
plan: ExecutionPlan, plan: ExecutionPlan,
@@ -335,17 +360,11 @@ export async function checkExecutionWorld(
const manifest = const manifest =
manifests.find((m) => m.packageRevisionId === expected.revision) ?? manifests.find((m) => m.packageRevisionId === expected.revision) ??
fail(expected.revision, "Missing package artifact"); fail(expected.revision, "Missing package artifact");
checkCoverage(expected, manifest); checkPackageCoverage(expected, manifest);
if (canonicalJson(manifest.versions) !== canonicalJson(plan.definition.versions)) if (canonicalJson(manifest.versions) !== canonicalJson(plan.definition.versions))
fail(expected.revision, "Incompatible runtime codecs"); fail(expected.revision, "Incompatible runtime codecs");
const files = new Map<string, ArtifactFile>(); await checkArtifactFiles(manifest, read);
for (const implementation of manifest.implementations) { for (const implementation of manifest.implementations) {
for (const file of implementation.files) {
const existing = files.get(file.path);
if (existing && canonicalJson(existing) !== canonicalJson(file))
fail(file.path, "Conflicting file descriptors");
files.set(file.path, file);
}
artifacts.push({ artifacts.push({
binding: JSON.stringify([expected.revision, implementation.id]), binding: JSON.stringify([expected.revision, implementation.id]),
target: implementation.target, target: implementation.target,
@@ -353,23 +372,12 @@ export async function checkExecutionWorld(
}); });
} }
for (const component of manifest.components ?? []) { for (const component of manifest.components ?? []) {
for (const file of component.files) {
const existing = files.get(file.path);
if (existing && canonicalJson(existing) !== canonicalJson(file))
fail(file.path, "Conflicting file descriptors");
files.set(file.path, file);
}
artifacts.push({ artifacts.push({
binding: JSON.stringify([expected.revision, "component", component.id]), binding: JSON.stringify([expected.revision, "component", component.id]),
target: "browser-renderer", target: "browser-renderer",
digest: component.contentDigest, digest: component.contentDigest,
}); });
} }
for (const file of files.values()) {
const bytes = await read(expected.revision, file.path, file.bytes);
if (bytes.byteLength !== file.bytes || createHash("sha256").update(bytes).digest("hex") !== file.digest)
fail(file.path, "Artifact bytes do not match the checked content digest");
}
} }
return { return {
codec: "camino-world-v1", codec: "camino-world-v1",
+224
View File
@@ -0,0 +1,224 @@
import type { BindingSchema, BindingContractSchema } from "../bindings/index.js";
import type { InterfaceRevision, PackageRevision } from "../capability-model/types.js";
import { canonicalJson } from "../capability-model/evolution.js";
import { moduleContract } from "../capability-model/modules.js";
import { generatePackageComponentBindings, publicQueryContract, type PublicQueryContract } from "./client-contracts.js";
import { moduleTypeContext } from "./module-contracts.js";
import { executionVersions, hexDigest, ExecutionPlanError, type PlanPackage } from "./plan.js";
import { preparationArtifact, type ReactPreparationArtifact } from "../react-build/declarations.js";
/** Inputs to compilation, independent of source pins and installed providers.
* Declared ports select interface contracts; the React consumer projects its
* imported interfaces, public queries and module inputs. Ordinary code
* and richer language type companions remain separate, explicit Nix inputs. */
export interface PackageBuildContract {
definition: {
codec: "camino-package-build-v1";
package: Omit<PlanPackage, "source" | "sourceDigest" | "bindingDigest">;
versions: typeof executionVersions;
bindings: BindingContractSchema;
queries: PublicQueryContract[];
component?: string;
moduleInterfaces: { revisionId: string; definitionId?: string; members: string[] }[];
diagnostics: string[];
};
digest: string;
}
const normalize = <T>(value: T): T => JSON.parse(canonicalJson(value)) as T;
const indexSchema = (schema: BindingSchema) => ({
interfaces: new Map<string, InterfaceRevision>(
[...schema.interfaces, ...(schema.interfaceTemplates ?? [])].map((entry) => [entry.revisionId, entry]),
),
packages: new Map<string, PackageRevision>(schema.packages.map((entry) => [entry.revisionId, entry])),
});
export function packageBuildContexts(
schema: BindingSchema,
): Record<string, { contract: PackageBuildContract; sourceDigest: string }> {
const index = indexSchema(schema);
return Object.fromEntries(
schema.packages.map((pkg) => [
pkg.revisionId,
{
contract: projectPackageContract(schema, pkg.revisionId, index),
sourceDigest: hexDigest({ revision: pkg.revisionId, source: pkg.source }),
},
]),
);
}
export function packageBuildContract(schema: BindingSchema, revision: string): PackageBuildContract {
return projectPackageContract(schema, revision, indexSchema(schema));
}
function projectPackageContract(
schema: BindingSchema,
revision: string,
index: ReturnType<typeof indexSchema>,
): PackageBuildContract {
const declared = index.packages.get(revision);
if (!declared) throw new ExecutionPlanError("UNKNOWN_BUILD_PACKAGE", revision, "Missing package declaration");
const pkg = normalize(declared);
const interfaces = (ids: Iterable<string>) =>
[...ids].map((id) => {
const iface = index.interfaces.get(id);
if (!iface) throw new ExecutionPlanError("UNKNOWN_BUILD_INTERFACE", id, "Missing declared interface");
return normalize(iface);
});
const modules = [...(pkg.modules ?? [])]
.sort((a, b) => a.id.localeCompare(b.id))
.map((module) => {
const contract = moduleContract(interfaces([module.interfaceRevisionId]), module);
if (!contract) throw new ExecutionPlanError("UNKNOWN_MODULE_CONTRACT", module.id, "Missing module contract");
return {
...module,
contract: contract.contract,
...(contract.subject ? { subject: contract.subject } : {}),
...moduleTypeContext(index.interfaces.get(module.interfaceRevisionId)!),
};
});
const components = modules.flatMap((module) =>
["org.quixos.react.component/1", "org.quixos.react.component/2", "org.quixos.react.subject-only/1"].includes(
module.contract,
) && module.subject?.kind === "interface"
? [
{
id: module.id,
displayName: module.displayName,
subject: module.subject.interfaceRevisionId,
runtime: "react" as const,
},
]
: [],
);
const shape: PackageBuildContract["definition"]["package"] = {
revision,
exports: [...pkg.exports]
.sort((a, b) => a.id.localeCompare(b.id))
.map((entry) => ({ id: entry.id, execution: entry.execution ?? { kind: "remote" } })),
...(modules.length ? { modules } : {}),
...(components.length ? { components } : {}),
...(pkg.controllers?.length ? { controllers: [...pkg.controllers].sort((a, b) => a.id.localeCompare(b.id)) } : {}),
};
const portInterfaces = new Set(
pkg.exports.flatMap((entry) =>
entry.dependencyPorts.flatMap((port) =>
port.requirement.kind === "interface" ? [port.requirement.interfaceRevisionId] : [],
),
),
);
const bindings: BindingContractSchema = {
format: "quixos-bindings",
version: 1,
packages: [
{
revisionId: pkg.revisionId,
exports: [...pkg.exports].sort((a, b) => a.id.localeCompare(b.id)),
...(pkg.genericExports ? { genericExports: pkg.genericExports } : {}),
},
],
interfaces: interfaces(portInterfaces)
.map(({ source, ...contract }) => contract)
.sort((a, b) => a.revisionId.localeCompare(b.revisionId)),
};
const preparation = pkg.buildArtifacts?.[preparationArtifact] as ReactPreparationArtifact | undefined;
const diagnostics = components.flatMap((component) => {
const input = preparation?.modules.find((entry) => entry.module === component.id);
return input && pkg.queryCompositions?.some((entry) => entry.id === input.composition)
? []
: [`${component.id}: declare checked inputs in quixos.react.json`];
});
// Only interfaces whose modules are exposed by these bindings need language
// type companions. Reference types themselves do not recursively import the
// referenced object's implementation or its conformance inventory.
const used = new Set<string>(Object.values(pkg.clientInterfaces ?? {}));
for (const module of modules) used.add(module.interfaceRevisionId);
for (const component of components) used.add(component.subject);
for (const entry of preparation?.modules ?? [])
for (const child of entry.children) if (child.role) used.add(child.role.interface);
const moduleInterfaces = interfaces(used)
.flatMap((iface: InterfaceRevision) => {
const members = iface.members
.filter((member) => member.kind === "module")
.map((member) => member.id)
.sort();
return members.length
? [
{
revisionId: iface.revisionId,
...(iface.application ? { definitionId: iface.application.definitionId } : {}),
members,
},
]
: [];
})
.sort((a, b) => a.revisionId.localeCompare(b.revisionId));
for (const module of modules)
for (const argument of index.interfaces.get(module.interfaceRevisionId)?.application?.arguments ?? [])
if (argument.kind === "object" && argument.target.kind === "interface")
used.add(argument.target.interfaceRevisionId);
const providers = new Set<string>(Object.values(pkg.clientPackages ?? {}));
for (const query of pkg.queries ?? [])
for (const imported of query.importedFragments ?? []) providers.add(imported.packageRevisionId);
providers.delete(pkg.revisionId);
const componentSchema: BindingSchema = {
format: "quixos-bindings",
version: 1,
interfaces: interfaces(used),
packages: [
pkg,
...[...providers].map((id) => {
const provider = index.packages.get(id);
if (!provider) throw new ExecutionPlanError("UNKNOWN_BUILD_PROVIDER", id, "Missing declared package provider");
return normalize(provider);
}),
],
};
const definition: PackageBuildContract["definition"] = {
codec: "camino-package-build-v1",
package: shape,
versions: { ...executionVersions },
bindings,
queries: [
...new Map(
pkg.exports.flatMap((entry) =>
entry.dependencyPorts.flatMap((port) => {
if (port.requirement.kind !== "query") return [];
const requirement = port.requirement;
const provider = index.packages.get(requirement.packageRevisionId);
if (!provider)
throw new ExecutionPlanError(
"UNKNOWN_QUERY_PROVIDER",
requirement.packageRevisionId,
"Missing declared query provider",
);
return [
[
JSON.stringify([requirement.packageRevisionId, requirement.queryId]),
publicQueryContract(provider, requirement.queryId),
] as const,
];
}),
),
).entries(),
]
.sort(([a], [b]) => a.localeCompare(b))
.map(([, query]) => query),
...(components.length && !diagnostics.length
? { component: generatePackageComponentBindings(componentSchema, shape) }
: {}),
moduleInterfaces,
diagnostics,
};
const normalized = JSON.parse(canonicalJson(definition)) as typeof definition;
return { definition: normalized, digest: hexDigest(normalized) };
}
export function checkPackageBuildContract(contract: PackageBuildContract): void {
if (contract.definition.codec !== "camino-package-build-v1" || hexDigest(contract.definition) !== contract.digest)
throw new ExecutionPlanError("INVALID_BUILD_CONTRACT", contract.digest, "Package contract digest mismatch");
if (contract.definition.diagnostics.length)
throw new ExecutionPlanError(
"INCOMPLETE_BUILD_CONTRACT",
contract.definition.package.revision,
contract.definition.diagnostics.join("; "),
);
}
+26 -11
View File
@@ -7,6 +7,9 @@ import type {
import { canonicalJson } from "../capability-model/evolution.js"; import { canonicalJson } from "../capability-model/evolution.js";
import { portableValueSchema, tsType } from "../bindings/portable.js"; import { portableValueSchema, tsType } from "../bindings/portable.js";
import { type ExecutionPlan, resolvePlannedOperation, hexDigest, ExecutionPlanError } from "./plan.js"; import { type ExecutionPlan, resolvePlannedOperation, hexDigest, ExecutionPlanError } from "./plan.js";
import type { BindingSchema } from "../bindings/index.js";
import type { PackageBuildContract } from "./build-contract.js";
import { preparationArtifact, type ReactPreparationArtifact } from "../react-build/declarations.js";
import { moduleTypeContext } from "./module-contracts.js"; import { moduleTypeContext } from "./module-contracts.js";
/** Public shape only: no slot inventory, implementation binding, or workspace identity. */ /** Public shape only: no slot inventory, implementation binding, or workspace identity. */
@@ -145,6 +148,13 @@ export function generatePackageQueryClientBindings(
runtimeModule = "@quixos/camino-replica-core", runtimeModule = "@quixos/camino-replica-core",
packages: readonly PackageRevision[] = [pkg], packages: readonly PackageRevision[] = [pkg],
): string { ): string {
const internal =
(pkg.buildArtifacts?.[preparationArtifact] as ReactPreparationArtifact | undefined)?.internalQueries ?? [];
pkg = {
...pkg,
queries: pkg.queries?.filter((query) => !internal.includes(query.id)),
checkedQueries: pkg.checkedQueries?.filter((query) => !internal.includes(query.declaration.id)),
};
const queries = (pkg.checkedQueries ?? []) const queries = (pkg.checkedQueries ?? [])
.filter((query) => !query.declaration.fragment) .filter((query) => !query.declaration.fragment)
.map( .map(
@@ -206,12 +216,21 @@ export function generatePackageQueryClientBindings(
/** Receipt-covered bindings expose the package's imports, component subjects and /** Receipt-covered bindings expose the package's imports, component subjects and
* declared queries, never the workspace's complete atom/interface inventory. */ * declared queries, never the workspace's complete atom/interface inventory. */
export function generateComponentClientBindings(plan: ExecutionPlan, packageRevision: string): string { export function generateComponentClientBindings(plan: ExecutionPlan, packageRevision: string): string {
const pkg = plan.definition.workspace.packageImports.find((pkg) => pkg.revisionId === packageRevision); const shape = plan.definition.packages.find((pkg) => pkg.revision === packageRevision);
const components = plan.definition.packages.find((pkg) => pkg.revision === packageRevision)?.components; if (!shape) throw new ExecutionPlanError("NO_COMPONENT_CONTRACT", packageRevision, "Unknown package");
const modules = plan.definition.packages.find((pkg) => pkg.revision === packageRevision)?.modules ?? []; return generatePackageComponentBindings(plan.definition.bindingSchema, shape);
}
export function generatePackageComponentBindings(
schema: BindingSchema,
shape: PackageBuildContract["definition"]["package"],
): string {
const packageRevision = shape.revision;
const pkg = schema.packages.find((pkg) => pkg.revisionId === packageRevision);
const components = shape.components;
const modules = shape.modules ?? [];
if (!pkg || !components?.length) if (!pkg || !components?.length)
throw new ExecutionPlanError("NO_COMPONENT_CONTRACT", packageRevision, "Package declares no component entries"); throw new ExecutionPlanError("NO_COMPONENT_CONTRACT", packageRevision, "Package declares no component entries");
const byId = new Map(plan.definition.workspace.interfaceImports.map((iface) => [iface.revisionId as string, iface])); const byId = new Map(schema.interfaces.map((iface) => [iface.revisionId as string, iface]));
const find = (id: string) => { const find = (id: string) => {
const contract = byId.get(id); const contract = byId.get(id);
if (!contract || contract.template) if (!contract || contract.template)
@@ -254,11 +273,7 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi
.filter((line) => !line.startsWith("import ")) .filter((line) => !line.startsWith("import "))
.join("\n") .join("\n")
.replace("export const interfaces =", "export const subjects ="); .replace("export const interfaces =", "export const subjects =");
const queries = generatePackageQueryClientBindings( const queries = generatePackageQueryClientBindings(pkg, "@quixos/camino-react", schema.packages)
pkg,
"@quixos/camino-react",
plan.definition.workspace.packageImports,
)
.split("\n") .split("\n")
.filter((line) => !line.startsWith("import ")) .filter((line) => !line.startsWith("import "))
.join("\n"); .join("\n");
@@ -270,7 +285,7 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi
queries + queries +
`\nexport const providers = {${Object.entries(pkg.clientPackages ?? {}) `\nexport const providers = {${Object.entries(pkg.clientPackages ?? {})
.map(([alias, id]) => { .map(([alias, id]) => {
const imported = plan.definition.workspace.packageImports.find((entry) => entry.revisionId === id); const imported = schema.packages.find((entry) => entry.revisionId === id);
if (!imported) throw new ExecutionPlanError("UNINSTALLED_PACKAGE", id, "Missing imported module provider"); if (!imported) throw new ExecutionPlanError("UNINSTALLED_PACKAGE", id, "Missing imported module provider");
return `${JSON.stringify(alias)}:{${(imported.modules ?? []).map((module) => `${JSON.stringify(module.displayName)}:${JSON.stringify({ packageRevision: id, id: module.id })}`).join(",")}}`; return `${JSON.stringify(alias)}:{${(imported.modules ?? []).map((module) => `${JSON.stringify(module.displayName)}:${JSON.stringify({ packageRevision: id, id: module.id })}`).join(",")}}`;
}) })
@@ -304,7 +319,7 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi
const handle = `PresentationHandle<${JSON.stringify(child.subject)}, ${extra}>`; const handle = `PresentationHandle<${JSON.stringify(child.subject)}, ${extra}>`;
return `readonly ${JSON.stringify(child.name)}: ${child.each ? `readonly ${handle}[]` : child.with || child.when ? `${handle} | null` : handle}`; return `readonly ${JSON.stringify(child.name)}: ${child.each ? `readonly ${handle}[]` : child.with || child.when ? `${handle} | null` : handle}`;
}); });
const input = `${tsType(composition.output)} & {readonly children: {${children.join(";")}}}`; const input = `${tsType(preparation.input ?? composition.output)} & {readonly children: {${children.join(";")}}}`;
return `${JSON.stringify(component.displayName)}: ComponentType<ComponentProps<${JSON.stringify(component.subject)}, ${JSON.stringify(module.contract)}, ${moduleArguments(find(module.interfaceRevisionId))}, ${input}> & ExtraModuleProps<${JSON.stringify(key)}>>;`; return `${JSON.stringify(component.displayName)}: ComponentType<ComponentProps<${JSON.stringify(component.subject)}, ${JSON.stringify(module.contract)}, ${moduleArguments(find(module.interfaceRevisionId))}, ${input}> & ExtraModuleProps<${JSON.stringify(key)}>>;`;
}) })
.join("\n") + .join("\n") +
+2
View File
@@ -22,3 +22,5 @@ export { reviewSemanticMajors } from "../capability-model/evolution.js";
export * from "./public-reflection.js"; export * from "./public-reflection.js";
export * from "./graph-execution.js"; export * from "./graph-execution.js";
export * from "./build-contract.js";
+7 -48
View File
@@ -11,13 +11,12 @@ import type {
OperationId, OperationId,
} from "../capability-model/types.js"; } from "../capability-model/types.js";
import { validateWorkspaceRevision } from "../capability-model/validation.js"; import { validateWorkspaceRevision } from "../capability-model/validation.js";
import { moduleContract } from "../capability-model/modules.js";
import { canonicalJson, contentDigest } from "../capability-model/evolution.js"; import { canonicalJson, contentDigest } from "../capability-model/evolution.js";
import { resolveForwardedBinding } from "../capability-model/forwarding.js"; import { resolveForwardedBinding } from "../capability-model/forwarding.js";
import { linkQueries, type LinkedQuery } from "../query/link.js"; import { linkQueries, type LinkedQuery } from "../query/link.js";
import { type BindingSchema } from "../bindings/index.js"; import { type BindingSchema } from "../bindings/index.js";
import { unsupportedQueryOperator } from "./query-operators.js"; import { unsupportedQueryOperator } from "./query-operators.js";
import { moduleTypeContext } from "./module-contracts.js"; import { packageBuildContexts } from "./build-contract.js";
export class ExecutionPlanError extends Error { export class ExecutionPlanError extends Error {
constructor( constructor(
@@ -78,30 +77,6 @@ export interface PlanPackage {
exports: { id: string; execution: ExecutionContract }[]; exports: { id: string; execution: ExecutionContract }[];
} }
/** React consumes these module contracts; other contracts remain metadata until
* an artifact consumer exists. This does not make modules callable operations. */
function componentDeclarations(workspace: WorkspaceRevision, pkg: WorkspaceRevision["packageImports"][number]) {
return [
...(pkg.modules ?? []).flatMap((module) => {
const contract = moduleContract(workspace.interfaceImports, module)!;
if (
!["org.quixos.react.component/1", "org.quixos.react.component/2", "org.quixos.react.subject-only/1"].includes(
contract.contract,
) ||
contract.subject?.kind !== "interface"
)
return [];
return [
{
id: module.id,
displayName: module.displayName,
subject: contract.subject.interfaceRevisionId,
runtime: "react" as const,
},
];
}),
];
}
export interface ExecutionPlanDefinition { export interface ExecutionPlanDefinition {
presentations?: NonNullable<WorkspaceRevision["presentations"]>; presentations?: NonNullable<WorkspaceRevision["presentations"]>;
codec: "camino-execution-plan-v1"; codec: "camino-execution-plan-v1";
@@ -543,37 +518,21 @@ export function compileExecutionPlan(
packages: sorted(workspace.packageImports, (p) => p.revisionId), packages: sorted(workspace.packageImports, (p) => p.revisionId),
}; };
const normalizedSchema = JSON.parse(canonicalJson(schema)) as BindingSchema; const normalizedSchema = JSON.parse(canonicalJson(schema)) as BindingSchema;
const buildContexts = packageBuildContexts(normalizedSchema);
const definition: ExecutionPlanDefinition = { const definition: ExecutionPlanDefinition = {
codec: "camino-execution-plan-v1", codec: "camino-execution-plan-v1",
workspace, workspace,
bindingSchema: normalizedSchema, bindingSchema: normalizedSchema,
...(workspace.presentations?.length ? { presentations: sorted(workspace.presentations, (p) => p.id) } : {}), ...(workspace.presentations?.length ? { presentations: sorted(workspace.presentations, (p) => p.id) } : {}),
packages: sorted(workspace.packageImports, (p) => p.revisionId).map((p) => ({ packages: sorted(workspace.packageImports, (p) => p.revisionId).map((p) => {
revision: p.revisionId, const contract = buildContexts[p.revisionId]!.contract;
return {
...contract.definition.package,
source: p.source, source: p.source,
sourceDigest: hexDigest({ revision: p.revisionId, source: p.source }), sourceDigest: hexDigest({ revision: p.revisionId, source: p.source }),
bindingDigest: hexDigest(normalizedSchema), bindingDigest: contract.digest,
...(p.modules?.length
? {
modules: sorted(p.modules, (module) => module.id).map((module) => {
const contract = moduleContract(workspace.interfaceImports, module)!;
return {
...module,
contract: contract.contract,
...(contract.subject ? { subject: contract.subject } : {}),
...moduleTypeContext(
workspace.interfaceImports.find((iface) => iface.revisionId === module.interfaceRevisionId)!,
),
}; };
}), }),
}
: {}),
...(p.controllers?.length ? { controllers: sorted(p.controllers, (controller) => controller.id) } : {}),
...(componentDeclarations(workspace, p).length
? { components: sorted(componentDeclarations(workspace, p), (component) => component.id) }
: {}),
exports: sorted(p.exports, (e) => e.id).map((e) => ({ id: e.id, execution: e.execution ?? { kind: "remote" } })),
})),
owners: sorted(workspace.conformances, (c) => c.id ?? "").map((c) => { owners: sorted(workspace.conformances, (c) => c.id ?? "").map((c) => {
if (!c.id) if (!c.id)
throw new ExecutionPlanError( throw new ExecutionPlanError(
+46 -21
View File
@@ -1,19 +1,23 @@
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import { generatePortableTypeScriptBindings, generateRemoteTypeScriptBindings } from "../bindings/portable.js"; import { generatePortableTypeScriptBindings, generateRemoteTypeScriptBindings } from "../bindings/portable.js";
import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js"; import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js";
import { type ArtifactReader, type PackageArtifacts } from "./artifacts.js"; import { type ArtifactReader, type PackageArtifacts, type ArtifactGroup } from "./artifacts.js";
import { loadArtifactWorld, type LoadedArtifactWorld } from "./files.js"; import { loadArtifactWorld, type LoadedArtifactWorld } from "./files.js";
import { generateComponentClientBindings, componentAssetDeclarations } from "./client-contracts.js"; import { componentAssetDeclarations } from "./client-contracts.js";
export interface ArtifactReceipt { import { checkPackageBuildContract, packageBuildContract, type PackageBuildContract } from "./build-contract.js";
export interface GroupReceipt {
schemaVersion: 1; schemaVersion: 1;
packageRevisionId: string; packageRevisionId: string;
bindingDigest: string; bindingDigest: string;
sourceDigest: string;
manifestDigest: string; manifestDigest: string;
generatorDigest: string; generatorDigest: string;
generated: { target: string; path: string; digest: string; bytes: number }[]; generated: { target: string; path: string; digest: string; bytes: number }[];
} }
export interface ArtifactReceipt extends GroupReceipt {
sourceDigest: string;
}
const hashBytes = (bytes: Uint8Array) => createHash("sha256").update(bytes).digest("hex"); const hashBytes = (bytes: Uint8Array) => createHash("sha256").update(bytes).digest("hex");
/** Nominal identities never become filesystem paths. */ /** Nominal identities never become filesystem paths. */
export const generatedBindingPath = (target: string) => `share/quixos/generated/target-${hexDigest(target)}.ts`; export const generatedBindingPath = (target: string) => `share/quixos/generated/target-${hexDigest(target)}.ts`;
@@ -24,20 +28,42 @@ export function expectedArtifactReceipt(
manifest: PackageArtifacts, manifest: PackageArtifacts,
generatorDigest: string, generatorDigest: string,
): { receipt: ArtifactReceipt; sources: Map<string, Uint8Array> } { ): { receipt: ArtifactReceipt; sources: Map<string, Uint8Array> } {
if (!/^[0-9a-f]{64}$/.test(generatorDigest)) throw new Error("Invalid trusted generator digest");
const pkg = plan.definition.packages.find((p) => p.revision === manifest.packageRevisionId); const pkg = plan.definition.packages.find((p) => p.revision === manifest.packageRevisionId);
const schemaPackage = plan.definition.bindingSchema.packages.find((p) => p.revisionId === manifest.packageRevisionId); if (!pkg || manifest.sourceDigest !== pkg.sourceDigest)
if ( throw new ExecutionPlanError("INVALID_RECEIPT", manifest.packageRevisionId, "Receipt package/source mismatch");
!pkg || return expectedPackageReceipt(
!schemaPackage || packageBuildContract(plan.definition.bindingSchema, pkg.revision),
manifest.bindingDigest !== pkg.bindingDigest || manifest,
manifest.sourceDigest !== pkg.sourceDigest pkg.sourceDigest,
) generatorDigest,
throw new ExecutionPlanError(
"INVALID_RECEIPT",
manifest.packageRevisionId,
"Receipt package/schema/source mismatch",
); );
}
export function expectedPackageReceipt(
contract: PackageBuildContract,
manifest: PackageArtifacts,
sourceDigest: string,
generatorDigest: string,
): { receipt: ArtifactReceipt; sources: Map<string, Uint8Array> } {
if (manifest.sourceDigest !== sourceDigest)
throw new ExecutionPlanError("INVALID_RECEIPT", manifest.packageRevisionId, "Receipt source mismatch");
const result = expectedGroupReceipt(contract, manifest, generatorDigest);
return { ...result, receipt: { ...result.receipt, sourceDigest } };
}
export function expectedGroupReceipt(
contract: PackageBuildContract,
manifest: ArtifactGroup,
generatorDigest: string,
): { receipt: GroupReceipt; sources: Map<string, Uint8Array> } {
checkPackageBuildContract(contract);
if (!/^[0-9a-f]{64}$/.test(generatorDigest)) throw new Error("Invalid trusted generator digest");
const pkg = contract.definition.package;
const schemaPackage = contract.definition.bindings.packages.find((p) => p.revisionId === pkg.revision)!;
if (
manifest.packageRevisionId !== pkg.revision ||
manifest.bindingDigest !== contract.digest ||
hexDigest(manifest.versions) !== hexDigest(contract.definition.versions)
)
throw new ExecutionPlanError("INVALID_RECEIPT", manifest.packageRevisionId, "Receipt package/contract mismatch");
// A process service may implement transactions in any language. Only the // A process service may implement transactions in any language. Only the
// checked JS module path promises generated TypeScript adapter evidence. // checked JS module path promises generated TypeScript adapter evidence.
const available = new Set( const available = new Set(
@@ -60,14 +86,14 @@ export function expectedArtifactReceipt(
const generated = targets.map((target) => { const generated = targets.map((target) => {
const path = generatedBindingPath(target); const path = generatedBindingPath(target);
const bytes = new TextEncoder().encode( const bytes = new TextEncoder().encode(
generatePortableTypeScriptBindings(plan.definition.bindingSchema, pkg.revision, { target }), generatePortableTypeScriptBindings(contract.definition.bindings, pkg.revision, { target }),
); );
sources.set(path, bytes); sources.set(path, bytes);
return { target, path, digest: hashBytes(bytes), bytes: bytes.length }; return { target, path, digest: hashBytes(bytes), bytes: bytes.length };
}); });
if (manifest.components?.length) { if (manifest.components?.length) {
for (const [path, source] of [ for (const [path, source] of [
["share/quixos/generated/client.ts", generateComponentClientBindings(plan, pkg.revision)], ["share/quixos/generated/client.ts", contract.definition.component!],
["share/quixos/generated/component-assets.d.ts", componentAssetDeclarations()], ["share/quixos/generated/component-assets.d.ts", componentAssetDeclarations()],
] as const) { ] as const) {
const bytes = new TextEncoder().encode(source); const bytes = new TextEncoder().encode(source);
@@ -88,7 +114,7 @@ export function expectedArtifactReceipt(
) { ) {
const path = "share/quixos/generated/remote.ts"; const path = "share/quixos/generated/remote.ts";
const bytes = new TextEncoder().encode( const bytes = new TextEncoder().encode(
generateRemoteTypeScriptBindings(plan.definition.bindingSchema, pkg.revision), generateRemoteTypeScriptBindings(contract.definition.bindings, pkg.revision),
); );
sources.set(path, bytes); sources.set(path, bytes);
generated.push({ target: "server", path, digest: hashBytes(bytes), bytes: bytes.length }); generated.push({ target: "server", path, digest: hashBytes(bytes), bytes: bytes.length });
@@ -97,8 +123,7 @@ export function expectedArtifactReceipt(
receipt: { receipt: {
schemaVersion: 1, schemaVersion: 1,
packageRevisionId: pkg.revision, packageRevisionId: pkg.revision,
bindingDigest: pkg.bindingDigest, bindingDigest: contract.digest,
sourceDigest: pkg.sourceDigest,
manifestDigest: hexDigest(manifest), manifestDigest: hexDigest(manifest),
generatorDigest, generatorDigest,
generated, generated,
+4
View File
@@ -17,9 +17,13 @@ export interface ReactChildDeclaration {
export interface ReactModulePreparation { export interface ReactModulePreparation {
module: string; module: string;
composition: string; composition: string;
/** Public input type before workspace query lowering adds private projections. */
input?: ValueType;
children: ReactChildDeclaration[]; children: ReactChildDeclaration[];
} }
export interface ReactPreparationArtifact { export interface ReactPreparationArtifact {
/** Frontend queries belong to installed preparation, not the authored client API. */
internalQueries?: string[];
modules: ReactModulePreparation[]; modules: ReactModulePreparation[];
} }
+2
View File
@@ -61,6 +61,8 @@ export async function expandPresentationQueries(workspace: WorkspaceRevision): P
allowances.push(allowance); allowances.push(allowance);
} }
declaration = { ...declaration, views: [...views.values()], allowances }; declaration = { ...declaration, views: [...views.values()], allowances };
const artifact = pkg.buildArtifacts?.[preparationArtifact] as ReactPreparationArtifact | undefined;
if (artifact) artifact.internalQueries = [...new Set([...(artifact.internalQueries ?? []), declaration.id])].sort();
const checked = await compileQuery(declaration, workspace.interfaceImports, async () => document, resolver); const checked = await compileQuery(declaration, workspace.interfaceImports, async () => document, resolver);
pkg.queries = [...(pkg.queries ?? []).filter((entry) => entry.id !== declaration.id), declaration]; pkg.queries = [...(pkg.queries ?? []).filter((entry) => entry.id !== declaration.id), declaration];
pkg.checkedQueries = [ pkg.checkedQueries = [
+4
View File
@@ -20,6 +20,10 @@ export const generatePackageQueries: PackageQueryGenerator = async (input) => {
const declarations = reactModules.length const declarations = reactModules.length
? presentationDeclarations(await input.readSource("quixos.react.json"), pkg, input.interfaces) ? presentationDeclarations(await input.readSource("quixos.react.json"), pkg, input.interfaces)
: undefined; : undefined;
if (declarations)
declarations.artifact.internalQueries = (pkg.queries ?? [])
.filter((query) => !before.has(query.id))
.map((query) => query.id);
return { return {
queries: (pkg.queries ?? []).filter((query) => !before.has(query.id)), queries: (pkg.queries ?? []).filter((query) => !before.has(query.id)),
documents, documents,
+1
View File
@@ -24,6 +24,7 @@ export async function completeWorkspace(workspace: WorkspaceRevision): Promise<v
const composition = parent.pkg.queryCompositions?.find((entry) => entry.id === parent.preparation.composition); const composition = parent.pkg.queryCompositions?.find((entry) => entry.id === parent.preparation.composition);
if (!composition || composition.output.kind !== "record") if (!composition || composition.output.kind !== "record")
throw Error(`Missing checked inputs for ${parent.module.displayName}`); throw Error(`Missing checked inputs for ${parent.module.displayName}`);
parent.preparation.input = structuredClone(composition.output);
const outputs = new Map(Object.entries(composition.output.fields)); const outputs = new Map(Object.entries(composition.output.fields));
const dependencies: string[] = []; const dependencies: string[] = [];
const children = parent.preparation.children.map((child) => { const children = parent.preparation.children.map((child) => {
+78
View File
@@ -0,0 +1,78 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { compileCapabilityResourceSource } from "../src/capability-language/index.js";
import { packageBuildContract } from "../src/execution-world/build-contract.js";
import { generatePortableTypeScriptBindings } from "../src/bindings/portable.js";
import type { BindingSchema } from "../src/bindings/index.js";
import type { InterfaceRevision } from "../src/capability-model/types.js";
function fixture() {
const source = { repository: "https://example.test/source", commit: "1".repeat(40) };
const interfaces = new Map<string, InterfaceRevision>();
for (const name of ["Value", "Unrelated"]) {
const result = compileCapabilityResourceSource(
`interface ${name} id "${name}" revision "${name}@1" {
value text id "text" : string { get id "text:get" execution snapshot; }
}`,
{ source },
);
assert.ok(result.ok && result.resource.kind === "interface", JSON.stringify(result.diagnostics));
interfaces.set(name, result.resource.revision);
}
const result = compileCapabilityResourceSource(
`import interface Value;
package Adapter id "adapter" revision "adapter@1" {
operation fetchText id "read" : unit -> string mode call execution snapshot targets [server]
receiver any requires { interface data id "value" : Value using [text.get]; };
}`,
{ source, environment: { interfaces, interfaceClosure: [...interfaces.values()] } },
);
assert.ok(result.ok && result.resource.kind === "package", JSON.stringify(result.diagnostics));
return {
format: "quixos-bindings",
version: 1,
interfaces: [...interfaces.values()],
packages: [result.resource.revision],
} satisfies BindingSchema;
}
test("package builds ignore exact pins and unrelated declarations, and preserve generated port types", () => {
const schema = fixture();
const original = packageBuildContract(schema, "adapter@1");
const changed = structuredClone(schema);
changed.interfaces.forEach((iface) => {
iface.source.commit = "2".repeat(40);
});
changed.packages[0]!.source.commit = "3".repeat(40);
changed.interfaces.find((iface) => iface.displayName === "Unrelated")!.members = [];
assert.deepEqual(packageBuildContract(changed, "adapter@1"), original);
assert.equal(JSON.stringify(original).includes("https://example.test/source"), false);
assert.equal(
generatePortableTypeScriptBindings(original.definition.bindings, "adapter@1", { target: "server" }),
generatePortableTypeScriptBindings(schema, "adapter@1", { target: "server" }),
);
});
test("changing a consumed interface or an owned port contract invalidates the build", () => {
const schema = fixture();
const original = packageBuildContract(schema, "adapter@1");
const changed = structuredClone(schema);
changed.interfaces[0]!.members[0]!.operations[0]!.outputType = { kind: "scalar", name: "bool" };
assert.notEqual(packageBuildContract(changed, "adapter@1").digest, original.digest);
const own = structuredClone(schema);
own.packages[0]!.exports[0]!.outputType = { kind: "scalar", name: "bool" };
assert.notEqual(packageBuildContract(own, "adapter@1").digest, original.digest);
});
test("a nominal reference does not import the referenced interface's entire dependency graph", () => {
const schema = fixture();
schema.packages[0]!.exports[0]!.inputType = {
kind: "object-ref",
expectation: { kind: "interface", interfaceRevisionId: schema.interfaces[1]!.revisionId },
};
const original = packageBuildContract(schema, "adapter@1");
schema.interfaces[1]!.members = [];
assert.deepEqual(packageBuildContract(schema, "adapter@1"), original);
schema.packages[0]!.exports[0]!.inputType = { kind: "scalar", name: "string" };
assert.notEqual(packageBuildContract(schema, "adapter@1").digest, original.digest);
});
+16
View File
@@ -10,6 +10,9 @@ import {
generatePackageQueryClientBindings, generatePackageQueryClientBindings,
} from "../src/execution-world/client-contracts.js"; } from "../src/execution-world/client-contracts.js";
import { compileQueryProgram } from "../src/execution-world/queries.js"; import { compileQueryProgram } from "../src/execution-world/queries.js";
import { presentationDeclarations, preparationArtifact } from "../src/react-build/declarations.js";
import { compileQueryComposition } from "../src/query/composition.js";
import type { PackageRevision } from "../src/capability-model/types.js";
const source = { repository: "https://example.test/device-owner.git", commit: "a".repeat(40) }; const source = { repository: "https://example.test/device-owner.git", commit: "a".repeat(40) };
const factory = compileCapabilityResourceSource( const factory = compileCapabilityResourceSource(
`external atom Entry id "atom:entry"; `external atom Entry id "atom:entry";
@@ -51,6 +54,17 @@ const view = compileCapabilityResourceSource(
); );
assert.ok(view.ok && view.resource.kind === "interface", JSON.stringify(view.diagnostics)); assert.ok(view.ok && view.resource.kind === "interface", JSON.stringify(view.diagnostics));
const viewInterface = view.resource.revision; const viewInterface = view.resource.revision;
function declareInputs(pkg: PackageRevision) {
const declarations = presentationDeclarations(
JSON.stringify({
modules: Object.fromEntries((pkg.modules ?? []).map((module) => [module.displayName, { inputs: [] }])),
}),
pkg,
[dataInterface, viewInterface],
);
pkg.buildArtifacts = { [preparationArtifact]: declarations.artifact };
pkg.queryCompositions = declarations.compositions.map((entry) => compileQueryComposition(entry, new Map()));
}
const ui = compileCapabilityResourceSource( const ui = compileCapabilityResourceSource(
'import interface Data; import interface View; package UI id "package:ui" revision "package:ui@1" { module Card id "component:card" implements View.render; }', 'import interface Data; import interface View; package UI id "package:ui" revision "package:ui@1" { module Card id "component:card" implements View.render; }',
{ {
@@ -65,6 +79,7 @@ const ui = compileCapabilityResourceSource(
}, },
); );
assert.ok(ui.ok && ui.resource.kind === "package", JSON.stringify(ui.diagnostics)); assert.ok(ui.ok && ui.resource.kind === "package", JSON.stringify(ui.diagnostics));
declareInputs(ui.resource.revision);
for (const variant of ["base", "added", "gateway", "gateway-next", "session"]) { for (const variant of ["base", "added", "gateway", "gateway-next", "session"]) {
const session = variant === "session", const session = variant === "session",
gateway = session || variant.startsWith("gateway"), gateway = session || variant.startsWith("gateway"),
@@ -107,6 +122,7 @@ package Owner id "package:owner" revision "package:owner@1" {
}, },
); );
assert.ok(pkg.ok && pkg.resource.kind === "package", JSON.stringify(pkg.diagnostics)); assert.ok(pkg.ok && pkg.resource.kind === "package", JSON.stringify(pkg.diagnostics));
if (session) declareInputs(pkg.resource.revision);
if (session) if (session)
pkg.resource.revision.checkedQueries = await Promise.all( pkg.resource.revision.checkedQueries = await Promise.all(
pkg.resource.revision.queries!.map((query) => pkg.resource.revision.queries!.map((query) =>