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.
|
- `bootstrap`: emits at most 2 KiB of protocol navigation, never memory content.
|
||||||
- `verify`: validates a recursively addressed causal tree and the 10-child limit.
|
- `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.
|
- `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.
|
- `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
|
```bash
|
||||||
node server-tools/codex-hldp-recursive-memory/hldp-memory.mjs bootstrap
|
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 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 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 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
|
node --test server-tools/codex-hldp-recursive-memory/hldp-memory.test.mjs
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,52 @@ export function verifyTree(tree) {
|
||||||
return errors;
|
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) {
|
function terms(query) {
|
||||||
const raw = query.toLowerCase().match(/[\p{L}\p{N}_-]+/gu) ?? [];
|
const raw = query.toLowerCase().match(/[\p{L}\p{N}_-]+/gu) ?? [];
|
||||||
const expanded = [];
|
const expanded = [];
|
||||||
|
|
@ -125,9 +171,14 @@ export function main(argv = process.argv.slice(2)) {
|
||||||
const errors = verifyTree(tree);
|
const errors = verifyTree(tree);
|
||||||
if (errors.length) fail(errors.join("\n"));
|
if (errors.length) fail(errors.join("\n"));
|
||||||
if (command === "verify") console.log(JSON.stringify({ ok: true, nodes: Object.keys(tree.nodes).length }));
|
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 === "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 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)) {
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import { fileURLToPath } from "node:url";
|
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() {
|
function validTree() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -29,6 +29,20 @@ function validTree() {
|
||||||
|
|
||||||
test("validates a bounded causal tree", () => assert.deepEqual(verifyTree(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", () => {
|
test("rejects more than ten siblings and missing why", () => {
|
||||||
const tree = validTree();
|
const tree = validTree();
|
||||||
tree.nodes.root.children = Array.from({ length: 11 }, (_, i) => `child-${i}`);
|
tree.nodes.root.children = Array.from({ length: 11 }, (_, i) => `child-${i}`);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue