pi-mega-compact 0.20.7 → 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 (117) hide show
  1. package/dist/config/vector-cortex.js +25 -0
  2. package/dist/config.js +1 -1
  3. package/dist/extensions/dashboard-server/api-contracts/vector-cortex-heal.js +11 -0
  4. package/dist/extensions/dashboard-server/route-dispatch.js +104 -0
  5. package/dist/extensions/dashboard-server/routes-rag-settings-vector-cortex.js +2 -0
  6. package/dist/extensions/dashboard-server/routes-vector-cortex-heal.js +86 -0
  7. package/dist/extensions/dashboard-server/routes-vector-cortex.js +3 -0
  8. package/dist/extensions/dashboard-server/routes.js +1 -1
  9. package/dist/extensions/dashboard-server/server.js +3 -82
  10. package/dist/src/config/vector-cortex.js +25 -0
  11. package/dist/src/config.js +1 -1
  12. package/dist/src/vector-cortex/heal/_acceptance-fixture.js +56 -0
  13. package/dist/src/vector-cortex/heal/_restore-fixture.js +96 -0
  14. package/dist/src/vector-cortex/heal/_vc6b-helpers.js +35 -0
  15. package/dist/src/vector-cortex/heal/closure-metrics.js +66 -0
  16. package/dist/src/vector-cortex/heal/closure-opt.js +242 -0
  17. package/dist/src/vector-cortex/heal/emit.js +69 -0
  18. package/dist/src/vector-cortex/heal/proof.js +210 -0
  19. package/dist/src/vector-cortex/heal/restore-emit.js +70 -0
  20. package/dist/src/vector-cortex/heal/restore-readers.js +114 -0
  21. package/dist/src/vector-cortex/heal/restore-types.js +66 -0
  22. package/dist/src/vector-cortex/heal/restore.js +140 -0
  23. package/dist/src/vector-cortex/heal/types.js +43 -0
  24. package/dist/src/vector-cortex/heal/verify.js +75 -0
  25. package/dist/vector-cortex/heal/_acceptance-fixture.js +56 -0
  26. package/dist/vector-cortex/heal/_restore-fixture.js +96 -0
  27. package/dist/vector-cortex/heal/_vc6b-helpers.js +35 -0
  28. package/dist/vector-cortex/heal/closure-metrics.js +66 -0
  29. package/dist/vector-cortex/heal/closure-opt.js +242 -0
  30. package/dist/vector-cortex/heal/emit.js +69 -0
  31. package/dist/vector-cortex/heal/proof.js +210 -0
  32. package/dist/vector-cortex/heal/restore-emit.js +70 -0
  33. package/dist/vector-cortex/heal/restore-readers.js +114 -0
  34. package/dist/vector-cortex/heal/restore-types.js +66 -0
  35. package/dist/vector-cortex/heal/restore.js +140 -0
  36. package/dist/vector-cortex/heal/types.js +43 -0
  37. package/dist/vector-cortex/heal/verify.js +75 -0
  38. package/extensions/dashboard-client/dist/assets/{AreaChart-DqTbaSRA.js → AreaChart-fxyq2KaH.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{AreaChart-DqTbaSRA.js.map → AreaChart-fxyq2KaH.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/{BarChart-BfNBeOvF.js → BarChart-D5EqAINP.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{BarChart-BfNBeOvF.js.map → BarChart-D5EqAINP.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/{CacheTab-C9SOMYB5.js → CacheTab-D4UWbzLo.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{CacheTab-C9SOMYB5.js.map → CacheTab-D4UWbzLo.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/{EventsTab-GsMKP10_.js → EventsTab-BiXquGrX.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{EventsTab-GsMKP10_.js.map → EventsTab-BiXquGrX.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{HealthTab-DAOCXwRP.js → HealthTab-D5QvoTjy.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{HealthTab-DAOCXwRP.js.map → HealthTab-D5QvoTjy.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-BtBeMkB5.js → MaintenanceTab-Cy_-CWgM.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-BtBeMkB5.js.map → MaintenanceTab-Cy_-CWgM.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DzndtgkZ.js → MemoryMapTab-DrAjgGOR.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DzndtgkZ.js.map → MemoryMapTab-DrAjgGOR.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/{MetricsTab-DyxJ4eb8.js → MetricsTab-BvPHcuFN.js} +2 -2
  53. package/extensions/dashboard-client/dist/assets/{MetricsTab-DyxJ4eb8.js.map → MetricsTab-BvPHcuFN.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/{OverviewTab-DCiEdVWR.js → OverviewTab-DsbIigOQ.js} +2 -2
  55. package/extensions/dashboard-client/dist/assets/{OverviewTab-DCiEdVWR.js.map → OverviewTab-DsbIigOQ.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/{ReposTab-B5SIqVHA.js → ReposTab-7v_uZX_a.js} +2 -2
  57. package/extensions/dashboard-client/dist/assets/{ReposTab-B5SIqVHA.js.map → ReposTab-7v_uZX_a.js.map} +1 -1
  58. package/extensions/dashboard-client/dist/assets/{SessionsTab-DLWFJ9xI.js → SessionsTab-BXGrv-6C.js} +2 -2
  59. package/extensions/dashboard-client/dist/assets/{SessionsTab-DLWFJ9xI.js.map → SessionsTab-BXGrv-6C.js.map} +1 -1
  60. package/extensions/dashboard-client/dist/assets/{SetupTab-CvGXZyej.js → SetupTab-QRUXa65y.js} +2 -2
  61. package/extensions/dashboard-client/dist/assets/{SetupTab-CvGXZyej.js.map → SetupTab-QRUXa65y.js.map} +1 -1
  62. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-wZuQGxLS.js → TimeSavedCard-p327ZZKT.js} +2 -2
  63. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-wZuQGxLS.js.map → TimeSavedCard-p327ZZKT.js.map} +1 -1
  64. package/extensions/dashboard-client/dist/assets/{TurnsTab-B7aOejch.js → TurnsTab-BcmBSlik.js} +2 -2
  65. package/extensions/dashboard-client/dist/assets/{TurnsTab-B7aOejch.js.map → TurnsTab-BcmBSlik.js.map} +1 -1
  66. package/extensions/dashboard-client/dist/assets/VectorCortexTab-D2fjKNyT.js +2 -0
  67. package/extensions/dashboard-client/dist/assets/VectorCortexTab-D2fjKNyT.js.map +1 -0
  68. package/extensions/dashboard-client/dist/assets/{WikiTab-DhWHjAtt.js → WikiTab-DLXFMuNy.js} +2 -2
  69. package/extensions/dashboard-client/dist/assets/{WikiTab-DhWHjAtt.js.map → WikiTab-DLXFMuNy.js.map} +1 -1
  70. package/extensions/dashboard-client/dist/assets/{button-BrC-W87h.js → button-C9CghmRx.js} +2 -2
  71. package/extensions/dashboard-client/dist/assets/{button-BrC-W87h.js.map → button-C9CghmRx.js.map} +1 -1
  72. package/extensions/dashboard-client/dist/assets/{card-0rgfRgr3.js → card-DWU-1y3t.js} +2 -2
  73. package/extensions/dashboard-client/dist/assets/{card-0rgfRgr3.js.map → card-DWU-1y3t.js.map} +1 -1
  74. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-DRresOQF.js → generateCategoricalChart-Dayy9dGn.js} +2 -2
  75. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-DRresOQF.js.map → generateCategoricalChart-Dayy9dGn.js.map} +1 -1
  76. package/extensions/dashboard-client/dist/assets/{index-DohmtZWL.js → index-D9PsW3VZ.js} +3 -3
  77. package/extensions/dashboard-client/dist/assets/{index-DohmtZWL.js.map → index-D9PsW3VZ.js.map} +1 -1
  78. package/extensions/dashboard-client/dist/assets/{switch-Bs8g_zB5.js → switch-CnLFAmVT.js} +2 -2
  79. package/extensions/dashboard-client/dist/assets/{switch-Bs8g_zB5.js.map → switch-CnLFAmVT.js.map} +1 -1
  80. package/extensions/dashboard-client/dist/assets/{toggle-DVVauIo8.js → toggle-HN5C7_vy.js} +2 -2
  81. package/extensions/dashboard-client/dist/assets/{toggle-DVVauIo8.js.map → toggle-HN5C7_vy.js.map} +1 -1
  82. package/extensions/dashboard-client/dist/assets/{useSSE-DxH1PTAR.js → useSSE-DoJfk3Ce.js} +2 -2
  83. package/extensions/dashboard-client/dist/assets/{useSSE-DxH1PTAR.js.map → useSSE-DoJfk3Ce.js.map} +1 -1
  84. package/extensions/dashboard-client/dist/index.html +1 -1
  85. package/extensions/dashboard-client/src/api/vector-cortex.ts +18 -0
  86. package/extensions/dashboard-client/src/tabs/VectorCortexClosureCard.tsx +46 -0
  87. package/extensions/dashboard-client/src/tabs/VectorCortexLedgerCard.tsx +80 -0
  88. package/extensions/dashboard-client/src/tabs/VectorCortexRestoreCard.tsx +42 -0
  89. package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +20 -164
  90. package/extensions/dashboard-client/src/tabs/VectorCortexTopologyCard.tsx +131 -0
  91. package/extensions/dashboard-client/src/types/vector-cortex.ts +30 -0
  92. package/extensions/dashboard-server/api-contracts/vector-cortex-heal.ts +73 -0
  93. package/extensions/dashboard-server/api-contracts/vector-cortex.ts +5 -0
  94. package/extensions/dashboard-server/route-dispatch.ts +114 -0
  95. package/extensions/dashboard-server/routes-rag-settings-vector-cortex.ts +12 -0
  96. package/extensions/dashboard-server/routes-vector-cortex-heal.ts +103 -0
  97. package/extensions/dashboard-server/routes-vector-cortex.ts +6 -0
  98. package/extensions/dashboard-server/routes.ts +2 -0
  99. package/extensions/dashboard-server/server.ts +3 -86
  100. package/package.json +1 -1
  101. package/src/config/vector-cortex.ts +27 -0
  102. package/src/config.ts +2 -0
  103. package/src/vector-cortex/heal/_acceptance-fixture.ts +108 -0
  104. package/src/vector-cortex/heal/_restore-fixture.ts +163 -0
  105. package/src/vector-cortex/heal/_vc6b-helpers.ts +55 -0
  106. package/src/vector-cortex/heal/closure-metrics.ts +73 -0
  107. package/src/vector-cortex/heal/closure-opt.ts +266 -0
  108. package/src/vector-cortex/heal/emit.ts +93 -0
  109. package/src/vector-cortex/heal/proof.ts +234 -0
  110. package/src/vector-cortex/heal/restore-emit.ts +94 -0
  111. package/src/vector-cortex/heal/restore-readers.ts +145 -0
  112. package/src/vector-cortex/heal/restore-types.ts +185 -0
  113. package/src/vector-cortex/heal/restore.ts +165 -0
  114. package/src/vector-cortex/heal/types.ts +248 -0
  115. package/src/vector-cortex/heal/verify.ts +93 -0
  116. package/extensions/dashboard-client/dist/assets/VectorCortexTab-eHVt9yDx.js +0 -2
  117. package/extensions/dashboard-client/dist/assets/VectorCortexTab-eHVt9yDx.js.map +0 -1
@@ -0,0 +1,266 @@
1
+ /**
2
+ * vector-cortex/heal/closure-opt.ts — VC6A deterministic transitive reduction
3
+ * over the ALREADY-MANDATORY VC4C closure.
4
+ *
5
+ * WHAT THIS DOES NOT DO: it does not close anything and it does not change which
6
+ * nodes are selected. `closeSelection` (VC4C) has already run and produced the
7
+ * mandatory set; CONTRACTS §plan and closure is explicit that "VC6 only
8
+ * optimizes restoration/self-healing". This module takes that closed set and
9
+ * shrinks the EDGE PLAN used to justify/re-walk it.
10
+ *
11
+ * THE REDUCTION. Requirements flow prerequisite `from` → dependent `to`, and
12
+ * closure walks them BACKWARD (selecting `to` pulls in `from`). So the
13
+ * requirement relation is `to ⇒ from`. If `c ⇒ b` and `b ⇒ a` are both in the
14
+ * plan, then a third edge `c ⇒ a` adds nothing: walking `c` already reaches `a`
15
+ * through `b`. That third edge is transitively implied and is the only kind of
16
+ * edge this module removes.
17
+ *
18
+ * WHAT IS NEVER REMOVED (task 3):
19
+ * - a `tool-pair` edge — the pair is atomic (PREVENT-PI-002);
20
+ * - any edge touching an anchor — the floor is preserved (PREVENT-PI-001);
21
+ * - a `contradicts` edge — the resolver reads it directly, so removing
22
+ * it would change the resolved set;
23
+ * - a SOLE dependency edge — the only edge pulling its prerequisite in.
24
+ * Every considered edge — retained or removed — is recorded in the proof, so the
25
+ * verifier can replay the decision rather than trust it.
26
+ *
27
+ * DETERMINISM. Vertices and edges are sorted by ID BYTES (the same bytewise
28
+ * comparator `reconstruct/closure.ts` uses), the reachability search drains a
29
+ * sorted worklist, and every emitted array is sorted. Two graphs that differ
30
+ * only in input order produce byte-identical proofs.
31
+ *
32
+ * PURITY. No storage, no console, no clock, no network (PREVENT-PI-004 /
33
+ * PREVENT-011). The FLAG DOES NOT GATE THIS ARITHMETIC: `MEGACOMPACT_VC6A=0`
34
+ * gates the reporter + dashboard seam only (see `emit.ts`), exactly as
35
+ * VC5B/VC5C do, so flag-off is byte-identical to the predecessor.
36
+ */
37
+
38
+ import type { ClosureEdge, ClosureGraph, ClosureResult } from "../reconstruct/types.js";
39
+ import type { ClosureProofRow, ClosureProofV2, RetainReason } from "./types.js";
40
+
41
+ /** Bytewise id comparator — identical to `reconstruct/closure.ts::sortedIds`. */
42
+ function byBytes(a: string, b: string): number {
43
+ return a < b ? -1 : a > b ? 1 : 0;
44
+ }
45
+
46
+ /** Total order over edges: `from`, then `to`, then `kind` — all bytewise. */
47
+ function compareEdges(x: ClosureEdge, y: ClosureEdge): number {
48
+ return byBytes(x.from, y.from) || byBytes(x.to, y.to) || byBytes(x.kind, y.kind);
49
+ }
50
+
51
+ /** Sorted copy of an edge list (never mutates the caller's array). */
52
+ function sortedEdges(edges: readonly ClosureEdge[]): ClosureEdge[] {
53
+ return [...edges].sort(compareEdges);
54
+ }
55
+
56
+ /**
57
+ * The requirement adjacency of a plan: for a dependent node, which prerequisites
58
+ * it pulls in. Mirrors `reconstruct/closure.ts::buildRequirements` — `depends`
59
+ * walks backward to the prerequisite, `tool-pair` walks BOTH ways because a pair
60
+ * is atomic. `contradicts` never pulls a node in and is therefore absent here
61
+ * (it is still protected from removal; it simply is not a traversal edge).
62
+ */
63
+ function requirementAdjacency(edges: readonly ClosureEdge[]): Map<string, string[]> {
64
+ const adj = new Map<string, string[]>();
65
+ const add = (key: string, value: string): void => {
66
+ const list = adj.get(key);
67
+ if (list === undefined) adj.set(key, [value]);
68
+ else list.push(value);
69
+ };
70
+ for (const e of edges) {
71
+ if (e.kind === "depends") add(e.to, e.from);
72
+ else if (e.kind === "tool-pair") {
73
+ add(e.to, e.from);
74
+ add(e.from, e.to);
75
+ }
76
+ }
77
+ for (const list of adj.values()) list.sort(byBytes);
78
+ return adj;
79
+ }
80
+
81
+ /**
82
+ * Is `target` reachable from `start` through the requirement adjacency WITHOUT
83
+ * using the direct `start ⇒ target` hop? Returns the witnessing intermediate
84
+ * vertex (the first hop of the detour, bytewise-smallest) or `null`.
85
+ *
86
+ * This is the "alternate path exists" test that licenses a removal. The direct
87
+ * hop is excluded so an edge can never justify its own removal; a cycle simply
88
+ * revisits `seen` and terminates.
89
+ */
90
+ function alternatePathVia(
91
+ adj: ReadonlyMap<string, string[]>,
92
+ start: string,
93
+ target: string,
94
+ ): string | null {
95
+ // First hops other than the direct one, in bytewise order so the witness is
96
+ // deterministic when several detours exist.
97
+ const firstHops = (adj.get(start) ?? []).filter((n) => n !== target);
98
+ for (const hop of firstHops) {
99
+ // Breadth-first from this hop; `seen` excludes `start` so a detour that
100
+ // loops back through the origin cannot be counted as progress.
101
+ const seen = new Set<string>([start]);
102
+ const worklist: string[] = [hop];
103
+ while (worklist.length > 0) {
104
+ worklist.sort(byBytes);
105
+ const current = worklist.shift();
106
+ if (current === undefined) break;
107
+ if (current === target) return hop;
108
+ if (seen.has(current)) continue;
109
+ seen.add(current);
110
+ for (const next of adj.get(current) ?? []) {
111
+ if (!seen.has(next)) worklist.push(next);
112
+ }
113
+ }
114
+ }
115
+ return null;
116
+ }
117
+
118
+ /**
119
+ * Is `target` reachable from `start` using ONLY the edges in `others` (a path of
120
+ * length >= 2, never the direct hop)? Returns the bytewise-smallest first-hop
121
+ * vertex (the witness `via`) or `null`.
122
+ *
123
+ * The transitive-reduction correctness rule: an edge `start ⇒ target` is
124
+ * redundant only if a length>=2 path exists through the OTHER edges. Crucially
125
+ * we EXCLUDE the edge under consideration, so removing one edge never invalidates
126
+ * the witness used to justify removing ANOTHER — a plain single-pass check over
127
+ * the full adjacency over-removes (HEAL-PROTECT-002: a tool pair backs two
128
+ * depends edges that would each look implied by the full graph but only survive
129
+ * together).
130
+ */
131
+ function alternatePathExcluding(
132
+ others: readonly ClosureEdge[],
133
+ start: string,
134
+ target: string,
135
+ ): string | null {
136
+ const adj = requirementAdjacency(others);
137
+ return alternatePathVia(adj, start, target);
138
+ }
139
+
140
+ /** The set of node ids marked as anchor-floor members in the graph. */
141
+ function anchorIds(graph: ClosureGraph): Set<string> {
142
+ const out = new Set<string>();
143
+ for (const n of graph.nodes) if (n.anchor === true) out.add(n.id);
144
+ return out;
145
+ }
146
+
147
+ /**
148
+ * Count how many `depends` edges pull in each prerequisite. A prerequisite with
149
+ * exactly one such edge has a SOLE dependency edge: removing it would drop the
150
+ * prerequisite from the closure entirely, so it is protected regardless of any
151
+ * alternate path.
152
+ */
153
+ function dependsFanIn(edges: readonly ClosureEdge[]): Map<string, number> {
154
+ const counts = new Map<string, number>();
155
+ for (const e of edges) {
156
+ if (e.kind !== "depends") continue;
157
+ counts.set(e.from, (counts.get(e.from) ?? 0) + 1);
158
+ }
159
+ return counts;
160
+ }
161
+
162
+ /**
163
+ * Classify a NON-removable edge, or return `null` when the edge is an ordinary
164
+ * `depends` edge that removal is at least allowed to consider.
165
+ *
166
+ * Order matters and is deliberate: the strongest structural guarantee is
167
+ * reported first, so a tool-pair edge that also touches an anchor is recorded as
168
+ * `tool-pair` and the proof reads as the reason a reviewer would give.
169
+ */
170
+ function protectedReason(
171
+ edge: ClosureEdge,
172
+ anchors: ReadonlySet<string>,
173
+ fanIn: ReadonlyMap<string, number>,
174
+ ): RetainReason | null {
175
+ if (edge.kind === "tool-pair") return "tool-pair";
176
+ if (edge.kind === "contradicts") return "contradiction";
177
+ if (anchors.has(edge.from) || anchors.has(edge.to)) return "anchor";
178
+ if ((fanIn.get(edge.from) ?? 0) <= 1) return "sole-dependency";
179
+ return null;
180
+ }
181
+
182
+ /** Input to one optimization run: the graph plus the conservative VC4C result. */
183
+ export interface ClosureOptInput {
184
+ readonly graph: ClosureGraph;
185
+ /** The ALREADY-COMPUTED conservative closure (VC4C `closeSelection`). */
186
+ readonly conservative: ClosureResult;
187
+ }
188
+
189
+ /**
190
+ * Optimize the already-mandatory closure by transitive reduction (task 2).
191
+ *
192
+ * Only edges INSIDE the closed selection are considered: an edge touching an
193
+ * unselected node is not part of the traversal plan for this closure at all, so
194
+ * it is neither retained nor removed — it is simply out of scope and produces no
195
+ * proof row.
196
+ *
197
+ * `selected` is copied through UNCHANGED. That is the whole safety property: the
198
+ * optimizer is allowed to make the plan cheaper, never to make it smaller.
199
+ */
200
+ export function optimizeClosure(input: ClosureOptInput): ClosureProofV2 {
201
+ const { graph, conservative } = input;
202
+ const selected = new Set(conservative.selected);
203
+ const anchors = anchorIds(graph);
204
+
205
+ // Consider only in-selection edges, in deterministic bytewise order.
206
+ const considered = sortedEdges(
207
+ graph.edges.filter((e) => selected.has(e.from) && selected.has(e.to)),
208
+ );
209
+ const fanIn = dependsFanIn(considered);
210
+
211
+ const rows: ClosureProofRow[] = [];
212
+ const retained: ClosureEdge[] = [];
213
+ const removed: ClosureEdge[] = [];
214
+
215
+ for (const e of considered) {
216
+ const guard = protectedReason(e, anchors, fanIn);
217
+ if (guard !== null) {
218
+ // Protected: retained unconditionally, EVEN WHEN an alternate path exists
219
+ // (HEAL-PROTECT-002). The reason names the guarantee, not the topology.
220
+ rows.push({ from: e.from, to: e.to, kind: e.kind, decision: "retained", reason: guard });
221
+ retained.push(e);
222
+ continue;
223
+ }
224
+ // Ordinary `depends` edge: removable iff the requirement it carries is
225
+ // already carried by a longer path `to ⇒ via ⇒ ... ⇒ from` through the OTHER
226
+ // edges (the edge under test is excluded so removals stay mutually valid).
227
+ const others = considered.filter((o) => o !== e);
228
+ const via = alternatePathExcluding(others, e.to, e.from);
229
+ if (via === null) {
230
+ rows.push({
231
+ from: e.from,
232
+ to: e.to,
233
+ kind: e.kind,
234
+ decision: "retained",
235
+ reason: "no-alternate-path",
236
+ });
237
+ retained.push(e);
238
+ continue;
239
+ }
240
+ rows.push({
241
+ from: e.from,
242
+ to: e.to,
243
+ kind: e.kind,
244
+ decision: "removed",
245
+ reason: "transitively-implied",
246
+ via,
247
+ });
248
+ removed.push(e);
249
+ }
250
+
251
+ return {
252
+ schema: "closure-proof-v2",
253
+ sessionId: graph.sessionId,
254
+ selected: [...conservative.selected],
255
+ retainedEdges: sortedEdges(retained),
256
+ removedEdges: sortedEdges(removed),
257
+ rows,
258
+ conservativeTraversals: considered.length,
259
+ optimizedTraversals: retained.length,
260
+ };
261
+ }
262
+
263
+ // Metrics + VC6B handoff (traversal savings, RestoreHintV1) live in the
264
+ // delegate-shell sibling `closure-metrics.ts` to keep this core reduction
265
+ // algorithm under the 300-line soft limit.
266
+ export { traversalSavings, restoreHints } from "./closure-metrics.js";
@@ -0,0 +1,93 @@
1
+ /**
2
+ * vector-cortex/heal/emit.ts — VC6A event reporter seam (task 5).
3
+ *
4
+ * Mirrors `../rollout/emit.ts`: a thin `safe()` wrapper around an optional
5
+ * injected `emit` (so unit tests stay pure — they pass `undefined` and inspect
6
+ * nothing), and two event names required verbatim by the sprint spec:
7
+ * - `vector_cortex_closure_optimized` — emitted when a proof is produced.
8
+ * - `vector_cortex_closure_proof_rejected` — emitted when verification fails.
9
+ *
10
+ * FLAG SEMANTICS (the invariant VC5B/VC5C established). The optimizer and
11
+ * verifier are PURE arithmetic and run REGARDLESS of the flag. The flag gates
12
+ * ONLY the reporting + dashboard seam: when `MEGACOMPACT_VC6A=0` we still compute
13
+ * the proof (needed for byte-identical parity with the predecessor), we just do
14
+ * not emit the VC6A-namespaced events. This is what makes flag-off byte-identical
15
+ * to VC5C — the arithmetic is never skipped, only the announcement.
16
+ *
17
+ * No console, no storage, no network (PREVENT-PI-004 / PREVENT-011). Every line
18
+ * is a structured JSON event with `ts` + `event`.
19
+ */
20
+
21
+ import { VC6A_ENABLED } from "../../config/vector-cortex.js";
22
+ import type { HealEventName } from "./types.js";
23
+
24
+ /** Optional emit fn injected by the runtime; tests pass `undefined`. */
25
+ export type HealEmit = (name: string, payload: unknown) => void;
26
+
27
+ /**
28
+ * Run `fn` only when an emit is available, swallowing and logging nothing on
29
+ * failure (PRACTICES: non-fatal stores). `fn` receives the emit so callers need
30
+ * not null-check.
31
+ */
32
+ function safe(emit: HealEmit | undefined, fn: (emit: HealEmit) => void): void {
33
+ if (emit === undefined) return;
34
+ try {
35
+ fn(emit);
36
+ } catch {
37
+ // Non-fatal: a reporting failure must never break the agent loop.
38
+ }
39
+ }
40
+
41
+ /** The event names VC6A emits, exported for the dashboard seam and tests. */
42
+ export const HEAL_EVENT_NAMES: readonly HealEventName[] = [
43
+ "vector_cortex_closure_optimized",
44
+ "vector_cortex_closure_proof_rejected",
45
+ ] as const;
46
+
47
+ /**
48
+ * Report a successful optimization. `MEGACOMPACT_VC6A=0` suppresses the VC6A
49
+ * event (flag-off parity) but does not affect the computation that produced
50
+ * `proof`.
51
+ */
52
+ export function reportClosureOptimized(
53
+ emit: HealEmit | undefined,
54
+ payload: {
55
+ readonly sessionId: string;
56
+ readonly removed: number;
57
+ readonly retained: number;
58
+ readonly savings: number;
59
+ },
60
+ ): void {
61
+ if (!VC6A_ENABLED()) return;
62
+ safe(emit, (e) =>
63
+ e("vector_cortex_closure_optimized", {
64
+ ts: undefined,
65
+ event: "vector_cortex_closure_optimized",
66
+ sessionId: payload.sessionId,
67
+ removedEdges: payload.removed,
68
+ retainedEdges: payload.retained,
69
+ traversalSavings: payload.savings,
70
+ }),
71
+ );
72
+ }
73
+
74
+ /** Report a rejected proof. Suppressed under flag-off, like the above. */
75
+ export function reportProofRejected(
76
+ emit: HealEmit | undefined,
77
+ payload: {
78
+ readonly sessionId: string;
79
+ readonly reason: string;
80
+ readonly mode: "B" | "C";
81
+ },
82
+ ): void {
83
+ if (!VC6A_ENABLED()) return;
84
+ safe(emit, (e) =>
85
+ e("vector_cortex_closure_proof_rejected", {
86
+ ts: undefined,
87
+ event: "vector_cortex_closure_proof_rejected",
88
+ sessionId: payload.sessionId,
89
+ reason: payload.reason,
90
+ fallbackMode: payload.mode,
91
+ }),
92
+ );
93
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * vector-cortex/heal/proof.ts — VC6A proof verifier (task 4).
3
+ *
4
+ * The optimizer (`closure-opt.ts`) is PURE and cheap, but a proof is cheap to
5
+ * LIE about: a buggy or tampered producer could ship a proof that removes an
6
+ * edge the conservative closure actually needs. The verifier does not trust the
7
+ * proof — it REPLAYS it against the conservative oracle from scratch.
8
+ *
9
+ * REPLAY. From the conservative `ClosureResult` we rebuild the requirement
10
+ * adjacency (`to ⇒ from` for `depends`, both directions for `tool-pair`) over
11
+ * the in-selection edges — exactly the relation `closure-opt` reduced over.
12
+ * For every row the proof claims to REMOVE, the verifier independently confirms
13
+ * the same alternate path exists. For every row it claims to RETAIN, it confirms
14
+ * the protection reason holds (or that no alternate path exists). Any row that
15
+ * fails its own predicate is a witness violation.
16
+ *
17
+ * SELECTED-SET DIVERGENCE (the cardinal sin). The proof must contain the SAME
18
+ * selected set as the conservative oracle, byte for byte. Closure optimization
19
+ * is allowed to make the plan cheaper, never smaller — so if the proof's
20
+ * `selected` differs from the oracle's, that is `HEAL_PROOF_SET_MISMATCH` and the
21
+ * repaired triad must fall back to mode B (the conservative closure) and
22
+ * ultimately C (VC5C's legacy prompt) if even that fails.
23
+ *
24
+ * PURE / DETERMINISTIC / LOCAL, like everything in `heal/`: no storage, no
25
+ * console, no clock, no network (PREVENT-PI-004 / PREVENT-011). The verifier
26
+ * reads only types and the two ClosureEdge/ClosureGraph/ClosureResult shapes.
27
+ */
28
+
29
+ import type { ClosureGraph, ClosureResult } from "../reconstruct/types.js";
30
+ import type {
31
+ ClosureProofRow,
32
+ ClosureProofV2,
33
+ HealFailureCode,
34
+ HealMode,
35
+ HealTriadOutcome,
36
+ ProofVerification,
37
+ } from "./types.js";
38
+
39
+ /** Bytewise id comparator — matches `closure-opt.ts` / `reconstruct/closure.ts`. */
40
+ function byBytes(a: string, b: string): number {
41
+ return a < b ? -1 : a > b ? 1 : 0;
42
+ }
43
+
44
+ /** Requirement adjacency over an edge list, identical to `closure-opt`. */
45
+ function requirementAdjacency(edges: readonly ClosureProofRow[]): Map<string, string[]> {
46
+ const adj = new Map<string, string[]>();
47
+ const add = (key: string, value: string): void => {
48
+ const list = adj.get(key);
49
+ if (list === undefined) adj.set(key, [value]);
50
+ else list.push(value);
51
+ };
52
+ for (const row of edges) {
53
+ if (row.kind === "contradicts") continue; // not a traversal edge
54
+ if (row.kind === "depends") add(row.to, row.from);
55
+ else if (row.kind === "tool-pair") {
56
+ add(row.to, row.from);
57
+ add(row.from, row.to);
58
+ }
59
+ }
60
+ for (const list of adj.values()) list.sort(byBytes);
61
+ return adj;
62
+ }
63
+
64
+ /** Does a detour `start ⇒ via ⇒ ... ⇒ target` exist, excluding the direct hop? */
65
+ function alternatePathVia(
66
+ adj: ReadonlyMap<string, string[]>,
67
+ start: string,
68
+ target: string,
69
+ ): string | null {
70
+ for (const hop of (adj.get(start) ?? []).filter((n) => n !== target)) {
71
+ const seen = new Set<string>([start]);
72
+ const worklist: string[] = [hop];
73
+ while (worklist.length > 0) {
74
+ worklist.sort(byBytes);
75
+ const current = worklist.shift();
76
+ if (current === undefined) break;
77
+ if (current === target) return hop;
78
+ if (seen.has(current)) continue;
79
+ seen.add(current);
80
+ for (const next of adj.get(current) ?? []) {
81
+ if (!seen.has(next)) worklist.push(next);
82
+ }
83
+ }
84
+ }
85
+ return null;
86
+ }
87
+
88
+ /**
89
+ * Verify a `ClosureProofV2` produced for an already-closed graph.
90
+ *
91
+ * Returns `{ ok: true, selected }` when the replay confirms the optimized plan is
92
+ * provably equivalent to the conservative closure — mode A may be used. On any
93
+ * divergence it returns `{ ok: false, codes }` listing every failure code; the
94
+ * caller routes to mode B (conservative) and, if that also fails, mode C (legacy,
95
+ * with `semanticLossStated`). The verifier is idempotent — replaying the same
96
+ * proof twice yields the same verdict, which `vc6a-acceptance.test.ts` asserts.
97
+ */
98
+ export function verifyProof(
99
+ proof: ClosureProofV2,
100
+ conservative: ClosureResult,
101
+ graph?: ClosureGraph,
102
+ ): ProofVerification {
103
+ const codes: HealFailureCode[] = [];
104
+ const inSelection = new Set(conservative.selected);
105
+ const anchors = new Set(
106
+ (graph?.nodes ?? []).filter((n) => n.anchor === true).map((n) => n.id),
107
+ );
108
+
109
+ // 1. Selected-set divergence is the hard gate (task 4: HEAL_PROOF_SET_MISMATCH).
110
+ const oracleSelected = [...conservative.selected].sort(byBytes);
111
+ const proofSelected = [...proof.selected].sort(byBytes);
112
+ const selectedMatch =
113
+ oracleSelected.length === proofSelected.length &&
114
+ oracleSelected.every((id, i) => id === proofSelected[i]);
115
+ if (!selectedMatch) codes.push("HEAL_PROOF_SET_MISMATCH");
116
+
117
+ // 2. Re-check each row against its own predicate. For a removal, the witness
118
+ // is validated against the OTHER rows (the reduced graph), so a removal
119
+ // never justifies another's witness — this catches both a missing-witness
120
+ // removal (HEAL-PROOF-003) and a protected edge that was removed
121
+ // (HEAL_PROOF_PROTECTED_REMOVED).
122
+
123
+ for (const row of proof.rows) {
124
+ // Out-of-scope rows (touch an unselected node) should never have been
125
+ // emitted; treat as a witness violation so a malformed proof fails loudly.
126
+ if (!inSelection.has(row.from) || !inSelection.has(row.to)) {
127
+ codes.push("HEAL_PROOF_WITNESS_INVALID");
128
+ continue;
129
+ }
130
+ if (row.decision === "removed") {
131
+ // A removal MUST carry a witness and the witness MUST still validate
132
+ // against the OTHER rows (the reduced graph) — never the row itself, so a
133
+ // removal never justifies another's witness (matches the optimizer's
134
+ // exclusion rule).
135
+ if (row.via === undefined) {
136
+ codes.push("HEAL_PROOF_WITNESS_INVALID");
137
+ continue;
138
+ }
139
+ const others = proof.rows.filter((r) => r !== row);
140
+ const witness = alternatePathVia(requirementAdjacency(others), row.to, row.from);
141
+ if (witness === null) {
142
+ codes.push("HEAL_PROOF_WITNESS_INVALID");
143
+ continue;
144
+ }
145
+ if (anchors.has(row.from) || anchors.has(row.to)) {
146
+ // Topology says removable, but an anchor edge is NEVER removable.
147
+ codes.push("HEAL_PROOF_PROTECTED_REMOVED");
148
+ continue;
149
+ }
150
+ if (row.kind === "tool-pair" || row.kind === "contradicts") {
151
+ // Both are protected regardless of topology.
152
+ codes.push("HEAL_PROOF_PROTECTED_REMOVED");
153
+ }
154
+ } else {
155
+ // A retained plain depends edge that claims `no-alternate-path` but
156
+ // actually HAS one (through the other rows) is an internal inconsistency:
157
+ // the optimizer kept a removable edge without removing it. The proof is
158
+ // internally contradictory, so it fails replay.
159
+ if (row.kind === "depends" && row.reason === "no-alternate-path") {
160
+ const others = proof.rows.filter((r) => r !== row);
161
+ const hasPath = alternatePathVia(requirementAdjacency(others), row.to, row.from);
162
+ if (hasPath !== null) codes.push("HEAL_PROOF_WITNESS_INVALID");
163
+ }
164
+ }
165
+ }
166
+
167
+ // 3. Completeness: every in-selection edge the optimizer considered must appear
168
+ // in the proof. A dropped row (the unique-injection test drops one) surfaces
169
+ // as HEAL_PROOF_INCOMPLETE even if the remaining rows replay fine — the proof
170
+ // must account for EVERY considered edge. `conservativeTraversals` is the
171
+ // optimizer's own record of how many edges it fed through, so `rows.length`
172
+ // must equal it exactly.
173
+ if (proof.rows.length < proof.conservativeTraversals) {
174
+ codes.push("HEAL_PROOF_INCOMPLETE");
175
+ }
176
+
177
+ if (codes.length === 0) {
178
+ return { ok: true, selected: proofSelected };
179
+ }
180
+ return { ok: false, codes: [...new Set(codes)] };
181
+ }
182
+
183
+ /**
184
+ * Drive the A/B/C resilient triad for one closure (task 5 wiring; VC6A exposes
185
+ * the decision; the actual selection/budget work is VC4C/VC5A respectively).
186
+ *
187
+ * - A: the optimized proof verified cleanly → ship the optimized plan.
188
+ * - B: proof rejected → fall back to the conservative VC4C closure (no opt).
189
+ * - C: B also failed (should not happen for a sound conservative closure) →
190
+ * state the legacy prompt path with `semanticLossStated`.
191
+ *
192
+ * This is PURE: it returns the verdict and which mode to use; it does not emit,
193
+ * store, or touch the network. The emit seam (`emit.ts`) consumes the outcome.
194
+ */
195
+ export function selectHealMode(
196
+ proof: ClosureProofV2,
197
+ conservative: ClosureResult,
198
+ graph?: ClosureGraph,
199
+ ): HealTriadOutcome {
200
+ const verification = verifyProof(proof, conservative, graph);
201
+ if (verification.ok) {
202
+ return {
203
+ mode: "A" as HealMode,
204
+ proof,
205
+ selected: [...proof.selected],
206
+ codes: [],
207
+ semanticLossStated: false,
208
+ };
209
+ }
210
+ // Mode B: conservative closure is sound by construction (it is the oracle the
211
+ // proof was measured against). Use it directly.
212
+ return {
213
+ mode: "B" as HealMode,
214
+ proof: null,
215
+ selected: [...conservative.selected],
216
+ codes: verification.codes,
217
+ semanticLossStated: false,
218
+ };
219
+ }
220
+
221
+ /**
222
+ * Convenience for the rare B-failure path: if even the conservative oracle's
223
+ * selection cannot be honored, state the legacy prompt and MARK semantic loss.
224
+ * Mode C is the last resort and the only place semantic loss is ever admitted.
225
+ */
226
+ export function legacyFallback(conservative: ClosureResult, reason: HealFailureCode): HealTriadOutcome {
227
+ return {
228
+ mode: "C" as HealMode,
229
+ proof: null,
230
+ selected: [...conservative.selected],
231
+ codes: [reason, "HEAL_CLOSURE_REJECTED"],
232
+ semanticLossStated: true,
233
+ };
234
+ }
@@ -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
+ }