pi-mega-compact 0.16.3 → 0.17.1
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 +91 -0
- package/dist/dedup/digest.js +30 -0
- package/dist/dedup/l1-lsh-v2.js +60 -0
- package/dist/dedup/l1-lsh.js +52 -0
- package/dist/dedup/l1-minhash-v2.js +193 -0
- package/dist/dedup/l1-minhash.js +91 -0
- package/dist/dedup/l1-verify.js +54 -0
- package/dist/dedup/mmr.js +45 -0
- package/dist/dedup/normalize.js +39 -0
- package/dist/dedup/raptor/buildHistory.js +164 -0
- package/dist/dedup/raptor/guardrails.js +83 -0
- package/dist/dedup/raptor/incremental.js +304 -0
- package/dist/dedup/raptor/index.js +190 -0
- package/dist/dedup/raptor/kmeans.js +152 -0
- package/dist/dedup/raptor/multilevel.js +184 -0
- package/dist/dedup/raptor/retrieval.js +93 -0
- package/dist/dedup/raptor/summarizer.js +86 -0
- package/dist/dedup/raptor/tree.js +202 -0
- package/dist/dedup/topk.js +60 -0
- package/dist/extensions/dashboard-server/api-contracts/endpoints/registry-ext.js +30 -0
- package/dist/extensions/dashboard-server/api-contracts/endpoints/registry.js +4 -14
- package/dist/extensions/dashboard-server/api-contracts/vector-cortex.js +12 -0
- package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +11 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex.js +292 -0
- package/dist/extensions/dashboard-server/routes.js +1 -0
- package/dist/extensions/dashboard-server/server.js +9 -1
- package/dist/extensions/mega-events/context-handler.js +11 -0
- package/dist/extensions/mega-runtime/vector-cortex-ledger.js +131 -0
- package/dist/extensions/mega-runtime/vector-cortex-safety.js +100 -0
- package/dist/src/config/vector-cortex.js +91 -0
- package/dist/src/config.js +6 -0
- package/dist/src/dedup/l1-lsh-v2.js +60 -0
- package/dist/src/dedup/l1-minhash-v2.js +193 -0
- package/dist/src/dedup/normalize.js +1 -1
- package/dist/src/vector-cortex/conformance/emit.js +46 -0
- package/dist/src/vector-cortex/conformance/manifest.js +235 -0
- package/dist/src/vector-cortex/conformance/runner.js +111 -0
- package/dist/src/vector-cortex/conformance/triadB-reader.js +95 -0
- package/dist/src/vector-cortex/eval/annotations.js +41 -0
- package/dist/src/vector-cortex/eval/metrics.js +86 -0
- package/dist/src/vector-cortex/eval/observer.js +61 -0
- package/dist/src/vector-cortex/eval/persist.js +67 -0
- package/dist/src/vector-cortex/eval/reader.js +29 -0
- package/dist/src/vector-cortex/eval/types.js +33 -0
- package/dist/src/vector-cortex/ledger/adapter.js +55 -0
- package/dist/src/vector-cortex/ledger/compat-journal.js +206 -0
- package/dist/src/vector-cortex/ledger/emit.js +53 -0
- package/dist/src/vector-cortex/ledger/event-codec.js +72 -0
- package/dist/src/vector-cortex/ledger/event-codecB.js +76 -0
- package/dist/src/vector-cortex/ledger/sqlite.js +200 -0
- package/dist/src/vector-cortex/ledger/store.js +184 -0
- package/dist/src/vector-cortex/ledger/types.js +52 -0
- package/dist/src/vector-cortex/ledger/validator.js +117 -0
- package/dist/src/vector-cortex/migrations/effective-cut-v2.js +103 -0
- package/dist/src/vector-cortex/migrations/minhash-v2.js +209 -0
- package/dist/src/vector-cortex/migrations/occurrence-v2.js +139 -0
- package/dist/src/vector-cortex/replay/cut.js +123 -0
- package/dist/src/vector-cortex/replay/emit.js +58 -0
- package/dist/src/vector-cortex/replay/replay.js +260 -0
- package/dist/src/vector-cortex/replay/replayB.js +73 -0
- package/dist/src/vector-cortex/replay/types.js +52 -0
- package/dist/src/vector-cortex/resilience/breaker-core.js +359 -0
- package/dist/src/vector-cortex/resilience/breaker.js +12 -0
- package/dist/src/vector-cortex/resilience/emit.js +56 -0
- package/dist/src/vector-cortex/resilience/spool-core.js +347 -0
- package/dist/src/vector-cortex/resilience/spool.js +12 -0
- package/dist/src/vector-cortex/resilience/types.js +32 -0
- package/dist/vector-cortex/conformance/emit.js +46 -0
- package/dist/vector-cortex/conformance/manifest.js +235 -0
- package/dist/vector-cortex/conformance/runner.js +111 -0
- package/dist/vector-cortex/conformance/triadB-reader.js +95 -0
- package/dist/vector-cortex/eval/annotations.js +41 -0
- package/dist/vector-cortex/eval/metrics.js +86 -0
- package/dist/vector-cortex/eval/observer.js +61 -0
- package/dist/vector-cortex/eval/persist.js +67 -0
- package/dist/vector-cortex/eval/reader.js +29 -0
- package/dist/vector-cortex/eval/types.js +33 -0
- package/dist/vector-cortex/ledger/adapter.js +55 -0
- package/dist/vector-cortex/ledger/compat-journal.js +206 -0
- package/dist/vector-cortex/ledger/emit.js +53 -0
- package/dist/vector-cortex/ledger/event-codec.js +72 -0
- package/dist/vector-cortex/ledger/event-codecB.js +76 -0
- package/dist/vector-cortex/ledger/sqlite.js +200 -0
- package/dist/vector-cortex/ledger/store.js +184 -0
- package/dist/vector-cortex/ledger/types.js +52 -0
- package/dist/vector-cortex/ledger/validator.js +117 -0
- package/dist/vector-cortex/migrations/effective-cut-v2.js +103 -0
- package/dist/vector-cortex/migrations/minhash-v2.js +209 -0
- package/dist/vector-cortex/migrations/occurrence-v2.js +139 -0
- package/dist/vector-cortex/replay/cut.js +123 -0
- package/dist/vector-cortex/replay/emit.js +58 -0
- package/dist/vector-cortex/replay/replay.js +260 -0
- package/dist/vector-cortex/replay/replayB.js +73 -0
- package/dist/vector-cortex/replay/types.js +52 -0
- package/dist/vector-cortex/resilience/breaker-core.js +359 -0
- package/dist/vector-cortex/resilience/breaker.js +12 -0
- package/dist/vector-cortex/resilience/emit.js +56 -0
- package/dist/vector-cortex/resilience/spool-core.js +347 -0
- package/dist/vector-cortex/resilience/spool.js +12 -0
- package/dist/vector-cortex/resilience/types.js +32 -0
- package/extensions/dashboard-client/dist/assets/{AreaChart-Y_X0Bbjd.js → AreaChart-BQXFF7UI.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-Y_X0Bbjd.js.map → AreaChart-BQXFF7UI.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-B9XDb-Ob.js → BarChart-BVtoz9Eg.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-B9XDb-Ob.js.map → BarChart-BVtoz9Eg.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-BUGVb1G1.js → CacheTab-trI2TIGy.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-BUGVb1G1.js.map → CacheTab-trI2TIGy.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-dOqLYTsy.js → EventsTab-CNUbk9__.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-dOqLYTsy.js.map → EventsTab-CNUbk9__.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-c2vezKg1.js → HealthTab-K-bd_CM-.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-c2vezKg1.js.map → HealthTab-K-bd_CM-.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-bQYHIcdx.js → MaintenanceTab-CoEioKzx.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-bQYHIcdx.js.map → MaintenanceTab-CoEioKzx.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-2L8pH6y2.js → MemoryMapTab-CJo24ulv.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-2L8pH6y2.js.map → MemoryMapTab-CJo24ulv.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CzUx1jpo.js → MetricsTab-DCNlQWWP.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CzUx1jpo.js.map → MetricsTab-DCNlQWWP.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-eCNoaclv.js → OverviewTab-CQQlyIQ6.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-eCNoaclv.js.map → OverviewTab-CQQlyIQ6.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-tb0IW3NF.js → ReposTab-Cqm82_yn.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-tb0IW3NF.js.map → ReposTab-Cqm82_yn.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-C-IoLw5h.js → SessionsTab-A8xTAdET.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-C-IoLw5h.js.map → SessionsTab-A8xTAdET.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-BvmFi8gf.js → SetupTab-CqaSgaZl.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-BvmFi8gf.js.map → SetupTab-CqaSgaZl.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DBH-cOlA.js → TimeSavedCard-C_f3q8GR.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DBH-cOlA.js.map → TimeSavedCard-C_f3q8GR.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DBMgmvnX.js → TurnsTab-BEsafACo.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DBMgmvnX.js.map → TurnsTab-BEsafACo.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-DD1CMK80.js +2 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-DD1CMK80.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{WikiTab-1LcQrXk3.js → WikiTab-Bf01M2_Z.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-1LcQrXk3.js.map → WikiTab-Bf01M2_Z.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-DtEsZTUE.js → button-BEJPTk_E.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-DtEsZTUE.js.map → button-BEJPTk_E.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-DcfTmw-4.js → card-DK1V382s.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-DcfTmw-4.js.map → card-DK1V382s.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-BsIxUDyw.js → generateCategoricalChart-CRtaPphL.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-BsIxUDyw.js.map → generateCategoricalChart-CRtaPphL.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-B93cewiX.js → index-CB6BslE4.js} +34 -29
- package/extensions/dashboard-client/dist/assets/index-CB6BslE4.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/index-CfvziyBi.css +1 -0
- package/extensions/dashboard-client/dist/assets/{switch-Bowd7jCD.js → switch-C4FV6B9k.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-Bowd7jCD.js.map → switch-C4FV6B9k.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-Bo7kJZ4u.js → toggle-GBmeENMd.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-Bo7kJZ4u.js.map → toggle-GBmeENMd.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-zNhb0c3u.js → useSSE-BOKZGSn1.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-zNhb0c3u.js.map → useSSE-BOKZGSn1.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +2 -2
- package/extensions/dashboard-client/src/App.tsx +2 -0
- package/extensions/dashboard-client/src/api/vector-cortex.ts +54 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +272 -0
- package/extensions/dashboard-client/src/tabs/registry.ts +4 -1
- package/extensions/dashboard-client/src/types/vector-cortex.ts +84 -0
- package/extensions/dashboard-client/tsconfig.json +1 -1
- package/extensions/dashboard-server/api-contracts/endpoints/registry-ext.ts +67 -0
- package/extensions/dashboard-server/api-contracts/endpoints/registry.ts +4 -21
- package/extensions/dashboard-server/api-contracts/index.ts +3 -0
- package/extensions/dashboard-server/api-contracts/vector-cortex.ts +136 -0
- package/extensions/dashboard-server/routes-rag-settings-helpers.ts +41 -0
- package/extensions/dashboard-server/routes-vector-cortex.ts +332 -0
- package/extensions/dashboard-server/routes.ts +6 -0
- package/extensions/dashboard-server/server.ts +8 -0
- package/extensions/mega-events/context-handler.ts +16 -0
- package/extensions/mega-runtime/vector-cortex-ledger.ts +165 -0
- package/extensions/mega-runtime/vector-cortex-safety.ts +181 -0
- package/package.json +2 -1
- package/src/config/vector-cortex.ts +98 -0
- package/src/config.ts +28 -0
- package/src/dedup/l1-lsh-v2.ts +78 -0
- package/src/dedup/l1-minhash-v2.ts +221 -0
- package/src/dedup/normalize.ts +1 -1
- package/src/store/sqlite/game-scores.ts +2 -1
- package/src/vector-cortex/conformance/emit.ts +56 -0
- package/src/vector-cortex/conformance/manifest.ts +298 -0
- package/src/vector-cortex/conformance/runner.ts +165 -0
- package/src/vector-cortex/conformance/triadB-reader.ts +110 -0
- package/src/vector-cortex/eval/annotations.ts +70 -0
- package/src/vector-cortex/eval/metrics.ts +113 -0
- package/src/vector-cortex/eval/observer.ts +86 -0
- package/src/vector-cortex/eval/persist.ts +73 -0
- package/src/vector-cortex/eval/reader.ts +45 -0
- package/src/vector-cortex/eval/types.ts +89 -0
- package/src/vector-cortex/ledger/adapter.ts +71 -0
- package/src/vector-cortex/ledger/compat-journal.ts +234 -0
- package/src/vector-cortex/ledger/emit.ts +86 -0
- package/src/vector-cortex/ledger/event-codec.ts +79 -0
- package/src/vector-cortex/ledger/event-codecB.ts +87 -0
- package/src/vector-cortex/ledger/sqlite.ts +265 -0
- package/src/vector-cortex/ledger/store.ts +380 -0
- package/src/vector-cortex/ledger/types.ts +141 -0
- package/src/vector-cortex/ledger/validator.ts +124 -0
- package/src/vector-cortex/migrations/effective-cut-v2.ts +149 -0
- package/src/vector-cortex/migrations/minhash-v2.ts +263 -0
- package/src/vector-cortex/migrations/occurrence-v2.ts +192 -0
- package/src/vector-cortex/replay/cut.ts +169 -0
- package/src/vector-cortex/replay/emit.ts +89 -0
- package/src/vector-cortex/replay/replay.ts +308 -0
- package/src/vector-cortex/replay/replayB.ts +97 -0
- package/src/vector-cortex/replay/types.ts +145 -0
- package/src/vector-cortex/resilience/breaker-core.ts +444 -0
- package/src/vector-cortex/resilience/breaker.ts +22 -0
- package/src/vector-cortex/resilience/emit.ts +99 -0
- package/src/vector-cortex/resilience/spool-core.ts +411 -0
- package/src/vector-cortex/resilience/spool.ts +22 -0
- package/src/vector-cortex/resilience/types.ts +158 -0
- package/extensions/dashboard-client/dist/assets/index-B93cewiX.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/index-C_tCbOI0.css +0 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/ledger/validator.ts — EventV2 canonical validation (VC1A).
|
|
3
|
+
*
|
|
4
|
+
* Canonical ordering is `(sessionId, seq, eventId bytewise UTF-8)`: the sort is
|
|
5
|
+
* by the eventId's UTF-8 BYTES (unsigned) — NOT by JS string code-unit order —
|
|
6
|
+
* so multi-byte eventIds sort bytewise. Deterministic failure detection with a
|
|
7
|
+
* fixed priority order:
|
|
8
|
+
* 1. EVT_DIGEST_MISMATCH — sha256(originalBytes) !== bytesDigest
|
|
9
|
+
* 2. EVT_UTF8_TAG_INVALID — stored `utf8` discriminant contradicting a strict
|
|
10
|
+
* fatal re-classification of originalBytes
|
|
11
|
+
* 3. EVT_DUPLICATE_ID — duplicate (sessionId, seq, eventId) occurrence
|
|
12
|
+
*
|
|
13
|
+
* The unique failure injection requirement: flip ONE stored byte while retaining
|
|
14
|
+
* the SHA-256 signature — the digest recomputation diverges and the validator
|
|
15
|
+
* returns EVT_DIGEST_MISMATCH (never a lossy replacement text, which is not
|
|
16
|
+
* produced anywhere in this module).
|
|
17
|
+
*
|
|
18
|
+
* Pure/deterministic — no console, no network, no side effects (PREVENT-PI-004).
|
|
19
|
+
*/
|
|
20
|
+
import nodeCrypto from "node:crypto";
|
|
21
|
+
import { classifyUtf8 } from "./event-codec.js";
|
|
22
|
+
/** Mode-A digest recompute (validator uses the codec's canonical digest rule). */
|
|
23
|
+
function digestOf(bytes) {
|
|
24
|
+
const hex = nodeCrypto.createHash("sha256").update(bytes).digest("hex");
|
|
25
|
+
return `sha256:${hex}`;
|
|
26
|
+
}
|
|
27
|
+
/** Bytewise UTF-8 comparator for eventId tiebreak (unsigned bytes, not code units). */
|
|
28
|
+
function compareEventIdBytes(a, b) {
|
|
29
|
+
const ab = Buffer.from(a, "utf8");
|
|
30
|
+
const bb = Buffer.from(b, "utf8");
|
|
31
|
+
const n = Math.min(ab.length, bb.length);
|
|
32
|
+
for (let i = 0; i < n; i++) {
|
|
33
|
+
if (ab[i] !== bb[i])
|
|
34
|
+
return ab[i] < bb[i] ? -1 : 1;
|
|
35
|
+
}
|
|
36
|
+
if (ab.length !== bb.length)
|
|
37
|
+
return ab.length < bb.length ? -1 : 1;
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
/** Comparator: `(sessionId, seq, eventId bytewise UTF-8)`. */
|
|
41
|
+
export function compareEvents(a, b) {
|
|
42
|
+
if (a.sessionId !== b.sessionId)
|
|
43
|
+
return a.sessionId < b.sessionId ? -1 : 1;
|
|
44
|
+
if (a.seq !== b.seq)
|
|
45
|
+
return a.seq < b.seq ? -1 : 1;
|
|
46
|
+
return compareEventIdBytes(a.eventId, b.eventId);
|
|
47
|
+
}
|
|
48
|
+
/** Ascending (sessionId, seq, eventId-bytes) sort. Returns a new array. */
|
|
49
|
+
export function sortEvents(events) {
|
|
50
|
+
return [...events].sort(compareEvents);
|
|
51
|
+
}
|
|
52
|
+
/** Recompute the digest and compare to the stored one (authority corruption). */
|
|
53
|
+
function digestMismatch(event) {
|
|
54
|
+
return digestOf(event.originalBytes) !== event.bytesDigest;
|
|
55
|
+
}
|
|
56
|
+
/** The stored utf8 discriminant must match a strict fatal re-classification. */
|
|
57
|
+
function utf8TagMismatch(event) {
|
|
58
|
+
const cls = classifyUtf8(event.originalBytes);
|
|
59
|
+
const stored = event.utf8;
|
|
60
|
+
if (stored.valid !== cls.valid)
|
|
61
|
+
return true;
|
|
62
|
+
if (!stored.valid) {
|
|
63
|
+
// Both invalid: never compare decoded text; only the (valid, base64) shape.
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (!cls.valid)
|
|
67
|
+
return true; // unreachable given `stored.valid === cls.valid`
|
|
68
|
+
// Both valid: compare the strict-decoded TEXT (no NFC — the tag stores raw text).
|
|
69
|
+
return stored.text !== cls.text;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Validate a batch of events. Returns `{ok:true, ordered}` when every event
|
|
73
|
+
* passes digest + UTF-8-tag consistency and no duplicate occurrence exists;
|
|
74
|
+
* otherwise `{ok:false, codes}` with the failure codes in fixed priority order,
|
|
75
|
+
* deduplicated, and an explicit issue list for diagnostics.
|
|
76
|
+
*/
|
|
77
|
+
export function validateEvents(events) {
|
|
78
|
+
const issues = [];
|
|
79
|
+
// Fixed priority order: DIGEST_MISMATCH, UTF8_TAG_INVALID, DUPLICATE_ID.
|
|
80
|
+
for (const e of events) {
|
|
81
|
+
if (digestMismatch(e)) {
|
|
82
|
+
issues.push({ code: "EVT_DIGEST_MISMATCH", sessionId: e.sessionId, seq: e.seq, eventId: e.eventId });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const e of events) {
|
|
86
|
+
if (utf8TagMismatch(e)) {
|
|
87
|
+
issues.push({ code: "EVT_UTF8_TAG_INVALID", sessionId: e.sessionId, seq: e.seq, eventId: e.eventId });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Unambiguous (sessionId, seq, eventId) occurrence key (seq stringified — a
|
|
91
|
+
// bigint cannot be JSON-serialized directly).
|
|
92
|
+
const key = (e) => JSON.stringify([e.sessionId, e.seq.toString(), e.eventId]);
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
for (const e of sortEvents(events)) {
|
|
95
|
+
const k = key(e);
|
|
96
|
+
if (seen.has(k)) {
|
|
97
|
+
issues.push({ code: "EVT_DUPLICATE_ID", sessionId: e.sessionId, seq: e.seq, eventId: e.eventId });
|
|
98
|
+
}
|
|
99
|
+
seen.add(k);
|
|
100
|
+
}
|
|
101
|
+
if (issues.length === 0) {
|
|
102
|
+
return { ok: true, ordered: sortEvents(events) };
|
|
103
|
+
}
|
|
104
|
+
// Surface the per-occurrence issues (real locators) alongside the deduped
|
|
105
|
+
// codes so the adapter/observability feed can name WHICH event failed.
|
|
106
|
+
return { ok: false, codes: dedupe(issues.map((i) => i.code)), issues };
|
|
107
|
+
}
|
|
108
|
+
function dedupe(codes) {
|
|
109
|
+
const out = [];
|
|
110
|
+
for (const c of codes) {
|
|
111
|
+
if (!out.includes(c))
|
|
112
|
+
out.push(c);
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
/** Export the canonical ordering comparator for consumers (replay etc.). */
|
|
117
|
+
export { compareEventIdBytes };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/migrations/effective-cut-v2.ts — M3 copy/validate/switch
|
|
3
|
+
* migration (VC0B).
|
|
4
|
+
*
|
|
5
|
+
* Freezes the v2 effective replay cut: `min(boundarySafeSeq, committedSeq,
|
|
6
|
+
* capturedHighWater)` with pair retreat and anchor floor. The migration copies
|
|
7
|
+
* the candidate into the staged slot, validates the STAGED pointer against the
|
|
8
|
+
* minima and pair/anchor invariants (plus copy-match), and ONLY THEN atomically
|
|
9
|
+
* switches the active pointer. The three phases are exposed separately so a
|
|
10
|
+
* crash between validate and switch retains the OLD pointer and a restart
|
|
11
|
+
* resumes idempotently (the unique failure-injection contract).
|
|
12
|
+
*
|
|
13
|
+
* This is an internal developer seam: no dashboard/API change is necessary. It
|
|
14
|
+
* operates against the EXISTING host state via an injected M3Host interface —
|
|
15
|
+
* the future v2 compat-journal is owned by VC1B and is not introduced here.
|
|
16
|
+
*/
|
|
17
|
+
import { computeEffectiveCutV2 } from "../replay/cut.js";
|
|
18
|
+
/** Migration failure codes (registered in M3-001..010 as conformance rows). */
|
|
19
|
+
export const M3_FAIL = {
|
|
20
|
+
MISSING_HOST: "M3_HOST_MISSING",
|
|
21
|
+
INVALID_MINIMA: "M3_MINIMA_VIOLATED",
|
|
22
|
+
PAIR_SPLIT: "M3_PAIR_SPLIT",
|
|
23
|
+
ANCHOR_CROSSED: "M3_ANCHOR_CROSSED",
|
|
24
|
+
COPY_MISMATCH: "M3_COPY_MISMATCH",
|
|
25
|
+
};
|
|
26
|
+
/** Pure effective-cut computation (min-of-three + retreat + floor). */
|
|
27
|
+
export function m3Compute(input) {
|
|
28
|
+
return computeEffectiveCutV2(input).cut.effectiveSeq;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Phase 1 — copy: compute the candidate effective cut and stage it via the host
|
|
32
|
+
* (without activating). Returns the staged seq.
|
|
33
|
+
*/
|
|
34
|
+
export function m3Copy(host, input) {
|
|
35
|
+
const effective = m3Compute(input);
|
|
36
|
+
host.writeStaged(effective);
|
|
37
|
+
return effective;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Phase 2 — validate: the host's STAGED pointer must equal the freshly computed
|
|
41
|
+
* effective cut (copy-match), respect the minima, not split a pair, and never
|
|
42
|
+
* fall below the anchor floor. Failure codes:
|
|
43
|
+
* M3_HOST_MISSING — no staged pointer at all
|
|
44
|
+
* M3_COPY_MISMATCH — staged != freshly computed effective
|
|
45
|
+
* M3_MINIMA_VIOLATED — effective exceeds a source minimum (or requested)
|
|
46
|
+
* M3_PAIR_SPLIT — effective splits a tool call/result pair
|
|
47
|
+
* M3_ANCHOR_CROSSED — effective is below the anchor floor
|
|
48
|
+
*/
|
|
49
|
+
export function m3Validate(host, input) {
|
|
50
|
+
const codes = [];
|
|
51
|
+
const { cut, retreats } = computeEffectiveCutV2(input);
|
|
52
|
+
const expected = cut.effectiveSeq;
|
|
53
|
+
const staged = host.stagedPointer();
|
|
54
|
+
if (staged === null) {
|
|
55
|
+
codes.push(M3_FAIL.MISSING_HOST);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
if (staged !== expected)
|
|
59
|
+
codes.push(M3_FAIL.COPY_MISMATCH);
|
|
60
|
+
if (staged > input.boundarySafeSeq ||
|
|
61
|
+
staged > host.committedSeq() ||
|
|
62
|
+
staged > input.capturedHighWater ||
|
|
63
|
+
staged > input.requestedSeq) {
|
|
64
|
+
codes.push(M3_FAIL.INVALID_MINIMA);
|
|
65
|
+
}
|
|
66
|
+
for (const p of input.pairs) {
|
|
67
|
+
if (p.callSeq <= staged && staged < p.resultSeq) {
|
|
68
|
+
codes.push(M3_FAIL.PAIR_SPLIT);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (staged < input.anchorFloor)
|
|
73
|
+
codes.push(M3_FAIL.ANCHOR_CROSSED);
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
ok: codes.length === 0,
|
|
77
|
+
codes,
|
|
78
|
+
retreats,
|
|
79
|
+
effectiveSeq: expected,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Phase 3 — switch: atomic activation of the staged pointer. Only call after
|
|
84
|
+
* m3Validate reports ok; the host performs the durable pointer flip.
|
|
85
|
+
*/
|
|
86
|
+
export function m3Switch(host, input) {
|
|
87
|
+
const effective = m3Compute(input);
|
|
88
|
+
host.switchPointer(effective);
|
|
89
|
+
return effective;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Run the full copy-validate-switch migration against a host. On validation
|
|
93
|
+
* failure the switch is NOT performed and the host retains its previous pointer
|
|
94
|
+
* (crash-safety / copy-validate-switch).
|
|
95
|
+
*/
|
|
96
|
+
export function migrateEffectiveCutV2(host, input) {
|
|
97
|
+
m3Copy(host, input);
|
|
98
|
+
const v = m3Validate(host, input);
|
|
99
|
+
if (!v.ok)
|
|
100
|
+
return { ok: false, codes: v.codes, effectiveSeq: v.effectiveSeq };
|
|
101
|
+
m3Switch(host, input);
|
|
102
|
+
return { ok: true, codes: [], effectiveSeq: v.effectiveSeq };
|
|
103
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/migrations/minhash-v2.ts — M4 MinHashV2 copy/validate/switch
|
|
3
|
+
* migration (VC1C).
|
|
4
|
+
*
|
|
5
|
+
* M4 versions the L1 dedup signature store: v2 signatures/buckets are written
|
|
6
|
+
* BESIDE v1, backfilled by checkpoint id, verified, and only then switched as
|
|
7
|
+
* the active version. It follows the same copy/validate/switch + resume
|
|
8
|
+
* contract as M2 but for the minhash index:
|
|
9
|
+
*
|
|
10
|
+
* - batch: writes v2 signatures + buckets for a set of checkpoint IDs, each
|
|
11
|
+
* tagged with the frozen version 2; never touches v1 rows.
|
|
12
|
+
* - backfill: resumable by checkpoint id — an interrupted run resumes without
|
|
13
|
+
* duplicate v2 signatures or active-pointer drift.
|
|
14
|
+
* - verify: counts per checkpoint match v1 and every v2 digest re-hashes.
|
|
15
|
+
* - switch: atomically flips the ACTIVE VERSION pointer from v1 to v2; until
|
|
16
|
+
* a verified switch, v1 remains active (interruption keeps old authority).
|
|
17
|
+
*
|
|
18
|
+
* Cross-version compare is REJECTED: a mixed v1-v2 similarity/bucket query
|
|
19
|
+
* returns `MINHASH_VERSION_MISMATCH` (never compares v1/v2 signatures).
|
|
20
|
+
*
|
|
21
|
+
* Pure logic over an injected M4Host (deterministic + testable; no console).
|
|
22
|
+
* PREVENT-002/011/PI-004 honored.
|
|
23
|
+
*/
|
|
24
|
+
import { createHash } from "node:crypto";
|
|
25
|
+
import { MINHASH_VERSION, minhashV2Signature, encodeSignatureV2, } from "../../dedup/l1-minhash-v2.js";
|
|
26
|
+
import { lshBandsV2, BANDS_V2, SIGNATURE_BYTES_EXPECTED_V2, } from "../../dedup/l1-lsh-v2.js";
|
|
27
|
+
import { createConformanceReporter, } from "../conformance/emit.js";
|
|
28
|
+
/** M4 failure codes. */
|
|
29
|
+
export const M4_FAIL = {
|
|
30
|
+
VERSION_MISMATCH: "MINHASH_VERSION_MISMATCH",
|
|
31
|
+
BACKFILL_PARTIAL: "M4_BACKFILL_PARTIAL",
|
|
32
|
+
COUNT_MISMATCH: "M4_COUNT_MISMATCH",
|
|
33
|
+
DIGEST_MISMATCH: "M4_DIGEST_MISMATCH",
|
|
34
|
+
};
|
|
35
|
+
/** Reject a cross-version compare. Returns the frozen mismatch code. */
|
|
36
|
+
export function crossVersionError() {
|
|
37
|
+
return M4_FAIL.VERSION_MISMATCH;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Compute v2 signature + buckets for one checkpoint (the authoritative bytes).
|
|
41
|
+
* Pure and cross-language deterministic.
|
|
42
|
+
*/
|
|
43
|
+
export function computeV2Row(host, checkpointId) {
|
|
44
|
+
const sessionId = host.sessionOf(checkpointId);
|
|
45
|
+
const source = host.sourceOf(checkpointId);
|
|
46
|
+
const sig = minhashV2Signature(source);
|
|
47
|
+
const bytes = encodeSignatureV2(sig);
|
|
48
|
+
const buckets = lshBandsV2(new Uint8Array(bytes), sessionId);
|
|
49
|
+
return {
|
|
50
|
+
checkpointId,
|
|
51
|
+
sessionId,
|
|
52
|
+
version: MINHASH_VERSION,
|
|
53
|
+
signatureBytes: new Uint8Array(bytes),
|
|
54
|
+
digest: `sha256:${sha256Hex(bytes)}`,
|
|
55
|
+
buckets,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function sha256Hex(bytes) {
|
|
59
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Backfill v2 signatures/buckets for all v1 checkpoint ids (resumable): rows
|
|
63
|
+
* already persisted with matching v2 digest/version are left untouched (no
|
|
64
|
+
* duplicates), and the active pointer stays at v1 until a verified switch.
|
|
65
|
+
* Returns the rows written this call (delta), never duplicates.
|
|
66
|
+
*
|
|
67
|
+
* An EXISTING row is re-computed and digest-verified before it is skipped; if
|
|
68
|
+
* the persisted row is corrupt (wrong version, wrong length, or a digest that
|
|
69
|
+
* no longer re-hashes its signatureBytes — e.g. a crash mid-write that left a
|
|
70
|
+
* truncated/bad row for an already-present id), it is recomputed and rewritten
|
|
71
|
+
* so corruption self-heals instead of being frozen by a later `m4Verify` failure.
|
|
72
|
+
*/
|
|
73
|
+
export function m4Backfill(host, reporter = createConformanceReporter()) {
|
|
74
|
+
const wanted = host.v1CheckpointIds();
|
|
75
|
+
const existing = new Map(host.storedV2().map((r) => [r.checkpointId, r]));
|
|
76
|
+
const writes = [];
|
|
77
|
+
for (const id of wanted) {
|
|
78
|
+
const row = computeV2Row(host, id);
|
|
79
|
+
const stored = existing.get(id);
|
|
80
|
+
if (stored !== undefined && v2RowMatches(stored, row))
|
|
81
|
+
continue; // healthy, no dup
|
|
82
|
+
writes.push(row); // absent, or stored row is corrupt -> (re)compute authoritative bytes
|
|
83
|
+
}
|
|
84
|
+
if (writes.length > 0) {
|
|
85
|
+
host.putV2(writes);
|
|
86
|
+
reporter.backfilled({
|
|
87
|
+
written: writes.length,
|
|
88
|
+
checkpointIds: writes.map((r) => r.checkpointId),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return writes;
|
|
92
|
+
}
|
|
93
|
+
/** True when a persisted row already matches the authoritative recompute. */
|
|
94
|
+
function v2RowMatches(stored, fresh) {
|
|
95
|
+
return (stored.version === MINHASH_VERSION &&
|
|
96
|
+
stored.checkpointId === fresh.checkpointId &&
|
|
97
|
+
stored.sessionId === fresh.sessionId &&
|
|
98
|
+
stored.signatureBytes.length === fresh.signatureBytes.length &&
|
|
99
|
+
stored.digest === fresh.digest &&
|
|
100
|
+
bucketsMatch(stored));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Re-derive the 64 bucket keys from the row's signatureBytes + sessionId and
|
|
104
|
+
* compare every value to the stored buckets. This closes a defensive
|
|
105
|
+
* verification gap: a persisted row whose signatureBytes + digest are correct
|
|
106
|
+
* but whose bucket VALUES were written wrong (a writer defect, or a partial
|
|
107
|
+
* bucket write that still held 64 entries) would otherwise pass digest+count
|
|
108
|
+
* checks and be treated as healthy — yet the buckets are exactly the data the
|
|
109
|
+
* live LSH-dedup path queries. Buckets are deterministic from sigBytes +
|
|
110
|
+
* sessionId, so a mismatch is always a real storage/author defect.
|
|
111
|
+
*/
|
|
112
|
+
function bucketsMatch(row) {
|
|
113
|
+
// A wrong-length signature can't be banded (lshBandsV2 throws); the caller
|
|
114
|
+
// already reports it as a count/version mismatch, so fail closed here too.
|
|
115
|
+
if (row.signatureBytes.length !== SIGNATURE_BYTES_EXPECTED_V2)
|
|
116
|
+
return false;
|
|
117
|
+
if (row.buckets.length !== BANDS_V2)
|
|
118
|
+
return false;
|
|
119
|
+
const expected = lshBandsV2(new Uint8Array(row.signatureBytes), row.sessionId);
|
|
120
|
+
for (let i = 0; i < BANDS_V2; i++) {
|
|
121
|
+
if (expected[i] !== row.buckets[i])
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Verify the backfilled v2 index: every v1 checkpoint has exactly one v2 row
|
|
128
|
+
* (reset -> resumable partial counts), each stored row's digest re-hashes, and
|
|
129
|
+
* each row has all 64 buckets. Never mutates.
|
|
130
|
+
*/
|
|
131
|
+
export function m4Verify(host) {
|
|
132
|
+
const codes = [];
|
|
133
|
+
const wanted = new Set(host.v1CheckpointIds());
|
|
134
|
+
const stored = host.storedV2();
|
|
135
|
+
const counts = new Map();
|
|
136
|
+
for (const r of stored) {
|
|
137
|
+
counts.set(r.checkpointId, (counts.get(r.checkpointId) ?? 0) + 1);
|
|
138
|
+
if (r.version !== MINHASH_VERSION)
|
|
139
|
+
codes.push(M4_FAIL.VERSION_MISMATCH);
|
|
140
|
+
if (r.signatureBytes.length !== 2048)
|
|
141
|
+
codes.push(M4_FAIL.COUNT_MISMATCH);
|
|
142
|
+
if (r.buckets.length !== BANDS_V2)
|
|
143
|
+
codes.push(M4_FAIL.COUNT_MISMATCH);
|
|
144
|
+
if (r.digest !== `sha256:${sha256Hex(r.signatureBytes)}`) {
|
|
145
|
+
codes.push(M4_FAIL.DIGEST_MISMATCH);
|
|
146
|
+
}
|
|
147
|
+
// Bucket VALUE verification: re-derive all 64 keys from signatureBytes +
|
|
148
|
+
// sessionId and compare them. Digest + count alone would let a row with
|
|
149
|
+
// correct sigBytes/digest but wrong bucket values pass; the buckets are the
|
|
150
|
+
// data the live LSH-dedup path queries, so a drifts here is a real defect.
|
|
151
|
+
if (!bucketsMatch(r))
|
|
152
|
+
codes.push(M4_FAIL.COUNT_MISMATCH);
|
|
153
|
+
}
|
|
154
|
+
// Every v1 checkpoint backfilled exactly once (count parity, no dup rows).
|
|
155
|
+
for (const id of wanted) {
|
|
156
|
+
const c = counts.get(id) ?? 0;
|
|
157
|
+
if (c === 0)
|
|
158
|
+
codes.push(M4_FAIL.BACKFILL_PARTIAL);
|
|
159
|
+
if (c > 1)
|
|
160
|
+
codes.push(M4_FAIL.COUNT_MISMATCH);
|
|
161
|
+
}
|
|
162
|
+
// No orphan v2 rows without a v1 checkpoint.
|
|
163
|
+
for (const id of counts.keys()) {
|
|
164
|
+
if (!wanted.has(id))
|
|
165
|
+
codes.push(M4_FAIL.COUNT_MISMATCH);
|
|
166
|
+
}
|
|
167
|
+
const ok = codes.length === 0;
|
|
168
|
+
return { ok, codes: dedupe(codes) };
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Switch the active version to v2. Only call after `m4Verify` reports ok; a
|
|
172
|
+
* crash/return before switch leaves v1 active (interruption keeps old
|
|
173
|
+
* authority, resumable idempotently).
|
|
174
|
+
*/
|
|
175
|
+
export function m4Switch(host) {
|
|
176
|
+
host.switchToV2();
|
|
177
|
+
}
|
|
178
|
+
/** Full M4 lifecycle: backfill -> verify -> switch; returns verify result. */
|
|
179
|
+
export function migrateMinhashV2(host, reporter = createConformanceReporter()) {
|
|
180
|
+
m4Backfill(host, reporter);
|
|
181
|
+
const v = m4Verify(host);
|
|
182
|
+
if (v.ok)
|
|
183
|
+
m4Switch(host);
|
|
184
|
+
return v;
|
|
185
|
+
}
|
|
186
|
+
/** Registered M4 conformance ID range (M4-001..008 + named M4-*). */
|
|
187
|
+
export const M4_IDS = [
|
|
188
|
+
"M4-001",
|
|
189
|
+
"M4-002",
|
|
190
|
+
"M4-003",
|
|
191
|
+
"M4-004",
|
|
192
|
+
"M4-005",
|
|
193
|
+
"M4-006",
|
|
194
|
+
"M4-007",
|
|
195
|
+
"M4-008",
|
|
196
|
+
];
|
|
197
|
+
/** Registered named M4 conformance IDs. */
|
|
198
|
+
export const M4_NAMED = [
|
|
199
|
+
"M4-HIGHBIT-001",
|
|
200
|
+
"M4-VERSION-002",
|
|
201
|
+
"M4-RESUME-003",
|
|
202
|
+
];
|
|
203
|
+
function dedupe(codes) {
|
|
204
|
+
const out = [];
|
|
205
|
+
for (const c of codes)
|
|
206
|
+
if (!out.includes(c))
|
|
207
|
+
out.push(c);
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/migrations/occurrence-v2.ts — M2 copy/validate/switch migration
|
|
3
|
+
* for the occurrence-v2 ledger + compat journal (VC1B).
|
|
4
|
+
*
|
|
5
|
+
* M2 converts the neutral v2 ledger into a lossless legacy export so an OLD
|
|
6
|
+
* binary may keep reading after a downgrade. It follows the compat-journal
|
|
7
|
+
* copy/validate/switch contract: COPY the v2 rows + journal records into the
|
|
8
|
+
* staged legacy export, VALIDATE the staged export (seq monotonicity + digest
|
|
9
|
+
* parity + unrepresentable rows listed), and ONLY THEN atomically SWITCH
|
|
10
|
+
* authority to the export. Each phase is exposed separately so a crash between
|
|
11
|
+
* validate and switch retains the OLD authority and a restart resumes
|
|
12
|
+
* idempotently (the unique failure-injection contract — terminate after
|
|
13
|
+
* `validated` and before `switched`, restart switches once without duplicate
|
|
14
|
+
* rows).
|
|
15
|
+
*
|
|
16
|
+
* Runs against an injected M2Host so the migration is deterministic and
|
|
17
|
+
* testable. Never mutates v2; the export is a NEW copy (silent direct downgrade
|
|
18
|
+
* is rejected). Registered conformance rows: M2-001..015 + MIG-DOWN-001.
|
|
19
|
+
* PREVENT-002/011/PI-004 honored; no console.log.
|
|
20
|
+
*/
|
|
21
|
+
import { MIG_DOWN_FAIL, verifyLegacyDigest } from "../ledger/compat-journal.js";
|
|
22
|
+
/** M2 failure codes (downgrade export). */
|
|
23
|
+
export const M2_FAIL = {
|
|
24
|
+
...MIG_DOWN_FAIL,
|
|
25
|
+
COPY_MISSING: "M2_COPY_MISSING",
|
|
26
|
+
UNREPRESENTABLE_UNLISTED: "M2_UNREPRESENTABLE_UNLISTED",
|
|
27
|
+
};
|
|
28
|
+
/** The freshly computed legacy export from the v2 journal (expected bytes). */
|
|
29
|
+
export function computeLegacyExport(host) {
|
|
30
|
+
return host.journalRows();
|
|
31
|
+
}
|
|
32
|
+
/** Phase 1 — copy: stage the legacy export without activating. Returns rows. */
|
|
33
|
+
export function m2Copy(host) {
|
|
34
|
+
const rows = computeLegacyExport(host);
|
|
35
|
+
host.writeStagedLegacy(rows);
|
|
36
|
+
return rows;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Phase 2 — validate: the staged export must equal the freshly computed one
|
|
40
|
+
* (copy-match), the journal must be active and have reached the `copied`
|
|
41
|
+
* phase, seq must be monotonic per session, digests must be well-formed, and
|
|
42
|
+
* every unrepresentable row must be listed. Advances to `validated` only when
|
|
43
|
+
* every check passes (so a crash here leaves authority unchanged).
|
|
44
|
+
*/
|
|
45
|
+
export function m2Validate(host) {
|
|
46
|
+
const codes = [];
|
|
47
|
+
const staged = host.stagedLegacy();
|
|
48
|
+
const expected = computeLegacyExport(host);
|
|
49
|
+
if (staged === null) {
|
|
50
|
+
codes.push(M2_FAIL.COPY_MISSING);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const sameRows = staged.length === expected.length &&
|
|
54
|
+
staged.every((r, i) => r.session === expected[i].session &&
|
|
55
|
+
r.seq === expected[i].seq &&
|
|
56
|
+
r.eventId === expected[i].eventId &&
|
|
57
|
+
r.unrepresentable === expected[i].unrepresentable);
|
|
58
|
+
if (!sameRows)
|
|
59
|
+
codes.push(M2_FAIL.COPY_MISSING);
|
|
60
|
+
// Seq monotonic per session (no regression).
|
|
61
|
+
const prev = new Map();
|
|
62
|
+
for (const r of staged) {
|
|
63
|
+
const last = prev.get(r.session);
|
|
64
|
+
if (last !== undefined && r.seq <= last)
|
|
65
|
+
codes.push(M2_FAIL.SEQ_REGRESSION);
|
|
66
|
+
prev.set(r.session, r.seq);
|
|
67
|
+
}
|
|
68
|
+
// Digests on representable rows must verify: recompute sha256 over the stored
|
|
69
|
+
// source and compare to the recorded digest (Q03 — real parity).
|
|
70
|
+
for (const r of staged) {
|
|
71
|
+
if (!r.unrepresentable && !verifyLegacyDigest(r.digest, r.legacyProjection)) {
|
|
72
|
+
codes.push(M2_FAIL.DIGEST_MISMATCH);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Every unrepresentable row is explicitly flagged (MIG-DOWN-003).
|
|
76
|
+
for (const r of expected) {
|
|
77
|
+
if (r.unrepresentable) {
|
|
78
|
+
const flag = staged.find((s) => s.session === r.session && s.seq === r.seq && s.unrepresentable);
|
|
79
|
+
if (!flag)
|
|
80
|
+
codes.push(M2_FAIL.UNREPRESENTABLE_UNLISTED);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!host.journalActive())
|
|
85
|
+
codes.push(M2_FAIL.NOT_ACTIVE);
|
|
86
|
+
if (host.phase() !== "copied")
|
|
87
|
+
codes.push(M2_FAIL.PHASE_UNREACHED);
|
|
88
|
+
if (codes.length === 0) {
|
|
89
|
+
const v = host.validateStaged();
|
|
90
|
+
if (!v.ok) {
|
|
91
|
+
// Q05: narrow the host's string codes through a runtime type guard instead
|
|
92
|
+
// of an unchecked cast — unknown codes are dropped, never mis-typed.
|
|
93
|
+
for (const c of v.codes)
|
|
94
|
+
if (isM2MigrationCode(c))
|
|
95
|
+
codes.push(c);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const ok = codes.length === 0;
|
|
99
|
+
return { ok, codes: dedupe(codes) };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Phase 3 — switch: atomic activation of the legacy export as the openable
|
|
103
|
+
* authority. Only call after m2Validate reports ok; the journal advances to
|
|
104
|
+
* `switched` and v2 becomes read-only-archival. Idempotent on resume.
|
|
105
|
+
*/
|
|
106
|
+
export function m2Switch(host) {
|
|
107
|
+
host.switchLegacy();
|
|
108
|
+
}
|
|
109
|
+
/** Registered M2 conformance ID range (M2-001..015). */
|
|
110
|
+
export const M2_IDS = [
|
|
111
|
+
"M2-001",
|
|
112
|
+
"M2-002",
|
|
113
|
+
"M2-003",
|
|
114
|
+
"M2-004",
|
|
115
|
+
"M2-005",
|
|
116
|
+
"M2-006",
|
|
117
|
+
"M2-007",
|
|
118
|
+
"M2-008",
|
|
119
|
+
"M2-009",
|
|
120
|
+
"M2-010",
|
|
121
|
+
"M2-011",
|
|
122
|
+
"M2-012",
|
|
123
|
+
"M2-013",
|
|
124
|
+
"M2-014",
|
|
125
|
+
"M2-015",
|
|
126
|
+
];
|
|
127
|
+
/** Registered MIG-DOWN conformance ID (MIG-DOWN-001). */
|
|
128
|
+
export const MIG_DOWN_IDS = ["MIG-DOWN-001"];
|
|
129
|
+
/** Runtime narrowing: is `code` a known M2 migration failure code? */
|
|
130
|
+
function isM2MigrationCode(code) {
|
|
131
|
+
return Object.values(M2_FAIL).includes(code);
|
|
132
|
+
}
|
|
133
|
+
function dedupe(codes) {
|
|
134
|
+
const out = [];
|
|
135
|
+
for (const c of codes)
|
|
136
|
+
if (!out.includes(c))
|
|
137
|
+
out.push(c);
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/replay/cut.ts — ReplayCutV2 effective-cut calculator (VC0B).
|
|
3
|
+
*
|
|
4
|
+
* M3 effective-cut rule: effective = min(boundarySafeSeq, committedSeq,
|
|
5
|
+
* capturedHighWater), capped by requestedSeq, then retreated to the call
|
|
6
|
+
* boundary whenever the candidate intersects a tool call/result pair, then
|
|
7
|
+
* clamped to the recent-anchor floor. On a tie among the min sources we choose
|
|
8
|
+
* the LOWER source order and record CUT_LOWEST_SOURCE_ORDER. This is the
|
|
9
|
+
* successor to the legacy capped-replay effective cut — the defect at
|
|
10
|
+
* docs/vector-cortex/SPRINT_PLAN.md M3 ("capped replay effective cut must
|
|
11
|
+
* respect boundary/commit/capture minima") is fixed here.
|
|
12
|
+
*
|
|
13
|
+
* Pure/deterministic — no network, no side effects (PREVENT-PI-004).
|
|
14
|
+
*/
|
|
15
|
+
const MIN_SOURCES = ["boundary", "committed", "captured"];
|
|
16
|
+
/**
|
|
17
|
+
* min-of-three, capped by requestedSeq, with deterministic lower-source
|
|
18
|
+
* tie-breaking. Returns the min value and every source that tied for it.
|
|
19
|
+
* On a tie the LOWER source order wins; `sources[0]` is the winning source.
|
|
20
|
+
*/
|
|
21
|
+
function minOfCapped(requestedSeq, boundarySafeSeq, committedSeq, capturedHighWater) {
|
|
22
|
+
const values = [
|
|
23
|
+
["boundary", boundarySafeSeq],
|
|
24
|
+
["committed", committedSeq],
|
|
25
|
+
["captured", capturedHighWater],
|
|
26
|
+
];
|
|
27
|
+
const minVal = values.reduce((a, [, v]) => (v < a ? v : a), requestedSeq);
|
|
28
|
+
const floors = values.filter(([, v]) => v === minVal).map(([s]) => s);
|
|
29
|
+
// If a source value exceeds requestedSeq it is capped away; the min is
|
|
30
|
+
// positioned at requestedSeq only when every source is above the request.
|
|
31
|
+
const value = floors.length > 0 ? minVal : requestedSeq;
|
|
32
|
+
return { value, sources: floors.length > 0 ? floors : ["boundary"] };
|
|
33
|
+
}
|
|
34
|
+
/** True when a cut at seq `e` does NOT split any tool call/result pair. */
|
|
35
|
+
export function cutIsPairSafe(e, pairs) {
|
|
36
|
+
for (const p of pairs) {
|
|
37
|
+
// Cutting at e keeps events with seq <= e. A pair is split when the call
|
|
38
|
+
// (callSeq) is kept but the result (resultSeq) is dropped.
|
|
39
|
+
if (p.callSeq <= e && e < p.resultSeq)
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Retreat `e` upward-locked toward the call boundary of any pair it splits.
|
|
46
|
+
* Returns the largest pair-safe e <= start, never below `floor`.
|
|
47
|
+
*
|
|
48
|
+
* `splitResolved` is true when a CUT_TOOL_PAIR_SPLIT retreat occurred; when a
|
|
49
|
+
* retreat would cross the floor the cut clamps AT the floor and the reason
|
|
50
|
+
* becomes CUT_ANCHOR_FLOOR (the floor is assumed pair-safe for legal inputs).
|
|
51
|
+
*/
|
|
52
|
+
function retreatAgainstPairs(start, pairs, floor) {
|
|
53
|
+
let e = start;
|
|
54
|
+
let split = false;
|
|
55
|
+
let floorClamped = false;
|
|
56
|
+
for (;;) {
|
|
57
|
+
let hit = false;
|
|
58
|
+
for (const p of pairs) {
|
|
59
|
+
if (p.callSeq <= e && e < p.resultSeq) {
|
|
60
|
+
hit = true;
|
|
61
|
+
const target = p.callSeq - 1n;
|
|
62
|
+
if (target < floor) {
|
|
63
|
+
// No-progress guard (VC0B-I12): the retreat target lies below the
|
|
64
|
+
// floor. If the floor itself still splits a pair we must not loop
|
|
65
|
+
// back onto it (that hangs the synchronous agent loop) — clamp AT
|
|
66
|
+
// the floor once and stop. The floor is taken as the legal cut.
|
|
67
|
+
e = floor;
|
|
68
|
+
floorClamped = true;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
e = target;
|
|
72
|
+
}
|
|
73
|
+
split = true;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Terminate when no pair splits e, OR when we have already clamped to the
|
|
78
|
+
// floor (the floor is an absolute lower bound; we never retreat past it
|
|
79
|
+
// even if it is not itself pair-safe).
|
|
80
|
+
if (!hit || floorClamped)
|
|
81
|
+
return { effective: e, split, floorClamped };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Compute the v2 effective cut. Pure with respect to the provided inputs.
|
|
86
|
+
*/
|
|
87
|
+
export function computeEffectiveCutV2(input) {
|
|
88
|
+
const { requestedSeq, boundarySafeSeq, committedSeq, capturedHighWater, anchorFloor, pairs } = input;
|
|
89
|
+
const retreats = [];
|
|
90
|
+
// Step 1 — min-of-three (capped by requestedSeq) with lower-source tie-break.
|
|
91
|
+
const min = minOfCapped(requestedSeq, boundarySafeSeq, committedSeq, capturedHighWater);
|
|
92
|
+
let effective = min.value;
|
|
93
|
+
// Tie among the min sources: resolved to the LOWER source order — record it.
|
|
94
|
+
if (min.sources.length > 1) {
|
|
95
|
+
retreats.push({
|
|
96
|
+
code: "CUT_LOWEST_SOURCE_ORDER",
|
|
97
|
+
fromSeq: effective,
|
|
98
|
+
toSeq: effective,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// Step 2 — pair retreat (never below the anchor floor).
|
|
102
|
+
const retreat = retreatAgainstPairs(effective, pairs, anchorFloor);
|
|
103
|
+
if (retreat.split && retreat.floorClamped) {
|
|
104
|
+
retreats.push({ code: "CUT_ANCHOR_FLOOR", fromSeq: effective, toSeq: retreat.effective });
|
|
105
|
+
}
|
|
106
|
+
else if (retreat.split) {
|
|
107
|
+
retreats.push({ code: "CUT_TOOL_PAIR_SPLIT", fromSeq: effective, toSeq: retreat.effective });
|
|
108
|
+
}
|
|
109
|
+
effective = retreat.effective;
|
|
110
|
+
// Step 3 — final anchor-floor clamp (safety; legal floors are already >=).
|
|
111
|
+
if (effective < anchorFloor) {
|
|
112
|
+
retreats.push({ code: "CUT_ANCHOR_FLOOR", fromSeq: effective, toSeq: anchorFloor });
|
|
113
|
+
effective = anchorFloor;
|
|
114
|
+
}
|
|
115
|
+
const cut = {
|
|
116
|
+
requestedSeq,
|
|
117
|
+
boundarySafeSeq,
|
|
118
|
+
committedSeq,
|
|
119
|
+
capturedHighWater,
|
|
120
|
+
effectiveSeq: effective,
|
|
121
|
+
};
|
|
122
|
+
return { cut, retreats };
|
|
123
|
+
}
|