@jjh32456/dsh-longlive 1.6.8 → 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 (288) hide show
  1. package/CHANGELOG.md +69 -1539
  2. package/LICENSE +1 -1
  3. package/QUICKSTART.md +33 -12
  4. package/README.en.md +17 -7
  5. package/README.md +29 -10
  6. package/SPEC.md +5 -5
  7. package/cordis.patch.yml +17 -12
  8. package/docs/ANSWER-GUARD.md +92 -0
  9. package/docs/ARCHITECTURE.md +3 -3
  10. package/docs/AUTO-RECALL-WIRING.md +1 -1
  11. package/docs/AUTO-RECALL.md +15 -9
  12. package/docs/CONFIG.md +143 -37
  13. package/docs/ERRORS.md +17 -5
  14. package/docs/FAQ.md +7 -7
  15. package/docs/FEATURES.md +66 -29
  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 +3 -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 +42 -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 +129 -18
  41. package/src/activity/model.js +11 -1
  42. package/src/activity/store.js +22 -3
  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 +145 -48
  58. package/src/deps.js +6 -4
  59. package/src/features.js +158 -8
  60. package/src/identity/index.js +13 -5
  61. package/src/identity/inject-position.js +2 -2
  62. package/src/index.js +97 -7
  63. package/src/lifecycle/gap-clocks.js +328 -0
  64. package/src/lifecycle/index.js +269 -0
  65. package/src/memory/activity-capture.js +229 -0
  66. package/src/memory/anchors.js +194 -0
  67. package/src/memory/audit-records.js +222 -0
  68. package/src/memory/auto-cards.js +401 -0
  69. package/src/memory/capture.js +442 -0
  70. package/src/memory/card-anchors.js +203 -0
  71. package/src/memory/conflicts.js +106 -0
  72. package/src/memory/decay.js +311 -0
  73. package/src/memory/distill.js +471 -0
  74. package/src/memory/event-index.js +917 -0
  75. package/src/memory/evidence-chain.js +295 -0
  76. package/src/memory/evidence.js +145 -0
  77. package/src/memory/filter-runner.js +1 -1
  78. package/src/memory/filter.js +5 -1
  79. package/src/memory/forget-v2.js +410 -0
  80. package/src/memory/forget.js +71 -0
  81. package/src/memory/frames-activation.js +247 -0
  82. package/src/memory/fts-query.js +174 -0
  83. package/src/memory/governance.js +604 -0
  84. package/src/memory/index.js +1 -0
  85. package/src/memory/l2-gate.js +169 -0
  86. package/src/memory/lock.js +121 -0
  87. package/src/memory/migrate.js +139 -0
  88. package/src/memory/recall-v2.js +223 -0
  89. package/src/memory/recall.js +65 -2
  90. package/src/memory/records.js +425 -0
  91. package/src/memory/retention.js +128 -0
  92. package/src/memory/review.js +124 -0
  93. package/src/memory/trust.js +88 -0
  94. package/src/memory/version-chain.js +264 -0
  95. package/src/memory/wal.js +137 -0
  96. package/src/memory/working-frame.js +516 -0
  97. package/src/metrics/classify.mjs +5 -3
  98. package/src/metrics/index.mjs +21 -1
  99. package/src/metrics/prestep-latency.mjs +300 -0
  100. package/src/metrics/store.mjs +25 -0
  101. package/src/policy-profile.js +18 -3
  102. package/src/recall/anaphora.mjs +87 -0
  103. package/src/recall/cards.mjs +7 -0
  104. package/src/recall/embedding.mjs +8 -8
  105. package/src/recall/entities.mjs +112 -0
  106. package/src/recall/index.js +235 -19
  107. package/src/recall/inject-order.mjs +10 -8
  108. package/src/recall/privacy.mjs +1 -1
  109. package/src/recall/recall-core.mjs +116 -10
  110. package/src/recall/shared-visibility.js +302 -0
  111. package/src/recall/time-normalizer.mjs +196 -0
  112. package/src/role/activation.js +191 -0
  113. package/src/role/attention.js +318 -0
  114. package/src/role/base.js +36 -0
  115. package/src/role/channels.js +423 -0
  116. package/src/role/index.js +425 -0
  117. package/src/role/interfaces.js +115 -0
  118. package/src/role/personality.js +370 -0
  119. package/src/role/readers.js +151 -0
  120. package/src/role/relationships.js +477 -0
  121. package/src/role/store.js +202 -0
  122. package/src/role/writeprotect.js +373 -0
  123. package/src/router/aliases.json +176 -25
  124. package/src/router/background/agent-runner.mjs +1 -1
  125. package/src/router/background/anchor.mjs +1 -1
  126. package/src/router/background/audit.mjs +2 -2
  127. package/src/router/background/budget-ledger.mjs +2 -2
  128. package/src/router/background/host-sim.mjs +1 -1
  129. package/src/router/background/memory.mjs +4 -4
  130. package/src/router/background/queue.mjs +1 -1
  131. package/src/router/background/retry.mjs +1 -1
  132. package/src/router/background/router.mjs +1 -1
  133. package/src/router/background/stream.mjs +1 -1
  134. package/src/router/background/tool-policy.mjs +1 -1
  135. package/src/router/design-headings.json +5 -5
  136. package/src/router/face/config.mjs +1 -1
  137. package/src/router/face/identity.mjs +3 -3
  138. package/src/router/judge.mjs +8 -7
  139. package/src/router/policies/generic.json +21 -4
  140. package/src/router/route.js +109 -22
  141. package/src/router/router.mjs +10 -6
  142. package/src/router/routing-rules.json +260 -445
  143. package/src/router/run-sim.mjs +8 -8
  144. package/src/router/runtime.js +23 -12
  145. package/src/router/service/config.mjs +5 -5
  146. package/src/router/service/context.mjs +1 -1
  147. package/src/router/service/queue.mjs +1 -1
  148. package/src/router/shadow.mjs +4 -3
  149. package/src/surgery-recovery.js +2 -2
  150. package/src/tool-output.js +2 -2
  151. package/src/tools.js +39 -7
  152. package/src/truncate-visible.js +2 -2
  153. package/src/ui/actions.js +1312 -0
  154. package/src/ui/panel-api.js +367 -0
  155. package/src/ui/panel-auth.js +251 -0
  156. package/src/ui/panel-integrity.js +243 -0
  157. package/src/ui/routes-readonly.js +695 -0
  158. package/src/ui/routes.js +3 -0
  159. package/docs/DEV.md +0 -279
  160. package/docs/PRESENCE-BUS-WIRING.md +0 -491
  161. package/docs/PRESENCE-BUS.md +0 -694
  162. package/docs/SURGERY.md +0 -186
  163. package/scripts/mark-core-cards.mjs +0 -289
  164. package/scripts/pack.mjs +0 -392
  165. package/scripts/router-proposals.mjs +0 -386
  166. package/src/router/fixtures/M1.json +0 -16
  167. package/src/router/fixtures/M2.json +0 -16
  168. package/src/router/fixtures/M3.json +0 -20
  169. package/src/router/fixtures/M4.json +0 -20
  170. package/src/router/fixtures/M5.json +0 -45
  171. package/src/router/fixtures/M6.json +0 -53
  172. package/src/router/fixtures/M7.json +0 -46
  173. package/src/router/fixtures/S1.json +0 -20
  174. package/src/router/fixtures/S2.json +0 -23
  175. package/src/router/fixtures/S3.json +0 -20
  176. package/src/router/fixtures/S4.json +0 -34
  177. package/src/router/fixtures/S5.json +0 -43
  178. package/src/router/fixtures/S6.json +0 -47
  179. package/src/router/fixtures/T1.json +0 -23
  180. package/src/router/fixtures/T2.json +0 -16
  181. package/src/router/fixtures/T3.json +0 -19
  182. package/src/router/fixtures/T4.json +0 -16
  183. package/src/router/fixtures/X1.json +0 -17
  184. package/src/router/fixtures/X2.json +0 -20
  185. package/src/router/gen-design-headings.mjs +0 -107
  186. package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
  187. package/test/activity-realtime.test.mjs +0 -959
  188. package/test/activity-stream.test.mjs +0 -512
  189. package/test/auto-recall.test.mjs +0 -474
  190. package/test/auto-truncate.test.mjs +0 -931
  191. package/test/autobind.test.mjs +0 -399
  192. package/test/bus.test.mjs +0 -1572
  193. package/test/cli-guard.test.mjs +0 -198
  194. package/test/conclusion.test.mjs +0 -408
  195. package/test/config-schema.test.mjs +0 -210
  196. package/test/d6-output.test.mjs +0 -194
  197. package/test/d6.test.mjs +0 -395
  198. package/test/deps.test.mjs +0 -342
  199. package/test/doctor-setup.test.mjs +0 -297
  200. package/test/embedding-recall.test.mjs +0 -347
  201. package/test/feature-flags.test.mjs +0 -585
  202. package/test/filter-shadow.test.mjs +0 -170
  203. package/test/fixtures/activity-append-child.mjs +0 -29
  204. package/test/fixtures/baseline-0.8.0-registration.json +0 -556
  205. package/test/fixtures/d6-header-tools.json +0 -1
  206. package/test/fixtures/lock-child.mjs +0 -67
  207. package/test/fixtures/pre-step-container.mjs +0 -100
  208. package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
  209. package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
  210. package/test/fixtures/profile-session-longlive-config.json +0 -20
  211. package/test/fixtures/proposal-cas-child.mjs +0 -66
  212. package/test/host-engine-status.test.mjs +0 -307
  213. package/test/hot-truncate.test.mjs +0 -1349
  214. package/test/identity-card-marker.test.mjs +0 -307
  215. package/test/identity-router/run.mjs +0 -721
  216. package/test/integration-pre-step.test.mjs +0 -553
  217. package/test/live.test.mjs +0 -1023
  218. package/test/maintenance-schedule.test.mjs +0 -538
  219. package/test/memory.test.mjs +0 -2828
  220. package/test/metrics.test.mjs +0 -478
  221. package/test/orchestrator.test.mjs +0 -266
  222. package/test/p1_probe.mjs +0 -97
  223. package/test/packaging.test.mjs +0 -184
  224. package/test/portability.test.mjs +0 -343
  225. package/test/rc4-regressions.test.mjs +0 -562
  226. package/test/rc5-regressions.test.mjs +0 -197
  227. package/test/rc6-chinese.test.mjs +0 -394
  228. package/test/router-1100/apply-off.test.mjs +0 -175
  229. package/test/router-1100/budget-reconcile.test.mjs +0 -215
  230. package/test/router-1100/config-context.test.mjs +0 -211
  231. package/test/router-1100/helpers.mjs +0 -129
  232. package/test/router-1100/hint-envelope.test.mjs +0 -150
  233. package/test/router-1100/lock-crossproc.test.mjs +0 -206
  234. package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
  235. package/test/router-1100/router-apply.test.mjs +0 -372
  236. package/test/router-1100/router-fixb.test.mjs +0 -602
  237. package/test/router-1100/router-proposals.test.mjs +0 -405
  238. package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
  239. package/test/router-1100/router-security-cr.test.mjs +0 -333
  240. package/test/router-1100/service-lifecycle.test.mjs +0 -220
  241. package/test/router-1100/tools-contract.test.mjs +0 -164
  242. package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
  243. package/test/router-1100/v6-contract.test.mjs +0 -158
  244. package/test/router-1100/v6b-contract.test.mjs +0 -293
  245. package/test/router-1100/v6c-contract.test.mjs +0 -249
  246. package/test/router-1100/wal-rotation.test.mjs +0 -375
  247. package/test/router-1100/wp1-config.test.mjs +0 -170
  248. package/test/router-1100/wp1-default-off.test.mjs +0 -77
  249. package/test/router-1100/wp1-model-status.test.mjs +0 -90
  250. package/test/router-1100/wp1-status.test.mjs +0 -220
  251. package/test/router-1100/wp1-tools.test.mjs +0 -146
  252. package/test/router-1100/wp5-cli.test.mjs +0 -240
  253. package/test/router-1100/wp5-model-auto.test.mjs +0 -418
  254. package/test/router-1100/wp6-merge.test.mjs +0 -386
  255. package/test/router-1100/wp6-wake.test.mjs +0 -502
  256. package/test/router-background/audit.test.mjs +0 -37
  257. package/test/router-background/budget-ledger.test.mjs +0 -330
  258. package/test/router-background/cross-process.test.mjs +0 -222
  259. package/test/router-background/fixtures/cross-child.mjs +0 -85
  260. package/test/router-background/fixtures/real-shapes.mjs +0 -239
  261. package/test/router-background/router.test.mjs +0 -1076
  262. package/test/router-background/stream-retry.test.mjs +0 -358
  263. package/test/router-background/tool-agent.test.mjs +0 -384
  264. package/test/router-face/bridge.test.mjs +0 -169
  265. package/test/router-face/face.test.mjs +0 -316
  266. package/test/router-failclosed.test.mjs +0 -466
  267. package/test/router-identity.test.mjs +0 -384
  268. package/test/router-l1-l2.contract.test.mjs +0 -1075
  269. package/test/router-memory-tools.test.mjs +0 -262
  270. package/test/router-policy.test.mjs +0 -225
  271. package/test/router-runtime.test.mjs +0 -438
  272. package/test/router-shadow.test.mjs +0 -287
  273. package/test/router.test.mjs +0 -903
  274. package/test/run-rc.mjs +0 -315
  275. package/test/run.mjs +0 -2469
  276. package/test/safety-defaults.test.mjs +0 -181
  277. package/test/seeded-load.test.mjs +0 -436
  278. package/test/session-original.test.mjs +0 -338
  279. package/test/state-truth.test.mjs +0 -937
  280. package/test/surgery-recovery.test.mjs +0 -936
  281. package/test/t7_drive.mjs +0 -151
  282. package/test/tool-output.test.mjs +0 -393
  283. package/test/tool-schema.test.mjs +0 -177
  284. package/test/tools_contract.test.mjs +0 -716
  285. package/test/truncate-visible.test.mjs +0 -1150
  286. package/test/ui-flags.test.mjs +0 -477
  287. package/test/v4-hardening.test.mjs +0 -427
  288. package/test/version-consistency.test.mjs +0 -286
@@ -0,0 +1,235 @@
1
+ /**
2
+ * engram 适配器(M2-E 解耦样板)
3
+ *
4
+ * 核心代码只通过 src/adapters/registry.js 使用本模块;本目录是唯一允许出现
5
+ * 第三方插件(engram)包名/工具名/动作名/注入标记字面量的地方。
6
+ *
7
+ * 统一接口:{ id, probe, call, health, mode }
8
+ * - probe(): 探测宿主是否提供 engram 能力(按宿主传入的工具清单/显式 mode),返回 { ok, mode, evidence }
9
+ * - call(): 经宿主 invoke 转发一次 engram 工具调用;adapter 缺席时返回降级结果,不抛错
10
+ * - health(): { id, mode, ok, degraded, detail },供 doctor/lint/registry 汇总
11
+ * - mode: 'active' | 'degraded' | 'absent'
12
+ */
13
+ import { readFileSync } from 'node:fs'
14
+
15
+ export const ID = 'engram'
16
+ export const PACKAGE_NAME = '@kenz1117/dsh-engram'
17
+ export const ACTION_PREFIX = 'engram.'
18
+ export const TOOL_PREFIX = 'engram_'
19
+ export const PROFILE_EDIT_ACTION = 'engram.profile.edit'
20
+ export const FORGET_ACTION = 'engram.forget'
21
+ export const SAVE_ACTION = 'engram.save'
22
+ export const FACTS_REPLACE_ACTION = 'engram.facts.replace'
23
+ export const CLASSIFY_PLUGIN_LABEL = 'plugin:dsh-engram'
24
+ export const EVIDENCE_PREFIX = 'engram:'
25
+ export const LEGACY_TOOL_NAMES = Object.freeze(['memory_save'])
26
+ export const INTERCEPTED_WRITE_TOOLS = Object.freeze([
27
+ 'engram_save',
28
+ 'engram_update',
29
+ 'engram_facts',
30
+ 'engram_forget',
31
+ 'engram_profile_edit',
32
+ ])
33
+ export const ACTION_NAMES = Object.freeze([
34
+ 'engram.search',
35
+ 'engram.save',
36
+ 'engram.facts.replace',
37
+ 'engram.forget',
38
+ 'engram.profile.edit',
39
+ ])
40
+ export const LEGACY_WRITE_TOOL_NAMES = Object.freeze(['memory_save'])
41
+ export const WRITE_TOOL_NAMES = Object.freeze(['memory_save', 'engram_save', 'engram_update', 'engram_facts', 'engram_forget', 'engram_profile_edit'])
42
+ export const ACTION_GROUPS = Object.freeze({
43
+ approval: ['engram.forget'],
44
+ write: ['engram.save', 'engram.facts.replace'],
45
+ profileEdit: ['engram.profile.edit'],
46
+ })
47
+ export const DEGRADE_TARGETS = Object.freeze({
48
+ 'engram.search': 'live',
49
+ 'engram.save': 'topics',
50
+ 'engram.facts.replace': 'topics',
51
+ 'engram.forget': 'topics',
52
+ 'engram.profile.edit': 'topics',
53
+ })
54
+ export const SESSION_MARKER_PATTERNS = Object.freeze([
55
+ /<engram_profile\b/i,
56
+ /<engram_memory_context\b/i,
57
+ ])
58
+
59
+ const DATA_FILES = Object.freeze({
60
+ 'routing-rules': { url: new URL('./data/routing-rules.json', import.meta.url), sourcePath: 'src/adapters/engram/data/routing-rules.json' },
61
+ aliases: { url: new URL('./data/aliases.json', import.meta.url), sourcePath: 'src/adapters/engram/data/aliases.json' },
62
+ policy: { url: new URL('./data/policy.json', import.meta.url), sourcePath: 'src/adapters/engram/data/policy.json' },
63
+ })
64
+ const dataCache = new Map()
65
+
66
+ /**
67
+ * 读取 adapter 数据文件;缺失/坏 JSON 一律 fail-safe:返回 ok:false + 错误记录,
68
+ * 由调用方(registry/核心 loader)降级继续跑,不抛错。
69
+ */
70
+ export function loadData(kind) {
71
+ if (dataCache.has(kind)) return dataCache.get(kind)
72
+ const entry = DATA_FILES[kind]
73
+ let result
74
+ if (!entry) {
75
+ result = { ok: false, kind, data: null, text: '', sourcePath: '', error: { code: 'E_ADAPTER_DATA_UNKNOWN', message: `未知 adapter 数据:${String(kind)}` } }
76
+ } else {
77
+ try {
78
+ const text = readFileSync(entry.url, 'utf8')
79
+ result = { ok: true, kind, data: JSON.parse(text), text, sourcePath: entry.sourcePath, error: null }
80
+ } catch (cause) {
81
+ result = {
82
+ ok: false,
83
+ kind,
84
+ data: null,
85
+ text: '',
86
+ sourcePath: entry.sourcePath,
87
+ error: {
88
+ code: cause && cause.code === 'ENOENT' ? 'E_ADAPTER_DATA_MISSING' : 'E_ADAPTER_DATA_INVALID',
89
+ message: `${kind} 数据不可用:${cause && cause.message ? cause.message : String(cause)}`,
90
+ },
91
+ }
92
+ }
93
+ }
94
+ dataCache.set(kind, result)
95
+ return result
96
+ }
97
+
98
+ export function resetDataCacheForTests() {
99
+ dataCache.clear()
100
+ }
101
+
102
+ export function isEngramToolName(name) {
103
+ if (typeof name !== 'string') return false
104
+ const trimmed = name.trim()
105
+ if (!trimmed) return false
106
+ return trimmed.startsWith(TOOL_PREFIX) || LEGACY_TOOL_NAMES.includes(trimmed)
107
+ }
108
+
109
+ export function isEngramActionName(name) {
110
+ if (typeof name !== 'string') return false
111
+ const trimmed = name.trim()
112
+ return trimmed.startsWith(ACTION_PREFIX) || trimmed.startsWith(TOOL_PREFIX)
113
+ }
114
+
115
+ function normalizeMode(value) {
116
+ return value === 'active' || value === 'degraded' || value === 'absent' ? value : null
117
+ }
118
+
119
+ /**
120
+ * @param {object} [options]
121
+ * @param {() => string[]} [options.tools] 宿主可用工具名列表(探测依据,不 import 第三方)
122
+ * @param {string} [options.mode] 显式覆盖 mode(测试/配置用)
123
+ * @param {(tool: string, args: object) => any} [options.invoke] 宿主工具调用通道
124
+ * @param {() => any} [options.afterProbe]
125
+ */
126
+ export function createEngramAdapter(options = {}) {
127
+ const toolsProvider = typeof options.tools === 'function' ? options.tools : () => (Array.isArray(options.tools) ? options.tools : [])
128
+ const invoke = typeof options.invoke === 'function' ? options.invoke : null
129
+ const forcedMode = normalizeMode(options.mode)
130
+ let last = null
131
+
132
+ function probe() {
133
+ if (forcedMode) {
134
+ last = { ok: forcedMode !== 'absent', mode: forcedMode, evidence: ['explicit-mode'], at: null }
135
+ return last
136
+ }
137
+ let tools = []
138
+ try {
139
+ const value = toolsProvider()
140
+ tools = Array.isArray(value) ? value : []
141
+ } catch {
142
+ tools = []
143
+ }
144
+ const hits = tools.filter(isEngramToolName)
145
+ const mode = hits.length > 0 ? 'active' : 'absent'
146
+ last = { ok: mode === 'active', mode, evidence: hits.slice(0, 8), at: null }
147
+ return last
148
+ }
149
+
150
+ function health() {
151
+ const state = last || probe()
152
+ return {
153
+ id: ID,
154
+ mode: state.mode,
155
+ ok: state.mode !== 'absent',
156
+ degraded: state.mode === 'degraded' || state.mode === 'absent',
157
+ detail: state.mode === 'absent' ? '未探测到 engram 工具,降级为文件记忆(topics/journal)' : state.mode === 'degraded' ? 'engram 部分能力不可用' : 'engram 可用',
158
+ evidence: state.evidence.slice(),
159
+ }
160
+ }
161
+
162
+ /** 动作级降级计划:插件缺席时给出降级目标与 degraded 标记,不抛错。 */
163
+ function plan(actionName) {
164
+ if (!isEngramActionName(actionName)) {
165
+ return { ok: false, degraded: false, code: 'E_ADAPTER_ACTION', action: actionName }
166
+ }
167
+ const state = last || probe()
168
+ if (state.mode === 'active') {
169
+ return { ok: true, degraded: false, mode: state.mode, target: ID, action: actionName }
170
+ }
171
+ return {
172
+ ok: false,
173
+ degraded: true,
174
+ mode: state.mode,
175
+ target: DEGRADE_TARGETS[actionName] || 'topics',
176
+ action: 'topics.write',
177
+ fallback: 'file-memory',
178
+ message: '外部记忆插件不可用:该动作降级到文件记忆(topics/journal),不阻塞',
179
+ }
180
+ }
181
+
182
+ function call(request = {}) {
183
+ const tool = typeof request.tool === 'string' ? request.tool : ''
184
+ if (!isEngramToolName(tool)) {
185
+ return { ok: false, degraded: false, code: 'E_ADAPTER_TOOL', message: `不是 engram 工具:${tool}` }
186
+ }
187
+ const state = last || probe()
188
+ if (state.mode === 'absent') {
189
+ return {
190
+ ok: false,
191
+ degraded: true,
192
+ mode: 'absent',
193
+ code: 'E_ADAPTER_ABSENT',
194
+ tool,
195
+ fallbackTarget: 'topics',
196
+ message: 'engram 不可用:降级到文件记忆(topics/journal),不阻塞',
197
+ }
198
+ }
199
+ if (typeof invoke !== 'function') {
200
+ return { ok: false, degraded: true, mode: state.mode, code: 'E_ADAPTER_NO_INVOKE', tool, message: '宿主未提供工具调用通道' }
201
+ }
202
+ try {
203
+ return { ok: true, degraded: state.mode !== 'active', mode: state.mode, tool, result: invoke(tool, request.args || {}) }
204
+ } catch (cause) {
205
+ return { ok: false, degraded: true, mode: state.mode, code: 'E_ADAPTER_CALL', tool, message: String(cause && cause.message ? cause.message : cause) }
206
+ }
207
+ }
208
+
209
+ return {
210
+ id: ID,
211
+ packageName: PACKAGE_NAME,
212
+ get mode() {
213
+ return (last || probe()).mode
214
+ },
215
+ probe,
216
+ call,
217
+ plan,
218
+ health,
219
+ loadData,
220
+ constants: Object.freeze({
221
+ ACTION_PREFIX,
222
+ TOOL_PREFIX,
223
+ LEGACY_TOOL_NAMES,
224
+ LEGACY_WRITE_TOOL_NAMES,
225
+ WRITE_TOOL_NAMES,
226
+ INTERCEPTED_WRITE_TOOLS,
227
+ ACTION_NAMES,
228
+ ACTION_GROUPS,
229
+ DEGRADE_TARGETS,
230
+ CLASSIFY_PLUGIN_LABEL,
231
+ EVIDENCE_PREFIX,
232
+ SESSION_MARKER_PATTERNS,
233
+ }),
234
+ }
235
+ }
@@ -0,0 +1,315 @@
1
+ /**
2
+ * 第三方插件适配器注册表(M2-E 解耦的唯一核心入口)
3
+ *
4
+ * 设计:核心代码不 import 任何第三方包、不出现第三方包名/工具名/动作名字面量;
5
+ * 所有第三方接触面(包名、工具名、动作名、规则/别名/策略片段、注入标记)都由
6
+ * adapter 提供,核心经本 registry 取用。适配器缺席/数据损坏时一律 fail-safe:
7
+ * 核心照常运行 + 显式 degraded 记录,不抛错、不阻塞。
8
+ *
9
+ * 统一接口(每个 adapter 必须提供):
10
+ * { id, probe, call, health, mode }
11
+ * 可选:loadData(kind) → { ok, data, error }
12
+ *
13
+ * 新增第三方插件 = 在 adapters/<id>/ 实现接口 + 在 createDefaultRegistry() 里注册,
14
+ * 并先登记到 scripts/lint-plugin-independence.mjs。
15
+ */
16
+ import { createEngramAdapter } from './engram/index.js'
17
+
18
+ export const ADAPTER_REGISTRY_VERSION = 1
19
+
20
+ function safeCall(fn, fallback) {
21
+ try {
22
+ return typeof fn === 'function' ? fn() : fallback
23
+ } catch (cause) {
24
+ return fallback
25
+ }
26
+ }
27
+
28
+ /**
29
+ * 创建一个独立 registry(测试用;生产用默认 registry)。
30
+ * @param {Array<object>} adapters
31
+ */
32
+ export function createAdapterRegistry(adapters = []) {
33
+ const entries = new Map()
34
+ const problems = []
35
+
36
+ const registry = {
37
+ register(adapter) {
38
+ if (!adapter || typeof adapter.id !== 'string' || !adapter.id.trim()) {
39
+ problems.push({ code: 'E_ADAPTER_INVALID', message: 'adapter 缺 id,忽略' })
40
+ return false
41
+ }
42
+ if (entries.has(adapter.id)) {
43
+ problems.push({ code: 'E_ADAPTER_DUPLICATE', message: `adapter id 重复:${adapter.id}` })
44
+ return false
45
+ }
46
+ entries.set(adapter.id, adapter)
47
+ return true
48
+ },
49
+ get(id) {
50
+ return entries.get(id) || null
51
+ },
52
+ list() {
53
+ return [...entries.values()]
54
+ },
55
+ ids() {
56
+ return [...entries.keys()]
57
+ },
58
+ /** 探测全部 adapter,返回 { modes, degraded, problems };单个 adapter 抛错不影响整体。 */
59
+ probeAll(options = {}) {
60
+ const modes = {}
61
+ const adapterProblems = []
62
+ for (const adapter of entries.values()) {
63
+ const result = safeCall(() => adapter.probe(options), { ok: false, mode: 'absent', evidence: [], error: { code: 'E_ADAPTER_PROBE' } })
64
+ modes[adapter.id] = result && result.mode ? result.mode : 'absent'
65
+ if (result && result.error) adapterProblems.push({ adapter: adapter.id, ...result.error })
66
+ }
67
+ return {
68
+ modes,
69
+ degraded: Object.values(modes).some((mode) => mode !== 'active'),
70
+ problems: [...adapterProblems],
71
+ }
72
+ },
73
+ /** 汇总 health;供 doctor/lint 输出。 */
74
+ healthAll(options = {}) {
75
+ const rows = []
76
+ for (const adapter of entries.values()) {
77
+ const health = safeCall(() => adapter.health(options), { id: adapter.id, mode: 'absent', degraded: true, detail: 'health() 异常' })
78
+ rows.push({ id: adapter.id, mode: health.mode || 'absent', ok: health.ok === true, degraded: health.degraded !== false, detail: health.detail || '' })
79
+ }
80
+ return { ok: rows.every((row) => row.ok), rows, problems: [...problems] }
81
+ },
82
+ /** 经适配器转发一次调用;adapter 缺席时返回降级结果。 */
83
+ call(id, request) {
84
+ const adapter = entries.get(id)
85
+ if (!adapter) {
86
+ return { ok: false, degraded: true, mode: 'absent', code: 'E_ADAPTER_UNKNOWN', message: `未注册的 adapter:${String(id)}` }
87
+ }
88
+ return safeCall(() => adapter.call(request), { ok: false, degraded: true, mode: 'absent', code: 'E_ADAPTER_CALL', message: 'adapter.call 异常' })
89
+ },
90
+ /** 合并各 adapter 的某类数据;失败仅记录 + degraded,不抛错。 */
91
+ loadData(kind) {
92
+ const items = []
93
+ const errors = []
94
+ const sources = []
95
+ for (const adapter of entries.values()) {
96
+ if (typeof adapter.loadData !== 'function') continue
97
+ const result = safeCall(() => adapter.loadData(kind), { ok: false, error: { code: 'E_ADAPTER_DATA', message: `adapter ${adapter.id} loadData 异常` } })
98
+ if (!result || !result.ok) {
99
+ errors.push({ adapter: adapter.id, kind, code: (result && result.error && result.error.code) || 'E_ADAPTER_DATA', message: (result && result.error && result.error.message) || 'adapter 数据不可用' })
100
+ continue
101
+ }
102
+ items.push({ adapter: adapter.id, data: result.data, text: result.text || '', sourcePath: result.sourcePath || '' })
103
+ sources.push(adapter.id)
104
+ }
105
+ return { ok: errors.length === 0, items, sources, errors, degraded: errors.length > 0 }
106
+ },
107
+ problems,
108
+ }
109
+
110
+ for (const adapter of adapters) registry.register(adapter)
111
+ return registry
112
+ }
113
+
114
+ /** 生产默认 registry:engram 是当前唯一样例;其它第三方插件只登记、不建 adapter。 */
115
+ export function createDefaultRegistry() {
116
+ return createAdapterRegistry([createEngramAdapter()])
117
+ }
118
+
119
+ export const defaultRegistry = createDefaultRegistry()
120
+
121
+ // ── 核心取用面(全部是中性命名,不含第三方字面量) ──────────────────────
122
+
123
+ export function adapterHealth() {
124
+ return defaultRegistry.healthAll()
125
+ }
126
+
127
+ export function adapterModes() {
128
+ return defaultRegistry.probeAll().modes
129
+ }
130
+
131
+ export function adapterEntries() {
132
+ return defaultRegistry.list().map((adapter) => ({ id: adapter.id, mode: safeCall(() => adapter.mode, 'absent') }))
133
+ }
134
+
135
+ export function adapterCall(id, request) {
136
+ return defaultRegistry.call(id, request)
137
+ }
138
+
139
+ /** 合并 adapter 提供的路由规则(保留 adapter 侧 _order 以复原原表顺序)。 */
140
+ export function loadAdapterRoutingRules() {
141
+ const loaded = defaultRegistry.loadData('routing-rules')
142
+ const rules = []
143
+ const targets = []
144
+ const errors = []
145
+ for (const item of loaded.items) {
146
+ const data = item.data || {}
147
+ if (Array.isArray(data.rules)) rules.push(...data.rules)
148
+ if (Array.isArray(data.targets)) targets.push(...data.targets)
149
+ }
150
+ errors.push(...loaded.errors)
151
+ return { ok: errors.length === 0, rules, targets, items: loaded.items, errors, degraded: errors.length > 0 }
152
+ }
153
+
154
+ /** 合并 adapter 提供的动作别名。 */
155
+ export function loadAdapterAliases() {
156
+ const loaded = defaultRegistry.loadData('aliases')
157
+ const actions = []
158
+ const errors = [...loaded.errors]
159
+ for (const item of loaded.items) {
160
+ const data = item.data || {}
161
+ if (Array.isArray(data.actions)) actions.push(...data.actions)
162
+ }
163
+ return { ok: errors.length === 0, actions, errors, degraded: errors.length > 0 }
164
+ }
165
+
166
+ /** 把 adapter 的策略片段(forbiddenActions / allowPrefixes)并入一份已校验的 profile。 */
167
+ export function applyAdapterPolicy(policy) {
168
+ const loaded = defaultRegistry.loadData('policy')
169
+ if (!loaded.ok || typeof policy !== 'object' || policy === null) {
170
+ return { policy, degraded: !loaded.ok, errors: loaded.errors }
171
+ }
172
+ const next = JSON.parse(JSON.stringify(policy))
173
+ const forbidden = new Set(next.rest && Array.isArray(next.rest.forbiddenActions) ? next.rest.forbiddenActions : [])
174
+ const allowed = new Set(next.writeScope && Array.isArray(next.writeScope.allowPrefixes) ? next.writeScope.allowPrefixes : [])
175
+ for (const item of loaded.items) {
176
+ const data = item.data || {}
177
+ for (const action of Array.isArray(data.forbiddenActions) ? data.forbiddenActions : []) forbidden.add(action)
178
+ for (const prefix of Array.isArray(data.allowPrefixes) ? data.allowPrefixes : []) allowed.add(prefix)
179
+ }
180
+ if (next.rest) next.rest.forbiddenActions = [...forbidden]
181
+ if (next.writeScope) next.writeScope.allowPrefixes = [...allowed]
182
+ return { policy: next, degraded: false, errors: [] }
183
+ }
184
+
185
+ export function optionalPeerNames() {
186
+ return defaultRegistry.list().map((adapter) => adapter.packageName).filter((name) => typeof name === 'string' && name.trim())
187
+ }
188
+
189
+ export function interceptedToolNames() {
190
+ const names = []
191
+ for (const adapter of defaultRegistry.list()) {
192
+ const constants = adapter.constants || {}
193
+ for (const name of constants.INTERCEPTED_WRITE_TOOLS || []) names.push(name)
194
+ for (const name of constants.LEGACY_TOOL_NAMES || []) names.push(name)
195
+ }
196
+ return [...new Set(names)]
197
+ }
198
+
199
+ export function classificationPluginLabels() {
200
+ return defaultRegistry.list().map((adapter) => adapter.constants && adapter.constants.CLASSIFY_PLUGIN_LABEL).filter(Boolean)
201
+ }
202
+
203
+ export function injectedMarkerPatterns() {
204
+ const patterns = []
205
+ for (const adapter of defaultRegistry.list()) {
206
+ for (const pattern of (adapter.constants && adapter.constants.SESSION_MARKER_PATTERNS) || []) patterns.push(pattern)
207
+ }
208
+ return patterns
209
+ }
210
+
211
+ export function pluginEvidencePrefixes() {
212
+ return defaultRegistry.list().map((adapter) => adapter.constants && adapter.constants.EVIDENCE_PREFIX).filter((prefix) => typeof prefix === 'string' && prefix)
213
+ }
214
+
215
+ // ── 语义判定(核心不关心是哪家插件) ────────────────────────────────
216
+
217
+ function externalActions() {
218
+ return defaultRegistry.list().flatMap((adapter) => (adapter.constants && adapter.constants.ACTION_NAMES) || [])
219
+ }
220
+
221
+ export function isExternalMemoryAction(action) {
222
+ if (typeof action !== 'string') return false
223
+ const trimmed = action.trim()
224
+ if (!trimmed) return false
225
+ return externalActions().some((name) => trimmed === name) || defaultRegistry.list().some((adapter) => {
226
+ const prefix = adapter.constants && adapter.constants.ACTION_PREFIX
227
+ return typeof prefix === 'string' && prefix && trimmed.startsWith(prefix)
228
+ })
229
+ }
230
+
231
+ export function isExternalMemoryTarget(target) {
232
+ if (typeof target !== 'string') return false
233
+ const trimmed = target.trim()
234
+ return defaultRegistry.list().some((adapter) => trimmed === adapter.id)
235
+ }
236
+
237
+ export function isExternalMemoryTool(toolName) {
238
+ if (typeof toolName !== 'string') return false
239
+ const trimmed = toolName.trim()
240
+ return defaultRegistry.list().some((adapter) => {
241
+ const constants = adapter.constants || {}
242
+ if ((constants.LEGACY_TOOL_NAMES || []).includes(trimmed)) return true
243
+ const prefix = constants.TOOL_PREFIX
244
+ return typeof prefix === 'string' && prefix && trimmed.startsWith(prefix)
245
+ })
246
+ }
247
+
248
+ export function isExternalMemoryKind(kind) {
249
+ if (typeof kind !== 'string') return false
250
+ return defaultRegistry.list().some((adapter) => kind.includes(adapter.id))
251
+ }
252
+
253
+ /** 按语义分组取第三方动作名(approval/write/profileEdit 等,由 adapter 声明)。 */
254
+ export function externalActionGroups(group) {
255
+ const out = []
256
+ for (const adapter of defaultRegistry.list()) {
257
+ const groups = adapter.constants && adapter.constants.ACTION_GROUPS
258
+ if (groups && Array.isArray(groups[group])) out.push(...groups[group])
259
+ }
260
+ return [...new Set(out)]
261
+ }
262
+
263
+ export function planExternalAction(action) {
264
+ for (const adapter of defaultRegistry.list()) {
265
+ if (typeof adapter.plan !== 'function') continue
266
+ const result = safeCall(() => adapter.plan(action), { ok: false, degraded: true, code: 'E_ADAPTER_PLAN' })
267
+ if (result && result.code !== 'E_ADAPTER_ACTION') return { ...result, adapter: adapter.id }
268
+ }
269
+ return { ok: false, degraded: false, code: 'E_ADAPTER_UNKNOWN_ACTION', action }
270
+ }
271
+
272
+ export function externalProfileEditAction() {
273
+ return externalActionGroups('profileEdit')[0] || null
274
+ }
275
+
276
+ export function externalLegacyToolNames() {
277
+ const out = []
278
+ for (const adapter of defaultRegistry.list()) {
279
+ const constants = adapter.constants || {}
280
+ out.push(...(constants.LEGACY_TOOL_NAMES || []))
281
+ }
282
+ return [...new Set(out)]
283
+ }
284
+
285
+ export function externalWriteToolNames() {
286
+ const out = []
287
+ for (const adapter of defaultRegistry.list()) {
288
+ const constants = adapter.constants || {}
289
+ out.push(...(constants.WRITE_TOOL_NAMES || []))
290
+ }
291
+ return [...new Set(out)]
292
+ }
293
+
294
+ export function externalLegacyWriteToolNames() {
295
+ const out = []
296
+ for (const adapter of defaultRegistry.list()) {
297
+ const constants = adapter.constants || {}
298
+ out.push(...(constants.LEGACY_WRITE_TOOL_NAMES || []))
299
+ }
300
+ return [...new Set(out)]
301
+ }
302
+
303
+ export function isExternalMemoryProfileEdit(action) {
304
+ return defaultRegistry.list().some((adapter) => {
305
+ const constants = adapter.constants || {}
306
+ return (constants.ACTION_NAMES || []).includes(action) && action.endsWith('.profile.edit')
307
+ })
308
+ }
309
+
310
+ export function isExternalMemoryDeleteAction(action) {
311
+ return defaultRegistry.list().some((adapter) => {
312
+ const constants = adapter.constants || {}
313
+ return (constants.ACTION_NAMES || []).includes(action) && action.endsWith('.forget')
314
+ })
315
+ }