Implement checked workspace integration contracts (WI-01)
This commit is contained in:
@@ -1,8 +1,73 @@
|
||||
import path from "node:path";
|
||||
import { loadCheckedArtifactWorld } from "./receipts.js";
|
||||
import { contentArtifactUrl } from "./files.js";
|
||||
import { selectArtifactImplementation, type ArtifactImplementation } from "./artifacts.js";
|
||||
import { ExecutionPlanError, type ExecutionPlan } from "./plan.js";
|
||||
import { selectArtifactImplementation, type ArtifactImplementation, type ComponentArtifact } from "./artifacts.js";
|
||||
import { ExecutionPlanError, hexDigest, type ExecutionPlan } from "./plan.js";
|
||||
import {
|
||||
publicOperationContract,
|
||||
publicQueryContract,
|
||||
type PublicOperationContract,
|
||||
type PublicQueryContract,
|
||||
} from "./client-contracts.js";
|
||||
|
||||
/** Activation is assigned by graph authority, never by a package artifact. */
|
||||
export interface ActiveExecutionIdentity {
|
||||
readonly workspace: string;
|
||||
readonly world: string;
|
||||
readonly plan: string;
|
||||
readonly epoch: string;
|
||||
}
|
||||
/** Host-owned execution selection. The lifecycle manager fills its checked inputs. */
|
||||
export interface ExecutionGroupIdentity {
|
||||
readonly logicalId: string;
|
||||
readonly executionDigest: string;
|
||||
}
|
||||
export interface ClientContractCatalog {
|
||||
readonly operations: readonly PublicOperationContract[];
|
||||
readonly queries: readonly PublicQueryContract[];
|
||||
readonly components: readonly InstalledComponent[];
|
||||
readonly presentations: readonly { id: string; subject: string; packageRevision: string; component: string }[];
|
||||
}
|
||||
/** Explicit projection prevents accidental spreading of filesystem roots or private bindings. */
|
||||
export function clientContractCatalog(
|
||||
plan: ExecutionPlan,
|
||||
installation: ExecutionInstallation,
|
||||
requestedPackages: readonly string[],
|
||||
): ClientContractCatalog {
|
||||
if (installation.plan !== plan.digest || hexDigest(plan.definition) !== plan.digest)
|
||||
throw new ExecutionPlanError(
|
||||
"CATALOG_PLAN_MISMATCH",
|
||||
plan.digest,
|
||||
"Installation and public catalog must use the same checked plan",
|
||||
);
|
||||
const packages = requestedPackages.map((revision) => {
|
||||
const pkg = plan.definition.workspace.packageImports.find((p) => p.revisionId === revision);
|
||||
if (!pkg)
|
||||
throw new ExecutionPlanError("UNINSTALLED_PACKAGE", revision, "Requested catalog package is not installed");
|
||||
return pkg;
|
||||
});
|
||||
// Interface metadata contains no authority or private implementation configuration.
|
||||
// A later admission optimization can narrow this set without changing operation identities.
|
||||
const operations = plan.definition.workspace.interfaceImports
|
||||
.filter((i) => !i.template)
|
||||
.flatMap((iface) =>
|
||||
iface.members.flatMap((member) =>
|
||||
member.operations
|
||||
.filter((o) => o.mode === "call")
|
||||
.map((operation) => publicOperationContract(iface, operation)),
|
||||
),
|
||||
);
|
||||
return {
|
||||
operations,
|
||||
queries: packages.flatMap((pkg) =>
|
||||
(pkg.checkedQueries ?? []).map((q) => publicQueryContract(pkg, q.declaration.id)),
|
||||
),
|
||||
components: installation.components.filter((c) => requestedPackages.includes(c.packageRevision)),
|
||||
presentations: (plan.definition.presentations ?? [])
|
||||
.filter((p) => requestedPackages.includes(p.packageRevisionId))
|
||||
.map((p) => ({ id: p.id, subject: p.subject, packageRevision: p.packageRevisionId, component: p.componentId })),
|
||||
};
|
||||
}
|
||||
|
||||
export interface PackageInstallationSelection {
|
||||
readonly browser?: { readonly id?: string };
|
||||
@@ -17,7 +82,7 @@ export interface ExecutionInstallation {
|
||||
readonly packages: readonly {
|
||||
readonly revision: string;
|
||||
readonly root: string;
|
||||
readonly service: ArtifactImplementation;
|
||||
readonly service?: ArtifactImplementation;
|
||||
readonly module?: ArtifactImplementation;
|
||||
readonly browserURL?: string;
|
||||
readonly browserRegistry?: string;
|
||||
@@ -25,6 +90,17 @@ export interface ExecutionInstallation {
|
||||
readonly routes: Readonly<Record<string, { readonly path: string; readonly mediaType: string }>>;
|
||||
readonly coreURL?: string;
|
||||
readonly engineURL?: string;
|
||||
readonly components: readonly InstalledComponent[];
|
||||
}
|
||||
/** Safe to expose in a presentation catalog: no source paths, state slots, or host configuration. */
|
||||
export interface InstalledComponent {
|
||||
readonly packageRevision: string;
|
||||
readonly id: string;
|
||||
readonly subject: string;
|
||||
readonly url: string;
|
||||
readonly exportName: string;
|
||||
readonly compatibility: ComponentArtifact["compatibility"];
|
||||
readonly sharedModules: Readonly<Record<keyof ComponentArtifact["sharedModules"], string>>;
|
||||
}
|
||||
/** Verify candidate receipts before assembling process entrypoints and browser assets.
|
||||
* This grants no authority to activate a world or run optional modules in process. */
|
||||
@@ -43,18 +119,49 @@ export async function loadExecutionInstallation(
|
||||
fail(revision, "Selection names a package outside this checked world");
|
||||
const routes: Record<string, { path: string; mediaType: string }> = Object.create(null);
|
||||
const packages: ExecutionInstallation["packages"][number][] = [];
|
||||
const components: InstalledComponent[] = [];
|
||||
const sharedComponentModules = new Map<string, string>();
|
||||
let coreURL: string | undefined, engineURL: string | undefined;
|
||||
for (const manifest of loaded.manifests) {
|
||||
const revision = manifest.packageRevisionId;
|
||||
const root = roots[revision]!;
|
||||
const selection = selections[revision] ?? {};
|
||||
const exports = plan.definition.packages.find((pkg) => pkg.revision === revision)!.exports;
|
||||
const service = selectArtifactImplementation(manifest, {
|
||||
...selection.service,
|
||||
target: "server",
|
||||
compatibility: "quixos-service-v1",
|
||||
exports: exports.map((entry) => entry.id),
|
||||
});
|
||||
const service =
|
||||
exports.length || selection.service
|
||||
? selectArtifactImplementation(manifest, {
|
||||
...selection.service,
|
||||
target: "server",
|
||||
compatibility: "quixos-service-v1",
|
||||
exports: exports.map((entry) => entry.id),
|
||||
})
|
||||
: undefined;
|
||||
for (const component of manifest.components ?? []) {
|
||||
const sharedModules = Object.fromEntries(
|
||||
Object.entries(component.sharedModules).map(([name, filePath]) => {
|
||||
const file = component.files.find((file) => file.path === filePath)!;
|
||||
const url = contentArtifactUrl(file),
|
||||
previous = sharedComponentModules.get(name);
|
||||
if (previous && previous !== url) fail(component.id, `Components disagree on shared module ${name}`);
|
||||
sharedComponentModules.set(name, url);
|
||||
return [name, url];
|
||||
}),
|
||||
) as InstalledComponent["sharedModules"];
|
||||
for (const file of component.files) {
|
||||
const url = contentArtifactUrl(file);
|
||||
if (routes[url] && routes[url].mediaType !== file.mediaType) fail(url, "Conflicting artifact media types");
|
||||
routes[url] ??= { path: path.join(root, file.path), mediaType: file.mediaType };
|
||||
}
|
||||
components.push({
|
||||
packageRevision: revision,
|
||||
id: component.id,
|
||||
subject: component.subject,
|
||||
compatibility: component.compatibility,
|
||||
exportName: component.exportName,
|
||||
sharedModules,
|
||||
url: contentArtifactUrl(component.files.find((file) => file.path === component.path)!),
|
||||
});
|
||||
}
|
||||
const module = selection.module
|
||||
? selectArtifactImplementation(manifest, {
|
||||
...selection.module,
|
||||
@@ -68,7 +175,7 @@ export async function loadExecutionInstallation(
|
||||
);
|
||||
if (!browserExports.length) {
|
||||
if (selection.browser) fail(revision, "Package has no browser exports to select");
|
||||
packages.push({ revision, root, service, ...(module ? { module } : {}) });
|
||||
packages.push({ revision, root, ...(service ? { service } : {}), ...(module ? { module } : {}) });
|
||||
continue;
|
||||
}
|
||||
const browser = selectArtifactImplementation(manifest, {
|
||||
@@ -97,7 +204,7 @@ export async function loadExecutionInstallation(
|
||||
packages.push({
|
||||
revision,
|
||||
root,
|
||||
service,
|
||||
...(service ? { service } : {}),
|
||||
...(module ? { module } : {}),
|
||||
browserURL: contentArtifactUrl(entry),
|
||||
browserRegistry: browser.registryExport,
|
||||
@@ -108,6 +215,7 @@ export async function loadExecutionInstallation(
|
||||
plan: plan.digest,
|
||||
world: loaded.digest,
|
||||
packages,
|
||||
components,
|
||||
routes,
|
||||
...(coreURL ? { coreURL, engineURL } : {}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user