Implement typed module contracts and conformance bindings

This commit is contained in:
Timothy J. Aveni
2026-09-22 13:51:32 -07:00
parent 60b44a6637
commit 7173751f37
33 changed files with 2963 additions and 2261 deletions
+6 -3
View File
@@ -39,7 +39,7 @@ export const componentSharedModules = [
export interface ComponentArtifact {
id: string;
subject: string;
compatibility: "quixos-react-component-v1";
compatibility: "org.quixos.react.esm/1";
path: string;
exportName: string;
sharedModules: Record<(typeof componentSharedModules)[number], string>;
@@ -171,7 +171,7 @@ function parseComponent(value: unknown, index: number): ComponentArtifact {
[],
path,
);
if (entry.compatibility !== "quixos-react-component-v1") fail(path, "Unsupported component ABI");
if (entry.compatibility !== "org.quixos.react.esm/1") fail(path, "Unsupported component ABI");
const files = parseArtifactFiles(entry.files, `${path}.files`);
const shared = object(entry.sharedModules, `${path}.sharedModules`);
keys(shared, [...componentSharedModules], [], `${path}.sharedModules`);
@@ -187,7 +187,7 @@ function parseComponent(value: unknown, index: number): ComponentArtifact {
const component: ComponentArtifact = {
id: text(entry.id, path),
subject: text(entry.subject, path),
compatibility: "quixos-react-component-v1",
compatibility: "org.quixos.react.esm/1",
path: entryPath,
exportName,
sharedModules,
@@ -272,6 +272,9 @@ export function parsePackageArtifacts(value: unknown): PackageArtifacts {
};
}
function checkCoverage(expected: PlanPackage, 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`);
if (expected.bindingDigest !== manifest.bindingDigest || expected.sourceDigest !== manifest.sourceDigest)
fail(expected.revision, "Substituted binding schema or source revision");
for (const component of manifest.components ?? []) {
+4 -5
View File
@@ -136,7 +136,8 @@ export function generatePackageQueryClientBindings(
* 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);
if (!pkg || !pkg.components?.length)
const components = plan.definition.packages.find((pkg) => pkg.revision === packageRevision)?.components;
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 find = (id: string) => {
@@ -148,9 +149,7 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi
const imports = Object.fromEntries(
Object.entries(pkg.clientInterfaces ?? {}).map(([alias, id]) => [alias, find(id)]),
);
const subjects = Object.fromEntries(
pkg.components.map((component) => [component.displayName, find(component.subject)]),
);
const subjects = Object.fromEntries(components.map((component) => [component.displayName, find(component.subject)]));
const interfaces = generateInterfaceClientBindings(imports, "@quixos/camino-react");
const subjectBindings = generateInterfaceClientBindings(subjects, "@quixos/camino-react")
.split("\n")
@@ -168,7 +167,7 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi
"\n" +
queries +
"\nexport interface Components {\n" +
pkg.components
components
.map(
(component) =>
`${JSON.stringify(component.displayName)}: ComponentType<ComponentProps<${JSON.stringify(component.subject)}>>;`,
+44 -2
View File
@@ -10,6 +10,7 @@ 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";
@@ -58,14 +59,41 @@ export interface PlannedQuery {
admittedOperations: string[];
}
export interface PlanPackage {
modules?: (NonNullable<WorkspaceRevision["packageImports"][number]["modules"]>[number] & {
contract: string;
subject?: import("../capability-model/types.js").ObjectExpectation;
})[];
controllers?: NonNullable<WorkspaceRevision["packageImports"][number]["controllers"]>;
components?: NonNullable<WorkspaceRevision["packageImports"][number]["components"]>;
components?: { id: string; displayName: string; subject: InterfaceRevisionId; runtime: "react" }[];
revision: string;
source: WorkspaceRevision["packageImports"][number]["source"];
sourceDigest: string;
bindingDigest: string;
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.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";
@@ -430,8 +458,22 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
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 } : {}),
};
}),
}
: {}),
...(p.controllers?.length ? { controllers: sorted(p.controllers, (controller) => controller.id) } : {}),
...(p.components?.length ? { components: sorted(p.components, (component) => component.id) } : {}),
...(componentDeclarations(workspace, p).length
? { components: sorted(componentDeclarations(workspace, p), (component) => component.id) }
: {}),
exports: sorted(p.exports, (e) => e.id).map((e) => ({ id: e.id, execution: e.execution ?? { kind: "remote" } })),
})),
owners: sorted(workspace.conformances, (c) => c.id ?? "").map((c) => {