pi-mega-compact 0.20.9 → 0.20.10

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 (87) hide show
  1. package/dist/config/vector-cortex.js +16 -0
  2. package/dist/config.js +1 -1
  3. package/dist/extensions/dashboard-server/route-dispatch.js +5 -0
  4. package/dist/extensions/dashboard-server/routes-rag-settings-vector-cortex.js +1 -0
  5. package/dist/extensions/dashboard-server/routes-vector-cortex-repair.js +63 -0
  6. package/dist/src/config/vector-cortex.js +16 -0
  7. package/dist/src/config.js +1 -1
  8. package/dist/src/vector-cortex/heal/_repair-fixture.js +70 -0
  9. package/dist/src/vector-cortex/heal/controller.js +165 -0
  10. package/dist/src/vector-cortex/heal/rebuild.js +136 -0
  11. package/dist/src/vector-cortex/heal/repair-emit.js +83 -0
  12. package/dist/src/vector-cortex/heal/repair-types.js +74 -0
  13. package/dist/src/vector-cortex/heal/types.js +1 -0
  14. package/dist/vector-cortex/heal/_repair-fixture.js +70 -0
  15. package/dist/vector-cortex/heal/controller.js +165 -0
  16. package/dist/vector-cortex/heal/rebuild.js +136 -0
  17. package/dist/vector-cortex/heal/repair-emit.js +83 -0
  18. package/dist/vector-cortex/heal/repair-types.js +74 -0
  19. package/dist/vector-cortex/heal/types.js +1 -0
  20. package/extensions/dashboard-client/dist/assets/{AreaChart-fxyq2KaH.js → AreaChart-BDMjyRQp.js} +2 -2
  21. package/extensions/dashboard-client/dist/assets/{AreaChart-fxyq2KaH.js.map → AreaChart-BDMjyRQp.js.map} +1 -1
  22. package/extensions/dashboard-client/dist/assets/{BarChart-D5EqAINP.js → BarChart-CqzPzOkj.js} +2 -2
  23. package/extensions/dashboard-client/dist/assets/{BarChart-D5EqAINP.js.map → BarChart-CqzPzOkj.js.map} +1 -1
  24. package/extensions/dashboard-client/dist/assets/{CacheTab-D4UWbzLo.js → CacheTab-Bo8SH9q8.js} +2 -2
  25. package/extensions/dashboard-client/dist/assets/{CacheTab-D4UWbzLo.js.map → CacheTab-Bo8SH9q8.js.map} +1 -1
  26. package/extensions/dashboard-client/dist/assets/{EventsTab-BiXquGrX.js → EventsTab-DIEs6zC-.js} +2 -2
  27. package/extensions/dashboard-client/dist/assets/{EventsTab-BiXquGrX.js.map → EventsTab-DIEs6zC-.js.map} +1 -1
  28. package/extensions/dashboard-client/dist/assets/{HealthTab-D5QvoTjy.js → HealthTab-3a8IjwIK.js} +2 -2
  29. package/extensions/dashboard-client/dist/assets/{HealthTab-D5QvoTjy.js.map → HealthTab-3a8IjwIK.js.map} +1 -1
  30. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-Cy_-CWgM.js → MaintenanceTab-CdKwAcXo.js} +2 -2
  31. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-Cy_-CWgM.js.map → MaintenanceTab-CdKwAcXo.js.map} +1 -1
  32. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DrAjgGOR.js → MemoryMapTab-D2hHuNj-.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DrAjgGOR.js.map → MemoryMapTab-D2hHuNj-.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/{MetricsTab-BvPHcuFN.js → MetricsTab-CFhIhqrr.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{MetricsTab-BvPHcuFN.js.map → MetricsTab-CFhIhqrr.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{OverviewTab-DsbIigOQ.js → OverviewTab-Dzp5ZoiA.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{OverviewTab-DsbIigOQ.js.map → OverviewTab-Dzp5ZoiA.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{ReposTab-7v_uZX_a.js → ReposTab-CFJT9mDR.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{ReposTab-7v_uZX_a.js.map → ReposTab-CFJT9mDR.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/{SessionsTab-BXGrv-6C.js → SessionsTab-BhP1z6_y.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{SessionsTab-BXGrv-6C.js.map → SessionsTab-BhP1z6_y.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/{SetupTab-QRUXa65y.js → SetupTab-2Glh7MKk.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{SetupTab-QRUXa65y.js.map → SetupTab-2Glh7MKk.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-p327ZZKT.js → TimeSavedCard-DmwBDhkw.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-p327ZZKT.js.map → TimeSavedCard-DmwBDhkw.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{TurnsTab-BcmBSlik.js → TurnsTab-CYIaOUUZ.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{TurnsTab-BcmBSlik.js.map → TurnsTab-CYIaOUUZ.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DD7vGaRS.js +2 -0
  49. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DD7vGaRS.js.map +1 -0
  50. package/extensions/dashboard-client/dist/assets/{WikiTab-DLXFMuNy.js → WikiTab-CP-JEd17.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{WikiTab-DLXFMuNy.js.map → WikiTab-CP-JEd17.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/{button-C9CghmRx.js → button-B1RhLsGs.js} +2 -2
  53. package/extensions/dashboard-client/dist/assets/{button-C9CghmRx.js.map → button-B1RhLsGs.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/{card-DWU-1y3t.js → card-BLPT2-8G.js} +2 -2
  55. package/extensions/dashboard-client/dist/assets/{card-DWU-1y3t.js.map → card-BLPT2-8G.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-Dayy9dGn.js → generateCategoricalChart-DVOMJzL2.js} +2 -2
  57. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-Dayy9dGn.js.map → generateCategoricalChart-DVOMJzL2.js.map} +1 -1
  58. package/extensions/dashboard-client/dist/assets/{index-D9PsW3VZ.js → index-CuLdiHRl.js} +3 -3
  59. package/extensions/dashboard-client/dist/assets/{index-D9PsW3VZ.js.map → index-CuLdiHRl.js.map} +1 -1
  60. package/extensions/dashboard-client/dist/assets/{switch-CnLFAmVT.js → switch-C5tqzhcl.js} +2 -2
  61. package/extensions/dashboard-client/dist/assets/{switch-CnLFAmVT.js.map → switch-C5tqzhcl.js.map} +1 -1
  62. package/extensions/dashboard-client/dist/assets/{toggle-HN5C7_vy.js → toggle-HcM6W2Yl.js} +2 -2
  63. package/extensions/dashboard-client/dist/assets/{toggle-HN5C7_vy.js.map → toggle-HcM6W2Yl.js.map} +1 -1
  64. package/extensions/dashboard-client/dist/assets/{useSSE-DoJfk3Ce.js → useSSE-BtWUs2kL.js} +2 -2
  65. package/extensions/dashboard-client/dist/assets/{useSSE-DoJfk3Ce.js.map → useSSE-BtWUs2kL.js.map} +1 -1
  66. package/extensions/dashboard-client/dist/index.html +1 -1
  67. package/extensions/dashboard-client/src/api/vector-cortex.ts +9 -0
  68. package/extensions/dashboard-client/src/tabs/VectorCortexRepairCard.tsx +43 -0
  69. package/extensions/dashboard-client/src/tabs/VectorCortexShardsCard.tsx +89 -0
  70. package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +14 -60
  71. package/extensions/dashboard-client/src/types/vector-cortex.ts +17 -0
  72. package/extensions/dashboard-server/api-contracts/vector-cortex-heal.ts +38 -0
  73. package/extensions/dashboard-server/api-contracts/vector-cortex.ts +1 -0
  74. package/extensions/dashboard-server/route-dispatch.ts +4 -0
  75. package/extensions/dashboard-server/routes-rag-settings-vector-cortex.ts +6 -0
  76. package/extensions/dashboard-server/routes-vector-cortex-repair.ts +72 -0
  77. package/package.json +1 -1
  78. package/src/config/vector-cortex.ts +17 -0
  79. package/src/config.ts +1 -0
  80. package/src/vector-cortex/heal/_repair-fixture.ts +138 -0
  81. package/src/vector-cortex/heal/controller.ts +189 -0
  82. package/src/vector-cortex/heal/rebuild.ts +200 -0
  83. package/src/vector-cortex/heal/repair-emit.ts +118 -0
  84. package/src/vector-cortex/heal/repair-types.ts +227 -0
  85. package/src/vector-cortex/heal/types.ts +28 -0
  86. package/extensions/dashboard-client/dist/assets/VectorCortexTab-D2fjKNyT.js +0 -2
  87. package/extensions/dashboard-client/dist/assets/VectorCortexTab-D2fjKNyT.js.map +0 -1
@@ -19,6 +19,7 @@ import {
19
19
  fetchVectorCortexRollout,
20
20
  fetchVectorCortexClosureProof,
21
21
  fetchVectorCortexRestore,
22
+ fetchVectorCortexRepair,
22
23
  fetchVectorCortexShards,
23
24
  fetchVectorCortexTopology,
24
25
  resetVectorCortexBreaker,
@@ -32,6 +33,7 @@ import {
32
33
  type VectorCortexRolloutView,
33
34
  type VectorCortexClosureProofView,
34
35
  type VectorCortexRestoreView,
36
+ type VectorCortexRepairView,
35
37
  type VectorCortexShardsView,
36
38
  type VectorCortexTopologyView,
37
39
  } from "../api/vector-cortex";
@@ -42,6 +44,11 @@ import { VectorCortexRenderCard } from "./VectorCortexRenderCard";
42
44
  import { VectorCortexRolloutCard } from "./VectorCortexRolloutCard";
43
45
  import { VectorCortexClosureCard } from "./VectorCortexClosureCard";
44
46
  import { VectorCortexRestoreCard } from "./VectorCortexRestoreCard";
47
+ import { VectorCortexRepairCard } from "./VectorCortexRepairCard";
48
+ import {
49
+ VectorCortexShardsCard,
50
+ VectorCortexReconstructCard,
51
+ } from "./VectorCortexShardsCard";
45
52
  import { VectorCortexPlansCard } from "./VectorCortexPlansCard";
46
53
  import { VectorCortexTopologyCard } from "./VectorCortexTopologyCard";
47
54
  import { VectorCortexLedgerCard } from "./VectorCortexLedgerCard";
@@ -71,6 +78,7 @@ export default function VectorCortexTab(): React.ReactElement {
71
78
  const [rollout, setRollout] = useState<VectorCortexRolloutView | null>(null);
72
79
  const [closureProof, setClosureProof] = useState<VectorCortexClosureProofView | null>(null);
73
80
  const [restore, setRestore] = useState<VectorCortexRestoreView | null>(null);
81
+ const [repair, setRepair] = useState<VectorCortexRepairView | null>(null);
74
82
 
75
83
  const poll = useCallback(() => {
76
84
  fetchVectorCortexEvaluation()
@@ -112,6 +120,9 @@ export default function VectorCortexTab(): React.ReactElement {
112
120
  fetchVectorCortexRestore().then(setRestore).catch(() => {
113
121
  /* exact-source-restoration card is best-effort (VC6B) */
114
122
  });
123
+ fetchVectorCortexRepair().then(setRepair).catch(() => {
124
+ /* self-healing derived-state card is best-effort (VC6C) */
125
+ });
115
126
  }, []);
116
127
 
117
128
  useEffect(() => {
@@ -263,71 +274,14 @@ export default function VectorCortexTab(): React.ReactElement {
263
274
  </CardContent>
264
275
  </Card>
265
276
  <VectorCortexTopologyCard topology={topology} query={query} />
266
- <Card>
267
- <CardHeader>
268
- <div className="flex items-center justify-between">
269
- <CardTitle>Dual-Tier Shards (VC4A)</CardTitle>
270
- {shards?.enabled ? (
271
- <Badge variant="success">ACTIVE</Badge>
272
- ) : (
273
- <Badge variant="danger">OFF</Badge>
274
- )}
275
- </div>
276
- </CardHeader>
277
- <CardContent>
278
- {!shards?.enabled ? (
279
- <div className="vc-empty">Dual-tier shards disabled (VC4A off).</div>
280
- ) : (
281
- <>
282
- <div className="grid grid-cols-2 gap-4 md:grid-cols-4">
283
- <Metric label="Semantic shards" value={String(shards.semanticCount)} />
284
- <Metric label="Exact shards" value={String(shards.exactCount)} />
285
- <Metric label="Byte total" value={String(shards.byteTotal)} />
286
- <Metric label="Protected bytes" value={String(shards.protectedByteTotal)} />
287
- </div>
288
- <div className="mt-3 text-xs text-muted-foreground">
289
- Reader-only count/byte aggregate; staged in-memory this sprint.
290
- </div>
291
- </>
292
- )}
293
- </CardContent>
294
- </Card>
295
- <Card>
296
- <CardHeader>
297
- <div className="flex items-center justify-between">
298
- <CardTitle>Reconstruction Fidelity (VC4C)</CardTitle>
299
- {reconstruct?.enabled ? (
300
- <Badge variant="success">ACTIVE</Badge>
301
- ) : (
302
- <Badge variant="danger">OFF</Badge>
303
- )}
304
- </div>
305
- </CardHeader>
306
- <CardContent>
307
- {!reconstruct?.enabled ? (
308
- <div className="vc-empty">Reconstruction fidelity disabled (VC4C off).</div>
309
- ) : (
310
- <>
311
- <div className="grid grid-cols-2 gap-4 md:grid-cols-4">
312
- <Metric label="Closure attempts" value={String(reconstruct.closureAttempts)} />
313
- <Metric label="Closure rejections" value={String(reconstruct.closureRejections)} />
314
- <Metric label="Validated" value={String(reconstruct.validatedCount)} />
315
- <Metric label="Invalidated" value={String(reconstruct.invalidatedCount)} />
316
- <Metric label="Span total" value={String(reconstruct.spanTotal)} />
317
- <Metric label="Byte total" value={String(reconstruct.byteTotal)} />
318
- </div>
319
- <div className="mt-3 text-xs text-muted-foreground">
320
- Reader-only closure/validation aggregate; staged in-memory this sprint.
321
- </div>
322
- </>
323
- )}
324
- </CardContent>
325
- </Card>
277
+ <VectorCortexShardsCard view={shards} />
278
+ <VectorCortexReconstructCard view={reconstruct} />
326
279
  <VectorCortexPlansCard view={plans} />
327
280
  <VectorCortexRenderCard view={render} />
328
281
  <VectorCortexRolloutCard view={rollout} />
329
282
  <VectorCortexClosureCard view={closureProof} />
330
283
  <VectorCortexRestoreCard view={restore} />
284
+ <VectorCortexRepairCard view={repair} />
331
285
  <VectorCortexLedgerCard ledger={ledger} />
332
286
  </div>
333
287
  );
@@ -225,6 +225,23 @@ export interface VectorCortexRestoreView {
225
225
  updatedAt: string;
226
226
  }
227
227
 
228
+ /**
229
+ * Reader-only self-healing derived-state view (VC6C). Counts + HEAL_REPAIR_*
230
+ * codes only — there is no payload endpoint, so no subsystem source bytes, gap
231
+ * ranges, high-water marks, root digests, or ledger text ever reach the client.
232
+ */
233
+ export interface VectorCortexRepairView {
234
+ enabled: boolean;
235
+ mode: "A" | "B" | "C";
236
+ repairAttempts: number;
237
+ repairsPlanned: number;
238
+ pointersSwitched: number;
239
+ backoffs: number;
240
+ lastBackoffMs: number | null;
241
+ lastFailure: string | null;
242
+ updatedAt: string;
243
+ }
244
+
228
245
  /** Reader-only closure-optimization diagnostics view (VC6A). Aggregate only. */
229
246
  export interface VectorCortexClosureProofView {
230
247
  enabled: boolean;
@@ -71,3 +71,41 @@ export interface VectorCortexRestoreView {
71
71
  /** ISO timestamp of the snapshot. */
72
72
  readonly updatedAt: string;
73
73
  }
74
+
75
+ /**
76
+ * Reader-only self-healing derived-state diagnostics view for
77
+ * GET /api/vector-cortex/repair (VC6C).
78
+ *
79
+ * COUNTS + ERROR CODES ONLY. Like the restore view, this surface deliberately
80
+ * carries no payload: no subsystem source bytes, no rebuilt derived rows, no
81
+ * subsystem names paired with their gap ranges, no high-water values, no root
82
+ * digests, and no ledger text (SECURITY_PRIVACY — a diagnostic surface never
83
+ * renders the authority's contents). Only the controller's rebuild counters,
84
+ * the backoff schedule's observable timing, and the last HEAL_REPAIR_* failure
85
+ * code are exposed; the per-subsystem gap detail lives in the structured event
86
+ * log, not the dashboard.
87
+ */
88
+ export interface VectorCortexRepairView {
89
+ /** Whether the VC6C self-healing derived-controller flag is enabled. */
90
+ readonly enabled: boolean;
91
+ /**
92
+ * Runtime triad mode: "A" targeted single-subsystem rebuild, "B" full
93
+ * deterministic rebuild (ambiguous gap), "C" derived state disabled (both
94
+ * rebuild paths failed, or the controller is not running at all).
95
+ */
96
+ readonly mode: "A" | "B" | "C";
97
+ /** Total rebuild attempts started by the controller (event counter). */
98
+ readonly repairAttempts: number;
99
+ /** Rebuilds planned after gap detection, before rate limiting (counter). */
100
+ readonly repairsPlanned: number;
101
+ /** Rebuilds that reached a verified atomic pointer switch (counter). */
102
+ readonly pointersSwitched: number;
103
+ /** Attempts deferred by the rate limiter / exponential backoff (counter). */
104
+ readonly backoffs: number;
105
+ /** Most recent deterministic backoff delay in ms, or null if none yet. */
106
+ readonly lastBackoffMs: number | null;
107
+ /** Last failure reason (a HEAL_REPAIR_* code), or null if none yet. */
108
+ readonly lastFailure: string | null;
109
+ /** ISO timestamp of the snapshot. */
110
+ readonly updatedAt: string;
111
+ }
@@ -377,4 +377,5 @@ export interface VectorCortexLedgerView {
377
377
  export type {
378
378
  VectorCortexClosureProofView,
379
379
  VectorCortexRestoreView,
380
+ VectorCortexRepairView,
380
381
  } from "./vector-cortex-heal.js";
@@ -57,6 +57,9 @@ import {
57
57
  handleVectorCortexClosureProof,
58
58
  handleVectorCortexRestore,
59
59
  } from "./routes.js";
60
+ // VC6C repair lives in its own module (routes-vector-cortex-repair.ts) so the
61
+ // heal route file stays well under the 400-line extension soft limit.
62
+ import { handleVectorCortexRepair } from "./routes-vector-cortex-repair.js";
60
63
 
61
64
  /**
62
65
  * Dispatch a request through every registered route handler.
@@ -110,5 +113,6 @@ export function dispatchRoutes(
110
113
  if (handleVectorCortexRollout(req, res, ctx)) return true;
111
114
  if (handleVectorCortexClosureProof(req, res, ctx)) return true;
112
115
  if (handleVectorCortexRestore(req, res, ctx)) return true;
116
+ if (handleVectorCortexRepair(req, res, ctx)) return true;
113
117
  return false;
114
118
  }
@@ -150,5 +150,11 @@ export const VECTOR_CORTEX_SETTINGS: SettingGroup = {
150
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
151
  true,
152
152
  ),
153
+ boolDirect(
154
+ "MEGACOMPACT_VC6C",
155
+ "VC6C Self-Healing Derived State",
156
+ "Self-healing derived-state controller: detects gaps between each derived subsystem's high-water mark and the durable authority high-water, then rebuilds derived state by copy -> root-digest verification -> atomic pointer switch, so a partially rebuilt subsystem is never made visible. A targeted single-subsystem rebuild is mode A; an ambiguous gap escalates to a full deterministic rebuild (mode B); if both rebuild paths fail the derived state is disabled rather than served stale (mode C). Rebuilds are rate-limited to one per subsystem per 5 minutes with deterministic exponential backoff, so a persistently failing subsystem cannot spin. OFF = byte-identical predecessor (VC6B); no controller runs, so the dashboard reports mode C.",
157
+ true,
158
+ ),
153
159
  ],
154
160
  };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * dashboard-server/routes-vector-cortex-repair.ts — VC6C self-healing
3
+ * derived-state dashboard route.
4
+ *
5
+ * Reader-only GET /api/vector-cortex/repair returning the self-healing
6
+ * controller's aggregate diagnostics: whether the flag is enabled, the runtime
7
+ * triad mode, cumulative rebuild/pointer-switch/backoff counters, the last
8
+ * deterministic backoff delay, and the last HEAL_REPAIR_* failure code.
9
+ *
10
+ * COUNTS + ERROR CODES ONLY. NEVER exposes subsystem source bytes, rebuilt
11
+ * derived rows, per-subsystem gap ranges, high-water marks, root digests, or
12
+ * ledger text (reader-only, SECURITY_PRIVACY — the per-subsystem gap detail
13
+ * lives in the structured event log, not the dashboard). There is no mutation
14
+ * seam here either: rebuilds are driven by the controller's own gap detection,
15
+ * never by a dashboard request, so non-GET is rejected outright.
16
+ *
17
+ * Split into its own file (rather than grown into routes-vector-cortex-heal.ts)
18
+ * to keep every extensions/ file well under the 400-line soft-as-hard limit.
19
+ *
20
+ * Guardrails: PREVENT-PI-004 (local in-process state only), PREVENT-011 (no
21
+ * `any`), reader-only aggregate (counts + codes only).
22
+ */
23
+
24
+ import type { IncomingMessage, ServerResponse } from "node:http";
25
+ import type { RouteContext } from "./routes-core.js";
26
+ import { VC6C_ENABLED } from "../../src/config.js";
27
+ import { sendJson } from "./routes-vector-cortex-shared.js";
28
+ import type { VectorCortexRepairView } from "./api-contracts/vector-cortex.js";
29
+
30
+ /**
31
+ * Reader-only GET /api/vector-cortex/repair (VC6C).
32
+ *
33
+ * Counts and HEAL_REPAIR_* codes only — this is a static reader-only aggregate
34
+ * seam (same shape as the VC6A/VC6B handlers): there is no payload endpoint and
35
+ * never will be, so no subsystem bytes, gap ranges, high-water marks, or root
36
+ * digests can leak here.
37
+ */
38
+ export function handleVectorCortexRepair(
39
+ req: IncomingMessage,
40
+ res: ServerResponse,
41
+ _ctx: RouteContext,
42
+ ): boolean {
43
+ const url = req.url ?? "";
44
+ const path = url.split("?")[0] ?? url;
45
+ if (path !== "/api/vector-cortex/repair") return false;
46
+ if (req.method !== "GET") {
47
+ sendJson(res, 405, { error: "method_not_allowed" });
48
+ return true;
49
+ }
50
+
51
+ const enabled = VC6C_ENABLED();
52
+ // Flag-off routes to mode C: with VC6C off no self-healing controller runs at
53
+ // all, so neither the targeted rebuild (mode A) nor the full deterministic
54
+ // rebuild (mode B) path is available. Mode C is exactly the spec's "derived
55
+ // state disabled" outcome — the honest OFF view is that derived state is not
56
+ // being healed, rather than implying a rebuild path that is not wired.
57
+ // Mirrors how VC6B's OFF view reports the disclose-loss mode it actually takes.
58
+ const mode: "A" | "B" | "C" = enabled ? "A" : "C";
59
+ const body: VectorCortexRepairView = {
60
+ enabled,
61
+ mode,
62
+ repairAttempts: 0,
63
+ repairsPlanned: 0,
64
+ pointersSwitched: 0,
65
+ backoffs: 0,
66
+ lastBackoffMs: null,
67
+ lastFailure: null,
68
+ updatedAt: new Date().toISOString(),
69
+ };
70
+ sendJson(res, 200, body);
71
+ return true;
72
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.20.9",
3
+ "version": "0.20.10",
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",
@@ -241,6 +241,23 @@ export const VC6A_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC6A");
241
241
  */
242
242
  export const VC6B_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC6B");
243
243
 
244
+ /**
245
+ * VC6C — self-healing derived controller. Default ON. `MEGACOMPACT_VC6C=0`
246
+ * disables and is byte-identical to the predecessor (VC6B): the `detectGaps` /
247
+ * `planRebuild` / `rebuildGeneration` / `switchPointer` arithmetic STILL RUNS (it
248
+ * is PURE — a high-water comparison plus a SHA-256 root-digest check, with an
249
+ * INJECTED clock and no storage or network), but the
250
+ * `vector_cortex_repair_planned` / `vector_cortex_repair_pointer_switched` /
251
+ * `vector_cortex_repair_backoff` events are never emitted and the dashboard
252
+ * repair diagnostics seam is suppressed. Flag OFF gates the reporter + dashboard
253
+ * seam, never the gap-detection/rebuild arithmetic (in particular, an unverified
254
+ * pointer switch stays refused with the flag off), so flag-off
255
+ * outbound/predecessor golden bytes match exactly. This flag MUST also be a
256
+ * dashboard SETTINGS toggle (visible in config UI, never in EXCLUDED_SETTINGS),
257
+ * mirroring VC4A/VC4B/VC4C/VC5A/VC5B/VC5C/VC6A/VC6B.
258
+ */
259
+ export const VC6C_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC6C");
260
+
244
261
  // ---------------------------------------------------------------------------
245
262
  // Breaker state machine constants (TRIAD_RESILIENCE.md §breaker).
246
263
  // Rolled numbers for one 60s window; VC0C consumes these at its breaker seam.
package/src/config.ts CHANGED
@@ -171,6 +171,7 @@ export {
171
171
  VC5C_ENABLED,
172
172
  VC6A_ENABLED,
173
173
  VC6B_ENABLED,
174
+ VC6C_ENABLED,
174
175
  BREAKER_WINDOW_MS,
175
176
  BREAKER_MIN_ATTEMPTS,
176
177
  BREAKER_PERF_FAILURES,
@@ -0,0 +1,138 @@
1
+ /**
2
+ * heal/_repair-fixture.ts — conformance fixture I/O for VC6C healing-controller rows.
3
+ *
4
+ * Sibling of `_restore-fixture.ts` (VC6B) and `_acceptance-fixture.ts` (VC6A);
5
+ * split out so no loader approaches the 300-line soft limit and so the
6
+ * BigInt/base64 decoding lives next to the contract it reconstitutes.
7
+ *
8
+ * DECODING IS THE POINT. Fixtures are canonical JSON, which cannot express
9
+ * bigints or bytes, so the corpus stores numeric `nowMs` / high-waters and
10
+ * base64 source bytes. These loaders turn those back into the REAL `RepairState`
11
+ * / `RebuildInput` objects the production controller consumes — no mocks, no
12
+ * stubs, no parallel "test shape". A lossy decode would make the digests or the
13
+ * planned ranges disagree and the acceptance test would fail loudly.
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 { Mode, RepairState } from "./repair-types.js";
21
+ import type { RebuildInput } from "./rebuild.js";
22
+ import { V2, readManifest } from "./_acceptance-fixture.js";
23
+
24
+ /** A subsystem state as it appears in JSON: high-waters are numbers, not bigints. */
25
+ export interface RepairFxState {
26
+ subsystem: string;
27
+ derivedHighWater: number;
28
+ authorityHighWater: number;
29
+ lastRebuildAt: number | null;
30
+ generation: number;
31
+ mode: Mode;
32
+ failedAttempts?: number;
33
+ authorityFrozen?: boolean;
34
+ }
35
+
36
+ export interface RepairFxRebuild {
37
+ subsystem: string;
38
+ generation: number;
39
+ currentGeneration: number;
40
+ sourceBytesBase64: string;
41
+ expectedDigest: string;
42
+ triadMode: Mode;
43
+ }
44
+
45
+ export interface RepairFxBackoff {
46
+ subsystem: string;
47
+ attempts: number[];
48
+ }
49
+
50
+ export interface RepairFxInput {
51
+ scenario: string;
52
+ /** Which real entry point the acceptance test drives. */
53
+ mode: "detect" | "backoff" | "rebuild";
54
+ /** The INJECTED monotonic clock — VC6C never reads a real clock. */
55
+ nowMs: number;
56
+ states: RepairFxState[];
57
+ rebuild?: RepairFxRebuild;
58
+ backoff?: RepairFxBackoff;
59
+ }
60
+
61
+ export interface RepairFxExpected {
62
+ ok: boolean;
63
+ code?: string;
64
+ plannedCount: number;
65
+ /** Exact [seqStart, seqEnd] of each plan, in plan order. */
66
+ ranges: number[][];
67
+ switched?: boolean;
68
+ generation?: number;
69
+ semanticLossStated?: boolean;
70
+ monotonic?: boolean;
71
+ capped?: boolean;
72
+ idempotent?: boolean;
73
+ }
74
+
75
+ export interface RepairFx {
76
+ id: string;
77
+ schema: string;
78
+ producer: string;
79
+ assertion: string;
80
+ kind: string;
81
+ input: RepairFxInput;
82
+ expected: RepairFxExpected;
83
+ }
84
+
85
+ /** Read one registered healing-controller fixture (asserting it IS registered). */
86
+ export function repairFixture(id: string): RepairFx {
87
+ const m = readManifest();
88
+ const row = m.fixtures.find(
89
+ (f) => f.id === id && f.path.startsWith("healing-controller/"),
90
+ );
91
+ assert.ok(row, `fixture ${id} registered under healing-controller/ in manifest`);
92
+ return JSON.parse(readFileSync(join(V2, row!.path), "utf8")) as RepairFx;
93
+ }
94
+
95
+ /** JSON numbers -> the bigint fields `RepairState` declares. */
96
+ export function decodeState(s: RepairFxState): RepairState {
97
+ return {
98
+ subsystem: s.subsystem,
99
+ derivedHighWater: BigInt(s.derivedHighWater),
100
+ authorityHighWater: BigInt(s.authorityHighWater),
101
+ lastRebuildAt: s.lastRebuildAt === null ? null : BigInt(s.lastRebuildAt),
102
+ generation: s.generation,
103
+ mode: s.mode,
104
+ ...(s.failedAttempts !== undefined ? { failedAttempts: s.failedAttempts } : {}),
105
+ ...(s.authorityFrozen !== undefined ? { authorityFrozen: s.authorityFrozen } : {}),
106
+ };
107
+ }
108
+
109
+ /** Reconstitute a real `RebuildInput` from its fixture row. */
110
+ export function decodeRebuild(r: RepairFxRebuild): RebuildInput {
111
+ return {
112
+ subsystem: r.subsystem,
113
+ range: {
114
+ sessionId: r.subsystem,
115
+ seqStart: 0n,
116
+ seqEnd: 0n,
117
+ byteStart: 0,
118
+ byteEnd: 0,
119
+ },
120
+ generation: r.generation,
121
+ sourceBytes: new Uint8Array(Buffer.from(r.sourceBytesBase64, "base64")),
122
+ expectedDigest: r.expectedDigest,
123
+ };
124
+ }
125
+
126
+ /** Flag-pinned wrapper: VC6C gated by MEGACOMPACT_VC6C (defaults ON). */
127
+ export function withVc6cFlagsOn(fn: () => void): () => void {
128
+ return (): void => {
129
+ const saved = process.env.MEGACOMPACT_VC6C;
130
+ process.env.MEGACOMPACT_VC6C = "1";
131
+ try {
132
+ fn();
133
+ } finally {
134
+ if (saved === undefined) delete process.env.MEGACOMPACT_VC6C;
135
+ else process.env.MEGACOMPACT_VC6C = saved;
136
+ }
137
+ };
138
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * vector-cortex/heal/controller.ts — VC6C gap detection + rebuild planning.
3
+ *
4
+ * Compares each derived subsystem's high-water to the durable authority
5
+ * high-water and emits a `RepairPlanV1` for every gap that is real, actionable,
6
+ * and not rate-limited. The controller DECIDES; `rebuild.ts` executes.
7
+ *
8
+ * THE AUTHORITY IS NEVER WRITTEN. Every function here takes `RepairState` (whose
9
+ * `authorityHighWater` is readonly) and returns plans. There is no code path,
10
+ * guarded or otherwise, that mutates authority — derived state is rebuildable,
11
+ * authority is not, so repair is strictly one-directional.
12
+ *
13
+ * FOUR REASONS NOT TO PLAN, in priority order. The order is the contract: a
14
+ * frozen authority outranks a rate limit, because "the frontier is not real yet"
15
+ * is a statement about CORRECTNESS while "you rebuilt recently" is only about
16
+ * pacing. Reporting RATE_LIMITED during an outage would tell an operator to wait
17
+ * five minutes for a rebuild that must never happen at all.
18
+ *
19
+ * 1. AUTHORITY FROZEN. During an outage the durable high-water freezes while
20
+ * the spool keeps accepting frames (TRIAD_RESILIENCE §frontier). A derived
21
+ * subsystem behind a frozen frontier is CORRECT, so we refuse to plan rather
22
+ * than chase the spool tail and materialize non-durable frames.
23
+ * 2. NO GAP. `derived >= authority` — nothing to do. Note `>`: a derived source
24
+ * AHEAD of authority is not repaired by rebuilding a backwards range (that
25
+ * would produce an inverted range); it is left alone for the breaker.
26
+ * 3. MODE C. Derived state is disabled for this subsystem. Re-planning a
27
+ * subsystem whose derived tier is intentionally off is a rebuild loop.
28
+ * 4. RATE LIMITED. One rebuild per subsystem per 5 minutes.
29
+ *
30
+ * DETERMINISTIC JITTER. The ±10% spread comes from a SHA-256 of the subsystem
31
+ * name plus the attempt, never `Math.random`. Two subsystems desynchronize (no
32
+ * thundering herd) while a single subsystem's schedule is byte-reproducible — a
33
+ * fixture can pin `backoffMs` exactly, which a PRNG would make untestable.
34
+ *
35
+ * PURE. `node:crypto` is the only dependency beyond types: no storage, no
36
+ * console, no network, and no clock — `nowMs` is always injected, which is what
37
+ * makes the fake-clock restart fixtures possible (PREVENT-PI-004 / PREVENT-011).
38
+ */
39
+
40
+ import { createHash } from "node:crypto";
41
+
42
+ import type {
43
+ RepairPlanV1,
44
+ RepairState,
45
+ RepairSubsystem,
46
+ ShardRange,
47
+ } from "./repair-types.js";
48
+ import {
49
+ REPAIR_BACKOFF_BASE_MS,
50
+ REPAIR_BACKOFF_CAP_MS,
51
+ REPAIR_BACKOFF_JITTER,
52
+ REPAIR_RATE_LIMIT_MS,
53
+ } from "./repair-types.js";
54
+
55
+ /**
56
+ * A stable fraction in [0,1) derived from the subsystem name + attempt.
57
+ *
58
+ * Uses the first 6 hex digits (24 bits) of SHA-256 — plenty of spread for a ±10%
59
+ * window, and small enough to stay exact in a float64 division.
60
+ */
61
+ function digestFraction(subsystem: RepairSubsystem, attempt: number): number {
62
+ const hex = createHash("sha256")
63
+ .update(`${subsystem}:${attempt}`)
64
+ .digest("hex")
65
+ .slice(0, 6);
66
+ return parseInt(hex, 16) / 0x1000000;
67
+ }
68
+
69
+ /**
70
+ * Deterministic exponential backoff: `30s * 2^attempt`, capped at 15 minutes,
71
+ * then spread by ±10% seeded from the subsystem digest.
72
+ *
73
+ * The cap is applied BEFORE the jitter so the jitter is a spread around the cap
74
+ * rather than a way to exceed it; the result is floored to an integer ms so the
75
+ * value is exactly representable in JSON (a fixture pins it verbatim).
76
+ *
77
+ * `attempt` is clamped at 0 and at 30: `2^attempt` for an unclamped large attempt
78
+ * would reach Infinity, and `Infinity * jitter` is NaN — a NaN backoff would
79
+ * schedule a plan that is never eligible and silently wedge the subsystem.
80
+ */
81
+ export function computeBackoff(subsystem: RepairSubsystem, attempt: number): number {
82
+ const safe = Math.min(Math.max(Math.floor(attempt), 0), 30);
83
+ const raw = REPAIR_BACKOFF_BASE_MS * 2 ** safe;
84
+ const capped = Math.min(raw, REPAIR_BACKOFF_CAP_MS);
85
+ // Map [0,1) onto [-1,+1) then scale to the ±10% window.
86
+ const spread = (digestFraction(subsystem, safe) * 2 - 1) * REPAIR_BACKOFF_JITTER;
87
+ return Math.max(0, Math.floor(capped * (1 + spread)));
88
+ }
89
+
90
+ /**
91
+ * True when this subsystem rebuilt less than 5 minutes ago.
92
+ *
93
+ * A subsystem that has NEVER rebuilt (`lastRebuildAt === null`) is never rate
94
+ * limited — the first repair after a restart must not be delayed by the absence
95
+ * of history.
96
+ */
97
+ export function isRateLimited(
98
+ lastRebuildAt: bigint | null,
99
+ nowMs: bigint,
100
+ ): boolean {
101
+ if (lastRebuildAt === null) return false;
102
+ return lastRebuildAt + BigInt(REPAIR_RATE_LIMIT_MS) > nowMs;
103
+ }
104
+
105
+ /**
106
+ * The seq window a rebuild must cover: the first UNBUILT seq through the
107
+ * authority frontier, inclusive.
108
+ *
109
+ * Byte bounds are intentionally 0..0. VC6C plans in SEQ space — the controller
110
+ * knows how far each derived tier has been built, but byte offsets belong to the
111
+ * shard/ledger layer that executes the rebuild. Inventing byte bounds here would
112
+ * be fabricating a fact the controller does not have.
113
+ */
114
+ function gapRange(state: RepairState): ShardRange {
115
+ return {
116
+ sessionId: state.subsystem,
117
+ seqStart: state.derivedHighWater + 1n,
118
+ seqEnd: state.authorityHighWater,
119
+ byteStart: 0,
120
+ byteEnd: 0,
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Build the plan for one subsystem's gap.
126
+ *
127
+ * Always targets `generation + 1`: a rebuild NEVER writes into the live
128
+ * generation, so a failed or half-written rebuild cannot corrupt what is
129
+ * currently being served (see `rebuild.ts` copy-then-switch).
130
+ *
131
+ * Exported for direct unit testing and for callers that have already decided a
132
+ * plan is warranted; `detectGaps` is the guarded entry point that applies the
133
+ * four refusal rules first.
134
+ */
135
+ export function planRebuild(state: RepairState, nowMs: bigint): RepairPlanV1 {
136
+ const backoffMs = computeBackoff(state.subsystem, state.failedAttempts ?? 0);
137
+ return {
138
+ schema: "repair-plan-v1",
139
+ subsystem: state.subsystem,
140
+ range: gapRange(state),
141
+ generation: state.generation + 1,
142
+ backoffMs,
143
+ scheduledAt: nowMs + BigInt(backoffMs),
144
+ };
145
+ }
146
+
147
+ /**
148
+ * True when a subsystem is eligible for a rebuild plan (the four refusal rules).
149
+ * Ordering matters — see the file header.
150
+ */
151
+ export function isPlannable(state: RepairState, nowMs: bigint): boolean {
152
+ if (state.authorityFrozen === true) return false;
153
+ if (state.derivedHighWater >= state.authorityHighWater) return false;
154
+ if (state.mode === "C") return false;
155
+ if (isRateLimited(state.lastRebuildAt, nowMs)) return false;
156
+ return true;
157
+ }
158
+
159
+ /**
160
+ * Detect derived gaps and plan a rebuild for each eligible subsystem.
161
+ *
162
+ * Output order follows INPUT order, not subsystem name: the caller controls
163
+ * priority (a topology gap may matter more than a closure gap), and re-sorting
164
+ * here would silently override that. Never throws — an unplannable subsystem is
165
+ * simply absent from the result, so a single wedged tier cannot stop the others
166
+ * from healing (PRACTICES: non-fatal).
167
+ */
168
+ export function detectGaps(
169
+ states: readonly RepairState[],
170
+ nowMs: bigint,
171
+ ): readonly RepairPlanV1[] {
172
+ const plans: RepairPlanV1[] = [];
173
+ for (const state of states) {
174
+ if (!isPlannable(state, nowMs)) continue;
175
+ plans.push(planRebuild(state, nowMs));
176
+ }
177
+ return plans;
178
+ }
179
+
180
+ /**
181
+ * The bound controller surface. A plain object (not a class): there is no
182
+ * instance state to hold — every input, including the clock, is injected.
183
+ */
184
+ export function createRepairController(): {
185
+ readonly detectGaps: typeof detectGaps;
186
+ readonly planRebuild: typeof planRebuild;
187
+ } {
188
+ return { detectGaps, planRebuild };
189
+ }