#!/usr/bin/env node import fs from "node:fs"; import { fileURLToPath } from "node:url"; export const BOOTSTRAP_LIMIT_BYTES = 2048; export const READ_NODE_LIMIT_BYTES = 16384; function fail(message) { throw new Error(message); } export function loadTree(file) { return JSON.parse(fs.readFileSync(file, "utf8")); } export function verifyTree(tree) { const errors = []; if (tree?.protocol !== "HLDP-v1.0") errors.push("protocol must be HLDP-v1.0"); if (!tree?.root || typeof tree.root !== "string") errors.push("root must be a non-empty path"); if (!tree?.nodes || typeof tree.nodes !== "object" || Array.isArray(tree.nodes)) errors.push("nodes must be an object"); if (errors.length) return errors; if (!tree.nodes[tree.root]) errors.push(`root node not found: ${tree.root}`); const required = ["path", "summary", "trigger", "emergence", "lock", "why"]; for (const [key, node] of Object.entries(tree.nodes)) { if (!node || typeof node !== "object") { errors.push(`${key}: node must be an object`); continue; } if (node.path !== key) errors.push(`${key}: node.path must equal its address`); for (const field of required) { if (typeof node[field] !== "string" || !node[field].trim()) errors.push(`${key}: missing ${field}`); } if (!Array.isArray(node.children)) errors.push(`${key}: children must be an array`); else { if (node.children.length > 10) errors.push(`${key}: more than 10 children`); if (new Set(node.children).size !== node.children.length) errors.push(`${key}: duplicate child path`); for (const child of node.children) if (!tree.nodes[child]) errors.push(`${key}: child not found: ${child}`); } if (!Array.isArray(node.sources) || node.sources.length === 0) errors.push(`${key}: sources must be a non-empty array`); if (!Array.isArray(node.rejected)) errors.push(`${key}: rejected must be an array`); } const visiting = new Set(); const visited = new Set(); function walk(key) { if (visiting.has(key)) return errors.push(`${key}: cycle detected`); if (visited.has(key) || !tree.nodes[key]) return; visiting.add(key); for (const child of tree.nodes[key].children ?? []) walk(child); visiting.delete(key); visited.add(key); } walk(tree.root); for (const key of Object.keys(tree.nodes)) if (!visited.has(key)) errors.push(`${key}: unreachable from root`); return errors; } function terms(query) { const raw = query.toLowerCase().match(/[\p{L}\p{N}_-]+/gu) ?? []; const expanded = []; for (const token of raw) { expanded.push(token); const characters = [...token]; if (/\p{Script=Han}/u.test(token) && characters.length > 1) { for (let i = 0; i < characters.length - 1; i += 1) { expanded.push(characters.slice(i, i + 2).join("")); } } } return [...new Set(expanded)]; } export function route(tree, query, max = 3, from = tree.root) { const current = tree.nodes[from]; if (!current) fail(`node not found: ${from}`); const needles = terms(query); return current.children .map((childPath, index) => { const child = tree.nodes[childPath]; const haystack = `${childPath} ${child?.summary ?? ""}`.toLowerCase(); const score = needles.reduce((sum, term) => sum + (haystack.includes(term) ? 1 : 0), 0); return { path: childPath, summary: child?.summary ?? "", score, order: index }; }) .sort((a, b) => b.score - a.score || a.order - b.order) .slice(0, Math.max(1, Math.min(3, Number(max) || 3))) .map(({ order: _order, ...candidate }) => candidate); } export function readNode(tree, nodePath, limit = READ_NODE_LIMIT_BYTES) { const node = tree.nodes[nodePath]; if (!node) fail(`node not found: ${nodePath}`); const encoded = JSON.stringify(node, null, 2); if (Buffer.byteLength(encoded) > limit) fail(`node exceeds ${limit} byte read limit`); return node; } export function bootstrap(protocolRoot) { const message = [ "HLDP v1 单对话恢复:只把协议根页作为导航,不自动加载记忆正文。", `根页:${protocolRoot}`, "边界:人格主体 != Codex宿主 != 运行系统 != 任务角色 != 执行授权。", "方法:先看根节点摘要;按当前问题一次下钻一层;仅证据缺口时读原文。", "保留:trigger/emergence/lock/why、转折与否决原因、失效条件、sources。", "禁止:整读胶囊/日志;每消息或工具调用注入;恢复失败阻断用户输入。" ].join("\n"); if (Buffer.byteLength(message) > BOOTSTRAP_LIMIT_BYTES) fail("bootstrap exceeds hard byte limit"); return message; } function parseArgs(argv) { const [command, ...rest] = argv; const options = {}; for (let i = 0; i < rest.length; i += 2) options[rest[i]?.replace(/^--/, "")] = rest[i + 1]; return { command, options }; } export function main(argv = process.argv.slice(2)) { const { command, options } = parseArgs(argv); if (command === "bootstrap") { console.log(bootstrap(options["protocol-root"] ?? "hldp/HLDP-RUNTIME-ROOT-v1.0.hdlp")); return; } if (!options.tree) fail("--tree is required"); const tree = loadTree(options.tree); const errors = verifyTree(tree); if (errors.length) fail(errors.join("\n")); if (command === "verify") console.log(JSON.stringify({ ok: true, nodes: Object.keys(tree.nodes).length })); else if (command === "route") console.log(JSON.stringify(route(tree, options.query ?? "", options.max, options.from), null, 2)); else if (command === "read-node") console.log(JSON.stringify(readNode(tree, options.path), null, 2)); else fail("command must be bootstrap, verify, route, or read-node"); } if (process.argv[1] === fileURLToPath(import.meta.url)) { try { main(); } catch (error) { console.error(error.message); process.exitCode = 1; } }