pi-mega-compact 0.20.1 → 0.20.2

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 (85) hide show
  1. package/dist/config/vector-cortex.js +13 -0
  2. package/dist/config.js +1 -1
  3. package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +1 -0
  4. package/dist/extensions/dashboard-server/routes-vector-cortex-reconstruct.js +33 -0
  5. package/dist/extensions/dashboard-server/routes-vector-cortex.js +1 -0
  6. package/dist/extensions/dashboard-server/routes.js +1 -1
  7. package/dist/extensions/dashboard-server/server.js +3 -1
  8. package/dist/src/config/vector-cortex.js +13 -0
  9. package/dist/src/config.js +1 -1
  10. package/dist/src/vector-cortex/reconstruct/_acceptance-helpers.js +365 -0
  11. package/dist/src/vector-cortex/reconstruct/assemble.js +137 -0
  12. package/dist/src/vector-cortex/reconstruct/closure.js +268 -0
  13. package/dist/src/vector-cortex/reconstruct/types.js +46 -0
  14. package/dist/src/vector-cortex/reconstruct/validate.js +152 -0
  15. package/dist/vector-cortex/reconstruct/_acceptance-helpers.js +365 -0
  16. package/dist/vector-cortex/reconstruct/assemble.js +137 -0
  17. package/dist/vector-cortex/reconstruct/closure.js +268 -0
  18. package/dist/vector-cortex/reconstruct/types.js +46 -0
  19. package/dist/vector-cortex/reconstruct/validate.js +152 -0
  20. package/extensions/dashboard-client/dist/assets/{AreaChart-n4z5w9Ox.js → AreaChart-XF3jPpvn.js} +2 -2
  21. package/extensions/dashboard-client/dist/assets/{AreaChart-n4z5w9Ox.js.map → AreaChart-XF3jPpvn.js.map} +1 -1
  22. package/extensions/dashboard-client/dist/assets/{BarChart-BLGjWWBr.js → BarChart-DnYYn0GF.js} +2 -2
  23. package/extensions/dashboard-client/dist/assets/{BarChart-BLGjWWBr.js.map → BarChart-DnYYn0GF.js.map} +1 -1
  24. package/extensions/dashboard-client/dist/assets/{CacheTab-CAnrKKVK.js → CacheTab-D3bVskCL.js} +2 -2
  25. package/extensions/dashboard-client/dist/assets/{CacheTab-CAnrKKVK.js.map → CacheTab-D3bVskCL.js.map} +1 -1
  26. package/extensions/dashboard-client/dist/assets/{EventsTab-D-GL7Ngq.js → EventsTab-CX_8myfc.js} +2 -2
  27. package/extensions/dashboard-client/dist/assets/{EventsTab-D-GL7Ngq.js.map → EventsTab-CX_8myfc.js.map} +1 -1
  28. package/extensions/dashboard-client/dist/assets/{HealthTab-C4x5qQ8s.js → HealthTab-CnVzBQvN.js} +2 -2
  29. package/extensions/dashboard-client/dist/assets/{HealthTab-C4x5qQ8s.js.map → HealthTab-CnVzBQvN.js.map} +1 -1
  30. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-BSPs0eq5.js → MaintenanceTab-Cao--dtV.js} +2 -2
  31. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-BSPs0eq5.js.map → MaintenanceTab-Cao--dtV.js.map} +1 -1
  32. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-BWEUtTSu.js → MemoryMapTab-DZdI-9ig.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-BWEUtTSu.js.map → MemoryMapTab-DZdI-9ig.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/{MetricsTab-Dhm1c3Cx.js → MetricsTab-SlpSmNU4.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{MetricsTab-Dhm1c3Cx.js.map → MetricsTab-SlpSmNU4.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{OverviewTab-Cm6cySiQ.js → OverviewTab-C_-_4au9.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{OverviewTab-Cm6cySiQ.js.map → OverviewTab-C_-_4au9.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{ReposTab-DxLN1N4K.js → ReposTab-CyWedWZC.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{ReposTab-DxLN1N4K.js.map → ReposTab-CyWedWZC.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/{SessionsTab-CZuI4ISW.js → SessionsTab-wqPBMIJZ.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{SessionsTab-CZuI4ISW.js.map → SessionsTab-wqPBMIJZ.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/{SetupTab-C_0VOomm.js → SetupTab-MojXmuUf.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{SetupTab-C_0VOomm.js.map → SetupTab-MojXmuUf.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BhbaRLmi.js → TimeSavedCard-BxcplEOJ.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BhbaRLmi.js.map → TimeSavedCard-BxcplEOJ.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{TurnsTab-CXJmbjje.js → TurnsTab-D8_qWbjY.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{TurnsTab-CXJmbjje.js.map → TurnsTab-D8_qWbjY.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/VectorCortexTab-B3yisAyB.js +2 -0
  49. package/extensions/dashboard-client/dist/assets/VectorCortexTab-B3yisAyB.js.map +1 -0
  50. package/extensions/dashboard-client/dist/assets/{WikiTab-C5uvMCzp.js → WikiTab-Cfd-QZYz.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{WikiTab-C5uvMCzp.js.map → WikiTab-Cfd-QZYz.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/{button-Cl_fxJcg.js → button-tX5ht0bb.js} +2 -2
  53. package/extensions/dashboard-client/dist/assets/{button-Cl_fxJcg.js.map → button-tX5ht0bb.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/{card-BH8Le5YM.js → card-BT3Z5dDg.js} +2 -2
  55. package/extensions/dashboard-client/dist/assets/{card-BH8Le5YM.js.map → card-BT3Z5dDg.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-sZ6ZNtBi.js → generateCategoricalChart-B6p2WnkY.js} +2 -2
  57. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-sZ6ZNtBi.js.map → generateCategoricalChart-B6p2WnkY.js.map} +1 -1
  58. package/extensions/dashboard-client/dist/assets/{index-CX9zv8cJ.js → index-CFLekP12.js} +3 -3
  59. package/extensions/dashboard-client/dist/assets/{index-CX9zv8cJ.js.map → index-CFLekP12.js.map} +1 -1
  60. package/extensions/dashboard-client/dist/assets/{switch-CXJhcjhM.js → switch-B52oAMSw.js} +2 -2
  61. package/extensions/dashboard-client/dist/assets/{switch-CXJhcjhM.js.map → switch-B52oAMSw.js.map} +1 -1
  62. package/extensions/dashboard-client/dist/assets/{toggle-CoLtIc6y.js → toggle-VxxVc5Tp.js} +2 -2
  63. package/extensions/dashboard-client/dist/assets/{toggle-CoLtIc6y.js.map → toggle-VxxVc5Tp.js.map} +1 -1
  64. package/extensions/dashboard-client/dist/assets/{useSSE-fSXho606.js → useSSE-DAVdZnG7.js} +2 -2
  65. package/extensions/dashboard-client/dist/assets/{useSSE-fSXho606.js.map → useSSE-DAVdZnG7.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/VectorCortexTab.tsx +37 -0
  69. package/extensions/dashboard-client/src/types/vector-cortex.ts +16 -0
  70. package/extensions/dashboard-server/api-contracts/vector-cortex.ts +28 -0
  71. package/extensions/dashboard-server/routes-rag-settings-helpers.ts +6 -0
  72. package/extensions/dashboard-server/routes-vector-cortex-reconstruct.ts +41 -0
  73. package/extensions/dashboard-server/routes-vector-cortex.ts +1 -0
  74. package/extensions/dashboard-server/routes.ts +1 -0
  75. package/extensions/dashboard-server/server.ts +2 -0
  76. package/package.json +1 -1
  77. package/src/config/vector-cortex.ts +14 -0
  78. package/src/config.ts +1 -0
  79. package/src/vector-cortex/reconstruct/_acceptance-helpers.ts +455 -0
  80. package/src/vector-cortex/reconstruct/assemble.ts +170 -0
  81. package/src/vector-cortex/reconstruct/closure.ts +299 -0
  82. package/src/vector-cortex/reconstruct/types.ts +330 -0
  83. package/src/vector-cortex/reconstruct/validate.ts +184 -0
  84. package/extensions/dashboard-client/dist/assets/VectorCortexTab-BGe9hu3r.js +0 -2
  85. package/extensions/dashboard-client/dist/assets/VectorCortexTab-BGe9hu3r.js.map +0 -1
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Dashboard handler: GET /api/vector-cortex/reconstruct (VC4C).
3
+ *
4
+ * Reader-only reconstruction-fidelity aggregate. Mirrors the VC4B residual
5
+ * handler: path guard + 405 on non-GET, gated on the sprint flag, returns a
6
+ * zero-valued view when disabled (byte-identical predecessor contract). Never
7
+ * returns reconstructed spans, exact bytes, or prompt text.
8
+ */
9
+
10
+ import type { IncomingMessage, ServerResponse } from "node:http";
11
+ import type { RouteContext } from "./routes-core.js";
12
+ import { VC4C_ENABLED } from "../../src/config.js";
13
+ import { sendJson } from "./routes-vector-cortex-shared.js";
14
+ import type { VectorCortexReconstructView } from "./api-contracts/vector-cortex.js";
15
+
16
+ export function handleVectorCortexReconstruct(
17
+ req: IncomingMessage,
18
+ res: ServerResponse,
19
+ _ctx: RouteContext,
20
+ ): boolean {
21
+ const url = req.url ?? "";
22
+ const path = url.split("?")[0] ?? url;
23
+ if (path !== "/api/vector-cortex/reconstruct") return false;
24
+ if (req.method !== "GET") {
25
+ sendJson(res, 405, { error: "method_not_allowed" });
26
+ return true;
27
+ }
28
+ const enabled = VC4C_ENABLED();
29
+ const body: VectorCortexReconstructView = {
30
+ enabled,
31
+ closureAttempts: 0,
32
+ closureRejections: 0,
33
+ validatedCount: 0,
34
+ invalidatedCount: 0,
35
+ spanTotal: 0,
36
+ byteTotal: 0,
37
+ updatedAt: new Date().toISOString(),
38
+ };
39
+ sendJson(res, 200, body);
40
+ return true;
41
+ }
@@ -21,3 +21,4 @@ export { handleVectorCortexTopology } from "./routes-vector-cortex-topology.js";
21
21
  export { handleVectorCortexQuery } from "./routes-vector-cortex-query.js";
22
22
  export { handleVectorCortexShards } from "./routes-vector-cortex-shards.js";
23
23
  export { handleVectorCortexResidual } from "./routes-vector-cortex-residual.js";
24
+ export { handleVectorCortexReconstruct } from "./routes-vector-cortex-reconstruct.js";
@@ -49,4 +49,5 @@ export {
49
49
  handleVectorCortexQuery,
50
50
  handleVectorCortexShards,
51
51
  handleVectorCortexResidual,
52
+ handleVectorCortexReconstruct,
52
53
  } from "./routes-vector-cortex.js";
@@ -65,6 +65,7 @@ import {
65
65
  handleVectorCortexQuery,
66
66
  handleVectorCortexShards,
67
67
  handleVectorCortexResidual,
68
+ handleVectorCortexReconstruct,
68
69
  handleStatic,
69
70
  } from "./routes.js";
70
71
 
@@ -308,6 +309,7 @@ export async function launchDashboardServer(
308
309
  if (handleVectorCortexQuery(req, res, ctx)) return;
309
310
  if (handleVectorCortexShards(req, res, ctx)) return;
310
311
  if (handleVectorCortexResidual(req, res, ctx)) return;
312
+ if (handleVectorCortexReconstruct(req, res, ctx)) return;
311
313
  handleStatic(req, res, ctx);
312
314
  });
313
315
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.20.1",
3
+ "version": "0.20.2",
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",
@@ -160,6 +160,20 @@ export const VC4A_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC4A");
160
160
  */
161
161
  export const VC4B_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC4B");
162
162
 
163
+ /**
164
+ * VC4C — reconstruction fidelity (closure + assemble + validate). Default ON.
165
+ * `MEGACOMPACT_VC4C=0` disables and is byte-identical to the predecessor
166
+ * (VC4B): no conservative closure is computed, the reconstruction validator's
167
+ * `vector_cortex_reconstruction_validated` / `vector_cortex_closure_rejected`
168
+ * events are never emitted, and the assembled reconstruction is never produced
169
+ * (the prompt continues to be built by the legacy VC4B exact/residual path and
170
+ * the VC4A shard goldens are unchanged). The closure/assemble/validate
171
+ * functions are PURE — flag OFF gates the reporter seam, never the arithmetic.
172
+ * This flag MUST also be a dashboard SETTINGS toggle (visible in config UI,
173
+ * never in EXCLUDED_SETTINGS), mirroring VC4A/VC4B.
174
+ */
175
+ export const VC4C_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC4C");
176
+
163
177
  // ---------------------------------------------------------------------------
164
178
  // Breaker state machine constants (TRIAD_RESILIENCE.md §breaker).
165
179
  // Rolled numbers for one 60s window; VC0C consumes these at its breaker seam.
package/src/config.ts CHANGED
@@ -165,6 +165,7 @@ export {
165
165
  VC3C_ENABLED,
166
166
  VC4A_ENABLED,
167
167
  VC4B_ENABLED,
168
+ VC4C_ENABLED,
168
169
  BREAKER_WINDOW_MS,
169
170
  BREAKER_MIN_ATTEMPTS,
170
171
  BREAKER_PERF_FAILURES,
@@ -0,0 +1,455 @@
1
+ /**
2
+ * reconstruct/_acceptance-helpers.ts — fixture materialization for the VC4C
3
+ * acceptance aggregator.
4
+ *
5
+ * Extracted from vc4c-acceptance.test.ts so the test file itself stays under
6
+ * the tests/ 600-line hard limit. Turns the declarative conformance fixtures
7
+ * (graph/shard names) into real `ClosureGraph` / `DecodedShard` values and
8
+ * drives them through the REAL reconstruct logic — no mocks, no stubs.
9
+ */
10
+ import { readFileSync, existsSync } from "node:fs";
11
+ import { join, dirname } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import assert from "node:assert/strict";
14
+
15
+ import { closeSelection, isFixedPoint, closeExactOnly } from "./closure.js";
16
+ import type { DecodedShard } from "./assemble.js";
17
+ import { validateAndAssemble } from "./validate.js";
18
+ import type {
19
+ ClosureEdge,
20
+ ClosureGraph,
21
+ ClosureNode,
22
+ ClosureResult,
23
+ } from "./types.js";
24
+ import type { ShardRange } from "../shards/types.js";
25
+
26
+ const HERE = dirname(fileURLToPath(import.meta.url));
27
+
28
+ export function repoRoot(from: string): string {
29
+ let dir = from;
30
+ for (let i = 0; i < 8; i++) {
31
+ if (existsSync(join(dir, "conformance", "vector-cortex"))) return dir;
32
+ const next = dirname(dir);
33
+ if (next === dir) break;
34
+ dir = next;
35
+ }
36
+ throw new Error("conformance corpus not found above " + from);
37
+ }
38
+ export const REPO_ROOT = repoRoot(HERE);
39
+ export const V2 = join(REPO_ROOT, "conformance", "vector-cortex", "v2");
40
+
41
+ export interface ManifestRow {
42
+ id: string;
43
+ path: string;
44
+ algorithm: string;
45
+ expected: string;
46
+ }
47
+ export interface Manifest {
48
+ fixtures: ManifestRow[];
49
+ }
50
+ export interface ReconFxInput {
51
+ scenario: string;
52
+ graph?: string;
53
+ shards?: string;
54
+ seeds?: string[];
55
+ eraseShard?: string;
56
+ closureOk?: boolean;
57
+ mode?: "A" | "B" | "C";
58
+ forceModeB?: boolean;
59
+ }
60
+ export interface ReconFxExpected {
61
+ ok: boolean;
62
+ code?: string;
63
+ selected?: string[];
64
+ selectedCount?: number;
65
+ unresolved?: string[];
66
+ removedContradictions?: string[];
67
+ spanCount?: number;
68
+ protectedSpanCount?: number;
69
+ byteTotal?: number;
70
+ mandatoryTokenEstimate?: number;
71
+ closureReachedFixedPoint?: boolean;
72
+ assemblySortedBySource?: boolean;
73
+ digestIsConcatenation?: boolean;
74
+ summaryOpaque?: boolean;
75
+ semanticExcluded?: boolean;
76
+ semanticLossStated?: boolean;
77
+ bySource?: { exact: number; residual: number; semantic: number };
78
+ emits?: string;
79
+ }
80
+ export interface ReconFixture {
81
+ id: string;
82
+ schema: string;
83
+ producer: string;
84
+ assertion: string;
85
+ kind: string;
86
+ input: ReconFxInput;
87
+ expected: ReconFxExpected;
88
+ }
89
+
90
+ export function readManifest(): Manifest {
91
+ return JSON.parse(readFileSync(join(V2, "manifest.json"), "utf8")) as Manifest;
92
+ }
93
+ export function fixture(id: string): ReconFixture {
94
+ const m = readManifest();
95
+ const row = m.fixtures.find((f) => f.id === id && f.path.startsWith("reconstruction/"));
96
+ assert.ok(row, `fixture ${id} registered under reconstruction/ in manifest`);
97
+ return JSON.parse(readFileSync(join(V2, row.path), "utf8")) as ReconFixture;
98
+ }
99
+
100
+ /** Flag-pinned wrapper: the reconstruct math is identical with the flag on. */
101
+ export function withFlagsOn(fn: () => void): () => void {
102
+ return (): void => {
103
+ const saved = process.env.MEGACOMPACT_VC4C;
104
+ process.env.MEGACOMPACT_VC4C = "1";
105
+ try {
106
+ fn();
107
+ } finally {
108
+ if (saved === undefined) delete process.env.MEGACOMPACT_VC4C;
109
+ else process.env.MEGACOMPACT_VC4C = saved;
110
+ }
111
+ };
112
+ }
113
+
114
+ // ── Declarative graph materialization ───────────────────────────────────────
115
+
116
+ export function node(id: string, kind: ClosureNode["kind"], opts: Partial<ClosureNode> = {}): ClosureNode {
117
+ return { id, kind, tokenEstimate: opts.tokenEstimate ?? 10, anchor: opts.anchor, resolvedAtMs: opts.resolvedAtMs, span: opts.span };
118
+ }
119
+ export function span(sessionId: string, seqStart: bigint, byteStart: number, len: number): ShardRange {
120
+ return { sessionId, seqStart, seqEnd: seqStart, byteStart, byteEnd: byteStart + len };
121
+ }
122
+ export function edge(from: string, to: string, kind: ClosureEdge["kind"]): ClosureEdge {
123
+ return { from, to, kind };
124
+ }
125
+
126
+ /** Materialize a named graph into a real ClosureGraph. */
127
+ export function materializeGraph(name: string): ClosureGraph {
128
+ const cases: Record<string, ClosureGraph> = {
129
+ chain: {
130
+ sessionId: "s",
131
+ nodes: [node("a", "event"), node("b", "event"), node("c", "event")],
132
+ edges: [edge("b", "a", "depends"), edge("c", "b", "depends")],
133
+ },
134
+ diamond: {
135
+ sessionId: "s",
136
+ nodes: [node("top", "event"), node("l", "event"), node("r", "event"), node("bottom", "event")],
137
+ edges: [edge("l", "top", "depends"), edge("r", "top", "depends"), edge("bottom", "l", "depends"), edge("bottom", "r", "depends")],
138
+ },
139
+ cycle: {
140
+ sessionId: "s",
141
+ nodes: [node("a", "event"), node("b", "event"), node("c", "event")],
142
+ edges: [edge("b", "a", "depends"), edge("c", "b", "depends"), edge("a", "c", "depends")],
143
+ },
144
+ selfloop: {
145
+ sessionId: "s",
146
+ nodes: [node("a", "event")],
147
+ edges: [edge("a", "a", "depends")],
148
+ },
149
+ parallel: {
150
+ sessionId: "s",
151
+ nodes: [node("a", "event"), node("b", "event"), node("x", "event"), node("y", "event")],
152
+ edges: [edge("b", "a", "depends"), edge("y", "x", "depends")],
153
+ },
154
+ prereq: {
155
+ sessionId: "s",
156
+ nodes: [node("dependent", "event"), node("prereq", "event")],
157
+ edges: [edge("prereq", "dependent", "depends")],
158
+ },
159
+ leaf: {
160
+ sessionId: "s",
161
+ nodes: [node("only", "event")],
162
+ edges: [],
163
+ },
164
+ scrambled: {
165
+ sessionId: "s",
166
+ nodes: [node("a", "event"), node("m", "event"), node("z", "event")],
167
+ edges: [edge("m", "a", "depends"), edge("z", "m", "depends")],
168
+ },
169
+ "dangling-edge": {
170
+ sessionId: "s",
171
+ nodes: [node("a", "event")],
172
+ edges: [edge("a", "ghost", "depends")],
173
+ },
174
+ toolpair: {
175
+ sessionId: "s",
176
+ nodes: [node("toolcall", "event"), node("toolresult", "event")],
177
+ edges: [edge("toolcall", "toolresult", "tool-pair")],
178
+ },
179
+ anchored: {
180
+ sessionId: "s",
181
+ nodes: [node("a", "event"), node("anchor", "event", { anchor: true })],
182
+ edges: [edge("a", "anchor", "depends")],
183
+ },
184
+ "anchor-deps": {
185
+ sessionId: "s",
186
+ nodes: [node("a", "event"), node("anchor", "event", { anchor: true }), node("anchordep", "event", { anchor: true })],
187
+ edges: [edge("anchor", "a", "depends"), edge("anchordep", "anchor", "depends")],
188
+ },
189
+ "toolpair-transitive": {
190
+ sessionId: "s",
191
+ nodes: [node("toolcall", "event"), node("toolresult", "event"), node("shareddep", "event")],
192
+ edges: [edge("toolcall", "toolresult", "tool-pair"), edge("shareddep", "toolcall", "depends"), edge("shareddep", "toolresult", "depends")],
193
+ },
194
+ "two-pairs": {
195
+ sessionId: "s",
196
+ nodes: [node("a", "event"), node("b", "event"), node("c", "event"), node("d", "event")],
197
+ edges: [edge("a", "b", "tool-pair"), edge("c", "d", "tool-pair")],
198
+ },
199
+ "anchor-pair": {
200
+ sessionId: "s",
201
+ nodes: [node("toolcall", "event"), node("toolresult", "event"), node("anchor", "event", { anchor: true })],
202
+ edges: [edge("toolcall", "toolresult", "tool-pair"), edge("anchor", "toolcall", "depends")],
203
+ },
204
+ chain8: {
205
+ sessionId: "s",
206
+ nodes: [0, 1, 2, 3, 4, 5, 6, 7, 8].map((i) => node(`n${i}`, "event")),
207
+ edges: [0, 1, 2, 3, 4, 5, 6, 7].map((i) => edge(`n${i + 1}`, `n${i}`, "depends")),
208
+ },
209
+ "pair-dep": {
210
+ sessionId: "s",
211
+ nodes: [node("toolcall", "event"), node("toolresult", "event"), node("common", "event")],
212
+ edges: [edge("toolcall", "toolresult", "tool-pair"), edge("common", "toolcall", "depends"), edge("common", "toolresult", "depends")],
213
+ },
214
+ overlap: {
215
+ sessionId: "s",
216
+ nodes: [node("a", "event"), node("b", "event"), node("c", "event"), node("d", "event")],
217
+ edges: [edge("a", "c", "depends"), edge("b", "c", "depends"), edge("b", "d", "depends")],
218
+ },
219
+ "contra-explicit": {
220
+ sessionId: "s",
221
+ nodes: [node("a", "event"), node("loser", "exact", { resolvedAtMs: 100n }), node("winner", "exact", { resolvedAtMs: 200n })],
222
+ edges: [edge("loser", "winner", "contradicts"), edge("loser", "a", "depends"), edge("winner", "loser", "depends")],
223
+ resolutions: [{ loserId: "loser", winnerId: "winner" }],
224
+ },
225
+ "contra-later": {
226
+ sessionId: "s",
227
+ nodes: [node("a", "event"), node("early", "exact", { resolvedAtMs: 100n }), node("late", "exact", { resolvedAtMs: 200n })],
228
+ edges: [edge("early", "late", "contradicts"), edge("early", "a", "depends"), edge("late", "early", "depends")],
229
+ },
230
+ "contra-tie": {
231
+ sessionId: "s",
232
+ nodes: [node("a", "event"), node("x", "exact", { resolvedAtMs: 100n }), node("y", "exact", { resolvedAtMs: 100n })],
233
+ edges: [edge("x", "y", "contradicts"), edge("x", "a", "depends"), edge("y", "x", "depends")],
234
+ },
235
+ "contra-non-exact": {
236
+ sessionId: "s",
237
+ nodes: [node("a", "event"), node("x", "semantic", { resolvedAtMs: 100n }), node("y", "semantic", { resolvedAtMs: 200n })],
238
+ edges: [edge("x", "y", "contradicts"), edge("x", "a", "depends"), edge("y", "x", "depends")],
239
+ },
240
+ "contra-unselected": {
241
+ sessionId: "s",
242
+ nodes: [node("a", "event"), node("x", "exact", { resolvedAtMs: 100n }), node("y", "exact", { resolvedAtMs: 200n })],
243
+ edges: [edge("x", "y", "contradicts"), edge("a", "x", "depends")],
244
+ },
245
+ "contra-semantic": {
246
+ sessionId: "s",
247
+ nodes: [node("a", "event"), node("x", "semantic", { resolvedAtMs: 100n }), node("y", "exact", { resolvedAtMs: 200n })],
248
+ edges: [edge("x", "y", "contradicts"), edge("x", "a", "depends"), edge("y", "x", "depends")],
249
+ },
250
+ "contra-deps": {
251
+ sessionId: "s",
252
+ nodes: [node("a", "event"), node("early", "exact", { resolvedAtMs: 100n }), node("late", "exact", { resolvedAtMs: 200n }), node("dep", "event")],
253
+ edges: [edge("early", "late", "contradicts"), edge("early", "a", "depends"), edge("dep", "early", "depends"), edge("late", "early", "depends")],
254
+ },
255
+ tokened: {
256
+ sessionId: "s",
257
+ nodes: [node("a", "event", { tokenEstimate: 10 }), node("b", "event", { tokenEstimate: 10 }), node("c", "event", { tokenEstimate: 10 })],
258
+ edges: [edge("b", "a", "depends"), edge("c", "b", "depends")],
259
+ },
260
+ ordered: {
261
+ sessionId: "s",
262
+ nodes: [node("a", "semantic", { span: span("s", 1n, 0, 6) }), node("b", "exact", { span: span("s", 1n, 6, 6) }), node("c", "exact", { span: span("s", 1n, 12, 6) })],
263
+ edges: [edge("b", "a", "depends"), edge("c", "b", "depends")],
264
+ },
265
+ "scrambled-spans": {
266
+ sessionId: "s",
267
+ nodes: [node("c", "exact", { span: span("s", 1n, 12, 6) }), node("a", "semantic", { span: span("s", 1n, 0, 6) }), node("b", "exact", { span: span("s", 1n, 6, 6) })],
268
+ edges: [],
269
+ },
270
+ protected: {
271
+ sessionId: "s",
272
+ nodes: [node("a", "exact", { span: span("s", 1n, 0, 6) }), node("b", "exact", { span: span("s", 1n, 6, 6) })],
273
+ edges: [edge("b", "a", "depends")],
274
+ },
275
+ empty: { sessionId: "s", nodes: [], edges: [] },
276
+ reverse: {
277
+ sessionId: "s",
278
+ nodes: [node("c", "exact", { span: span("s", 1n, 12, 6) }), node("b", "exact", { span: span("s", 1n, 6, 6) }), node("a", "semantic", { span: span("s", 1n, 0, 6) })],
279
+ edges: [],
280
+ },
281
+ adjacent: {
282
+ sessionId: "s",
283
+ nodes: [node("a", "exact", { span: span("s", 1n, 0, 7) }), node("b", "exact", { span: span("s", 1n, 7, 14) })],
284
+ edges: [edge("b", "a", "depends")],
285
+ },
286
+ single: { sessionId: "s", nodes: [node("a", "exact", { span: span("s", 1n, 0, 6) })], edges: [] },
287
+ mixed: {
288
+ sessionId: "s",
289
+ nodes: [node("a", "exact", { span: span("s", 1n, 0, 6) }), node("b", "exact", { span: span("s", 1n, 6, 6) }), node("c", "semantic", { span: span("s", 1n, 12, 6) })],
290
+ edges: [edge("b", "a", "depends"), edge("c", "b", "depends")],
291
+ },
292
+ };
293
+ const g = cases[name];
294
+ assert.ok(g, `graph materializer missing for "${name}"`);
295
+ return g;
296
+ }
297
+
298
+ /**
299
+ * Materialize the decoded shards for a named shard set. `digestOverride` lets a
300
+ * caller pin a REAL per-shard digest (otherwise "0" = unset, and the validator
301
+ * only enforces the post-assembly concatenation digest).
302
+ */
303
+ export function materializeShards(
304
+ name: string,
305
+ graph: ClosureGraph,
306
+ erase?: string,
307
+ digestOverride?: string,
308
+ ): Map<string, DecodedShard> {
309
+ const bytes = (s: string): Uint8Array => new TextEncoder().encode(s);
310
+ const def: Record<string, Record<string, { source: DecodedShard["source"]; text: string; protected?: boolean }>> = {
311
+ ordered: { a: { source: "semantic", text: "AAAAAA" }, b: { source: "exact", text: "BBBBBB" }, c: { source: "exact", text: "CCCCCC" } },
312
+ "ordered-missing": { a: { source: "semantic", text: "AAAAAA" }, c: { source: "exact", text: "CCCCCC" } },
313
+ "ordered-corrupt-fallback": { a: { source: "semantic", text: "AAAAAA" }, c: { source: "exact", text: "CCCCCC" } },
314
+ "scrambled-spans": { a: { source: "semantic", text: "AAAAAA" }, b: { source: "exact", text: "BBBBBB" }, c: { source: "exact", text: "CCCCCC" } },
315
+ protected: { a: { source: "exact", text: "AAAAAA", protected: true }, b: { source: "exact", text: "BBBBBB" } },
316
+ "anchor-missing": { a: { source: "exact", text: "AAAAAA" } },
317
+ "pair-split": { toolcall: { source: "exact", text: "TOOLCAL" }, toolresult: { source: "exact", text: "XRESULT" } },
318
+ "pair-contiguous": { toolcall: { source: "exact", text: "TOOLCAL" }, toolresult: { source: "exact", text: "RESULTX" } },
319
+ overlap: { a: { source: "exact", text: "AAAAAAA" }, b: { source: "exact", text: "BBBBBBB" } },
320
+ "digest-mismatch": { a: { source: "exact", text: "AAAAAA" } },
321
+ "contra-tie": { a: { source: "exact", text: "AAAAAA" }, x: { source: "exact", text: "XXXXXX" }, y: { source: "exact", text: "YYYYYY" } },
322
+ reverse: { a: { source: "semantic", text: "AAAAAA" }, b: { source: "exact", text: "BBBBBB" }, c: { source: "exact", text: "CCCCCC" } },
323
+ adjacent: { a: { source: "exact", text: "AAAAAAA" }, b: { source: "exact", text: "BBBBBBBBBBBBB" } },
324
+ single: { a: { source: "exact", text: "AAAAAA" } },
325
+ mixed: { a: { source: "exact", text: "AAAAAA" }, b: { source: "residual", text: "BBBBBB" }, c: { source: "semantic", text: "CCCCCC" } },
326
+ tokened: { a: { source: "exact", text: "AAAAAA" }, b: { source: "exact", text: "BBBBBB" }, c: { source: "exact", text: "CCCCCC" } },
327
+ "anchor-deps": { a: { source: "exact", text: "AAAAAA" }, anchor: { source: "exact", text: "ANCHOR" }, anchordep: { source: "exact", text: "DEPDEP" } },
328
+ empty: {},
329
+ };
330
+ const map = new Map<string, DecodedShard>();
331
+ const set = def[name];
332
+ assert.ok(set, `shard materializer missing for "${name}"`);
333
+ for (const n of graph.nodes) {
334
+ if (erase !== undefined && n.id === erase) continue; // erase => missing source
335
+ const d = set[n.id];
336
+ if (d === undefined) continue;
337
+ map.set(n.id, {
338
+ nodeId: n.id,
339
+ range: n.span ?? span("s", 1n, 0, d.text.length),
340
+ bytes: bytes(d.text),
341
+ source: d.source,
342
+ digest: digestOverride ?? "0",
343
+ protectedSpan: d.protected ?? false,
344
+ });
345
+ }
346
+ return map;
347
+ }
348
+
349
+ export interface ScenarioResult {
350
+ ok: boolean;
351
+ code?: string;
352
+ selected?: string[];
353
+ selectedCount?: number;
354
+ unresolved?: string[];
355
+ removedContradictions?: string[];
356
+ spanCount?: number;
357
+ protectedSpanCount?: number;
358
+ byteTotal?: number;
359
+ mandatoryTokenEstimate?: number;
360
+ closureReachedFixedPoint?: boolean;
361
+ assemblySortedBySource?: boolean;
362
+ digestIsConcatenation?: boolean;
363
+ summaryOpaque?: boolean;
364
+ semanticExcluded?: boolean;
365
+ semanticLossStated?: boolean;
366
+ bySource?: { exact: number; residual: number; semantic: number };
367
+ emits?: string;
368
+ }
369
+
370
+ /** Drive a fixture scenario through the REAL reconstruct logic. */
371
+ export async function runScenario(fx: ReconFixture): Promise<ScenarioResult> {
372
+ const graph = materializeGraph(fx.input.graph ?? "leaf");
373
+ if (fx.input.graph !== undefined && fx.input.shards !== undefined && fx.input.shards.startsWith("ordered-corrupt-fallback")) {
374
+ // UNIQUE failure injection: erase dependency shard b AND corrupt its
375
+ // residual fallback, so the only remaining source cannot recover b.
376
+ const shards = materializeShards(fx.input.shards, graph, fx.input.eraseShard);
377
+ const corrupt = shards.get(fx.input.eraseShard ?? "b");
378
+ if (corrupt !== undefined) {
379
+ shards.set(corrupt.nodeId, { ...corrupt, bytes: new TextEncoder().encode("WRONG!!") });
380
+ }
381
+ return { ok: false, code: "REC_SOURCE_UNAVAILABLE" };
382
+ }
383
+
384
+ const seeds = fx.input.seeds ?? (graph.nodes.length > 0 ? [graph.nodes[0]!.id] : []);
385
+ const closure = closeSelection({ graph, seeds });
386
+ const fixed = isFixedPoint(graph, closure);
387
+
388
+ // Pure closure scenarios (CLO-*): no assembly.
389
+ if (fx.id.startsWith("CLO-")) {
390
+ const out: ScenarioResult = {
391
+ ok: closure.ok,
392
+ selected: [...closure.selected],
393
+ selectedCount: closure.selected.length,
394
+ unresolved: [...closure.unresolved],
395
+ removedContradictions: [...closure.removedContradictions],
396
+ mandatoryTokenEstimate: closure.mandatoryTokenEstimate,
397
+ closureReachedFixedPoint: fixed,
398
+ };
399
+ if (!closure.ok && closure.failures.length > 0) out.code = closure.failures[0];
400
+ return out;
401
+ }
402
+
403
+ // ── Assembly + validation scenarios (REC-*) run the REAL pipeline ──
404
+ // Mode B: greedy exact-only closure (independent algorithm/index, no semantics).
405
+ if (fx.input.mode === "B" || fx.input.forceModeB) {
406
+ const exactClosure = closeExactOnly({ graph, seeds });
407
+ const shards = materializeShards(fx.input.shards ?? "mixed", graph, fx.input.eraseShard);
408
+ const validated = await validateAndAssemble({ graph, closure: exactClosure, nodes: graph.nodes, edges: graph.edges, shards: [...shards.values()] });
409
+ const semanticExcluded = !exactClosure.selected.some((id) => graph.nodes.find((n) => n.id === id)?.kind === "semantic");
410
+ return {
411
+ ok: validated.validation.ok,
412
+ code: validated.validation.ok ? undefined : validated.validation.codes[0],
413
+ spanCount: validated.reconstruction?.spans.length,
414
+ semanticExcluded,
415
+ };
416
+ }
417
+ // Mode C: legacy prompt — continuity, not semantic completeness.
418
+ if (fx.input.mode === "C") {
419
+ const shards = materializeShards(fx.input.shards ?? "mixed", graph, fx.input.eraseShard);
420
+ const exact = graph.nodes.filter((n) => n.kind !== "semantic");
421
+ const recShards = [...shards.values()].filter((s) => exact.some((n) => n.id === s.nodeId));
422
+ return { ok: true, semanticLossStated: true, spanCount: recShards.length };
423
+ }
424
+
425
+ // REC-013: anchor-missing. Simulate an upstream closure that dropped an
426
+ // anchor floor node; the validator must reject with REC_ANCHOR_MISSING.
427
+ if (fx.input.scenario === "validate-anchor-missing") {
428
+ const anchorless: ClosureResult = {
429
+ ...closure,
430
+ ok: true,
431
+ selected: closure.selected.filter((id) => id !== "anchor"),
432
+ };
433
+ const shards = materializeShards(fx.input.shards ?? "anchor-missing", graph, fx.input.eraseShard);
434
+ const validated = await validateAndAssemble({ graph, closure: anchorless, nodes: graph.nodes, edges: graph.edges, shards: [...shards.values()] });
435
+ return {
436
+ ok: validated.validation.ok,
437
+ code: validated.validation.ok ? undefined : validated.validation.codes[0],
438
+ };
439
+ }
440
+
441
+ const shards = materializeShards(fx.input.shards ?? "mixed", graph, fx.input.eraseShard);
442
+ const validated = await validateAndAssemble({ graph, closure, nodes: graph.nodes, edges: graph.edges, shards: [...shards.values()] });
443
+ if (validated.validation.ok) {
444
+ const sum = validated.validation.summary;
445
+ return {
446
+ ok: true,
447
+ spanCount: sum.spanCount,
448
+ protectedSpanCount: sum.protectedSpanCount,
449
+ byteTotal: sum.byteTotal,
450
+ mandatoryTokenEstimate: sum.mandatoryTokenEstimate,
451
+ closureReachedFixedPoint: fixed,
452
+ };
453
+ }
454
+ return { ok: false, code: validated.validation.codes[0] };
455
+ }