#!/usr/bin/env node /** * cli.js — Minimal CLI for inspecting and running Arborix bundles. * * Usage: * node cli.js inspect * node cli.js run [exportName] [input] */ import { readFileSync } from "node:fs"; import { parseBundle, parseManifest } from "./bundle.js"; import { parseNodeSection as parseNodeSectionMerkle } from "./merkle.js"; 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"; // ── Commands ──────────────────────────────────────────────────────────────── function cmdInspect(bundlePath) { const buffer = readFileSync(bundlePath); try { const manifest = parseManifest(buffer); validateManifest(manifest); const nodeSectionBytes = parseNodeSectionBundle(buffer); const { nodeMap } = parseNodeSectionMerkle(nodeSectionBytes); console.log(`Bundle: ${bundlePath}`); console.log(""); printManifestInfo(manifest, " "); 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)}...`); } // 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."); } catch (e) { console.error(`Error: ${e.message}`); process.exit(1); } } function cmdRun(bundlePath, exportName, inputArg) { const buffer = readFileSync(bundlePath); let result; try { const manifest = parseManifest(buffer); validateManifest(manifest); 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); } // 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)); } catch (e) { console.error(`Error: ${e.message}`); process.exit(1); } } // ── Tree reconstruction ───────────────────────────────────────────────────── /** * Reconstruct a tree from a node map. * * Node map: Map * * 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 ──────────────────────────────────────────────────────────────────── const args = process.argv.slice(2); const command = args[0]; switch (command) { case "inspect": { if (args.length < 2) { console.error("Usage: node cli.js inspect "); process.exit(1); } cmdInspect(args[1]); break; } case "run": { if (args.length < 2) { console.error("Usage: node cli.js run [exportName] [input]"); process.exit(1); } cmdRun(args[1], args[2], args[3]); break; } default: console.log("Arborix JS Runtime"); console.log(""); console.log("Usage:"); console.log(" node cli.js inspect "); console.log(" node cli.js run [exportName] [input]"); break; }