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:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user