@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,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BM25 index over the immutable event log.
|
|
3
|
+
*/
|
|
4
|
+
import { tokenize } from './tokenize.mjs';
|
|
5
|
+
|
|
6
|
+
export class BM25Index {
|
|
7
|
+
constructor({ k1 = 1.5, b = 0.75 } = {}) {
|
|
8
|
+
this.k1 = k1;
|
|
9
|
+
this.b = b;
|
|
10
|
+
this.docs = [];
|
|
11
|
+
this.postings = new Map();
|
|
12
|
+
this.docLengths = [];
|
|
13
|
+
this.avgLength = 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
build(records) {
|
|
17
|
+
this.docs = records.map((record) => ({
|
|
18
|
+
turn: record.turn,
|
|
19
|
+
seqStart: record.seqStart,
|
|
20
|
+
seqEnd: record.seqEnd,
|
|
21
|
+
}));
|
|
22
|
+
this.postings = new Map();
|
|
23
|
+
this.docLengths = [];
|
|
24
|
+
let total = 0;
|
|
25
|
+
records.forEach((record, index) => {
|
|
26
|
+
const tokens = tokenize(record.text);
|
|
27
|
+
this.docLengths[index] = tokens.length;
|
|
28
|
+
total += tokens.length;
|
|
29
|
+
const tf = new Map();
|
|
30
|
+
for (const token of tokens) tf.set(token, (tf.get(token) ?? 0) + 1);
|
|
31
|
+
for (const [token, count] of tf) {
|
|
32
|
+
if (!this.postings.has(token)) this.postings.set(token, new Map());
|
|
33
|
+
this.postings.get(token).set(index, count);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
this.avgLength = records.length === 0 ? 0 : total / records.length;
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
search(query, { topK = 20, minCoverage = 0 } = {}) {
|
|
41
|
+
const tokens = [...new Set(tokenize(query))];
|
|
42
|
+
if (tokens.length === 0) return [];
|
|
43
|
+
const scores = new Map();
|
|
44
|
+
const matched = new Map();
|
|
45
|
+
const n = this.docs.length;
|
|
46
|
+
for (const token of tokens) {
|
|
47
|
+
const posting = this.postings.get(token);
|
|
48
|
+
if (posting === undefined) continue;
|
|
49
|
+
const df = posting.size;
|
|
50
|
+
const idf = Math.log(1 + (n - df + 0.5) / (df + 0.5));
|
|
51
|
+
for (const [index, tf] of posting) {
|
|
52
|
+
const dl = this.docLengths[index] || 1;
|
|
53
|
+
const denom = tf + this.k1 * (1 - this.b + this.b * (dl / (this.avgLength || 1)));
|
|
54
|
+
const score = idf * ((tf * (this.k1 + 1)) / denom);
|
|
55
|
+
scores.set(index, (scores.get(index) ?? 0) + score);
|
|
56
|
+
if (!matched.has(index)) matched.set(index, new Set());
|
|
57
|
+
matched.get(index).add(token);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return [...scores.entries()]
|
|
61
|
+
.map(([index, score]) => ({
|
|
62
|
+
...this.docs[index],
|
|
63
|
+
score,
|
|
64
|
+
coverage: (matched.get(index)?.size ?? 0) / tokens.length,
|
|
65
|
+
}))
|
|
66
|
+
.filter((hit) => hit.coverage >= minCoverage)
|
|
67
|
+
.sort((a, b) => b.score - a.score)
|
|
68
|
+
.slice(0, topK);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
toJSON() {
|
|
72
|
+
return {
|
|
73
|
+
k1: this.k1,
|
|
74
|
+
b: this.b,
|
|
75
|
+
docs: this.docs,
|
|
76
|
+
docLengths: this.docLengths,
|
|
77
|
+
avgLength: this.avgLength,
|
|
78
|
+
postings: [...this.postings.entries()].map(([token, posting]) => [token, [...posting.entries()]]),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static fromJSON(raw) {
|
|
83
|
+
const index = new BM25Index({ k1: raw.k1, b: raw.b });
|
|
84
|
+
index.docs = raw.docs;
|
|
85
|
+
index.docLengths = raw.docLengths;
|
|
86
|
+
index.avgLength = raw.avgLength;
|
|
87
|
+
index.postings = new Map(raw.postings.map(([token, entries]) => [token, new Map(entries)]));
|
|
88
|
+
return index;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function mergeRanges(hits) {
|
|
93
|
+
const sorted = [...hits].sort((a, b) => a.seqStart - b.seqStart);
|
|
94
|
+
const ranges = [];
|
|
95
|
+
for (const hit of sorted) {
|
|
96
|
+
const last = ranges[ranges.length - 1];
|
|
97
|
+
if (last !== undefined && hit.seqStart <= last.seqEnd + 1) {
|
|
98
|
+
last.seqEnd = Math.max(last.seqEnd, hit.seqEnd);
|
|
99
|
+
last.turns += 1;
|
|
100
|
+
last.lastTurn = hit.turn;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
ranges.push({
|
|
104
|
+
seqStart: hit.seqStart,
|
|
105
|
+
seqEnd: hit.seqEnd,
|
|
106
|
+
turns: 1,
|
|
107
|
+
lastTurn: hit.turn,
|
|
108
|
+
score: hit.score,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return ranges;
|
|
112
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confidence calibration utilities for the local facet head.
|
|
3
|
+
* Labels here are weak T-derived labels, so this is provisional calibration,
|
|
4
|
+
* not human-gold calibration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export function reliabilityBins(points, { bins = 10 } = {}) {
|
|
8
|
+
const buckets = Array.from({ length: bins }, (_, index) => ({
|
|
9
|
+
lower: index / bins,
|
|
10
|
+
upper: (index + 1) / bins,
|
|
11
|
+
count: 0,
|
|
12
|
+
confidenceSum: 0,
|
|
13
|
+
correct: 0,
|
|
14
|
+
}));
|
|
15
|
+
for (const point of points) {
|
|
16
|
+
const confidence = Math.max(0, Math.min(1, point.confidence));
|
|
17
|
+
const index = Math.min(bins - 1, Math.floor(confidence * bins));
|
|
18
|
+
const bucket = buckets[index];
|
|
19
|
+
bucket.count += 1;
|
|
20
|
+
bucket.confidenceSum += confidence;
|
|
21
|
+
bucket.correct += point.correct ? 1 : 0;
|
|
22
|
+
}
|
|
23
|
+
return buckets.map((bucket) => ({
|
|
24
|
+
lower: bucket.lower,
|
|
25
|
+
upper: bucket.upper,
|
|
26
|
+
count: bucket.count,
|
|
27
|
+
confidence: bucket.count === 0 ? 0 : bucket.confidenceSum / bucket.count,
|
|
28
|
+
accuracy: bucket.count === 0 ? 0 : bucket.correct / bucket.count,
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function thresholdCurve(points, { steps = 20 } = {}) {
|
|
33
|
+
const curve = [];
|
|
34
|
+
for (let index = 1; index <= steps; index += 1) {
|
|
35
|
+
const threshold = index / steps;
|
|
36
|
+
const accepted = points.filter((point) => point.confidence >= threshold);
|
|
37
|
+
const correct = accepted.filter((point) => point.correct).length;
|
|
38
|
+
curve.push({
|
|
39
|
+
threshold,
|
|
40
|
+
coverage: points.length === 0 ? 0 : accepted.length / points.length,
|
|
41
|
+
precision: accepted.length === 0 ? 0 : correct / accepted.length,
|
|
42
|
+
accepted: accepted.length,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return curve;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function chooseThreshold(points, { targetPrecision = 0.9, minCoverage = 0.1 } = {}) {
|
|
49
|
+
const curve = thresholdCurve(points);
|
|
50
|
+
const eligible = curve
|
|
51
|
+
.filter((item) => item.precision >= targetPrecision && item.coverage >= minCoverage)
|
|
52
|
+
.sort((a, b) => b.coverage - a.coverage);
|
|
53
|
+
return eligible[0] ?? null;
|
|
54
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Shared chatter/register score (not a topic concept). */
|
|
2
|
+
import { extractKeywords } from './bc-recall.mjs';
|
|
3
|
+
|
|
4
|
+
const CJK = /[\u4e00-\u9fff]{2,6}/g;
|
|
5
|
+
const FUNCTION = /[了的着吗呢吧啊]/g;
|
|
6
|
+
|
|
7
|
+
export function buildDf(texts) {
|
|
8
|
+
const df = new Map();
|
|
9
|
+
for (const text of texts) {
|
|
10
|
+
const tokens = [...String(text ?? '').matchAll(CJK)].map((match) => match[0]);
|
|
11
|
+
for (const token of new Set(tokens)) df.set(token, (df.get(token) ?? 0) + 1);
|
|
12
|
+
}
|
|
13
|
+
return df;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function chatterScore(text, df = new Map()) {
|
|
17
|
+
const source = String(text ?? '');
|
|
18
|
+
const tokens = [...source.matchAll(CJK)].map((match) => match[0]);
|
|
19
|
+
const proper = tokens.filter((token) => (df.get(token) ?? 0) <= 3).length;
|
|
20
|
+
const properDensity = tokens.length ? proper / tokens.length : 0;
|
|
21
|
+
const fnDensity = (source.match(FUNCTION) ?? []).length / Math.max(1, source.length);
|
|
22
|
+
const keywordSparse = extractKeywords(source, { limit: 10 }).length < 3 ? 1 : 0;
|
|
23
|
+
const quotes = (source.match(/[「」“”"——]/g) ?? []).length;
|
|
24
|
+
return 0.30 * (source.length < 60 ? 1 : 0)
|
|
25
|
+
+ 0.25 * Math.min(1, properDensity)
|
|
26
|
+
+ 0.20 * Math.min(1, fnDensity * 20)
|
|
27
|
+
+ 0.15 * keywordSparse
|
|
28
|
+
+ 0.10 * (quotes > 0 ? 1 : 0);
|
|
29
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C map file: user term -> B generic concepts (+ optional K / minB / summary).
|
|
3
|
+
*
|
|
4
|
+
* Old:
|
|
5
|
+
* 跑团 => 游戏 / 故事 / 对话
|
|
6
|
+
* New:
|
|
7
|
+
* 跑团 => B:游戏/故事/剧情/对话 | K:Coopanion/Coo | minB:2 | 摘要:...
|
|
8
|
+
*
|
|
9
|
+
* One line per term; `#` starts a comment.
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
export function defaultCMapPath(home = process.env.USERPROFILE ?? process.env.HOME ?? os.homedir()) {
|
|
16
|
+
return path.join(home, '.dsh', 'continuity', 'cmap.md');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function splitTerms(text) {
|
|
20
|
+
return String(text ?? '').split(/[\/,,、]/).map((word) => word.trim()).filter(Boolean);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function defaultParseError({ line, text, message }) {
|
|
24
|
+
try {
|
|
25
|
+
const home = process.env.USERPROFILE ?? process.env.HOME ?? os.homedir();
|
|
26
|
+
const file = path.join(home, '.dsh', 'continuity', 'gate-errors.jsonl');
|
|
27
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
28
|
+
fs.appendFileSync(file, JSON.stringify({
|
|
29
|
+
at: new Date().toISOString(),
|
|
30
|
+
code: 'cmap-parse-failed',
|
|
31
|
+
line,
|
|
32
|
+
text: String(text ?? '').slice(0, 40),
|
|
33
|
+
message,
|
|
34
|
+
}) + '\n', 'utf8');
|
|
35
|
+
} catch { /* parse logging must not break loading */ }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parseCMapText(text, { onError = defaultParseError } = {}) {
|
|
39
|
+
const entries = [];
|
|
40
|
+
const lines = String(text ?? '').split(/\r?\n/);
|
|
41
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
42
|
+
const raw = lines[index];
|
|
43
|
+
const line = raw.replace(/#.*$/, '').trim();
|
|
44
|
+
if (!line) continue;
|
|
45
|
+
const match = line.match(/^(.+?)\s*(?:=>|->|→)\s*(.+)$/);
|
|
46
|
+
if (!match) {
|
|
47
|
+
onError?.({ line: index + 1, text: raw, message: 'missing =>' });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const term = match[1].trim();
|
|
51
|
+
if (!term) {
|
|
52
|
+
onError?.({ line: index + 1, text: raw, message: 'empty term' });
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let body = match[2].trim();
|
|
57
|
+
let summary = '';
|
|
58
|
+
const summaryMarker = body.lastIndexOf('摘要:') >= 0 ? '摘要:' : '摘要:';
|
|
59
|
+
const summaryAt = body.lastIndexOf(summaryMarker);
|
|
60
|
+
if (summaryAt >= 0) {
|
|
61
|
+
summary = body.slice(summaryAt + summaryMarker.length).trim();
|
|
62
|
+
body = body.slice(0, summaryAt).trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let generic = [];
|
|
66
|
+
let keywords = [];
|
|
67
|
+
let minB = 0;
|
|
68
|
+
let limit = 20;
|
|
69
|
+
if (/(?:^|\|)\s*B\s*[::]/i.test(body)) {
|
|
70
|
+
for (const segment of body.split(/\s*\|\s*/)) {
|
|
71
|
+
const item = segment.trim();
|
|
72
|
+
if (!item) continue;
|
|
73
|
+
const pair = item.match(/^([A-Za-z]+)\s*[::]\s*(.*)$/);
|
|
74
|
+
if (!pair) continue;
|
|
75
|
+
const key = pair[1].toLowerCase();
|
|
76
|
+
const value = pair[2].trim();
|
|
77
|
+
if (key === 'b') generic = splitTerms(value);
|
|
78
|
+
else if (key === 'k') keywords = splitTerms(value);
|
|
79
|
+
else if (key === 'minb') {
|
|
80
|
+
const parsed = Number.parseInt(value, 10);
|
|
81
|
+
if (Number.isInteger(parsed) && parsed >= 0) minB = parsed;
|
|
82
|
+
} else if (key === 'limit') {
|
|
83
|
+
const parsed = Number.parseInt(value, 10);
|
|
84
|
+
if (Number.isInteger(parsed) && parsed > 0) limit = parsed;
|
|
85
|
+
}
|
|
86
|
+
// Unknown segments are ignored on purpose.
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
generic = splitTerms(body);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (generic.length === 0) {
|
|
93
|
+
onError?.({ line: index + 1, text: raw, message: 'empty B list' });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
entries.push({ term, generic, keywords, minB, limit, summary });
|
|
97
|
+
}
|
|
98
|
+
return entries;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function loadCMapEntries(file = defaultCMapPath(), { onError = defaultParseError } = {}) {
|
|
102
|
+
try {
|
|
103
|
+
return parseCMapText(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''), { onError });
|
|
104
|
+
} catch {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A-layer concepts (display only).
|
|
3
|
+
*
|
|
4
|
+
* Format: name | description | optional coverage | kind
|
|
5
|
+
* `kind` is `topic` (default) or `workflow`; workflow concepts are excluded
|
|
6
|
+
* from the default A browse but remain valid labels.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
export function defaultConceptsPath(home = process.env.USERPROFILE ?? process.env.HOME ?? os.homedir()) {
|
|
13
|
+
return path.join(home, '.dsh', 'continuity', 'concepts.md');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseConcepts(text) {
|
|
17
|
+
const rows = [];
|
|
18
|
+
for (const raw of String(text ?? '').split(/\r?\n/)) {
|
|
19
|
+
const line = raw.replace(/#.*$/, '').trim();
|
|
20
|
+
if (!line) continue;
|
|
21
|
+
const parts = line.split('|').map((part) => part.trim());
|
|
22
|
+
if (parts.length < 2 || !parts[0] || !parts[1]) continue;
|
|
23
|
+
const coverage = Number.parseInt(parts[2] ?? '', 10);
|
|
24
|
+
const rawKind = String(parts[3] ?? '').trim().toLowerCase().replace(/^kind\s*=\s*/, '');
|
|
25
|
+
rows.push({
|
|
26
|
+
label: parts[0],
|
|
27
|
+
description: parts[1],
|
|
28
|
+
coverage: Number.isInteger(coverage) ? coverage : null,
|
|
29
|
+
kind: rawKind === 'workflow' ? 'workflow' : 'topic',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return rows;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function loadConcepts(file = defaultConceptsPath()) {
|
|
36
|
+
try {
|
|
37
|
+
return parseConcepts(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coreference output -> EventModel slots and referent candidates.
|
|
3
|
+
* Input clusters are expected to be normalizeCoref() output.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function isLikelyName(text) {
|
|
7
|
+
return /^[A-Z][A-Za-z0-9'_-]{1,30}$/u.test(String(text ?? '').trim());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function corefReferents(clusters = []) {
|
|
11
|
+
const referents = [];
|
|
12
|
+
for (const cluster of clusters) {
|
|
13
|
+
const canonical = cluster.canonical ?? cluster.mentions?.find((m) => m.isCanonical)?.text ?? null;
|
|
14
|
+
if (!canonical) continue;
|
|
15
|
+
referents.push({
|
|
16
|
+
clusterId: cluster.clusterId ?? null,
|
|
17
|
+
canonical,
|
|
18
|
+
mentions: (cluster.mentions ?? []).map((mention) => mention.text),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return referents;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function applyCorefToSlots(slots = {}, clusters = []) {
|
|
25
|
+
const next = {
|
|
26
|
+
characters: [...(slots.characters ?? [])],
|
|
27
|
+
location: slots.location ?? null,
|
|
28
|
+
goal: slots.goal ?? null,
|
|
29
|
+
objects: [...(slots.objects ?? [])],
|
|
30
|
+
};
|
|
31
|
+
const referents = corefReferents(clusters);
|
|
32
|
+
for (const referent of referents) {
|
|
33
|
+
if (isLikelyName(referent.canonical)) {
|
|
34
|
+
if (!next.characters.includes(referent.canonical)) next.characters.push(referent.canonical);
|
|
35
|
+
} else if (!next.objects.includes(referent.canonical)) {
|
|
36
|
+
next.objects.push(referent.canonical);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { slots: next, referents };
|
|
40
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event-bound alias ledger.
|
|
3
|
+
*
|
|
4
|
+
* A term becomes an alias only when:
|
|
5
|
+
* - it is not already a known alias
|
|
6
|
+
* - the user/Coo has used it at least twice
|
|
7
|
+
* - it appears in at least two distinct events
|
|
8
|
+
* - its role is stable
|
|
9
|
+
* - it maps to an existing facet
|
|
10
|
+
*
|
|
11
|
+
* Assistant-only mentions never promote. If no target facet exists, the term
|
|
12
|
+
* is an accommodation candidate (new concept), not an alias.
|
|
13
|
+
*/
|
|
14
|
+
import { DEFAULT_ALIASES } from '../facets/schema.mjs';
|
|
15
|
+
|
|
16
|
+
function pickDominant(counts) {
|
|
17
|
+
const entries = Object.entries(counts).sort((a, b) => b[1] - a[1]);
|
|
18
|
+
if (entries.length === 0) return null;
|
|
19
|
+
const total = entries.reduce((sum, [, count]) => sum + count, 0);
|
|
20
|
+
return { value: entries[0][0], count: entries[0][1], total, ratio: entries[0][1] / total };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class EventAliasLedger {
|
|
24
|
+
constructor({ minUser = 2, minEvents = 2, terms = new Map() } = {}) {
|
|
25
|
+
this.minUser = minUser;
|
|
26
|
+
this.minEvents = minEvents;
|
|
27
|
+
this.terms = terms;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
observe({ term, source = 'user', eventId, role = null, target = null, nowTurn = 0, kind = 'mention' }) {
|
|
31
|
+
const clean = String(term ?? '').trim();
|
|
32
|
+
if (clean.length === 0) return { action: 'none', reason: 'no-term' };
|
|
33
|
+
if (DEFAULT_ALIASES[clean] !== undefined) return { action: 'none', reason: 'known-alias' };
|
|
34
|
+
|
|
35
|
+
const record = this.terms.get(clean) ?? {
|
|
36
|
+
term: clean,
|
|
37
|
+
user: 0,
|
|
38
|
+
assistant: 0,
|
|
39
|
+
events: new Set(),
|
|
40
|
+
roles: {},
|
|
41
|
+
targets: {},
|
|
42
|
+
firstSeen: nowTurn,
|
|
43
|
+
lastSeen: nowTurn,
|
|
44
|
+
strong: false,
|
|
45
|
+
proposed: false,
|
|
46
|
+
accommodated: false,
|
|
47
|
+
};
|
|
48
|
+
if (source === 'user' || (source === 'coo' || source === 'peer')) record.user += 1;
|
|
49
|
+
else record.assistant += 1;
|
|
50
|
+
record.events.add(eventId);
|
|
51
|
+
if (role !== null) record.roles[role] = (record.roles[role] ?? 0) + 1;
|
|
52
|
+
if (target !== null) record.targets[target] = (record.targets[target] ?? 0) + 1;
|
|
53
|
+
if (kind === 'definition' || kind === 'quoted') record.strong = true;
|
|
54
|
+
record.lastSeen = nowTurn;
|
|
55
|
+
this.terms.set(clean, record);
|
|
56
|
+
|
|
57
|
+
if (record.proposed) return { action: 'none', reason: 'already-proposed', record };
|
|
58
|
+
if (record.accommodated) return { action: 'none', reason: 'already-accommodated', record };
|
|
59
|
+
if (record.user < this.minUser) return { action: 'none', reason: 'need-user-repeat', record };
|
|
60
|
+
if (record.events.size < this.minEvents) return { action: 'none', reason: 'need-event-repeat', record };
|
|
61
|
+
|
|
62
|
+
const dominantRole = pickDominant(record.roles);
|
|
63
|
+
if (dominantRole !== null && dominantRole.ratio < 0.8) return { action: 'none', reason: 'role-unstable', record };
|
|
64
|
+
const dominantTarget = pickDominant(record.targets);
|
|
65
|
+
if (dominantTarget === null) {
|
|
66
|
+
record.accommodated = true;
|
|
67
|
+
return {
|
|
68
|
+
action: 'accommodate',
|
|
69
|
+
reason: 'no-target-facet',
|
|
70
|
+
term: clean,
|
|
71
|
+
text: `[新概念?] ${clean}(暂无 facet 归属)`,
|
|
72
|
+
record,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
record.proposed = true;
|
|
77
|
+
return {
|
|
78
|
+
action: 'propose',
|
|
79
|
+
reason: 'cross-event-assimilation',
|
|
80
|
+
term: clean,
|
|
81
|
+
role: dominantRole?.value ?? null,
|
|
82
|
+
target: dominantTarget.value,
|
|
83
|
+
text: `[新别名?] ${clean} → ${dominantTarget.value}`,
|
|
84
|
+
record,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only event log with hash chaining.
|
|
3
|
+
* The log is a normalized projection of an immutable source; it is never
|
|
4
|
+
* rewritten in place after the initial build.
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
function canonical(value) {
|
|
11
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
12
|
+
if (value !== null && typeof value === 'object') {
|
|
13
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
|
|
14
|
+
}
|
|
15
|
+
return JSON.stringify(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sha256(text) {
|
|
19
|
+
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function eventHash(record, prevHash) {
|
|
23
|
+
return sha256(`${prevHash}\n${canonical(record)}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildEventLog(rows) {
|
|
27
|
+
const records = [];
|
|
28
|
+
let prevHash = 'GENESIS';
|
|
29
|
+
for (const row of [...rows].sort((a, b) => (a.turn - b.turn) || (a.seqStart - b.seqStart))) {
|
|
30
|
+
const body = {
|
|
31
|
+
turn: row.turn,
|
|
32
|
+
seqStart: row.seqStart,
|
|
33
|
+
seqEnd: row.seqEnd,
|
|
34
|
+
text: String(row.text ?? ''),
|
|
35
|
+
facets: row.facets ?? {},
|
|
36
|
+
labelSource: row.labelSource ?? 'unknown',
|
|
37
|
+
};
|
|
38
|
+
const hash = eventHash(body, prevHash);
|
|
39
|
+
records.push({ ...body, prevHash, hash });
|
|
40
|
+
prevHash = hash;
|
|
41
|
+
}
|
|
42
|
+
return records;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function verifyEventLog(records) {
|
|
46
|
+
let prevHash = 'GENESIS';
|
|
47
|
+
const errors = [];
|
|
48
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
49
|
+
const record = records[index];
|
|
50
|
+
if (record.prevHash !== prevHash) errors.push({ index, reason: 'prev-hash-mismatch' });
|
|
51
|
+
const expected = eventHash({
|
|
52
|
+
turn: record.turn,
|
|
53
|
+
seqStart: record.seqStart,
|
|
54
|
+
seqEnd: record.seqEnd,
|
|
55
|
+
text: record.text,
|
|
56
|
+
facets: record.facets,
|
|
57
|
+
labelSource: record.labelSource,
|
|
58
|
+
}, prevHash);
|
|
59
|
+
if (expected !== record.hash) errors.push({ index, reason: 'hash-mismatch' });
|
|
60
|
+
prevHash = record.hash;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
ok: errors.length === 0,
|
|
64
|
+
count: records.length,
|
|
65
|
+
rootHash: records.length === 0 ? 'GENESIS' : records[records.length - 1].hash,
|
|
66
|
+
errors,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function writeEventLog(file, records) {
|
|
71
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
72
|
+
const text = records.map((record) => JSON.stringify(record)).join('\n') + '\n';
|
|
73
|
+
fs.writeFileSync(file, text, 'utf8');
|
|
74
|
+
return { file, bytes: Buffer.byteLength(text), ...verifyEventLog(records) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function readEventLog(file) {
|
|
78
|
+
return fs.readFileSync(file, 'utf8').split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function appendEvent(file, record) {
|
|
82
|
+
const records = fs.existsSync(file) ? readEventLog(file) : [];
|
|
83
|
+
const prevHash = records.length === 0 ? 'GENESIS' : records[records.length - 1].hash;
|
|
84
|
+
const body = {
|
|
85
|
+
turn: record.turn,
|
|
86
|
+
seqStart: record.seqStart,
|
|
87
|
+
seqEnd: record.seqEnd,
|
|
88
|
+
text: String(record.text ?? ''),
|
|
89
|
+
facets: record.facets ?? {},
|
|
90
|
+
labelSource: record.labelSource ?? 'unknown',
|
|
91
|
+
};
|
|
92
|
+
const next = { ...body, prevHash, hash: eventHash(body, prevHash) };
|
|
93
|
+
fs.appendFileSync(file, JSON.stringify(next) + '\n', 'utf8');
|
|
94
|
+
return next;
|
|
95
|
+
}
|