pi-mega-compact 0.20.14 → 0.20.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/vector-cortex.js +20 -0
- package/dist/config.js +1 -1
- package/dist/extensions/dashboard-server/route-dispatch.js +1 -0
- package/dist/extensions/dashboard-server/routes-rag-settings-vector-cortex.js +1 -0
- package/dist/src/config/vector-cortex.js +20 -0
- package/dist/src/config.js +1 -1
- package/dist/src/vector-cortex/migrations/request-hash-v2-ops.js +143 -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/migrations/request-hash-v2-ops.js +143 -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-CV8WqMmt.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-VUd_iOtO.js.map → AreaChart-CV8WqMmt.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-BoqeJ5mM.js → BarChart-M-HEBq9l.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-BoqeJ5mM.js.map → BarChart-M-HEBq9l.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-1ON7meDK.js → CacheTab-DJ_yPAhP.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-1ON7meDK.js.map → CacheTab-DJ_yPAhP.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-Dr9eqt1k.js → EventsTab-DNVlKzt6.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-Dr9eqt1k.js.map → EventsTab-DNVlKzt6.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-Ciaigd4i.js → HealthTab-B5yjuInb.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-Ciaigd4i.js.map → HealthTab-B5yjuInb.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DjZRr3Lf.js → MaintenanceTab-DoQedESF.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DjZRr3Lf.js.map → MaintenanceTab-DoQedESF.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-BpZXHA7j.js → MemoryMapTab-CuL5CzdH.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-BpZXHA7j.js.map → MemoryMapTab-CuL5CzdH.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-QemocY5k.js → MetricsTab-CZSTaCpK.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-QemocY5k.js.map → MetricsTab-CZSTaCpK.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-VtC1F-mO.js → OverviewTab-vkZlXRW6.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-VtC1F-mO.js.map → OverviewTab-vkZlXRW6.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-CJD1cBxp.js → ReposTab-DgPD96rF.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-CJD1cBxp.js.map → ReposTab-DgPD96rF.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-Cj0erPR4.js → SessionsTab-XPbWh7XN.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-Cj0erPR4.js.map → SessionsTab-XPbWh7XN.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-AOVROZ4G.js → SetupTab-Dz74XAtR.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-AOVROZ4G.js.map → SetupTab-Dz74XAtR.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-ZliVn8DH.js → TimeSavedCard-FynCpJdx.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-ZliVn8DH.js.map → TimeSavedCard-FynCpJdx.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DsoTWeIh.js → TurnsTab-C7m3GDy0.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DsoTWeIh.js.map → TurnsTab-C7m3GDy0.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-jRd_Iztk.js +2 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-jRd_Iztk.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{WikiTab-CK8fcW1l.js → WikiTab-C6viLa0u.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-CK8fcW1l.js.map → WikiTab-C6viLa0u.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-DdPcvex1.js → button-mBEgbBVU.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-DdPcvex1.js.map → button-mBEgbBVU.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-DXFuZph_.js → card-CalUQAKG.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-DXFuZph_.js.map → card-CalUQAKG.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-BagDVGWn.js → generateCategoricalChart-CMmkuwj5.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-BagDVGWn.js.map → generateCategoricalChart-CMmkuwj5.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-DVdDtRY4.js → index-Dz8GPBOE.js} +3 -3
- package/extensions/dashboard-client/dist/assets/{index-DVdDtRY4.js.map → index-Dz8GPBOE.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{switch-CtdtGyUR.js → switch-8QKO6CTU.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-CtdtGyUR.js.map → switch-8QKO6CTU.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-Cvvwlbye.js → toggle-9eCkWI4q.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-Cvvwlbye.js.map → toggle-9eCkWI4q.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-DvlceHjV.js → useSSE-s6IqOKoD.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-DvlceHjV.js.map → useSSE-s6IqOKoD.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/VectorCortexTab.tsx +23 -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/route-dispatch.ts +1 -0
- package/extensions/dashboard-server/routes-rag-settings-vector-cortex.ts +6 -0
- package/package.json +1 -1
- package/src/config/vector-cortex.ts +21 -0
- package/src/config.ts +1 -0
- package/src/vector-cortex/migrations/request-hash-v2-ops.ts +195 -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
|
@@ -191,6 +191,26 @@ export const VC7A_ENABLED = () => sprintFlag("MEGACOMPACT_VC7A");
|
|
|
191
191
|
* EXCLUDED_SETTINGS), mirroring VC4A..VC7A.
|
|
192
192
|
*/
|
|
193
193
|
export const VC7B_ENABLED = () => sprintFlag("MEGACOMPACT_VC7B");
|
|
194
|
+
/**
|
|
195
|
+
* VC7C — cache miss diagnostics, cache-level breakers and the completed M5
|
|
196
|
+
* request-hash-v2 switch. Default ON. `MEGACOMPACT_VC7C=0` disables and is
|
|
197
|
+
* byte-identical to the predecessor (VC7B): the miss `classify`, the breaker
|
|
198
|
+
* demotion decision and the M5 request-hash-v2 ARITHMETIC STILL RUN (they are
|
|
199
|
+
* PURE — an exclusive ordered comparison over already-computed digests plus a
|
|
200
|
+
* canonical length-prefixed SHA-256, with no clock, storage, or network), so a
|
|
201
|
+
* miss is classified into the SAME single class, a collision / stale generation
|
|
202
|
+
* / digest failure / profile mismatch still demotes BEFORE any cache serve, and
|
|
203
|
+
* the v2 hash of a given request is unchanged with the flag off. A cache is
|
|
204
|
+
* never allowed to serve a stale or colliding entry merely because reporting is
|
|
205
|
+
* off — that would be a correctness change, not a reporting change.
|
|
206
|
+
*
|
|
207
|
+
* The flag gates ONLY the `vector_cortex_cache_miss_classified` /
|
|
208
|
+
* `vector_cortex_cache_serve_blocked` events and the cache-diagnostics dashboard
|
|
209
|
+
* seam, which reports `enabled:false` + mode C when off. This flag MUST also be
|
|
210
|
+
* a dashboard SETTINGS toggle (visible in config UI, never in
|
|
211
|
+
* EXCLUDED_SETTINGS), mirroring VC4A..VC7B.
|
|
212
|
+
*/
|
|
213
|
+
export const VC7C_ENABLED = () => sprintFlag("MEGACOMPACT_VC7C");
|
|
194
214
|
// Breaker state machine constants (TRIAD_RESILIENCE.md §breaker) extracted to
|
|
195
215
|
// vector-cortex-breakers.ts to keep this file under the 300-line soft limit.
|
|
196
216
|
export { BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./vector-cortex-breakers.js";
|
package/dist/config.js
CHANGED
|
@@ -114,4 +114,4 @@ export const NEW_UI = () => ragEnabled("MEGACOMPACT_NEW_UI");
|
|
|
114
114
|
// default ON, `=0`/`_DISABLED` off. Re-exported from src/config/vector-cortex.ts
|
|
115
115
|
// so root consumers share one source of truth.
|
|
116
116
|
// ---------------------------------------------------------------------------
|
|
117
|
-
export { VC0A_ENABLED, VC0B_ENABLED, VC1A_ENABLED, VC0C_ENABLED, VC1B_ENABLED, VC1C_ENABLED, VC2A_ENABLED, VC2B_ENABLED, VC2C_ENABLED, VC3A_ENABLED, VC3B_ENABLED, VC3C_ENABLED, VC4A_ENABLED, VC4B_ENABLED, VC4C_ENABLED, VC5A_ENABLED, VC5B_ENABLED, VC5C_ENABLED, VC6A_ENABLED, VC6B_ENABLED, VC6C_ENABLED, VC7A_ENABLED, VC7B_ENABLED, BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./config/vector-cortex.js";
|
|
117
|
+
export { VC0A_ENABLED, VC0B_ENABLED, VC1A_ENABLED, VC0C_ENABLED, VC1B_ENABLED, VC1C_ENABLED, VC2A_ENABLED, VC2B_ENABLED, VC2C_ENABLED, VC3A_ENABLED, VC3B_ENABLED, VC3C_ENABLED, VC4A_ENABLED, VC4B_ENABLED, VC4C_ENABLED, VC5A_ENABLED, VC5B_ENABLED, VC5C_ENABLED, VC6A_ENABLED, VC6B_ENABLED, VC6C_ENABLED, VC7A_ENABLED, VC7B_ENABLED, VC7C_ENABLED, BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./config/vector-cortex.js";
|
|
@@ -20,6 +20,7 @@ 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
24
|
/**
|
|
24
25
|
* Dispatch a request through every registered route handler.
|
|
25
26
|
* Returns true if a handler claimed the request (ended the response).
|
|
@@ -44,5 +44,6 @@ export const VECTOR_CORTEX_SETTINGS = {
|
|
|
44
44
|
boolDirect("MEGACOMPACT_VC6C", "VC6C Self-Healing Derived State", "Self-healing derived-state controller: detects gaps between each derived subsystem's high-water mark and the durable authority high-water, then rebuilds derived state by copy -> root-digest verification -> atomic pointer switch, so a partially rebuilt subsystem is never made visible. A targeted single-subsystem rebuild is mode A; an ambiguous gap escalates to a full deterministic rebuild (mode B); if both rebuild paths fail the derived state is disabled rather than served stale (mode C). Rebuilds are rate-limited to one per subsystem per 5 minutes with deterministic exponential backoff, so a persistently failing subsystem cannot spin. OFF = byte-identical predecessor (VC6B); no controller runs, so the dashboard reports mode C.", true),
|
|
45
45
|
boolDirect("MEGACOMPACT_VC7A", "VC7A Frozen Range Crystals", "Frozen range crystals: caches a rendered prompt under an IMMUTABLE key built from the source ranges it covers, the digest of those covered bytes, the validated durable dependency high-water, and the renderer + provider profile — and deliberately NOT the global ledger frontier, so an unrelated append leaves the key unchanged instead of invalidating every crystal on every turn. Any covered-byte, dependency, renderer, or profile change invalidates 100%. Ranges are sorted by source start and overlapping ranges are rejected (CRY_RANGE_OVERLAP) rather than merged. The store is content-addressed and write-once: identical bytes re-written are idempotent, but a same-key/different-bytes write returns CRY_KEY_COLLISION and never overwrites, surfacing renderer non-determinism instead of hiding it. A store hit is mode A, a miss/collision forces a fresh deterministic render (mode B), and an unavailable store bypasses the cache entirely (mode C). OFF = byte-identical predecessor (VC6C); the crystal/store arithmetic still runs, only the reporter + dashboard seam is suppressed.", true),
|
|
46
46
|
boolDirect("MEGACOMPACT_VC7B", "VC7B Provider Cache Economics", "Provider cache economics: extends the VC5B provider registry with cache economics (read/write price, TTL, minimum prefix, and a mandatory exclusion fixture id). Answers whether reusing a frozen crystal is actually worth it — a cache WRITE typically costs more than an uncached token, so a cache only pays off once a written prefix is re-read enough times before TTL expiry. Net savings are reported (never clamped at zero, so a losing cache stays visible) and every exclusion MUST name a proving conformance fixture or it is rejected (ECON_EXCLUSION_UNPROVEN). A compiled crystal boundary still reuses the VC7A key unchanged, and a randomized session-level experiment assigns arms by a stable hash so a lost assignment journal re-derives the same arm. OFF = byte-identical predecessor (VC7A); the economics/compiler/experiment arithmetic still runs, only the reporter + dashboard seam is suppressed.", true),
|
|
47
|
+
boolDirect("MEGACOMPACT_VC7C", "VC7C Cache Diagnostics & Breakers", "Cache miss diagnostics + cache-level breakers + the completed M5 request-hash-v2 switch. Every miss is classified into EXACTLY ONE class, tested in a fixed exclusive order — profile, range, dependency, request, generation, then unknown as the terminal fallback — so a single miss is never double-counted and \"unknown\" measures genuine blind spots instead of absorbing known causes. Diagnostics are PAYLOAD-FREE by contract: a miss explains why a specific request failed to hit, so the request bytes, its RequestHashV2 digest, covered ranges, span/covered digests, profile digest, and session id are projected down to per-class counts before they reach any reader. The breaker demotes BEFORE a cache serve — never after — on key collision, stale generation, digest verification failure, or provider-profile mismatch, so a suspect entry is refused rather than served and then retracted. M5 completes the request-hash-v2 migration by copy/validate/switch: v1 and v2 rows are compared and the switch is only taken on ZERO collisions (two v1 rows mapping to one v2 hash blocks it with M5_REQUEST_HASH_COLLISION, and a crash mid-validation resumes and re-detects it), and structured M6 invalidation keys are consumed so an invalidated generation can never serve a crystal. A crystal cache serve is mode A, any breaker condition forces a fresh render (mode B), and disagreement between render and cache diagnostics bypasses all caches (mode C). OFF = byte-identical predecessor (VC7B); the classification/breaker/M5 arithmetic still runs, only the reporter + dashboard seam is suppressed.", true),
|
|
47
48
|
],
|
|
48
49
|
};
|
|
@@ -191,6 +191,26 @@ export const VC7A_ENABLED = () => sprintFlag("MEGACOMPACT_VC7A");
|
|
|
191
191
|
* EXCLUDED_SETTINGS), mirroring VC4A..VC7A.
|
|
192
192
|
*/
|
|
193
193
|
export const VC7B_ENABLED = () => sprintFlag("MEGACOMPACT_VC7B");
|
|
194
|
+
/**
|
|
195
|
+
* VC7C — cache miss diagnostics, cache-level breakers and the completed M5
|
|
196
|
+
* request-hash-v2 switch. Default ON. `MEGACOMPACT_VC7C=0` disables and is
|
|
197
|
+
* byte-identical to the predecessor (VC7B): the miss `classify`, the breaker
|
|
198
|
+
* demotion decision and the M5 request-hash-v2 ARITHMETIC STILL RUN (they are
|
|
199
|
+
* PURE — an exclusive ordered comparison over already-computed digests plus a
|
|
200
|
+
* canonical length-prefixed SHA-256, with no clock, storage, or network), so a
|
|
201
|
+
* miss is classified into the SAME single class, a collision / stale generation
|
|
202
|
+
* / digest failure / profile mismatch still demotes BEFORE any cache serve, and
|
|
203
|
+
* the v2 hash of a given request is unchanged with the flag off. A cache is
|
|
204
|
+
* never allowed to serve a stale or colliding entry merely because reporting is
|
|
205
|
+
* off — that would be a correctness change, not a reporting change.
|
|
206
|
+
*
|
|
207
|
+
* The flag gates ONLY the `vector_cortex_cache_miss_classified` /
|
|
208
|
+
* `vector_cortex_cache_serve_blocked` events and the cache-diagnostics dashboard
|
|
209
|
+
* seam, which reports `enabled:false` + mode C when off. This flag MUST also be
|
|
210
|
+
* a dashboard SETTINGS toggle (visible in config UI, never in
|
|
211
|
+
* EXCLUDED_SETTINGS), mirroring VC4A..VC7B.
|
|
212
|
+
*/
|
|
213
|
+
export const VC7C_ENABLED = () => sprintFlag("MEGACOMPACT_VC7C");
|
|
194
214
|
// Breaker state machine constants (TRIAD_RESILIENCE.md §breaker) extracted to
|
|
195
215
|
// vector-cortex-breakers.ts to keep this file under the 300-line soft limit.
|
|
196
216
|
export { BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./vector-cortex-breakers.js";
|
package/dist/src/config.js
CHANGED
|
@@ -114,4 +114,4 @@ export const NEW_UI = () => ragEnabled("MEGACOMPACT_NEW_UI");
|
|
|
114
114
|
// default ON, `=0`/`_DISABLED` off. Re-exported from src/config/vector-cortex.ts
|
|
115
115
|
// so root consumers share one source of truth.
|
|
116
116
|
// ---------------------------------------------------------------------------
|
|
117
|
-
export { VC0A_ENABLED, VC0B_ENABLED, VC1A_ENABLED, VC0C_ENABLED, VC1B_ENABLED, VC1C_ENABLED, VC2A_ENABLED, VC2B_ENABLED, VC2C_ENABLED, VC3A_ENABLED, VC3B_ENABLED, VC3C_ENABLED, VC4A_ENABLED, VC4B_ENABLED, VC4C_ENABLED, VC5A_ENABLED, VC5B_ENABLED, VC5C_ENABLED, VC6A_ENABLED, VC6B_ENABLED, VC6C_ENABLED, VC7A_ENABLED, VC7B_ENABLED, BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./config/vector-cortex.js";
|
|
117
|
+
export { VC0A_ENABLED, VC0B_ENABLED, VC1A_ENABLED, VC0C_ENABLED, VC1B_ENABLED, VC1C_ENABLED, VC2A_ENABLED, VC2B_ENABLED, VC2C_ENABLED, VC3A_ENABLED, VC3B_ENABLED, VC3C_ENABLED, VC4A_ENABLED, VC4B_ENABLED, VC4C_ENABLED, VC5A_ENABLED, VC5B_ENABLED, VC5C_ENABLED, VC6A_ENABLED, VC6B_ENABLED, VC6C_ENABLED, VC7A_ENABLED, VC7B_ENABLED, VC7C_ENABLED, BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./config/vector-cortex.js";
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* request-hash-v2-ops.ts — M5 migration operational logic (copy/verify/switch).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from request-hash-v2.ts to keep the parent file under the 300-line
|
|
5
|
+
* soft limit (soft-as-hard gate). Pure logic over an injected M5Host;
|
|
6
|
+
* deterministic + testable; no console. PREVENT-002/011/PI-004 honored.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { invalidationKey } from "../topology/query.js";
|
|
10
|
+
import { M5_FAIL, REQUEST_HASH_LEGACY_VERSION, } from "./request-hash-v2-types.js";
|
|
11
|
+
/** Length-prefixed framing so the folded fields cannot alias. */
|
|
12
|
+
function field(value) {
|
|
13
|
+
return `${Buffer.byteLength(value, "utf8")}:${value}`;
|
|
14
|
+
}
|
|
15
|
+
const identity = (profileId, requestDigest) => `${profileId}::${requestDigest}`;
|
|
16
|
+
/**
|
|
17
|
+
* The authoritative v2 hash: SHA-256 over the version tag, profile, request
|
|
18
|
+
* digest, and economics version. Length-prefixed for injectivity, exactly as the
|
|
19
|
+
* crystal key encoder frames its fields.
|
|
20
|
+
*/
|
|
21
|
+
export function deriveRequestHashV2(profileId, requestDigest, economicsVersion) {
|
|
22
|
+
return createHash("sha256")
|
|
23
|
+
.update(field("request-hash-v2") +
|
|
24
|
+
field(profileId) +
|
|
25
|
+
field(requestDigest) +
|
|
26
|
+
field(economicsVersion), "utf8")
|
|
27
|
+
.digest("hex");
|
|
28
|
+
}
|
|
29
|
+
/** Derive the v2 row for one v1 row. */
|
|
30
|
+
export function deriveRequestHashRow(host, v1) {
|
|
31
|
+
const economicsVersion = host.economicsVersionOf(v1.profileId);
|
|
32
|
+
return {
|
|
33
|
+
profileId: v1.profileId,
|
|
34
|
+
// IDENTITY-PRESERVING: carried through verbatim, never re-derived.
|
|
35
|
+
requestDigest: v1.requestDigest,
|
|
36
|
+
economicsVersion,
|
|
37
|
+
hash: deriveRequestHashV2(v1.profileId, v1.requestDigest, economicsVersion),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Economics versions encode the generation they were minted under. We treat the
|
|
42
|
+
* trailing numeric segment as the generation; if it is not numeric the row is
|
|
43
|
+
* considered tied to generation 0 (always live unless explicitly invalidated).
|
|
44
|
+
*/
|
|
45
|
+
function generationFromEconomics(economicsVersion) {
|
|
46
|
+
const match = /(\d+)$/.exec(economicsVersion);
|
|
47
|
+
return match ? BigInt(match[1]) : 0n;
|
|
48
|
+
}
|
|
49
|
+
export function detectCollision(host) {
|
|
50
|
+
const byHash = new Map();
|
|
51
|
+
for (const v1 of host.v1Rows()) {
|
|
52
|
+
const row = deriveRequestHashRow(host, v1);
|
|
53
|
+
const prior = byHash.get(row.hash);
|
|
54
|
+
if (prior !== undefined &&
|
|
55
|
+
prior !== identity(v1.profileId, v1.requestDigest)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
byHash.set(row.hash, identity(v1.profileId, v1.requestDigest));
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
export function isGenerationInvalidated(host, row) {
|
|
63
|
+
const session = host.sessionOf(row.profileId);
|
|
64
|
+
const live = host.liveGenerationOf(session);
|
|
65
|
+
const keyForLive = invalidationKey(session, live);
|
|
66
|
+
const keyForRow = invalidationKey(session, generationFromEconomics(row.economicsVersion));
|
|
67
|
+
return keyForLive !== keyForRow;
|
|
68
|
+
}
|
|
69
|
+
export function m5Copy(host) {
|
|
70
|
+
const existing = new Map(host.existingV2().map((r) => [identity(r.profileId, r.requestDigest), r]));
|
|
71
|
+
const wanted = [];
|
|
72
|
+
for (const v1 of host.v1Rows()) {
|
|
73
|
+
const fresh = deriveRequestHashRow(host, v1);
|
|
74
|
+
if (isGenerationInvalidated(host, fresh))
|
|
75
|
+
continue;
|
|
76
|
+
const stored = existing.get(identity(v1.profileId, v1.requestDigest));
|
|
77
|
+
if (stored !== undefined &&
|
|
78
|
+
stored.hash === fresh.hash &&
|
|
79
|
+
stored.economicsVersion === fresh.economicsVersion) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
wanted.push(fresh);
|
|
83
|
+
}
|
|
84
|
+
if (wanted.length > 0)
|
|
85
|
+
host.putV2(wanted);
|
|
86
|
+
return { written: wanted, all: [...host.existingV2()] };
|
|
87
|
+
}
|
|
88
|
+
function dedupe(codes) {
|
|
89
|
+
const out = [];
|
|
90
|
+
for (const c of codes)
|
|
91
|
+
if (!out.includes(c))
|
|
92
|
+
out.push(c);
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
export function m5Verify(host) {
|
|
96
|
+
const codes = [];
|
|
97
|
+
const stored = host.existingV2();
|
|
98
|
+
const v1Rows = host.v1Rows();
|
|
99
|
+
const counts = new Map();
|
|
100
|
+
for (const r of stored) {
|
|
101
|
+
const id = identity(r.profileId, r.requestDigest);
|
|
102
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
103
|
+
}
|
|
104
|
+
const expected = new Set();
|
|
105
|
+
for (const v1 of v1Rows) {
|
|
106
|
+
const id = identity(v1.profileId, v1.requestDigest);
|
|
107
|
+
expected.add(id);
|
|
108
|
+
const n = counts.get(id) ?? 0;
|
|
109
|
+
if (n === 0)
|
|
110
|
+
codes.push(M5_FAIL.COPY_PARTIAL);
|
|
111
|
+
if (n > 1)
|
|
112
|
+
codes.push(M5_FAIL.COUNT_MISMATCH);
|
|
113
|
+
}
|
|
114
|
+
const v1Digests = new Set(v1Rows.map((r) => identity(r.profileId, r.requestDigest)));
|
|
115
|
+
for (const r of stored) {
|
|
116
|
+
const fresh = deriveRequestHashV2(r.profileId, r.requestDigest, r.economicsVersion);
|
|
117
|
+
if (r.hash !== fresh)
|
|
118
|
+
codes.push(M5_FAIL.DIGEST_MISMATCH);
|
|
119
|
+
const id = identity(r.profileId, r.requestDigest);
|
|
120
|
+
if (!v1Digests.has(id)) {
|
|
121
|
+
codes.push(expected.size > 0 ? M5_FAIL.IDENTITY_DRIFT : M5_FAIL.COUNT_MISMATCH);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (detectCollision(host))
|
|
125
|
+
codes.push(M5_FAIL.REQUEST_HASH_COLLISION);
|
|
126
|
+
return { ok: codes.length === 0, codes: dedupe(codes) };
|
|
127
|
+
}
|
|
128
|
+
export function m5Switch(host) {
|
|
129
|
+
if (host.activeVersion() !== REQUEST_HASH_LEGACY_VERSION) {
|
|
130
|
+
return { ok: false, codes: [M5_FAIL.NOT_ON_LEGACY] };
|
|
131
|
+
}
|
|
132
|
+
const verify = m5Verify(host);
|
|
133
|
+
if (!verify.ok)
|
|
134
|
+
return verify;
|
|
135
|
+
if (detectCollision(host))
|
|
136
|
+
return { ok: false, codes: [M5_FAIL.REQUEST_HASH_COLLISION] };
|
|
137
|
+
host.switchToV2();
|
|
138
|
+
return { ok: true, codes: [] };
|
|
139
|
+
}
|
|
140
|
+
export function migrateRequestHashV2(host) {
|
|
141
|
+
m5Copy(host);
|
|
142
|
+
return m5Switch(host);
|
|
143
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* request-hash-v2-types.ts — M5 request-hash-v2 migration types.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from request-hash-v2.ts to keep the parent file under the 300-line
|
|
5
|
+
* soft limit (soft-as-hard gate). Pure type + constant definitions; no logic.
|
|
6
|
+
* PREVENT-002/011/PI-004 honored.
|
|
7
|
+
*/
|
|
8
|
+
/** The active-version value the v2 pointer is switched to. */
|
|
9
|
+
export const REQUEST_HASH_V2_VERSION = 2;
|
|
10
|
+
/** The legacy (predecessor) active version. */
|
|
11
|
+
export const REQUEST_HASH_LEGACY_VERSION = 1;
|
|
12
|
+
/** M5 failure codes. */
|
|
13
|
+
export const M5_FAIL = {
|
|
14
|
+
COPY_PARTIAL: "M5_COPY_PARTIAL",
|
|
15
|
+
COUNT_MISMATCH: "M5_COUNT_MISMATCH",
|
|
16
|
+
DIGEST_MISMATCH: "M5_DIGEST_MISMATCH",
|
|
17
|
+
IDENTITY_DRIFT: "M5_IDENTITY_DRIFT",
|
|
18
|
+
/** Two distinct v1 rows map to the same v2 hash — blocks the switch. (VC7C) */
|
|
19
|
+
REQUEST_HASH_COLLISION: "M5_REQUEST_HASH_COLLISION",
|
|
20
|
+
/** The active pointer is not on v1; switching would be a no-op or a regression. */
|
|
21
|
+
NOT_ON_LEGACY: "M5_NOT_ON_LEGACY",
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Registered M5 conformance IDs (M5-001..020). The acceptance test reads these
|
|
25
|
+
* rows from the v2 `migrations/` domain and asserts each returns its manifest
|
|
26
|
+
* bytes or exactly its listed failure code. Mirrors M6_IDS / M4_IDS.
|
|
27
|
+
*/
|
|
28
|
+
export const M5_IDS = Array.from({ length: 20 }, (_v, i) => `M5-${String(i + 1).padStart(3, "0")}`);
|
|
29
|
+
/** Named M5 rows surfaced by the conformance corpus (mirrors M6_NAMED_IDS). */
|
|
30
|
+
export const M5_NAMED_IDS = ["M5-COLLIDE-002"];
|
|
@@ -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,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* request-hash-v2-ops.ts — M5 migration operational logic (copy/verify/switch).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from request-hash-v2.ts to keep the parent file under the 300-line
|
|
5
|
+
* soft limit (soft-as-hard gate). Pure logic over an injected M5Host;
|
|
6
|
+
* deterministic + testable; no console. PREVENT-002/011/PI-004 honored.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { invalidationKey } from "../topology/query.js";
|
|
10
|
+
import { M5_FAIL, REQUEST_HASH_LEGACY_VERSION, } from "./request-hash-v2-types.js";
|
|
11
|
+
/** Length-prefixed framing so the folded fields cannot alias. */
|
|
12
|
+
function field(value) {
|
|
13
|
+
return `${Buffer.byteLength(value, "utf8")}:${value}`;
|
|
14
|
+
}
|
|
15
|
+
const identity = (profileId, requestDigest) => `${profileId}::${requestDigest}`;
|
|
16
|
+
/**
|
|
17
|
+
* The authoritative v2 hash: SHA-256 over the version tag, profile, request
|
|
18
|
+
* digest, and economics version. Length-prefixed for injectivity, exactly as the
|
|
19
|
+
* crystal key encoder frames its fields.
|
|
20
|
+
*/
|
|
21
|
+
export function deriveRequestHashV2(profileId, requestDigest, economicsVersion) {
|
|
22
|
+
return createHash("sha256")
|
|
23
|
+
.update(field("request-hash-v2") +
|
|
24
|
+
field(profileId) +
|
|
25
|
+
field(requestDigest) +
|
|
26
|
+
field(economicsVersion), "utf8")
|
|
27
|
+
.digest("hex");
|
|
28
|
+
}
|
|
29
|
+
/** Derive the v2 row for one v1 row. */
|
|
30
|
+
export function deriveRequestHashRow(host, v1) {
|
|
31
|
+
const economicsVersion = host.economicsVersionOf(v1.profileId);
|
|
32
|
+
return {
|
|
33
|
+
profileId: v1.profileId,
|
|
34
|
+
// IDENTITY-PRESERVING: carried through verbatim, never re-derived.
|
|
35
|
+
requestDigest: v1.requestDigest,
|
|
36
|
+
economicsVersion,
|
|
37
|
+
hash: deriveRequestHashV2(v1.profileId, v1.requestDigest, economicsVersion),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Economics versions encode the generation they were minted under. We treat the
|
|
42
|
+
* trailing numeric segment as the generation; if it is not numeric the row is
|
|
43
|
+
* considered tied to generation 0 (always live unless explicitly invalidated).
|
|
44
|
+
*/
|
|
45
|
+
function generationFromEconomics(economicsVersion) {
|
|
46
|
+
const match = /(\d+)$/.exec(economicsVersion);
|
|
47
|
+
return match ? BigInt(match[1]) : 0n;
|
|
48
|
+
}
|
|
49
|
+
export function detectCollision(host) {
|
|
50
|
+
const byHash = new Map();
|
|
51
|
+
for (const v1 of host.v1Rows()) {
|
|
52
|
+
const row = deriveRequestHashRow(host, v1);
|
|
53
|
+
const prior = byHash.get(row.hash);
|
|
54
|
+
if (prior !== undefined &&
|
|
55
|
+
prior !== identity(v1.profileId, v1.requestDigest)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
byHash.set(row.hash, identity(v1.profileId, v1.requestDigest));
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
export function isGenerationInvalidated(host, row) {
|
|
63
|
+
const session = host.sessionOf(row.profileId);
|
|
64
|
+
const live = host.liveGenerationOf(session);
|
|
65
|
+
const keyForLive = invalidationKey(session, live);
|
|
66
|
+
const keyForRow = invalidationKey(session, generationFromEconomics(row.economicsVersion));
|
|
67
|
+
return keyForLive !== keyForRow;
|
|
68
|
+
}
|
|
69
|
+
export function m5Copy(host) {
|
|
70
|
+
const existing = new Map(host.existingV2().map((r) => [identity(r.profileId, r.requestDigest), r]));
|
|
71
|
+
const wanted = [];
|
|
72
|
+
for (const v1 of host.v1Rows()) {
|
|
73
|
+
const fresh = deriveRequestHashRow(host, v1);
|
|
74
|
+
if (isGenerationInvalidated(host, fresh))
|
|
75
|
+
continue;
|
|
76
|
+
const stored = existing.get(identity(v1.profileId, v1.requestDigest));
|
|
77
|
+
if (stored !== undefined &&
|
|
78
|
+
stored.hash === fresh.hash &&
|
|
79
|
+
stored.economicsVersion === fresh.economicsVersion) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
wanted.push(fresh);
|
|
83
|
+
}
|
|
84
|
+
if (wanted.length > 0)
|
|
85
|
+
host.putV2(wanted);
|
|
86
|
+
return { written: wanted, all: [...host.existingV2()] };
|
|
87
|
+
}
|
|
88
|
+
function dedupe(codes) {
|
|
89
|
+
const out = [];
|
|
90
|
+
for (const c of codes)
|
|
91
|
+
if (!out.includes(c))
|
|
92
|
+
out.push(c);
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
export function m5Verify(host) {
|
|
96
|
+
const codes = [];
|
|
97
|
+
const stored = host.existingV2();
|
|
98
|
+
const v1Rows = host.v1Rows();
|
|
99
|
+
const counts = new Map();
|
|
100
|
+
for (const r of stored) {
|
|
101
|
+
const id = identity(r.profileId, r.requestDigest);
|
|
102
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
103
|
+
}
|
|
104
|
+
const expected = new Set();
|
|
105
|
+
for (const v1 of v1Rows) {
|
|
106
|
+
const id = identity(v1.profileId, v1.requestDigest);
|
|
107
|
+
expected.add(id);
|
|
108
|
+
const n = counts.get(id) ?? 0;
|
|
109
|
+
if (n === 0)
|
|
110
|
+
codes.push(M5_FAIL.COPY_PARTIAL);
|
|
111
|
+
if (n > 1)
|
|
112
|
+
codes.push(M5_FAIL.COUNT_MISMATCH);
|
|
113
|
+
}
|
|
114
|
+
const v1Digests = new Set(v1Rows.map((r) => identity(r.profileId, r.requestDigest)));
|
|
115
|
+
for (const r of stored) {
|
|
116
|
+
const fresh = deriveRequestHashV2(r.profileId, r.requestDigest, r.economicsVersion);
|
|
117
|
+
if (r.hash !== fresh)
|
|
118
|
+
codes.push(M5_FAIL.DIGEST_MISMATCH);
|
|
119
|
+
const id = identity(r.profileId, r.requestDigest);
|
|
120
|
+
if (!v1Digests.has(id)) {
|
|
121
|
+
codes.push(expected.size > 0 ? M5_FAIL.IDENTITY_DRIFT : M5_FAIL.COUNT_MISMATCH);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (detectCollision(host))
|
|
125
|
+
codes.push(M5_FAIL.REQUEST_HASH_COLLISION);
|
|
126
|
+
return { ok: codes.length === 0, codes: dedupe(codes) };
|
|
127
|
+
}
|
|
128
|
+
export function m5Switch(host) {
|
|
129
|
+
if (host.activeVersion() !== REQUEST_HASH_LEGACY_VERSION) {
|
|
130
|
+
return { ok: false, codes: [M5_FAIL.NOT_ON_LEGACY] };
|
|
131
|
+
}
|
|
132
|
+
const verify = m5Verify(host);
|
|
133
|
+
if (!verify.ok)
|
|
134
|
+
return verify;
|
|
135
|
+
if (detectCollision(host))
|
|
136
|
+
return { ok: false, codes: [M5_FAIL.REQUEST_HASH_COLLISION] };
|
|
137
|
+
host.switchToV2();
|
|
138
|
+
return { ok: true, codes: [] };
|
|
139
|
+
}
|
|
140
|
+
export function migrateRequestHashV2(host) {
|
|
141
|
+
m5Copy(host);
|
|
142
|
+
return m5Switch(host);
|
|
143
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* request-hash-v2-types.ts — M5 request-hash-v2 migration types.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from request-hash-v2.ts to keep the parent file under the 300-line
|
|
5
|
+
* soft limit (soft-as-hard gate). Pure type + constant definitions; no logic.
|
|
6
|
+
* PREVENT-002/011/PI-004 honored.
|
|
7
|
+
*/
|
|
8
|
+
/** The active-version value the v2 pointer is switched to. */
|
|
9
|
+
export const REQUEST_HASH_V2_VERSION = 2;
|
|
10
|
+
/** The legacy (predecessor) active version. */
|
|
11
|
+
export const REQUEST_HASH_LEGACY_VERSION = 1;
|
|
12
|
+
/** M5 failure codes. */
|
|
13
|
+
export const M5_FAIL = {
|
|
14
|
+
COPY_PARTIAL: "M5_COPY_PARTIAL",
|
|
15
|
+
COUNT_MISMATCH: "M5_COUNT_MISMATCH",
|
|
16
|
+
DIGEST_MISMATCH: "M5_DIGEST_MISMATCH",
|
|
17
|
+
IDENTITY_DRIFT: "M5_IDENTITY_DRIFT",
|
|
18
|
+
/** Two distinct v1 rows map to the same v2 hash — blocks the switch. (VC7C) */
|
|
19
|
+
REQUEST_HASH_COLLISION: "M5_REQUEST_HASH_COLLISION",
|
|
20
|
+
/** The active pointer is not on v1; switching would be a no-op or a regression. */
|
|
21
|
+
NOT_ON_LEGACY: "M5_NOT_ON_LEGACY",
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Registered M5 conformance IDs (M5-001..020). The acceptance test reads these
|
|
25
|
+
* rows from the v2 `migrations/` domain and asserts each returns its manifest
|
|
26
|
+
* bytes or exactly its listed failure code. Mirrors M6_IDS / M4_IDS.
|
|
27
|
+
*/
|
|
28
|
+
export const M5_IDS = Array.from({ length: 20 }, (_v, i) => `M5-${String(i + 1).padStart(3, "0")}`);
|
|
29
|
+
/** Named M5 rows surfaced by the conformance corpus (mirrors M6_NAMED_IDS). */
|
|
30
|
+
export const M5_NAMED_IDS = ["M5-COLLIDE-002"];
|