@jjh32456/dsh-longlive 1.6.7 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/CHANGELOG.md +70 -1508
  2. package/LICENSE +1 -1
  3. package/QUICKSTART.md +34 -13
  4. package/README.en.md +18 -7
  5. package/README.md +34 -9
  6. package/SPEC.md +5 -5
  7. package/cordis.patch.yml +23 -9
  8. package/docs/ANSWER-GUARD.md +92 -0
  9. package/docs/ARCHITECTURE.md +4 -3
  10. package/docs/AUTO-RECALL-WIRING.md +1 -1
  11. package/docs/AUTO-RECALL.md +15 -9
  12. package/docs/CONFIG.md +168 -35
  13. package/docs/ERRORS.md +17 -5
  14. package/docs/FAQ.md +7 -7
  15. package/docs/FEATURES.md +96 -28
  16. package/docs/MIGRATION-1.6.0.md +2 -2
  17. package/docs/OPERATIONS.md +6 -6
  18. package/docs/PANEL-INTEGRATION.md +6 -0
  19. package/docs/PRIVACY.md +88 -0
  20. package/docs/QUICKSTART.md +5 -5
  21. package/docs/README.en.md +4 -2
  22. package/docs/RESIDENT-WATCHER.md +1 -1
  23. package/docs/SESSION-WATCH.md +6 -6
  24. package/docs/TROUBLESHOOTING.md +4 -4
  25. package/package.json +47 -117
  26. package/runbook.md +2 -2
  27. package/scripts/backfill-auto-cards.mjs +98 -0
  28. package/scripts/install-surgery-task.ps1 +5 -2
  29. package/scripts/lib/session-watch-fixture.mjs +1 -1
  30. package/scripts/longlive-doctor.mjs +6 -0
  31. package/scripts/migrate-180.mjs +163 -0
  32. package/scripts/recall-index-build.mjs +128 -0
  33. package/scripts/session-watch-drill.mjs +1 -1
  34. package/scripts/surgery-run.cmd +1 -1
  35. package/scripts/unattended-surgery.ps1 +5 -2
  36. package/scripts/verify-card-anchors.mjs +138 -0
  37. package/src/actions/audit.js +125 -0
  38. package/src/actions/evidence.js +122 -0
  39. package/src/actions/fs.js +162 -0
  40. package/src/activity/index.js +760 -0
  41. package/src/activity/model.js +351 -0
  42. package/src/activity/store.js +644 -0
  43. package/src/activity/thinking.js +176 -0
  44. package/src/adapters/constants.js +11 -0
  45. package/src/adapters/engram/data/aliases.json +51 -0
  46. package/src/adapters/engram/data/policy.json +8 -0
  47. package/src/adapters/engram/data/routing-rules.json +546 -0
  48. package/src/adapters/engram/index.js +235 -0
  49. package/src/adapters/registry.js +315 -0
  50. package/src/answer/audit.js +193 -0
  51. package/src/answer/evidence.js +30 -0
  52. package/src/answer/guard.js +1370 -0
  53. package/src/answer/index.js +1067 -0
  54. package/src/answer/query.js +111 -0
  55. package/src/answer/second-hop.mjs +101 -0
  56. package/src/auto-truncate.js +1 -1
  57. package/src/config.js +171 -47
  58. package/src/deps.js +6 -4
  59. package/src/features.js +182 -6
  60. package/src/heartbeat.js +156 -23
  61. package/src/identity/handover.js +52 -1
  62. package/src/identity/index.js +13 -5
  63. package/src/identity/inject-position.js +2 -2
  64. package/src/index.js +97 -7
  65. package/src/lifecycle/gap-clocks.js +328 -0
  66. package/src/lifecycle/index.js +269 -0
  67. package/src/live.js +22 -2
  68. package/src/memory/activity-capture.js +229 -0
  69. package/src/memory/anchors.js +194 -0
  70. package/src/memory/audit-records.js +222 -0
  71. package/src/memory/auto-cards.js +401 -0
  72. package/src/memory/capture.js +442 -0
  73. package/src/memory/card-anchors.js +203 -0
  74. package/src/memory/conflicts.js +106 -0
  75. package/src/memory/decay.js +311 -0
  76. package/src/memory/distill.js +471 -0
  77. package/src/memory/event-index.js +917 -0
  78. package/src/memory/evidence-chain.js +295 -0
  79. package/src/memory/evidence.js +145 -0
  80. package/src/memory/filter-runner.js +1 -1
  81. package/src/memory/filter.js +5 -1
  82. package/src/memory/forget-v2.js +410 -0
  83. package/src/memory/forget.js +71 -0
  84. package/src/memory/frames-activation.js +247 -0
  85. package/src/memory/fts-query.js +174 -0
  86. package/src/memory/governance.js +604 -0
  87. package/src/memory/index.js +1 -0
  88. package/src/memory/l2-gate.js +169 -0
  89. package/src/memory/lock.js +121 -0
  90. package/src/memory/migrate.js +139 -0
  91. package/src/memory/recall-v2.js +223 -0
  92. package/src/memory/recall.js +65 -2
  93. package/src/memory/records.js +425 -0
  94. package/src/memory/retention.js +128 -0
  95. package/src/memory/review.js +124 -0
  96. package/src/memory/trust.js +88 -0
  97. package/src/memory/version-chain.js +264 -0
  98. package/src/memory/wal.js +137 -0
  99. package/src/memory/working-frame.js +516 -0
  100. package/src/metrics/classify.mjs +5 -3
  101. package/src/metrics/index.mjs +21 -1
  102. package/src/metrics/prestep-latency.mjs +300 -0
  103. package/src/metrics/store.mjs +25 -0
  104. package/src/policy-profile.js +18 -3
  105. package/src/recall/anaphora.mjs +87 -0
  106. package/src/recall/cards.mjs +7 -0
  107. package/src/recall/embedding.mjs +8 -8
  108. package/src/recall/entities.mjs +112 -0
  109. package/src/recall/index.js +235 -19
  110. package/src/recall/inject-order.mjs +10 -8
  111. package/src/recall/privacy.mjs +105 -16
  112. package/src/recall/recall-core.mjs +116 -10
  113. package/src/recall/shared-visibility.js +302 -0
  114. package/src/recall/time-normalizer.mjs +196 -0
  115. package/src/role/activation.js +191 -0
  116. package/src/role/attention.js +318 -0
  117. package/src/role/base.js +36 -0
  118. package/src/role/channels.js +423 -0
  119. package/src/role/index.js +425 -0
  120. package/src/role/interfaces.js +115 -0
  121. package/src/role/personality.js +370 -0
  122. package/src/role/readers.js +151 -0
  123. package/src/role/relationships.js +477 -0
  124. package/src/role/store.js +202 -0
  125. package/src/role/writeprotect.js +373 -0
  126. package/src/router/aliases.json +176 -25
  127. package/src/router/background/agent-runner.mjs +1 -1
  128. package/src/router/background/anchor.mjs +1 -1
  129. package/src/router/background/audit.mjs +2 -2
  130. package/src/router/background/budget-ledger.mjs +2 -2
  131. package/src/router/background/host-sim.mjs +1 -1
  132. package/src/router/background/memory.mjs +4 -4
  133. package/src/router/background/queue.mjs +1 -1
  134. package/src/router/background/retry.mjs +1 -1
  135. package/src/router/background/router.mjs +1 -1
  136. package/src/router/background/stream.mjs +1 -1
  137. package/src/router/background/tool-policy.mjs +1 -1
  138. package/src/router/design-headings.json +5 -5
  139. package/src/router/face/config.mjs +1 -1
  140. package/src/router/face/identity.mjs +3 -3
  141. package/src/router/judge.mjs +8 -7
  142. package/src/router/policies/generic.json +21 -4
  143. package/src/router/route.js +109 -22
  144. package/src/router/router.mjs +10 -6
  145. package/src/router/routing-rules.json +260 -445
  146. package/src/router/run-sim.mjs +8 -8
  147. package/src/router/runtime.js +23 -12
  148. package/src/router/service/board.mjs +95 -15
  149. package/src/router/service/config.mjs +5 -5
  150. package/src/router/service/context.mjs +1 -1
  151. package/src/router/service/hint.mjs +14 -0
  152. package/src/router/service/queue.mjs +1 -1
  153. package/src/router/service/service.mjs +27 -11
  154. package/src/router/shadow.mjs +4 -3
  155. package/src/surgery-recovery.js +2 -2
  156. package/src/tool-output.js +2 -2
  157. package/src/tools.js +39 -7
  158. package/src/truncate-visible.js +2 -2
  159. package/src/ui/actions.js +1312 -0
  160. package/src/ui/panel-api.js +367 -0
  161. package/src/ui/panel-auth.js +251 -0
  162. package/src/ui/panel-integrity.js +243 -0
  163. package/src/ui/routes-readonly.js +695 -0
  164. package/src/ui/routes.js +3 -0
  165. package/docs/DEV.md +0 -279
  166. package/docs/PRESENCE-BUS-WIRING.md +0 -491
  167. package/docs/PRESENCE-BUS.md +0 -694
  168. package/docs/SURGERY.md +0 -186
  169. package/scripts/mark-core-cards.mjs +0 -289
  170. package/scripts/pack.mjs +0 -392
  171. package/scripts/router-proposals.mjs +0 -386
  172. package/src/router/fixtures/M1.json +0 -16
  173. package/src/router/fixtures/M2.json +0 -16
  174. package/src/router/fixtures/M3.json +0 -20
  175. package/src/router/fixtures/M4.json +0 -20
  176. package/src/router/fixtures/M5.json +0 -45
  177. package/src/router/fixtures/M6.json +0 -53
  178. package/src/router/fixtures/M7.json +0 -46
  179. package/src/router/fixtures/S1.json +0 -20
  180. package/src/router/fixtures/S2.json +0 -23
  181. package/src/router/fixtures/S3.json +0 -20
  182. package/src/router/fixtures/S4.json +0 -34
  183. package/src/router/fixtures/S5.json +0 -43
  184. package/src/router/fixtures/S6.json +0 -47
  185. package/src/router/fixtures/T1.json +0 -23
  186. package/src/router/fixtures/T2.json +0 -16
  187. package/src/router/fixtures/T3.json +0 -19
  188. package/src/router/fixtures/T4.json +0 -16
  189. package/src/router/fixtures/X1.json +0 -17
  190. package/src/router/fixtures/X2.json +0 -20
  191. package/src/router/gen-design-headings.mjs +0 -107
  192. package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
  193. package/test/auto-recall.test.mjs +0 -474
  194. package/test/auto-truncate.test.mjs +0 -931
  195. package/test/autobind.test.mjs +0 -385
  196. package/test/bus.test.mjs +0 -1572
  197. package/test/cli-guard.test.mjs +0 -198
  198. package/test/conclusion.test.mjs +0 -408
  199. package/test/config-schema.test.mjs +0 -204
  200. package/test/d6-output.test.mjs +0 -194
  201. package/test/d6.test.mjs +0 -395
  202. package/test/deps.test.mjs +0 -342
  203. package/test/doctor-setup.test.mjs +0 -297
  204. package/test/embedding-recall.test.mjs +0 -347
  205. package/test/feature-flags.test.mjs +0 -568
  206. package/test/filter-shadow.test.mjs +0 -170
  207. package/test/fixtures/baseline-0.8.0-registration.json +0 -556
  208. package/test/fixtures/d6-header-tools.json +0 -1
  209. package/test/fixtures/lock-child.mjs +0 -67
  210. package/test/fixtures/pre-step-container.mjs +0 -100
  211. package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
  212. package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
  213. package/test/fixtures/profile-session-longlive-config.json +0 -20
  214. package/test/fixtures/proposal-cas-child.mjs +0 -66
  215. package/test/host-engine-status.test.mjs +0 -307
  216. package/test/hot-truncate.test.mjs +0 -1349
  217. package/test/identity-card-marker.test.mjs +0 -307
  218. package/test/identity-router/run.mjs +0 -721
  219. package/test/integration-pre-step.test.mjs +0 -553
  220. package/test/live.test.mjs +0 -1020
  221. package/test/maintenance-schedule.test.mjs +0 -538
  222. package/test/memory.test.mjs +0 -2828
  223. package/test/metrics.test.mjs +0 -478
  224. package/test/orchestrator.test.mjs +0 -266
  225. package/test/p1_probe.mjs +0 -97
  226. package/test/packaging.test.mjs +0 -182
  227. package/test/portability.test.mjs +0 -343
  228. package/test/rc4-regressions.test.mjs +0 -562
  229. package/test/rc5-regressions.test.mjs +0 -197
  230. package/test/rc6-chinese.test.mjs +0 -394
  231. package/test/router-1100/apply-off.test.mjs +0 -175
  232. package/test/router-1100/budget-reconcile.test.mjs +0 -215
  233. package/test/router-1100/config-context.test.mjs +0 -211
  234. package/test/router-1100/helpers.mjs +0 -129
  235. package/test/router-1100/hint-envelope.test.mjs +0 -150
  236. package/test/router-1100/lock-crossproc.test.mjs +0 -206
  237. package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
  238. package/test/router-1100/router-apply.test.mjs +0 -372
  239. package/test/router-1100/router-fixb.test.mjs +0 -602
  240. package/test/router-1100/router-proposals.test.mjs +0 -405
  241. package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
  242. package/test/router-1100/router-security-cr.test.mjs +0 -333
  243. package/test/router-1100/service-lifecycle.test.mjs +0 -220
  244. package/test/router-1100/tools-contract.test.mjs +0 -164
  245. package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
  246. package/test/router-1100/v6-contract.test.mjs +0 -158
  247. package/test/router-1100/v6b-contract.test.mjs +0 -293
  248. package/test/router-1100/v6c-contract.test.mjs +0 -249
  249. package/test/router-1100/wal-rotation.test.mjs +0 -375
  250. package/test/router-1100/wp1-config.test.mjs +0 -170
  251. package/test/router-1100/wp1-default-off.test.mjs +0 -77
  252. package/test/router-1100/wp1-model-status.test.mjs +0 -90
  253. package/test/router-1100/wp1-status.test.mjs +0 -220
  254. package/test/router-1100/wp1-tools.test.mjs +0 -146
  255. package/test/router-1100/wp5-cli.test.mjs +0 -240
  256. package/test/router-1100/wp5-model-auto.test.mjs +0 -418
  257. package/test/router-1100/wp6-merge.test.mjs +0 -386
  258. package/test/router-1100/wp6-wake.test.mjs +0 -502
  259. package/test/router-background/audit.test.mjs +0 -37
  260. package/test/router-background/budget-ledger.test.mjs +0 -330
  261. package/test/router-background/cross-process.test.mjs +0 -222
  262. package/test/router-background/fixtures/cross-child.mjs +0 -85
  263. package/test/router-background/fixtures/real-shapes.mjs +0 -239
  264. package/test/router-background/router.test.mjs +0 -1044
  265. package/test/router-background/stream-retry.test.mjs +0 -358
  266. package/test/router-background/tool-agent.test.mjs +0 -384
  267. package/test/router-face/bridge.test.mjs +0 -169
  268. package/test/router-face/face.test.mjs +0 -316
  269. package/test/router-failclosed.test.mjs +0 -466
  270. package/test/router-identity.test.mjs +0 -384
  271. package/test/router-l1-l2.contract.test.mjs +0 -1075
  272. package/test/router-memory-tools.test.mjs +0 -262
  273. package/test/router-policy.test.mjs +0 -225
  274. package/test/router-runtime.test.mjs +0 -438
  275. package/test/router-shadow.test.mjs +0 -287
  276. package/test/router.test.mjs +0 -903
  277. package/test/run-rc.mjs +0 -299
  278. package/test/run.mjs +0 -2469
  279. package/test/safety-defaults.test.mjs +0 -179
  280. package/test/seeded-load.test.mjs +0 -436
  281. package/test/session-original.test.mjs +0 -338
  282. package/test/surgery-recovery.test.mjs +0 -936
  283. package/test/t7_drive.mjs +0 -151
  284. package/test/tool-output.test.mjs +0 -393
  285. package/test/tool-schema.test.mjs +0 -177
  286. package/test/tools_contract.test.mjs +0 -716
  287. package/test/truncate-visible.test.mjs +0 -1150
  288. package/test/ui-flags.test.mjs +0 -477
  289. package/test/v4-hardening.test.mjs +0 -427
@@ -1,474 +0,0 @@
1
- /**
2
- * 自测:node --test test/
3
- * 只用内联卡做断言,不依赖 X:\memory-store(真实卡的集成断言会 detect 到 H: 时补跑)。
4
- */
5
-
6
- import test from 'node:test';
7
- import assert from 'node:assert/strict';
8
- import fs from 'node:fs';
9
- import { fileURLToPath } from 'node:url';
10
-
11
- import {
12
- DEFAULTS,
13
- PRIVACY_HIDDEN_MARK,
14
- buildQueryText,
15
- buildUnits,
16
- chooseRecall,
17
- clipToTokens,
18
- containment,
19
- defaultPrivacyScan,
20
- estimateTokens,
21
- phraseWeight,
22
- pickExcerpt,
23
- renderInjection,
24
- scoreCard,
25
- } from '../src/recall/recall-core.mjs';
26
- import { privacyGateStatus, privacyScan } from '../src/recall/privacy.mjs';
27
- import {
28
- cardPathsInMessage,
29
- identityInjectedPaths,
30
- isRealUserMessage,
31
- mergeInjectionOrder,
32
- } from '../src/recall/inject-order.mjs';
33
-
34
- const CARD_A = {
35
- path: 'topics/a.md',
36
- title: 'A 卡',
37
- scopeIn: ['沙箱', '0xC0000142', 'pwsh'],
38
- aliases: ['工具链与沙箱'],
39
- body: [
40
- '# A 卡',
41
- '',
42
- '## 入口',
43
- '- **pwsh**:沙箱模式下进程起不来(0xC0000142),shell 调用需要 danger-full-access。',
44
- '- 另一条无关的话:今天的天气不错。',
45
- '',
46
- '## 隐私段',
47
- '- 本人男 32 岁。**⚠ 隐私:这两行不外传。**',
48
- ].join('\n'),
49
- };
50
-
51
- const CARD_B = {
52
- path: 'topics/b.md',
53
- title: 'B 卡',
54
- scopeIn: ['LUFS', '响度'],
55
- aliases: ['耳朵'],
56
- body: ['# B 卡', '- 响度 -15.24 LUFS / +0.22 dBTP 超标。'].join('\n'),
57
- };
58
-
59
- const CARD_LOCAL = {
60
- path: 'topics/secret.md',
61
- title: '只在本地',
62
- scopeIn: ['沙箱'],
63
- localOnly: true,
64
- body: '- 沙箱相关但不能外发。',
65
- };
66
-
67
- test('phraseWeight 与 e1 一致', () => {
68
- assert.equal(phraseWeight('ab'), 1);
69
- assert.equal(phraseWeight('abcd'), 1.5);
70
- assert.equal(phraseWeight('abcdef'), 2);
71
- assert.equal(phraseWeight('abcdefgh'), 2.5);
72
- assert.equal(phraseWeight('abcdefghijkl'), 2.5);
73
- });
74
-
75
- test('没命中就什么都不注入', () => {
76
- const r = chooseRecall({ queryText: '今天晚饭吃什么', cards: [CARD_A, CARD_B], budget: 1200 });
77
- assert.deepEqual(r.items, []);
78
- assert.equal(r.tokens, 0);
79
- assert.equal(r.hits.eligible, 0);
80
- assert.equal(renderInjection(r), '');
81
- });
82
-
83
- test('空查询 / 空卡表不报错', () => {
84
- assert.deepEqual(chooseRecall({ queryText: '', cards: [CARD_A] }).items, []);
85
- assert.deepEqual(chooseRecall({ queryText: '沙箱', cards: [] }).items, []);
86
- assert.deepEqual(chooseRecall().items, []);
87
- });
88
-
89
- test('命中后只给标题与摘要,摘要里保留命中的那一块', () => {
90
- const r = chooseRecall({ queryText: 'pwsh 沙箱 0xC0000142', cards: [CARD_A, CARD_B], budget: 1200 });
91
- assert.equal(r.items.length, 1);
92
- assert.equal(r.items[0].path, 'topics/a.md');
93
- assert.match(r.items[0].excerpt, /0xC0000142/);
94
- assert.doesNotMatch(r.items[0].excerpt, /天气/); // 无关块不该抢预算
95
- assert.ok(r.tokens > 0);
96
- });
97
-
98
- test('隐私块整块剔除,local_only 整卡跳过', () => {
99
- const r = chooseRecall({ queryText: '沙箱 0xC0000142', cards: [CARD_A], budget: 1200 });
100
- const text = renderInjection(r);
101
- assert.doesNotMatch(text, /32 岁|不外传|隐私/);
102
-
103
- const lr = chooseRecall({ queryText: '沙箱', cards: [CARD_LOCAL], budget: 1200 });
104
- assert.deepEqual(lr.items, []);
105
- assert.equal(lr.hits.droppedLocalOnly, 1);
106
- });
107
-
108
- test('预算硬约束:token 不超 budget,且预算越小摘要越短', () => {
109
- const q = 'pwsh 沙箱 0xC0000142 LUFS 响度 耳朵';
110
- const big = chooseRecall({ queryText: q, cards: [CARD_A, CARD_B], budget: 400 });
111
- const small = chooseRecall({ queryText: q, cards: [CARD_A, CARD_B], budget: 120 });
112
- assert.ok(big.tokens <= 400, `big.tokens=${big.tokens}`);
113
- assert.ok(small.tokens <= 120, `small.tokens=${small.tokens}`);
114
- assert.ok(small.tokens <= big.tokens);
115
- assert.equal(small.items.length, 1, '预算只够一张卡时不硬塞第二张');
116
- });
117
-
118
- test('maxItems 生效', () => {
119
- const r = chooseRecall({ queryText: '沙箱 pwsh LUFS 响度', cards: [CARD_A, CARD_B], budget: 1200, options: { maxItems: 1 } });
120
- assert.equal(r.items.length, 1);
121
- });
122
-
123
- test('渲染后的 token 一定不超预算(含标题/路径/相关度元数据)', () => {
124
- for (const budget of [60, 120, 200, 400, 900]) {
125
- const r = chooseRecall({
126
- queryText: 'pwsh 沙箱 0xC0000142 LUFS 响度 耳朵',
127
- cards: [CARD_A, CARD_B],
128
- budget,
129
- });
130
- assert.ok(r.tokens <= budget, `budget=${budget} tokens=${r.tokens}`);
131
- assert.equal(r.tokens, estimateTokens(renderInjection(r)));
132
- }
133
- });
134
-
135
- test('去重:已注入过的路径 / 已注入过的同文摘要都不再注入', () => {
136
- const seen = chooseRecall({
137
- queryText: 'pwsh 沙箱 0xC0000142',
138
- cards: [CARD_A],
139
- budget: 1200,
140
- seenPaths: ['topics\\a.md'],
141
- });
142
- assert.deepEqual(seen.items, []);
143
- assert.equal(seen.hits.droppedBySeen, 1);
144
-
145
- const first = chooseRecall({ queryText: 'pwsh 沙箱 0xC0000142', cards: [CARD_A], budget: 1200 });
146
- const second = chooseRecall({
147
- queryText: 'pwsh 沙箱 0xC0000142',
148
- cards: [CARD_A],
149
- budget: 1200,
150
- injectedTexts: [renderInjection(first)],
151
- });
152
- assert.deepEqual(second.items, []);
153
- assert.equal(second.hits.droppedByDedupe, 1);
154
- });
155
-
156
- test('containment 是「候选被已注入文本覆盖的比例」', () => {
157
- assert.equal(containment('沙箱模式下进程起不来', '废话废话废话废话'), 0);
158
- assert.ok(containment('沙箱模式下进程起不来', '沙箱模式下进程起不来,需要 danger-full-access 才行') > 0.9);
159
- });
160
-
161
- test('排序:分数高者在前,同分按路径稳定排序', () => {
162
- const r = chooseRecall({ queryText: '沙箱 pwsh LUFS', cards: [CARD_B, CARD_A], budget: 1200 });
163
- assert.deepEqual(r.items.map((i) => i.path), ['topics/a.md', 'topics/b.md']);
164
- const again = chooseRecall({ queryText: '沙箱 pwsh LUFS', cards: [CARD_B, CARD_A], budget: 1200 });
165
- assert.deepEqual(again.items, r.items, '同样输入必须同样输出');
166
- });
167
-
168
- test('buildQueryText:首条 + 最近 2 条 + cwd,且总长受限', () => {
169
- const q = buildQueryText({
170
- firstUserText: '第一句',
171
- recentTexts: ['一', '二', '三'],
172
- cwd: 'C:\\work\\x',
173
- maxRecent: 2,
174
- maxChars: 200,
175
- });
176
- assert.match(q, /第一句/);
177
- assert.match(q, /三/);
178
- assert.doesNotMatch(q, /【最近第 1 条】一/);
179
- assert.match(q, /C:\\work\\x/);
180
- assert.ok(q.length <= 200);
181
-
182
- const capped = buildQueryText({ firstUserText: 'x'.repeat(5000), maxChars: 300 });
183
- assert.ok(capped.length <= 300);
184
- });
185
-
186
- test('estimateTokens 是上界口径、单调不降', () => {
187
- assert.ok(estimateTokens('中文四字') >= 4);
188
- assert.ok(estimateTokens('abcdefgh') >= 2);
189
- assert.ok(estimateTokens('中文四字 and more') >= estimateTokens('中文四字'));
190
- assert.equal(estimateTokens(''), 0);
191
- });
192
-
193
- test('clipToTokens 只裁不扩', () => {
194
- const s = '一二三四五六七八九十'.repeat(20);
195
- const clipped = clipToTokens(s, 30);
196
- assert.ok(estimateTokens(clipped) <= 30);
197
- assert.ok(clipped.endsWith('…'));
198
- assert.equal(clipToTokens('短句', 100), '短句');
199
- });
200
-
201
- test('buildUnits 丢掉标题与分隔线,保留 bullet 与续行', () => {
202
- const units = buildUnits('# T\n\n## S\n- a\n 续行\n---\n- b\n');
203
- assert.ok(units.every((u) => !u.text.startsWith('#')));
204
- assert.equal(units.length, 2);
205
- assert.match(units[0].text, /续行/);
206
- });
207
-
208
- test('pickExcerpt 永不返回空串(长块也会被裁进预算)', () => {
209
- const card = { path: 'x', title: 'x', body: '- ' + '很长的句子'.repeat(200) };
210
- const ex = pickExcerpt(card, [], 40);
211
- assert.ok(ex.length > 0);
212
- assert.ok(estimateTokens(ex) <= 40);
213
- });
214
-
215
- test('默认预算在设计的 800–2000 区间', () => {
216
- assert.ok(DEFAULTS.budgetTokens >= 800 && DEFAULTS.budgetTokens <= 2000);
217
- });
218
-
219
- test('scoreCard 只做字面包含(不做同义词)', () => {
220
- const hit = scoreCard('沙箱里起不来', CARD_A);
221
- assert.equal(hit.hitCount, 1);
222
- const miss = scoreCard('容器里起不来', CARD_A);
223
- assert.equal(miss.hitCount, 0);
224
- });
225
-
226
- /* 真实卡集成断言:只在 H 盘存在时跑(CI/别的机器上跳过) */
227
- const REAL = 'X:/memory-store/topics';
228
- test('真实 people.md 的隐私两行不会进摘要', { skip: !fs.existsSync(REAL) }, async () => {
229
- const { loadCards } = await import('../src/recall/cards.mjs');
230
- const cards = loadCards('X:/memory-store');
231
- const q = '他是我主人 鲸鲸 大肥鱼 伙伴 他说';
232
- const r = chooseRecall({ queryText: q, cards, budget: 1200 });
233
- const text = renderInjection(r);
234
- assert.doesNotMatch(text, /32 岁|湖北|不外传/);
235
- assert.ok(r.tokens <= 1200);
236
- });
237
-
238
- test('真实卡里 local_only 若有则一张都不注入', { skip: !fs.existsSync(REAL) }, async () => {
239
- const { loadCards } = await import('../src/recall/cards.mjs');
240
- const cards = loadCards('X:/memory-store');
241
- const locals = cards.filter((c) => c.localOnly);
242
- const r = chooseRecall({ queryText: cards.map((c) => [...c.scopeIn, ...c.aliases].join(' ')).join(' '), cards, budget: 1200 });
243
- for (const c of locals) assert.ok(!r.items.some((i) => i.path === c.path));
244
- });
245
-
246
- /* ================================================================== *
247
- * mustFix #2:插入顺序(顺序 B 也不能重复注入开机卡)
248
- * ================================================================== */
249
-
250
- const BOOT_TEXT = [
251
- '【开机卡】上次停在 auto-recall 的隐私门。',
252
- '完整交接:X:\\memory-store\\handover\\2026-11-01-x.md',
253
- ].join('\n');
254
-
255
- /* 旧版越界 source.kind 的兼容夹具:生产代码已不再写这些 kind,
256
- 仅用于验证历史消息仍能被识别 / 合并(新契约见 integration-pre-step)。 */
257
- function legacyBootMessage(text = BOOT_TEXT, source = {}) {
258
- return {
259
- role: 'user',
260
- content: [{ type: 'text', text }],
261
- source: {
262
- kind: 'plugin:dsh-identity-router',
263
- form: 'snapshot',
264
- sections: [{ name: 'identity-router.open-card', text }],
265
- ...source,
266
- },
267
- };
268
- }
269
-
270
- function legacyAutoMessage(text = '【自动想起】x') {
271
- return {
272
- role: 'user',
273
- content: [{ type: 'text', text }],
274
- source: { kind: 'plugin:dsh-auto-recall', form: 'snapshot', sections: [{ name: 'auto-recall.cards', text }] },
275
- };
276
- }
277
-
278
- test('顺序:auto-recall 消息排在开机卡之后,重复合并只剩一条', () => {
279
- const boot = legacyBootMessage();
280
- const once = mergeInjectionOrder([boot], legacyAutoMessage('auto-1'));
281
- assert.deepEqual(once.map((m) => m.source.kind), ['plugin:dsh-identity-router', 'plugin:dsh-auto-recall']);
282
-
283
- // 顺序 B 的极端形态:auto 先注入过一条,identity 后跑又在末尾追加了开机卡。
284
- const orderB = [legacyAutoMessage('auto-1'), boot];
285
- const fixed = mergeInjectionOrder(orderB, legacyAutoMessage('auto-2'));
286
- assert.equal(fixed.filter((m) => m.source.kind === 'plugin:dsh-auto-recall').length, 1);
287
- assert.equal(fixed[0].source.kind, 'plugin:dsh-identity-router', '开机卡必须在前');
288
- assert.equal(fixed[1].source.kind, 'plugin:dsh-auto-recall');
289
- assert.equal(fixed.length, 2);
290
-
291
- // 幂等:同一消息 merge 两次,长度不变。
292
- assert.equal(mergeInjectionOrder(fixed, fixed[1]).length, 2);
293
- });
294
-
295
- test('顺序:没有开机卡时追加到末尾,不插到用户消息前面', () => {
296
- const userMsg = { role: 'user', content: [{ type: 'text', text: '你好' }] };
297
- const out = mergeInjectionOrder([userMsg], legacyAutoMessage());
298
- assert.deepEqual(out.map((m) => m.role), ['user', 'user']);
299
- assert.equal(out[1].source.kind, 'plugin:dsh-auto-recall');
300
- });
301
-
302
- test('firstUserText 口径:isRealUserMessage 排除注入消息与 runtime-context', () => {
303
- const userMsg = { role: 'user', content: [{ type: 'text', text: '真实提问' }], source: { kind: 'user' } };
304
- assert.equal(isRealUserMessage(userMsg), true);
305
-
306
- // v4 注入契约:kind=session-reference(隐藏上下文),不是用户的话。
307
- const injectedRef = {
308
- role: 'user',
309
- content: [{ type: 'text', text: BOOT_TEXT }],
310
- source: { kind: 'session-reference', form: 'recall', version: 1, references: [] },
311
- };
312
- assert.equal(isRealUserMessage(injectedRef), false);
313
- // 历史消息 / 外部注入:kind='user' + 正文标记同样排除。
314
- const injectedBoot = { role: 'user', content: [{ type: 'text', text: BOOT_TEXT }], source: { kind: 'user' } };
315
- assert.equal(isRealUserMessage(injectedBoot), false);
316
- const injectedAuto = { role: 'user', content: [{ type: 'text', text: '【自动想起】x' }], source: { kind: 'user' } };
317
- assert.equal(isRealUserMessage(injectedAuto), false);
318
-
319
- // 旧版 plugin:* kind 同样排除。
320
- assert.equal(isRealUserMessage(legacyBootMessage()), false);
321
- // runtime-context 是宿主上下文,不是用户输入。
322
- assert.equal(isRealUserMessage({ role: 'user', content: [{ type: 'text', text: 'runtime' }], source: { kind: 'runtime-context' } }), false);
323
- // assistant / tool 消息一律不算。
324
- assert.equal(isRealUserMessage({ role: 'assistant', content: [{ type: 'text', text: '回答' }] }), false);
325
- });
326
-
327
- test('路径级排除:开机卡里出现的 handover 路径进 seenPaths,同一张卡不再注入', () => {
328
- const card = {
329
- path: 'handover/2026-11-01-x.md',
330
- dir: 'handover',
331
- title: '接手卡',
332
- scopeIn: ['沙箱'],
333
- aliases: [],
334
- body: '- 沙箱备忘:auto-recall 的门已经接好。',
335
- };
336
- const boot = legacyBootMessage();
337
- const paths = identityInjectedPaths([boot], { cards: [card] });
338
- assert.deepEqual(paths, ['handover/2026-11-01-x.md']);
339
-
340
- const without = chooseRecall({ queryText: '沙箱', cards: [card], budget: 1200 });
341
- assert.equal(without.items.length, 1, '不排除时本来会注入');
342
- const withSeen = chooseRecall({ queryText: '沙箱', cards: [card], budget: 1200, seenPaths: paths });
343
- assert.deepEqual(withSeen.items, []);
344
- assert.equal(withSeen.hits.droppedBySeen, 1);
345
- });
346
-
347
- test('路径级排除三级来源:source.cardPaths / 正文路径 / 交接卡标题', () => {
348
- const cards = [
349
- { path: 'handover/2026-11-01-y.md', dir: 'handover', title: '接手卡标题够长', scopeIn: [], body: '' },
350
- { path: 'topics/t.md', dir: 'topics', title: '普通卡', scopeIn: [], body: '' },
351
- ];
352
- const declared = legacyBootMessage('没有路径', { cardPaths: ['handover/2026-11-01-y.md'] });
353
- assert.deepEqual(cardPathsInMessage(declared, { cards }), ['handover/2026-11-01-y.md']);
354
-
355
- const inText = legacyBootMessage('见 X:\\memory-store\\topics\\t.md 与 topics/t.md');
356
- assert.deepEqual(cardPathsInMessage(inText, { cards }), ['topics/t.md']);
357
-
358
- const byTitle = legacyBootMessage('这次的开机卡取自《接手卡标题够长》,没有写路径。');
359
- assert.deepEqual(cardPathsInMessage(byTitle, { cards }), ['handover/2026-11-01-y.md']);
360
- });
361
-
362
- /* ================================================================== *
363
- * mustFix #4:隐私门(PII 未标 local_only 也不能漏)
364
- * ================================================================== */
365
-
366
- const CARD_PII = {
367
- path: 'topics/pii.md',
368
- title: '客户资料',
369
- scopeIn: ['客户资料'],
370
- aliases: [],
371
- body: '- 客户资料正文:联系人手机 13700002222,邮箱 client@example.com,按常规口径保存。',
372
- };
373
-
374
- test('隐私门:正文里的手机号/邮箱即使没标 local_only 也不注入', () => {
375
- const r = chooseRecall({ queryText: '客户资料', cards: [CARD_PII], budget: 1200 });
376
- assert.deepEqual(r.items, []);
377
- assert.equal(r.hits.droppedByPrivacy, 1);
378
- assert.doesNotMatch(renderInjection(r), /13700002222|client@example\.com/);
379
- });
380
-
381
- test('隐私门:privacyPolicy=title 时只留标题 + [已隐藏]', () => {
382
- const r = chooseRecall({
383
- queryText: '客户资料',
384
- cards: [CARD_PII],
385
- budget: 1200,
386
- options: { privacyPolicy: 'title' },
387
- });
388
- assert.equal(r.items.length, 1);
389
- assert.equal(r.items[0].excerpt, PRIVACY_HIDDEN_MARK);
390
- assert.equal(r.items[0].privacyRedacted, true);
391
- assert.equal(r.hits.redactedByPrivacy, 1);
392
- const text = renderInjection(r);
393
- assert.match(text, /\[已隐藏\]/);
394
- assert.doesNotMatch(text, /13700002222|client@example\.com/);
395
- });
396
-
397
- test('隐私门:扫描器抛错按命中处理(fail-closed)', () => {
398
- const r = chooseRecall({
399
- queryText: '客户资料',
400
- cards: [CARD_PII],
401
- budget: 1200,
402
- options: { privacyScan: () => { throw new Error('gate down'); } },
403
- });
404
- assert.deepEqual(r.items, []);
405
- assert.equal(r.hits.droppedByPrivacy, 1);
406
- });
407
-
408
- test('隐私门:真实 privacy-gate 的 scanPrivacy 能识别手机号与邮箱', () => {
409
- const s = privacyGateStatus;
410
- assert.equal(typeof s, 'object');
411
- assert.equal(typeof s.loaded, 'boolean');
412
- if (s.loaded) assert.match(String(s.from), /privacy-gate/);
413
- const hit = privacyScan('联系 13700002222 或 client@example.com');
414
- assert.equal(hit.verdict, 'local_only');
415
- assert.ok(hit.spans.length >= 2);
416
- assert.equal(privacyScan('node --test test/auto-recall.test.mjs').verdict, 'none');
417
- // defaultPrivacyScan 是同一口径的兜底
418
- assert.equal(defaultPrivacyScan('13700002222').verdict, 'local_only');
419
- assert.equal(defaultPrivacyScan('第 137 行 2026-11-04').verdict, 'none');
420
- });
421
-
422
- const FIXTURE_ROOT = fileURLToPath(new URL('./fixtures/privacy-memory', import.meta.url));
423
- test('隐私门:合成夹具 pii-unmarked.md 被拦住、secret-local-only.md 整卡跳过', { skip: !fs.existsSync(FIXTURE_ROOT) }, async () => {
424
- const { loadCards } = await import('../src/recall/cards.mjs');
425
- const cards = loadCards(FIXTURE_ROOT);
426
- const r = chooseRecall({ queryText: '客户资料 保密配方 秘密配方', cards, budget: 1200, options: { privacyScan } });
427
- const text = renderInjection(r);
428
- assert.doesNotMatch(text, /13700002222|client@example\.com/);
429
- assert.ok(r.hits.droppedByPrivacy >= 1, 'pii-unmarked 必须被隐私门拦下');
430
- assert.ok(r.hits.droppedLocalOnly >= 1, 'secret-local-only 必须整卡跳过');
431
- assert.ok(!r.items.some((i) => i.path.endsWith('secret-local-only.md')));
432
- });
433
-
434
- test('隐私门:真实 13 卡回归 0 泄漏', { skip: !fs.existsSync(REAL) }, async () => {
435
- const { loadCards } = await import('../src/recall/cards.mjs');
436
- const cards = loadCards('X:/memory-store');
437
- const query = cards.map((c) => [...c.scopeIn, ...c.aliases].join(' ')).join(' ');
438
- for (const budget of [1200, 100000]) {
439
- const r = chooseRecall({ queryText: query, cards, budget, options: { privacyScan } });
440
- const text = renderInjection(r);
441
- assert.equal(defaultPrivacyScan(text).verdict, 'none', `budget=${budget} 渲染文本里出现 PII`);
442
- assert.doesNotMatch(text, /(?<!\d)1[3-9]\d{9}(?!\d)|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/);
443
- }
444
- });
445
-
446
- /* ================================================================== *
447
- * mustFix #8:文本级去重只防字面重复(不夸大成语义去重)
448
- * ================================================================== */
449
-
450
- test('去重只防字面:换个说法(低 bigram 重合)仍会注入', () => {
451
- const first = chooseRecall({ queryText: 'pwsh 沙箱 0xC0000142', cards: [CARD_A], budget: 1200 });
452
- const excerpt = first.items[0].excerpt;
453
- const paraphrase = '工具链上 pwsh 进程起不来,需要放开权限。';
454
- assert.ok(containment(excerpt, paraphrase) < DEFAULTS.dedupeContainment);
455
- const second = chooseRecall({
456
- queryText: 'pwsh 沙箱 0xC0000142',
457
- cards: [CARD_A],
458
- budget: 1200,
459
- injectedTexts: [paraphrase],
460
- });
461
- assert.equal(second.items.length, 1, '语义同义但字面不同 → 去重不生效(已知局限)');
462
- });
463
-
464
- test('去重只防字面:整段被已注入文本包住时才会丢', () => {
465
- const first = chooseRecall({ queryText: 'pwsh 沙箱 0xC0000142', cards: [CARD_A], budget: 1200 });
466
- const dup = chooseRecall({
467
- queryText: 'pwsh 沙箱 0xC0000142',
468
- cards: [CARD_A],
469
- budget: 1200,
470
- injectedTexts: [`开场白\n${renderInjection(first)}\n结束语`],
471
- });
472
- assert.deepEqual(dup.items, []);
473
- assert.equal(dup.hits.droppedByDedupe, 1);
474
- });