pi-mega-compact 0.20.8 → 0.20.9

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 (96) hide show
  1. package/dist/config/vector-cortex.js +13 -0
  2. package/dist/config.js +1 -1
  3. package/dist/extensions/dashboard-server/route-dispatch.js +3 -1
  4. package/dist/extensions/dashboard-server/routes-rag-settings-vector-cortex.js +1 -0
  5. package/dist/extensions/dashboard-server/routes-vector-cortex-heal.js +42 -3
  6. package/dist/extensions/dashboard-server/routes-vector-cortex.js +3 -1
  7. package/dist/extensions/dashboard-server/routes.js +1 -1
  8. package/dist/src/config/vector-cortex.js +13 -0
  9. package/dist/src/config.js +1 -1
  10. package/dist/src/vector-cortex/heal/_restore-fixture.js +96 -0
  11. package/dist/src/vector-cortex/heal/_vc6b-helpers.js +35 -0
  12. package/dist/src/vector-cortex/heal/restore-emit.js +70 -0
  13. package/dist/src/vector-cortex/heal/restore-readers.js +114 -0
  14. package/dist/src/vector-cortex/heal/restore-types.js +66 -0
  15. package/dist/src/vector-cortex/heal/restore.js +140 -0
  16. package/dist/src/vector-cortex/heal/types.js +1 -0
  17. package/dist/src/vector-cortex/heal/verify.js +75 -0
  18. package/dist/vector-cortex/heal/_restore-fixture.js +96 -0
  19. package/dist/vector-cortex/heal/_vc6b-helpers.js +35 -0
  20. package/dist/vector-cortex/heal/restore-emit.js +70 -0
  21. package/dist/vector-cortex/heal/restore-readers.js +114 -0
  22. package/dist/vector-cortex/heal/restore-types.js +66 -0
  23. package/dist/vector-cortex/heal/restore.js +140 -0
  24. package/dist/vector-cortex/heal/types.js +1 -0
  25. package/dist/vector-cortex/heal/verify.js +75 -0
  26. package/extensions/dashboard-client/dist/assets/{AreaChart-5yxaI8IO.js → AreaChart-fxyq2KaH.js} +2 -2
  27. package/extensions/dashboard-client/dist/assets/{AreaChart-5yxaI8IO.js.map → AreaChart-fxyq2KaH.js.map} +1 -1
  28. package/extensions/dashboard-client/dist/assets/{BarChart-BoHhCqH-.js → BarChart-D5EqAINP.js} +2 -2
  29. package/extensions/dashboard-client/dist/assets/{BarChart-BoHhCqH-.js.map → BarChart-D5EqAINP.js.map} +1 -1
  30. package/extensions/dashboard-client/dist/assets/{CacheTab-Di6L3CsF.js → CacheTab-D4UWbzLo.js} +2 -2
  31. package/extensions/dashboard-client/dist/assets/{CacheTab-Di6L3CsF.js.map → CacheTab-D4UWbzLo.js.map} +1 -1
  32. package/extensions/dashboard-client/dist/assets/{EventsTab-Cdff3h6s.js → EventsTab-BiXquGrX.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{EventsTab-Cdff3h6s.js.map → EventsTab-BiXquGrX.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/{HealthTab-MthYYVLw.js → HealthTab-D5QvoTjy.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{HealthTab-MthYYVLw.js.map → HealthTab-D5QvoTjy.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-D9G4ntsG.js → MaintenanceTab-Cy_-CWgM.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-D9G4ntsG.js.map → MaintenanceTab-Cy_-CWgM.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CN8Lr5Qf.js → MemoryMapTab-DrAjgGOR.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CN8Lr5Qf.js.map → MemoryMapTab-DrAjgGOR.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/{MetricsTab-CgO32kSo.js → MetricsTab-BvPHcuFN.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{MetricsTab-CgO32kSo.js.map → MetricsTab-BvPHcuFN.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/{OverviewTab-Cb4kR0h1.js → OverviewTab-DsbIigOQ.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{OverviewTab-Cb4kR0h1.js.map → OverviewTab-DsbIigOQ.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/{ReposTab-Dt1FGnMR.js → ReposTab-7v_uZX_a.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{ReposTab-Dt1FGnMR.js.map → ReposTab-7v_uZX_a.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{SessionsTab-D7D2RW6U.js → SessionsTab-BXGrv-6C.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{SessionsTab-D7D2RW6U.js.map → SessionsTab-BXGrv-6C.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/{SetupTab-CgV5xCzz.js → SetupTab-QRUXa65y.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{SetupTab-CgV5xCzz.js.map → SetupTab-QRUXa65y.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CCNuJjyO.js → TimeSavedCard-p327ZZKT.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CCNuJjyO.js.map → TimeSavedCard-p327ZZKT.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/{TurnsTab-Cv1OCttJ.js → TurnsTab-BcmBSlik.js} +2 -2
  53. package/extensions/dashboard-client/dist/assets/{TurnsTab-Cv1OCttJ.js.map → TurnsTab-BcmBSlik.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/VectorCortexTab-D2fjKNyT.js +2 -0
  55. package/extensions/dashboard-client/dist/assets/VectorCortexTab-D2fjKNyT.js.map +1 -0
  56. package/extensions/dashboard-client/dist/assets/{WikiTab-B48QnQrK.js → WikiTab-DLXFMuNy.js} +2 -2
  57. package/extensions/dashboard-client/dist/assets/{WikiTab-B48QnQrK.js.map → WikiTab-DLXFMuNy.js.map} +1 -1
  58. package/extensions/dashboard-client/dist/assets/{button-BBQl2MiF.js → button-C9CghmRx.js} +2 -2
  59. package/extensions/dashboard-client/dist/assets/{button-BBQl2MiF.js.map → button-C9CghmRx.js.map} +1 -1
  60. package/extensions/dashboard-client/dist/assets/{card-BYbefUur.js → card-DWU-1y3t.js} +2 -2
  61. package/extensions/dashboard-client/dist/assets/{card-BYbefUur.js.map → card-DWU-1y3t.js.map} +1 -1
  62. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-swm0YZe0.js → generateCategoricalChart-Dayy9dGn.js} +2 -2
  63. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-swm0YZe0.js.map → generateCategoricalChart-Dayy9dGn.js.map} +1 -1
  64. package/extensions/dashboard-client/dist/assets/{index-DeIky7xm.js → index-D9PsW3VZ.js} +3 -3
  65. package/extensions/dashboard-client/dist/assets/{index-DeIky7xm.js.map → index-D9PsW3VZ.js.map} +1 -1
  66. package/extensions/dashboard-client/dist/assets/{switch-CctU78ww.js → switch-CnLFAmVT.js} +2 -2
  67. package/extensions/dashboard-client/dist/assets/{switch-CctU78ww.js.map → switch-CnLFAmVT.js.map} +1 -1
  68. package/extensions/dashboard-client/dist/assets/{toggle-BRCNCoiI.js → toggle-HN5C7_vy.js} +2 -2
  69. package/extensions/dashboard-client/dist/assets/{toggle-BRCNCoiI.js.map → toggle-HN5C7_vy.js.map} +1 -1
  70. package/extensions/dashboard-client/dist/assets/{useSSE-WfOiWDH_.js → useSSE-DoJfk3Ce.js} +2 -2
  71. package/extensions/dashboard-client/dist/assets/{useSSE-WfOiWDH_.js.map → useSSE-DoJfk3Ce.js.map} +1 -1
  72. package/extensions/dashboard-client/dist/index.html +1 -1
  73. package/extensions/dashboard-client/src/api/vector-cortex.ts +9 -0
  74. package/extensions/dashboard-client/src/tabs/VectorCortexRestoreCard.tsx +42 -0
  75. package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +8 -0
  76. package/extensions/dashboard-client/src/types/vector-cortex.ts +16 -0
  77. package/extensions/dashboard-server/api-contracts/vector-cortex-heal.ts +30 -0
  78. package/extensions/dashboard-server/api-contracts/vector-cortex.ts +4 -1
  79. package/extensions/dashboard-server/route-dispatch.ts +2 -0
  80. package/extensions/dashboard-server/routes-rag-settings-vector-cortex.ts +6 -0
  81. package/extensions/dashboard-server/routes-vector-cortex-heal.ts +51 -4
  82. package/extensions/dashboard-server/routes-vector-cortex.ts +6 -1
  83. package/extensions/dashboard-server/routes.ts +1 -0
  84. package/package.json +1 -1
  85. package/src/config/vector-cortex.ts +14 -0
  86. package/src/config.ts +1 -0
  87. package/src/vector-cortex/heal/_restore-fixture.ts +163 -0
  88. package/src/vector-cortex/heal/_vc6b-helpers.ts +55 -0
  89. package/src/vector-cortex/heal/restore-emit.ts +94 -0
  90. package/src/vector-cortex/heal/restore-readers.ts +145 -0
  91. package/src/vector-cortex/heal/restore-types.ts +185 -0
  92. package/src/vector-cortex/heal/restore.ts +165 -0
  93. package/src/vector-cortex/heal/types.ts +22 -0
  94. package/src/vector-cortex/heal/verify.ts +93 -0
  95. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DTabQo6h.js +0 -2
  96. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DTabQo6h.js.map +0 -1
@@ -374,4 +374,7 @@ export interface VectorCortexLedgerView {
374
374
  readonly updatedAt: string;
375
375
  }
376
376
 
377
- export type { VectorCortexClosureProofView } from "./vector-cortex-heal.js";
377
+ export type {
378
+ VectorCortexClosureProofView,
379
+ VectorCortexRestoreView,
380
+ } from "./vector-cortex-heal.js";
@@ -55,6 +55,7 @@ import {
55
55
  handleVectorCortexRender,
56
56
  handleVectorCortexRollout,
57
57
  handleVectorCortexClosureProof,
58
+ handleVectorCortexRestore,
58
59
  } from "./routes.js";
59
60
 
60
61
  /**
@@ -108,5 +109,6 @@ export function dispatchRoutes(
108
109
  if (handleVectorCortexRender(req, res, ctx)) return true;
109
110
  if (handleVectorCortexRollout(req, res, ctx)) return true;
110
111
  if (handleVectorCortexClosureProof(req, res, ctx)) return true;
112
+ if (handleVectorCortexRestore(req, res, ctx)) return true;
111
113
  return false;
112
114
  }
@@ -144,5 +144,11 @@ export const VECTOR_CORTEX_SETTINGS: SettingGroup = {
144
144
  "Advanced closure optimization: deterministically reduces the already-mandatory VC4C closure by transitive reduction over depends edges, emitting a ClosureProofV2 receipt per closure and a verifier that replays reductions against the conservative oracle (HEAL_PROOF_SET_MISMATCH on selected-set divergence). Protected edges (tool-pair / anchor / contradiction / sole-dependency) are never removed. The optimized selected set is byte-identical to the conservative closure. OFF = byte-identical predecessor (VC5C); arithmetic runs, only the reporter + dashboard seam is suppressed.",
145
145
  true,
146
146
  ),
147
+ boolDirect(
148
+ "MEGACOMPACT_VC6B",
149
+ "VC6B Exact Source Restoration",
150
+ "Exact source restoration: restores the original bytes of closure spans ONLY from an exact shard (mode A) or a verified ledger range scan (mode B) — never inferred, reconstructed, or paraphrased from embeddings or semantic text. Requests are hard-bounded at 64 spans / 4MiB (HEAL_RESTORE_LIMIT); every span's SHA-256 is recomputed and bytes are inserted only after all requested span metadata validates, so a single bad span fails the whole request closed (HEAL_RESTORE_DIGEST_MISMATCH, HEAL_RESTORE_RANGE_MISMATCH). When no exact source exists (HEAL_RESTORE_SOURCE_MISSING) the old context is omitted and the loss is disclosed (mode C) rather than filled in. OFF = byte-identical predecessor (VC6A).",
151
+ true,
152
+ ),
147
153
  ],
148
154
  };
@@ -1,6 +1,6 @@
1
1
  /**
2
- * dashboard-server/routes-vector-cortex-heal.ts — VC6A closure-optimization
3
- * dashboard route.
2
+ * dashboard-server/routes-vector-cortex-heal.ts — VC6A closure-optimization and
3
+ * VC6B exact-source-restoration dashboard routes.
4
4
  *
5
5
  * Reader-only GET /api/vector-cortex/closure-proof returning aggregate-only
6
6
  * closure diagnostics: whether the flag is enabled, the runtime triad mode, and
@@ -12,15 +12,22 @@
12
12
  * (the conservative VC4C closure, no reduction) — that is exactly the spec's
13
13
  * fallback rule — so `mode:"B"` + `enabled:false` is the byte-stable OFF view.
14
14
  *
15
+ * Reader-only GET /api/vector-cortex/restore (VC6B) returns restore COUNTS and
16
+ * HEAL_RESTORE_* error codes only. There is NO payload endpoint for restoration,
17
+ * ever — no restored bytes, span ids, node ids, byte ranges, or ledger text.
18
+ *
15
19
  * Guardrails: PREVENT-PI-004 (local in-process state only), PREVENT-011 (no
16
20
  * `any`), reader-only aggregate (counts + mode only).
17
21
  */
18
22
 
19
23
  import type { IncomingMessage, ServerResponse } from "node:http";
20
24
  import type { RouteContext } from "./routes-core.js";
21
- import { VC6A_ENABLED } from "../../src/config.js";
25
+ import { VC6A_ENABLED, VC6B_ENABLED } from "../../src/config.js";
22
26
  import { sendJson } from "./routes-vector-cortex-shared.js";
23
- import type { VectorCortexClosureProofView } from "./api-contracts/vector-cortex.js";
27
+ import type {
28
+ VectorCortexClosureProofView,
29
+ VectorCortexRestoreView,
30
+ } from "./api-contracts/vector-cortex.js";
24
31
 
25
32
  /** Reader-only GET /api/vector-cortex/closure-proof (VC6A). */
26
33
  export function handleVectorCortexClosureProof(
@@ -54,3 +61,43 @@ export function handleVectorCortexClosureProof(
54
61
  sendJson(res, 200, body);
55
62
  return true;
56
63
  }
64
+
65
+ /**
66
+ * Reader-only GET /api/vector-cortex/restore (VC6B).
67
+ *
68
+ * Counts and HEAL_RESTORE_* codes only — this is a static reader-only aggregate
69
+ * seam (same shape as the VC6A handler): there is no payload endpoint and never
70
+ * will be, so no restored bytes/span ids/node ids/ledger text can leak here.
71
+ */
72
+ export function handleVectorCortexRestore(
73
+ req: IncomingMessage,
74
+ res: ServerResponse,
75
+ _ctx: RouteContext,
76
+ ): boolean {
77
+ const url = req.url ?? "";
78
+ const path = url.split("?")[0] ?? url;
79
+ if (path !== "/api/vector-cortex/restore") return false;
80
+ if (req.method !== "GET") {
81
+ sendJson(res, 405, { error: "method_not_allowed" });
82
+ return true;
83
+ }
84
+
85
+ const enabled = VC6B_ENABLED();
86
+ // Flag-off routes to mode C: with VC6B off there is no exact-restoration path
87
+ // at all, so the honest OFF view is the disclose-loss mode (old context is
88
+ // omitted rather than inferred) — mirroring how VC6A's OFF view reports the
89
+ // conservative fallback it actually takes.
90
+ const mode: "A" | "B" | "C" = enabled ? "A" : "C";
91
+ const body: VectorCortexRestoreView = {
92
+ enabled,
93
+ mode,
94
+ restoreAttempts: 0,
95
+ restoredCount: 0,
96
+ missingCount: 0,
97
+ digestRejections: 0,
98
+ lastRejection: null,
99
+ updatedAt: new Date().toISOString(),
100
+ };
101
+ sendJson(res, 200, body);
102
+ return true;
103
+ }
@@ -9,6 +9,8 @@
9
9
  * VC0C health/reset-> routes-vector-cortex-health.ts(GET /health, POST /breakers/reset)
10
10
  * VC1B ledger -> routes-vector-cortex-ledger.ts (GET /ledger)
11
11
  * VC3A topology -> routes-vector-cortex-topology.ts(GET /topology)
12
+ * VC6A closure -> routes-vector-cortex-heal.ts (GET /closure-proof)
13
+ * VC6B restore -> routes-vector-cortex-heal.ts (GET /restore)
12
14
  *
13
15
  * Guardrails: PREVENT-PI-004 (local filesystem read only), PREVENT-011 (no
14
16
  * `any`), reader-only aggregates (never payloads/prompts/ledger text).
@@ -25,4 +27,7 @@ export { handleVectorCortexReconstruct } from "./routes-vector-cortex-reconstruc
25
27
  export { handleVectorCortexPlans } from "./routes-vector-cortex-plans.js";
26
28
  export { handleVectorCortexRender } from "./routes-vector-cortex-render.js";
27
29
  export { handleVectorCortexRollout } from "./routes-vector-cortex-rollout.js";
28
- export { handleVectorCortexClosureProof } from "./routes-vector-cortex-heal.js";
30
+ export {
31
+ handleVectorCortexClosureProof,
32
+ handleVectorCortexRestore,
33
+ } from "./routes-vector-cortex-heal.js";
@@ -54,4 +54,5 @@ export {
54
54
  handleVectorCortexRender,
55
55
  handleVectorCortexRollout,
56
56
  handleVectorCortexClosureProof,
57
+ handleVectorCortexRestore,
57
58
  } from "./routes-vector-cortex.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.20.8",
3
+ "version": "0.20.9",
4
4
  "description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
5
5
  "type": "module",
6
6
  "license": "BSD-3-Clause",
@@ -227,6 +227,20 @@ export const VC5C_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC5C");
227
227
  */
228
228
  export const VC6A_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC6A");
229
229
 
230
+ /**
231
+ * VC6B — exact source restoration. Default ON. `MEGACOMPACT_VC6B=0` disables and
232
+ * is byte-identical to the predecessor (VC6A): the `restoreSources` /
233
+ * `verifyRestored` arithmetic STILL RUNS (it is PURE — an exact-source read plus
234
+ * a SHA-256 comparison, with no clock, storage, or network), but the
235
+ * `vector_cortex_source_restored` / `vector_cortex_restore_digest_rejected`
236
+ * events are never emitted and the dashboard restoration diagnostics seam is
237
+ * suppressed. Flag OFF gates the reporter + dashboard seam, never the
238
+ * restore/verify arithmetic, so flag-off outbound/predecessor golden bytes match
239
+ * exactly. This flag MUST also be a dashboard SETTINGS toggle (visible in config
240
+ * UI, never in EXCLUDED_SETTINGS), mirroring VC4A/VC4B/VC4C/VC5A/VC5B/VC5C/VC6A.
241
+ */
242
+ export const VC6B_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC6B");
243
+
230
244
  // ---------------------------------------------------------------------------
231
245
  // Breaker state machine constants (TRIAD_RESILIENCE.md §breaker).
232
246
  // Rolled numbers for one 60s window; VC0C consumes these at its breaker seam.
package/src/config.ts CHANGED
@@ -170,6 +170,7 @@ export {
170
170
  VC5B_ENABLED,
171
171
  VC5C_ENABLED,
172
172
  VC6A_ENABLED,
173
+ VC6B_ENABLED,
173
174
  BREAKER_WINDOW_MS,
174
175
  BREAKER_MIN_ATTEMPTS,
175
176
  BREAKER_PERF_FAILURES,
@@ -0,0 +1,163 @@
1
+ /**
2
+ * heal/_restore-fixture.ts — conformance fixture I/O for VC6B restoration rows.
3
+ *
4
+ * Sibling of `_acceptance-fixture.ts` (which owns the VC6A closure-optimization
5
+ * rows); split out so neither file approaches the 300-line soft limit and so the
6
+ * base64/BigInt decoding lives next to the contract it reconstitutes.
7
+ *
8
+ * DECODING IS THE POINT. Fixtures are canonical JSON, which cannot express bytes
9
+ * or bigints, so the corpus stores `originalBytesBase64` + numeric `seq`. These
10
+ * loaders turn those back into the REAL `ExactShardV1` / `EventV2` objects the
11
+ * production readers consume — no mocks, no stubs, no parallel "test shape". If
12
+ * the decode were lossy the digests would not match and the acceptance test would
13
+ * fail loudly, which is exactly the guarantee we want from a byte-identity sprint.
14
+ */
15
+
16
+ import { readFileSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import assert from "node:assert/strict";
19
+
20
+ import type { EventV2, ExactShardV1, ShardRange } from "./restore-types.js";
21
+ import { V2, readManifest } from "./_acceptance-fixture.js";
22
+
23
+ /** A `ShardRange` as it appears in JSON: `seq` bounds are numbers, not bigints. */
24
+ export interface RestoreFxRange {
25
+ sessionId: string;
26
+ seqStart: number;
27
+ seqEnd: number;
28
+ byteStart: number;
29
+ byteEnd: number;
30
+ }
31
+
32
+ export interface RestoreFxSpan {
33
+ nodeId: string;
34
+ range: RestoreFxRange;
35
+ digest: string;
36
+ }
37
+
38
+ export interface RestoreFxShard {
39
+ sessionId: string;
40
+ range: RestoreFxRange;
41
+ originalBytesBase64: string;
42
+ digest: string;
43
+ byteCount: number;
44
+ case: "tool-pair" | "anchor" | "invalid-utf8" | "anchor+invalid";
45
+ }
46
+
47
+ export interface RestoreFxEvent {
48
+ sessionId: string;
49
+ seq: number;
50
+ eventId: string;
51
+ role: "policy" | "user" | "assistant" | "tool";
52
+ kind: string;
53
+ originalBytesBase64: string;
54
+ bytesDigest: string;
55
+ occurredAtMs: number;
56
+ toolCallId?: string;
57
+ }
58
+
59
+ export interface RestoreFxInput {
60
+ scenario: string;
61
+ sessionId: string;
62
+ request: { spans: RestoreFxSpan[] };
63
+ exactShards: RestoreFxShard[];
64
+ ledgerEvents: RestoreFxEvent[];
65
+ }
66
+
67
+ export interface RestoreFxExpected {
68
+ ok: boolean;
69
+ code?: string;
70
+ restoredCount: number;
71
+ missingCount: number;
72
+ mode: "A" | "B" | "C";
73
+ }
74
+
75
+ export interface RestoreFx {
76
+ id: string;
77
+ schema: string;
78
+ producer: string;
79
+ assertion: string;
80
+ kind: string;
81
+ input: RestoreFxInput;
82
+ expected: RestoreFxExpected;
83
+ }
84
+
85
+ /** Read one registered restoration fixture (asserting it IS registered). */
86
+ export function restorationFixture(id: string): RestoreFx {
87
+ const m = readManifest();
88
+ const row = m.fixtures.find((f) => f.id === id && f.path.startsWith("restoration/"));
89
+ assert.ok(row, `fixture ${id} registered under restoration/ in manifest`);
90
+ return JSON.parse(readFileSync(join(V2, row!.path), "utf8")) as RestoreFx;
91
+ }
92
+
93
+ /** base64 -> exact bytes, byte-for-byte (invalid UTF-8 included). */
94
+ export function decodeBytes(base64: string): Uint8Array {
95
+ return new Uint8Array(Buffer.from(base64, "base64"));
96
+ }
97
+
98
+ /** JSON number seq bounds -> the bigint bounds `ShardRange` declares. */
99
+ export function decodeRange(r: RestoreFxRange): ShardRange {
100
+ return {
101
+ sessionId: r.sessionId,
102
+ seqStart: BigInt(r.seqStart),
103
+ seqEnd: BigInt(r.seqEnd),
104
+ byteStart: r.byteStart,
105
+ byteEnd: r.byteEnd,
106
+ };
107
+ }
108
+
109
+ /** Reconstitute a real `ExactShardV1` from its fixture row. */
110
+ export function decodeShard(s: RestoreFxShard): ExactShardV1 {
111
+ return {
112
+ schema: "exact-shard-v1",
113
+ sessionId: s.sessionId,
114
+ range: decodeRange(s.range),
115
+ kind: "exact",
116
+ originalBytes: decodeBytes(s.originalBytesBase64),
117
+ digest: s.digest,
118
+ byteCount: s.byteCount,
119
+ case: s.case,
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Reconstitute a real `EventV2`. `utf8` is re-derived by strict classification
125
+ * (never lossy replacement) so an invalid-UTF-8 fixture produces the
126
+ * `{valid:false, base64}` discriminant the VC1A contract requires.
127
+ */
128
+ export function decodeEvent(e: RestoreFxEvent): EventV2 {
129
+ const bytes = decodeBytes(e.originalBytesBase64);
130
+ let utf8: EventV2["utf8"];
131
+ try {
132
+ utf8 = { valid: true, text: new TextDecoder("utf-8", { fatal: true }).decode(bytes) };
133
+ } catch {
134
+ utf8 = { valid: false, base64: e.originalBytesBase64 };
135
+ }
136
+ return {
137
+ schema: "event-v2",
138
+ sessionId: e.sessionId,
139
+ seq: BigInt(e.seq),
140
+ eventId: e.eventId,
141
+ role: e.role,
142
+ kind: e.kind,
143
+ originalBytes: bytes,
144
+ bytesDigest: e.bytesDigest as EventV2["bytesDigest"],
145
+ utf8,
146
+ ...(e.toolCallId !== undefined ? { toolCallId: e.toolCallId } : {}),
147
+ occurredAtMs: BigInt(e.occurredAtMs),
148
+ };
149
+ }
150
+
151
+ /** Flag-pinned wrapper: VC6B gated by MEGACOMPACT_VC6B (defaults ON). */
152
+ export function withVc6bFlagsOn(fn: () => void): () => void {
153
+ return (): void => {
154
+ const saved = process.env.MEGACOMPACT_VC6B;
155
+ process.env.MEGACOMPACT_VC6B = "1";
156
+ try {
157
+ fn();
158
+ } finally {
159
+ if (saved === undefined) delete process.env.MEGACOMPACT_VC6B;
160
+ else process.env.MEGACOMPACT_VC6B = saved;
161
+ }
162
+ };
163
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * heal/_vc6b-helpers.ts — shared decode/run helpers for VC6B acceptance tests.
3
+ *
4
+ * Extracted from vc6b-acceptance.test.ts so no single test file crosses the
5
+ * 300-line soft limit (soft-as-hard gate). Each sibling test file imports from
6
+ * here rather than duplicating the decode/runReal logic.
7
+ */
8
+
9
+ import { createHash } from "node:crypto";
10
+
11
+ import type {
12
+ RestoreReader,
13
+ RestoreRequestV1,
14
+ RestoreResultV1,
15
+ } from "./restore-types.js";
16
+ import { restoreSources } from "./restore.js";
17
+ import { verifyRestored } from "./verify.js";
18
+ import type { RestoreFx } from "./_restore-fixture.js";
19
+ import { decodeRange, decodeShard, decodeEvent } from "./_restore-fixture.js";
20
+
21
+ export const enc = (s: string): Uint8Array => new Uint8Array(Buffer.from(s));
22
+ export const hex = (b: Uint8Array): string =>
23
+ createHash("sha256").update(b).digest("hex");
24
+
25
+ export function decodeFx(fx: RestoreFx): {
26
+ request: RestoreRequestV1;
27
+ reader: RestoreReader;
28
+ } {
29
+ return {
30
+ request: {
31
+ schema: "restore-request-v1",
32
+ sessionId: fx.input.sessionId,
33
+ spans: fx.input.request.spans.map((s) => ({
34
+ nodeId: s.nodeId,
35
+ range: decodeRange(s.range),
36
+ digest: s.digest,
37
+ })),
38
+ },
39
+ reader: {
40
+ exactShards: fx.input.exactShards.map(decodeShard),
41
+ ledgerEvents: fx.input.ledgerEvents.map(decodeEvent),
42
+ },
43
+ };
44
+ }
45
+
46
+ export function runReal(fx: RestoreFx): {
47
+ request: RestoreRequestV1;
48
+ reader: RestoreReader;
49
+ result: RestoreResultV1;
50
+ verification: ReturnType<typeof verifyRestored>;
51
+ } {
52
+ const { request, reader } = decodeFx(fx);
53
+ const result = restoreSources(request, reader);
54
+ return { request, reader, result, verification: verifyRestored(result, request) };
55
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * vector-cortex/heal/restore-emit.ts — VC6B event reporter seam.
3
+ *
4
+ * Mirrors `./emit.ts`: a thin `safe()` wrapper around an optional injected
5
+ * `emit` (unit tests pass `undefined` and stay pure), and the two event names the
6
+ * sprint spec requires verbatim:
7
+ * - `vector_cortex_source_restored` — a restore batch completed.
8
+ * - `vector_cortex_restore_digest_rejected` — a source failed its digest check.
9
+ *
10
+ * FLAG SEMANTICS. `restoreSources` / `verifyRestored` are PURE arithmetic and run
11
+ * REGARDLESS of `MEGACOMPACT_VC6B`. The flag gates ONLY this reporting +
12
+ * dashboard seam: with the flag off we still restore and still verify, we just do
13
+ * not announce it under the VC6B event namespace. That is what makes flag-off
14
+ * byte-identical to VC6A — the computation is never skipped, only the emission.
15
+ *
16
+ * PAYLOAD DISCIPLINE. These events carry COUNTS and MODES only — never restored
17
+ * bytes, never node text, never a digest of user content. The exact ledger is not
18
+ * diagnostic data (SECURITY_PRIVACY), and a restoration event is exactly the
19
+ * place where an unguarded `payload` field would leak the entire transcript into
20
+ * a log file.
21
+ *
22
+ * No console, no storage, no network (PREVENT-PI-004 / PREVENT-011). Every line
23
+ * is a structured JSON event with `ts` + `event`.
24
+ */
25
+
26
+ import { VC6B_ENABLED } from "../../config/vector-cortex.js";
27
+ import type { RestoreEventName, RestoreFailureCode } from "./restore-types.js";
28
+
29
+ /** Optional emit fn injected by the runtime; tests pass `undefined`. */
30
+ export type RestoreEmit = (name: string, payload: unknown) => void;
31
+
32
+ /** Run `fn` only when an emit exists; a reporting failure is never fatal. */
33
+ function safe(emit: RestoreEmit | undefined, fn: (emit: RestoreEmit) => void): void {
34
+ if (emit === undefined) return;
35
+ try {
36
+ fn(emit);
37
+ } catch {
38
+ // Non-fatal: a reporting failure must never break the agent loop.
39
+ }
40
+ }
41
+
42
+ /** The event names VC6B emits, exported for the dashboard seam and tests. */
43
+ export const RESTORE_EVENT_NAMES: readonly RestoreEventName[] = [
44
+ "vector_cortex_source_restored",
45
+ "vector_cortex_restore_digest_rejected",
46
+ ] as const;
47
+
48
+ /**
49
+ * Report a completed restoration. Counts and mode only — enough to see whether
50
+ * the shard index is serving reads (mode A) or every span is falling through to
51
+ * a ledger scan (mode B), without disclosing what was restored.
52
+ */
53
+ export function reportSourceRestored(
54
+ emit: RestoreEmit | undefined,
55
+ payload: {
56
+ readonly sessionId: string;
57
+ readonly restoredCount: number;
58
+ readonly missingCount: number;
59
+ readonly mode: "A" | "B" | "C";
60
+ },
61
+ ): void {
62
+ if (!VC6B_ENABLED()) return;
63
+ safe(emit, (e) =>
64
+ e("vector_cortex_source_restored", {
65
+ ts: undefined,
66
+ event: "vector_cortex_source_restored",
67
+ sessionId: payload.sessionId,
68
+ restoredCount: payload.restoredCount,
69
+ missingCount: payload.missingCount,
70
+ mode: payload.mode,
71
+ }),
72
+ );
73
+ }
74
+
75
+ /** Report a rejected source (digest/limit/range). Suppressed under flag-off. */
76
+ export function reportRestoreDigestRejected(
77
+ emit: RestoreEmit | undefined,
78
+ payload: {
79
+ readonly sessionId: string;
80
+ readonly code: RestoreFailureCode;
81
+ readonly mode: "A" | "B" | "C";
82
+ },
83
+ ): void {
84
+ if (!VC6B_ENABLED()) return;
85
+ safe(emit, (e) =>
86
+ e("vector_cortex_restore_digest_rejected", {
87
+ ts: undefined,
88
+ event: "vector_cortex_restore_digest_rejected",
89
+ sessionId: payload.sessionId,
90
+ code: payload.code,
91
+ mode: payload.mode,
92
+ }),
93
+ );
94
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * vector-cortex/heal/restore-readers.ts — the two EXACT source readers (VC6B).
3
+ *
4
+ * Split out of `restore.ts` so the orchestrator stays a short, readable policy
5
+ * file and each reader can be reasoned about on its own (PRACTICES: split at the
6
+ * 300-line soft limit, delegate-shell + impl).
7
+ *
8
+ * Both readers share one discipline: they return bytes ONLY after recomputing
9
+ * SHA-256 over the bytes they actually hold and comparing it to the digest the
10
+ * REQUEST pinned. Neither reader trusts the digest recorded alongside its own
11
+ * source — an `ExactShardV1.digest` is metadata that lives in the same file as
12
+ * the bytes, so a swapped file carries a matching (wrong) pair. Only the
13
+ * caller-supplied digest is authoritative, and only a fresh hash of the fetched
14
+ * bytes can be checked against it.
15
+ *
16
+ * Pure/deterministic/local: `node:crypto` only (a Node built-in, not a network
17
+ * call), no storage, no console (PREVENT-PI-004 / PREVENT-011).
18
+ */
19
+
20
+ import { createHash } from "node:crypto";
21
+
22
+ import type { EventV2, ExactShardV1, ShardRange } from "./restore-types.js";
23
+
24
+ /**
25
+ * SHA-256 over bytes as LOWERCASE HEX with NO `sha256:` prefix — the
26
+ * `ExactShardV1.digest` / `ReconstructionSpan.digest` / `RestoreSpanRequest`
27
+ * convention. `EventV2.bytesDigest` prefixes this same value with `sha256:`.
28
+ */
29
+ export function sha256Hex(bytes: Uint8Array): string {
30
+ return createHash("sha256").update(bytes).digest("hex");
31
+ }
32
+
33
+ /**
34
+ * Strip the `sha256:` prefix from an `EventV2.bytesDigest` so it can be compared
35
+ * against a bare hex digest. A value that does not carry the prefix is returned
36
+ * unchanged rather than mangled — the comparison then simply fails, which is the
37
+ * correct outcome for a malformed record.
38
+ */
39
+ export function bareHex(digest: string): string {
40
+ return digest.startsWith("sha256:") ? digest.slice("sha256:".length) : digest;
41
+ }
42
+
43
+ /**
44
+ * Structural range equality. All five components must match: two spans over the
45
+ * same seq window but different byte offsets are DIFFERENT spans (a re-encoded
46
+ * or re-offset stream is not the stream that was requested).
47
+ */
48
+ export function rangeEquals(a: ShardRange, b: ShardRange): boolean {
49
+ return (
50
+ a.sessionId === b.sessionId &&
51
+ a.seqStart === b.seqStart &&
52
+ a.seqEnd === b.seqEnd &&
53
+ a.byteStart === b.byteStart &&
54
+ a.byteEnd === b.byteEnd
55
+ );
56
+ }
57
+
58
+ /** What a reader attempt produced: bytes, a stated failure, or simply nothing. */
59
+ export type ReadOutcome =
60
+ /** Bytes fetched AND verified against the request digest. */
61
+ | { readonly kind: "bytes"; readonly bytes: Uint8Array }
62
+ /** A source existed but its bytes did not hash to the pinned digest. */
63
+ | { readonly kind: "digest-mismatch" }
64
+ /** No source of this tier covers the span; try the next tier. */
65
+ | { readonly kind: "absent" };
66
+
67
+ /**
68
+ * Mode A — indexed exact-shard read.
69
+ *
70
+ * Selection is by RANGE identity plus the shard's own recorded digest, which
71
+ * narrows the candidate set the way a real index lookup would. The accepted
72
+ * candidate is then re-hashed from its `originalBytes`: this second check is
73
+ * what catches a shard whose bytes were swapped AFTER the index lookup resolved
74
+ * (the sprint's unique failure injection). A candidate that matches by range but
75
+ * fails either digest check reports `digest-mismatch` — it does NOT silently fall
76
+ * through to the ledger, because a corrupt exact shard is a fact worth surfacing.
77
+ */
78
+ export function readExactShard(
79
+ shards: readonly ExactShardV1[],
80
+ range: ShardRange,
81
+ digest: string,
82
+ ): ReadOutcome {
83
+ const byRange = shards.filter((s) => rangeEquals(s.range, range));
84
+ if (byRange.length === 0) return { kind: "absent" };
85
+
86
+ const candidate = byRange.find((s) => s.digest === digest);
87
+ if (candidate === undefined) return { kind: "digest-mismatch" };
88
+
89
+ // Defense in depth: the shard's recorded digest is metadata, the bytes are the
90
+ // authority. Re-hash what we actually hold.
91
+ const bytes = candidate.originalBytes;
92
+ if (sha256Hex(bytes) !== digest) return { kind: "digest-mismatch" };
93
+ return { kind: "bytes", bytes };
94
+ }
95
+
96
+ /**
97
+ * Mode B — ledger range scan.
98
+ *
99
+ * An INDEPENDENT path from mode A: no shard index is consulted. The occurrence
100
+ * records covering `[seqStart..seqEnd]` are selected, sorted ascending by seq
101
+ * (the caller's array order is untrusted — a scan that concatenated records in
102
+ * arrival order would produce plausible-looking but wrong bytes), each record's
103
+ * own `bytesDigest` is verified, and the concatenation is hashed against the
104
+ * span digest.
105
+ *
106
+ * Both checks matter. Per-record verification localizes corruption to a single
107
+ * occurrence; the span-level hash catches a scan that is individually valid but
108
+ * collectively wrong — a missing record in the middle of the range, or a range
109
+ * that covers different content than the requester believed.
110
+ */
111
+ export function readLedgerSpan(
112
+ events: readonly EventV2[],
113
+ range: ShardRange,
114
+ digest: string,
115
+ ): ReadOutcome {
116
+ const covering = events
117
+ .filter(
118
+ (e) =>
119
+ e.sessionId === range.sessionId &&
120
+ e.seq >= range.seqStart &&
121
+ e.seq <= range.seqEnd,
122
+ )
123
+ .sort((a, b) => (a.seq < b.seq ? -1 : a.seq > b.seq ? 1 : 0));
124
+
125
+ if (covering.length === 0) return { kind: "absent" };
126
+
127
+ // Per-record byte authority (VC1A): a record whose own digest disagrees with
128
+ // its bytes is corrupt, and a corrupt record can never contribute to a restore.
129
+ for (const e of covering) {
130
+ if (sha256Hex(e.originalBytes) !== bareHex(e.bytesDigest)) {
131
+ return { kind: "digest-mismatch" };
132
+ }
133
+ }
134
+
135
+ const total = covering.reduce((sum, e) => sum + e.originalBytes.length, 0);
136
+ const bytes = new Uint8Array(total);
137
+ let offset = 0;
138
+ for (const e of covering) {
139
+ bytes.set(e.originalBytes, offset);
140
+ offset += e.originalBytes.length;
141
+ }
142
+
143
+ if (sha256Hex(bytes) !== digest) return { kind: "digest-mismatch" };
144
+ return { kind: "bytes", bytes };
145
+ }