Implement checked workspace integration contracts (WI-01)
This commit is contained in:
@@ -27,6 +27,24 @@ export interface PackageArtifacts {
|
||||
sourceDigest: string;
|
||||
versions: ExecutionPlan["definition"]["versions"];
|
||||
implementations: ArtifactImplementation[];
|
||||
components?: ComponentArtifact[];
|
||||
}
|
||||
export const componentSharedModules = [
|
||||
"react",
|
||||
"react-dom",
|
||||
"react-dom/client",
|
||||
"react/jsx-runtime",
|
||||
"@quixos/camino-react",
|
||||
] as const;
|
||||
export interface ComponentArtifact {
|
||||
id: string;
|
||||
subject: string;
|
||||
compatibility: "quixos-react-component-v1";
|
||||
path: string;
|
||||
exportName: string;
|
||||
sharedModules: Record<(typeof componentSharedModules)[number], string>;
|
||||
files: ArtifactFile[];
|
||||
contentDigest: string;
|
||||
}
|
||||
export interface ImplementationSelection {
|
||||
target: string;
|
||||
@@ -119,12 +137,73 @@ export function implementationDigest(implementation: Omit<ArtifactImplementation
|
||||
files: [...implementation.files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)),
|
||||
});
|
||||
}
|
||||
export function componentArtifactDigest(component: Omit<ComponentArtifact, "contentDigest">): string {
|
||||
return hexDigest({
|
||||
...component,
|
||||
files: [...component.files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)),
|
||||
});
|
||||
}
|
||||
function parseArtifactFiles(value: unknown, path: string): ArtifactFile[] {
|
||||
const files = array(value, path).map((value): ArtifactFile => {
|
||||
const file = object(value, path);
|
||||
keys(file, ["path", "digest", "bytes", "mediaType"], [], path);
|
||||
if (!Number.isSafeInteger(file.bytes) || (file.bytes as number) < 0 || (file.bytes as number) > 256 * 1024 * 1024)
|
||||
fail(path, "Artifact file exceeds size bound");
|
||||
return {
|
||||
path: artifactPath(file.path),
|
||||
digest: digest(file.digest, path),
|
||||
bytes: file.bytes as number,
|
||||
mediaType: text(file.mediaType, path),
|
||||
};
|
||||
});
|
||||
unique(
|
||||
files.map((file) => file.path),
|
||||
path,
|
||||
);
|
||||
return files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
||||
}
|
||||
function parseComponent(value: unknown, index: number): ComponentArtifact {
|
||||
const path = `components[${index}]`,
|
||||
entry = object(value, path);
|
||||
keys(
|
||||
entry,
|
||||
["id", "subject", "compatibility", "path", "exportName", "sharedModules", "files", "contentDigest"],
|
||||
[],
|
||||
path,
|
||||
);
|
||||
if (entry.compatibility !== "quixos-react-component-v1") fail(path, "Unsupported component ABI");
|
||||
const files = parseArtifactFiles(entry.files, `${path}.files`);
|
||||
const shared = object(entry.sharedModules, `${path}.sharedModules`);
|
||||
keys(shared, [...componentSharedModules], [], `${path}.sharedModules`);
|
||||
const sharedModules = Object.fromEntries(
|
||||
componentSharedModules.map((name) => [name, artifactPath(shared[name])]),
|
||||
) as ComponentArtifact["sharedModules"];
|
||||
const entryPath = artifactPath(entry.path),
|
||||
exportName = text(entry.exportName, path);
|
||||
if (!/^[$A-Z_a-z][$\w]*$/.test(exportName)) fail(path, "Expected a JavaScript component export name");
|
||||
for (const modulePath of [entryPath, ...Object.values(sharedModules)])
|
||||
if (!files.some((file) => file.path === modulePath && file.mediaType === "text/javascript"))
|
||||
fail(path, "Component entry and shared modules require declared JavaScript files");
|
||||
const component: ComponentArtifact = {
|
||||
id: text(entry.id, path),
|
||||
subject: text(entry.subject, path),
|
||||
compatibility: "quixos-react-component-v1",
|
||||
path: entryPath,
|
||||
exportName,
|
||||
sharedModules,
|
||||
files,
|
||||
contentDigest: digest(entry.contentDigest, path),
|
||||
};
|
||||
const { contentDigest, ...body } = component;
|
||||
if (componentArtifactDigest(body) !== contentDigest) fail(path, "Component metadata digest mismatch");
|
||||
return component;
|
||||
}
|
||||
export function parsePackageArtifacts(value: unknown): PackageArtifacts {
|
||||
const record = object(value, "manifest");
|
||||
keys(
|
||||
record,
|
||||
["schemaVersion", "packageRevisionId", "bindingDigest", "sourceDigest", "versions", "implementations"],
|
||||
[],
|
||||
["components"],
|
||||
"manifest",
|
||||
);
|
||||
if (record.schemaVersion !== 1) fail("manifest", "Unsupported artifact manifest version");
|
||||
@@ -152,26 +231,7 @@ export function parsePackageArtifacts(value: unknown): PackageArtifacts {
|
||||
if (entry.compatibility === "quixos-js-module-v1") text(entry.registryExport, `${path}.registryExport`);
|
||||
const exports = array(entry.exports, `${path}.exports`).map((value) => text(value, `${path}.exports`));
|
||||
unique(exports, `${path}.exports`);
|
||||
const files = array(entry.files, `${path}.files`).map((value): ArtifactFile => {
|
||||
const file = object(value, `${path}.file`);
|
||||
keys(file, ["path", "digest", "bytes", "mediaType"], [], `${path}.file`);
|
||||
if (
|
||||
!Number.isSafeInteger(file.bytes) ||
|
||||
(file.bytes as number) < 0 ||
|
||||
(file.bytes as number) > 256 * 1024 * 1024
|
||||
)
|
||||
fail(path, "Artifact file exceeds size bound");
|
||||
return {
|
||||
path: artifactPath(file.path),
|
||||
digest: digest(file.digest, path),
|
||||
bytes: file.bytes as number,
|
||||
mediaType: text(file.mediaType, path),
|
||||
};
|
||||
});
|
||||
unique(
|
||||
files.map((file) => file.path),
|
||||
`${path}.files`,
|
||||
);
|
||||
const files = parseArtifactFiles(entry.files, `${path}.files`);
|
||||
const entryPath = artifactPath(entry.path);
|
||||
if (!files.some((file) => file.path === entryPath)) fail(path, "Entrypoint is not covered by the file manifest");
|
||||
const result: ArtifactImplementation = {
|
||||
@@ -194,6 +254,13 @@ export function parsePackageArtifacts(value: unknown): PackageArtifacts {
|
||||
implementations.map((implementation) => implementation.id),
|
||||
"implementations",
|
||||
);
|
||||
const components =
|
||||
record.components === undefined ? undefined : array(record.components, "components").map(parseComponent);
|
||||
if (components)
|
||||
unique(
|
||||
components.map((component) => component.id),
|
||||
"components",
|
||||
);
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
packageRevisionId: text(record.packageRevisionId, "packageRevisionId"),
|
||||
@@ -201,11 +268,20 @@ export function parsePackageArtifacts(value: unknown): PackageArtifacts {
|
||||
sourceDigest: digest(record.sourceDigest, "sourceDigest"),
|
||||
versions: versions as unknown as PackageArtifacts["versions"],
|
||||
implementations,
|
||||
...(components ? { components } : {}),
|
||||
};
|
||||
}
|
||||
function checkCoverage(expected: PlanPackage, manifest: PackageArtifacts): void {
|
||||
if (expected.bindingDigest !== manifest.bindingDigest || expected.sourceDigest !== manifest.sourceDigest)
|
||||
fail(expected.revision, "Substituted binding schema or source revision");
|
||||
for (const component of manifest.components ?? []) {
|
||||
const declaration = expected.components?.find((entry) => entry.id === component.id);
|
||||
if (!declaration || declaration.subject !== component.subject || declaration.runtime !== "react")
|
||||
fail(component.id, "Component differs from its declared subject/runtime contract");
|
||||
}
|
||||
for (const component of expected.components ?? [])
|
||||
if (!manifest.components?.some((entry) => entry.id === component.id))
|
||||
fail(component.id, "Missing component artifact");
|
||||
for (const implementation of manifest.implementations)
|
||||
for (const id of implementation.exports) {
|
||||
const entry = expected.exports.find((e) => e.id === id);
|
||||
@@ -272,6 +348,19 @@ export async function checkExecutionWorld(
|
||||
digest: implementation.contentDigest,
|
||||
});
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import type {
|
||||
InterfaceRevision,
|
||||
InterfaceOperation,
|
||||
ExecutionContract,
|
||||
PackageRevision,
|
||||
} from "../capability-model/types.js";
|
||||
import { canonicalJson } from "../capability-model/evolution.js";
|
||||
import { portableValueSchema, tsType } from "../bindings/portable.js";
|
||||
import { type ExecutionPlan, resolvePlannedOperation, hexDigest, ExecutionPlanError } from "./plan.js";
|
||||
|
||||
/** Public shape only: no slot inventory, implementation binding, or workspace identity. */
|
||||
export interface PublicOperationContract {
|
||||
interface: string;
|
||||
operation: string;
|
||||
scope: "object" | "class";
|
||||
execution: ExecutionContract;
|
||||
input: unknown;
|
||||
output: unknown;
|
||||
}
|
||||
export function publicOperationContract(
|
||||
iface: InterfaceRevision,
|
||||
operation: InterfaceOperation,
|
||||
): PublicOperationContract {
|
||||
if (iface.template || operation.mode !== "call" || !iface.members.some((m) => m.operations.includes(operation)))
|
||||
throw new ExecutionPlanError(
|
||||
"UNSUPPORTED_CLIENT_CONTRACT",
|
||||
iface.revisionId,
|
||||
"Expected a closed interface call operation",
|
||||
);
|
||||
const execution = operation.execution ?? { kind: "remote" };
|
||||
return {
|
||||
interface: iface.revisionId,
|
||||
operation: operation.id,
|
||||
scope: operation.scope ?? "object",
|
||||
execution: execution.kind === "remote" ? execution : { ...execution, targets: [...execution.targets].sort() },
|
||||
input: portableValueSchema(operation.inputType),
|
||||
output: portableValueSchema(operation.outputType),
|
||||
};
|
||||
}
|
||||
/** Resolve a public contract to internal dispatch. This does not authorize the receiver. */
|
||||
export function linkInterfaceOperation(plan: ExecutionPlan, atom: string, contract: PublicOperationContract) {
|
||||
if (hexDigest(plan.definition) !== plan.digest)
|
||||
throw new ExecutionPlanError("INVALID_PLAN", atom, "Invalid execution plan digest");
|
||||
const iface = plan.definition.workspace.interfaceImports.find((i) => i.revisionId === contract.interface);
|
||||
const operation = iface?.members.flatMap((m) => m.operations).find((o) => o.id === contract.operation);
|
||||
if (!iface || !operation || canonicalJson(publicOperationContract(iface, operation)) !== canonicalJson(contract))
|
||||
throw new ExecutionPlanError(
|
||||
"CLIENT_CONTRACT_MISMATCH",
|
||||
`${contract.interface}/${contract.operation}`,
|
||||
"Installed interface differs from the imported operation contract",
|
||||
);
|
||||
return resolvePlannedOperation(plan, atom, contract.interface, contract.operation);
|
||||
}
|
||||
|
||||
export interface PublicQueryContract {
|
||||
package: string;
|
||||
query: string;
|
||||
definitionDigest: string;
|
||||
root: { kind: "object" | "workspace-interface"; interface: string };
|
||||
placement: "server" | "replica" | "local";
|
||||
sourceQuery?: string;
|
||||
input: unknown;
|
||||
output: unknown;
|
||||
}
|
||||
export function publicQueryContract(pkg: PackageRevision, queryId: string): PublicQueryContract {
|
||||
const query = pkg.checkedQueries?.find((q) => q.declaration.id === queryId);
|
||||
if (!query)
|
||||
throw new ExecutionPlanError("UNCHECKED_QUERY", `${pkg.revisionId}/${queryId}`, "Expected a checked package query");
|
||||
const placement = query.declaration.placement;
|
||||
return {
|
||||
package: pkg.revisionId,
|
||||
query: queryId,
|
||||
definitionDigest: query.definitionDigest,
|
||||
root: query.declaration.root,
|
||||
placement: placement?.kind ?? "server",
|
||||
...(placement?.kind === "local" ? { sourceQuery: placement.sourceQueryId } : {}),
|
||||
input: portableValueSchema(query.variables),
|
||||
output: portableValueSchema(query.output),
|
||||
};
|
||||
}
|
||||
export function linkPackageQuery(plan: ExecutionPlan, contract: PublicQueryContract) {
|
||||
if (hexDigest(plan.definition) !== plan.digest)
|
||||
throw new ExecutionPlanError("INVALID_PLAN", contract.query, "Invalid execution plan digest");
|
||||
const pkg = plan.definition.workspace.packageImports.find((p) => p.revisionId === contract.package);
|
||||
if (!pkg || canonicalJson(publicQueryContract(pkg, contract.query)) !== canonicalJson(contract))
|
||||
throw new ExecutionPlanError(
|
||||
"CLIENT_QUERY_MISMATCH",
|
||||
`${contract.package}/${contract.query}`,
|
||||
"Installed query differs from the imported query contract",
|
||||
);
|
||||
const query = plan.definition.queries.find(
|
||||
(q) => q.query.packageRevisionId === contract.package && q.query.checked.declaration.id === contract.query,
|
||||
);
|
||||
if (!query) throw new ExecutionPlanError("UNINSTALLED_QUERY", contract.query, "No installed query binding");
|
||||
return query;
|
||||
}
|
||||
|
||||
/** Generate only the explicitly imported interfaces. No concrete atom inventory is needed. */
|
||||
export function generateInterfaceClientBindings(
|
||||
imports: Readonly<Record<string, InterfaceRevision>>,
|
||||
runtimeModule = "@quixos/camino-replica-core",
|
||||
): string {
|
||||
const entries = Object.entries(imports).map(([alias, iface]) => {
|
||||
if (iface.template)
|
||||
throw new ExecutionPlanError(
|
||||
"OPEN_CLIENT_CONTRACT",
|
||||
alias,
|
||||
"Specialize the interface before generating a client",
|
||||
);
|
||||
const members = iface.members.map((member) => {
|
||||
const operations = member.operations
|
||||
.filter((op) => op.mode === "call")
|
||||
.map(
|
||||
(op) =>
|
||||
`${JSON.stringify(op.displayName)}: defineInterfaceOperation<${tsType(op.inputType)}, ${tsType(op.outputType)}>(${JSON.stringify(publicOperationContract(iface, op))})`,
|
||||
);
|
||||
return `${JSON.stringify(member.displayName)}: {${operations.join(",\n")}}`;
|
||||
});
|
||||
return `${JSON.stringify(alias)}: {${members.join(",\n")}}`;
|
||||
});
|
||||
return `// Generated from imported interface contracts. Regenerate; do not edit.\nimport { defineInterfaceOperation, type PreparedChange, type ManagedReference } from ${JSON.stringify(runtimeModule)};\nexport const interfaces = {${entries.join(",\n")}} as const;\n`;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ export * from "./artifacts.js";
|
||||
export * from "./ports.js";
|
||||
export * from "./files.js";
|
||||
export * from "./installation.js";
|
||||
export * from "./client-contracts.js";
|
||||
|
||||
export * from "./receipts.js";
|
||||
|
||||
|
||||
@@ -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 } : {}),
|
||||
};
|
||||
|
||||
@@ -58,6 +58,8 @@ export interface PlannedQuery {
|
||||
admittedOperations: string[];
|
||||
}
|
||||
export interface PlanPackage {
|
||||
controllers?: NonNullable<WorkspaceRevision["packageImports"][number]["controllers"]>;
|
||||
components?: NonNullable<WorkspaceRevision["packageImports"][number]["components"]>;
|
||||
revision: string;
|
||||
source: WorkspaceRevision["packageImports"][number]["source"];
|
||||
sourceDigest: string;
|
||||
@@ -65,6 +67,7 @@ export interface PlanPackage {
|
||||
exports: { id: string; execution: ExecutionContract }[];
|
||||
}
|
||||
export interface ExecutionPlanDefinition {
|
||||
presentations?: NonNullable<WorkspaceRevision["presentations"]>;
|
||||
codec: "camino-execution-plan-v1";
|
||||
workspace: WorkspaceRevision;
|
||||
bindingSchema: BindingSchema;
|
||||
@@ -421,11 +424,14 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
|
||||
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.controllers?.length ? { controllers: sorted(p.controllers, (controller) => controller.id) } : {}),
|
||||
...(p.components?.length ? { components: sorted(p.components, (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) => {
|
||||
|
||||
Reference in New Issue
Block a user