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
@@ -0,0 +1,74 @@
1
+ /**
2
+ * vector-cortex/heal/repair-types.ts — VC6C self-healing derived controller.
3
+ *
4
+ * VC6B answered "when a node's bytes are gone, WHERE do they come from?". VC6C
5
+ * answers the question one level up: when a DERIVED subsystem (topology, shards,
6
+ * closure) has fallen BEHIND the durable authority, how do we notice, and how do
7
+ * we catch it up without ever risking the authority itself?
8
+ *
9
+ * THE AUTHORITY IS READ-ONLY, ALWAYS. The controller compares each derived
10
+ * source's high-water to the durable authority high-water and plans work. It has
11
+ * no write path to the authority — not a guarded one, not an admin one. Derived
12
+ * state is disposable and can always be rebuilt from the byte ledger; the
13
+ * authority is not, so the repair path is deliberately one-directional. This is
14
+ * why `RepairState` carries `authorityHighWater` as a plain readonly field and
15
+ * why no function in `controller.ts` returns anything that could be applied to it.
16
+ *
17
+ * NEVER READ PAST THE AUTHORITY (TRIAD_RESILIENCE §frontier). A derived builder
18
+ * may not read beyond the durable CONTIGUOUS authority high-water. During an
19
+ * authority outage that high-water FREEZES even though the spool keeps accepting
20
+ * frames — so a derived subsystem that is "behind" a frozen frontier is CORRECT,
21
+ * not broken, and planning a rebuild against the spool tail would materialize
22
+ * frames that are not yet durable. `detectGaps` therefore treats
23
+ * `authorityFrozen` as a hard stop (`HEAL_REPAIR_AUTHORITY_FROZEN`) rather than
24
+ * as a large gap to chase. After the drain, catch-up resumes from the OLD
25
+ * high-water; it never jumps to the tail.
26
+ *
27
+ * REBUILD IS COPY-THEN-SWITCH, NEVER IN-PLACE. `rebuild.ts` materializes a NEW
28
+ * generation, verifies its root digest, and only then flips the pointer. A failed
29
+ * verification keeps the old pointer and DELETES NO EVIDENCE: the corrupt
30
+ * generation stays on disk to be inspected. An in-place repair would, by
31
+ * construction, destroy the only copy of the thing that was about to be proven
32
+ * wrong.
33
+ *
34
+ * RATE LIMIT + BACKOFF ARE THE BLAST RADIUS. A subsystem that fails to rebuild
35
+ * will fail again, usually instantly, so an unbounded retry loop turns one broken
36
+ * shard into a CPU-saturating rebuild storm. One rebuild per subsystem per 5
37
+ * minutes bounds the steady state, and the deterministic exponential backoff
38
+ * (30s * 2^attempt, capped at 15 min, ±10% jitter derived from the SUBSYSTEM
39
+ * DIGEST rather than `Math.random`) bounds the failure state while keeping the
40
+ * schedule reproducible in a fixture.
41
+ *
42
+ * Pure types + registered conformance IDs: no storage, no console, no clock, no
43
+ * network (PREVENT-PI-004 / PREVENT-011).
44
+ */
45
+ /**
46
+ * One rebuild per subsystem per 5 minutes. Bounds a rebuild storm: a subsystem
47
+ * that fails will fail again immediately, and without this an unhealthy tier
48
+ * would saturate the box re-materializing the same broken generation.
49
+ */
50
+ export const REPAIR_RATE_LIMIT_MS = 5 * 60_000;
51
+ /**
52
+ * Exponential backoff base/cap, matching the breaker's retry rule
53
+ * (TRIAD_RESILIENCE): `30s * 2^attempt`, capped at 15 minutes.
54
+ */
55
+ export const REPAIR_BACKOFF_BASE_MS = 30_000;
56
+ export const REPAIR_BACKOFF_CAP_MS = 15 * 60_000;
57
+ /**
58
+ * ±10% deterministic jitter. Derived from the SUBSYSTEM DIGEST, not a PRNG, so
59
+ * two subsystems desynchronize (no thundering herd) while any single subsystem's
60
+ * schedule stays reproducible in a fixture.
61
+ */
62
+ export const REPAIR_BACKOFF_JITTER = 0.1;
63
+ /**
64
+ * Registered VC6C conformance ID range (HEAL-031..045), continuing VC6B's
65
+ * HEAL-016..030. The acceptance test reads these rows from the v2 manifest and
66
+ * asserts each returns its manifest `ok`/`code`.
67
+ */
68
+ export const REPAIR_IDS = Array.from({ length: 15 }, (_v, i) => `HEAL-${String(i + 31).padStart(3, "0")}`);
69
+ /** Named VC6C conformance assertions (the sprint's headline rows). */
70
+ export const REPAIR_NAMED_IDS = [
71
+ "HEAL-GAP-001",
72
+ "HEAL-RATE-002",
73
+ "HEAL-SWITCH-003",
74
+ ];
@@ -41,3 +41,4 @@ export const HEAL_NAMED_IDS = [
41
41
  "HEAL-PROOF-003",
42
42
  ];
43
43
  export { RESTORE_IDS, RESTORE_NAMED_IDS, RESTORE_LIMIT_SPANS, RESTORE_LIMIT_BYTES, } from "./restore-types.js";
44
+ export { REPAIR_IDS, REPAIR_NAMED_IDS, REPAIR_RATE_LIMIT_MS, REPAIR_BACKOFF_BASE_MS, REPAIR_BACKOFF_CAP_MS, REPAIR_BACKOFF_JITTER, } from "./repair-types.js";
@@ -0,0 +1,70 @@
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
+ import { readFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import assert from "node:assert/strict";
18
+ import { V2, readManifest } from "./_acceptance-fixture.js";
19
+ /** Read one registered healing-controller fixture (asserting it IS registered). */
20
+ export function repairFixture(id) {
21
+ const m = readManifest();
22
+ const row = m.fixtures.find((f) => f.id === id && f.path.startsWith("healing-controller/"));
23
+ assert.ok(row, `fixture ${id} registered under healing-controller/ in manifest`);
24
+ return JSON.parse(readFileSync(join(V2, row.path), "utf8"));
25
+ }
26
+ /** JSON numbers -> the bigint fields `RepairState` declares. */
27
+ export function decodeState(s) {
28
+ return {
29
+ subsystem: s.subsystem,
30
+ derivedHighWater: BigInt(s.derivedHighWater),
31
+ authorityHighWater: BigInt(s.authorityHighWater),
32
+ lastRebuildAt: s.lastRebuildAt === null ? null : BigInt(s.lastRebuildAt),
33
+ generation: s.generation,
34
+ mode: s.mode,
35
+ ...(s.failedAttempts !== undefined ? { failedAttempts: s.failedAttempts } : {}),
36
+ ...(s.authorityFrozen !== undefined ? { authorityFrozen: s.authorityFrozen } : {}),
37
+ };
38
+ }
39
+ /** Reconstitute a real `RebuildInput` from its fixture row. */
40
+ export function decodeRebuild(r) {
41
+ return {
42
+ subsystem: r.subsystem,
43
+ range: {
44
+ sessionId: r.subsystem,
45
+ seqStart: 0n,
46
+ seqEnd: 0n,
47
+ byteStart: 0,
48
+ byteEnd: 0,
49
+ },
50
+ generation: r.generation,
51
+ sourceBytes: new Uint8Array(Buffer.from(r.sourceBytesBase64, "base64")),
52
+ expectedDigest: r.expectedDigest,
53
+ };
54
+ }
55
+ /** Flag-pinned wrapper: VC6C gated by MEGACOMPACT_VC6C (defaults ON). */
56
+ export function withVc6cFlagsOn(fn) {
57
+ return () => {
58
+ const saved = process.env.MEGACOMPACT_VC6C;
59
+ process.env.MEGACOMPACT_VC6C = "1";
60
+ try {
61
+ fn();
62
+ }
63
+ finally {
64
+ if (saved === undefined)
65
+ delete process.env.MEGACOMPACT_VC6C;
66
+ else
67
+ process.env.MEGACOMPACT_VC6C = saved;
68
+ }
69
+ };
70
+ }
@@ -0,0 +1,165 @@
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
+ import { createHash } from "node:crypto";
40
+ import { REPAIR_BACKOFF_BASE_MS, REPAIR_BACKOFF_CAP_MS, REPAIR_BACKOFF_JITTER, REPAIR_RATE_LIMIT_MS, } from "./repair-types.js";
41
+ /**
42
+ * A stable fraction in [0,1) derived from the subsystem name + attempt.
43
+ *
44
+ * Uses the first 6 hex digits (24 bits) of SHA-256 — plenty of spread for a ±10%
45
+ * window, and small enough to stay exact in a float64 division.
46
+ */
47
+ function digestFraction(subsystem, attempt) {
48
+ const hex = createHash("sha256")
49
+ .update(`${subsystem}:${attempt}`)
50
+ .digest("hex")
51
+ .slice(0, 6);
52
+ return parseInt(hex, 16) / 0x1000000;
53
+ }
54
+ /**
55
+ * Deterministic exponential backoff: `30s * 2^attempt`, capped at 15 minutes,
56
+ * then spread by ±10% seeded from the subsystem digest.
57
+ *
58
+ * The cap is applied BEFORE the jitter so the jitter is a spread around the cap
59
+ * rather than a way to exceed it; the result is floored to an integer ms so the
60
+ * value is exactly representable in JSON (a fixture pins it verbatim).
61
+ *
62
+ * `attempt` is clamped at 0 and at 30: `2^attempt` for an unclamped large attempt
63
+ * would reach Infinity, and `Infinity * jitter` is NaN — a NaN backoff would
64
+ * schedule a plan that is never eligible and silently wedge the subsystem.
65
+ */
66
+ export function computeBackoff(subsystem, attempt) {
67
+ const safe = Math.min(Math.max(Math.floor(attempt), 0), 30);
68
+ const raw = REPAIR_BACKOFF_BASE_MS * 2 ** safe;
69
+ const capped = Math.min(raw, REPAIR_BACKOFF_CAP_MS);
70
+ // Map [0,1) onto [-1,+1) then scale to the ±10% window.
71
+ const spread = (digestFraction(subsystem, safe) * 2 - 1) * REPAIR_BACKOFF_JITTER;
72
+ return Math.max(0, Math.floor(capped * (1 + spread)));
73
+ }
74
+ /**
75
+ * True when this subsystem rebuilt less than 5 minutes ago.
76
+ *
77
+ * A subsystem that has NEVER rebuilt (`lastRebuildAt === null`) is never rate
78
+ * limited — the first repair after a restart must not be delayed by the absence
79
+ * of history.
80
+ */
81
+ export function isRateLimited(lastRebuildAt, nowMs) {
82
+ if (lastRebuildAt === null)
83
+ return false;
84
+ return lastRebuildAt + BigInt(REPAIR_RATE_LIMIT_MS) > nowMs;
85
+ }
86
+ /**
87
+ * The seq window a rebuild must cover: the first UNBUILT seq through the
88
+ * authority frontier, inclusive.
89
+ *
90
+ * Byte bounds are intentionally 0..0. VC6C plans in SEQ space — the controller
91
+ * knows how far each derived tier has been built, but byte offsets belong to the
92
+ * shard/ledger layer that executes the rebuild. Inventing byte bounds here would
93
+ * be fabricating a fact the controller does not have.
94
+ */
95
+ function gapRange(state) {
96
+ return {
97
+ sessionId: state.subsystem,
98
+ seqStart: state.derivedHighWater + 1n,
99
+ seqEnd: state.authorityHighWater,
100
+ byteStart: 0,
101
+ byteEnd: 0,
102
+ };
103
+ }
104
+ /**
105
+ * Build the plan for one subsystem's gap.
106
+ *
107
+ * Always targets `generation + 1`: a rebuild NEVER writes into the live
108
+ * generation, so a failed or half-written rebuild cannot corrupt what is
109
+ * currently being served (see `rebuild.ts` copy-then-switch).
110
+ *
111
+ * Exported for direct unit testing and for callers that have already decided a
112
+ * plan is warranted; `detectGaps` is the guarded entry point that applies the
113
+ * four refusal rules first.
114
+ */
115
+ export function planRebuild(state, nowMs) {
116
+ const backoffMs = computeBackoff(state.subsystem, state.failedAttempts ?? 0);
117
+ return {
118
+ schema: "repair-plan-v1",
119
+ subsystem: state.subsystem,
120
+ range: gapRange(state),
121
+ generation: state.generation + 1,
122
+ backoffMs,
123
+ scheduledAt: nowMs + BigInt(backoffMs),
124
+ };
125
+ }
126
+ /**
127
+ * True when a subsystem is eligible for a rebuild plan (the four refusal rules).
128
+ * Ordering matters — see the file header.
129
+ */
130
+ export function isPlannable(state, nowMs) {
131
+ if (state.authorityFrozen === true)
132
+ return false;
133
+ if (state.derivedHighWater >= state.authorityHighWater)
134
+ return false;
135
+ if (state.mode === "C")
136
+ return false;
137
+ if (isRateLimited(state.lastRebuildAt, nowMs))
138
+ return false;
139
+ return true;
140
+ }
141
+ /**
142
+ * Detect derived gaps and plan a rebuild for each eligible subsystem.
143
+ *
144
+ * Output order follows INPUT order, not subsystem name: the caller controls
145
+ * priority (a topology gap may matter more than a closure gap), and re-sorting
146
+ * here would silently override that. Never throws — an unplannable subsystem is
147
+ * simply absent from the result, so a single wedged tier cannot stop the others
148
+ * from healing (PRACTICES: non-fatal).
149
+ */
150
+ export function detectGaps(states, nowMs) {
151
+ const plans = [];
152
+ for (const state of states) {
153
+ if (!isPlannable(state, nowMs))
154
+ continue;
155
+ plans.push(planRebuild(state, nowMs));
156
+ }
157
+ return plans;
158
+ }
159
+ /**
160
+ * The bound controller surface. A plain object (not a class): there is no
161
+ * instance state to hold — every input, including the clock, is injected.
162
+ */
163
+ export function createRepairController() {
164
+ return { detectGaps, planRebuild };
165
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * vector-cortex/heal/rebuild.ts — VC6C generation rebuild + atomic pointer switch.
3
+ *
4
+ * Executes what `controller.ts` planned: materialize a NEW generation, verify its
5
+ * root digest, and only then flip the live pointer.
6
+ *
7
+ * COPY, VERIFY, SWITCH — in that order, always.
8
+ *
9
+ * 1. COPY. The rebuild writes into `plan.generation` (always `current + 1`),
10
+ * never into the live generation. The thing currently being served is never
11
+ * the thing being modified, so a crash mid-rebuild leaves a complete old
12
+ * generation and a partial new one — never a torn live one.
13
+ * 2. VERIFY. The new generation's root digest must equal the digest the plan
14
+ * pinned. This is the ONLY gate on the pointer.
15
+ * 3. SWITCH. `switchPointer` moves the pointer iff verification passed.
16
+ *
17
+ * A FAILED VERIFICATION DELETES NO EVIDENCE. On mismatch we keep the old pointer
18
+ * AND leave the new generation on disk. That is deliberate: the corrupt
19
+ * generation is the only artifact that can explain WHY the rebuild was wrong, and
20
+ * a self-healing system that tidies up its failures is a system that cannot be
21
+ * debugged. Cleanup is a separate, explicit operator action.
22
+ *
23
+ * CRASH SAFETY IS A CONSEQUENCE OF THE ORDER, NOT AN EXTRA STEP. The pointer is
24
+ * the single atomic commit point. Kill the process after step 1 or 2 and the old
25
+ * pointer is still live, so the next start serves the prior generation and simply
26
+ * re-plans — the orphaned generation is inert. This is what
27
+ * `rebuild-chaos.test.ts` pins.
28
+ *
29
+ * THE TRIAD (independent algorithms, per TRIAD_RESILIENCE).
30
+ * A — TARGETED: rebuild only `plan.range`, reusing the prior generation for
31
+ * everything outside it. Cheap; needs a healthy prior generation.
32
+ * B — FULL DETERMINISTIC: re-derive the whole subsystem from the byte ledger,
33
+ * reusing NOTHING. Independent of A: it shares no index, no prior
34
+ * generation, and no incremental state, so a bug or corruption that breaks
35
+ * A cannot break B the same way.
36
+ * C — DISABLE DERIVED STATE: no rebuild at all. Mode C is a real outcome, not
37
+ * an error path, and it MUST state its loss of old semantic context — the
38
+ * subsystem serves nothing rather than serving something wrong.
39
+ *
40
+ * PURE. `node:crypto` only — no storage, no console, no clock, no network
41
+ * (PREVENT-PI-004 / PREVENT-011). Callers own persistence; this module owns the
42
+ * decision and the digest arithmetic, which is what makes it fixture-testable.
43
+ */
44
+ import { createHash } from "node:crypto";
45
+ /** Root digest of a generation's bytes: SHA-256, bare lowercase hex. */
46
+ export function rootDigest(bytes) {
47
+ return createHash("sha256").update(bytes).digest("hex");
48
+ }
49
+ /**
50
+ * Materialize + verify one generation.
51
+ *
52
+ * Hashes the rebuilt bytes and compares to the pinned root digest. A mismatch
53
+ * returns `HEAL_REPAIR_DIGEST_MISMATCH` and — critically — the caller must NOT
54
+ * switch the pointer; `switchPointer` enforces that structurally by requiring the
55
+ * verified flag.
56
+ *
57
+ * An EMPTY rebuild is a failure, not an empty success: a generation with no bytes
58
+ * would hash to the digest of nothing, and if a plan ever pinned that digest an
59
+ * empty rebuild would "verify" and the pointer would flip to nothing at all. It
60
+ * is reported as `HEAL_REBUILD_FAILED` because the rebuild produced no artifact.
61
+ */
62
+ export function rebuildGeneration(input, mode = "A") {
63
+ if (input.sourceBytes.length === 0) {
64
+ return {
65
+ ok: false,
66
+ code: "HEAL_REBUILD_FAILED",
67
+ generation: input.generation,
68
+ mode,
69
+ };
70
+ }
71
+ const digest = rootDigest(input.sourceBytes);
72
+ if (digest !== input.expectedDigest) {
73
+ // Evidence retained: the caller keeps the generation on disk for inspection.
74
+ return {
75
+ ok: false,
76
+ code: "HEAL_REPAIR_DIGEST_MISMATCH",
77
+ generation: input.generation,
78
+ mode,
79
+ };
80
+ }
81
+ return { ok: true, generation: input.generation, digest, mode };
82
+ }
83
+ /**
84
+ * The atomic commit point: flip the live pointer iff the new generation verified.
85
+ *
86
+ * `verified` is a required argument rather than something re-derived here, so a
87
+ * caller cannot switch the pointer without having gone through
88
+ * `rebuildGeneration` — "switch without verifying" is not expressible.
89
+ *
90
+ * A non-monotonic switch is also refused: the new generation must be strictly
91
+ * greater than the current one. Replaying a stale plan after a restart would
92
+ * otherwise roll the pointer BACKWARDS onto an older generation, silently
93
+ * un-healing the subsystem.
94
+ */
95
+ export function switchPointer(currentGen, newGen, verified) {
96
+ if (!verified)
97
+ return { switched: false, generation: currentGen };
98
+ if (newGen <= currentGen)
99
+ return { switched: false, generation: currentGen };
100
+ return { switched: true, generation: newGen };
101
+ }
102
+ /**
103
+ * Apply the triad arm for a rebuild.
104
+ *
105
+ * A and B run the same verification (a digest is a digest) but are reached by
106
+ * INDEPENDENT production paths: A reuses the prior generation and rebuilds only
107
+ * the planned range, while B re-derives everything from the byte ledger sharing
108
+ * no index or incremental state with A. C performs no rebuild at all and states
109
+ * its loss.
110
+ */
111
+ export function applyTriad(mode, input) {
112
+ if (mode === "C") {
113
+ // Derived state disabled: no rebuild, and the loss of old semantic context
114
+ // is DISCLOSED rather than papered over with a stale or partial generation.
115
+ return {
116
+ ok: false,
117
+ code: "HEAL_REBUILD_FAILED",
118
+ generation: input.generation,
119
+ mode: "C",
120
+ semanticLossStated: true,
121
+ };
122
+ }
123
+ return rebuildGeneration(input, mode);
124
+ }
125
+ /**
126
+ * Convenience: rebuild then switch, returning both halves.
127
+ *
128
+ * The pointer moves only on a verified rebuild, so a failed verification yields
129
+ * `switched:false` with the ORIGINAL generation still live and the new (corrupt)
130
+ * generation left intact on disk for inspection.
131
+ */
132
+ export function rebuildAndSwitch(input, currentGen, mode = "A") {
133
+ const result = applyTriad(mode, input);
134
+ const pointer = switchPointer(currentGen, input.generation, result.ok);
135
+ return { result, pointer };
136
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * vector-cortex/heal/repair-emit.ts — VC6C event reporter seam.
3
+ *
4
+ * Mirrors `./restore-emit.ts`: a thin `safe()` wrapper around an optional
5
+ * injected `emit`, and the three event names the sprint spec requires verbatim:
6
+ * - `vector_cortex_repair_planned` — a gap was detected, rebuild queued.
7
+ * - `vector_cortex_repair_pointer_switched`— a verified generation went live.
8
+ * - `vector_cortex_repair_backoff` — suppressed (rate limit) or failed.
9
+ *
10
+ * FLAG SEMANTICS. `detectGaps` / `planRebuild` / `rebuildGeneration` /
11
+ * `switchPointer` are PURE arithmetic and run REGARDLESS of `MEGACOMPACT_VC6C`.
12
+ * The flag gates ONLY this reporting + dashboard seam: with the flag off we still
13
+ * detect gaps, still verify digests, and still refuse unverified pointer
14
+ * switches — we just do not announce it under the VC6C event namespace. That is
15
+ * what makes flag-off byte-identical to VC6B: the computation is never skipped,
16
+ * only the emission.
17
+ *
18
+ * PAYLOAD DISCIPLINE. These events carry the SUBSYSTEM NAME, generation numbers,
19
+ * timings, and codes — never rebuilt bytes, never a root digest of user content,
20
+ * never a seq range's contents. The subsystem name is an operator-facing
21
+ * identifier ("topology"), not user data. A repair event is exactly the place
22
+ * where an unguarded `payload` field would leak a rebuilt transcript into a log.
23
+ *
24
+ * No console, no storage, no network (PREVENT-PI-004 / PREVENT-011). Every line
25
+ * is a structured JSON event with `ts` + `event`.
26
+ */
27
+ import { VC6C_ENABLED } from "../../config/vector-cortex.js";
28
+ /** Run `fn` only when an emit exists; a reporting failure is never fatal. */
29
+ function safe(emit, fn) {
30
+ if (emit === undefined)
31
+ return;
32
+ try {
33
+ fn(emit);
34
+ }
35
+ catch {
36
+ // Non-fatal: a reporting failure must never break the agent loop.
37
+ }
38
+ }
39
+ /** The event names VC6C emits, exported for the dashboard seam and tests. */
40
+ export const REPAIR_EVENT_NAMES = [
41
+ "vector_cortex_repair_planned",
42
+ "vector_cortex_repair_pointer_switched",
43
+ "vector_cortex_repair_backoff",
44
+ ];
45
+ /** Report a planned rebuild: which subsystem, which generation, what delay. */
46
+ export function reportRepairPlanned(emit, payload) {
47
+ if (!VC6C_ENABLED())
48
+ return;
49
+ safe(emit, (e) => e("vector_cortex_repair_planned", {
50
+ ts: undefined,
51
+ event: "vector_cortex_repair_planned",
52
+ subsystem: payload.subsystem,
53
+ generation: payload.generation,
54
+ backoffMs: payload.backoffMs,
55
+ gapSize: payload.gapSize,
56
+ }));
57
+ }
58
+ /** Report the atomic commit: a verified generation became live. */
59
+ export function reportRepairPointerSwitched(emit, payload) {
60
+ if (!VC6C_ENABLED())
61
+ return;
62
+ safe(emit, (e) => e("vector_cortex_repair_pointer_switched", {
63
+ ts: undefined,
64
+ event: "vector_cortex_repair_pointer_switched",
65
+ subsystem: payload.subsystem,
66
+ fromGeneration: payload.fromGeneration,
67
+ toGeneration: payload.toGeneration,
68
+ mode: payload.mode,
69
+ }));
70
+ }
71
+ /** Report a suppressed or failed rebuild now waiting out its backoff. */
72
+ export function reportRepairBackoff(emit, payload) {
73
+ if (!VC6C_ENABLED())
74
+ return;
75
+ safe(emit, (e) => e("vector_cortex_repair_backoff", {
76
+ ts: undefined,
77
+ event: "vector_cortex_repair_backoff",
78
+ subsystem: payload.subsystem,
79
+ code: payload.code,
80
+ backoffMs: payload.backoffMs,
81
+ attempt: payload.attempt,
82
+ }));
83
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * vector-cortex/heal/repair-types.ts — VC6C self-healing derived controller.
3
+ *
4
+ * VC6B answered "when a node's bytes are gone, WHERE do they come from?". VC6C
5
+ * answers the question one level up: when a DERIVED subsystem (topology, shards,
6
+ * closure) has fallen BEHIND the durable authority, how do we notice, and how do
7
+ * we catch it up without ever risking the authority itself?
8
+ *
9
+ * THE AUTHORITY IS READ-ONLY, ALWAYS. The controller compares each derived
10
+ * source's high-water to the durable authority high-water and plans work. It has
11
+ * no write path to the authority — not a guarded one, not an admin one. Derived
12
+ * state is disposable and can always be rebuilt from the byte ledger; the
13
+ * authority is not, so the repair path is deliberately one-directional. This is
14
+ * why `RepairState` carries `authorityHighWater` as a plain readonly field and
15
+ * why no function in `controller.ts` returns anything that could be applied to it.
16
+ *
17
+ * NEVER READ PAST THE AUTHORITY (TRIAD_RESILIENCE §frontier). A derived builder
18
+ * may not read beyond the durable CONTIGUOUS authority high-water. During an
19
+ * authority outage that high-water FREEZES even though the spool keeps accepting
20
+ * frames — so a derived subsystem that is "behind" a frozen frontier is CORRECT,
21
+ * not broken, and planning a rebuild against the spool tail would materialize
22
+ * frames that are not yet durable. `detectGaps` therefore treats
23
+ * `authorityFrozen` as a hard stop (`HEAL_REPAIR_AUTHORITY_FROZEN`) rather than
24
+ * as a large gap to chase. After the drain, catch-up resumes from the OLD
25
+ * high-water; it never jumps to the tail.
26
+ *
27
+ * REBUILD IS COPY-THEN-SWITCH, NEVER IN-PLACE. `rebuild.ts` materializes a NEW
28
+ * generation, verifies its root digest, and only then flips the pointer. A failed
29
+ * verification keeps the old pointer and DELETES NO EVIDENCE: the corrupt
30
+ * generation stays on disk to be inspected. An in-place repair would, by
31
+ * construction, destroy the only copy of the thing that was about to be proven
32
+ * wrong.
33
+ *
34
+ * RATE LIMIT + BACKOFF ARE THE BLAST RADIUS. A subsystem that fails to rebuild
35
+ * will fail again, usually instantly, so an unbounded retry loop turns one broken
36
+ * shard into a CPU-saturating rebuild storm. One rebuild per subsystem per 5
37
+ * minutes bounds the steady state, and the deterministic exponential backoff
38
+ * (30s * 2^attempt, capped at 15 min, ±10% jitter derived from the SUBSYSTEM
39
+ * DIGEST rather than `Math.random`) bounds the failure state while keeping the
40
+ * schedule reproducible in a fixture.
41
+ *
42
+ * Pure types + registered conformance IDs: no storage, no console, no clock, no
43
+ * network (PREVENT-PI-004 / PREVENT-011).
44
+ */
45
+ /**
46
+ * One rebuild per subsystem per 5 minutes. Bounds a rebuild storm: a subsystem
47
+ * that fails will fail again immediately, and without this an unhealthy tier
48
+ * would saturate the box re-materializing the same broken generation.
49
+ */
50
+ export const REPAIR_RATE_LIMIT_MS = 5 * 60_000;
51
+ /**
52
+ * Exponential backoff base/cap, matching the breaker's retry rule
53
+ * (TRIAD_RESILIENCE): `30s * 2^attempt`, capped at 15 minutes.
54
+ */
55
+ export const REPAIR_BACKOFF_BASE_MS = 30_000;
56
+ export const REPAIR_BACKOFF_CAP_MS = 15 * 60_000;
57
+ /**
58
+ * ±10% deterministic jitter. Derived from the SUBSYSTEM DIGEST, not a PRNG, so
59
+ * two subsystems desynchronize (no thundering herd) while any single subsystem's
60
+ * schedule stays reproducible in a fixture.
61
+ */
62
+ export const REPAIR_BACKOFF_JITTER = 0.1;
63
+ /**
64
+ * Registered VC6C conformance ID range (HEAL-031..045), continuing VC6B's
65
+ * HEAL-016..030. The acceptance test reads these rows from the v2 manifest and
66
+ * asserts each returns its manifest `ok`/`code`.
67
+ */
68
+ export const REPAIR_IDS = Array.from({ length: 15 }, (_v, i) => `HEAL-${String(i + 31).padStart(3, "0")}`);
69
+ /** Named VC6C conformance assertions (the sprint's headline rows). */
70
+ export const REPAIR_NAMED_IDS = [
71
+ "HEAL-GAP-001",
72
+ "HEAL-RATE-002",
73
+ "HEAL-SWITCH-003",
74
+ ];
@@ -41,3 +41,4 @@ export const HEAL_NAMED_IDS = [
41
41
  "HEAL-PROOF-003",
42
42
  ];
43
43
  export { RESTORE_IDS, RESTORE_NAMED_IDS, RESTORE_LIMIT_SPANS, RESTORE_LIMIT_BYTES, } from "./restore-types.js";
44
+ export { REPAIR_IDS, REPAIR_NAMED_IDS, REPAIR_RATE_LIMIT_MS, REPAIR_BACKOFF_BASE_MS, REPAIR_BACKOFF_CAP_MS, REPAIR_BACKOFF_JITTER, } from "./repair-types.js";