pi-mega-compact 0.20.13 → 0.20.15

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 (103) hide show
  1. package/dist/config/vector-cortex-early.js +89 -0
  2. package/dist/config/vector-cortex-flag.js +25 -0
  3. package/dist/config/vector-cortex.js +43 -83
  4. package/dist/config.js +1 -1
  5. package/dist/extensions/dashboard-server/api-contracts/vector-cortex-economics.js +20 -0
  6. package/dist/extensions/dashboard-server/route-dispatch.js +7 -0
  7. package/dist/extensions/dashboard-server/routes-rag-settings-vector-cortex.js +2 -0
  8. package/dist/extensions/dashboard-server/routes-vector-cortex-economics.js +59 -0
  9. package/dist/src/config/vector-cortex-early.js +89 -0
  10. package/dist/src/config/vector-cortex-flag.js +25 -0
  11. package/dist/src/config/vector-cortex.js +43 -83
  12. package/dist/src/config.js +1 -1
  13. package/dist/src/vector-cortex/cache/_economics-fixture.js +41 -0
  14. package/dist/src/vector-cortex/cache/compiler.js +201 -0
  15. package/dist/src/vector-cortex/cache/economics-emit.js +85 -0
  16. package/dist/src/vector-cortex/migrations/request-hash-v2-ops.js +143 -0
  17. package/dist/src/vector-cortex/migrations/request-hash-v2-types.js +30 -0
  18. package/dist/src/vector-cortex/migrations/request-hash-v2.js +56 -0
  19. package/dist/src/vector-cortex/provider/economics-ids.js +29 -0
  20. package/dist/src/vector-cortex/provider/economics.js +176 -0
  21. package/dist/src/vector-cortex/provider/experiments.js +166 -0
  22. package/dist/vector-cortex/cache/_economics-fixture.js +41 -0
  23. package/dist/vector-cortex/cache/compiler.js +201 -0
  24. package/dist/vector-cortex/cache/economics-emit.js +85 -0
  25. package/dist/vector-cortex/migrations/request-hash-v2-ops.js +143 -0
  26. package/dist/vector-cortex/migrations/request-hash-v2-types.js +30 -0
  27. package/dist/vector-cortex/migrations/request-hash-v2.js +56 -0
  28. package/dist/vector-cortex/provider/economics-ids.js +29 -0
  29. package/dist/vector-cortex/provider/economics.js +176 -0
  30. package/dist/vector-cortex/provider/experiments.js +166 -0
  31. package/extensions/dashboard-client/dist/assets/{AreaChart-CB-U7ViX.js → AreaChart-CV8WqMmt.js} +2 -2
  32. package/extensions/dashboard-client/dist/assets/{AreaChart-CB-U7ViX.js.map → AreaChart-CV8WqMmt.js.map} +1 -1
  33. package/extensions/dashboard-client/dist/assets/{BarChart-DnLw0fxA.js → BarChart-M-HEBq9l.js} +2 -2
  34. package/extensions/dashboard-client/dist/assets/{BarChart-DnLw0fxA.js.map → BarChart-M-HEBq9l.js.map} +1 -1
  35. package/extensions/dashboard-client/dist/assets/{CacheTab-CjyTVDiP.js → CacheTab-DJ_yPAhP.js} +2 -2
  36. package/extensions/dashboard-client/dist/assets/{CacheTab-CjyTVDiP.js.map → CacheTab-DJ_yPAhP.js.map} +1 -1
  37. package/extensions/dashboard-client/dist/assets/{EventsTab-Bz3QEWam.js → EventsTab-DNVlKzt6.js} +2 -2
  38. package/extensions/dashboard-client/dist/assets/{EventsTab-Bz3QEWam.js.map → EventsTab-DNVlKzt6.js.map} +1 -1
  39. package/extensions/dashboard-client/dist/assets/{HealthTab-CUylEvEY.js → HealthTab-B5yjuInb.js} +2 -2
  40. package/extensions/dashboard-client/dist/assets/{HealthTab-CUylEvEY.js.map → HealthTab-B5yjuInb.js.map} +1 -1
  41. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-Cz_hj_E7.js → MaintenanceTab-DoQedESF.js} +2 -2
  42. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-Cz_hj_E7.js.map → MaintenanceTab-DoQedESF.js.map} +1 -1
  43. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-Dh8brIR1.js → MemoryMapTab-CuL5CzdH.js} +2 -2
  44. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-Dh8brIR1.js.map → MemoryMapTab-CuL5CzdH.js.map} +1 -1
  45. package/extensions/dashboard-client/dist/assets/{MetricsTab-CkqSXc6e.js → MetricsTab-CZSTaCpK.js} +2 -2
  46. package/extensions/dashboard-client/dist/assets/{MetricsTab-CkqSXc6e.js.map → MetricsTab-CZSTaCpK.js.map} +1 -1
  47. package/extensions/dashboard-client/dist/assets/{OverviewTab-DGbJcx6d.js → OverviewTab-vkZlXRW6.js} +2 -2
  48. package/extensions/dashboard-client/dist/assets/{OverviewTab-DGbJcx6d.js.map → OverviewTab-vkZlXRW6.js.map} +1 -1
  49. package/extensions/dashboard-client/dist/assets/{ReposTab-BawS3nfF.js → ReposTab-DgPD96rF.js} +2 -2
  50. package/extensions/dashboard-client/dist/assets/{ReposTab-BawS3nfF.js.map → ReposTab-DgPD96rF.js.map} +1 -1
  51. package/extensions/dashboard-client/dist/assets/{SessionsTab-CUVnmYks.js → SessionsTab-XPbWh7XN.js} +2 -2
  52. package/extensions/dashboard-client/dist/assets/{SessionsTab-CUVnmYks.js.map → SessionsTab-XPbWh7XN.js.map} +1 -1
  53. package/extensions/dashboard-client/dist/assets/{SetupTab-cxTuyZNk.js → SetupTab-Dz74XAtR.js} +2 -2
  54. package/extensions/dashboard-client/dist/assets/{SetupTab-cxTuyZNk.js.map → SetupTab-Dz74XAtR.js.map} +1 -1
  55. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Cv5SENkE.js → TimeSavedCard-FynCpJdx.js} +2 -2
  56. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Cv5SENkE.js.map → TimeSavedCard-FynCpJdx.js.map} +1 -1
  57. package/extensions/dashboard-client/dist/assets/{TurnsTab-yl6BBG_F.js → TurnsTab-C7m3GDy0.js} +2 -2
  58. package/extensions/dashboard-client/dist/assets/{TurnsTab-yl6BBG_F.js.map → TurnsTab-C7m3GDy0.js.map} +1 -1
  59. package/extensions/dashboard-client/dist/assets/VectorCortexTab-jRd_Iztk.js +2 -0
  60. package/extensions/dashboard-client/dist/assets/VectorCortexTab-jRd_Iztk.js.map +1 -0
  61. package/extensions/dashboard-client/dist/assets/{WikiTab-DKv-0xCQ.js → WikiTab-C6viLa0u.js} +2 -2
  62. package/extensions/dashboard-client/dist/assets/{WikiTab-DKv-0xCQ.js.map → WikiTab-C6viLa0u.js.map} +1 -1
  63. package/extensions/dashboard-client/dist/assets/{button-CVPrO4UU.js → button-mBEgbBVU.js} +2 -2
  64. package/extensions/dashboard-client/dist/assets/{button-CVPrO4UU.js.map → button-mBEgbBVU.js.map} +1 -1
  65. package/extensions/dashboard-client/dist/assets/{card-BFLnQJEo.js → card-CalUQAKG.js} +2 -2
  66. package/extensions/dashboard-client/dist/assets/{card-BFLnQJEo.js.map → card-CalUQAKG.js.map} +1 -1
  67. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-B-IUoLd1.js → generateCategoricalChart-CMmkuwj5.js} +2 -2
  68. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-B-IUoLd1.js.map → generateCategoricalChart-CMmkuwj5.js.map} +1 -1
  69. package/extensions/dashboard-client/dist/assets/{index-Do749WlW.js → index-Dz8GPBOE.js} +3 -3
  70. package/extensions/dashboard-client/dist/assets/{index-Do749WlW.js.map → index-Dz8GPBOE.js.map} +1 -1
  71. package/extensions/dashboard-client/dist/assets/{switch-3uFnZmtq.js → switch-8QKO6CTU.js} +2 -2
  72. package/extensions/dashboard-client/dist/assets/{switch-3uFnZmtq.js.map → switch-8QKO6CTU.js.map} +1 -1
  73. package/extensions/dashboard-client/dist/assets/{toggle-C1rYLeXe.js → toggle-9eCkWI4q.js} +2 -2
  74. package/extensions/dashboard-client/dist/assets/{toggle-C1rYLeXe.js.map → toggle-9eCkWI4q.js.map} +1 -1
  75. package/extensions/dashboard-client/dist/assets/{useSSE-D1qzLzgR.js → useSSE-s6IqOKoD.js} +2 -2
  76. package/extensions/dashboard-client/dist/assets/{useSSE-D1qzLzgR.js.map → useSSE-s6IqOKoD.js.map} +1 -1
  77. package/extensions/dashboard-client/dist/index.html +1 -1
  78. package/extensions/dashboard-client/src/api/vector-cortex.ts +18 -0
  79. package/extensions/dashboard-client/src/tabs/VectorCortexEconomicsCard.tsx +41 -0
  80. package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +24 -114
  81. package/extensions/dashboard-client/src/tabs/useVectorCortexPoll.ts +123 -0
  82. package/extensions/dashboard-client/src/types/vector-cortex-vc7.ts +63 -0
  83. package/extensions/dashboard-client/src/types/vector-cortex.ts +5 -19
  84. package/extensions/dashboard-server/api-contracts/vector-cortex-economics.ts +47 -0
  85. package/extensions/dashboard-server/route-dispatch.ts +6 -0
  86. package/extensions/dashboard-server/routes-rag-settings-vector-cortex.ts +12 -0
  87. package/extensions/dashboard-server/routes-vector-cortex-economics.ts +68 -0
  88. package/package.json +1 -1
  89. package/src/config/vector-cortex-early.ts +99 -0
  90. package/src/config/vector-cortex-flag.ts +24 -0
  91. package/src/config/vector-cortex.ts +55 -89
  92. package/src/config.ts +2 -0
  93. package/src/vector-cortex/cache/_economics-fixture.ts +128 -0
  94. package/src/vector-cortex/cache/compiler.ts +280 -0
  95. package/src/vector-cortex/cache/economics-emit.ts +116 -0
  96. package/src/vector-cortex/migrations/request-hash-v2-ops.ts +195 -0
  97. package/src/vector-cortex/migrations/request-hash-v2-types.ts +87 -0
  98. package/src/vector-cortex/migrations/request-hash-v2.ts +80 -0
  99. package/src/vector-cortex/provider/economics-ids.ts +38 -0
  100. package/src/vector-cortex/provider/economics.ts +289 -0
  101. package/src/vector-cortex/provider/experiments.ts +248 -0
  102. package/extensions/dashboard-client/dist/assets/VectorCortexTab-94fqVjat.js +0 -2
  103. package/extensions/dashboard-client/dist/assets/VectorCortexTab-94fqVjat.js.map +0 -1
@@ -114,4 +114,4 @@ export const NEW_UI = () => ragEnabled("MEGACOMPACT_NEW_UI");
114
114
  // default ON, `=0`/`_DISABLED` off. Re-exported from src/config/vector-cortex.ts
115
115
  // so root consumers share one source of truth.
116
116
  // ---------------------------------------------------------------------------
117
- export { VC0A_ENABLED, VC0B_ENABLED, VC1A_ENABLED, VC0C_ENABLED, VC1B_ENABLED, VC1C_ENABLED, VC2A_ENABLED, VC2B_ENABLED, VC2C_ENABLED, VC3A_ENABLED, VC3B_ENABLED, VC3C_ENABLED, VC4A_ENABLED, VC4B_ENABLED, VC4C_ENABLED, VC5A_ENABLED, VC5B_ENABLED, VC5C_ENABLED, VC6A_ENABLED, VC6B_ENABLED, VC6C_ENABLED, VC7A_ENABLED, BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./config/vector-cortex.js";
117
+ export { VC0A_ENABLED, VC0B_ENABLED, VC1A_ENABLED, VC0C_ENABLED, VC1B_ENABLED, VC1C_ENABLED, VC2A_ENABLED, VC2B_ENABLED, VC2C_ENABLED, VC3A_ENABLED, VC3B_ENABLED, VC3C_ENABLED, VC4A_ENABLED, VC4B_ENABLED, VC4C_ENABLED, VC5A_ENABLED, VC5B_ENABLED, VC5C_ENABLED, VC6A_ENABLED, VC6B_ENABLED, VC6C_ENABLED, VC7A_ENABLED, VC7B_ENABLED, VC7C_ENABLED, BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./config/vector-cortex.js";
@@ -0,0 +1,41 @@
1
+ /**
2
+ * cache/_economics-fixture.ts — conformance fixture I/O for VC7B economics rows.
3
+ *
4
+ * Sibling of `./_crystal-fixture.ts`, same job for a different corpus: turn
5
+ * canonical JSON back into the REAL production types the economics / compiler /
6
+ * experiments modules consume. Fixtures cannot express bigints, so token counts
7
+ * stored as numbers are converted here — if that conversion were lossy the
8
+ * computed digests would diverge and the acceptance rows would fail loudly.
9
+ *
10
+ * No mocks, no stubs, no parallel "test shape": the decoded objects ARE
11
+ * `ProviderEconomicsV1` / `CacheUsageV1` / `CacheExperimentV1` and are fed
12
+ * verbatim into `validateProfileEconomics`, `computeEconomics`, `compileCrystalBoundaries`,
13
+ * and `assignExperiment`.
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 "../heal/_acceptance-fixture.js";
19
+ /** Read one registered cache-economics fixture (asserting it IS registered). */
20
+ export function economicsFixture(id) {
21
+ const m = readManifest();
22
+ const row = m.fixtures.find((f) => f.id === id && f.path.startsWith("cache-economics/"));
23
+ assert.ok(row, `fixture ${id} registered under cache-economics/ in manifest`);
24
+ return JSON.parse(readFileSync(join(V2, row.path), "utf8"));
25
+ }
26
+ /** Flag-pinned wrapper: VC7B gated by MEGACOMPACT_VC7B (defaults ON). */
27
+ export function withVc7bFlag(value, fn) {
28
+ return () => {
29
+ const saved = process.env.MEGACOMPACT_VC7B;
30
+ process.env.MEGACOMPACT_VC7B = value;
31
+ try {
32
+ fn();
33
+ }
34
+ finally {
35
+ if (saved === undefined)
36
+ delete process.env.MEGACOMPACT_VC7B;
37
+ else
38
+ process.env.MEGACOMPACT_VC7B = saved;
39
+ }
40
+ };
41
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * vector-cortex/cache/compiler.ts — VC7B provider-safe crystal boundary compiler.
3
+ *
4
+ * A provider caches a PREFIX of the request. So where you cut the prompt into
5
+ * cacheable segments determines whether anything is reusable at all: cut too
6
+ * finely and every segment falls under the provider's `minPrefix` and nothing is
7
+ * cacheable; cut in the wrong place and a segment that changes every turn sits
8
+ * in front of one that never changes, invalidating everything behind it. This
9
+ * file turns validated source ranges plus a profile's limits into boundaries
10
+ * that the provider can actually cache.
11
+ *
12
+ * THE ONE INVARIANT THAT OUTRANKS EVERYTHING: THE COMPILER NEVER CHANGES
13
+ * REQUEST IDENTITY. It selects where boundaries FALL; it never reorders, never
14
+ * merges across a session, never drops a range, never rewrites bytes. The
15
+ * concatenation of the compiled boundaries covers exactly the same ranges, in
16
+ * exactly the same canonical order, as the input — so the VC5B canonical request
17
+ * digest and the VC7A crystal key are byte-identical before and after
18
+ * compilation. `boundariesPreserveIdentity()` states this as an executable
19
+ * check, and the acceptance suite runs it on every compiled row. If compilation
20
+ * could alter identity it would be a cache-poisoning engine rather than an
21
+ * optimizer: two different conversations could compile to one cache key.
22
+ *
23
+ * WHY MERGE FORWARD, AND ONLY WITHIN A SESSION. Ranges below `minPrefix` are not
24
+ * independently cacheable, so the compiler merges each undersized range into the
25
+ * one that FOLLOWS it in canonical order — forward, so the merged segment keeps
26
+ * the earlier range's start and the prefix relationship is preserved. Merging is
27
+ * confined to a single session because ranges from different sessions cover
28
+ * disjoint byte streams; a cross-session segment would claim a contiguity that
29
+ * does not exist. A trailing undersized run with nothing to merge into is
30
+ * emitted as its own boundary marked `cacheable: false` — reported honestly as
31
+ * uncacheable rather than padded, dropped, or silently attached backwards.
32
+ *
33
+ * BOUNDED. `maxSegments` caps the output so a caller-shaped range list cannot be
34
+ * turned into an unbounded segmentation (`COMP_SEGMENT_LIMIT`).
35
+ *
36
+ * PURE. No clock, no storage, no console, no network (PREVENT-PI-004 /
37
+ * PREVENT-011). Runs identically with `MEGACOMPACT_VC7B` on or off — the flag
38
+ * gates only the reporter/dashboard seam in `./economics-emit.ts`.
39
+ */
40
+ import { createHash } from "node:crypto";
41
+ import { compareSpans, sortSpans, validateRanges } from "./crystal.js";
42
+ /** Conventional default limits (4 bytes/token is the usual English estimate). */
43
+ export const DEFAULT_COMPILER_LIMITS = {
44
+ minPrefix: 1024,
45
+ maxSegments: 64,
46
+ bytesPerToken: 4,
47
+ };
48
+ /** Covered bytes of one range. */
49
+ function spanBytes(s) {
50
+ return s.endByte - s.startByte;
51
+ }
52
+ /**
53
+ * Byte→token estimate. Deliberately a floor: under-estimating tokens can only
54
+ * make the compiler MORE conservative about calling a segment cacheable, which
55
+ * fails safe (a wrongly-cacheable segment would be priced as a hit it never got).
56
+ */
57
+ export function tokensForBytes(bytes, bytesPerToken) {
58
+ return Math.floor(bytes / bytesPerToken);
59
+ }
60
+ /** `sha256:` digest over a segment's ranges — length-prefixed, so injective. */
61
+ function boundaryDigest(ranges) {
62
+ const h = createHash("sha256");
63
+ for (const s of ranges) {
64
+ const parts = [
65
+ s.sessionId,
66
+ s.startSeq.toString(),
67
+ s.endSeq.toString(),
68
+ String(s.startByte),
69
+ String(s.endByte),
70
+ s.digest,
71
+ ];
72
+ for (const p of parts)
73
+ h.update(`${Buffer.byteLength(p, "utf8")}:${p}`, "utf8");
74
+ }
75
+ return `sha256:${h.digest("hex")}`;
76
+ }
77
+ /** Build one boundary from a contiguous same-session run of ranges. */
78
+ function makeBoundary(ranges, limits) {
79
+ const byteCount = ranges.reduce((n, s) => n + spanBytes(s), 0);
80
+ const tokenCount = tokensForBytes(byteCount, limits.bytesPerToken);
81
+ const first = ranges[0];
82
+ return {
83
+ schema: "crystal-boundary-v1",
84
+ sessionId: first === undefined ? "" : first.sessionId,
85
+ ranges,
86
+ byteCount,
87
+ tokenCount,
88
+ cacheable: tokenCount >= limits.minPrefix,
89
+ digest: boundaryDigest(ranges),
90
+ };
91
+ }
92
+ function validLimits(l) {
93
+ return (Number.isSafeInteger(l.minPrefix) &&
94
+ l.minPrefix >= 0 &&
95
+ Number.isSafeInteger(l.maxSegments) &&
96
+ l.maxSegments > 0 &&
97
+ Number.isSafeInteger(l.bytesPerToken) &&
98
+ l.bytesPerToken >= 1);
99
+ }
100
+ /**
101
+ * Compile validated ranges into provider-safe cache boundaries.
102
+ *
103
+ * Ranges are first put in canonical order (the SAME `sortSpans` order the VC7A
104
+ * key encoder uses — sharing the comparator is what keeps the two subsystems
105
+ * from disagreeing about what "canonical" means). They are then grouped by
106
+ * session, and within each session an undersized run is merged FORWARD until it
107
+ * meets `minPrefix`.
108
+ *
109
+ * Identity is verified before returning: the flattened output must equal the
110
+ * canonical input exactly. That check should never fire — it is a guard against
111
+ * a future edit to the merge loop quietly reordering or dropping a range.
112
+ */
113
+ export function compileCrystalBoundaries(ranges, limits = DEFAULT_COMPILER_LIMITS) {
114
+ if (!validLimits(limits))
115
+ return { ok: false, codes: ["COMP_LIMIT_INVALID"] };
116
+ // Reuse VC7A's validator verbatim: the compiler must never accept a range set
117
+ // the crystal key would reject, or the two would disagree about what is legal.
118
+ const rangeCodes = validateRanges(ranges);
119
+ if (rangeCodes.length > 0)
120
+ return { ok: false, codes: ["COMP_RANGE_INVALID"] };
121
+ const sorted = sortSpans(ranges);
122
+ const boundaries = [];
123
+ let run = [];
124
+ const flushRun = () => {
125
+ if (run.length > 0) {
126
+ boundaries.push(makeBoundary(run, limits));
127
+ run = [];
128
+ }
129
+ };
130
+ for (const span of sorted) {
131
+ const head = run[0];
132
+ // A session change always closes the run: a boundary never spans sessions,
133
+ // because two sessions' byte streams are not contiguous with each other.
134
+ if (head !== undefined && head.sessionId !== span.sessionId)
135
+ flushRun();
136
+ run.push(span);
137
+ const bytes = run.reduce((n, s) => n + spanBytes(s), 0);
138
+ // Close as soon as the run is independently cacheable; anything smaller keeps
139
+ // absorbing the next range (merge-forward).
140
+ if (tokensForBytes(bytes, limits.bytesPerToken) >= limits.minPrefix)
141
+ flushRun();
142
+ }
143
+ // A trailing undersized run has nothing left to merge into: emit it honestly
144
+ // as a non-cacheable boundary rather than padding or dropping it.
145
+ flushRun();
146
+ if (boundaries.length > limits.maxSegments) {
147
+ return { ok: false, codes: ["COMP_SEGMENT_LIMIT"] };
148
+ }
149
+ if (!boundariesPreserveIdentity(sorted, boundaries)) {
150
+ return { ok: false, codes: ["COMP_IDENTITY_DRIFT"] };
151
+ }
152
+ let cacheableCount = 0;
153
+ let cacheableTokens = 0;
154
+ for (const b of boundaries) {
155
+ if (b.cacheable) {
156
+ cacheableCount += 1;
157
+ cacheableTokens += b.tokenCount;
158
+ }
159
+ }
160
+ return { ok: true, boundaries, cacheableCount, cacheableTokens };
161
+ }
162
+ /**
163
+ * The executable form of the sprint's headline invariant: flattening the
164
+ * compiled boundaries must reproduce the canonical input ranges EXACTLY — same
165
+ * ranges, same order, same pinned digests.
166
+ *
167
+ * Comparison is field-by-field on identity (session, seq bounds, byte bounds,
168
+ * digest), never by count or by digest alone: a compiler bug that swapped two
169
+ * equal-length ranges would keep the count and the byte total identical while
170
+ * changing what the request means.
171
+ */
172
+ export function boundariesPreserveIdentity(input, boundaries) {
173
+ const flat = [];
174
+ for (const b of boundaries)
175
+ flat.push(...b.ranges);
176
+ const canonical = sortSpans(input);
177
+ if (flat.length !== canonical.length)
178
+ return false;
179
+ for (let i = 0; i < flat.length; i += 1) {
180
+ const a = flat[i];
181
+ const b = canonical[i];
182
+ if (a === undefined || b === undefined)
183
+ return false;
184
+ if (compareSpans(a, b) !== 0)
185
+ return false;
186
+ // compareSpans intentionally ignores the pinned digest (it orders by source
187
+ // position). Identity includes the covered bytes, so check it explicitly.
188
+ if (a.digest !== b.digest)
189
+ return false;
190
+ }
191
+ return true;
192
+ }
193
+ /**
194
+ * Compile the ranges of an existing crystal key. Returns the boundaries plus the
195
+ * UNCHANGED key: compilation is an optimization of how the render is segmented,
196
+ * never a re-keying. Returning the same key object makes that explicit at the
197
+ * call site — a caller cannot accidentally pick up a "compiled key" that differs.
198
+ */
199
+ export function compileForKey(key, limits = DEFAULT_COMPILER_LIMITS) {
200
+ return { key, compiled: compileCrystalBoundaries(key.sourceRanges, limits) };
201
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * vector-cortex/cache/economics-emit.ts — VC7B event reporter seam.
3
+ *
4
+ * Mirrors `./crystal-emit.ts`: a thin `safe()` wrapper around an optional
5
+ * injected `emit` (unit tests pass `undefined` and stay pure), and the two event
6
+ * names the sprint spec requires verbatim:
7
+ * - `vector_cortex_cache_experiment_assigned` — a session entered an arm.
8
+ * - `vector_cortex_cache_economics_estimated` — net savings were computed.
9
+ *
10
+ * FLAG SEMANTICS. `computeEconomics`, `compileCrystalBoundaries` and
11
+ * `assignExperiment` are PURE and run REGARDLESS of `MEGACOMPACT_VC7B`. The flag
12
+ * gates ONLY this reporting + dashboard seam: with the flag off a session still
13
+ * hashes to the SAME arm, the compiler still produces the SAME boundaries, and
14
+ * the same net savings are still computed — we just do not announce them under
15
+ * the VC7B event namespace, and the dashboard reports `enabled:false` + mode C.
16
+ * That is what makes flag-off byte-identical to VC7A: the arithmetic is never
17
+ * skipped, only the emission.
18
+ *
19
+ * PAYLOAD DISCIPLINE. These events carry the ARM, the BUCKET, integer money
20
+ * AGGREGATES, and the evidence label — never a session id, never covered ranges,
21
+ * never frozen bytes, never a request or covered digest. A session id here would
22
+ * re-identify a user's conversation in a log file, and the bucket already carries
23
+ * everything needed to audit the split (SECURITY_PRIVACY — the exact ledger is
24
+ * not diagnostic data). The `evidence` label travels WITH every economics event
25
+ * so a downstream aggregator can never mistake a shadow estimate for a measured,
26
+ * randomized result.
27
+ *
28
+ * No console, no storage, no network (PREVENT-PI-004 / PREVENT-011). Every line
29
+ * is a structured JSON event with `ts` + `event`.
30
+ */
31
+ import { VC7B_ENABLED } from "../../config/vector-cortex.js";
32
+ /** Run `fn` only when an emit exists; a reporting failure is never fatal. */
33
+ function safe(emit, fn) {
34
+ if (emit === undefined)
35
+ return;
36
+ try {
37
+ fn(emit);
38
+ }
39
+ catch {
40
+ // Non-fatal: a reporting failure must never break the agent loop.
41
+ }
42
+ }
43
+ /** The event names VC7B emits, exported for the dashboard seam and tests. */
44
+ export const ECONOMICS_EVENT_NAMES = [
45
+ "vector_cortex_cache_experiment_assigned",
46
+ "vector_cortex_cache_economics_estimated",
47
+ ];
48
+ /**
49
+ * Report a session's experiment assignment.
50
+ *
51
+ * Arm + bucket + source only. The bucket is sufficient to audit that the split
52
+ * is being honored; the SESSION ID is deliberately absent so the event log
53
+ * cannot be used to reconstruct who was in which arm.
54
+ */
55
+ export function reportCacheExperimentAssigned(emit, payload) {
56
+ if (!VC7B_ENABLED())
57
+ return;
58
+ safe(emit, (e) => e("vector_cortex_cache_experiment_assigned", {
59
+ ts: undefined,
60
+ event: "vector_cortex_cache_experiment_assigned",
61
+ experimentId: payload.experimentId,
62
+ arm: payload.arm,
63
+ bucket: payload.bucket,
64
+ source: payload.source,
65
+ }));
66
+ }
67
+ /**
68
+ * Report computed cache economics.
69
+ *
70
+ * Integer micro-unit aggregates plus the evidence label. `netSavings` may be
71
+ * NEGATIVE and is reported as such — a cache that lost money is the outcome this
72
+ * telemetry exists to surface, so it is never clamped on the way out.
73
+ */
74
+ export function reportCacheEconomicsEstimated(emit, payload) {
75
+ if (!VC7B_ENABLED())
76
+ return;
77
+ safe(emit, (e) => e("vector_cortex_cache_economics_estimated", {
78
+ ts: undefined,
79
+ event: "vector_cortex_cache_economics_estimated",
80
+ profileId: payload.profileId,
81
+ netSavings: payload.netSavings,
82
+ tokenSavings: payload.tokenSavings,
83
+ evidence: payload.evidence,
84
+ }));
85
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * request-hash-v2-ops.ts — M5 migration operational logic (copy/verify/switch).
3
+ *
4
+ * Extracted from request-hash-v2.ts to keep the parent file under the 300-line
5
+ * soft limit (soft-as-hard gate). Pure logic over an injected M5Host;
6
+ * deterministic + testable; no console. PREVENT-002/011/PI-004 honored.
7
+ */
8
+ import { createHash } from "node:crypto";
9
+ import { invalidationKey } from "../topology/query.js";
10
+ import { M5_FAIL, REQUEST_HASH_LEGACY_VERSION, } from "./request-hash-v2-types.js";
11
+ /** Length-prefixed framing so the folded fields cannot alias. */
12
+ function field(value) {
13
+ return `${Buffer.byteLength(value, "utf8")}:${value}`;
14
+ }
15
+ const identity = (profileId, requestDigest) => `${profileId}::${requestDigest}`;
16
+ /**
17
+ * The authoritative v2 hash: SHA-256 over the version tag, profile, request
18
+ * digest, and economics version. Length-prefixed for injectivity, exactly as the
19
+ * crystal key encoder frames its fields.
20
+ */
21
+ export function deriveRequestHashV2(profileId, requestDigest, economicsVersion) {
22
+ return createHash("sha256")
23
+ .update(field("request-hash-v2") +
24
+ field(profileId) +
25
+ field(requestDigest) +
26
+ field(economicsVersion), "utf8")
27
+ .digest("hex");
28
+ }
29
+ /** Derive the v2 row for one v1 row. */
30
+ export function deriveRequestHashRow(host, v1) {
31
+ const economicsVersion = host.economicsVersionOf(v1.profileId);
32
+ return {
33
+ profileId: v1.profileId,
34
+ // IDENTITY-PRESERVING: carried through verbatim, never re-derived.
35
+ requestDigest: v1.requestDigest,
36
+ economicsVersion,
37
+ hash: deriveRequestHashV2(v1.profileId, v1.requestDigest, economicsVersion),
38
+ };
39
+ }
40
+ /**
41
+ * Economics versions encode the generation they were minted under. We treat the
42
+ * trailing numeric segment as the generation; if it is not numeric the row is
43
+ * considered tied to generation 0 (always live unless explicitly invalidated).
44
+ */
45
+ function generationFromEconomics(economicsVersion) {
46
+ const match = /(\d+)$/.exec(economicsVersion);
47
+ return match ? BigInt(match[1]) : 0n;
48
+ }
49
+ export function detectCollision(host) {
50
+ const byHash = new Map();
51
+ for (const v1 of host.v1Rows()) {
52
+ const row = deriveRequestHashRow(host, v1);
53
+ const prior = byHash.get(row.hash);
54
+ if (prior !== undefined &&
55
+ prior !== identity(v1.profileId, v1.requestDigest)) {
56
+ return true;
57
+ }
58
+ byHash.set(row.hash, identity(v1.profileId, v1.requestDigest));
59
+ }
60
+ return false;
61
+ }
62
+ export function isGenerationInvalidated(host, row) {
63
+ const session = host.sessionOf(row.profileId);
64
+ const live = host.liveGenerationOf(session);
65
+ const keyForLive = invalidationKey(session, live);
66
+ const keyForRow = invalidationKey(session, generationFromEconomics(row.economicsVersion));
67
+ return keyForLive !== keyForRow;
68
+ }
69
+ export function m5Copy(host) {
70
+ const existing = new Map(host.existingV2().map((r) => [identity(r.profileId, r.requestDigest), r]));
71
+ const wanted = [];
72
+ for (const v1 of host.v1Rows()) {
73
+ const fresh = deriveRequestHashRow(host, v1);
74
+ if (isGenerationInvalidated(host, fresh))
75
+ continue;
76
+ const stored = existing.get(identity(v1.profileId, v1.requestDigest));
77
+ if (stored !== undefined &&
78
+ stored.hash === fresh.hash &&
79
+ stored.economicsVersion === fresh.economicsVersion) {
80
+ continue;
81
+ }
82
+ wanted.push(fresh);
83
+ }
84
+ if (wanted.length > 0)
85
+ host.putV2(wanted);
86
+ return { written: wanted, all: [...host.existingV2()] };
87
+ }
88
+ function dedupe(codes) {
89
+ const out = [];
90
+ for (const c of codes)
91
+ if (!out.includes(c))
92
+ out.push(c);
93
+ return out;
94
+ }
95
+ export function m5Verify(host) {
96
+ const codes = [];
97
+ const stored = host.existingV2();
98
+ const v1Rows = host.v1Rows();
99
+ const counts = new Map();
100
+ for (const r of stored) {
101
+ const id = identity(r.profileId, r.requestDigest);
102
+ counts.set(id, (counts.get(id) ?? 0) + 1);
103
+ }
104
+ const expected = new Set();
105
+ for (const v1 of v1Rows) {
106
+ const id = identity(v1.profileId, v1.requestDigest);
107
+ expected.add(id);
108
+ const n = counts.get(id) ?? 0;
109
+ if (n === 0)
110
+ codes.push(M5_FAIL.COPY_PARTIAL);
111
+ if (n > 1)
112
+ codes.push(M5_FAIL.COUNT_MISMATCH);
113
+ }
114
+ const v1Digests = new Set(v1Rows.map((r) => identity(r.profileId, r.requestDigest)));
115
+ for (const r of stored) {
116
+ const fresh = deriveRequestHashV2(r.profileId, r.requestDigest, r.economicsVersion);
117
+ if (r.hash !== fresh)
118
+ codes.push(M5_FAIL.DIGEST_MISMATCH);
119
+ const id = identity(r.profileId, r.requestDigest);
120
+ if (!v1Digests.has(id)) {
121
+ codes.push(expected.size > 0 ? M5_FAIL.IDENTITY_DRIFT : M5_FAIL.COUNT_MISMATCH);
122
+ }
123
+ }
124
+ if (detectCollision(host))
125
+ codes.push(M5_FAIL.REQUEST_HASH_COLLISION);
126
+ return { ok: codes.length === 0, codes: dedupe(codes) };
127
+ }
128
+ export function m5Switch(host) {
129
+ if (host.activeVersion() !== REQUEST_HASH_LEGACY_VERSION) {
130
+ return { ok: false, codes: [M5_FAIL.NOT_ON_LEGACY] };
131
+ }
132
+ const verify = m5Verify(host);
133
+ if (!verify.ok)
134
+ return verify;
135
+ if (detectCollision(host))
136
+ return { ok: false, codes: [M5_FAIL.REQUEST_HASH_COLLISION] };
137
+ host.switchToV2();
138
+ return { ok: true, codes: [] };
139
+ }
140
+ export function migrateRequestHashV2(host) {
141
+ m5Copy(host);
142
+ return m5Switch(host);
143
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * request-hash-v2-types.ts — M5 request-hash-v2 migration types.
3
+ *
4
+ * Extracted from request-hash-v2.ts to keep the parent file under the 300-line
5
+ * soft limit (soft-as-hard gate). Pure type + constant definitions; no logic.
6
+ * PREVENT-002/011/PI-004 honored.
7
+ */
8
+ /** The active-version value the v2 pointer is switched to. */
9
+ export const REQUEST_HASH_V2_VERSION = 2;
10
+ /** The legacy (predecessor) active version. */
11
+ export const REQUEST_HASH_LEGACY_VERSION = 1;
12
+ /** M5 failure codes. */
13
+ export const M5_FAIL = {
14
+ COPY_PARTIAL: "M5_COPY_PARTIAL",
15
+ COUNT_MISMATCH: "M5_COUNT_MISMATCH",
16
+ DIGEST_MISMATCH: "M5_DIGEST_MISMATCH",
17
+ IDENTITY_DRIFT: "M5_IDENTITY_DRIFT",
18
+ /** Two distinct v1 rows map to the same v2 hash — blocks the switch. (VC7C) */
19
+ REQUEST_HASH_COLLISION: "M5_REQUEST_HASH_COLLISION",
20
+ /** The active pointer is not on v1; switching would be a no-op or a regression. */
21
+ NOT_ON_LEGACY: "M5_NOT_ON_LEGACY",
22
+ };
23
+ /**
24
+ * Registered M5 conformance IDs (M5-001..020). The acceptance test reads these
25
+ * rows from the v2 `migrations/` domain and asserts each returns its manifest
26
+ * bytes or exactly its listed failure code. Mirrors M6_IDS / M4_IDS.
27
+ */
28
+ export const M5_IDS = Array.from({ length: 20 }, (_v, i) => `M5-${String(i + 1).padStart(3, "0")}`);
29
+ /** Named M5 rows surfaced by the conformance corpus (mirrors M6_NAMED_IDS). */
30
+ export const M5_NAMED_IDS = ["M5-COLLIDE-002"];
@@ -0,0 +1,56 @@
1
+ /**
2
+ * vector-cortex/migrations/request-hash-v2.ts — M5 request-hash-v2 migration
3
+ * (COPY + VALIDATE + SWITCH).
4
+ *
5
+ * Delegate-shell: types + constants live in request-hash-v2-types.ts and the
6
+ * operational logic (copy/verify/switch/collision) lives in
7
+ * request-hash-v2-ops.ts. This file re-exports the public surface so callers
8
+ * import from a single module. Split to keep under the 300-line soft limit
9
+ * (soft-as-hard gate).
10
+ *
11
+ * M5 versions the canonical request hash: the predecessor hashed the outbound
12
+ * request under the v1 scheme; v2 folds in the provider profile's ECONOMICS
13
+ * version so a pricing/TTL/exclusion change cannot silently reuse a cache
14
+ * identity minted under different economics. Like M4/M6 it follows the
15
+ * copy/validate/switch contract:
16
+ *
17
+ * - copy: resumable per (profile, request) — an interrupted run resumes
18
+ * without duplicate rows or active-pointer drift.
19
+ * - validate: every v1 row has exactly one v2 row, every v2 digest re-hashes
20
+ * from its own declared fields, the migration is IDENTITY-PRESERVING
21
+ * (a v2 row carries the same `requestDigest` as its v1 source — v2
22
+ * changes how a CACHE KEY is derived, never what the request IS), and
23
+ * — the M5-specific invariant ADDED in VC7C — there are ZERO
24
+ * collisions: no two distinct v1 rows may map to one v2 hash. A
25
+ * collision (`M5_REQUEST_HASH_COLLISION`) means two different
26
+ * conversations would share a cache key, the most dangerous outcome
27
+ * in the subsystem, so it blocks the switch outright.
28
+ * - switch: ATOMICALLY flip the active pointer to v2 via `host.switchToV2()`.
29
+ * VC7B deferred this; VC7C performs it — but ONLY after re-validating
30
+ * against freshly-read host state at switch time.
31
+ *
32
+ * WHY THE COLLISION CHECK RUNS AT SWITCH TIME, NOT VALIDATE TIME. The brief's
33
+ * failure-injection contract is explicit: crash after M5 validation, inject a
34
+ * collision into host state, then resume — and the RESUMED run must detect
35
+ * `M5_REQUEST_HASH_COLLISION`. If the collision were detected only from the result
36
+ * of an earlier `m5Verify` call, the cached result would be replayed and the
37
+ * injected collision would be invisible. So `m5Switch` RE-READS the host (`v1Rows`,
38
+ * `existingV2`, `activeVersion`) and RE-RUNS the collision check against that live
39
+ * state. Validation is a precondition; the switch is the only place that proves the
40
+ * hazard is absent *right now*. This is the same resume-after-crash discipline as
41
+ * M4/M6: a migration that trusts a stale verification is a migration that loses
42
+ * data on restart.
43
+ *
44
+ * M6 INVALIDATION CONSUMPTION. A v2 request hash is only as trustworthy as the
45
+ * generation it was minted under. The switch consumes M6's structured invalidation
46
+ * keys via the REAL API (`invalidationKey` from `../topology/query.js`) rather than
47
+ * inventing one: when a v2 row's economics version maps to an invalidated router
48
+ * generation, that row cannot be promoted to active. We do not re-derive a
49
+ * generation here — topology is the authority on what generation is live — we only
50
+ * refuse to switch a row whose generation is dead. This keeps M5 from resurrecting
51
+ * cache identities tied to a generation the router has already invalidated.
52
+ *
53
+ * PREVENT-002/011/PI-004 honored.
54
+ */
55
+ export { REQUEST_HASH_V2_VERSION, REQUEST_HASH_LEGACY_VERSION, M5_FAIL, M5_IDS, M5_NAMED_IDS, } from "./request-hash-v2-types.js";
56
+ export { deriveRequestHashV2, deriveRequestHashRow, detectCollision, isGenerationInvalidated, m5Copy, m5Verify, m5Switch, migrateRequestHashV2, } from "./request-hash-v2-ops.js";
@@ -0,0 +1,29 @@
1
+ /**
2
+ * vector-cortex/provider/economics-ids.ts — VC7B conformance ID registrations.
3
+ *
4
+ * Extracted from economics.ts to keep that file under the 300-line soft limit
5
+ * (soft-as-hard gate). The ID ranges and named rows are pure data — splitting
6
+ * them out mirrors how vector-cortex-breakers.ts was extracted from
7
+ * vector-cortex.ts. economics.ts re-exports them so no consumer import path
8
+ * changes.
9
+ *
10
+ * Pi-agnostic, dependency-free (PREVENT-PI-004 / PREVENT-011).
11
+ */
12
+ /**
13
+ * Registered VC7B cache-economics conformance ID range (CACHE-001..015). The
14
+ * acceptance aggregator reads these rows from the v2 manifest and asserts each
15
+ * returns its manifest `ok`/`code`.
16
+ */
17
+ export const CACHE_IDS = Array.from({ length: 15 }, (_v, i) => `CACHE-${String(i + 1).padStart(3, "0")}`);
18
+ /**
19
+ * Registered VC7B provider-economics conformance rows (PRO-024..030), continuing
20
+ * VC7A's PRO-016..023. These pin the ECONOMICS half of a provider profile:
21
+ * pricing validity, the exclusion-proof rule, and TTL/min-prefix eligibility.
22
+ */
23
+ export const ECONOMICS_PROVIDER_IDS = Array.from({ length: 7 }, (_v, i) => `PRO-${String(i + 24).padStart(3, "0")}`);
24
+ /** Named VC7B conformance assertions (the sprint's headline rows). */
25
+ export const ECONOMICS_NAMED_IDS = [
26
+ "CACHE-COST-001",
27
+ "CACHE-EXCLUDE-002",
28
+ "CACHE-RANDOM-003",
29
+ ];