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.
- package/dist/config/vector-cortex.js +19 -0
- package/dist/config.js +1 -1
- package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +2 -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 +208 -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 +19 -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/src/vector-cortex/topology/build.js +232 -0
- package/dist/src/vector-cortex/topology/index.js +113 -0
- package/dist/src/vector-cortex/topology/types.js +54 -0
- package/dist/src/vector-cortex/vc3b-support.js +125 -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/dist/vector-cortex/topology/build.js +232 -0
- package/dist/vector-cortex/topology/index.js +113 -0
- package/dist/vector-cortex/topology/types.js +54 -0
- package/dist/vector-cortex/vc3b-support.js +125 -0
- package/extensions/dashboard-client/dist/assets/{AreaChart-DywU5SSt.js → AreaChart-CSaJZXru.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-DywU5SSt.js.map → AreaChart-CSaJZXru.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-But7WN6K.js → BarChart-CQouNj1g.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-But7WN6K.js.map → BarChart-CQouNj1g.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-7aZWa1DR.js → CacheTab-m_eiCYoL.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-7aZWa1DR.js.map → CacheTab-m_eiCYoL.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-Bf_boNG2.js → EventsTab-DRyOwjYN.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-Bf_boNG2.js.map → EventsTab-DRyOwjYN.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-AbGc1nXP.js → HealthTab-Dj9k40TJ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-AbGc1nXP.js.map → HealthTab-Dj9k40TJ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-BfHHhn5F.js → MaintenanceTab-Bk9mhYfD.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-BfHHhn5F.js.map → MaintenanceTab-Bk9mhYfD.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DrcWti0-.js → MemoryMapTab-C7ve49VG.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DrcWti0-.js.map → MemoryMapTab-C7ve49VG.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-ALiglXQq.js → MetricsTab-DzjhqZA9.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-ALiglXQq.js.map → MetricsTab-DzjhqZA9.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-nnaJdNyR.js → OverviewTab-CFQdfIm_.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-nnaJdNyR.js.map → OverviewTab-CFQdfIm_.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-BTK0nofM.js → ReposTab-DyjwEFUr.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-BTK0nofM.js.map → ReposTab-DyjwEFUr.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-RXPNqO6x.js → SessionsTab-njLIy7ab.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-RXPNqO6x.js.map → SessionsTab-njLIy7ab.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-Dab0AVfs.js → SetupTab-UHIs8-Lh.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-Dab0AVfs.js.map → SetupTab-UHIs8-Lh.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DIVG8hdt.js → TimeSavedCard-inWHjCxg.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DIVG8hdt.js.map → TimeSavedCard-inWHjCxg.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-9uz2fBwx.js → TurnsTab-CXJYugBu.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-9uz2fBwx.js.map → TurnsTab-CXJYugBu.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-DkS0Azx9.js +2 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-DkS0Azx9.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{WikiTab-CpcgaggS.js → WikiTab-BJ4A2oi9.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-CpcgaggS.js.map → WikiTab-BJ4A2oi9.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-B0Q2fuKY.js → button-BWl-moyn.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-B0Q2fuKY.js.map → button-BWl-moyn.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-Cnik3TFr.js → card-C58RVPOk.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-Cnik3TFr.js.map → card-C58RVPOk.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-DrSwgn1V.js → generateCategoricalChart-vgeOQU3L.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-DrSwgn1V.js.map → generateCategoricalChart-vgeOQU3L.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/index-CYpzq9Lm.css +1 -0
- package/extensions/dashboard-client/dist/assets/{index-DraytcOY.js → index-lVrWQjpu.js} +3 -3
- package/extensions/dashboard-client/dist/assets/{index-DraytcOY.js.map → index-lVrWQjpu.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{switch-DVQ1aB9b.js → switch-CSeplFQT.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-DVQ1aB9b.js.map → switch-CSeplFQT.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-Dkppx-49.js → toggle-CQRxgVRi.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-Dkppx-49.js.map → toggle-CQRxgVRi.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-CPkjU4Gx.js → useSSE-BUz6IOI-.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-CPkjU4Gx.js.map → useSSE-BUz6IOI-.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 +89 -0
- package/extensions/dashboard-client/src/types/vector-cortex.ts +28 -0
- package/extensions/dashboard-server/api-contracts/vector-cortex.ts +41 -0
- package/extensions/dashboard-server/routes-rag-settings-helpers.ts +12 -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 +244 -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 +21 -0
- package/src/config.ts +2 -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/src/vector-cortex/topology/build.ts +259 -0
- package/src/vector-cortex/topology/index.ts +143 -0
- package/src/vector-cortex/topology/types.ts +156 -0
- package/src/vector-cortex/vc3b-support.ts +142 -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,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";
|