@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,376 @@
1
+ /**
2
+ * relabel-migrate.mjs — 历史重打(**只改 topic 四字段**)的进程内启动迁移。
3
+ *
4
+ * 为什么要在进程内做:bc-index-updater 写盘是整文件重写,`records` 是启动时读进内存的。
5
+ * 外部替换文件后只要再跑一轮对话,就会被内存里的旧标签整文件覆盖。
6
+ *
7
+ * 触发方式(显式 opt-in,升级本身绝不自动打 marker):
8
+ * 在 session 目录放 `bc-index.relabel-pending.json`:
9
+ * { "fields": ["topic"], "baseline": "self-loo", "conceptsHash": "<concepts.md 的 sha256>" }
10
+ * conceptsHash 不符 → abort(保留 marker),避免拿旧表重标。
11
+ *
12
+ * 行为:
13
+ * 1. 读 raw、按 updater 同规则重建每轮 text
14
+ * 2. 一轮打标拿到所有 topic 的 raw 分 → 算 self-loo 均值(本 session 自己)
15
+ * 3. 用 centered≥centerMin 且 lead≥lead 判 topic(与 4.11.23+ labeler 同口径)
16
+ * 4. 只改 genericTopic / topicScore / topicConfidence / topicMinScore
17
+ * 5. 备份 index → 写 tmp → 校验(行数、非目标字段深度相等)→ rename → 删 marker
18
+ * 6. 出错:保留 marker 与原始 index,把错误抛给调用方(启动可继续,不阻塞服务)
19
+ *
20
+ * 不做的事:
21
+ * · 不改 workflow(等 workflow baseline 成熟)
22
+ * · 不改 raw
23
+ * · 不自动创建 marker
24
+ */
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+ import zlib from 'node:zlib';
28
+ import { createHash } from 'node:crypto';
29
+ import { fileURLToPath } from 'node:url';
30
+
31
+ const INJECTED = /^\[(?:抽取|工作线|标签|规则|习惯|技能|归位|候选|存\?|闸|回正|学习|追忆|记忆|continuity:[a-z-]+)\]/;
32
+ const MACHINE = /^(?:Current runtime context|Current working directory|Current DSH file policy|Approval prompts|Time sampled)/;
33
+
34
+ const TOPIC_FIELDS = ['genericTopic', 'topicScore', 'topicConfidence', 'topicMinScore'];
35
+ // labelStamp 是第五个"允许变更"的元字段:迁移把它写成当前依据的戳,
36
+ // 否则第一次迁移会被自己的"非目标字段零改动"校验 abort(创 2026-11-04 指出)。
37
+ const STAMP_FIELD = 'labelStamp';
38
+ const ALLOWED_FIELDS = [...TOPIC_FIELDS, STAMP_FIELD];
39
+
40
+ export function conceptsHash(file) {
41
+ return createHash('sha256').update(fs.readFileSync(file)).digest('hex');
42
+ }
43
+
44
+ export function readMarker(file) {
45
+ if (!fs.existsSync(file)) return null;
46
+ try {
47
+ return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ function readRawLines(file) {
54
+ if (!/\.zstd$/i.test(file)) {
55
+ return fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '').split(/\r?\n/).filter(Boolean);
56
+ }
57
+ const buf = fs.readFileSync(file);
58
+ const MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
59
+ const starts = [];
60
+ for (let i = 0; ; ) {
61
+ const k = buf.indexOf(MAGIC, i);
62
+ if (k < 0) break;
63
+ starts.push(k);
64
+ i = k + 4;
65
+ }
66
+ const lines = [];
67
+ for (const start of starts) {
68
+ let out;
69
+ try { out = zlib.zstdDecompressSync(buf.subarray(start)); } catch { continue; }
70
+ for (const line of out.toString('utf8').split(/\r?\n/)) if (line) lines.push(line);
71
+ }
72
+ return lines;
73
+ }
74
+
75
+ function turnsFromEvents(events, eventText) {
76
+ const markers = events
77
+ .map((event) => ({ seq: Number(event?.seq), turn: Number(event?.data?.turn) }))
78
+ .filter((m) => Number.isInteger(m.seq) && Number.isInteger(m.turn))
79
+ .sort((a, b) => a.seq - b.seq);
80
+ const turnAt = (seq) => {
81
+ let found = null;
82
+ for (const marker of markers) { if (marker.seq > seq) break; found = marker.turn; }
83
+ return found ?? markers.find((m) => m.seq >= seq)?.turn ?? null;
84
+ };
85
+ const isHuman = (event) => {
86
+ const source = event?.data?.source ?? event?.source;
87
+ if (source && typeof source.kind === 'string' && source.kind !== 'user') return false;
88
+ if (source && source.peer === true) return false;
89
+ const text = eventText(event, { includeReasoning: false }).trim();
90
+ return Boolean(text) && !INJECTED.test(text) && !MACHINE.test(text);
91
+ };
92
+ const turns = new Map();
93
+ for (const event of events) {
94
+ const type = event?.type;
95
+ if (type !== 'user/message' && type !== 'assistant/message') continue;
96
+ if (type === 'user/message' && !isHuman(event)) continue;
97
+ const text = eventText(event, { includeReasoning: false }).trim();
98
+ if (!text || INJECTED.test(text) || MACHINE.test(text)) continue;
99
+ const seq = Number(event?.seq);
100
+ const turn = Number.isInteger(Number(event?.data?.turn)) ? Number(event.data.turn) : turnAt(seq);
101
+ if (!Number.isInteger(turn)) continue;
102
+ const item = turns.get(turn) ?? { turn, userTexts: [], assistantTexts: [] };
103
+ if (type === 'user/message') item.userTexts.push(text);
104
+ else item.assistantTexts.push(text);
105
+ turns.set(turn, item);
106
+ }
107
+ return turns;
108
+ }
109
+
110
+ /** 单轮判定:与 labeler 的 centered≥centerMin 且 lead≥lead 同口径。 */
111
+ export function decideTopic(scores, baseline, { topicMinScore = 0.33, centerMin = 0.02, lead = 0.02 } = {}) {
112
+ const items = Object.entries(scores)
113
+ .filter(([, raw]) => Number.isFinite(raw))
114
+ .map(([concept, raw]) => ({ concept, raw, centered: raw - (Number.isFinite(baseline?.[concept]) ? baseline[concept] : 0) }))
115
+ .sort((a, b) => b.centered - a.centered);
116
+ const top = items[0];
117
+ const second = items[1];
118
+ if (!top) return null;
119
+ if (top.raw < topicMinScore) return null;
120
+ if (top.centered < centerMin) return null;
121
+ if (second && top.centered - second.centered < lead) return null;
122
+ return { concept: top.concept, raw: top.raw, centered: top.centered, confidence: confidenceOf(top.raw) };
123
+ }
124
+
125
+ function confidenceOf(score) {
126
+ if (score >= 0.6) return 'strong';
127
+ if (score >= 0.45) return 'normal';
128
+ return 'weak';
129
+ }
130
+
131
+ /**
132
+ * 执行一次迁移。返回 {status, ...},不抛异常(除非调用方要求)。
133
+ *
134
+ * 事件来源二选一(in-process 与 CLI):
135
+ * · events: async () => Event[] —— 插件内用(宿主没有 raw 文件路径,只有 sessionQuery)
136
+ * · rawFile: string —— CLI 用(本地会话文件)
137
+ * 两者都没给 → abort 'no-event-source'。
138
+ *
139
+ * @param {object} opts
140
+ * @param {string} opts.indexFile
141
+ * @param {string} [opts.rawFile]
142
+ * @param {() => Promise<object[]>} [opts.events]
143
+ * @param {string} opts.conceptsFile
144
+ * @param {string} opts.markerFile
145
+ * @param {object} opts.labeler createBLabeler(...) 的实例
146
+ * @param {(event, options?) => string} opts.eventText session-log 的 eventText
147
+ * @param {Function} [opts.log]
148
+ */
149
+ export async function runRelabelMigration({ indexFile, rawFile, events: eventsProvider, conceptsFile, markerFile, labeler, eventText, forceFull = false, log = () => {} }) {
150
+ const marker = readMarker(markerFile);
151
+ if (!marker) return { status: 'no-marker' };
152
+ const fields = Array.isArray(marker.fields) ? marker.fields : [];
153
+ if (!fields.includes('topic')) return { status: 'skip', reason: 'marker.fields 不含 topic' };
154
+ if (marker.baseline !== 'self-loo') return { status: 'skip', reason: '只支持 baseline=self-loo' };
155
+ const hasEventsProvider = typeof eventsProvider === 'function';
156
+ const hasRawFile = Boolean(rawFile) && fs.existsSync(rawFile);
157
+ if (!hasEventsProvider && !hasRawFile) {
158
+ log('[relabel] abort:既没有 events provider,也没有可读的 rawFile');
159
+ return { status: 'abort', reason: 'no-event-source' };
160
+ }
161
+ const hash = conceptsHash(conceptsFile);
162
+ if (marker.conceptsHash && marker.conceptsHash !== hash) {
163
+ log(`[relabel] abort:concepts.md 已变(marker ${marker.conceptsHash.slice(0, 12)} ≠ 当前 ${hash.slice(0, 12)})`);
164
+ return { status: 'abort', reason: 'concepts-hash-mismatch' };
165
+ }
166
+ // 阈值:marker 可覆盖,必须有限且在区间内;非法值回退默认并 warn
167
+ const rawThresholds = marker.thresholds ?? {};
168
+ const finiteIn = (value, min, max, fallback, name) => {
169
+ const n = Number(value);
170
+ if (value === undefined) return fallback;
171
+ if (Number.isFinite(n) && n >= min && n <= max) return n;
172
+ log(`[relabel] warn:marker.thresholds.${name}=${value} 非法,用默认 ${fallback}`);
173
+ return fallback;
174
+ };
175
+ const thresholds = {
176
+ topicMinScore: finiteIn(rawThresholds.topicMinScore, 0.2, 0.9, 0.33, 'topicMinScore'),
177
+ centerMin: finiteIn(rawThresholds.centerMin, 0, 0.5, 0.02, 'centerMin'),
178
+ lead: finiteIn(rawThresholds.lead, 0, 0.5, 0.02, 'lead'),
179
+ };
180
+ // labelStamp 的依据必须真实,但**不能**用包版本:
181
+ // 发版(哪怕只改文档)会让 stamp 全员失效、触发一次全量重扫(创 2026-11-04 定)。
182
+ // 来源顺序:marker.labelerVersion(仅回归覆盖)→ bc-label.mjs 的 LABELER_VERSION 常量。
183
+ let labelerVersion = typeof marker.labelerVersion === 'string' ? marker.labelerVersion : null;
184
+ if (!labelerVersion) {
185
+ const { LABELER_VERSION } = await import('./bc-label.mjs');
186
+ labelerVersion = Number.isFinite(Number(LABELER_VERSION)) ? String(LABELER_VERSION) : null;
187
+ }
188
+ if (!labelerVersion) {
189
+ log('[relabel] abort:拿不到 labelerVersion(bc-label.mjs 未导出 LABELER_VERSION,marker 也没给)');
190
+ return { status: 'abort', reason: 'no-labeler-version' };
191
+ }
192
+ const stamp = createHash('sha256')
193
+ .update(`${hash}|${thresholds.topicMinScore}|${thresholds.centerMin}|${thresholds.lead}|${labelerVersion}`)
194
+ .digest('hex')
195
+ .slice(0, 12);
196
+
197
+ const started = Date.now();
198
+ log('[relabel] 开始:取事件');
199
+ const rawEvents = hasEventsProvider
200
+ ? await eventsProvider()
201
+ : readRawLines(rawFile).map((line) => {
202
+ try { return JSON.parse(line.replace(/^\uFEFF/, '')); } catch { return null; }
203
+ }).filter(Boolean);
204
+ if (!Array.isArray(rawEvents) || rawEvents.length === 0) {
205
+ log('[relabel] abort:事件为空');
206
+ return { status: 'abort', reason: 'empty-events' };
207
+ }
208
+ const events = [];
209
+ const seen = new Set();
210
+ for (const event of rawEvents) {
211
+ const seq = Number(event?.seq);
212
+ if (Number.isInteger(seq)) { if (seen.has(seq)) continue; seen.add(seq); }
213
+ events.push(event);
214
+ }
215
+ events.sort((a, b) => Number(a.seq) - Number(b.seq));
216
+ const turns = turnsFromEvents(events, eventText);
217
+ log(`[relabel] 事件 ${events.length} · 轮次 ${turns.size}`);
218
+
219
+ const records = fs.readFileSync(indexFile, 'utf8').replace(/^\uFEFF/, '')
220
+ .split(/\r?\n/).filter((line) => line.trim() !== '').map((line) => JSON.parse(line));
221
+
222
+ // 安全增量:只有“全表 stamp 都等于本次 stamp”才整体跳过。
223
+ // 不做逐行跳过:self-LOO baseline 会因为少算 stale 行而偏,写盘会得到错误的 centered。
224
+ if (!forceFull && records.length > 0 && records.every((record) => record.labelStamp === stamp)) {
225
+ fs.unlinkSync(markerFile);
226
+ log('[relabel] 跳过:所有行 labelStamp 已是当前值');
227
+ return {
228
+ status: 'done',
229
+ noop: true,
230
+ stats: { rows: records.length, relabelled: 0, unresolved: 0, topicChanged: 0, toEmpty: 0, fromEmpty: 0, same: records.length },
231
+ };
232
+ }
233
+
234
+ // 覆盖前置检查:**先验证事件能不能覆盖 index 的全部 turn,再开始打标**。
235
+ // 若宿主给的是有界快照,原来的流程会先白烧几秒到几分钟 CPU(风扇先转一下),
236
+ // 打标完才发现大量行重建不出来。宁可在 0.1 秒内失败。
237
+ const indexTurns = records
238
+ .map((record) => Number(record.turn))
239
+ .filter((turn) => Number.isInteger(turn));
240
+ const missingTurns = indexTurns.filter((turn) => !turns.has(turn));
241
+ if (missingTurns.length > 0) {
242
+ log(`[relabel] abort:事件覆盖不足(事件 ${events.length} · 推出轮次 ${turns.size} · index ${indexTurns.length} 轮 · 缺 ${missingTurns.length} 轮)`);
243
+ return {
244
+ status: 'abort',
245
+ reason: 'read-not-covering',
246
+ events: events.length,
247
+ turnsFromEvents: turns.size,
248
+ indexTurns: indexTurns.length,
249
+ missing: missingTurns.length,
250
+ missingSample: missingTurns.slice(0, 10),
251
+ };
252
+ }
253
+
254
+ // 第一遍:每个可重打轮次拿全部 topic 的 raw 分(居中/领先都不设限,只取分)
255
+ const scored = new Map();
256
+ let done = 0;
257
+ const topicsSeen = new Set();
258
+ for (const record of records) {
259
+ const turn = Number(record.turn);
260
+ const entry = Number.isInteger(turn) ? turns.get(turn) : null;
261
+ if (!entry) continue;
262
+ const text = [...entry.userTexts, ...entry.assistantTexts].join('\n');
263
+ const res = await labeler.labelChunked(text, {
264
+ topK: 24, minScore: 0.25, topicMinScore: -Infinity, topicCenterMin: -Infinity, topicLead: -Infinity, topicTopK: 24,
265
+ });
266
+ const scores = {};
267
+ for (const item of res.topic ?? []) {
268
+ if (Number.isFinite(item.score)) scores[item.concept] = item.score;
269
+ topicsSeen.add(item.concept);
270
+ }
271
+ scored.set(turn, scores);
272
+ done += 1;
273
+ if (done % 200 === 0) log(`[relabel] 打标 ${done} 轮 · 已用 ${Math.round((Date.now() - started) / 1000)}s`);
274
+ }
275
+ if (scored.size === 0) {
276
+ log('[relabel] abort:没有可重打的轮次');
277
+ return { status: 'abort', reason: 'no-turns' };
278
+ }
279
+
280
+ // apply 是 all-or-nothing:只要有一行重建不出 text,就在写备份之前 abort。
281
+ // 不能指望后面的字段校验兜底——旧行若已带同 stamp,字段校验会通过,
282
+ // 结果就是"rename 成功 + 删 marker",却留下未重标的行(创 2026-11-04 指出)。
283
+ const unresolvedTurns = records
284
+ .map((record) => Number(record.turn))
285
+ .filter((turn) => !Number.isInteger(turn) || !scored.has(turn));
286
+ if (unresolvedTurns.length > 0) {
287
+ log(`[relabel] abort:${unresolvedTurns.length} 行无法重建 text(例如 turn ${unresolvedTurns.slice(0, 5).join(', ')}),未写任何文件`);
288
+ return { status: 'abort', reason: 'unresolved-rows', unresolved: unresolvedTurns.slice(0, 20) };
289
+ }
290
+
291
+ // self-loo 均值:本 session 自己
292
+ const sums = {};
293
+ const counts = {};
294
+ for (const scores of scored.values()) {
295
+ for (const [concept, raw] of Object.entries(scores)) {
296
+ sums[concept] = (sums[concept] ?? 0) + raw;
297
+ counts[concept] = (counts[concept] ?? 0) + 1;
298
+ }
299
+ }
300
+ const globalMean = Object.values(sums).reduce((a, b) => a + b, 0) / Math.max(1, Object.keys(sums).length);
301
+ const looFor = (scores) => {
302
+ const out = {};
303
+ for (const [concept, mean] of Object.entries(sums)) {
304
+ const own = scores[concept];
305
+ const n = counts[concept] ?? 0;
306
+ out[concept] = n > 1 && Number.isFinite(own) ? (mean - own) / (n - 1) : mean / Math.max(1, n);
307
+ }
308
+ // 缺失项用全局均值兜底(与生产 baseline 的 missing 回退同口径)
309
+ return new Proxy(out, { get: (target, key) => (Number.isFinite(target[key]) ? target[key] : globalMean) });
310
+ };
311
+
312
+ // 第二遍:判定 + 改写(只四个字段)
313
+ const updated = [];
314
+ const stats = { rows: records.length, relabelled: 0, unresolved: 0, topicChanged: 0, toEmpty: 0, fromEmpty: 0, same: 0 };
315
+ for (const record of records) {
316
+ const turn = Number(record.turn);
317
+ const scores = Number.isInteger(turn) ? scored.get(turn) : null;
318
+ if (!scores) {
319
+ stats.unresolved += 1;
320
+ updated.push(JSON.stringify(record));
321
+ continue;
322
+ }
323
+ const decision = decideTopic(scores, looFor(scores), thresholds);
324
+ const oldTopic = Array.isArray(record.genericTopic) && record.genericTopic.length > 0 ? record.genericTopic[0] : null;
325
+ const next = {};
326
+ for (const [key, value] of Object.entries(record)) {
327
+ if (TOPIC_FIELDS.includes(key) || key === STAMP_FIELD) continue;
328
+ next[key] = value;
329
+ }
330
+ next.genericTopic = decision ? [decision.concept] : [];
331
+ if (decision) {
332
+ next.topicScore = Number(decision.raw.toFixed(6));
333
+ next.topicConfidence = decision.confidence;
334
+ }
335
+ next.topicMinScore = thresholds.topicMinScore;
336
+ next[STAMP_FIELD] = stamp;
337
+ if (oldTopic !== (decision?.concept ?? null)) {
338
+ stats.topicChanged += 1;
339
+ if (oldTopic && !decision) stats.toEmpty += 1;
340
+ if (!oldTopic && decision) stats.fromEmpty += 1;
341
+ } else {
342
+ stats.same += 1;
343
+ }
344
+ stats.relabelled += 1;
345
+ updated.push(JSON.stringify(next));
346
+ }
347
+
348
+ // 备份 + 写 tmp + 校验 + rename
349
+ const backup = `${indexFile}.bak-${new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14)}`;
350
+ fs.copyFileSync(indexFile, backup);
351
+ const tmp = `${indexFile}.relabel-tmp-${process.pid}`;
352
+ fs.writeFileSync(tmp, updated.join('\n') + '\n', 'utf8');
353
+
354
+ const after = fs.readFileSync(tmp, 'utf8').split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
355
+ if (after.length !== records.length) {
356
+ log(`[relabel] abort:行数不一致 ${records.length} → ${after.length}(原文件未动)`);
357
+ return { status: 'abort', reason: 'length-mismatch' };
358
+ }
359
+ let touched = 0;
360
+ for (let i = 0; i < records.length; i += 1) {
361
+ for (const key of Object.keys(records[i])) {
362
+ if (ALLOWED_FIELDS.includes(key)) continue;
363
+ if (JSON.stringify(records[i][key]) !== JSON.stringify(after[i][key])) touched += 1;
364
+ }
365
+ // labelStamp 只允许写成"本次的戳";写别的值等于改了依据,算越界
366
+ if (after[i][STAMP_FIELD] !== stamp) touched += 1;
367
+ }
368
+ if (touched > 0) {
369
+ log(`[relabel] abort:非目标字段被改动 ${touched} 处(原文件未动)`);
370
+ return { status: 'abort', reason: 'field-mismatch' };
371
+ }
372
+ fs.renameSync(tmp, indexFile);
373
+ fs.unlinkSync(markerFile);
374
+ log(`[relabel] 完成:重打 ${stats.relabelled} · unresolved ${stats.unresolved} · 换 topic ${stats.topicChanged}(→空 ${stats.toEmpty} · 空→topic ${stats.fromEmpty})· 不变 ${stats.same} · 备份 ${path.basename(backup)}`);
375
+ return { status: 'done', stats, backup, elapsedMs: Date.now() - started, topics: topicsSeen.size };
376
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Adapter for ReLiK entity-linking output.
3
+ * Accepts the documented RelikOutput shape:
4
+ * { text, spans: [{start,end,text}], candidates: { span: [ [ {text,id}, ... ] ] } }
5
+ */
6
+ import { normalizeEntityLinks } from './structured-adapters.mjs';
7
+
8
+ export function relikToLinks(output = {}) {
9
+ const spans = Array.isArray(output.spans) ? output.spans : [];
10
+ const candidateGroups = output.candidates?.span ?? [];
11
+ const links = [];
12
+ for (let index = 0; index < spans.length; index += 1) {
13
+ const span = spans[index];
14
+ const candidates = candidateGroups[index]?.[0] ?? candidateGroups[0]?.[0] ?? [];
15
+ const top = candidates.find((candidate) => candidate.text === span.label || candidate.text === span.text) ?? candidates[0] ?? null;
16
+ links.push({
17
+ text: span.text ?? output.text?.slice(span.start, span.end) ?? '',
18
+ entityId: top?.id ?? null,
19
+ score: Number.isFinite(top?.score) ? top.score : null,
20
+ start: span.start ?? null,
21
+ end: span.end ?? null,
22
+ });
23
+ }
24
+ return normalizeEntityLinks(links);
25
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Special keywords are names the user/LLM wants exact-matched:
3
+ * Coo, Continuity, DSHC, project names, handles, etc.
4
+ *
5
+ * They are not B generic concepts and cannot be decomposed into them.
6
+ * The list is intentionally small and editable.
7
+ */
8
+ import fs from 'node:fs';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+
12
+ export const DEFAULT_SPECIAL_KEYWORDS = Object.freeze([
13
+ 'Coo', 'Continuity', 'DSHC', 'DeepSeek Harness', '创', '🐋',
14
+ ]);
15
+
16
+ export function defaultSpecialKeywordsPath(home = process.env.USERPROFILE ?? process.env.HOME ?? os.homedir()) {
17
+ return path.join(home, '.dsh', 'continuity', 'keywords.md');
18
+ }
19
+
20
+ export function parseSpecialKeywords(text) {
21
+ return String(text ?? '')
22
+ .split(/\r?\n/)
23
+ .map((line) => line.replace(/#.*$/, '').trim())
24
+ .filter((line) => line.length > 0 && !line.startsWith('<!--'));
25
+ }
26
+
27
+ export function loadSpecialKeywords(file = defaultSpecialKeywordsPath()) {
28
+ try {
29
+ const words = parseSpecialKeywords(fs.readFileSync(file, 'utf8'));
30
+ return words.length > 0 ? words : [...DEFAULT_SPECIAL_KEYWORDS];
31
+ } catch {
32
+ return [...DEFAULT_SPECIAL_KEYWORDS];
33
+ }
34
+ }
35
+
36
+ function isLatin(word) {
37
+ return /^[a-z0-9 _.-]+$/i.test(word);
38
+ }
39
+
40
+ export function matchSpecialKeywords(text, keywords = DEFAULT_SPECIAL_KEYWORDS) {
41
+ const source = String(text ?? '');
42
+ const found = [];
43
+ for (const raw of keywords) {
44
+ const keyword = String(raw ?? '').trim();
45
+ if (!keyword) continue;
46
+ let hit = false;
47
+ if (isLatin(keyword)) {
48
+ const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
49
+ hit = new RegExp(`(^|[^a-z0-9_])${escaped}(?=$|[^a-z0-9_])`, 'i').test(source);
50
+ } else {
51
+ hit = source.includes(keyword);
52
+ }
53
+ if (hit) found.push(keyword);
54
+ }
55
+ return [...new Set(found)];
56
+ }
57
+
58
+ export function withSpecialKeywords(text, keywords = []) {
59
+ return [...new Set([...keywords, ...matchSpecialKeywords(text)])];
60
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Adapters for future SRL / coreference / entity-linking model outputs.
3
+ * They normalize model output into EventModel slots and referent candidates.
4
+ */
5
+
6
+ const AGENT_LABELS = new Set(['ARG0', 'A0']);
7
+ const PATIENT_LABELS = new Set(['ARG1', 'A1']);
8
+ const LOCATION_LABELS = new Set(['ARGM-LOC', 'AM-LOC', 'LOC']);
9
+ const GOAL_LABELS = new Set(['ARGM-GOL', 'AM-GOL', 'GOL']);
10
+
11
+ export function normalizeSrl({ predicate = null, arguments: args = [] } = {}) {
12
+ const slots = { characters: [], location: null, goal: null, objects: [] };
13
+ for (const arg of args) {
14
+ if (typeof arg?.text !== 'string') continue;
15
+ const label = String(arg.label ?? '').toUpperCase();
16
+ if (AGENT_LABELS.has(label)) slots.characters.push(arg.text);
17
+ else if (PATIENT_LABELS.has(label)) slots.objects.push(arg.text);
18
+ else if (LOCATION_LABELS.has(label) && slots.location === null) slots.location = arg.text;
19
+ else if (GOAL_LABELS.has(label)) slots.goal = arg.text;
20
+ }
21
+ slots.characters = [...new Set(slots.characters)];
22
+ slots.objects = [...new Set(slots.objects)];
23
+ return { predicate, slots };
24
+ }
25
+
26
+ export function srlToSlots({ words = [], tags = [], verb = null } = {}) {
27
+ const groups = [];
28
+ let current = null;
29
+ for (let index = 0; index < tags.length; index += 1) {
30
+ const tag = String(tags[index] ?? 'O');
31
+ if (tag === 'O' || tag.startsWith('B-')) {
32
+ if (current) groups.push(current);
33
+ current = tag === 'O' ? null : { label: tag.slice(2), words: [] };
34
+ }
35
+ if (current) current.words.push(words[index]);
36
+ }
37
+ if (current) groups.push(current);
38
+ const slots = { characters: [], location: null, goal: null, objects: [] };
39
+ for (const group of groups) {
40
+ const text = group.words.join(' ');
41
+ if (!text) continue;
42
+ if (AGENT_LABELS.has(group.label)) slots.characters.push(text);
43
+ else if (PATIENT_LABELS.has(group.label)) slots.objects.push(text);
44
+ else if (LOCATION_LABELS.has(group.label) || group.label === 'ARGM-DIR') slots.location ??= text;
45
+ else if (GOAL_LABELS.has(group.label)) slots.goal ??= text;
46
+ }
47
+ slots.characters = [...new Set(slots.characters)];
48
+ slots.objects = [...new Set(slots.objects)];
49
+ return { predicate: verb, slots };
50
+ }
51
+
52
+ export function normalizeCoref(clusters = []) {
53
+ return clusters.map((cluster, index) => {
54
+ const mentions = (cluster.mentions ?? []).filter((mention) => typeof mention.text === 'string');
55
+ const canonical = mentions.find((mention) => mention.isCanonical) ?? mentions[0] ?? null;
56
+ return {
57
+ clusterId: `coref:${index + 1}`,
58
+ canonical: canonical?.text ?? null,
59
+ mentions: mentions.map((mention) => ({
60
+ text: mention.text,
61
+ start: mention.start ?? null,
62
+ end: mention.end ?? null,
63
+ })),
64
+ };
65
+ });
66
+ }
67
+
68
+ export function normalizeEntityLinks(links = []) {
69
+ return links.map((link) => ({
70
+ text: String(link.text ?? ''),
71
+ entityId: link.entityId ?? null,
72
+ referent: link.entityId === null || link.entityId === undefined ? 'NIL' : String(link.entityId),
73
+ score: Number.isFinite(link.score) ? link.score : null,
74
+ }));
75
+ }