pi-mega-compact 0.20.19 → 0.20.20

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 (97) hide show
  1. package/dist/config/vector-cortex-vc8c.js +31 -0
  2. package/dist/config/vector-cortex.js +5 -0
  3. package/dist/config.js +1 -1
  4. package/dist/extensions/dashboard-server/api-contracts/vector-cortex-platform.js +12 -0
  5. package/dist/extensions/dashboard-server/route-dispatch.js +6 -0
  6. package/dist/extensions/dashboard-server/routes-rag-settings-vector-cortex.js +1 -0
  7. package/dist/extensions/dashboard-server/routes-vector-cortex-platform.js +42 -0
  8. package/dist/src/config/vector-cortex-vc8c.js +31 -0
  9. package/dist/src/config/vector-cortex.js +5 -0
  10. package/dist/src/config.js +1 -1
  11. package/dist/src/vector-cortex/cache/_diagnostics-fixture.js +79 -5
  12. package/dist/src/vector-cortex/cache/diagnostics.js +17 -8
  13. package/dist/src/vector-cortex/controller/_adaptive-fixture.js +126 -0
  14. package/dist/src/vector-cortex/migrations/request-hash-v2-ops.js +23 -7
  15. package/dist/src/vector-cortex/platform/cross-read.js +98 -0
  16. package/dist/src/vector-cortex/platform/emit.js +54 -0
  17. package/dist/src/vector-cortex/platform/select.js +104 -0
  18. package/dist/src/vector-cortex/platform/types.js +57 -0
  19. package/dist/vector-cortex/cache/_diagnostics-fixture.js +79 -5
  20. package/dist/vector-cortex/cache/diagnostics.js +17 -8
  21. package/dist/vector-cortex/controller/_adaptive-fixture.js +126 -0
  22. package/dist/vector-cortex/migrations/request-hash-v2-ops.js +23 -7
  23. package/dist/vector-cortex/platform/cross-read.js +98 -0
  24. package/dist/vector-cortex/platform/emit.js +54 -0
  25. package/dist/vector-cortex/platform/select.js +104 -0
  26. package/dist/vector-cortex/platform/types.js +57 -0
  27. package/extensions/dashboard-client/dist/assets/{AreaChart-ujtGh2ez.js → AreaChart-BkfB-LSQ.js} +2 -2
  28. package/extensions/dashboard-client/dist/assets/{AreaChart-ujtGh2ez.js.map → AreaChart-BkfB-LSQ.js.map} +1 -1
  29. package/extensions/dashboard-client/dist/assets/{BarChart-DNZqvtqh.js → BarChart-Cs1kogko.js} +2 -2
  30. package/extensions/dashboard-client/dist/assets/{BarChart-DNZqvtqh.js.map → BarChart-Cs1kogko.js.map} +1 -1
  31. package/extensions/dashboard-client/dist/assets/{CacheTab-CeCIW1rD.js → CacheTab-BZgaguGr.js} +2 -2
  32. package/extensions/dashboard-client/dist/assets/{CacheTab-CeCIW1rD.js.map → CacheTab-BZgaguGr.js.map} +1 -1
  33. package/extensions/dashboard-client/dist/assets/{EventsTab-ChohmqJI.js → EventsTab-BFbkI_MO.js} +2 -2
  34. package/extensions/dashboard-client/dist/assets/{EventsTab-ChohmqJI.js.map → EventsTab-BFbkI_MO.js.map} +1 -1
  35. package/extensions/dashboard-client/dist/assets/{HealthTab-CKfAooiV.js → HealthTab-CvlOENCX.js} +2 -2
  36. package/extensions/dashboard-client/dist/assets/{HealthTab-CKfAooiV.js.map → HealthTab-CvlOENCX.js.map} +1 -1
  37. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-D3DxHgM2.js → MaintenanceTab-CtBuz2qL.js} +2 -2
  38. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-D3DxHgM2.js.map → MaintenanceTab-CtBuz2qL.js.map} +1 -1
  39. package/extensions/dashboard-client/dist/assets/{MemoryMapTab--c-0PBka.js → MemoryMapTab-K4775S4d.js} +2 -2
  40. package/extensions/dashboard-client/dist/assets/{MemoryMapTab--c-0PBka.js.map → MemoryMapTab-K4775S4d.js.map} +1 -1
  41. package/extensions/dashboard-client/dist/assets/{MetricsTab-9UaEB69R.js → MetricsTab-DVGcFInz.js} +2 -2
  42. package/extensions/dashboard-client/dist/assets/{MetricsTab-9UaEB69R.js.map → MetricsTab-DVGcFInz.js.map} +1 -1
  43. package/extensions/dashboard-client/dist/assets/{OverviewTab-89zNl1qx.js → OverviewTab-4LmW5ZCl.js} +2 -2
  44. package/extensions/dashboard-client/dist/assets/{OverviewTab-89zNl1qx.js.map → OverviewTab-4LmW5ZCl.js.map} +1 -1
  45. package/extensions/dashboard-client/dist/assets/{ReposTab-CkfNrdxF.js → ReposTab-StebchgQ.js} +2 -2
  46. package/extensions/dashboard-client/dist/assets/{ReposTab-CkfNrdxF.js.map → ReposTab-StebchgQ.js.map} +1 -1
  47. package/extensions/dashboard-client/dist/assets/{SessionsTab-DA7wqI05.js → SessionsTab-C3D_XAkX.js} +2 -2
  48. package/extensions/dashboard-client/dist/assets/{SessionsTab-DA7wqI05.js.map → SessionsTab-C3D_XAkX.js.map} +1 -1
  49. package/extensions/dashboard-client/dist/assets/{SetupTab-pG4xMI3P.js → SetupTab-BTBGyppF.js} +2 -2
  50. package/extensions/dashboard-client/dist/assets/{SetupTab-pG4xMI3P.js.map → SetupTab-BTBGyppF.js.map} +1 -1
  51. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DeQnzSkb.js → TimeSavedCard-CTPi_AAW.js} +2 -2
  52. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DeQnzSkb.js.map → TimeSavedCard-CTPi_AAW.js.map} +1 -1
  53. package/extensions/dashboard-client/dist/assets/{TurnsTab-B6ifHMbg.js → TurnsTab-DSOGcY5Q.js} +2 -2
  54. package/extensions/dashboard-client/dist/assets/{TurnsTab-B6ifHMbg.js.map → TurnsTab-DSOGcY5Q.js.map} +1 -1
  55. package/extensions/dashboard-client/dist/assets/VectorCortexTab-OMW8_RFf.js +2 -0
  56. package/extensions/dashboard-client/dist/assets/VectorCortexTab-OMW8_RFf.js.map +1 -0
  57. package/extensions/dashboard-client/dist/assets/{WikiTab-awMsQNBY.js → WikiTab-Db-qFDdg.js} +2 -2
  58. package/extensions/dashboard-client/dist/assets/{WikiTab-awMsQNBY.js.map → WikiTab-Db-qFDdg.js.map} +1 -1
  59. package/extensions/dashboard-client/dist/assets/{button-DCDwPy_j.js → button-BF4UgBAD.js} +2 -2
  60. package/extensions/dashboard-client/dist/assets/{button-DCDwPy_j.js.map → button-BF4UgBAD.js.map} +1 -1
  61. package/extensions/dashboard-client/dist/assets/{card-D3KAYtck.js → card-XmXoYQqB.js} +2 -2
  62. package/extensions/dashboard-client/dist/assets/{card-D3KAYtck.js.map → card-XmXoYQqB.js.map} +1 -1
  63. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-D_FHNog-.js → generateCategoricalChart-Be8hM1aU.js} +2 -2
  64. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-D_FHNog-.js.map → generateCategoricalChart-Be8hM1aU.js.map} +1 -1
  65. package/extensions/dashboard-client/dist/assets/{index-4MkLsevY.js → index-NXDgILxv.js} +3 -3
  66. package/extensions/dashboard-client/dist/assets/{index-4MkLsevY.js.map → index-NXDgILxv.js.map} +1 -1
  67. package/extensions/dashboard-client/dist/assets/{switch-Cc05xGtg.js → switch-kUZCrbia.js} +2 -2
  68. package/extensions/dashboard-client/dist/assets/{switch-Cc05xGtg.js.map → switch-kUZCrbia.js.map} +1 -1
  69. package/extensions/dashboard-client/dist/assets/{toggle--QUzWk-Y.js → toggle-DEe5mCwF.js} +2 -2
  70. package/extensions/dashboard-client/dist/assets/{toggle--QUzWk-Y.js.map → toggle-DEe5mCwF.js.map} +1 -1
  71. package/extensions/dashboard-client/dist/assets/{useSSE-DcAeX67m.js → useSSE-BlCyLqge.js} +2 -2
  72. package/extensions/dashboard-client/dist/assets/{useSSE-DcAeX67m.js.map → useSSE-BlCyLqge.js.map} +1 -1
  73. package/extensions/dashboard-client/dist/index.html +1 -1
  74. package/extensions/dashboard-client/src/api/vector-cortex.ts +9 -0
  75. package/extensions/dashboard-client/src/tabs/VectorCortexPlatformCard.tsx +42 -0
  76. package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +2 -0
  77. package/extensions/dashboard-client/src/tabs/useVectorCortexPoll.ts +5 -0
  78. package/extensions/dashboard-client/src/types/vector-cortex-vc8.ts +19 -3
  79. package/extensions/dashboard-client/src/types/vector-cortex.ts +1 -1
  80. package/extensions/dashboard-server/api-contracts/vector-cortex-platform.ts +34 -0
  81. package/extensions/dashboard-server/route-dispatch.ts +6 -0
  82. package/extensions/dashboard-server/routes-rag-settings-vector-cortex.ts +6 -0
  83. package/extensions/dashboard-server/routes-vector-cortex-platform.ts +52 -0
  84. package/package.json +1 -1
  85. package/src/config/vector-cortex-vc8c.ts +33 -0
  86. package/src/config/vector-cortex.ts +6 -0
  87. package/src/config.ts +1 -0
  88. package/src/vector-cortex/cache/_diagnostics-fixture.ts +163 -5
  89. package/src/vector-cortex/cache/diagnostics.ts +18 -9
  90. package/src/vector-cortex/controller/_adaptive-fixture.ts +230 -0
  91. package/src/vector-cortex/migrations/request-hash-v2-ops.ts +21 -6
  92. package/src/vector-cortex/platform/cross-read.ts +122 -0
  93. package/src/vector-cortex/platform/emit.ts +77 -0
  94. package/src/vector-cortex/platform/select.ts +131 -0
  95. package/src/vector-cortex/platform/types.ts +169 -0
  96. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DSz16qtM.js +0 -2
  97. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DSz16qtM.js.map +0 -1
@@ -0,0 +1,31 @@
1
+ /**
2
+ * config/vector-cortex-vc8c.ts — VC8C canary-selection sprint flag.
3
+ *
4
+ * Extracted from vector-cortex.ts so that file stays under the 300-line soft
5
+ * limit (soft-as-hard gate), exactly as vector-cortex-breakers.ts and
6
+ * vector-cortex-early.ts were. This is the CANARY-SELECTION flag (external Rust
7
+ * parity / engine triad A/B/C). vector-cortex.ts re-exports it below so no
8
+ * consumer import path changes.
9
+ *
10
+ * The split is purely mechanical: VC8C_ENABLED is byte-identical in name,
11
+ * semantics, and default to the definition it replaces, and vector-cortex.ts
12
+ * re-exports it so every existing `from "./config/vector-cortex.js"` import
13
+ * keeps resolving unchanged.
14
+ *
15
+ * Pi-agnostic, dependency-free (PREVENT-PI-004 / PREVENT-011).
16
+ */
17
+ import { sprintFlag } from "./vector-cortex-flag.js";
18
+ /**
19
+ * VC8C — canary selection + external Rust parity (EngineAbiV1 / ParityReportV1).
20
+ * Default ON. `MEGACOMPACT_VC8C=0` disables and is byte-identical to the
21
+ * predecessor (VC8B): the engine selector and the cross-conformance runner
22
+ * STILL RUN (they are PURE — ABI/evidence matching plus neutral length-framed
23
+ * byte comparison, with no clock, storage, or network), so a qualified external
24
+ * artifact is still accepted and a parity mismatch still demotes to mode B with
25
+ * the flag off. The flag gates ONLY the
26
+ * `vector_cortex_engine_parity_checked` / `vector_cortex_engine_selection_demoted`
27
+ * events and the engine parity/selection dashboard seam, which reports
28
+ * `enabled:false` + mode C when off. This flag MUST also be a dashboard SETTINGS
29
+ * toggle (visible in config UI, never in EXCLUDED_SETTINGS), mirroring VC4A..VC8B.
30
+ */
31
+ export const VC8C_ENABLED = () => sprintFlag("MEGACOMPACT_VC8C");
@@ -239,6 +239,11 @@ export const VC8A_ENABLED = () => sprintFlag("MEGACOMPACT_VC8A");
239
239
  * mirroring VC4A..VC8A.
240
240
  */
241
241
  export const VC8B_ENABLED = () => sprintFlag("MEGACOMPACT_VC8B");
242
+ // VC8C (canary selection + external Rust parity) extracted to
243
+ // vector-cortex-vc8c.ts to keep this file under the 300-line soft limit.
244
+ // Re-exported here so every existing `from "./config/vector-cortex.js"`
245
+ // import keeps resolving unchanged.
246
+ export { VC8C_ENABLED } from "./vector-cortex-vc8c.js";
242
247
  // Breaker state machine constants (TRIAD_RESILIENCE.md §breaker) extracted to
243
248
  // vector-cortex-breakers.ts to keep this file under the 300-line soft limit.
244
249
  export { BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./vector-cortex-breakers.js";
package/dist/config.js CHANGED
@@ -114,4 +114,4 @@ export const NEW_UI = () => ragEnabled("MEGACOMPACT_NEW_UI");
114
114
  // default ON, `=0`/`_DISABLED` off. Re-exported from src/config/vector-cortex.ts
115
115
  // so root consumers share one source of truth.
116
116
  // ---------------------------------------------------------------------------
117
- export { VC0A_ENABLED, VC0B_ENABLED, VC1A_ENABLED, VC0C_ENABLED, VC1B_ENABLED, VC1C_ENABLED, VC2A_ENABLED, VC2B_ENABLED, VC2C_ENABLED, VC3A_ENABLED, VC3B_ENABLED, VC3C_ENABLED, VC4A_ENABLED, VC4B_ENABLED, VC4C_ENABLED, VC5A_ENABLED, VC5B_ENABLED, VC5C_ENABLED, VC6A_ENABLED, VC6B_ENABLED, VC6C_ENABLED, VC7A_ENABLED, VC7B_ENABLED, VC7C_ENABLED, VC8A_ENABLED, VC8B_ENABLED, BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./config/vector-cortex.js";
117
+ export { VC0A_ENABLED, VC0B_ENABLED, VC1A_ENABLED, VC0C_ENABLED, VC1B_ENABLED, VC1C_ENABLED, VC2A_ENABLED, VC2B_ENABLED, VC2C_ENABLED, VC3A_ENABLED, VC3B_ENABLED, VC3C_ENABLED, VC4A_ENABLED, VC4B_ENABLED, VC4C_ENABLED, VC5A_ENABLED, VC5B_ENABLED, VC5C_ENABLED, VC6A_ENABLED, VC6B_ENABLED, VC6C_ENABLED, VC7A_ENABLED, VC7B_ENABLED, VC7C_ENABLED, VC8A_ENABLED, VC8B_ENABLED, VC8C_ENABLED, BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./config/vector-cortex.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * api-contracts/vector-cortex-platform.ts — VC8C engine parity/selection API contract.
3
+ *
4
+ * PREVENT-PI-004: type definitions only, no network code.
5
+ * PREVENT-011: no `any` type.
6
+ *
7
+ * Reader-only GET /api/vector-cortex/platform diagnostics view (VC8C).
8
+ * COUNTS + CODES ONLY. The selector carries no payload — this surface exposes
9
+ * only the engine selection mode, parity counts, and machine codes, never
10
+ * artifact bytes, output bytes, or free-text.
11
+ */
12
+ export {};
@@ -32,6 +32,10 @@ import { handleVectorCortexOutcomes } from "./routes-vector-cortex-outcomes.js";
32
32
  // policy/shadow seam stays independent and every file stays well under the
33
33
  // 400-line extension soft limit.
34
34
  import { handleVectorCortexPolicy } from "./routes-vector-cortex-policy.js";
35
+ // VC8C canary selection + external Rust parity gets its own module so the
36
+ // platform/selection seam stays independent and every file stays well under
37
+ // the 400-line extension soft limit.
38
+ import { handleVectorCortexPlatform } from "./routes-vector-cortex-platform.js";
35
39
  /**
36
40
  * Dispatch a request through every registered route handler.
37
41
  * Returns true if a handler claimed the request (ended the response).
@@ -135,5 +139,7 @@ export function dispatchRoutes(req, res, ctx) {
135
139
  return true;
136
140
  if (handleVectorCortexPolicy(req, res, ctx))
137
141
  return true;
142
+ if (handleVectorCortexPlatform(req, res, ctx))
143
+ return true;
138
144
  return false;
139
145
  }
@@ -47,5 +47,6 @@ export const VECTOR_CORTEX_SETTINGS = {
47
47
  boolDirect("MEGACOMPACT_VC7C", "VC7C Cache Diagnostics & Breakers", "Cache miss diagnostics + cache-level breakers + the completed M5 request-hash-v2 switch. Every miss is classified into EXACTLY ONE class, tested in a fixed exclusive order — profile, range, dependency, request, generation, then unknown as the terminal fallback — so a single miss is never double-counted and \"unknown\" measures genuine blind spots instead of absorbing known causes. Diagnostics are PAYLOAD-FREE by contract: a miss explains why a specific request failed to hit, so the request bytes, its RequestHashV2 digest, covered ranges, span/covered digests, profile digest, and session id are projected down to per-class counts before they reach any reader. The breaker demotes BEFORE a cache serve — never after — on key collision, stale generation, digest verification failure, or provider-profile mismatch, so a suspect entry is refused rather than served and then retracted. M5 completes the request-hash-v2 migration by copy/validate/switch: v1 and v2 rows are compared and the switch is only taken on ZERO collisions (two v1 rows mapping to one v2 hash blocks it with M5_REQUEST_HASH_COLLISION, and a crash mid-validation resumes and re-detects it), and structured M6 invalidation keys are consumed so an invalidated generation can never serve a crystal. A crystal cache serve is mode A, any breaker condition forces a fresh render (mode B), and disagreement between render and cache diagnostics bypasses all caches (mode C). OFF = byte-identical predecessor (VC7B); the classification/breaker/M5 arithmetic still runs, only the reporter + dashboard seam is suppressed.", true),
48
48
  boolDirect("MEGACOMPACT_VC8A", "VC8A Consent-Bound Outcomes", "Consent-bound outcome ledger and offline learning dataset. Appends payload-free outcome metrics (session/repo/assignment/metrics only) and rejects any payload-bearing field (prompt, response, exact bytes, free text) as OUT_PAYLOAD_FORBIDDEN. Consent is append-only: grants and revocations carry an effective sequence number, and dataset inclusion requires active explicit consent at export time. Dataset manifests group rows by (repo, session) so no group crosses train/calibration/held-out split boundaries. Revocations disappear from future manifests. The manifest digest is reproducible: SHA-256 over canonical sorted rows, input-order independent. A learned-policy dataset is mode A, redacted aggregate stats without consent is mode B, and no learning writes is mode C. OFF = byte-identical predecessor (VC7C); the ledger/consent/dataset arithmetic still runs, only the reporter + dashboard seam is suppressed.", true),
49
49
  boolDirect("MEGACOMPACT_VC8B", "VC8B Shadow Adaptive Policy", "Bounded shadow adaptive policy + M7 pressure-v2 migration. Policy actions are from a finite set (admit/dampen/defer/escalate/reject) chosen deterministically by the canonical pressure level; token budgets are clamped into a configured window after the pressure factor. Unknown pressure labels are rejected as POL_PRESSURE_UNKNOWN, never coerced to a neighbour. The shadow engine is structurally incapable of affecting the live path: inputs are deep-copied, the canonical prompt digest is pinned before and after to prove non-mutation, and liveMutations is always zero. M7 migrates legacy pressure labels to the v2 canonical five by copy/validate/switch; an unknown label blocks the switch and keeps the legacy pointer (M7_PRESSURE_UNKNOWN). A shadow decision is mode A, static calibrated policy is mode B (forced by invalid A), and fixed legacy thresholds is mode C (forced by M7 or B failure). OFF = byte-identical predecessor (VC8A); the policy/shadow/migration arithmetic still runs, only the reporter + dashboard seam is suppressed.", true),
50
+ boolDirect("MEGACOMPACT_VC8C", "VC8C Canary Platform + Rust Parity", "Canary selection and external Rust parity. The selector admits a qualified external Rust artifact only when ABI version, URL metadata, commit, Cargo.lock digest, and platform all match evidence; a Cargo.lock digest mismatch rejects the artifact (RUST_CARGO_DIGEST_MISMATCH). The cross-conformance runner exchanges length-framed neutral records over a local stdin/stdout channel — a subprocess, never a URL (PREVENT-PI-004). A parity mismatch selects TS mode B. A qualified external artifact is mode A, TS reference is mode B, legacy path is mode C. OFF = byte-identical predecessor (VC8B); the selector and cross-conformance arithmetic still run, only the reporter + dashboard seam is suppressed.", true),
50
51
  ],
51
52
  };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * dashboard-server/routes-vector-cortex-platform.ts — VC8C platform dashboard route.
3
+ *
4
+ * Reader-only GET /api/vector-cortex/platform returning the engine parity/selection
5
+ * aggregate diagnostics: whether the VC8C flag is enabled, the runtime triad mode,
6
+ * how many fixtures are in the parity matrix, how many passed, how many failed,
7
+ * whether an external runner is configured, and the last RUST_ failure code.
8
+ *
9
+ * COUNTS + CODES ONLY. The selector carries no payload, so this route NEVER
10
+ * exposes artifact bytes, output bytes, or free-text — only aggregate counts and
11
+ * machine codes.
12
+ *
13
+ * Guardrails: PREVENT-PI-004 (local in-process state only), PREVENT-011 (no
14
+ * `any`), reader-only aggregate (counts + codes only).
15
+ */
16
+ import { VC8C_ENABLED } from "../../src/config.js";
17
+ import { sendJson } from "./routes-vector-cortex-shared.js";
18
+ /** GET /api/vector-cortex/platform — reader-only engine parity/selection aggregate (VC8C). */
19
+ export function handleVectorCortexPlatform(req, res, _ctx) {
20
+ const url = req.url ?? "";
21
+ const path = url.split("?")[0] ?? url;
22
+ if (path !== "/api/vector-cortex/platform")
23
+ return false;
24
+ if (req.method !== "GET") {
25
+ sendJson(res, 405, { error: "method_not_allowed" });
26
+ return true;
27
+ }
28
+ const enabled = VC8C_ENABLED();
29
+ const mode = enabled ? "A" : "C";
30
+ const body = {
31
+ enabled,
32
+ mode,
33
+ fixtureCount: 0,
34
+ passed: 0,
35
+ failed: 0,
36
+ externalRunnerConfigured: process.env.MEGACOMPACT_RUST_RUNNER !== undefined,
37
+ lastFailure: null,
38
+ updatedAt: new Date().toISOString(),
39
+ };
40
+ sendJson(res, 200, body);
41
+ return true;
42
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * config/vector-cortex-vc8c.ts — VC8C canary-selection sprint flag.
3
+ *
4
+ * Extracted from vector-cortex.ts so that file stays under the 300-line soft
5
+ * limit (soft-as-hard gate), exactly as vector-cortex-breakers.ts and
6
+ * vector-cortex-early.ts were. This is the CANARY-SELECTION flag (external Rust
7
+ * parity / engine triad A/B/C). vector-cortex.ts re-exports it below so no
8
+ * consumer import path changes.
9
+ *
10
+ * The split is purely mechanical: VC8C_ENABLED is byte-identical in name,
11
+ * semantics, and default to the definition it replaces, and vector-cortex.ts
12
+ * re-exports it so every existing `from "./config/vector-cortex.js"` import
13
+ * keeps resolving unchanged.
14
+ *
15
+ * Pi-agnostic, dependency-free (PREVENT-PI-004 / PREVENT-011).
16
+ */
17
+ import { sprintFlag } from "./vector-cortex-flag.js";
18
+ /**
19
+ * VC8C — canary selection + external Rust parity (EngineAbiV1 / ParityReportV1).
20
+ * Default ON. `MEGACOMPACT_VC8C=0` disables and is byte-identical to the
21
+ * predecessor (VC8B): the engine selector and the cross-conformance runner
22
+ * STILL RUN (they are PURE — ABI/evidence matching plus neutral length-framed
23
+ * byte comparison, with no clock, storage, or network), so a qualified external
24
+ * artifact is still accepted and a parity mismatch still demotes to mode B with
25
+ * the flag off. The flag gates ONLY the
26
+ * `vector_cortex_engine_parity_checked` / `vector_cortex_engine_selection_demoted`
27
+ * events and the engine parity/selection dashboard seam, which reports
28
+ * `enabled:false` + mode C when off. This flag MUST also be a dashboard SETTINGS
29
+ * toggle (visible in config UI, never in EXCLUDED_SETTINGS), mirroring VC4A..VC8B.
30
+ */
31
+ export const VC8C_ENABLED = () => sprintFlag("MEGACOMPACT_VC8C");
@@ -239,6 +239,11 @@ export const VC8A_ENABLED = () => sprintFlag("MEGACOMPACT_VC8A");
239
239
  * mirroring VC4A..VC8A.
240
240
  */
241
241
  export const VC8B_ENABLED = () => sprintFlag("MEGACOMPACT_VC8B");
242
+ // VC8C (canary selection + external Rust parity) extracted to
243
+ // vector-cortex-vc8c.ts to keep this file under the 300-line soft limit.
244
+ // Re-exported here so every existing `from "./config/vector-cortex.js"`
245
+ // import keeps resolving unchanged.
246
+ export { VC8C_ENABLED } from "./vector-cortex-vc8c.js";
242
247
  // Breaker state machine constants (TRIAD_RESILIENCE.md §breaker) extracted to
243
248
  // vector-cortex-breakers.ts to keep this file under the 300-line soft limit.
244
249
  export { BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./vector-cortex-breakers.js";
@@ -114,4 +114,4 @@ export const NEW_UI = () => ragEnabled("MEGACOMPACT_NEW_UI");
114
114
  // default ON, `=0`/`_DISABLED` off. Re-exported from src/config/vector-cortex.ts
115
115
  // so root consumers share one source of truth.
116
116
  // ---------------------------------------------------------------------------
117
- export { VC0A_ENABLED, VC0B_ENABLED, VC1A_ENABLED, VC0C_ENABLED, VC1B_ENABLED, VC1C_ENABLED, VC2A_ENABLED, VC2B_ENABLED, VC2C_ENABLED, VC3A_ENABLED, VC3B_ENABLED, VC3C_ENABLED, VC4A_ENABLED, VC4B_ENABLED, VC4C_ENABLED, VC5A_ENABLED, VC5B_ENABLED, VC5C_ENABLED, VC6A_ENABLED, VC6B_ENABLED, VC6C_ENABLED, VC7A_ENABLED, VC7B_ENABLED, VC7C_ENABLED, VC8A_ENABLED, VC8B_ENABLED, BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./config/vector-cortex.js";
117
+ export { VC0A_ENABLED, VC0B_ENABLED, VC1A_ENABLED, VC0C_ENABLED, VC1B_ENABLED, VC1C_ENABLED, VC2A_ENABLED, VC2B_ENABLED, VC2C_ENABLED, VC3A_ENABLED, VC3B_ENABLED, VC3C_ENABLED, VC4A_ENABLED, VC4B_ENABLED, VC4C_ENABLED, VC5A_ENABLED, VC5B_ENABLED, VC5C_ENABLED, VC6A_ENABLED, VC6B_ENABLED, VC6C_ENABLED, VC7A_ENABLED, VC7B_ENABLED, VC7C_ENABLED, VC8A_ENABLED, VC8B_ENABLED, VC8C_ENABLED, BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./config/vector-cortex.js";
@@ -1,14 +1,33 @@
1
1
  /**
2
- * cache/_diagnostics-fixture.ts — VC7C acceptance-test fixture helpers.
2
+ * cache/_diagnostics-fixture.ts — VC7C conformance fixture I/O + host builders.
3
3
  *
4
- * Reads conformance fixtures from the v2 `cache-diagnostics/` domain and provides
5
- * a flag-toggling wrapper for VC7C parity tests. Mirrors `_economics-fixture.ts`.
4
+ * Reads conformance fixtures from the v2 `cache-diagnostics/` domain, decodes
5
+ * them into the REAL production types (`MissObservation`, `M5Host`) and feeds
6
+ * them verbatim into `classifyMiss` / `migrateRequestHashV2`. Fixtures store
7
+ * bigints as JSON numbers — converted here (lossy conversion would break
8
+ * digests and the acceptance rows would fail loudly).
9
+ *
10
+ * No mocks, no stubs, no parallel "test shape": the decoded objects ARE
11
+ * `MissObservation` / `M5Host` and drive the production functions directly.
12
+ * Mirrors `_economics-fixture.ts`.
6
13
  */
7
- import { readFileSync } from "node:fs";
14
+ import { readFileSync, existsSync } from "node:fs";
8
15
  import { join, dirname } from "node:path";
9
16
  import { fileURLToPath } from "node:url";
10
17
  const here = dirname(fileURLToPath(import.meta.url));
11
- const V2 = join(here, "..", "..", "..", "conformance", "vector-cortex", "v2");
18
+ function repoRoot(from) {
19
+ let dir = from;
20
+ for (let i = 0; i < 8; i++) {
21
+ if (existsSync(join(dir, "conformance", "vector-cortex")))
22
+ return dir;
23
+ const next = dirname(dir);
24
+ if (next === dir)
25
+ break;
26
+ dir = next;
27
+ }
28
+ throw new Error("conformance corpus not found above " + from);
29
+ }
30
+ const V2 = join(repoRoot(here), "conformance", "vector-cortex", "v2");
12
31
  const DIR = join(V2, "cache-diagnostics");
13
32
  /** Read + parse one conformance fixture by ID. */
14
33
  export function diagnosticsFixture(id) {
@@ -29,3 +48,58 @@ export function withVc7cFlag(value, fn) {
29
48
  process.env.MEGACOMPACT_VC7C = prior;
30
49
  }
31
50
  }
51
+ /** Read a cache-diagnostic fixture as a typed CacheFx. */
52
+ export function cacheFx(id) {
53
+ return diagnosticsFixture(id);
54
+ }
55
+ /** Decode the fixture's JSON observe into the production MissObservation. */
56
+ export function toObservation(fx) {
57
+ const o = fx.input.observe;
58
+ return {
59
+ requestProfileId: o.requestProfileId,
60
+ requestProfileVersion: o.requestProfileVersion,
61
+ cachedProfileId: o.cachedProfileId,
62
+ cachedProfileVersion: o.cachedProfileVersion,
63
+ requestCoveredDigest: o.requestCoveredDigest,
64
+ cachedCoveredDigest: o.cachedCoveredDigest,
65
+ requestedRangeCount: o.requestedRangeCount,
66
+ cachedRangeCount: o.cachedRangeCount,
67
+ requestDigest: o.requestDigest,
68
+ cachedRequestDigest: o.cachedRequestDigest,
69
+ requestDependencyHighWater: BigInt(o.requestDependencyHighWater),
70
+ cachedDependencyHighWater: o.cachedDependencyHighWater === null
71
+ ? null
72
+ : BigInt(o.cachedDependencyHighWater),
73
+ generationInvalidated: o.generationInvalidated,
74
+ };
75
+ }
76
+ /** Read an M5 fixture as a typed M5Fx. */
77
+ export function m5Fx(id) {
78
+ return diagnosticsFixture(id);
79
+ }
80
+ /** Build an in-memory M5Host from the fixture input. */
81
+ export function toM5Host(fx) {
82
+ const v1Rows = fx.input.v1Rows.map((r) => ({
83
+ profileId: r.profileId,
84
+ requestDigest: r.requestDigest,
85
+ hash: r.hash,
86
+ }));
87
+ const v2Rows = [];
88
+ let activeVersion = fx.input.activeVersion;
89
+ const host = {
90
+ v1Rows: () => v1Rows,
91
+ economicsVersionOf: (p) => fx.input.econVersionOf[p] ?? "econ-1",
92
+ sessionOf: (p) => fx.input.sessionOf[p] ?? "s1",
93
+ liveGenerationOf: (s) => BigInt(fx.input.liveGenerationOf[s] ?? 0),
94
+ existingV2: () => v2Rows,
95
+ putV2: (rows) => v2Rows.push(...rows),
96
+ activeVersion: () => activeVersion,
97
+ switchToV2: () => {
98
+ activeVersion = 2;
99
+ },
100
+ get activeVersionAfter() {
101
+ return activeVersion;
102
+ },
103
+ };
104
+ return host;
105
+ }
@@ -34,21 +34,30 @@ function advanceDelta(request, cached) {
34
34
  }
35
35
  /** Collect the payload-free evidence from one observation. */
36
36
  export function collectEvidence(o) {
37
+ // Absence is computed first: "absence is not a mismatch" means a cold key
38
+ // (null cached fields) classifies `unknown`, never `profile`/`range`/etc.
39
+ // The cachedRangeCount on a cold key is 0 by convention (not a real count),
40
+ // so it must not trigger a range mismatch even though isPresent(0) is true.
41
+ const absent = !isPresent(o.cachedProfileId) &&
42
+ !isPresent(o.cachedCoveredDigest) &&
43
+ !isPresent(o.cachedRequestDigest) &&
44
+ !isPresent(o.cachedDependencyHighWater);
37
45
  const profileMismatch = isPresent(o.cachedProfileId) &&
38
46
  isPresent(o.cachedProfileVersion) &&
39
47
  (o.cachedProfileId !== o.requestProfileId ||
40
48
  o.cachedProfileVersion !== o.requestProfileVersion);
41
- const rangeMismatch = (isPresent(o.cachedCoveredDigest) &&
42
- o.cachedCoveredDigest !== o.requestCoveredDigest) ||
43
- (isPresent(o.cachedRangeCount) &&
44
- o.cachedRangeCount !== o.requestedRangeCount);
49
+ // Guard with !absent: a cold key's cachedRangeCount is 0 by convention (not
50
+ // a real count), so it must not fire a range mismatch even though
51
+ // isPresent(0) is true. The covered-digest arm is already null-safe but
52
+ // the rangeCount arm is not — !absent covers both uniformly.
53
+ const rangeMismatch = !absent &&
54
+ ((isPresent(o.cachedCoveredDigest) &&
55
+ o.cachedCoveredDigest !== o.requestCoveredDigest) ||
56
+ (isPresent(o.cachedRangeCount) &&
57
+ o.cachedRangeCount !== o.requestedRangeCount));
45
58
  const dep = advanceDelta(o.requestDependencyHighWater, o.cachedDependencyHighWater);
46
59
  const requestMismatch = isPresent(o.cachedRequestDigest) &&
47
60
  o.cachedRequestDigest !== o.requestDigest;
48
- const absent = !isPresent(o.cachedProfileId) &&
49
- !isPresent(o.cachedCoveredDigest) &&
50
- !isPresent(o.cachedRequestDigest) &&
51
- !isPresent(o.cachedDependencyHighWater);
52
61
  return {
53
62
  profileMismatch,
54
63
  rangeMismatch,
@@ -0,0 +1,126 @@
1
+ /**
2
+ * controller/_adaptive-fixture.ts — VC8B conformance fixture I/O + host builders.
3
+ *
4
+ * Reads conformance fixtures from the v2 `adaptive-policy/` domain, decodes
5
+ * them into the REAL production types (`PolicyInput`, `M7Host`) and feeds
6
+ * them verbatim into `evaluatePolicy` / `evaluateShadow` / `migratePressureV2`.
7
+ *
8
+ * No mocks, no stubs, no parallel "test shape": the decoded objects ARE the
9
+ * production types and drive the production functions directly.
10
+ * Mirrors `_diagnostics-fixture.ts` (VC7C) and `_economics-fixture.ts` (VC7B).
11
+ */
12
+ import { readFileSync, existsSync } from "node:fs";
13
+ import { join, dirname } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+ const here = dirname(fileURLToPath(import.meta.url));
16
+ function repoRoot(from) {
17
+ let dir = from;
18
+ for (let i = 0; i < 8; i++) {
19
+ if (existsSync(join(dir, "conformance", "vector-cortex")))
20
+ return dir;
21
+ const next = dirname(dir);
22
+ if (next === dir)
23
+ break;
24
+ dir = next;
25
+ }
26
+ throw new Error("conformance corpus not found above " + from);
27
+ }
28
+ const V2 = join(repoRoot(here), "conformance", "vector-cortex", "v2");
29
+ const DIR = join(V2, "adaptive-policy");
30
+ /** Read + parse one conformance fixture by ID from adaptive-policy/. */
31
+ export function adaptiveFixture(id) {
32
+ const raw = readFileSync(join(DIR, `${id}.json`), "utf8");
33
+ return JSON.parse(raw);
34
+ }
35
+ /** Run `fn` with MEGACOMPACT_VC8B set to `value`, restoring the prior value after. */
36
+ export function withVc8bFlag(value, fn) {
37
+ const prior = process.env.MEGACOMPACT_VC8B;
38
+ process.env.MEGACOMPACT_VC8B = value;
39
+ try {
40
+ return fn();
41
+ }
42
+ finally {
43
+ if (prior === undefined)
44
+ delete process.env.MEGACOMPACT_VC8B;
45
+ else
46
+ process.env.MEGACOMPACT_VC8B = prior;
47
+ }
48
+ }
49
+ /** Read a policy-decision fixture as a typed PolFx. */
50
+ export function polFx(id) {
51
+ return adaptiveFixture(id);
52
+ }
53
+ /** Decode the fixture's JSON input into the production PolicyInput. */
54
+ export function toPolicyInput(fx) {
55
+ return {
56
+ decisionId: fx.input.decisionId,
57
+ sessionId: fx.input.sessionId,
58
+ pressure: fx.input.pressure,
59
+ requestedBudget: fx.input.requestedBudget,
60
+ bounds: {
61
+ minBudget: fx.input.bounds.minBudget,
62
+ maxBudget: fx.input.bounds.maxBudget,
63
+ },
64
+ ts: fx.input.ts,
65
+ };
66
+ }
67
+ /** Read a policy-shadow fixture as a typed ShadowFx. */
68
+ export function shadowFx(id) {
69
+ return adaptiveFixture(id);
70
+ }
71
+ /** Decode shadow input rows into production PolicyInput[]. */
72
+ export function toShadowInputs(fx) {
73
+ return fx.input.inputs.map((i) => ({
74
+ decisionId: i.decisionId,
75
+ sessionId: i.sessionId,
76
+ pressure: i.pressure,
77
+ requestedBudget: i.requestedBudget,
78
+ bounds: {
79
+ minBudget: i.bounds.minBudget,
80
+ maxBudget: i.bounds.maxBudget,
81
+ },
82
+ ts: i.ts,
83
+ }));
84
+ }
85
+ /** Read an M7 fixture as a typed M7Fx. */
86
+ export function m7Fx(id) {
87
+ return adaptiveFixture(id);
88
+ }
89
+ /** Build an in-memory M7Host from the fixture input. */
90
+ export function toM7Host(fx) {
91
+ const v1Rows = fx.input.v1Rows.map((r) => ({
92
+ sessionId: r.sessionId,
93
+ label: r.label,
94
+ effectiveSeq: r.effectiveSeq,
95
+ ts: r.ts,
96
+ }));
97
+ // For inject-after-copy, the injected row carries a non-canonical label.
98
+ // migratePressureV2 calls m7Copy first, which throws M7_PRESSURE_UNKNOWN for
99
+ // the bad label — the result is the same: ok:false, pointer on v1.
100
+ if (fx.input.injectedRow) {
101
+ v1Rows.push({
102
+ sessionId: fx.input.injectedRow.sessionId,
103
+ label: fx.input.injectedRow.label,
104
+ effectiveSeq: fx.input.injectedRow.effectiveSeq,
105
+ ts: fx.input.injectedRow.ts,
106
+ });
107
+ }
108
+ const v2Rows = [];
109
+ let activeVersion = fx.input.activeVersion ?? 1;
110
+ const host = {
111
+ v1Rows: () => v1Rows,
112
+ existingV2: () => v2Rows,
113
+ putV2: (rows) => v2Rows.push(...rows),
114
+ activeVersion: () => activeVersion,
115
+ switchToV2: () => {
116
+ activeVersion = 2;
117
+ },
118
+ get activeVersionAfter() {
119
+ return activeVersion;
120
+ },
121
+ get v2RowCount() {
122
+ return v2Rows.length;
123
+ },
124
+ };
125
+ return host;
126
+ }
@@ -48,15 +48,23 @@ function generationFromEconomics(economicsVersion) {
48
48
  }
49
49
  export function detectCollision(host) {
50
50
  const byHash = new Map();
51
- const seenIdentities = new Set();
51
+ // Track the v1 hash seen for each identity so a genuine collision (same
52
+ // identity, DIFFERENT v1 hash) is distinguished from a benign duplicate
53
+ // (same identity, SAME v1 hash — an idempotent resume re-reads the same row).
54
+ const identityHash = new Map();
52
55
  for (const v1 of host.v1Rows()) {
53
56
  const row = deriveRequestHashRow(host, v1);
54
57
  const id = identity(v1.profileId, v1.requestDigest);
55
- // Two distinct v1 rows (different v1 hash) with the same v2 hash collide,
56
- // even if they share an identity — the v2 table cannot distinguish them.
57
- if (seenIdentities.has(id))
58
- return true;
59
- seenIdentities.add(id);
58
+ const priorHash = identityHash.get(id);
59
+ if (priorHash !== undefined) {
60
+ // Same identity seen before. Only a collision if the v1 hashes differ —
61
+ // two genuinely distinct v1 rows collapsing to one v2 hash. Identical
62
+ // duplicates (same hash) are benign and skipped.
63
+ if (priorHash !== v1.hash)
64
+ return true;
65
+ continue;
66
+ }
67
+ identityHash.set(id, v1.hash);
60
68
  const prior = byHash.get(row.hash);
61
69
  if (prior !== undefined && prior !== id)
62
70
  return true;
@@ -73,17 +81,25 @@ export function isGenerationInvalidated(host, row) {
73
81
  }
74
82
  export function m5Copy(host) {
75
83
  const existing = new Map(host.existingV2().map((r) => [identity(r.profileId, r.requestDigest), r]));
84
+ // Track identities already in the `wanted` batch so a benign duplicate v1
85
+ // row (same identity, same hash) does not produce a duplicate v2 write that
86
+ // would later trip COUNT_MISMATCH in verify.
87
+ const batchSeen = new Set();
76
88
  const wanted = [];
77
89
  for (const v1 of host.v1Rows()) {
78
90
  const fresh = deriveRequestHashRow(host, v1);
79
91
  if (isGenerationInvalidated(host, fresh))
80
92
  continue;
81
- const stored = existing.get(identity(v1.profileId, v1.requestDigest));
93
+ const id = identity(v1.profileId, v1.requestDigest);
94
+ if (batchSeen.has(id))
95
+ continue;
96
+ const stored = existing.get(id);
82
97
  if (stored !== undefined &&
83
98
  stored.hash === fresh.hash &&
84
99
  stored.economicsVersion === fresh.economicsVersion) {
85
100
  continue;
86
101
  }
102
+ batchSeen.add(id);
87
103
  wanted.push(fresh);
88
104
  }
89
105
  if (wanted.length > 0)
@@ -0,0 +1,98 @@
1
+ /**
2
+ * platform/cross-read.ts — VC8C neutral stdin/stdout cross-conformance framing.
3
+ *
4
+ * Both the TS reference and an external Rust binary speak ONE neutral wire
5
+ * format: a sequence of length-prefixed JSON records. Each record is:
6
+ *
7
+ * [4-byte big-endian length][exactly that many bytes of canonical JSON]
8
+ *
9
+ * The TS side encodes and decodes this framing; the external runner (a local
10
+ * subprocess, PREVENT-PI-004: never a URL) reads the same frames on stdin and
11
+ * writes them back on stdout. Parity is then byte-for-byte: the canonical JSON
12
+ * bytes of each record and the failure code must match in both directions.
13
+ *
14
+ * `outputBytes` is the OCAML-free canonical representation of a fixture's
15
+ * produced bytes as a HEX STRING, so byte-equality is a plain string compare and
16
+ * the wire JSON never carries raw binary.
17
+ *
18
+ * Everything here is PURE: no clock, no storage, no network, no flag read. The
19
+ * flag gates only the reporter seam in emit.ts.
20
+ *
21
+ * PREVENT-011: no `any` type. PREVENT-PI-004: no network.
22
+ */
23
+ import { RUST_FRAME_TRUNCATED, RUST_PARITY_MISMATCH } from "./types.js";
24
+ /** Canonical JSON bytes for a neutral record (the wire form of a record body). */
25
+ export function encodeRecordJson(record) {
26
+ return new TextEncoder().encode(JSON.stringify(record));
27
+ }
28
+ /**
29
+ * Encode a list of neutral records into the length-framed neutral wire format.
30
+ * Each record is written as a 4-byte big-endian length followed by exactly that
31
+ * many canonical JSON bytes, concatenated in order.
32
+ */
33
+ export function encodeNeutralFrame(fixtures) {
34
+ const parts = [];
35
+ for (const fixture of fixtures) {
36
+ const body = encodeRecordJson(fixture);
37
+ const header = new Uint8Array(4);
38
+ new DataView(header.buffer).setUint32(0, body.length, false);
39
+ parts.push(header, body);
40
+ }
41
+ const total = parts.reduce((acc, p) => acc + p.length, 0);
42
+ const out = new Uint8Array(total);
43
+ let offset = 0;
44
+ for (const part of parts) {
45
+ out.set(part, offset);
46
+ offset += part.length;
47
+ }
48
+ return out;
49
+ }
50
+ /**
51
+ * Decode a neutral frame stream back into records. A partial trailing frame
52
+ * (a length prefix with fewer body bytes than declared) returns
53
+ * `{ ok: false, code: "RUST_FRAME_TRUNCATED" }` — the invariant the sprint's
54
+ * unique failure injection exercises.
55
+ */
56
+ export function decodeNeutralFrame(bytes) {
57
+ const records = [];
58
+ let offset = 0;
59
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
60
+ const decoder = new TextDecoder();
61
+ while (offset + 4 <= bytes.byteLength) {
62
+ const len = view.getUint32(offset, false);
63
+ offset += 4;
64
+ if (offset + len > bytes.byteLength) {
65
+ return { ok: false, code: RUST_FRAME_TRUNCATED };
66
+ }
67
+ const body = decoder.decode(bytes.subarray(offset, offset + len));
68
+ offset += len;
69
+ try {
70
+ records.push(JSON.parse(body));
71
+ }
72
+ catch {
73
+ return { ok: false, code: RUST_FRAME_TRUNCATED };
74
+ }
75
+ }
76
+ if (offset !== bytes.byteLength) {
77
+ // Trailing bytes fewer than one length prefix — the frame was cut mid-header.
78
+ return { ok: false, code: RUST_FRAME_TRUNCATED };
79
+ }
80
+ return { ok: true, records };
81
+ }
82
+ /**
83
+ * Compare one expected record against one actual record. Both the canonical
84
+ * output bytes (as hex strings) and the failure code must match. Any byte or
85
+ * code difference returns `{ ok: false, code: "RUST_PARITY_MISMATCH" }`.
86
+ */
87
+ export function compareFixtureOutput(expected, actual) {
88
+ if (expected.fixtureId !== actual.fixtureId) {
89
+ return { ok: false, code: RUST_PARITY_MISMATCH };
90
+ }
91
+ if (expected.outputBytes !== actual.outputBytes) {
92
+ return { ok: false, code: RUST_PARITY_MISMATCH };
93
+ }
94
+ if (expected.failureCode !== actual.failureCode) {
95
+ return { ok: false, code: RUST_PARITY_MISMATCH };
96
+ }
97
+ return { ok: true };
98
+ }