@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.
Files changed (258) hide show
  1. package/INSTALL.md +25 -0
  2. package/PANEL.md +92 -0
  3. package/README.md +286 -2
  4. package/RELEASE-NOTES.md +732 -0
  5. package/bin/bc-acceptance-full.mjs +91 -0
  6. package/bin/bc-acceptance-tight.mjs +66 -0
  7. package/bin/bc-acceptance.mjs +22 -0
  8. package/bin/bc-add-keywords.mjs +21 -0
  9. package/bin/bc-demo.mjs +47 -0
  10. package/bin/bc-expand-hint.mjs +179 -0
  11. package/bin/bc-ingest-index.mjs +35 -0
  12. package/bin/bc-ingest.mjs +31 -0
  13. package/bin/bc-label-demo.mjs +18 -0
  14. package/bin/bc-relabel.mjs +150 -0
  15. package/bin/bc-sessionize.mjs +59 -0
  16. package/bin/coref_pipeline.py +47 -0
  17. package/bin/coref_service.py +57 -0
  18. package/bin/debug_ckip.py +16 -0
  19. package/bin/debug_ner_zh.py +18 -0
  20. package/bin/doctor.mjs +275 -0
  21. package/bin/en_ner_error_audit.py +33 -0
  22. package/bin/eval-label-rules.mjs +342 -0
  23. package/bin/export-turns.py +68 -0
  24. package/bin/extract-user-paragraph.mjs +14 -0
  25. package/bin/extraction-composite-smoke.mjs +28 -0
  26. package/bin/extraction-latency.mjs +58 -0
  27. package/bin/extraction-user-smoke.mjs +28 -0
  28. package/bin/extraction_service.py +164 -0
  29. package/bin/facet-active-experiment.mjs +110 -0
  30. package/bin/facet-covid.mjs +8 -0
  31. package/bin/facet-curve.mjs +27 -0
  32. package/bin/facet-embedding-train.mjs +20 -0
  33. package/bin/facet-embedding-validate.mjs +83 -0
  34. package/bin/facet-eval-roleplay.mjs +42 -0
  35. package/bin/facet-eval.mjs +38 -0
  36. package/bin/facet-fold.mjs +31 -0
  37. package/bin/facet-load.mjs +16 -0
  38. package/bin/facet-mlp-scan.mjs +45 -0
  39. package/bin/facet-mvp.mjs +34 -0
  40. package/bin/facet-online-experiments.mjs +78 -0
  41. package/bin/facet-online-race.mjs +99 -0
  42. package/bin/facet-online-validate.mjs +51 -0
  43. package/bin/facet-panel.mjs +22 -0
  44. package/bin/facet-passage.mjs +41 -0
  45. package/bin/facet-random-validate.mjs +48 -0
  46. package/bin/facet-roleplay.mjs +33 -0
  47. package/bin/facet-scan.mjs +56 -0
  48. package/bin/facet-segment.mjs +34 -0
  49. package/bin/facet-train.mjs +19 -0
  50. package/bin/facet-validate.mjs +103 -0
  51. package/bin/facet-zeroshot.mjs +14 -0
  52. package/bin/install-templates.mjs +72 -0
  53. package/bin/memory-bench.mjs +53 -0
  54. package/bin/memory-build-summary.mjs +43 -0
  55. package/bin/memory-build-vector.mjs +31 -0
  56. package/bin/memory-build.mjs +41 -0
  57. package/bin/memory-calibration.mjs +70 -0
  58. package/bin/memory-closed-loop.mjs +119 -0
  59. package/bin/memory-dry-run.mjs +70 -0
  60. package/bin/memory-eval.mjs +104 -0
  61. package/bin/memory-event-alias-test.mjs +89 -0
  62. package/bin/memory-extraction-bench.mjs +58 -0
  63. package/bin/memory-load-gate-replay.mjs +137 -0
  64. package/bin/memory-loop-test.mjs +90 -0
  65. package/bin/memory-pre-turn-harness.mjs +63 -0
  66. package/bin/memory-query.mjs +39 -0
  67. package/bin/memory-replay-hints.mjs +84 -0
  68. package/bin/memory-scale.mjs +58 -0
  69. package/bin/memory-scenario-test.mjs +146 -0
  70. package/bin/memory-user-loop.mjs +95 -0
  71. package/bin/ner_bench_tiny_zh.py +59 -0
  72. package/bin/ner_bench_zh.py +60 -0
  73. package/bin/ner_conll_en.py +139 -0
  74. package/bin/ner_direct_en.py +123 -0
  75. package/bin/ner_direct_zh.py +100 -0
  76. package/bin/ner_pipeline.py +208 -0
  77. package/bin/noise-selfcheck.mjs +50 -0
  78. package/bin/pack-integrity.mjs +47 -0
  79. package/bin/panel-check.mjs +57 -0
  80. package/bin/plugin-integrity.mjs +74 -0
  81. package/bin/preview-live.mjs +13 -0
  82. package/bin/public-install-check.mjs +222 -0
  83. package/bin/relabel-apply.mjs +272 -0
  84. package/bin/relabel-dry-run.mjs +381 -0
  85. package/bin/relik_pipeline.py +39 -0
  86. package/bin/relik_service.py +52 -0
  87. package/bin/srl_pipeline.py +55 -0
  88. package/bin/srl_service.py +68 -0
  89. package/bin/summary-index.mjs +151 -0
  90. package/bin/summary-query.mjs +79 -0
  91. package/bin/vec-check.mjs +27 -0
  92. package/bin/zh_ner_error_audit.py +36 -0
  93. package/bin/zh_ner_slot_eval.py +38 -0
  94. package/client.js +760 -0
  95. package/cordis.patch.yml +378 -0
  96. package/docs/API-NOTES.md +1743 -0
  97. package/docs/ARCHITECTURE.md +355 -0
  98. package/docs/CACHE.md +253 -0
  99. package/docs/CONDUCT.md +30 -0
  100. package/docs/CREATOR-HANDOFF.md +102 -0
  101. package/docs/EXPERIMENT.md +136 -0
  102. package/docs/FACET-MVP-REPORT.md +473 -0
  103. package/docs/GUIDE-0.4.66.md +195 -0
  104. package/docs/GUIDE.md +127 -0
  105. package/docs/MECHANISM.md +243 -0
  106. package/docs/NEW-INSTANCE-HANDOFF.md +219 -0
  107. package/docs/README.md +37 -0
  108. package/docs/SESSION-LAYOUT.md +151 -0
  109. package/docs/history-map.md +249 -0
  110. package/docs/relabel-migration-wiring.md +366 -0
  111. package/guides/00-READ-FIRST.md +206 -0
  112. package/guides/01-INSTALL.md +207 -0
  113. package/guides/02-TABLES.md +225 -0
  114. package/guides/03-AUDIT.md +201 -0
  115. package/guides/04-REBUILD.md +306 -0
  116. package/guides/05-FIRST-RUN.md +191 -0
  117. package/guides/06-EMBEDDING.md +146 -0
  118. package/guides/07-PTC-AND-ASSETS.md +221 -0
  119. package/icon.svg +7 -0
  120. package/locale/en.json +6 -0
  121. package/locale/zh.json +6 -0
  122. package/models/README.md +13 -0
  123. package/models/Xenova/bge-small-zh-v1.5/config.json +31 -0
  124. package/models/Xenova/bge-small-zh-v1.5/onnx/model_quantized.onnx +0 -0
  125. package/models/Xenova/bge-small-zh-v1.5/tokenizer.json +21278 -0
  126. package/models/Xenova/bge-small-zh-v1.5/tokenizer_config.json +15 -0
  127. package/package.json +139 -4
  128. package/src/asset-tool.mjs +237 -0
  129. package/src/assets.mjs +244 -0
  130. package/src/backfill.mjs +74 -0
  131. package/src/backup.mjs +80 -0
  132. package/src/deadend.mjs +95 -0
  133. package/src/deleted-store.mjs +35 -0
  134. package/src/engine.mjs +3831 -0
  135. package/src/facets/classifier.mjs +146 -0
  136. package/src/facets/embedding-service.mjs +53 -0
  137. package/src/facets/embedding-worker.mjs +27 -0
  138. package/src/facets/embedding.mjs +163 -0
  139. package/src/facets/extract.mjs +80 -0
  140. package/src/facets/fold-plan.mjs +84 -0
  141. package/src/facets/hybrid.mjs +29 -0
  142. package/src/facets/index.mjs +113 -0
  143. package/src/facets/linear-probe.mjs +43 -0
  144. package/src/facets/load.mjs +37 -0
  145. package/src/facets/mlp-probe.mjs +54 -0
  146. package/src/facets/mlp-service.mjs +53 -0
  147. package/src/facets/mlp-worker.mjs +26 -0
  148. package/src/facets/online-knn.mjs +58 -0
  149. package/src/facets/online-models.mjs +227 -0
  150. package/src/facets/panel-state.mjs +23 -0
  151. package/src/facets/panel.mjs +73 -0
  152. package/src/facets/random.mjs +26 -0
  153. package/src/facets/scanner.mjs +35 -0
  154. package/src/facets/schema.mjs +75 -0
  155. package/src/facets/seed-examples.mjs +58 -0
  156. package/src/facets/sensitivity.mjs +98 -0
  157. package/src/facets/service.mjs +53 -0
  158. package/src/facets/store.mjs +39 -0
  159. package/src/facets/worker.mjs +23 -0
  160. package/src/folds-store.mjs +109 -0
  161. package/src/groups.mjs +167 -0
  162. package/src/handoff-tool.mjs +51 -0
  163. package/src/handoff.mjs +127 -0
  164. package/src/index.mjs +649 -0
  165. package/src/inject-policy.mjs +17 -0
  166. package/src/learning.mjs +88 -0
  167. package/src/memory/alias-proposal.mjs +40 -0
  168. package/src/memory/bc-audit.mjs +245 -0
  169. package/src/memory/bc-index-updater.mjs +404 -0
  170. package/src/memory/bc-index.mjs +48 -0
  171. package/src/memory/bc-label.mjs +220 -0
  172. package/src/memory/bc-panel-state.mjs +107 -0
  173. package/src/memory/bc-pre-step.mjs +463 -0
  174. package/src/memory/bc-recall.mjs +470 -0
  175. package/src/memory/bc-shift.mjs +123 -0
  176. package/src/memory/bc-store.mjs +41 -0
  177. package/src/memory/bm25.mjs +112 -0
  178. package/src/memory/calibration.mjs +54 -0
  179. package/src/memory/chatter.mjs +29 -0
  180. package/src/memory/cmap-file.mjs +107 -0
  181. package/src/memory/concepts-file.mjs +41 -0
  182. package/src/memory/coref-slots.mjs +40 -0
  183. package/src/memory/event-alias-ledger.mjs +87 -0
  184. package/src/memory/event-log.mjs +95 -0
  185. package/src/memory/event-model.mjs +132 -0
  186. package/src/memory/event-segmenter.mjs +36 -0
  187. package/src/memory/event-summary-tree.mjs +122 -0
  188. package/src/memory/extraction-adapter.mjs +76 -0
  189. package/src/memory/extraction-composite.mjs +75 -0
  190. package/src/memory/extraction-core.mjs +76 -0
  191. package/src/memory/extraction-pre-step.mjs +161 -0
  192. package/src/memory/extraction-provider.mjs +300 -0
  193. package/src/memory/extraction-score.mjs +36 -0
  194. package/src/memory/facet-stabilizer.mjs +59 -0
  195. package/src/memory/history-map.mjs +139 -0
  196. package/src/memory/language-router.mjs +30 -0
  197. package/src/memory/load-gate.mjs +161 -0
  198. package/src/memory/mechanical-note.mjs +104 -0
  199. package/src/memory/onboarding.mjs +163 -0
  200. package/src/memory/persona.mjs +78 -0
  201. package/src/memory/planner.mjs +249 -0
  202. package/src/memory/pre-turn.mjs +80 -0
  203. package/src/memory/query-aliases.mjs +39 -0
  204. package/src/memory/recall-cue.mjs +43 -0
  205. package/src/memory/recall.mjs +65 -0
  206. package/src/memory/referent-candidates.mjs +55 -0
  207. package/src/memory/referent-ledger.mjs +86 -0
  208. package/src/memory/relabel-migrate.mjs +376 -0
  209. package/src/memory/relik-adapter.mjs +25 -0
  210. package/src/memory/special-keywords.mjs +60 -0
  211. package/src/memory/structured-adapters.mjs +75 -0
  212. package/src/memory/summary-index.mjs +335 -0
  213. package/src/memory/summary-tree.mjs +110 -0
  214. package/src/memory/term-extractor.mjs +44 -0
  215. package/src/memory/tokenize.mjs +9 -0
  216. package/src/memory/vector-index.mjs +54 -0
  217. package/src/memory/window-summary.mjs +133 -0
  218. package/src/mood.mjs +133 -0
  219. package/src/peer-tool.mjs +63 -0
  220. package/src/peer.mjs +144 -0
  221. package/src/policy.mjs +170 -0
  222. package/src/preflight.mjs +113 -0
  223. package/src/provenance.mjs +94 -0
  224. package/src/range.mjs +75 -0
  225. package/src/recall-exact-tool.mjs +267 -0
  226. package/src/recall-tool.mjs +521 -0
  227. package/src/recheck.mjs +99 -0
  228. package/src/save-start.mjs +29 -0
  229. package/src/session-guard.mjs +140 -0
  230. package/src/session-log-tool.mjs +322 -0
  231. package/src/session-log.mjs +606 -0
  232. package/src/skills.mjs +111 -0
  233. package/src/split-range.mjs +115 -0
  234. package/src/state.mjs +198 -0
  235. package/src/t0.mjs +149 -0
  236. package/src/tag-confirm.mjs +78 -0
  237. package/src/tag-detect.mjs +42 -0
  238. package/src/tag-maintenance.mjs +285 -0
  239. package/src/tag-reject.mjs +73 -0
  240. package/src/tags-store.mjs +94 -0
  241. package/src/tags.mjs +692 -0
  242. package/src/threads-backend.mjs +26 -0
  243. package/src/threads-core.mjs +1887 -0
  244. package/src/threads-store.mjs +207 -0
  245. package/src/threads-tool.mjs +522 -0
  246. package/src/threads.mjs +929 -0
  247. package/src/tool-preview.mjs +427 -0
  248. package/src/topic.mjs +63 -0
  249. package/src/training.mjs +75 -0
  250. package/src/turn-tag.mjs +62 -0
  251. package/src/user-turn.mjs +39 -0
  252. package/templates/cmap.md +11 -0
  253. package/templates/concepts.md +12 -0
  254. package/templates/habits.md +40 -0
  255. package/templates/keywords.md +6 -0
  256. package/templates/rebuild.md +134 -0
  257. package/templates/rules-safe.md +184 -0
  258. package/templates/rules.md +177 -0
@@ -0,0 +1,91 @@
1
+ import fs from 'node:fs';
2
+ import { loadIndexJsonl } from '../src/memory/bc-index.mjs';
3
+ import {
4
+ createCMap, expandWindow, learnFromExplanation, markAsked, renderNote, retrieve,
5
+ } from '../src/memory/bc-recall.mjs';
6
+
7
+ const root = 'C:/Users/35794/Documents/DeepSeek\'s Sandbox/coopanion-continuity';
8
+ const indexPath = `${root}/projects/bc-recall/data/index.jsonl`;
9
+ const sourcePath = `${root}/backups/continuity-tags-20261031/online400/continuity-1500.jsonl`;
10
+ const records = loadIndexJsonl(indexPath);
11
+ const rawRows = fs.readFileSync(sourcePath, 'utf8').split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
12
+ const rawByTurn = new Map(rawRows.map((row) => [row.turn, row]));
13
+ const relevant = new Set(rawRows.filter((row) => row.facets?.activity === '跑团').map((row) => row.turn));
14
+ const cmap = createCMap();
15
+
16
+ function inspect(hit) {
17
+ if (!hit) return null;
18
+ const row = rawByTurn.get(hit.turn);
19
+ return {
20
+ turn: hit.turn,
21
+ seqStart: hit.seqStart,
22
+ seqEnd: hit.seqEnd,
23
+ sourceFacet: row?.facets?.activity ?? null,
24
+ text: String(row?.text ?? '').replace(/\s+/g, ' ').slice(0, 180),
25
+ };
26
+ }
27
+
28
+ function summarize(name, result) {
29
+ const turns = (result.hits ?? []).map((hit) => hit.turn);
30
+ const relevantHits = turns.filter((turn) => relevant.has(turn)).length;
31
+ return {
32
+ name,
33
+ kind: result.kind,
34
+ via: result.via ?? null,
35
+ hits: turns.length,
36
+ relevantHits,
37
+ extraHits: turns.length - relevantHits,
38
+ recall: relevant.size === 0 ? 0 : Number((relevantHits / relevant.size).toFixed(3)),
39
+ note: renderNote(result),
40
+ samples: (result.hits ?? []).slice(0, 3).map(inspect),
41
+ };
42
+ }
43
+
44
+ const out = { records: records.length, relevantTurns: relevant.size, cases: [] };
45
+
46
+ // 1. B direct game
47
+ out.cases.push(summarize('游戏 B direct', retrieve({ records, cmap, query: '游戏', minConceptScore: 0.35 })));
48
+
49
+ // 2. Cold start + once
50
+ const cold = retrieve({ records, cmap, query: '跑团' });
51
+ out.cases.push(summarize('跑团 cold', cold));
52
+ markAsked(cmap, '跑团');
53
+ out.cases.push(summarize('跑团 after ask', retrieve({ records, cmap, query: '跑团' })));
54
+
55
+ // 3. Learn C and retrieve
56
+ const learned = learnFromExplanation(cmap, '跑团', '就是游戏、故事、对话', { records });
57
+ const retrieved = retrieve({ records: learned.records, cmap, query: '跑团', minConceptScore: 0.35 });
58
+ const window = expandWindow(learned.records, retrieved.hits?.[0], { maxTurns: 3 });
59
+ out.cases.push({
60
+ ...summarize('跑团 after C', retrieved),
61
+ window,
62
+ windowSeqs: window.turns.map((turn) => rawByTurn.get(turn)?.seqStart ?? null),
63
+ });
64
+
65
+ // 4. Technical concept
66
+ out.cases.push(summarize('技术 B direct', retrieve({ records, cmap, query: '技术', minConceptScore: 0.35 })));
67
+
68
+ // 5. Unseen concept + synthetic record
69
+ const synthetic = {
70
+ turn: 9999, seqStart: 999900, seqEnd: 999900,
71
+ generic: ['病毒', '公共卫生'], terms: ['COVID-19'],
72
+ conceptScores: { 病毒: 0.61, 公共卫生: 0.55 }, confidence: 0.61, status: 'weak',
73
+ };
74
+ const withSynthetic = [...learned.records, synthetic];
75
+ const virusCold = retrieve({ records: withSynthetic, cmap, query: 'COVID-19' });
76
+ out.cases.push(summarize('COVID-19 cold', virusCold));
77
+ markAsked(cmap, 'COVID-19');
78
+ learnFromExplanation(cmap, 'COVID-19', '就是病毒、公共卫生', { records: withSynthetic });
79
+ const virusAfter = retrieve({ records: withSynthetic, cmap, query: 'COVID-19', minConceptScore: 0.35 });
80
+ out.cases.push({
81
+ ...summarize('COVID-19 after C', virusAfter),
82
+ syntheticText: 'COVID-19 起源报告,实验室泄漏,自然宿主,冷链传播。',
83
+ });
84
+
85
+ // 6. Truly unknown term
86
+ out.cases.push(summarize('量子计算 cold', retrieve({ records, cmap, query: '量子计算' })));
87
+ markAsked(cmap, '量子计算');
88
+ out.cases.push(summarize('量子计算 after ask', retrieve({ records, cmap, query: '量子计算' })));
89
+
90
+ fs.writeFileSync(`${root}/projects/bc-recall/acceptance-full.json`, JSON.stringify(out, null, 2));
91
+ console.log(JSON.stringify(out, null, 2));
@@ -0,0 +1,66 @@
1
+ import fs from 'node:fs';
2
+ import { loadIndexJsonl } from '../src/memory/bc-index.mjs';
3
+ import {
4
+ createCMap, learnFromExplanation, markAsked, renderNote, retrieve,
5
+ } from '../src/memory/bc-recall.mjs';
6
+
7
+ const root = 'C:/Users/35794/Documents/DeepSeek\'s Sandbox/coopanion-continuity';
8
+ const records = loadIndexJsonl(`${root}/projects/bc-recall/data/index.jsonl`);
9
+ const rows = fs.readFileSync(`${root}/backups/continuity-tags-20261031/online400/continuity-1500.jsonl`, 'utf8').split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
10
+ const rawByTurn = new Map(rows.map((row) => [row.turn, row]));
11
+ const relevant = new Set(rows.filter((row) => row.facets?.activity === '跑团').map((row) => row.turn));
12
+ const cmap = createCMap();
13
+
14
+ function sample(turn) {
15
+ const row = rawByTurn.get(turn);
16
+ return row === undefined ? null : {
17
+ turn,
18
+ seqStart: row.seqStart,
19
+ sourceFacet: row.facets?.activity ?? null,
20
+ text: String(row.text).replace(/\s+/g, ' ').slice(0, 120),
21
+ };
22
+ }
23
+
24
+ function caseOf(name, result) {
25
+ const turns = (result.hits ?? []).map((hit) => hit.turn);
26
+ return {
27
+ name,
28
+ kind: result.kind,
29
+ via: result.via ?? null,
30
+ hits: turns.length,
31
+ relevantHits: turns.filter((turn) => relevant.has(turn)).length,
32
+ extraHits: turns.filter((turn) => !relevant.has(turn)).length,
33
+ note: renderNote(result),
34
+ seedSamples: turns.slice(0, 3).map(sample),
35
+ };
36
+ }
37
+
38
+ const out = { records: records.length, relevantTurns: relevant.size, cases: [] };
39
+ out.cases.push(caseOf('游戏 B direct', retrieve({ records, cmap, query: '游戏', minConceptScore: 0.35 })));
40
+ out.cases.push(caseOf('跑团 cold', retrieve({ records, cmap, query: '跑团' })));
41
+ markAsked(cmap, '跑团');
42
+ out.cases.push(caseOf('跑团 after ask', retrieve({ records, cmap, query: '跑团' })));
43
+ const learned = learnFromExplanation(cmap, '跑团', '就是游戏、故事、对话', { records });
44
+ const run = retrieve({ records: learned.records, cmap, query: '跑团', minConceptScore: 0.35 });
45
+ out.cases.push(caseOf('跑团 after C', run));
46
+ // Only an explicit LLM read, not an automatic expansion.
47
+ const seedTurn = run.hits?.[0]?.turn;
48
+ if (seedTurn !== undefined) {
49
+ const turns = [];
50
+ for (let turn = seedTurn - 3; turn <= seedTurn + 3; turn += 1) {
51
+ if (rawByTurn.has(turn)) turns.push(turn);
52
+ }
53
+ out.llmRead = { seedTurn, chosenTurns: turns, samples: turns.map(sample) };
54
+ }
55
+ out.cases.push(caseOf('技术 B direct', retrieve({ records, cmap, query: '技术', minConceptScore: 0.35 })));
56
+ const synthetic = { turn: 9999, seqStart: 999900, seqEnd: 999900, generic: ['病毒', '公共卫生'], terms: ['COVID-19'], keywords: ['COVID-19', '实验室泄漏', '自然宿主', '冷链传播'], conceptScores: { 病毒: 0.61, 公共卫生: 0.55 }, confidence: 0.61, status: 'weak' };
57
+ const withSynthetic = [...learned.records, synthetic];
58
+ out.cases.push(caseOf('COVID-19 cold', retrieve({ records: withSynthetic, cmap, query: 'COVID-19' })));
59
+ markAsked(cmap, 'COVID-19');
60
+ learnFromExplanation(cmap, 'COVID-19', '就是病毒、公共卫生', { records: withSynthetic });
61
+ out.cases.push(caseOf('COVID-19 after C', retrieve({ records: withSynthetic, cmap, query: 'COVID-19', minConceptScore: 0.35 })));
62
+ out.cases.push(caseOf('量子计算 cold', retrieve({ records, cmap, query: '量子计算' })));
63
+ markAsked(cmap, '量子计算');
64
+ out.cases.push(caseOf('量子计算 after ask', retrieve({ records, cmap, query: '量子计算' })));
65
+ fs.writeFileSync(`${root}/projects/bc-recall/acceptance-tight.json`, JSON.stringify(out, null, 2));
66
+ console.log(JSON.stringify(out, null, 2));
@@ -0,0 +1,22 @@
1
+ import fs from 'node:fs';
2
+ import { loadBCStore } from '../src/memory/bc-store.mjs';
3
+ import { learnFromExplanation, retrieve, renderNote } from '../src/memory/bc-recall.mjs';
4
+
5
+ const file = process.argv[2] ?? 'C:/Users/35794/Documents/DeepSeek\'s Sandbox/coopanion-continuity/projects/bc-recall/data/store.json';
6
+ const store = loadBCStore(file);
7
+ const before = {};
8
+ for (const query of ['游戏', '跑团', '病毒']) {
9
+ const result = retrieve({ records: store.records, cmap: store.cmap, query, minConceptScore: 0.35 });
10
+ before[query] = { kind: result.kind, via: result.via ?? null, hits: result.hits?.length ?? 0, note: renderNote(result) };
11
+ }
12
+ const learned = learnFromExplanation(store.cmap, '跑团', '就是游戏、故事、对话', { records: store.records });
13
+ const after = retrieve({ records: learned.records, cmap: store.cmap, query: '跑团', minConceptScore: 0.35 });
14
+ const report = {
15
+ store: file,
16
+ records: store.records.length,
17
+ before,
18
+ learned: { ok: learned.ok, generic: learned.generic },
19
+ after: { kind: after.kind, via: after.via, hits: after.hits?.length ?? 0, note: renderNote(after) },
20
+ };
21
+ fs.writeFileSync('C:/Users/35794/Documents/DeepSeek\'s Sandbox/coopanion-continuity/projects/bc-recall/acceptance.json', JSON.stringify(report, null, 2));
22
+ console.log(JSON.stringify(report, null, 2));
@@ -0,0 +1,21 @@
1
+ import fs from 'node:fs';
2
+ import { extractKeywords } from '../src/memory/bc-recall.mjs';
3
+ import { loadSpecialKeywords, matchSpecialKeywords } from '../src/memory/special-keywords.mjs';
4
+ import { loadIndexJsonl, saveIndexJsonl } from '../src/memory/bc-index.mjs';
5
+
6
+ const root = 'C:/Users/35794/Documents/DeepSeek\'s Sandbox/coopanion-continuity';
7
+ const indexFile = `${root}/projects/bc-recall/data/index.jsonl`;
8
+ const sourceFile = `${root}/backups/continuity-tags-20261031/online400/continuity-1500.jsonl`;
9
+ const records = loadIndexJsonl(indexFile);
10
+ const rows = fs.readFileSync(sourceFile, 'utf8').split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
11
+ const textByTurn = new Map(rows.map((row) => [row.turn, row.text]));
12
+ const specialKeywords = loadSpecialKeywords();
13
+ for (const record of records) {
14
+ const text = textByTurn.get(record.turn) ?? '';
15
+ record.keywords = [...new Set([
16
+ ...extractKeywords(text, { limit: 12 }),
17
+ ...matchSpecialKeywords(text, specialKeywords),
18
+ ])];
19
+ }
20
+ saveIndexJsonl(indexFile, records);
21
+ console.log(JSON.stringify({ records: records.length, sample: records.slice(0, 2).map((record) => ({ turn: record.turn, keywords: record.keywords })) }, null, 2));
@@ -0,0 +1,47 @@
1
+ import fs from 'node:fs';
2
+ import { backfillRecords, createCMap, learnTerm, makeRecord, renderNote, retrieve } from '../src/memory/bc-recall.mjs';
3
+
4
+ const rows = fs.readFileSync('C:/Users/35794/Documents/DeepSeek\'s Sandbox/coopanion-continuity/backups/continuity-tags-20261031/online400/continuity-1500.jsonl', 'utf8')
5
+ .split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line))
6
+ .filter((row) => row.facets?.activity === '跑团');
7
+
8
+ const genericOf = (facets) => {
9
+ const out = [];
10
+ if (facets.activity === '跑团') out.push('游戏');
11
+ if (facets.activity === '对话') out.push('对话');
12
+ if (['开发', '调试', '测试', '文档'].includes(facets.activity)) out.push('技术');
13
+ if (facets.topic === '剧情') out.push('故事');
14
+ if (['技术', '设备', '记忆', '规则'].includes(facets.topic)) out.push(facets.topic);
15
+ return [...new Set(out)];
16
+ };
17
+
18
+ let records = rows.map((row) => makeRecord({
19
+ turn: row.turn,
20
+ seqStart: row.seqStart,
21
+ seqEnd: row.seqEnd,
22
+ text: row.text,
23
+ generic: genericOf(row.facets),
24
+ terms: row.facets.activity === '跑团' ? ['跑团'] : [],
25
+ }));
26
+ records = backfillRecords(records, '跑团', ['游戏', '故事']);
27
+ records.push(makeRecord({
28
+ turn: 900,
29
+ seqStart: 5000,
30
+ seqEnd: 5001,
31
+ text: 'COVID-19 起源报告,实验室泄漏,自然宿主,冷链传播。',
32
+ generic: ['病毒', '公共卫生'],
33
+ terms: ['COVID-19', '实验室泄漏'],
34
+ status: 'weak',
35
+ confidence: 0.33,
36
+ }));
37
+
38
+ const cmap = createCMap();
39
+ const out = [];
40
+ for (const query of ['游戏', '跑团', '病毒', 'COVID-19', '疫情']) {
41
+ const result = retrieve({ records, cmap, query });
42
+ out.push({ query, kind: result.kind, via: result.via ?? null, note: renderNote(result), hits: result.hits?.length ?? 0 });
43
+ }
44
+ learnTerm(cmap, '跑团', ['游戏', '故事', '对话']);
45
+ const learned = retrieve({ records, cmap, query: '跑团' });
46
+ out.push({ query: '跑团(after C)', kind: learned.kind, via: learned.via, note: renderNote(learned), hits: learned.hits?.length ?? 0 });
47
+ console.log(JSON.stringify({ records: records.length, examples: out }, null, 2));
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Offline expand-hint reminder tool.
4
+ * Reads an index + turn texts, clusters, and prints candidate topic clusters.
5
+ * Never writes concepts/index. --write only appends to expand-hint.jsonl.
6
+ */
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import os from 'node:os';
10
+ import { embedTexts } from '../src/facets/embedding.mjs';
11
+ import { extractKeywords } from '../src/memory/bc-recall.mjs';
12
+ import { loadIndexJsonl } from '../src/memory/bc-index.mjs';
13
+ import { loadConcepts } from '../src/memory/concepts-file.mjs';
14
+
15
+ function arg(name, fallback = null) {
16
+ const at = process.argv.indexOf(`--${name}`);
17
+ return at >= 0 ? process.argv[at + 1] ?? fallback : fallback;
18
+ }
19
+ const has = (name) => process.argv.includes(`--${name}`);
20
+ const indexFile = arg('index', path.join(os.homedir(), '.dsh', 'continuity', 'bc-index.jsonl'));
21
+ const conceptsFile = arg('concepts', path.join(os.homedir(), '.dsh', 'continuity', 'concepts.md'));
22
+ const turnsFile = arg('turns');
23
+ const outFile = arg('out', path.join(os.homedir(), '.dsh', 'continuity', 'expand-hint.jsonl'));
24
+ const verdict = arg('record-verdict');
25
+ if (verdict) {
26
+ if (!['accepted', 'rejected'].includes(verdict)) {
27
+ console.error('--record-verdict must be accepted or rejected');
28
+ process.exit(2);
29
+ }
30
+ const event = {
31
+ at: new Date().toISOString(),
32
+ event: 'verdict',
33
+ by: 'cli',
34
+ verdict,
35
+ auditAt: arg('audit-at', ''),
36
+ reason: arg('reason', ''),
37
+ };
38
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
39
+ fs.appendFileSync(outFile, `${JSON.stringify(event)}\n`, 'utf8');
40
+ console.log(`recorded ${verdict}`);
41
+ process.exit(0);
42
+ }
43
+ if (!turnsFile) {
44
+ console.error('missing --turns <turns.jsonl>');
45
+ process.exit(2);
46
+ }
47
+ const until = Number(arg('until', Infinity));
48
+ const records = loadIndexJsonl(indexFile).filter((record) => !Number.isFinite(until) || record.turn <= until);
49
+ if (records.length === 0) {
50
+ console.error('index has no records');
51
+ process.exit(2);
52
+ }
53
+ const rows = loadConcepts(conceptsFile);
54
+ if (rows.length === 0) {
55
+ console.error('concepts.md has no valid rows');
56
+ process.exit(2);
57
+ }
58
+ const concepts = rows.map((row) => row.label);
59
+ const textByTurn = new Map(fs.readFileSync(turnsFile, 'utf8').split(/\r?\n/).filter(Boolean).map((line) => {
60
+ const row = JSON.parse(line);
61
+ return [row.turn, row.text];
62
+ }));
63
+ const missing = records.filter((record) => !textByTurn.has(record.turn));
64
+ if (missing.length > 0) {
65
+ console.error(`missing turn text for ${missing.length} records: ${missing.slice(0, 8).map((r) => r.turn).join(', ')}`);
66
+ process.exit(3);
67
+ }
68
+
69
+ const N = records.length;
70
+ const k = Number(arg('k', Math.max(20, Math.min(60, Math.round(N / 50))))) || 30;
71
+ const minSize = Number(arg('min-size', 10)) || 10;
72
+ const topN = Number(arg('top', 5)) || 5;
73
+ const maxTopic = Number(arg('max-topic', 0.5));
74
+
75
+ const norm = (v) => {
76
+ const n = Math.sqrt(v.reduce((sum, x) => sum + x * x, 0)) || 1;
77
+ return v.map((x) => x / n);
78
+ };
79
+ const cos = (a, b) => {
80
+ let dot = 0;
81
+ for (let i = 0; i < Math.min(a.length, b.length); i += 1) dot += a[i] * b[i];
82
+ return dot;
83
+ };
84
+ const texts = records.map((record) => String(textByTurn.get(record.turn) ?? ''));
85
+ const vectors = [];
86
+ for (let i = 0; i < texts.length; i += 16) vectors.push(...await embedTexts(texts.slice(i, i + 16)));
87
+ const points = records.map((record, i) => ({ turn: record.turn, v: norm(vectors[i]), record }));
88
+
89
+ // chatterScore
90
+ const df = new Map();
91
+ const tokensByTurn = new Map();
92
+ for (const [turn, text] of textByTurn) {
93
+ const tokens = [...String(text).matchAll(/[\u4e00-\u9fff]{2,6}/g)].map((m) => m[0]);
94
+ tokensByTurn.set(turn, tokens);
95
+ for (const token of new Set(tokens)) df.set(token, (df.get(token) ?? 0) + 1);
96
+ }
97
+ const functionRe = /[浜嗙殑鐫€鍚楀憿鍚у晩]/g;
98
+ const chatterScore = (turn) => {
99
+ const text = String(textByTurn.get(turn) ?? '');
100
+ const tokens = tokensByTurn.get(turn) ?? [];
101
+ const cjkTotal = tokens.length;
102
+ const proper = tokens.filter((token) => (df.get(token) ?? 0) <= 3).length;
103
+ const properDensity = cjkTotal ? proper / cjkTotal : 0;
104
+ const fnCount = (text.match(functionRe) ?? []).length;
105
+ const fnDensity = fnCount / Math.max(1, text.length);
106
+ const keywordSparse = extractKeywords(text, { limit: 10 }).length < 3 ? 1 : 0;
107
+ const quotes = (text.match(/[「」“”"——]/g) ?? []).length;
108
+ return 0.30 * (text.length < 60 ? 1 : 0)
109
+ + 0.25 * Math.min(1, properDensity)
110
+ + 0.20 * Math.min(1, fnDensity * 20)
111
+ + 0.15 * keywordSparse
112
+ + 0.10 * (quotes > 0 ? 1 : 0);
113
+ };
114
+
115
+ // Deterministic k-means (seeded init, cosine).
116
+ let seed = 42;
117
+ const rand = () => {
118
+ seed = (seed * 1664525 + 1013904223) >>> 0;
119
+ return seed / 2 ** 32;
120
+ };
121
+ const centers = [];
122
+ const used = new Set();
123
+ while (centers.length < k) {
124
+ const index = Math.floor(rand() * points.length);
125
+ if (used.has(index)) continue;
126
+ used.add(index);
127
+ centers.push(points[index].v);
128
+ }
129
+ let groups = [];
130
+ for (let iteration = 0; iteration < 15; iteration += 1) {
131
+ groups = centers.map((center) => ({ center, members: [] }));
132
+ for (const point of points) {
133
+ let best = 0;
134
+ let bestScore = -Infinity;
135
+ for (let c = 0; c < centers.length; c += 1) {
136
+ const score = cos(point.v, centers[c]);
137
+ if (score > bestScore) { bestScore = score; best = c; }
138
+ }
139
+ groups[best].members.push(point);
140
+ }
141
+ for (let c = 0; c < centers.length; c += 1) {
142
+ if (groups[c].members.length === 0) continue;
143
+ const sum = new Array(points[0].v.length).fill(0);
144
+ for (const point of groups[c].members) for (let i = 0; i < sum.length; i += 1) sum[i] += point.v[i];
145
+ centers[c] = norm(sum);
146
+ }
147
+ }
148
+ const clusters = groups.filter((group) => group.members.length >= minSize).map((group) => {
149
+ const turns = group.members.map((point) => point.turn);
150
+ const topicMax = group.members.reduce((sum, point) => {
151
+ const scores = point.record.conceptScores ?? {};
152
+ return sum + Math.max(0, ...concepts.map((name) => scores[name] ?? 0));
153
+ }, 0) / group.members.length;
154
+ const chatter = group.members.reduce((sum, point) => sum + chatterScore(point.turn), 0) / group.members.length;
155
+ const kw = [...new Set(extractKeywords(turns.map((turn) => textByTurn.get(turn) ?? '').join('\n'), { limit: 30 })
156
+ .map((word) => String(word).toLowerCase()))].filter((word) => word.length >= 2).slice(0, 6);
157
+ return { turns, size: turns.length, start: Math.min(...turns), end: Math.max(...turns), topicMax, chatter, kw };
158
+ });
159
+ const candidates = clusters.filter((cluster) => cluster.topicMax < maxTopic)
160
+ .sort((a, b) => a.topicMax - b.topicMax || b.size - a.size)
161
+ .slice(0, topN);
162
+ if (candidates.length === 0) {
163
+ console.log('no candidates');
164
+ process.exit(0);
165
+ }
166
+ for (const item of candidates) {
167
+ console.log(`turn ${item.start}-${item.end} - ${item.size}轮 - topicMax ${item.topicMax.toFixed(2)} - chatter ${item.chatter.toFixed(2)} - kw ${item.kw.join('/')}`);
168
+ console.log(' → topicMax<0.5 且 kw 是话题词(非人名/道具/英文标识符)才考虑加;加就一步:concepts.md 追加 `名字 | 词1、词2… | 覆盖估算`');
169
+ }
170
+ if (has('write')) {
171
+ const at = new Date().toISOString();
172
+ const lines = candidates.map((item) => JSON.stringify({
173
+ at, event: 'pending', range: [item.start, item.end], size: item.size,
174
+ topicMax: Number(item.topicMax.toFixed(2)), chatter: Number(item.chatter.toFixed(2)), kw: item.kw,
175
+ }));
176
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
177
+ fs.appendFileSync(outFile, `${lines.join('\n')}\n`, 'utf8');
178
+ }
179
+
@@ -0,0 +1,35 @@
1
+ import fs from 'node:fs';
2
+ import { embedTexts } from '../src/facets/embedding.mjs';
3
+ import { createBLabeler } from '../src/memory/bc-label.mjs';
4
+ import { makeRecord } from '../src/memory/bc-recall.mjs';
5
+ import { loadSpecialKeywords, matchSpecialKeywords } from '../src/memory/special-keywords.mjs';
6
+ import { saveIndexJsonl } from '../src/memory/bc-index.mjs';
7
+
8
+ const source = 'C:/Users/35794/Documents/DeepSeek\'s Sandbox/coopanion-continuity/backups/continuity-tags-20261031/online400/continuity-1500.jsonl';
9
+ const target = 'C:/Users/35794/Documents/DeepSeek\'s Sandbox/coopanion-continuity/projects/bc-recall/data/index.jsonl';
10
+ const rows = fs.readFileSync(source, 'utf8').split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
11
+ const specialKeywords = loadSpecialKeywords();
12
+ const labeler = createBLabeler({ embed: embedTexts });
13
+ const records = [];
14
+ for (const row of rows) {
15
+ if (!Number.isInteger(row.turn) || !Number.isInteger(row.seqStart)) continue;
16
+ const label = await labeler.label(row.text, { topK: 3, minScore: 0.25 });
17
+ const generic = label.candidates.map((candidate) => candidate.concept);
18
+ const conceptScores = Object.fromEntries(label.candidates.map((candidate) => [candidate.concept, candidate.score]));
19
+ const terms = [];
20
+ if (row.facets?.activity === '跑团') terms.push('跑团');
21
+ if (/COVID|实验室泄漏|自然宿主|冷链传播/.test(row.text)) terms.push('COVID-19');
22
+ records.push(makeRecord({
23
+ turn: row.turn,
24
+ seqStart: row.seqStart,
25
+ seqEnd: row.seqEnd,
26
+ generic,
27
+ terms,
28
+ keywords: matchSpecialKeywords(row.text, specialKeywords),
29
+ conceptScores,
30
+ confidence: label.top?.score ?? 0,
31
+ status: 'weak',
32
+ }));
33
+ }
34
+ saveIndexJsonl(target, records);
35
+ console.log(JSON.stringify({ source, target, rows: rows.length, records: records.length }, null, 2));
@@ -0,0 +1,31 @@
1
+ import fs from 'node:fs';
2
+ import { embedTexts } from '../src/facets/embedding.mjs';
3
+ import { createBLabeler } from '../src/memory/bc-label.mjs';
4
+ import { makeRecord } from '../src/memory/bc-recall.mjs';
5
+ import { emptyStore, saveBCStore } from '../src/memory/bc-store.mjs';
6
+
7
+ const source = process.argv[2] ?? 'C:/Users/35794/Documents/DeepSeek\'s Sandbox/coopanion-continuity/backups/continuity-tags-20261031/online400/continuity-1500.jsonl';
8
+ const target = process.argv[3] ?? 'C:/Users/35794/Documents/DeepSeek\'s Sandbox/coopanion-continuity/projects/bc-recall/data/store.json';
9
+ const rows = fs.readFileSync(source, 'utf8').split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line))
10
+ .filter((row) => row.facets?.activity === '跑团');
11
+ const labeler = createBLabeler({ embed: embedTexts });
12
+ const store = emptyStore();
13
+ for (const row of rows) {
14
+ const label = await labeler.label(row.text, { topK: 3, minScore: 0.25 });
15
+ const generic = label.candidates.map((candidate) => candidate.concept);
16
+ const conceptScores = Object.fromEntries(label.candidates.map((candidate) => [candidate.concept, candidate.score]));
17
+ const terms = ['跑团'];
18
+ store.records.push(makeRecord({
19
+ turn: row.turn,
20
+ seqStart: row.seqStart,
21
+ seqEnd: row.seqEnd,
22
+ text: row.text,
23
+ generic,
24
+ terms,
25
+ confidence: label.top?.score ?? 0,
26
+ conceptScores,
27
+ status: 'weak',
28
+ }));
29
+ }
30
+ saveBCStore(target, store);
31
+ console.log(JSON.stringify({ source, target, records: store.records.length, sample: store.records.slice(0, 3) }, null, 2));
@@ -0,0 +1,18 @@
1
+ import { embedTexts } from '../src/facets/embedding.mjs';
2
+ import { createBLabeler } from '../src/memory/bc-label.mjs';
3
+
4
+ const labeler = createBLabeler({ embed: embedTexts });
5
+ for (const text of [
6
+ '我把七把钥匙收进兜里,走到门口,用鞋尖量了量潮线到船位的距离。',
7
+ '我昨天看了COVID-19的起源报告,病毒可能来自实验室泄漏,也提到自然宿主和冷链传播。',
8
+ '我们接下来去跑团吧。',
9
+ '帮我看一下这个函数的 bug。',
10
+ ]) {
11
+ const result = await labeler.label(text);
12
+ console.log(JSON.stringify({
13
+ text: text.slice(0, 40),
14
+ top: result.top,
15
+ candidates: result.candidates,
16
+ unknown: result.unknown,
17
+ }, null, 2));
18
+ }
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * One-shot concept migration: re-label every turn in an existing index using
4
+ * concepts.md, write a NEW file, and validate before replacing anything.
5
+ *
6
+ * Usage:
7
+ * node bin/bc-relabel.mjs --index <bc-index.jsonl> --turns <turns.jsonl> \
8
+ * --concepts <concepts.md> --out <new-index.jsonl> [--write]
9
+ *
10
+ * turns.jsonl: one JSON object per line: { "turn": number, "text": string }
11
+ * The tool never edits the source index. Without --write it only reports.
12
+ */
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import { createBLabeler } from '../src/memory/bc-label.mjs';
16
+ import { embedTexts } from '../src/facets/embedding.mjs';
17
+ import { loadIndexJsonl, saveIndexJsonl } from '../src/memory/bc-index.mjs';
18
+ import { loadConcepts } from '../src/memory/concepts-file.mjs';
19
+ import { buildDf, chatterScore } from '../src/memory/chatter.mjs';
20
+
21
+ function arg(name, fallback = null) {
22
+ const index = process.argv.indexOf(`--${name}`);
23
+ return index >= 0 ? process.argv[index + 1] ?? fallback : fallback;
24
+ }
25
+ function has(name) {
26
+ return process.argv.includes(`--${name}`);
27
+ }
28
+
29
+ const indexFile = arg('index');
30
+ const turnsFile = arg('turns');
31
+ const conceptsFile = arg('concepts');
32
+ const outFile = arg('out');
33
+ if (!indexFile || !turnsFile || !outFile) {
34
+ console.error('missing --index / --turns / --out');
35
+ process.exit(2);
36
+ }
37
+ const rows = loadConcepts(conceptsFile ?? undefined);
38
+ console.log(JSON.stringify({ concepts: rows.length, names: rows.map((row) => row.label) }, null, 2));
39
+
40
+ const labels = Object.fromEntries(rows.map((row) => [row.label, row.description]));
41
+ const turns = new Map();
42
+ for (const line of fs.readFileSync(turnsFile, 'utf8').split(/\r?\n/)) {
43
+ if (!line.trim()) continue;
44
+ const item = JSON.parse(line);
45
+ if (Number.isInteger(item.turn) && typeof item.text === 'string') turns.set(item.turn, item.text);
46
+ }
47
+ const records = loadIndexJsonl(indexFile);
48
+ const missing = records.filter((record) => !turns.has(record.turn)).map((record) => record.turn);
49
+ if (missing.length > 0) {
50
+ console.error(`missing turn text for ${missing.length} records: ${missing.slice(0, 10).join(', ')}`);
51
+ process.exit(3);
52
+ }
53
+ if (has('vectors')) {
54
+ const df = buildDf(records.map((record) => turns.get(record.turn) ?? ''));
55
+ const next = [];
56
+ for (let i = 0; i < records.length; i += 16) {
57
+ const batch = records.slice(i, i + 16);
58
+ const todo = batch.filter((record) => typeof record.vec !== 'string' || !record.vec);
59
+ const vectors = todo.length > 0 ? await embedTexts(todo.map((record) => turns.get(record.turn) ?? '')) : [];
60
+ let cursor = 0;
61
+ for (const record of batch) {
62
+ if (typeof record.vec === 'string' && record.vec) {
63
+ next.push(record);
64
+ } else {
65
+ const vector = vectors[cursor++];
66
+ next.push({
67
+ ...record,
68
+ vec: Buffer.from(new Float32Array(vector).buffer).toString('base64'),
69
+ chatter: Number(chatterScore(turns.get(record.turn) ?? '', df).toFixed(4)),
70
+ });
71
+ }
72
+ }
73
+ }
74
+ const bad = next.filter((record) => {
75
+ const buffer = Buffer.from(record.vec ?? '', 'base64');
76
+ return !buffer.length || !(record.chatter >= 0 && record.chatter <= 1);
77
+ });
78
+ console.log(JSON.stringify({ records: next.length, existing: records.length - next.filter((record) => !record.vec).length, bad: bad.length }, null, 2));
79
+ if (bad.length > 0) process.exit(4);
80
+ if (!has('write')) {
81
+ console.log('dry-run only; add --write to emit the vector index');
82
+ process.exit(0);
83
+ }
84
+ saveIndexJsonl(outFile, next);
85
+ console.log(`wrote ${outFile}`);
86
+ process.exit(0);
87
+ }
88
+ if (rows.length === 0) {
89
+ console.error('concepts.md has no valid rows');
90
+ process.exit(2);
91
+ }
92
+ const labeler = createBLabeler({ embed: embedTexts, labels, concepts: Object.keys(labels) });
93
+ const checks = [
94
+ [1323, '跑团叙事'],
95
+ [6, '插件开发'],
96
+ [866, '线程标签'],
97
+ [1584, '手机与流量'],
98
+ [1598, '权限与通知'],
99
+ [1612, '权限与通知'],
100
+ [827, '补丁与验证'],
101
+ [827, '备份与重启'],
102
+ [866, '关键词与标签'],
103
+ ];
104
+ const next = [];
105
+ for (const record of records) {
106
+ const label = await labeler.label(turns.get(record.turn), { topK: Object.keys(labels).length, minScore: 0 });
107
+ const scores = Object.fromEntries(label.candidates.map((candidate) => [candidate.concept, candidate.score]));
108
+ const generic = label.candidates.filter((candidate) => candidate.score >= 0.25).slice(0, 3).map((candidate) => candidate.concept);
109
+ next.push({
110
+ ...record,
111
+ generic,
112
+ conceptScores: scores,
113
+ confidence: label.top?.score ?? 0,
114
+ status: 'weak',
115
+ });
116
+ }
117
+ const checkResults = checks.map(([turn, concept]) => {
118
+ const record = next.find((item) => item.turn === turn);
119
+ if (!record) return { turn, concept, missing: true };
120
+ const ranked = Object.entries(record.conceptScores ?? {}).sort((a, b) => b[1] - a[1]);
121
+ const rank = ranked.findIndex(([name]) => name === concept) + 1;
122
+ return {
123
+ turn,
124
+ concept,
125
+ textLength: String(turns.get(turn) ?? '').length,
126
+ score: record.conceptScores?.[concept] ?? null,
127
+ rank,
128
+ top3: ranked.slice(0, 3).map(([name, score]) => ({ name, score: Number(score.toFixed(3)) })),
129
+ vector: turn === 827 ? ranked.map(([name, score]) => ({ name, score: Number(score.toFixed(3)) })) : undefined,
130
+ };
131
+ });
132
+ const bad = next.filter((record) => (record.generic ?? []).some((name) => !labels[name]));
133
+ if (bad.length > 0) {
134
+ console.error(`records with unknown concept names: ${bad.length}`);
135
+ process.exit(4);
136
+ }
137
+ console.log(JSON.stringify({ index: indexFile, records: records.length, out: outFile, missing: 0, bad: 0, checks: checkResults }, null, 2));
138
+ if (!has('write')) {
139
+ console.log('dry-run only; add --write to emit the new index');
140
+ process.exit(0);
141
+ }
142
+ const temp = `${outFile}.tmp`;
143
+ saveIndexJsonl(temp, next);
144
+ const parsed = loadIndexJsonl(temp);
145
+ if (parsed.length !== records.length) {
146
+ console.error(`validation failed: wrote ${parsed.length}, expected ${records.length}`);
147
+ process.exit(5);
148
+ }
149
+ fs.renameSync(temp, outFile);
150
+ console.log(`wrote ${outFile}`);