guanghu-ice-heart/server-tools/codex-hldp-recursive-memory/hldp-memory.mjs

135 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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; }
}