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,95 @@
1
+ /**
2
+ * conformance/triadB-reader.ts — TRIAD-B independent exact reader (VC1C).
3
+ *
4
+ * Deliberately shares NO code and NO cached-seed instance with the runner
5
+ * modules (l1-minhash-v2.ts / l1-lsh-v2.ts). It re-derives the minhash-v2
6
+ * signature and 64 bucket keys from first principles, using only the PUBLISHED
7
+ * (a,b) seed pairs passed in (read by the caller from
8
+ * conformance/.../minhash/seeds-v2.json — the table authored independently by
9
+ * scripts/gen-fixtures/minhash.mjs, the same process that produced the frozen
10
+ * fixture bytes). Because B shares no subroutine with the runner, a systematic
11
+ * error in the runner's splitmix64 / seed-derivation / signature arithmetic
12
+ * cannot satisfy both triad A and triad B.
13
+ *
14
+ * Pure compute, no deps, no storage, no network (PREVENT-PI-004 / PREVENT-011).
15
+ */
16
+ import { createHash } from "node:crypto";
17
+ const MASK64 = 0xffffffffffffffffn;
18
+ const P = (1n << 61n) - 1n; // 2^61 - 1
19
+ const SLOTS = 256;
20
+ const U64 = 8;
21
+ const BANDS = 64;
22
+ /** 64-bit FNV-1a over UTF-8 bytes or a raw byte buffer (own, independent loop). */
23
+ function fnv1a64(input) {
24
+ let h = 0xcbf29ce484222325n;
25
+ for (const b of Buffer.from(input)) {
26
+ h = ((h ^ BigInt(b)) * 0x100000001b3n) & MASK64;
27
+ }
28
+ return h;
29
+ }
30
+ /**
31
+ * Re-derive the 2048-byte minhash-v2 signature and (optionally) the 64 LSH
32
+ * bucket keys for `text` within `session`, from published seed pairs only —
33
+ * never the runner module.
34
+ */
35
+ export function independentReaderV2(text, session, seedPairs, withBuckets) {
36
+ // Normalization: ANSI-strip, NFC, case-fold, CRLF->LF, whitespace collapse,
37
+ // 32K cap — mirrored from the frozen VC1C spec.
38
+ let norm = (text || "")
39
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
40
+ .normalize("NFC")
41
+ .toLowerCase()
42
+ .replace(/\r\n?/g, "\n")
43
+ .replace(/\s+/g, " ")
44
+ .trim();
45
+ if (norm.length > 32768)
46
+ norm = norm.slice(0, 32768);
47
+ // Five-code-point shingles (deduped, capped at 50_000).
48
+ const grams = [];
49
+ if (norm.length > 0) {
50
+ const cps = Array.from(norm);
51
+ const seen = new Set();
52
+ const short = cps.length < 5;
53
+ for (let i = 0; i < (short ? 1 : cps.length - 4); i++) {
54
+ const h = short ? fnv1a64(norm) : fnv1a64(cps.slice(i, i + 5).join(""));
55
+ if (!seen.has(h)) {
56
+ seen.add(h);
57
+ grams.push(h);
58
+ }
59
+ if (!short && seen.size >= 50000)
60
+ break;
61
+ }
62
+ }
63
+ // Signature: slot i = min over shingles of (a_i*x + b_i) mod p (exact BigInt).
64
+ const sig = new Array(SLOTS).fill(P);
65
+ for (let i = 0; i < SLOTS && grams.length > 0; i++) {
66
+ const pair = seedPairs[i];
67
+ const a = BigInt(pair.a);
68
+ const b = BigInt(pair.b);
69
+ let min = P;
70
+ for (const x of grams) {
71
+ const h = (a * x + b) % P;
72
+ if (h < min)
73
+ min = h;
74
+ }
75
+ sig[i] = min;
76
+ }
77
+ // Own 256 x u64 little-endian encoder.
78
+ const buf = Buffer.allocUnsafe(SLOTS * U64);
79
+ for (let i = 0; i < SLOTS; i++)
80
+ buf.writeBigUInt64LE(sig[i], i * U64);
81
+ const bytes = new Uint8Array(buf);
82
+ const digest = createHash("sha256").update(bytes).digest("hex");
83
+ // 64 buckets: FNV-1a over `<session>|v2|` + each 32-byte band (LE hex).
84
+ const buckets = [];
85
+ if (withBuckets) {
86
+ const prefix = Buffer.from(`${session}|v2|`, "utf8");
87
+ for (let band = 0; band < BANDS; band++) {
88
+ const bandBytes = Buffer.from(bytes.slice(band * 32, band * 32 + 32));
89
+ const out = Buffer.allocUnsafe(U64);
90
+ out.writeBigUInt64LE(fnv1a64(Buffer.concat([prefix, bandBytes])), 0);
91
+ buckets.push(`b${band}:${out.toString("hex")}`);
92
+ }
93
+ }
94
+ return { bytes, digest, buckets };
95
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * vector-cortex/eval/annotations.ts — redaction metadata + JSONL serialization.
3
+ *
4
+ * Payload bytes, prompts, and exact ledger text are NEVER serialized. Before a
5
+ * JSONL line is written, each such field is replaced by a SHA-256 digest + byte
6
+ * count + redaction kind (EVAL-REDACT-002: prompt bytes never appear in JSONL).
7
+ * The redaction journal is itself the AnnotationV1 output.
8
+ *
9
+ * Uses node:crypto for the digest (pure local math, PREVENT-PI-004 safe).
10
+ */
11
+ import { createHash } from "node:crypto";
12
+ /** SHA-256 over bytes, unpadded base64 (conformance canonical binary form). */
13
+ export function digestBytes(bytes) {
14
+ return createHash("sha256").update(bytes).digest("base64");
15
+ }
16
+ /** Replace every raw content field with its digest/count metadata. */
17
+ export function redactAnnotation(itemId, contents) {
18
+ const redactions = contents.map((c) => ({
19
+ field: c.field,
20
+ digest: digestBytes(c.bytes),
21
+ bytes: c.bytes.byteLength,
22
+ kind: c.kind,
23
+ }));
24
+ return {
25
+ itemId,
26
+ redactions,
27
+ redactedCount: redactions.length,
28
+ };
29
+ }
30
+ /**
31
+ * Serialize one set of raw contents to a single redacted JSONL line. Returns
32
+ * the annotation with its digest metadata and the JSONL text; the raw bytes
33
+ * never appear in the returned string.
34
+ */
35
+ export function serializeRedactedJsonl(itemId, contents) {
36
+ const annotation = redactAnnotation(itemId, contents);
37
+ return {
38
+ annotation,
39
+ jsonl: `${JSON.stringify(annotation)}\n`,
40
+ };
41
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * vector-cortex/eval/metrics.ts — canonical metric ordering + fixed histogram.
3
+ *
4
+ * 1. Stable sort rows by `(session, seq, event)` — session string byte order,
5
+ * then integer seq ascending, then event-name byte order as tiebreak
6
+ * (EVAL-ORDER-003: equal seq rows use event-name order).
7
+ * 2. Bucket latency values at exactly 1/5/10/25/50/100/250ms with inclusive
8
+ * boundaries (EVAL-BUCKET-001); values past 250ms kept in a separate
9
+ * overflow bucket. Histogram totals are permutation-stable.
10
+ *
11
+ * Pure/no side effects (PREVENT-PI-004).
12
+ */
13
+ import { LATENCY_BUCKETS } from "./types.js";
14
+ /** Inclusive-boundary bucket edges, with a sentinel past the largest edge. */
15
+ const EDGES = LATENCY_BUCKETS;
16
+ const OVERFLOW_INDEX = EDGES.length;
17
+ /** Histogram cell counts aligned to EDGES, plus a trailing overflow cell. */
18
+ export function emptyHistogram() {
19
+ return new Array(EDGES.length + 1).fill(0);
20
+ }
21
+ /** True when a sample carries a latency (`unit === "ms"`) datum. */
22
+ export function isLatencySample(m) {
23
+ return m.unit === "ms";
24
+ }
25
+ /** Bucket a latency ms value into a cell index (0..EDGES.length, last=overflow). */
26
+ export function bucketIndex(value) {
27
+ if (!Number.isFinite(value) || value < 0)
28
+ return OVERFLOW_INDEX;
29
+ for (let i = 0; i < EDGES.length; i++) {
30
+ if (value <= EDGES[i])
31
+ return i; // inclusive upper boundary
32
+ }
33
+ return OVERFLOW_INDEX;
34
+ }
35
+ /**
36
+ * Bucket latency sample values across all rows into the fixed histogram.
37
+ * Overflow is kept separate (counted in `overflow` and the trailing cell).
38
+ */
39
+ export function bucketHistogram(rows) {
40
+ const cells = emptyHistogram();
41
+ let overflow = 0;
42
+ for (const m of rows) {
43
+ if (!isLatencySample(m))
44
+ continue;
45
+ const idx = bucketIndex(m.value);
46
+ cells[idx] += 1;
47
+ if (idx === EDGES.length)
48
+ overflow += 1;
49
+ }
50
+ return {
51
+ cells,
52
+ edges: [...EDGES],
53
+ overflow,
54
+ total: cells.reduce((a, b) => a + b, 0),
55
+ };
56
+ }
57
+ /**
58
+ * Stable sort MetricEventV1 rows by `(session, seq, event)`:
59
+ * - session: UTF-8 / JS string byte order;
60
+ * - seq: ascending numeric;
61
+ * - event: ascending string byte order (tiebreak for equal seq).
62
+ * Equal content stays separate occurrences (never deduplicated here).
63
+ */
64
+ export function sortCanonical(rows) {
65
+ const out = [...rows];
66
+ out.sort((a, b) => {
67
+ const s = a.session < b.session ? -1 : a.session > b.session ? 1 : 0;
68
+ if (s !== 0)
69
+ return s;
70
+ if (a.seq !== b.seq)
71
+ return a.seq - b.seq;
72
+ return a.event < b.event ? -1 : a.event > b.event ? 1 : 0;
73
+ });
74
+ return out;
75
+ }
76
+ /**
77
+ * Assisted sort plus histogram, returning both under one result.
78
+ * The observer (mode A) calls this before JSONL serialization.
79
+ */
80
+ export function buildMetrics(rows) {
81
+ return {
82
+ rows: sortCanonical(rows),
83
+ histogram: bucketHistogram(rows),
84
+ rejects: [],
85
+ };
86
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * vector-cortex/eval/observer.ts — VC0A structured observer (triad mode A).
3
+ *
4
+ * Connects to the evaluation reader: accepts MetricEventV1 samples, maintains
5
+ * the canonical `(session, seq, event)` ordered set plus the fixed latency
6
+ * histogram, and emits two structured local events:
7
+ * - vector_cortex_eval_sample_recorded (one per accepted sample)
8
+ * - vector_cortex_eval_redaction_rejected (when a redaction fails)
9
+ *
10
+ * Mode C (observer absent) is the responsibility of the caller: no observes(),
11
+ * no writes, byte-identical to the predecessor. This module never touches the
12
+ * network (PREVENT-PI-004) — it only manipulates in-memory rows and emits
13
+ * structured log lines through an injected logging callback so it stays
14
+ * pi-agnostic and deterministically testable.
15
+ */
16
+ import { buildMetrics, sortCanonical } from "./metrics.js";
17
+ /**
18
+ * Build a structured observer (mode A). Best-effort: emitting never throws
19
+ * into the extension path (non-fatal observability).
20
+ */
21
+ export function createEvalObserver(opts) {
22
+ const rows = [];
23
+ function emit(event, fields) {
24
+ try {
25
+ opts.emit(event, fields);
26
+ }
27
+ catch {
28
+ /* non-fatal observability — never break the agent loop */
29
+ }
30
+ }
31
+ return {
32
+ record(sample) {
33
+ rows.push(sample);
34
+ if (opts.persist) {
35
+ try {
36
+ opts.persist(sample);
37
+ }
38
+ catch {
39
+ /* non-fatal observability */
40
+ }
41
+ }
42
+ emit("vector_cortex_eval_sample_recorded", {
43
+ session: sample.session,
44
+ seq: sample.seq,
45
+ event: sample.event,
46
+ unit: sample.unit,
47
+ mode: sample.mode,
48
+ });
49
+ },
50
+ rejectRedaction(field, reason) {
51
+ emit("vector_cortex_eval_redaction_rejected", { field, reason });
52
+ },
53
+ rows() {
54
+ return sortCanonical(rows);
55
+ },
56
+ };
57
+ }
58
+ /** Aggregate metrics result from an observer's rows (canonical + histogram). */
59
+ export function observerMetrics(result) {
60
+ return buildMetrics(result.rows());
61
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * vector-cortex/eval/persist.ts — redacted eval JSONL persistence (VC0A).
3
+ *
4
+ * Bridges the in-memory observer (mode A) and the dashboard reader (separate
5
+ * process) via a disk JSONL under the state dir. Writes are append-only and
6
+ * hold only MetricEventV1 metric rows plus redaction metadata — never payloads,
7
+ * prompts, or exact ledger text (EVAL-REDACT-002). Best-effort and non-fatal
8
+ * (PREVENT-PI-004: local filesystem only; no network).
9
+ *
10
+ * PREVENT-011: no `any` type.
11
+ */
12
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ /** Location of the redacted evaluation JSONL under a state dir. */
15
+ export function evalJsonlPath(stateDir) {
16
+ return join(stateDir, "vector-cortex", "evaluation.jsonl");
17
+ }
18
+ /** Append one redacted metric row as a canonical JSON line (append-only). */
19
+ export function appendEvalRow(stateDir, row) {
20
+ try {
21
+ const path = evalJsonlPath(stateDir);
22
+ mkdirSync(join(stateDir, "vector-cortex"), { recursive: true, mode: 0o700 });
23
+ // PER_EVAL_PERMISSION: 0600 file inherits state-dir permission policy.
24
+ writeFileSync(path, row.map((r) => `${JSON.stringify(r)}\n`).join(""), {
25
+ flag: "a",
26
+ mode: 0o600,
27
+ });
28
+ // guardrails-allow PREVENT-PI-004: local filesystem eval JSONL (no network)
29
+ }
30
+ catch {
31
+ /* non-fatal observability — never break the agent loop */
32
+ }
33
+ }
34
+ /**
35
+ * Read every redacted metric row from the JSONL. Malformed or truncated final
36
+ * lines are skipped (callers surface EVAL_JSONL_TRUNCATED); the caller decides
37
+ * whether that is fatal. Returns an empty array when the file is absent.
38
+ */
39
+ export function readEvalRows(stateDir) {
40
+ const out = [];
41
+ let raw;
42
+ try {
43
+ raw = readFileSync(evalJsonlPath(stateDir), "utf8");
44
+ }
45
+ catch {
46
+ return out;
47
+ }
48
+ for (const line of raw.split("\n")) {
49
+ if (line.trim().length === 0)
50
+ continue;
51
+ try {
52
+ const parsed = JSON.parse(line);
53
+ if (typeof parsed.session === "string" &&
54
+ typeof parsed.seq === "number" &&
55
+ typeof parsed.event === "string" &&
56
+ typeof parsed.value === "number" &&
57
+ typeof parsed.unit === "string" &&
58
+ (parsed.mode === "A" || parsed.mode === "B" || parsed.mode === "C")) {
59
+ out.push(parsed);
60
+ }
61
+ }
62
+ catch {
63
+ /* malformed line — skip; EVAL_JSONL_TRUNCATED surfaced by caller */
64
+ }
65
+ }
66
+ return out;
67
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * vector-cortex/eval/reader.ts — evaluation reader (aggregate-only).
3
+ *
4
+ * Reader-only capability: `summarizeEvalRows` collapses a set of MetricEventV1
5
+ * rows into an aggregate summary for the dashboard `GET /api/vector-cortex/
6
+ * evaluation` endpoint. No writable capability — the observer writes via its
7
+ * own path; this reader never mutates. Predecessor (flag-off / mode C) emits
8
+ * an empty summary byte-identical to "no data".
9
+ *
10
+ * Pure + local, zero network (PREVENT-PI-004).
11
+ */
12
+ import { buildMetrics } from "./metrics.js";
13
+ /** Aggregate rows into a reader-only summary (histogram + per-mode counts). */
14
+ export function summarizeEvalRows(rows) {
15
+ const byMode = { A: 0, B: 0, C: 0 };
16
+ for (const r of rows)
17
+ byMode[r.mode] += 1;
18
+ const metrics = buildMetrics(rows);
19
+ return {
20
+ samples: rows.length,
21
+ byMode,
22
+ histogram: {
23
+ edges: metrics.histogram.edges,
24
+ cells: metrics.histogram.cells,
25
+ overflow: metrics.histogram.overflow,
26
+ total: metrics.histogram.total,
27
+ },
28
+ };
29
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * vector-cortex/eval/types.ts — MetricEventV1 / AnnotationV1 contract types
3
+ * and the registered EVAL conformance ID range (EVAL-001..010).
4
+ *
5
+ * Owned by VC0A (baseline observability). Consumes reviewer-accepted predecessor
6
+ * contracts and common contracts only. These are pure type/schema definitions —
7
+ * no runtime network, no side effects (PREVENT-PI-004 / PREVENT-011).
8
+ */
9
+ /** Units the evaluator understands. Unknown units reject as EVAL_UNIT_UNKNOWN. */
10
+ export const UNITS = ["ms", "bytes", "count", "ratio"];
11
+ /**
12
+ * Histogram bucket edges for latency, in milliseconds, inclusive on both
13
+ * boundaries (EVAL-BUCKET-001): values at exactly 1ms and 250ms land in
14
+ * those buckets. Overflow (value > largest edge) is kept separate.
15
+ */
16
+ export const LATENCY_BUCKETS = [1, 5, 10, 25, 50, 100, 250];
17
+ /**
18
+ * Registered EVAL conformance ID range (EVAL-001..010). The acceptance test
19
+ * reads these rows from the v2 manifest and asserts their expected bytes/results.
20
+ * Registered BEFORE evaluation logic so the manifest/fixture seam is stable.
21
+ */
22
+ export const EVAL_IDS = [
23
+ "EVAL-001",
24
+ "EVAL-002",
25
+ "EVAL-003",
26
+ "EVAL-004",
27
+ "EVAL-005",
28
+ "EVAL-006",
29
+ "EVAL-007",
30
+ "EVAL-008",
31
+ "EVAL-009",
32
+ "EVAL-010",
33
+ ];
@@ -0,0 +1,55 @@
1
+ /**
2
+ * vector-cortex/ledger/adapter.ts — EventV2 ledger adapter (VC1A, task 5).
3
+ *
4
+ * A thin, pi-agnostic adapter combining the Mode-A codec, the canonical
5
+ * validator, and the VC1A emit seam so `vector_cortex_event_decoded` and
6
+ * `vector_cortex_event_validation_failed` are emitted by REAL code paths (not a
7
+ * hand-invoked reporter), gated by `VC1A_ENABLED()` inside `createLedgerReporter`.
8
+ *
9
+ * The live producer (wiring this into `extensions/mega-compact.ts` /
10
+ * `src/engine.ts`) is deferred to VC1C/VC1B (mirroring VC0B-I08); this sprint
11
+ * ships the seam + adapter as the clean single surface, with the acceptance
12
+ * aggregator exercising the real gated-emission path.
13
+ *
14
+ * Pure orchestration — no storage, no network, no side effects (PREVENT-PI-004).
15
+ */
16
+ import { createEventCodec } from "./event-codec.js";
17
+ import { validateEvents } from "./validator.js";
18
+ import { createLedgerReporter } from "./emit.js";
19
+ /** Build the ledger adapter over an injected emit callback (optional). */
20
+ export function createLedgerAdapter(emit) {
21
+ const codec = createEventCodec();
22
+ const reporter = createLedgerReporter(emit);
23
+ return {
24
+ encode: (input) => codec.encode(input),
25
+ decode(event) {
26
+ const bytes = codec.decode(event);
27
+ reporter.eventDecoded({
28
+ session: event.sessionId,
29
+ seq: event.seq.toString(),
30
+ eventId: event.eventId,
31
+ bytes: bytes.length,
32
+ utf8Valid: event.utf8.valid,
33
+ });
34
+ return bytes;
35
+ },
36
+ validate(events) {
37
+ const result = validateEvents(events);
38
+ if (!result.ok) {
39
+ // Emit one event per failing occurrence with its REAL locator — a
40
+ // consumer of the feed can identify WHICH event failed (VC1A-I05).
41
+ for (const issue of result.issues) {
42
+ reporter.validationFailed({
43
+ session: issue.sessionId,
44
+ seq: issue.seq.toString(),
45
+ eventId: issue.eventId,
46
+ code: issue.code,
47
+ });
48
+ }
49
+ }
50
+ return result;
51
+ },
52
+ codec,
53
+ reporter,
54
+ };
55
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * vector-cortex/ledger/compat-journal.ts — CompatJournalV1 (VC1B, task 4).
3
+ *
4
+ * The downgrade-safety journal. From v2 activation every accepted v2 append
5
+ * atomically appends a record holding the original bytes, IDs, and a legacy
6
+ * projection or an explicit `unrepresentable` marker. The journal tracks its own
7
+ * lifetime state (prepared → copied → validated → switched) so the M2
8
+ * downgrade is resumable: a crash mid-journal never corrupts v2, and a restart
9
+ * resumes idempotently (the unique failure-injection contract).
10
+ *
11
+ * `active()` is true once ANY v2 append has been journaled — an old binary may
12
+ * only open the exported legacy copy, never v2 directly, once the journal is
13
+ * active (CONTRACTS §Store). Unrepresentable rows (e.g. invalid UTF-8 bytes have
14
+ * no lossless legacy projection) are listed on prepare and carried through the
15
+ * copy/validate/switch lifecycle.
16
+ *
17
+ * Durable in the same isolated occurrence-v2 SQLite DB as the ledger rows
18
+ * (append-only provenance: journal records are never UPDATE'd; only the
19
+ * singleton state row advances phase). PREVENT-002 parameterized; PREVENT-011
20
+ * no `any`; PREVENT-PI-004 local filesystem only; no console.log.
21
+ */
22
+ import { createHash } from "node:crypto";
23
+ /** Downgrade-phase validation failure codes (registered as conformance rows). */
24
+ export const MIG_DOWN_FAIL = {
25
+ NOT_ACTIVE: "MIG_DOWN_NOT_ACTIVE",
26
+ PHASE_UNREACHED: "MIG_DOWN_PHASE_UNREACHED",
27
+ SEQ_REGRESSION: "MIG_DOWN_SEQ_REGRESSION",
28
+ DIGEST_MISMATCH: "MIG_DOWN_DIGEST_MISMATCH",
29
+ SWITCH_UNSAFE: "MIG_DOWN_SWITCH_UNSAFE",
30
+ };
31
+ const SCHEMA = `
32
+ CREATE TABLE IF NOT EXISTS compat_journal_v1 (
33
+ session TEXT NOT NULL,
34
+ seq INTEGER NOT NULL,
35
+ event_id TEXT NOT NULL,
36
+ digest TEXT NOT NULL,
37
+ kind TEXT NOT NULL,
38
+ legacy_projection TEXT,
39
+ unrepresentable INTEGER NOT NULL DEFAULT 0,
40
+ PRIMARY KEY (session, seq)
41
+ ) STRICT;
42
+
43
+ CREATE TABLE IF NOT EXISTS compat_journal_state (
44
+ id INTEGER PRIMARY KEY CHECK (id = 1),
45
+ phase TEXT NOT NULL
46
+ ) STRICT;
47
+ `;
48
+ /** Ensure the journal tables exist and seed the singleton phase row. */
49
+ export function initCompatJournal(db) {
50
+ db.exec(SCHEMA);
51
+ db.prepare(`INSERT OR IGNORE INTO compat_journal_state (id, phase) VALUES (1, 'prepared')`).run();
52
+ }
53
+ function phaseOf(db) {
54
+ const row = db
55
+ .prepare(`SELECT phase FROM compat_journal_state WHERE id = 1`)
56
+ .get();
57
+ return row?.phase ?? "prepared";
58
+ }
59
+ /** Read the durable journal phase (for the M2 migration host seam). */
60
+ export function journalPhase(db) {
61
+ return phaseOf(db);
62
+ }
63
+ function setPhase(db, phase) {
64
+ db.prepare(`UPDATE compat_journal_state SET phase = @phase WHERE id = 1`).run({
65
+ "@phase": phase,
66
+ });
67
+ }
68
+ /**
69
+ * Verify a recorded digest by RECOMPUTING sha256 over the base64 source embedded
70
+ * in the legacy projection and comparing it to the recorded digest (Q03: real
71
+ * digest parity, not a `sha256:` prefix check). Returns false when the projection
72
+ * is absent or malformed, or the digest does not match the recomputed hash.
73
+ */
74
+ export function verifyLegacyDigest(digest, legacyProjection) {
75
+ if (legacyProjection === null)
76
+ return false;
77
+ let rec;
78
+ try {
79
+ rec = JSON.parse(legacyProjection);
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ const source = rec.source;
85
+ if (typeof source !== "string")
86
+ return false;
87
+ const recomputed = `sha256:${createHash("sha256").update(Buffer.from(source, "base64")).digest("hex")}`;
88
+ return recomputed === digest;
89
+ }
90
+ /**
91
+ * Build the CompatJournalV1 over the isolated occurrence-v2 DB. Call
92
+ * `initCompatJournal` once before use. All record writes are append-only and
93
+ * best-effort/non-fatal so the agent loop is never broken (PREVENT-PI-004).
94
+ */
95
+ export function createCompatJournal(db) {
96
+ return {
97
+ active() {
98
+ try {
99
+ const row = db
100
+ .prepare(`SELECT 1 AS hit FROM compat_journal_v1 LIMIT 1`)
101
+ .get();
102
+ return row !== undefined;
103
+ }
104
+ catch {
105
+ return false;
106
+ }
107
+ },
108
+ record(input) {
109
+ const { occurrence, legacyProjection } = input;
110
+ try {
111
+ const unrep = legacyProjection === null;
112
+ db.prepare(`INSERT INTO compat_journal_v1
113
+ (session, seq, event_id, digest, kind, legacy_projection, unrepresentable)
114
+ VALUES (@session, @seq, @event_id, @digest, @kind, @legacy, @unrep)
115
+ ON CONFLICT(session, seq) DO NOTHING`).run({
116
+ "@session": occurrence.session,
117
+ "@seq": Number(occurrence.seq),
118
+ "@event_id": occurrence.eventId,
119
+ "@digest": occurrence.digest,
120
+ "@kind": occurrence.kind,
121
+ // Q06: an unrepresentable row stores NO projection (legacy_projection
122
+ // stays NULL); we never write a base64 body we cannot round-trip.
123
+ "@legacy": legacyProjection,
124
+ "@unrep": unrep ? 1 : 0,
125
+ });
126
+ }
127
+ catch {
128
+ /* non-fatal journal write — never break the agent loop */
129
+ }
130
+ },
131
+ isUnrepresentable(eventId, digest) {
132
+ try {
133
+ const row = db
134
+ .prepare(`SELECT unrepresentable AS u FROM compat_journal_v1
135
+ WHERE event_id = @event_id AND digest = @digest
136
+ ORDER BY seq DESC LIMIT 1`)
137
+ .get({ "@event_id": eventId, "@digest": digest });
138
+ return row?.u === 1;
139
+ }
140
+ catch {
141
+ return false;
142
+ }
143
+ },
144
+ prepare() {
145
+ const rows = db
146
+ .prepare(`SELECT session, seq, event_id, digest, kind, legacy_projection, unrepresentable
147
+ FROM compat_journal_v1 ORDER BY session ASC, seq ASC`)
148
+ .all();
149
+ setPhase(db, "prepared");
150
+ return rows.filter((r) => r.unrepresentable === 1).map((r) => r.event_id);
151
+ },
152
+ copied() {
153
+ setPhase(db, "copied");
154
+ },
155
+ validate() {
156
+ const codes = [];
157
+ const phase = phaseOf(db);
158
+ if (!isRepresentableState(phase, "copied"))
159
+ codes.push(MIG_DOWN_FAIL.PHASE_UNREACHED);
160
+ if (!this.active())
161
+ codes.push(MIG_DOWN_FAIL.NOT_ACTIVE);
162
+ // Seq must be strictly increasing within a session (no regression).
163
+ const prev = new Map();
164
+ const rows = db
165
+ .prepare(`SELECT session, seq FROM compat_journal_v1 ORDER BY session ASC, seq ASC`)
166
+ .all();
167
+ for (const r of rows) {
168
+ const last = prev.get(r.session);
169
+ if (last !== undefined && r.seq <= last)
170
+ codes.push(MIG_DOWN_FAIL.SEQ_REGRESSION);
171
+ prev.set(r.session, r.seq);
172
+ }
173
+ // Digests must verify: recompute sha256 over the stored source and compare
174
+ // to the recorded digest (Q03 — real parity, not just a `sha256:` prefix).
175
+ for (const r of db
176
+ .prepare(`SELECT digest, legacy_projection FROM compat_journal_v1 WHERE unrepresentable = 0`)
177
+ .all()) {
178
+ if (r.legacy_projection === null)
179
+ continue;
180
+ if (!verifyLegacyDigest(r.digest, r.legacy_projection)) {
181
+ codes.push(MIG_DOWN_FAIL.DIGEST_MISMATCH);
182
+ }
183
+ }
184
+ const ok = codes.length === 0;
185
+ if (ok)
186
+ setPhase(db, "validated");
187
+ return { ok, codes: dedupe(codes) };
188
+ },
189
+ switched() {
190
+ if (this.active())
191
+ setPhase(db, "switched");
192
+ },
193
+ };
194
+ }
195
+ /** A phase has reached at least `required` in the prepare→…→switched order. */
196
+ function isRepresentableState(phase, required) {
197
+ const order = ["prepared", "copied", "validated", "switched"];
198
+ return order.indexOf(phase) >= order.indexOf(required);
199
+ }
200
+ function dedupe(codes) {
201
+ const out = [];
202
+ for (const c of codes)
203
+ if (!out.includes(c))
204
+ out.push(c);
205
+ return out;
206
+ }