pi-mega-compact 0.18.1 → 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.
Files changed (108) hide show
  1. package/assets/vector-cortex/encoder-v1/manifest.json +1 -0
  2. package/assets/vector-cortex/encoder-v1/model-card.json +1 -0
  3. package/assets/vector-cortex/encoder-v1/model.onnx +0 -0
  4. package/assets/vector-cortex/encoder-v1/tokenizer.json +1 -0
  5. package/dist/config/vector-cortex.js +19 -0
  6. package/dist/config.js +1 -1
  7. package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +2 -0
  8. package/dist/extensions/dashboard-server/routes-vector-cortex-eval.js +49 -0
  9. package/dist/extensions/dashboard-server/routes-vector-cortex-health.js +233 -0
  10. package/dist/extensions/dashboard-server/routes-vector-cortex-ledger.js +82 -0
  11. package/dist/extensions/dashboard-server/routes-vector-cortex-shared.js +46 -0
  12. package/dist/extensions/dashboard-server/routes-vector-cortex-topology.js +68 -0
  13. package/dist/extensions/dashboard-server/routes-vector-cortex.js +12 -285
  14. package/dist/extensions/dashboard-server/routes.js +1 -1
  15. package/dist/extensions/dashboard-server/server.js +3 -1
  16. package/dist/src/config/vector-cortex.js +19 -0
  17. package/dist/src/config.js +1 -1
  18. package/dist/src/vector-cortex/cortex/sqlite.js +393 -0
  19. package/dist/src/vector-cortex/cortex/store.js +139 -0
  20. package/dist/src/vector-cortex/cortex/types.js +44 -0
  21. package/dist/src/vector-cortex/encoder/calibrate.js +174 -0
  22. package/dist/src/vector-cortex/encoder/emit-vc2c.js +59 -0
  23. package/dist/src/vector-cortex/encoder/fallback.js +62 -0
  24. package/dist/src/vector-cortex/encoder/select.js +172 -0
  25. package/dist/src/vector-cortex/encoder/types.js +31 -0
  26. package/dist/vector-cortex/cortex/sqlite.js +393 -0
  27. package/dist/vector-cortex/cortex/store.js +139 -0
  28. package/dist/vector-cortex/cortex/types.js +44 -0
  29. package/dist/vector-cortex/encoder/calibrate.js +174 -0
  30. package/dist/vector-cortex/encoder/emit-vc2c.js +59 -0
  31. package/dist/vector-cortex/encoder/fallback.js +62 -0
  32. package/dist/vector-cortex/encoder/select.js +172 -0
  33. package/dist/vector-cortex/encoder/types.js +31 -0
  34. package/extensions/dashboard-client/dist/assets/{AreaChart-BQXFF7UI.js → AreaChart-B9KYKrUE.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{AreaChart-BQXFF7UI.js.map → AreaChart-B9KYKrUE.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{BarChart-BVtoz9Eg.js → BarChart-D1sWkR6j.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{BarChart-BVtoz9Eg.js.map → BarChart-D1sWkR6j.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{CacheTab-trI2TIGy.js → CacheTab-BR8MYcJq.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{CacheTab-trI2TIGy.js.map → CacheTab-BR8MYcJq.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/{EventsTab-CNUbk9__.js → EventsTab-BRNpDMvP.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{EventsTab-CNUbk9__.js.map → EventsTab-BRNpDMvP.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/{HealthTab-K-bd_CM-.js → HealthTab-rr5nXpjq.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{HealthTab-K-bd_CM-.js.map → HealthTab-rr5nXpjq.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-CoEioKzx.js → MaintenanceTab-CtjPWnzN.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-CoEioKzx.js.map → MaintenanceTab-CtjPWnzN.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CJo24ulv.js → MemoryMapTab-5SnFDIYc.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CJo24ulv.js.map → MemoryMapTab-5SnFDIYc.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/{MetricsTab-DCNlQWWP.js → MetricsTab-4MZfwRhq.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{MetricsTab-DCNlQWWP.js.map → MetricsTab-4MZfwRhq.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/assets/{OverviewTab-CQQlyIQ6.js → OverviewTab-BAKENnbk.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{OverviewTab-CQQlyIQ6.js.map → OverviewTab-BAKENnbk.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/{ReposTab-Cqm82_yn.js → ReposTab-7-z_OmZo.js} +2 -2
  53. package/extensions/dashboard-client/dist/assets/{ReposTab-Cqm82_yn.js.map → ReposTab-7-z_OmZo.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/{SessionsTab-A8xTAdET.js → SessionsTab-D_r2ki0y.js} +2 -2
  55. package/extensions/dashboard-client/dist/assets/{SessionsTab-A8xTAdET.js.map → SessionsTab-D_r2ki0y.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/{SetupTab-CqaSgaZl.js → SetupTab-DPujtYgv.js} +2 -2
  57. package/extensions/dashboard-client/dist/assets/{SetupTab-CqaSgaZl.js.map → SetupTab-DPujtYgv.js.map} +1 -1
  58. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-C_f3q8GR.js → TimeSavedCard-CLHy30t6.js} +2 -2
  59. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-C_f3q8GR.js.map → TimeSavedCard-CLHy30t6.js.map} +1 -1
  60. package/extensions/dashboard-client/dist/assets/{TurnsTab-BEsafACo.js → TurnsTab-Coul6CHc.js} +2 -2
  61. package/extensions/dashboard-client/dist/assets/{TurnsTab-BEsafACo.js.map → TurnsTab-Coul6CHc.js.map} +1 -1
  62. package/extensions/dashboard-client/dist/assets/VectorCortexTab-rqBpWc7A.js +2 -0
  63. package/extensions/dashboard-client/dist/assets/VectorCortexTab-rqBpWc7A.js.map +1 -0
  64. package/extensions/dashboard-client/dist/assets/{WikiTab-Bf01M2_Z.js → WikiTab-hryG5oyY.js} +2 -2
  65. package/extensions/dashboard-client/dist/assets/{WikiTab-Bf01M2_Z.js.map → WikiTab-hryG5oyY.js.map} +1 -1
  66. package/extensions/dashboard-client/dist/assets/{button-BEJPTk_E.js → button-Dxo95lRI.js} +2 -2
  67. package/extensions/dashboard-client/dist/assets/{button-BEJPTk_E.js.map → button-Dxo95lRI.js.map} +1 -1
  68. package/extensions/dashboard-client/dist/assets/{card-DK1V382s.js → card-FXwhLF_u.js} +2 -2
  69. package/extensions/dashboard-client/dist/assets/{card-DK1V382s.js.map → card-FXwhLF_u.js.map} +1 -1
  70. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-CRtaPphL.js → generateCategoricalChart-M-GeRM8U.js} +2 -2
  71. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-CRtaPphL.js.map → generateCategoricalChart-M-GeRM8U.js.map} +1 -1
  72. package/extensions/dashboard-client/dist/assets/{index-CB6BslE4.js → index-BIZZXvoy.js} +3 -3
  73. package/extensions/dashboard-client/dist/assets/{index-CB6BslE4.js.map → index-BIZZXvoy.js.map} +1 -1
  74. package/extensions/dashboard-client/dist/assets/index-CYpzq9Lm.css +1 -0
  75. package/extensions/dashboard-client/dist/assets/{switch-C4FV6B9k.js → switch-C-d4tkN1.js} +2 -2
  76. package/extensions/dashboard-client/dist/assets/{switch-C4FV6B9k.js.map → switch-C-d4tkN1.js.map} +1 -1
  77. package/extensions/dashboard-client/dist/assets/{toggle-GBmeENMd.js → toggle-Bfmucdcl.js} +2 -2
  78. package/extensions/dashboard-client/dist/assets/{toggle-GBmeENMd.js.map → toggle-Bfmucdcl.js.map} +1 -1
  79. package/extensions/dashboard-client/dist/assets/{useSSE-BOKZGSn1.js → useSSE-CxK_wdmk.js} +2 -2
  80. package/extensions/dashboard-client/dist/assets/{useSSE-BOKZGSn1.js.map → useSSE-CxK_wdmk.js.map} +1 -1
  81. package/extensions/dashboard-client/dist/index.html +2 -2
  82. package/extensions/dashboard-client/src/api/vector-cortex.ts +9 -0
  83. package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +36 -0
  84. package/extensions/dashboard-client/src/types/vector-cortex.ts +26 -0
  85. package/extensions/dashboard-server/api-contracts/vector-cortex.ts +34 -0
  86. package/extensions/dashboard-server/routes-rag-settings-helpers.ts +12 -0
  87. package/extensions/dashboard-server/routes-vector-cortex-eval.ts +59 -0
  88. package/extensions/dashboard-server/routes-vector-cortex-health.ts +273 -0
  89. package/extensions/dashboard-server/routes-vector-cortex-ledger.ts +92 -0
  90. package/extensions/dashboard-server/routes-vector-cortex-shared.ts +53 -0
  91. package/extensions/dashboard-server/routes-vector-cortex-topology.ts +83 -0
  92. package/extensions/dashboard-server/routes-vector-cortex.ts +12 -324
  93. package/extensions/dashboard-server/routes.ts +1 -0
  94. package/extensions/dashboard-server/server.ts +2 -0
  95. package/package.json +2 -1
  96. package/src/config/vector-cortex.ts +21 -0
  97. package/src/config.ts +2 -0
  98. package/src/vector-cortex/cortex/sqlite.ts +489 -0
  99. package/src/vector-cortex/cortex/store.ts +202 -0
  100. package/src/vector-cortex/cortex/types.ts +194 -0
  101. package/src/vector-cortex/encoder/calibrate.ts +225 -0
  102. package/src/vector-cortex/encoder/emit-vc2c.ts +70 -0
  103. package/src/vector-cortex/encoder/fallback.ts +99 -0
  104. package/src/vector-cortex/encoder/select.ts +237 -0
  105. package/src/vector-cortex/encoder/types.ts +116 -0
  106. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DD1CMK80.js +0 -2
  107. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DD1CMK80.js.map +0 -1
  108. 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 };