@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,381 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * relabel-dry-run.mjs — 历史重打的**只读演练**:只输出"如果重打,会改哪些字段"。
4
+ *
5
+ * 不写 bc-index.jsonl、不写 baseline、不写 raw。想真正写盘必须另有写盘脚本,且先备份。
6
+ *
7
+ * 用法:
8
+ * node bin/relabel-dry-run.mjs --index <bc-index.jsonl> --raw <session.v4.jsonl.zstd>
9
+ * [--concepts <concepts.md>] [--baseline-file <file>]
10
+ * [--center-min 0.02] [--lead 0.02] [--limit 500] [--json]
11
+ *
12
+ * 选项:
13
+ * --index <file> 必填,bc-index.jsonl
14
+ * --raw <file> 必填,raw 会话(.jsonl 或 .jsonl.zstd,帧式也支持)
15
+ * --concepts <file> A 表(默认 <pkg>/templates/concepts.md)
16
+ * --baseline-file <file> 显式 baseline({topic:mean} 或 {version,window,samples})。
17
+ * 不给则用"被对齐上的那些轮的 leave-one-out 均值"——**只用于演练,
18
+ * 绝不能当生产 baseline**;输出里会标明来源。
19
+ * --center-min / --lead / --topic-min-score / --min-score / --chunk-chars / --max-chunks
20
+ * --limit <n> 只看前 n 条 diff(默认 60)
21
+ * --json 只输出 JSON
22
+ *
23
+ * 对齐规则:只用 turn 对齐 index 与 raw;raw 里找不到该轮文本 → 记 unresolved,不猜。
24
+ */
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+ import zlib from 'node:zlib';
28
+ import { fileURLToPath } from 'node:url';
29
+
30
+ const here = path.dirname(fileURLToPath(import.meta.url));
31
+ const pkgRoot = path.resolve(here, '..');
32
+
33
+ const argv = process.argv.slice(2);
34
+ const opt = (name, fallback = null) => {
35
+ const i = argv.indexOf(`--${name}`);
36
+ if (i < 0) return fallback;
37
+ const next = argv[i + 1];
38
+ return next === undefined || next.startsWith('--') ? true : next;
39
+ };
40
+ const num = (name, fallback) => {
41
+ const v = opt(name);
42
+ if (v === null || v === true) return fallback;
43
+ const n = Number(v);
44
+ return Number.isFinite(n) ? n : fallback;
45
+ };
46
+
47
+ const indexFile = opt('index');
48
+ const rawFile = opt('raw');
49
+ if (!indexFile || indexFile === true || !rawFile || rawFile === true) {
50
+ console.error('用法:node bin/relabel-dry-run.mjs --index <bc-index.jsonl> --raw <session.jsonl[.zstd]>');
51
+ process.exit(2);
52
+ }
53
+
54
+ const conceptsPath = (() => {
55
+ const explicit = opt('concepts');
56
+ if (explicit && explicit !== true) return explicit;
57
+ const candidate = path.join(pkgRoot, 'templates', 'concepts.md');
58
+ return fs.existsSync(candidate) ? candidate : null;
59
+ })();
60
+ if (!conceptsPath || !fs.existsSync(conceptsPath)) {
61
+ console.error('找不到 A 表:请用 --concepts 指定');
62
+ process.exit(2);
63
+ }
64
+
65
+ const centerMin = num('center-min', 0.02);
66
+ const lead = num('lead', 0.02);
67
+ const topicMinScore = num('topic-min-score', 0.33);
68
+ const minScore = num('min-score', 0.25);
69
+ const chunkChars = num('chunk-chars', 600);
70
+ const maxChunks = num('max-chunks', 12);
71
+ const limit = num('limit', 60);
72
+ const asJson = Boolean(opt('json', false));
73
+ const baselineFile = opt('baseline-file');
74
+
75
+ const { createBLabeler } = await import('file://' + path.join(pkgRoot, 'src/memory/bc-label.mjs'));
76
+ const { loadConcepts } = await import('file://' + path.join(pkgRoot, 'src/memory/concepts-file.mjs'));
77
+ const { embedTexts } = await import('file://' + path.join(pkgRoot, 'src/facets/embedding.mjs'));
78
+ const { eventText } = await import('file://' + path.join(pkgRoot, 'src/session-log.mjs'));
79
+
80
+ const INJECTED = /^\[(?:抽取|工作线|标签|规则|习惯|技能|归位|候选|存\?|闸|回正|学习|追忆|记忆|continuity:[a-z-]+)\]/;
81
+ const MACHINE = /^(?:Current runtime context|Current working directory|Current DSH file policy|Approval prompts|Time sampled)/;
82
+
83
+ const conceptRows = loadConcepts(conceptsPath);
84
+ const labels = Object.fromEntries(conceptRows.map((r) => [r.label, r.description]));
85
+ const kinds = Object.fromEntries(conceptRows.map((r) => [r.label, r.kind]));
86
+ const concepts = conceptRows.map((r) => r.label);
87
+ const topics = conceptRows.filter((r) => r.kind === 'topic').map((r) => r.label);
88
+ const workflowNames = conceptRows.filter((r) => r.kind === 'workflow').map((r) => r.label);
89
+
90
+ // ---------- raw 读取:支持纯 jsonl 与"一串 zstd 帧" ----------
91
+ function readRawLines(file) {
92
+ const lines = [];
93
+ if (/\.zstd$/i.test(file)) {
94
+ const buf = fs.readFileSync(file);
95
+ const MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
96
+ const starts = [];
97
+ for (let i = 0; ; ) {
98
+ const k = buf.indexOf(MAGIC, i);
99
+ if (k < 0) break;
100
+ starts.push(k);
101
+ i = k + 4;
102
+ }
103
+ for (const start of starts) {
104
+ let out;
105
+ try { out = zlib.zstdDecompressSync(buf.subarray(start)); } catch { continue; }
106
+ for (const line of out.toString('utf8').split(/\r?\n/)) if (line) lines.push(line);
107
+ }
108
+ return lines;
109
+ }
110
+ return fs.readFileSync(file, 'utf8').split(/\r?\n/).filter(Boolean);
111
+ }
112
+
113
+ const rawLines = readRawLines(rawFile);
114
+ const events = [];
115
+ const seenSeq = new Set();
116
+ for (const line of rawLines) {
117
+ let event;
118
+ try { event = JSON.parse(line.replace(/^\uFEFF/, '')); } catch { continue; }
119
+ const seq = Number(event?.seq);
120
+ if (Number.isInteger(seq)) {
121
+ if (seenSeq.has(seq)) continue;
122
+ seenSeq.add(seq);
123
+ }
124
+ events.push(event);
125
+ }
126
+ events.sort((a, b) => Number(a.seq) - Number(b.seq));
127
+
128
+ // ---------- 与 updater 相同的 turnsFromEvents ----------
129
+ const markers = events
130
+ .map((event) => ({ seq: Number(event?.seq), turn: Number(event?.data?.turn) }))
131
+ .filter((item) => Number.isInteger(item.seq) && Number.isInteger(item.turn))
132
+ .sort((a, b) => a.seq - b.seq);
133
+ const turnAt = (seq) => {
134
+ let found = null;
135
+ for (const marker of markers) {
136
+ if (marker.seq > seq) break;
137
+ found = marker.turn;
138
+ }
139
+ if (found !== null) return found;
140
+ return markers.find((marker) => marker.seq >= seq)?.turn ?? null;
141
+ };
142
+ const isHuman = (event) => {
143
+ const source = event?.data?.source ?? event?.source;
144
+ if (source && typeof source.kind === 'string' && source.kind !== 'user') return false;
145
+ if (source && source.peer === true) return false;
146
+ const text = eventText(event, { includeReasoning: false }).trim();
147
+ return Boolean(text) && !INJECTED.test(text) && !MACHINE.test(text);
148
+ };
149
+
150
+ const turns = new Map();
151
+ for (const event of events) {
152
+ const type = event?.type;
153
+ if (type !== 'user/message' && type !== 'assistant/message') continue;
154
+ if (type === 'user/message' && !isHuman(event)) continue;
155
+ const text = eventText(event, { includeReasoning: false }).trim();
156
+ if (!text || INJECTED.test(text) || MACHINE.test(text)) continue;
157
+ const seq = Number(event?.seq);
158
+ const turn = Number.isInteger(Number(event?.data?.turn)) ? Number(event.data.turn) : turnAt(seq);
159
+ if (!Number.isInteger(turn)) continue;
160
+ const item = turns.get(turn) ?? { turn, userTexts: [], assistantTexts: [] };
161
+ if (type === 'user/message') item.userTexts.push(text);
162
+ else item.assistantTexts.push(text);
163
+ turns.set(turn, item);
164
+ }
165
+
166
+ // ---------- index 读取 ----------
167
+ const indexRows = fs.readFileSync(indexFile, 'utf8')
168
+ .replace(/^\uFEFF/, '')
169
+ .split(/\r?\n/)
170
+ .filter((line) => line.trim() !== '')
171
+ .map((line) => JSON.parse(line));
172
+
173
+ const labeler = createBLabeler({ embed: embedTexts, labels, concepts, kinds });
174
+
175
+ const topicOfRecord = (record) => {
176
+ if (Array.isArray(record.genericTopic) && record.genericTopic.length > 0) return record.genericTopic[0];
177
+ const generic = Array.isArray(record.generic) ? record.generic : [];
178
+ return generic.find((name) => kinds[name] === 'topic') ?? null;
179
+ };
180
+ const workflowsOfRecord = (record) => {
181
+ if (Array.isArray(record.genericWorkflow)) return record.genericWorkflow;
182
+ const generic = Array.isArray(record.generic) ? record.generic : [];
183
+ return generic.filter((name) => kinds[name] === 'workflow');
184
+ };
185
+ const scoreOfRecord = (record, concept) => {
186
+ const scores = record.conceptScores ?? {};
187
+ const value = Number(scores[concept]);
188
+ return Number.isFinite(value) ? value : null;
189
+ };
190
+
191
+ // ---------- baseline ----------
192
+ // v2 文件形如 {version:2, topics:{...}, workflows:{...}};v1 只有 samples(当作 topics)
193
+ let explicitBaseline = null;
194
+ let workflowBaseline = null;
195
+ let baselineSource = 'leave-one-out(演练用)';
196
+ if (baselineFile && baselineFile !== true) {
197
+ const raw = JSON.parse(fs.readFileSync(baselineFile, 'utf8').replace(/^\uFEFF/, ''));
198
+ const topicSource = raw?.topics && typeof raw.topics === 'object'
199
+ ? raw.topics
200
+ : (raw?.samples && typeof raw.samples === 'object' ? raw.samples : raw);
201
+ const workflowSource = raw?.workflows && typeof raw.workflows === 'object' ? raw.workflows : null;
202
+
203
+ const buildMap = (source, names) => Object.fromEntries(names.map((name) => {
204
+ const value = source?.[name];
205
+ if (Array.isArray(value)) {
206
+ const nums = value.map((x) => Number(x)).filter((x) => Number.isFinite(x));
207
+ return [name, nums.length > 0 ? nums.reduce((a, b) => a + b, 0) / nums.length : Number.NaN];
208
+ }
209
+ if (value === undefined || value === null || value === '') return [name, Number.NaN];
210
+ const n = Number(value);
211
+ return [name, Number.isFinite(n) ? n : Number.NaN];
212
+ }));
213
+
214
+ const fill = (map, names) => {
215
+ const known = Object.values(map).filter((v) => Number.isFinite(v));
216
+ if (known.length === 0) return null;
217
+ const fallback = known.reduce((a, b) => a + b, 0) / known.length;
218
+ for (const name of names) if (!Number.isFinite(map[name])) map[name] = fallback;
219
+ return map;
220
+ };
221
+
222
+ explicitBaseline = fill(buildMap(topicSource, topics), topics);
223
+ if (workflowSource) workflowBaseline = fill(buildMap(workflowSource, workflowNames), workflowNames);
224
+
225
+ if (!explicitBaseline && !workflowBaseline) {
226
+ baselineSource = 'raw-bootstrap(文件无样本)';
227
+ } else if (explicitBaseline) {
228
+ baselineSource = `file:${baselineFile}`;
229
+ }
230
+ }
231
+
232
+ // ---------- 逐轮重打(只读) ----------
233
+ const evaluated = [];
234
+ for (const record of indexRows) {
235
+ const turn = Number(record.turn);
236
+ if (!Number.isInteger(turn)) continue;
237
+ const entry = turns.get(turn);
238
+ if (!entry) {
239
+ evaluated.push({ record, turn, unresolved: true });
240
+ continue;
241
+ }
242
+ const text = [...entry.userTexts, ...entry.assistantTexts].join('\n');
243
+ evaluated.push({ record, turn, text, unresolved: false });
244
+ }
245
+
246
+ // leave-one-out baseline 需要用每轮 raw 分;没给文件时先粗算一遍
247
+ let looBaseline = null;
248
+ if (!explicitBaseline) {
249
+ const sums = Object.fromEntries(topics.map((t) => [t, 0]));
250
+ let count = 0;
251
+ for (const item of evaluated) {
252
+ if (item.unresolved) continue;
253
+ const res = await labeler.labelChunked(item.text, {
254
+ topK: 3, minScore, topicMinScore, topicCenterMin: -Infinity, topicLead: -Infinity,
255
+ chunkChars, maxChunks, topicTopK: 1,
256
+ });
257
+ const scores = res.topicScores ?? {};
258
+ const keys = topics.filter((t) => Number.isFinite(Number(scores[t])));
259
+ if (keys.length === 0) continue;
260
+ for (const t of keys) sums[t] += Number(scores[t]);
261
+ count += 1;
262
+ }
263
+ if (count > 0) {
264
+ looBaseline = Object.fromEntries(topics.map((t) => [t, sums[t] / count]));
265
+ baselineSource = `leave-one-out(演练用,${count} 轮)`;
266
+ } else {
267
+ baselineSource = 'raw-bootstrap(无可用轮次)';
268
+ }
269
+ }
270
+
271
+ const diffs = [];
272
+ const summary = {
273
+ indexRows: indexRows.length,
274
+ evaluated: 0,
275
+ unresolved: 0,
276
+ changed: 0,
277
+ unchanged: 0,
278
+ emptyToTopic: 0,
279
+ topicToEmpty: 0,
280
+ topicChanged: 0,
281
+ workflowChanged: 0,
282
+ scoreChanged: 0,
283
+ baselineSource,
284
+ };
285
+
286
+ for (const item of evaluated) {
287
+ if (item.unresolved) { summary.unresolved += 1; continue; }
288
+ summary.evaluated += 1;
289
+
290
+ let baseline = explicitBaseline ?? looBaseline;
291
+ if (!explicitBaseline && looBaseline) {
292
+ // 近似 leave-one-out:把本轮 raw 分从均值里扣掉(用粗算的那一轮分不够精确,宁可标来源)
293
+ baseline = looBaseline;
294
+ }
295
+
296
+ const res = await labeler.labelChunked(item.text, {
297
+ topK: 3,
298
+ minScore,
299
+ topicMinScore,
300
+ topicCenterMin: centerMin,
301
+ topicLead: lead,
302
+ chunkChars,
303
+ maxChunks,
304
+ topicTopK: 1,
305
+ topicBaseline: baseline ?? undefined,
306
+ workflowBaseline: workflowBaseline ?? undefined,
307
+ });
308
+
309
+ const oldTopic = topicOfRecord(item.record);
310
+ const oldWorkflows = workflowsOfRecord(item.record);
311
+ const oldScore = oldTopic ? scoreOfRecord(item.record, oldTopic) : null;
312
+ const oldConfidence = Number.isFinite(Number(item.record.topicConfidence)) ? Number(item.record.topicConfidence) : null;
313
+
314
+ const newTopic = (res.topic ?? [])[0]?.concept ?? null;
315
+ const newWorkflows = (res.workflow ?? []).map((w) => w.concept);
316
+ const newScore = newTopic ? Number(res.topicScores?.[newTopic] ?? res.topic?.[0]?.score ?? 0) : null;
317
+ const newConfidence = newTopic ? Number(res.topicConfidence ?? 0) : null;
318
+
319
+ const changedTopic = (oldTopic ?? null) !== (newTopic ?? null);
320
+ const changedWorkflow = oldWorkflows.join('|') !== newWorkflows.join('|');
321
+ const changedScore = oldScore !== null && newScore !== null && Math.abs(oldScore - newScore) > 1e-6;
322
+ const changed = changedTopic || changedWorkflow || changedScore || (oldConfidence ?? null) !== (newConfidence ?? null);
323
+
324
+ if (changed) summary.changed += 1; else summary.unchanged += 1;
325
+ if (!oldTopic && newTopic) summary.emptyToTopic += 1;
326
+ if (oldTopic && !newTopic) summary.topicToEmpty += 1;
327
+ if (oldTopic && newTopic && oldTopic !== newTopic) summary.topicChanged += 1;
328
+ if (changedWorkflow) summary.workflowChanged += 1;
329
+ if (changedScore) summary.scoreChanged += 1;
330
+
331
+ diffs.push({
332
+ turn: item.turn,
333
+ oldTopic,
334
+ newTopic,
335
+ oldWorkflows,
336
+ newWorkflows,
337
+ oldTopicScore: oldScore,
338
+ newTopicScore: newScore,
339
+ oldConfidence,
340
+ newConfidence,
341
+ changed,
342
+ });
343
+ }
344
+
345
+ const payload = {
346
+ summary,
347
+ diffs,
348
+ note: explicitBaseline
349
+ ? 'baseline 来自文件;本脚本只读,未写任何文件'
350
+ : 'baseline 由被对齐轮次自身算出(leave-one-out 近似),仅用于演练,不可当生产 baseline;本脚本只读',
351
+ };
352
+
353
+ const looRounds = Number((summary.baselineSource.match(/leave-one-out\(演练用,(\d+) 轮\)/) ?? [])[1] ?? 0);
354
+ const lowSample = !explicitBaseline && looRounds > 0 && looRounds < 100;
355
+ if (lowSample) {
356
+ payload.warning = `baseline 仅 ${looRounds} 轮(<100),结果仅供参考,不能当生产结论`;
357
+ }
358
+
359
+ if (asJson) {
360
+ console.log(JSON.stringify(payload, null, 2));
361
+ } else {
362
+ console.log(` index ${summary.indexRows} 行 · 可重打 ${summary.evaluated} · unresolved ${summary.unresolved}`);
363
+ console.log(` A 表 ${conceptRows.length} 条(topic ${topics.length})· 判据 centered≥${centerMin} lead≥${lead} · baseline ${summary.baselineSource}`);
364
+ if (lowSample) console.log(` ⚠ ${payload.warning}`);
365
+ console.log(` 变更 ${summary.changed} · 不变 ${summary.unchanged} · 空→topic ${summary.emptyToTopic} · topic→空 ${summary.topicToEmpty}`);
366
+ console.log(` 换 topic ${summary.topicChanged} · 换 workflow ${summary.workflowChanged} · 分数变化 ${summary.scoreChanged}`);
367
+ console.log('');
368
+ const shown = diffs.filter((d) => d.changed).slice(0, limit);
369
+ if (shown.length === 0) {
370
+ console.log(' 没有会变更的行。');
371
+ } else {
372
+ console.log(' turn oldTopic → newTopic score wf');
373
+ for (const d of shown) {
374
+ const score = `${d.oldTopicScore === null ? '-' : d.oldTopicScore.toFixed(3)}→${d.newTopicScore === null ? '-' : d.newTopicScore.toFixed(3)}`;
375
+ console.log(` ${String(d.turn).padStart(5)} ${String(d.oldTopic ?? '(空)').padEnd(12)} → ${String(d.newTopic ?? '(空)').padEnd(14)} ${score.padEnd(13)} ${d.oldWorkflows.join('/') || '-'} → ${d.newWorkflows.join('/') || '-'}`);
376
+ }
377
+ if (diffs.filter((d) => d.changed).length > shown.length) {
378
+ console.log(` … 还有 ${diffs.filter((d) => d.changed).length - shown.length} 条,用 --limit 或 --json 看全`);
379
+ }
380
+ }
381
+ }
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """ReLiK reader-only entity linking. Input JSONL: {id,text,candidates:[...]}."""
4
+ import json
5
+ import os
6
+ import sys
7
+
8
+ from relik import Relik
9
+ from relik.reader.pytorch_modules.span import RelikReaderForSpanExtraction
10
+
11
+ reader_path = os.environ["RELIK_READER"]
12
+ reader = RelikReaderForSpanExtraction(reader_path)
13
+ relik = Relik(reader=reader, task="span", window_size=32, window_stride=16)
14
+
15
+ src, out = sys.argv[1], sys.argv[2]
16
+ rows = []
17
+ for line in open(src, encoding="utf-8"):
18
+ if not line.strip():
19
+ continue
20
+ item = json.loads(line)
21
+ result = relik(item["text"], candidates=item.get("candidates", []))
22
+ spans = [
23
+ {"start": span.start, "end": span.end, "text": span.text, "label": span.label}
24
+ for span in result.spans
25
+ ]
26
+ candidates = result.candidates.span if result.candidates is not None else []
27
+ serial_candidates = []
28
+ for span_group in candidates:
29
+ span_group_out = []
30
+ for candidate_group in span_group:
31
+ span_group_out.append([
32
+ {"text": getattr(doc, "text", None), "id": str(getattr(doc, "id", None))}
33
+ for doc in candidate_group
34
+ ])
35
+ serial_candidates.append(span_group_out)
36
+ rows.append({"id": item.get("id"), "text": item["text"], "spans": spans, "candidates": serial_candidates})
37
+ with open(out, "w", encoding="utf-8") as f:
38
+ json.dump({"n": len(rows), "rows": rows}, f, ensure_ascii=False, indent=2)
39
+ print(json.dumps({"n": len(rows), "with_spans": sum(1 for row in rows if row["spans"])}, ensure_ascii=False, indent=2))
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """One-shot ReLiK reader service: stdin {text,candidates}, stdout {ok, links}."""
4
+ import json
5
+ import os
6
+ import sys
7
+
8
+ sys.stdout.reconfigure(encoding="utf-8")
9
+ sys.stderr.reconfigure(encoding="utf-8")
10
+
11
+ from relik import Relik # noqa: E402
12
+ from relik.reader.pytorch_modules.span import RelikReaderForSpanExtraction # noqa: E402
13
+
14
+ reader = RelikReaderForSpanExtraction(os.environ["RELIK_READER"])
15
+ relik = Relik(reader=reader, task="span", window_size=32, window_stride=16)
16
+
17
+ def handle(request):
18
+ if request.get("health"):
19
+ return {"ok": True, "health": "ready"}
20
+ try:
21
+ result = relik(str(request.get("text", "")), candidates=request.get("candidates") or [])
22
+ links = []
23
+ candidate_groups = result.candidates.span if result.candidates is not None else []
24
+ for index, span in enumerate(result.spans):
25
+ group = []
26
+ if candidate_groups and index < len(candidate_groups) and candidate_groups[index]:
27
+ group = candidate_groups[index][0]
28
+ elif candidate_groups and candidate_groups[0]:
29
+ group = candidate_groups[0][0]
30
+ top = next((candidate for candidate in group if candidate.text in (span.label, span.text)), group[0] if group else None)
31
+ links.append({
32
+ "text": span.text,
33
+ "entityId": str(top.id) if top is not None else None,
34
+ "score": None,
35
+ "start": span.start,
36
+ "end": span.end,
37
+ })
38
+ return {"ok": True, "links": links, "spans": [{"text": s.text, "start": s.start, "end": s.end} for s in result.spans]}
39
+ except Exception as error:
40
+ return {"ok": False, "reason": type(error).__name__, "message": str(error)[:300]}
41
+
42
+
43
+ if "--serve" in sys.argv:
44
+ for raw in sys.stdin.buffer:
45
+ line = raw.decode("utf-8", "replace").strip()
46
+ if not line:
47
+ continue
48
+ request = json.loads(line)
49
+ print(json.dumps({"id": request.get("id"), **handle(request)}, ensure_ascii=False), flush=True)
50
+ else:
51
+ request = json.loads(sys.stdin.buffer.read().decode("utf-8"))
52
+ print(json.dumps(handle(request), ensure_ascii=False))
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """SRL pipeline: transformer-srl + local CoNLL2012 archive + NLTK POS fallback."""
4
+ import json
5
+ import os
6
+ import sys
7
+
8
+ import nltk
9
+ import spacy
10
+
11
+
12
+ def add_pos(doc):
13
+ tags = nltk.pos_tag([token.text for token in doc])
14
+ for token, (_, tag) in zip(doc, tags):
15
+ token.tag_ = tag
16
+ if tag.startswith("VB"):
17
+ token.pos_ = "VERB"
18
+ elif tag.startswith("NN"):
19
+ token.pos_ = "NOUN"
20
+ elif tag.startswith("JJ"):
21
+ token.pos_ = "ADJ"
22
+ elif tag.startswith("RB"):
23
+ token.pos_ = "ADV"
24
+ elif tag in ("IN", "TO"):
25
+ token.pos_ = "ADP"
26
+ elif tag in ("DT", "PRP", "PRP$"):
27
+ token.pos_ = "DET"
28
+ else:
29
+ token.pos_ = "X"
30
+ return doc
31
+
32
+
33
+ blank = spacy.blank("en")
34
+ blank.add_pipe(add_pos)
35
+ spacy.load = lambda *args, **kwargs: blank
36
+
37
+ import transformer_srl.dataset_readers # noqa: E402,F401
38
+ import transformer_srl.models # noqa: E402,F401
39
+ from allennlp.models.archival import load_archive # noqa: E402
40
+ from transformer_srl import predictors # noqa: E402
41
+
42
+ archive = load_archive(os.environ["SRL_ARCHIVE"])
43
+ predictor = predictors.SrlTransformersPredictor.from_archive(archive, "transformer_srl")
44
+
45
+ src, out = sys.argv[1], sys.argv[2]
46
+ rows = []
47
+ for line in open(src, encoding="utf-8"):
48
+ if not line.strip():
49
+ continue
50
+ item = json.loads(line)
51
+ result = predictor.predict(sentence=item["text"])
52
+ rows.append({"id": item.get("id"), "text": item["text"], **result})
53
+ with open(out, "w", encoding="utf-8") as f:
54
+ json.dump({"n": len(rows), "rows": rows}, f, ensure_ascii=False, indent=2)
55
+ print(json.dumps({"n": len(rows), "with_verbs": sum(1 for row in rows if row["verbs"])}, ensure_ascii=False, indent=2))
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """One-shot SRL service: stdin {text}, stdout {ok, srl}."""
4
+ import json
5
+ import os
6
+ import sys
7
+
8
+ import nltk
9
+ import spacy
10
+
11
+ sys.stdout.reconfigure(encoding="utf-8")
12
+ sys.stderr.reconfigure(encoding="utf-8")
13
+
14
+
15
+ def add_pos(doc):
16
+ tags = nltk.pos_tag([token.text for token in doc])
17
+ for token, (_, tag) in zip(doc, tags):
18
+ token.tag_ = tag
19
+ if tag.startswith("VB"):
20
+ token.pos_ = "VERB"
21
+ elif tag.startswith("NN"):
22
+ token.pos_ = "NOUN"
23
+ elif tag.startswith("JJ"):
24
+ token.pos_ = "ADJ"
25
+ elif tag.startswith("RB"):
26
+ token.pos_ = "ADV"
27
+ elif tag in ("IN", "TO"):
28
+ token.pos_ = "ADP"
29
+ elif tag in ("DT", "PRP", "PRP$"):
30
+ token.pos_ = "DET"
31
+ else:
32
+ token.pos_ = "X"
33
+ return doc
34
+
35
+
36
+ blank = spacy.blank("en")
37
+ blank.add_pipe(add_pos)
38
+ spacy.load = lambda *args, **kwargs: blank
39
+
40
+ import transformer_srl.dataset_readers # noqa: E402,F401
41
+ import transformer_srl.models # noqa: E402,F401
42
+ from allennlp.models.archival import load_archive # noqa: E402
43
+ from transformer_srl import predictors # noqa: E402
44
+
45
+ archive = load_archive(os.environ["SRL_ARCHIVE"])
46
+ predictor = predictors.SrlTransformersPredictor.from_archive(archive, "transformer_srl")
47
+
48
+ def handle(request):
49
+ if request.get("health"):
50
+ return {"ok": True, "health": "ready"}
51
+ try:
52
+ result = predictor.predict(sentence=str(request.get("text", "")))
53
+ verb = result["verbs"][0] if result["verbs"] else None
54
+ return {"ok": True, "srl": {"words": result["words"], "verb": verb}}
55
+ except Exception as error:
56
+ return {"ok": False, "reason": type(error).__name__, "message": str(error)[:300]}
57
+
58
+
59
+ if "--serve" in sys.argv:
60
+ for raw in sys.stdin.buffer:
61
+ line = raw.decode("utf-8", "replace").strip()
62
+ if not line:
63
+ continue
64
+ request = json.loads(line)
65
+ print(json.dumps({"id": request.get("id"), **handle(request)}, ensure_ascii=False), flush=True)
66
+ else:
67
+ request = json.loads(sys.stdin.buffer.read().decode("utf-8"))
68
+ print(json.dumps(handle(request), ensure_ascii=False))