pi-mega-compact 0.19.0 → 0.19.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 +10 -0
- package/dist/config.js +1 -1
- package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +1 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex-eval.js +49 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex-health.js +233 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex-ledger.js +82 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex-shared.js +46 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex-topology.js +68 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex.js +12 -373
- package/dist/extensions/dashboard-server/routes.js +1 -1
- package/dist/extensions/dashboard-server/server.js +3 -1
- package/dist/src/config/vector-cortex.js +10 -0
- package/dist/src/config.js +1 -1
- package/dist/src/vector-cortex/cortex/sqlite.js +393 -0
- package/dist/src/vector-cortex/cortex/store.js +139 -0
- package/dist/src/vector-cortex/cortex/types.js +44 -0
- package/dist/vector-cortex/cortex/sqlite.js +393 -0
- package/dist/vector-cortex/cortex/store.js +139 -0
- package/dist/vector-cortex/cortex/types.js +44 -0
- package/extensions/dashboard-client/dist/assets/{AreaChart-DywU5SSt.js → AreaChart-B9KYKrUE.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-DywU5SSt.js.map → AreaChart-B9KYKrUE.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-But7WN6K.js → BarChart-D1sWkR6j.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-But7WN6K.js.map → BarChart-D1sWkR6j.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-7aZWa1DR.js → CacheTab-BR8MYcJq.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-7aZWa1DR.js.map → CacheTab-BR8MYcJq.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-Bf_boNG2.js → EventsTab-BRNpDMvP.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-Bf_boNG2.js.map → EventsTab-BRNpDMvP.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-AbGc1nXP.js → HealthTab-rr5nXpjq.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-AbGc1nXP.js.map → HealthTab-rr5nXpjq.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-BfHHhn5F.js → MaintenanceTab-CtjPWnzN.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-BfHHhn5F.js.map → MaintenanceTab-CtjPWnzN.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DrcWti0-.js → MemoryMapTab-5SnFDIYc.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DrcWti0-.js.map → MemoryMapTab-5SnFDIYc.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-ALiglXQq.js → MetricsTab-4MZfwRhq.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-ALiglXQq.js.map → MetricsTab-4MZfwRhq.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-nnaJdNyR.js → OverviewTab-BAKENnbk.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-nnaJdNyR.js.map → OverviewTab-BAKENnbk.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-BTK0nofM.js → ReposTab-7-z_OmZo.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-BTK0nofM.js.map → ReposTab-7-z_OmZo.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-RXPNqO6x.js → SessionsTab-D_r2ki0y.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-RXPNqO6x.js.map → SessionsTab-D_r2ki0y.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-Dab0AVfs.js → SetupTab-DPujtYgv.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-Dab0AVfs.js.map → SetupTab-DPujtYgv.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DIVG8hdt.js → TimeSavedCard-CLHy30t6.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DIVG8hdt.js.map → TimeSavedCard-CLHy30t6.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-9uz2fBwx.js → TurnsTab-Coul6CHc.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-9uz2fBwx.js.map → TurnsTab-Coul6CHc.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-rqBpWc7A.js +2 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-rqBpWc7A.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{WikiTab-CpcgaggS.js → WikiTab-hryG5oyY.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-CpcgaggS.js.map → WikiTab-hryG5oyY.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-B0Q2fuKY.js → button-Dxo95lRI.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-B0Q2fuKY.js.map → button-Dxo95lRI.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-Cnik3TFr.js → card-FXwhLF_u.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-Cnik3TFr.js.map → card-FXwhLF_u.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-DrSwgn1V.js → generateCategoricalChart-M-GeRM8U.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-DrSwgn1V.js.map → generateCategoricalChart-M-GeRM8U.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-DraytcOY.js → index-BIZZXvoy.js} +3 -3
- package/extensions/dashboard-client/dist/assets/{index-DraytcOY.js.map → index-BIZZXvoy.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/index-CYpzq9Lm.css +1 -0
- package/extensions/dashboard-client/dist/assets/{switch-DVQ1aB9b.js → switch-C-d4tkN1.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-DVQ1aB9b.js.map → switch-C-d4tkN1.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-Dkppx-49.js → toggle-Bfmucdcl.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-Dkppx-49.js.map → toggle-Bfmucdcl.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-CPkjU4Gx.js → useSSE-CxK_wdmk.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-CPkjU4Gx.js.map → useSSE-CxK_wdmk.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +2 -2
- package/extensions/dashboard-client/src/api/vector-cortex.ts +9 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +34 -0
- package/extensions/dashboard-client/src/types/vector-cortex.ts +15 -0
- package/extensions/dashboard-server/api-contracts/vector-cortex.ts +23 -0
- package/extensions/dashboard-server/routes-rag-settings-helpers.ts +6 -0
- package/extensions/dashboard-server/routes-vector-cortex-eval.ts +59 -0
- package/extensions/dashboard-server/routes-vector-cortex-health.ts +273 -0
- package/extensions/dashboard-server/routes-vector-cortex-ledger.ts +92 -0
- package/extensions/dashboard-server/routes-vector-cortex-shared.ts +53 -0
- package/extensions/dashboard-server/routes-vector-cortex-topology.ts +83 -0
- package/extensions/dashboard-server/routes-vector-cortex.ts +12 -431
- package/extensions/dashboard-server/routes.ts +1 -0
- package/extensions/dashboard-server/server.ts +2 -0
- package/package.json +1 -1
- package/src/config/vector-cortex.ts +11 -0
- package/src/config.ts +1 -0
- package/src/vector-cortex/cortex/sqlite.ts +489 -0
- package/src/vector-cortex/cortex/store.ts +202 -0
- package/src/vector-cortex/cortex/types.ts +194 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-DnKY61EJ.js +0 -2
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-DnKY61EJ.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/index-CfvziyBi.css +0 -1
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/cortex/sqlite.ts — cortex derived-record SQLite store, Mode A
|
|
3
|
+
* (VC3A).
|
|
4
|
+
*
|
|
5
|
+
* A self-contained, isolated SQLite store (`node:sqlite` `DatabaseSync`) over its
|
|
6
|
+
* OWN database file — NOT the host `sqlite.db` — so the derived cortex store is a
|
|
7
|
+
* separate additive artifact. Holds the immutable derived `cortex_record_v1`
|
|
8
|
+
* rows keyed by `(source_high_water, algorithm_version, id)` (CTX-KEY-002: the
|
|
9
|
+
* same id at a different algorithm version stays DISTINCT) and the
|
|
10
|
+
* `cortex_generation_v1` generation rows (the digest-pinned derived front).
|
|
11
|
+
*
|
|
12
|
+
* Append enforces composite-key immutability: an exact
|
|
13
|
+
* `(source_high_water, algorithm_version, id)` re-append with the SAME
|
|
14
|
+
* `payload_digest` is acknowledged idempotently; the same key with a DIFFERENT
|
|
15
|
+
* digest is a `CTX_KEY_CONFLICT` (immutable derived records never mutate).
|
|
16
|
+
*
|
|
17
|
+
* Rebuild is deterministic: it sorts records by
|
|
18
|
+
* `(source_high_water, algorithm_version, id)`, verifies every `payload_digest`,
|
|
19
|
+
* computes ONE `root_digest` over the canonical sorted key list (order
|
|
20
|
+
* independent, CTX-REBUILD-003), and writes + activates a new generation.
|
|
21
|
+
*
|
|
22
|
+
* PREVENT-002: every query is parameterized. PREVENT-011: no `any`.
|
|
23
|
+
* PREVENT-PI-004: local filesystem only, no network. No console.log.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { DatabaseSync } from "node:sqlite";
|
|
27
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
28
|
+
import { dirname } from "node:path";
|
|
29
|
+
import { createHash } from "node:crypto";
|
|
30
|
+
import type {
|
|
31
|
+
CortexAppendResult,
|
|
32
|
+
CortexGenerationV1,
|
|
33
|
+
CortexRebuildCode,
|
|
34
|
+
CortexRecordV1,
|
|
35
|
+
} from "./types.js";
|
|
36
|
+
|
|
37
|
+
/** DB row shape for cortex_record_v1 (snake_case columns). */
|
|
38
|
+
interface CortexRecordRow {
|
|
39
|
+
// With readBigInts:true (set in openCortexStore) node:sqlite returns EVERY
|
|
40
|
+
// INTEGER column as bigint — not only values >MAX_SAFE_INTEGER. rowToRecord
|
|
41
|
+
// normalizes via BigInt(...)/Number(...) for both the number|bigint union.
|
|
42
|
+
source_high_water: number | bigint;
|
|
43
|
+
algorithm_version: number;
|
|
44
|
+
id: string;
|
|
45
|
+
kind: string;
|
|
46
|
+
payload_digest: string;
|
|
47
|
+
payload_bytes: Uint8Array | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** DB row shape for cortex_generation_v1 (snake_case columns). */
|
|
51
|
+
interface CortexGenerationRow {
|
|
52
|
+
id: string;
|
|
53
|
+
ordinal: number | bigint;
|
|
54
|
+
source_high_water: number | bigint;
|
|
55
|
+
record_count: number;
|
|
56
|
+
root_digest: string;
|
|
57
|
+
active: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const SCHEMA = `
|
|
61
|
+
CREATE TABLE IF NOT EXISTS cortex_record_v1 (
|
|
62
|
+
source_high_water INTEGER NOT NULL,
|
|
63
|
+
algorithm_version INTEGER NOT NULL,
|
|
64
|
+
id TEXT NOT NULL,
|
|
65
|
+
kind TEXT NOT NULL,
|
|
66
|
+
payload_digest TEXT NOT NULL,
|
|
67
|
+
payload_bytes BLOB NOT NULL,
|
|
68
|
+
PRIMARY KEY (source_high_water, algorithm_version, id)
|
|
69
|
+
) STRICT;
|
|
70
|
+
|
|
71
|
+
CREATE TABLE IF NOT EXISTS cortex_generation_v1 (
|
|
72
|
+
id TEXT PRIMARY KEY,
|
|
73
|
+
ordinal INTEGER NOT NULL,
|
|
74
|
+
source_high_water INTEGER NOT NULL,
|
|
75
|
+
record_count INTEGER NOT NULL,
|
|
76
|
+
root_digest TEXT NOT NULL,
|
|
77
|
+
active INTEGER NOT NULL DEFAULT 0
|
|
78
|
+
) STRICT;
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
/** Default digest for a record when the caller omits one (sha256 over bytes). */
|
|
82
|
+
export function cortexDigest(bytes: Uint8Array): string {
|
|
83
|
+
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Toggle real storage writability on a cortex store connection. `readOnly=true`
|
|
88
|
+
* sets `PRAGMA query_only`, making SQLite itself refuse every INSERT/UPDATE —
|
|
89
|
+
* the genuine storage-failure (SQLITE_FULL-class) path used by the failure
|
|
90
|
+
* injection tests. Reads (SELECT) remain available, so a read-only store still
|
|
91
|
+
* serves the reader and rebuild recovery.
|
|
92
|
+
*/
|
|
93
|
+
export function setStoreReadOnly(db: DatabaseSync, readOnly: boolean): void {
|
|
94
|
+
db.exec(readOnly ? "PRAGMA query_only = ON" : "PRAGMA query_only = OFF");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Open (or reuse) the isolated cortex derived-store DB handle. */
|
|
98
|
+
export function openCortexStore(dbPath: string): DatabaseSync {
|
|
99
|
+
const dir = dirname(dbPath);
|
|
100
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
101
|
+
// readBigInts: return INTEGER columns exceeding Number.MAX_SAFE_INTEGER as BigInt
|
|
102
|
+
// so a caller's `bigint` sourceHighWater/ordinal round-trips exactly (Q06 —
|
|
103
|
+
// never truncate through a `Number()` double).
|
|
104
|
+
const db = new DatabaseSync(dbPath, { readBigInts: true });
|
|
105
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
106
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
107
|
+
db.exec(SCHEMA);
|
|
108
|
+
return db;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function rowToRecord(row: CortexRecordRow): CortexRecordV1 {
|
|
112
|
+
return {
|
|
113
|
+
schema: "cortex-record-v1",
|
|
114
|
+
sourceHighWater: BigInt(row.source_high_water),
|
|
115
|
+
algorithmVersion: Number(row.algorithm_version),
|
|
116
|
+
id: row.id,
|
|
117
|
+
kind: row.kind,
|
|
118
|
+
payloadDigest: row.payload_digest,
|
|
119
|
+
payloadBytes: row.payload_bytes ? Buffer.from(row.payload_bytes) : new Uint8Array(0),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Count accepted derived records. */
|
|
124
|
+
export function countCortexRecords(db: DatabaseSync): number {
|
|
125
|
+
const row = db.prepare(`SELECT COUNT(*) AS cnt FROM cortex_record_v1`).get() as { cnt: number | bigint };
|
|
126
|
+
return typeof row.cnt === "bigint" ? Number(row.cnt) : row.cnt;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Accepted records in ascending (sourceHighWater, algorithmVersion, id). */
|
|
130
|
+
export function readCortexRecords(db: DatabaseSync): CortexRecordV1[] {
|
|
131
|
+
const rows = db
|
|
132
|
+
.prepare(
|
|
133
|
+
`SELECT source_high_water, algorithm_version, id, kind, payload_digest, payload_bytes
|
|
134
|
+
FROM cortex_record_v1
|
|
135
|
+
ORDER BY source_high_water ASC, algorithm_version ASC, id ASC`,
|
|
136
|
+
)
|
|
137
|
+
.all() as unknown as CortexRecordRow[];
|
|
138
|
+
return rows.map(rowToRecord);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The single record at a composite key, or undefined. */
|
|
142
|
+
export function readCortexRecord(
|
|
143
|
+
db: DatabaseSync,
|
|
144
|
+
sourceHighWater: bigint,
|
|
145
|
+
algorithmVersion: number,
|
|
146
|
+
id: string,
|
|
147
|
+
): CortexRecordV1 | undefined {
|
|
148
|
+
const row = db
|
|
149
|
+
.prepare(
|
|
150
|
+
`SELECT source_high_water, algorithm_version, id, kind, payload_digest, payload_bytes
|
|
151
|
+
FROM cortex_record_v1
|
|
152
|
+
WHERE source_high_water = @hw AND algorithm_version = @av AND id = @id LIMIT 1`,
|
|
153
|
+
)
|
|
154
|
+
.get({
|
|
155
|
+
"@hw": sourceHighWater,
|
|
156
|
+
"@av": algorithmVersion,
|
|
157
|
+
"@id": id,
|
|
158
|
+
}) as CortexRecordRow | undefined;
|
|
159
|
+
return row ? rowToRecord(row) : undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Whether an exact composite-key record already exists. */
|
|
163
|
+
function findRecordByKey(
|
|
164
|
+
db: DatabaseSync,
|
|
165
|
+
sourceHighWater: bigint,
|
|
166
|
+
algorithmVersion: number,
|
|
167
|
+
id: string,
|
|
168
|
+
): CortexRecordV1 | undefined {
|
|
169
|
+
return readCortexRecord(db, sourceHighWater, algorithmVersion, id);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Classify an existing-key hit: an exact `(key, digest)` match is an idempotent
|
|
174
|
+
* ack of the immutable record; the same key with a DIFFERENT digest is a
|
|
175
|
+
* `CTX_KEY_CONFLICT` (immutable derived records never mutate).
|
|
176
|
+
*/
|
|
177
|
+
function idempotentOrConflict(
|
|
178
|
+
existing: CortexRecordV1,
|
|
179
|
+
candidate: CortexRecordV1,
|
|
180
|
+
payloadDigest: string,
|
|
181
|
+
): CortexAppendResult {
|
|
182
|
+
if (existing.payloadDigest === payloadDigest) {
|
|
183
|
+
// Idempotent ack of an identical immutable record.
|
|
184
|
+
return { ok: true, record: existing };
|
|
185
|
+
}
|
|
186
|
+
return { ok: false, code: "CTX_KEY_CONFLICT", rejected: candidate };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Append one derived record. Exact `(sourceHighWater, algorithmVersion, id)`
|
|
191
|
+
* with the SAME payloadDigest is acknowledged idempotently (returning the
|
|
192
|
+
* existing immutable row); the same key with a DIFFERENT digest is a conflict.
|
|
193
|
+
* A storage error is surfaced as `CTX_APPEND_FAILED` so the caller's writer can
|
|
194
|
+
* treat it non-fatally. A concurrent unique-constraint race is classified as an
|
|
195
|
+
* idempotent ack / key-conflict (re-checked against the committed row), never
|
|
196
|
+
* misreported as a storage failure.
|
|
197
|
+
*/
|
|
198
|
+
export function insertCortexRecord(
|
|
199
|
+
db: DatabaseSync,
|
|
200
|
+
input: {
|
|
201
|
+
readonly sourceHighWater: bigint;
|
|
202
|
+
readonly algorithmVersion: number;
|
|
203
|
+
readonly id: string;
|
|
204
|
+
readonly kind: string;
|
|
205
|
+
readonly payloadDigest?: string;
|
|
206
|
+
readonly payloadBytes: Uint8Array;
|
|
207
|
+
},
|
|
208
|
+
): CortexAppendResult {
|
|
209
|
+
const payloadDigest = input.payloadDigest ?? cortexDigest(input.payloadBytes);
|
|
210
|
+
const record: CortexRecordV1 = {
|
|
211
|
+
schema: "cortex-record-v1",
|
|
212
|
+
sourceHighWater: input.sourceHighWater,
|
|
213
|
+
algorithmVersion: input.algorithmVersion,
|
|
214
|
+
id: input.id,
|
|
215
|
+
kind: input.kind,
|
|
216
|
+
payloadDigest,
|
|
217
|
+
payloadBytes: input.payloadBytes,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const existing = findRecordByKey(db, input.sourceHighWater, input.algorithmVersion, input.id);
|
|
221
|
+
if (existing) {
|
|
222
|
+
return idempotentOrConflict(existing, record, payloadDigest);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
db.prepare(
|
|
227
|
+
`INSERT INTO cortex_record_v1
|
|
228
|
+
(source_high_water, algorithm_version, id, kind, payload_digest, payload_bytes)
|
|
229
|
+
VALUES (@hw, @av, @id, @kind, @digest, @bytes)`,
|
|
230
|
+
).run({
|
|
231
|
+
"@hw": input.sourceHighWater,
|
|
232
|
+
"@av": input.algorithmVersion,
|
|
233
|
+
"@id": input.id,
|
|
234
|
+
"@kind": input.kind,
|
|
235
|
+
"@digest": payloadDigest,
|
|
236
|
+
"@bytes": Buffer.from(input.payloadBytes),
|
|
237
|
+
});
|
|
238
|
+
} catch {
|
|
239
|
+
// A UNIQUE/PK constraint race (two writers pass the SELECT above, the loser
|
|
240
|
+
// hits the composite PK) is semantically an idempotent ack or a key conflict
|
|
241
|
+
// — NOT a storage failure — so re-check the now-committed row and classify it
|
|
242
|
+
// correctly instead of misreporting CTX_APPEND_FAILED. Any other error is a
|
|
243
|
+
// genuine non-fatal storage failure (e.g. SQLITE_FULL) and surfaces as-is.
|
|
244
|
+
const recheck = readCortexRecord(db, input.sourceHighWater, input.algorithmVersion, input.id);
|
|
245
|
+
if (recheck) {
|
|
246
|
+
return idempotentOrConflict(recheck, record, payloadDigest);
|
|
247
|
+
}
|
|
248
|
+
return { ok: false, code: "CTX_APPEND_FAILED", rejected: record };
|
|
249
|
+
}
|
|
250
|
+
return { ok: true, record };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* ONE deterministic root digest over the canonical sorted key list. Records are
|
|
255
|
+
* sorted by `(sourceHighWater, algorithmVersion, id)`; each key contributes
|
|
256
|
+
* `sourceHighWater|algorithmVersion|id|kind|payloadDigest`. Order-independent
|
|
257
|
+
* (CTX-REBUILD-003): shuffled insertion yields an identical root digest.
|
|
258
|
+
*/
|
|
259
|
+
export function generationRootDigest(records: readonly CortexRecordV1[]): string {
|
|
260
|
+
const sorted = [...records].sort(cmpRecordKey);
|
|
261
|
+
const h = createHash("sha256");
|
|
262
|
+
for (const r of sorted) {
|
|
263
|
+
h.update(`${r.sourceHighWater.toString()}|${r.algorithmVersion}|${r.id}|${r.kind}|${r.payloadDigest}\n`);
|
|
264
|
+
}
|
|
265
|
+
return h.digest("hex");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Stable record ordering by the composite key. */
|
|
269
|
+
export function cmpRecordKey(a: CortexRecordV1, b: CortexRecordV1): number {
|
|
270
|
+
if (a.sourceHighWater < b.sourceHighWater) return -1;
|
|
271
|
+
if (a.sourceHighWater > b.sourceHighWater) return 1;
|
|
272
|
+
if (a.algorithmVersion < b.algorithmVersion) return -1;
|
|
273
|
+
if (a.algorithmVersion > b.algorithmVersion) return 1;
|
|
274
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** The active generation, or undefined. */
|
|
278
|
+
export function activeGeneration(db: DatabaseSync): CortexGenerationV1 | undefined {
|
|
279
|
+
const row = db
|
|
280
|
+
.prepare(
|
|
281
|
+
`SELECT id, ordinal, source_high_water, record_count, root_digest, active
|
|
282
|
+
FROM cortex_generation_v1 WHERE active = 1 LIMIT 1`,
|
|
283
|
+
)
|
|
284
|
+
.get() as CortexGenerationRow | undefined;
|
|
285
|
+
return row ? rowToGeneration(row) : undefined;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** All generations in ascending ordinal order (evidence retained). */
|
|
289
|
+
export function listCortexGenerations(db: DatabaseSync): CortexGenerationV1[] {
|
|
290
|
+
const rows = db
|
|
291
|
+
.prepare(
|
|
292
|
+
`SELECT id, ordinal, source_high_water, record_count, root_digest, active
|
|
293
|
+
FROM cortex_generation_v1 ORDER BY ordinal ASC`,
|
|
294
|
+
)
|
|
295
|
+
.all() as unknown as CortexGenerationRow[];
|
|
296
|
+
return rows.map(rowToGeneration);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** A generation by id, or undefined. */
|
|
300
|
+
function generationById(db: DatabaseSync, generationId: string): CortexGenerationV1 | undefined {
|
|
301
|
+
const row = db
|
|
302
|
+
.prepare(
|
|
303
|
+
`SELECT id, ordinal, source_high_water, record_count, root_digest, active
|
|
304
|
+
FROM cortex_generation_v1 WHERE id = @id LIMIT 1`,
|
|
305
|
+
)
|
|
306
|
+
.get({ "@id": generationId }) as CortexGenerationRow | undefined;
|
|
307
|
+
return row ? rowToGeneration(row) : undefined;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The existing generation carrying the SAME rootDigest and recordCount, or
|
|
312
|
+
* undefined. Deterministic rebuild of unchanged inputs must REUSE the identical
|
|
313
|
+
* generation already recorded durably rather than append a fresh duplicate
|
|
314
|
+
* (idempotency) — this scan is what makes that reuse reachable, since the
|
|
315
|
+
* ordinal-derived id is always new in the no-opts flow.
|
|
316
|
+
*/
|
|
317
|
+
function generationByRootDigest(
|
|
318
|
+
db: DatabaseSync,
|
|
319
|
+
rootDigest: string,
|
|
320
|
+
recordCount: number,
|
|
321
|
+
): CortexGenerationV1 | undefined {
|
|
322
|
+
const row = db
|
|
323
|
+
.prepare(
|
|
324
|
+
`SELECT id, ordinal, source_high_water, record_count, root_digest, active
|
|
325
|
+
FROM cortex_generation_v1
|
|
326
|
+
WHERE root_digest = @digest AND record_count = @count LIMIT 1`,
|
|
327
|
+
)
|
|
328
|
+
.get({ "@digest": rootDigest, "@count": recordCount }) as CortexGenerationRow | undefined;
|
|
329
|
+
return row ? rowToGeneration(row) : undefined;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function rowToGeneration(row: CortexGenerationRow): CortexGenerationV1 {
|
|
333
|
+
return {
|
|
334
|
+
schema: "cortex-generation-v1",
|
|
335
|
+
id: row.id,
|
|
336
|
+
sourceHighWater: BigInt(row.source_high_water),
|
|
337
|
+
recordCount: typeof row.record_count === "bigint" ? Number(row.record_count) : row.record_count,
|
|
338
|
+
rootDigest: row.root_digest,
|
|
339
|
+
ordinal: BigInt(row.ordinal),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Deterministically rebuild a generation from ALL accepted records. Sorts keys,
|
|
345
|
+
* verifies each payload digest (CTX_PAYLOAD_DIGEST_MISMATCH on corruption),
|
|
346
|
+
* computes ONE root digest, and writes + activates a new generation with an
|
|
347
|
+
* incrementing ordinal. Does NOT delete prior generations (evidence retained).
|
|
348
|
+
*
|
|
349
|
+
* Idempotency (DTST-CORE invariant): a rebuild of UNCHANGED accepted inputs
|
|
350
|
+
* (same sorted set → same rootDigest + recordCount) REUSES the identical
|
|
351
|
+
* generation already recorded durably — it activates and returns that existing
|
|
352
|
+
* generation instead of appending a fresh duplicate row, so repeated rebuilds
|
|
353
|
+
* with no record changes yield one generation, not gen-1, gen-2, ... carrying
|
|
354
|
+
* the same digest.
|
|
355
|
+
*
|
|
356
|
+
* When `opts.authorityHighWater` is supplied, the derived frontier (max record
|
|
357
|
+
* sourceHighWater) must not exceed the contiguous durable authority high-water
|
|
358
|
+
* (normative in CONTRACTS.md); an over-run is rejected with
|
|
359
|
+
* `CTX_HIGH_WATER_EXCEEDED` and NO generation is written.
|
|
360
|
+
*
|
|
361
|
+
* A rebuild only reports `ok:true` when the generation INSERT + activate actually
|
|
362
|
+
* persisted. If the write degrades (any storage failure, e.g. SQLITE_FULL via
|
|
363
|
+
* `PRAGMA query_only`), it returns `ok:false, code:"CTX_REBUILD_FAILED"` — never
|
|
364
|
+
* a fabricated generation that has no durable row. That non-ok result is what
|
|
365
|
+
* prevents the caller from emitting a misleading `vector_cortex_generation_rebuilt`
|
|
366
|
+
* event or exposing a generation id that does not exist.
|
|
367
|
+
*
|
|
368
|
+
* The reuse path (a prior generation already carries the same rootDigest +
|
|
369
|
+
* recordCount) also runs its activation through the SAME error-to-CTX_REBUILD_FAILED
|
|
370
|
+
* conversion: under read-only storage (`PRAGMA query_only`) the activation UPDATE
|
|
371
|
+
* is refused by SQLite, and the rebuild must surface `CTX_REBUILD_FAILED` rather
|
|
372
|
+
* than let a raw SQLITE_READONLY escape to `store.admin().rebuild()` and break
|
|
373
|
+
* the agent loop. Reads remain available, so the durable generation is intact.
|
|
374
|
+
*/
|
|
375
|
+
export function rebuildCortexGeneration(
|
|
376
|
+
db: DatabaseSync,
|
|
377
|
+
opts: {
|
|
378
|
+
readonly generationOrdinal?: bigint;
|
|
379
|
+
readonly authorityHighWater?: bigint;
|
|
380
|
+
} = {},
|
|
381
|
+
): { ok: true; generation: CortexGenerationV1 } | { ok: false; code: CortexRebuildCode } {
|
|
382
|
+
const records = readCortexRecords(db);
|
|
383
|
+
const sorted = [...records].sort(cmpRecordKey);
|
|
384
|
+
// Verify every record digest against its immutable payload bytes.
|
|
385
|
+
for (const r of sorted) {
|
|
386
|
+
if (r.payloadDigest !== cortexDigest(r.payloadBytes)) {
|
|
387
|
+
return { ok: false, code: "CTX_PAYLOAD_DIGEST_MISMATCH" };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const rootDigest = generationRootDigest(sorted);
|
|
391
|
+
const sourceHighWater = sorted.length > 0 ? sorted[sorted.length - 1]!.sourceHighWater : 0n;
|
|
392
|
+
|
|
393
|
+
// Normative derived-frontier invariant (CONTRACTS.md): the derived frontier can
|
|
394
|
+
// never exceed the contiguous durable authority high-water. Reject the rebuild
|
|
395
|
+
// (write nothing) when a bound is supplied and the derived records outrun it.
|
|
396
|
+
if (opts.authorityHighWater !== undefined && sourceHighWater > opts.authorityHighWater) {
|
|
397
|
+
return { ok: false, code: "CTX_HIGH_WATER_EXCEEDED" };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const sortedCount = sorted.length;
|
|
401
|
+
// Deterministic rebuild of unchanged inputs must REUSE the identical generation
|
|
402
|
+
// already recorded durably (idempotent, no duplicate generation bloat). If a
|
|
403
|
+
// prior generation carries the same rootDigest + recordCount, activate it and
|
|
404
|
+
// return it instead of appending a fresh duplicate row.
|
|
405
|
+
const reused = generationByRootDigest(db, rootDigest, sortedCount);
|
|
406
|
+
if (reused) {
|
|
407
|
+
// The generation row ALREADY exists durably (reads still work under
|
|
408
|
+
// `PRAGMA query_only`), but activating it is a write. Guard it the same way
|
|
409
|
+
// as the insert path so a storage failure (e.g. SQLITE_READONLY) degrades to
|
|
410
|
+
// CTX_REBUILD_FAILED instead of escaping uncaught into admin.rebuild().
|
|
411
|
+
try {
|
|
412
|
+
activateGeneration(db, reused.id);
|
|
413
|
+
} catch {
|
|
414
|
+
return { ok: false, code: "CTX_REBUILD_FAILED" };
|
|
415
|
+
}
|
|
416
|
+
return { ok: true, generation: reused };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const existing = listCortexGenerations(db);
|
|
420
|
+
const base = opts.generationOrdinal ?? (existing.length ? existing[existing.length - 1]!.ordinal + 1n : 1n);
|
|
421
|
+
const ordinal = base > 0n ? base : 1n;
|
|
422
|
+
const generationId = `gen-${ordinal.toString()}`;
|
|
423
|
+
|
|
424
|
+
db.exec("SAVEPOINT mc_cortex_gen");
|
|
425
|
+
try {
|
|
426
|
+
db.prepare(
|
|
427
|
+
`INSERT INTO cortex_generation_v1
|
|
428
|
+
(id, ordinal, source_high_water, record_count, root_digest, active)
|
|
429
|
+
VALUES (@id, @ordinal, @hw, @count, @digest, 0)`,
|
|
430
|
+
).run({
|
|
431
|
+
"@id": generationId,
|
|
432
|
+
"@ordinal": ordinal,
|
|
433
|
+
"@hw": sourceHighWater,
|
|
434
|
+
"@count": sortedCount,
|
|
435
|
+
"@digest": rootDigest,
|
|
436
|
+
});
|
|
437
|
+
activateGeneration(db, generationId);
|
|
438
|
+
db.exec("RELEASE mc_cortex_gen");
|
|
439
|
+
} catch {
|
|
440
|
+
try {
|
|
441
|
+
db.exec("ROLLBACK TO mc_cortex_gen");
|
|
442
|
+
} catch {
|
|
443
|
+
/* savepoint may already be gone on a failed exec */
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
db.exec("RELEASE mc_cortex_gen");
|
|
447
|
+
} catch {
|
|
448
|
+
/* savepoint may already be gone */
|
|
449
|
+
}
|
|
450
|
+
// The generation INSERT/activate did not persist. Report the non-fatal
|
|
451
|
+
// storage failure — never a fabricated generation that has no durable row.
|
|
452
|
+
return { ok: false, code: "CTX_REBUILD_FAILED" };
|
|
453
|
+
}
|
|
454
|
+
const gen = generationById(db, generationId);
|
|
455
|
+
if (!gen) {
|
|
456
|
+
// Defensive: no durable row is visible, so this rebuild did not persist.
|
|
457
|
+
return { ok: false, code: "CTX_REBUILD_FAILED" };
|
|
458
|
+
}
|
|
459
|
+
return { ok: true, generation: gen };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Switch the active generation pointer (evidence retained; nothing deleted). */
|
|
463
|
+
export function switchCortexGeneration(db: DatabaseSync, generationId: string): { ok: boolean; code?: string } {
|
|
464
|
+
if (!generationById(db, generationId)) return { ok: false, code: "CTX_GENERATION_NOT_FOUND" };
|
|
465
|
+
// Activating writes to cortex_generation_v1.active; under read-only storage
|
|
466
|
+
// (`PRAGMA query_only`) SQLite refuses the UPDATE. Convert that storage failure
|
|
467
|
+
// into the documented `{ok:false}` result so a caller of the typed admin surface
|
|
468
|
+
// never receives an uncaught SQLITE_READONLY exception (Q02 — every store
|
|
469
|
+
// mutation is non-fatal).
|
|
470
|
+
try {
|
|
471
|
+
activateGeneration(db, generationId);
|
|
472
|
+
} catch {
|
|
473
|
+
return { ok: false, code: "CTX_SWITCH_FAILED" };
|
|
474
|
+
}
|
|
475
|
+
return { ok: true };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function activateGeneration(db: DatabaseSync, generationId: string): void {
|
|
479
|
+
db.prepare(`UPDATE cortex_generation_v1 SET active = 0 WHERE active = 1`).run();
|
|
480
|
+
db.prepare(`UPDATE cortex_generation_v1 SET active = 1 WHERE id = @id`).run({ "@id": generationId });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** Highest sourceHighWater across all accepted records (the derived frontier). */
|
|
484
|
+
export function maxSourceHighWater(db: DatabaseSync): bigint {
|
|
485
|
+
const row = db
|
|
486
|
+
.prepare(`SELECT MAX(source_high_water) AS m FROM cortex_record_v1`)
|
|
487
|
+
.get() as { m: number | bigint | null };
|
|
488
|
+
return row.m === null ? 0n : BigInt(row.m);
|
|
489
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/cortex/store.ts — VC3A capability-separated cortex store contracts.
|
|
3
|
+
*
|
|
4
|
+
* Owns the `createCortexStore` factory producing `CortexReader` / `CortexWriter`
|
|
5
|
+
* / `CortexAdmin` capability views over the isolated cortex SQLite store. The
|
|
6
|
+
* writer exposes append only, the reader exposes query only, and the admin alone
|
|
7
|
+
* can rebuild/switch generations (task 2). No callbacks or event emitters flow
|
|
8
|
+
* from the store; writes are non-fatal (a failure logs and never breaks the agent
|
|
9
|
+
* loop).
|
|
10
|
+
*
|
|
11
|
+
* The two VC3A events (task 5) are emitted by this store:
|
|
12
|
+
*
|
|
13
|
+
* vector_cortex_record_append_failed — an append was rejected / failed
|
|
14
|
+
* vector_cortex_generation_rebuilt — the admin rebuilt + activated a generation
|
|
15
|
+
*
|
|
16
|
+
* Both are gated on `MEGACOMPACT_VC3A` so the flag-OFF path emits zero events
|
|
17
|
+
* (mode C parity, byte-identical predecessor).
|
|
18
|
+
*
|
|
19
|
+
* Capability gating mirrors the host `asReader/asWriter/asAdmin` and VC1B ledger
|
|
20
|
+
* pattern. Each consumer receives ONLY what it needs. The dashboard reader-only
|
|
21
|
+
* GET is built on the reader surface alone.
|
|
22
|
+
*
|
|
23
|
+
* Pi-agnostic. No console.log. No network (PREVENT-PI-004). No `any`
|
|
24
|
+
* (PREVENT-011).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { VC3A_ENABLED } from "../../config/vector-cortex.js";
|
|
28
|
+
import { Logger } from "../../log.js";
|
|
29
|
+
import type {
|
|
30
|
+
CortexAdmin,
|
|
31
|
+
CortexAppendInput,
|
|
32
|
+
CortexGenerationV1,
|
|
33
|
+
CortexReader,
|
|
34
|
+
CortexTopologySummary,
|
|
35
|
+
CortexWriter,
|
|
36
|
+
} from "./types.js";
|
|
37
|
+
import {
|
|
38
|
+
openCortexStore,
|
|
39
|
+
insertCortexRecord,
|
|
40
|
+
readCortexRecord,
|
|
41
|
+
countCortexRecords,
|
|
42
|
+
readCortexRecords,
|
|
43
|
+
rebuildCortexGeneration,
|
|
44
|
+
switchCortexGeneration,
|
|
45
|
+
activeGeneration,
|
|
46
|
+
listCortexGenerations,
|
|
47
|
+
maxSourceHighWater,
|
|
48
|
+
} from "./sqlite.js";
|
|
49
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
50
|
+
|
|
51
|
+
/** Optional structured-event emitter (same shape as the other VC seams). */
|
|
52
|
+
export type CortexEmit = (event: string, fields: Record<string, unknown>) => void;
|
|
53
|
+
|
|
54
|
+
/** The token that gates closures to a single capability (PREVENT-011-free). */
|
|
55
|
+
const _capability: unique symbol = Symbol("mc-cortex-capability");
|
|
56
|
+
|
|
57
|
+
interface ReaderToken {
|
|
58
|
+
readonly [_capability]: "reader";
|
|
59
|
+
}
|
|
60
|
+
interface WriterToken {
|
|
61
|
+
readonly [_capability]: "writer";
|
|
62
|
+
}
|
|
63
|
+
interface AdminToken {
|
|
64
|
+
readonly [_capability]: "admin";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A capability-gated cortex handle: access only what you were handed. */
|
|
68
|
+
export interface CortexHandle {
|
|
69
|
+
readonly reader: () => CortexReader & ReaderToken;
|
|
70
|
+
readonly writer: () => CortexWriter & WriterToken;
|
|
71
|
+
readonly admin: () => CortexAdmin & AdminToken;
|
|
72
|
+
/** Close the underlying DB handle (test/sandbox teardown). */
|
|
73
|
+
readonly close: () => void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create the capability-separated cortex derived store over its OWN isolated
|
|
78
|
+
* SQLite DB. `emit` is optional; the two VC3A events are only emitted when
|
|
79
|
+
* `VC3A_ENABLED()` and an emitter is supplied (mode-C parity: flag OFF / no
|
|
80
|
+
* emitter => zero observability writes).
|
|
81
|
+
*
|
|
82
|
+
* Normalization: `stateDir` gives the standard daemon location
|
|
83
|
+
* `<stateDir>/vector-cortex/cortex.db`; a bare `dbPath` overrides it
|
|
84
|
+
* (tests/rehearsal isolate the cortex store). An injected `db` (an already-open
|
|
85
|
+
* `DatabaseSync`) is a dependency-injection seam used by the failure-injection
|
|
86
|
+
* tests to drive the REAL storage-refusal path (e.g. `PRAGMA query_only`) through
|
|
87
|
+
* the store's own writer — a genuine store, no mock.
|
|
88
|
+
*/
|
|
89
|
+
export function createCortexStore(
|
|
90
|
+
opts:
|
|
91
|
+
| { readonly stateDir: string }
|
|
92
|
+
| { readonly dbPath: string }
|
|
93
|
+
| { readonly db: DatabaseSync },
|
|
94
|
+
emit?: CortexEmit,
|
|
95
|
+
): CortexHandle {
|
|
96
|
+
const db =
|
|
97
|
+
"db" in opts
|
|
98
|
+
? opts.db
|
|
99
|
+
: openCortexStore(
|
|
100
|
+
"dbPath" in opts
|
|
101
|
+
? opts.dbPath
|
|
102
|
+
: `${opts.stateDir}/vector-cortex/cortex.db`,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// A caller that invokes the writer/admin seam without injecting an emitter gets
|
|
106
|
+
// a REAL default producer (structured logger) so telemetry is never silently
|
|
107
|
+
// dropped — an explicit `emit:` replaces it. The dashboard reader-only route
|
|
108
|
+
// never appends/rebuilds, so it fires nothing regardless; a future writer/admin
|
|
109
|
+
// host integration enjoys default observability out of the box (VC3B wiring).
|
|
110
|
+
const sink = emit ?? defaultEmitFor();
|
|
111
|
+
const fire = (event: string, fields: Record<string, unknown>): void => {
|
|
112
|
+
if (!VC3A_ENABLED()) return;
|
|
113
|
+
try {
|
|
114
|
+
sink(event, fields);
|
|
115
|
+
} catch {
|
|
116
|
+
/* non-fatal observability — never break the agent loop */
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const asReader = (): CortexReader & ReaderToken => ({
|
|
121
|
+
kind: "CortexReader",
|
|
122
|
+
[_capability]: "reader" as const,
|
|
123
|
+
recordCount: () => countCortexRecords(db),
|
|
124
|
+
readRecords: () => readCortexRecords(db),
|
|
125
|
+
readRecord: (sourceHighWater, algorithmVersion, id) =>
|
|
126
|
+
readCortexRecord(db, sourceHighWater, algorithmVersion, id),
|
|
127
|
+
latestGeneration: () => activeGeneration(db),
|
|
128
|
+
topologySummary: () => topologyOf(db, VC3A_ENABLED()),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const asWriter = (): CortexWriter & WriterToken => ({
|
|
132
|
+
kind: "CortexWriter",
|
|
133
|
+
[_capability]: "writer" as const,
|
|
134
|
+
append(input) {
|
|
135
|
+
const result = insertCortexRecord(db, input);
|
|
136
|
+
// Any rejected/failed append (CTX_KEY_CONFLICT, CTX_APPEND_FAILED, or any
|
|
137
|
+
// future code) surfaces the SAME event; the `code` field distinguishes a
|
|
138
|
+
// key-conflict (rejected append) from a storage failure (CTX_APPEND_FAILED).
|
|
139
|
+
if (!result.ok) {
|
|
140
|
+
fire("vector_cortex_record_append_failed", {
|
|
141
|
+
sourceHighWater: input.sourceHighWater.toString(),
|
|
142
|
+
algorithmVersion: input.algorithmVersion,
|
|
143
|
+
id: input.id,
|
|
144
|
+
kind: input.kind,
|
|
145
|
+
code: result.code,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const asAdmin = (): CortexAdmin & AdminToken => ({
|
|
153
|
+
kind: "CortexAdmin",
|
|
154
|
+
[_capability]: "admin" as const,
|
|
155
|
+
rebuild(authorityHighWater?: bigint) {
|
|
156
|
+
const result = rebuildCortexGeneration(db, { authorityHighWater });
|
|
157
|
+
if (result.ok) {
|
|
158
|
+
fire("vector_cortex_generation_rebuilt", {
|
|
159
|
+
generationId: result.generation.id,
|
|
160
|
+
ordinal: result.generation.ordinal.toString(),
|
|
161
|
+
sourceHighWater: result.generation.sourceHighWater.toString(),
|
|
162
|
+
recordCount: result.generation.recordCount,
|
|
163
|
+
rootDigest: result.generation.rootDigest,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
},
|
|
168
|
+
switchGeneration: (generationId) => switchCortexGeneration(db, generationId),
|
|
169
|
+
listGenerations: () => listCortexGenerations(db),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return { reader: asReader, writer: asWriter, admin: asAdmin, close: () => db.close() };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Build the reader-only topology summary (the dashboard GET payload). */
|
|
176
|
+
function topologyOf(db: DatabaseSync, enabled: boolean): CortexTopologySummary {
|
|
177
|
+
const gen = activeGeneration(db);
|
|
178
|
+
return {
|
|
179
|
+
enabled,
|
|
180
|
+
generationId: gen ? gen.id : null,
|
|
181
|
+
rootDigest: gen ? gen.rootDigest : null,
|
|
182
|
+
sourceHighWater: (gen ? gen.sourceHighWater : maxSourceHighWater(db)).toString(),
|
|
183
|
+
recordCount: countCortexRecords(db),
|
|
184
|
+
ordinal: gen ? gen.ordinal.toString() : null,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* A deterministic default emitter backed by the structured logger. Supplying an
|
|
190
|
+
* explicit `emit:` replaces it. Making the default a REAL producer means a caller
|
|
191
|
+
* that just invokes the writer/admin seam without injecting an emitter still
|
|
192
|
+
* yields structured telemetry instead of silently dropping every event.
|
|
193
|
+
*/
|
|
194
|
+
function defaultEmitFor(): CortexEmit {
|
|
195
|
+
const logger = new Logger();
|
|
196
|
+
return (event, fields) => {
|
|
197
|
+
logger.info(event, fields);
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Re-export the generation type for convenience in admin consumers. */
|
|
202
|
+
export type { CortexGenerationV1, CortexAppendInput };
|