@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,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixed facet schema (v1).
|
|
3
|
+
*
|
|
4
|
+
* The schema is deliberately small and closed. The extractor may only emit
|
|
5
|
+
* values listed here; an unknown concept belongs in `unknown`, not in a new
|
|
6
|
+
* facet value. Schema evolution is a separate, versioned operation.
|
|
7
|
+
*/
|
|
8
|
+
export const SCHEMA_VERSION = 1;
|
|
9
|
+
|
|
10
|
+
export const SLOTS = Object.freeze([
|
|
11
|
+
'activity',
|
|
12
|
+
'interaction',
|
|
13
|
+
'random',
|
|
14
|
+
'participants',
|
|
15
|
+
'medium',
|
|
16
|
+
'topic',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export const VALUES = Object.freeze({
|
|
20
|
+
activity: ['跑团', '开发', '调试', '测试', '文档', '对话', '其他'],
|
|
21
|
+
interaction: ['回合制', '指令式', '自由对话', '无'],
|
|
22
|
+
random: ['有', '无'],
|
|
23
|
+
participants: ['单人', '多人', '协作', '对抗', '无'],
|
|
24
|
+
medium: ['文字', '代码', '图像', '语音', '无'],
|
|
25
|
+
topic: ['剧情', '技术', '设备', '记忆', '规则', '其他'],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/** Query aliases are cached expansions, not identities. */
|
|
29
|
+
export const DEFAULT_ALIASES = Object.freeze({
|
|
30
|
+
// Aliases are retrieval anchors, not full identity bundles. Keep them
|
|
31
|
+
// minimal: a maximal bundle makes every technical turn match 开发 and 调试.
|
|
32
|
+
跑团: { activity: '跑团' },
|
|
33
|
+
开发: { activity: '开发' },
|
|
34
|
+
调试: { activity: '调试' },
|
|
35
|
+
测试: { activity: '测试' },
|
|
36
|
+
记忆: { topic: '记忆' },
|
|
37
|
+
规则: { topic: '规则' },
|
|
38
|
+
图像: { topic: '设备', medium: '图像' },
|
|
39
|
+
技术: { topic: '技术' },
|
|
40
|
+
TRPG: { activity: '跑团' },
|
|
41
|
+
掷骰子: { activity: '跑团' },
|
|
42
|
+
角色扮演: { activity: '跑团' },
|
|
43
|
+
头像: { topic: '设备', medium: '图像' },
|
|
44
|
+
立绘: { topic: '设备', medium: '图像' },
|
|
45
|
+
记忆宫殿: { topic: '记忆' },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export function facetKey(slot, value) {
|
|
49
|
+
return `${slot}:${value}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isKnownFacet(slot, value) {
|
|
53
|
+
return SLOTS.includes(slot) && Array.isArray(VALUES[slot]) && VALUES[slot].includes(value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function normalizeFacets(input) {
|
|
57
|
+
const out = {};
|
|
58
|
+
if (Array.isArray(input)) {
|
|
59
|
+
for (const item of input) {
|
|
60
|
+
const [slot, ...rest] = String(item).split(':');
|
|
61
|
+
const value = rest.join(':').trim();
|
|
62
|
+
if (isKnownFacet(slot, value)) out[slot] = value;
|
|
63
|
+
}
|
|
64
|
+
} else if (input !== null && typeof input === 'object') {
|
|
65
|
+
for (const slot of SLOTS) {
|
|
66
|
+
const value = input[slot];
|
|
67
|
+
if (isKnownFacet(slot, value)) out[slot] = value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function aliasFacets(alias) {
|
|
74
|
+
return normalizeFacets(DEFAULT_ALIASES[String(alias ?? '').trim()] ?? {});
|
|
75
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-written seed phrases for cold-start facet extraction.
|
|
3
|
+
* These are not labels from history; they are a prior over the fixed schema.
|
|
4
|
+
*/
|
|
5
|
+
import { SLOTS, VALUES } from './schema.mjs';
|
|
6
|
+
|
|
7
|
+
export const SEED_EXAMPLES = Object.freeze({
|
|
8
|
+
activity: {
|
|
9
|
+
跑团: ['我们掷骰决定行动,角色在场景里探索。', '跑团开始,我先描述角色要做什么。'],
|
|
10
|
+
开发: ['写代码实现这个插件功能。', '修改模块并打包发布。'],
|
|
11
|
+
调试: ['看报错日志,定位进程和配置问题。', '这个 bug 复现了,先查调用栈。'],
|
|
12
|
+
测试: ['跑测试用例,检查回归。', '新增断言并验证结果。'],
|
|
13
|
+
文档: ['写安装说明和交接文档。', '整理报告,记录结论。'],
|
|
14
|
+
对话: ['我们聊聊这个东西的意义。', '你怎么看这件事?'],
|
|
15
|
+
其他: ['处理一个不属于上述类别的杂项。'],
|
|
16
|
+
},
|
|
17
|
+
interaction: {
|
|
18
|
+
回合制: ['一轮一轮来,等我描述完再继续。', '轮到你了,下一步怎么行动?'],
|
|
19
|
+
指令式: ['执行这个命令,把结果给我。', '按步骤完成操作。'],
|
|
20
|
+
自由对话: ['随便聊聊,不用按流程。', '我们自由讨论一下。'],
|
|
21
|
+
无: ['没有明确的交互形式。'],
|
|
22
|
+
},
|
|
23
|
+
participants: {
|
|
24
|
+
单人: ['我一个人在检查这个文件。', '我自己先想一下。'],
|
|
25
|
+
多人: ['几个人一起讨论方案。', '多人协作完成。'],
|
|
26
|
+
协作: ['我们一起把这件事做完。', '你和我配合处理。'],
|
|
27
|
+
对抗: ['双方对抗,各自争取胜利。', '对手和我对立。'],
|
|
28
|
+
无: ['没有参与者信息。'],
|
|
29
|
+
},
|
|
30
|
+
medium: {
|
|
31
|
+
文字: ['用中文写一段说明。', '这段是纯文本对话。'],
|
|
32
|
+
代码: ['这里是 TypeScript 代码和命令行。', '```js\nconst x = 1\n```'],
|
|
33
|
+
图像: [' 看这张图。', '生成一张头像和对照图。'],
|
|
34
|
+
语音: ['听这段语音。', '用 TTS 读出来。'],
|
|
35
|
+
无: ['没有媒介。'],
|
|
36
|
+
},
|
|
37
|
+
topic: {
|
|
38
|
+
剧情: ['故事里的角色走进灯室。', '这一段是剧情推进。'],
|
|
39
|
+
技术: ['插件运行在 Node 上,改 engine 逻辑。', '接口和实现细节。'],
|
|
40
|
+
设备: ['手机、显卡和性能对比。', '这台设备的配置。'],
|
|
41
|
+
记忆: ['记忆宫殿、连续性和身份。', '我们之前聊过的那段记忆。'],
|
|
42
|
+
规则: ['规则要求每轮写 TAG。', '政策和闸的判定。'],
|
|
43
|
+
其他: ['其他话题。'],
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export function seedRows() {
|
|
48
|
+
const rows = [];
|
|
49
|
+
for (const slot of SLOTS) {
|
|
50
|
+
if (slot === 'random') continue;
|
|
51
|
+
for (const value of VALUES[slot]) {
|
|
52
|
+
for (const text of SEED_EXAMPLES[slot]?.[value] ?? []) {
|
|
53
|
+
rows.push({ text, facets: { [slot]: value } });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return rows;
|
|
58
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Facet relevance scoring and temporal sensitivity.
|
|
3
|
+
*
|
|
4
|
+
* No LLM. A turn/topic score is an IDF-weighted cosine with a coverage penalty;
|
|
5
|
+
* the temporal series subtracts the corpus baseline so unrelated topics stay
|
|
6
|
+
* near zero.
|
|
7
|
+
*/
|
|
8
|
+
import { normalizeFacets } from './schema.mjs';
|
|
9
|
+
|
|
10
|
+
function vectorize(facets) {
|
|
11
|
+
return Object.entries(normalizeFacets(facets ?? {}));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function facetScore(facets, queryFacets, idf) {
|
|
15
|
+
const x = vectorize(facets);
|
|
16
|
+
const q = vectorize(queryFacets);
|
|
17
|
+
if (x.length === 0 || q.length === 0) return { score: 0, coverage: 0, hits: 0 };
|
|
18
|
+
|
|
19
|
+
const weight = (slot, value) => idf?.get?.(`${slot}:${value}`) ?? 1;
|
|
20
|
+
const xMap = new Map(x);
|
|
21
|
+
let dot = 0;
|
|
22
|
+
let hits = 0;
|
|
23
|
+
let qNorm = 0;
|
|
24
|
+
let xNorm = 0;
|
|
25
|
+
for (const [slot, value] of q) {
|
|
26
|
+
const w = weight(slot, value);
|
|
27
|
+
qNorm += w * w;
|
|
28
|
+
if (xMap.get(slot) === value) {
|
|
29
|
+
dot += w * w;
|
|
30
|
+
hits += 1;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (const [slot, value] of x) {
|
|
34
|
+
const w = weight(slot, value);
|
|
35
|
+
xNorm += w * w;
|
|
36
|
+
}
|
|
37
|
+
if (qNorm === 0 || xNorm === 0) return { score: 0, coverage: 0, hits: 0 };
|
|
38
|
+
const cosine = dot / Math.sqrt(qNorm * xNorm);
|
|
39
|
+
const coverage = hits / q.length;
|
|
40
|
+
return { score: cosine * coverage * coverage, coverage, hits };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function sensitivitySeries(entries, queryFacets, {
|
|
44
|
+
idf = null,
|
|
45
|
+
window = 2,
|
|
46
|
+
weights = [0.5, 0.3, 0.2],
|
|
47
|
+
} = {}) {
|
|
48
|
+
const raw = entries.map((entry) => ({
|
|
49
|
+
turn: entry.turn,
|
|
50
|
+
seqStart: entry.seqStart,
|
|
51
|
+
seqEnd: entry.seqEnd,
|
|
52
|
+
time: entry.time ?? null,
|
|
53
|
+
...facetScore(entry.facets, queryFacets, idf),
|
|
54
|
+
}));
|
|
55
|
+
const baseline = raw.length === 0 ? 0 : raw.reduce((sum, row) => sum + row.score, 0) / raw.length;
|
|
56
|
+
const positive = raw.map((row) => Math.max(0, row.score - baseline));
|
|
57
|
+
const smoothed = positive.map((value, index) => {
|
|
58
|
+
let sum = 0;
|
|
59
|
+
let weightSum = 0;
|
|
60
|
+
for (let k = 0; k <= Math.min(window, index); k += 1) {
|
|
61
|
+
const weight = weights[k] ?? 0;
|
|
62
|
+
sum += positive[index - k] * weight;
|
|
63
|
+
weightSum += weight;
|
|
64
|
+
}
|
|
65
|
+
return weightSum === 0 ? 0 : sum / weightSum;
|
|
66
|
+
});
|
|
67
|
+
return raw.map((row, index) => ({
|
|
68
|
+
...row,
|
|
69
|
+
baseline,
|
|
70
|
+
positive: positive[index],
|
|
71
|
+
smoothed: smoothed[index],
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function foldPriority(entries, queryFacets, {
|
|
76
|
+
idf = null,
|
|
77
|
+
recentTurns = 20,
|
|
78
|
+
protectScore = 0.5,
|
|
79
|
+
} = {}) {
|
|
80
|
+
if (entries.length === 0) return [];
|
|
81
|
+
const latestTurn = Math.max(...entries.map((entry) => entry.turn));
|
|
82
|
+
return entries
|
|
83
|
+
.filter((entry) => entry.turn <= latestTurn - recentTurns)
|
|
84
|
+
.map((entry) => {
|
|
85
|
+
const score = facetScore(entry.facets, queryFacets, idf).score;
|
|
86
|
+
const age = latestTurn - entry.turn;
|
|
87
|
+
return {
|
|
88
|
+
turn: entry.turn,
|
|
89
|
+
seqStart: entry.seqStart,
|
|
90
|
+
seqEnd: entry.seqEnd,
|
|
91
|
+
score,
|
|
92
|
+
protected: score >= protectScore,
|
|
93
|
+
priority: (1 - Math.min(1, score)) * age,
|
|
94
|
+
};
|
|
95
|
+
})
|
|
96
|
+
.filter((entry) => !entry.protected)
|
|
97
|
+
.sort((a, b) => b.priority - a.priority);
|
|
98
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main-thread client for the facet worker.
|
|
3
|
+
*/
|
|
4
|
+
import { Worker } from 'node:worker_threads';
|
|
5
|
+
|
|
6
|
+
export function createFacetService({ modelPath, timeoutMs = 5000 } = {}) {
|
|
7
|
+
if (typeof modelPath !== 'string' || modelPath.length === 0) {
|
|
8
|
+
throw new Error('facet service: modelPath is required');
|
|
9
|
+
}
|
|
10
|
+
const worker = new Worker(new URL('./worker.mjs', import.meta.url), {
|
|
11
|
+
workerData: { modelPath },
|
|
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('facet 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('facet service: closed'));
|
|
48
|
+
}
|
|
49
|
+
pending.clear();
|
|
50
|
+
await worker.terminate();
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence for facet MVP: index + cursor + residual.
|
|
3
|
+
* JSON files are read/written atomically from the CLI; the pure index remains
|
|
4
|
+
* the source of truth for querying.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
function atomicWrite(file, text) {
|
|
10
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
11
|
+
const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
12
|
+
fs.writeFileSync(temp, text, 'utf8');
|
|
13
|
+
fs.renameSync(temp, file);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function saveIndex(file, index) {
|
|
17
|
+
atomicWrite(file, `${JSON.stringify({
|
|
18
|
+
total: index.total,
|
|
19
|
+
idf: [...index.idf.entries()],
|
|
20
|
+
entries: index.entries,
|
|
21
|
+
}, null, 2)}\n`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function loadIndex(file) {
|
|
25
|
+
const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
26
|
+
return {
|
|
27
|
+
total: Number(raw.total) || 0,
|
|
28
|
+
idf: new Map(Array.isArray(raw.idf) ? raw.idf : []),
|
|
29
|
+
entries: Array.isArray(raw.entries) ? raw.entries : [],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function saveCursor(file, cursor) {
|
|
34
|
+
atomicWrite(file, `${JSON.stringify(cursor, null, 2)}\n`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function loadCursor(file) {
|
|
38
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
|
|
39
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Facet worker: loads a serialized local small model and answers extract
|
|
3
|
+
* requests off the host main thread.
|
|
4
|
+
*/
|
|
5
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import { deserializeClassifier } from './classifier.mjs';
|
|
8
|
+
import { hybridExtractor } from './hybrid.mjs';
|
|
9
|
+
|
|
10
|
+
const raw = JSON.parse(fs.readFileSync(workerData.modelPath, 'utf8'));
|
|
11
|
+
const model = deserializeClassifier(raw);
|
|
12
|
+
const extract = hybridExtractor(model);
|
|
13
|
+
|
|
14
|
+
parentPort.on('message', (message) => {
|
|
15
|
+
const id = message?.id;
|
|
16
|
+
try {
|
|
17
|
+
const text = String(message?.text ?? '');
|
|
18
|
+
const result = extract(text);
|
|
19
|
+
parentPort.postMessage({ id, ok: true, result });
|
|
20
|
+
} catch (error) {
|
|
21
|
+
parentPort.postMessage({ id, ok: false, error: String(error?.message ?? error) });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Folded range sidecar (~/.dsh/continuity/folds.json).
|
|
3
|
+
*
|
|
4
|
+
* The engine records every compaction it performs: the original shadowed range
|
|
5
|
+
* and the gist summary seq. find/load use this local record to distinguish
|
|
6
|
+
* visible seqs from folded ones without querying the session log.
|
|
7
|
+
*/
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
11
|
+
|
|
12
|
+
let writeQueue = Promise.resolve();
|
|
13
|
+
|
|
14
|
+
export function continuityHome() {
|
|
15
|
+
const override = process.env.DSH_CONTINUITY_HOME;
|
|
16
|
+
return override !== undefined && override.length > 0
|
|
17
|
+
? override
|
|
18
|
+
: join(homedir(), '.dsh', 'continuity');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function foldsFile() {
|
|
22
|
+
return join(continuityHome(), 'folds.json');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeFold(value) {
|
|
26
|
+
const from = Number(value?.from);
|
|
27
|
+
const to = Number(value?.to);
|
|
28
|
+
const gistSeq = Number(value?.gistSeq);
|
|
29
|
+
if (!Number.isInteger(from) || !Number.isInteger(to) || from > to) return null;
|
|
30
|
+
const count = Number(value?.count);
|
|
31
|
+
return {
|
|
32
|
+
from,
|
|
33
|
+
to,
|
|
34
|
+
gistSeq: Number.isInteger(gistSeq) ? gistSeq : null,
|
|
35
|
+
...(Number.isInteger(count) && count >= 0 ? { count } : {}),
|
|
36
|
+
...(typeof value?.compactionId === 'string' && value.compactionId ? { compactionId: value.compactionId } : {}),
|
|
37
|
+
at: typeof value?.at === 'string' ? value.at : new Date().toISOString(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @returns {Promise<{folds: object[], missing: boolean, corrupt: boolean}>} */
|
|
42
|
+
export async function readFolds(sessionId) {
|
|
43
|
+
const id = String(sessionId);
|
|
44
|
+
try {
|
|
45
|
+
const raw = await readFile(foldsFile(), 'utf8');
|
|
46
|
+
const parsed = JSON.parse(raw);
|
|
47
|
+
const session = parsed?.sessions?.[id];
|
|
48
|
+
const folds = Array.isArray(session?.folds)
|
|
49
|
+
? session.folds.map(normalizeFold).filter(Boolean)
|
|
50
|
+
: [];
|
|
51
|
+
return { folds, missing: false, corrupt: false };
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error?.code === 'ENOENT') return { folds: [], missing: true, corrupt: false };
|
|
54
|
+
return { folds: [], missing: false, corrupt: true };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Append one fold record for a session; serialized and atomic. */
|
|
59
|
+
export async function recordFold(sessionId, { from, to, gistSeq, count, compactionId } = {}) {
|
|
60
|
+
const id = String(sessionId);
|
|
61
|
+
const fold = normalizeFold({ from, to, gistSeq, count, compactionId });
|
|
62
|
+
if (fold === null) return null;
|
|
63
|
+
const operation = writeQueue.then(async () => {
|
|
64
|
+
let state = { version: 1, sessions: {} };
|
|
65
|
+
try {
|
|
66
|
+
const raw = await readFile(foldsFile(), 'utf8');
|
|
67
|
+
const parsed = JSON.parse(raw);
|
|
68
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
69
|
+
state = {
|
|
70
|
+
version: 1,
|
|
71
|
+
sessions: parsed.sessions !== null && typeof parsed.sessions === 'object' && !Array.isArray(parsed.sessions)
|
|
72
|
+
? parsed.sessions
|
|
73
|
+
: {},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
} catch { /* missing/corrupt: start clean */ }
|
|
77
|
+
const session = state.sessions[id] !== null && typeof state.sessions[id] === 'object'
|
|
78
|
+
&& !Array.isArray(state.sessions[id])
|
|
79
|
+
? state.sessions[id]
|
|
80
|
+
: { folds: [] };
|
|
81
|
+
const folds = Array.isArray(session.folds) ? session.folds : [];
|
|
82
|
+
folds.push(fold);
|
|
83
|
+
state.sessions[id] = { folds, updatedAt: new Date().toISOString() };
|
|
84
|
+
const dir = continuityHome();
|
|
85
|
+
await mkdir(dir, { recursive: true });
|
|
86
|
+
const file = foldsFile();
|
|
87
|
+
const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
88
|
+
try {
|
|
89
|
+
await writeFile(temp, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
90
|
+
await rename(temp, file);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
try { await unlink(temp); } catch { /* best effort */ }
|
|
93
|
+
throw new Error(`continuity: cannot write ${file}: ${error?.message ?? error}`);
|
|
94
|
+
}
|
|
95
|
+
return fold;
|
|
96
|
+
});
|
|
97
|
+
writeQueue = operation.catch(() => {});
|
|
98
|
+
return operation;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Latest fold containing seq, or null. */
|
|
102
|
+
export function foldForSeq(folds, seq) {
|
|
103
|
+
const number = Number(seq);
|
|
104
|
+
for (let index = (folds?.length ?? 0) - 1; index >= 0; index -= 1) {
|
|
105
|
+
const fold = folds[index];
|
|
106
|
+
if (number >= fold.from && number <= fold.to) return fold;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
package/src/groups.mjs
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 归拢探测(纯函数):从标签关键词与 multi-hit 日志里提出“该有父”的候选。
|
|
3
|
+
* 只提议,不建组;名字优先取查询原话,其次取共享词。
|
|
4
|
+
*/
|
|
5
|
+
const SCAFFOLD = new Set(["T", "标签", "记录", "线头", "thread", "tag"]);
|
|
6
|
+
|
|
7
|
+
export function normalizeTags(tags) {
|
|
8
|
+
return Object.entries(tags?.tags ?? {}).map(([id, t]) => ({
|
|
9
|
+
id,
|
|
10
|
+
alias: t?.alias ?? id,
|
|
11
|
+
label: t?.label ?? "",
|
|
12
|
+
keywords: Array.isArray(t?.keywords) ? t.keywords.filter((k) => k && !SCAFFOLD.has(k)) : [],
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function jaccard(a, b) {
|
|
17
|
+
const A = new Set(a), B = new Set(b);
|
|
18
|
+
if (A.size === 0 || B.size === 0) return 0;
|
|
19
|
+
let inter = 0;
|
|
20
|
+
for (const x of A) if (B.has(x)) inter++;
|
|
21
|
+
return inter / (A.size + B.size - inter);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 返回提议数组;每条 {kind, members, score, name}。 */
|
|
25
|
+
export function detectGroups(tags, routeLines = [], options = {}) {
|
|
26
|
+
const minJaccard = Number(options.minJaccard ?? 0.5);
|
|
27
|
+
const minPairHits = Number(options.minPairHits ?? 2);
|
|
28
|
+
const rawEntries = normalizeTags(tags);
|
|
29
|
+
const entries = options.groups ? collapseGroups(rawEntries, options.groups) : rawEntries;
|
|
30
|
+
const proposals = [];
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < entries.length; i++) {
|
|
33
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
34
|
+
const a = entries[i], b = entries[j];
|
|
35
|
+
const score = jaccard(a.keywords, b.keywords);
|
|
36
|
+
if (score < minJaccard) continue;
|
|
37
|
+
const shared = a.keywords.filter((k) => b.keywords.includes(k));
|
|
38
|
+
proposals.push({ kind: "keyword-overlap", members: [a.alias, b.alias], score: Number(score.toFixed(2)), name: shared[0] ?? "" });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const multi = routeLines
|
|
43
|
+
.map((line) => { try { return JSON.parse(line); } catch { return null; } })
|
|
44
|
+
.filter((x) => x && typeof x.conclusion === "string" && x.conclusion.startsWith("multi:"))
|
|
45
|
+
.map((x) => ({ head: x.head ?? "", tags: x.conclusion.slice(6).split(",").filter(Boolean) }));
|
|
46
|
+
const pair = new Map();
|
|
47
|
+
for (const m of multi) {
|
|
48
|
+
for (let i = 0; i < m.tags.length; i++) {
|
|
49
|
+
for (let j = i + 1; j < m.tags.length; j++) {
|
|
50
|
+
const key = [m.tags[i], m.tags[j]].sort().join("|");
|
|
51
|
+
const cur = pair.get(key) ?? { count: 0, head: m.head };
|
|
52
|
+
cur.count++;
|
|
53
|
+
pair.set(key, cur);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const [key, v] of pair) {
|
|
58
|
+
if (v.count < minPairHits) continue;
|
|
59
|
+
// 父名优先取成员关键词的交集;没有就留空(由实例或人提炼),原句只作凭据。
|
|
60
|
+
const ids = key.split("|");
|
|
61
|
+
const kws = ids.map((id) => entries.find((e) => e.alias === id || e.id === id)?.keywords ?? []);
|
|
62
|
+
const shared = kws.length >= 2 ? kws[0].filter((k) => kws[1].includes(k)) : [];
|
|
63
|
+
proposals.push({ kind: "query-ambiguity", members: ids, score: v.count, name: shared[0] ?? "", evidence: String(v.head).slice(0, 60) });
|
|
64
|
+
}
|
|
65
|
+
return proposals;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 按 groups 把成员标签折叠成一个节点(组名),避免同一主题的多个阶段被当成独立标签。
|
|
71
|
+
* groups 形如 { "标签治理": { members: ["T16","T21"] } }。
|
|
72
|
+
*/
|
|
73
|
+
export function collapseGroups(entries, groups) {
|
|
74
|
+
const byMember = new Map();
|
|
75
|
+
for (const [name, g] of Object.entries(groups ?? {})) {
|
|
76
|
+
for (const m of g?.members ?? []) byMember.set(m, name);
|
|
77
|
+
}
|
|
78
|
+
const collapsed = new Map();
|
|
79
|
+
for (const e of entries) {
|
|
80
|
+
const key = byMember.get(e.alias) ?? byMember.get(e.id) ?? e.alias ?? e.id;
|
|
81
|
+
const cur = collapsed.get(key) ?? { id: key, alias: key, label: groups?.[key]?.label ?? e.label, keywords: [] };
|
|
82
|
+
cur.keywords.push(...e.keywords);
|
|
83
|
+
collapsed.set(key, cur);
|
|
84
|
+
}
|
|
85
|
+
return [...collapsed.values()];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 把组名递归展开成标签 id 集合。members 里可以嵌套组名。
|
|
91
|
+
* 环引用直接抛错(A⊃B⊃A 是数据错误,不该静默)。
|
|
92
|
+
*/
|
|
93
|
+
export function expandGroup(groups, name, seen = new Set()) {
|
|
94
|
+
if (!groups || typeof groups !== "object") return new Set();
|
|
95
|
+
if (!groups[name]) throw new Error("未知组: " + name);
|
|
96
|
+
if (seen.has(name)) throw new Error("组循环引用: " + [...seen, name].join(" -> "));
|
|
97
|
+
const next = new Set(seen);
|
|
98
|
+
next.add(name);
|
|
99
|
+
const out = new Set();
|
|
100
|
+
for (const member of groups[name].members ?? []) {
|
|
101
|
+
if (groups[member]) {
|
|
102
|
+
for (const id of expandGroup(groups, member, next)) out.add(id);
|
|
103
|
+
} else {
|
|
104
|
+
out.add(member);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 从多命中证据里提炼父名:取这些句子里反复出现、但不在两个标签关键词里的 2–4 字词。
|
|
113
|
+
* 找不到就返回空串(交给提问)。
|
|
114
|
+
*/
|
|
115
|
+
export function nameFromEvidence(heads, keywordsA = [], keywordsB = []) {
|
|
116
|
+
const banned = new Set([...(keywordsA ?? []), ...(keywordsB ?? [])]);
|
|
117
|
+
const texts = (heads ?? []).map((h) => String(h ?? ""));
|
|
118
|
+
const df = new Map();
|
|
119
|
+
for (const text of texts) {
|
|
120
|
+
const seen = new Set();
|
|
121
|
+
for (let n = 2; n <= 4; n++) {
|
|
122
|
+
for (let i = 0; i + n <= text.length; i++) {
|
|
123
|
+
const g = text.slice(i, i + n);
|
|
124
|
+
if (!/^[\u4e00-\u9fa5]+$/.test(g)) continue;
|
|
125
|
+
if (banned.has(g)) continue;
|
|
126
|
+
seen.add(g);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
for (const g of seen) df.set(g, (df.get(g) ?? 0) + 1);
|
|
130
|
+
}
|
|
131
|
+
// 词边界:真词两侧要么“干净”(句首/标点),要么邻字多样;碎片两侧恒定(如“机性”)。
|
|
132
|
+
const isBoundary = (ch) => ch === "" || /[\s,。!?、;:""''()()\[\]【】—…·]/.test(ch);
|
|
133
|
+
const sideOk = (term, side) => {
|
|
134
|
+
const map = new Map();
|
|
135
|
+
let allBoundary = true;
|
|
136
|
+
for (const text of texts) {
|
|
137
|
+
let i = -1;
|
|
138
|
+
while ((i = text.indexOf(term, i + 1)) >= 0) {
|
|
139
|
+
const ch = side === "prev" ? (i > 0 ? text[i - 1] : "") : (i + term.length < text.length ? text[i + term.length] : "");
|
|
140
|
+
if (!isBoundary(ch)) { allBoundary = false; map.set(ch, (map.get(ch) ?? 0) + 1); }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (allBoundary) return true;
|
|
144
|
+
const total = [...map.values()].reduce((a, b) => a + b, 0);
|
|
145
|
+
if (total === 0) return true;
|
|
146
|
+
let h = 0;
|
|
147
|
+
for (const c of map.values()) { const p2 = c / total; h -= p2 * Math.log2(p2); }
|
|
148
|
+
return h >= 0.9;
|
|
149
|
+
};
|
|
150
|
+
const boundaryOk = (term) => sideOk(term, "prev") && sideOk(term, "next");
|
|
151
|
+
// 与标签关键词共享 ≥2 字的片段也算“已有关键词的一部分”,不能当父名(如“机性价”)。
|
|
152
|
+
const grams2 = (s) => { const out = new Set(); for (let i = 0; i + 2 <= s.length; i++) out.add(s.slice(i, i + 2)); return out; };
|
|
153
|
+
const bannedSub = (g) => {
|
|
154
|
+
const gg = grams2(g);
|
|
155
|
+
return [...banned].some((k) => {
|
|
156
|
+
if (typeof k !== "string" || k.length < 2) return false;
|
|
157
|
+
if (g.includes(k) || k.includes(g)) return true;
|
|
158
|
+
for (const x of grams2(k)) if (gg.has(x)) return true;
|
|
159
|
+
return false;
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
const ranked = [...df.entries()]
|
|
163
|
+
// 长词(≥3)比 2 字词可靠;2 字词必须两侧都多样,否则多半是碎片。
|
|
164
|
+
.filter(([g, c]) => c >= 2 && !bannedSub(g) && (g.length >= 3 || boundaryOk(g)))
|
|
165
|
+
.sort((a, b) => (b[0].length - a[0].length) || (b[1] - a[1]));
|
|
166
|
+
return ranked.length > 0 ? ranked[0][0] : "";
|
|
167
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { assetMarker } from './assets.mjs';
|
|
3
|
+
import { handoffText, writeHandoff } from './handoff.mjs';
|
|
4
|
+
|
|
5
|
+
const PARAMETERS = {
|
|
6
|
+
type: 'object',
|
|
7
|
+
additionalProperties: false,
|
|
8
|
+
properties: {
|
|
9
|
+
childSessionId: { type: 'string', description: 'Child/PTC session id.' },
|
|
10
|
+
runId: { type: 'string', description: 'PTC workflow run id.' },
|
|
11
|
+
summary: { type: 'string', description: 'Bounded task result for the parent; do not truncate decisions.' },
|
|
12
|
+
status: { type: 'string', description: 'Optional status, default ok.' },
|
|
13
|
+
decisions: { type: 'array', items: { type: 'string' }, description: 'Why the child chose this result.' },
|
|
14
|
+
open: { type: 'array', items: { type: 'string' }, description: 'Unfinished work.' },
|
|
15
|
+
artifacts: { type: 'array', items: { type: 'string' }, description: 'Relative artifact paths.' },
|
|
16
|
+
sourceSeqs: { type: 'array', items: { type: 'integer' }, description: 'Child event seqs worth retrieving.' },
|
|
17
|
+
confidence: { type: 'string', enum: ['high', 'medium', 'low'] },
|
|
18
|
+
},
|
|
19
|
+
required: ['childSessionId', 'runId', 'summary'],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function defaultHome() {
|
|
23
|
+
return join(process.env.USERPROFILE ?? process.env.HOME ?? '', '.dsh', 'continuity');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createHandoffTool({ home = defaultHome() } = {}) {
|
|
27
|
+
return {
|
|
28
|
+
name: 'ptc_handoff',
|
|
29
|
+
description: 'Register a PTC child result as a parent-visible child artifact. '
|
|
30
|
+
+ 'Stores the full envelope under assets/ptc and returns a marker; use child_asset to retrieve it.',
|
|
31
|
+
parameters: PARAMETERS,
|
|
32
|
+
output: {
|
|
33
|
+
schema: { type: 'string' },
|
|
34
|
+
render: (_args, value) => [{ type: 'text', text: String(value ?? '') }],
|
|
35
|
+
},
|
|
36
|
+
timeoutMs: 20_000,
|
|
37
|
+
isConcurrencySafe: () => true,
|
|
38
|
+
async execute(args, exec) {
|
|
39
|
+
const input = args !== null && typeof args === 'object' ? args : {};
|
|
40
|
+
const parentSessionId = exec?.agent?.session?.id ?? exec?.agent?.id;
|
|
41
|
+
if (parentSessionId === undefined || parentSessionId === null) {
|
|
42
|
+
throw new Error('ptc_handoff: no current parent session id');
|
|
43
|
+
}
|
|
44
|
+
const result = await writeHandoff({ home, parentSessionId: String(parentSessionId), envelope: input });
|
|
45
|
+
return [
|
|
46
|
+
handoffText(result.envelope),
|
|
47
|
+
assetMarker(result.written.row, 'child'),
|
|
48
|
+
].join('\n');
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|