@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,88 @@
1
+ import fs from 'node:fs';
2
+
3
+ /**
4
+ * 学习候选:从工具回执里找“同一类错误”,聚合成候选教训。
5
+ * 只做发现与计数;写规则仍由实例决定(插件只把候选和提问推给实例)。
6
+ */
7
+ export const ERROR_RE = /(SyntaxError|TypeError|ReferenceError|RangeError|Cannot find module|EPERM|ENOENT|Traceback|anchor count [1-9]|Unexpected token|is not a function|undefined is not)/;
8
+
9
+ /** 把一条回执归一成一个签名;不是错误就返回 null。 */
10
+ export function signatureOf(text) {
11
+ const body = String(text ?? "");
12
+ // 探针不算教训:带 PROBE_ 的签名是我们自己刻意制造的失败。
13
+ if (body.includes("PROBE_")) return null;
14
+ if (!body) return null;
15
+ // 只学“真正失败”的回执:harness 对非零退出会追加 [exit code: N]。
16
+ // 正常退出的脚本即使文字里引用了错误行,也不算教训(否则会学自己的报告)。
17
+ if (!/\[exit code: [1-9]\d*\]/.test(body)) return null;
18
+ const line = body.split("\n").find((l) => ERROR_RE.test(l) && !/^\s*\[/.test(l) && !/^\s*\d+x\s/.test(l) && !/\(seq \d+\.\.\d+\)/.test(l));
19
+ if (!line) return null;
20
+ return line
21
+ .replace(/[A-Za-z]:\\[^\s"']+/g, "<path>")
22
+ .replace(/\d+/g, "<n>")
23
+ .trim()
24
+ .slice(0, 140);
25
+ }
26
+
27
+ /** 记一次出现。返回签名(或 null)。 */
28
+ export function note(map, seq, text) {
29
+ const sig = signatureOf(text);
30
+ if (sig === null) return null;
31
+ const cur = map.get(sig) ?? { count: 0, firstSeq: null, lastSeq: null };
32
+ cur.count += 1;
33
+ // 扫描顺序是从新到旧,所以 first/last 按 seq 数值取 min/max,不能按"先见到"。
34
+ cur.firstSeq = cur.firstSeq === null ? seq : Math.min(cur.firstSeq, seq);
35
+ cur.lastSeq = cur.lastSeq === null ? seq : Math.max(cur.lastSeq, seq);
36
+ map.set(sig, cur);
37
+ return sig;
38
+ }
39
+
40
+ /** 出现 ≥ min 次的候选,按次数降序。 */
41
+ export function topCandidates(map, min = 2, limit = 3) {
42
+ return [...map.entries()]
43
+ .filter(([, g]) => g.count >= min)
44
+ .sort((a, b) => b[1].count - a[1].count)
45
+ .slice(0, limit)
46
+ .map(([sig, g]) => ({ sig, count: g.count, firstSeq: g.firstSeq, lastSeq: g.lastSeq }));
47
+ }
48
+
49
+ /**
50
+ * 扫描给定节点里新增的 tool/result,记入 map。
51
+ * 返回新的 cursor;纯函数,便于离线用真实事件验证。
52
+ */
53
+ export function scanResults(map, { nodes, eventAt, cursor = 0, init = false, lookback = 500 }) {
54
+ const list = Array.isArray(nodes) ? nodes : nodes ? [...nodes] : [];
55
+ let maxSeq = cursor;
56
+ // init=true:从尾扫到 0(cursor 会提前停);首次:只回看**最新的** lookback 个节点。
57
+ const lowest = init ? 0 : Math.max(0, list.length - lookback);
58
+ for (let i = list.length - 1; i >= lowest; i--) {
59
+ const seq = list[i];
60
+ if (typeof seq !== 'number' || seq <= cursor) continue;
61
+ if (seq > maxSeq) maxSeq = seq;
62
+ const event = eventAt(seq);
63
+ if (event?.type !== 'tool/result') continue;
64
+ const c = event.data?.message?.content ?? event.data?.content ?? [];
65
+ const body = typeof c === 'string' ? c : Array.isArray(c) ? c.map((x) => x?.text ?? '').join(' ') : '';
66
+ note(map, seq, body);
67
+ }
68
+ return maxSeq;
69
+ }
70
+ /** 读已答记录(每行 {sig, decision}),返回签名集合。 */
71
+ export function loadAnswered(file) {
72
+ const set = new Set();
73
+ try {
74
+ const text = fs.readFileSync(file, 'utf8');
75
+ for (const line of text.split(/\r?\n/)) {
76
+ if (!line.trim()) continue;
77
+ try { const row = JSON.parse(line); if (row?.sig) set.add(row.sig); } catch { /* 坏行跳过 */ }
78
+ }
79
+ } catch { /* 文件不存在 = 没有已答记录 */ }
80
+ return set;
81
+ }
82
+
83
+ /** 过滤掉已答过的候选(按完整签名或前缀匹配)。 */
84
+ export function filterAnswered(candidates, answered) {
85
+ const list = [...answered];
86
+ // 历史记录可能只存了前缀,且前缀可能位于签名中间:用双向包含判定。
87
+ return candidates.filter((c) => !list.some((a) => a && (c.sig === a || c.sig.includes(a) || a.includes(c.sig))));
88
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Alias-candidate state: watch new terms, propose an alias only after repeated
3
+ * use, never on the first mention, and never for an existing alias.
4
+ */
5
+ import { DEFAULT_ALIASES } from '../facets/schema.mjs';
6
+
7
+ export class AliasProposalState {
8
+ constructor({ threshold = 2, cooldownTurns = 10, counts = new Map(), proposed = new Set(), lastProposalTurn = null } = {}) {
9
+ this.threshold = threshold;
10
+ this.cooldownTurns = cooldownTurns;
11
+ this.counts = counts;
12
+ this.proposed = proposed;
13
+ this.lastProposalTurn = lastProposalTurn;
14
+ }
15
+
16
+ observe({ term, facets = {}, nowTurn = 0 }) {
17
+ const clean = String(term ?? '').trim();
18
+ if (clean.length === 0) return { action: 'none', reason: 'no-term' };
19
+ if (DEFAULT_ALIASES[clean] !== undefined) return { action: 'none', reason: 'known-alias' };
20
+ const count = (this.counts.get(clean) ?? 0) + 1;
21
+ this.counts.set(clean, count);
22
+ if (count < this.threshold) return { action: 'none', reason: 'need-repeat', count };
23
+ if (this.proposed.has(clean)) return { action: 'none', reason: 'already-proposed', count };
24
+ if (this.lastProposalTurn !== null && nowTurn - this.lastProposalTurn < this.cooldownTurns) {
25
+ return { action: 'none', reason: 'cooldown', count };
26
+ }
27
+ const target = facets.activity ?? facets.topic ?? null;
28
+ if (target === null) return { action: 'none', reason: 'no-target', count };
29
+ this.proposed.add(clean);
30
+ this.lastProposalTurn = nowTurn;
31
+ return {
32
+ action: 'propose',
33
+ reason: 'proposed',
34
+ term: clean,
35
+ target,
36
+ text: `[新别名?] ${clean} → ${target}`,
37
+ count,
38
+ };
39
+ }
40
+ }
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Automatic, non-blocking expand-hint audit.
3
+ *
4
+ * Uses vec + chatter + keywords already stored in bc-index.
5
+ * Writes only expand-hint.jsonl + expand-hint.pending.json + audit-state.json.
6
+ * Never changes concepts or the index.
7
+ */
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { randomUUID } from 'node:crypto';
11
+ import { loadIndexJsonl } from './bc-index.mjs';
12
+ import { loadConcepts, defaultConceptsPath } from './concepts-file.mjs';
13
+
14
+ const norm = (v) => {
15
+ const n = Math.sqrt(v.reduce((sum, x) => sum + x * x, 0)) || 1;
16
+ return v.map((x) => x / n);
17
+ };
18
+ const cos = (a, b) => {
19
+ let dot = 0;
20
+ for (let i = 0; i < Math.min(a.length, b.length); i += 1) dot += a[i] * b[i];
21
+ return dot;
22
+ };
23
+ const readJson = (file, fallback) => {
24
+ try { return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')); } catch { return fallback; }
25
+ };
26
+ const writeJson = (file, value) => {
27
+ fs.mkdirSync(path.dirname(file), { recursive: true });
28
+ fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
29
+ };
30
+ const decodeVec = (text) => {
31
+ if (typeof text !== 'string' || text.length === 0) throw new Error('audit: empty vector');
32
+ if (text.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(text)) throw new Error('audit: invalid base64');
33
+ const buffer = Buffer.from(text, 'base64');
34
+ if (buffer.length === 0 || buffer.length % 4 !== 0) throw new Error('audit: invalid vector length');
35
+ const values = Array.from(new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4));
36
+ if (values.length === 0 || values.some((value) => !Number.isFinite(value))) {
37
+ throw new Error('audit: vector contains non-finite values');
38
+ }
39
+ if (values.every((value) => value === 0)) throw new Error('audit: zero vector');
40
+ return values;
41
+ };
42
+
43
+ export function auditPaths(indexFile) {
44
+ const dir = path.dirname(indexFile);
45
+ return {
46
+ stateFile: path.join(dir, 'audit-state.json'),
47
+ hintFile: path.join(dir, 'expand-hint.jsonl'),
48
+ pendingFile: path.join(dir, 'expand-hint.pending.json'),
49
+ };
50
+ }
51
+
52
+ export function readAuditState(indexFile) {
53
+ const value = readJson(auditPaths(indexFile).stateFile, null);
54
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
55
+ ? value
56
+ : { lastAuditTurn: 0 };
57
+ }
58
+
59
+ const AUDIT_RUNNING_TTL_MS = 15 * 60 * 1000;
60
+ const auditLockFile = (indexFile) => path.join(path.dirname(indexFile), 'audit.lock');
61
+
62
+ function acquireAuditLock(indexFile) {
63
+ const file = auditLockFile(indexFile);
64
+ const token = randomUUID();
65
+ try {
66
+ const fd = fs.openSync(file, 'wx');
67
+ fs.writeFileSync(fd, JSON.stringify({ token, pid: process.pid, at: Date.now() }));
68
+ fs.closeSync(fd);
69
+ return token;
70
+ } catch (error) {
71
+ if (error?.code !== 'EEXIST') return null;
72
+ try {
73
+ const stat = fs.statSync(file);
74
+ const future = stat.mtimeMs > Date.now() + 5 * 60 * 1000;
75
+ if (!future && Date.now() - stat.mtimeMs <= AUDIT_RUNNING_TTL_MS) return null;
76
+ const stale = readJson(file, null);
77
+ if (!stale || typeof stale !== 'object') return null;
78
+ const quarantine = `${file}.stale.${process.pid}.${randomUUID()}`;
79
+ fs.renameSync(file, quarantine);
80
+ const moved = readJson(quarantine, null);
81
+ if (moved?.token !== stale.token) {
82
+ try { fs.renameSync(quarantine, file); } catch { /* lost race */ }
83
+ return null;
84
+ }
85
+ try { fs.unlinkSync(quarantine); } catch { /* ignore */ }
86
+ return acquireAuditLock(indexFile);
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+ }
92
+
93
+ function releaseAuditLock(indexFile, token) {
94
+ const file = auditLockFile(indexFile);
95
+ try {
96
+ const current = readJson(file, null);
97
+ if (current?.token !== token) return false;
98
+ fs.unlinkSync(file);
99
+ return true;
100
+ } catch {
101
+ return false;
102
+ }
103
+ }
104
+
105
+ export function consumePendingHint(indexFile) {
106
+ const { pendingFile, hintFile } = auditPaths(indexFile);
107
+ const pending = readJson(pendingFile, null);
108
+ if (!pending || typeof pending.text !== 'string' || !pending.text) return null;
109
+ try { fs.unlinkSync(pendingFile); } catch { /* ignore */ }
110
+ try {
111
+ fs.appendFileSync(hintFile, `${JSON.stringify({ at: new Date().toISOString(), event: 'delivered', text: pending.text })}\n`, 'utf8');
112
+ } catch { /* ignore */ }
113
+ return pending.text;
114
+ }
115
+
116
+ export function maybeRunAudit({ indexFile, conceptsFile = null, lastTurn, onError = null } = {}) {
117
+ if (!Number.isInteger(lastTurn)) return false;
118
+ let state = readAuditState(indexFile);
119
+ if (lastTurn - (state.lastAuditTurn ?? 0) < 200) return false;
120
+ if (state.running === true) {
121
+ const startedAt = Number(state.runningAt ?? 0);
122
+ const future = startedAt > Date.now() + 5 * 60 * 1000;
123
+ const stale = !Number.isFinite(startedAt) || startedAt <= 0 || future || Date.now() - startedAt > AUDIT_RUNNING_TTL_MS;
124
+ if (!stale) return false;
125
+ }
126
+ const token = acquireAuditLock(indexFile);
127
+ if (!token) return false;
128
+ try {
129
+ state = readAuditState(indexFile);
130
+ if (lastTurn - (state.lastAuditTurn ?? 0) < 200) {
131
+ releaseAuditLock(indexFile, token);
132
+ return false;
133
+ }
134
+ if (state.running === true) {
135
+ const startedAt = Number(state.runningAt ?? 0);
136
+ const future = startedAt > Date.now() + 5 * 60 * 1000;
137
+ const stale = !Number.isFinite(startedAt) || startedAt <= 0 || future || Date.now() - startedAt > AUDIT_RUNNING_TTL_MS;
138
+ if (!stale) {
139
+ releaseAuditLock(indexFile, token);
140
+ return false;
141
+ }
142
+ state = { ...state, running: false, runningAt: null, recoveredAt: new Date().toISOString() };
143
+ }
144
+ writeJson(auditPaths(indexFile).stateFile, { ...state, running: true, runningAt: Date.now() });
145
+ setImmediate(() => {
146
+ runAudit({ indexFile, conceptsFile })
147
+ .then(() => releaseAuditLock(indexFile, token))
148
+ .catch((error) => {
149
+ try { onError?.(error); } catch { /* ignore */ }
150
+ writeJson(auditPaths(indexFile).stateFile, { ...readAuditState(indexFile), running: false, runningAt: null, lastError: String(error?.message ?? error) });
151
+ releaseAuditLock(indexFile, token);
152
+ });
153
+ });
154
+ return true;
155
+ } catch {
156
+ releaseAuditLock(indexFile, token);
157
+ return false;
158
+ }
159
+ }
160
+
161
+ export async function runAudit({ indexFile, conceptsFile } = {}) {
162
+ const records = loadIndexJsonl(indexFile);
163
+ const rows = loadConcepts(conceptsFile ?? defaultConceptsPath());
164
+ if (records.length === 0 || rows.length === 0) throw new Error('audit needs records and concepts');
165
+ const withVec = records.filter((record) => typeof record.vec === 'string' && record.vec.length > 0);
166
+ const missing = records.length - withVec.length;
167
+ if (withVec.length === 0 || missing / records.length > 0.1) {
168
+ throw new Error(`audit needs vec on at least 90% of records (${withVec.length}/${records.length})`);
169
+ }
170
+ const concepts = rows.map((row) => row.label);
171
+ const points = [];
172
+ let dimension = null;
173
+ for (const record of withVec) {
174
+ const values = decodeVec(record.vec);
175
+ if (dimension === null) dimension = values.length;
176
+ else if (values.length !== dimension) throw new Error(`audit: mixed vector dimensions (${dimension} vs ${values.length})`);
177
+ points.push({ turn: record.turn, v: norm(values), record });
178
+ }
179
+ const k = Math.min(points.length, Math.max(20, Math.min(60, Math.round(points.length / 50))));
180
+ let seed = 42;
181
+ const rand = () => { seed = (seed * 1664525 + 1013904223) >>> 0; return seed / 2 ** 32; };
182
+ const centers = [];
183
+ const used = new Set();
184
+ while (centers.length < k) {
185
+ const index = Math.floor(rand() * points.length);
186
+ if (used.has(index)) continue;
187
+ used.add(index);
188
+ centers.push(points[index].v);
189
+ }
190
+ let groups = [];
191
+ for (let iteration = 0; iteration < 12; iteration += 1) {
192
+ groups = centers.map((center) => ({ center, members: [] }));
193
+ for (const point of points) {
194
+ let best = 0;
195
+ let bestScore = -Infinity;
196
+ for (let c = 0; c < centers.length; c += 1) {
197
+ const score = cos(point.v, centers[c]);
198
+ if (score > bestScore) { bestScore = score; best = c; }
199
+ }
200
+ groups[best].members.push(point);
201
+ }
202
+ for (let c = 0; c < centers.length; c += 1) {
203
+ if (groups[c].members.length === 0) continue;
204
+ const sum = new Array(points[0].v.length).fill(0);
205
+ for (const point of groups[c].members) for (let i = 0; i < sum.length; i += 1) sum[i] += point.v[i];
206
+ centers[c] = norm(sum);
207
+ }
208
+ }
209
+ const clusters = groups.filter((group) => group.members.length >= 10).map((group) => {
210
+ const members = group.members;
211
+ const topicMax = members.reduce((sum, point) => {
212
+ const scores = point.record.conceptScores ?? {};
213
+ return sum + Math.max(0, ...concepts.map((name) => scores[name] ?? 0));
214
+ }, 0) / members.length;
215
+ const chatter = members.reduce((sum, point) => sum + (point.record.chatter ?? 0), 0) / members.length;
216
+ const turns = members.map((point) => point.turn).sort((a, b) => a - b);
217
+ const kw = [...new Set(members.flatMap((point) => point.record.keywords ?? []))].slice(0, 6);
218
+ return { start: turns[0], end: turns.at(-1), size: turns.length, topicMax, chatter, kw };
219
+ });
220
+ const candidates = clusters.filter((cluster) => cluster.topicMax < 0.5)
221
+ .sort((a, b) => a.topicMax - b.topicMax || b.size - a.size).slice(0, 5);
222
+ const state = readAuditState(indexFile);
223
+ const at = new Date().toISOString();
224
+ if (candidates.length > 0) {
225
+ const lines = candidates.map((item) => ({
226
+ at, event: 'pending', range: [item.start, item.end], size: item.size,
227
+ topicMax: Number(item.topicMax.toFixed(2)), chatter: Number(item.chatter.toFixed(2)), kw: item.kw,
228
+ }));
229
+ fs.appendFileSync(auditPaths(indexFile).hintFile, `${lines.map((line) => JSON.stringify(line)).join('\n')}\n`, 'utf8');
230
+ const hint = ' → topicMax<0.5 且 kw 是话题词(非人名/道具/英文标识符)才考虑加;加就一步:concepts.md 追加 `名字 | 词1、词2… | 覆盖估算`';
231
+ const text = `[continuity:audit] 距上次 200 轮,${candidates.length} 个候选:\n`
232
+ + candidates.map((item) => `- turn ${item.start}-${item.end} · ${item.size}轮 · topicMax ${item.topicMax.toFixed(2)} · chatter ${item.chatter.toFixed(2)} · kw ${item.kw.join('/')}\n${hint}`).join('\n');
233
+ writeJson(auditPaths(indexFile).pendingFile, { at, text });
234
+ }
235
+ writeJson(auditPaths(indexFile).stateFile, {
236
+ lastAuditTurn: records.at(-1)?.turn ?? state.lastAuditTurn ?? 0,
237
+ nextAuditTurn: (records.at(-1)?.turn ?? 0) + 200,
238
+ lastResult: candidates.length > 0 ? 'candidates' : 'no-candidates',
239
+ candidates: candidates.length,
240
+ at,
241
+ running: false,
242
+ runningAt: null,
243
+ });
244
+ return { candidates: candidates.length };
245
+ }