@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
@@ -0,0 +1,604 @@
1
+ // src/memory/governance.js — 1.8.0 / v2.8 §15.1-A1:Claim / Evidence / Bank + 授权矩阵
2
+ //
3
+ // 三个实体(写死,后续 A2/A3/A4 在其上扩展):
4
+ // Claim {id, bankId, subject:{entity,attribute}, content, kind, salience, sourceTier,
5
+ // trustScore, evidence:{version, items[]}, conflict, conflict_with[], state}
6
+ // Evidence {id, claimId, anchorId, sha256, excerptRef, verifier, verifiedAt, version,
7
+ // supersedes, sourceDoc}
8
+ // Bank {bankId, scope:user|project|org, owner, readers[], writers[], auditors[]}
9
+ //
10
+ // 授权矩阵(§15.1,写死):
11
+ // user read {user,project,org} write {user,project} audit {user,project}
12
+ // host read all write {project,org} audit all
13
+ // agent read {user} write {user} 需人判 (requiresHumanReview)
14
+ // plugin read {user} write {user} 需人判
15
+ // 越权 → E_BANK_FORBIDDEN + `.longlive/audit/bank.jsonl` 一行 denied(不静默)。
16
+ //
17
+ // 文件:
18
+ // <memoryRoot>/.longlive/claims.jsonl Claim 只追加(版本由 evidenceVersion 单调)
19
+ // <memoryRoot>/.longlive/banks.json 自定义 bank 注册表(可缺省,内置三个默认 bank)
20
+ // <memoryRoot>/.longlive/audit/bank.jsonl 越权/授权审计
21
+ // 旧 records.jsonl 保持只读兼容(listRecords 仍可读);迁移脚本 migrate-180.mjs 写入 claims.jsonl。
22
+
23
+ import crypto from 'node:crypto';
24
+
25
+ import { appendAuditRecord } from './audit-records.js';
26
+ import { trustScoreAt } from './trust.js';
27
+ import fs from 'node:fs';
28
+ import path from 'node:path';
29
+
30
+ export const CLAIMS_SCHEMA = 'claims/v1';
31
+ export const BANKS_SCHEMA = 'banks/v1';
32
+ export const BANK_SCOPES = Object.freeze(['user', 'project', 'org']);
33
+ export const BANK_ACTIONS = Object.freeze(['read', 'write', 'audit']);
34
+ export const BANK_ROLES = Object.freeze(['user', 'host', 'agent', 'plugin']);
35
+ export const CLAIM_STATES = Object.freeze(['active', 'verified', 'disputed', 'superseded', 'archived', 'unverified']);
36
+
37
+ export const DEFAULT_BANKS = Object.freeze([
38
+ { bankId: 'bank:user', scope: 'user', owner: 'user', readers: ['user', 'host'], writers: ['user'], auditors: ['user', 'host'] },
39
+ { bankId: 'bank:project', scope: 'project', owner: 'project', readers: ['user', 'host'], writers: ['user', 'host'], auditors: ['user', 'host'] },
40
+ { bankId: 'bank:org', scope: 'org', owner: 'org', readers: ['user', 'host'], writers: ['host'], auditors: ['host'] },
41
+ ]);
42
+
43
+ /** Authorization matrix (role × action × scope) — written down, not policy-engine. */
44
+ export const BANK_MATRIX = Object.freeze({
45
+ user: Object.freeze({
46
+ read: Object.freeze(['user', 'project', 'org']),
47
+ write: Object.freeze(['user', 'project']),
48
+ audit: Object.freeze(['user', 'project']),
49
+ }),
50
+ host: Object.freeze({
51
+ read: Object.freeze(['user', 'project', 'org']),
52
+ write: Object.freeze(['project', 'org']),
53
+ audit: Object.freeze(['user', 'project', 'org']),
54
+ }),
55
+ agent: Object.freeze({
56
+ read: Object.freeze(['user']),
57
+ write: Object.freeze(['user']),
58
+ audit: Object.freeze([]),
59
+ }),
60
+ plugin: Object.freeze({
61
+ read: Object.freeze(['user']),
62
+ write: Object.freeze(['user']),
63
+ audit: Object.freeze([]),
64
+ }),
65
+ });
66
+
67
+ export class GovernanceError extends Error {
68
+ constructor(code, message, detail = undefined, status = 403) {
69
+ super(message);
70
+ this.name = 'GovernanceError';
71
+ this.code = code;
72
+ this.status = status;
73
+ if (detail !== undefined) this.detail = detail;
74
+ }
75
+ }
76
+
77
+ export function claimsPathOf(memoryRoot) {
78
+ return path.join(path.resolve(String(memoryRoot)), '.longlive', 'claims.jsonl');
79
+ }
80
+
81
+ export function banksPathOf(memoryRoot) {
82
+ return path.join(path.resolve(String(memoryRoot)), '.longlive', 'banks.json');
83
+ }
84
+
85
+ export function bankAuditPathOf(memoryRoot) {
86
+ return path.join(path.resolve(String(memoryRoot)), '.longlive', 'audit', 'bank.jsonl');
87
+ }
88
+
89
+ function sha256Hex(text) {
90
+ return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
91
+ }
92
+
93
+ function sha1Short(text, length = 12) {
94
+ return crypto.createHash('sha1').update(text, 'utf8').digest('hex').slice(0, length);
95
+ }
96
+
97
+ function nowIso(now) {
98
+ const value = typeof now === 'function' ? now() : new Date();
99
+ return (value instanceof Date ? value : new Date(value)).toISOString();
100
+ }
101
+
102
+ /**
103
+ * Pure authorization decision.
104
+ * @returns {{allowed:boolean, requiresHumanReview:boolean, role:string, action:string, scope:string, code:string}}
105
+ */
106
+ export function canBankAccess({ role, action, scope }) {
107
+ const roleKey = BANK_ROLES.includes(role) ? role : null;
108
+ const actionKey = BANK_ACTIONS.includes(action) ? action : null;
109
+ const scopeKey = BANK_SCOPES.includes(scope) ? scope : null;
110
+ if (roleKey === null) return { allowed: false, requiresHumanReview: false, role, action, scope, code: 'E_BANK_ROLE' };
111
+ if (actionKey === null) return { allowed: false, requiresHumanReview: false, role, action, scope, code: 'E_BANK_ACTION' };
112
+ if (scopeKey === null) return { allowed: false, requiresHumanReview: false, role, action, scope, code: 'E_BANK_SCOPE' };
113
+ const allowed = BANK_MATRIX[roleKey][actionKey].includes(scopeKey);
114
+ const requiresHumanReview = allowed && roleKey !== 'user' && roleKey !== 'host' && actionKey === 'write';
115
+ return { allowed, requiresHumanReview, role: roleKey, action: actionKey, scope: scopeKey, code: allowed ? 'OK' : 'E_BANK_FORBIDDEN' };
116
+ }
117
+
118
+ function appendBankAudit(memoryRoot, line) {
119
+ try {
120
+ fs.mkdirSync(path.dirname(bankAuditPathOf(memoryRoot)), { recursive: true });
121
+ fs.appendFileSync(bankAuditPathOf(memoryRoot), `${JSON.stringify(line)}\n`, 'utf8');
122
+ } catch {
123
+ /* audit is best effort; the decision itself already failed closed */
124
+ }
125
+ }
126
+
127
+ /** Assert access; denied access is audited with denied:true then throws E_BANK_FORBIDDEN. */
128
+ export function assertBankAccess({ memoryRoot, role, action, scope, actor = null, reason = '', now = () => new Date() } = {}) {
129
+ const decision = canBankAccess({ role, action, scope });
130
+ if (!decision.allowed) {
131
+ appendBankAudit(memoryRoot, {
132
+ schema: 'bank-audit/v1',
133
+ at: nowIso(now),
134
+ role: decision.role ?? role,
135
+ action: decision.action ?? action,
136
+ scope: decision.scope ?? scope,
137
+ actor,
138
+ allowed: false,
139
+ denied: true,
140
+ code: decision.code,
141
+ reason,
142
+ });
143
+ throw new GovernanceError('E_BANK_FORBIDDEN', `bank 越权:role=${role} action=${action} scope=${scope}`, decision);
144
+ }
145
+ return decision;
146
+ }
147
+
148
+ /** Register/read custom banks; built-in three always available. */
149
+ export function listBanks({ memoryRoot }) {
150
+ const builtIn = DEFAULT_BANKS.map((bank) => ({ ...bank, builtin: true }));
151
+ let custom = [];
152
+ try {
153
+ const parsed = JSON.parse(fs.readFileSync(banksPathOf(memoryRoot), 'utf8'));
154
+ if (Array.isArray(parsed?.banks)) custom = parsed.banks;
155
+ } catch {
156
+ custom = [];
157
+ }
158
+ return [...builtIn, ...custom];
159
+ }
160
+
161
+ export function registerBank({ memoryRoot, bank, now = () => new Date() }) {
162
+ const value = bank && typeof bank === 'object' ? bank : null;
163
+ if (!value || typeof value.bankId !== 'string' || value.bankId === '') {
164
+ throw new GovernanceError('E_BANK_ID', 'bank.bankId required', undefined, 400);
165
+ }
166
+ if (!BANK_SCOPES.includes(value.scope)) {
167
+ throw new GovernanceError('E_BANK_SCOPE', `bank.scope 非法:${String(value.scope)}`, undefined, 400);
168
+ }
169
+ const banks = listBanks({ memoryRoot }).filter((entry) => entry.builtin !== true);
170
+ const next = banks.filter((entry) => entry.bankId !== value.bankId);
171
+ next.push({
172
+ bankId: value.bankId,
173
+ scope: value.scope,
174
+ owner: value.owner ?? null,
175
+ readers: Array.isArray(value.readers) ? value.readers : ['host'],
176
+ writers: Array.isArray(value.writers) ? value.writers : ['host'],
177
+ auditors: Array.isArray(value.auditors) ? value.auditors : ['host'],
178
+ createdAt: value.createdAt ?? nowIso(now),
179
+ });
180
+ const file = banksPathOf(memoryRoot);
181
+ fs.mkdirSync(path.dirname(file), { recursive: true });
182
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
183
+ fs.writeFileSync(tmp, `${JSON.stringify({ schema: BANKS_SCHEMA, banks: next }, null, 2)}\n`, 'utf8');
184
+ fs.renameSync(tmp, file);
185
+ return { ok: true, banks: next, file };
186
+ }
187
+
188
+ /** Claim/Evidence entity normalization (backward compatible with v2.6.4 records). */
189
+ export function normalizeEvidence(entry, claimId = null) {
190
+ const value = entry && typeof entry === 'object' ? entry : {};
191
+ return {
192
+ id: typeof value.id === 'string' ? value.id : `ev_${sha1Short(`${claimId ?? ''}|${value.anchorId ?? ''}|${value.sha256 ?? ''}`)}`,
193
+ claimId: typeof value.claimId === 'string' ? value.claimId : claimId,
194
+ anchorId: typeof value.anchorId === 'string' ? value.anchorId : null,
195
+ sha256: typeof value.sha256 === 'string' ? value.sha256 : null,
196
+ excerptRef: typeof value.excerptRef === 'string' ? value.excerptRef : null,
197
+ verifier: typeof value.verifier === 'string' ? value.verifier : null,
198
+ verifiedAt: value.verifiedAt ?? null,
199
+ version: Number.isInteger(value.version) ? value.version : 1,
200
+ supersedes: typeof value.supersedes === 'string' ? value.supersedes : null,
201
+ sourceDoc: typeof value.sourceDoc === 'string' ? value.sourceDoc : null,
202
+ };
203
+ }
204
+
205
+ export function normalizeSubject(subject, fallbackId = null) {
206
+ if (subject && typeof subject === 'object' && typeof subject.entity === 'string') {
207
+ return {
208
+ entity: subject.entity.trim().toLowerCase(),
209
+ attribute: typeof subject.attribute === 'string' && subject.attribute.trim() !== '' ? subject.attribute.trim().toLowerCase() : null,
210
+ };
211
+ }
212
+ const raw = typeof subject === 'string' && subject.trim() !== '' ? subject.trim().toLowerCase() : fallbackId;
213
+ return { entity: raw ?? 'unknown', attribute: null };
214
+ }
215
+
216
+ export function normalizeClaim(row) {
217
+ const value = row && typeof row === 'object' ? row : {};
218
+ const claimId = typeof value.id === 'string' && value.id !== '' ? value.id : `clm_${sha1Short(JSON.stringify(value))}`;
219
+ const evidenceItems = Array.isArray(value.evidence)
220
+ ? value.evidence.map((entry) => normalizeEvidence(entry, claimId))
221
+ : (Array.isArray(value.evidence?.items) ? value.evidence.items.map((entry) => normalizeEvidence(entry, claimId)) : []);
222
+ const evidenceVersion = Number.isInteger(value.evidenceVersion)
223
+ ? value.evidenceVersion
224
+ : (Number.isInteger(value.evidence?.version) ? value.evidence.version : evidenceItems.length);
225
+ return {
226
+ schema: CLAIMS_SCHEMA,
227
+ id: claimId,
228
+ bankId: typeof value.bankId === 'string' && value.bankId !== '' ? value.bankId : 'bank:project',
229
+ subject: normalizeSubject(value.subject, claimId),
230
+ content: typeof value.content === 'string' ? value.content : '',
231
+ kind: typeof value.kind === 'string' ? value.kind : 'fact',
232
+ salience: Number.isInteger(value.salience) ? value.salience : 3,
233
+ sourceTier: ['user', 'host', 'agent', 'plugin'].includes(value.sourceTier) ? value.sourceTier : 'host',
234
+ trustScore: typeof value.trustScore === 'number' ? value.trustScore : null,
235
+ verifyCount: Number.isInteger(value.verifyCount) ? value.verifyCount : 0,
236
+ claimVersion: Number.isInteger(value.claimVersion) ? value.claimVersion : 1,
237
+ supersedes: Array.isArray(value.supersedes) ? value.supersedes.map(Number).filter(Number.isInteger) : [],
238
+ supersededBy: Number.isInteger(value.supersededBy) ? value.supersededBy : null,
239
+ evidence: { version: evidenceVersion, items: evidenceItems },
240
+ conflict: value.conflict === true,
241
+ conflict_with: Array.isArray(value.conflict_with) ? value.conflict_with : [],
242
+ state: CLAIM_STATES.includes(value.state) ? value.state : (['active', 'superseded', 'forgotten', 'archived', 'restored'].includes(value.status) ? value.status : 'active'),
243
+ project: value.project ?? null,
244
+ anchors: Array.isArray(value.anchors) ? value.anchors : [],
245
+ review: value.review && typeof value.review === 'object' ? value.review : null,
246
+ createdAt: value.createdAt ?? null,
247
+ updatedAt: value.updatedAt ?? null,
248
+ audit: value.audit && typeof value.audit === 'object' ? value.audit : {},
249
+ legacy: value.legacy === true,
250
+ };
251
+ }
252
+
253
+ export function readClaimsForHost({ memoryRoot }) {
254
+ const file = claimsPathOf(memoryRoot);
255
+ const items = [];
256
+ const bad = [];
257
+ let present = false;
258
+ let text = '';
259
+ try {
260
+ text = fs.readFileSync(file, 'utf8');
261
+ present = true;
262
+ } catch {
263
+ text = '';
264
+ }
265
+ for (const line of text.split('\n')) {
266
+ if (line.trim() === '') continue;
267
+ try {
268
+ items.push(normalizeClaim(JSON.parse(line)));
269
+ } catch {
270
+ bad.push(line.slice(0, 120));
271
+ }
272
+ }
273
+ // GOV-05: append-only rows keep every version; the read view marks older versions.
274
+ const maxById = new Map();
275
+ for (const item of items) {
276
+ const version = Number.isInteger(item.claimVersion) ? item.claimVersion : 1;
277
+ if (!maxById.has(item.id) || version > maxById.get(item.id)) maxById.set(item.id, version);
278
+ }
279
+ const marked = items.map((item) => {
280
+ const max = maxById.get(item.id) ?? 1;
281
+ const version = Number.isInteger(item.claimVersion) ? item.claimVersion : 1;
282
+ if (version >= max) return item;
283
+ return { ...item, state: ['forgotten', 'archived'].includes(item.state) ? item.state : 'superseded', supersededBy: max };
284
+ });
285
+ return { schema: CLAIMS_SCHEMA, present, items: marked, badLines: bad, file };
286
+ }
287
+
288
+ /**
289
+ * GOV-02 read axis (A5): Claim reads require a host-injected actor AND are
290
+ * filtered by the bank authorization matrix for that actor's role. An actor
291
+ * that can read none of the present claims is refused with E_BANK_FORBIDDEN.
292
+ */
293
+ export function readClaims({ memoryRoot, actor = null, now = () => new Date() }) {
294
+ if (!actor || typeof actor.kind !== 'string' || actor.kind === '') {
295
+ throw new GovernanceError('E_ACTOR_REQUIRED', '读 Claim 需要宿主 actor(禁止匿名读)。', undefined, 401);
296
+ }
297
+ const role = actor.kind;
298
+ if (!BANK_ROLES.includes(role)) {
299
+ throw new GovernanceError('E_BANK_FORBIDDEN', `未知 actor 角色:${role}`, undefined, 403);
300
+ }
301
+ const result = readClaimsForHost({ memoryRoot });
302
+ const scopeOf = new Map(listBanks({ memoryRoot }).map((bank) => [bank.bankId, bank.scope]));
303
+ const readable = [];
304
+ const denied = [];
305
+ for (const claim of result.items) {
306
+ const scope = scopeOf.get(claim.bankId) ?? null;
307
+ const decision = canBankAccess({ role, action: 'read', scope });
308
+ if (decision.allowed) readable.push(claim);
309
+ else denied.push({ id: claim.id, bankId: claim.bankId ?? null });
310
+ }
311
+ if (result.items.length > 0 && readable.length === 0) {
312
+ appendBankAudit(memoryRoot, { schema: 'bank-audit/v1', at: nowIso(now), role, action: 'read', scope: null, allowed: false, denied: true, code: 'E_BANK_FORBIDDEN', deniedCount: denied.length, reason: 'read-axis' });
313
+ auditRow(memoryRoot, { action: 'denied', recordId: denied[0]?.id ?? null, readerTier: role, now, extra: { code: 'E_BANK_FORBIDDEN', readAxis: true } });
314
+ throw new GovernanceError('E_BANK_FORBIDDEN', `读轴越权:role=${role} 可读 0/${result.items.length} 条(bank/scope 授权矩阵拒绝)。`, { role, deniedCount: denied.length }, 403);
315
+ }
316
+ // A5: read auditing for answer/recall 生产路径 is written there (not on every
317
+ // internal readClaims call — that would turn panel reads into audit noise).
318
+ return { ...result, items: readable, total: result.items.length, deniedCount: denied.length };
319
+ }
320
+
321
+ /** Append a claim (idempotent by id: same id+content returns existing). */
322
+ export function appendClaim({ memoryRoot, claim, now = () => new Date() }) {
323
+ const normalized = normalizeClaim({ ...claim, createdAt: claim?.createdAt ?? nowIso(now), updatedAt: nowIso(now) });
324
+ const existing = readClaimsForHost({ memoryRoot }).items.find((item) => item.id === normalized.id);
325
+ let next = normalized;
326
+ if (existing) {
327
+ const sameContent = existing.content === normalized.content
328
+ && JSON.stringify(existing.evidence?.items ?? []) === JSON.stringify(normalized.evidence?.items ?? []);
329
+ if (sameContent) {
330
+ return { ok: true, appended: false, claim: existing, file: claimsPathOf(memoryRoot) };
331
+ }
332
+ // GOV-05: append-only version chain — v1 stays on disk, new row is claimVersion+1.
333
+ const prevVersion = Number.isInteger(existing.claimVersion) ? existing.claimVersion : 1;
334
+ next = normalizeClaim({
335
+ ...normalized,
336
+ claimVersion: prevVersion + 1,
337
+ supersedes: [...new Set([...(existing.supersedes ?? []), prevVersion])],
338
+ createdAt: existing.createdAt ?? normalized.createdAt,
339
+ updatedAt: nowIso(now),
340
+ });
341
+ }
342
+ const file = claimsPathOf(memoryRoot);
343
+ fs.mkdirSync(path.dirname(file), { recursive: true });
344
+ fs.appendFileSync(file, `${JSON.stringify(next)}\n`, 'utf8');
345
+ return { ok: true, appended: true, claim: next, file };
346
+ }
347
+
348
+ /**
349
+ * GOV-05 claim.verify — host-internal writer (NOT a panel action).
350
+ * CAS on expectedVersion (== current max claimVersion); increments verifyCount,
351
+ * recomputes trustScore server-side, appends a new version (append-only).
352
+ */
353
+ export function claimVerify({ memoryRoot, claimId, expectedVersion, verifier = 'host', actor = null, now = () => new Date() } = {}) {
354
+ const role = actor?.kind ?? null;
355
+ if (role === null || role === '') throw new GovernanceError('E_ACTOR_REQUIRED', 'claim.verify 需要宿主 actor。', undefined, 401);
356
+ if (role !== 'host') throw new GovernanceError('E_BANK_FORBIDDEN', 'claim.verify 只允许 host 内部写入器(不经面板动作面)。', { role }, 403);
357
+ if (!Number.isInteger(expectedVersion) || expectedVersion < 1) {
358
+ throw new GovernanceError('E_EXPECTED_VERSION_REQUIRED', 'claim.verify 需要 expectedVersion(正整数 CAS)。', undefined, 400);
359
+ }
360
+ const id = String(claimId ?? '');
361
+ if (id === '') throw new GovernanceError('E_CLAIM_ID', 'claim.verify 需要 claimId。', undefined, 400);
362
+ const all = readClaimsForHost({ memoryRoot }).items.filter((item) => item.id === id);
363
+ if (all.length === 0) throw new GovernanceError('E_CLAIM_NOT_FOUND', `claim 不存在:${id}`, undefined, 404);
364
+ const latest = all.reduce((best, item) => ((item.claimVersion ?? 1) > (best.claimVersion ?? 1) ? item : best), all[0]);
365
+ const currentVersion = Number.isInteger(latest.claimVersion) ? latest.claimVersion : 1;
366
+ if (currentVersion !== expectedVersion) {
367
+ throw new GovernanceError('E_CAS_CONFLICT', `claim 版本冲突:expected=${expectedVersion} current=${currentVersion}`, { expectedVersion, currentVersion }, 409);
368
+ }
369
+ const verifyCount = (Number.isInteger(latest.verifyCount) ? latest.verifyCount : 0) + 1;
370
+ const score = trustScoreAt({ sourceTier: latest.sourceTier, verifyCount, createdAt: latest.createdAt, now });
371
+ const next = normalizeClaim({
372
+ ...latest,
373
+ claimVersion: currentVersion + 1,
374
+ verifyCount,
375
+ trustScore: score.trustScore,
376
+ supersedes: [...new Set([...(latest.supersedes ?? []), currentVersion])],
377
+ updatedAt: nowIso(now),
378
+ audit: { ...(latest.audit ?? {}), lastVerifiedBy: String(verifier), lastVerifiedAt: nowIso(now) },
379
+ });
380
+ const file = claimsPathOf(memoryRoot);
381
+ fs.mkdirSync(path.dirname(file), { recursive: true });
382
+ fs.appendFileSync(file, `${JSON.stringify(next)}\n`, 'utf8');
383
+ auditRow(memoryRoot, { action: 'verify', recordId: id, readerTier: role, now, extra: { claimVersion: next.claimVersion, verifyCount, verifier: String(verifier) } });
384
+ return { ok: true, claim: next, claimVersion: next.claimVersion, verifyCount, trustScore: next.trustScore, file };
385
+ }
386
+
387
+ /** Queue a claim awaiting human review (EA-09: never land it as active first). */
388
+ export function enqueueReview({ memoryRoot, item, now = () => new Date() }) {
389
+ const file = path.join(path.resolve(String(memoryRoot)), '.longlive', 'review', 'queue.jsonl');
390
+ fs.mkdirSync(path.dirname(file), { recursive: true });
391
+ const line = JSON.stringify({ schema: 'review-queue/v1', state: 'pending', ...item, createdAt: nowIso(now) });
392
+ fs.appendFileSync(file, `${line}\n`, 'utf8');
393
+ return { file, item: JSON.parse(line) };
394
+ }
395
+
396
+ /** Reject client-supplied trust/state inputs (GOV-03 P0). */
397
+ export function assertNoTrustInput(claim) {
398
+ const value = claim && typeof claim === 'object' ? claim : {};
399
+ const forbidden = ['sourceTier', 'trustScore', 'verifyCount', 'claimVersion'].filter((key) => Object.prototype.hasOwnProperty.call(value, key));
400
+ if (forwardState(value.state)) forbidden.push('state');
401
+ if (forbidden.length > 0) {
402
+ throw new GovernanceError('E_TRUST_INPUT_FORBIDDEN', `trust/state 字段只能由服务端计算,禁止 payload 直传:${forbidden.join(',')}`, { forbidden }, 400);
403
+ }
404
+ }
405
+
406
+ function forwardState(state) {
407
+ return state !== undefined && state !== null && !['active', 'disputed'].includes(state);
408
+ }
409
+
410
+ function bankScopeOf(memoryRoot, bankId) {
411
+ const bank = listBanks({ memoryRoot }).find((entry) => entry.bankId === bankId);
412
+ return bank ? bank.scope : null;
413
+ }
414
+
415
+ function auditRow(memoryRoot, { action, recordId, readerTier, extra, now }) {
416
+ try {
417
+ appendAuditRecord({
418
+ memoryRoot,
419
+ action,
420
+ recordId: recordId ?? null,
421
+ readerTier: readerTier ?? null,
422
+ questionId: 'unknown',
423
+ expectedTurns: null,
424
+ answerText: null,
425
+ now,
426
+ extra,
427
+ });
428
+ } catch {
429
+ /* audit must not be silently lost: callers still fail closed on the primary path */
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Write path (GOV-01/03): role + scope are SERVER-derived from the host actor
435
+ * (token-bound); bank scope must agree with the requested scope; trust fields
436
+ * are computed by the server and client-supplied values are rejected.
437
+ */
438
+ export function writeClaim({ memoryRoot, claim, role: claimedRole = null, scope: claimedScope = null, actor = null, now = () => new Date() }) {
439
+ // 1) actor is host-injected; missing actor never falls back to a role.
440
+ if (!actor || typeof actor.kind !== 'string' || actor.kind === '') {
441
+ auditRow(memoryRoot, { action: 'denied', recordId: claim?.id, readerTier: 'unknown', now, extra: { code: 'E_ACTOR_REQUIRED' } });
442
+ throw new GovernanceError('E_ACTOR_REQUIRED', 'actor 必须由宿主注入(令牌绑定),缺省拒绝。', undefined, 401);
443
+ }
444
+ const role = actor.kind;
445
+ // Caller-declared role/scope are NEVER trusted: any mismatch with the token actor
446
+ // or the bank's scope is a forgery attempt (GOV-01).
447
+ if (claimedRole !== null && claimedRole !== role) {
448
+ auditRow(memoryRoot, { action: 'denied', recordId: claim?.id, readerTier: role, now, extra: { code: 'E_BANK_FORBIDDEN', claimedRole } });
449
+ throw new GovernanceError('E_BANK_FORBIDDEN', `role 伪造:payload=${claimedRole} actor=${role}`, { claimedRole, role }, 403);
450
+ }
451
+ if (!BANK_ROLES.includes(role)) {
452
+ auditRow(memoryRoot, { action: 'denied', recordId: claim?.id, readerTier: null, now, extra: { code: 'E_BANK_ROLE' } });
453
+ throw new GovernanceError('E_BANK_ROLE', `未知 actor 角色:${role}`, undefined, 403);
454
+ }
455
+ const claimId = claim?.id ?? null;
456
+ // 2) trust/state inputs must never come from the payload (denied attempt is audited).
457
+ try {
458
+ assertNoTrustInput(claim);
459
+ } catch (error) {
460
+ auditRow(memoryRoot, { action: 'denied', recordId: claim?.id ?? null, readerTier: role, now, extra: { code: error.code ?? 'E_TRUST_INPUT_FORBIDDEN' } });
461
+ throw error;
462
+ }
463
+ // 3) bank/scope are server-derived: claim.bankId decides scope, caller cannot override.
464
+ const bankId = typeof claim?.bankId === 'string' ? claim.bankId : 'bank:project';
465
+ const scope = bankScopeOf(memoryRoot, bankId);
466
+ if (scope === null) {
467
+ auditRow(memoryRoot, { action: 'denied', recordId: claimId, readerTier: role, now, extra: { code: 'E_BANK_FORBIDDEN', bankId } });
468
+ throw new GovernanceError('E_BANK_FORBIDDEN', `未知 bank:${bankId}`, { bankId }, 403);
469
+ }
470
+ if (claimedScope !== null && claimedScope !== scope) {
471
+ auditRow(memoryRoot, { action: 'denied', recordId: claimId, readerTier: role, now, extra: { code: 'E_BANK_FORBIDDEN', claimedScope, bankId } });
472
+ throw new GovernanceError('E_BANK_FORBIDDEN', `scope 伪造:payload=${claimedScope} bank=${bankId} scope=${scope}`, { claimedScope, bankId, scope }, 403);
473
+ }
474
+ const decision = canBankAccess({ role, action: 'write', scope });
475
+ if (!decision.allowed) {
476
+ appendBankAuditFor(memoryRoot, { role, scope, claimId, now });
477
+ auditRow(memoryRoot, { action: 'denied', recordId: claimId, readerTier: role, now, extra: { code: 'E_BANK_FORBIDDEN', bankId, scope } });
478
+ throw new GovernanceError('E_BANK_FORBIDDEN', `bank 越权:role=${role} bank=${bankId} scope=${scope}`, decision, 403);
479
+ }
480
+ // 4) server computes trust/state/sourceTier (client values already rejected).
481
+ const score = trustScoreAt({ sourceTier: role, verifyCount: 0, createdAt: null, now });
482
+ const normalized = normalizeClaim({
483
+ ...claim,
484
+ bankId,
485
+ sourceTier: role,
486
+ trustScore: score.trustScore,
487
+ verifyCount: 0,
488
+ claimVersion: 1,
489
+ state: 'active',
490
+ });
491
+ if (decision.requiresHumanReview) {
492
+ const queued = enqueueReview({ memoryRoot, now, item: { kind: 'claim', target: { kind: 'claim', id: normalized.id }, claim: normalized, reason: 'E_HUMAN_REVIEW_REQUIRED', requestedBy: role } });
493
+ auditRow(memoryRoot, { action: 'review', recordId: normalized.id, readerTier: role, now, extra: { code: 'E_HUMAN_REVIEW_REQUIRED' } });
494
+ return { ok: true, appended: false, queued: true, requiresHumanReview: true, code: 'E_HUMAN_REVIEW_REQUIRED', review: queued.item };
495
+ }
496
+ const result = appendClaim({ memoryRoot, claim: normalized, now });
497
+ auditRow(memoryRoot, { action: 'write', recordId: normalized.id, readerTier: role, now, extra: { bankId, scope, trustScore: normalized.trustScore } });
498
+ return { ...result, requiresHumanReview: false };
499
+ }
500
+
501
+ function appendBankAuditFor(memoryRoot, { role, scope, claimId, now }) {
502
+ try {
503
+ const dir = path.join(path.resolve(String(memoryRoot)), '.longlive', 'audit');
504
+ fs.mkdirSync(dir, { recursive: true });
505
+ fs.appendFileSync(path.join(dir, 'bank.jsonl'), `${JSON.stringify({ schema: 'bank-audit/v1', at: nowIso(now), role, action: 'write', scope, allowed: false, denied: true, code: 'E_BANK_FORBIDDEN', recordId: claimId })}\n`, 'utf8');
506
+ } catch {
507
+ /* best effort */
508
+ }
509
+ }
510
+
511
+ /** Map a runtime record (records.jsonl writer) to a Claim for the runtime consumer path. */
512
+ /** A5: server-derived producer tier. Incoming record.sourceTier is NEVER trusted. */
513
+ export function serverTierOfRecord(record) {
514
+ const value = record && typeof record === 'object' ? record : {};
515
+ const createdBy = String(value?.audit?.createdBy ?? '');
516
+ if (createdBy === 'memory-note') return 'user';
517
+ if (createdBy === 'activity-capture') return 'agent';
518
+ if (createdBy.startsWith('capture')) return 'host';
519
+ // Unknown producer => fail-safe agent (never silently escalate to host/user).
520
+ return 'agent';
521
+ }
522
+
523
+ export function claimFromRecord(record, { now = () => new Date() } = {}) {
524
+ const value = record && typeof record === 'object' ? record : {};
525
+ const id = typeof value.id === 'string' && value.id.startsWith('clm_') ? value.id : `clm_${sha1Short(String(value.id ?? JSON.stringify(value)))}`;
526
+ // A5: sourceTier + trustScore are SERVER-computed. Caller-supplied record.sourceTier/
527
+ // trustScore/verifyCount/state are ignored (only evidence/anchors/content are data).
528
+ const sourceTier = serverTierOfRecord(value);
529
+ const createdAt = value.createdAt ?? nowIso(now);
530
+ const score = trustScoreAt({ sourceTier, verifyCount: 0, createdAt, now });
531
+ const bankId = sourceTier === 'user' || sourceTier === 'agent' || sourceTier === 'plugin' ? 'bank:user' : 'bank:project';
532
+ return normalizeClaim({
533
+ id,
534
+ bankId,
535
+ subject: value.subject ?? { entity: String(value.id ?? id) },
536
+ content: value.content ?? '',
537
+ kind: value.kind ?? 'fact',
538
+ salience: value.salience,
539
+ sourceTier,
540
+ trustScore: score.trustScore,
541
+ verifyCount: 0,
542
+ claimVersion: 1,
543
+ evidence: { version: Array.isArray(value.evidence) ? value.evidence.length : 0, items: Array.isArray(value.evidence) ? value.evidence : [] },
544
+ state: 'active',
545
+ project: value.project ?? null,
546
+ anchors: Array.isArray(value.anchors) ? value.anchors : [],
547
+ createdAt,
548
+ updatedAt: nowIso(now),
549
+ audit: { ...(value.audit ?? {}), bridgedFromRecords: true, serverDerivedTier: true },
550
+ });
551
+ }
552
+
553
+ /** Runtime dual-write: every records write also lands a Claim + audit write row. */
554
+ export function appendClaimFromRecord({ memoryRoot, record, now = () => new Date() }) {
555
+ const claim = claimFromRecord(record, { now });
556
+ const result = appendClaim({ memoryRoot, claim, now });
557
+ if (result.appended) {
558
+ auditRow(memoryRoot, { action: 'write', recordId: claim.id, readerTier: claim.sourceTier, now, extra: { kind: 'claim-from-record', bridgeRecordId: record?.id ?? null } });
559
+ }
560
+ return { claim: result.claim ?? claim, appended: result.appended === true };
561
+ }
562
+
563
+ /** Migration helper: map one legacy records.jsonl row to a Claim (pure). */
564
+ export function claimFromLegacyRecord(row) {
565
+ const value = row && typeof row === 'object' ? row : {};
566
+ const legacyId = typeof value.id === 'string' ? value.id : `rec:${sha1Short(JSON.stringify(value))}`;
567
+ const claimId = legacyId.startsWith('clm_') ? legacyId : `clm_${sha1Short(legacyId)}`;
568
+ const evidenceItems = Array.isArray(value.evidence) ? value.evidence : [];
569
+ return normalizeClaim({
570
+ id: claimId,
571
+ bankId: typeof value.bankId === 'string' ? value.bankId : 'bank:project',
572
+ subject: value.subject ?? { entity: legacyId, attribute: null },
573
+ content: value.content ?? '',
574
+ kind: value.kind ?? 'fact',
575
+ salience: value.salience,
576
+ // 迁移不因缺字段提权:未知来源按 agent 处理(绝不升成 host)。
577
+ sourceTier: ['user', 'host', 'agent', 'plugin'].includes(value.sourceTier) ? value.sourceTier : 'agent',
578
+ trustScore: value.trustScore,
579
+ evidence: {
580
+ version: evidenceItems.length,
581
+ items: evidenceItems.map((entry) => ({
582
+ ...entry,
583
+ verifier: entry.verifier ?? null,
584
+ sourceDoc: entry.sourceDoc ?? 'records.jsonl',
585
+ })),
586
+ },
587
+ conflict: value.conflict === true,
588
+ conflict_with: Array.isArray(value.conflict_with) ? value.conflict_with : [],
589
+ state: value.state ?? value.status ?? 'active',
590
+ project: value.project ?? null,
591
+ anchors: Array.isArray(value.anchors) ? value.anchors : [],
592
+ review: value.review && typeof value.review === 'object' ? value.review : null,
593
+ createdAt: value.createdAt ?? null,
594
+ legacy: true,
595
+ audit: {
596
+ ...(value.audit ?? {}),
597
+ migratedFrom: 'records.jsonl',
598
+ migratedAt: nowIso(),
599
+ sourceTierDefaulted: value.sourceTier === undefined,
600
+ },
601
+ });
602
+ }
603
+
604
+ export { sha256Hex };
@@ -72,6 +72,7 @@ function readCard(root, abs) {
72
72
  summary: firstSummaryLine(parsed.body),
73
73
  legacy: parsed.legacy,
74
74
  fields: parsed.fields,
75
+ extras: parsed.extras ?? {},
75
76
  };
76
77
  }
77
78