pi-mega-compact 0.16.3 → 0.17.1

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 (207) hide show
  1. package/dist/config/vector-cortex.js +91 -0
  2. package/dist/dedup/digest.js +30 -0
  3. package/dist/dedup/l1-lsh-v2.js +60 -0
  4. package/dist/dedup/l1-lsh.js +52 -0
  5. package/dist/dedup/l1-minhash-v2.js +193 -0
  6. package/dist/dedup/l1-minhash.js +91 -0
  7. package/dist/dedup/l1-verify.js +54 -0
  8. package/dist/dedup/mmr.js +45 -0
  9. package/dist/dedup/normalize.js +39 -0
  10. package/dist/dedup/raptor/buildHistory.js +164 -0
  11. package/dist/dedup/raptor/guardrails.js +83 -0
  12. package/dist/dedup/raptor/incremental.js +304 -0
  13. package/dist/dedup/raptor/index.js +190 -0
  14. package/dist/dedup/raptor/kmeans.js +152 -0
  15. package/dist/dedup/raptor/multilevel.js +184 -0
  16. package/dist/dedup/raptor/retrieval.js +93 -0
  17. package/dist/dedup/raptor/summarizer.js +86 -0
  18. package/dist/dedup/raptor/tree.js +202 -0
  19. package/dist/dedup/topk.js +60 -0
  20. package/dist/extensions/dashboard-server/api-contracts/endpoints/registry-ext.js +30 -0
  21. package/dist/extensions/dashboard-server/api-contracts/endpoints/registry.js +4 -14
  22. package/dist/extensions/dashboard-server/api-contracts/vector-cortex.js +12 -0
  23. package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +11 -0
  24. package/dist/extensions/dashboard-server/routes-vector-cortex.js +292 -0
  25. package/dist/extensions/dashboard-server/routes.js +1 -0
  26. package/dist/extensions/dashboard-server/server.js +9 -1
  27. package/dist/extensions/mega-events/context-handler.js +11 -0
  28. package/dist/extensions/mega-runtime/vector-cortex-ledger.js +131 -0
  29. package/dist/extensions/mega-runtime/vector-cortex-safety.js +100 -0
  30. package/dist/src/config/vector-cortex.js +91 -0
  31. package/dist/src/config.js +6 -0
  32. package/dist/src/dedup/l1-lsh-v2.js +60 -0
  33. package/dist/src/dedup/l1-minhash-v2.js +193 -0
  34. package/dist/src/dedup/normalize.js +1 -1
  35. package/dist/src/vector-cortex/conformance/emit.js +46 -0
  36. package/dist/src/vector-cortex/conformance/manifest.js +235 -0
  37. package/dist/src/vector-cortex/conformance/runner.js +111 -0
  38. package/dist/src/vector-cortex/conformance/triadB-reader.js +95 -0
  39. package/dist/src/vector-cortex/eval/annotations.js +41 -0
  40. package/dist/src/vector-cortex/eval/metrics.js +86 -0
  41. package/dist/src/vector-cortex/eval/observer.js +61 -0
  42. package/dist/src/vector-cortex/eval/persist.js +67 -0
  43. package/dist/src/vector-cortex/eval/reader.js +29 -0
  44. package/dist/src/vector-cortex/eval/types.js +33 -0
  45. package/dist/src/vector-cortex/ledger/adapter.js +55 -0
  46. package/dist/src/vector-cortex/ledger/compat-journal.js +206 -0
  47. package/dist/src/vector-cortex/ledger/emit.js +53 -0
  48. package/dist/src/vector-cortex/ledger/event-codec.js +72 -0
  49. package/dist/src/vector-cortex/ledger/event-codecB.js +76 -0
  50. package/dist/src/vector-cortex/ledger/sqlite.js +200 -0
  51. package/dist/src/vector-cortex/ledger/store.js +184 -0
  52. package/dist/src/vector-cortex/ledger/types.js +52 -0
  53. package/dist/src/vector-cortex/ledger/validator.js +117 -0
  54. package/dist/src/vector-cortex/migrations/effective-cut-v2.js +103 -0
  55. package/dist/src/vector-cortex/migrations/minhash-v2.js +209 -0
  56. package/dist/src/vector-cortex/migrations/occurrence-v2.js +139 -0
  57. package/dist/src/vector-cortex/replay/cut.js +123 -0
  58. package/dist/src/vector-cortex/replay/emit.js +58 -0
  59. package/dist/src/vector-cortex/replay/replay.js +260 -0
  60. package/dist/src/vector-cortex/replay/replayB.js +73 -0
  61. package/dist/src/vector-cortex/replay/types.js +52 -0
  62. package/dist/src/vector-cortex/resilience/breaker-core.js +359 -0
  63. package/dist/src/vector-cortex/resilience/breaker.js +12 -0
  64. package/dist/src/vector-cortex/resilience/emit.js +56 -0
  65. package/dist/src/vector-cortex/resilience/spool-core.js +347 -0
  66. package/dist/src/vector-cortex/resilience/spool.js +12 -0
  67. package/dist/src/vector-cortex/resilience/types.js +32 -0
  68. package/dist/vector-cortex/conformance/emit.js +46 -0
  69. package/dist/vector-cortex/conformance/manifest.js +235 -0
  70. package/dist/vector-cortex/conformance/runner.js +111 -0
  71. package/dist/vector-cortex/conformance/triadB-reader.js +95 -0
  72. package/dist/vector-cortex/eval/annotations.js +41 -0
  73. package/dist/vector-cortex/eval/metrics.js +86 -0
  74. package/dist/vector-cortex/eval/observer.js +61 -0
  75. package/dist/vector-cortex/eval/persist.js +67 -0
  76. package/dist/vector-cortex/eval/reader.js +29 -0
  77. package/dist/vector-cortex/eval/types.js +33 -0
  78. package/dist/vector-cortex/ledger/adapter.js +55 -0
  79. package/dist/vector-cortex/ledger/compat-journal.js +206 -0
  80. package/dist/vector-cortex/ledger/emit.js +53 -0
  81. package/dist/vector-cortex/ledger/event-codec.js +72 -0
  82. package/dist/vector-cortex/ledger/event-codecB.js +76 -0
  83. package/dist/vector-cortex/ledger/sqlite.js +200 -0
  84. package/dist/vector-cortex/ledger/store.js +184 -0
  85. package/dist/vector-cortex/ledger/types.js +52 -0
  86. package/dist/vector-cortex/ledger/validator.js +117 -0
  87. package/dist/vector-cortex/migrations/effective-cut-v2.js +103 -0
  88. package/dist/vector-cortex/migrations/minhash-v2.js +209 -0
  89. package/dist/vector-cortex/migrations/occurrence-v2.js +139 -0
  90. package/dist/vector-cortex/replay/cut.js +123 -0
  91. package/dist/vector-cortex/replay/emit.js +58 -0
  92. package/dist/vector-cortex/replay/replay.js +260 -0
  93. package/dist/vector-cortex/replay/replayB.js +73 -0
  94. package/dist/vector-cortex/replay/types.js +52 -0
  95. package/dist/vector-cortex/resilience/breaker-core.js +359 -0
  96. package/dist/vector-cortex/resilience/breaker.js +12 -0
  97. package/dist/vector-cortex/resilience/emit.js +56 -0
  98. package/dist/vector-cortex/resilience/spool-core.js +347 -0
  99. package/dist/vector-cortex/resilience/spool.js +12 -0
  100. package/dist/vector-cortex/resilience/types.js +32 -0
  101. package/extensions/dashboard-client/dist/assets/{AreaChart-Y_X0Bbjd.js → AreaChart-BQXFF7UI.js} +2 -2
  102. package/extensions/dashboard-client/dist/assets/{AreaChart-Y_X0Bbjd.js.map → AreaChart-BQXFF7UI.js.map} +1 -1
  103. package/extensions/dashboard-client/dist/assets/{BarChart-B9XDb-Ob.js → BarChart-BVtoz9Eg.js} +2 -2
  104. package/extensions/dashboard-client/dist/assets/{BarChart-B9XDb-Ob.js.map → BarChart-BVtoz9Eg.js.map} +1 -1
  105. package/extensions/dashboard-client/dist/assets/{CacheTab-BUGVb1G1.js → CacheTab-trI2TIGy.js} +2 -2
  106. package/extensions/dashboard-client/dist/assets/{CacheTab-BUGVb1G1.js.map → CacheTab-trI2TIGy.js.map} +1 -1
  107. package/extensions/dashboard-client/dist/assets/{EventsTab-dOqLYTsy.js → EventsTab-CNUbk9__.js} +2 -2
  108. package/extensions/dashboard-client/dist/assets/{EventsTab-dOqLYTsy.js.map → EventsTab-CNUbk9__.js.map} +1 -1
  109. package/extensions/dashboard-client/dist/assets/{HealthTab-c2vezKg1.js → HealthTab-K-bd_CM-.js} +2 -2
  110. package/extensions/dashboard-client/dist/assets/{HealthTab-c2vezKg1.js.map → HealthTab-K-bd_CM-.js.map} +1 -1
  111. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-bQYHIcdx.js → MaintenanceTab-CoEioKzx.js} +2 -2
  112. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-bQYHIcdx.js.map → MaintenanceTab-CoEioKzx.js.map} +1 -1
  113. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-2L8pH6y2.js → MemoryMapTab-CJo24ulv.js} +2 -2
  114. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-2L8pH6y2.js.map → MemoryMapTab-CJo24ulv.js.map} +1 -1
  115. package/extensions/dashboard-client/dist/assets/{MetricsTab-CzUx1jpo.js → MetricsTab-DCNlQWWP.js} +2 -2
  116. package/extensions/dashboard-client/dist/assets/{MetricsTab-CzUx1jpo.js.map → MetricsTab-DCNlQWWP.js.map} +1 -1
  117. package/extensions/dashboard-client/dist/assets/{OverviewTab-eCNoaclv.js → OverviewTab-CQQlyIQ6.js} +2 -2
  118. package/extensions/dashboard-client/dist/assets/{OverviewTab-eCNoaclv.js.map → OverviewTab-CQQlyIQ6.js.map} +1 -1
  119. package/extensions/dashboard-client/dist/assets/{ReposTab-tb0IW3NF.js → ReposTab-Cqm82_yn.js} +2 -2
  120. package/extensions/dashboard-client/dist/assets/{ReposTab-tb0IW3NF.js.map → ReposTab-Cqm82_yn.js.map} +1 -1
  121. package/extensions/dashboard-client/dist/assets/{SessionsTab-C-IoLw5h.js → SessionsTab-A8xTAdET.js} +2 -2
  122. package/extensions/dashboard-client/dist/assets/{SessionsTab-C-IoLw5h.js.map → SessionsTab-A8xTAdET.js.map} +1 -1
  123. package/extensions/dashboard-client/dist/assets/{SetupTab-BvmFi8gf.js → SetupTab-CqaSgaZl.js} +2 -2
  124. package/extensions/dashboard-client/dist/assets/{SetupTab-BvmFi8gf.js.map → SetupTab-CqaSgaZl.js.map} +1 -1
  125. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DBH-cOlA.js → TimeSavedCard-C_f3q8GR.js} +2 -2
  126. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DBH-cOlA.js.map → TimeSavedCard-C_f3q8GR.js.map} +1 -1
  127. package/extensions/dashboard-client/dist/assets/{TurnsTab-DBMgmvnX.js → TurnsTab-BEsafACo.js} +2 -2
  128. package/extensions/dashboard-client/dist/assets/{TurnsTab-DBMgmvnX.js.map → TurnsTab-BEsafACo.js.map} +1 -1
  129. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DD1CMK80.js +2 -0
  130. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DD1CMK80.js.map +1 -0
  131. package/extensions/dashboard-client/dist/assets/{WikiTab-1LcQrXk3.js → WikiTab-Bf01M2_Z.js} +2 -2
  132. package/extensions/dashboard-client/dist/assets/{WikiTab-1LcQrXk3.js.map → WikiTab-Bf01M2_Z.js.map} +1 -1
  133. package/extensions/dashboard-client/dist/assets/{button-DtEsZTUE.js → button-BEJPTk_E.js} +2 -2
  134. package/extensions/dashboard-client/dist/assets/{button-DtEsZTUE.js.map → button-BEJPTk_E.js.map} +1 -1
  135. package/extensions/dashboard-client/dist/assets/{card-DcfTmw-4.js → card-DK1V382s.js} +2 -2
  136. package/extensions/dashboard-client/dist/assets/{card-DcfTmw-4.js.map → card-DK1V382s.js.map} +1 -1
  137. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-BsIxUDyw.js → generateCategoricalChart-CRtaPphL.js} +2 -2
  138. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-BsIxUDyw.js.map → generateCategoricalChart-CRtaPphL.js.map} +1 -1
  139. package/extensions/dashboard-client/dist/assets/{index-B93cewiX.js → index-CB6BslE4.js} +34 -29
  140. package/extensions/dashboard-client/dist/assets/index-CB6BslE4.js.map +1 -0
  141. package/extensions/dashboard-client/dist/assets/index-CfvziyBi.css +1 -0
  142. package/extensions/dashboard-client/dist/assets/{switch-Bowd7jCD.js → switch-C4FV6B9k.js} +2 -2
  143. package/extensions/dashboard-client/dist/assets/{switch-Bowd7jCD.js.map → switch-C4FV6B9k.js.map} +1 -1
  144. package/extensions/dashboard-client/dist/assets/{toggle-Bo7kJZ4u.js → toggle-GBmeENMd.js} +2 -2
  145. package/extensions/dashboard-client/dist/assets/{toggle-Bo7kJZ4u.js.map → toggle-GBmeENMd.js.map} +1 -1
  146. package/extensions/dashboard-client/dist/assets/{useSSE-zNhb0c3u.js → useSSE-BOKZGSn1.js} +2 -2
  147. package/extensions/dashboard-client/dist/assets/{useSSE-zNhb0c3u.js.map → useSSE-BOKZGSn1.js.map} +1 -1
  148. package/extensions/dashboard-client/dist/index.html +2 -2
  149. package/extensions/dashboard-client/src/App.tsx +2 -0
  150. package/extensions/dashboard-client/src/api/vector-cortex.ts +54 -0
  151. package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +272 -0
  152. package/extensions/dashboard-client/src/tabs/registry.ts +4 -1
  153. package/extensions/dashboard-client/src/types/vector-cortex.ts +84 -0
  154. package/extensions/dashboard-client/tsconfig.json +1 -1
  155. package/extensions/dashboard-server/api-contracts/endpoints/registry-ext.ts +67 -0
  156. package/extensions/dashboard-server/api-contracts/endpoints/registry.ts +4 -21
  157. package/extensions/dashboard-server/api-contracts/index.ts +3 -0
  158. package/extensions/dashboard-server/api-contracts/vector-cortex.ts +136 -0
  159. package/extensions/dashboard-server/routes-rag-settings-helpers.ts +41 -0
  160. package/extensions/dashboard-server/routes-vector-cortex.ts +332 -0
  161. package/extensions/dashboard-server/routes.ts +6 -0
  162. package/extensions/dashboard-server/server.ts +8 -0
  163. package/extensions/mega-events/context-handler.ts +16 -0
  164. package/extensions/mega-runtime/vector-cortex-ledger.ts +165 -0
  165. package/extensions/mega-runtime/vector-cortex-safety.ts +181 -0
  166. package/package.json +2 -1
  167. package/src/config/vector-cortex.ts +98 -0
  168. package/src/config.ts +28 -0
  169. package/src/dedup/l1-lsh-v2.ts +78 -0
  170. package/src/dedup/l1-minhash-v2.ts +221 -0
  171. package/src/dedup/normalize.ts +1 -1
  172. package/src/store/sqlite/game-scores.ts +2 -1
  173. package/src/vector-cortex/conformance/emit.ts +56 -0
  174. package/src/vector-cortex/conformance/manifest.ts +298 -0
  175. package/src/vector-cortex/conformance/runner.ts +165 -0
  176. package/src/vector-cortex/conformance/triadB-reader.ts +110 -0
  177. package/src/vector-cortex/eval/annotations.ts +70 -0
  178. package/src/vector-cortex/eval/metrics.ts +113 -0
  179. package/src/vector-cortex/eval/observer.ts +86 -0
  180. package/src/vector-cortex/eval/persist.ts +73 -0
  181. package/src/vector-cortex/eval/reader.ts +45 -0
  182. package/src/vector-cortex/eval/types.ts +89 -0
  183. package/src/vector-cortex/ledger/adapter.ts +71 -0
  184. package/src/vector-cortex/ledger/compat-journal.ts +234 -0
  185. package/src/vector-cortex/ledger/emit.ts +86 -0
  186. package/src/vector-cortex/ledger/event-codec.ts +79 -0
  187. package/src/vector-cortex/ledger/event-codecB.ts +87 -0
  188. package/src/vector-cortex/ledger/sqlite.ts +265 -0
  189. package/src/vector-cortex/ledger/store.ts +380 -0
  190. package/src/vector-cortex/ledger/types.ts +141 -0
  191. package/src/vector-cortex/ledger/validator.ts +124 -0
  192. package/src/vector-cortex/migrations/effective-cut-v2.ts +149 -0
  193. package/src/vector-cortex/migrations/minhash-v2.ts +263 -0
  194. package/src/vector-cortex/migrations/occurrence-v2.ts +192 -0
  195. package/src/vector-cortex/replay/cut.ts +169 -0
  196. package/src/vector-cortex/replay/emit.ts +89 -0
  197. package/src/vector-cortex/replay/replay.ts +308 -0
  198. package/src/vector-cortex/replay/replayB.ts +97 -0
  199. package/src/vector-cortex/replay/types.ts +145 -0
  200. package/src/vector-cortex/resilience/breaker-core.ts +444 -0
  201. package/src/vector-cortex/resilience/breaker.ts +22 -0
  202. package/src/vector-cortex/resilience/emit.ts +99 -0
  203. package/src/vector-cortex/resilience/spool-core.ts +411 -0
  204. package/src/vector-cortex/resilience/spool.ts +22 -0
  205. package/src/vector-cortex/resilience/types.ts +158 -0
  206. package/extensions/dashboard-client/dist/assets/index-B93cewiX.js.map +0 -1
  207. package/extensions/dashboard-client/dist/assets/index-C_tCbOI0.css +0 -1
@@ -0,0 +1,184 @@
1
+ /**
2
+ * vector-cortex/ledger/store.ts — VC1B capability-separated ledger contracts.
3
+ *
4
+ * Owns `LedgerReader` / `LedgerWriter` / `LedgerAdmin` and `CompatJournalV1`.
5
+ * The ledger is append-only, seq is monotonic per session, a tool RESULT names
6
+ * exactly one earlier call in the same session, and duplicates are identified by
7
+ * `(eventId, digest)` only (two occurrences may share bytes at distinct seq).
8
+ *
9
+ * Capability gating mirrors the host `asReader/asWriter/asAdmin` pattern: a
10
+ * consumer receives ONLY what it needs. Dashboard GET receives `LedgerReader`;
11
+ * only ingestion receives `LedgerWriter`; only the migration coordinator
12
+ * receives `LedgerAdmin`. This keeps the neutral byte-authority ledger closed to
13
+ * anything but the sanctioned channels (CONTRACTS §Store and migration
14
+ * contracts).
15
+ *
16
+ * Pure type/schema definitions + small pure predicates. No storage, no console,
17
+ * no network, no side effects (PREVENT-PI-004 / PREVENT-011).
18
+ */
19
+ import { VC1B_ENABLED } from "../../config/vector-cortex.js";
20
+ import { openOccurrenceStore, appendOccurrence, ledgerHighWater, readSessionOccurrences, readFromSeq, countOccurrences, hasOccurrence, } from "./sqlite.js";
21
+ import { initCompatJournal, createCompatJournal, journalPhase, } from "./compat-journal.js";
22
+ import { m2Copy, m2Validate, m2Switch, } from "../migrations/occurrence-v2.js";
23
+ /** The token that gates closures to a single capability (PREVENT-011-free). */
24
+ const _capability = Symbol("mc-ledger-capability");
25
+ /**
26
+ * Create the capability-separated occurrence-v2 ledger over its OWN isolated
27
+ * SQLite DB. `emit` is optional; occurrence-appended and compat-switch-committed
28
+ * events are only emitted when VC1B_ENABLED() and an emitter is supplied (mode-C
29
+ * parity: flag OFF / no emitter => zero observability writes).
30
+ *
31
+ * Normalization: `stateDir` gives the standard daemon location
32
+ * `<stateDir>/vector-cortex/occurrence-v2.db`; a bare `dbPath` overrides it
33
+ * (tests/rehearsal isolate the ledger). The compat journal shares the same DB
34
+ * so a v2 append + its journal record commit atomically.
35
+ */
36
+ export function createLedgerStore(opts, emit) {
37
+ const dbPath = "dbPath" in opts
38
+ ? opts.dbPath
39
+ : `${opts.stateDir}/vector-cortex/occurrence-v2.db`;
40
+ const db = openOccurrenceStore(dbPath);
41
+ initCompatJournal(db);
42
+ const compat = createCompatJournal(db);
43
+ const fire = (event, fields) => {
44
+ if (!VC1B_ENABLED())
45
+ return;
46
+ try {
47
+ emit?.(event, fields);
48
+ }
49
+ catch {
50
+ /* non-fatal observability — never break the agent loop */
51
+ }
52
+ };
53
+ const asReader = () => ({
54
+ kind: "LedgerReader",
55
+ [_capability]: "reader",
56
+ highWater: (session) => ledgerHighWater(db, session),
57
+ readSession: (session) => readSessionOccurrences(db, session),
58
+ readFrom: (session, fromSeq) => readFromSeq(db, session, fromSeq),
59
+ count: (session) => countOccurrences(db, session),
60
+ hasOccurrence: (session, eventId, digest) => hasOccurrence(db, session, eventId, digest),
61
+ });
62
+ const asWriter = () => ({
63
+ kind: "LedgerWriter",
64
+ [_capability]: "writer",
65
+ append(input) {
66
+ // The occurrence insert and its compat-journal record commit atomically
67
+ // inside one nested savepoint (CONTRACTS §Store: atomically appends).
68
+ db.exec("SAVEPOINT mc_ledger_app");
69
+ try {
70
+ const result = appendOccurrence(db, input);
71
+ if (result.ok) {
72
+ compat.record({
73
+ occurrence: result.occurrence,
74
+ legacyProjection: legacyProjectionOf(result.occurrence),
75
+ });
76
+ }
77
+ if (result.ok) {
78
+ db.exec("RELEASE mc_ledger_app");
79
+ fire("vector_cortex_occurrence_appended", {
80
+ session: result.occurrence.session,
81
+ seq: result.occurrence.seq.toString(),
82
+ eventId: result.occurrence.eventId,
83
+ digest: result.occurrence.digest,
84
+ kind: result.occurrence.kind,
85
+ });
86
+ }
87
+ else {
88
+ db.exec("ROLLBACK TO mc_ledger_app");
89
+ db.exec("RELEASE mc_ledger_app");
90
+ }
91
+ return result;
92
+ }
93
+ catch (e) {
94
+ db.exec("ROLLBACK TO mc_ledger_app");
95
+ db.exec("RELEASE mc_ledger_app");
96
+ throw e;
97
+ }
98
+ },
99
+ appendBatch(inputs) {
100
+ const out = [];
101
+ for (const input of inputs)
102
+ out.push(this.append(input));
103
+ return out;
104
+ },
105
+ });
106
+ const asAdmin = () => ({
107
+ kind: "LedgerAdmin",
108
+ [_capability]: "admin",
109
+ compat,
110
+ migrateOccurrenceV2() {
111
+ // copy => validate => switch; resumable & idempotent. The M2 host wraps
112
+ // the live DB + journal so every phase transition advances the SAME
113
+ // singleton journal state machine (prepared→copied→validated→switched).
114
+ const unrepresentable = compat.prepare().length;
115
+ const mig = migrateHost(db, compat);
116
+ m2Copy(mig);
117
+ const v = m2Validate(mig);
118
+ if (v.ok) {
119
+ m2Switch(mig);
120
+ fire("vector_cortex_compat_switch_committed", {
121
+ session: "all",
122
+ unrepresentable,
123
+ });
124
+ }
125
+ return { ok: v.ok, codes: v.codes };
126
+ },
127
+ });
128
+ return { reader: asReader, writer: asWriter, admin: asAdmin, close: () => db.close() };
129
+ }
130
+ /**
131
+ * Lossless legacy projection for a v2 occurrence: its identity envelope plus
132
+ * base64 source bytes. Returns null (→ journal `unrepresentable` marker) when the
133
+ * source bytes do not form valid UTF-8, which the legacy pipeline cannot round-trip
134
+ * losslessly — those rows are listed rather than silently coerced (MIG-DOWN-003).
135
+ */
136
+ function legacyProjectionOf(occ) {
137
+ const source = Buffer.from(occ.sourceBytes);
138
+ try {
139
+ new TextDecoder("utf-8", { fatal: true }).decode(source);
140
+ }
141
+ catch {
142
+ return null;
143
+ }
144
+ return JSON.stringify({
145
+ session: occ.session,
146
+ eventId: occ.eventId,
147
+ kind: occ.kind,
148
+ digest: occ.digest,
149
+ source: source.toString("base64"),
150
+ });
151
+ }
152
+ /** Build an M2Host over the live ledger DB + journal (for migrateOccurrenceV2). */
153
+ function migrateHost(db, journal) {
154
+ let staged = null;
155
+ return {
156
+ db,
157
+ phase: () => journalPhase(db),
158
+ journalActive: () => journal.active(),
159
+ journalRows: () => journalRowsOf(db),
160
+ writeStagedLegacy: (rows) => {
161
+ staged = rows;
162
+ journal.copied();
163
+ },
164
+ stagedLegacy: () => staged,
165
+ validateStaged: () => journal.validate(),
166
+ switchLegacy: () => journal.switched(),
167
+ };
168
+ }
169
+ /** Read the journaled occurrences as legacy-export rows. */
170
+ function journalRowsOf(db) {
171
+ const rows = db
172
+ .prepare(`SELECT session, seq, event_id, digest, kind, legacy_projection, unrepresentable
173
+ FROM compat_journal_v1 ORDER BY session ASC, seq ASC`)
174
+ .all();
175
+ return rows.map((r) => ({
176
+ session: r.session,
177
+ seq: BigInt(r.seq),
178
+ eventId: r.event_id,
179
+ digest: r.digest,
180
+ kind: r.kind,
181
+ legacyProjection: r.legacy_projection,
182
+ unrepresentable: r.unrepresentable === 1,
183
+ }));
184
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * vector-cortex/ledger/types.ts — EventV2 / EventCodec byte-authority contract
3
+ * types, validation result codes, and the registered EVT conformance ID range.
4
+ *
5
+ * Owned by VC1A (canonical byte events). Consumes only reviewer-accepted
6
+ * predecessor contracts and [common contracts](../../CONTRACTS.md §EventV2),
7
+ * which are NORMATIVE here. `originalBytes` and its SHA-256 digest are the byte
8
+ * authority; strict UTF-8 classification is never lossy; `canonicalNfc` is a
9
+ * DERIVED comparison/search key only (never identity, digest, or reconstruction).
10
+ *
11
+ * Pure type/schema definitions + a short pure digest/classify predicate runtime
12
+ * (validator codes). No network, no side effects (PREVENT-PI-004 / PREVENT-011).
13
+ */
14
+ /**
15
+ * Registered EVT conformance ID range (EVT-001..030). The acceptance test reads
16
+ * these rows from the v2 manifest and asserts each returns its manifest bytes or
17
+ * exactly its listed failure code. Mirrors CUT_IDS / M3_IDS in replay/types.ts.
18
+ * EVT-001..015 are owned by VC1A (codec/validator); EVT-016..030 are owned by
19
+ * VC1B (occurrence ledger, see ledger/store.ts).
20
+ */
21
+ export const EVT_IDS = [
22
+ "EVT-001",
23
+ "EVT-002",
24
+ "EVT-003",
25
+ "EVT-004",
26
+ "EVT-005",
27
+ "EVT-006",
28
+ "EVT-007",
29
+ "EVT-008",
30
+ "EVT-009",
31
+ "EVT-010",
32
+ "EVT-011",
33
+ "EVT-012",
34
+ "EVT-013",
35
+ "EVT-014",
36
+ "EVT-015",
37
+ "EVT-016",
38
+ "EVT-017",
39
+ "EVT-018",
40
+ "EVT-019",
41
+ "EVT-020",
42
+ "EVT-021",
43
+ "EVT-022",
44
+ "EVT-023",
45
+ "EVT-024",
46
+ "EVT-025",
47
+ "EVT-026",
48
+ "EVT-027",
49
+ "EVT-028",
50
+ "EVT-029",
51
+ "EVT-030",
52
+ ];
@@ -0,0 +1,117 @@
1
+ /**
2
+ * vector-cortex/ledger/validator.ts — EventV2 canonical validation (VC1A).
3
+ *
4
+ * Canonical ordering is `(sessionId, seq, eventId bytewise UTF-8)`: the sort is
5
+ * by the eventId's UTF-8 BYTES (unsigned) — NOT by JS string code-unit order —
6
+ * so multi-byte eventIds sort bytewise. Deterministic failure detection with a
7
+ * fixed priority order:
8
+ * 1. EVT_DIGEST_MISMATCH — sha256(originalBytes) !== bytesDigest
9
+ * 2. EVT_UTF8_TAG_INVALID — stored `utf8` discriminant contradicting a strict
10
+ * fatal re-classification of originalBytes
11
+ * 3. EVT_DUPLICATE_ID — duplicate (sessionId, seq, eventId) occurrence
12
+ *
13
+ * The unique failure injection requirement: flip ONE stored byte while retaining
14
+ * the SHA-256 signature — the digest recomputation diverges and the validator
15
+ * returns EVT_DIGEST_MISMATCH (never a lossy replacement text, which is not
16
+ * produced anywhere in this module).
17
+ *
18
+ * Pure/deterministic — no console, no network, no side effects (PREVENT-PI-004).
19
+ */
20
+ import nodeCrypto from "node:crypto";
21
+ import { classifyUtf8 } from "./event-codec.js";
22
+ /** Mode-A digest recompute (validator uses the codec's canonical digest rule). */
23
+ function digestOf(bytes) {
24
+ const hex = nodeCrypto.createHash("sha256").update(bytes).digest("hex");
25
+ return `sha256:${hex}`;
26
+ }
27
+ /** Bytewise UTF-8 comparator for eventId tiebreak (unsigned bytes, not code units). */
28
+ function compareEventIdBytes(a, b) {
29
+ const ab = Buffer.from(a, "utf8");
30
+ const bb = Buffer.from(b, "utf8");
31
+ const n = Math.min(ab.length, bb.length);
32
+ for (let i = 0; i < n; i++) {
33
+ if (ab[i] !== bb[i])
34
+ return ab[i] < bb[i] ? -1 : 1;
35
+ }
36
+ if (ab.length !== bb.length)
37
+ return ab.length < bb.length ? -1 : 1;
38
+ return 0;
39
+ }
40
+ /** Comparator: `(sessionId, seq, eventId bytewise UTF-8)`. */
41
+ export function compareEvents(a, b) {
42
+ if (a.sessionId !== b.sessionId)
43
+ return a.sessionId < b.sessionId ? -1 : 1;
44
+ if (a.seq !== b.seq)
45
+ return a.seq < b.seq ? -1 : 1;
46
+ return compareEventIdBytes(a.eventId, b.eventId);
47
+ }
48
+ /** Ascending (sessionId, seq, eventId-bytes) sort. Returns a new array. */
49
+ export function sortEvents(events) {
50
+ return [...events].sort(compareEvents);
51
+ }
52
+ /** Recompute the digest and compare to the stored one (authority corruption). */
53
+ function digestMismatch(event) {
54
+ return digestOf(event.originalBytes) !== event.bytesDigest;
55
+ }
56
+ /** The stored utf8 discriminant must match a strict fatal re-classification. */
57
+ function utf8TagMismatch(event) {
58
+ const cls = classifyUtf8(event.originalBytes);
59
+ const stored = event.utf8;
60
+ if (stored.valid !== cls.valid)
61
+ return true;
62
+ if (!stored.valid) {
63
+ // Both invalid: never compare decoded text; only the (valid, base64) shape.
64
+ return false;
65
+ }
66
+ if (!cls.valid)
67
+ return true; // unreachable given `stored.valid === cls.valid`
68
+ // Both valid: compare the strict-decoded TEXT (no NFC — the tag stores raw text).
69
+ return stored.text !== cls.text;
70
+ }
71
+ /**
72
+ * Validate a batch of events. Returns `{ok:true, ordered}` when every event
73
+ * passes digest + UTF-8-tag consistency and no duplicate occurrence exists;
74
+ * otherwise `{ok:false, codes}` with the failure codes in fixed priority order,
75
+ * deduplicated, and an explicit issue list for diagnostics.
76
+ */
77
+ export function validateEvents(events) {
78
+ const issues = [];
79
+ // Fixed priority order: DIGEST_MISMATCH, UTF8_TAG_INVALID, DUPLICATE_ID.
80
+ for (const e of events) {
81
+ if (digestMismatch(e)) {
82
+ issues.push({ code: "EVT_DIGEST_MISMATCH", sessionId: e.sessionId, seq: e.seq, eventId: e.eventId });
83
+ }
84
+ }
85
+ for (const e of events) {
86
+ if (utf8TagMismatch(e)) {
87
+ issues.push({ code: "EVT_UTF8_TAG_INVALID", sessionId: e.sessionId, seq: e.seq, eventId: e.eventId });
88
+ }
89
+ }
90
+ // Unambiguous (sessionId, seq, eventId) occurrence key (seq stringified — a
91
+ // bigint cannot be JSON-serialized directly).
92
+ const key = (e) => JSON.stringify([e.sessionId, e.seq.toString(), e.eventId]);
93
+ const seen = new Set();
94
+ for (const e of sortEvents(events)) {
95
+ const k = key(e);
96
+ if (seen.has(k)) {
97
+ issues.push({ code: "EVT_DUPLICATE_ID", sessionId: e.sessionId, seq: e.seq, eventId: e.eventId });
98
+ }
99
+ seen.add(k);
100
+ }
101
+ if (issues.length === 0) {
102
+ return { ok: true, ordered: sortEvents(events) };
103
+ }
104
+ // Surface the per-occurrence issues (real locators) alongside the deduped
105
+ // codes so the adapter/observability feed can name WHICH event failed.
106
+ return { ok: false, codes: dedupe(issues.map((i) => i.code)), issues };
107
+ }
108
+ function dedupe(codes) {
109
+ const out = [];
110
+ for (const c of codes) {
111
+ if (!out.includes(c))
112
+ out.push(c);
113
+ }
114
+ return out;
115
+ }
116
+ /** Export the canonical ordering comparator for consumers (replay etc.). */
117
+ export { compareEventIdBytes };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * vector-cortex/migrations/effective-cut-v2.ts — M3 copy/validate/switch
3
+ * migration (VC0B).
4
+ *
5
+ * Freezes the v2 effective replay cut: `min(boundarySafeSeq, committedSeq,
6
+ * capturedHighWater)` with pair retreat and anchor floor. The migration copies
7
+ * the candidate into the staged slot, validates the STAGED pointer against the
8
+ * minima and pair/anchor invariants (plus copy-match), and ONLY THEN atomically
9
+ * switches the active pointer. The three phases are exposed separately so a
10
+ * crash between validate and switch retains the OLD pointer and a restart
11
+ * resumes idempotently (the unique failure-injection contract).
12
+ *
13
+ * This is an internal developer seam: no dashboard/API change is necessary. It
14
+ * operates against the EXISTING host state via an injected M3Host interface —
15
+ * the future v2 compat-journal is owned by VC1B and is not introduced here.
16
+ */
17
+ import { computeEffectiveCutV2 } from "../replay/cut.js";
18
+ /** Migration failure codes (registered in M3-001..010 as conformance rows). */
19
+ export const M3_FAIL = {
20
+ MISSING_HOST: "M3_HOST_MISSING",
21
+ INVALID_MINIMA: "M3_MINIMA_VIOLATED",
22
+ PAIR_SPLIT: "M3_PAIR_SPLIT",
23
+ ANCHOR_CROSSED: "M3_ANCHOR_CROSSED",
24
+ COPY_MISMATCH: "M3_COPY_MISMATCH",
25
+ };
26
+ /** Pure effective-cut computation (min-of-three + retreat + floor). */
27
+ export function m3Compute(input) {
28
+ return computeEffectiveCutV2(input).cut.effectiveSeq;
29
+ }
30
+ /**
31
+ * Phase 1 — copy: compute the candidate effective cut and stage it via the host
32
+ * (without activating). Returns the staged seq.
33
+ */
34
+ export function m3Copy(host, input) {
35
+ const effective = m3Compute(input);
36
+ host.writeStaged(effective);
37
+ return effective;
38
+ }
39
+ /**
40
+ * Phase 2 — validate: the host's STAGED pointer must equal the freshly computed
41
+ * effective cut (copy-match), respect the minima, not split a pair, and never
42
+ * fall below the anchor floor. Failure codes:
43
+ * M3_HOST_MISSING — no staged pointer at all
44
+ * M3_COPY_MISMATCH — staged != freshly computed effective
45
+ * M3_MINIMA_VIOLATED — effective exceeds a source minimum (or requested)
46
+ * M3_PAIR_SPLIT — effective splits a tool call/result pair
47
+ * M3_ANCHOR_CROSSED — effective is below the anchor floor
48
+ */
49
+ export function m3Validate(host, input) {
50
+ const codes = [];
51
+ const { cut, retreats } = computeEffectiveCutV2(input);
52
+ const expected = cut.effectiveSeq;
53
+ const staged = host.stagedPointer();
54
+ if (staged === null) {
55
+ codes.push(M3_FAIL.MISSING_HOST);
56
+ }
57
+ else {
58
+ if (staged !== expected)
59
+ codes.push(M3_FAIL.COPY_MISMATCH);
60
+ if (staged > input.boundarySafeSeq ||
61
+ staged > host.committedSeq() ||
62
+ staged > input.capturedHighWater ||
63
+ staged > input.requestedSeq) {
64
+ codes.push(M3_FAIL.INVALID_MINIMA);
65
+ }
66
+ for (const p of input.pairs) {
67
+ if (p.callSeq <= staged && staged < p.resultSeq) {
68
+ codes.push(M3_FAIL.PAIR_SPLIT);
69
+ break;
70
+ }
71
+ }
72
+ if (staged < input.anchorFloor)
73
+ codes.push(M3_FAIL.ANCHOR_CROSSED);
74
+ }
75
+ return {
76
+ ok: codes.length === 0,
77
+ codes,
78
+ retreats,
79
+ effectiveSeq: expected,
80
+ };
81
+ }
82
+ /**
83
+ * Phase 3 — switch: atomic activation of the staged pointer. Only call after
84
+ * m3Validate reports ok; the host performs the durable pointer flip.
85
+ */
86
+ export function m3Switch(host, input) {
87
+ const effective = m3Compute(input);
88
+ host.switchPointer(effective);
89
+ return effective;
90
+ }
91
+ /**
92
+ * Run the full copy-validate-switch migration against a host. On validation
93
+ * failure the switch is NOT performed and the host retains its previous pointer
94
+ * (crash-safety / copy-validate-switch).
95
+ */
96
+ export function migrateEffectiveCutV2(host, input) {
97
+ m3Copy(host, input);
98
+ const v = m3Validate(host, input);
99
+ if (!v.ok)
100
+ return { ok: false, codes: v.codes, effectiveSeq: v.effectiveSeq };
101
+ m3Switch(host, input);
102
+ return { ok: true, codes: [], effectiveSeq: v.effectiveSeq };
103
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * vector-cortex/migrations/minhash-v2.ts — M4 MinHashV2 copy/validate/switch
3
+ * migration (VC1C).
4
+ *
5
+ * M4 versions the L1 dedup signature store: v2 signatures/buckets are written
6
+ * BESIDE v1, backfilled by checkpoint id, verified, and only then switched as
7
+ * the active version. It follows the same copy/validate/switch + resume
8
+ * contract as M2 but for the minhash index:
9
+ *
10
+ * - batch: writes v2 signatures + buckets for a set of checkpoint IDs, each
11
+ * tagged with the frozen version 2; never touches v1 rows.
12
+ * - backfill: resumable by checkpoint id — an interrupted run resumes without
13
+ * duplicate v2 signatures or active-pointer drift.
14
+ * - verify: counts per checkpoint match v1 and every v2 digest re-hashes.
15
+ * - switch: atomically flips the ACTIVE VERSION pointer from v1 to v2; until
16
+ * a verified switch, v1 remains active (interruption keeps old authority).
17
+ *
18
+ * Cross-version compare is REJECTED: a mixed v1-v2 similarity/bucket query
19
+ * returns `MINHASH_VERSION_MISMATCH` (never compares v1/v2 signatures).
20
+ *
21
+ * Pure logic over an injected M4Host (deterministic + testable; no console).
22
+ * PREVENT-002/011/PI-004 honored.
23
+ */
24
+ import { createHash } from "node:crypto";
25
+ import { MINHASH_VERSION, minhashV2Signature, encodeSignatureV2, } from "../../dedup/l1-minhash-v2.js";
26
+ import { lshBandsV2, BANDS_V2, SIGNATURE_BYTES_EXPECTED_V2, } from "../../dedup/l1-lsh-v2.js";
27
+ import { createConformanceReporter, } from "../conformance/emit.js";
28
+ /** M4 failure codes. */
29
+ export const M4_FAIL = {
30
+ VERSION_MISMATCH: "MINHASH_VERSION_MISMATCH",
31
+ BACKFILL_PARTIAL: "M4_BACKFILL_PARTIAL",
32
+ COUNT_MISMATCH: "M4_COUNT_MISMATCH",
33
+ DIGEST_MISMATCH: "M4_DIGEST_MISMATCH",
34
+ };
35
+ /** Reject a cross-version compare. Returns the frozen mismatch code. */
36
+ export function crossVersionError() {
37
+ return M4_FAIL.VERSION_MISMATCH;
38
+ }
39
+ /**
40
+ * Compute v2 signature + buckets for one checkpoint (the authoritative bytes).
41
+ * Pure and cross-language deterministic.
42
+ */
43
+ export function computeV2Row(host, checkpointId) {
44
+ const sessionId = host.sessionOf(checkpointId);
45
+ const source = host.sourceOf(checkpointId);
46
+ const sig = minhashV2Signature(source);
47
+ const bytes = encodeSignatureV2(sig);
48
+ const buckets = lshBandsV2(new Uint8Array(bytes), sessionId);
49
+ return {
50
+ checkpointId,
51
+ sessionId,
52
+ version: MINHASH_VERSION,
53
+ signatureBytes: new Uint8Array(bytes),
54
+ digest: `sha256:${sha256Hex(bytes)}`,
55
+ buckets,
56
+ };
57
+ }
58
+ function sha256Hex(bytes) {
59
+ return createHash("sha256").update(bytes).digest("hex");
60
+ }
61
+ /**
62
+ * Backfill v2 signatures/buckets for all v1 checkpoint ids (resumable): rows
63
+ * already persisted with matching v2 digest/version are left untouched (no
64
+ * duplicates), and the active pointer stays at v1 until a verified switch.
65
+ * Returns the rows written this call (delta), never duplicates.
66
+ *
67
+ * An EXISTING row is re-computed and digest-verified before it is skipped; if
68
+ * the persisted row is corrupt (wrong version, wrong length, or a digest that
69
+ * no longer re-hashes its signatureBytes — e.g. a crash mid-write that left a
70
+ * truncated/bad row for an already-present id), it is recomputed and rewritten
71
+ * so corruption self-heals instead of being frozen by a later `m4Verify` failure.
72
+ */
73
+ export function m4Backfill(host, reporter = createConformanceReporter()) {
74
+ const wanted = host.v1CheckpointIds();
75
+ const existing = new Map(host.storedV2().map((r) => [r.checkpointId, r]));
76
+ const writes = [];
77
+ for (const id of wanted) {
78
+ const row = computeV2Row(host, id);
79
+ const stored = existing.get(id);
80
+ if (stored !== undefined && v2RowMatches(stored, row))
81
+ continue; // healthy, no dup
82
+ writes.push(row); // absent, or stored row is corrupt -> (re)compute authoritative bytes
83
+ }
84
+ if (writes.length > 0) {
85
+ host.putV2(writes);
86
+ reporter.backfilled({
87
+ written: writes.length,
88
+ checkpointIds: writes.map((r) => r.checkpointId),
89
+ });
90
+ }
91
+ return writes;
92
+ }
93
+ /** True when a persisted row already matches the authoritative recompute. */
94
+ function v2RowMatches(stored, fresh) {
95
+ return (stored.version === MINHASH_VERSION &&
96
+ stored.checkpointId === fresh.checkpointId &&
97
+ stored.sessionId === fresh.sessionId &&
98
+ stored.signatureBytes.length === fresh.signatureBytes.length &&
99
+ stored.digest === fresh.digest &&
100
+ bucketsMatch(stored));
101
+ }
102
+ /**
103
+ * Re-derive the 64 bucket keys from the row's signatureBytes + sessionId and
104
+ * compare every value to the stored buckets. This closes a defensive
105
+ * verification gap: a persisted row whose signatureBytes + digest are correct
106
+ * but whose bucket VALUES were written wrong (a writer defect, or a partial
107
+ * bucket write that still held 64 entries) would otherwise pass digest+count
108
+ * checks and be treated as healthy — yet the buckets are exactly the data the
109
+ * live LSH-dedup path queries. Buckets are deterministic from sigBytes +
110
+ * sessionId, so a mismatch is always a real storage/author defect.
111
+ */
112
+ function bucketsMatch(row) {
113
+ // A wrong-length signature can't be banded (lshBandsV2 throws); the caller
114
+ // already reports it as a count/version mismatch, so fail closed here too.
115
+ if (row.signatureBytes.length !== SIGNATURE_BYTES_EXPECTED_V2)
116
+ return false;
117
+ if (row.buckets.length !== BANDS_V2)
118
+ return false;
119
+ const expected = lshBandsV2(new Uint8Array(row.signatureBytes), row.sessionId);
120
+ for (let i = 0; i < BANDS_V2; i++) {
121
+ if (expected[i] !== row.buckets[i])
122
+ return false;
123
+ }
124
+ return true;
125
+ }
126
+ /**
127
+ * Verify the backfilled v2 index: every v1 checkpoint has exactly one v2 row
128
+ * (reset -> resumable partial counts), each stored row's digest re-hashes, and
129
+ * each row has all 64 buckets. Never mutates.
130
+ */
131
+ export function m4Verify(host) {
132
+ const codes = [];
133
+ const wanted = new Set(host.v1CheckpointIds());
134
+ const stored = host.storedV2();
135
+ const counts = new Map();
136
+ for (const r of stored) {
137
+ counts.set(r.checkpointId, (counts.get(r.checkpointId) ?? 0) + 1);
138
+ if (r.version !== MINHASH_VERSION)
139
+ codes.push(M4_FAIL.VERSION_MISMATCH);
140
+ if (r.signatureBytes.length !== 2048)
141
+ codes.push(M4_FAIL.COUNT_MISMATCH);
142
+ if (r.buckets.length !== BANDS_V2)
143
+ codes.push(M4_FAIL.COUNT_MISMATCH);
144
+ if (r.digest !== `sha256:${sha256Hex(r.signatureBytes)}`) {
145
+ codes.push(M4_FAIL.DIGEST_MISMATCH);
146
+ }
147
+ // Bucket VALUE verification: re-derive all 64 keys from signatureBytes +
148
+ // sessionId and compare them. Digest + count alone would let a row with
149
+ // correct sigBytes/digest but wrong bucket values pass; the buckets are the
150
+ // data the live LSH-dedup path queries, so a drifts here is a real defect.
151
+ if (!bucketsMatch(r))
152
+ codes.push(M4_FAIL.COUNT_MISMATCH);
153
+ }
154
+ // Every v1 checkpoint backfilled exactly once (count parity, no dup rows).
155
+ for (const id of wanted) {
156
+ const c = counts.get(id) ?? 0;
157
+ if (c === 0)
158
+ codes.push(M4_FAIL.BACKFILL_PARTIAL);
159
+ if (c > 1)
160
+ codes.push(M4_FAIL.COUNT_MISMATCH);
161
+ }
162
+ // No orphan v2 rows without a v1 checkpoint.
163
+ for (const id of counts.keys()) {
164
+ if (!wanted.has(id))
165
+ codes.push(M4_FAIL.COUNT_MISMATCH);
166
+ }
167
+ const ok = codes.length === 0;
168
+ return { ok, codes: dedupe(codes) };
169
+ }
170
+ /**
171
+ * Switch the active version to v2. Only call after `m4Verify` reports ok; a
172
+ * crash/return before switch leaves v1 active (interruption keeps old
173
+ * authority, resumable idempotently).
174
+ */
175
+ export function m4Switch(host) {
176
+ host.switchToV2();
177
+ }
178
+ /** Full M4 lifecycle: backfill -> verify -> switch; returns verify result. */
179
+ export function migrateMinhashV2(host, reporter = createConformanceReporter()) {
180
+ m4Backfill(host, reporter);
181
+ const v = m4Verify(host);
182
+ if (v.ok)
183
+ m4Switch(host);
184
+ return v;
185
+ }
186
+ /** Registered M4 conformance ID range (M4-001..008 + named M4-*). */
187
+ export const M4_IDS = [
188
+ "M4-001",
189
+ "M4-002",
190
+ "M4-003",
191
+ "M4-004",
192
+ "M4-005",
193
+ "M4-006",
194
+ "M4-007",
195
+ "M4-008",
196
+ ];
197
+ /** Registered named M4 conformance IDs. */
198
+ export const M4_NAMED = [
199
+ "M4-HIGHBIT-001",
200
+ "M4-VERSION-002",
201
+ "M4-RESUME-003",
202
+ ];
203
+ function dedupe(codes) {
204
+ const out = [];
205
+ for (const c of codes)
206
+ if (!out.includes(c))
207
+ out.push(c);
208
+ return out;
209
+ }