@theseus-whale/dsh-continuity 0.0.0-stage → 4.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +25 -0
- package/PANEL.md +92 -0
- package/README.md +286 -2
- package/RELEASE-NOTES.md +732 -0
- package/bin/bc-acceptance-full.mjs +91 -0
- package/bin/bc-acceptance-tight.mjs +66 -0
- package/bin/bc-acceptance.mjs +22 -0
- package/bin/bc-add-keywords.mjs +21 -0
- package/bin/bc-demo.mjs +47 -0
- package/bin/bc-expand-hint.mjs +179 -0
- package/bin/bc-ingest-index.mjs +35 -0
- package/bin/bc-ingest.mjs +31 -0
- package/bin/bc-label-demo.mjs +18 -0
- package/bin/bc-relabel.mjs +150 -0
- package/bin/bc-sessionize.mjs +59 -0
- package/bin/coref_pipeline.py +47 -0
- package/bin/coref_service.py +57 -0
- package/bin/debug_ckip.py +16 -0
- package/bin/debug_ner_zh.py +18 -0
- package/bin/doctor.mjs +275 -0
- package/bin/en_ner_error_audit.py +33 -0
- package/bin/eval-label-rules.mjs +342 -0
- package/bin/export-turns.py +68 -0
- package/bin/extract-user-paragraph.mjs +14 -0
- package/bin/extraction-composite-smoke.mjs +28 -0
- package/bin/extraction-latency.mjs +58 -0
- package/bin/extraction-user-smoke.mjs +28 -0
- package/bin/extraction_service.py +164 -0
- package/bin/facet-active-experiment.mjs +110 -0
- package/bin/facet-covid.mjs +8 -0
- package/bin/facet-curve.mjs +27 -0
- package/bin/facet-embedding-train.mjs +20 -0
- package/bin/facet-embedding-validate.mjs +83 -0
- package/bin/facet-eval-roleplay.mjs +42 -0
- package/bin/facet-eval.mjs +38 -0
- package/bin/facet-fold.mjs +31 -0
- package/bin/facet-load.mjs +16 -0
- package/bin/facet-mlp-scan.mjs +45 -0
- package/bin/facet-mvp.mjs +34 -0
- package/bin/facet-online-experiments.mjs +78 -0
- package/bin/facet-online-race.mjs +99 -0
- package/bin/facet-online-validate.mjs +51 -0
- package/bin/facet-panel.mjs +22 -0
- package/bin/facet-passage.mjs +41 -0
- package/bin/facet-random-validate.mjs +48 -0
- package/bin/facet-roleplay.mjs +33 -0
- package/bin/facet-scan.mjs +56 -0
- package/bin/facet-segment.mjs +34 -0
- package/bin/facet-train.mjs +19 -0
- package/bin/facet-validate.mjs +103 -0
- package/bin/facet-zeroshot.mjs +14 -0
- package/bin/install-templates.mjs +72 -0
- package/bin/memory-bench.mjs +53 -0
- package/bin/memory-build-summary.mjs +43 -0
- package/bin/memory-build-vector.mjs +31 -0
- package/bin/memory-build.mjs +41 -0
- package/bin/memory-calibration.mjs +70 -0
- package/bin/memory-closed-loop.mjs +119 -0
- package/bin/memory-dry-run.mjs +70 -0
- package/bin/memory-eval.mjs +104 -0
- package/bin/memory-event-alias-test.mjs +89 -0
- package/bin/memory-extraction-bench.mjs +58 -0
- package/bin/memory-load-gate-replay.mjs +137 -0
- package/bin/memory-loop-test.mjs +90 -0
- package/bin/memory-pre-turn-harness.mjs +63 -0
- package/bin/memory-query.mjs +39 -0
- package/bin/memory-replay-hints.mjs +84 -0
- package/bin/memory-scale.mjs +58 -0
- package/bin/memory-scenario-test.mjs +146 -0
- package/bin/memory-user-loop.mjs +95 -0
- package/bin/ner_bench_tiny_zh.py +59 -0
- package/bin/ner_bench_zh.py +60 -0
- package/bin/ner_conll_en.py +139 -0
- package/bin/ner_direct_en.py +123 -0
- package/bin/ner_direct_zh.py +100 -0
- package/bin/ner_pipeline.py +208 -0
- package/bin/noise-selfcheck.mjs +50 -0
- package/bin/pack-integrity.mjs +47 -0
- package/bin/panel-check.mjs +57 -0
- package/bin/plugin-integrity.mjs +74 -0
- package/bin/preview-live.mjs +13 -0
- package/bin/public-install-check.mjs +222 -0
- package/bin/relabel-apply.mjs +272 -0
- package/bin/relabel-dry-run.mjs +381 -0
- package/bin/relik_pipeline.py +39 -0
- package/bin/relik_service.py +52 -0
- package/bin/srl_pipeline.py +55 -0
- package/bin/srl_service.py +68 -0
- package/bin/summary-index.mjs +151 -0
- package/bin/summary-query.mjs +79 -0
- package/bin/vec-check.mjs +27 -0
- package/bin/zh_ner_error_audit.py +36 -0
- package/bin/zh_ner_slot_eval.py +38 -0
- package/client.js +760 -0
- package/cordis.patch.yml +378 -0
- package/docs/API-NOTES.md +1743 -0
- package/docs/ARCHITECTURE.md +355 -0
- package/docs/CACHE.md +253 -0
- package/docs/CONDUCT.md +30 -0
- package/docs/CREATOR-HANDOFF.md +102 -0
- package/docs/EXPERIMENT.md +136 -0
- package/docs/FACET-MVP-REPORT.md +473 -0
- package/docs/GUIDE-0.4.66.md +195 -0
- package/docs/GUIDE.md +127 -0
- package/docs/MECHANISM.md +243 -0
- package/docs/NEW-INSTANCE-HANDOFF.md +219 -0
- package/docs/README.md +37 -0
- package/docs/SESSION-LAYOUT.md +151 -0
- package/docs/history-map.md +249 -0
- package/docs/relabel-migration-wiring.md +366 -0
- package/guides/00-READ-FIRST.md +206 -0
- package/guides/01-INSTALL.md +207 -0
- package/guides/02-TABLES.md +225 -0
- package/guides/03-AUDIT.md +201 -0
- package/guides/04-REBUILD.md +306 -0
- package/guides/05-FIRST-RUN.md +191 -0
- package/guides/06-EMBEDDING.md +146 -0
- package/guides/07-PTC-AND-ASSETS.md +221 -0
- package/icon.svg +7 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/models/README.md +13 -0
- package/models/Xenova/bge-small-zh-v1.5/config.json +31 -0
- package/models/Xenova/bge-small-zh-v1.5/onnx/model_quantized.onnx +0 -0
- package/models/Xenova/bge-small-zh-v1.5/tokenizer.json +21278 -0
- package/models/Xenova/bge-small-zh-v1.5/tokenizer_config.json +15 -0
- package/package.json +139 -4
- package/src/asset-tool.mjs +237 -0
- package/src/assets.mjs +244 -0
- package/src/backfill.mjs +74 -0
- package/src/backup.mjs +80 -0
- package/src/deadend.mjs +95 -0
- package/src/deleted-store.mjs +35 -0
- package/src/engine.mjs +3831 -0
- package/src/facets/classifier.mjs +146 -0
- package/src/facets/embedding-service.mjs +53 -0
- package/src/facets/embedding-worker.mjs +27 -0
- package/src/facets/embedding.mjs +163 -0
- package/src/facets/extract.mjs +80 -0
- package/src/facets/fold-plan.mjs +84 -0
- package/src/facets/hybrid.mjs +29 -0
- package/src/facets/index.mjs +113 -0
- package/src/facets/linear-probe.mjs +43 -0
- package/src/facets/load.mjs +37 -0
- package/src/facets/mlp-probe.mjs +54 -0
- package/src/facets/mlp-service.mjs +53 -0
- package/src/facets/mlp-worker.mjs +26 -0
- package/src/facets/online-knn.mjs +58 -0
- package/src/facets/online-models.mjs +227 -0
- package/src/facets/panel-state.mjs +23 -0
- package/src/facets/panel.mjs +73 -0
- package/src/facets/random.mjs +26 -0
- package/src/facets/scanner.mjs +35 -0
- package/src/facets/schema.mjs +75 -0
- package/src/facets/seed-examples.mjs +58 -0
- package/src/facets/sensitivity.mjs +98 -0
- package/src/facets/service.mjs +53 -0
- package/src/facets/store.mjs +39 -0
- package/src/facets/worker.mjs +23 -0
- package/src/folds-store.mjs +109 -0
- package/src/groups.mjs +167 -0
- package/src/handoff-tool.mjs +51 -0
- package/src/handoff.mjs +127 -0
- package/src/index.mjs +649 -0
- package/src/inject-policy.mjs +17 -0
- package/src/learning.mjs +88 -0
- package/src/memory/alias-proposal.mjs +40 -0
- package/src/memory/bc-audit.mjs +245 -0
- package/src/memory/bc-index-updater.mjs +404 -0
- package/src/memory/bc-index.mjs +48 -0
- package/src/memory/bc-label.mjs +220 -0
- package/src/memory/bc-panel-state.mjs +107 -0
- package/src/memory/bc-pre-step.mjs +463 -0
- package/src/memory/bc-recall.mjs +470 -0
- package/src/memory/bc-shift.mjs +123 -0
- package/src/memory/bc-store.mjs +41 -0
- package/src/memory/bm25.mjs +112 -0
- package/src/memory/calibration.mjs +54 -0
- package/src/memory/chatter.mjs +29 -0
- package/src/memory/cmap-file.mjs +107 -0
- package/src/memory/concepts-file.mjs +41 -0
- package/src/memory/coref-slots.mjs +40 -0
- package/src/memory/event-alias-ledger.mjs +87 -0
- package/src/memory/event-log.mjs +95 -0
- package/src/memory/event-model.mjs +132 -0
- package/src/memory/event-segmenter.mjs +36 -0
- package/src/memory/event-summary-tree.mjs +122 -0
- package/src/memory/extraction-adapter.mjs +76 -0
- package/src/memory/extraction-composite.mjs +75 -0
- package/src/memory/extraction-core.mjs +76 -0
- package/src/memory/extraction-pre-step.mjs +161 -0
- package/src/memory/extraction-provider.mjs +300 -0
- package/src/memory/extraction-score.mjs +36 -0
- package/src/memory/facet-stabilizer.mjs +59 -0
- package/src/memory/history-map.mjs +139 -0
- package/src/memory/language-router.mjs +30 -0
- package/src/memory/load-gate.mjs +161 -0
- package/src/memory/mechanical-note.mjs +104 -0
- package/src/memory/onboarding.mjs +163 -0
- package/src/memory/persona.mjs +78 -0
- package/src/memory/planner.mjs +249 -0
- package/src/memory/pre-turn.mjs +80 -0
- package/src/memory/query-aliases.mjs +39 -0
- package/src/memory/recall-cue.mjs +43 -0
- package/src/memory/recall.mjs +65 -0
- package/src/memory/referent-candidates.mjs +55 -0
- package/src/memory/referent-ledger.mjs +86 -0
- package/src/memory/relabel-migrate.mjs +376 -0
- package/src/memory/relik-adapter.mjs +25 -0
- package/src/memory/special-keywords.mjs +60 -0
- package/src/memory/structured-adapters.mjs +75 -0
- package/src/memory/summary-index.mjs +335 -0
- package/src/memory/summary-tree.mjs +110 -0
- package/src/memory/term-extractor.mjs +44 -0
- package/src/memory/tokenize.mjs +9 -0
- package/src/memory/vector-index.mjs +54 -0
- package/src/memory/window-summary.mjs +133 -0
- package/src/mood.mjs +133 -0
- package/src/peer-tool.mjs +63 -0
- package/src/peer.mjs +144 -0
- package/src/policy.mjs +170 -0
- package/src/preflight.mjs +113 -0
- package/src/provenance.mjs +94 -0
- package/src/range.mjs +75 -0
- package/src/recall-exact-tool.mjs +267 -0
- package/src/recall-tool.mjs +521 -0
- package/src/recheck.mjs +99 -0
- package/src/save-start.mjs +29 -0
- package/src/session-guard.mjs +140 -0
- package/src/session-log-tool.mjs +322 -0
- package/src/session-log.mjs +606 -0
- package/src/skills.mjs +111 -0
- package/src/split-range.mjs +115 -0
- package/src/state.mjs +198 -0
- package/src/t0.mjs +149 -0
- package/src/tag-confirm.mjs +78 -0
- package/src/tag-detect.mjs +42 -0
- package/src/tag-maintenance.mjs +285 -0
- package/src/tag-reject.mjs +73 -0
- package/src/tags-store.mjs +94 -0
- package/src/tags.mjs +692 -0
- package/src/threads-backend.mjs +26 -0
- package/src/threads-core.mjs +1887 -0
- package/src/threads-store.mjs +207 -0
- package/src/threads-tool.mjs +522 -0
- package/src/threads.mjs +929 -0
- package/src/tool-preview.mjs +427 -0
- package/src/topic.mjs +63 -0
- package/src/training.mjs +75 -0
- package/src/turn-tag.mjs +62 -0
- package/src/user-turn.mjs +39 -0
- package/templates/cmap.md +11 -0
- package/templates/concepts.md +12 -0
- package/templates/habits.md +40 -0
- package/templates/keywords.md +6 -0
- package/templates/rebuild.md +134 -0
- package/templates/rules-safe.md +184 -0
- package/templates/rules.md +177 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Idempotent setup for the three template files.
|
|
4
|
+
*
|
|
5
|
+
* Dry-run by default. `--apply` is explicit: it copies rules.md / habits.md only
|
|
6
|
+
* when missing, and appends the AGENTS pointer only when absent. AGENTS is
|
|
7
|
+
* backed up before any write.
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
15
|
+
const templates = path.join(root, 'templates');
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const apply = args.includes('--apply');
|
|
18
|
+
const argOf = (name) => {
|
|
19
|
+
const i = args.indexOf(name);
|
|
20
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
21
|
+
};
|
|
22
|
+
const contHome = process.env.DSH_CONTINUITY_HOME
|
|
23
|
+
? process.env.DSH_CONTINUITY_HOME
|
|
24
|
+
: path.join(os.homedir(), '.dsh', 'continuity');
|
|
25
|
+
const agentsFile = argOf('--agents') ?? path.join(os.homedir(), '.dsh', 'AGENTS.md');
|
|
26
|
+
const markers = ['## Continuity 的 rules.md', '## Continuity 的规则层'];
|
|
27
|
+
|
|
28
|
+
function read(file) {
|
|
29
|
+
try { return fs.readFileSync(file, 'utf8'); } catch { return null; }
|
|
30
|
+
}
|
|
31
|
+
function backup(file) {
|
|
32
|
+
if (!fs.existsSync(file)) return null;
|
|
33
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
34
|
+
const dst = `${file}.bak-${stamp}`;
|
|
35
|
+
fs.copyFileSync(file, dst);
|
|
36
|
+
return dst;
|
|
37
|
+
}
|
|
38
|
+
function copyIfMissing(name) {
|
|
39
|
+
const src = path.join(templates, name);
|
|
40
|
+
const dst = path.join(contHome, name);
|
|
41
|
+
if (!fs.existsSync(src)) throw new Error(`missing template: ${src}`);
|
|
42
|
+
if (fs.existsSync(dst)) return `keep ${dst}`;
|
|
43
|
+
if (apply) {
|
|
44
|
+
fs.mkdirSync(contHome, { recursive: true });
|
|
45
|
+
fs.copyFileSync(src, dst);
|
|
46
|
+
return `create ${dst}`;
|
|
47
|
+
}
|
|
48
|
+
return `would create ${dst}`;
|
|
49
|
+
}
|
|
50
|
+
function mergeAgents() {
|
|
51
|
+
const snippetSrc = path.join(templates, 'AGENTS-continuity.md');
|
|
52
|
+
if (!fs.existsSync(snippetSrc)) throw new Error(`missing template: ${snippetSrc}`);
|
|
53
|
+
const existing = read(agentsFile);
|
|
54
|
+
if (existing === null) {
|
|
55
|
+
if (apply) fs.mkdirSync(path.dirname(agentsFile), { recursive: true });
|
|
56
|
+
const text = fs.readFileSync(snippetSrc, 'utf8');
|
|
57
|
+
if (apply) fs.writeFileSync(agentsFile, text, 'utf8');
|
|
58
|
+
return `create ${agentsFile}`;
|
|
59
|
+
}
|
|
60
|
+
if (markers.some((m) => existing.includes(m))) return `keep ${agentsFile} (pointer present)`;
|
|
61
|
+
const text = existing.replace(/\s*$/, '') + '\n\n' + fs.readFileSync(snippetSrc, 'utf8');
|
|
62
|
+
if (!apply) return `would append pointer to ${agentsFile}`;
|
|
63
|
+
const bak = backup(agentsFile);
|
|
64
|
+
fs.writeFileSync(agentsFile, text, 'utf8');
|
|
65
|
+
return `append pointer to ${agentsFile}${bak ? ` (backup ${bak})` : ''}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(`mode: ${apply ? 'APPLY' : 'DRY-RUN (use --apply)'}`);
|
|
69
|
+
console.log(`continuity: ${contHome}`);
|
|
70
|
+
console.log(`agents: ${agentsFile}`);
|
|
71
|
+
for (const name of ['rules.md', 'habits.md']) console.log(copyIfMissing(name));
|
|
72
|
+
console.log(mergeAgents());
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readEventLog } from '../src/memory/event-log.mjs';
|
|
5
|
+
import { BM25Index } from '../src/memory/bm25.mjs';
|
|
6
|
+
import { VectorIndex } from '../src/memory/vector-index.mjs';
|
|
7
|
+
import { buildPlanner, summaryIndex } from '../src/memory/planner.mjs';
|
|
8
|
+
import { embedTexts } from '../src/facets/embedding.mjs';
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const argOf = (name) => {
|
|
12
|
+
const i = args.indexOf(name);
|
|
13
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
14
|
+
};
|
|
15
|
+
const dir = argOf('--dir');
|
|
16
|
+
if (!dir) {
|
|
17
|
+
console.error('usage: node bin/memory-bench.mjs --dir .durable-memory');
|
|
18
|
+
process.exit(2);
|
|
19
|
+
}
|
|
20
|
+
const records = readEventLog(path.join(dir, 'events.jsonl'));
|
|
21
|
+
const bm25 = BM25Index.fromJSON(JSON.parse(fs.readFileSync(path.join(dir, 'bm25.json'), 'utf8')));
|
|
22
|
+
const vectors = VectorIndex.fromJSON(JSON.parse(fs.readFileSync(path.join(dir, 'vectors.json'), 'utf8')));
|
|
23
|
+
const tree = JSON.parse(fs.readFileSync(path.join(dir, 'summary-tree.json'), 'utf8'));
|
|
24
|
+
const planner = buildPlanner({ records, bm25, vectorIndex: vectors, summaryBm25: summaryIndex(tree) });
|
|
25
|
+
const set = JSON.parse(fs.readFileSync(path.join(dir, 'QUERY-SET.json'), 'utf8'));
|
|
26
|
+
const rows = [];
|
|
27
|
+
for (const testCase of set.cases) {
|
|
28
|
+
const expected = new Set(testCase.expected);
|
|
29
|
+
const [queryVector] = await embedTexts([testCase.query]);
|
|
30
|
+
const plan = planner.query(testCase.query, { queryVector, topK: 10 });
|
|
31
|
+
const got = plan.fused.map((hit) => hit.turn);
|
|
32
|
+
const top = got.slice(0, 10);
|
|
33
|
+
const tp = top.filter((turn) => expected.has(turn)).length;
|
|
34
|
+
rows.push({
|
|
35
|
+
query: testCase.query,
|
|
36
|
+
route: plan.route,
|
|
37
|
+
expected: expected.size,
|
|
38
|
+
returned: top.length,
|
|
39
|
+
hits: tp,
|
|
40
|
+
precision: top.length === 0 ? 0 : tp / top.length,
|
|
41
|
+
recall: expected.size === 0 ? (top.length === 0 ? 1 : 0) : tp / expected.size,
|
|
42
|
+
top: top.slice(0, 5),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const aggregate = {
|
|
46
|
+
precision: rows.reduce((sum, row) => sum + row.precision, 0) / rows.length,
|
|
47
|
+
recall: rows.reduce((sum, row) => sum + row.recall, 0) / rows.length,
|
|
48
|
+
emptyCorrect: rows.filter((row) => row.expected === 0 && row.returned === 0).length,
|
|
49
|
+
emptyQueries: rows.filter((row) => row.expected === 0).length,
|
|
50
|
+
};
|
|
51
|
+
const output = { dir, set: set.provenance, aggregate, rows };
|
|
52
|
+
fs.writeFileSync(path.join(dir, 'QUERY-BENCH.json'), JSON.stringify(output, null, 2));
|
|
53
|
+
console.log(JSON.stringify(output, null, 2));
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { buildSummaryTree, verifySummaryTree } from '../src/memory/summary-tree.mjs';
|
|
5
|
+
import { readEventLog } from '../src/memory/event-log.mjs';
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const argOf = (name) => {
|
|
9
|
+
const i = args.indexOf(name);
|
|
10
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
11
|
+
};
|
|
12
|
+
const eventsFile = argOf('--events');
|
|
13
|
+
const outDir = argOf('--out') ?? '.durable-memory';
|
|
14
|
+
if (!eventsFile) {
|
|
15
|
+
console.error('usage: node bin/memory-build-summary.mjs --events events.jsonl --out .durable-memory');
|
|
16
|
+
process.exit(2);
|
|
17
|
+
}
|
|
18
|
+
const records = readEventLog(eventsFile);
|
|
19
|
+
const tree = buildSummaryTree(records, {
|
|
20
|
+
segmentSize: Number(argOf('--segment')) || 8,
|
|
21
|
+
chapterSize: Number(argOf('--chapter')) || 8,
|
|
22
|
+
});
|
|
23
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
24
|
+
const treeFile = path.join(outDir, 'summary-tree.json');
|
|
25
|
+
fs.writeFileSync(treeFile, `${JSON.stringify(tree, null, 2)}\n`, 'utf8');
|
|
26
|
+
const manifestFile = path.join(outDir, 'manifest.json');
|
|
27
|
+
if (fs.existsSync(manifestFile)) {
|
|
28
|
+
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
29
|
+
manifest.layers.summary = {
|
|
30
|
+
file: 'summary-tree.json',
|
|
31
|
+
version: tree.version,
|
|
32
|
+
nodeCount: tree.nodeCount,
|
|
33
|
+
treeRootHash: tree.treeRootHash,
|
|
34
|
+
};
|
|
35
|
+
fs.writeFileSync(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
36
|
+
}
|
|
37
|
+
const verification = verifySummaryTree(tree);
|
|
38
|
+
console.log(JSON.stringify({
|
|
39
|
+
out: treeFile,
|
|
40
|
+
...verification,
|
|
41
|
+
levels: tree.levels,
|
|
42
|
+
treeRootHash: tree.treeRootHash,
|
|
43
|
+
}, null, 2));
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readEventLog } from '../src/memory/event-log.mjs';
|
|
5
|
+
import { VectorIndex, loadEmbeddings } from '../src/memory/vector-index.mjs';
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const argOf = (name) => {
|
|
9
|
+
const i = args.indexOf(name);
|
|
10
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
11
|
+
};
|
|
12
|
+
const eventsFile = argOf('--events');
|
|
13
|
+
const embeddingsFile = argOf('--embeddings');
|
|
14
|
+
const outDir = argOf('--out') ?? '.durable-memory';
|
|
15
|
+
if (!eventsFile || !embeddingsFile) {
|
|
16
|
+
console.error('usage: node bin/memory-build-vector.mjs --events events.jsonl --embeddings embeddings.json --out .durable-memory');
|
|
17
|
+
process.exit(2);
|
|
18
|
+
}
|
|
19
|
+
const records = readEventLog(eventsFile);
|
|
20
|
+
const vectors = loadEmbeddings(embeddingsFile);
|
|
21
|
+
const index = new VectorIndex().build(records, vectors);
|
|
22
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
23
|
+
const file = path.join(outDir, 'vectors.json');
|
|
24
|
+
fs.writeFileSync(file, `${JSON.stringify(index.toJSON())}\n`, 'utf8');
|
|
25
|
+
const manifestFile = path.join(outDir, 'manifest.json');
|
|
26
|
+
if (fs.existsSync(manifestFile)) {
|
|
27
|
+
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
28
|
+
manifest.layers.vector = { file: 'vectors.json', docs: index.docs.length };
|
|
29
|
+
fs.writeFileSync(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
30
|
+
}
|
|
31
|
+
console.log(JSON.stringify({ out: file, docs: index.docs.length }, null, 2));
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { buildEventLog, writeEventLog, verifyEventLog, readEventLog } from '../src/memory/event-log.mjs';
|
|
6
|
+
import { BM25Index } from '../src/memory/bm25.mjs';
|
|
7
|
+
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const argOf = (name) => {
|
|
10
|
+
const i = args.indexOf(name);
|
|
11
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
12
|
+
};
|
|
13
|
+
const input = argOf('--input');
|
|
14
|
+
const outDir = argOf('--out') ?? '.durable-memory';
|
|
15
|
+
if (!input) {
|
|
16
|
+
console.error('usage: node bin/memory-build.mjs --input rows.jsonl --out .durable-memory');
|
|
17
|
+
process.exit(2);
|
|
18
|
+
}
|
|
19
|
+
const source = fs.readFileSync(input);
|
|
20
|
+
const sourceSha = createHash('sha256').update(source).digest('hex');
|
|
21
|
+
const rows = source.toString('utf8').split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
|
|
22
|
+
const records = buildEventLog(rows);
|
|
23
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
24
|
+
const eventFile = path.join(outDir, 'events.jsonl');
|
|
25
|
+
const eventResult = writeEventLog(eventFile, records);
|
|
26
|
+
const bm25 = new BM25Index().build(records);
|
|
27
|
+
fs.writeFileSync(path.join(outDir, 'bm25.json'), `${JSON.stringify(bm25.toJSON())}\n`, 'utf8');
|
|
28
|
+
const manifest = {
|
|
29
|
+
schema: 1,
|
|
30
|
+
sourceFile: path.basename(input),
|
|
31
|
+
sourceSha,
|
|
32
|
+
eventCount: records.length,
|
|
33
|
+
rootHash: eventResult.rootHash,
|
|
34
|
+
layers: {
|
|
35
|
+
event: { file: 'events.jsonl', count: records.length, rootHash: eventResult.rootHash },
|
|
36
|
+
bm25: { file: 'bm25.json', docs: bm25.docs.length },
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
fs.writeFileSync(path.join(outDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
40
|
+
const verification = verifyEventLog(readEventLog(eventFile));
|
|
41
|
+
console.log(JSON.stringify({ outDir, sourceSha, eventFile, ...verification, bm25Docs: bm25.docs.length }, null, 2));
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Local facet-head calibration on the copied history.
|
|
4
|
+
* Labels are weak T-derived labels, not human gold.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { readEventLog } from '../src/memory/event-log.mjs';
|
|
9
|
+
import { deserializeMlp, predictMlp } from '../src/facets/mlp-probe.mjs';
|
|
10
|
+
import { reliabilityBins, thresholdCurve, chooseThreshold } from '../src/memory/calibration.mjs';
|
|
11
|
+
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const argOf = (name) => {
|
|
14
|
+
const i = args.indexOf(name);
|
|
15
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
16
|
+
};
|
|
17
|
+
const dir = argOf('--dir');
|
|
18
|
+
const modelPath = argOf('--model');
|
|
19
|
+
const embeddingsPath = argOf('--embeddings');
|
|
20
|
+
if (!dir || !modelPath || !embeddingsPath) {
|
|
21
|
+
console.error('usage: node bin/memory-calibration.mjs --dir .durable-memory --model MLP-MODEL.json --embeddings embeddings-1500.json');
|
|
22
|
+
process.exit(2);
|
|
23
|
+
}
|
|
24
|
+
const records = readEventLog(path.join(dir, 'events.jsonl'));
|
|
25
|
+
const vectors = new Map(JSON.parse(fs.readFileSync(embeddingsPath, 'utf8')).map((row) => [row.turn, row.vector]));
|
|
26
|
+
const model = deserializeMlp(JSON.parse(fs.readFileSync(modelPath, 'utf8')));
|
|
27
|
+
const classes = new Set(model.slots.activity.classes);
|
|
28
|
+
const points = [];
|
|
29
|
+
let skipped = 0;
|
|
30
|
+
for (const record of records) {
|
|
31
|
+
const vector = vectors.get(record.turn);
|
|
32
|
+
if (vector === undefined) {
|
|
33
|
+
skipped += 1;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const actual = record.facets.activity;
|
|
37
|
+
if (!classes.has(actual)) {
|
|
38
|
+
skipped += 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const prediction = predictMlp(model, vector);
|
|
42
|
+
points.push({
|
|
43
|
+
turn: record.turn,
|
|
44
|
+
confidence: prediction.confidence.activity,
|
|
45
|
+
correct: prediction.facets.activity === actual,
|
|
46
|
+
predicted: prediction.facets.activity,
|
|
47
|
+
actual,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const summary = {
|
|
51
|
+
model: path.basename(modelPath),
|
|
52
|
+
total: records.length,
|
|
53
|
+
evaluated: points.length,
|
|
54
|
+
skipped,
|
|
55
|
+
accuracy: points.filter((point) => point.correct).length / points.length,
|
|
56
|
+
averageConfidence: points.reduce((sum, point) => sum + point.confidence, 0) / points.length,
|
|
57
|
+
bins: reliabilityBins(points),
|
|
58
|
+
curve: thresholdCurve(points),
|
|
59
|
+
target90: chooseThreshold(points, { targetPrecision: 0.9 }),
|
|
60
|
+
target80: chooseThreshold(points, { targetPrecision: 0.8 }),
|
|
61
|
+
};
|
|
62
|
+
fs.writeFileSync(path.join(dir, 'CALIBRATION.json'), JSON.stringify(summary, null, 2));
|
|
63
|
+
console.log(JSON.stringify({
|
|
64
|
+
evaluated: summary.evaluated,
|
|
65
|
+
skipped: summary.skipped,
|
|
66
|
+
accuracy: +summary.accuracy.toFixed(3),
|
|
67
|
+
averageConfidence: +summary.averageConfidence.toFixed(3),
|
|
68
|
+
target90: summary.target90,
|
|
69
|
+
target80: summary.target80,
|
|
70
|
+
}, null, 2));
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Closed-loop probe: local MLP facet prediction -> gate -> scene candidate.
|
|
4
|
+
* Compares raw MLP vs retrieval-stabilized facets.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { readEventLog } from '../src/memory/event-log.mjs';
|
|
9
|
+
import { BM25Index } from '../src/memory/bm25.mjs';
|
|
10
|
+
import { VectorIndex } from '../src/memory/vector-index.mjs';
|
|
11
|
+
import { embedTexts } from '../src/facets/embedding.mjs';
|
|
12
|
+
import { deserializeMlp, predictMlp } from '../src/facets/mlp-probe.mjs';
|
|
13
|
+
import { MemoryLoadState, latestCandidate, candidateSegment } from '../src/memory/load-gate.mjs';
|
|
14
|
+
import { stabilizeFacets } from '../src/memory/facet-stabilizer.mjs';
|
|
15
|
+
import { summaryIndex } from '../src/memory/planner.mjs';
|
|
16
|
+
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const argOf = (name) => {
|
|
19
|
+
const i = args.indexOf(name);
|
|
20
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
21
|
+
};
|
|
22
|
+
const dir = argOf('--dir');
|
|
23
|
+
const modelPath = argOf('--model');
|
|
24
|
+
if (!dir || !modelPath) {
|
|
25
|
+
console.error('usage: node bin/memory-closed-loop.mjs --dir .durable-memory --model MLP-MODEL.json');
|
|
26
|
+
process.exit(2);
|
|
27
|
+
}
|
|
28
|
+
const records = readEventLog(path.join(dir, 'events.jsonl'));
|
|
29
|
+
const bm25 = BM25Index.fromJSON(JSON.parse(fs.readFileSync(path.join(dir, 'bm25.json'), 'utf8')));
|
|
30
|
+
const vectors = VectorIndex.fromJSON(JSON.parse(fs.readFileSync(path.join(dir, 'vectors.json'), 'utf8')));
|
|
31
|
+
const model = deserializeMlp(JSON.parse(fs.readFileSync(modelPath, 'utf8')));
|
|
32
|
+
const tree = JSON.parse(fs.readFileSync(path.join(dir, 'summary-tree.json'), 'utf8'));
|
|
33
|
+
const summaries = summaryIndex(tree);
|
|
34
|
+
const dev = { activity: '开发', topic: '技术' };
|
|
35
|
+
const messages = [
|
|
36
|
+
['我们现在开始跑团吧', 'load'],
|
|
37
|
+
['开团', 'load'],
|
|
38
|
+
['TRPG 开一把', 'load'],
|
|
39
|
+
['来一局', 'load'],
|
|
40
|
+
['规则怎么写', 'none'],
|
|
41
|
+
['规则文档在哪', 'none'],
|
|
42
|
+
['上次跑团怎么样了', 'load'],
|
|
43
|
+
['钥匙最后交给谁了', 'load'],
|
|
44
|
+
['沈砚后来呢', 'load'],
|
|
45
|
+
['跑团这个设计挺有意思', 'none'],
|
|
46
|
+
['我推门进灯室', 'hint'],
|
|
47
|
+
['Coo:灯室见', 'load'],
|
|
48
|
+
['开始写文档', 'none'],
|
|
49
|
+
['更新 README', 'none'],
|
|
50
|
+
['开始调试插件', 'none'],
|
|
51
|
+
['修一下这个 bug', 'none'],
|
|
52
|
+
['今天天气不错', 'none'],
|
|
53
|
+
['量子计算怎么样', 'none'],
|
|
54
|
+
];
|
|
55
|
+
const vectorsByText = new Map();
|
|
56
|
+
for (const [text] of messages) {
|
|
57
|
+
const [vector] = await embedTexts([text]);
|
|
58
|
+
vectorsByText.set(text, vector);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const rows = [];
|
|
62
|
+
for (const [text, expected] of messages) {
|
|
63
|
+
const vector = vectorsByText.get(text);
|
|
64
|
+
const raw = predictMlp(model, vector);
|
|
65
|
+
const keyword = bm25.search(text, { topK: 5, minCoverage: 0.4 });
|
|
66
|
+
const semantic = vectors.search(vector, { topK: 5 });
|
|
67
|
+
const summary = summaries.search(text, { topK: 3, minCoverage: 0.3 });
|
|
68
|
+
const stabilized = stabilizeFacets({ raw, text, records, keyword, semantic, summary });
|
|
69
|
+
const candidate = latestCandidate(records, 'activity', stabilized.facets.activity);
|
|
70
|
+
const segment = candidateSegment(records, candidate);
|
|
71
|
+
const gateState = new MemoryLoadState({ loadedFacets: dev, loadedAtTurn: 1490 });
|
|
72
|
+
const rawCandidate = latestCandidate(records, 'activity', raw.facets.activity);
|
|
73
|
+
const rawGate = gateState.decide({
|
|
74
|
+
text,
|
|
75
|
+
currentFacets: raw.facets,
|
|
76
|
+
confidence: raw.confidence.activity ?? 0,
|
|
77
|
+
nowTurn: 1500,
|
|
78
|
+
hasCandidate: rawCandidate !== null,
|
|
79
|
+
candidate: rawCandidate,
|
|
80
|
+
role: text.startsWith('Coo') ? 'coo' : 'user',
|
|
81
|
+
});
|
|
82
|
+
const stableGate = gateState.decide({
|
|
83
|
+
text,
|
|
84
|
+
currentFacets: stabilized.facets,
|
|
85
|
+
confidence: stabilized.confidence.activity ?? 0,
|
|
86
|
+
nowTurn: 1500,
|
|
87
|
+
hasCandidate: candidate !== null,
|
|
88
|
+
candidate,
|
|
89
|
+
role: text.startsWith('Coo') ? 'coo' : 'user',
|
|
90
|
+
});
|
|
91
|
+
rows.push({
|
|
92
|
+
text,
|
|
93
|
+
expected,
|
|
94
|
+
rawPass: rawGate.action === expected,
|
|
95
|
+
stablePass: stableGate.action === expected,
|
|
96
|
+
rawFacets: raw.facets,
|
|
97
|
+
rawConfidence: Object.fromEntries(Object.entries(raw.confidence).map(([key, value]) => [key, +value.toFixed(2)])),
|
|
98
|
+
evidence: { activity: stabilized.evidence.activity, topic: stabilized.evidence.topic },
|
|
99
|
+
stabilized: stabilized.facets,
|
|
100
|
+
stabilizeReason: stabilized.reason,
|
|
101
|
+
rawGate: rawGate.action,
|
|
102
|
+
rawReason: rawGate.reason,
|
|
103
|
+
stableGate: stableGate.action,
|
|
104
|
+
stableReason: stableGate.reason,
|
|
105
|
+
candidateTurn: candidate?.turn ?? null,
|
|
106
|
+
segmentTurns: segment?.turns ?? 0,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const summary = {
|
|
110
|
+
cases: rows.length,
|
|
111
|
+
rawPass: rows.filter((row) => row.rawPass).length,
|
|
112
|
+
stablePass: rows.filter((row) => row.stablePass).length,
|
|
113
|
+
rawFailures: rows.filter((row) => !row.rawPass).map((row) => row.text),
|
|
114
|
+
stableFailures: rows.filter((row) => !row.stablePass).map((row) => row.text),
|
|
115
|
+
lowActivityConfidence: rows.filter((row) => row.rawConfidence.activity < 0.7).length,
|
|
116
|
+
changedByStabilizer: rows.filter((row) => row.rawFacets.activity !== row.stabilized.activity).length,
|
|
117
|
+
};
|
|
118
|
+
console.log(JSON.stringify({ model: path.basename(modelPath), summary, rows }, null, 2));
|
|
119
|
+
fs.writeFileSync(path.join(dir, 'CLOSED-LOOP.json'), JSON.stringify({ model: path.basename(modelPath), summary, rows }, null, 2));
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Offline dry run: query -> planner -> seq ranges -> simulated load.
|
|
4
|
+
* No live session and no writes.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { readEventLog } from '../src/memory/event-log.mjs';
|
|
9
|
+
import { BM25Index } from '../src/memory/bm25.mjs';
|
|
10
|
+
import { VectorIndex } from '../src/memory/vector-index.mjs';
|
|
11
|
+
import { buildPlanner, summaryIndex } from '../src/memory/planner.mjs';
|
|
12
|
+
import { embedTexts } from '../src/facets/embedding.mjs';
|
|
13
|
+
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
const argOf = (name) => {
|
|
16
|
+
const i = args.indexOf(name);
|
|
17
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
18
|
+
};
|
|
19
|
+
const dir = argOf('--dir');
|
|
20
|
+
if (!dir) {
|
|
21
|
+
console.error('usage: node bin/memory-dry-run.mjs --dir .durable-memory');
|
|
22
|
+
process.exit(2);
|
|
23
|
+
}
|
|
24
|
+
const records = readEventLog(path.join(dir, 'events.jsonl'));
|
|
25
|
+
const byTurn = new Map(records.map((record) => [record.turn, record]));
|
|
26
|
+
const bm25 = BM25Index.fromJSON(JSON.parse(fs.readFileSync(path.join(dir, 'bm25.json'), 'utf8')));
|
|
27
|
+
const vectors = VectorIndex.fromJSON(JSON.parse(fs.readFileSync(path.join(dir, 'vectors.json'), 'utf8')));
|
|
28
|
+
const tree = JSON.parse(fs.readFileSync(path.join(dir, 'summary-tree.json'), 'utf8'));
|
|
29
|
+
const planner = buildPlanner({ records, bm25, vectorIndex: vectors, summaryBm25: summaryIndex(tree) });
|
|
30
|
+
const set = JSON.parse(fs.readFileSync(path.join(dir, 'QUERY-SET.json'), 'utf8'));
|
|
31
|
+
|
|
32
|
+
const rows = [];
|
|
33
|
+
for (const testCase of set.cases) {
|
|
34
|
+
const expected = new Set(testCase.expected);
|
|
35
|
+
const [queryVector] = await embedTexts([testCase.query]);
|
|
36
|
+
const plan = planner.query(testCase.query, { queryVector, topK: 10 });
|
|
37
|
+
const ranges = [];
|
|
38
|
+
const loaded = [];
|
|
39
|
+
for (const hit of plan.fused.slice(0, 10)) {
|
|
40
|
+
const record = byTurn.get(hit.turn);
|
|
41
|
+
if (record === undefined) {
|
|
42
|
+
ranges.push({ turn: hit.turn, valid: false });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
ranges.push({ turn: hit.turn, seqStart: record.seqStart, seqEnd: record.seqEnd, valid: true });
|
|
46
|
+
loaded.push(record);
|
|
47
|
+
}
|
|
48
|
+
const text = loaded.map((record) => record.text).join('\n');
|
|
49
|
+
const hitTurns = loaded.map((record) => record.turn).filter((turn) => expected.has(turn));
|
|
50
|
+
rows.push({
|
|
51
|
+
query: testCase.query,
|
|
52
|
+
route: plan.route,
|
|
53
|
+
expected: expected.size,
|
|
54
|
+
loadedTurns: loaded.length,
|
|
55
|
+
expectedHits: hitTurns.length,
|
|
56
|
+
allRangesValid: ranges.every((range) => range.valid),
|
|
57
|
+
chars: text.length,
|
|
58
|
+
ranges,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const summary = {
|
|
62
|
+
queries: rows.length,
|
|
63
|
+
allRangesValid: rows.every((row) => row.allRangesValid),
|
|
64
|
+
expectedHitQueries: rows.filter((row) => row.expected === 0 || row.expectedHits > 0).length,
|
|
65
|
+
emptyCorrect: rows.filter((row) => row.expected === 0 && row.loadedTurns === 0).length,
|
|
66
|
+
emptyQueries: rows.filter((row) => row.expected === 0).length,
|
|
67
|
+
};
|
|
68
|
+
const output = { dir, summary, rows };
|
|
69
|
+
fs.writeFileSync(path.join(dir, 'DRY-RUN.json'), JSON.stringify(output, null, 2));
|
|
70
|
+
console.log(JSON.stringify(output, null, 2));
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Retrieval ablation: facet / BM25 / vector / summary / routed planner.
|
|
4
|
+
* Ground truth is the weak facet label or an explicit text predicate.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { readEventLog } from '../src/memory/event-log.mjs';
|
|
9
|
+
import { BM25Index } from '../src/memory/bm25.mjs';
|
|
10
|
+
import { VectorIndex } from '../src/memory/vector-index.mjs';
|
|
11
|
+
import { buildPlanner, summaryIndex } from '../src/memory/planner.mjs';
|
|
12
|
+
import { buildFacetIndex, queryFacets } from '../src/facets/index.mjs';
|
|
13
|
+
import { aliasFacets } from '../src/facets/schema.mjs';
|
|
14
|
+
import { embedTexts } from '../src/facets/embedding.mjs';
|
|
15
|
+
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const argOf = (name) => {
|
|
18
|
+
const i = args.indexOf(name);
|
|
19
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
20
|
+
};
|
|
21
|
+
const dir = argOf('--dir');
|
|
22
|
+
if (!dir) {
|
|
23
|
+
console.error('usage: node bin/memory-eval.mjs --dir .durable-memory');
|
|
24
|
+
process.exit(2);
|
|
25
|
+
}
|
|
26
|
+
const records = readEventLog(path.join(dir, 'events.jsonl'));
|
|
27
|
+
const bm25 = BM25Index.fromJSON(JSON.parse(fs.readFileSync(path.join(dir, 'bm25.json'), 'utf8')));
|
|
28
|
+
const vectors = VectorIndex.fromJSON(JSON.parse(fs.readFileSync(path.join(dir, 'vectors.json'), 'utf8')));
|
|
29
|
+
const tree = JSON.parse(fs.readFileSync(path.join(dir, 'summary-tree.json'), 'utf8'));
|
|
30
|
+
const facetIndex = buildFacetIndex(records);
|
|
31
|
+
const summaries = summaryIndex(tree);
|
|
32
|
+
const planner = buildPlanner({ records, bm25, vectorIndex: vectors, summaryBm25: summaries });
|
|
33
|
+
|
|
34
|
+
const cases = [];
|
|
35
|
+
for (const [query, core] of [
|
|
36
|
+
['跑团', { activity: '跑团' }],
|
|
37
|
+
['开发', { activity: '开发' }],
|
|
38
|
+
['调试', { activity: '调试' }],
|
|
39
|
+
['记忆', { topic: '记忆' }],
|
|
40
|
+
['规则', { topic: '规则' }],
|
|
41
|
+
['图像', { topic: '设备', medium: '图像' }],
|
|
42
|
+
['技术', { topic: '技术' }],
|
|
43
|
+
]) {
|
|
44
|
+
cases.push({
|
|
45
|
+
query,
|
|
46
|
+
expected: new Set(records.filter((record) => Object.entries(core).every(([key, value]) => record.facets[key] === value)).map((record) => record.turn)),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
cases.push({
|
|
50
|
+
query: 'd100 钥匙',
|
|
51
|
+
expected: new Set(records.filter((record) => record.facets.activity === '跑团' && /d100|钥匙/.test(record.text)).map((record) => record.turn)),
|
|
52
|
+
});
|
|
53
|
+
cases.push({
|
|
54
|
+
query: 'TAG 闸',
|
|
55
|
+
expected: new Set(records.filter((record) => /TAG|打标/.test(record.text) && /闸/.test(record.text)).map((record) => record.turn)),
|
|
56
|
+
});
|
|
57
|
+
cases.push({ query: '量子计算', expected: new Set() });
|
|
58
|
+
|
|
59
|
+
function rankMetrics(got, expected) {
|
|
60
|
+
const top = got.slice(0, 10);
|
|
61
|
+
const tp = top.filter((turn) => expected.has(turn)).length;
|
|
62
|
+
return {
|
|
63
|
+
precision: top.length === 0 ? 0 : tp / top.length,
|
|
64
|
+
recall: expected.size === 0 ? (top.length === 0 ? 1 : 0) : tp / expected.size,
|
|
65
|
+
hits: tp,
|
|
66
|
+
returned: top.length,
|
|
67
|
+
expected: expected.size,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const rows = [];
|
|
72
|
+
for (const testCase of cases) {
|
|
73
|
+
const [queryVector] = await embedTexts([testCase.query]);
|
|
74
|
+
const alias = aliasFacets(testCase.query);
|
|
75
|
+
const facet = Object.keys(alias).length > 0
|
|
76
|
+
? queryFacets(facetIndex, testCase.query, { topK: 10 }).matches.map((hit) => hit.turn)
|
|
77
|
+
: [];
|
|
78
|
+
const keyword = bm25.search(testCase.query, { topK: 10, minCoverage: 0.6 }).map((hit) => hit.turn);
|
|
79
|
+
const semantic = vectors.search(queryVector, { topK: 10 }).map((hit) => hit.turn);
|
|
80
|
+
const summary = summaries.search(testCase.query, { topK: 5, minCoverage: 0.5 }).map((hit) => hit.turn);
|
|
81
|
+
const plan = planner.query(testCase.query, { queryVector, topK: 10 });
|
|
82
|
+
const hybrid = plan.fused.map((hit) => hit.turn);
|
|
83
|
+
rows.push({
|
|
84
|
+
query: testCase.query,
|
|
85
|
+
route: plan.route,
|
|
86
|
+
expected: testCase.expected.size,
|
|
87
|
+
facet: rankMetrics(facet, testCase.expected),
|
|
88
|
+
bm25: rankMetrics(keyword, testCase.expected),
|
|
89
|
+
vector: rankMetrics(semantic, testCase.expected),
|
|
90
|
+
summary: rankMetrics(summary, testCase.expected),
|
|
91
|
+
hybrid: rankMetrics(hybrid, testCase.expected),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const aggregate = {};
|
|
96
|
+
for (const layer of ['facet', 'bm25', 'vector', 'summary', 'hybrid']) {
|
|
97
|
+
aggregate[layer] = {
|
|
98
|
+
precision: rows.reduce((sum, row) => sum + row[layer].precision, 0) / rows.length,
|
|
99
|
+
recall: rows.reduce((sum, row) => sum + row[layer].recall, 0) / rows.length,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const output = { dir, cases: rows, aggregate };
|
|
103
|
+
fs.writeFileSync(path.join(dir, 'RETRIEVAL-ABLATION.json'), JSON.stringify(output, null, 2));
|
|
104
|
+
console.log(JSON.stringify(output, null, 2));
|