168 lines
5.0 KiB
JavaScript
168 lines
5.0 KiB
JavaScript
/**
|
|
* manifest.js — Minimal manifest parsing and export lookup.
|
|
*
|
|
* The manifest is a JSON object with fields:
|
|
* schema, bundleType, tree, runtime, closure, roots, exports,
|
|
* imports, sections, metadata
|
|
*
|
|
* We parse only what we need for runtime entrypoint selection.
|
|
*/
|
|
|
|
/**
|
|
* Validate the manifest against the runtime profile requirements.
|
|
* Throws on violation.
|
|
*/
|
|
export function validateManifest(manifest) {
|
|
if (manifest.schema !== "arboricx.bundle.manifest.v1") {
|
|
throw new Error(
|
|
`unsupported manifest schema: ${manifest.schema}`
|
|
);
|
|
}
|
|
if (manifest.bundleType !== "tree-calculus-executable-object") {
|
|
throw new Error(
|
|
`unsupported bundle type: ${manifest.bundleType}`
|
|
);
|
|
}
|
|
|
|
const tree = manifest.tree;
|
|
if (tree.calculus !== "tree-calculus.v1") {
|
|
throw new Error(`unsupported calculus: ${tree.calculus}`);
|
|
}
|
|
if (tree.nodeHash.algorithm !== "sha256") {
|
|
throw new Error(
|
|
`unsupported node hash algorithm: ${tree.nodeHash.algorithm}`
|
|
);
|
|
}
|
|
if (tree.nodeHash.domain !== "arboricx.merkle.node.v1") {
|
|
throw new Error(
|
|
`unsupported node hash domain: ${tree.nodeHash.domain}`
|
|
);
|
|
}
|
|
if (tree.nodePayload !== "arboricx.merkle.payload.v1") {
|
|
throw new Error(`unsupported node payload: ${tree.nodePayload}`);
|
|
}
|
|
|
|
const runtime = manifest.runtime;
|
|
if (runtime.semantics !== "tree-calculus.v1") {
|
|
throw new Error(`unsupported runtime semantics: ${runtime.semantics}`);
|
|
}
|
|
if (runtime.abi !== "arboricx.abi.tree.v1") {
|
|
throw new Error(`unsupported runtime ABI: ${runtime.abi}`);
|
|
}
|
|
if (runtime.capabilities && runtime.capabilities.length > 0) {
|
|
throw new Error(
|
|
`host/runtime capabilities not supported: ${runtime.capabilities.join(", ")}`
|
|
);
|
|
}
|
|
|
|
if (manifest.closure !== "complete") {
|
|
throw new Error("bundle v1 requires closure = complete");
|
|
}
|
|
if (manifest.imports && manifest.imports.length > 0) {
|
|
throw new Error("bundle v1 requires an empty imports list");
|
|
}
|
|
if (!manifest.roots || manifest.roots.length === 0) {
|
|
throw new Error("manifest has no roots");
|
|
}
|
|
if (!manifest.exports || manifest.exports.length === 0) {
|
|
throw new Error("manifest has no exports");
|
|
}
|
|
|
|
for (const exp of manifest.exports) {
|
|
if (!exp.name) {
|
|
throw new Error("manifest export has empty name");
|
|
}
|
|
if (!exp.root) {
|
|
throw new Error("manifest export has empty root");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Select an export hash given a requested name.
|
|
*
|
|
* Selection strategy:
|
|
* 1. Explicit export name
|
|
* 2. Export named "main"
|
|
* 3. Single export (auto-select)
|
|
* 4. Error if multiple exports and no "main"
|
|
*/
|
|
export function selectExport(manifest, requestedName) {
|
|
const exports = manifest.exports || [];
|
|
|
|
// Strategy 1: explicit name
|
|
if (requestedName) {
|
|
const found = exports.find((e) => e.name === requestedName);
|
|
if (found) {
|
|
return found;
|
|
}
|
|
throw new Error(
|
|
`requested export "${requestedName}" not found. Available: ${exports.map((e) => e.name).join(", ")}`
|
|
);
|
|
}
|
|
|
|
// Strategy 2: prefer "main"
|
|
const mainExport = exports.find((e) => e.name === "main");
|
|
if (mainExport) {
|
|
return mainExport;
|
|
}
|
|
|
|
// Strategy 3: single export
|
|
if (exports.length === 1) {
|
|
return exports[0];
|
|
}
|
|
|
|
// Strategy 4: multiple exports, require explicit
|
|
throw new Error(
|
|
`multiple exports available but none named "main": ${exports.map((e) => e.name).join(", ")}. Specify an export name.`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get all root hashes from the manifest.
|
|
*/
|
|
export function getRootHashes(manifest) {
|
|
return (manifest.roots || []).map((r) => r.hash);
|
|
}
|
|
|
|
/**
|
|
* Get all export names.
|
|
*/
|
|
export function getExportNames(manifest) {
|
|
return (manifest.exports || []).map((e) => e.name);
|
|
}
|
|
|
|
/**
|
|
* Print manifest summary info.
|
|
*/
|
|
export function printManifestInfo(manifest, indent = "") {
|
|
const tree = manifest.tree;
|
|
const runtime = manifest.runtime;
|
|
|
|
console.log(`${indent}Schema: ${manifest.schema}`);
|
|
console.log(`${indent}Bundle type: ${manifest.bundleType}`);
|
|
console.log(`${indent}Closure: ${manifest.closure}`);
|
|
console.log(`${indent}Tree calculus: ${tree.calculus}`);
|
|
console.log(`${indent}Hash algo: ${tree.nodeHash.algorithm}`);
|
|
console.log(`${indent}Hash domain: ${tree.nodeHash.domain}`);
|
|
console.log(`${indent}Runtime: ${runtime.semantics}`);
|
|
console.log(`${indent}ABI: ${runtime.abi}`);
|
|
console.log(`${indent}Evaluation: ${runtime.evaluation || "N/A"}`);
|
|
console.log("");
|
|
console.log(`${indent}Roots (${getRootHashes(manifest).length}):`);
|
|
for (const root of getRootHashes(manifest)) {
|
|
console.log(`${indent} ${root.substring(0, 16)}...`);
|
|
}
|
|
console.log("");
|
|
console.log(`${indent}Exports (${getExportNames(manifest).length}):`);
|
|
for (const name of getExportNames(manifest)) {
|
|
console.log(`${indent} ${name}`);
|
|
}
|
|
|
|
const meta = manifest.metadata;
|
|
if (meta && meta.createdBy) {
|
|
console.log("");
|
|
console.log(`${indent}Created by: ${meta.createdBy}`);
|
|
}
|
|
}
|