pi-mega-compact 0.19.0 → 0.19.2

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 (101) hide show
  1. package/dist/config/vector-cortex.js +19 -0
  2. package/dist/config.js +1 -1
  3. package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +2 -0
  4. package/dist/extensions/dashboard-server/routes-vector-cortex-eval.js +49 -0
  5. package/dist/extensions/dashboard-server/routes-vector-cortex-health.js +233 -0
  6. package/dist/extensions/dashboard-server/routes-vector-cortex-ledger.js +82 -0
  7. package/dist/extensions/dashboard-server/routes-vector-cortex-shared.js +46 -0
  8. package/dist/extensions/dashboard-server/routes-vector-cortex-topology.js +208 -0
  9. package/dist/extensions/dashboard-server/routes-vector-cortex.js +12 -373
  10. package/dist/extensions/dashboard-server/routes.js +1 -1
  11. package/dist/extensions/dashboard-server/server.js +3 -1
  12. package/dist/src/config/vector-cortex.js +19 -0
  13. package/dist/src/config.js +1 -1
  14. package/dist/src/vector-cortex/cortex/sqlite.js +393 -0
  15. package/dist/src/vector-cortex/cortex/store.js +139 -0
  16. package/dist/src/vector-cortex/cortex/types.js +44 -0
  17. package/dist/src/vector-cortex/topology/build.js +232 -0
  18. package/dist/src/vector-cortex/topology/index.js +113 -0
  19. package/dist/src/vector-cortex/topology/types.js +54 -0
  20. package/dist/src/vector-cortex/vc3b-support.js +125 -0
  21. package/dist/vector-cortex/cortex/sqlite.js +393 -0
  22. package/dist/vector-cortex/cortex/store.js +139 -0
  23. package/dist/vector-cortex/cortex/types.js +44 -0
  24. package/dist/vector-cortex/topology/build.js +232 -0
  25. package/dist/vector-cortex/topology/index.js +113 -0
  26. package/dist/vector-cortex/topology/types.js +54 -0
  27. package/dist/vector-cortex/vc3b-support.js +125 -0
  28. package/extensions/dashboard-client/dist/assets/{AreaChart-DywU5SSt.js → AreaChart-CSaJZXru.js} +2 -2
  29. package/extensions/dashboard-client/dist/assets/{AreaChart-DywU5SSt.js.map → AreaChart-CSaJZXru.js.map} +1 -1
  30. package/extensions/dashboard-client/dist/assets/{BarChart-But7WN6K.js → BarChart-CQouNj1g.js} +2 -2
  31. package/extensions/dashboard-client/dist/assets/{BarChart-But7WN6K.js.map → BarChart-CQouNj1g.js.map} +1 -1
  32. package/extensions/dashboard-client/dist/assets/{CacheTab-7aZWa1DR.js → CacheTab-m_eiCYoL.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{CacheTab-7aZWa1DR.js.map → CacheTab-m_eiCYoL.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/{EventsTab-Bf_boNG2.js → EventsTab-DRyOwjYN.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{EventsTab-Bf_boNG2.js.map → EventsTab-DRyOwjYN.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{HealthTab-AbGc1nXP.js → HealthTab-Dj9k40TJ.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{HealthTab-AbGc1nXP.js.map → HealthTab-Dj9k40TJ.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-BfHHhn5F.js → MaintenanceTab-Bk9mhYfD.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-BfHHhn5F.js.map → MaintenanceTab-Bk9mhYfD.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DrcWti0-.js → MemoryMapTab-C7ve49VG.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DrcWti0-.js.map → MemoryMapTab-C7ve49VG.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/{MetricsTab-ALiglXQq.js → MetricsTab-DzjhqZA9.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{MetricsTab-ALiglXQq.js.map → MetricsTab-DzjhqZA9.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/{OverviewTab-nnaJdNyR.js → OverviewTab-CFQdfIm_.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{OverviewTab-nnaJdNyR.js.map → OverviewTab-CFQdfIm_.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{ReposTab-BTK0nofM.js → ReposTab-DyjwEFUr.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{ReposTab-BTK0nofM.js.map → ReposTab-DyjwEFUr.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/{SessionsTab-RXPNqO6x.js → SessionsTab-njLIy7ab.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{SessionsTab-RXPNqO6x.js.map → SessionsTab-njLIy7ab.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/assets/{SetupTab-Dab0AVfs.js → SetupTab-UHIs8-Lh.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{SetupTab-Dab0AVfs.js.map → SetupTab-UHIs8-Lh.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DIVG8hdt.js → TimeSavedCard-inWHjCxg.js} +2 -2
  53. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DIVG8hdt.js.map → TimeSavedCard-inWHjCxg.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/{TurnsTab-9uz2fBwx.js → TurnsTab-CXJYugBu.js} +2 -2
  55. package/extensions/dashboard-client/dist/assets/{TurnsTab-9uz2fBwx.js.map → TurnsTab-CXJYugBu.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DkS0Azx9.js +2 -0
  57. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DkS0Azx9.js.map +1 -0
  58. package/extensions/dashboard-client/dist/assets/{WikiTab-CpcgaggS.js → WikiTab-BJ4A2oi9.js} +2 -2
  59. package/extensions/dashboard-client/dist/assets/{WikiTab-CpcgaggS.js.map → WikiTab-BJ4A2oi9.js.map} +1 -1
  60. package/extensions/dashboard-client/dist/assets/{button-B0Q2fuKY.js → button-BWl-moyn.js} +2 -2
  61. package/extensions/dashboard-client/dist/assets/{button-B0Q2fuKY.js.map → button-BWl-moyn.js.map} +1 -1
  62. package/extensions/dashboard-client/dist/assets/{card-Cnik3TFr.js → card-C58RVPOk.js} +2 -2
  63. package/extensions/dashboard-client/dist/assets/{card-Cnik3TFr.js.map → card-C58RVPOk.js.map} +1 -1
  64. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-DrSwgn1V.js → generateCategoricalChart-vgeOQU3L.js} +2 -2
  65. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-DrSwgn1V.js.map → generateCategoricalChart-vgeOQU3L.js.map} +1 -1
  66. package/extensions/dashboard-client/dist/assets/index-CYpzq9Lm.css +1 -0
  67. package/extensions/dashboard-client/dist/assets/{index-DraytcOY.js → index-lVrWQjpu.js} +3 -3
  68. package/extensions/dashboard-client/dist/assets/{index-DraytcOY.js.map → index-lVrWQjpu.js.map} +1 -1
  69. package/extensions/dashboard-client/dist/assets/{switch-DVQ1aB9b.js → switch-CSeplFQT.js} +2 -2
  70. package/extensions/dashboard-client/dist/assets/{switch-DVQ1aB9b.js.map → switch-CSeplFQT.js.map} +1 -1
  71. package/extensions/dashboard-client/dist/assets/{toggle-Dkppx-49.js → toggle-CQRxgVRi.js} +2 -2
  72. package/extensions/dashboard-client/dist/assets/{toggle-Dkppx-49.js.map → toggle-CQRxgVRi.js.map} +1 -1
  73. package/extensions/dashboard-client/dist/assets/{useSSE-CPkjU4Gx.js → useSSE-BUz6IOI-.js} +2 -2
  74. package/extensions/dashboard-client/dist/assets/{useSSE-CPkjU4Gx.js.map → useSSE-BUz6IOI-.js.map} +1 -1
  75. package/extensions/dashboard-client/dist/index.html +2 -2
  76. package/extensions/dashboard-client/src/api/vector-cortex.ts +9 -0
  77. package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +89 -0
  78. package/extensions/dashboard-client/src/types/vector-cortex.ts +28 -0
  79. package/extensions/dashboard-server/api-contracts/vector-cortex.ts +41 -0
  80. package/extensions/dashboard-server/routes-rag-settings-helpers.ts +12 -0
  81. package/extensions/dashboard-server/routes-vector-cortex-eval.ts +59 -0
  82. package/extensions/dashboard-server/routes-vector-cortex-health.ts +273 -0
  83. package/extensions/dashboard-server/routes-vector-cortex-ledger.ts +92 -0
  84. package/extensions/dashboard-server/routes-vector-cortex-shared.ts +53 -0
  85. package/extensions/dashboard-server/routes-vector-cortex-topology.ts +244 -0
  86. package/extensions/dashboard-server/routes-vector-cortex.ts +12 -431
  87. package/extensions/dashboard-server/routes.ts +1 -0
  88. package/extensions/dashboard-server/server.ts +2 -0
  89. package/package.json +1 -1
  90. package/src/config/vector-cortex.ts +21 -0
  91. package/src/config.ts +2 -0
  92. package/src/vector-cortex/cortex/sqlite.ts +489 -0
  93. package/src/vector-cortex/cortex/store.ts +202 -0
  94. package/src/vector-cortex/cortex/types.ts +194 -0
  95. package/src/vector-cortex/topology/build.ts +259 -0
  96. package/src/vector-cortex/topology/index.ts +143 -0
  97. package/src/vector-cortex/topology/types.ts +156 -0
  98. package/src/vector-cortex/vc3b-support.ts +142 -0
  99. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DnKY61EJ.js +0 -2
  100. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DnKY61EJ.js.map +0 -1
  101. package/extensions/dashboard-client/dist/assets/index-CfvziyBi.css +0 -1
@@ -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 };
@@ -0,0 +1,194 @@
1
+ /**
2
+ * vector-cortex/cortex/types.ts — capability-gated cortex store contract (VC3A).
3
+ *
4
+ * Owns `CortexReader` / `CortexWriter` / `CortexAdmin` and `CortexRecordV1`
5
+ * (the immutable derived record). Consumes only reviewer-accepted predecessor
6
+ * contracts and [common contracts](../../CONTRACTS.md §Store and migration
7
+ * contracts), which are NORMATIVE here: the derived frontier cannot exceed the
8
+ * contiguous durable authority high-water and cannot advance during
9
+ * authority/spool outage.
10
+ *
11
+ * A CortexRecordV1 is a DERIVED record keyed by
12
+ * `(sourceHighWater, algorithmVersion, id)`. The same `id` at a different
13
+ * `algorithmVersion` (or a different `sourceHighWater`) is a DISTINCT record —
14
+ * never collapsed (CTX-KEY-002). Records are immutable: re-appending an exact
15
+ * `(sourceHighWater, algorithmVersion, id)` with the SAME `payloadDigest` is an
16
+ * idempotent acknowledge; the same key with a DIFFERENT digest is a conflict.
17
+ *
18
+ * Capability gating mirrors the host `asReader/asWriter/asAdmin` pattern and the
19
+ * VC1B ledger: the writer exposes append only, the reader exposes query only,
20
+ * and the admin alone can rebuild / switch generations. No callbacks or event
21
+ * emitters flow from the store; writes are non-fatal (failures log and never
22
+ * break the agent loop).
23
+ *
24
+ * Pure type/schema definitions + small pure predicates. No storage, no console,
25
+ * no network, no side effects (PREVENT-PI-004 / PREVENT-011).
26
+ */
27
+
28
+ /**
29
+ * A derived cortex record — byte authority is `payloadBytes`; `payloadDigest` is
30
+ * the authoritative `sha256:<hex>` over those bytes. `sourceHighWater` is the
31
+ * durable authority high-water the record was derived AT (the derived frontier
32
+ * cannot exceed it); `algorithmVersion` is the producing algorithm revision.
33
+ */
34
+ export interface CortexRecordV1 {
35
+ readonly schema: "cortex-record-v1";
36
+ /** Durable contiguous authority high-water this record derives from. */
37
+ readonly sourceHighWater: bigint;
38
+ /** Producing algorithm revision (records at distinct versions stay distinct). */
39
+ readonly algorithmVersion: number;
40
+ /** Stable record identity within the derived store. */
41
+ readonly id: string;
42
+ /** Record kind (semantic / dependency / contradiction / synthetic...). */
43
+ readonly kind: string;
44
+ /** Authoritative `sha256:<hex>` of `payloadBytes` (immutability anchor). */
45
+ readonly payloadDigest: string;
46
+ /** Immutable derived payload bytes (byte authority for the record). */
47
+ readonly payloadBytes: Uint8Array;
48
+ }
49
+
50
+ /**
51
+ * A derived GENEration — a named, digest-pinned front over the sorted accepted
52
+ * records. `rootDigest` is ONE deterministic digest over the canonical sorted
53
+ * key list (order-independent, CTX-REBUILD-003); `sourceHighWater` is the derived
54
+ * frontier (max sourceHighWater across its records).
55
+ */
56
+ export interface CortexGenerationV1 {
57
+ readonly schema: "cortex-generation-v1";
58
+ readonly id: string;
59
+ readonly sourceHighWater: bigint;
60
+ readonly recordCount: number;
61
+ readonly rootDigest: string;
62
+ /** Monotonic rebuild ordinal (never regresses for an accepted generation). */
63
+ readonly ordinal: bigint;
64
+ }
65
+
66
+ /** Failure codes the writer surfaces on a rejected / failed append. */
67
+ export type CortexAppendCode =
68
+ /** Storage failure (e.g. SQLITE_FULL). Non-fatal; host continues in mode C. */
69
+ | "CTX_APPEND_FAILED"
70
+ /** Same (sourceHighWater, algorithmVersion, id) but a different payloadDigest. */
71
+ | "CTX_KEY_CONFLICT";
72
+
73
+ /** Result of a single append attempt. */
74
+ export type CortexAppendResult =
75
+ | { ok: true; record: CortexRecordV1 }
76
+ | { ok: false; code: CortexAppendCode; rejected: CortexRecordV1 };
77
+
78
+ /** Failure codes the admin surfaces on a rejected rebuild. */
79
+ export type CortexRebuildCode =
80
+ /** A record's payloadDigest does not match its payloadBytes (authority corrupt). */
81
+ | "CTX_PAYLOAD_DIGEST_MISMATCH"
82
+ /** A record exceeds the caller's declared generation source high-water. */
83
+ | "CTX_HIGH_WATER_EXCEEDED"
84
+ /** The generation INSERT/activate write did not persist (storage failure, e.g. SQLITE_FULL). */
85
+ | "CTX_REBUILD_FAILED";
86
+
87
+ /**
88
+ * Reader capability: query-only. The dashboard's reader-only GET
89
+ * `GET /api/vector-cortex/topology` is built on exactly this surface and nothing
90
+ * more — it can read the generation/topology summary and records but never
91
+ * append, rebuild, or switch generations.
92
+ */
93
+ export interface CortexReader {
94
+ readonly kind: "CortexReader";
95
+ /** Count of accepted derived records. */
96
+ recordCount(): number;
97
+ /** Accepted records in ascending `(sourceHighWater, algorithmVersion, id)`. */
98
+ readRecords(): readonly CortexRecordV1[];
99
+ /** The single record at a composite key, or undefined. */
100
+ readRecord(sourceHighWater: bigint, algorithmVersion: number, id: string): CortexRecordV1 | undefined;
101
+ /** The latest (active) generation, or undefined when none rebuilt yet. */
102
+ latestGeneration(): CortexGenerationV1 | undefined;
103
+ /** Reader-only topology summary (the dashboard GET payload). */
104
+ topologySummary(): CortexTopologySummary;
105
+ }
106
+
107
+ /**
108
+ * Writer capability: append-only. Can add immutable derived records — but cannot
109
+ * query arbitrary history, rebuild, or touch generations. Enforces the composite
110
+ * key immutability on every append.
111
+ */
112
+ export interface CortexWriter {
113
+ readonly kind: "CortexWriter";
114
+ /** Append one derived record; idempotent ack on exact key+digest, conflict otherwise. */
115
+ append(input: CortexAppendInput): CortexAppendResult;
116
+ }
117
+
118
+ /**
119
+ * Admin capability: maintenance + generation control ONLY. The sole surface with
120
+ * rebuild / switch-generations capability — never exposed to ingestion or the
121
+ * dashboard reader.
122
+ */
123
+ export interface CortexAdmin {
124
+ readonly kind: "CortexAdmin";
125
+ /**
126
+ * Deterministically rebuild a generation from ALL accepted records: sorts keys,
127
+ * verifies each payload digest, computes ONE root digest, writes + activates a
128
+ * new generation. Idempotent with respect to unchanged accepted inputs.
129
+ *
130
+ * `authorityHighWater` (optional) is the contiguous durable authority high-water
131
+ * the rebuild must not outrun. When supplied and the derived frontier (max
132
+ * record sourceHighWater) exceeds it, the rebuild is rejected with
133
+ * `CTX_HIGH_WATER_EXCEEDED` and NO generation is written — the derived frontier
134
+ * cannot exceed the durable authority high-water (normative in CONTRACTS.md).
135
+ * Omit it to rebuild without an authority bound.
136
+ */
137
+ rebuild(authorityHighWater?: bigint):
138
+ | { ok: true; generation: CortexGenerationV1 }
139
+ | { ok: false; code: CortexRebuildCode };
140
+ /** Switch the active generation pointer without deleting evidence. */
141
+ switchGeneration(generationId: string): { ok: boolean; code?: string };
142
+ /** List every generation id in ascending ordinal order (evidence retained). */
143
+ listGenerations(): readonly CortexGenerationV1[];
144
+ }
145
+
146
+ /** Input shape of a single append (record fields minus the readonly full type). */
147
+ export interface CortexAppendInput {
148
+ readonly sourceHighWater: bigint;
149
+ readonly algorithmVersion: number;
150
+ readonly id: string;
151
+ readonly kind: string;
152
+ readonly payloadDigest?: string;
153
+ readonly payloadBytes: Uint8Array;
154
+ }
155
+
156
+ /**
157
+ * Reader-only topology summary — the exact payload of the dashboard
158
+ * `GET /api/vector-cortex/topology` reader-only GET. Aggregate only: root digest
159
+ * prefix, record count, derived frontier. Never exposes writer/admin surfaces or
160
+ * raw record payloads.
161
+ */
162
+ export interface CortexTopologySummary {
163
+ /** Whether the VC3A cortex-store flag is enabled. */
164
+ readonly enabled: boolean;
165
+ /** Active generation id, or null when no generation rebuilt yet. */
166
+ readonly generationId: string | null;
167
+ /** Active generation root digest (or null). */
168
+ readonly rootDigest: string | null;
169
+ /** Derived frontier (active generation sourceHighWater, or "0"). */
170
+ readonly sourceHighWater: string;
171
+ /** Accepted derived record count. */
172
+ readonly recordCount: number;
173
+ /** Monotonic rebuild ordinal (or null). */
174
+ readonly ordinal: string | null;
175
+ }
176
+
177
+ /**
178
+ * Registered CTX conformance ID range (CTX-001..010). The acceptance test reads
179
+ * these rows from the v2 manifest and asserts each returns its manifest bytes or
180
+ * exactly its listed failure code. The three NAMED assertions
181
+ * (CTX-CAP-001 / CTX-KEY-002 / CTX-REBUILD-003) live in the acceptance test.
182
+ */
183
+ export const CTX_IDS = [
184
+ "CTX-001",
185
+ "CTX-002",
186
+ "CTX-003",
187
+ "CTX-004",
188
+ "CTX-005",
189
+ "CTX-006",
190
+ "CTX-007",
191
+ "CTX-008",
192
+ "CTX-009",
193
+ "CTX-010",
194
+ ] as const;
@@ -0,0 +1,259 @@
1
+ /**
2
+ * vector-cortex/topology/build.ts — deterministic cortical topology builder (VC3B).
3
+ *
4
+ * Implements the deterministic graph construction (tasks 2–4):
5
+ * - For each (source, head) retain only candidates whose score is strictly
6
+ * above the calibrated threshold.
7
+ * - Stable-sort winning candidates by score DESCENDING, then by unsigned
8
+ * target-ID bytes ASCENDING (TOP-TIE-002 — equal scores sort by target ID).
9
+ * - Cap each (source, head) at top-k=16; the seventeenth eligible neighbor is
10
+ * excluded (TOP-K-001).
11
+ * - Remove self edges (source === target); reject them as TOP_SELF_EDGE.
12
+ * - Reject non-finite scores as TOP_SCORE_NONFINITE — a single bad head's NaN
13
+ * rejects only its own edge and never poisons other heads.
14
+ * - Encode dependency edges as single directed records; contradiction edges as
15
+ * symmetric PAIRED records (source→target and target→source) (TOP-KIND-003).
16
+ *
17
+ * This is the mode-A producer in the sprint triad: a multi-head topology index.
18
+ * The other two modes (B linear VectorSet scan, C source-seq/keyword) must yield
19
+ * the same threshold/tie/cap invariants and the same graph digest. Pure
20
+ * deterministic function — no I/O, no console, no network (PREVENT-PI-004),
21
+ * no `any` (PREVENT-011).
22
+ */
23
+ import {
24
+ TOP_K,
25
+ type TopologyBuildResult,
26
+ type TopologyCandidate,
27
+ type TopologyEdgeV1,
28
+ type TopologyInput,
29
+ type TopologyNodeV1,
30
+ type TopologyRejection,
31
+ type TopologyV1,
32
+ } from "./types.js";
33
+
34
+ /**
35
+ * Field (`|`) and record (`~`) separators used by the canonical digest framing in
36
+ * index.ts. Id values (source/target/head) carrying either byte would corrupt the
37
+ * framing and make distinct graphs collide on an identical digest, so the build
38
+ * rejects such candidates with TOP_FRAMING_SEP (Q04, mirrors the dashboard's
39
+ * parseCandidatePayload guard) — enforced at this pure seam, not only at one caller.
40
+ */
41
+ const FRAMING_SEP = /[|~]/;
42
+
43
+ /**
44
+ * Build the deterministic topology graph from calibrated-threshold candidates.
45
+ * Order-independent across any permutation of `input.candidates`.
46
+ */
47
+ export function buildTopology(input: TopologyInput): TopologyBuildResult {
48
+ const { sessionId, sourceHighWater, threshold } = input;
49
+
50
+ // Group winning candidates by (source, head), stable-sorting each group by
51
+ // score desc then unsigned target-ID bytes, capping at top-k.
52
+ const byGroup = new Map<string, TopologyCandidate[]>();
53
+ const rejections: TopologyRejection[] = [];
54
+
55
+ for (const c of input.candidates) {
56
+ // Q04 — framing hardening: the canonical digest serialization in index.ts uses
57
+ // `|` (FIELD_SEP) and `~` (RECORD_SEP) as unambiguous delimiters. Those are
58
+ // collision-free only while id values (source/target/head) contain neither
59
+ // byte; a content-derived id carrying either would make two distinct graphs
60
+ // hash to an identical digest. Enforce the no-separator-in-id constraint at
61
+ // this pure build seam (not only at the dashboard caller) by rejecting such a
62
+ // candidate in isolation with TOP_FRAMING_SEP — the digest stays well-defined
63
+ // for ANY input.
64
+ if (FRAMING_SEP.test(c.source) || FRAMING_SEP.test(c.target) || FRAMING_SEP.test(c.head)) {
65
+ rejections.push(reject(c, "TOP_FRAMING_SEP"));
66
+ continue;
67
+ }
68
+ // Non-finite score: reject this candidate in isolation (never poison others).
69
+ if (!Number.isFinite(c.score)) {
70
+ rejections.push(reject(c, "TOP_SCORE_NONFINITE"));
71
+ continue;
72
+ }
73
+ if (c.source === c.target) {
74
+ rejections.push(reject(c, "TOP_SELF_EDGE"));
75
+ continue;
76
+ }
77
+ if (c.score <= threshold) continue; // below calibrated threshold
78
+ const key = groupKey(c.source, c.head);
79
+ const group = byGroup.get(key);
80
+ if (group) group.push(c);
81
+ else byGroup.set(key, [c]);
82
+ }
83
+
84
+ // Stable sort each (source, head) group: score desc, then unsigned target-ID
85
+ // bytes asc. Keep exactly the top-k=16 (the 17th eligible neighbor is excluded).
86
+ const selected: TopologyCandidate[] = [];
87
+ for (const group of byGroup.values()) {
88
+ group.sort(compareCandidates);
89
+ for (let i = 0; i < Math.min(group.length, TOP_K); i++) {
90
+ selected.push(group[i]);
91
+ }
92
+ }
93
+
94
+ // Encode dependency edges as directed records; contradiction edges as
95
+ // symmetric PAIRED records. Duplicate (source,target,head) contradictions
96
+ // collapse: the same symmetric pair is emitted once per direction — dedupe to
97
+ // keep the paired graph well-formed and the digest stable.
98
+ //
99
+ // IMPORTANT: `selected` is first ordered by a canonical global comparator so
100
+ // every dedup decision (which head represents a collapsed contradiction pair,
101
+ // which identical directed edge is kept) is deterministic and INDEPENDENT of
102
+ // the incoming candidate order. Without this, reversing the input order could
103
+ // flip which head represents a symmetric pair and change the digest.
104
+ //
105
+ // The comparator sorts by SCORE DESCENDING FIRST, so first-writer-wins dedup
106
+ // keeps the MAXIMUM score per collapsed relation. Two candidates emitting the
107
+ // same directed edge (same source,target,head) with different scores keep the
108
+ // higher one; a contradiction pair a↔b with different scores is claimed by the
109
+ // higher-scoring candidate regardless of which end is "source". A downstream
110
+ // consumer (VC3C receives TopologyV1) therefore gets the strongest score for a
111
+ // collapsed relation, never the weakest, and the winner is decided by score —
112
+ // not by source-ID byte order. (Score ties fall through to bytewise keys so
113
+ // the total order stays deterministic.)
114
+ selected.sort(compareSelected);
115
+ const edges: Map<string, TopologyEdgeV1> = new Map();
116
+ const nodes = new Map<string, TopologyNodeKind>();
117
+ for (const c of selected) {
118
+ nodes.set(c.source, nodeKind(c));
119
+ nodes.set(c.target, nodeKind(c));
120
+ if (c.kind === "contradiction") {
121
+ // Symmetric pair: emit source→target and target→source exactly once, keyed
122
+ // so that a reversed duplicate from another candidate collapses to one pair.
123
+ const fwd = pairKey(c.source, c.target);
124
+ const rev = pairKey(c.target, c.source);
125
+ const hasFwd = edges.has(fwd) || edges.has(rev);
126
+ if (!hasFwd) {
127
+ edges.set(fwd, { source: c.source, target: c.target, head: c.head, score: c.score, direction: "contradiction" });
128
+ edges.set(rev, { source: c.target, target: c.source, head: c.head, score: c.score, direction: "contradiction" });
129
+ }
130
+ continue;
131
+ }
132
+ // Directed dependency: distinct per (source, target, head).
133
+ const depKey = dirKey(c.source, c.target, c.head, "dependency");
134
+ if (!edges.has(depKey)) {
135
+ edges.set(depKey, { source: c.source, target: c.target, head: c.head, score: c.score, direction: "dependency" });
136
+ }
137
+ }
138
+
139
+ const nodeList: TopologyNodeV1[] = [...nodes.entries()]
140
+ .map(([id, kind]) => ({ id, kind }))
141
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
142
+ const edgeList = [...edges.values()].sort(compareEdges);
143
+
144
+ const graph: TopologyV1 = {
145
+ schema: "topology-v1",
146
+ sessionId,
147
+ sourceHighWater,
148
+ threshold,
149
+ nodeCount: nodeList.length,
150
+ edgeCount: edgeList.length,
151
+ generationDigest: "",
152
+ nodes: nodeList,
153
+ edges: edgeList,
154
+ };
155
+
156
+ return { ok: true, topology: graph, rejected: rejections };
157
+ }
158
+
159
+ function reject(c: TopologyCandidate, code: TopologyRejection["code"]): TopologyRejection {
160
+ return { source: c.source, target: c.target, head: c.head, score: c.score, code };
161
+ }
162
+
163
+ function groupKey(source: string, head: string): string {
164
+ return JSON.stringify([source, head]);
165
+ }
166
+
167
+ function pairKey(a: string, b: string): string {
168
+ return JSON.stringify([a, b, " "]);
169
+ }
170
+
171
+ function dirKey(a: string, b: string, head: string, direction: string): string {
172
+ return JSON.stringify([a, b, head, direction]);
173
+ }
174
+
175
+ function compareCandidates(a: TopologyCandidate, b: TopologyCandidate): number {
176
+ if (a.score !== b.score) return a.score > b.score ? -1 : 1;
177
+ // Equal scores: sort by unsigned target-ID bytes ascending (TOP-TIE-002).
178
+ const byTarget = unsignedBytesCompare(a.target, b.target);
179
+ if (byTarget !== 0) return byTarget;
180
+ // Equal score + target: break the tie by kind bytes so the within-group sort is
181
+ // a TOTAL order, independent of input order (Q01). Without this, two tied
182
+ // candidates of differing kind that land exactly at the top-k=16 boundary would
183
+ // fall back to JS stable order = candidate input order, flipping which edge
184
+ // survives and changing the graph digest. Mirrors compareSelected's `kind` key.
185
+ return unsignedBytesCompare(a.kind, b.kind);
186
+ }
187
+
188
+ /**
189
+ * Total, deterministic order over selected candidates used to drive dedup before
190
+ * encoding. Independent of the incoming input order so a collapsed contradiction
191
+ * pair always picks the same representing head and identical directed edges keep
192
+ * the same winner.
193
+ *
194
+ * SCORE DESCENDING IS THE PRIMARY KEY: first-writer-wins dedup on this order
195
+ * retains the highest score for every collapsed relation (Q01). Only after
196
+ * scores are equal do we fall through to bytewise source/target/head/kind keys,
197
+ * which keeps a sound deterministic tie-break (a score tie collapses to the
198
+ * same representing head regardless of input order).
199
+ */
200
+ function compareSelected(a: TopologyCandidate, b: TopologyCandidate): number {
201
+ return (
202
+ (a.score < b.score ? 1 : a.score > b.score ? -1 : 0) ||
203
+ unsignedBytesCompare(a.source, b.source) ||
204
+ unsignedBytesCompare(a.target, b.target) ||
205
+ unsignedBytesCompare(a.head, b.head) ||
206
+ unsignedBytesCompare(a.kind, b.kind)
207
+ );
208
+ }
209
+
210
+ function compareEdges(a: TopologyEdgeV1, b: TopologyEdgeV1): number {
211
+ return (
212
+ unsignedBytesCompare(a.source, b.source) ||
213
+ unsignedBytesCompare(a.target, b.target) ||
214
+ unsignedBytesCompare(a.head, b.head) ||
215
+ unsignedBytesCompare(a.direction, b.direction) ||
216
+ (a.score < b.score ? -1 : a.score > b.score ? 1 : 0)
217
+ );
218
+ }
219
+
220
+ /**
221
+ * Unsigned bytewise comparison over UTF-8 bytes of two strings. String
222
+ * `<`/`>` in JS is UTF-16 code-unit order, which equals UTF-8 byte order for
223
+ * the ASCII identifiers used (and for our domain); we implement an explicit
224
+ * unsigned-byte comparator so ordering is defined by spec, not JS internals.
225
+ */
226
+ function unsignedBytesCompare(a: string, b: string): number {
227
+ const ba = Buffer.from(a, "utf8");
228
+ const bb = Buffer.from(b, "utf8");
229
+ const n = Math.min(ba.length, bb.length);
230
+ for (let i = 0; i < n; i++) {
231
+ if (ba[i] !== bb[i]) return ba[i] < bb[i] ? -1 : 1;
232
+ }
233
+ return ba.length < bb.length ? -1 : ba.length > bb.length ? 1 : 0;
234
+ }
235
+
236
+ /**
237
+ * Node kind for a retained edge: a node is marked by the record kind it is
238
+ * derived from. A contradiction candidate marks both endpoints "contradiction";
239
+ * a dependency candidate marks both endpoints "dependency" (Q02) — the node
240
+ * kind matches the producing record kind, never a synthetic placeholder.
241
+ *
242
+ * NODE-KIND SEMANTICS (documented, Q03): a node participates in the kind of
243
+ * whichever retained edge LABELLED it LAST during the linear pass over `selected`.
244
+ * Because `selected` is sorted SCORE-DESCENDING first, the last label a node
245
+ * receives comes from its LOWEST-scoring retained edge — i.e. when a node is
246
+ * touched by both a dependency and a contradiction edge (of equal or lower
247
+ * score), the WEAKEST relation's kind wins the node's `kind` field, not the
248
+ * strongest. This is deliberate and DETERMINISTIC (the total compareSelected
249
+ * order, with the Q01 `kind` tie-break, fixes the pass order independent of
250
+ * input order), and mode B reproduces it exactly. A downstream VC3C consumer
251
+ * must read node.kind as "the relation kind of the weakest retained edge
252
+ * incident to this node", not "the dominant/strongest relation". Per-edge
253
+ * direction + kind are authoritative on the edges themselves.
254
+ */
255
+ function nodeKind(c: TopologyCandidate): TopologyNodeKind {
256
+ return c.kind === "contradiction" ? "contradiction" : "dependency";
257
+ }
258
+
259
+ type TopologyNodeKind = "semantic" | "dependency" | "contradiction" | "synthetic";