pi-mega-compact 0.16.3 → 0.17.0
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 +81 -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 +10 -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 +81 -0
- package/dist/src/config.js +6 -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/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/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/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-CAnWomhw.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-Y_X0Bbjd.js.map → AreaChart-CAnWomhw.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-B9XDb-Ob.js → BarChart-lZ2-s71k.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-B9XDb-Ob.js.map → BarChart-lZ2-s71k.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-BUGVb1G1.js → CacheTab-BL2Thh66.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-BUGVb1G1.js.map → CacheTab-BL2Thh66.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-dOqLYTsy.js → EventsTab-DEh2Z9fp.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-dOqLYTsy.js.map → EventsTab-DEh2Z9fp.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-c2vezKg1.js → HealthTab-DTwKclxk.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-c2vezKg1.js.map → HealthTab-DTwKclxk.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-bQYHIcdx.js → MaintenanceTab-DDfBEdbX.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-bQYHIcdx.js.map → MaintenanceTab-DDfBEdbX.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-2L8pH6y2.js → MemoryMapTab-Dgfmg-0Q.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-2L8pH6y2.js.map → MemoryMapTab-Dgfmg-0Q.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CzUx1jpo.js → MetricsTab-DOcMm0Tx.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CzUx1jpo.js.map → MetricsTab-DOcMm0Tx.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-eCNoaclv.js → OverviewTab-DHT5aoHg.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-eCNoaclv.js.map → OverviewTab-DHT5aoHg.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-tb0IW3NF.js → ReposTab-CGAmtR_c.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-tb0IW3NF.js.map → ReposTab-CGAmtR_c.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-C-IoLw5h.js → SessionsTab-BIgx6iKT.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-C-IoLw5h.js.map → SessionsTab-BIgx6iKT.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-BvmFi8gf.js → SetupTab-ADghKuEI.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-BvmFi8gf.js.map → SetupTab-ADghKuEI.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DBH-cOlA.js → TimeSavedCard-C33UiiOM.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DBH-cOlA.js.map → TimeSavedCard-C33UiiOM.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DBMgmvnX.js → TurnsTab-DdbL9wPN.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DBMgmvnX.js.map → TurnsTab-DdbL9wPN.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-Cl1pbEdR.js +2 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-Cl1pbEdR.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{WikiTab-1LcQrXk3.js → WikiTab-Ch00OFKs.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-1LcQrXk3.js.map → WikiTab-Ch00OFKs.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-DtEsZTUE.js → button-ByeEdo2g.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-DtEsZTUE.js.map → button-ByeEdo2g.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-DcfTmw-4.js → card-DesoLXvc.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-DcfTmw-4.js.map → card-DesoLXvc.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-BsIxUDyw.js → generateCategoricalChart-CCAB7Iz4.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-BsIxUDyw.js.map → generateCategoricalChart-CCAB7Iz4.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-B93cewiX.js → index-B6cgyAPr.js} +34 -29
- package/extensions/dashboard-client/dist/assets/index-B6cgyAPr.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-DIZC-oe-.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-Bowd7jCD.js.map → switch-DIZC-oe-.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-Bo7kJZ4u.js → toggle-CIoWaH2e.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-Bo7kJZ4u.js.map → toggle-CIoWaH2e.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-zNhb0c3u.js → useSSE-Bv4943l1.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-zNhb0c3u.js.map → useSSE-Bv4943l1.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-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 +35 -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 +87 -0
- package/src/config.ts +27 -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/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,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/ledger/store.ts — VC1B capability-separated ledger contracts.
|
|
3
|
+
*
|
|
4
|
+
* Owns `LedgerReader` / `LedgerWriter` / `LedgerAdmin` and `CompatJournalV1`.
|
|
5
|
+
* The ledger is append-only, seq is monotonic per session, a tool RESULT names
|
|
6
|
+
* exactly one earlier call in the same session, and duplicates are identified by
|
|
7
|
+
* `(eventId, digest)` only (two occurrences may share bytes at distinct seq).
|
|
8
|
+
*
|
|
9
|
+
* Capability gating mirrors the host `asReader/asWriter/asAdmin` pattern: a
|
|
10
|
+
* consumer receives ONLY what it needs. Dashboard GET receives `LedgerReader`;
|
|
11
|
+
* only ingestion receives `LedgerWriter`; only the migration coordinator
|
|
12
|
+
* receives `LedgerAdmin`. This keeps the neutral byte-authority ledger closed to
|
|
13
|
+
* anything but the sanctioned channels (CONTRACTS §Store and migration
|
|
14
|
+
* contracts).
|
|
15
|
+
*
|
|
16
|
+
* Pure type/schema definitions + small pure predicates. No storage, no console,
|
|
17
|
+
* no network, no side effects (PREVENT-PI-004 / PREVENT-011).
|
|
18
|
+
*/
|
|
19
|
+
import { VC1B_ENABLED } from "../../config/vector-cortex.js";
|
|
20
|
+
import { openOccurrenceStore, appendOccurrence, ledgerHighWater, readSessionOccurrences, readFromSeq, countOccurrences, hasOccurrence, } from "./sqlite.js";
|
|
21
|
+
import { initCompatJournal, createCompatJournal, journalPhase, } from "./compat-journal.js";
|
|
22
|
+
import { m2Copy, m2Validate, m2Switch, } from "../migrations/occurrence-v2.js";
|
|
23
|
+
/** The token that gates closures to a single capability (PREVENT-011-free). */
|
|
24
|
+
const _capability = Symbol("mc-ledger-capability");
|
|
25
|
+
/**
|
|
26
|
+
* Create the capability-separated occurrence-v2 ledger over its OWN isolated
|
|
27
|
+
* SQLite DB. `emit` is optional; occurrence-appended and compat-switch-committed
|
|
28
|
+
* events are only emitted when VC1B_ENABLED() and an emitter is supplied (mode-C
|
|
29
|
+
* parity: flag OFF / no emitter => zero observability writes).
|
|
30
|
+
*
|
|
31
|
+
* Normalization: `stateDir` gives the standard daemon location
|
|
32
|
+
* `<stateDir>/vector-cortex/occurrence-v2.db`; a bare `dbPath` overrides it
|
|
33
|
+
* (tests/rehearsal isolate the ledger). The compat journal shares the same DB
|
|
34
|
+
* so a v2 append + its journal record commit atomically.
|
|
35
|
+
*/
|
|
36
|
+
export function createLedgerStore(opts, emit) {
|
|
37
|
+
const dbPath = "dbPath" in opts
|
|
38
|
+
? opts.dbPath
|
|
39
|
+
: `${opts.stateDir}/vector-cortex/occurrence-v2.db`;
|
|
40
|
+
const db = openOccurrenceStore(dbPath);
|
|
41
|
+
initCompatJournal(db);
|
|
42
|
+
const compat = createCompatJournal(db);
|
|
43
|
+
const fire = (event, fields) => {
|
|
44
|
+
if (!VC1B_ENABLED())
|
|
45
|
+
return;
|
|
46
|
+
try {
|
|
47
|
+
emit?.(event, fields);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
/* non-fatal observability — never break the agent loop */
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const asReader = () => ({
|
|
54
|
+
kind: "LedgerReader",
|
|
55
|
+
[_capability]: "reader",
|
|
56
|
+
highWater: (session) => ledgerHighWater(db, session),
|
|
57
|
+
readSession: (session) => readSessionOccurrences(db, session),
|
|
58
|
+
readFrom: (session, fromSeq) => readFromSeq(db, session, fromSeq),
|
|
59
|
+
count: (session) => countOccurrences(db, session),
|
|
60
|
+
hasOccurrence: (session, eventId, digest) => hasOccurrence(db, session, eventId, digest),
|
|
61
|
+
});
|
|
62
|
+
const asWriter = () => ({
|
|
63
|
+
kind: "LedgerWriter",
|
|
64
|
+
[_capability]: "writer",
|
|
65
|
+
append(input) {
|
|
66
|
+
// The occurrence insert and its compat-journal record commit atomically
|
|
67
|
+
// inside one nested savepoint (CONTRACTS §Store: atomically appends).
|
|
68
|
+
db.exec("SAVEPOINT mc_ledger_app");
|
|
69
|
+
try {
|
|
70
|
+
const result = appendOccurrence(db, input);
|
|
71
|
+
if (result.ok) {
|
|
72
|
+
compat.record({
|
|
73
|
+
occurrence: result.occurrence,
|
|
74
|
+
legacyProjection: legacyProjectionOf(result.occurrence),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (result.ok) {
|
|
78
|
+
db.exec("RELEASE mc_ledger_app");
|
|
79
|
+
fire("vector_cortex_occurrence_appended", {
|
|
80
|
+
session: result.occurrence.session,
|
|
81
|
+
seq: result.occurrence.seq.toString(),
|
|
82
|
+
eventId: result.occurrence.eventId,
|
|
83
|
+
digest: result.occurrence.digest,
|
|
84
|
+
kind: result.occurrence.kind,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
db.exec("ROLLBACK TO mc_ledger_app");
|
|
89
|
+
db.exec("RELEASE mc_ledger_app");
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
db.exec("ROLLBACK TO mc_ledger_app");
|
|
95
|
+
db.exec("RELEASE mc_ledger_app");
|
|
96
|
+
throw e;
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
appendBatch(inputs) {
|
|
100
|
+
const out = [];
|
|
101
|
+
for (const input of inputs)
|
|
102
|
+
out.push(this.append(input));
|
|
103
|
+
return out;
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
const asAdmin = () => ({
|
|
107
|
+
kind: "LedgerAdmin",
|
|
108
|
+
[_capability]: "admin",
|
|
109
|
+
compat,
|
|
110
|
+
migrateOccurrenceV2() {
|
|
111
|
+
// copy => validate => switch; resumable & idempotent. The M2 host wraps
|
|
112
|
+
// the live DB + journal so every phase transition advances the SAME
|
|
113
|
+
// singleton journal state machine (prepared→copied→validated→switched).
|
|
114
|
+
const unrepresentable = compat.prepare().length;
|
|
115
|
+
const mig = migrateHost(db, compat);
|
|
116
|
+
m2Copy(mig);
|
|
117
|
+
const v = m2Validate(mig);
|
|
118
|
+
if (v.ok) {
|
|
119
|
+
m2Switch(mig);
|
|
120
|
+
fire("vector_cortex_compat_switch_committed", {
|
|
121
|
+
session: "all",
|
|
122
|
+
unrepresentable,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return { ok: v.ok, codes: v.codes };
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
return { reader: asReader, writer: asWriter, admin: asAdmin, close: () => db.close() };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Lossless legacy projection for a v2 occurrence: its identity envelope plus
|
|
132
|
+
* base64 source bytes. Returns null (→ journal `unrepresentable` marker) when the
|
|
133
|
+
* source bytes do not form valid UTF-8, which the legacy pipeline cannot round-trip
|
|
134
|
+
* losslessly — those rows are listed rather than silently coerced (MIG-DOWN-003).
|
|
135
|
+
*/
|
|
136
|
+
function legacyProjectionOf(occ) {
|
|
137
|
+
const source = Buffer.from(occ.sourceBytes);
|
|
138
|
+
try {
|
|
139
|
+
new TextDecoder("utf-8", { fatal: true }).decode(source);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return JSON.stringify({
|
|
145
|
+
session: occ.session,
|
|
146
|
+
eventId: occ.eventId,
|
|
147
|
+
kind: occ.kind,
|
|
148
|
+
digest: occ.digest,
|
|
149
|
+
source: source.toString("base64"),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/** Build an M2Host over the live ledger DB + journal (for migrateOccurrenceV2). */
|
|
153
|
+
function migrateHost(db, journal) {
|
|
154
|
+
let staged = null;
|
|
155
|
+
return {
|
|
156
|
+
db,
|
|
157
|
+
phase: () => journalPhase(db),
|
|
158
|
+
journalActive: () => journal.active(),
|
|
159
|
+
journalRows: () => journalRowsOf(db),
|
|
160
|
+
writeStagedLegacy: (rows) => {
|
|
161
|
+
staged = rows;
|
|
162
|
+
journal.copied();
|
|
163
|
+
},
|
|
164
|
+
stagedLegacy: () => staged,
|
|
165
|
+
validateStaged: () => journal.validate(),
|
|
166
|
+
switchLegacy: () => journal.switched(),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** Read the journaled occurrences as legacy-export rows. */
|
|
170
|
+
function journalRowsOf(db) {
|
|
171
|
+
const rows = db
|
|
172
|
+
.prepare(`SELECT session, seq, event_id, digest, kind, legacy_projection, unrepresentable
|
|
173
|
+
FROM compat_journal_v1 ORDER BY session ASC, seq ASC`)
|
|
174
|
+
.all();
|
|
175
|
+
return rows.map((r) => ({
|
|
176
|
+
session: r.session,
|
|
177
|
+
seq: BigInt(r.seq),
|
|
178
|
+
eventId: r.event_id,
|
|
179
|
+
digest: r.digest,
|
|
180
|
+
kind: r.kind,
|
|
181
|
+
legacyProjection: r.legacy_projection,
|
|
182
|
+
unrepresentable: r.unrepresentable === 1,
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/ledger/types.ts — EventV2 / EventCodec byte-authority contract
|
|
3
|
+
* types, validation result codes, and the registered EVT conformance ID range.
|
|
4
|
+
*
|
|
5
|
+
* Owned by VC1A (canonical byte events). Consumes only reviewer-accepted
|
|
6
|
+
* predecessor contracts and [common contracts](../../CONTRACTS.md §EventV2),
|
|
7
|
+
* which are NORMATIVE here. `originalBytes` and its SHA-256 digest are the byte
|
|
8
|
+
* authority; strict UTF-8 classification is never lossy; `canonicalNfc` is a
|
|
9
|
+
* DERIVED comparison/search key only (never identity, digest, or reconstruction).
|
|
10
|
+
*
|
|
11
|
+
* Pure type/schema definitions + a short pure digest/classify predicate runtime
|
|
12
|
+
* (validator codes). No network, no side effects (PREVENT-PI-004 / PREVENT-011).
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Registered EVT conformance ID range (EVT-001..030). The acceptance test reads
|
|
16
|
+
* these rows from the v2 manifest and asserts each returns its manifest bytes or
|
|
17
|
+
* exactly its listed failure code. Mirrors CUT_IDS / M3_IDS in replay/types.ts.
|
|
18
|
+
* EVT-001..015 are owned by VC1A (codec/validator); EVT-016..030 are owned by
|
|
19
|
+
* VC1B (occurrence ledger, see ledger/store.ts).
|
|
20
|
+
*/
|
|
21
|
+
export const EVT_IDS = [
|
|
22
|
+
"EVT-001",
|
|
23
|
+
"EVT-002",
|
|
24
|
+
"EVT-003",
|
|
25
|
+
"EVT-004",
|
|
26
|
+
"EVT-005",
|
|
27
|
+
"EVT-006",
|
|
28
|
+
"EVT-007",
|
|
29
|
+
"EVT-008",
|
|
30
|
+
"EVT-009",
|
|
31
|
+
"EVT-010",
|
|
32
|
+
"EVT-011",
|
|
33
|
+
"EVT-012",
|
|
34
|
+
"EVT-013",
|
|
35
|
+
"EVT-014",
|
|
36
|
+
"EVT-015",
|
|
37
|
+
"EVT-016",
|
|
38
|
+
"EVT-017",
|
|
39
|
+
"EVT-018",
|
|
40
|
+
"EVT-019",
|
|
41
|
+
"EVT-020",
|
|
42
|
+
"EVT-021",
|
|
43
|
+
"EVT-022",
|
|
44
|
+
"EVT-023",
|
|
45
|
+
"EVT-024",
|
|
46
|
+
"EVT-025",
|
|
47
|
+
"EVT-026",
|
|
48
|
+
"EVT-027",
|
|
49
|
+
"EVT-028",
|
|
50
|
+
"EVT-029",
|
|
51
|
+
"EVT-030",
|
|
52
|
+
];
|
|
@@ -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,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
|
+
}
|