dsh-memu-memory 1.2.0 → 1.2.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/AGENTS.md +1 -1
- package/README.md +19 -8
- package/README.zh.md +12 -6
- package/docs/contributing.md +16 -3
- package/docs/env-and-commands.md +8 -0
- package/docs/host-api.md +3 -1
- package/docs/memory-semantics.md +21 -2
- package/docs/troubleshooting.md +42 -3
- package/lib/activity-log.js +22 -5
- package/lib/backup.js +24 -2
- package/lib/binding-diagnostics.js +3 -3
- package/lib/client.js +1610 -117
- package/lib/consolidation-worker.js +68 -17
- package/lib/curator-evidence.js +131 -0
- package/lib/curator-policy.js +81 -0
- package/lib/curator.js +1299 -83
- package/lib/cycle.js +3 -0
- package/lib/embedding-download.js +39 -4
- package/lib/engine.js +6 -3
- package/lib/global-graph.js +282 -0
- package/lib/graph.js +190 -14
- package/lib/inbound.js +19 -0
- package/lib/index.js +33 -1
- package/lib/inject.js +277 -17
- package/lib/lexicon.js +1115 -0
- package/lib/local-vector-service.js +8 -6
- package/lib/managed-runtime.js +33 -2
- package/lib/memory.js +585 -76
- package/lib/memu.js +2 -2
- package/lib/migrate.js +83 -2
- package/lib/private-links.js +587 -0
- package/lib/private.js +128 -7
- package/lib/prompts/global-curator.md +76 -19
- package/lib/prompts/private-consolidation.coding.md +4 -1
- package/lib/prompts/private-consolidation.md +3 -1
- package/lib/prompts/private-consolidation.mixed.md +3 -1
- package/lib/prompts/private-consolidation.rp.md +3 -1
- package/lib/prompts.js +1 -1
- package/lib/rpc.js +622 -13
- package/lib/state.js +4 -1
- package/lib/text-retrieval.js +661 -0
- package/lib/text-similarity.js +124 -0
- package/lib/tools.js +230 -16
- package/lib/vector-migration.js +28 -3
- package/lib/vector-profile.js +24 -0
- package/lib/webui/standalone.css +17 -0
- package/lib/webui/standalone.js +2 -0
- package/package.json +3 -2
- package/scripts/acceptance-local.mjs +8 -5
- package/scripts/curator-pair-calibrate.mjs +259 -0
- package/scripts/lexicon-calibrate.mjs +230 -0
- package/scripts/lexicon-dedupe.mjs +318 -0
- package/scripts/replay.mjs +573 -0
- package/tests/absorb-review.test.mjs +353 -0
- package/tests/appendix-b.test.mjs +6 -2
- package/tests/appendix-b2.test.mjs +2 -2
- package/tests/client-export.test.mjs +520 -0
- package/tests/client-render.test.mjs +19 -3
- package/tests/consolidation-credential.test.mjs +16 -5
- package/tests/consolidation-feed.test.mjs +96 -0
- package/tests/consolidation-fork.test.mjs +144 -10
- package/tests/consolidation-snapshot.test.mjs +82 -4
- package/tests/curator-absorb.test.mjs +443 -0
- package/tests/curator-calibrate.test.mjs +173 -0
- package/tests/curator-clusters.test.mjs +281 -0
- package/tests/curator-commit.test.mjs +324 -0
- package/tests/curator-evidence.test.mjs +102 -0
- package/tests/curator-link.test.mjs +316 -0
- package/tests/curator-pairing.test.mjs +252 -0
- package/tests/curator-probe.test.mjs +15 -6
- package/tests/curator-review-mode.test.mjs +314 -0
- package/tests/disclosure.test.mjs +2 -1
- package/tests/fixtures/curator-pairs/labeled.v1.jsonl +90 -0
- package/tests/fixtures/lexicon-stub-hooks.mjs +15 -0
- package/tests/fixtures/lexicon-stub.mjs +284 -0
- package/tests/graph-cascade.test.mjs +223 -0
- package/tests/graph.test.mjs +113 -0
- package/tests/help-page.test.mjs +415 -0
- package/tests/hybrid-recall.test.mjs +32 -2
- package/tests/inbound.test.mjs +6 -3
- package/tests/inject-guide.test.mjs +28 -0
- package/tests/inject.test.mjs +157 -5
- package/tests/lexicon-calibrate.test.mjs +48 -0
- package/tests/lexicon-dedupe.test.mjs +171 -0
- package/tests/lexicon-frequency.test.mjs +179 -0
- package/tests/lexicon-inject.test.mjs +410 -0
- package/tests/lexicon-perf.test.mjs +436 -0
- package/tests/lexicon-ui.test.mjs +547 -0
- package/tests/lexicon.test.mjs +922 -0
- package/tests/managed-memory.test.mjs +55 -0
- package/tests/managed-runtime.test.mjs +32 -0
- package/tests/private-links.test.mjs +445 -0
- package/tests/prompts.test.mjs +35 -0
- package/tests/recall-dedup.test.mjs +446 -0
- package/tests/sidebar-beat.test.mjs +5 -1
- package/tests/tool-schema.test.mjs +1 -1
- package/tests/tools.test.mjs +112 -3
- package/tests/vector-migration.test.mjs +58 -1
- package/tests/webui-auth.test.mjs +348 -2
- package/tests/webui.test.mjs +191 -2
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{"id":"sf-001","label":"same-fact","lang":"zh","tags":["paraphrase"],"a":{"id":"e-sf-001a","track":"memory","domain":"general","title":"部署窗口约定","content":"每周四凌晨两点到四点是固定的部署窗口,窗口内只允许灰度发布。","appliesTo":["deploy"]},"b":{"id":"e-sf-001b","track":"memory","domain":"general","title":"部署窗口约定","content":"每周四凌晨两点到四点是固定部署窗口,窗口里只允许灰度发布操作。","appliesTo":["deploy"]}}
|
|
2
|
+
{"id":"sf-002","label":"same-fact","lang":"zh","tags":["paraphrase"],"a":{"id":"e-sf-002a","track":"memory","domain":"general","title":"汇报风格","content":"用户在正式汇报里习惯先给结论再给细节。","appliesTo":["work"]},"b":{"id":"e-sf-002b","track":"memory","domain":"general","title":"汇报风格","content":"用户在正式汇报时习惯先给结论再讲细节。","appliesTo":["work"]}}
|
|
3
|
+
{"id":"sf-003","label":"same-fact","lang":"zh","tags":["paraphrase"],"a":{"id":"e-sf-003a","track":"memory","domain":"coding","title":"代码评审范围","content":"提交评审前必须跑完整测试,未通过的改动不进入评审。","appliesTo":["coding"]},"b":{"id":"e-sf-003b","track":"memory","domain":"coding","title":"代码评审范围","content":"提交评审前必须跑完整测试,没通过的改动不进评审。","appliesTo":["coding"]}}
|
|
4
|
+
{"id":"sf-004","label":"same-fact","lang":"zh","tags":["paraphrase"],"a":{"id":"e-sf-004a","track":"memory","domain":"general","title":"周刊习惯","content":"用户每周五下午写项目周报,习惯列三件事。","appliesTo":[]},"b":{"id":"e-sf-004b","track":"memory","domain":"general","title":"周刊习惯","content":"用户每周五下午写项目周报,习惯列出三件事。","appliesTo":[]}}
|
|
5
|
+
{"id":"sf-005","label":"same-fact","lang":"en","tags":["paraphrase"],"a":{"id":"e-sf-005a","track":"memory","domain":"coding","title":"TypeScript strict","content":"User prefers TypeScript strict mode in every project.","appliesTo":["coding"]},"b":{"id":"e-sf-005b","track":"memory","domain":"coding","title":"TypeScript strict","content":"User prefers strict TypeScript mode in all projects.","appliesTo":["coding"]}}
|
|
6
|
+
{"id":"sf-006","label":"same-fact","lang":"en","tags":["paraphrase"],"a":{"id":"e-sf-006a","track":"memory","domain":"coding","title":"PR review flow","content":"Every PR needs one approval before merging to main.","appliesTo":["coding"]},"b":{"id":"e-sf-006b","track":"memory","domain":"coding","title":"PR review flow","content":"Each PR requires one approval before merging to main.","appliesTo":["coding"]}}
|
|
7
|
+
{"id":"sf-007","label":"same-fact","lang":"en","tags":["paraphrase"],"a":{"id":"e-sf-007a","track":"memory","domain":"general","title":"Standup notes","content":"Standup notes are posted to the team channel every morning.","appliesTo":[]},"b":{"id":"e-sf-007b","track":"memory","domain":"general","title":"Standup notes","content":"Standup notes are posted to the team channel each morning.","appliesTo":[]}}
|
|
8
|
+
{"id":"sf-008","label":"same-fact","lang":"en","tags":["paraphrase"],"a":{"id":"e-sf-008a","track":"memory","domain":"coding","title":"Branch naming","content":"Feature branches are named after the ticket id.","appliesTo":["coding"]},"b":{"id":"e-sf-008b","track":"memory","domain":"coding","title":"Branch naming","content":"Feature branches are always named after the ticket id.","appliesTo":["coding"]}}
|
|
9
|
+
{"id":"sf-009","label":"same-fact","lang":"mixed","tags":["paraphrase"],"a":{"id":"e-sf-009a","track":"memory","domain":"coding","title":"Commit message","content":"Commit message 用英文,第一行不超过 50 字。","appliesTo":["coding"]},"b":{"id":"e-sf-009b","track":"memory","domain":"coding","title":"Commit message","content":"Commit message 写英文,首行不超过 50 字。","appliesTo":["coding"]}}
|
|
10
|
+
{"id":"sf-010","label":"same-fact","lang":"mixed","tags":["paraphrase"],"a":{"id":"e-sf-010a","track":"memory","domain":"general","title":"Debug 习惯","content":"遇到 bug 先写最小复现,再查文档。","appliesTo":[]},"b":{"id":"e-sf-010b","track":"memory","domain":"general","title":"Debug 习惯","content":"遇到 bug 先写最小复现用例,再查文档。","appliesTo":[]}}
|
|
11
|
+
{"id":"sf-011","label":"same-fact","lang":"mixed","tags":["paraphrase"],"a":{"id":"e-sf-011a","track":"memory","domain":"general","title":"部署确认","content":"发版后 user 会手动过一遍核心流程。","appliesTo":["deploy"]},"b":{"id":"e-sf-011b","track":"memory","domain":"general","title":"部署确认","content":"发版后 user 手动过一遍核心流程。","appliesTo":["deploy"]}}
|
|
12
|
+
{"id":"sf-012","label":"same-fact","lang":"zh","tags":["paraphrase"],"a":{"id":"e-sf-012a","track":"memory","domain":"general","title":"笔记工具","content":"用户用 Obsidian 管理笔记,按项目分库。","appliesTo":[]},"b":{"id":"e-sf-012b","track":"memory","domain":"general","title":"笔记工具","content":"用户用 Obsidian 管笔记,按项目分库。","appliesTo":[]}}
|
|
13
|
+
{"id":"sf-013","label":"same-fact","lang":"zh","tags":["length-gap","containment"],"a":{"id":"e-sf-013a","track":"memory","domain":"coding","title":"缓存过期","content":"Redis 缓存统一设置 30 秒过期。","appliesTo":["backend"]},"b":{"id":"e-sf-013b","track":"memory","domain":"coding","title":"缓存过期","content":"Redis 缓存统一设置 30 秒过期。过期后直接回源查询,回源结果写回缓存;缓存击穿用互斥锁保护。","appliesTo":["backend"]}}
|
|
14
|
+
{"id":"sf-014","label":"same-fact","lang":"en","tags":["length-gap","containment"],"a":{"id":"e-sf-014a","track":"memory","domain":"coding","title":"Deploy freeze","content":"No deploys on Friday afternoons.","appliesTo":["deploy"]},"b":{"id":"e-sf-014b","track":"memory","domain":"coding","title":"Deploy freeze","content":"No deploys on Friday afternoons. Friday afternoon deploys are frozen; hotfixes need on-call sign-off.","appliesTo":["deploy"]}}
|
|
15
|
+
{"id":"sf-015","label":"same-fact","lang":"mixed","tags":["length-gap","containment"],"a":{"id":"e-sf-015a","track":"memory","domain":"coding","title":"Lint 规则","content":"提交前必须先过 lint。","appliesTo":["coding"]},"b":{"id":"e-sf-015b","track":"memory","domain":"coding","title":"Lint 规则","content":"提交前必须先过 lint;lint 不过不允许提交,格式问题交给 format 自动修复。","appliesTo":["coding"]}}
|
|
16
|
+
{"id":"sf-016","label":"same-fact","lang":"zh","tags":["negation-same"],"a":{"id":"e-sf-016a","track":"memory","domain":"general","title":"会议点名","content":"用户不喜欢在会上被突然点名发言。","appliesTo":["work"]},"b":{"id":"e-sf-016b","track":"memory","domain":"general","title":"会议点名","content":"用户不喜欢在会上被突然点名回答。","appliesTo":["work"]}}
|
|
17
|
+
{"id":"sf-017","label":"same-fact","lang":"en","tags":["negation-same"],"a":{"id":"e-sf-017a","track":"memory","domain":"general","title":"No surprises","content":"User does not like surprise scope changes mid-sprint.","appliesTo":["work"]},"b":{"id":"e-sf-017b","track":"memory","domain":"general","title":"No surprises","content":"User does not like unexpected scope changes mid-sprint.","appliesTo":["work"]}}
|
|
18
|
+
{"id":"sf-018","label":"same-fact","lang":"zh","tags":["dates-same"],"a":{"id":"e-sf-018a","track":"memory","domain":"general","title":"发版节奏","content":"每周三 18:00 准时发版,P0 故障才顺延。","appliesTo":["deploy"]},"b":{"id":"e-sf-018b","track":"memory","domain":"general","title":"发版节奏","content":"每周三 18:00 准时发版,遇到 P0 故障才顺延。","appliesTo":["deploy"]}}
|
|
19
|
+
{"id":"sf-019","label":"same-fact","lang":"zh","tags":["dates-same"],"a":{"id":"e-sf-019a","track":"memory","domain":"general","title":"周会时间","content":"项目周会固定在周二 10:30,时长 45 分钟。","appliesTo":["work"]},"b":{"id":"e-sf-019b","track":"memory","domain":"general","title":"周会时间","content":"周二 10:30 的项目周会固定开,时长 45 分钟。","appliesTo":["work"]}}
|
|
20
|
+
{"id":"sf-020","label":"same-fact","lang":"en","tags":["dates-same"],"a":{"id":"e-sf-020a","track":"memory","domain":"general","title":"Release day","content":"Releases ship every Wednesday at 18:00 UTC.","appliesTo":["deploy"]},"b":{"id":"e-sf-020b","track":"memory","domain":"general","title":"Release day","content":"Releases ship each Wednesday at 18:00 UTC.","appliesTo":["deploy"]}}
|
|
21
|
+
{"id":"sf-021","label":"same-fact","lang":"zh","tags":["alias"],"a":{"id":"e-sf-021a","track":"memory","domain":"general","title":"记忆插件","content":"用户说的记忆插件指 dsh-memu-memory 这个插件。","appliesTo":[]},"b":{"id":"e-sf-021b","track":"memory","domain":"general","title":"记忆插件","content":"用户说的记忆插件就是 dsh-memu-memory 这个插件。","appliesTo":[]}}
|
|
22
|
+
{"id":"sf-022","label":"same-fact","lang":"en","tags":["alias"],"a":{"id":"e-sf-022a","track":"memory","domain":"general","title":"Memory plugin","content":"The user calls dsh-memu-memory the memory plugin.","appliesTo":[]},"b":{"id":"e-sf-022b","track":"memory","domain":"general","title":"Memory plugin","content":"The user always calls dsh-memu-memory the memory plugin.","appliesTo":[]}}
|
|
23
|
+
{"id":"sf-023","label":"same-fact","lang":"mixed","tags":["alias"],"a":{"id":"e-sf-023a","track":"memory","domain":"coding","title":"评测脚本","content":"用户说的 eval 脚本就是 scripts/replay.mjs。","appliesTo":["coding"]},"b":{"id":"e-sf-023b","track":"memory","domain":"coding","title":"评测脚本","content":"用户说的 eval 脚本指的是 scripts/replay.mjs。","appliesTo":["coding"]}}
|
|
24
|
+
{"id":"sf-024","label":"same-fact","lang":"zh","tags":["paraphrase","skill"],"a":{"id":"e-sf-024a","track":"skill","domain":"tooling","title":"发布清单","content":"发版前按清单逐项确认:版本号、changelog、回归标记。","appliesTo":["deploy"]},"b":{"id":"e-sf-024b","track":"skill","domain":"tooling","title":"发布清单","content":"发版前按清单逐项确认版本号、changelog 和回归标记。","appliesTo":["deploy"]}}
|
|
25
|
+
{"id":"sf-025","label":"same-fact","lang":"en","tags":["paraphrase","skill"],"a":{"id":"e-sf-025a","track":"skill","domain":"coding","title":"Code style","content":"Keep functions under 40 lines and extract helpers early.","appliesTo":["coding"]},"b":{"id":"e-sf-025b","track":"skill","domain":"coding","title":"Code style","content":"Keep functions below 40 lines and extract helpers early.","appliesTo":["coding"]}}
|
|
26
|
+
{"id":"sf-026","label":"same-fact","lang":"zh","tags":["paraphrase"],"a":{"id":"e-sf-026a","track":"memory","domain":"personal","title":"作息","content":"用户习惯 23:30 前睡觉,早上 7 点起。","appliesTo":[]},"b":{"id":"e-sf-026b","track":"memory","domain":"personal","title":"作息","content":"用户习惯 23:30 前睡觉,早上 7 点起床。","appliesTo":[]}}
|
|
27
|
+
{"id":"sf-027","label":"same-fact","lang":"en","tags":["paraphrase"],"a":{"id":"e-sf-027a","track":"memory","domain":"personal","title":"Coffee rule","content":"No coffee after 15:00, decaf only.","appliesTo":[]},"b":{"id":"e-sf-027b","track":"memory","domain":"personal","title":"Coffee rule","content":"No coffee after 15:00, only decaf.","appliesTo":[]}}
|
|
28
|
+
{"id":"sf-028","label":"same-fact","lang":"zh","tags":["punctuation"],"a":{"id":"e-sf-028a","track":"memory","domain":"general","title":"周报模板","content":"周报固定三段:进展、风险、需要的支持。","appliesTo":["work"]},"b":{"id":"e-sf-028b","track":"memory","domain":"general","title":"周报模板","content":"周报固定三段:进展、风险、需要的支持.","appliesTo":["work"]}}
|
|
29
|
+
{"id":"sf-029","label":"same-fact","lang":"en","tags":["paraphrase"],"a":{"id":"e-sf-029a","track":"memory","domain":"general","title":"Focus hours","content":"Mornings are deep-work time; meetings go after lunch.","appliesTo":["work"]},"b":{"id":"e-sf-029b","track":"memory","domain":"general","title":"Focus hours","content":"Mornings are deep work time; meetings go after lunch.","appliesTo":["work"]}}
|
|
30
|
+
{"id":"sf-030","label":"same-fact","lang":"zh","tags":["paraphrase"],"a":{"id":"e-sf-030a","track":"memory","domain":"general","title":"回复节奏","content":"用户偏好异步沟通,消息不必秒回。","appliesTo":["work"]},"b":{"id":"e-sf-030b","track":"memory","domain":"general","title":"回复节奏","content":"用户偏好异步沟通,消息不用秒回。","appliesTo":["work"]}}
|
|
31
|
+
{"id":"rl-001","label":"related","lang":"zh","tags":["time-shift"],"a":{"id":"e-rl-001a","track":"memory","domain":"general","title":"站会安排","content":"每周一上午十点开站会,时长半小时。","appliesTo":["work"]},"b":{"id":"e-rl-001b","track":"memory","domain":"general","title":"站会安排","content":"每周一上午十点开站会,时长缩短到二十分钟。","appliesTo":["work"]}}
|
|
32
|
+
{"id":"rl-002","label":"related","lang":"zh","tags":["same-prefix"],"a":{"id":"e-rl-002a","track":"memory","domain":"coding","title":"部署窗口","content":"部署窗口是每周四凌晨两点到四点,只做灰度发布。","appliesTo":["deploy"]},"b":{"id":"e-rl-002b","track":"memory","domain":"coding","title":"部署窗口","content":"部署窗口是每周四凌晨两点到四点,全量发布要等监控确认。","appliesTo":["deploy"]}}
|
|
33
|
+
{"id":"rl-003","label":"related","lang":"zh","tags":["dates-differ"],"a":{"id":"e-rl-003a","track":"memory","domain":"general","title":"站会时间","content":"站会固定在每周一 10:00 开始,先过昨日进展,再排今日任务。","appliesTo":["work"]},"b":{"id":"e-rl-003b","track":"memory","domain":"general","title":"站会时间","content":"站会固定在每周一 11:00 开始,先过昨日进展,再排今日任务。","appliesTo":["work"]}}
|
|
34
|
+
{"id":"rl-004","label":"related","lang":"en","tags":["same-prefix"],"a":{"id":"e-rl-004a","track":"memory","domain":"coding","title":"Release checklist","content":"Release checklist starts with the changelog entry.","appliesTo":["deploy"]},"b":{"id":"e-rl-004b","track":"memory","domain":"coding","title":"Release checklist","content":"Release checklist ends with the regression sweep.","appliesTo":["deploy"]}}
|
|
35
|
+
{"id":"rl-005","label":"related","lang":"en","tags":["same-topic"],"a":{"id":"e-rl-005a","track":"memory","domain":"general","title":"Editor setup","content":"User edits frontend code in VS Code with vim bindings.","appliesTo":["coding"]},"b":{"id":"e-rl-005b","track":"memory","domain":"general","title":"Editor setup","content":"User edits backend code in VS Code with emacs bindings on Linux.","appliesTo":["coding"]}}
|
|
36
|
+
{"id":"rl-006","label":"related","lang":"mixed","tags":["same-prefix"],"a":{"id":"e-rl-006a","track":"memory","domain":"coding","title":"测试策略","content":"核心模块的测试策略是先写单测再写集成测试。","appliesTo":["coding"]},"b":{"id":"e-rl-006b","track":"memory","domain":"coding","title":"测试策略","content":"边缘模块的测试策略是先写集成测试再补单测。","appliesTo":["coding"]}}
|
|
37
|
+
{"id":"rl-007","label":"related","lang":"zh","tags":["same-topic"],"a":{"id":"e-rl-007a","track":"memory","domain":"general","title":"笔记结构","content":"用户按项目给笔记分库,库内再按主题分区。","appliesTo":[]},"b":{"id":"e-rl-007b","track":"memory","domain":"general","title":"笔记结构","content":"用户按项目给笔记分库,库内按时间归档。","appliesTo":[]}}
|
|
38
|
+
{"id":"rl-008","label":"related","lang":"en","tags":["same-topic"],"a":{"id":"e-rl-008a","track":"memory","domain":"general","title":"Meeting slots","content":"User books deep work before 11:00 every day.","appliesTo":["work"]},"b":{"id":"e-rl-008b","track":"memory","domain":"general","title":"Meeting slots","content":"User books meetings after 14:00 every day.","appliesTo":["work"]}}
|
|
39
|
+
{"id":"rl-009","label":"related","lang":"zh","tags":["same-prefix"],"a":{"id":"e-rl-009a","track":"memory","domain":"coding","title":"代码评审","content":"代码评审重点是接口设计与错误处理是否到位。","appliesTo":["coding"]},"b":{"id":"e-rl-009b","track":"memory","domain":"coding","title":"评审重点","content":"评审重点是测试覆盖与错误处理是否到位。","appliesTo":["coding"]}}
|
|
40
|
+
{"id":"rl-010","label":"related","lang":"en","tags":["same-prefix"],"a":{"id":"e-rl-010a","track":"memory","domain":"coding","title":"Error handling","content":"API errors return a code plus a human message.","appliesTo":["coding"]},"b":{"id":"e-rl-010b","track":"memory","domain":"coding","title":"Error handling","content":"API errors always carry a code plus a human-readable message.","appliesTo":["coding"]}}
|
|
41
|
+
{"id":"rl-011","label":"related","lang":"mixed","tags":["same-topic"],"a":{"id":"e-rl-011a","track":"memory","domain":"general","title":"文档习惯","content":"user 写文档喜欢先列 FAQ 再展开细节。","appliesTo":[]},"b":{"id":"e-rl-011b","track":"memory","domain":"general","title":"文档习惯","content":"user 写文档喜欢先画架构图再展开细节。","appliesTo":[]}}
|
|
42
|
+
{"id":"rl-012","label":"related","lang":"zh","tags":["same-prefix"],"a":{"id":"e-rl-012a","track":"memory","domain":"personal","title":"健身安排","content":"用户每周二、周四晚上去健身房练力量。","appliesTo":[]},"b":{"id":"e-rl-012b","track":"memory","domain":"personal","title":"健身安排","content":"用户每周二、周四晚上去健身房上团课,周日休息。","appliesTo":[]}}
|
|
43
|
+
{"id":"rl-013","label":"related","lang":"en","tags":["same-topic"],"a":{"id":"e-rl-013a","track":"memory","domain":"personal","title":"Reading list","content":"User reads one technical book per quarter.","appliesTo":[]},"b":{"id":"e-rl-013b","track":"memory","domain":"personal","title":"Reading list","content":"User reads two fiction books per month.","appliesTo":[]}}
|
|
44
|
+
{"id":"rl-014","label":"related","lang":"zh","tags":["dates-differ"],"a":{"id":"e-rl-014a","track":"memory","domain":"general","title":"复盘会","content":"项目复盘会定在每月最后一个周五下午。","appliesTo":["work"]},"b":{"id":"e-rl-014b","track":"memory","domain":"general","title":"复盘会","content":"项目复盘会定在每季度最后一个周五上午十点。","appliesTo":["work"]}}
|
|
45
|
+
{"id":"rl-015","label":"related","lang":"en","tags":["dates-differ"],"a":{"id":"e-rl-015a","track":"memory","domain":"general","title":"1:1 cadence","content":"The 1:1 with the lead happens every Tuesday.","appliesTo":["work"]},"b":{"id":"e-rl-015b","track":"memory","domain":"general","title":"1:1 cadence","content":"The 1:1 with the lead happens on the first Tuesday of the month.","appliesTo":["work"]}}
|
|
46
|
+
{"id":"rl-016","label":"related","lang":"zh","tags":["negation-differ"],"a":{"id":"e-rl-016a","track":"memory","domain":"general","title":"香菜偏好","content":"用户吃香菜,面里可以正常加。","appliesTo":[]},"b":{"id":"e-rl-016b","track":"memory","domain":"general","title":"香菜偏好","content":"用户不吃香菜,面里绝对不要放。","appliesTo":[]}}
|
|
47
|
+
{"id":"rl-017","label":"related","lang":"en","tags":["negation-differ"],"a":{"id":"e-rl-017a","track":"memory","domain":"general","title":"Alerts","content":"User wants alerts for every failed deploy.","appliesTo":["deploy"]},"b":{"id":"e-rl-017b","track":"memory","domain":"general","title":"Alerts","content":"User wants alerts for every failed deploy, but no alerts for nightly job noise.","appliesTo":["deploy"]}}
|
|
48
|
+
{"id":"rl-018","label":"related","lang":"zh","tags":["same-prefix"],"a":{"id":"e-rl-018a","track":"memory","domain":"coding","title":"依赖升级","content":"依赖升级每月做一次,先升补丁版本。","appliesTo":["coding"]},"b":{"id":"e-rl-018b","track":"memory","domain":"coding","title":"依赖升级","content":"依赖升级每月做一次,大版本要单独开分支。","appliesTo":["coding"]}}
|
|
49
|
+
{"id":"rl-019","label":"related","lang":"mixed","tags":["same-prefix"],"a":{"id":"e-rl-019a","track":"memory","domain":"coding","title":"日志级别","content":"生产环境日志级别默认 info,trace 要手动开。","appliesTo":["coding"]},"b":{"id":"e-rl-019b","track":"memory","domain":"coding","title":"日志级别","content":"生产环境日志级别默认 info,trace 要手动开,error 单独落盘并保留 7 天。","appliesTo":["coding"]}}
|
|
50
|
+
{"id":"rl-020","label":"related","lang":"en","tags":["same-prefix"],"a":{"id":"e-rl-020a","track":"memory","domain":"general","title":"Backup plan","content":"Backups run nightly and keep 14 days.","appliesTo":["deploy"]},"b":{"id":"e-rl-020b","track":"memory","domain":"general","title":"Backup plan","content":"Backups run nightly and keep a full quarter before rotation.","appliesTo":["deploy"]}}
|
|
51
|
+
{"id":"rl-021","label":"related","lang":"zh","tags":["alias-related"],"a":{"id":"e-rl-021a","track":"memory","domain":"general","title":"存储引擎","content":"项目里的存储引擎指 SQLite 本地库。","appliesTo":["coding"]},"b":{"id":"e-rl-021b","track":"memory","domain":"general","title":"向量引擎","content":"项目里的存储引擎指 MemU 向量库。","appliesTo":["coding"]}}
|
|
52
|
+
{"id":"rl-022","label":"related","lang":"en","tags":["alias-related"],"a":{"id":"e-rl-022a","track":"memory","domain":"coding","title":"The runner","content":"The runner means the nightly replay script.","appliesTo":["coding"]},"b":{"id":"e-rl-022b","track":"memory","domain":"coding","title":"The runner","content":"The runner means the smoke workflow, not the replay script.","appliesTo":["coding"]}}
|
|
53
|
+
{"id":"rl-023","label":"related","lang":"mixed","tags":["same-topic"],"a":{"id":"e-rl-023a","track":"memory","domain":"general","title":"沟通渠道","content":"工作沟通 user 用邮件,私人用微信。","appliesTo":[]},"b":{"id":"e-rl-023b","track":"memory","domain":"general","title":"沟通渠道","content":"工作沟通 user 用钉钉,私人用 telegram。","appliesTo":[]}}
|
|
54
|
+
{"id":"rl-024","label":"related","lang":"zh","tags":["same-topic"],"a":{"id":"e-rl-024a","track":"memory","domain":"general","title":"学习方式","content":"用户学新框架先跑官方示例再读源码。","appliesTo":[]},"b":{"id":"e-rl-024b","track":"memory","domain":"general","title":"学习方式","content":"用户学新框架先跑官方示例,再读文档和源码注释。","appliesTo":[]}}
|
|
55
|
+
{"id":"rl-025","label":"related","lang":"en","tags":["same-topic"],"a":{"id":"e-rl-025a","track":"memory","domain":"coding","title":"Perf budget","content":"Pages must load in under two seconds on 3G.","appliesTo":["coding"]},"b":{"id":"e-rl-025b","track":"memory","domain":"coding","title":"Perf budget","content":"APIs must respond in under 200ms at p95.","appliesTo":["coding"]}}
|
|
56
|
+
{"id":"rl-026","label":"related","lang":"zh","tags":["same-prefix"],"a":{"id":"e-rl-026a","track":"skill","domain":"tooling","title":"排障步骤","content":"排障第一步是看活动日志,再看引擎日志。","appliesTo":["coding"]},"b":{"id":"e-rl-026b","track":"skill","domain":"tooling","title":"排障步骤","content":"排障第一步是看活动日志,第二步才复现问题。","appliesTo":["coding"]}}
|
|
57
|
+
{"id":"rl-027","label":"related","lang":"zh","tags":["same-topic"],"a":{"id":"e-rl-027a","track":"memory","domain":"general","title":"咖啡偏好","content":"用户下午的咖啡只喝美式,上午喝拿铁。","appliesTo":[]},"b":{"id":"e-rl-027b","track":"memory","domain":"general","title":"咖啡偏好","content":"用户下午的咖啡只喝美式,但加班时喝双份。","appliesTo":[]}}
|
|
58
|
+
{"id":"rl-028","label":"related","lang":"en","tags":["same-topic"],"a":{"id":"e-rl-028a","track":"memory","domain":"general","title":"Keyboard","content":"User carries a 65 percent mechanical keyboard.","appliesTo":[]},"b":{"id":"e-rl-028b","track":"memory","domain":"general","title":"Keyboard","content":"User carries a 65 percent keyboard; the ergo split stays home.","appliesTo":[]}}
|
|
59
|
+
{"id":"rl-029","label":"related","lang":"mixed","tags":["same-prefix"],"a":{"id":"e-rl-029a","track":"memory","domain":"coding","title":"Code review SLA","content":"PR 要在 24 小时内给出首个 review。","appliesTo":["coding"]},"b":{"id":"e-rl-029b","track":"memory","domain":"coding","title":"Code review SLA","content":"PR 要在 48 小时内给出最终结论。","appliesTo":["coding"]}}
|
|
60
|
+
{"id":"rl-030","label":"related","lang":"zh","tags":["same-prefix"],"a":{"id":"e-rl-030a","track":"memory","domain":"general","title":"值班规则","content":"线上值班期间手机必须保持畅通。","appliesTo":["deploy"]},"b":{"id":"e-rl-030b","track":"memory","domain":"general","title":"值班规则","content":"线上值班期间手机必须保持畅通,两小时巡检一次大盘,交接要写记录。","appliesTo":["deploy"]}}
|
|
61
|
+
{"id":"nn-001","label":"none","lang":"zh","tags":["different-topic"],"a":{"id":"e-nn-001a","track":"memory","domain":"general","title":"部署窗口约定","content":"每周四凌晨两点到四点是固定的部署窗口,窗口内只允许灰度发布。","appliesTo":["deploy"]},"b":{"id":"e-nn-001b","track":"memory","domain":"personal","title":"摄影器材","content":"用户的相机是全画幅微单,常用 35mm 定焦。","appliesTo":[]}}
|
|
62
|
+
{"id":"nn-002","label":"none","lang":"zh","tags":["different-topic"],"a":{"id":"e-nn-002a","track":"memory","domain":"general","title":"汇报风格","content":"用户在正式汇报里习惯先给结论再给细节。","appliesTo":["work"]},"b":{"id":"e-nn-002b","track":"memory","domain":"personal","title":"宠物","content":"用户养了一只橘猫,名叫年糕。","appliesTo":[]}}
|
|
63
|
+
{"id":"nn-003","label":"none","lang":"en","tags":["different-topic"],"a":{"id":"e-nn-003a","track":"memory","domain":"coding","title":"TypeScript strict","content":"User prefers TypeScript strict mode in every project.","appliesTo":["coding"]},"b":{"id":"e-nn-003b","track":"memory","domain":"general","title":"Hiking","content":"User hikes every other weekend when the weather holds.","appliesTo":[]}}
|
|
64
|
+
{"id":"nn-004","label":"none","lang":"en","tags":["different-topic"],"a":{"id":"e-nn-004a","track":"memory","domain":"coding","title":"PR review flow","content":"Every PR needs one approval before merging to main.","appliesTo":["coding"]},"b":{"id":"e-nn-004b","track":"memory","domain":"personal","title":"Guitar","content":"User practices fingerstyle guitar for twenty minutes daily.","appliesTo":[]}}
|
|
65
|
+
{"id":"nn-005","label":"none","lang":"zh","tags":["different-domain"],"a":{"id":"e-nn-005a","track":"memory","domain":"rp","title":"叙事视角","content":"RP 跑团里用户偏好第二人称叙事,节奏偏慢。","appliesTo":[]},"b":{"id":"e-nn-005b","track":"memory","domain":"coding","title":"构建工具","content":"项目构建统一用 pnpm,monorepo 走 workspace。","appliesTo":["coding"]}}
|
|
66
|
+
{"id":"nn-006","label":"none","lang":"mixed","tags":["different-topic"],"a":{"id":"e-nn-006a","track":"memory","domain":"general","title":"Commit message","content":"Commit message 用英文,第一行不超过 50 字。","appliesTo":["coding"]},"b":{"id":"e-nn-006b","track":"memory","domain":"personal","title":"旅行偏好","content":"user 旅行时喜欢住民宿,不跟团。","appliesTo":[]}}
|
|
67
|
+
{"id":"nn-007","label":"none","lang":"zh","tags":["cross-track"],"a":{"id":"e-nn-007a","track":"skill","domain":"tooling","title":"发布清单","content":"发版前按清单逐项确认:版本号、changelog、回归标记。","appliesTo":["deploy"]},"b":{"id":"e-nn-007b","track":"memory","domain":"personal","title":"饮食禁忌","content":"用户对花生过敏,零食要避开。","appliesTo":[]}}
|
|
68
|
+
{"id":"nn-008","label":"none","lang":"en","tags":["cross-track"],"a":{"id":"e-nn-008a","track":"skill","domain":"coding","title":"Code style","content":"Keep functions under 40 lines and extract helpers early.","appliesTo":["coding"]},"b":{"id":"e-nn-008b","track":"memory","domain":"general","title":"Podcast habit","content":"User listens to one long podcast episode per week.","appliesTo":[]}}
|
|
69
|
+
{"id":"nn-009","label":"none","lang":"zh","tags":["different-topic"],"a":{"id":"e-nn-009a","track":"memory","domain":"general","title":"周刊习惯","content":"用户每周五下午写项目周报,习惯列三件事。","appliesTo":["work"]},"b":{"id":"e-nn-009b","track":"memory","domain":"general","title":"方言背景","content":"用户是杭州人,会说一点杭州话。","appliesTo":[]}}
|
|
70
|
+
{"id":"nn-010","label":"none","lang":"zh","tags":["different-topic"],"a":{"id":"e-nn-010a","track":"memory","domain":"general","title":"笔记工具","content":"用户用 Obsidian 管理笔记,按项目分库。","appliesTo":[]},"b":{"id":"e-nn-010b","track":"memory","domain":"general","title":"运动损伤","content":"用户右膝有旧伤,深蹲要减重。","appliesTo":[]}}
|
|
71
|
+
{"id":"nn-011","label":"none","lang":"en","tags":["different-topic"],"a":{"id":"e-nn-011a","track":"memory","domain":"general","title":"Standup notes","content":"Standup notes are posted to the team channel every morning.","appliesTo":["work"]},"b":{"id":"e-nn-011b","track":"memory","domain":"general","title":"Chess hobby","content":"User plays correspondence chess at a slow pace.","appliesTo":[]}}
|
|
72
|
+
{"id":"nn-012","label":"none","lang":"en","tags":["different-topic"],"a":{"id":"e-nn-012a","track":"memory","domain":"coding","title":"Branch naming","content":"Feature branches are named after the ticket id.","appliesTo":["coding"]},"b":{"id":"e-nn-012b","track":"memory","domain":"personal","title":"Skiing level","content":"User skis blue runs confidently, blacks with a guide.","appliesTo":[]}}
|
|
73
|
+
{"id":"nn-013","label":"none","lang":"mixed","tags":["different-topic"],"a":{"id":"e-nn-013a","track":"memory","domain":"coding","title":"Debug 习惯","content":"遇到 bug 先写最小复现,再查文档。","appliesTo":["coding"]},"b":{"id":"e-nn-013b","track":"memory","domain":"personal","title":"晨跑路线","content":"user 晨跑固定沿河边往返五公里。","appliesTo":[]}}
|
|
74
|
+
{"id":"nn-014","label":"none","lang":"zh","tags":["different-topic"],"a":{"id":"e-nn-014a","track":"memory","domain":"general","title":"回复节奏","content":"用户偏好异步沟通,消息不必秒回。","appliesTo":["work"]},"b":{"id":"e-nn-014b","track":"memory","domain":"general","title":"视力情况","content":"用户近视六百度,戴框架眼镜。","appliesTo":[]}}
|
|
75
|
+
{"id":"nn-015","label":"none","lang":"en","tags":["different-topic"],"a":{"id":"e-nn-015a","track":"memory","domain":"general","title":"Focus hours","content":"Mornings are deep-work time; meetings go after lunch.","appliesTo":["work"]},"b":{"id":"e-nn-015b","track":"memory","domain":"personal","title":"Baking","content":"User bakes sourdough on Sunday mornings.","appliesTo":[]}}
|
|
76
|
+
{"id":"nn-016","label":"none","lang":"zh","tags":["different-topic"],"a":{"id":"e-nn-016a","track":"memory","domain":"personal","title":"作息","content":"用户习惯 23:30 前睡觉,早上 7 点起床。","appliesTo":[]},"b":{"id":"e-nn-016b","track":"memory","domain":"coding","title":"测试框架","content":"仓库统一用 node:test 跑测试,不引第三方框架。","appliesTo":["coding"]}}
|
|
77
|
+
{"id":"nn-017","label":"none","lang":"en","tags":["different-topic"],"a":{"id":"e-nn-017a","track":"memory","domain":"personal","title":"Coffee rule","content":"No coffee after 15:00, only decaf.","appliesTo":[]},"b":{"id":"e-nn-017b","track":"memory","domain":"coding","title":"CI policy","content":"CI must stay green before any merge queue entry.","appliesTo":["coding"]}}
|
|
78
|
+
{"id":"nn-018","label":"none","lang":"zh","tags":["different-topic"],"a":{"id":"e-nn-018a","track":"memory","domain":"general","title":"周报模板","content":"周报固定三段:进展、风险、需要的支持。","appliesTo":["work"]},"b":{"id":"e-nn-018b","track":"memory","domain":"general","title":"旧机回收","content":"用户的旧手机折价回收给了平台。","appliesTo":[]}}
|
|
79
|
+
{"id":"nn-019","label":"none","lang":"en","tags":["different-topic"],"a":{"id":"e-nn-019a","track":"memory","domain":"general","title":"Release day","content":"Releases ship every Wednesday at 18:00 UTC.","appliesTo":["deploy"]},"b":{"id":"e-nn-019b","track":"memory","domain":"personal","title":"Vinyl collection","content":"User collects jazz vinyl records from the 60s.","appliesTo":[]}}
|
|
80
|
+
{"id":"nn-020","label":"none","lang":"zh","tags":["different-topic"],"a":{"id":"e-nn-020a","track":"memory","domain":"coding","title":"缓存过期","content":"Redis 缓存统一设置 30 秒过期。","appliesTo":["backend"]},"b":{"id":"e-nn-020b","track":"memory","domain":"personal","title":"园艺","content":"用户在阳台种了薄荷和迷迭香。","appliesTo":[]}}
|
|
81
|
+
{"id":"nn-021","label":"none","lang":"mixed","tags":["different-topic"],"a":{"id":"e-nn-021a","track":"memory","domain":"coding","title":"Lint 规则","content":"提交前必须先过 lint。","appliesTo":["coding"]},"b":{"id":"e-nn-021b","track":"memory","domain":"personal","title":"Board games","content":"user 每月约朋友玩一次桌游,偏策略类。","appliesTo":[]}}
|
|
82
|
+
{"id":"nn-022","label":"none","lang":"zh","tags":["different-domain"],"a":{"id":"e-nn-022a","track":"memory","domain":"rp","title":"跑团日程","content":"RP 跑团固定周六晚上开团,三小时一场。","appliesTo":[]},"b":{"id":"e-nn-022b","track":"memory","domain":"coding","title":"代码归属","content":"实验性代码统一放 experiments 分支,不合主干。","appliesTo":["coding"]}}
|
|
83
|
+
{"id":"nn-023","label":"none","lang":"zh","tags":["different-topic"],"a":{"id":"e-nn-023a","track":"memory","domain":"general","title":"记忆插件","content":"用户说的记忆插件指 dsh-memu-memory 这个插件。","appliesTo":[]},"b":{"id":"e-nn-023b","track":"memory","domain":"general","title":"手机套餐","content":"用户的手机套餐是八元保号套餐。","appliesTo":[]}}
|
|
84
|
+
{"id":"nn-024","label":"none","lang":"en","tags":["different-topic"],"a":{"id":"e-nn-024a","track":"memory","domain":"general","title":"Memory plugin","content":"The user always calls dsh-memu-memory the memory plugin.","appliesTo":[]},"b":{"id":"e-nn-024b","track":"memory","domain":"personal","title":"Running shoes","content":"User replaces running shoes every 800 kilometers.","appliesTo":[]}}
|
|
85
|
+
{"id":"nn-025","label":"none","lang":"mixed","tags":["different-topic"],"a":{"id":"e-nn-025a","track":"memory","domain":"coding","title":"评测脚本","content":"用户说的 eval 脚本就是 scripts/replay.mjs。","appliesTo":["coding"]},"b":{"id":"e-nn-025b","track":"memory","domain":"personal","title":"雨具习惯","content":"user 的包里常年放着一把折叠伞。","appliesTo":[]}}
|
|
86
|
+
{"id":"nn-026","label":"none","lang":"zh","tags":["length-gap-none"],"a":{"id":"e-nn-026a","track":"memory","domain":"general","title":"借书证","content":"用户的图书馆借书证到期时间是明年 3 月。","appliesTo":[]},"b":{"id":"e-nn-026b","track":"memory","domain":"coding","title":"接入文档","content":"新服务接入前要先读接入文档,确认鉴权方式与限流阈值,再在沙箱环境跑通示例,最后才允许申请生产密钥。","appliesTo":["coding"]}}
|
|
87
|
+
{"id":"nn-027","label":"none","lang":"en","tags":["length-gap-none"],"a":{"id":"e-nn-027a","track":"memory","domain":"personal","title":"Gym card","content":"The gym membership card expires in March.","appliesTo":[]},"b":{"id":"e-nn-027b","track":"memory","domain":"coding","title":"Onboarding","content":"New services onboard through the sandbox first: read the integration doc, verify the auth flow, run the sample, then request production keys.","appliesTo":["coding"]}}
|
|
88
|
+
{"id":"nn-028","label":"none","lang":"zh","tags":["different-topic"],"a":{"id":"e-nn-028a","track":"memory","domain":"general","title":"站会安排","content":"每周一上午十点开站会,时长半小时。","appliesTo":["work"]},"b":{"id":"e-nn-028b","track":"memory","domain":"general","title":"电池更换","content":"用户的鼠标电池三个月换一次。","appliesTo":[]}}
|
|
89
|
+
{"id":"nn-029","label":"none","lang":"en","tags":["different-topic"],"a":{"id":"e-nn-029a","track":"memory","domain":"general","title":"Editor setup","content":"User edits frontend code in VS Code with vim bindings.","appliesTo":["coding"]},"b":{"id":"e-nn-029b","track":"memory","domain":"personal","title":"Tea stash","content":"User keeps a small stash of oolong from Fujian.","appliesTo":[]}}
|
|
90
|
+
{"id":"nn-030","label":"none","lang":"zh","tags":["different-topic"],"a":{"id":"e-nn-030a","track":"memory","domain":"general","title":"值班规则","content":"线上值班期间手机必须保持畅通。","appliesTo":["deploy"]},"b":{"id":"e-nn-030b","track":"memory","domain":"personal","title":"旧照扫描","content":"用户把家里的旧照片扫成了电子版。","appliesTo":[]}}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// 集成回归时移除:lib/lexicon.js 真身(旧版,另一代理并行实现)落地之前,
|
|
2
|
+
// 把核心层模块解析到同目录的轻量 stub。判定保持保守——真身存在时一律放行原解析。
|
|
3
|
+
// 与 tests/loader.mjs 的 resolve 映射同一机制(node:module register hooks)。
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
|
|
6
|
+
const stub = new URL('./lexicon-stub.mjs', import.meta.url)
|
|
7
|
+
const real = new URL('../../lib/lexicon.js', import.meta.url)
|
|
8
|
+
|
|
9
|
+
export function resolve(specifier, context, next) {
|
|
10
|
+
if (specifier === './lexicon.js' && String(context.parentURL || '').includes('/lib/')) {
|
|
11
|
+
if (fs.existsSync(real)) return next(specifier, context)
|
|
12
|
+
return { url: stub.href, shortCircuit: true, format: 'module' }
|
|
13
|
+
}
|
|
14
|
+
return next(specifier, context)
|
|
15
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// 集成回归时移除:lib/lexicon.js 真身落地后,删除本文件、lexicon-stub-hooks.mjs 以及
|
|
2
|
+
// tests/lexicon-ui.test.mjs 顶部的 register 调用。
|
|
3
|
+
//
|
|
4
|
+
// 轻量替身:只覆盖 P3 消费的冻结签名与四条语义——非用户来源的普通词条直接 active、
|
|
5
|
+
// sensitive trigger 的模型/curator 提案 typed rejection、非用户改敏感字段 typed
|
|
6
|
+
// rejection、active/archived 生命周期(archive/restore)。不承诺与真核心一致的锁、
|
|
7
|
+
// CAS、原子写或容量上限;真实文件落在 dataDir()/lexicon/ 下,因此备份清单与 restore
|
|
8
|
+
// allowlist 测试仍然走真实磁盘。
|
|
9
|
+
import path from 'node:path'
|
|
10
|
+
import { dataDir, ensureDir, readJson, writeJson } from '../../lib/paths.js'
|
|
11
|
+
|
|
12
|
+
const KINDS = ['slang', 'meme', 'trigger', 'term']
|
|
13
|
+
// 公开生命周期(§2.1):pending/rejected 只作为 legacy/墓碑数据存在,新写不产出。
|
|
14
|
+
const PUBLIC_STATUSES = ['active', 'archived']
|
|
15
|
+
const SENSITIVE_FIELDS = ['term', 'kind', 'sensitivity', 'context', 'meaning']
|
|
16
|
+
// 与设计 §1.2 同向的近似规范化:NFKC、大小写折叠、空白折叠、去标点/符号。
|
|
17
|
+
const normalizeLexeme = (value) => String(value ?? '')
|
|
18
|
+
.normalize('NFKC')
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/\s+/g, ' ')
|
|
21
|
+
.replace(/[\p{P}\p{S}]+/gu, '')
|
|
22
|
+
.trim()
|
|
23
|
+
|
|
24
|
+
const fileFor = (libraryId) => path.join(dataDir(), 'lexicon', `${libraryId}.json`)
|
|
25
|
+
const emptyStore = (libraryId) => ({ version: 1, revision: 0, libraryId, terms: {}, rejected: {} })
|
|
26
|
+
|
|
27
|
+
export function loadLexicon(libraryId) {
|
|
28
|
+
const raw = readJson(fileFor(libraryId), null)
|
|
29
|
+
if (!raw || typeof raw !== 'object' || raw.terms == null) return emptyStore(libraryId)
|
|
30
|
+
return {
|
|
31
|
+
version: 1,
|
|
32
|
+
revision: Number(raw.revision) || 0,
|
|
33
|
+
libraryId,
|
|
34
|
+
terms: raw.terms && typeof raw.terms === 'object' ? raw.terms : {},
|
|
35
|
+
rejected: raw.rejected && typeof raw.rejected === 'object' ? raw.rejected : {},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function saveLexicon(store) {
|
|
40
|
+
ensureDir(path.dirname(fileFor(store.libraryId)))
|
|
41
|
+
writeJson(fileFor(store.libraryId), {
|
|
42
|
+
version: store.version,
|
|
43
|
+
revision: store.revision,
|
|
44
|
+
libraryId: store.libraryId,
|
|
45
|
+
terms: store.terms,
|
|
46
|
+
rejected: store.rejected || {},
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function fail(code, message) {
|
|
51
|
+
const error = new Error(message)
|
|
52
|
+
error.code = code
|
|
53
|
+
return error
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 与 lib/context.js 的 contextVisible 同语义的精简版:P3 只依赖"越界条目搜不到"。
|
|
57
|
+
function contextVisible(context, current) {
|
|
58
|
+
if (!context) return true
|
|
59
|
+
for (const key of ['channelId', 'characterId', 'worldId', 'branchId']) {
|
|
60
|
+
if (context[key] && context[key] !== current?.[key]) return false
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(context.knownBy) && context.knownBy.length && !context.knownBy.includes(current?.characterId)) return false
|
|
63
|
+
return true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let seq = 0
|
|
67
|
+
const nextId = () => {
|
|
68
|
+
seq += 1
|
|
69
|
+
return `lex-stub-${String(seq).padStart(4, '0')}`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function sensitiveDelta(existing, next) {
|
|
73
|
+
return SENSITIVE_FIELDS.filter((field) => {
|
|
74
|
+
const before = field === 'context' ? JSON.stringify(existing.context || {}) : String(existing[field] ?? '')
|
|
75
|
+
const after = field === 'context' ? JSON.stringify(next.context || {}) : String(next[field] ?? '')
|
|
76
|
+
return before !== after
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 写入/更新:与真核心同一套公开语义(§2.1)。created:false + notOwned:true 表示
|
|
82
|
+
* 撞上了一条非用户来源不能动的词条(archived/rejected/legacy 修订草稿)。
|
|
83
|
+
*/
|
|
84
|
+
export function upsertLexiconTerm(options = {}) {
|
|
85
|
+
const libraryId = String(options.libraryId || '')
|
|
86
|
+
if (!libraryId) throw fail('LEXICON_LIBRARY_REQUIRED', 'libraryId is required')
|
|
87
|
+
const term = String(options.term || '').trim()
|
|
88
|
+
const kind = String(options.kind || 'term')
|
|
89
|
+
const meaning = String(options.meaning || '').trim()
|
|
90
|
+
if (!term) throw fail('LEXICON_TERM_REQUIRED', 'term is required')
|
|
91
|
+
if (!meaning) throw fail('LEXICON_MEANING_REQUIRED', 'meaning is required')
|
|
92
|
+
if (!KINDS.includes(kind)) throw fail('LEXICON_KIND_INVALID', `unknown lexicon kind: ${kind}`)
|
|
93
|
+
const source = String(options.source || 'user')
|
|
94
|
+
const isUser = source === 'user'
|
|
95
|
+
// trigger 默认 sensitive(§1.2)。
|
|
96
|
+
const sensitivity = kind === 'trigger' && options.sensitivity == null ? 'sensitive' : String(options.sensitivity || 'normal')
|
|
97
|
+
// §2.1:非用户来源不能建 sensitive trigger —— typed rejection。
|
|
98
|
+
if (!isUser && kind === 'trigger' && sensitivity === 'sensitive') {
|
|
99
|
+
throw fail('LEXICON_SENSITIVE_FORBIDDEN', 'sensitive triggers are user-managed; model/curator proposals are rejected')
|
|
100
|
+
}
|
|
101
|
+
// pending/rejected 不可由新调用方设置:统一收口成 active(非用户来源也不可设 archived)。
|
|
102
|
+
const requested = PUBLIC_STATUSES.includes(String(options.status)) ? String(options.status) : 'active'
|
|
103
|
+
const wantedStatus = isUser ? requested : 'active'
|
|
104
|
+
|
|
105
|
+
const store = loadLexicon(libraryId)
|
|
106
|
+
const normalizedTerm = normalizeLexeme(term)
|
|
107
|
+
const aliases = Array.isArray(options.aliases) ? options.aliases.map((a) => String(a)) : []
|
|
108
|
+
const normalizedAliases = aliases.map(normalizeLexeme).filter(Boolean)
|
|
109
|
+
const context = options.context && typeof options.context === 'object' ? { ...options.context } : {}
|
|
110
|
+
const now = new Date().toISOString()
|
|
111
|
+
// 冲突:同一规范词(term 或 alias)已存在时更新既有词条,不新建(§1.2 唯一性)。
|
|
112
|
+
// 归档条目按 term 等值参与(includeArchived 口径)。
|
|
113
|
+
let targetId = null
|
|
114
|
+
for (const [id, e] of Object.entries(store.terms)) {
|
|
115
|
+
const status = e?.status || 'active'
|
|
116
|
+
const normalizedEntryTerm = normalizeLexeme(e?.term)
|
|
117
|
+
if (status === 'archived') {
|
|
118
|
+
if (normalizedEntryTerm === normalizedTerm) { targetId = id; break }
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
const keys = new Set([normalizedEntryTerm, ...(Array.isArray(e?.normalizedAliases) ? e.normalizedAliases : []).map((a) => String(a).toLowerCase())])
|
|
122
|
+
if (keys.has(normalizedTerm) || normalizedAliases.some((a) => keys.has(a))) { targetId = id; break }
|
|
123
|
+
}
|
|
124
|
+
const created = targetId === null
|
|
125
|
+
let revived = false
|
|
126
|
+
if (!created) {
|
|
127
|
+
const e = store.terms[targetId]
|
|
128
|
+
const status = e.status || 'active'
|
|
129
|
+
// archived / rejected / legacy 修订草稿:非用户来源不复活、不改写。
|
|
130
|
+
const immutable = status === 'archived' || status === 'rejected' || (status === 'pending' && Boolean(e.revisionOf))
|
|
131
|
+
if (immutable && !isUser) return { term: e, termId: targetId, created: false, notOwned: true }
|
|
132
|
+
if (!isUser) {
|
|
133
|
+
if (e.kind === 'trigger' && e.sensitivity === 'sensitive') {
|
|
134
|
+
throw fail('LEXICON_SENSITIVE_FIELDS_FORBIDDEN', 'only the user manages sensitive triggers')
|
|
135
|
+
}
|
|
136
|
+
const changed = sensitiveDelta(e, { ...e, term, kind, sensitivity, context, meaning })
|
|
137
|
+
if (changed.length) throw fail('LEXICON_SENSITIVE_FIELDS_FORBIDDEN', `non-user writes cannot change ${changed.join(', ')}`)
|
|
138
|
+
}
|
|
139
|
+
const revivedEntry = status !== 'active'
|
|
140
|
+
e.term = term
|
|
141
|
+
e.normalizedTerm = normalizedTerm
|
|
142
|
+
e.kind = kind
|
|
143
|
+
e.meaning = meaning
|
|
144
|
+
if (options.usage != null) e.usage = String(options.usage)
|
|
145
|
+
e.aliases = aliases
|
|
146
|
+
e.normalizedAliases = normalizedAliases
|
|
147
|
+
if (options.context !== undefined) e.context = context
|
|
148
|
+
if (options.knownBy !== undefined) e.knownBy = Array.isArray(options.knownBy) ? options.knownBy.map(String) : []
|
|
149
|
+
if (options.visibility != null) e.visibility = String(options.visibility)
|
|
150
|
+
e.sensitivity = sensitivity
|
|
151
|
+
if (typeof options.strength === 'number') e.strength = options.strength
|
|
152
|
+
if (typeof options.confidence === 'number') e.confidence = options.confidence
|
|
153
|
+
e.status = isUser ? wantedStatus : 'active'
|
|
154
|
+
e.source = source
|
|
155
|
+
if (options.evidence !== undefined) e.evidence = Array.isArray(options.evidence) ? options.evidence : []
|
|
156
|
+
e.updatedAt = now
|
|
157
|
+
revived = revivedEntry
|
|
158
|
+
if (revivedEntry) {
|
|
159
|
+
delete e.revisionOf
|
|
160
|
+
delete e.previousRevision
|
|
161
|
+
e.reviewedAt = now
|
|
162
|
+
e.reviewedBy = source
|
|
163
|
+
e.reviewReason = null
|
|
164
|
+
if (status === 'rejected' && e.fingerprint) delete store.rejected[e.fingerprint]
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
targetId = nextId()
|
|
168
|
+
store.terms[targetId] = {
|
|
169
|
+
term,
|
|
170
|
+
normalizedTerm,
|
|
171
|
+
kind,
|
|
172
|
+
meaning,
|
|
173
|
+
usage: options.usage == null ? '' : String(options.usage),
|
|
174
|
+
examples: Array.isArray(options.examples) ? options.examples.map(String) : [],
|
|
175
|
+
aliases,
|
|
176
|
+
normalizedAliases,
|
|
177
|
+
context,
|
|
178
|
+
knownBy: Array.isArray(options.knownBy) ? options.knownBy.map(String) : [],
|
|
179
|
+
visibility: String(options.visibility || 'shared'),
|
|
180
|
+
sensitivity,
|
|
181
|
+
strength: typeof options.strength === 'number' ? options.strength : 0.5,
|
|
182
|
+
confidence: typeof options.confidence === 'number' ? options.confidence : null,
|
|
183
|
+
status: wantedStatus,
|
|
184
|
+
source,
|
|
185
|
+
evidence: Array.isArray(options.evidence) ? options.evidence : [],
|
|
186
|
+
sourceMemoryId: options.sourceMemoryId || null,
|
|
187
|
+
createdAt: now,
|
|
188
|
+
updatedAt: now,
|
|
189
|
+
reviewedAt: null,
|
|
190
|
+
reviewedBy: null,
|
|
191
|
+
reviewReason: null,
|
|
192
|
+
hitCounts: { candidate: 0, injection: 0, query: 0 },
|
|
193
|
+
lastInjectedAt: null,
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
store.revision += 1
|
|
197
|
+
saveLexicon(store)
|
|
198
|
+
return { term: store.terms[targetId], termId: targetId, created, ...(revived ? { revived: true } : {}) }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** 归档(§2.1):只有 active → archived;不写 rejected 墓碑。 */
|
|
202
|
+
export function archiveLexiconTerm({ libraryId, termId } = {}) {
|
|
203
|
+
const store = loadLexicon(libraryId)
|
|
204
|
+
const e = store.terms[termId]
|
|
205
|
+
if (!e) throw fail('LEXICON_NOT_FOUND', 'unknown lexicon term')
|
|
206
|
+
const status = e.status || 'active'
|
|
207
|
+
if (status !== 'active') throw fail('LEXICON_INVALID_STATUS', `only active terms can be archived; ${termId} is ${status}`)
|
|
208
|
+
e.status = 'archived'
|
|
209
|
+
e.updatedAt = new Date().toISOString()
|
|
210
|
+
store.revision += 1
|
|
211
|
+
saveLexicon(store)
|
|
212
|
+
return { term: e, termId, action: 'archive' }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** 恢复(§2.1):只有 archived → active;legacy 修订草稿拒绝独立恢复。 */
|
|
216
|
+
export function restoreLexiconTerm({ libraryId, termId } = {}) {
|
|
217
|
+
const store = loadLexicon(libraryId)
|
|
218
|
+
const e = store.terms[termId]
|
|
219
|
+
if (!e) throw fail('LEXICON_NOT_FOUND', 'unknown lexicon term')
|
|
220
|
+
const status = e.status || 'active'
|
|
221
|
+
if (status !== 'archived') throw fail('LEXICON_INVALID_STATUS', `only archived terms can be restored; ${termId} is ${status}`)
|
|
222
|
+
if (e.revisionOf && (store.terms[e.revisionOf]?.status || 'active') === 'active') {
|
|
223
|
+
throw fail('LEXICON_REVISION_IMMUTABLE', 'a legacy revision draft cannot be restored on its own')
|
|
224
|
+
}
|
|
225
|
+
const normalizedTerm = normalizeLexeme(e.term)
|
|
226
|
+
const aliases = Array.isArray(e.normalizedAliases) ? e.normalizedAliases : []
|
|
227
|
+
for (const [id, other] of Object.entries(store.terms)) {
|
|
228
|
+
if (id === termId) continue
|
|
229
|
+
const otherStatus = other.status || 'active'
|
|
230
|
+
if (otherStatus !== 'active' && otherStatus !== 'pending') continue
|
|
231
|
+
const otherAliases = Array.isArray(other.normalizedAliases) ? other.normalizedAliases : []
|
|
232
|
+
if (other.normalizedTerm === normalizedTerm || otherAliases.includes(normalizedTerm)
|
|
233
|
+
|| aliases.includes(other.normalizedTerm) || otherAliases.some((alias) => aliases.includes(alias))) {
|
|
234
|
+
throw fail('LEXICON_ALIAS_CONFLICT', `restoring ${termId} would collide with ${id}`)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
e.status = 'active'
|
|
238
|
+
e.updatedAt = new Date().toISOString()
|
|
239
|
+
store.revision += 1
|
|
240
|
+
saveLexicon(store)
|
|
241
|
+
return { term: e, termId, action: 'restore' }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function deleteLexiconTerm({ libraryId, termId } = {}) {
|
|
245
|
+
const store = loadLexicon(libraryId)
|
|
246
|
+
if (!store.terms[termId]) throw fail('LEXICON_TERM_NOT_FOUND', 'unknown lexicon term')
|
|
247
|
+
delete store.terms[termId]
|
|
248
|
+
store.revision += 1
|
|
249
|
+
saveLexicon(store)
|
|
250
|
+
return { termId, deleted: true }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function searchLexicon(options = {}, opts = {}) {
|
|
254
|
+
const libraryId = String(options.libraryId || '')
|
|
255
|
+
const includeArchived = opts?.includeArchived === true || options.includeArchived === true
|
|
256
|
+
const store = loadLexicon(libraryId)
|
|
257
|
+
const query = normalizeLexeme(options.query)
|
|
258
|
+
if (!query) return []
|
|
259
|
+
const memoryContext = options.memoryContext || {}
|
|
260
|
+
const hits = []
|
|
261
|
+
for (const [termId, e] of Object.entries(store.terms)) {
|
|
262
|
+
const status = e.status || 'active'
|
|
263
|
+
if (status !== 'active' && !(includeArchived && status === 'archived')) continue
|
|
264
|
+
if (!contextVisible(e.context, memoryContext)) continue
|
|
265
|
+
const haystack = [normalizeLexeme(e.term), ...(Array.isArray(e.normalizedAliases) ? e.normalizedAliases : []).map(normalizeLexeme), normalizeLexeme(e.meaning)].filter(Boolean)
|
|
266
|
+
if (!haystack.some((value) => value.includes(query) || query.includes(value))) continue
|
|
267
|
+
hits.push({ id: termId, ...e, matchedBy: 'exact' })
|
|
268
|
+
if (hits.length >= 8) break
|
|
269
|
+
}
|
|
270
|
+
return hits
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** 可见词条:active 默认;includeArchived 追加 archived(§2.1)。pending/rejected 永不返回。 */
|
|
274
|
+
export function visibleLexiconTerms(libraryId, memoryContext = {}, opts = {}) {
|
|
275
|
+
const includeArchived = opts?.includeArchived === true
|
|
276
|
+
const store = loadLexicon(libraryId)
|
|
277
|
+
return Object.entries(store.terms)
|
|
278
|
+
.filter(([, e]) => {
|
|
279
|
+
const status = e.status || 'active'
|
|
280
|
+
return status === 'active' || (includeArchived && status === 'archived')
|
|
281
|
+
})
|
|
282
|
+
.filter(([, e]) => contextVisible(e.context, memoryContext))
|
|
283
|
+
.map(([id, e]) => ({ termId: id, id, ...e }))
|
|
284
|
+
}
|