feat(hldp): review causal note quality
This commit is contained in:
parent
fb1096c5ec
commit
090fe333d4
3 changed files with 69 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
Loading…
Reference in a new issue