pi-mega-compact 0.20.15 → 0.20.16

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 (76) hide show
  1. package/dist/extensions/dashboard-server/api-contracts/vector-cortex-diagnostics.js +35 -0
  2. package/dist/extensions/dashboard-server/route-dispatch.js +6 -1
  3. package/dist/extensions/dashboard-server/routes-vector-cortex-diagnostics.js +68 -0
  4. package/dist/src/vector-cortex/cache/_diagnostics-fixture.js +31 -0
  5. package/dist/src/vector-cortex/cache/breaker.js +110 -0
  6. package/dist/src/vector-cortex/cache/diagnostics-emit.js +64 -0
  7. package/dist/src/vector-cortex/cache/diagnostics-types.js +15 -0
  8. package/dist/src/vector-cortex/cache/diagnostics.js +97 -0
  9. package/dist/src/vector-cortex/migrations/request-hash-v2-ops.js +17 -6
  10. package/dist/vector-cortex/cache/_diagnostics-fixture.js +31 -0
  11. package/dist/vector-cortex/cache/breaker.js +110 -0
  12. package/dist/vector-cortex/cache/diagnostics-emit.js +64 -0
  13. package/dist/vector-cortex/cache/diagnostics-types.js +15 -0
  14. package/dist/vector-cortex/cache/diagnostics.js +97 -0
  15. package/dist/vector-cortex/migrations/request-hash-v2-ops.js +17 -6
  16. package/extensions/dashboard-client/dist/assets/{AreaChart-CV8WqMmt.js → AreaChart-B_FKg8xJ.js} +2 -2
  17. package/extensions/dashboard-client/dist/assets/{AreaChart-CV8WqMmt.js.map → AreaChart-B_FKg8xJ.js.map} +1 -1
  18. package/extensions/dashboard-client/dist/assets/{BarChart-M-HEBq9l.js → BarChart-BU5RJ0In.js} +2 -2
  19. package/extensions/dashboard-client/dist/assets/{BarChart-M-HEBq9l.js.map → BarChart-BU5RJ0In.js.map} +1 -1
  20. package/extensions/dashboard-client/dist/assets/{CacheTab-DJ_yPAhP.js → CacheTab-CbX033mt.js} +2 -2
  21. package/extensions/dashboard-client/dist/assets/{CacheTab-DJ_yPAhP.js.map → CacheTab-CbX033mt.js.map} +1 -1
  22. package/extensions/dashboard-client/dist/assets/{EventsTab-DNVlKzt6.js → EventsTab-D8Z_4gSB.js} +2 -2
  23. package/extensions/dashboard-client/dist/assets/{EventsTab-DNVlKzt6.js.map → EventsTab-D8Z_4gSB.js.map} +1 -1
  24. package/extensions/dashboard-client/dist/assets/{HealthTab-B5yjuInb.js → HealthTab-BUUkoQXo.js} +2 -2
  25. package/extensions/dashboard-client/dist/assets/{HealthTab-B5yjuInb.js.map → HealthTab-BUUkoQXo.js.map} +1 -1
  26. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DoQedESF.js → MaintenanceTab-DKZQmY1T.js} +2 -2
  27. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DoQedESF.js.map → MaintenanceTab-DKZQmY1T.js.map} +1 -1
  28. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CuL5CzdH.js → MemoryMapTab-DwMGbe3h.js} +2 -2
  29. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CuL5CzdH.js.map → MemoryMapTab-DwMGbe3h.js.map} +1 -1
  30. package/extensions/dashboard-client/dist/assets/{MetricsTab-CZSTaCpK.js → MetricsTab-C85lieXW.js} +2 -2
  31. package/extensions/dashboard-client/dist/assets/{MetricsTab-CZSTaCpK.js.map → MetricsTab-C85lieXW.js.map} +1 -1
  32. package/extensions/dashboard-client/dist/assets/{OverviewTab-vkZlXRW6.js → OverviewTab-D7VPibBT.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{OverviewTab-vkZlXRW6.js.map → OverviewTab-D7VPibBT.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/{ReposTab-DgPD96rF.js → ReposTab-eIHk_mDG.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{ReposTab-DgPD96rF.js.map → ReposTab-eIHk_mDG.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{SessionsTab-XPbWh7XN.js → SessionsTab-EYOHEzc3.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{SessionsTab-XPbWh7XN.js.map → SessionsTab-EYOHEzc3.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{SetupTab-Dz74XAtR.js → SetupTab-DXpiXu4i.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{SetupTab-Dz74XAtR.js.map → SetupTab-DXpiXu4i.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-FynCpJdx.js → TimeSavedCard-DUsUC_sm.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-FynCpJdx.js.map → TimeSavedCard-DUsUC_sm.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/{TurnsTab-C7m3GDy0.js → TurnsTab-w7qdB0px.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{TurnsTab-C7m3GDy0.js.map → TurnsTab-w7qdB0px.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/VectorCortexTab-BmgdxV5K.js +2 -0
  45. package/extensions/dashboard-client/dist/assets/VectorCortexTab-BmgdxV5K.js.map +1 -0
  46. package/extensions/dashboard-client/dist/assets/{WikiTab-C6viLa0u.js → WikiTab-B4hwpXpw.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{WikiTab-C6viLa0u.js.map → WikiTab-B4hwpXpw.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/{button-mBEgbBVU.js → button-BnbvfQjm.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{button-mBEgbBVU.js.map → button-BnbvfQjm.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/assets/{card-CalUQAKG.js → card-DCyvWfpF.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{card-CalUQAKG.js.map → card-DCyvWfpF.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-CMmkuwj5.js → generateCategoricalChart-b3HeKJLU.js} +2 -2
  53. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-CMmkuwj5.js.map → generateCategoricalChart-b3HeKJLU.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/{index-Dz8GPBOE.js → index-DhlcXMlE.js} +3 -3
  55. package/extensions/dashboard-client/dist/assets/{index-Dz8GPBOE.js.map → index-DhlcXMlE.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/{switch-8QKO6CTU.js → switch-B74pQOFy.js} +2 -2
  57. package/extensions/dashboard-client/dist/assets/{switch-8QKO6CTU.js.map → switch-B74pQOFy.js.map} +1 -1
  58. package/extensions/dashboard-client/dist/assets/{toggle-9eCkWI4q.js → toggle-bXDrMX-A.js} +2 -2
  59. package/extensions/dashboard-client/dist/assets/{toggle-9eCkWI4q.js.map → toggle-bXDrMX-A.js.map} +1 -1
  60. package/extensions/dashboard-client/dist/assets/{useSSE-s6IqOKoD.js → useSSE-Yq86SUA0.js} +2 -2
  61. package/extensions/dashboard-client/dist/assets/{useSSE-s6IqOKoD.js.map → useSSE-Yq86SUA0.js.map} +1 -1
  62. package/extensions/dashboard-client/dist/index.html +1 -1
  63. package/extensions/dashboard-client/src/tabs/VectorCortexDiagnosticsCard.tsx +46 -0
  64. package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +2 -0
  65. package/extensions/dashboard-server/api-contracts/vector-cortex-diagnostics.ts +76 -0
  66. package/extensions/dashboard-server/route-dispatch.ts +5 -1
  67. package/extensions/dashboard-server/routes-vector-cortex-diagnostics.ts +77 -0
  68. package/package.json +1 -1
  69. package/src/vector-cortex/cache/_diagnostics-fixture.ts +32 -0
  70. package/src/vector-cortex/cache/breaker.ts +124 -0
  71. package/src/vector-cortex/cache/diagnostics-emit.ts +90 -0
  72. package/src/vector-cortex/cache/diagnostics-types.ts +87 -0
  73. package/src/vector-cortex/cache/diagnostics.ts +119 -0
  74. package/src/vector-cortex/migrations/request-hash-v2-ops.ts +15 -9
  75. package/extensions/dashboard-client/dist/assets/VectorCortexTab-jRd_Iztk.js +0 -2
  76. package/extensions/dashboard-client/dist/assets/VectorCortexTab-jRd_Iztk.js.map +0 -1
@@ -0,0 +1,35 @@
1
+ /**
2
+ * api-contracts/vector-cortex-diagnostics.ts — VC7C cache-diagnostics API
3
+ * contract.
4
+ *
5
+ * PREVENT-PI-004: type definitions only, no network code.
6
+ * PREVENT-011: no `any` type.
7
+ *
8
+ * Reader-only GET /api/vector-cortex/cache-diagnostics view (VC7C).
9
+ *
10
+ * COUNTS + CODES ONLY, and this surface is the most dangerous one in the whole
11
+ * VC7 cache phase. A cache MISS DIAGNOSTIC exists to answer "why did this exact
12
+ * request not hit the cache?", and the honest answer is naturally phrased in
13
+ * terms of the very things that must never leave the process: the full request
14
+ * payload that was hashed, its RequestHashV2 digest, the covered source ranges,
15
+ * the span/covered digests, the provider profile digest and the session id that
16
+ * frames the conversation. Every one of those is precisely the evidence a
17
+ * debugger would want inline — which is exactly why an unguarded diagnostic
18
+ * field here leaks the framed conversation itself (SECURITY_PRIVACY). The
19
+ * classification is therefore projected down to a COUNT PER MISS CLASS before it
20
+ * ever reaches this contract: the class names are a closed enumeration fixed by
21
+ * the sprint, so a count discloses nothing about content.
22
+ *
23
+ * This view exposes ONLY:
24
+ * - whether the VC7C flag is enabled and the runtime triad mode it implies;
25
+ * - one count per exclusive miss class (profile, range, dependency, request,
26
+ * generation, unknown) — classification is exclusive, so a single miss
27
+ * increments exactly one of these six counters and they sum to the total;
28
+ * - how many cache serves the breaker blocked (a count, never the blocked key
29
+ * or the entry it would have served);
30
+ * - the breaker's observable state and the last CACHE/M5 rejection code.
31
+ * Request payloads, request digests, covered ranges, span digests, profile
32
+ * digests, and session ids live in the structured event log / conformance
33
+ * corpus, never here.
34
+ */
35
+ export {};
@@ -20,7 +20,10 @@ import { handleVectorCortexCrystals } from "./routes-vector-cortex-crystals.js";
20
20
  // independent of the crystals handler and every file stays well under the
21
21
  // 400-line extension soft limit.
22
22
  import { handleVectorCortexEconomics } from "./routes-vector-cortex-economics.js";
23
- // VC7C cache miss diagnostics + breakers handler will be imported here when VC7C ships.
23
+ // VC7C cache miss diagnostics + breakers gets its own module so the
24
+ // diagnostics seam stays independent of economics and every file stays well
25
+ // under the 400-line extension soft limit.
26
+ import { handleVectorCortexDiagnostics } from "./routes-vector-cortex-diagnostics.js";
24
27
  /**
25
28
  * Dispatch a request through every registered route handler.
26
29
  * Returns true if a handler claimed the request (ended the response).
@@ -118,5 +121,7 @@ export function dispatchRoutes(req, res, ctx) {
118
121
  return true;
119
122
  if (handleVectorCortexEconomics(req, res, ctx))
120
123
  return true;
124
+ if (handleVectorCortexDiagnostics(req, res, ctx))
125
+ return true;
121
126
  return false;
122
127
  }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * dashboard-server/routes-vector-cortex-diagnostics.ts — VC7C cache-diagnostics
3
+ * dashboard route.
4
+ *
5
+ * Reader-only GET /api/vector-cortex/cache-diagnostics returning the cache miss
6
+ * classification aggregate: whether the VC7C flag is enabled, the runtime triad
7
+ * mode, one count per exclusive miss class (profile, range, dependency, request,
8
+ * generation, unknown), how many cache serves the breaker blocked, the breaker
9
+ * state, and the last CACHE/M5 failure code.
10
+ *
11
+ * COUNTS + CODES ONLY. A cache miss diagnostic answers "why did THIS request
12
+ * miss?", so the natural (and forbidden) payload is the request itself: the
13
+ * hashed request bytes, its RequestHashV2 digest, the covered source ranges, the
14
+ * span/covered digests, the provider profile digest, and the session id. This
15
+ * route NEVER exposes any of them — the classification is projected to a count
16
+ * per closed-enumeration class before it reaches the wire (reader-only,
17
+ * SECURITY_PRIVACY). There is no mutation seam: misses are classified and
18
+ * breakers are tripped by the cache serve path, never by a dashboard request,
19
+ * and in particular the breaker can NOT be reset through this route. Non-GET is
20
+ * rejected outright.
21
+ *
22
+ * Split into its own file (rather than grown into routes-vector-cortex-economics.ts)
23
+ * to keep every extensions/ file well under the 400-line soft-as-hard limit.
24
+ *
25
+ * Guardrails: PREVENT-PI-004 (local in-process state only), PREVENT-011 (no
26
+ * `any`), reader-only aggregate (counts + codes only).
27
+ */
28
+ import { VC7C_ENABLED } from "../../src/config.js";
29
+ import { sendJson } from "./routes-vector-cortex-shared.js";
30
+ /**
31
+ * Reader-only GET /api/vector-cortex/cache-diagnostics (VC7C).
32
+ *
33
+ * Per-miss-class counts, breaker state, and CACHE and M5 codes only — a static
34
+ * reader-only aggregate seam with the same shape as the VC7B economics handler.
35
+ */
36
+ export function handleVectorCortexDiagnostics(req, res, _ctx) {
37
+ const url = req.url ?? "";
38
+ const path = url.split("?")[0] ?? url;
39
+ if (path !== "/api/vector-cortex/cache-diagnostics")
40
+ return false;
41
+ if (req.method !== "GET") {
42
+ sendJson(res, 405, { error: "method_not_allowed" });
43
+ return true;
44
+ }
45
+ const enabled = VC7C_ENABLED();
46
+ // Flag-off routes to mode C: with VC7C off the diagnostics/breaker reporter is
47
+ // suppressed, so no cache serve is attested here and the surface reports the
48
+ // all-cache bypass outcome. Reporting A (crystal served) or B (fresh render
49
+ // forced by a breaker) would attest a cache decision this seam is not wired to
50
+ // observe. Mirrors how the VC7A/VC7B OFF views report the mode they take.
51
+ const mode = enabled ? "A" : "C";
52
+ const body = {
53
+ enabled,
54
+ mode,
55
+ profileMisses: 0,
56
+ rangeMisses: 0,
57
+ dependencyMisses: 0,
58
+ requestMisses: 0,
59
+ generationMisses: 0,
60
+ unknownMisses: 0,
61
+ serveBlocked: 0,
62
+ breakerState: "closed",
63
+ lastFailure: null,
64
+ updatedAt: new Date().toISOString(),
65
+ };
66
+ sendJson(res, 200, body);
67
+ return true;
68
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * cache/_diagnostics-fixture.ts — VC7C acceptance-test fixture helpers.
3
+ *
4
+ * Reads conformance fixtures from the v2 `cache-diagnostics/` domain and provides
5
+ * a flag-toggling wrapper for VC7C parity tests. Mirrors `_economics-fixture.ts`.
6
+ */
7
+ import { readFileSync } from "node:fs";
8
+ import { join, dirname } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ const here = dirname(fileURLToPath(import.meta.url));
11
+ const V2 = join(here, "..", "..", "..", "conformance", "vector-cortex", "v2");
12
+ const DIR = join(V2, "cache-diagnostics");
13
+ /** Read + parse one conformance fixture by ID. */
14
+ export function diagnosticsFixture(id) {
15
+ const raw = readFileSync(join(DIR, `${id}.json`), "utf8");
16
+ return JSON.parse(raw);
17
+ }
18
+ /** Run `fn` with MEGACOMPACT_VC7C set to `value`, restoring the prior value after. */
19
+ export function withVc7cFlag(value, fn) {
20
+ const prior = process.env.MEGACOMPACT_VC7C;
21
+ process.env.MEGACOMPACT_VC7C = value;
22
+ try {
23
+ return fn();
24
+ }
25
+ finally {
26
+ if (prior === undefined)
27
+ delete process.env.MEGACOMPACT_VC7C;
28
+ else
29
+ process.env.MEGACOMPACT_VC7C = prior;
30
+ }
31
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * cache/breaker.ts — VC7C cache-serve breaker (composes VC0C's createBreaker).
3
+ *
4
+ * This file does NOT reinvent a state machine. It wraps the TRIAD breaker from
5
+ * `../resilience/breaker-core.js` (VC0C) — the same CLOSED_A / OPEN_B / OPEN_C /
6
+ * PROBE_* / MANUAL_HALT states that protect every other subsystem — and adds the
7
+ * ONE cache-specific decision: whether a classified miss should block a cache
8
+ * SERVE before it can answer from a stale or invalid identity.
9
+ *
10
+ * TRIP KIND CLASSIFICATION (per team-lead correction + TRIAD_RESILIENCE §breaker):
11
+ * The four cache demotion conditions map onto `BreakerTripKind` as follows:
12
+ *
13
+ * CORRECTNESS trips (trip on the FIRST failure, no rolling window):
14
+ * - profile mismatch — the crystal was minted under a different provider
15
+ * profile; serving it would return bytes for the wrong
16
+ * model tier. This is never "recovered from" by waiting.
17
+ * - range mismatch — the crystal's covered digest differs from the
18
+ * request's; the crystal covers different spans and
19
+ * serving it would return another conversation's frozen
20
+ * bytes. (Includes digest failure / collision.)
21
+ * - request mismatch — the request digest differs; the crystal was built
22
+ * for a different request entirely.
23
+ *
24
+ * PERFORMANCE trips (accumulated over a rolling window before opening):
25
+ * - dependency advanced — the dependency high-water moved past the crystal's
26
+ * cached position. The crystal is stale but not WRONG;
27
+ * a single advance is normal churn. Repeated advances
28
+ * across a window indicate the cache is systematically
29
+ * behind the frontier — that is a performance signal.
30
+ * - generation invalidated — the M6 router generation was invalidated. A single
31
+ * invalidation is expected during a router cut; repeated
32
+ * invalidations indicate a systematic generation
33
+ * instability.
34
+ *
35
+ * The distinction matters because a CORRECTNESS trip opens the breaker IMMEDIATELY
36
+ * (first failure), while a PERFORMANCE trip requires BREAKER_PERF_FAILURES within
37
+ * BREAKER_WINDOW_MS. The VC0C breaker-core already implements this via
38
+ * `BreakerTripKind`; we map each miss class to its trip kind and let breaker-core
39
+ * handle the window/threshold logic.
40
+ *
41
+ * "PROBE OUTPUT IS NEVER SERVED" (TRIAD_RESILIENCE line 13): when the breaker is in
42
+ * PROBE_A or PROBE_B, the probe is a TEST serve — its output must not be returned
43
+ * to the caller. `decideCacheServe` enforces this: PROBE_* states always return
44
+ * `block: true` and the fallback mode, never serving from cache.
45
+ *
46
+ * The decision is a CORRECTNESS behavior and is NEVER flag-gated (unlike the
47
+ * reporter seam in `./diagnostics-emit.ts`): flag-off must be byte-identical to
48
+ * the predecessor. `breakerRetryDelay()` from VC0C already implements the
49
+ * deterministic +-10% jitter from the subsystem digest — we use it, don't re-derive.
50
+ *
51
+ * PREVENT-002/011/PI-004 honored.
52
+ */
53
+ import { createBreaker } from "../resilience/breaker-core.js";
54
+ /** The cache subsystem the breaker tracks (one triad state machine). */
55
+ export const CACHE_SUBSYSTEM = "vector-cortex-cache-serve";
56
+ /** Build the cache breaker. Composes VC0C — no parallel state machine here. */
57
+ export function createCacheBreaker(opts) {
58
+ return createBreaker({ now: opts?.now, onEvent: opts?.onEvent });
59
+ }
60
+ /**
61
+ * Map a miss class to its breaker trip kind. Profile, range (digest/collision),
62
+ * and request mismatches are CORRECTNESS trips — they trip on the FIRST failure
63
+ * because serving a crystal with the wrong identity is never safe, no matter how
64
+ * rarely it happens. Dependency advance and generation invalidation are
65
+ * PERFORMANCE trips — a single occurrence is normal churn; only repeated
66
+ * failures within a window indicate a systematic problem.
67
+ */
68
+ export function tripKindForMiss(missClass) {
69
+ switch (missClass) {
70
+ case "profile":
71
+ case "range":
72
+ case "request":
73
+ return "correctness";
74
+ case "dependency":
75
+ case "generation":
76
+ return "performance";
77
+ case "unknown":
78
+ return "performance";
79
+ }
80
+ }
81
+ /**
82
+ * Whether a classified miss must block the cache serve. Pure over the class; the
83
+ * ONLY thing that is allowed to demote an otherwise-healthy cache is a real
84
+ * disagreement between what was cached and what the request now requires.
85
+ * `unknown` is not auto-blocked — the triad's own performance window handles it.
86
+ */
87
+ export function shouldBlockServe(missClass) {
88
+ return missClass !== "unknown";
89
+ }
90
+ /**
91
+ * Decide the triad mode to render under, given a classified miss and the live
92
+ * breaker. A blockable miss forces mode B (fresh render) unless the breaker is
93
+ * already in a deeper open state (C = all-cache bypass) — never contradicting the
94
+ * triad's own resilience verdict. PROBE_* states are NEVER served from cache
95
+ * (TRIAD_RESILIENCE line 13: "probe output is never served").
96
+ */
97
+ export function decideCacheServe(missClass, breaker) {
98
+ const tripKind = tripKindForMiss(missClass);
99
+ if (!shouldBlockServe(missClass)) {
100
+ return { block: false, fallbackMode: breaker.modeFor(CACHE_SUBSYSTEM), tripKind };
101
+ }
102
+ const record = breaker.snapshot(CACHE_SUBSYSTEM);
103
+ if (record.state === "OPEN_C" || record.state === "MANUAL_HALT") {
104
+ return { block: true, fallbackMode: "C", tripKind };
105
+ }
106
+ if (record.state === "PROBE_A" || record.state === "PROBE_B") {
107
+ return { block: true, fallbackMode: record.state === "PROBE_A" ? "C" : "B", tripKind };
108
+ }
109
+ return { block: true, fallbackMode: "B", tripKind };
110
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * cache/diagnostics-emit.ts — VC7C reporter seam (FLAG-GATED).
3
+ *
4
+ * Mirrors `./economics-emit.ts`: a thin `safe()` wrapper around an optional
5
+ * injected `emit`, and the two event names the sprint spec requires verbatim:
6
+ * - `vector_cortex_cache_miss_classified` — a miss was classified.
7
+ * - `vector_cortex_cache_serve_blocked` — a cache serve was blocked by a
8
+ * breaker condition before it could answer from a stale/invalid identity.
9
+ *
10
+ * FLAG SEMANTICS. `classifyMiss` (`./diagnostics.ts`) is PURE and runs REGARDLESS
11
+ * of `MEGACOMPACT_VC7C`. The flag gates ONLY this reporting + dashboard seam:
12
+ * with the flag off the classifier still returns the SAME class, the breaker
13
+ * still blocks the SAME serve — we just do not announce them under the VC7C
14
+ * event namespace, and the dashboard reports `enabled:false` + mode C. That is
15
+ * what makes flag-off byte-identical to the predecessor (VC7B): the arithmetic
16
+ * and the correctness decision are never skipped, only the emission. (NOTE the
17
+ * asymmetry vs VC7B: VC7B gated pure telemetry; here the blocked-serve DECISION
18
+ * is a correctness behavior and is never flag-gated — only its announcement is.)
19
+ *
20
+ * PAYLOAD DISCIPLINE. These events carry the `missClass` and the payload-free
21
+ * `evidence` booleans/counts (see `./diagnostics-types.ts`) — never a session
22
+ * id, never a covered range, never a request or covered digest. The classifier
23
+ * is payload-free by construction, so there is no slot to leak into
24
+ * (SECURITY_PRIVACY — the exact ledger is not diagnostic data).
25
+ *
26
+ * No console, no storage, no network (PREVENT-PI-004 / PREVENT-011). Every line
27
+ * is a structured JSON event with `ts` + `event`.
28
+ */
29
+ import { VC7C_ENABLED } from "../../config/vector-cortex.js";
30
+ /** Run `fn` only when an emit exists; a reporting failure is never fatal. */
31
+ function safe(emit, fn) {
32
+ if (emit === undefined)
33
+ return;
34
+ try {
35
+ fn(emit);
36
+ }
37
+ catch {
38
+ // Non-fatal: a reporting failure must never break the agent loop.
39
+ }
40
+ }
41
+ /** Report a classified miss. Gated only by the reporter/dashboard flag. */
42
+ export function reportCacheMissClassified(emit, diagnostic) {
43
+ if (!VC7C_ENABLED())
44
+ return;
45
+ safe(emit, (e) => e("vector_cortex_cache_miss_classified", {
46
+ ts: undefined,
47
+ event: "vector_cortex_cache_miss_classified",
48
+ schema: diagnostic.schema,
49
+ missClass: diagnostic.missClass,
50
+ evidence: diagnostic.evidence,
51
+ }));
52
+ }
53
+ /** Report a cache serve that was blocked before answering. Gated by the flag. */
54
+ export function reportCacheServeBlocked(emit, blocked) {
55
+ if (!VC7C_ENABLED())
56
+ return;
57
+ safe(emit, (e) => e("vector_cortex_cache_serve_blocked", {
58
+ ts: undefined,
59
+ event: "vector_cortex_cache_serve_blocked",
60
+ missClass: blocked.missClass,
61
+ triadState: blocked.triadState,
62
+ reason: blocked.reason,
63
+ }));
64
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * cache/diagnostics-types.ts — VC7C cache miss-diagnostic contract types.
3
+ *
4
+ * Single source of truth for the miss-classification vocabulary. Kept separate
5
+ * from the arithmetic (`diagnostics.ts`) so the dashboard client, the emit layer
6
+ * and the acceptance test all import ONE definition. No logic here; pure types
7
+ * + the registered ID lists. PREVENT-011 honored (no `any`).
8
+ */
9
+ /** Registered conformance IDs (CACHE-016..030). */
10
+ export const CACHE_DIAGNOSTIC_IDS = Array.from({ length: 15 }, (_v, i) => `CACHE-${String(i + 16).padStart(3, "0")}`);
11
+ /** Named headlines surfaced by the conformance corpus. */
12
+ export const CACHE_DIAGNOSTIC_NAMED_IDS = [
13
+ "CACHE-MISS-001",
14
+ "CACHE-STALE-003",
15
+ ];
@@ -0,0 +1,97 @@
1
+ /**
2
+ * cache/diagnostics.ts — VC7C pure miss classification (flag-independent).
3
+ *
4
+ * The classifier is PURE arithmetic: given a `MissObservation` it returns
5
+ * exactly ONE `MissClass` and the payload-free `MissEvidence`. It is NOT gated by
6
+ * MEGACOMPACT_VC7C — flag-off must be byte-identical to the predecessor, so the
7
+ * SAME class a user sees today, they see tomorrow. The flag gates only the
8
+ * reporter/dashboard seam (see `diagnostics-emit.ts`).
9
+ *
10
+ * Exclusive ranking: profile -> range -> dependency -> request -> generation ->
11
+ * unknown. First match wins; the ranking itself is the contract and is pinned by
12
+ * CACHE-016..030. "Absence is not a mismatch": a cold key (null cached fields)
13
+ * classifies `unknown`, never `profile`. PREVENT-002/011/PI-004 honored.
14
+ */
15
+ /** A cached field is present iff it is non-null. */
16
+ function isPresent(value) {
17
+ return value !== null;
18
+ }
19
+ /**
20
+ * Clamp the dependency delta to a non-negative, safe-integer count. A request
21
+ * BEHIND the cached frontier is not an advance (delta 0); a huge advance
22
+ * saturates rather than losing precision.
23
+ */
24
+ function advanceDelta(request, cached) {
25
+ if (cached === null)
26
+ return { advanced: false, delta: 0 };
27
+ if (request <= cached)
28
+ return { advanced: false, delta: 0 };
29
+ const diff = request - cached;
30
+ if (diff > BigInt(Number.MAX_SAFE_INTEGER)) {
31
+ return { advanced: true, delta: Number.MAX_SAFE_INTEGER };
32
+ }
33
+ return { advanced: true, delta: Number(diff) };
34
+ }
35
+ /** Collect the payload-free evidence from one observation. */
36
+ export function collectEvidence(o) {
37
+ const profileMismatch = isPresent(o.cachedProfileId) &&
38
+ isPresent(o.cachedProfileVersion) &&
39
+ (o.cachedProfileId !== o.requestProfileId ||
40
+ o.cachedProfileVersion !== o.requestProfileVersion);
41
+ const rangeMismatch = (isPresent(o.cachedCoveredDigest) &&
42
+ o.cachedCoveredDigest !== o.requestCoveredDigest) ||
43
+ (isPresent(o.cachedRangeCount) &&
44
+ o.cachedRangeCount !== o.requestedRangeCount);
45
+ const dep = advanceDelta(o.requestDependencyHighWater, o.cachedDependencyHighWater);
46
+ const requestMismatch = isPresent(o.cachedRequestDigest) &&
47
+ o.cachedRequestDigest !== o.requestDigest;
48
+ const absent = !isPresent(o.cachedProfileId) &&
49
+ !isPresent(o.cachedCoveredDigest) &&
50
+ !isPresent(o.cachedRequestDigest) &&
51
+ !isPresent(o.cachedDependencyHighWater);
52
+ return {
53
+ profileMismatch,
54
+ rangeMismatch,
55
+ dependencyAdvanced: dep.advanced,
56
+ requestMismatch,
57
+ generationInvalidated: o.generationInvalidated,
58
+ requestedRangeCount: o.requestedRangeCount,
59
+ // Nothing was cached when the observation is absent, so a stale
60
+ // cachedRangeCount on the observation is not real — report 0.
61
+ cachedRangeCount: o.cachedRangeCount === null || absent ? 0 : o.cachedRangeCount,
62
+ dependencyDelta: dep.delta,
63
+ absent,
64
+ };
65
+ }
66
+ /** The exclusive ranking: first true cause wins. */
67
+ export function classFor(e) {
68
+ if (e.profileMismatch)
69
+ return "profile";
70
+ if (e.rangeMismatch)
71
+ return "range";
72
+ if (e.dependencyAdvanced)
73
+ return "dependency";
74
+ if (e.requestMismatch)
75
+ return "request";
76
+ if (e.generationInvalidated)
77
+ return "generation";
78
+ return "unknown";
79
+ }
80
+ /** Classify one observation: exactly one deterministic class. */
81
+ export function classifyMiss(o) {
82
+ const evidence = collectEvidence(o);
83
+ return {
84
+ schema: "cache-diagnostic-v1",
85
+ missClass: classFor(evidence),
86
+ evidence,
87
+ };
88
+ }
89
+ /**
90
+ * A miss a caller can self-heal on the next turn (re-fetch under a fresh
91
+ * profile / a new generation), so a triad recovery path may treat it as
92
+ * transient. Only profile + generation qualify; the rest are hard mismatches.
93
+ * An `unknown` miss is NEVER transient.
94
+ */
95
+ export function isTransientMiss(d) {
96
+ return d.missClass === "profile" || d.missClass === "generation";
97
+ }
@@ -48,14 +48,19 @@ function generationFromEconomics(economicsVersion) {
48
48
  }
49
49
  export function detectCollision(host) {
50
50
  const byHash = new Map();
51
+ const seenIdentities = new Set();
51
52
  for (const v1 of host.v1Rows()) {
52
53
  const row = deriveRequestHashRow(host, v1);
54
+ const id = identity(v1.profileId, v1.requestDigest);
55
+ // Two distinct v1 rows (different v1 hash) with the same v2 hash collide,
56
+ // even if they share an identity — the v2 table cannot distinguish them.
57
+ if (seenIdentities.has(id))
58
+ return true;
59
+ seenIdentities.add(id);
53
60
  const prior = byHash.get(row.hash);
54
- if (prior !== undefined &&
55
- prior !== identity(v1.profileId, v1.requestDigest)) {
61
+ if (prior !== undefined && prior !== id)
56
62
  return true;
57
- }
58
- byHash.set(row.hash, identity(v1.profileId, v1.requestDigest));
63
+ byHash.set(row.hash, id);
59
64
  }
60
65
  return false;
61
66
  }
@@ -104,6 +109,10 @@ export function m5Verify(host) {
104
109
  const expected = new Set();
105
110
  for (const v1 of v1Rows) {
106
111
  const id = identity(v1.profileId, v1.requestDigest);
112
+ // Dead-generation rows are intentionally skipped by copy; do not flag.
113
+ const fresh = deriveRequestHashRow(host, v1);
114
+ if (isGenerationInvalidated(host, fresh))
115
+ continue;
107
116
  expected.add(id);
108
117
  const n = counts.get(id) ?? 0;
109
118
  if (n === 0)
@@ -129,11 +138,13 @@ export function m5Switch(host) {
129
138
  if (host.activeVersion() !== REQUEST_HASH_LEGACY_VERSION) {
130
139
  return { ok: false, codes: [M5_FAIL.NOT_ON_LEGACY] };
131
140
  }
141
+ // Collision check runs before copy-completeness verify: a structural
142
+ // collision is a switch-blocker regardless of copy state.
143
+ if (detectCollision(host))
144
+ return { ok: false, codes: [M5_FAIL.REQUEST_HASH_COLLISION] };
132
145
  const verify = m5Verify(host);
133
146
  if (!verify.ok)
134
147
  return verify;
135
- if (detectCollision(host))
136
- return { ok: false, codes: [M5_FAIL.REQUEST_HASH_COLLISION] };
137
148
  host.switchToV2();
138
149
  return { ok: true, codes: [] };
139
150
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * cache/_diagnostics-fixture.ts — VC7C acceptance-test fixture helpers.
3
+ *
4
+ * Reads conformance fixtures from the v2 `cache-diagnostics/` domain and provides
5
+ * a flag-toggling wrapper for VC7C parity tests. Mirrors `_economics-fixture.ts`.
6
+ */
7
+ import { readFileSync } from "node:fs";
8
+ import { join, dirname } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ const here = dirname(fileURLToPath(import.meta.url));
11
+ const V2 = join(here, "..", "..", "..", "conformance", "vector-cortex", "v2");
12
+ const DIR = join(V2, "cache-diagnostics");
13
+ /** Read + parse one conformance fixture by ID. */
14
+ export function diagnosticsFixture(id) {
15
+ const raw = readFileSync(join(DIR, `${id}.json`), "utf8");
16
+ return JSON.parse(raw);
17
+ }
18
+ /** Run `fn` with MEGACOMPACT_VC7C set to `value`, restoring the prior value after. */
19
+ export function withVc7cFlag(value, fn) {
20
+ const prior = process.env.MEGACOMPACT_VC7C;
21
+ process.env.MEGACOMPACT_VC7C = value;
22
+ try {
23
+ return fn();
24
+ }
25
+ finally {
26
+ if (prior === undefined)
27
+ delete process.env.MEGACOMPACT_VC7C;
28
+ else
29
+ process.env.MEGACOMPACT_VC7C = prior;
30
+ }
31
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * cache/breaker.ts — VC7C cache-serve breaker (composes VC0C's createBreaker).
3
+ *
4
+ * This file does NOT reinvent a state machine. It wraps the TRIAD breaker from
5
+ * `../resilience/breaker-core.js` (VC0C) — the same CLOSED_A / OPEN_B / OPEN_C /
6
+ * PROBE_* / MANUAL_HALT states that protect every other subsystem — and adds the
7
+ * ONE cache-specific decision: whether a classified miss should block a cache
8
+ * SERVE before it can answer from a stale or invalid identity.
9
+ *
10
+ * TRIP KIND CLASSIFICATION (per team-lead correction + TRIAD_RESILIENCE §breaker):
11
+ * The four cache demotion conditions map onto `BreakerTripKind` as follows:
12
+ *
13
+ * CORRECTNESS trips (trip on the FIRST failure, no rolling window):
14
+ * - profile mismatch — the crystal was minted under a different provider
15
+ * profile; serving it would return bytes for the wrong
16
+ * model tier. This is never "recovered from" by waiting.
17
+ * - range mismatch — the crystal's covered digest differs from the
18
+ * request's; the crystal covers different spans and
19
+ * serving it would return another conversation's frozen
20
+ * bytes. (Includes digest failure / collision.)
21
+ * - request mismatch — the request digest differs; the crystal was built
22
+ * for a different request entirely.
23
+ *
24
+ * PERFORMANCE trips (accumulated over a rolling window before opening):
25
+ * - dependency advanced — the dependency high-water moved past the crystal's
26
+ * cached position. The crystal is stale but not WRONG;
27
+ * a single advance is normal churn. Repeated advances
28
+ * across a window indicate the cache is systematically
29
+ * behind the frontier — that is a performance signal.
30
+ * - generation invalidated — the M6 router generation was invalidated. A single
31
+ * invalidation is expected during a router cut; repeated
32
+ * invalidations indicate a systematic generation
33
+ * instability.
34
+ *
35
+ * The distinction matters because a CORRECTNESS trip opens the breaker IMMEDIATELY
36
+ * (first failure), while a PERFORMANCE trip requires BREAKER_PERF_FAILURES within
37
+ * BREAKER_WINDOW_MS. The VC0C breaker-core already implements this via
38
+ * `BreakerTripKind`; we map each miss class to its trip kind and let breaker-core
39
+ * handle the window/threshold logic.
40
+ *
41
+ * "PROBE OUTPUT IS NEVER SERVED" (TRIAD_RESILIENCE line 13): when the breaker is in
42
+ * PROBE_A or PROBE_B, the probe is a TEST serve — its output must not be returned
43
+ * to the caller. `decideCacheServe` enforces this: PROBE_* states always return
44
+ * `block: true` and the fallback mode, never serving from cache.
45
+ *
46
+ * The decision is a CORRECTNESS behavior and is NEVER flag-gated (unlike the
47
+ * reporter seam in `./diagnostics-emit.ts`): flag-off must be byte-identical to
48
+ * the predecessor. `breakerRetryDelay()` from VC0C already implements the
49
+ * deterministic +-10% jitter from the subsystem digest — we use it, don't re-derive.
50
+ *
51
+ * PREVENT-002/011/PI-004 honored.
52
+ */
53
+ import { createBreaker } from "../resilience/breaker-core.js";
54
+ /** The cache subsystem the breaker tracks (one triad state machine). */
55
+ export const CACHE_SUBSYSTEM = "vector-cortex-cache-serve";
56
+ /** Build the cache breaker. Composes VC0C — no parallel state machine here. */
57
+ export function createCacheBreaker(opts) {
58
+ return createBreaker({ now: opts?.now, onEvent: opts?.onEvent });
59
+ }
60
+ /**
61
+ * Map a miss class to its breaker trip kind. Profile, range (digest/collision),
62
+ * and request mismatches are CORRECTNESS trips — they trip on the FIRST failure
63
+ * because serving a crystal with the wrong identity is never safe, no matter how
64
+ * rarely it happens. Dependency advance and generation invalidation are
65
+ * PERFORMANCE trips — a single occurrence is normal churn; only repeated
66
+ * failures within a window indicate a systematic problem.
67
+ */
68
+ export function tripKindForMiss(missClass) {
69
+ switch (missClass) {
70
+ case "profile":
71
+ case "range":
72
+ case "request":
73
+ return "correctness";
74
+ case "dependency":
75
+ case "generation":
76
+ return "performance";
77
+ case "unknown":
78
+ return "performance";
79
+ }
80
+ }
81
+ /**
82
+ * Whether a classified miss must block the cache serve. Pure over the class; the
83
+ * ONLY thing that is allowed to demote an otherwise-healthy cache is a real
84
+ * disagreement between what was cached and what the request now requires.
85
+ * `unknown` is not auto-blocked — the triad's own performance window handles it.
86
+ */
87
+ export function shouldBlockServe(missClass) {
88
+ return missClass !== "unknown";
89
+ }
90
+ /**
91
+ * Decide the triad mode to render under, given a classified miss and the live
92
+ * breaker. A blockable miss forces mode B (fresh render) unless the breaker is
93
+ * already in a deeper open state (C = all-cache bypass) — never contradicting the
94
+ * triad's own resilience verdict. PROBE_* states are NEVER served from cache
95
+ * (TRIAD_RESILIENCE line 13: "probe output is never served").
96
+ */
97
+ export function decideCacheServe(missClass, breaker) {
98
+ const tripKind = tripKindForMiss(missClass);
99
+ if (!shouldBlockServe(missClass)) {
100
+ return { block: false, fallbackMode: breaker.modeFor(CACHE_SUBSYSTEM), tripKind };
101
+ }
102
+ const record = breaker.snapshot(CACHE_SUBSYSTEM);
103
+ if (record.state === "OPEN_C" || record.state === "MANUAL_HALT") {
104
+ return { block: true, fallbackMode: "C", tripKind };
105
+ }
106
+ if (record.state === "PROBE_A" || record.state === "PROBE_B") {
107
+ return { block: true, fallbackMode: record.state === "PROBE_A" ? "C" : "B", tripKind };
108
+ }
109
+ return { block: true, fallbackMode: "B", tripKind };
110
+ }