@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,477 @@
1
+ // src/role/relationships.js — 角色内核 · relationship graph(v2.8 §16.3 ③)
2
+ //
3
+ // 设计口径(§16.3):
4
+ // - `.longlive/role/relationships.json`:nodes `{personId,tier,confidence,updatedAt}`,
5
+ // edges `{personId→personId, relation, lastSeenAt}`;tier ∈ stranger|familiar|trusted|intimate;
6
+ // - 动作 G7 `relationship.update`(人判 + reason≥10字 + CAS + 审计 + 可回退标记);
7
+ // - 接口:`relationshipTier → recall 排序权重乘子 {1.0,1.05,1.1,1.15}`
8
+ // (只排序,不改事实真值/保护谓词)。
9
+ //
10
+ // 任务书扩到「用户-项目-会话-事件」关系:本文件用统一的 `from/to` 字符串标识,
11
+ // 项目/会话/事件写成 `project:<name>`、`session:<id>`、`event:<id>` 形态(schema 不变);
12
+ // person 节点额外记录称呼 `appellations[]`(老大/主人等)。
13
+ //
14
+ // 读写纪律:写路径校验 + CAS + 审计(append-only `.longlive/role/audit.jsonl`),
15
+ // 读路径永不抛;坏文件降级为 present:false/degraded:true。
16
+
17
+ import {
18
+ RoleError, ROLE_CODES, appendJsonl, ensureRoleDir, iso, nowMs, readJsonSafe, readJsonlSafe, rolePaths, tryRolePaths, writeJsonAtomic,
19
+ } from './store.js';
20
+
21
+ export const RELATIONSHIPS_SCHEMA = 'relationships/v1';
22
+ export const RELATIONSHIP_TIERS = Object.freeze(['stranger', 'familiar', 'trusted', 'intimate']);
23
+ /** 三接口之一:tier → recall 排序权重乘子(只影响排序)。 */
24
+ export const TIER_WEIGHTS = Object.freeze({
25
+ stranger: 1.0,
26
+ familiar: 1.05,
27
+ trusted: 1.1,
28
+ intimate: 1.15,
29
+ });
30
+ /** 新关系的默认置信度(人判更新 tier 时未显式给 confidence 的落点)。 */
31
+ export const TIER_CONFIDENCE = Object.freeze({
32
+ stranger: 0.3,
33
+ familiar: 0.55,
34
+ trusted: 0.8,
35
+ intimate: 0.95,
36
+ });
37
+ export const RELATIONSHIP_DEFAULTS = Object.freeze({
38
+ tier: 'stranger',
39
+ multiplier: TIER_WEIGHTS.stranger,
40
+ confidence: TIER_CONFIDENCE.stranger,
41
+ appellationMax: 20,
42
+ appellationChars: 40,
43
+ edgesMax: 200,
44
+ });
45
+ export const RELATIONSHIP_CODES = Object.freeze({
46
+ PARAMS: ROLE_CODES.PARAMS,
47
+ TIER: 'E_RELATIONSHIP_TIER',
48
+ PERSON: 'E_RELATIONSHIP_PERSON',
49
+ REASON: 'E_RELATIONSHIP_REASON',
50
+ CAS: ROLE_CODES.CAS_CONFLICT,
51
+ CORRUPT: ROLE_CODES.CORRUPT,
52
+ });
53
+
54
+ function relationshipError(code, message, details = undefined) {
55
+ const error = new RoleError(code, message, details);
56
+ error.status = code === RELATIONSHIP_CODES.CAS ? 409 : 422;
57
+ return error;
58
+ }
59
+
60
+ export function isRelationshipTier(value) {
61
+ return RELATIONSHIP_TIERS.includes(String(value ?? ''));
62
+ }
63
+
64
+ /** Tier → recall weight multiplier; invalid tier degrades to stranger's 1.0. */
65
+ export function relationshipTierMultiplier(tier) {
66
+ const key = String(tier ?? '');
67
+ return Object.prototype.hasOwnProperty.call(TIER_WEIGHTS, key) ? TIER_WEIGHTS[key] : TIER_WEIGHTS.stranger;
68
+ }
69
+
70
+ function normalizePersonId(value) {
71
+ const id = typeof value === 'string' ? value.trim() : '';
72
+ if (id === '' || Array.from(id).length > 120) {
73
+ throw relationshipError(RELATIONSHIP_CODES.PERSON, `personId 非法:${JSON.stringify(value)}`);
74
+ }
75
+ return id;
76
+ }
77
+
78
+ function normalizeReason(value) {
79
+ const reason = typeof value === 'string' ? value.trim() : '';
80
+ if (Array.from(reason).length < 10) {
81
+ throw relationshipError(RELATIONSHIP_CODES.REASON, 'relationship.update 需要 reason(≥10 字的人判理由)。');
82
+ }
83
+ return reason;
84
+ }
85
+
86
+ function normalizeAppellations(value, existing = []) {
87
+ const merged = new Map();
88
+ for (const item of [...existing, ...(Array.isArray(value) ? value : [])]) {
89
+ if (typeof item !== 'string') continue;
90
+ const text = item.trim();
91
+ if (text === '') continue;
92
+ const clipped = Array.from(text).slice(0, RELATIONSHIP_DEFAULTS.appellationChars).join('');
93
+ merged.set(clipped, clipped);
94
+ }
95
+ return [...merged.values()].slice(-RELATIONSHIP_DEFAULTS.appellationMax);
96
+ }
97
+
98
+ function normalizeEdge(raw) {
99
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
100
+ const from = typeof raw.from === 'string' ? raw.from.trim() : '';
101
+ const to = typeof raw.to === 'string' ? raw.to.trim() : '';
102
+ const relation = typeof raw.relation === 'string' ? raw.relation.trim() : '';
103
+ if (from === '' || to === '' || relation === '') return null;
104
+ const lastSeenAt = typeof raw.lastSeenAt === 'string' && Number.isFinite(Date.parse(raw.lastSeenAt))
105
+ ? raw.lastSeenAt
106
+ : null;
107
+ return { from, to, relation, lastSeenAt };
108
+ }
109
+
110
+ function normalizeNode(raw) {
111
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
112
+ if (typeof raw.personId !== 'string' || raw.personId.trim() === '') return null;
113
+ const tier = isRelationshipTier(raw.tier) ? String(raw.tier) : RELATIONSHIP_DEFAULTS.tier;
114
+ const confidence = typeof raw.confidence === 'number' && Number.isFinite(raw.confidence)
115
+ ? Math.min(1, Math.max(0, raw.confidence))
116
+ : TIER_CONFIDENCE[tier] ?? RELATIONSHIP_DEFAULTS.confidence;
117
+ return {
118
+ personId: raw.personId.trim(),
119
+ tier,
120
+ confidence,
121
+ updatedAt: typeof raw.updatedAt === 'string' && Number.isFinite(Date.parse(raw.updatedAt)) ? raw.updatedAt : null,
122
+ version: Number.isInteger(raw.version) && raw.version >= 0 ? raw.version : (raw.updatedAt ? 1 : 0),
123
+ aliases: Array.isArray(raw.aliases) ? raw.aliases.filter((item) => typeof item === 'string').slice(0, 20) : [],
124
+ appellations: normalizeAppellations(raw.appellations),
125
+ };
126
+ }
127
+
128
+ /** Read the graph; nodes always as an array (legacy object-map tolerated). */
129
+ export function readRelationships({ memoryRoot, caller = null, auditKey = null } = {}) {
130
+ const paths = tryRolePaths(memoryRoot);
131
+ if (paths === null) {
132
+ return {
133
+ schema: RELATIONSHIPS_SCHEMA,
134
+ present: false,
135
+ degraded: true,
136
+ updatedAt: null,
137
+ nodes: [],
138
+ edges: [],
139
+ count: 0,
140
+ code: 'E_RELATIONSHIPS_ABSENT',
141
+ };
142
+ }
143
+ const read = readJsonSafe(paths.relationships, { memoryRoot, auditKey, caller });
144
+ if (read.tampered === true || read.denied === true) {
145
+ return {
146
+ schema: RELATIONSHIPS_SCHEMA,
147
+ present: false,
148
+ degraded: true,
149
+ tampered: read.tampered === true,
150
+ denied: read.denied === true,
151
+ subject: read.subject ?? null,
152
+ updatedAt: null,
153
+ nodes: [],
154
+ edges: [],
155
+ count: 0,
156
+ code: read.code,
157
+ message: read.message ?? null,
158
+ };
159
+ }
160
+ if (!read.ok || !read.value || typeof read.value !== 'object') {
161
+ return {
162
+ schema: RELATIONSHIPS_SCHEMA,
163
+ present: read.present === true,
164
+ degraded: read.present === true,
165
+ updatedAt: null,
166
+ nodes: [],
167
+ edges: [],
168
+ count: 0,
169
+ code: read.present === true ? 'E_RELATIONSHIPS_CORRUPT' : 'E_RELATIONSHIPS_ABSENT',
170
+ };
171
+ }
172
+ const rawNodes = Array.isArray(read.value.nodes)
173
+ ? read.value.nodes
174
+ : (read.value.nodes && typeof read.value.nodes === 'object'
175
+ ? Object.entries(read.value.nodes).map(([personId, node]) => ({ personId, ...(node ?? {}) }))
176
+ : []);
177
+ const nodes = [];
178
+ let bad = 0;
179
+ for (const raw of rawNodes) {
180
+ const node = normalizeNode(raw);
181
+ if (node === null) { bad += 1; continue; }
182
+ nodes.push(node);
183
+ }
184
+ const edges = [];
185
+ for (const raw of Array.isArray(read.value.edges) ? read.value.edges : []) {
186
+ const edge = normalizeEdge(raw);
187
+ if (edge === null) { bad += 1; continue; }
188
+ edges.push(edge);
189
+ }
190
+ nodes.sort((left, right) => left.personId.localeCompare(right.personId));
191
+ return {
192
+ schema: RELATIONSHIPS_SCHEMA,
193
+ present: true,
194
+ degraded: bad > 0,
195
+ updatedAt: typeof read.value.updatedAt === 'string' ? read.value.updatedAt : null,
196
+ nodes,
197
+ edges,
198
+ count: nodes.length,
199
+ bad,
200
+ subject: read.value.subject ?? null,
201
+ code: bad > 0 ? 'E_RELATIONSHIPS_PARTIAL' : 'OK',
202
+ };
203
+ }
204
+
205
+ /** Tier for one person; missing/invalid store → stranger (never throws). */
206
+ export function relationshipTierOf({ memoryRoot, personId, caller = null, auditKey = null } = {}) {
207
+ const graph = readRelationships({ memoryRoot, caller, auditKey });
208
+ const hit = graph.nodes.find((node) => node.personId === String(personId ?? ''));
209
+ return hit ? hit.tier : RELATIONSHIP_DEFAULTS.tier;
210
+ }
211
+
212
+ /** 三接口 #1(读侧):personId → recall 排序权重乘子。 */
213
+ export function recallWeightFor({ memoryRoot, personId, caller = null, auditKey = null } = {}) {
214
+ const tier = relationshipTierOf({ memoryRoot, personId, caller, auditKey });
215
+ return { personId: typeof personId === 'string' ? personId : null, tier, multiplier: relationshipTierMultiplier(tier) };
216
+ }
217
+
218
+ /**
219
+ * 三接口 #1(纯排序器):把 relationshipTier 乘到 score 上再排序。
220
+ * 只改变顺序与分数,不改变 item 内容/事实真值(调用方保留原对象字段)。
221
+ * @param {Array<object>} items each item may carry `personId`/`actor` + numeric `score`
222
+ * @param {{memoryRoot?:string, graph?:object, resolvePerson?:Function}} [options]
223
+ */
224
+ export function applyRelationshipWeights(items, options = {}) {
225
+ const list = Array.isArray(items) ? items : [];
226
+ const graph = options.graph && Array.isArray(options.graph.nodes)
227
+ ? options.graph
228
+ : readRelationships({ memoryRoot: options.memoryRoot, caller: options.caller ?? null, auditKey: options.auditKey ?? null });
229
+ const tierByPerson = new Map(graph.nodes.map((node) => [node.personId, node.tier]));
230
+ const resolvePerson = typeof options.resolvePerson === 'function'
231
+ ? options.resolvePerson
232
+ : (item) => (typeof item?.personId === 'string' && item.personId !== '' ? item.personId
233
+ : (typeof item?.actor === 'string' ? item.actor : null));
234
+ const decorated = list.map((item, index) => {
235
+ const personId = resolvePerson(item);
236
+ const tier = personId !== null && tierByPerson.has(personId) ? tierByPerson.get(personId) : RELATIONSHIP_DEFAULTS.tier;
237
+ const multiplier = relationshipTierMultiplier(tier);
238
+ const baseScore = typeof item?.score === 'number' && Number.isFinite(item.score) ? item.score : 0;
239
+ return { item, index, personId, tier, multiplier, baseScore, score: baseScore * multiplier };
240
+ });
241
+ decorated.sort((left, right) => (right.score - left.score)
242
+ || left.index - right.index);
243
+ return {
244
+ items: decorated.map((entry) => entry.item),
245
+ ranked: decorated.map((entry) => ({
246
+ personId: entry.personId,
247
+ tier: entry.tier,
248
+ multiplier: entry.multiplier,
249
+ baseScore: entry.baseScore,
250
+ score: entry.score,
251
+ })),
252
+ source: 'relationship',
253
+ };
254
+ }
255
+
256
+ /** Monotonic audit seq (append-only role audit log). */
257
+ function nextAuditSeq(paths, options = {}) {
258
+ const read = readJsonlSafe(paths.audit, { limit: 10_000, memoryRoot: options.memoryRoot, auditKey: options.auditKey ?? null });
259
+ const rows = read.rows ?? [];
260
+ let max = 0;
261
+ for (let index = rows.length - 1; index >= 0; index -= 1) {
262
+ const seq = rows[index]?.seq;
263
+ if (Number.isInteger(seq) && seq > max) max = seq;
264
+ }
265
+ return max + 1;
266
+ }
267
+
268
+ function appendRoleAudit(paths, entry, options = {}) {
269
+ const row = { seq: nextAuditSeq(paths, options), at: iso(Date.now()), ...entry };
270
+ appendJsonl(paths.audit, row, { memoryRoot: options.memoryRoot, auditKey: options.auditKey ?? null, actorKind: options.actorKind ?? 'host' });
271
+ return row;
272
+ }
273
+
274
+ function readGraphFile(paths, options = {}) {
275
+ const read = readJsonSafe(paths.relationships, { memoryRoot: options.memoryRoot, auditKey: options.auditKey ?? null });
276
+ if (read.tampered === true) {
277
+ return { v: 1, updatedAt: null, nodes: [], edges: [], corrupt: true, tampered: true };
278
+ }
279
+ if (!read.ok || !read.value || typeof read.value !== 'object') {
280
+ return {
281
+ v: 1,
282
+ updatedAt: null,
283
+ nodes: [],
284
+ edges: [],
285
+ corrupt: read.present === true,
286
+ subject: null,
287
+ };
288
+ }
289
+ const value = read.value;
290
+ const rawNodes = Array.isArray(value.nodes)
291
+ ? value.nodes
292
+ : (value.nodes && typeof value.nodes === 'object'
293
+ ? Object.entries(value.nodes).map(([personId, node]) => ({ personId, ...(node ?? {}) }))
294
+ : []);
295
+ const nodes = rawNodes.map(normalizeNode).filter(Boolean);
296
+ const edges = (Array.isArray(value.edges) ? value.edges : []).map(normalizeEdge).filter(Boolean);
297
+ return {
298
+ v: 1,
299
+ updatedAt: typeof value.updatedAt === 'string' ? value.updatedAt : null,
300
+ nodes,
301
+ edges,
302
+ corrupt: false,
303
+ subject: value.subject ?? null,
304
+ };
305
+ }
306
+
307
+ /**
308
+ * Internal tier commit shared by `relationship.update` and its undo row:
309
+ * validates + CAS + atomic write + exactly one audit row.
310
+ */
311
+ function commitTierChange({
312
+ memoryRoot,
313
+ personId,
314
+ tier,
315
+ reason,
316
+ expectedVersion,
317
+ actor = 'host',
318
+ confidence = undefined,
319
+ appellations = undefined,
320
+ edges = undefined,
321
+ now = () => new Date(),
322
+ auditAction = 'relationship.update',
323
+ undoInfo = null,
324
+ undoable = false,
325
+ auditKey = null,
326
+ actorKind = 'host',
327
+ subject = undefined,
328
+ userId = undefined,
329
+ projectId = undefined,
330
+ readers = undefined,
331
+ } = {}) {
332
+ const id = normalizePersonId(personId);
333
+ const nextTier = String(tier ?? '');
334
+ if (!isRelationshipTier(nextTier)) {
335
+ throw relationshipError(RELATIONSHIP_CODES.TIER, `tier 非法:${JSON.stringify(tier)}(允许 ${RELATIONSHIP_TIERS.join('|')})`);
336
+ }
337
+ const decidedReason = normalizeReason(reason);
338
+ const expected = Number.isInteger(expectedVersion) ? expectedVersion : 0;
339
+ const paths = rolePaths(memoryRoot);
340
+ const graph = readGraphFile(paths, { memoryRoot, auditKey });
341
+ if (graph.tampered === true) {
342
+ throw relationshipError('E_INTEGRITY_TAMPER', 'relationships.json HMAC 校验失败(被篡改或缺少侧车),拒绝写入并拒绝加载。');
343
+ }
344
+ if (graph.corrupt) {
345
+ throw relationshipError(RELATIONSHIP_CODES.CORRUPT, 'relationships.json 损坏,拒绝在无法校验 CAS 的前提下写入(先人工修复/删除该文件)。');
346
+ }
347
+ const index = graph.nodes.findIndex((node) => node.personId === id);
348
+ const current = index >= 0 ? graph.nodes[index] : null;
349
+ const currentVersion = current ? (Number.isInteger(current.version) ? current.version : 0) : 0;
350
+ if (expected !== currentVersion) {
351
+ throw relationshipError(
352
+ RELATIONSHIP_CODES.CAS,
353
+ `版本冲突:expectedVersion=${expected} 当前=${currentVersion}(personId=${id})`,
354
+ { current: currentVersion, expected, personId: id },
355
+ );
356
+ }
357
+ const timestamp = iso(nowMs(now()));
358
+ const node = {
359
+ personId: id,
360
+ tier: nextTier,
361
+ confidence: Number.isFinite(confidence)
362
+ ? Math.min(1, Math.max(0, Number(confidence)))
363
+ : (current?.confidence ?? TIER_CONFIDENCE[nextTier] ?? RELATIONSHIP_DEFAULTS.confidence),
364
+ updatedAt: timestamp,
365
+ version: currentVersion + 1,
366
+ aliases: current?.aliases ?? [],
367
+ appellations: normalizeAppellations(appellations, current?.appellations ?? []),
368
+ };
369
+ const nextEdges = graph.edges.slice();
370
+ for (const raw of Array.isArray(edges) ? edges : []) {
371
+ const edge = normalizeEdge({ ...raw, lastSeenAt: raw?.lastSeenAt ?? timestamp });
372
+ if (!edge) continue;
373
+ const at = nextEdges.findIndex((item) => item.from === edge.from && item.to === edge.to && item.relation === edge.relation);
374
+ if (at >= 0) nextEdges[at] = { ...nextEdges[at], lastSeenAt: edge.lastSeenAt };
375
+ else nextEdges.push(edge);
376
+ }
377
+ if (nextEdges.length > RELATIONSHIP_DEFAULTS.edgesMax) nextEdges.splice(0, nextEdges.length - RELATIONSHIP_DEFAULTS.edgesMax);
378
+ if (index >= 0) graph.nodes[index] = node;
379
+ else graph.nodes.push(node);
380
+ graph.nodes.sort((left, right) => left.personId.localeCompare(right.personId));
381
+ ensureRoleDir(memoryRoot);
382
+ const nextSubject = (subject !== undefined || userId !== undefined || projectId !== undefined || readers !== undefined)
383
+ ? {
384
+ sessionId: subject?.sessionId ?? graph.subject?.sessionId ?? null,
385
+ userId: subject?.userId ?? userId ?? graph.subject?.userId ?? null,
386
+ projectId: subject?.projectId ?? projectId ?? graph.subject?.projectId ?? null,
387
+ readers: [
388
+ ...(Array.isArray(graph.subject?.readers) ? graph.subject.readers : []),
389
+ ...(Array.isArray(subject?.readers) ? subject.readers : []),
390
+ ...(Array.isArray(readers) ? readers : []),
391
+ ],
392
+ }
393
+ : undefined;
394
+ writeJsonAtomic(paths.relationships, {
395
+ v: 1,
396
+ updatedAt: timestamp,
397
+ nodes: graph.nodes,
398
+ edges: nextEdges,
399
+ }, { memoryRoot, auditKey, actorKind, subject: nextSubject });
400
+ const audit = appendRoleAudit(paths, {
401
+ action: auditAction,
402
+ actor: typeof actor === 'string' && actor !== '' ? actor : 'host',
403
+ personId: id,
404
+ reason: decidedReason,
405
+ before: { tier: current?.tier ?? null, version: currentVersion },
406
+ after: { tier: nextTier, version: node.version },
407
+ // `relationship.update` 行带 undo 回退标记;undo 行自身不再可 undo。
408
+ undo: undoInfo ?? (undoable ? { previousTier: current?.tier ?? null, previousVersion: currentVersion } : null),
409
+ source: 'role.relationships',
410
+ }, { memoryRoot, auditKey, actorKind });
411
+ return {
412
+ ok: true,
413
+ node,
414
+ previous: { tier: current?.tier ?? null, version: currentVersion },
415
+ undoable,
416
+ auditSeq: audit.seq,
417
+ };
418
+ }
419
+
420
+ /**
421
+ * G7 `relationship.update`:人判关系等级更新。
422
+ * - reason ≥10 字;tier 枚举;CAS:expectedVersion 必须等于当前节点 version(新节点=0);
423
+ * - 写 relationships.json(原子)+ 审计行(含 undo.previousTier,供 undo 回退标记);
424
+ * - 只改关系层,不改任何记忆事实/保护谓词。
425
+ */
426
+ export function updateRelationship(options = {}) {
427
+ const result = commitTierChange({
428
+ ...options,
429
+ auditAction: 'relationship.update',
430
+ undoable: true,
431
+ });
432
+ return {
433
+ ...result,
434
+ undoable: true,
435
+ undo: {
436
+ action: 'relationship.update.undo',
437
+ personId: result.node.personId,
438
+ tier: result.previous.tier ?? 'stranger',
439
+ expectedVersion: result.node.version,
440
+ },
441
+ };
442
+ }
443
+
444
+ /** Undo half of `relationship.update`: restore the previous tier as a new version. */
445
+ export function restoreRelationship({
446
+ memoryRoot,
447
+ personId,
448
+ tier,
449
+ reason = '按审计记录回退关系等级(relationship.update undo)',
450
+ expectedVersion,
451
+ actor = 'host',
452
+ now = () => new Date(),
453
+ auditKey = null,
454
+ actorKind = 'host',
455
+ } = {}) {
456
+ const result = commitTierChange({
457
+ memoryRoot,
458
+ personId,
459
+ tier,
460
+ reason,
461
+ expectedVersion,
462
+ actor,
463
+ now,
464
+ auditKey,
465
+ actorKind,
466
+ auditAction: 'relationship.update.undo',
467
+ undoable: false,
468
+ });
469
+ return { ok: true, node: result.node, restoredTier: result.node.tier, auditSeq: result.auditSeq, undoable: false };
470
+ }
471
+
472
+ /** Appellation history for one person (称呼记录). */
473
+ export function appellationsOf({ memoryRoot, personId, caller = null, auditKey = null } = {}) {
474
+ const graph = readRelationships({ memoryRoot, caller, auditKey });
475
+ const hit = graph.nodes.find((node) => node.personId === String(personId ?? ''));
476
+ return hit ? [...hit.appellations] : [];
477
+ }
@@ -0,0 +1,202 @@
1
+ // src/role/store.js — 角色内核共享文件层(v2.8 §16.3 + §16.8 A 写保护)
2
+ //
3
+ // 所有角色内核数据都落在 `<memoryRoot>/.longlive/role/` 下:
4
+ // attention.jsonl 注意力队列(append-only;容量压缩由 attention.js 负责)
5
+ // relationships.json 关系图(nodes/edges + CAS version + subject/readers 边界)
6
+ // personality.json 人格快照(版本化 + subject/readers 边界)
7
+ // channels.json 渠道注册表(只读状态 + 已注册适配器元数据)
8
+ // audit.jsonl 角色层审计(追加:relationship.update / personality.snapshot …)
9
+ //
10
+ // §16.8 A:role/** 是宿主私有数据 —— 写路径必须有宿主身份(actorKind=host/panel)
11
+ // 并带 HMAC 侧车(`<file>.hmac`);读路径校验侧车,篡改/缺侧车 → `E_INTEGRITY_TAMPER`
12
+ // 且拒绝加载。密钥与算法在 writeprotect.js;本文件把两者接到所有 role 读写上。
13
+ //
14
+ // 读路径永不抛:坏文件/篡改返回 degraded + code,让面板/注入降级而不是崩。
15
+
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+
19
+ import { isInsideRoot, memoryError } from '../memory/paths.js';
20
+ import {
21
+ ROLE_CODES as BASE_CODES, RoleError as BaseRoleError, iso, nowMs, sha256Hex,
22
+ } from './base.js';
23
+ import {
24
+ appendProtectedJsonl,
25
+ isSubjectReadable,
26
+ normalizeRoleSubject,
27
+ readProtectedJsonValue,
28
+ readProtectedJsonl,
29
+ verifyProtectedFile,
30
+ writeProtectedJson,
31
+ } from './writeprotect.js';
32
+
33
+ export const ROLE_DIR = path.join('.longlive', 'role');
34
+ export const ROLE_SCHEMA_VERSION = 1;
35
+
36
+ /** 角色层错误码(写路径抛 RoleError,读路径降级不抛)。 */
37
+ export const ROLE_CODES = Object.freeze({
38
+ ...BASE_CODES,
39
+ TAMPER: 'E_INTEGRITY_TAMPER',
40
+ WRITE_DENY: 'E_WRITE_DENY',
41
+ ACCESS_DENIED: 'E_ACCESS_DENIED',
42
+ });
43
+ export { iso, nowMs, sha256Hex };
44
+ export const RoleError = BaseRoleError;
45
+
46
+ export function requireMemoryRoot(memoryRoot) {
47
+ const root = typeof memoryRoot === 'string' && memoryRoot.trim() !== '' ? path.resolve(memoryRoot.trim()) : '';
48
+ if (root === '') {
49
+ throw new RoleError(ROLE_CODES.PARAMS, '角色内核需要 memoryRoot(<memoryRoot>/.longlive/role/)');
50
+ }
51
+ return root;
52
+ }
53
+
54
+ export function roleDir(memoryRoot) {
55
+ return path.join(requireMemoryRoot(memoryRoot), ROLE_DIR);
56
+ }
57
+
58
+ export function roleFile(memoryRoot, name) {
59
+ const file = path.join(roleDir(memoryRoot), name);
60
+ if (!isInsideRoot(requireMemoryRoot(memoryRoot), file)) {
61
+ throw memoryError(ROLE_CODES.PATH_OUTSIDE_ROOT, `角色层路径越界:${file}`);
62
+ }
63
+ return file;
64
+ }
65
+
66
+ export function rolePaths(memoryRoot) {
67
+ const dir = roleDir(memoryRoot);
68
+ return {
69
+ dir,
70
+ attention: path.join(dir, 'attention.jsonl'),
71
+ relationships: path.join(dir, 'relationships.json'),
72
+ personality: path.join(dir, 'personality.json'),
73
+ channels: path.join(dir, 'channels.json'),
74
+ audit: path.join(dir, 'audit.jsonl'),
75
+ hostKey: path.join(path.dirname(path.dirname(dir)), '.hostkey'),
76
+ };
77
+ }
78
+
79
+ /** `rolePaths` for read paths: null instead of throwing on a bad/unset root. */
80
+ export function tryRolePaths(memoryRoot) {
81
+ try {
82
+ return rolePaths(memoryRoot);
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ export function ensureRoleDir(memoryRoot) {
89
+ const dir = roleDir(memoryRoot);
90
+ fs.mkdirSync(dir, { recursive: true });
91
+ return dir;
92
+ }
93
+
94
+ function roleRootOf(file) {
95
+ // <root>/.longlive/role/<file> -> <root>
96
+ return path.dirname(path.dirname(path.dirname(path.resolve(file))));
97
+ }
98
+
99
+ /**
100
+ * §16.8 A 写保护 JSON write(宿主身份 + HMAC 侧车)。
101
+ * options: {memoryRoot, auditKey, actorKind, subject}
102
+ * `subject`(§16.8 E 四元边界)会合并进文件顶层 subject 字段。
103
+ */
104
+ export function writeJsonAtomic(file, value, options = {}) {
105
+ const memoryRoot = options.memoryRoot ?? roleRootOf(file);
106
+ let payload = value;
107
+ if (options.subject && value && typeof value === 'object' && !Array.isArray(value)) {
108
+ payload = { ...value, subject: normalizeRoleSubject({ ...(value.subject ?? {}), ...options.subject }) };
109
+ }
110
+ return writeProtectedJson({
111
+ memoryRoot,
112
+ file,
113
+ value: payload,
114
+ auditKey: options.auditKey ?? null,
115
+ actorKind: options.actorKind ?? 'host',
116
+ });
117
+ }
118
+
119
+ /** §16.8 A 写保护 JSONL append(宿主身份 + 全量侧车重签)。 */
120
+ export function appendJsonl(file, row, options = {}) {
121
+ const memoryRoot = options.memoryRoot ?? roleRootOf(file);
122
+ return appendProtectedJsonl({
123
+ memoryRoot,
124
+ file,
125
+ row,
126
+ auditKey: options.auditKey ?? null,
127
+ actorKind: options.actorKind ?? 'host',
128
+ });
129
+ }
130
+
131
+ /**
132
+ * Read-only JSON with §16.8 A verification + §16.8 E subject/readers boundary.
133
+ * Never throws. `options.caller={actorKind,userId}` enables the boundary
134
+ * (default host/panel bypass). Returns {ok, present, value, tampered, denied, code}.
135
+ */
136
+ export function readJsonSafe(file, options = {}) {
137
+ try {
138
+ const memoryRoot = options.memoryRoot ?? roleRootOf(file);
139
+ const verified = readProtectedJsonValue({ memoryRoot, file, auditKey: options.auditKey ?? null });
140
+ if (verified.ok) {
141
+ if (verified.absent) return { ok: false, present: false, absent: true, value: null, tampered: false, denied: false, code: 'ENOENT' };
142
+ const value = verified.value;
143
+ if (options.caller && value && typeof value === 'object' && !Array.isArray(value) && value.subject) {
144
+ const subject = normalizeRoleSubject(value.subject);
145
+ const verdict = isSubjectReadable(subject, options.caller);
146
+ if (!verdict.ok) {
147
+ return {
148
+ ok: false, present: false, absent: false, value: null,
149
+ tampered: false, denied: true, code: verdict.code, subject,
150
+ message: `跨用户读取被拒(${verdict.reason})`,
151
+ };
152
+ }
153
+ }
154
+ return { ok: true, present: true, absent: false, value, tampered: false, denied: false, code: null };
155
+ }
156
+ return {
157
+ ok: false, present: true, absent: false, value: null,
158
+ tampered: true, denied: false, code: verified.code ?? ROLE_CODES.TAMPER, message: verified.message,
159
+ };
160
+ } catch (error) {
161
+ return {
162
+ ok: false, present: true, absent: false, value: null,
163
+ tampered: true, denied: false, code: error?.code ?? ROLE_CODES.TAMPER, message: error?.message,
164
+ };
165
+ }
166
+ }
167
+
168
+ /** Read-only JSONL with §16.8 A verification. Never throws. */
169
+ export function readJsonlSafe(file, { limit = 500, memoryRoot = undefined, auditKey = null } = {}) {
170
+ try {
171
+ const root = memoryRoot ?? roleRootOf(file);
172
+ const verified = readProtectedJsonl({ memoryRoot: root, file, auditKey, limit });
173
+ if (!verified.ok) {
174
+ return { present: true, rows: [], bad: 0, total: 0, tampered: true, code: verified.code ?? ROLE_CODES.TAMPER, error: verified.code };
175
+ }
176
+ if (verified.absent) return { present: false, rows: [], bad: 0, total: 0, tampered: false, error: 'ENOENT' };
177
+ return { present: true, rows: verified.rows, bad: verified.bad, total: verified.total, tampered: false, error: null };
178
+ } catch (error) {
179
+ return { present: true, rows: [], bad: 0, total: 0, tampered: true, code: error?.code ?? ROLE_CODES.TAMPER, error: error?.code };
180
+ }
181
+ }
182
+
183
+ /** Verify one protected file without parsing (host tamper probe / tests). */
184
+ export function verifyRoleFile(file, options = {}) {
185
+ const memoryRoot = options.memoryRoot ?? roleRootOf(file);
186
+ return verifyProtectedFile({ memoryRoot, file, auditKey: options.auditKey ?? null });
187
+ }
188
+
189
+ /** Read a profile-ish source file only when it stays inside the memory root. */
190
+ export function readInsideRoot(memoryRoot, candidate) {
191
+ const root = requireMemoryRoot(memoryRoot);
192
+ const resolved = path.resolve(String(candidate ?? ''));
193
+ if (!isInsideRoot(root, resolved)) {
194
+ return { ok: false, code: ROLE_CODES.PATH_OUTSIDE_ROOT, path: resolved };
195
+ }
196
+ try {
197
+ const text = fs.readFileSync(resolved, 'utf8');
198
+ return { ok: true, path: resolved, text: text.replace(/^\uFEFF/, ''), bytes: Buffer.byteLength(text, 'utf8') };
199
+ } catch (error) {
200
+ return { ok: false, code: error?.code === 'ENOENT' ? 'ENOENT' : (error?.code ?? 'EREAD'), path: resolved };
201
+ }
202
+ }