pi-mega-compact 0.20.14 → 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.
- package/dist/config/vector-cortex.js +20 -0
- package/dist/config.js +1 -1
- package/dist/extensions/dashboard-server/api-contracts/vector-cortex-diagnostics.js +35 -0
- package/dist/extensions/dashboard-server/route-dispatch.js +6 -0
- package/dist/extensions/dashboard-server/routes-rag-settings-vector-cortex.js +1 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex-diagnostics.js +68 -0
- package/dist/src/config/vector-cortex.js +20 -0
- package/dist/src/config.js +1 -1
- package/dist/src/vector-cortex/cache/_diagnostics-fixture.js +31 -0
- package/dist/src/vector-cortex/cache/breaker.js +110 -0
- package/dist/src/vector-cortex/cache/diagnostics-emit.js +64 -0
- package/dist/src/vector-cortex/cache/diagnostics-types.js +15 -0
- package/dist/src/vector-cortex/cache/diagnostics.js +97 -0
- package/dist/src/vector-cortex/migrations/request-hash-v2-ops.js +154 -0
- package/dist/src/vector-cortex/migrations/request-hash-v2-types.js +30 -0
- package/dist/src/vector-cortex/migrations/request-hash-v2.js +42 -147
- package/dist/vector-cortex/cache/_diagnostics-fixture.js +31 -0
- package/dist/vector-cortex/cache/breaker.js +110 -0
- package/dist/vector-cortex/cache/diagnostics-emit.js +64 -0
- package/dist/vector-cortex/cache/diagnostics-types.js +15 -0
- package/dist/vector-cortex/cache/diagnostics.js +97 -0
- package/dist/vector-cortex/migrations/request-hash-v2-ops.js +154 -0
- package/dist/vector-cortex/migrations/request-hash-v2-types.js +30 -0
- package/dist/vector-cortex/migrations/request-hash-v2.js +42 -147
- package/extensions/dashboard-client/dist/assets/{AreaChart-VUd_iOtO.js → AreaChart-B_FKg8xJ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-VUd_iOtO.js.map → AreaChart-B_FKg8xJ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-BoqeJ5mM.js → BarChart-BU5RJ0In.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-BoqeJ5mM.js.map → BarChart-BU5RJ0In.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-1ON7meDK.js → CacheTab-CbX033mt.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-1ON7meDK.js.map → CacheTab-CbX033mt.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-Dr9eqt1k.js → EventsTab-D8Z_4gSB.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-Dr9eqt1k.js.map → EventsTab-D8Z_4gSB.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-Ciaigd4i.js → HealthTab-BUUkoQXo.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-Ciaigd4i.js.map → HealthTab-BUUkoQXo.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DjZRr3Lf.js → MaintenanceTab-DKZQmY1T.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DjZRr3Lf.js.map → MaintenanceTab-DKZQmY1T.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-BpZXHA7j.js → MemoryMapTab-DwMGbe3h.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-BpZXHA7j.js.map → MemoryMapTab-DwMGbe3h.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-QemocY5k.js → MetricsTab-C85lieXW.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-QemocY5k.js.map → MetricsTab-C85lieXW.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-VtC1F-mO.js → OverviewTab-D7VPibBT.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-VtC1F-mO.js.map → OverviewTab-D7VPibBT.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-CJD1cBxp.js → ReposTab-eIHk_mDG.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-CJD1cBxp.js.map → ReposTab-eIHk_mDG.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-Cj0erPR4.js → SessionsTab-EYOHEzc3.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-Cj0erPR4.js.map → SessionsTab-EYOHEzc3.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-AOVROZ4G.js → SetupTab-DXpiXu4i.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-AOVROZ4G.js.map → SetupTab-DXpiXu4i.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-ZliVn8DH.js → TimeSavedCard-DUsUC_sm.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-ZliVn8DH.js.map → TimeSavedCard-DUsUC_sm.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DsoTWeIh.js → TurnsTab-w7qdB0px.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DsoTWeIh.js.map → TurnsTab-w7qdB0px.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-BmgdxV5K.js +2 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-BmgdxV5K.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{WikiTab-CK8fcW1l.js → WikiTab-B4hwpXpw.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-CK8fcW1l.js.map → WikiTab-B4hwpXpw.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-DdPcvex1.js → button-BnbvfQjm.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-DdPcvex1.js.map → button-BnbvfQjm.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-DXFuZph_.js → card-DCyvWfpF.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-DXFuZph_.js.map → card-DCyvWfpF.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-BagDVGWn.js → generateCategoricalChart-b3HeKJLU.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-BagDVGWn.js.map → generateCategoricalChart-b3HeKJLU.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-DVdDtRY4.js → index-DhlcXMlE.js} +3 -3
- package/extensions/dashboard-client/dist/assets/{index-DVdDtRY4.js.map → index-DhlcXMlE.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{switch-CtdtGyUR.js → switch-B74pQOFy.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-CtdtGyUR.js.map → switch-B74pQOFy.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-Cvvwlbye.js → toggle-bXDrMX-A.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-Cvvwlbye.js.map → toggle-bXDrMX-A.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-DvlceHjV.js → useSSE-Yq86SUA0.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-DvlceHjV.js.map → useSSE-Yq86SUA0.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +1 -1
- package/extensions/dashboard-client/src/api/vector-cortex.ts +9 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexDiagnosticsCard.tsx +46 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +25 -121
- package/extensions/dashboard-client/src/tabs/useVectorCortexPoll.ts +123 -0
- package/extensions/dashboard-client/src/types/vector-cortex-vc7.ts +63 -0
- package/extensions/dashboard-client/src/types/vector-cortex.ts +5 -33
- package/extensions/dashboard-server/api-contracts/vector-cortex-diagnostics.ts +76 -0
- package/extensions/dashboard-server/route-dispatch.ts +5 -0
- package/extensions/dashboard-server/routes-rag-settings-vector-cortex.ts +6 -0
- package/extensions/dashboard-server/routes-vector-cortex-diagnostics.ts +77 -0
- package/package.json +1 -1
- package/src/config/vector-cortex.ts +21 -0
- package/src/config.ts +1 -0
- package/src/vector-cortex/cache/_diagnostics-fixture.ts +32 -0
- package/src/vector-cortex/cache/breaker.ts +124 -0
- package/src/vector-cortex/cache/diagnostics-emit.ts +90 -0
- package/src/vector-cortex/cache/diagnostics-types.ts +87 -0
- package/src/vector-cortex/cache/diagnostics.ts +119 -0
- package/src/vector-cortex/migrations/request-hash-v2-ops.ts +201 -0
- package/src/vector-cortex/migrations/request-hash-v2-types.ts +87 -0
- package/src/vector-cortex/migrations/request-hash-v2.ts +65 -213
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-BBa7rp6C.js +0 -2
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-BBa7rp6C.js.map +0 -1
|
@@ -0,0 +1,154 @@
|
|
|
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
|
+
const seenIdentities = new Set();
|
|
52
|
+
for (const v1 of host.v1Rows()) {
|
|
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);
|
|
60
|
+
const prior = byHash.get(row.hash);
|
|
61
|
+
if (prior !== undefined && prior !== id)
|
|
62
|
+
return true;
|
|
63
|
+
byHash.set(row.hash, id);
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
export function isGenerationInvalidated(host, row) {
|
|
68
|
+
const session = host.sessionOf(row.profileId);
|
|
69
|
+
const live = host.liveGenerationOf(session);
|
|
70
|
+
const keyForLive = invalidationKey(session, live);
|
|
71
|
+
const keyForRow = invalidationKey(session, generationFromEconomics(row.economicsVersion));
|
|
72
|
+
return keyForLive !== keyForRow;
|
|
73
|
+
}
|
|
74
|
+
export function m5Copy(host) {
|
|
75
|
+
const existing = new Map(host.existingV2().map((r) => [identity(r.profileId, r.requestDigest), r]));
|
|
76
|
+
const wanted = [];
|
|
77
|
+
for (const v1 of host.v1Rows()) {
|
|
78
|
+
const fresh = deriveRequestHashRow(host, v1);
|
|
79
|
+
if (isGenerationInvalidated(host, fresh))
|
|
80
|
+
continue;
|
|
81
|
+
const stored = existing.get(identity(v1.profileId, v1.requestDigest));
|
|
82
|
+
if (stored !== undefined &&
|
|
83
|
+
stored.hash === fresh.hash &&
|
|
84
|
+
stored.economicsVersion === fresh.economicsVersion) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
wanted.push(fresh);
|
|
88
|
+
}
|
|
89
|
+
if (wanted.length > 0)
|
|
90
|
+
host.putV2(wanted);
|
|
91
|
+
return { written: wanted, all: [...host.existingV2()] };
|
|
92
|
+
}
|
|
93
|
+
function dedupe(codes) {
|
|
94
|
+
const out = [];
|
|
95
|
+
for (const c of codes)
|
|
96
|
+
if (!out.includes(c))
|
|
97
|
+
out.push(c);
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
export function m5Verify(host) {
|
|
101
|
+
const codes = [];
|
|
102
|
+
const stored = host.existingV2();
|
|
103
|
+
const v1Rows = host.v1Rows();
|
|
104
|
+
const counts = new Map();
|
|
105
|
+
for (const r of stored) {
|
|
106
|
+
const id = identity(r.profileId, r.requestDigest);
|
|
107
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
108
|
+
}
|
|
109
|
+
const expected = new Set();
|
|
110
|
+
for (const v1 of v1Rows) {
|
|
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;
|
|
116
|
+
expected.add(id);
|
|
117
|
+
const n = counts.get(id) ?? 0;
|
|
118
|
+
if (n === 0)
|
|
119
|
+
codes.push(M5_FAIL.COPY_PARTIAL);
|
|
120
|
+
if (n > 1)
|
|
121
|
+
codes.push(M5_FAIL.COUNT_MISMATCH);
|
|
122
|
+
}
|
|
123
|
+
const v1Digests = new Set(v1Rows.map((r) => identity(r.profileId, r.requestDigest)));
|
|
124
|
+
for (const r of stored) {
|
|
125
|
+
const fresh = deriveRequestHashV2(r.profileId, r.requestDigest, r.economicsVersion);
|
|
126
|
+
if (r.hash !== fresh)
|
|
127
|
+
codes.push(M5_FAIL.DIGEST_MISMATCH);
|
|
128
|
+
const id = identity(r.profileId, r.requestDigest);
|
|
129
|
+
if (!v1Digests.has(id)) {
|
|
130
|
+
codes.push(expected.size > 0 ? M5_FAIL.IDENTITY_DRIFT : M5_FAIL.COUNT_MISMATCH);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (detectCollision(host))
|
|
134
|
+
codes.push(M5_FAIL.REQUEST_HASH_COLLISION);
|
|
135
|
+
return { ok: codes.length === 0, codes: dedupe(codes) };
|
|
136
|
+
}
|
|
137
|
+
export function m5Switch(host) {
|
|
138
|
+
if (host.activeVersion() !== REQUEST_HASH_LEGACY_VERSION) {
|
|
139
|
+
return { ok: false, codes: [M5_FAIL.NOT_ON_LEGACY] };
|
|
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] };
|
|
145
|
+
const verify = m5Verify(host);
|
|
146
|
+
if (!verify.ok)
|
|
147
|
+
return verify;
|
|
148
|
+
host.switchToV2();
|
|
149
|
+
return { ok: true, codes: [] };
|
|
150
|
+
}
|
|
151
|
+
export function migrateRequestHashV2(host) {
|
|
152
|
+
m5Copy(host);
|
|
153
|
+
return m5Switch(host);
|
|
154
|
+
}
|
|
@@ -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"];
|
|
@@ -1,161 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* vector-cortex/migrations/request-hash-v2.ts — M5 request-hash-v2
|
|
3
|
-
*
|
|
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).
|
|
4
10
|
*
|
|
5
11
|
* M5 versions the canonical request hash: the predecessor hashed the outbound
|
|
6
12
|
* request under the v1 scheme; v2 folds in the provider profile's ECONOMICS
|
|
7
13
|
* version so a pricing/TTL/exclusion change cannot silently reuse a cache
|
|
8
14
|
* identity minted under different economics. Like M4/M6 it follows the
|
|
9
|
-
* copy/validate/switch contract
|
|
15
|
+
* copy/validate/switch contract:
|
|
10
16
|
*
|
|
11
17
|
* - copy: resumable per (profile, request) — an interrupted run resumes
|
|
12
18
|
* without duplicate rows or active-pointer drift.
|
|
13
19
|
* - validate: every v1 row has exactly one v2 row, every v2 digest re-hashes
|
|
14
|
-
* from its own declared fields,
|
|
15
|
-
* the
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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.
|
|
22
31
|
*
|
|
23
|
-
* WHY THE SWITCH
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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.
|
|
31
43
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
/** M5 failure codes. */
|
|
41
|
-
export const M5_FAIL = {
|
|
42
|
-
COPY_PARTIAL: "M5_COPY_PARTIAL",
|
|
43
|
-
COUNT_MISMATCH: "M5_COUNT_MISMATCH",
|
|
44
|
-
DIGEST_MISMATCH: "M5_DIGEST_MISMATCH",
|
|
45
|
-
IDENTITY_DRIFT: "M5_IDENTITY_DRIFT",
|
|
46
|
-
SWITCH_DEFERRED: "M5_SWITCH_DEFERRED",
|
|
47
|
-
};
|
|
48
|
-
/** Length-prefixed framing so the folded fields cannot alias. */
|
|
49
|
-
function field(value) {
|
|
50
|
-
return `${Buffer.byteLength(value, "utf8")}:${value}`;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* The authoritative v2 hash: SHA-256 over the version tag, profile, request
|
|
54
|
-
* digest, and economics version. Length-prefixed for injectivity, exactly as the
|
|
55
|
-
* crystal key encoder frames its fields.
|
|
56
|
-
*/
|
|
57
|
-
export function deriveRequestHashV2(profileId, requestDigest, economicsVersion) {
|
|
58
|
-
return createHash("sha256")
|
|
59
|
-
.update(field("request-hash-v2") + field(profileId) + field(requestDigest) + field(economicsVersion), "utf8")
|
|
60
|
-
.digest("hex");
|
|
61
|
-
}
|
|
62
|
-
/** Derive the v2 row for one v1 row. */
|
|
63
|
-
export function deriveRequestHashRow(host, v1) {
|
|
64
|
-
const economicsVersion = host.economicsVersionOf(v1.profileId);
|
|
65
|
-
return {
|
|
66
|
-
profileId: v1.profileId,
|
|
67
|
-
// IDENTITY-PRESERVING: carried through verbatim, never re-derived.
|
|
68
|
-
requestDigest: v1.requestDigest,
|
|
69
|
-
economicsVersion,
|
|
70
|
-
hash: deriveRequestHashV2(v1.profileId, v1.requestDigest, economicsVersion),
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
const identity = (profileId, requestDigest) => `${profileId}::${requestDigest}`;
|
|
74
|
-
/**
|
|
75
|
-
* Copy every v1 row into a v2 row BESIDE it (resumable, idempotent). Rows already
|
|
76
|
-
* persisted with a matching derivation are left untouched; absent or stale rows
|
|
77
|
-
* are (re)written from the authoritative derivation. The active pointer is never
|
|
78
|
-
* touched — v1 remains authoritative throughout VC7B.
|
|
79
|
-
*/
|
|
80
|
-
export function m5Copy(host) {
|
|
81
|
-
const existing = new Map(host.existingV2().map((r) => [identity(r.profileId, r.requestDigest), r]));
|
|
82
|
-
const wanted = [];
|
|
83
|
-
for (const v1 of host.v1Rows()) {
|
|
84
|
-
const fresh = deriveRequestHashRow(host, v1);
|
|
85
|
-
const stored = existing.get(identity(v1.profileId, v1.requestDigest));
|
|
86
|
-
if (stored !== undefined &&
|
|
87
|
-
stored.hash === fresh.hash &&
|
|
88
|
-
stored.economicsVersion === fresh.economicsVersion) {
|
|
89
|
-
continue; // healthy, already copied
|
|
90
|
-
}
|
|
91
|
-
wanted.push(fresh);
|
|
92
|
-
}
|
|
93
|
-
if (wanted.length > 0)
|
|
94
|
-
host.putV2(wanted);
|
|
95
|
-
return { written: wanted, all: [...host.existingV2()] };
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Validate the copied set: exactly one v2 row per v1 row, every v2 hash
|
|
99
|
-
* re-derives from its own declared fields, no orphan v2 rows, and every v2 row's
|
|
100
|
-
* request digest equals its v1 source's (identity preservation). Never mutates.
|
|
101
|
-
*/
|
|
102
|
-
export function m5Verify(host) {
|
|
103
|
-
const codes = [];
|
|
104
|
-
const stored = host.existingV2();
|
|
105
|
-
const v1Rows = host.v1Rows();
|
|
106
|
-
const counts = new Map();
|
|
107
|
-
for (const r of stored) {
|
|
108
|
-
const id = identity(r.profileId, r.requestDigest);
|
|
109
|
-
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
110
|
-
}
|
|
111
|
-
const expected = new Set();
|
|
112
|
-
for (const v1 of v1Rows) {
|
|
113
|
-
const id = identity(v1.profileId, v1.requestDigest);
|
|
114
|
-
expected.add(id);
|
|
115
|
-
const n = counts.get(id) ?? 0;
|
|
116
|
-
if (n === 0)
|
|
117
|
-
codes.push(M5_FAIL.COPY_PARTIAL);
|
|
118
|
-
if (n > 1)
|
|
119
|
-
codes.push(M5_FAIL.COUNT_MISMATCH);
|
|
120
|
-
}
|
|
121
|
-
// Every v1 request digest that must survive the migration unchanged.
|
|
122
|
-
const v1Digests = new Set(v1Rows.map((r) => identity(r.profileId, r.requestDigest)));
|
|
123
|
-
for (const r of stored) {
|
|
124
|
-
const fresh = deriveRequestHashV2(r.profileId, r.requestDigest, r.economicsVersion);
|
|
125
|
-
if (r.hash !== fresh)
|
|
126
|
-
codes.push(M5_FAIL.DIGEST_MISMATCH);
|
|
127
|
-
const id = identity(r.profileId, r.requestDigest);
|
|
128
|
-
// A v2 row whose identity has no v1 source either drifted or is an orphan.
|
|
129
|
-
if (!v1Digests.has(id)) {
|
|
130
|
-
codes.push(expected.size > 0 ? M5_FAIL.IDENTITY_DRIFT : M5_FAIL.COUNT_MISMATCH);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return { ok: codes.length === 0, codes: dedupe(codes) };
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* The switch — DEFERRED TO VC7C BY DESIGN.
|
|
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.
|
|
137
52
|
*
|
|
138
|
-
*
|
|
139
|
-
* lifecycle is complete and reviewable, and so a caller that tries to switch
|
|
140
|
-
* early gets an explicit `M5_SWITCH_DEFERRED` instead of silence.
|
|
141
|
-
*/
|
|
142
|
-
export function m5Switch(host) {
|
|
143
|
-
void host;
|
|
144
|
-
return { ok: false, codes: [M5_FAIL.SWITCH_DEFERRED] };
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* The VC7B M5 lifecycle: copy -> verify. NO SWITCH. Returns the verify result;
|
|
148
|
-
* the active version pointer is guaranteed to still be
|
|
149
|
-
* `REQUEST_HASH_LEGACY_VERSION` on return.
|
|
53
|
+
* PREVENT-002/011/PI-004 honored.
|
|
150
54
|
*/
|
|
151
|
-
export
|
|
152
|
-
|
|
153
|
-
return m5Verify(host);
|
|
154
|
-
}
|
|
155
|
-
function dedupe(codes) {
|
|
156
|
-
const out = [];
|
|
157
|
-
for (const c of codes)
|
|
158
|
-
if (!out.includes(c))
|
|
159
|
-
out.push(c);
|
|
160
|
-
return out;
|
|
161
|
-
}
|
|
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,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
|
+
];
|