Arboricx bundle format 1.1

We don't need SHA verification or Merkle dags in our transport bundle. Content
stores can handle both bundle and term verification and hashing.
This commit is contained in:
2026-05-11 19:53:37 -05:00
parent e0b1e95729
commit 31bf7094f4
45 changed files with 4032 additions and 7127 deletions

View File

@@ -1,249 +1,104 @@
#!/usr/bin/env node
/**
* cli.js — Minimal CLI for inspecting and running Arboricx bundles.
* cli.js — Arboricx JS host shell via libarboricx C ABI.
*
* Usage:
* node cli.js inspect <bundle>
* node cli.js run <bundle> [exportName] [input]
* node cli.js inspect <bundle.arboricx>
* node cli.js run <bundle.arboricx> [args...]
*/
import { readFileSync } from "node:fs";
import { parseBundle, parseManifest } from "./bundle.js";
import { parseNodeSection as parseNodeSectionMerkle } from "./merkle.js";
import { readFileSync } from 'node:fs';
import {
validateManifest,
selectExport,
printManifestInfo,
} from "./manifest.js";
import { parseNodeSection as parseNodeSectionBundle } from "./bundle.js";
import {
verifyNodeHashes,
verifyClosure,
verifyRootClosure,
} from "./merkle.js";
import { isTree, apply, triage, isFork, isStem } from "./tree.js";
import { decodeResult, formatTree } from "./codecs.js";
init,
free,
loadBundleDefault,
reduce,
app,
ofNumber,
ofString,
decode,
decodeType,
findLib,
} from './lib.js';
// ── Commands ────────────────────────────────────────────────────────────────
// ── Commands ────────────────────────────────────────────────────────────────
function cmdInspect(bundlePath) {
const buffer = readFileSync(bundlePath);
const ctx = init();
try {
const manifest = parseManifest(buffer);
validateManifest(manifest);
const nodeSectionBytes = parseNodeSectionBundle(buffer);
const { nodeMap } = parseNodeSectionMerkle(nodeSectionBytes);
const bundle = readFileSync(bundlePath);
console.log(`Bundle: ${bundlePath}`);
console.log("");
console.log(`Size: ${bundle.length} bytes\n`);
printManifestInfo(manifest, " ");
const term = loadBundleDefault(ctx, bundle);
const result = reduce(ctx, term);
console.log(` Nodes: ${nodeMap.size}`);
// Verify hashes
const { verified: hashesOk, mismatches } = verifyNodeHashes(nodeMap);
console.log(` Hash verification: ${hashesOk ? "OK" : "FAIL"}`);
for (const m of mismatches) {
console.log(` MISMATCH ${m.type} ${m.hash.substring(0, 16)}... expected ${m.expected.substring(0, 16)}...`);
const type = decodeType(ctx, result);
let value;
try {
value = decode(ctx, result);
} catch {
value = '(raw tree)';
}
// Verify closure
const { complete: closureOk, missing } = verifyClosure(nodeMap);
console.log(` Closure verification: ${closureOk ? "OK" : "FAIL"}`);
for (const m of missing) {
console.log(` MISSING ${m.parent.substring(0, 16)}... → ${m.child.substring(0, 16)}...`);
}
// Verify root closure for each export
for (const exp of manifest.exports || []) {
const { complete, missingRoots } = verifyRootClosure(
nodeMap,
exp.root
);
if (!complete) {
console.log(
` Root closure for "${exp.name}": FAIL — missing: ${missingRoots
.map((r) => r.substring(0, 16) + "...")
.join(", ")}`
);
}
}
console.log("");
console.log("Inspection complete.");
console.log(`Type: ${type}`);
console.log(`Value: ${value}`);
} catch (e) {
console.error(`Error: ${e.message}`);
process.exit(1);
} finally {
free(ctx);
}
}
function cmdRun(bundlePath, exportName, inputArg) {
const buffer = readFileSync(bundlePath);
let result;
function cmdRun(bundlePath, args) {
const ctx = init();
try {
const manifest = parseManifest(buffer);
validateManifest(manifest);
const bundle = readFileSync(bundlePath);
let term = loadBundleDefault(ctx, bundle);
const selectedExport = selectExport(manifest, exportName);
const nodeSectionBytes = parseNodeSectionBundle(buffer);
const { nodeMap } = parseNodeSectionMerkle(nodeSectionBytes);
// Verify hashes
const { verified, mismatches } = verifyNodeHashes(nodeMap);
if (!verified) {
console.error(
`Node hash mismatch:\n ${mismatches
.map((m) => ` ${m.type}: ${m.hash} (expected ${m.expected})`)
.join("\n")}`
);
process.exit(1);
for (const arg of args) {
const argTree = /^\d+$/.test(arg) ? ofNumber(ctx, BigInt(arg)) : ofString(ctx, arg);
term = app(ctx, term, argTree);
}
// Reconstruct the tree for the selected export
const root = buildTreeFromNodeMap(nodeMap, selectedExport.root);
if (!isTree(root)) {
console.error("Reconstructed root is not a valid tree value");
process.exit(1);
}
// Apply input if provided
let term = root;
if (inputArg !== undefined) {
// TODO: parse input (string/number) into a tree
// For now, just run the term as-is
}
// Reduce with fuel limit
const finalTerm = reduce(term, 1_000_000);
// Print result as tree calculus form
console.log(formatTree(finalTerm));
const result = reduce(ctx, term);
console.log(decode(ctx, result));
} catch (e) {
console.error(`Error: ${e.message}`);
process.exit(1);
} finally {
free(ctx);
}
}
// ── Tree reconstruction ─────────────────────────────────────────────────────
/**
* Reconstruct a tree from a node map.
*
* Node map: Map<hexHash, { type, childHash?, leftHash?, rightHash? }>
*
* Returns the tree representation: [] for Leaf, [child] for Stem, [right, left] for Fork.
* Uses memoization to avoid re-processing nodes.
*/
export function buildTreeFromNodeMap(nodeMap, hash, memo = new Map()) {
if (memo.has(hash)) return memo.get(hash);
const node = nodeMap.get(hash);
if (!node) {
throw new Error(`missing node in bundle: ${hash}`);
}
let tree;
switch (node.type) {
case "leaf":
tree = [];
break;
case "stem":
tree = [buildTreeFromNodeMap(nodeMap, node.childHash, memo)];
break;
case "fork":
tree = [
buildTreeFromNodeMap(nodeMap, node.rightHash, memo),
buildTreeFromNodeMap(nodeMap, node.leftHash, memo),
];
break;
default:
throw new Error(`unknown node type: ${node.type}`);
}
memo.set(hash, tree);
return tree;
}
// ── Reduction ───────────────────────────────────────────────────────────────
/**
* Reduce a term to normal form with a fuel limit.
* Uses the stack-based approach from the TS evaluator.
*/
export function reduce(term, fuel) {
const stack = [term];
let remaining = fuel;
while (stack.length >= 2 && remaining-- > 0) {
// Pop right (top), then left
const b = stack.pop(); // right
const a = stack.pop(); // left
if (stack.length >= 2) {
// Push a back for potential further reduction
stack.push(a);
}
const result = apply(a, b);
if (isTree(result)) {
// If result is a value, push it. But if it's a Fork/Stem,
// we need to push its components for further reduction.
if (isFork(result)) {
// Push right first (so it's popped second), then left
stack.push(result[1]); // left
stack.push(result[0]); // right
} else if (isStem(result)) {
stack.push(result[0]); // child
} else {
stack.push(result); // Leaf
}
} else {
// Not a tree — push as-is (shouldn't happen after buildTree)
stack.push(result);
}
}
if (remaining <= 0) {
throw new Error("reduction step limit exceeded");
}
if (stack.length === 1) {
return stack[0];
}
return stack[0]; // fallback
}
// ── Main ────────────────────────────────────────────────────────────────────
// ── Main ─────────────────────────────────────────────────────────────────────
const args = process.argv.slice(2);
const command = args[0];
switch (command) {
case "inspect": {
case 'inspect': {
if (args.length < 2) {
console.error("Usage: node cli.js inspect <bundle>");
console.error('Usage: node cli.js inspect <bundle.arboricx>');
process.exit(1);
}
cmdInspect(args[1]);
break;
}
case "run": {
case 'run': {
if (args.length < 2) {
console.error("Usage: node cli.js run <bundle> [exportName] [input]");
console.error('Usage: node cli.js run <bundle.arboricx> [args...]');
process.exit(1);
}
cmdRun(args[1], args[2], args[3]);
cmdRun(args[1], args.slice(2));
break;
}
default:
console.log("Arboricx JS Runtime");
console.log("");
console.log("Usage:");
console.log(" node cli.js inspect <bundle>");
console.log(" node cli.js run <bundle> [exportName] [input]");
console.log('Arboricx JS Host (via libarboricx FFI)');
console.log('');
console.log('Usage:');
console.log(' node cli.js inspect <bundle.arboricx>');
console.log(' node cli.js run <bundle.arboricx> [args...]');
break;
}