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,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,225 @@
1
+ /**
2
+ * vector-cortex/encoder/calibrate.ts — VC2C calibration fit (task 2).
3
+ *
4
+ * Fits a `CalibrationV1` using ONLY the calibration split. Held-out
5
+ * (test/eval) labels are STRICTLY PROHIBITED from the fit inputs: the fit
6
+ * function rejects (ENC_QUALIFICATION_HELD_OUT_IN_FIT) any example whose
7
+ * `itemId` appears in the caller's held-out set. Ties in example score are
8
+ * broken deterministically by item ID (stable score/id ties), never by arrival
9
+ * order, so the fit is invariant to row order.
10
+ *
11
+ * The calibration split assignment is grouped by repository+session
12
+ * (EVALUATION.md §corpus): every label-bearing item carries a `repository` +
13
+ * `session` group, and the split digest is the canonical SHA-256 over the
14
+ * sorted group list. A single repo/session group NEVER crosses split boundaries
15
+ * (the caller seeds a group wholly into the calibration split or not at all).
16
+ *
17
+ * The fit itself is a deterministic seeded per-head temperature + threshold
18
+ * calibration over the calibration-only examples (real isotonic/Platt weights
19
+ * land with trained weights; the contract, split isolation, held-out
20
+ * prohibition, stable ties, and frozen-temperature/threshold surface are all
21
+ * normative here). Pi-agnostic, zero network (PREVENT-PI-004), no `any`
22
+ * (PREVENT-011).
23
+ */
24
+
25
+ import { createHash } from "node:crypto";
26
+ import {
27
+ ENCODER_HEAD_ORDER,
28
+ ENCODER_SEED,
29
+ ENC_QUALIFICATION_FAIL,
30
+ type CalibrationV1,
31
+ type EncoderHeadName,
32
+ } from "./types.js";
33
+
34
+ /** A scored, label-bearing calibration item. `heldOutIds` forbid review labels. */
35
+ export interface CalibrationExample {
36
+ readonly itemId: string;
37
+ readonly head: EncoderHeadName;
38
+ readonly score: number;
39
+ /** Binary ground-truth label (0/1) against which the raw score is calibrated. */
40
+ readonly label: 0 | 1;
41
+ /** Repository group (EVALUATION.md §corpus) — the split unit. */
42
+ readonly repository: string;
43
+ /** Session group (within the repository). */
44
+ readonly session: string;
45
+ }
46
+
47
+ export interface CalibrationFitOptions {
48
+ /** Seed of the deterministic fit (defaults to ENCODER_SEED). */
49
+ readonly seed?: number;
50
+ }
51
+
52
+ export type CalibrationFitResult =
53
+ | { ok: true; calibration: CalibrationV1 }
54
+ | { ok: false; code: string; reason: string };
55
+
56
+ /** Canonical digests of a sorted stable representation (order-invariant). */
57
+ function digestStrings(values: readonly string[]): string {
58
+ const sorted = [...values].sort();
59
+ return createHash("sha256").update(sorted.join("\n")).digest("hex");
60
+ }
61
+
62
+ /**
63
+ * Compute the calibration split digest for a set of (repository, session) groups
64
+ * present in the fit. The group list is canonicalized (sorted, deduped) so the
65
+ * digest is invariant to row order within the input. A caller that seeds WHOLE
66
+ * groups into the calibration split guarantees no group crosses a boundary.
67
+ */
68
+ /**
69
+ * Render one (repository, session) group as an injective canonical string.
70
+ * Each field is length-prefixed (`<len>:<value>`), so two distinct pairs can
71
+ * never collide to the same rendering — e.g. `{r:"a", s:"b::c"}` renders as
72
+ * `1:a3:b::c` while `{r:"a::b", s:"c"}` renders as `4:a::b1:c`. A plain
73
+ * `repository::session` join would conflate those when identifiers happen to
74
+ * contain "::"; length-prefixing makes the split digest sound for arbitrary
75
+ * repository/session identifiers.
76
+ */
77
+ function renderGroup(g: { repository: string; session: string }): string {
78
+ return `${g.repository.length}:${g.repository}${g.session.length}:${g.session}`;
79
+ }
80
+
81
+ export function calibrationSplitDigest(groups: readonly { repository: string; session: string }[]): string {
82
+ const rendered = new Set<string>();
83
+ for (const g of groups) rendered.add(renderGroup(g));
84
+ return digestStrings([...rendered]);
85
+ }
86
+
87
+ /** A deterministic 32-bit LCG step (matches the heads/runtime projectors). */
88
+ function nextState(state: number): number {
89
+ return (state * 1664525 + 1013904223) >>> 0;
90
+ }
91
+
92
+ /**
93
+ * Deterministic per-head temperature in a stable, healthy range (e.g. 0.8..1.5).
94
+ *
95
+ * The caller-supplied `seed` is mixed into the LCG state through independent
96
+ * steps so it ALWAYS affects the temperature — even when it equals ENCODER_SEED.
97
+ * (A naive `ENCODER_SEED ^ head.length ^ seed` cancels the two seed terms when
98
+ * `seed === ENCODER_SEED`, leaving a pure function of the head — the default
99
+ * path would make the seed option inert. Here the seed seeds the PRNG first,
100
+ * then the head name is folded in, so both vary the fit independently.) The fit
101
+ * stays deterministic for a fixed (seed, head) across processes.
102
+ */
103
+ function fitTemperature(head: EncoderHeadName, seed: number): number {
104
+ let state = (seed >>> 0) ^ 0x9e3779b9;
105
+ state = nextState(state);
106
+ state = (state ^ (head.length >>> 0)) >>> 0;
107
+ state = nextState(state);
108
+ state = (state ^ 0x85ebca6b) >>> 0;
109
+ state = nextState(state);
110
+ const r = (state / 4294967296) % 1;
111
+ return 0.8 + r * 0.7; // 0.8 .. 1.5
112
+ }
113
+
114
+ /**
115
+ * Deterministic per-head decision threshold derived from the calibration
116
+ * distribution. For a head with BOTH classes present the threshold is the
117
+ * midpoint between the highest-scoring negative (label 0) and the lowest-scoring
118
+ * positive (label 1) — a true between-class balance point that NEVER lands on a
119
+ * negative example's own score (code-review Q03): a future inference at the
120
+ * highest calibration negative is still classified negative, and one at the
121
+ * lowest calibration positive is still classified positive. Degenerate heads with
122
+ * a single class fall back conservatively (no positives -> just above the top
123
+ * observed score; no negatives -> just below the lowest observed positive) and an
124
+ * empty head defaults to 0.5. Scoring is order-invariant: ties in score resolve by
125
+ * item ID bytewise (stable score/id ties), never by arrival order, so the fit is
126
+ * invariant to row order. This frozen threshold is a normative placeholder (real
127
+ * trained weights land later).
128
+ */
129
+ function fitThreshold(
130
+ head: EncoderHeadName,
131
+ examples: readonly CalibrationExample[],
132
+ ): number {
133
+ const headEx = examples
134
+ .filter((e) => e.head === head)
135
+ .slice()
136
+ .sort((a, b) => (a.score - b.score) || (a.itemId < b.itemId ? -1 : a.itemId > b.itemId ? 1 : 0));
137
+ if (headEx.length === 0) return 0.5;
138
+ let highestNeg = -Infinity;
139
+ let lowestPos = Infinity;
140
+ for (const e of headEx) {
141
+ if (e.label === 0) highestNeg = Math.max(highestNeg, e.score);
142
+ else lowestPos = Math.min(lowestPos, e.score);
143
+ }
144
+ if (lowestPos === Infinity) {
145
+ // Only negatives observed — no positive class to balance against. Set the
146
+ // threshold just above the top observed score so no observed negative is
147
+ // re-admitted (a genuine future positive must exceed all calibration negatives).
148
+ return Math.max(0.5, highestNeg + 0.05);
149
+ }
150
+ if (highestNeg === -Infinity) {
151
+ // Only positives observed — set the threshold just below the lowest observed
152
+ // positive so every observed positive is admitted.
153
+ return Math.max(0, lowestPos - 0.05);
154
+ }
155
+ // Both classes present: the midpoint strictly between the highest negative and
156
+ // the lowest positive is the between-class balance point (Q03).
157
+ return (highestNeg + lowestPos) / 2;
158
+ }
159
+
160
+ /**
161
+ * Fit `CalibrationV1` over the calibration split only (task 2).
162
+ *
163
+ * - rejects any item whose `itemId` is in `heldOutIds` (held-out labels are
164
+ * prohibited from fit inputs).
165
+ * - treats `groups` as the calibration split units; the emitted split digest
166
+ * covers only the groups ACTUALLY present in the fit examples (plus the
167
+ * declared `groups`, when supplied — see below).
168
+ * - stable score/id ties (never arrival order).
169
+ *
170
+ * Held-out labels are prohibited by construction: the caller passes the full set
171
+ * of held-out item IDs, and the fit fails loudly if any calibration input is
172
+ * actually a held-out item — the fit can never silently learn from review labels.
173
+ */
174
+ export function fitCalibration(
175
+ examples: readonly CalibrationExample[],
176
+ options: CalibrationFitOptions & {
177
+ /** Item IDs that belong to the held-out (test/eval) split. Any calibration
178
+ * input whose id appears here is a fit violation. */
179
+ readonly heldOutIds?: readonly string[];
180
+ /** Declared calibration groups (repository+session). When supplied, the split
181
+ * digest covers these; otherwise it covers the groups actually in `examples`.
182
+ * Whole groups never cross a split boundary when the caller seeds a group
183
+ * into exactly one split. */
184
+ readonly groups?: readonly { repository: string; session: string }[];
185
+ } = {},
186
+ ): CalibrationFitResult {
187
+ const seed = options.seed ?? ENCODER_SEED;
188
+ const heldOut = new Set(options.heldOutIds ?? []);
189
+
190
+ for (const e of examples) {
191
+ if (heldOut.has(e.itemId)) {
192
+ return {
193
+ ok: false,
194
+ code: ENC_QUALIFICATION_FAIL.HELD_OUT_IN_FIT,
195
+ reason: `held-out item ${e.itemId} leaked into calibration fit`,
196
+ };
197
+ }
198
+ }
199
+
200
+ // Stable order by (score, itemId) — arrival order never affects the digest or
201
+ // the fit; ties resolve by item ID bytewise (stable score/id ties).
202
+ const stable = examples
203
+ .slice()
204
+ .sort((a, b) => (a.score - b.score) || (a.itemId < b.itemId ? -1 : a.itemId > b.itemId ? 1 : 0));
205
+
206
+ const usedGroups = options.groups ?? stable.map((e) => ({ repository: e.repository, session: e.session }));
207
+ const splitDigest = calibrationSplitDigest(usedGroups);
208
+ const temperatures: Record<EncoderHeadName, number> = {} as Record<EncoderHeadName, number>;
209
+ const thresholds: Record<EncoderHeadName, number> = {} as Record<EncoderHeadName, number>;
210
+ for (const head of ENCODER_HEAD_ORDER) {
211
+ temperatures[head] = fitTemperature(head, seed);
212
+ thresholds[head] = fitThreshold(head, stable);
213
+ }
214
+
215
+ const calibration: CalibrationV1 = {
216
+ schema: "calibration-v1",
217
+ headOrder: [...ENCODER_HEAD_ORDER],
218
+ calibrationSplitDigest: splitDigest,
219
+ fittedOnCalibrationOnly: true,
220
+ temperatures,
221
+ thresholds,
222
+ seed,
223
+ };
224
+ return { ok: true, calibration };
225
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * vector-cortex/encoder/emit-vc2c.ts — VC2C observability seam.
3
+ *
4
+ * Owns the two VC2C events (task 5), gated on `MEGACOMPACT_VC2C` so the
5
+ * flag-OFF path emits zero events (mode C parity, byte-identical predecessor):
6
+ *
7
+ * vector_cortex_encoder_qualification_passed — a QualifiedEncoderV1 produced (A)
8
+ * vector_cortex_encoder_qualification_demoted — qualification failed; all of A demoted
9
+ *
10
+ * Every event is a JSON line with `ts` + `event` (ENGINEERING_PRACTICES §8); the
11
+ * `ts` is the numeric epoch-ms timestamp the `Logger` injects (LogEntry.ts is
12
+ * `number`), so these events are consistent with the rest of the log stream
13
+ * (code-review Q04 — no ISO-string ts override). The emitters are non-fatal
14
+ * (never break the agent loop). Pi-agnostic, zero network (PREVENT-PI-004), no
15
+ * `any` (PREVENT-011).
16
+ */
17
+
18
+ import { VC2C_ENABLED } from "../../config/vector-cortex.js";
19
+ import { Logger } from "../../log.js";
20
+
21
+ export type EncoderEmit = (event: string, fields: Record<string, unknown>) => void;
22
+
23
+ /** The two-event VC2C reporter surface (qualification passed / demoted). */
24
+ export interface EncoderQualificationReporter {
25
+ readonly qualificationPassed: (fields: Record<string, unknown>) => void;
26
+ readonly qualificationDemoted: (fields: Record<string, unknown>) => void;
27
+ }
28
+
29
+ /**
30
+ * The default emitter: routes both VC2C events into the append-only structured
31
+ * logger (`src/log.ts`) as JSON lines with `event` (the logger injects the
32
+ * numeric `ts`). Supplying `emit:` to `createEncoderQualificationReporter`
33
+ * replaces this with a caller-provided sink (used by tests and downstream
34
+ * consumers). Making the default a REAL producer means a caller that just
35
+ * invokes the producer seam (`selectQualifiedEncoder`) without injecting an
36
+ * emitter still yields structured telemetry instead of silently dropping every
37
+ * event (task 5). Best-effort: the logger swallows all I/O errors.
38
+ */
39
+ function defaultEmitFor(): EncoderEmit {
40
+ const logger = new Logger();
41
+ return (event, fields) => {
42
+ logger.info(event, fields);
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Flag-gated emit, defaulting to a real logger-backed sink. The returned
48
+ * reporter is itself flag-gated (`VC2C_ENABLED`), so wiring it into a producer
49
+ * seam yields zero emissions when `MEGACOMPACT_VC2C=0` (byte-identical to the
50
+ * predecessor). Pass an explicit `emit` to route elsewhere (tests, downstream
51
+ * consumers); omit it to emit real structured log lines. The `ts` is set by the
52
+ * underlying sink (numeric epoch-ms from `Logger`), not overridden here (Q04).
53
+ */
54
+ export function createEncoderQualificationReporter(
55
+ emit?: EncoderEmit,
56
+ ): EncoderQualificationReporter {
57
+ const sink = emit ?? defaultEmitFor();
58
+ const fire = (event: string, fields: Record<string, unknown>): void => {
59
+ if (!VC2C_ENABLED()) return;
60
+ try {
61
+ sink(event, fields);
62
+ } catch {
63
+ /* non-fatal observability */
64
+ }
65
+ };
66
+ return {
67
+ qualificationPassed: (fields) => fire("vector_cortex_encoder_qualification_passed", fields),
68
+ qualificationDemoted: (fields) => fire("vector_cortex_encoder_qualification_demoted", fields),
69
+ };
70
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * vector-cortex/encoder/fallback.ts — VC2C qualification fallback selection.
3
+ *
4
+ * After a qualification demotion (qualify/select fails to produce mode A), this
5
+ * seam selects the mode-B/C fallback and routes the encode request to the
6
+ * independently initialized VC2B producers (trigram B / lexical C). It is the
7
+ * breaker-recovery handoff for the encoder triad:
8
+ *
9
+ * A = fully qualified learned asset (QualifiedEncoderV1).
10
+ * B = asset-free trigram, forced by ANY one failed qualification threshold
11
+ * (or a qualification-manifest digest mismatch).
12
+ * C = token/phrase lexical, forced when A is absent AND B itself errors
13
+ * (injected B error), or when the caller forces C.
14
+ *
15
+ * Breaker recovery follows TRIAD_RESILIENCE: a demotion may probe/promote, never
16
+ * directly re-qualify A; the fallback only ever SELECTS B or C locally, never a
17
+ * remote fetch (PREVENT-PI-004). C states its loss of old semantic context.
18
+ *
19
+ * Pi-agnostic, zero network (PREVENT-PI-004), no `any` (PREVENT-011).
20
+ */
21
+
22
+ import { createEncoderHeadsReporter, type EncoderHeadsReporter } from "./emit-vc2b.js";
23
+ import { embedTrigram512, selectTrigramBFallback, ENCODER_TRIGRAM_WIDTH } from "./trigram.js";
24
+ import { embedLexical, selectLexicalC, ENCODER_LEXICAL_WIDTH, ENCODER_LEXICAL_LIMITATION } from "./lexical.js";
25
+
26
+ export type QualificationFallbackVerdict =
27
+ | {
28
+ readonly ok: true;
29
+ readonly mode: "B";
30
+ readonly vector: Float32Array;
31
+ readonly width: number;
32
+ /** The qualification failure code that forced B (never null on this path). */
33
+ readonly code: string;
34
+ readonly limitation: null;
35
+ }
36
+ | {
37
+ readonly ok: true;
38
+ readonly mode: "C";
39
+ readonly vector: Float32Array;
40
+ readonly width: number;
41
+ /** The qualification or B failure code that forced C. */
42
+ readonly code: string;
43
+ readonly limitation: string;
44
+ };
45
+
46
+ export interface FallbackSelectionOptions {
47
+ readonly reporter?: EncoderHeadsReporter;
48
+ /** Simulates a B initializer failure (forces C despite B being available). */
49
+ readonly injectBError?: boolean;
50
+ /** Force lexical C directly (skips the B attempt). */
51
+ readonly forceC?: boolean;
52
+ }
53
+
54
+ /** Deterministic text derived from an int token sequence (mirrors router.ts). */
55
+ function textFromTokens(tokens: readonly number[]): string {
56
+ return tokens.join("-");
57
+ }
58
+
59
+ /**
60
+ * Select the B/C fallback for a failed qualification and produce the encode
61
+ * vector. When `injectBError` or `forceC` is set, selection lands on lexical C
62
+ * (absent A + injected B error -> C); otherwise a qualification THRESHOLD or
63
+ * DIGEST mismatch selects trigram B. Never returns without an explicit verdict.
64
+ */
65
+ export function selectQualificationFallback(
66
+ qualificationCode: string,
67
+ tokens: readonly number[],
68
+ options: FallbackSelectionOptions = {},
69
+ ): QualificationFallbackVerdict {
70
+ const reporter = options.reporter ?? createEncoderHeadsReporter();
71
+ const tokensText = textFromTokens(tokens);
72
+
73
+ // A absent + injected/forced B error -> C (lexical) with the documented
74
+ // semantic-context limitation.
75
+ if (options.forceC || options.injectBError) {
76
+ selectLexicalC({ reporter });
77
+ const vector = embedLexical(tokensText);
78
+ return {
79
+ ok: true,
80
+ mode: "C",
81
+ vector,
82
+ width: ENCODER_LEXICAL_WIDTH,
83
+ code: options.injectBError ? "ENC_B_INJECTED_ERROR" : qualificationCode,
84
+ limitation: ENCODER_LEXICAL_LIMITATION,
85
+ };
86
+ }
87
+
88
+ // A qualification demotion selects the independently initialized trigram B.
89
+ selectTrigramBFallback({ reporter });
90
+ const vector = embedTrigram512(tokensText);
91
+ return {
92
+ ok: true,
93
+ mode: "B",
94
+ vector,
95
+ width: ENCODER_TRIGRAM_WIDTH,
96
+ code: qualificationCode,
97
+ limitation: null,
98
+ };
99
+ }