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:
@@ -80,6 +80,9 @@
|
||||
mkdir -p "$out"
|
||||
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/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"
|
||||
substitute nix/checked-candidate.nix "$out/share/checked-candidate.nix" \
|
||||
--replace-fail '@nixpkgs@' '${pkgs.path}'
|
||||
|
||||
@@ -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 }),
|
||||
);
|
||||
+19
-11
@@ -24,18 +24,26 @@ let
|
||||
package = builtins.getFlake (
|
||||
"path:" + builtins.unsafeDiscardStringContext (toString packageSource)
|
||||
);
|
||||
pkgs = import package.inputs.nixpkgs { inherit system; };
|
||||
buildContext = import ./package-context.nix {
|
||||
inherit pkgs packageRevisionId;
|
||||
protocol = builtins.storePath generator;
|
||||
plan = builtins.path {
|
||||
path = /. + plan;
|
||||
name = "candidate-execution-plan.json";
|
||||
};
|
||||
};
|
||||
checked =
|
||||
package.quixosPackages.${system}.checkedArtifacts
|
||||
or (throw "Package ${packageRevisionId} lacks checkedArtifacts for the portable execution world.");
|
||||
in
|
||||
checked {
|
||||
inherit packageRevisionId;
|
||||
plan = builtins.path {
|
||||
path = /. + plan;
|
||||
name = "candidate-execution-plan.json";
|
||||
};
|
||||
generator = builtins.storePath generator;
|
||||
core = builtins.storePath core;
|
||||
sharedRuntime = builtins.storePath sharedRuntime;
|
||||
typescript = builtins.storePath typescript;
|
||||
}
|
||||
checked (
|
||||
buildContext
|
||||
// {
|
||||
inherit packageRevisionId;
|
||||
generator = builtins.storePath generator;
|
||||
core = builtins.storePath core;
|
||||
sharedRuntime = builtins.storePath sharedRuntime;
|
||||
typescript = builtins.storePath typescript;
|
||||
}
|
||||
)
|
||||
|
||||
+146
-90
@@ -20,26 +20,45 @@ let
|
||||
ref = "refs/tags/quixos-reachability/${source.commit}";
|
||||
shallow = true;
|
||||
};
|
||||
load =
|
||||
ancestors: source:
|
||||
if builtins.elem (sourceKey source) ancestors then
|
||||
throw "Source dependency cycle at ${sourceKey source}"
|
||||
else if builtins.length ancestors >= 100 then
|
||||
throw "Source dependency depth exceeds 100"
|
||||
else
|
||||
let
|
||||
directory = fetch source;
|
||||
lockFile = pkgs.runCommand "qx-source-lock.json" { } ''
|
||||
${protocol}/bin/quixos-lock-check ${directory}/quixos.lock > "$out"
|
||||
'';
|
||||
lock = builtins.fromJSON (builtins.readFile lockFile);
|
||||
# Memoize by exact source identity. A diamond-shaped dependency graph must
|
||||
# not recursively evaluate every path through the same imported subtree.
|
||||
loadSource =
|
||||
source:
|
||||
let
|
||||
directory = fetch source;
|
||||
lockFile = pkgs.runCommand "qx-source-lock.json" { } ''
|
||||
${protocol}/bin/quixos-lock-check ${directory}/quixos.lock > "$out"
|
||||
'';
|
||||
lock = builtins.fromJSON (builtins.readFile lockFile);
|
||||
in
|
||||
source // { inherit directory lock; };
|
||||
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 (
|
||||
entry:
|
||||
load (ancestors ++ [ (sourceKey source) ]) (entry.source // { inherit (entry) kind binding; })
|
||||
) lock.resources;
|
||||
in
|
||||
source // { inherit directory children lock; };
|
||||
root = load [ ] { inherit kind repository commit; };
|
||||
resource:
|
||||
nodeTable.${sourceKey (resource.source // { inherit (resource) kind; })}
|
||||
// {
|
||||
inherit (resource) binding;
|
||||
}
|
||||
) entry.value.lock.resources;
|
||||
};
|
||||
}) sourceGraph
|
||||
);
|
||||
root = nodeTable.${sourceKey { inherit kind repository commit; }};
|
||||
configuration =
|
||||
if builtins.pathExists (root.directory + "/quixos.check.json") then
|
||||
builtins.fromJSON (builtins.readFile (root.directory + "/quixos.check.json"))
|
||||
@@ -50,54 +69,61 @@ let
|
||||
"git+${root.lock.quixos.repository}?rev=${root.lock.quixos.commit}&ref=refs/tags/quixos-reachability/${root.lock.quixos.commit}"
|
||||
);
|
||||
runtimePackages = runtime.packages.${system};
|
||||
flatten = node: [ node ] ++ pkgs.lib.concatMap flatten node.children;
|
||||
capabilityFlatten =
|
||||
node:
|
||||
if node.kind == "build" then
|
||||
[ ]
|
||||
else
|
||||
[ node ] ++ pkgs.lib.concatMap capabilityFlatten node.children;
|
||||
nodes = builtins.attrValues (
|
||||
builtins.listToAttrs (
|
||||
map (node: {
|
||||
name = sourceKey node;
|
||||
value = node;
|
||||
}) (flatten root)
|
||||
)
|
||||
);
|
||||
resourceClosure =
|
||||
includeBuild: node:
|
||||
map (entry: entry.value) (
|
||||
builtins.genericClosure {
|
||||
startSet = pkgs.lib.optional (includeBuild || node.kind != "build") {
|
||||
key = sourceKey node;
|
||||
value = node;
|
||||
};
|
||||
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
|
||||
# neither discovers React contracts nor loads executable package code.
|
||||
queryFrontends = pkgs.lib.unique (
|
||||
pkgs.lib.concatMap (
|
||||
node:
|
||||
let
|
||||
config = node.directory + "/quixos.check.json";
|
||||
in
|
||||
if builtins.pathExists config then
|
||||
(builtins.fromJSON (builtins.readFile config)).queryFrontends or [ ]
|
||||
else
|
||||
[ ]
|
||||
) (capabilityFlatten root)
|
||||
);
|
||||
queryFrontends =
|
||||
node:
|
||||
pkgs.lib.unique (
|
||||
pkgs.lib.concatMap (
|
||||
node:
|
||||
let
|
||||
config = node.directory + "/quixos.check.json";
|
||||
in
|
||||
if builtins.pathExists config then
|
||||
(builtins.fromJSON (builtins.readFile config)).queryFrontends or [ ]
|
||||
else
|
||||
[ ]
|
||||
) (capabilityFlatten node)
|
||||
);
|
||||
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"
|
||||
else
|
||||
throw "Unsupported query frontend selection: ${builtins.toJSON queryFrontends}";
|
||||
snapshots = pkgs.writeText "qx-nix-source-graph.json" (
|
||||
builtins.toJSON {
|
||||
resources = map (node: {
|
||||
inherit (node)
|
||||
kind
|
||||
repository
|
||||
commit
|
||||
directory
|
||||
;
|
||||
}) (builtins.filter (node: node.kind != "workspace") nodes);
|
||||
}
|
||||
);
|
||||
throw "Unsupported query frontend selection: ${builtins.toJSON (queryFrontends node)}";
|
||||
snapshots =
|
||||
node:
|
||||
pkgs.writeText "qx-nix-source-graph.json" (
|
||||
builtins.toJSON {
|
||||
resources = map (node: {
|
||||
inherit (node)
|
||||
kind
|
||||
repository
|
||||
commit
|
||||
directory
|
||||
;
|
||||
}) (builtins.filter (entry: entry.kind != "workspace") (resourceClosure true node));
|
||||
}
|
||||
);
|
||||
compile =
|
||||
node:
|
||||
pkgs.runCommand "qx-${node.kind}-contract" { } ''
|
||||
@@ -106,19 +132,22 @@ let
|
||||
if node.kind == "workspace" then
|
||||
''
|
||||
${protocol}/bin/quixos-workspace-compile --root ${node.directory} \
|
||||
${queryGeneratorFlags} \
|
||||
${queryGeneratorFlags node} \
|
||||
--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" \
|
||||
${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
|
||||
''
|
||||
${protocol}/bin/quixos-resource-compile --root ${node.directory} \
|
||||
${queryGeneratorFlags} \
|
||||
${queryGeneratorFlags node} \
|
||||
--kind ${node.kind} --repository ${pkgs.lib.escapeShellArg node.repository} \
|
||||
--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"
|
||||
''
|
||||
}
|
||||
@@ -137,7 +166,7 @@ let
|
||||
}) (builtins.filter (dependency: dependency.kind == "build") node.children)
|
||||
);
|
||||
reactContracts =
|
||||
node:
|
||||
node: moduleInterfaces:
|
||||
let
|
||||
interfaces = builtins.attrValues (
|
||||
builtins.listToAttrs (
|
||||
@@ -159,6 +188,17 @@ let
|
||||
in
|
||||
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
|
||||
[
|
||||
(build {
|
||||
@@ -174,15 +214,26 @@ let
|
||||
node:
|
||||
let
|
||||
compiled = compile node;
|
||||
candidate = builtins.fromJSON (builtins.readFile "${compiled}/candidate.json");
|
||||
componentEntries =
|
||||
if execution then
|
||||
(pkgs.lib.findFirst (
|
||||
entry: entry.revision == candidate.revision.revisionId
|
||||
) { } (builtins.fromJSON (builtins.readFile "${contract}/execution-plan.json")).definition.packages)
|
||||
.components or [ ]
|
||||
candidate =
|
||||
if execution && kind == "workspace" then
|
||||
{
|
||||
revision =
|
||||
pkgs.lib.findFirst
|
||||
(pkg: pkg.source.repository == node.repository && pkg.source.commit == node.commit)
|
||||
(throw "Source package missing from workspace compilation: ${node.repository}")
|
||||
(builtins.fromJSON (builtins.readFile "${contract}/execution-plan.json"))
|
||||
.definition.bindingSchema.packages;
|
||||
}
|
||||
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 (
|
||||
"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.");
|
||||
artifact = checked (
|
||||
if execution then
|
||||
{
|
||||
plan = "${contract}/execution-plan.json";
|
||||
buildContext
|
||||
// {
|
||||
generator = protocol;
|
||||
core = runtimePackages.camino-replica-core;
|
||||
sharedRuntime = runtimePackages.camino-shared-runtime;
|
||||
@@ -208,7 +259,7 @@ let
|
||||
react = {
|
||||
sdk = runtimePackages.camino-react;
|
||||
runtime = runtimePackages.camino-react-runtime;
|
||||
contracts = reactContracts node;
|
||||
contracts = reactContracts node buildContract.definition.moduleInterfaces;
|
||||
};
|
||||
}
|
||||
else
|
||||
@@ -236,16 +287,18 @@ let
|
||||
[ ]
|
||||
else
|
||||
map checkPackage (
|
||||
builtins.filter (node: node.kind == "package") (
|
||||
builtins.attrValues (
|
||||
builtins.listToAttrs (
|
||||
map (node: {
|
||||
name = sourceKey node;
|
||||
value = node;
|
||||
}) (capabilityFlatten root)
|
||||
builtins.filter
|
||||
(node: node.kind == "package" && (kind == "workspace" || sourceKey node == sourceKey root))
|
||||
(
|
||||
builtins.attrValues (
|
||||
builtins.listToAttrs (
|
||||
map (node: {
|
||||
name = sourceKey node;
|
||||
value = node;
|
||||
}) (capabilityFlatten root)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
manifest = pkgs.writeText "qx-candidate-checks.json" (builtins.toJSON checks);
|
||||
bindingOptions = pkgs.writeText "qx-typescript-options.json" (
|
||||
@@ -254,8 +307,6 @@ let
|
||||
)
|
||||
);
|
||||
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") { } ''
|
||||
mkdir -p "$out"
|
||||
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}/graph.json "$out/graph.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
|
||||
''
|
||||
@@ -291,7 +342,12 @@ pkgs.runCommand (if contractOnly then "qx-contract" else "qx-checked-candidate")
|
||||
${pkgs.lib.optionalString (
|
||||
kind != "workspace"
|
||||
) ''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 \
|
||||
${pkgs.lib.escapeShellArg (builtins.fromJSON (builtins.readFile "${contract}/candidate.json")).revision.revisionId} \
|
||||
"$out/bindings.ts" ${bindingOptions}
|
||||
|
||||
@@ -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 }),
|
||||
}),
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -9,6 +9,14 @@ import type { CompiledCapabilityResourceRepository } from "../capability-languag
|
||||
import { genericImplementationType } from "./generics.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. */
|
||||
export type BindingSchema = {
|
||||
format: "quixos-bindings";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { portableReflectionType } from "../execution-world/reflection.js";
|
||||
import type { BindingSchema } from "./index.js";
|
||||
import type { BindingContractSchema } from "./index.js";
|
||||
import type {
|
||||
ValueType,
|
||||
ObjectExpectation,
|
||||
@@ -121,7 +121,7 @@ interface Method {
|
||||
explicitInput?: boolean;
|
||||
}
|
||||
function methods(
|
||||
schema: BindingSchema,
|
||||
schema: BindingContractSchema,
|
||||
entry: PackageExport,
|
||||
port: DependencyPort,
|
||||
target: string,
|
||||
@@ -247,21 +247,21 @@ export interface PortableBindingOptions {
|
||||
}
|
||||
/** Generate one target's handlers. The same source signatures are used in browser/server builds. */
|
||||
export function generatePortableTypeScriptBindings(
|
||||
schema: BindingSchema,
|
||||
schema: BindingContractSchema,
|
||||
revision: string,
|
||||
options: PortableBindingOptions,
|
||||
): string {
|
||||
return generateBindings(schema, revision, options, false);
|
||||
}
|
||||
export function generateRemoteTypeScriptBindings(
|
||||
schema: BindingSchema,
|
||||
schema: BindingContractSchema,
|
||||
revision: string,
|
||||
options: { runtimeModule?: string } = {},
|
||||
): string {
|
||||
return generateBindings(schema, revision, { ...options, target: "server" }, true);
|
||||
}
|
||||
function generateBindings(
|
||||
schema: BindingSchema,
|
||||
schema: BindingContractSchema,
|
||||
revision: string,
|
||||
options: PortableBindingOptions,
|
||||
remote: boolean,
|
||||
|
||||
@@ -80,9 +80,8 @@ export async function executionScaffold(
|
||||
devShells.default = (import (helpers + "/quixos-package-helpers.nix")).mkCaminoYarnShell { inherit pkgs; };
|
||||
quixosPackages.checkedArtifacts = context:
|
||||
let
|
||||
plan = builtins.fromJSON (builtins.readFile context.plan);
|
||||
package = pkgs.lib.findFirst (entry: entry.revision == context.packageRevisionId)
|
||||
(throw "Package missing from checked plan") plan.definition.packages;
|
||||
contract = builtins.fromJSON (builtins.readFile context.buildContract);
|
||||
package = contract.definition.package;
|
||||
portable = builtins.filter (entry: entry.execution.kind != "remote") package.exports;
|
||||
remote = builtins.filter (entry: entry.execution.kind == "remote") package.exports;
|
||||
modules = import ./components.nix;
|
||||
@@ -107,7 +106,7 @@ in camino.assembleCaminoPackage {
|
||||
);
|
||||
create(
|
||||
"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") {
|
||||
if (!spec.declaration)
|
||||
|
||||
@@ -168,8 +168,8 @@ export const planStructure = async (rootPath: string, request: StructuralRequest
|
||||
});
|
||||
if (compiled.resource.kind === "package") {
|
||||
const configuration = JSON.parse(await fs.readFile(path.join(resourceRoot, "quixos.check.json"), "utf8"));
|
||||
// Execution bindings require the complete workspace plan and are owned
|
||||
// by checkedArtifacts, not a package-only structural preview.
|
||||
// Execution bindings are immutable build outputs owned by checkedArtifacts,
|
||||
// not source edits produced by a structural preview.
|
||||
const artifacts =
|
||||
configuration.backend === "execution"
|
||||
? []
|
||||
|
||||
@@ -5,7 +5,7 @@ import process from "node:process";
|
||||
import { compileWorkspaceRepository } from "./assembly.js";
|
||||
import { createGitCapabilityResolver } from "./git-resolver.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 {
|
||||
planEvolution,
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
const usage = `usage: quixos-workspace-compile --root DIRECTORY --checkout-root DIRECTORY
|
||||
[--snapshot-map PATH] [--graph-out PATH] [--workspace-id ID] [--workspace-revision-id ID]
|
||||
[--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]
|
||||
|
||||
Resolves a workspace's recursive resource-lock graph, clones every exact
|
||||
@@ -41,6 +41,7 @@ const parseArgs = (args: string[]) => {
|
||||
graphOut: values.get("--graph-out"),
|
||||
schemasOut: values.get("--schemas-out"),
|
||||
executionPlanOut: values.get("--execution-plan-out"),
|
||||
buildContextsOut: values.get("--build-contexts-out"),
|
||||
snapshotMap: values.get("--snapshot-map"),
|
||||
workspaceId: values.get("--workspace-id"),
|
||||
workspaceRevisionId: values.get("--workspace-revision-id"),
|
||||
@@ -108,11 +109,11 @@ const main = async () => {
|
||||
);
|
||||
}
|
||||
const candidate = { ...assembled.workspace, executionContracts: runtimeContracts(assembled.workspace) };
|
||||
if (options.executionPlanOut) {
|
||||
await writeFile(
|
||||
options.executionPlanOut,
|
||||
`${JSON.stringify(compileExecutionPlan(assembled.workspace), null, 2)}\n`,
|
||||
);
|
||||
if (options.executionPlanOut || options.buildContextsOut) {
|
||||
const plan = compileExecutionPlan(assembled.workspace);
|
||||
if (options.executionPlanOut) await writeFile(options.executionPlanOut, `${JSON.stringify(plan, null, 2)}\n`);
|
||||
if (options.buildContextsOut)
|
||||
await writeFile(options.buildContextsOut, JSON.stringify(packageBuildContexts(plan.definition.bindingSchema)));
|
||||
}
|
||||
if (options.schemasOut) {
|
||||
const schemas: Record<string, unknown> = {};
|
||||
|
||||
@@ -20,15 +20,19 @@ export interface ArtifactImplementation {
|
||||
files: ArtifactFile[];
|
||||
contentDigest: string;
|
||||
}
|
||||
export interface PackageArtifacts {
|
||||
export interface ArtifactGroup {
|
||||
schemaVersion: 1;
|
||||
packageRevisionId: string;
|
||||
bindingDigest: string;
|
||||
sourceDigest: string;
|
||||
versions: ExecutionPlan["definition"]["versions"];
|
||||
implementations: ArtifactImplementation[];
|
||||
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 = [
|
||||
"react",
|
||||
"react-dom",
|
||||
@@ -199,11 +203,11 @@ function parseComponent(value: unknown, index: number): ComponentArtifact {
|
||||
if (componentArtifactDigest(body) !== contentDigest) fail(path, "Component metadata digest mismatch");
|
||||
return component;
|
||||
}
|
||||
export function parsePackageArtifacts(value: unknown): PackageArtifacts {
|
||||
export function parseArtifactGroup(value: unknown): ArtifactGroup {
|
||||
const record = object(value, "manifest");
|
||||
keys(
|
||||
record,
|
||||
["schemaVersion", "packageRevisionId", "bindingDigest", "sourceDigest", "versions", "implementations"],
|
||||
["schemaVersion", "packageRevisionId", "bindingDigest", "versions", "implementations"],
|
||||
["components"],
|
||||
"manifest",
|
||||
);
|
||||
@@ -266,13 +270,19 @@ export function parsePackageArtifacts(value: unknown): PackageArtifacts {
|
||||
schemaVersion: 1,
|
||||
packageRevisionId: text(record.packageRevisionId, "packageRevisionId"),
|
||||
bindingDigest: digest(record.bindingDigest, "bindingDigest"),
|
||||
sourceDigest: digest(record.sourceDigest, "sourceDigest"),
|
||||
versions: versions as unknown as PackageArtifacts["versions"],
|
||||
implementations,
|
||||
...(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 ?? [])
|
||||
if (!expected.components?.some((component) => component.id === module.id))
|
||||
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>;
|
||||
/** 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. */
|
||||
export async function checkExecutionWorld(
|
||||
plan: ExecutionPlan,
|
||||
@@ -335,17 +360,11 @@ export async function checkExecutionWorld(
|
||||
const manifest =
|
||||
manifests.find((m) => m.packageRevisionId === expected.revision) ??
|
||||
fail(expected.revision, "Missing package artifact");
|
||||
checkCoverage(expected, manifest);
|
||||
checkPackageCoverage(expected, manifest);
|
||||
if (canonicalJson(manifest.versions) !== canonicalJson(plan.definition.versions))
|
||||
fail(expected.revision, "Incompatible runtime codecs");
|
||||
const files = new Map<string, ArtifactFile>();
|
||||
await checkArtifactFiles(manifest, read);
|
||||
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({
|
||||
binding: JSON.stringify([expected.revision, implementation.id]),
|
||||
target: implementation.target,
|
||||
@@ -353,23 +372,12 @@ export async function checkExecutionWorld(
|
||||
});
|
||||
}
|
||||
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({
|
||||
binding: JSON.stringify([expected.revision, "component", component.id]),
|
||||
target: "browser-renderer",
|
||||
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 {
|
||||
codec: "camino-world-v1",
|
||||
|
||||
@@ -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("; "),
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,9 @@ import type {
|
||||
import { canonicalJson } from "../capability-model/evolution.js";
|
||||
import { portableValueSchema, tsType } from "../bindings/portable.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";
|
||||
|
||||
/** Public shape only: no slot inventory, implementation binding, or workspace identity. */
|
||||
@@ -145,6 +148,13 @@ export function generatePackageQueryClientBindings(
|
||||
runtimeModule = "@quixos/camino-replica-core",
|
||||
packages: readonly PackageRevision[] = [pkg],
|
||||
): 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 ?? [])
|
||||
.filter((query) => !query.declaration.fragment)
|
||||
.map(
|
||||
@@ -206,12 +216,21 @@ export function generatePackageQueryClientBindings(
|
||||
/** Receipt-covered bindings expose the package's imports, component subjects and
|
||||
* declared queries, never the workspace's complete atom/interface inventory. */
|
||||
export function generateComponentClientBindings(plan: ExecutionPlan, packageRevision: string): string {
|
||||
const pkg = plan.definition.workspace.packageImports.find((pkg) => pkg.revisionId === packageRevision);
|
||||
const components = plan.definition.packages.find((pkg) => pkg.revision === packageRevision)?.components;
|
||||
const modules = plan.definition.packages.find((pkg) => pkg.revision === packageRevision)?.modules ?? [];
|
||||
const shape = plan.definition.packages.find((pkg) => pkg.revision === packageRevision);
|
||||
if (!shape) throw new ExecutionPlanError("NO_COMPONENT_CONTRACT", packageRevision, "Unknown package");
|
||||
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)
|
||||
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 contract = byId.get(id);
|
||||
if (!contract || contract.template)
|
||||
@@ -254,11 +273,7 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi
|
||||
.filter((line) => !line.startsWith("import "))
|
||||
.join("\n")
|
||||
.replace("export const interfaces =", "export const subjects =");
|
||||
const queries = generatePackageQueryClientBindings(
|
||||
pkg,
|
||||
"@quixos/camino-react",
|
||||
plan.definition.workspace.packageImports,
|
||||
)
|
||||
const queries = generatePackageQueryClientBindings(pkg, "@quixos/camino-react", schema.packages)
|
||||
.split("\n")
|
||||
.filter((line) => !line.startsWith("import "))
|
||||
.join("\n");
|
||||
@@ -270,7 +285,7 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi
|
||||
queries +
|
||||
`\nexport const providers = {${Object.entries(pkg.clientPackages ?? {})
|
||||
.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");
|
||||
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}>`;
|
||||
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)}>>;`;
|
||||
})
|
||||
.join("\n") +
|
||||
|
||||
@@ -22,3 +22,5 @@ export { reviewSemanticMajors } from "../capability-model/evolution.js";
|
||||
export * from "./public-reflection.js";
|
||||
|
||||
export * from "./graph-execution.js";
|
||||
|
||||
export * from "./build-contract.js";
|
||||
|
||||
+11
-52
@@ -11,13 +11,12 @@ import type {
|
||||
OperationId,
|
||||
} from "../capability-model/types.js";
|
||||
import { validateWorkspaceRevision } from "../capability-model/validation.js";
|
||||
import { moduleContract } from "../capability-model/modules.js";
|
||||
import { canonicalJson, contentDigest } from "../capability-model/evolution.js";
|
||||
import { resolveForwardedBinding } from "../capability-model/forwarding.js";
|
||||
import { linkQueries, type LinkedQuery } from "../query/link.js";
|
||||
import { type BindingSchema } from "../bindings/index.js";
|
||||
import { unsupportedQueryOperator } from "./query-operators.js";
|
||||
import { moduleTypeContext } from "./module-contracts.js";
|
||||
import { packageBuildContexts } from "./build-contract.js";
|
||||
|
||||
export class ExecutionPlanError extends Error {
|
||||
constructor(
|
||||
@@ -78,30 +77,6 @@ export interface PlanPackage {
|
||||
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 {
|
||||
presentations?: NonNullable<WorkspaceRevision["presentations"]>;
|
||||
codec: "camino-execution-plan-v1";
|
||||
@@ -543,37 +518,21 @@ export function compileExecutionPlan(
|
||||
packages: sorted(workspace.packageImports, (p) => p.revisionId),
|
||||
};
|
||||
const normalizedSchema = JSON.parse(canonicalJson(schema)) as BindingSchema;
|
||||
const buildContexts = packageBuildContexts(normalizedSchema);
|
||||
const definition: ExecutionPlanDefinition = {
|
||||
codec: "camino-execution-plan-v1",
|
||||
workspace,
|
||||
bindingSchema: normalizedSchema,
|
||||
...(workspace.presentations?.length ? { presentations: sorted(workspace.presentations, (p) => p.id) } : {}),
|
||||
packages: sorted(workspace.packageImports, (p) => p.revisionId).map((p) => ({
|
||||
revision: p.revisionId,
|
||||
source: p.source,
|
||||
sourceDigest: hexDigest({ revision: p.revisionId, source: p.source }),
|
||||
bindingDigest: hexDigest(normalizedSchema),
|
||||
...(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" } })),
|
||||
})),
|
||||
packages: sorted(workspace.packageImports, (p) => p.revisionId).map((p) => {
|
||||
const contract = buildContexts[p.revisionId]!.contract;
|
||||
return {
|
||||
...contract.definition.package,
|
||||
source: p.source,
|
||||
sourceDigest: hexDigest({ revision: p.revisionId, source: p.source }),
|
||||
bindingDigest: contract.digest,
|
||||
};
|
||||
}),
|
||||
owners: sorted(workspace.conformances, (c) => c.id ?? "").map((c) => {
|
||||
if (!c.id)
|
||||
throw new ExecutionPlanError(
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { generatePortableTypeScriptBindings, generateRemoteTypeScriptBindings } from "../bindings/portable.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 { 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;
|
||||
packageRevisionId: string;
|
||||
bindingDigest: string;
|
||||
sourceDigest: string;
|
||||
manifestDigest: string;
|
||||
generatorDigest: string;
|
||||
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");
|
||||
/** Nominal identities never become filesystem paths. */
|
||||
export const generatedBindingPath = (target: string) => `share/quixos/generated/target-${hexDigest(target)}.ts`;
|
||||
@@ -24,20 +28,42 @@ export function expectedArtifactReceipt(
|
||||
manifest: PackageArtifacts,
|
||||
generatorDigest: string,
|
||||
): { 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 schemaPackage = plan.definition.bindingSchema.packages.find((p) => p.revisionId === manifest.packageRevisionId);
|
||||
if (!pkg || manifest.sourceDigest !== pkg.sourceDigest)
|
||||
throw new ExecutionPlanError("INVALID_RECEIPT", manifest.packageRevisionId, "Receipt package/source mismatch");
|
||||
return expectedPackageReceipt(
|
||||
packageBuildContract(plan.definition.bindingSchema, pkg.revision),
|
||||
manifest,
|
||||
pkg.sourceDigest,
|
||||
generatorDigest,
|
||||
);
|
||||
}
|
||||
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 (
|
||||
!pkg ||
|
||||
!schemaPackage ||
|
||||
manifest.bindingDigest !== pkg.bindingDigest ||
|
||||
manifest.sourceDigest !== pkg.sourceDigest
|
||||
manifest.packageRevisionId !== pkg.revision ||
|
||||
manifest.bindingDigest !== contract.digest ||
|
||||
hexDigest(manifest.versions) !== hexDigest(contract.definition.versions)
|
||||
)
|
||||
throw new ExecutionPlanError(
|
||||
"INVALID_RECEIPT",
|
||||
manifest.packageRevisionId,
|
||||
"Receipt package/schema/source mismatch",
|
||||
);
|
||||
throw new ExecutionPlanError("INVALID_RECEIPT", manifest.packageRevisionId, "Receipt package/contract mismatch");
|
||||
// A process service may implement transactions in any language. Only the
|
||||
// checked JS module path promises generated TypeScript adapter evidence.
|
||||
const available = new Set(
|
||||
@@ -60,14 +86,14 @@ export function expectedArtifactReceipt(
|
||||
const generated = targets.map((target) => {
|
||||
const path = generatedBindingPath(target);
|
||||
const bytes = new TextEncoder().encode(
|
||||
generatePortableTypeScriptBindings(plan.definition.bindingSchema, pkg.revision, { target }),
|
||||
generatePortableTypeScriptBindings(contract.definition.bindings, pkg.revision, { target }),
|
||||
);
|
||||
sources.set(path, bytes);
|
||||
return { target, path, digest: hashBytes(bytes), bytes: bytes.length };
|
||||
});
|
||||
if (manifest.components?.length) {
|
||||
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()],
|
||||
] as const) {
|
||||
const bytes = new TextEncoder().encode(source);
|
||||
@@ -88,7 +114,7 @@ export function expectedArtifactReceipt(
|
||||
) {
|
||||
const path = "share/quixos/generated/remote.ts";
|
||||
const bytes = new TextEncoder().encode(
|
||||
generateRemoteTypeScriptBindings(plan.definition.bindingSchema, pkg.revision),
|
||||
generateRemoteTypeScriptBindings(contract.definition.bindings, pkg.revision),
|
||||
);
|
||||
sources.set(path, bytes);
|
||||
generated.push({ target: "server", path, digest: hashBytes(bytes), bytes: bytes.length });
|
||||
@@ -97,8 +123,7 @@ export function expectedArtifactReceipt(
|
||||
receipt: {
|
||||
schemaVersion: 1,
|
||||
packageRevisionId: pkg.revision,
|
||||
bindingDigest: pkg.bindingDigest,
|
||||
sourceDigest: pkg.sourceDigest,
|
||||
bindingDigest: contract.digest,
|
||||
manifestDigest: hexDigest(manifest),
|
||||
generatorDigest,
|
||||
generated,
|
||||
|
||||
@@ -17,9 +17,13 @@ export interface ReactChildDeclaration {
|
||||
export interface ReactModulePreparation {
|
||||
module: string;
|
||||
composition: string;
|
||||
/** Public input type before workspace query lowering adds private projections. */
|
||||
input?: ValueType;
|
||||
children: ReactChildDeclaration[];
|
||||
}
|
||||
export interface ReactPreparationArtifact {
|
||||
/** Frontend queries belong to installed preparation, not the authored client API. */
|
||||
internalQueries?: string[];
|
||||
modules: ReactModulePreparation[];
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,8 @@ export async function expandPresentationQueries(workspace: WorkspaceRevision): P
|
||||
allowances.push(allowance);
|
||||
}
|
||||
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);
|
||||
pkg.queries = [...(pkg.queries ?? []).filter((entry) => entry.id !== declaration.id), declaration];
|
||||
pkg.checkedQueries = [
|
||||
|
||||
@@ -20,6 +20,10 @@ export const generatePackageQueries: PackageQueryGenerator = async (input) => {
|
||||
const declarations = reactModules.length
|
||||
? presentationDeclarations(await input.readSource("quixos.react.json"), pkg, input.interfaces)
|
||||
: undefined;
|
||||
if (declarations)
|
||||
declarations.artifact.internalQueries = (pkg.queries ?? [])
|
||||
.filter((query) => !before.has(query.id))
|
||||
.map((query) => query.id);
|
||||
return {
|
||||
queries: (pkg.queries ?? []).filter((query) => !before.has(query.id)),
|
||||
documents,
|
||||
|
||||
@@ -24,6 +24,7 @@ export async function completeWorkspace(workspace: WorkspaceRevision): Promise<v
|
||||
const composition = parent.pkg.queryCompositions?.find((entry) => entry.id === parent.preparation.composition);
|
||||
if (!composition || composition.output.kind !== "record")
|
||||
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 dependencies: string[] = [];
|
||||
const children = parent.preparation.children.map((child) => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -10,6 +10,9 @@ import {
|
||||
generatePackageQueryClientBindings,
|
||||
} from "../src/execution-world/client-contracts.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 factory = compileCapabilityResourceSource(
|
||||
`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));
|
||||
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(
|
||||
'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));
|
||||
declareInputs(ui.resource.revision);
|
||||
for (const variant of ["base", "added", "gateway", "gateway-next", "session"]) {
|
||||
const session = variant === "session",
|
||||
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));
|
||||
if (session) declareInputs(pkg.resource.revision);
|
||||
if (session)
|
||||
pkg.resource.revision.checkedQueries = await Promise.all(
|
||||
pkg.resource.revision.queries!.map((query) =>
|
||||
|
||||
Reference in New Issue
Block a user