@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.
- package/INSTALL.md +25 -0
- package/PANEL.md +92 -0
- package/README.md +286 -2
- package/RELEASE-NOTES.md +732 -0
- package/bin/bc-acceptance-full.mjs +91 -0
- package/bin/bc-acceptance-tight.mjs +66 -0
- package/bin/bc-acceptance.mjs +22 -0
- package/bin/bc-add-keywords.mjs +21 -0
- package/bin/bc-demo.mjs +47 -0
- package/bin/bc-expand-hint.mjs +179 -0
- package/bin/bc-ingest-index.mjs +35 -0
- package/bin/bc-ingest.mjs +31 -0
- package/bin/bc-label-demo.mjs +18 -0
- package/bin/bc-relabel.mjs +150 -0
- package/bin/bc-sessionize.mjs +59 -0
- package/bin/coref_pipeline.py +47 -0
- package/bin/coref_service.py +57 -0
- package/bin/debug_ckip.py +16 -0
- package/bin/debug_ner_zh.py +18 -0
- package/bin/doctor.mjs +275 -0
- package/bin/en_ner_error_audit.py +33 -0
- package/bin/eval-label-rules.mjs +342 -0
- package/bin/export-turns.py +68 -0
- package/bin/extract-user-paragraph.mjs +14 -0
- package/bin/extraction-composite-smoke.mjs +28 -0
- package/bin/extraction-latency.mjs +58 -0
- package/bin/extraction-user-smoke.mjs +28 -0
- package/bin/extraction_service.py +164 -0
- package/bin/facet-active-experiment.mjs +110 -0
- package/bin/facet-covid.mjs +8 -0
- package/bin/facet-curve.mjs +27 -0
- package/bin/facet-embedding-train.mjs +20 -0
- package/bin/facet-embedding-validate.mjs +83 -0
- package/bin/facet-eval-roleplay.mjs +42 -0
- package/bin/facet-eval.mjs +38 -0
- package/bin/facet-fold.mjs +31 -0
- package/bin/facet-load.mjs +16 -0
- package/bin/facet-mlp-scan.mjs +45 -0
- package/bin/facet-mvp.mjs +34 -0
- package/bin/facet-online-experiments.mjs +78 -0
- package/bin/facet-online-race.mjs +99 -0
- package/bin/facet-online-validate.mjs +51 -0
- package/bin/facet-panel.mjs +22 -0
- package/bin/facet-passage.mjs +41 -0
- package/bin/facet-random-validate.mjs +48 -0
- package/bin/facet-roleplay.mjs +33 -0
- package/bin/facet-scan.mjs +56 -0
- package/bin/facet-segment.mjs +34 -0
- package/bin/facet-train.mjs +19 -0
- package/bin/facet-validate.mjs +103 -0
- package/bin/facet-zeroshot.mjs +14 -0
- package/bin/install-templates.mjs +72 -0
- package/bin/memory-bench.mjs +53 -0
- package/bin/memory-build-summary.mjs +43 -0
- package/bin/memory-build-vector.mjs +31 -0
- package/bin/memory-build.mjs +41 -0
- package/bin/memory-calibration.mjs +70 -0
- package/bin/memory-closed-loop.mjs +119 -0
- package/bin/memory-dry-run.mjs +70 -0
- package/bin/memory-eval.mjs +104 -0
- package/bin/memory-event-alias-test.mjs +89 -0
- package/bin/memory-extraction-bench.mjs +58 -0
- package/bin/memory-load-gate-replay.mjs +137 -0
- package/bin/memory-loop-test.mjs +90 -0
- package/bin/memory-pre-turn-harness.mjs +63 -0
- package/bin/memory-query.mjs +39 -0
- package/bin/memory-replay-hints.mjs +84 -0
- package/bin/memory-scale.mjs +58 -0
- package/bin/memory-scenario-test.mjs +146 -0
- package/bin/memory-user-loop.mjs +95 -0
- package/bin/ner_bench_tiny_zh.py +59 -0
- package/bin/ner_bench_zh.py +60 -0
- package/bin/ner_conll_en.py +139 -0
- package/bin/ner_direct_en.py +123 -0
- package/bin/ner_direct_zh.py +100 -0
- package/bin/ner_pipeline.py +208 -0
- package/bin/noise-selfcheck.mjs +50 -0
- package/bin/pack-integrity.mjs +47 -0
- package/bin/panel-check.mjs +57 -0
- package/bin/plugin-integrity.mjs +74 -0
- package/bin/preview-live.mjs +13 -0
- package/bin/public-install-check.mjs +222 -0
- package/bin/relabel-apply.mjs +272 -0
- package/bin/relabel-dry-run.mjs +381 -0
- package/bin/relik_pipeline.py +39 -0
- package/bin/relik_service.py +52 -0
- package/bin/srl_pipeline.py +55 -0
- package/bin/srl_service.py +68 -0
- package/bin/summary-index.mjs +151 -0
- package/bin/summary-query.mjs +79 -0
- package/bin/vec-check.mjs +27 -0
- package/bin/zh_ner_error_audit.py +36 -0
- package/bin/zh_ner_slot_eval.py +38 -0
- package/client.js +760 -0
- package/cordis.patch.yml +378 -0
- package/docs/API-NOTES.md +1743 -0
- package/docs/ARCHITECTURE.md +355 -0
- package/docs/CACHE.md +253 -0
- package/docs/CONDUCT.md +30 -0
- package/docs/CREATOR-HANDOFF.md +102 -0
- package/docs/EXPERIMENT.md +136 -0
- package/docs/FACET-MVP-REPORT.md +473 -0
- package/docs/GUIDE-0.4.66.md +195 -0
- package/docs/GUIDE.md +127 -0
- package/docs/MECHANISM.md +243 -0
- package/docs/NEW-INSTANCE-HANDOFF.md +219 -0
- package/docs/README.md +37 -0
- package/docs/SESSION-LAYOUT.md +151 -0
- package/docs/history-map.md +249 -0
- package/docs/relabel-migration-wiring.md +366 -0
- package/guides/00-READ-FIRST.md +206 -0
- package/guides/01-INSTALL.md +207 -0
- package/guides/02-TABLES.md +225 -0
- package/guides/03-AUDIT.md +201 -0
- package/guides/04-REBUILD.md +306 -0
- package/guides/05-FIRST-RUN.md +191 -0
- package/guides/06-EMBEDDING.md +146 -0
- package/guides/07-PTC-AND-ASSETS.md +221 -0
- package/icon.svg +7 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/models/README.md +13 -0
- package/models/Xenova/bge-small-zh-v1.5/config.json +31 -0
- package/models/Xenova/bge-small-zh-v1.5/onnx/model_quantized.onnx +0 -0
- package/models/Xenova/bge-small-zh-v1.5/tokenizer.json +21278 -0
- package/models/Xenova/bge-small-zh-v1.5/tokenizer_config.json +15 -0
- package/package.json +139 -4
- package/src/asset-tool.mjs +237 -0
- package/src/assets.mjs +244 -0
- package/src/backfill.mjs +74 -0
- package/src/backup.mjs +80 -0
- package/src/deadend.mjs +95 -0
- package/src/deleted-store.mjs +35 -0
- package/src/engine.mjs +3831 -0
- package/src/facets/classifier.mjs +146 -0
- package/src/facets/embedding-service.mjs +53 -0
- package/src/facets/embedding-worker.mjs +27 -0
- package/src/facets/embedding.mjs +163 -0
- package/src/facets/extract.mjs +80 -0
- package/src/facets/fold-plan.mjs +84 -0
- package/src/facets/hybrid.mjs +29 -0
- package/src/facets/index.mjs +113 -0
- package/src/facets/linear-probe.mjs +43 -0
- package/src/facets/load.mjs +37 -0
- package/src/facets/mlp-probe.mjs +54 -0
- package/src/facets/mlp-service.mjs +53 -0
- package/src/facets/mlp-worker.mjs +26 -0
- package/src/facets/online-knn.mjs +58 -0
- package/src/facets/online-models.mjs +227 -0
- package/src/facets/panel-state.mjs +23 -0
- package/src/facets/panel.mjs +73 -0
- package/src/facets/random.mjs +26 -0
- package/src/facets/scanner.mjs +35 -0
- package/src/facets/schema.mjs +75 -0
- package/src/facets/seed-examples.mjs +58 -0
- package/src/facets/sensitivity.mjs +98 -0
- package/src/facets/service.mjs +53 -0
- package/src/facets/store.mjs +39 -0
- package/src/facets/worker.mjs +23 -0
- package/src/folds-store.mjs +109 -0
- package/src/groups.mjs +167 -0
- package/src/handoff-tool.mjs +51 -0
- package/src/handoff.mjs +127 -0
- package/src/index.mjs +649 -0
- package/src/inject-policy.mjs +17 -0
- package/src/learning.mjs +88 -0
- package/src/memory/alias-proposal.mjs +40 -0
- package/src/memory/bc-audit.mjs +245 -0
- package/src/memory/bc-index-updater.mjs +404 -0
- package/src/memory/bc-index.mjs +48 -0
- package/src/memory/bc-label.mjs +220 -0
- package/src/memory/bc-panel-state.mjs +107 -0
- package/src/memory/bc-pre-step.mjs +463 -0
- package/src/memory/bc-recall.mjs +470 -0
- package/src/memory/bc-shift.mjs +123 -0
- package/src/memory/bc-store.mjs +41 -0
- package/src/memory/bm25.mjs +112 -0
- package/src/memory/calibration.mjs +54 -0
- package/src/memory/chatter.mjs +29 -0
- package/src/memory/cmap-file.mjs +107 -0
- package/src/memory/concepts-file.mjs +41 -0
- package/src/memory/coref-slots.mjs +40 -0
- package/src/memory/event-alias-ledger.mjs +87 -0
- package/src/memory/event-log.mjs +95 -0
- package/src/memory/event-model.mjs +132 -0
- package/src/memory/event-segmenter.mjs +36 -0
- package/src/memory/event-summary-tree.mjs +122 -0
- package/src/memory/extraction-adapter.mjs +76 -0
- package/src/memory/extraction-composite.mjs +75 -0
- package/src/memory/extraction-core.mjs +76 -0
- package/src/memory/extraction-pre-step.mjs +161 -0
- package/src/memory/extraction-provider.mjs +300 -0
- package/src/memory/extraction-score.mjs +36 -0
- package/src/memory/facet-stabilizer.mjs +59 -0
- package/src/memory/history-map.mjs +139 -0
- package/src/memory/language-router.mjs +30 -0
- package/src/memory/load-gate.mjs +161 -0
- package/src/memory/mechanical-note.mjs +104 -0
- package/src/memory/onboarding.mjs +163 -0
- package/src/memory/persona.mjs +78 -0
- package/src/memory/planner.mjs +249 -0
- package/src/memory/pre-turn.mjs +80 -0
- package/src/memory/query-aliases.mjs +39 -0
- package/src/memory/recall-cue.mjs +43 -0
- package/src/memory/recall.mjs +65 -0
- package/src/memory/referent-candidates.mjs +55 -0
- package/src/memory/referent-ledger.mjs +86 -0
- package/src/memory/relabel-migrate.mjs +376 -0
- package/src/memory/relik-adapter.mjs +25 -0
- package/src/memory/special-keywords.mjs +60 -0
- package/src/memory/structured-adapters.mjs +75 -0
- package/src/memory/summary-index.mjs +335 -0
- package/src/memory/summary-tree.mjs +110 -0
- package/src/memory/term-extractor.mjs +44 -0
- package/src/memory/tokenize.mjs +9 -0
- package/src/memory/vector-index.mjs +54 -0
- package/src/memory/window-summary.mjs +133 -0
- package/src/mood.mjs +133 -0
- package/src/peer-tool.mjs +63 -0
- package/src/peer.mjs +144 -0
- package/src/policy.mjs +170 -0
- package/src/preflight.mjs +113 -0
- package/src/provenance.mjs +94 -0
- package/src/range.mjs +75 -0
- package/src/recall-exact-tool.mjs +267 -0
- package/src/recall-tool.mjs +521 -0
- package/src/recheck.mjs +99 -0
- package/src/save-start.mjs +29 -0
- package/src/session-guard.mjs +140 -0
- package/src/session-log-tool.mjs +322 -0
- package/src/session-log.mjs +606 -0
- package/src/skills.mjs +111 -0
- package/src/split-range.mjs +115 -0
- package/src/state.mjs +198 -0
- package/src/t0.mjs +149 -0
- package/src/tag-confirm.mjs +78 -0
- package/src/tag-detect.mjs +42 -0
- package/src/tag-maintenance.mjs +285 -0
- package/src/tag-reject.mjs +73 -0
- package/src/tags-store.mjs +94 -0
- package/src/tags.mjs +692 -0
- package/src/threads-backend.mjs +26 -0
- package/src/threads-core.mjs +1887 -0
- package/src/threads-store.mjs +207 -0
- package/src/threads-tool.mjs +522 -0
- package/src/threads.mjs +929 -0
- package/src/tool-preview.mjs +427 -0
- package/src/topic.mjs +63 -0
- package/src/training.mjs +75 -0
- package/src/turn-tag.mjs +62 -0
- package/src/user-turn.mjs +39 -0
- package/templates/cmap.md +11 -0
- package/templates/concepts.md +12 -0
- package/templates/habits.md +40 -0
- package/templates/keywords.md +6 -0
- package/templates/rebuild.md +134 -0
- package/templates/rules-safe.md +184 -0
- package/templates/rules.md +177 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frozen-embedding linear probe.
|
|
3
|
+
* Weights are exported from a local logistic-regression training run.
|
|
4
|
+
*/
|
|
5
|
+
export function deserializeLinearProbe(raw) {
|
|
6
|
+
return {
|
|
7
|
+
slots: Object.fromEntries(Object.entries(raw?.slots ?? {}).map(([slot, value]) => [slot, {
|
|
8
|
+
classes: value.classes,
|
|
9
|
+
coef: value.coef,
|
|
10
|
+
intercept: value.intercept,
|
|
11
|
+
}])),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function softmax(scores) {
|
|
16
|
+
const max = Math.max(...scores);
|
|
17
|
+
const exp = scores.map((score) => Math.exp(score - max));
|
|
18
|
+
const sum = exp.reduce((a, b) => a + b, 0) || 1;
|
|
19
|
+
return exp.map((value) => value / sum);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function predictLinearProbe(model, vector) {
|
|
23
|
+
const facets = {};
|
|
24
|
+
const confidence = {};
|
|
25
|
+
for (const [slot, slotModel] of Object.entries(model.slots)) {
|
|
26
|
+
const scores = slotModel.classes.map((_, classIndex) => {
|
|
27
|
+
let value = slotModel.intercept[classIndex] ?? 0;
|
|
28
|
+
const coef = slotModel.coef[classIndex];
|
|
29
|
+
for (let i = 0; i < vector.length; i += 1) value += coef[i] * vector[i];
|
|
30
|
+
return value;
|
|
31
|
+
});
|
|
32
|
+
const probs = softmax(scores);
|
|
33
|
+
let best = 0;
|
|
34
|
+
for (let i = 1; i < probs.length; i += 1) if (probs[i] > probs[best]) best = i;
|
|
35
|
+
facets[slot] = slotModel.classes[best];
|
|
36
|
+
confidence[slot] = probs[best];
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
facets,
|
|
40
|
+
confidence,
|
|
41
|
+
unknown: !Object.keys(facets).some((slot) => slot !== 'random'),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a facet query to loadable seqs.
|
|
3
|
+
* The plugin still uses the normal `continuity.load` path; this only turns
|
|
4
|
+
* alias/query facets into seqs and ranges.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import { queryFacets } from './index.mjs';
|
|
8
|
+
import { loadIndex } from './store.mjs';
|
|
9
|
+
import { aliasFacets } from './schema.mjs';
|
|
10
|
+
|
|
11
|
+
export function loadFacetQuery(query, {
|
|
12
|
+
indexFile,
|
|
13
|
+
topK = 20,
|
|
14
|
+
maxSeqs = 200,
|
|
15
|
+
} = {}) {
|
|
16
|
+
if (typeof indexFile !== 'string' || indexFile.length === 0) {
|
|
17
|
+
throw new Error('facet load: indexFile is required');
|
|
18
|
+
}
|
|
19
|
+
const index = loadIndex(indexFile);
|
|
20
|
+
const result = queryFacets(index, query, { topK });
|
|
21
|
+
const seqs = [];
|
|
22
|
+
for (const match of result.matches) {
|
|
23
|
+
const start = Number(match.seqStart);
|
|
24
|
+
const end = Number(match.seqEnd);
|
|
25
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || end < start) continue;
|
|
26
|
+
for (let seq = start; seq <= end && seqs.length < maxSeqs; seq += 1) seqs.push(seq);
|
|
27
|
+
if (seqs.length >= maxSeqs) break;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
query,
|
|
31
|
+
facets: aliasFacets(query),
|
|
32
|
+
ranges: result.ranges,
|
|
33
|
+
seqs,
|
|
34
|
+
truncated: seqs.length >= maxSeqs,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frozen BGE + per-slot MLP head, exported from sklearn.
|
|
3
|
+
* Two dense layers with ReLU; output softmax.
|
|
4
|
+
*/
|
|
5
|
+
export function deserializeMlp(raw) {
|
|
6
|
+
return {
|
|
7
|
+
slots: Object.fromEntries(Object.entries(raw?.slots ?? {}).map(([slot, value]) => [slot, {
|
|
8
|
+
classes: value.classes,
|
|
9
|
+
w1: value.w1,
|
|
10
|
+
b1: value.b1,
|
|
11
|
+
w2: value.w2,
|
|
12
|
+
b2: value.b2,
|
|
13
|
+
}])),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function relu(value) {
|
|
18
|
+
return value > 0 ? value : 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function softmax(scores) {
|
|
22
|
+
const max = Math.max(...scores);
|
|
23
|
+
const exp = scores.map((score) => Math.exp(score - max));
|
|
24
|
+
const sum = exp.reduce((a, b) => a + b, 0) || 1;
|
|
25
|
+
return exp.map((value) => value / sum);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function predictMlp(model, vector) {
|
|
29
|
+
const facets = {};
|
|
30
|
+
const confidence = {};
|
|
31
|
+
for (const [slot, slotModel] of Object.entries(model.slots)) {
|
|
32
|
+
const hidden = new Array(slotModel.b1.length);
|
|
33
|
+
for (let h = 0; h < hidden.length; h += 1) {
|
|
34
|
+
let sum = slotModel.b1[h];
|
|
35
|
+
for (let d = 0; d < vector.length; d += 1) sum += slotModel.w1[d][h] * vector[d];
|
|
36
|
+
hidden[h] = relu(sum);
|
|
37
|
+
}
|
|
38
|
+
const scores = slotModel.classes.map((_, c) => {
|
|
39
|
+
let sum = slotModel.b2[c];
|
|
40
|
+
for (let h = 0; h < hidden.length; h += 1) sum += slotModel.w2[h][c] * hidden[h];
|
|
41
|
+
return sum;
|
|
42
|
+
});
|
|
43
|
+
const probs = softmax(scores);
|
|
44
|
+
let best = 0;
|
|
45
|
+
for (let i = 1; i < probs.length; i += 1) if (probs[i] > probs[best]) best = i;
|
|
46
|
+
facets[slot] = slotModel.classes[best];
|
|
47
|
+
confidence[slot] = probs[best];
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
facets,
|
|
51
|
+
confidence,
|
|
52
|
+
unknown: !Object.keys(facets).some((slot) => slot !== 'random'),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main-thread client for the MLP facet worker.
|
|
3
|
+
*/
|
|
4
|
+
import { Worker } from 'node:worker_threads';
|
|
5
|
+
|
|
6
|
+
export function createMlpFacetService({ modelPath, embeddingModel, timeoutMs = 120000 } = {}) {
|
|
7
|
+
if (typeof modelPath !== 'string' || modelPath.length === 0) {
|
|
8
|
+
throw new Error('mlp service: modelPath is required');
|
|
9
|
+
}
|
|
10
|
+
const worker = new Worker(new URL('./mlp-worker.mjs', import.meta.url), {
|
|
11
|
+
workerData: { modelPath, embeddingModel },
|
|
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('mlp 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('mlp service: closed'));
|
|
48
|
+
}
|
|
49
|
+
pending.clear();
|
|
50
|
+
await worker.terminate();
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MLP facet worker: BGE embedding + per-slot MLP in worker_threads.
|
|
3
|
+
*/
|
|
4
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import { embedTexts } from './embedding.mjs';
|
|
7
|
+
import { deserializeMlp, predictMlp } from './mlp-probe.mjs';
|
|
8
|
+
import { detectRandom } from './random.mjs';
|
|
9
|
+
|
|
10
|
+
const model = deserializeMlp(JSON.parse(fs.readFileSync(workerData.modelPath, 'utf8')));
|
|
11
|
+
const embeddingModel = workerData.embeddingModel;
|
|
12
|
+
|
|
13
|
+
parentPort.on('message', async (message) => {
|
|
14
|
+
const id = message?.id;
|
|
15
|
+
try {
|
|
16
|
+
const text = String(message?.text ?? '');
|
|
17
|
+
const [vector] = await embedTexts([text], embeddingModel);
|
|
18
|
+
const result = predictMlp(model, vector);
|
|
19
|
+
result.facets.random = detectRandom(text).value;
|
|
20
|
+
if (result.facets.participants === undefined) result.facets.participants = '协作';
|
|
21
|
+
result.unknown = !Object.keys(result.facets).some((slot) => slot !== 'random');
|
|
22
|
+
parentPort.postMessage({ id, ok: true, result });
|
|
23
|
+
} catch (error) {
|
|
24
|
+
parentPort.postMessage({ id, ok: false, error: String(error?.message ?? error) });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prequential (online) facet learner.
|
|
3
|
+
*
|
|
4
|
+
* At turn t it predicts from examples seen before t, then accepts the true
|
|
5
|
+
* label. No future labels are used.
|
|
6
|
+
*/
|
|
7
|
+
import { SLOTS, normalizeFacets } from './schema.mjs';
|
|
8
|
+
|
|
9
|
+
function cosine(a, b) {
|
|
10
|
+
if (a.length !== b.length) return 0;
|
|
11
|
+
let dot = 0;
|
|
12
|
+
for (let i = 0; i < a.length; i += 1) dot += a[i] * b[i];
|
|
13
|
+
return dot;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class OnlineFacetKnn {
|
|
17
|
+
constructor({ k = 5, minSimilarity = 0.2, minVote = 0.5 } = {}) {
|
|
18
|
+
this.k = k;
|
|
19
|
+
this.minSimilarity = minSimilarity;
|
|
20
|
+
this.minVote = minVote;
|
|
21
|
+
this.examples = [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
add(vector, facets) {
|
|
25
|
+
this.examples.push({ vector, facets: normalizeFacets(facets ?? {}) });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
predict(vector) {
|
|
29
|
+
const facets = {};
|
|
30
|
+
const confidence = {};
|
|
31
|
+
for (const slot of SLOTS) {
|
|
32
|
+
const neighbors = this.examples
|
|
33
|
+
.filter((example) => example.facets[slot] !== undefined)
|
|
34
|
+
.map((example) => ({ example, score: cosine(vector, example.vector) }))
|
|
35
|
+
.filter((item) => item.score >= this.minSimilarity)
|
|
36
|
+
.sort((a, b) => b.score - a.score)
|
|
37
|
+
.slice(0, this.k);
|
|
38
|
+
if (neighbors.length === 0) continue;
|
|
39
|
+
const votes = new Map();
|
|
40
|
+
let total = 0;
|
|
41
|
+
for (const item of neighbors) {
|
|
42
|
+
const value = item.example.facets[slot];
|
|
43
|
+
votes.set(value, (votes.get(value) ?? 0) + item.score);
|
|
44
|
+
total += item.score;
|
|
45
|
+
}
|
|
46
|
+
const [value, weight] = [...votes.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
47
|
+
const vote = total === 0 ? 0 : weight / total;
|
|
48
|
+
if (vote < this.minVote) continue;
|
|
49
|
+
facets[slot] = value;
|
|
50
|
+
confidence[slot] = vote;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
facets,
|
|
54
|
+
confidence,
|
|
55
|
+
unknown: !Object.keys(facets).some((slot) => slot !== 'random'),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True incremental facet learners.
|
|
3
|
+
* All consume one labelled turn at a time; no batch retraining.
|
|
4
|
+
*/
|
|
5
|
+
import { SLOTS, normalizeFacets } from './schema.mjs';
|
|
6
|
+
|
|
7
|
+
function cosine(a, b) {
|
|
8
|
+
let dot = 0;
|
|
9
|
+
for (let i = 0; i < a.length; i += 1) dot += a[i] * b[i];
|
|
10
|
+
return dot;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function softmax(scores) {
|
|
14
|
+
const max = Math.max(...scores);
|
|
15
|
+
const exp = scores.map((score) => Math.exp(score - max));
|
|
16
|
+
const sum = exp.reduce((a, b) => a + b, 0) || 1;
|
|
17
|
+
return exp.map((value) => value / sum);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function result(facets, confidence) {
|
|
21
|
+
return {
|
|
22
|
+
facets,
|
|
23
|
+
confidence,
|
|
24
|
+
unknown: !Object.keys(facets).some((slot) => slot !== 'random'),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Running mean per facet value, cosine nearest centroid. */
|
|
29
|
+
export class OnlineCentroid {
|
|
30
|
+
constructor({ minSimilarity = 0.15 } = {}) {
|
|
31
|
+
this.minSimilarity = minSimilarity;
|
|
32
|
+
this.state = Object.fromEntries(SLOTS.map((slot) => [slot, new Map()]));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
predict(vector) {
|
|
36
|
+
const facets = {};
|
|
37
|
+
const confidence = {};
|
|
38
|
+
for (const slot of SLOTS) {
|
|
39
|
+
let best = null;
|
|
40
|
+
for (const [value, cell] of this.state[slot]) {
|
|
41
|
+
if (cell.count === 0) continue;
|
|
42
|
+
const centroid = cell.sum.map((x) => x / cell.count);
|
|
43
|
+
const score = cosine(vector, centroid);
|
|
44
|
+
if (best === null || score > best.score) best = { value, score };
|
|
45
|
+
}
|
|
46
|
+
if (best === null || best.score < this.minSimilarity) continue;
|
|
47
|
+
facets[slot] = best.value;
|
|
48
|
+
confidence[slot] = best.score;
|
|
49
|
+
}
|
|
50
|
+
return result(facets, confidence);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
update(vector, facets) {
|
|
54
|
+
for (const [slot, value] of Object.entries(normalizeFacets(facets ?? {}))) {
|
|
55
|
+
let cell = this.state[slot].get(value);
|
|
56
|
+
if (cell === undefined) {
|
|
57
|
+
cell = { count: 0, sum: new Array(vector.length).fill(0) };
|
|
58
|
+
this.state[slot].set(value, cell);
|
|
59
|
+
}
|
|
60
|
+
cell.count += 1;
|
|
61
|
+
for (let i = 0; i < vector.length; i += 1) cell.sum[i] += vector[i];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Running mean + diagonal variance, Gaussian-like score. */
|
|
67
|
+
export class OnlineCentroidVariance {
|
|
68
|
+
constructor({ minSimilarity = 0.1, varianceWeight = 0.5 } = {}) {
|
|
69
|
+
this.minSimilarity = minSimilarity;
|
|
70
|
+
this.varianceWeight = varianceWeight;
|
|
71
|
+
this.state = Object.fromEntries(SLOTS.map((slot) => [slot, new Map()]));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
predict(vector) {
|
|
75
|
+
const facets = {};
|
|
76
|
+
const confidence = {};
|
|
77
|
+
for (const slot of SLOTS) {
|
|
78
|
+
let best = null;
|
|
79
|
+
for (const [value, cell] of this.state[slot]) {
|
|
80
|
+
if (cell.count < 2) continue;
|
|
81
|
+
let dot = 0;
|
|
82
|
+
let dist = 0;
|
|
83
|
+
for (let i = 0; i < vector.length; i += 1) {
|
|
84
|
+
const mean = cell.sum[i] / cell.count;
|
|
85
|
+
const variance = Math.max(1e-6, cell.sumsq[i] / cell.count - mean * mean);
|
|
86
|
+
dot += vector[i] * mean;
|
|
87
|
+
dist += ((vector[i] - mean) ** 2) / variance;
|
|
88
|
+
}
|
|
89
|
+
const score = dot - this.varianceWeight * (dist / vector.length);
|
|
90
|
+
if (best === null || score > best.score) best = { value, score };
|
|
91
|
+
}
|
|
92
|
+
if (best === null || best.score < this.minSimilarity) continue;
|
|
93
|
+
facets[slot] = best.value;
|
|
94
|
+
confidence[slot] = best.score;
|
|
95
|
+
}
|
|
96
|
+
return result(facets, confidence);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
update(vector, facets) {
|
|
100
|
+
for (const [slot, value] of Object.entries(normalizeFacets(facets ?? {}))) {
|
|
101
|
+
let cell = this.state[slot].get(value);
|
|
102
|
+
if (cell === undefined) {
|
|
103
|
+
cell = {
|
|
104
|
+
count: 0,
|
|
105
|
+
sum: new Array(vector.length).fill(0),
|
|
106
|
+
sumsq: new Array(vector.length).fill(0),
|
|
107
|
+
};
|
|
108
|
+
this.state[slot].set(value, cell);
|
|
109
|
+
}
|
|
110
|
+
cell.count += 1;
|
|
111
|
+
for (let i = 0; i < vector.length; i += 1) {
|
|
112
|
+
cell.sum[i] += vector[i];
|
|
113
|
+
cell.sumsq[i] += vector[i] * vector[i];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function tokens(text) {
|
|
120
|
+
const raw = String(text ?? '').toLowerCase();
|
|
121
|
+
const words = raw.match(/[a-z0-9_]+/g) ?? [];
|
|
122
|
+
const chinese = [...raw.replace(/[^\u4e00-\u9fff]/g, '')];
|
|
123
|
+
const bigrams = [];
|
|
124
|
+
for (let i = 0; i + 1 < chinese.length; i += 1) bigrams.push(chinese[i] + chinese[i + 1]);
|
|
125
|
+
return [...words, ...bigrams];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Multinomial Naive Bayes with additive smoothing. */
|
|
129
|
+
export class OnlineNaiveBayes {
|
|
130
|
+
constructor({ alpha = 1 } = {}) {
|
|
131
|
+
this.alpha = alpha;
|
|
132
|
+
this.state = Object.fromEntries(SLOTS.map((slot) => [slot, new Map()]));
|
|
133
|
+
this.prior = Object.fromEntries(SLOTS.map((slot) => [slot, new Map()]));
|
|
134
|
+
this.total = Object.fromEntries(SLOTS.map((slot) => [slot, 0]));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
predict(text) {
|
|
138
|
+
const t = tokens(text);
|
|
139
|
+
const facets = {};
|
|
140
|
+
const confidence = {};
|
|
141
|
+
for (const slot of SLOTS) {
|
|
142
|
+
if (this.total[slot] === 0) continue;
|
|
143
|
+
let best = null;
|
|
144
|
+
for (const [value, docs] of this.prior[slot]) {
|
|
145
|
+
let score = Math.log((docs + this.alpha) / (this.total[slot] + this.alpha * this.prior[slot].size));
|
|
146
|
+
const counts = this.state[slot].get(value);
|
|
147
|
+
let tokenTotal = 0;
|
|
148
|
+
for (const count of counts.values()) tokenTotal += count;
|
|
149
|
+
for (const token of t) {
|
|
150
|
+
score += Math.log(((counts.get(token) ?? 0) + this.alpha) / (tokenTotal + this.alpha * counts.size));
|
|
151
|
+
}
|
|
152
|
+
if (best === null || score > best.score) best = { value, score };
|
|
153
|
+
}
|
|
154
|
+
if (best === null) continue;
|
|
155
|
+
facets[slot] = best.value;
|
|
156
|
+
confidence[slot] = best.score;
|
|
157
|
+
}
|
|
158
|
+
return result(facets, confidence);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
update(text, facets) {
|
|
162
|
+
for (const [slot, value] of Object.entries(normalizeFacets(facets ?? {}))) {
|
|
163
|
+
if (!this.prior[slot].has(value)) {
|
|
164
|
+
this.prior[slot].set(value, 0);
|
|
165
|
+
this.state[slot].set(value, new Map());
|
|
166
|
+
}
|
|
167
|
+
this.prior[slot].set(value, this.prior[slot].get(value) + 1);
|
|
168
|
+
this.total[slot] += 1;
|
|
169
|
+
const counts = this.state[slot].get(value);
|
|
170
|
+
for (const token of tokens(text)) counts.set(token, (counts.get(token) ?? 0) + 1);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** One-pass online softmax regression. */
|
|
176
|
+
export class OnlineSoftmax {
|
|
177
|
+
constructor({ classes, dim, learningRate = 0.02 } = {}) {
|
|
178
|
+
this.classes = classes;
|
|
179
|
+
this.dim = dim;
|
|
180
|
+
this.learningRate = learningRate;
|
|
181
|
+
this.weights = new Map();
|
|
182
|
+
this.bias = new Map();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
ensure(value) {
|
|
186
|
+
if (!this.weights.has(value)) {
|
|
187
|
+
this.weights.set(value, new Array(this.dim).fill(0));
|
|
188
|
+
this.bias.set(value, 0);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
predict(vector) {
|
|
193
|
+
if (this.classes.length === 0) return result({}, {});
|
|
194
|
+
const scores = this.classes.map((value) => {
|
|
195
|
+
this.ensure(value);
|
|
196
|
+
const weights = this.weights.get(value);
|
|
197
|
+
let sum = this.bias.get(value);
|
|
198
|
+
for (let i = 0; i < vector.length; i += 1) sum += weights[i] * vector[i];
|
|
199
|
+
return sum;
|
|
200
|
+
});
|
|
201
|
+
const probs = softmax(scores);
|
|
202
|
+
let best = 0;
|
|
203
|
+
for (let i = 1; i < probs.length; i += 1) if (probs[i] > probs[best]) best = i;
|
|
204
|
+
return result({ activity: this.classes[best] }, { activity: probs[best] });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
update(vector, facets) {
|
|
208
|
+
const truth = normalizeFacets(facets ?? {});
|
|
209
|
+
if (truth.activity === undefined) return;
|
|
210
|
+
this.ensure(truth.activity);
|
|
211
|
+
const scores = this.classes.map((value) => {
|
|
212
|
+
this.ensure(value);
|
|
213
|
+
const weights = this.weights.get(value);
|
|
214
|
+
let sum = this.bias.get(value);
|
|
215
|
+
for (let i = 0; i < vector.length; i += 1) sum += weights[i] * vector[i];
|
|
216
|
+
return sum;
|
|
217
|
+
});
|
|
218
|
+
const probs = softmax(scores);
|
|
219
|
+
for (let c = 0; c < this.classes.length; c += 1) {
|
|
220
|
+
const value = this.classes[c];
|
|
221
|
+
const gradient = probs[c] - (value === truth.activity ? 1 : 0);
|
|
222
|
+
const weights = this.weights.get(value);
|
|
223
|
+
for (let i = 0; i < vector.length; i += 1) weights[i] -= this.learningRate * gradient * vector[i];
|
|
224
|
+
this.bias.set(value, this.bias.get(value) - this.learningRate * gradient);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only facet panel snapshot for the host endpoint.
|
|
3
|
+
* The UI works without this file; absence is reported as available:false.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
export const FACET_PANEL_FILE = 'facet-panel.json';
|
|
9
|
+
|
|
10
|
+
export function buildFacetState({ home = process.env.USERPROFILE ?? process.env.HOME } = {}) {
|
|
11
|
+
const file = path.join(String(home ?? ''), '.dsh', 'continuity', FACET_PANEL_FILE);
|
|
12
|
+
try {
|
|
13
|
+
const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
14
|
+
return { available: true, file, generatedAt: new Date().toISOString(), panel: raw };
|
|
15
|
+
} catch (error) {
|
|
16
|
+
return {
|
|
17
|
+
available: false,
|
|
18
|
+
file,
|
|
19
|
+
generatedAt: new Date().toISOString(),
|
|
20
|
+
reason: error?.code === 'ENOENT' ? 'missing' : String(error?.message ?? error),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Facet panel snapshot and load guidance.
|
|
3
|
+
*
|
|
4
|
+
* Pure data for a future panel/load hook: counts, current alias-facet pairs,
|
|
5
|
+
* a sensitivity curve, and one-line load hints. No UI dependency.
|
|
6
|
+
*/
|
|
7
|
+
import { DEFAULT_ALIASES, SLOTS, aliasFacets, normalizeFacets } from './schema.mjs';
|
|
8
|
+
import { sensitivitySeries } from './sensitivity.mjs';
|
|
9
|
+
import { buildFacetIndex, queryFacets } from './index.mjs';
|
|
10
|
+
|
|
11
|
+
export function buildFacetPanel(entries, {
|
|
12
|
+
idf = null,
|
|
13
|
+
currentAlias = null,
|
|
14
|
+
aliases = DEFAULT_ALIASES,
|
|
15
|
+
} = {}) {
|
|
16
|
+
const counts = Object.fromEntries(SLOTS.map((slot) => [slot, {}]));
|
|
17
|
+
let unknown = 0;
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
const facets = normalizeFacets(entry.facets ?? {});
|
|
20
|
+
if (Object.keys(facets).length === 0) unknown += 1;
|
|
21
|
+
for (const [slot, value] of Object.entries(facets)) {
|
|
22
|
+
counts[slot][value] = (counts[slot][value] ?? 0) + 1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const facetSum = Object.values(counts).reduce((sum, values) => (
|
|
26
|
+
sum + Object.values(values).reduce((a, b) => a + b, 0)
|
|
27
|
+
), 0);
|
|
28
|
+
const alias = currentAlias !== null && aliases[String(currentAlias)] ? String(currentAlias) : null;
|
|
29
|
+
const query = alias === null ? {} : aliasFacets(alias);
|
|
30
|
+
const series = Object.keys(query).length === 0
|
|
31
|
+
? []
|
|
32
|
+
: sensitivitySeries(entries, query, { idf });
|
|
33
|
+
return {
|
|
34
|
+
totalTurns: entries.length,
|
|
35
|
+
unknown,
|
|
36
|
+
facetSum,
|
|
37
|
+
counts,
|
|
38
|
+
currentTopic: alias === null ? null : {
|
|
39
|
+
alias,
|
|
40
|
+
facets: query,
|
|
41
|
+
pairs: Object.entries(query).map(([slot, value]) => ({
|
|
42
|
+
slot,
|
|
43
|
+
value,
|
|
44
|
+
turns: counts[slot]?.[value] ?? 0,
|
|
45
|
+
})),
|
|
46
|
+
},
|
|
47
|
+
curve: series,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildLoadHint(entries, {
|
|
52
|
+
idf = null,
|
|
53
|
+
query = '跑团',
|
|
54
|
+
recentTurns = 20,
|
|
55
|
+
minScore = 0.2,
|
|
56
|
+
currentTurn = null,
|
|
57
|
+
} = {}) {
|
|
58
|
+
const index = idf === null ? buildFacetIndex(entries) : { entries, idf, total: entries.length };
|
|
59
|
+
const result = queryFacets(index, query, { topK: entries.length });
|
|
60
|
+
if (result.matches.length === 0) return null;
|
|
61
|
+
const latestTurn = currentTurn ?? Math.max(...entries.map((entry) => entry.turn));
|
|
62
|
+
const recent = result.matches.filter((hit) => latestTurn - hit.turn <= recentTurns);
|
|
63
|
+
if (recent.length > 0) return null;
|
|
64
|
+
const top = result.matches[0];
|
|
65
|
+
if (top.score < minScore) return null;
|
|
66
|
+
const range = result.ranges[result.ranges.length - 1] ?? null;
|
|
67
|
+
return {
|
|
68
|
+
query,
|
|
69
|
+
text: `[话题线索] ${query}(候选 ${result.ranges.length} 段,最近 seq ${range?.seqStart ?? top.seqStart}–${range?.seqEnd ?? top.seqEnd},置信 ${top.score.toFixed(2)})`,
|
|
70
|
+
ranges: result.ranges,
|
|
71
|
+
top,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explicit random-signal detector.
|
|
3
|
+
*
|
|
4
|
+
* The last 100 turns contain no dice words, so a model trained only on them
|
|
5
|
+
* collapses to "无". This detector is the positive-label source for random:
|
|
6
|
+
* it is rule-based and deliberately narrow.
|
|
7
|
+
*/
|
|
8
|
+
export const RANDOM_HINTS = Object.freeze([
|
|
9
|
+
'掷骰',
|
|
10
|
+
'骰子',
|
|
11
|
+
'骰',
|
|
12
|
+
'd20',
|
|
13
|
+
'检定',
|
|
14
|
+
'抽卡',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export function detectRandom(text) {
|
|
18
|
+
const raw = String(text ?? '');
|
|
19
|
+
const lower = raw.toLowerCase();
|
|
20
|
+
const hits = RANDOM_HINTS.filter((hint) => lower.includes(hint));
|
|
21
|
+
const dice = lower.match(/(?:^|[^a-z0-9])d\d{1,3}(?![a-z0-9])/g) ?? [];
|
|
22
|
+
const getRandom = lower.includes('get-random');
|
|
23
|
+
if (dice.length > 0) hits.push('dN');
|
|
24
|
+
if (getRandom) hits.push('get-random');
|
|
25
|
+
return { value: hits.length > 0 ? '有' : '无', hits: [...new Set(hits)] };
|
|
26
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental scanner. It advances a seq cursor and queues unknowns; it never
|
|
3
|
+
* blocks the cursor waiting for a schema answer.
|
|
4
|
+
*/
|
|
5
|
+
import { normalizeFacets } from './schema.mjs';
|
|
6
|
+
import { extractFacets } from './extract.mjs';
|
|
7
|
+
|
|
8
|
+
export function emptyCursor(schemaVersion = 1) {
|
|
9
|
+
return { schemaVersion, lastSeq: -1, residual: [] };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function scanRows(rows, cursor = emptyCursor(), { extract = extractFacets } = {}) {
|
|
13
|
+
const state = {
|
|
14
|
+
schemaVersion: cursor.schemaVersion ?? 1,
|
|
15
|
+
lastSeq: Number.isInteger(cursor.lastSeq) ? cursor.lastSeq : -1,
|
|
16
|
+
residual: Array.isArray(cursor.residual) ? [...cursor.residual] : [],
|
|
17
|
+
};
|
|
18
|
+
const entries = [];
|
|
19
|
+
for (const row of [...rows].sort((a, b) => a.seqStart - b.seqStart)) {
|
|
20
|
+
if (row.seqStart <= state.lastSeq) continue;
|
|
21
|
+
const extracted = row.facets !== undefined
|
|
22
|
+
? { facets: normalizeFacets(row.facets), evidence: {}, unknown: false }
|
|
23
|
+
: extract(row.text ?? '');
|
|
24
|
+
const facets = extracted.facets ?? {};
|
|
25
|
+
const unknown = extracted.unknown === true || Object.keys(facets).length === 0;
|
|
26
|
+
if (unknown) state.residual.push({ seqStart: row.seqStart, seqEnd: row.seqEnd, turn: row.turn });
|
|
27
|
+
entries.push({ turn: row.turn, seqStart: row.seqStart, seqEnd: row.seqEnd, facets, unknown });
|
|
28
|
+
state.lastSeq = Math.max(state.lastSeq, row.seqEnd);
|
|
29
|
+
}
|
|
30
|
+
return { cursor: state, entries };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function residualCount(cursor) {
|
|
34
|
+
return Array.isArray(cursor?.residual) ? cursor.residual.length : 0;
|
|
35
|
+
}
|