pi-mega-compact 0.20.15 → 0.20.17
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 +14 -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/api-contracts/vector-cortex-outcomes.js +13 -0
- package/dist/extensions/dashboard-server/route-dispatch.js +12 -1
- 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/extensions/dashboard-server/routes-vector-cortex-outcomes.js +79 -0
- package/dist/extensions/mega-dashboard.js +29 -9
- package/dist/src/config/vector-cortex.js +14 -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 +17 -6
- package/dist/src/vector-cortex/outcomes/consent.js +69 -0
- package/dist/src/vector-cortex/outcomes/dataset.js +105 -0
- package/dist/src/vector-cortex/outcomes/emit.js +52 -0
- package/dist/src/vector-cortex/outcomes/ledger.js +77 -0
- package/dist/src/vector-cortex/outcomes/types.js +34 -0
- 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 +17 -6
- package/dist/vector-cortex/outcomes/consent.js +69 -0
- package/dist/vector-cortex/outcomes/dataset.js +105 -0
- package/dist/vector-cortex/outcomes/emit.js +52 -0
- package/dist/vector-cortex/outcomes/ledger.js +77 -0
- package/dist/vector-cortex/outcomes/types.js +34 -0
- package/extensions/dashboard-client/dist/assets/{AreaChart-CV8WqMmt.js → AreaChart-BOGp2sag.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-CV8WqMmt.js.map → AreaChart-BOGp2sag.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-M-HEBq9l.js → BarChart-BHgA-QPL.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-M-HEBq9l.js.map → BarChart-BHgA-QPL.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-DJ_yPAhP.js → CacheTab-6E7lwM74.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-DJ_yPAhP.js.map → CacheTab-6E7lwM74.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-DNVlKzt6.js → EventsTab-BEtNmFHz.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-DNVlKzt6.js.map → EventsTab-BEtNmFHz.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-B5yjuInb.js → HealthTab-DGxZulRH.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-B5yjuInb.js.map → HealthTab-DGxZulRH.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DoQedESF.js → MaintenanceTab-C86jppQE.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DoQedESF.js.map → MaintenanceTab-C86jppQE.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CuL5CzdH.js → MemoryMapTab-J3VVxooZ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CuL5CzdH.js.map → MemoryMapTab-J3VVxooZ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CZSTaCpK.js → MetricsTab-yn9AFumo.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CZSTaCpK.js.map → MetricsTab-yn9AFumo.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-vkZlXRW6.js → OverviewTab-BMarUiq3.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-vkZlXRW6.js.map → OverviewTab-BMarUiq3.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-DgPD96rF.js → ReposTab-DxnzMive.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-DgPD96rF.js.map → ReposTab-DxnzMive.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-XPbWh7XN.js → SessionsTab-DXKpDUuC.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-XPbWh7XN.js.map → SessionsTab-DXKpDUuC.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-Dz74XAtR.js → SetupTab-BctuV-CJ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-Dz74XAtR.js.map → SetupTab-BctuV-CJ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-FynCpJdx.js → TimeSavedCard-D8yOC96S.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-FynCpJdx.js.map → TimeSavedCard-D8yOC96S.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-C7m3GDy0.js → TurnsTab-BBxqGYH-.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-C7m3GDy0.js.map → TurnsTab-BBxqGYH-.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-BTF1AGCJ.js +2 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-BTF1AGCJ.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{WikiTab-C6viLa0u.js → WikiTab-DdJQSagO.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-C6viLa0u.js.map → WikiTab-DdJQSagO.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-mBEgbBVU.js → button-B4Q9h_8Y.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-mBEgbBVU.js.map → button-B4Q9h_8Y.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-CalUQAKG.js → card-WyR31hUV.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-CalUQAKG.js.map → card-WyR31hUV.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-CMmkuwj5.js → generateCategoricalChart-pTio6Bq3.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-CMmkuwj5.js.map → generateCategoricalChart-pTio6Bq3.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-Dz8GPBOE.js → index-BoLUiNsd.js} +3 -3
- package/extensions/dashboard-client/dist/assets/{index-Dz8GPBOE.js.map → index-BoLUiNsd.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{switch-8QKO6CTU.js → switch-5dieXxoo.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-8QKO6CTU.js.map → switch-5dieXxoo.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-9eCkWI4q.js → toggle-BFc-7_LL.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-9eCkWI4q.js.map → toggle-BFc-7_LL.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-s6IqOKoD.js → useSSE-BqmKNBAI.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-s6IqOKoD.js.map → useSSE-BqmKNBAI.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/VectorCortexOutcomesCard.tsx +43 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +4 -0
- package/extensions/dashboard-client/src/tabs/useVectorCortexPoll.ts +5 -0
- package/extensions/dashboard-client/src/types/vector-cortex-vc8.ts +25 -0
- package/extensions/dashboard-client/src/types/vector-cortex.ts +2 -0
- package/extensions/dashboard-server/api-contracts/vector-cortex-diagnostics.ts +76 -0
- package/extensions/dashboard-server/api-contracts/vector-cortex-outcomes.ts +55 -0
- package/extensions/dashboard-server/route-dispatch.ts +10 -1
- package/extensions/dashboard-server/routes-rag-settings-vector-cortex.ts +6 -0
- package/extensions/dashboard-server/routes-vector-cortex-diagnostics.ts +77 -0
- package/extensions/dashboard-server/routes-vector-cortex-outcomes.ts +94 -0
- package/extensions/mega-dashboard.ts +27 -8
- package/package.json +1 -1
- package/src/config/vector-cortex.ts +15 -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 +15 -9
- package/src/vector-cortex/outcomes/consent.ts +88 -0
- package/src/vector-cortex/outcomes/dataset.ts +132 -0
- package/src/vector-cortex/outcomes/emit.ts +70 -0
- package/src/vector-cortex/outcomes/ledger.ts +88 -0
- package/src/vector-cortex/outcomes/types.ts +104 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-jRd_Iztk.js +0 -2
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-jRd_Iztk.js.map +0 -1
|
@@ -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,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* outcomes/consent.ts — VC8A append-only consent grants/revocations.
|
|
3
|
+
*
|
|
4
|
+
* ConsentV1 is append-only: grants and revocations are appended with an
|
|
5
|
+
* effective sequence number. A session has active explicit consent at time T
|
|
6
|
+
* if its most recent record at or before T is a grant.
|
|
7
|
+
*
|
|
8
|
+
* Dataset inclusion requires active explicit consent at export time. This
|
|
9
|
+
* module provides the pure consent-evaluation functions; the dataset module
|
|
10
|
+
* calls `hasActiveConsent` at export time.
|
|
11
|
+
*
|
|
12
|
+
* PREVENT-PI-004: no network. PREVENT-011: no `any` type.
|
|
13
|
+
*/
|
|
14
|
+
import { CONSENT_SCHEMA_V1 } from "./types.js";
|
|
15
|
+
/**
|
|
16
|
+
* Evaluate whether a session has active explicit consent at or before the
|
|
17
|
+
* given effective sequence high-water. True if the most recent record at or
|
|
18
|
+
* before `effectiveHighWater` is a grant.
|
|
19
|
+
*/
|
|
20
|
+
export function hasActiveConsent(records, sessionId, effectiveHighWater) {
|
|
21
|
+
const relevant = records
|
|
22
|
+
.filter((r) => r.sessionId === sessionId && r.effectiveSeq <= effectiveHighWater)
|
|
23
|
+
.sort((a, b) => a.effectiveSeq - b.effectiveSeq);
|
|
24
|
+
if (relevant.length === 0)
|
|
25
|
+
return false;
|
|
26
|
+
return relevant[relevant.length - 1].action === "grant";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Append a consent grant for a session at the next effective sequence.
|
|
30
|
+
* Returns a new ConsentV1 record (append-only — never mutates the input).
|
|
31
|
+
*/
|
|
32
|
+
export function appendGrant(sessionId, effectiveSeq, ts) {
|
|
33
|
+
return {
|
|
34
|
+
schema: CONSENT_SCHEMA_V1,
|
|
35
|
+
consentId: `consent-${sessionId}-${effectiveSeq}`,
|
|
36
|
+
sessionId,
|
|
37
|
+
action: "grant",
|
|
38
|
+
effectiveSeq,
|
|
39
|
+
ts,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Append a consent revocation for a session at the next effective sequence.
|
|
44
|
+
* Returns a new ConsentV1 record (append-only — never mutates the input).
|
|
45
|
+
*/
|
|
46
|
+
export function appendRevoke(sessionId, effectiveSeq, ts) {
|
|
47
|
+
return {
|
|
48
|
+
schema: CONSENT_SCHEMA_V1,
|
|
49
|
+
consentId: `consent-${sessionId}-${effectiveSeq}`,
|
|
50
|
+
sessionId,
|
|
51
|
+
action: "revoke",
|
|
52
|
+
effectiveSeq,
|
|
53
|
+
ts,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get the effective consent high-water for a session — the maximum effectiveSeq
|
|
58
|
+
* across all consent records for that session. Used by the dataset exporter
|
|
59
|
+
* to capture a single consent snapshot at export time.
|
|
60
|
+
*/
|
|
61
|
+
export function consentHighWater(records, sessionId) {
|
|
62
|
+
let max = 0;
|
|
63
|
+
for (const r of records) {
|
|
64
|
+
if (r.sessionId === sessionId && r.effectiveSeq > max) {
|
|
65
|
+
max = r.effectiveSeq;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return max;
|
|
69
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* outcomes/dataset.ts — VC8A dataset manifest builder.
|
|
3
|
+
*
|
|
4
|
+
* Groups outcome rows by (repo, session) so no group crosses
|
|
5
|
+
* train/calibration/held-out split boundaries. Revocations disappear from
|
|
6
|
+
* future manifests because the consent check excludes them.
|
|
7
|
+
*
|
|
8
|
+
* The manifest digest is reproducible: it is computed over the canonical
|
|
9
|
+
* sorted rows, so input order does not affect the digest.
|
|
10
|
+
*
|
|
11
|
+
* The build/digest functions are PURE. The flag gates ONLY the reporter seam,
|
|
12
|
+
* never the arithmetic.
|
|
13
|
+
*
|
|
14
|
+
* PREVENT-PI-004: no network. PREVENT-011: no `any` type.
|
|
15
|
+
*/
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
17
|
+
import { DATASET_MANIFEST_SCHEMA_V1, } from "./types.js";
|
|
18
|
+
import { hasActiveConsent } from "./consent.js";
|
|
19
|
+
/** SHA-256 over the canonical sorted rows. */
|
|
20
|
+
export function manifestDigest(rows) {
|
|
21
|
+
const sorted = [...rows].sort((a, b) => {
|
|
22
|
+
const ka = `${a.repoId}:${a.sessionId}:${a.outcomeId}`;
|
|
23
|
+
const kb = `${b.repoId}:${b.sessionId}:${b.outcomeId}`;
|
|
24
|
+
return ka < kb ? -1 : ka > kb ? 1 : 0;
|
|
25
|
+
});
|
|
26
|
+
const text = sorted
|
|
27
|
+
.map((r) => `${r.outcomeId}|${r.repoId}|${r.sessionId}|${r.split}`)
|
|
28
|
+
.join("\n");
|
|
29
|
+
return createHash("sha256").update(Buffer.from(text, "utf8")).digest("hex");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Split a list of session groups into train/calibration/held-out. Groups are
|
|
33
|
+
* assigned sequentially so no group crosses split boundaries.
|
|
34
|
+
*/
|
|
35
|
+
function assignSplits(groupKeys) {
|
|
36
|
+
const result = new Map();
|
|
37
|
+
const n = groupKeys.length;
|
|
38
|
+
const trainEnd = Math.ceil(n * 0.7);
|
|
39
|
+
const calEnd = Math.ceil(n * 0.85);
|
|
40
|
+
for (let i = 0; i < n; i++) {
|
|
41
|
+
const split = i < trainEnd ? "train" : i < calEnd ? "calibration" : "held-out";
|
|
42
|
+
result.set(groupKeys[i], split);
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build a dataset manifest from outcomes, consent records, and a consent
|
|
48
|
+
* high-water. Only outcomes whose session has active consent at the
|
|
49
|
+
* high-water are included. Revoked sessions are excluded entirely.
|
|
50
|
+
*
|
|
51
|
+
* Throws OUT_CONSENT_MISSING if an outcome's session has no consent at all.
|
|
52
|
+
*/
|
|
53
|
+
export function buildManifest(outcomes, consentRecords, consentHighWaterValue) {
|
|
54
|
+
const groups = new Map();
|
|
55
|
+
const included = [];
|
|
56
|
+
for (const outcome of outcomes) {
|
|
57
|
+
const highWater = consentHighWaterValue;
|
|
58
|
+
if (!hasActiveConsent(consentRecords, outcome.sessionId, highWater)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
included.push(outcome);
|
|
62
|
+
const key = `${outcome.repoId}:${outcome.sessionId}`;
|
|
63
|
+
if (!groups.has(key)) {
|
|
64
|
+
groups.set(key, { repoId: outcome.repoId, sessionId: outcome.sessionId });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const groupKeys = [...groups.keys()].sort();
|
|
68
|
+
const splitMap = assignSplits(groupKeys);
|
|
69
|
+
const rows = included.map((o) => {
|
|
70
|
+
const key = `${o.repoId}:${o.sessionId}`;
|
|
71
|
+
const split = splitMap.get(key) ?? "held-out";
|
|
72
|
+
return {
|
|
73
|
+
outcomeId: o.outcomeId,
|
|
74
|
+
repoId: o.repoId,
|
|
75
|
+
sessionId: o.sessionId,
|
|
76
|
+
split,
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
const trainRows = rows.filter((r) => r.split === "train");
|
|
80
|
+
const calRows = rows.filter((r) => r.split === "calibration");
|
|
81
|
+
const heldRows = rows.filter((r) => r.split === "held-out");
|
|
82
|
+
return {
|
|
83
|
+
schema: DATASET_MANIFEST_SCHEMA_V1,
|
|
84
|
+
manifestId: `manifest-${manifestDigest(rows).slice(0, 16)}`,
|
|
85
|
+
rows,
|
|
86
|
+
splitDigests: {
|
|
87
|
+
"train": manifestDigest(trainRows),
|
|
88
|
+
"calibration": manifestDigest(calRows),
|
|
89
|
+
"held-out": manifestDigest(heldRows),
|
|
90
|
+
},
|
|
91
|
+
createdAt: new Date(0).toISOString(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Check if a manifest has any nonconsented records. Returns false if every
|
|
96
|
+
* row's session has active consent at the given high-water.
|
|
97
|
+
*/
|
|
98
|
+
export function hasNonconsentedRecords(manifest, consentRecords, consentHighWaterValue) {
|
|
99
|
+
for (const row of manifest.rows) {
|
|
100
|
+
if (!hasActiveConsent(consentRecords, row.sessionId, consentHighWaterValue)) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* outcomes/emit.ts — VC8A reporter seam (FLAG-GATED).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the VC7C diagnostics-emit pattern: a thin `safe()` wrapper around an
|
|
5
|
+
* optional injected `emit`, and the two event names the sprint spec requires:
|
|
6
|
+
* - `vector_cortex_outcome_appended` — an outcome was appended to the ledger.
|
|
7
|
+
* - `vector_cortex_dataset_record_excluded` — a row was excluded from a manifest.
|
|
8
|
+
*
|
|
9
|
+
* FLAG SEMANTICS. The ledger/consent/dataset arithmetic STILL RUNS regardless
|
|
10
|
+
* of `MEGACOMPACT_VC8A`. The flag gates ONLY this reporting + dashboard seam:
|
|
11
|
+
* flag-off is byte-identical to the predecessor (VC7C).
|
|
12
|
+
*
|
|
13
|
+
* PAYLOAD DISCIPLINE. These events carry only the outcomeId/sessionId codes
|
|
14
|
+
* and aggregate counts — never prompt bytes, response text, or free-text.
|
|
15
|
+
*
|
|
16
|
+
* PREVENT-PI-004: no network. PREVENT-011: no `any` type.
|
|
17
|
+
*/
|
|
18
|
+
import { VC8A_ENABLED } from "../../config/vector-cortex.js";
|
|
19
|
+
/** Run `fn` only when an emit exists; a reporting failure is never fatal. */
|
|
20
|
+
function safe(emit, fn) {
|
|
21
|
+
if (emit === undefined)
|
|
22
|
+
return;
|
|
23
|
+
try {
|
|
24
|
+
fn(emit);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Non-fatal: a reporting failure must never break the agent loop.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Report an outcome appended to the ledger. Gated by the flag. */
|
|
31
|
+
export function reportOutcomeAppended(emit, outcome) {
|
|
32
|
+
if (!VC8A_ENABLED())
|
|
33
|
+
return;
|
|
34
|
+
safe(emit, (e) => e("vector_cortex_outcome_appended", {
|
|
35
|
+
ts: undefined,
|
|
36
|
+
event: "vector_cortex_outcome_appended",
|
|
37
|
+
outcomeId: outcome.outcomeId,
|
|
38
|
+
sessionId: outcome.sessionId,
|
|
39
|
+
repoId: outcome.repoId,
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
/** Report a row excluded from a dataset manifest. Gated by the flag. */
|
|
43
|
+
export function reportDatasetRecordExcluded(emit, excluded) {
|
|
44
|
+
if (!VC8A_ENABLED())
|
|
45
|
+
return;
|
|
46
|
+
safe(emit, (e) => e("vector_cortex_dataset_record_excluded", {
|
|
47
|
+
ts: undefined,
|
|
48
|
+
event: "vector_cortex_dataset_record_excluded",
|
|
49
|
+
outcomeId: excluded.outcomeId,
|
|
50
|
+
reason: excluded.reason,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* outcomes/ledger.ts — VC8A outcome ledger append + validation.
|
|
3
|
+
*
|
|
4
|
+
* Appends outcome fields for session/repo/assignment/metrics ONLY. Rejects
|
|
5
|
+
* prompt, response, exact bytes, or free-text payload fields as
|
|
6
|
+
* OUT_PAYLOAD_FORBIDDEN. The ledger is append-only — no mutation.
|
|
7
|
+
*
|
|
8
|
+
* The append/validate functions are PURE. The flag gates ONLY the reporter
|
|
9
|
+
* seam (emit), never the validation arithmetic, so flag-off is byte-identical
|
|
10
|
+
* to the predecessor.
|
|
11
|
+
*
|
|
12
|
+
* PREVENT-PI-004: no network. PREVENT-011: no `any` type.
|
|
13
|
+
*/
|
|
14
|
+
import { OUTCOME_SCHEMA_V1, OUT_PAYLOAD_FORBIDDEN, } from "./types.js";
|
|
15
|
+
/** Fields that must never appear in an outcome — they carry payload. */
|
|
16
|
+
const FORBIDDEN_FIELDS = new Set([
|
|
17
|
+
"prompt",
|
|
18
|
+
"response",
|
|
19
|
+
"exactBytes",
|
|
20
|
+
"freeText",
|
|
21
|
+
"content",
|
|
22
|
+
"text",
|
|
23
|
+
"payload",
|
|
24
|
+
"body",
|
|
25
|
+
"message",
|
|
26
|
+
"reply",
|
|
27
|
+
"output",
|
|
28
|
+
"completion",
|
|
29
|
+
"snippet",
|
|
30
|
+
]);
|
|
31
|
+
/** Validate that the input object has no payload-bearing fields. */
|
|
32
|
+
export function validateOutcome(input) {
|
|
33
|
+
for (const key of Object.keys(input)) {
|
|
34
|
+
if (FORBIDDEN_FIELDS.has(key)) {
|
|
35
|
+
throw { code: OUT_PAYLOAD_FORBIDDEN, field: key };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (typeof input["outcomeId"] !== "string" || !input["outcomeId"]) {
|
|
39
|
+
throw { code: OUT_PAYLOAD_FORBIDDEN, field: "outcomeId" };
|
|
40
|
+
}
|
|
41
|
+
if (typeof input["sessionId"] !== "string" || !input["sessionId"]) {
|
|
42
|
+
throw { code: OUT_PAYLOAD_FORBIDDEN, field: "sessionId" };
|
|
43
|
+
}
|
|
44
|
+
if (typeof input["repoId"] !== "string" || !input["repoId"]) {
|
|
45
|
+
throw { code: OUT_PAYLOAD_FORBIDDEN, field: "repoId" };
|
|
46
|
+
}
|
|
47
|
+
if (typeof input["assignment"] !== "string" || !input["assignment"]) {
|
|
48
|
+
throw { code: OUT_PAYLOAD_FORBIDDEN, field: "assignment" };
|
|
49
|
+
}
|
|
50
|
+
const metrics = Array.isArray(input["metrics"]) ? input["metrics"] : [];
|
|
51
|
+
for (const m of metrics) {
|
|
52
|
+
if (typeof m !== "object" || m === null) {
|
|
53
|
+
throw { code: OUT_PAYLOAD_FORBIDDEN, field: "metrics" };
|
|
54
|
+
}
|
|
55
|
+
const row = m;
|
|
56
|
+
if (typeof row["code"] !== "string" || typeof row["value"] !== "number" ||
|
|
57
|
+
typeof row["unit"] !== "string") {
|
|
58
|
+
throw { code: OUT_PAYLOAD_FORBIDDEN, field: "metrics" };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
schema: OUTCOME_SCHEMA_V1,
|
|
63
|
+
outcomeId: input["outcomeId"],
|
|
64
|
+
sessionId: input["sessionId"],
|
|
65
|
+
repoId: input["repoId"],
|
|
66
|
+
assignment: input["assignment"],
|
|
67
|
+
metrics: metrics,
|
|
68
|
+
ts: typeof input["ts"] === "string" ? input["ts"] : new Date(0).toISOString(),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Append an outcome to the ledger (append-only). Returns the validated
|
|
73
|
+
* OutcomeV1 or throws OUT_PAYLOAD_FORBIDDEN.
|
|
74
|
+
*/
|
|
75
|
+
export function appendOutcome(input) {
|
|
76
|
+
return validateOutcome(input);
|
|
77
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* outcomes/types.ts — VC8A consent-bound outcome ledger type definitions.
|
|
3
|
+
*
|
|
4
|
+
* Payload-free metrics only — no prompt bytes, response text, or free-text.
|
|
5
|
+
* OutcomeV1 carries session/repo/assignment/metrics fields ONLY.
|
|
6
|
+
* ConsentV1 is append-only grants/revocations with effective sequence.
|
|
7
|
+
* DatasetManifestV1 groups rows/digests/split (train/calibration/held-out).
|
|
8
|
+
*
|
|
9
|
+
* Conformance IDs OUT-001..OUT-025 are registered here as the single source of
|
|
10
|
+
* truth for the sprint's conformance rows.
|
|
11
|
+
*
|
|
12
|
+
* PREVENT-PI-004: type definitions only, no network code.
|
|
13
|
+
* PREVENT-011: no `any` type.
|
|
14
|
+
*/
|
|
15
|
+
/** Schema version for OutcomeV1. */
|
|
16
|
+
export const OUTCOME_SCHEMA_V1 = "outcome-v1";
|
|
17
|
+
/** Schema version for ConsentV1. */
|
|
18
|
+
export const CONSENT_SCHEMA_V1 = "consent-v1";
|
|
19
|
+
/** Schema version for DatasetManifestV1. */
|
|
20
|
+
export const DATASET_MANIFEST_SCHEMA_V1 = "dataset-manifest-v1";
|
|
21
|
+
/** Failure code when a payload-bearing field is passed to the ledger. */
|
|
22
|
+
export const OUT_PAYLOAD_FORBIDDEN = "OUT_PAYLOAD_FORBIDDEN";
|
|
23
|
+
/** Failure code when consent is missing at export time. */
|
|
24
|
+
export const OUT_CONSENT_MISSING = "OUT_CONSENT_MISSING";
|
|
25
|
+
/** Failure code when a group would cross split boundaries. */
|
|
26
|
+
export const OUT_SPLIT_VIOLATION = "OUT_SPLIT_VIOLATION";
|
|
27
|
+
/** Conformance IDs OUT-001..OUT-025 for the 25 numbered rows. */
|
|
28
|
+
export const OUTCOMES_CONFORMANCE_IDS = Array.from({ length: 25 }, (_, i) => `OUT-${String(i + 1).padStart(3, "0")}`);
|
|
29
|
+
/** Named conformance fixtures for the sprint's headline assertions. */
|
|
30
|
+
export const OUTCOMES_NAMED_FIXTURES = [
|
|
31
|
+
"OUT-CONSENT-001",
|
|
32
|
+
"OUT-REVOKE-002",
|
|
33
|
+
"OUT-SPLIT-003",
|
|
34
|
+
];
|
|
@@ -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
|
+
}
|