@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,146 @@
1
+ /**
2
+ * Tiny local facet classifier (multinomial Naive Bayes over char bigrams).
3
+ *
4
+ * This is the "small model" seam: it learns from accepted facet labels and can
5
+ * be replaced by an ONNX embedding/SetFit model later without changing the
6
+ * index/query API. No external dependency.
7
+ */
8
+ import { SLOTS, VALUES, normalizeFacets } from './schema.mjs';
9
+
10
+ const ALPHA = 1;
11
+
12
+ function tokens(text) {
13
+ const source = String(text ?? '').toLowerCase();
14
+ const out = [];
15
+ const words = source.match(/[a-z0-9_]+/g) ?? [];
16
+ for (const word of words) out.push(`w:${word}`);
17
+ for (const chunk of source.match(/[\u4e00-\u9fff]+/g) ?? []) {
18
+ const chars = [...chunk];
19
+ for (let i = 0; i < chars.length; i += 1) {
20
+ out.push(`c:${chars[i]}`);
21
+ if (i + 1 < chars.length) out.push(`b:${chars[i]}${chars[i + 1]}`);
22
+ }
23
+ }
24
+ return [...new Set(out)].slice(0, 800);
25
+ }
26
+
27
+ export function trainFacetClassifier(rows) {
28
+ const model = { version: 1, slots: {}, vocab: new Set() };
29
+ for (const slot of SLOTS) {
30
+ model.slots[slot] = {
31
+ docs: 0,
32
+ values: Object.fromEntries(VALUES[slot].map((value) => [value, { docs: 0, tokens: new Map(), total: 0 }])),
33
+ };
34
+ }
35
+ for (const row of rows) {
36
+ const facets = normalizeFacets(row.facets ?? {});
37
+ const list = tokens(row.text);
38
+ for (const token of list) model.vocab.add(token);
39
+ for (const slot of SLOTS) {
40
+ const value = facets[slot];
41
+ if (value === undefined) continue;
42
+ const bucket = model.slots[slot];
43
+ const cell = bucket.values[value];
44
+ bucket.docs += 1;
45
+ cell.docs += 1;
46
+ for (const token of list) {
47
+ cell.tokens.set(token, (cell.tokens.get(token) ?? 0) + 1);
48
+ cell.total += 1;
49
+ }
50
+ }
51
+ }
52
+ model.vocab = [...model.vocab];
53
+ return model;
54
+ }
55
+
56
+ export function predictFacets(model, text) {
57
+ const list = tokens(text);
58
+ const facets = {};
59
+ const confidence = {};
60
+ for (const slot of SLOTS) {
61
+ const bucket = model.slots?.[slot];
62
+ if (!bucket || bucket.docs === 0) continue;
63
+ const vocabSize = Math.max(1, model.vocab?.length ?? 1);
64
+ const scored = [];
65
+ for (const value of VALUES[slot]) {
66
+ const cell = bucket.values[value];
67
+ if (!cell || cell.docs === 0) continue;
68
+ let score = Math.log((cell.docs + ALPHA) / (bucket.docs + ALPHA * VALUES[slot].length));
69
+ const denom = cell.total + ALPHA * vocabSize;
70
+ for (const token of list) {
71
+ const count = cell.tokens.get(token) ?? 0;
72
+ score += Math.log((count + ALPHA) / denom);
73
+ }
74
+ scored.push({ value, score });
75
+ }
76
+ scored.sort((a, b) => b.score - a.score);
77
+ if (scored.length === 0) continue;
78
+ const top = scored[0];
79
+ const second = scored[1];
80
+ const margin = second === undefined ? 1 : top.score - second.score;
81
+ if (margin < 0.2) continue;
82
+ facets[slot] = top.value;
83
+ confidence[slot] = Math.min(1, margin / 2);
84
+ }
85
+ return { facets, confidence, unknown: Object.keys(facets).length === 0 };
86
+ }
87
+
88
+ export function evaluate(model, rows) {
89
+ const report = {};
90
+ for (const slot of SLOTS) {
91
+ let total = 0;
92
+ let correct = 0;
93
+ let covered = 0;
94
+ for (const row of rows) {
95
+ const truth = normalizeFacets(row.facets ?? {})[slot];
96
+ if (truth === undefined) continue;
97
+ total += 1;
98
+ const predicted = predictFacets(model, row.text).facets[slot];
99
+ if (predicted === undefined) continue;
100
+ covered += 1;
101
+ if (predicted === truth) correct += 1;
102
+ }
103
+ report[slot] = {
104
+ total,
105
+ covered,
106
+ correct,
107
+ coverage: total === 0 ? 0 : covered / total,
108
+ accuracy: covered === 0 ? 0 : correct / covered,
109
+ };
110
+ }
111
+ return report;
112
+ }
113
+
114
+ export function classifierExtractor(model) {
115
+ return (text) => predictFacets(model, text);
116
+ }
117
+
118
+ export function serializeClassifier(model) {
119
+ const slots = {};
120
+ for (const [slot, bucket] of Object.entries(model.slots ?? {})) {
121
+ slots[slot] = {
122
+ docs: bucket.docs,
123
+ values: Object.fromEntries(Object.entries(bucket.values).map(([value, cell]) => [value, {
124
+ docs: cell.docs,
125
+ total: cell.total,
126
+ tokens: [...cell.tokens.entries()],
127
+ }])),
128
+ };
129
+ }
130
+ return { version: 1, vocab: model.vocab ?? [], slots };
131
+ }
132
+
133
+ export function deserializeClassifier(raw) {
134
+ const model = { version: 1, vocab: Array.isArray(raw?.vocab) ? raw.vocab : [], slots: {} };
135
+ for (const [slot, bucket] of Object.entries(raw?.slots ?? {})) {
136
+ model.slots[slot] = {
137
+ docs: Number(bucket.docs) || 0,
138
+ values: Object.fromEntries(Object.entries(bucket.values ?? {}).map(([value, cell]) => [value, {
139
+ docs: Number(cell.docs) || 0,
140
+ total: Number(cell.total) || 0,
141
+ tokens: new Map(Array.isArray(cell.tokens) ? cell.tokens : []),
142
+ }])),
143
+ };
144
+ }
145
+ return model;
146
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Main-thread client for the embedding facet worker.
3
+ */
4
+ import { Worker } from 'node:worker_threads';
5
+
6
+ export function createEmbeddingFacetService({ prototypesPath, model, nbModelPath, timeoutMs = 120000 } = {}) {
7
+ if (typeof prototypesPath !== 'string' || prototypesPath.length === 0) {
8
+ throw new Error('embedding service: prototypesPath is required');
9
+ }
10
+ const worker = new Worker(new URL('./embedding-worker.mjs', import.meta.url), {
11
+ workerData: { prototypesPath, model, nbModelPath },
12
+ });
13
+ let nextId = 1;
14
+ const pending = new Map();
15
+
16
+ worker.on('message', (message) => {
17
+ const entry = pending.get(message?.id);
18
+ if (entry === undefined) return;
19
+ pending.delete(message.id);
20
+ clearTimeout(entry.timer);
21
+ if (message.ok) entry.resolve(message.result);
22
+ else entry.reject(new Error(message.error));
23
+ });
24
+ worker.on('error', (error) => {
25
+ for (const entry of pending.values()) {
26
+ clearTimeout(entry.timer);
27
+ entry.reject(error);
28
+ }
29
+ pending.clear();
30
+ });
31
+
32
+ return {
33
+ extract(text) {
34
+ const id = nextId++;
35
+ return new Promise((resolve, reject) => {
36
+ const timer = setTimeout(() => {
37
+ pending.delete(id);
38
+ reject(new Error('embedding service: timeout'));
39
+ }, timeoutMs);
40
+ pending.set(id, { resolve, reject, timer });
41
+ worker.postMessage({ id, text: String(text ?? '') });
42
+ });
43
+ },
44
+ async close() {
45
+ for (const entry of pending.values()) {
46
+ clearTimeout(entry.timer);
47
+ entry.reject(new Error('embedding service: closed'));
48
+ }
49
+ pending.clear();
50
+ await worker.terminate();
51
+ },
52
+ };
53
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Embedding facet worker. Loads prototypes and the ONNX/WASM embedding model
3
+ * inside worker_threads; the host never blocks on model inference.
4
+ */
5
+ import { parentPort, workerData } from 'node:worker_threads';
6
+ import fs from 'node:fs';
7
+ import { deserializePrototypes, fuseFacets, predictEmbedding } from './embedding.mjs';
8
+ import { deserializeClassifier } from './classifier.mjs';
9
+ import { hybridExtractor } from './hybrid.mjs';
10
+
11
+ const prototypes = deserializePrototypes(JSON.parse(fs.readFileSync(workerData.prototypesPath, 'utf8')));
12
+ const model = workerData.model ?? prototypes.model;
13
+ const fallback = workerData.nbModelPath
14
+ ? hybridExtractor(deserializeClassifier(JSON.parse(fs.readFileSync(workerData.nbModelPath, 'utf8'))))
15
+ : null;
16
+
17
+ parentPort.on('message', async (message) => {
18
+ const id = message?.id;
19
+ try {
20
+ const text = String(message?.text ?? '');
21
+ const embedded = await predictEmbedding(prototypes, text, model);
22
+ const result = fallback === null ? embedded : fuseFacets(embedded, fallback(text));
23
+ parentPort.postMessage({ id, ok: true, result });
24
+ } catch (error) {
25
+ parentPort.postMessage({ id, ok: false, error: String(error?.message ?? error) });
26
+ }
27
+ });
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Local embedding prototypes for facets.
3
+ *
4
+ * Uses transformers.js (ONNX/WASM) with a multilingual small embedding model.
5
+ * The model is downloaded once into the local HF cache; no text leaves the
6
+ * machine. Prototypes are built from accepted facet labels.
7
+ */
8
+ import { SLOTS, VALUES, normalizeFacets } from './schema.mjs';
9
+ import { detectRandom } from './random.mjs';
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+
14
+ let extractorPromise = null;
15
+ const MODEL_ID = 'Xenova/bge-small-zh-v1.5';
16
+ const LOCAL_MODELS = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../models');
17
+
18
+ export async function loadEmbedder(model = MODEL_ID) {
19
+ if (extractorPromise !== null) return extractorPromise;
20
+ const { pipeline, env } = await import('@huggingface/transformers');
21
+ const localConfig = path.join(LOCAL_MODELS, model, 'config.json');
22
+ if (fs.existsSync(localConfig)) {
23
+ env.allowRemoteModels = false;
24
+ env.localModelPath = LOCAL_MODELS;
25
+ } else {
26
+ env.allowRemoteModels = true;
27
+ env.remoteHost = process.env.FACET_HF_ENDPOINT || 'https://hf-mirror.com';
28
+ }
29
+ try {
30
+ extractorPromise = await pipeline('feature-extraction', model, { dtype: 'q8' });
31
+ } catch (error) {
32
+ extractorPromise = null;
33
+ throw new Error(
34
+ 'embedding runtime unavailable: '
35
+ + `${error?.message ?? error}. `
36
+ + 'If pnpm skipped onnxruntime-node, run "pnpm approve-builds" then '
37
+ + '"pnpm rebuild onnxruntime-node sharp" in the DSH profile, and retry.',
38
+ );
39
+ }
40
+ return extractorPromise;
41
+ }
42
+
43
+ export async function embedTexts(texts, model) {
44
+ const extractor = await loadEmbedder(model);
45
+ const output = await extractor(texts, { pooling: 'mean', normalize: true });
46
+ return output.tolist();
47
+ }
48
+
49
+ function normalizeVector(vector) {
50
+ const norm = Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0)) || 1;
51
+ return vector.map((value) => value / norm);
52
+ }
53
+
54
+ function average(vectors) {
55
+ if (vectors.length === 0) return [];
56
+ const dims = vectors[0].length;
57
+ const sum = new Array(dims).fill(0);
58
+ for (const vector of vectors) {
59
+ for (let i = 0; i < dims; i += 1) sum[i] += vector[i];
60
+ }
61
+ return normalizeVector(sum.map((value) => value / vectors.length));
62
+ }
63
+
64
+ function cosine(a, b) {
65
+ if (a.length !== b.length) return 0;
66
+ let dot = 0;
67
+ for (let i = 0; i < a.length; i += 1) dot += a[i] * b[i];
68
+ return dot;
69
+ }
70
+
71
+ export async function buildEmbeddingPrototypes(rows, model) {
72
+ const source = rows.filter((row) => row.text && row.facets);
73
+ const vectors = await embedTexts(source.map((row) => row.text), model);
74
+ const slots = {};
75
+ for (const slot of SLOTS) {
76
+ slots[slot] = { values: {} };
77
+ for (const value of VALUES[slot]) {
78
+ const selected = [];
79
+ source.forEach((row, index) => {
80
+ if (normalizeFacets(row.facets)[slot] === value) selected.push(vectors[index]);
81
+ });
82
+ if (selected.length > 0) slots[slot].values[value] = average(selected);
83
+ }
84
+ }
85
+ return { model, slots };
86
+ }
87
+
88
+ export async function predictEmbedding(prototypes, text, model) {
89
+ const [vector] = await embedTexts([text], model ?? prototypes.model);
90
+ const facets = {};
91
+ const confidence = {};
92
+ for (const slot of SLOTS) {
93
+ let best = null;
94
+ let second = null;
95
+ for (const [value, centroid] of Object.entries(prototypes.slots?.[slot]?.values ?? {})) {
96
+ const score = cosine(vector, centroid);
97
+ if (best === null || score > best.score) {
98
+ second = best;
99
+ best = { value, score };
100
+ } else if (second === null || score > second.score) {
101
+ second = { value, score };
102
+ }
103
+ }
104
+ if (best === null) continue;
105
+ const margin = second === null ? 1 : best.score - second.score;
106
+ if (margin < 0.01) continue;
107
+ facets[slot] = best.value;
108
+ confidence[slot] = margin;
109
+ }
110
+ const random = detectRandom(text);
111
+ facets.random = random.value;
112
+ confidence.random = random.value === '有' ? 1 : 0.5;
113
+ return {
114
+ facets,
115
+ confidence,
116
+ unknown: !Object.keys(facets).some((slot) => slot !== 'random'),
117
+ };
118
+ }
119
+
120
+ export function embeddingExtractor(prototypes, model) {
121
+ return async (text) => predictEmbedding(prototypes, text, model);
122
+ }
123
+
124
+ export function serializePrototypes(prototypes) {
125
+ return {
126
+ model: prototypes.model,
127
+ slots: Object.fromEntries(Object.entries(prototypes.slots ?? {}).map(([slot, bucket]) => [slot, {
128
+ values: Object.fromEntries(Object.entries(bucket.values ?? {})),
129
+ }])),
130
+ };
131
+ }
132
+
133
+ export function deserializePrototypes(raw) {
134
+ return {
135
+ model: raw?.model,
136
+ slots: Object.fromEntries(Object.entries(raw?.slots ?? {}).map(([slot, bucket]) => [slot, {
137
+ values: Object.fromEntries(Object.entries(bucket.values ?? {})),
138
+ }])),
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Slot-aware fusion: embedding wins only where it is calibrated; participants
144
+ * stays with the local NB because embedding prototypes overfit that slot.
145
+ */
146
+ export function fuseFacets(embedding, fallback, {
147
+ embeddingSlots = ['activity', 'interaction', 'medium', 'topic'],
148
+ } = {}) {
149
+ const facets = { ...fallback.facets };
150
+ const confidence = { ...fallback.confidence };
151
+ for (const slot of embeddingSlots) {
152
+ if (embedding?.facets?.[slot] === undefined) continue;
153
+ facets[slot] = embedding.facets[slot];
154
+ confidence[slot] = embedding.confidence?.[slot] ?? 0;
155
+ }
156
+ if (embedding?.facets?.random !== undefined) facets.random = embedding.facets.random;
157
+ return {
158
+ facets,
159
+ confidence,
160
+ unknown: !Object.keys(facets).some((slot) => slot !== 'random'),
161
+ };
162
+ }
163
+
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Rule-based fallback extractor for facet MVP.
3
+ *
4
+ * This is intentionally not an LLM. It gives the index/query path something
5
+ * runnable before a local ONNX classifier is added. Missing slots stay missing;
6
+ * they are not guessed.
7
+ */
8
+ import { SLOTS, VALUES, normalizeFacets } from './schema.mjs';
9
+
10
+ const LEXICON = {
11
+ activity: {
12
+ 跑团: ['跑团', '掷骰', '骰子', 'D20', '角色卡', '检定', 'KP', '守渡人', '剧情推进'],
13
+ 开发: ['开发', '实现', '写代码', '补丁', '插件', '接口'],
14
+ 调试: ['调试', '排查', '探针', '日志', '报错', '崩溃'],
15
+ 测试: ['测试', '自检', '验收', '用例', '回归'],
16
+ 文档: ['文档', '手册', '说明', 'README', '写文档'],
17
+ 对话: ['对话', '聊天', '你觉得', '感受', '关系'],
18
+ },
19
+ interaction: {
20
+ 回合制: ['回合', '轮到我', '下一回合', '行动', '掷骰'],
21
+ 指令式: ['命令', '执行', '运行', '工具', '脚本'],
22
+ 自由对话: ['聊天', '对话', '问', '回答'],
23
+ },
24
+ random: {
25
+ 有: ['掷骰', '骰子', 'D20', '随机', '概率', '检定'],
26
+ 无: [],
27
+ },
28
+ participants: {
29
+ 单人: ['单人', '我自己', '独自'],
30
+ 多人: ['多人'],
31
+ 协作: ['协作', '一起', '帮我', '我们', '配合'],
32
+ 对抗: ['对抗', '敌人', '战斗'],
33
+ },
34
+ medium: {
35
+ 文字: ['文字', '正文', '文本', '故事'],
36
+ 代码: ['代码', '函数', '源码', '脚本'],
37
+ 图像: ['图片', '头像', '画图', '图像'],
38
+ 语音: ['语音', '口述'],
39
+ },
40
+ topic: {
41
+ 剧情: ['剧情', '故事', '角色', '场景', '任务'],
42
+ 技术: ['技术', '代码', '插件', '系统', '接口'],
43
+ 设备: ['设备', '手机', '电脑', '硬件'],
44
+ 记忆: ['记忆', '回忆', '上下文', '线头'],
45
+ 规则: ['规则', '判据', '阈值', '闸', '约束'],
46
+ },
47
+ };
48
+
49
+ function scoreSlot(text, slot) {
50
+ const values = LEXICON[slot] ?? {};
51
+ let best = null;
52
+ for (const [value, words] of Object.entries(values)) {
53
+ if (!VALUES[slot].includes(value)) continue;
54
+ let score = 0;
55
+ for (const word of words) {
56
+ if (text.includes(word)) score += 1;
57
+ }
58
+ if (score > 0 && (best === null || score > best.score)) best = { value, score };
59
+ }
60
+ return best;
61
+ }
62
+
63
+ export function extractFacets(text) {
64
+ const source = String(text ?? '');
65
+ const facets = {};
66
+ const evidence = {};
67
+ for (const slot of SLOTS) {
68
+ const hit = scoreSlot(source, slot);
69
+ if (hit !== null) {
70
+ facets[slot] = hit.value;
71
+ evidence[slot] = hit.score;
72
+ }
73
+ }
74
+ // random is binary: if the text has no random signal, it is not guessed here.
75
+ return {
76
+ facets: normalizeFacets(facets),
77
+ evidence,
78
+ unknown: Object.keys(facets).length === 0,
79
+ };
80
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Facet-aware fold planning.
3
+ *
4
+ * Produces candidate contiguous ranges for the high-trigger compaction path.
5
+ * The engine still decides policy; this module only ranks old ranges and
6
+ * protects high-relevance current-topic turns.
7
+ */
8
+ import { facetScore } from './sensitivity.mjs';
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+
12
+ export function loadFacetFoldFile(file) {
13
+ const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
14
+ const entries = Array.isArray(raw?.entries) ? raw.entries : [];
15
+ const query = raw?.query ?? {};
16
+ const idf = new Map(Array.isArray(raw?.idf) ? raw.idf : []);
17
+ return { entries, query, idf };
18
+ }
19
+
20
+ export function defaultFacetFoldFile({
21
+ home = process.env.USERPROFILE ?? process.env.HOME,
22
+ } = {}) {
23
+ return path.join(String(home ?? ''), '.dsh', 'continuity', 'facet-fold.json');
24
+ }
25
+
26
+ export function planFacetFold(entries, {
27
+ query = {},
28
+ idf = null,
29
+ recentTurns = 20,
30
+ budgetTokens = 20000,
31
+ tokensBySeq = new Map(),
32
+ protectScore = 0.5,
33
+ } = {}) {
34
+ if (entries.length === 0) return { ranges: [], protected: [], seqs: [], tokens: 0 };
35
+ const latestTurn = Math.max(...entries.map((entry) => entry.turn));
36
+ const protectedEntries = [];
37
+ const candidates = [];
38
+ for (const entry of entries) {
39
+ const score = facetScore(entry.facets, query, idf).score;
40
+ const old = latestTurn - entry.turn > recentTurns;
41
+ if (score >= protectScore || !old) {
42
+ protectedEntries.push({ turn: entry.turn, seqStart: entry.seqStart, seqEnd: entry.seqEnd, score });
43
+ continue;
44
+ }
45
+ candidates.push({ ...entry, score, priority: (1 - Math.min(1, score)) * (latestTurn - entry.turn) });
46
+ }
47
+ candidates.sort((a, b) => b.priority - a.priority);
48
+
49
+ const ranges = [];
50
+ let used = 0;
51
+ const usedSeqs = new Set();
52
+ for (const entry of candidates) {
53
+ const seqs = [];
54
+ const start = Number(entry.seqStart);
55
+ const end = Number(entry.seqEnd);
56
+ if (!Number.isInteger(start) || !Number.isInteger(end) || end < start) continue;
57
+ let tokens = 0;
58
+ for (let seq = start; seq <= end; seq += 1) {
59
+ if (usedSeqs.has(seq)) continue;
60
+ const next = Number(tokensBySeq.get?.(seq) ?? 0);
61
+ if (used + tokens + next > budgetTokens) break;
62
+ seqs.push(seq);
63
+ tokens += next;
64
+ }
65
+ if (seqs.length === 0) continue;
66
+ for (const seq of seqs) usedSeqs.add(seq);
67
+ used += tokens;
68
+ ranges.push({
69
+ seqStart: seqs[0],
70
+ seqEnd: seqs[seqs.length - 1],
71
+ turns: 1,
72
+ tokens,
73
+ score: entry.score,
74
+ priority: entry.priority,
75
+ });
76
+ if (used >= budgetTokens) break;
77
+ }
78
+ return {
79
+ ranges,
80
+ protected: protectedEntries,
81
+ seqs: [...usedSeqs].sort((a, b) => a - b),
82
+ tokens: used,
83
+ };
84
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Hybrid local extractor: Naive Bayes for the learned slots, explicit rules for
3
+ * binary signals that the tiny student model cannot learn from 100 examples.
4
+ */
5
+ import { predictFacets } from './classifier.mjs';
6
+ import { detectRandom } from './random.mjs';
7
+
8
+ export function hybridExtractor(model, { minConfidence = 0 } = {}) {
9
+ const threshold = Number(minConfidence) || 0;
10
+ return (text) => {
11
+ const base = predictFacets(model, text);
12
+ const facets = { ...base.facets };
13
+ const confidence = { ...base.confidence };
14
+ const random = detectRandom(text);
15
+ facets.random = random.value;
16
+ confidence.random = random.value === '有' ? 1 : 0.5;
17
+ if (threshold > 0) {
18
+ for (const slot of Object.keys(facets)) {
19
+ if (slot === 'random') continue;
20
+ if ((confidence[slot] ?? 0) < threshold) delete facets[slot];
21
+ }
22
+ }
23
+ return {
24
+ facets,
25
+ confidence,
26
+ unknown: !Object.keys(facets).some((slot) => slot !== 'random'),
27
+ };
28
+ };
29
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Facet index + alias query. Pure data; no session reads.
3
+ *
4
+ * entries: [{ turn, seqStart, seqEnd, facets: {slot:value}, unknown }]
5
+ * query: alias string -> aliasFacets() OR explicit facet object
6
+ */
7
+ import { SLOTS, aliasFacets, normalizeFacets } from './schema.mjs';
8
+ import { extractFacets } from './extract.mjs';
9
+
10
+ function facetEntries(facets) {
11
+ return Object.entries(normalizeFacets(facets ?? {}));
12
+ }
13
+
14
+ export function buildFacetIndex(rows, { extract = extractFacets } = {}) {
15
+ const entries = [];
16
+ const df = new Map();
17
+ for (const row of [...rows].sort((a, b) => a.turn - b.turn)) {
18
+ const extracted = row.facets !== undefined
19
+ ? { facets: normalizeFacets(row.facets), evidence: {}, unknown: false }
20
+ : extract(row.text ?? '');
21
+ const facets = extracted.facets ?? {};
22
+ const keys = facetEntries(facets);
23
+ for (const [slot, value] of keys) {
24
+ const key = `${slot}:${value}`;
25
+ df.set(key, (df.get(key) ?? 0) + 1);
26
+ }
27
+ entries.push({
28
+ turn: row.turn,
29
+ seqStart: row.seqStart,
30
+ seqEnd: row.seqEnd,
31
+ facets,
32
+ unknown: extracted.unknown === true || keys.length === 0,
33
+ });
34
+ }
35
+ const total = entries.length;
36
+ const idf = new Map();
37
+ for (const [key, count] of df) {
38
+ idf.set(key, Math.log((1 + total) / (1 + count)) + 1);
39
+ }
40
+ return { entries, idf, total };
41
+ }
42
+
43
+ function resolveQuery(query, index) {
44
+ if (query !== null && typeof query === 'object' && !Array.isArray(query)) {
45
+ return { name: null, facets: normalizeFacets(query) };
46
+ }
47
+ const name = String(query ?? '').trim();
48
+ const facets = aliasFacets(name);
49
+ if (Object.keys(facets).length > 0) return { name, facets };
50
+ const extracted = extractFacets(name);
51
+ return { name, facets: extracted.facets };
52
+ }
53
+
54
+ export function queryFacets(index, query, { topK = 20, minCoverage = 0.6 } = {}) {
55
+ const resolved = resolveQuery(query, index);
56
+ const queryEntries = facetEntries(resolved.facets);
57
+ if (queryEntries.length === 0) return { query: resolved.name, facets: {}, matches: [], ranges: [] };
58
+
59
+ const matches = [];
60
+ for (const entry of index.entries) {
61
+ const matchesList = [];
62
+ let idfSum = 0;
63
+ for (const [slot, value] of queryEntries) {
64
+ if (entry.facets[slot] !== value) continue;
65
+ const key = `${slot}:${value}`;
66
+ const weight = index.idf.get(key) ?? 1;
67
+ idfSum += weight;
68
+ matchesList.push(key);
69
+ }
70
+ const coverage = matchesList.length / queryEntries.length;
71
+ if (coverage < minCoverage) continue;
72
+ matches.push({
73
+ turn: entry.turn,
74
+ seqStart: entry.seqStart,
75
+ seqEnd: entry.seqEnd,
76
+ coverage,
77
+ score: idfSum * coverage,
78
+ matched: matchesList,
79
+ });
80
+ }
81
+ matches.sort((a, b) => b.score - a.score || a.turn - b.turn);
82
+ const top = matches.slice(0, Math.max(1, topK));
83
+ const ranges = [];
84
+ for (const hit of top) {
85
+ const last = ranges[ranges.length - 1];
86
+ if (last !== undefined && hit.turn === last.lastTurn + 1) {
87
+ last.lastTurn = hit.turn;
88
+ last.seqEnd = Math.max(last.seqEnd, hit.seqEnd);
89
+ last.turns += 1;
90
+ } else {
91
+ ranges.push({ seqStart: hit.seqStart, seqEnd: hit.seqEnd, turns: 1, lastTurn: hit.turn, score: hit.score });
92
+ }
93
+ }
94
+ return {
95
+ query: resolved.name,
96
+ facets: resolved.facets,
97
+ matches: top,
98
+ ranges,
99
+ };
100
+ }
101
+
102
+ export function indexStats(index) {
103
+ const unknown = index.entries.filter((entry) => entry.unknown).length;
104
+ const bySlot = Object.fromEntries(SLOTS.map((slot) => [slot, {}]));
105
+ for (const entry of index.entries) {
106
+ for (const slot of SLOTS) {
107
+ const value = entry.facets[slot];
108
+ if (value === undefined) continue;
109
+ bySlot[slot][value] = (bySlot[slot][value] ?? 0) + 1;
110
+ }
111
+ }
112
+ return { total: index.total, unknown, bySlot };
113
+ }