@remnic/core 9.69.5 → 9.69.7

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 (268) hide show
  1. package/dist/access-admin-ops-surface.d.ts +1 -1
  2. package/dist/access-admin-ops-surface.js +24 -24
  3. package/dist/access-authorization-probe.d.ts +1 -1
  4. package/dist/access-authorization-probe.js +25 -25
  5. package/dist/access-boundary.d.ts +1 -1
  6. package/dist/access-boundary.js +24 -24
  7. package/dist/access-cli.js +80 -80
  8. package/dist/access-extraction-force-flush.d.ts +1 -1
  9. package/dist/access-extraction-force-flush.js +24 -24
  10. package/dist/access-http-lcm-compaction.d.ts +1 -1
  11. package/dist/access-http-lifecycle-flush.d.ts +1 -1
  12. package/dist/access-http-offline-stream.d.ts +1 -1
  13. package/dist/access-http-query.js +26 -26
  14. package/dist/access-http.d.ts +2 -2
  15. package/dist/access-http.js +42 -42
  16. package/dist/access-identity-continuity-surface.js +24 -24
  17. package/dist/access-lcm-surface.d.ts +1 -1
  18. package/dist/access-lcm-surface.js +24 -24
  19. package/dist/access-mcp.d.ts +1 -1
  20. package/dist/access-mcp.js +33 -33
  21. package/dist/access-namespace-preflight.js +24 -24
  22. package/dist/access-observe-write-surface.d.ts +1 -1
  23. package/dist/access-observe-write-surface.js +24 -24
  24. package/dist/access-offline-manifest.js +3 -3
  25. package/dist/access-operations-batch.js +27 -27
  26. package/dist/access-operations.d.ts +6 -6
  27. package/dist/access-operations.js +32 -32
  28. package/dist/access-recall-concurrency.d.ts +1 -1
  29. package/dist/access-recall-concurrency.js +24 -24
  30. package/dist/access-recall-response.d.ts +1 -1
  31. package/dist/access-recall-response.js +24 -24
  32. package/dist/access-recall-surface.d.ts +1 -1
  33. package/dist/access-recall-surface.js +24 -24
  34. package/dist/access-schema.d.ts +89 -89
  35. package/dist/access-schema.js +5 -5
  36. package/dist/access-service-helpers.d.ts +1 -1
  37. package/dist/access-service.d.ts +1 -1
  38. package/dist/access-service.js +24 -24
  39. package/dist/access-surface-catalog.d.ts +1 -1
  40. package/dist/access-surface-catalog.js +26 -26
  41. package/dist/active-recall.js +6 -6
  42. package/dist/adapters/index.js +7 -7
  43. package/dist/adapters/registry.js +3 -3
  44. package/dist/briefing.js +6 -6
  45. package/dist/{capsule-crypto-YO5QJ6L3.js → capsule-crypto-7FJQINUR.js} +2 -2
  46. package/dist/causal-behavior.js +2 -2
  47. package/dist/causal-chain.js +2 -2
  48. package/dist/causal-consolidation.js +9 -9
  49. package/dist/causal-retrieval.js +2 -2
  50. package/dist/causal-trajectory-graph.js +2 -2
  51. package/dist/causal-trajectory.js +1 -1
  52. package/dist/{chunk-7RZJWZ46.js → chunk-227UFSEQ.js} +5 -5
  53. package/dist/{chunk-YNF4ZT3X.js → chunk-26JWBSWD.js} +4 -4
  54. package/dist/{chunk-7VA7PARC.js → chunk-2MW3UQNX.js} +2 -2
  55. package/dist/{chunk-BXLOS5AJ.js → chunk-2NLLXCJG.js} +2 -2
  56. package/dist/{chunk-JBPKEARU.js → chunk-2QSZNTDO.js} +7 -7
  57. package/dist/{chunk-P6GKRT2H.js → chunk-2Z3DG5CQ.js} +1 -1
  58. package/dist/{chunk-NM5EOKN7.js → chunk-6C57JBJ3.js} +2 -2
  59. package/dist/{chunk-DD2BKHSD.js → chunk-6EE5LJSZ.js} +2 -2
  60. package/dist/{chunk-VDX2J7OX.js → chunk-7LCGFUMB.js} +4 -4
  61. package/dist/{chunk-YPNTBYEV.js → chunk-A3WMMQAH.js} +3 -3
  62. package/dist/{chunk-SZZGVXKC.js → chunk-ANN5VMHT.js} +2 -2
  63. package/dist/{chunk-TP3N5364.js → chunk-APGIIJRE.js} +2 -2
  64. package/dist/{chunk-OLBZ2WGG.js → chunk-C2JCVZWX.js} +2 -2
  65. package/dist/{chunk-7DXW5V2W.js → chunk-CTCGHNWN.js} +5 -5
  66. package/dist/{chunk-JJAOVJ3E.js → chunk-DJZWNKFO.js} +2 -2
  67. package/dist/{chunk-XOGYSWQZ.js → chunk-DZNNWPQ7.js} +17 -17
  68. package/dist/{chunk-6JYTP3JP.js → chunk-E5WTGEWX.js} +3 -3
  69. package/dist/{chunk-CGFDT752.js → chunk-E6Z4LWNJ.js} +4 -4
  70. package/dist/{chunk-PY5E3UOD.js → chunk-EG6NUYY3.js} +2 -2
  71. package/dist/{chunk-EJJ57PTY.js → chunk-GD7D7WP2.js} +2 -2
  72. package/dist/{chunk-QDBG45NK.js → chunk-GFGQOZGG.js} +10 -10
  73. package/dist/{chunk-7TCISOAJ.js → chunk-GH7UF5BV.js} +136 -45
  74. package/dist/chunk-GH7UF5BV.js.map +1 -0
  75. package/dist/{chunk-EVLHZ6F3.js → chunk-GISV63M2.js} +2 -2
  76. package/dist/{chunk-MDQXW235.js → chunk-GTZLPFAT.js} +2 -2
  77. package/dist/{chunk-YERBUSU2.js → chunk-H3HOC6UT.js} +2 -2
  78. package/dist/{chunk-AJFKQJQK.js → chunk-HINMCD7V.js} +2 -2
  79. package/dist/{chunk-ISDSX7GW.js → chunk-IGUWU5BD.js} +1 -1
  80. package/dist/{chunk-SKVSBTFQ.js → chunk-IWO4HEOL.js} +2 -2
  81. package/dist/{chunk-6EXRODDF.js → chunk-J2TT3RV6.js} +2 -2
  82. package/dist/{chunk-XJJYSX6F.js → chunk-JCUG7AXZ.js} +4 -4
  83. package/dist/{chunk-BX6GW4KC.js → chunk-JMAG7WZ7.js} +2 -2
  84. package/dist/{chunk-VSWRJQQW.js → chunk-KJUWWN3T.js} +2 -1
  85. package/dist/{chunk-W6V2XVUO.js → chunk-KLLSFJBJ.js} +3 -3
  86. package/dist/{chunk-C5FRCM4B.js → chunk-LHGIBXVX.js} +2 -2
  87. package/dist/{chunk-PGD3DXZP.js → chunk-M5UJJUON.js} +3 -3
  88. package/dist/{chunk-5C4LXMSK.js → chunk-MWHSWK4W.js} +34 -34
  89. package/dist/{chunk-WTWHORVW.js → chunk-N27L5IY2.js} +5 -5
  90. package/dist/{chunk-4OH6FGAO.js → chunk-N5VXPTXC.js} +12 -12
  91. package/dist/{chunk-HDEQRI2O.js → chunk-NAZROP6I.js} +2 -2
  92. package/dist/{chunk-R3YVNAJB.js → chunk-NDE7CH6C.js} +2 -2
  93. package/dist/{chunk-BIJAWZYW.js → chunk-OLT3YTDH.js} +4 -4
  94. package/dist/{chunk-GM35IHF7.js → chunk-PRK4WTPU.js} +2 -2
  95. package/dist/{chunk-LGVIKA7O.js → chunk-QAITDA5B.js} +6 -6
  96. package/dist/{chunk-MOUCCUET.js → chunk-QGUFN25Q.js} +2 -2
  97. package/dist/{chunk-5HP6LVWU.js → chunk-QHYSECIG.js} +2 -2
  98. package/dist/{chunk-SZ3SRRYZ.js → chunk-RXS4SQ5S.js} +2 -2
  99. package/dist/{chunk-6KQLDPKA.js → chunk-S6AHHQ6C.js} +2 -2
  100. package/dist/{chunk-NAB4MKTJ.js → chunk-S6DX26S3.js} +3 -3
  101. package/dist/{chunk-R4FPBSFD.js → chunk-SO73GYOZ.js} +2 -2
  102. package/dist/{chunk-V3KLIYTW.js → chunk-SXQZMC7R.js} +2 -2
  103. package/dist/{chunk-JHTF22NQ.js → chunk-TA7UZZMW.js} +2 -2
  104. package/dist/{chunk-3X7Z467X.js → chunk-UJMOG5LL.js} +2 -2
  105. package/dist/{chunk-TGDFKC7X.js → chunk-VOPOPTZ7.js} +2 -2
  106. package/dist/{chunk-IFHT77WK.js → chunk-WF2WCZCR.js} +2 -2
  107. package/dist/{chunk-6F5WBXE2.js → chunk-X2NFFBTO.js} +4 -4
  108. package/dist/{chunk-IHFN3JTR.js → chunk-X5TGZAP2.js} +3 -3
  109. package/dist/{chunk-RJ57OOCD.js → chunk-X66BHMAU.js} +2 -2
  110. package/dist/{chunk-DQEMWVMT.js → chunk-X7Y7WX73.js} +1 -1
  111. package/dist/{chunk-EJF6AV2U.js → chunk-XHGDFN7A.js} +6 -6
  112. package/dist/{chunk-FGV7V2N4.js → chunk-Y3WMJAZ3.js} +2 -2
  113. package/dist/{chunk-ZET6A3OF.js → chunk-YHCBJULX.js} +2 -2
  114. package/dist/{chunk-EAG7YMIS.js → chunk-YVMTXBJJ.js} +2 -2
  115. package/dist/{chunk-OUKBMQ2O.js → chunk-Z3SQEEB6.js} +2 -2
  116. package/dist/{chunk-OUAK37ID.js → chunk-ZNXPGZJQ.js} +2 -2
  117. package/dist/{chunk-MVH7YAWY.js → chunk-ZPN6SSWA.js} +2 -2
  118. package/dist/{cli-Bvy4jdBu.d.ts → cli-CFpMhqz5.d.ts} +1 -1
  119. package/dist/cli.d.ts +2 -2
  120. package/dist/cli.js +61 -61
  121. package/dist/coding/export-okf-codegraph.js +6 -6
  122. package/dist/coding/pre-action-gate.js +2 -2
  123. package/dist/compounding/engine.js +6 -6
  124. package/dist/config.js +6 -6
  125. package/dist/connectors/codex-materialize-runner.js +6 -6
  126. package/dist/connectors/index.js +6 -6
  127. package/dist/convergence-refresh.js +8 -8
  128. package/dist/conversation-index/backend.js +2 -2
  129. package/dist/corpus-watermark.js +8 -8
  130. package/dist/entity-retrieval.js +6 -6
  131. package/dist/external-wiki-access.d.ts +1 -1
  132. package/dist/external-wiki-access.js +25 -25
  133. package/dist/external-wiki-mcp-tools.d.ts +1 -1
  134. package/dist/extraction-span-source-hash.d.ts +45 -0
  135. package/dist/extraction-span-source-hash.js +52 -0
  136. package/dist/extraction-span-source-hash.js.map +1 -0
  137. package/dist/extraction.js +6 -6
  138. package/dist/{graph-edge-decay-FXGELBFA.js → graph-edge-decay-DGTBFALQ.js} +4 -4
  139. package/dist/graph-snapshot.js +3 -3
  140. package/dist/graph.js +2 -2
  141. package/dist/harmonic-retrieval.js +1 -1
  142. package/dist/index.d.ts +706 -681
  143. package/dist/index.js +92 -86
  144. package/dist/lcm/engine.js +2 -2
  145. package/dist/lcm/index.js +2 -2
  146. package/dist/location/index.js +8 -8
  147. package/dist/maintenance/memory-governance.js +6 -6
  148. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +6 -6
  149. package/dist/maintenance/rebuild-memory-projection.js +7 -7
  150. package/dist/mcp-memory-inspector-app.d.ts +1 -1
  151. package/dist/memory-subject-cli.d.ts +1 -1
  152. package/dist/memory-subject.d.ts +1 -1
  153. package/dist/namespaces/migrate.js +7 -7
  154. package/dist/namespaces/search.js +4 -4
  155. package/dist/namespaces/storage.js +6 -6
  156. package/dist/offline-sync.js +2 -2
  157. package/dist/operator-doctor-corpus.js +9 -9
  158. package/dist/operator-toolkit.js +22 -22
  159. package/dist/orchestration/maintenance.js +15 -15
  160. package/dist/orchestrator.js +82 -82
  161. package/dist/{public-http-2MyshWxl.d.ts → public-http-C9-CHc40.d.ts} +1 -1
  162. package/dist/recall-state-view-zero-diff.d.ts +23 -0
  163. package/dist/recall-state-view-zero-diff.js +84 -0
  164. package/dist/recall-state-view-zero-diff.js.map +1 -0
  165. package/dist/resume-bundles.js +7 -7
  166. package/dist/schemas.d.ts +176 -176
  167. package/dist/search/factory.js +3 -3
  168. package/dist/search/index.js +3 -3
  169. package/dist/semantic-consolidation.js +7 -7
  170. package/dist/semantic-rule-promotion.js +6 -6
  171. package/dist/semantic-rule-verifier.js +6 -6
  172. package/dist/shared-context/manager.d.ts +6 -6
  173. package/dist/source-agent-qualifier.js +6 -6
  174. package/dist/standup/build.js +6 -6
  175. package/dist/storage.js +5 -5
  176. package/dist/support-passport/index.d.ts +4 -4
  177. package/dist/support-passport/index.js +27 -27
  178. package/dist/transfer/backup.js +2 -2
  179. package/dist/transfer/capsule-export.js +2 -2
  180. package/dist/transfer/capsule-import.js +2 -2
  181. package/dist/transfer/export-okf.js +7 -7
  182. package/dist/transfer/types.d.ts +78 -78
  183. package/dist/verified-recall.js +6 -6
  184. package/dist/who-knows-cli.d.ts +1 -1
  185. package/dist/who-knows-cli.js +7 -7
  186. package/dist/who-knows.d.ts +1 -1
  187. package/dist/who-knows.js +6 -6
  188. package/package.json +2 -2
  189. package/src/activity/index.ts +1 -0
  190. package/src/activity/journal-vault-provenance.test.ts +120 -0
  191. package/src/activity/journal-vault-provenance.ts +67 -0
  192. package/src/activity/timeline/index.ts +1 -0
  193. package/src/activity/timeline/week-config-hash.test.ts +207 -0
  194. package/src/activity/timeline/week-config-hash.ts +83 -0
  195. package/src/extraction-span-source-hash.test.ts +165 -0
  196. package/src/extraction-span-source-hash.ts +100 -0
  197. package/src/recall-state-view-zero-diff.test.ts +282 -0
  198. package/src/recall-state-view-zero-diff.ts +125 -0
  199. package/src/shared-context/authority-precedence.test.ts +127 -0
  200. package/src/shared-context/authority-precedence.ts +68 -0
  201. package/dist/chunk-7TCISOAJ.js.map +0 -1
  202. package/dist/{access-service-LbwWl05g.d.ts → access-service-DcfM0Vf0.d.ts} +64 -64
  203. /package/dist/{capsule-crypto-YO5QJ6L3.js.map → capsule-crypto-7FJQINUR.js.map} +0 -0
  204. /package/dist/{chunk-7RZJWZ46.js.map → chunk-227UFSEQ.js.map} +0 -0
  205. /package/dist/{chunk-YNF4ZT3X.js.map → chunk-26JWBSWD.js.map} +0 -0
  206. /package/dist/{chunk-7VA7PARC.js.map → chunk-2MW3UQNX.js.map} +0 -0
  207. /package/dist/{chunk-BXLOS5AJ.js.map → chunk-2NLLXCJG.js.map} +0 -0
  208. /package/dist/{chunk-JBPKEARU.js.map → chunk-2QSZNTDO.js.map} +0 -0
  209. /package/dist/{chunk-P6GKRT2H.js.map → chunk-2Z3DG5CQ.js.map} +0 -0
  210. /package/dist/{chunk-NM5EOKN7.js.map → chunk-6C57JBJ3.js.map} +0 -0
  211. /package/dist/{chunk-DD2BKHSD.js.map → chunk-6EE5LJSZ.js.map} +0 -0
  212. /package/dist/{chunk-VDX2J7OX.js.map → chunk-7LCGFUMB.js.map} +0 -0
  213. /package/dist/{chunk-YPNTBYEV.js.map → chunk-A3WMMQAH.js.map} +0 -0
  214. /package/dist/{chunk-SZZGVXKC.js.map → chunk-ANN5VMHT.js.map} +0 -0
  215. /package/dist/{chunk-TP3N5364.js.map → chunk-APGIIJRE.js.map} +0 -0
  216. /package/dist/{chunk-OLBZ2WGG.js.map → chunk-C2JCVZWX.js.map} +0 -0
  217. /package/dist/{chunk-7DXW5V2W.js.map → chunk-CTCGHNWN.js.map} +0 -0
  218. /package/dist/{chunk-JJAOVJ3E.js.map → chunk-DJZWNKFO.js.map} +0 -0
  219. /package/dist/{chunk-XOGYSWQZ.js.map → chunk-DZNNWPQ7.js.map} +0 -0
  220. /package/dist/{chunk-6JYTP3JP.js.map → chunk-E5WTGEWX.js.map} +0 -0
  221. /package/dist/{chunk-CGFDT752.js.map → chunk-E6Z4LWNJ.js.map} +0 -0
  222. /package/dist/{chunk-PY5E3UOD.js.map → chunk-EG6NUYY3.js.map} +0 -0
  223. /package/dist/{chunk-EJJ57PTY.js.map → chunk-GD7D7WP2.js.map} +0 -0
  224. /package/dist/{chunk-QDBG45NK.js.map → chunk-GFGQOZGG.js.map} +0 -0
  225. /package/dist/{chunk-EVLHZ6F3.js.map → chunk-GISV63M2.js.map} +0 -0
  226. /package/dist/{chunk-MDQXW235.js.map → chunk-GTZLPFAT.js.map} +0 -0
  227. /package/dist/{chunk-YERBUSU2.js.map → chunk-H3HOC6UT.js.map} +0 -0
  228. /package/dist/{chunk-AJFKQJQK.js.map → chunk-HINMCD7V.js.map} +0 -0
  229. /package/dist/{chunk-ISDSX7GW.js.map → chunk-IGUWU5BD.js.map} +0 -0
  230. /package/dist/{chunk-SKVSBTFQ.js.map → chunk-IWO4HEOL.js.map} +0 -0
  231. /package/dist/{chunk-6EXRODDF.js.map → chunk-J2TT3RV6.js.map} +0 -0
  232. /package/dist/{chunk-XJJYSX6F.js.map → chunk-JCUG7AXZ.js.map} +0 -0
  233. /package/dist/{chunk-BX6GW4KC.js.map → chunk-JMAG7WZ7.js.map} +0 -0
  234. /package/dist/{chunk-VSWRJQQW.js.map → chunk-KJUWWN3T.js.map} +0 -0
  235. /package/dist/{chunk-W6V2XVUO.js.map → chunk-KLLSFJBJ.js.map} +0 -0
  236. /package/dist/{chunk-C5FRCM4B.js.map → chunk-LHGIBXVX.js.map} +0 -0
  237. /package/dist/{chunk-PGD3DXZP.js.map → chunk-M5UJJUON.js.map} +0 -0
  238. /package/dist/{chunk-5C4LXMSK.js.map → chunk-MWHSWK4W.js.map} +0 -0
  239. /package/dist/{chunk-WTWHORVW.js.map → chunk-N27L5IY2.js.map} +0 -0
  240. /package/dist/{chunk-4OH6FGAO.js.map → chunk-N5VXPTXC.js.map} +0 -0
  241. /package/dist/{chunk-HDEQRI2O.js.map → chunk-NAZROP6I.js.map} +0 -0
  242. /package/dist/{chunk-R3YVNAJB.js.map → chunk-NDE7CH6C.js.map} +0 -0
  243. /package/dist/{chunk-BIJAWZYW.js.map → chunk-OLT3YTDH.js.map} +0 -0
  244. /package/dist/{chunk-GM35IHF7.js.map → chunk-PRK4WTPU.js.map} +0 -0
  245. /package/dist/{chunk-LGVIKA7O.js.map → chunk-QAITDA5B.js.map} +0 -0
  246. /package/dist/{chunk-MOUCCUET.js.map → chunk-QGUFN25Q.js.map} +0 -0
  247. /package/dist/{chunk-5HP6LVWU.js.map → chunk-QHYSECIG.js.map} +0 -0
  248. /package/dist/{chunk-SZ3SRRYZ.js.map → chunk-RXS4SQ5S.js.map} +0 -0
  249. /package/dist/{chunk-6KQLDPKA.js.map → chunk-S6AHHQ6C.js.map} +0 -0
  250. /package/dist/{chunk-NAB4MKTJ.js.map → chunk-S6DX26S3.js.map} +0 -0
  251. /package/dist/{chunk-R4FPBSFD.js.map → chunk-SO73GYOZ.js.map} +0 -0
  252. /package/dist/{chunk-V3KLIYTW.js.map → chunk-SXQZMC7R.js.map} +0 -0
  253. /package/dist/{chunk-JHTF22NQ.js.map → chunk-TA7UZZMW.js.map} +0 -0
  254. /package/dist/{chunk-3X7Z467X.js.map → chunk-UJMOG5LL.js.map} +0 -0
  255. /package/dist/{chunk-TGDFKC7X.js.map → chunk-VOPOPTZ7.js.map} +0 -0
  256. /package/dist/{chunk-IFHT77WK.js.map → chunk-WF2WCZCR.js.map} +0 -0
  257. /package/dist/{chunk-6F5WBXE2.js.map → chunk-X2NFFBTO.js.map} +0 -0
  258. /package/dist/{chunk-IHFN3JTR.js.map → chunk-X5TGZAP2.js.map} +0 -0
  259. /package/dist/{chunk-RJ57OOCD.js.map → chunk-X66BHMAU.js.map} +0 -0
  260. /package/dist/{chunk-DQEMWVMT.js.map → chunk-X7Y7WX73.js.map} +0 -0
  261. /package/dist/{chunk-EJF6AV2U.js.map → chunk-XHGDFN7A.js.map} +0 -0
  262. /package/dist/{chunk-FGV7V2N4.js.map → chunk-Y3WMJAZ3.js.map} +0 -0
  263. /package/dist/{chunk-ZET6A3OF.js.map → chunk-YHCBJULX.js.map} +0 -0
  264. /package/dist/{chunk-EAG7YMIS.js.map → chunk-YVMTXBJJ.js.map} +0 -0
  265. /package/dist/{chunk-OUKBMQ2O.js.map → chunk-Z3SQEEB6.js.map} +0 -0
  266. /package/dist/{chunk-OUAK37ID.js.map → chunk-ZNXPGZJQ.js.map} +0 -0
  267. /package/dist/{chunk-MVH7YAWY.js.map → chunk-ZPN6SSWA.js.map} +0 -0
  268. /package/dist/{graph-edge-decay-FXGELBFA.js.map → graph-edge-decay-DGTBFALQ.js.map} +0 -0
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Span source stamp/verify (issue #2333).
3
+ *
4
+ * Offsets index the exact normalized string sent to the model. If that string
5
+ * drifts before materialization, the offsets point at different characters and
6
+ * a wrong quote is persisted as verbatim source. The stamp lets materialization
7
+ * detect the drift without carrying the source text alongside it.
8
+ *
9
+ * Pure apart from hashing: no I/O, no clock, no randomness. The input text is
10
+ * never mutated and never appears in a result or error message.
11
+ */
12
+
13
+ import { createHash } from "node:crypto";
14
+
15
+ export interface SpanSourceStamp {
16
+ /** Hex digest of the exact string the model was shown. */
17
+ hash: string;
18
+ /** Length of that string, so a mismatch reports which dimension drifted. */
19
+ length: number;
20
+ }
21
+
22
+ export type SpanSourceCheck =
23
+ | { ok: true }
24
+ | {
25
+ ok: false;
26
+ error: "length_mismatch" | "hash_mismatch";
27
+ expected: SpanSourceStamp;
28
+ actual: SpanSourceStamp;
29
+ };
30
+
31
+ const HASH_PATTERN = /^[0-9a-f]{64}$/;
32
+
33
+ function requireText(text: string): void {
34
+ if (typeof text !== "string") {
35
+ throw new TypeError(`text must be a string, got ${typeof text}`);
36
+ }
37
+ }
38
+
39
+ function requireStamp(expected: SpanSourceStamp): void {
40
+ if (!Object.hasOwn(expected, "hash")) {
41
+ throw new RangeError("expected stamp is missing field hash");
42
+ }
43
+ if (!Object.hasOwn(expected, "length")) {
44
+ throw new RangeError("expected stamp is missing field length");
45
+ }
46
+ const { hash, length } = expected;
47
+ if (typeof hash !== "string" || !HASH_PATTERN.test(hash)) {
48
+ throw new RangeError(
49
+ "expected stamp field hash must be 64 lowercase hex characters",
50
+ );
51
+ }
52
+ if (!Number.isInteger(length) || length < 0) {
53
+ throw new RangeError(
54
+ "expected stamp field length must be a non-negative integer",
55
+ );
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Hash the exact string. No trimming, no newline normalization.
61
+ *
62
+ * The digest covers UTF-16LE code units, not UTF-8 bytes: Node's UTF-8
63
+ * encoder replaces an unpaired surrogate with U+FFFD before hashing, so
64
+ * "\ud800", "\ud801", and "\ufffd" all produce ONE digest under utf8 and
65
+ * drift between them would verify as unchanged. Unpaired surrogates reach
66
+ * real memory text through JSON escapes, and this stamp exists to catch
67
+ * exactly that class of silent substitution, so it hashes a lossless
68
+ * representation of the string instead.
69
+ */
70
+ export function stampSpanSource(text: string): SpanSourceStamp {
71
+ requireText(text);
72
+ const hash = createHash("sha256").update(Buffer.from(text, "utf16le")).digest("hex");
73
+ return { hash, length: text.length };
74
+ }
75
+
76
+ /**
77
+ * Verify the materialized string against its stamp. Length is compared first:
78
+ * it is the cheaper, more diagnostic dimension. Mismatch results carry both
79
+ * stamps and never the text itself.
80
+ */
81
+ export function verifySpanSource(
82
+ text: string,
83
+ expected: SpanSourceStamp,
84
+ ): SpanSourceCheck {
85
+ requireText(text);
86
+ requireStamp(expected);
87
+ const actual = stampSpanSource(text);
88
+ // Project the caller's stamp down to exactly the two stamp fields. Echoing
89
+ // the original object would re-export any extra property it carries — a
90
+ // `source` field holding the text defeats the whole point of a stamp, which
91
+ // exists so the content does not have to travel.
92
+ const safeExpected: SpanSourceStamp = { hash: expected.hash, length: expected.length };
93
+ if (actual.length !== safeExpected.length) {
94
+ return { ok: false, error: "length_mismatch", expected: safeExpected, actual };
95
+ }
96
+ if (actual.hash !== safeExpected.hash) {
97
+ return { ok: false, error: "hash_mismatch", expected: safeExpected, actual };
98
+ }
99
+ return { ok: true };
100
+ }
@@ -0,0 +1,282 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { parseConfig } from "./config.js";
5
+ import { RecallResultFormatter } from "./orchestration/recall-result-formatter.js";
6
+ import { applyRecallStateViews } from "./recall-state-view-wire.js";
7
+ import { checkStateViewZeroDiff, type StateViewLine } from "./recall-state-view-zero-diff.js";
8
+
9
+ function line(memoryId: string, text: string, stateLabel = "current"): StateViewLine {
10
+ return { memoryId, text, stateLabel };
11
+ }
12
+
13
+ test("identical current-only lists pass", () => {
14
+ const baseline = [line("a", "Works at Acme"), line("b", "Prefers dark mode")];
15
+ const annotated = baseline.map((l) => ({ ...l }));
16
+ assert.deepEqual(checkStateViewZeroDiff({ baseline, annotated }), {
17
+ ok: true,
18
+ reason: "verified",
19
+ });
20
+ assert.deepEqual(checkStateViewZeroDiff({ baseline: [], annotated: [] }), {
21
+ ok: true,
22
+ reason: "verified",
23
+ });
24
+ });
25
+
26
+ test("changed current line reports current_line_changed with the id", () => {
27
+ const baseline = [line("a", "Was a baker"), line("b", "Runs daily")];
28
+ const annotated = [line("a", "Was a baker at a shop"), line("b", "Runs daily")];
29
+ assert.deepEqual(checkStateViewZeroDiff({ baseline, annotated }), {
30
+ ok: false,
31
+ error: "current_line_changed",
32
+ memoryId: "a",
33
+ });
34
+ });
35
+
36
+ test("trailing space on a current line is a diff", () => {
37
+ const baseline = [line("a", "Was a baker")];
38
+ const annotated = [line("a", "Was a baker ")];
39
+ assert.deepEqual(checkStateViewZeroDiff({ baseline, annotated }), {
40
+ ok: false,
41
+ error: "current_line_changed",
42
+ memoryId: "a",
43
+ });
44
+ });
45
+
46
+ test("reordered lines report order_changed", () => {
47
+ const baseline = [line("a", "A"), line("b", "B")];
48
+ const reordered = [line("b", "B"), line("a", "A")];
49
+ assert.deepEqual(checkStateViewZeroDiff({ baseline, annotated: reordered }), {
50
+ ok: false,
51
+ error: "order_changed",
52
+ });
53
+ });
54
+
55
+ test("dropped or added current lines report order_changed", () => {
56
+ const baseline = [line("a", "A"), line("b", "B")];
57
+ const dropped = [line("a", "A")];
58
+ assert.deepEqual(checkStateViewZeroDiff({ baseline, annotated: dropped }), {
59
+ ok: false,
60
+ error: "order_changed",
61
+ });
62
+ const added = [line("a", "A"), line("b", "B"), line("c", "C")];
63
+ assert.deepEqual(checkStateViewZeroDiff({ baseline, annotated: added }), {
64
+ ok: false,
65
+ error: "order_changed",
66
+ });
67
+ });
68
+
69
+ test("text mismatch wins over order mismatch", () => {
70
+ const baseline = [line("a", "A"), line("b", "B")];
71
+ const annotated = [line("b", "B"), line("a", "Changed")];
72
+ assert.deepEqual(checkStateViewZeroDiff({ baseline, annotated }), {
73
+ ok: false,
74
+ error: "current_line_changed",
75
+ memoryId: "a",
76
+ });
77
+ });
78
+
79
+ test("historical or transition item means the guarantee does not apply", () => {
80
+ const baseline = [line("new", "Current job")];
81
+ const withHistorical = [line("old", "Was a baker", "historical"), line("new", "Current job")];
82
+ assert.deepEqual(checkStateViewZeroDiff({ baseline, annotated: withHistorical }), {
83
+ ok: true,
84
+ reason: "not_applicable",
85
+ });
86
+ const withTransition = [line("mid", "Bridge job", "transition"), line("new", "Current job")];
87
+ assert.deepEqual(checkStateViewZeroDiff({ baseline, annotated: withTransition }), {
88
+ ok: true,
89
+ reason: "not_applicable",
90
+ });
91
+ });
92
+
93
+ test("non-current label in baseline throws RangeError", () => {
94
+ const baseline = [line("old", "Was a baker", "historical")];
95
+ assert.throws(
96
+ () => checkStateViewZeroDiff({ baseline, annotated: [] }),
97
+ (err: unknown) => err instanceof RangeError && /baseline/.test(String(err.message)),
98
+ );
99
+ });
100
+
101
+ test("unknown state label throws TypeError listing allowed values", () => {
102
+ const annotated = [line("a", "Text", "bogus")];
103
+ assert.throws(
104
+ () => checkStateViewZeroDiff({ baseline: [], annotated }),
105
+ (err: unknown) =>
106
+ err instanceof TypeError &&
107
+ /state label/.test(String(err.message)) &&
108
+ /current, historical, transition/.test(String(err.message)),
109
+ );
110
+ });
111
+
112
+ test("duplicate memoryId throws RangeError in either list", () => {
113
+ assert.throws(
114
+ () =>
115
+ checkStateViewZeroDiff({
116
+ baseline: [line("a", "A"), line("a", "A")],
117
+ annotated: [],
118
+ }),
119
+ (err: unknown) => err instanceof RangeError && /duplicate/.test(String(err.message)),
120
+ );
121
+ assert.throws(
122
+ () =>
123
+ checkStateViewZeroDiff({
124
+ baseline: [line("a", "A")],
125
+ annotated: [line("b", "B"), line("b", "B again")],
126
+ }),
127
+ (err: unknown) => err instanceof RangeError && /duplicate/.test(String(err.message)),
128
+ );
129
+ });
130
+
131
+ test("inputs are not mutated", () => {
132
+ const baseline = [line("a", "A"), line("b", "B")];
133
+ const annotated = [line("old", "Old", "historical"), line("a", "A"), line("b", "B")];
134
+ const baselineCopy = structuredClone(baseline);
135
+ const annotatedCopy = structuredClone(annotated);
136
+ checkStateViewZeroDiff({ baseline, annotated });
137
+ assert.deepEqual(baseline, baselineCopy);
138
+ assert.deepEqual(annotated, annotatedCopy);
139
+ });
140
+
141
+ /** A fixture row: the guard's line shape plus the fields the live route reads. */
142
+ type LiveFixtureRow = StateViewLine & {
143
+ score: number;
144
+ supersededAt?: string;
145
+ supersededBy?: string;
146
+ };
147
+
148
+ // Review round 2: the earlier follow-up ran `injectStateViewLines`, which has
149
+ // no production caller. The live route in orchestration/recall-entry.ts is
150
+ // applyRecallStateViews -> RecallResultFormatter.formatQmdResultEntries, so
151
+ // these cases drive those two real stages. A regression that reorders, drops,
152
+ // or perturbs current-only entries in widening OR in live formatting now
153
+ // fails here.
154
+ function liveEntries(
155
+ results: readonly LiveFixtureRow[],
156
+ query: string,
157
+ stateViewsEnabled: boolean,
158
+ ): { entries: string[]; widened: { id?: string; stateLabel?: string }[] } {
159
+ // parseConfig intentionally does not carry `recallStateViews` (see
160
+ // recall-state-view-wire.ts: "parseConfig cannot grow"), so operators set it
161
+ // on the live config object. Setting it INSIDE parseConfig silently yields a
162
+ // disabled render, which makes a zero-diff assertion vacuous.
163
+ const parsed = parseConfig({ memoryDir: "/tmp/remnic-zero-diff-test" });
164
+ const config = stateViewsEnabled
165
+ ? ({ ...parsed, recallStateViews: true } as typeof parsed)
166
+ : parsed;
167
+ const qmdResults = results.map((result) => ({
168
+ path: `/tmp/remnic-zero-diff-test/facts/${result.memoryId}.md`,
169
+ score: result.score,
170
+ snippet: result.text,
171
+ // `resultStateViewId` reads `id ?? docid` — NOT memoryId. A fixture keyed
172
+ // only on memoryId gives every row the empty id, so widening drops any
173
+ // superseded row and the test silently measures nothing.
174
+ id: result.memoryId,
175
+ memoryId: result.memoryId,
176
+ stateLabel: result.stateLabel,
177
+ ...(result.supersededAt ? { supersededAt: result.supersededAt } : {}),
178
+ ...(result.supersededBy ? { supersededBy: result.supersededBy } : {}),
179
+ }));
180
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- test fixture
181
+ const widened = applyRecallStateViews(qmdResults as any[], query, config);
182
+ const formatter = new RecallResultFormatter(config);
183
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- test fixture
184
+ const entries = formatter.formatQmdResultEntries("Relevant Memories", widened as any[]).entries;
185
+ return { entries, widened: widened as unknown as { id?: string; stateLabel?: string }[] };
186
+ }
187
+
188
+ function linesFrom(
189
+ entries: readonly string[],
190
+ results: readonly LiveFixtureRow[],
191
+ ): StateViewLine[] {
192
+ return entries.map((text, index) => ({
193
+ memoryId: results[index]!.memoryId,
194
+ text,
195
+ stateLabel: results[index]!.stateLabel,
196
+ }));
197
+ }
198
+
199
+ test("the enabled route labels an admitted superseded row; disabled leaves labels alone", () => {
200
+ const results = [
201
+ { memoryId: "m-1", text: "we use PostgreSQL", stateLabel: "current" as const, score: 0.9 },
202
+ {
203
+ memoryId: "m-0",
204
+ text: "we use SQLite",
205
+ stateLabel: "current" as const,
206
+ supersededAt: "2026-08-01",
207
+ supersededBy: "m-1",
208
+ score: 0.7,
209
+ },
210
+ ];
211
+ const query = "what changed about the database?";
212
+ const enabled = liveEntries(results, query, true);
213
+ const disabled = liveEntries(results, query, false);
214
+
215
+ // Observable effect #1: widening relabels the superseded row, and only when
216
+ // the live config carries the flag. This fails if recallStateViews is ignored.
217
+ assert.equal(
218
+ enabled.widened.find((row) => row.id === "m-0")?.stateLabel,
219
+ "historical",
220
+ "the enabled route must relabel the superseded row",
221
+ );
222
+ assert.equal(
223
+ disabled.widened.find((row) => row.id === "m-0")?.stateLabel,
224
+ "current",
225
+ "the disabled route must not touch labels",
226
+ );
227
+ // NOTE: the live formatter renders no superseded prefix — formatSupersededPrefix
228
+ // is reachable only from renderStateViewLine, which has no production caller.
229
+ // So the rendered entries are identical here, and asserting a rendered
230
+ // annotation through this route would be asserting a feature that is not
231
+ // wired. That gap belongs to the rendering slice, not to this guard.
232
+ assert.deepEqual(enabled.entries, disabled.entries);
233
+ });
234
+
235
+ test("the enabled route drops a superseded row whose successor is absent", () => {
236
+ // Observable effect #2, and the sharper one: an orphaned superseded row is
237
+ // admitted when state views are off and dropped when they are on, so the
238
+ // entry count itself depends on the live config reaching widening.
239
+ const results = [
240
+ { memoryId: "m-9", text: "unrelated current fact", stateLabel: "current" as const, score: 0.9 },
241
+ {
242
+ memoryId: "m-0",
243
+ text: "we use SQLite",
244
+ stateLabel: "current" as const,
245
+ supersededAt: "2026-08-01",
246
+ supersededBy: "m-absent",
247
+ score: 0.7,
248
+ },
249
+ ];
250
+ const query = "what changed about the database?";
251
+ assert.equal(liveEntries(results, query, false).entries.length, 2, "disabled admits both rows");
252
+ assert.equal(liveEntries(results, query, true).entries.length, 1, "enabled drops the orphan");
253
+ });
254
+ test("the live recall route is zero-diff for a current-only result set", () => {
255
+ const results = [
256
+ { memoryId: "m-1", text: "the API limit is 100/min", stateLabel: "current" as const, score: 0.9 },
257
+ { memoryId: "m-2", text: "we chose SQLite", stateLabel: "current" as const, score: 0.8 },
258
+ ];
259
+ const query = "what changed about the database?";
260
+ const baseline = liveEntries(results, query, false).entries;
261
+ const annotated = liveEntries(results, query, true).entries;
262
+
263
+ assert.deepEqual(annotated, baseline, "state views must not touch a current-only render");
264
+ const check = checkStateViewZeroDiff({
265
+ baseline: linesFrom(baseline, results),
266
+ annotated: linesFrom(annotated, results),
267
+ });
268
+ assert.deepEqual(check, { ok: true, reason: "verified" });
269
+ });
270
+
271
+ test("the live route still renders every current entry it was given", () => {
272
+ // Guards the inverse failure: an empty or dropped render would satisfy a
273
+ // naive zero-diff comparison, so assert the payload actually survives.
274
+ const results = [
275
+ { memoryId: "m-1", text: "the API limit is 100/min", stateLabel: "current" as const, score: 0.9 },
276
+ { memoryId: "m-2", text: "we chose SQLite", stateLabel: "current" as const, score: 0.8 },
277
+ ];
278
+ const entries = liveEntries(results, "what changed about the database?", true).entries;
279
+ assert.equal(entries.length, 2);
280
+ assert.match(entries[0]!, /the API limit is 100\/min/);
281
+ assert.match(entries[1]!, /we chose SQLite/);
282
+ });
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Zero-diff guard for recall state views (issue #1952).
3
+ *
4
+ * The issue promises: current items are unchanged — zero diff for existing
5
+ * users when no historical item qualifies. This module makes that promise
6
+ * checkable. It compares the lines the pipeline would inject WITHOUT state
7
+ * views (baseline) against the lines it would inject WITH them (annotated).
8
+ *
9
+ * Ok reasons are named for what a reader will assume they mean:
10
+ * - `verified`: no historical or transition item is present, so the promise
11
+ * applies, and the byte-exact comparison ran and held.
12
+ * - `not_applicable`: at least one historical or transition item is present,
13
+ * so the "when no historical item qualifies" precondition is false and
14
+ * nothing was compared.
15
+ *
16
+ * Text comparison is byte-exact; a trailing space added by a renderer is a
17
+ * diff. Current-line text mismatches are reported before order mismatches,
18
+ * so `order_changed` means every line matched by memoryId had identical
19
+ * text and only the memoryId sequence moved.
20
+ *
21
+ * Pure: no I/O, inputs are never mutated. Tests drive it through the live
22
+ * recall route (`applyRecallStateViews` then
23
+ * `RecallResultFormatter.formatQmdResultEntries`); wiring the guard into a
24
+ * runtime caller is a later slice.
25
+ */
26
+ import type { StateLabel } from "./recall-state-view.js";
27
+
28
+ export interface StateViewLine {
29
+ memoryId: string;
30
+ /** The line as it would be injected. */
31
+ text: string;
32
+ stateLabel: string;
33
+ }
34
+
35
+ export type ZeroDiffCheck =
36
+ | { ok: true; reason: "verified" | "not_applicable" }
37
+ | { ok: false; error: "current_line_changed"; memoryId: string }
38
+ | { ok: false; error: "order_changed" };
39
+
40
+ const STATE_LABELS = Object.freeze(
41
+ ["current", "historical", "transition"] as const satisfies readonly StateLabel[],
42
+ );
43
+
44
+ function isStateLabel(value: string): value is StateLabel {
45
+ return (STATE_LABELS as readonly string[]).includes(value);
46
+ }
47
+
48
+ function validateLines(lines: readonly StateViewLine[], name: string): void {
49
+ for (const line of lines) {
50
+ if (line === null || typeof line !== "object") {
51
+ throw new TypeError(`${name} line must be an object`);
52
+ }
53
+ if (typeof line.memoryId !== "string") {
54
+ throw new TypeError(`${name} line memoryId must be a string`);
55
+ }
56
+ if (typeof line.text !== "string") {
57
+ throw new TypeError(`${name} line text must be a string`);
58
+ }
59
+ if (typeof line.stateLabel !== "string" || !isStateLabel(line.stateLabel)) {
60
+ throw new TypeError(
61
+ `unknown state label: ${JSON.stringify(line.stateLabel)} (allowed: ${STATE_LABELS.join(", ")})`,
62
+ );
63
+ }
64
+ }
65
+ const seen = new Set<string>();
66
+ for (const line of lines) {
67
+ if (seen.has(line.memoryId)) {
68
+ throw new RangeError(`duplicate memoryId in ${name}: ${line.memoryId}`);
69
+ }
70
+ seen.add(line.memoryId);
71
+ }
72
+ }
73
+
74
+ export function checkStateViewZeroDiff(input: {
75
+ baseline: readonly StateViewLine[];
76
+ annotated: readonly StateViewLine[];
77
+ }): ZeroDiffCheck {
78
+ if (input === null || typeof input !== "object") {
79
+ throw new TypeError("input must be an object");
80
+ }
81
+ if (!Array.isArray(input.baseline)) {
82
+ throw new TypeError("baseline must be an array");
83
+ }
84
+ if (!Array.isArray(input.annotated)) {
85
+ throw new TypeError("annotated must be an array");
86
+ }
87
+ const baseline = input.baseline;
88
+ const annotated = input.annotated;
89
+ validateLines(baseline, "baseline");
90
+ validateLines(annotated, "annotated");
91
+ for (const line of baseline) {
92
+ if (line.stateLabel !== "current") {
93
+ throw new RangeError(
94
+ `baseline may only contain current lines, got ${JSON.stringify(line.stateLabel)}`,
95
+ );
96
+ }
97
+ }
98
+
99
+ const hasHistorical = annotated.some(
100
+ (line) => line.stateLabel === "historical" || line.stateLabel === "transition",
101
+ );
102
+ if (hasHistorical) {
103
+ return { ok: true, reason: "not_applicable" };
104
+ }
105
+
106
+ const baselineText = new Map<string, string>();
107
+ for (const line of baseline) {
108
+ baselineText.set(line.memoryId, line.text);
109
+ }
110
+ for (const line of annotated) {
111
+ if (baselineText.has(line.memoryId) && baselineText.get(line.memoryId) !== line.text) {
112
+ return { ok: false, error: "current_line_changed", memoryId: line.memoryId };
113
+ }
114
+ }
115
+
116
+ if (baseline.length !== annotated.length) {
117
+ return { ok: false, error: "order_changed" };
118
+ }
119
+ for (let i = 0; i < baseline.length; i++) {
120
+ if (baseline[i].memoryId !== annotated[i].memoryId) {
121
+ return { ok: false, error: "order_changed" };
122
+ }
123
+ }
124
+ return { ok: true, reason: "verified" };
125
+ }
@@ -0,0 +1,127 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import { SHARED_AUTHORITIES } from "./governance.js";
5
+ import {
6
+ compareSharedAuthority,
7
+ resolveSharedAuthority,
8
+ SHARED_AUTHORITY_CLASSES,
9
+ } from "./authority-precedence.js";
10
+
11
+ test("SHARED_AUTHORITY_CLASSES is the canonical governance list, not a copy", () => {
12
+ assert.equal(SHARED_AUTHORITY_CLASSES, SHARED_AUTHORITIES);
13
+ });
14
+
15
+ test("resolveSharedAuthority defaults absent authority to informational", () => {
16
+ assert.equal(resolveSharedAuthority({}), "informational");
17
+ assert.equal(resolveSharedAuthority({ authority: undefined }), "informational");
18
+ assert.equal(resolveSharedAuthority({ authority: null }), "informational");
19
+ });
20
+
21
+ test("resolveSharedAuthority honors binding only with the explicit boolean flag", () => {
22
+ assert.equal(resolveSharedAuthority({ authority: "binding", allowBinding: true }), "binding");
23
+ assert.equal(resolveSharedAuthority({ authority: "binding" }), "advisory");
24
+ assert.equal(resolveSharedAuthority({ authority: "binding", allowBinding: undefined }), "advisory");
25
+ assert.equal(resolveSharedAuthority({ authority: "binding", allowBinding: false }), "advisory");
26
+ });
27
+
28
+ test("resolveSharedAuthority keeps advisory and informational regardless of the flag", () => {
29
+ assert.equal(resolveSharedAuthority({ authority: "advisory" }), "advisory");
30
+ assert.equal(resolveSharedAuthority({ authority: "advisory", allowBinding: true }), "advisory");
31
+ assert.equal(resolveSharedAuthority({ authority: "informational" }), "informational");
32
+ assert.equal(resolveSharedAuthority({ authority: "informational", allowBinding: true }), "informational");
33
+ });
34
+
35
+ test("resolveSharedAuthority maps unrecognized strings to informational without normalizing", () => {
36
+ // Flag on proves these stay informational because of the exact-value
37
+ // check, not because binding was denied.
38
+ for (const authority of ["BINDING", "mandatory", " binding", "binding ", ""]) {
39
+ assert.equal(resolveSharedAuthority({ authority, allowBinding: true }), "informational");
40
+ }
41
+ });
42
+
43
+ test("resolveSharedAuthority maps non-string authority values to informational", () => {
44
+ for (const authority of [3, 0, true, false, { authority: "binding" }, ["binding"], Number.NaN]) {
45
+ assert.equal(resolveSharedAuthority({ authority }), "informational");
46
+ }
47
+ });
48
+
49
+ test("resolveSharedAuthority rejects a non-boolean allowBinding", () => {
50
+ for (const allowBinding of ["true", 1, null, "yes"] as unknown[]) {
51
+ assert.throws(
52
+ () => resolveSharedAuthority({ authority: "binding", allowBinding: allowBinding as never }),
53
+ { name: "TypeError", message: /allowBinding/ },
54
+ );
55
+ }
56
+ });
57
+ test("compareSharedAuthority orders every class pair by ascending privilege", () => {
58
+ const expected: Record<string, number> = { informational: 0, advisory: 1, binding: 2 };
59
+ for (const a of SHARED_AUTHORITY_CLASSES) {
60
+ for (const b of SHARED_AUTHORITY_CLASSES) {
61
+ const forward = compareSharedAuthority(a, b);
62
+ const backward = compareSharedAuthority(b, a);
63
+ assert.equal(Math.sign(forward), Math.sign(expected[a] - expected[b]));
64
+ assert.equal(Math.sign(backward) || 0, (-Math.sign(forward)) || 0);
65
+ }
66
+ }
67
+ assert.equal(compareSharedAuthority("informational", "informational"), 0);
68
+ assert.equal(compareSharedAuthority("advisory", "advisory"), 0);
69
+ assert.equal(compareSharedAuthority("binding", "binding"), 0);
70
+ });
71
+
72
+ test("compareSharedAuthority sorts unknown classes as informational", () => {
73
+ assert.equal(compareSharedAuthority("mandatory", "informational"), 0);
74
+ assert.equal(compareSharedAuthority("informational", "mandatory"), 0);
75
+ assert.equal(compareSharedAuthority("mandatory", "mandatory"), 0);
76
+ assert.equal(compareSharedAuthority("mandatory", "advisory") < 0, true);
77
+ assert.equal(compareSharedAuthority("advisory", "mandatory") > 0, true);
78
+ assert.equal(compareSharedAuthority("mandatory", "binding") < 0, true);
79
+ assert.equal(compareSharedAuthority("BINDING", "advisory") < 0, true);
80
+ });
81
+
82
+ test("sorting a shuffled mixed list is deterministic", () => {
83
+ const shuffled = [
84
+ "binding",
85
+ "informational",
86
+ "advisory",
87
+ "mandatory",
88
+ "advisory",
89
+ "informational",
90
+ "binding",
91
+ "informational",
92
+ ];
93
+ const first = [...shuffled].sort(compareSharedAuthority);
94
+ const second = [...shuffled].sort(compareSharedAuthority);
95
+ assert.deepEqual(first, [
96
+ "informational",
97
+ "mandatory",
98
+ "informational",
99
+ "informational",
100
+ "advisory",
101
+ "advisory",
102
+ "binding",
103
+ "binding",
104
+ ]);
105
+ assert.deepEqual(first, second);
106
+ });
107
+
108
+ // Review round 2: the flag was read twice — once to validate, once to decide.
109
+ // A getter that changes between those reads is the whole attack.
110
+ test("a changing allowBinding getter cannot smuggle binding through", () => {
111
+ let reads = 0;
112
+ const input = {
113
+ authority: "binding",
114
+ get allowBinding() {
115
+ reads += 1;
116
+ // undefined for the validation read, true for the decision read.
117
+ return reads === 1 ? undefined : true;
118
+ },
119
+ };
120
+ assert.equal(resolveSharedAuthority(input), "advisory");
121
+ assert.equal(reads, 1, "the switch must be read exactly once");
122
+ });
123
+
124
+ test("an explicit boolean opt-in still honors binding", () => {
125
+ assert.equal(resolveSharedAuthority({ authority: "binding", allowBinding: true }), "binding");
126
+ assert.equal(resolveSharedAuthority({ authority: "binding", allowBinding: false }), "advisory");
127
+ });