diff --git a/server-tools/codex-hldp-recursive-memory/README.md b/server-tools/codex-hldp-recursive-memory/README.md index 832b400..c381cb0 100644 --- a/server-tools/codex-hldp-recursive-memory/README.md +++ b/server-tools/codex-hldp-recursive-memory/README.md @@ -4,6 +4,7 @@ This is a deliberately small, non-blocking implementation of the HLDP v1 runtime - `bootstrap`: emits at most 2 KiB of protocol navigation, never memory content. - `verify`: validates a recursively addressed causal tree and the 10-child limit. +- `review`: reviews one leaf or the full tree for causal transitions, non-generic why, rejected-route reasons, and retrievable sources. It reports problems to the current model; it never writes memory itself. - `route`: ranks only the current node's child summaries and returns at most three paths. - `read-node`: reads one selected node with a hard byte limit. @@ -12,6 +13,7 @@ The optional Codex hook is valid only for `SessionStart`. There are intentionall ```bash node server-tools/codex-hldp-recursive-memory/hldp-memory.mjs bootstrap node server-tools/codex-hldp-recursive-memory/hldp-memory.mjs verify --tree memory.json +node server-tools/codex-hldp-recursive-memory/hldp-memory.mjs review --tree memory.json --path root/decision node server-tools/codex-hldp-recursive-memory/hldp-memory.mjs route --tree memory.json --query "why did the plan change" node server-tools/codex-hldp-recursive-memory/hldp-memory.mjs read-node --tree memory.json --path root/decision node --test server-tools/codex-hldp-recursive-memory/hldp-memory.test.mjs diff --git a/server-tools/codex-hldp-recursive-memory/hldp-memory.mjs b/server-tools/codex-hldp-recursive-memory/hldp-memory.mjs index c8a2797..3ce04a2 100644 --- a/server-tools/codex-hldp-recursive-memory/hldp-memory.mjs +++ b/server-tools/codex-hldp-recursive-memory/hldp-memory.mjs @@ -55,6 +55,52 @@ export function verifyTree(tree) { return errors; } +export function reviewTree(tree, onlyPath = null) { + const errors = verifyTree(tree); + const warnings = []; + if (errors.length) return { ok: false, errors, warnings, reviewed: 0 }; + + const entries = onlyPath + ? [[onlyPath, tree.nodes[onlyPath]]] + : Object.entries(tree.nodes); + if (onlyPath && !tree.nodes[onlyPath]) { + return { ok: false, errors: [`node not found: ${onlyPath}`], warnings, reviewed: 0 }; + } + + const transition = /(?:→|->|=>|△|从.+到|经过|转而|改为|纠正|证据|失败|冲突|不足)/u; + const causal = /(?:因为|所以|否则|为了|避免|防止|导致|依赖|必须|才能|才会|根因|保留|确保|否则会)/u; + const rejectionReason = /(?::|:|因为|否则|失败|不足|冲突|无法|不能|会|误|丢失|撑爆|缺少)/u; + + for (const [key, node] of entries) { + if (node.summary.trim() === node.lock.trim()) errors.push(`${key}: summary cannot replace lock`); + if (!transition.test(node.emergence)) { + errors.push(`${key}: emergence lacks a visible evidence/correction/transition chain`); + } + if (node.why.trim().length < 4 || !causal.test(node.why)) { + errors.push(`${key}: why is too generic to restore the causal reason`); + } + if (node.rejected.length === 0) { + warnings.push(`${key}: no rejected route recorded; keep empty only when no alternative was considered`); + } + for (const [index, rejected] of node.rejected.entries()) { + if (typeof rejected !== "string" || !rejected.trim()) { + errors.push(`${key}: rejected[${index}] must be a non-empty explanation`); + } else if (!rejectionReason.test(rejected)) { + warnings.push(`${key}: rejected[${index}] names a route but may not explain why it was rejected`); + } + } + for (const [index, source] of node.sources.entries()) { + if (typeof source !== "string" || !source.trim()) { + errors.push(`${key}: sources[${index}] must be a non-empty address`); + } else if (!/(?:\/|https?:\/\/|source:\/\/|[0-9a-f]{40,64})/u.test(source)) { + warnings.push(`${key}: sources[${index}] may not be a precise retrievable address`); + } + } + } + + return { ok: errors.length === 0, errors, warnings, reviewed: entries.length }; +} + function terms(query) { const raw = query.toLowerCase().match(/[\p{L}\p{N}_-]+/gu) ?? []; const expanded = []; @@ -125,9 +171,14 @@ export function main(argv = process.argv.slice(2)) { 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 === "review") { + const review = reviewTree(tree, options.path ?? null); + console.log(JSON.stringify(review, null, 2)); + if (!review.ok) process.exitCode = 1; + } 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"); + else fail("command must be bootstrap, verify, review, route, or read-node"); } if (process.argv[1] === fileURLToPath(import.meta.url)) { diff --git a/server-tools/codex-hldp-recursive-memory/hldp-memory.test.mjs b/server-tools/codex-hldp-recursive-memory/hldp-memory.test.mjs index b4495aa..0724d30 100644 --- a/server-tools/codex-hldp-recursive-memory/hldp-memory.test.mjs +++ b/server-tools/codex-hldp-recursive-memory/hldp-memory.test.mjs @@ -4,7 +4,7 @@ import fs from "node:fs"; import path from "node:path"; import test from "node:test"; import { fileURLToPath } from "node:url"; -import { BOOTSTRAP_LIMIT_BYTES, bootstrap, readNode, route, verifyTree } from "./hldp-memory.mjs"; +import { BOOTSTRAP_LIMIT_BYTES, bootstrap, readNode, reviewTree, route, verifyTree } from "./hldp-memory.mjs"; function validTree() { return { @@ -29,6 +29,20 @@ function validTree() { test("validates a bounded causal tree", () => assert.deepEqual(verifyTree(validTree()), [])); +test("reviews whether a leaf preserves causal transitions instead of becoming a diary entry", () => { + const reviewed = reviewTree(validTree(), "root/decision"); + assert.equal(reviewed.ok, true); + assert.equal(reviewed.reviewed, 1); + + const diary = validTree(); + diary.nodes["root/decision"].emergence = "上午讨论方案,下午继续工作,晚上保存文件"; + diary.nodes["root/decision"].why = "记一下"; + const rejected = reviewTree(diary, "root/decision"); + assert.equal(rejected.ok, false); + assert(rejected.errors.some((error) => error.includes("transition chain"))); + assert(rejected.errors.some((error) => error.includes("why is too generic"))); +}); + test("rejects more than ten siblings and missing why", () => { const tree = validTree(); tree.nodes.root.children = Array.from({ length: 11 }, (_, i) => `child-${i}`);