pi-mega-compact 0.20.35 → 0.20.36

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.
@@ -0,0 +1,28 @@
1
+ /**
2
+ * config/vector-cortex-ml5a.ts — ML5-A five-head training + calibration flag.
3
+ *
4
+ * Extracted from vector-cortex.ts so that file stays under the 300-line soft
5
+ * limit (soft-as-hard gate), exactly as vector-cortex-vc9a.ts..vector-cortex-vc9d.ts
6
+ * were. This is the ML5 training sprint flag. vector-cortex.ts re-exports the
7
+ * ENUM below and root src/config.ts re-exports it, so no consumer import path
8
+ * changes.
9
+ *
10
+ * The split is purely mechanical: ML5A_ENABLED is byte-identical in name,
11
+ * semantics, and default to the definition it replaces, and vector-cortex.ts
12
+ * re-exports it so every existing `from "./config/vector-cortex.js"` import
13
+ * keeps resolving unchanged.
14
+ *
15
+ * Pi-agnostic, dependency-free (PREVENT-PI-004 / PREVENT-011).
16
+ */
17
+ import { sprintFlag } from "./vector-cortex-flag.js";
18
+ /**
19
+ * ML5-A — five-head training + calibrated onnx asset. Default ON.
20
+ * `MEGACOMPACT_ML5_A=0` disables and is byte-identical to the placeholder
21
+ * predecessor (VC2C-era): `calibrate.ts`/`heads.ts` keep serving the LCG fake
22
+ * projections and the placeholder `fitTemperature`/`fitThreshold`, mode B
23
+ * trigram continues serving, and no trained artifact is loaded (a fresh/no
24
+ * corpus also no-ops gracefully — asset_emitted:false, placeholder behavior,
25
+ * byte-identical). This flag MUST also be a dashboard SETTINGS toggle (visible
26
+ * in config UI, never in EXCLUDED_SETTINGS), mirroring VC4A..VC9D.
27
+ */
28
+ export const ML5A_ENABLED = () => sprintFlag("MEGACOMPACT_ML5_A");
@@ -239,16 +239,15 @@ export const VC8A_ENABLED = () => sprintFlag("MEGACOMPACT_VC8A");
239
239
  * mirroring VC4A..VC8A.
240
240
  */
241
241
  export const VC8B_ENABLED = () => sprintFlag("MEGACOMPACT_VC8B");
242
- // VC8C (canary selection + external Rust parity) extracted to
243
- // vector-cortex-vc8c.ts to keep this file under the 300-line soft limit.
244
- // Re-exported here so every existing `from "./config/vector-cortex.js"`
245
- // import keeps resolving unchanged.
242
+ // VC8C (canary selection + Rust parity) extracted to vector-cortex-vc8c.ts;
243
+ // re-exported so existing `./config/vector-cortex.js` imports keep resolving.
246
244
  export { VC8C_ENABLED } from "./vector-cortex-vc8c.js";
247
- // VC9A/VC9B/VC9C/VC9D split to vector-cortex-vc9{a,b,c,d}.ts to stay under the 300-line soft limit.
245
+ // VC9A/VC9B/VC9C/VC9D/PCC/ML5A split to sibling files to stay under the 300-line soft limit.
248
246
  export { VC9A_ENABLED } from "./vector-cortex-vc9a.js";
249
247
  export { VC9B_ENABLED } from "./vector-cortex-vc9b.js";
250
248
  export { VC9C_ENABLED } from "./vector-cortex-vc9c.js";
251
249
  export { VC9D_ENABLED } from "./vector-cortex-vc9d.js";
252
250
  export { PCC_ENABLED } from "./vector-cortex-pcc.js";
251
+ export { ML5A_ENABLED } from "./vector-cortex-ml5a.js";
253
252
  // Breaker constants (TRIAD_RESILIENCE.md §breaker) extracted to vector-cortex-breakers.ts.
254
253
  export { BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./vector-cortex-breakers.js";
package/dist/config.js CHANGED
@@ -114,4 +114,4 @@ export const NEW_UI = () => ragEnabled("MEGACOMPACT_NEW_UI");
114
114
  // default ON, `=0`/`_DISABLED` off. Re-exported from src/config/vector-cortex.ts
115
115
  // so root consumers share one source of truth.
116
116
  // ---------------------------------------------------------------------------
117
- export { VC0A_ENABLED, VC0B_ENABLED, VC1A_ENABLED, VC0C_ENABLED, VC1B_ENABLED, VC1C_ENABLED, VC2A_ENABLED, VC2B_ENABLED, VC2C_ENABLED, VC3A_ENABLED, VC3B_ENABLED, VC3C_ENABLED, VC4A_ENABLED, VC4B_ENABLED, VC4C_ENABLED, VC5A_ENABLED, VC5B_ENABLED, VC5C_ENABLED, VC6A_ENABLED, VC6B_ENABLED, VC6C_ENABLED, VC7A_ENABLED, VC7B_ENABLED, VC7C_ENABLED, VC8A_ENABLED, VC8B_ENABLED, VC8C_ENABLED, VC9A_ENABLED, VC9B_ENABLED, VC9C_ENABLED, VC9D_ENABLED, PCC_ENABLED, BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./config/vector-cortex.js";
117
+ export { VC0A_ENABLED, VC0B_ENABLED, VC1A_ENABLED, VC0C_ENABLED, VC1B_ENABLED, VC1C_ENABLED, VC2A_ENABLED, VC2B_ENABLED, VC2C_ENABLED, VC3A_ENABLED, VC3B_ENABLED, VC3C_ENABLED, VC4A_ENABLED, VC4B_ENABLED, VC4C_ENABLED, VC5A_ENABLED, VC5B_ENABLED, VC5C_ENABLED, VC6A_ENABLED, VC6B_ENABLED, VC6C_ENABLED, VC7A_ENABLED, VC7B_ENABLED, VC7C_ENABLED, VC8A_ENABLED, VC8B_ENABLED, VC8C_ENABLED, VC9A_ENABLED, VC9B_ENABLED, VC9C_ENABLED, VC9D_ENABLED, PCC_ENABLED, ML5A_ENABLED, BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./config/vector-cortex.js";
@@ -53,5 +53,6 @@ export const VECTOR_CORTEX_SETTINGS = {
53
53
  boolDirect("MEGACOMPACT_VC9C", "VC9C SetupTab Cortex Sub-tab", "SetupTab Cortex sub-tab (client UI): a Cortex sub-tab inside SetupTab that consumes the VC9A status endpoint (GET /api/setup-cortex-status) + the VC9B action endpoints. It surfaces the encoder mode A/B/C, asset digest prefix, qualification verdict + threshold failures, the open hard-gate blockers, and the confirmation-gated fetch/bench/verify actions. OFF = byte-identical predecessor (VC9B-era): the Cortex sub-tab is filtered from SUB_TABS and the Setup tab renders exactly as before.", true),
54
54
  boolDirect("MEGACOMPACT_VC9D", "VC9D Embedder Detect Consolidation", "Embedder-detect consolidation + VC9 workstream roll-up: memoizes /api/setup-detect against the mutable input (resolved binary path + mtime) so consecutive requests reuse the result without re-spawning, and unifies the embedder + cortex sub-tabs' 5s poll contract. OFF = byte-identical predecessor (VC9C-era): detect spawns fresh per request and the embedder poll keeps its previous cadence.", true),
55
55
  boolDirect("MEGACOMPACT_PC_C", "PC-C Dashboard Cache Visibility", "Dashboard per-turn prompt-cache visibility: surfaces the per-turn stable-prefix ratio trend (GET /api/prefix-stability) in the CacheTab PrefixStabilityCard. Reads aggregate ratios/counts from the local monitoring events log only — no payload bytes. OFF = byte-identical predecessor (PC-B-era): /api/prefix-stability returns 404 and the CacheTab omits the PrefixStabilityCard.", true),
56
+ boolDirect("MEGACOMPACT_ML5_A", "ML5-A Five-Head Training Load", "ML5-A real trained-head loading: loadHeadProjections (trained-heads-v1) feeds selectQualifiedEncoder (trainedHeadsPath atomic demotion) + loadCalibrationV1. ON (default) = a pinned trained-heads path must load for mode A. OFF = loaders return null and selection ignores trainedHeadsPath — byte-identical to the placeholder-weighted VC2C path.", true),
56
57
  ],
57
58
  };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * config/vector-cortex-ml5a.ts — ML5-A five-head training + calibration flag.
3
+ *
4
+ * Extracted from vector-cortex.ts so that file stays under the 300-line soft
5
+ * limit (soft-as-hard gate), exactly as vector-cortex-vc9a.ts..vector-cortex-vc9d.ts
6
+ * were. This is the ML5 training sprint flag. vector-cortex.ts re-exports the
7
+ * ENUM below and root src/config.ts re-exports it, so no consumer import path
8
+ * changes.
9
+ *
10
+ * The split is purely mechanical: ML5A_ENABLED is byte-identical in name,
11
+ * semantics, and default to the definition it replaces, and vector-cortex.ts
12
+ * re-exports it so every existing `from "./config/vector-cortex.js"` import
13
+ * keeps resolving unchanged.
14
+ *
15
+ * Pi-agnostic, dependency-free (PREVENT-PI-004 / PREVENT-011).
16
+ */
17
+ import { sprintFlag } from "./vector-cortex-flag.js";
18
+ /**
19
+ * ML5-A — five-head training + calibrated onnx asset. Default ON.
20
+ * `MEGACOMPACT_ML5_A=0` disables and is byte-identical to the placeholder
21
+ * predecessor (VC2C-era): `calibrate.ts`/`heads.ts` keep serving the LCG fake
22
+ * projections and the placeholder `fitTemperature`/`fitThreshold`, mode B
23
+ * trigram continues serving, and no trained artifact is loaded (a fresh/no
24
+ * corpus also no-ops gracefully — asset_emitted:false, placeholder behavior,
25
+ * byte-identical). This flag MUST also be a dashboard SETTINGS toggle (visible
26
+ * in config UI, never in EXCLUDED_SETTINGS), mirroring VC4A..VC9D.
27
+ */
28
+ export const ML5A_ENABLED = () => sprintFlag("MEGACOMPACT_ML5_A");
@@ -239,16 +239,15 @@ export const VC8A_ENABLED = () => sprintFlag("MEGACOMPACT_VC8A");
239
239
  * mirroring VC4A..VC8A.
240
240
  */
241
241
  export const VC8B_ENABLED = () => sprintFlag("MEGACOMPACT_VC8B");
242
- // VC8C (canary selection + external Rust parity) extracted to
243
- // vector-cortex-vc8c.ts to keep this file under the 300-line soft limit.
244
- // Re-exported here so every existing `from "./config/vector-cortex.js"`
245
- // import keeps resolving unchanged.
242
+ // VC8C (canary selection + Rust parity) extracted to vector-cortex-vc8c.ts;
243
+ // re-exported so existing `./config/vector-cortex.js` imports keep resolving.
246
244
  export { VC8C_ENABLED } from "./vector-cortex-vc8c.js";
247
- // VC9A/VC9B/VC9C/VC9D split to vector-cortex-vc9{a,b,c,d}.ts to stay under the 300-line soft limit.
245
+ // VC9A/VC9B/VC9C/VC9D/PCC/ML5A split to sibling files to stay under the 300-line soft limit.
248
246
  export { VC9A_ENABLED } from "./vector-cortex-vc9a.js";
249
247
  export { VC9B_ENABLED } from "./vector-cortex-vc9b.js";
250
248
  export { VC9C_ENABLED } from "./vector-cortex-vc9c.js";
251
249
  export { VC9D_ENABLED } from "./vector-cortex-vc9d.js";
252
250
  export { PCC_ENABLED } from "./vector-cortex-pcc.js";
251
+ export { ML5A_ENABLED } from "./vector-cortex-ml5a.js";
253
252
  // Breaker constants (TRIAD_RESILIENCE.md §breaker) extracted to vector-cortex-breakers.ts.
254
253
  export { BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./vector-cortex-breakers.js";
@@ -114,4 +114,4 @@ export const NEW_UI = () => ragEnabled("MEGACOMPACT_NEW_UI");
114
114
  // default ON, `=0`/`_DISABLED` off. Re-exported from src/config/vector-cortex.ts
115
115
  // so root consumers share one source of truth.
116
116
  // ---------------------------------------------------------------------------
117
- export { VC0A_ENABLED, VC0B_ENABLED, VC1A_ENABLED, VC0C_ENABLED, VC1B_ENABLED, VC1C_ENABLED, VC2A_ENABLED, VC2B_ENABLED, VC2C_ENABLED, VC3A_ENABLED, VC3B_ENABLED, VC3C_ENABLED, VC4A_ENABLED, VC4B_ENABLED, VC4C_ENABLED, VC5A_ENABLED, VC5B_ENABLED, VC5C_ENABLED, VC6A_ENABLED, VC6B_ENABLED, VC6C_ENABLED, VC7A_ENABLED, VC7B_ENABLED, VC7C_ENABLED, VC8A_ENABLED, VC8B_ENABLED, VC8C_ENABLED, VC9A_ENABLED, VC9B_ENABLED, VC9C_ENABLED, VC9D_ENABLED, PCC_ENABLED, BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./config/vector-cortex.js";
117
+ export { VC0A_ENABLED, VC0B_ENABLED, VC1A_ENABLED, VC0C_ENABLED, VC1B_ENABLED, VC1C_ENABLED, VC2A_ENABLED, VC2B_ENABLED, VC2C_ENABLED, VC3A_ENABLED, VC3B_ENABLED, VC3C_ENABLED, VC4A_ENABLED, VC4B_ENABLED, VC4C_ENABLED, VC5A_ENABLED, VC5B_ENABLED, VC5C_ENABLED, VC6A_ENABLED, VC6B_ENABLED, VC6C_ENABLED, VC7A_ENABLED, VC7B_ENABLED, VC7C_ENABLED, VC8A_ENABLED, VC8B_ENABLED, VC8C_ENABLED, VC9A_ENABLED, VC9B_ENABLED, VC9C_ENABLED, VC9D_ENABLED, PCC_ENABLED, ML5A_ENABLED, BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./config/vector-cortex.js";
@@ -22,6 +22,8 @@
22
22
  * (PREVENT-011).
23
23
  */
24
24
  import { createHash } from "node:crypto";
25
+ import { readFileSync } from "node:fs";
26
+ import { ML5A_ENABLED } from "../../config/vector-cortex.js";
25
27
  import { ENCODER_HEAD_ORDER, ENCODER_SEED, ENC_QUALIFICATION_FAIL, } from "./types.js";
26
28
  /** Canonical digests of a sorted stable representation (order-invariant). */
27
29
  function digestStrings(values) {
@@ -172,3 +174,56 @@ export function fitCalibration(examples, options = {}) {
172
174
  };
173
175
  return { ok: true, calibration };
174
176
  }
177
+ /**
178
+ * Load a persisted `CalibrationV1` artifact (schema "calibration-v1") from disk.
179
+ * ML5-A: gated on MEGACOMPACT_ML5_A; flag-off, absent file, malformed JSON,
180
+ * wrong schema, non-canonical five-head order, or non-finite temp/threshold each
181
+ * return null (non-fatal, never throws). Deterministic, local (PREVENT-PI-004).
182
+ */
183
+ export function loadCalibrationV1(path) {
184
+ if (!ML5A_ENABLED())
185
+ return null;
186
+ let raw;
187
+ try {
188
+ raw = readFileSync(path, "utf8");
189
+ }
190
+ catch {
191
+ return null;
192
+ }
193
+ let parsed;
194
+ try {
195
+ parsed = JSON.parse(raw);
196
+ }
197
+ catch {
198
+ return null;
199
+ }
200
+ const r = parsed;
201
+ if (!r || r["schema"] !== "calibration-v1")
202
+ return null;
203
+ const order = r["headOrder"];
204
+ if (!Array.isArray(order))
205
+ return null;
206
+ if (order.length !== ENCODER_HEAD_ORDER.length || !ENCODER_HEAD_ORDER.every((h, i) => order[i] === h)) {
207
+ return null;
208
+ }
209
+ const temperatures = r["temperatures"];
210
+ const thresholds = r["thresholds"];
211
+ const splitDigest = r["calibrationSplitDigest"];
212
+ if (!temperatures || !thresholds || typeof splitDigest !== "string" || splitDigest.length !== 64)
213
+ return null;
214
+ for (const h of ENCODER_HEAD_ORDER) {
215
+ const t = Number(temperatures[h]);
216
+ const th = Number(thresholds[h]);
217
+ if (!Number.isFinite(t) || !Number.isFinite(th))
218
+ return null;
219
+ }
220
+ return {
221
+ schema: "calibration-v1",
222
+ headOrder: [...ENCODER_HEAD_ORDER],
223
+ calibrationSplitDigest: splitDigest,
224
+ fittedOnCalibrationOnly: true,
225
+ temperatures: { ...temperatures },
226
+ thresholds: { ...thresholds },
227
+ seed: Number(r["seed"] ?? ENCODER_SEED),
228
+ };
229
+ }
@@ -19,6 +19,8 @@
19
19
  *
20
20
  * Pi-agnostic, zero network (PREVENT-PI-004), no `any` (PREVENT-011).
21
21
  */
22
+ import { readFileSync } from "node:fs";
23
+ import { ML5A_ENABLED } from "../../config/vector-cortex.js";
22
24
  import { ENCODER_HEAD_DIMS, ENCODER_HEAD_ORDER, ENCODER_HEAD_LOSS_WEIGHTS, ENCODER_HEAD_LOSS_SUM, ENCODER_SEED, } from "./types.js";
23
25
  import { createEncoderHeadsReporter, NOOP_VC2B_REPORTER, } from "./emit-vc2b.js";
24
26
  /** The stable head index of a head name (its position in ENCODER_HEAD_ORDER). */
@@ -110,4 +112,89 @@ export function encodeVectorSet(tokens, options = {}) {
110
112
  export function headLossWeights() {
111
113
  return { ...ENCODER_HEAD_LOSS_WEIGHTS };
112
114
  }
115
+ /** True when every head's output dim + weight length matches the contract. */
116
+ export function headsShapeValid(t) {
117
+ return ENCODER_HEAD_ORDER.every((h) => t.dims[h] === ENCODER_HEAD_DIMS[h] && t.weights[h].length === ENCODER_HEAD_DIMS[h] * t.trunkDim);
118
+ }
119
+ /**
120
+ * Load a `trained-heads-v1` artifact into a `HeadProjectionTable`. Gated on
121
+ * MEGACOMPACT_ML5_A: flag-off, absent file, malformed JSON, wrong schema,
122
+ * wrong seed, or a shape mismatch each return null (non-fatal). Deterministic
123
+ * and local (PREVENT-PI-004).
124
+ */
125
+ export function loadHeadProjections(path) {
126
+ if (!ML5A_ENABLED())
127
+ return null;
128
+ let raw;
129
+ try {
130
+ raw = readFileSync(path, "utf8");
131
+ }
132
+ catch {
133
+ return null;
134
+ }
135
+ let parsed;
136
+ try {
137
+ parsed = JSON.parse(raw);
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ const r = parsed;
143
+ if (!r || r["schema"] !== "trained-heads-v1")
144
+ return null;
145
+ if (r["seed"] !== ENCODER_SEED)
146
+ return null;
147
+ const dims = r["dims"];
148
+ const heads = r["heads"];
149
+ if (!dims || !heads)
150
+ return null;
151
+ const trunkDim = Number(r["trunkDim"] ?? 0);
152
+ if (!Number.isFinite(trunkDim) || trunkDim <= 0)
153
+ return null;
154
+ const weights = {};
155
+ const temperatures = {};
156
+ for (const h of ENCODER_HEAD_ORDER) {
157
+ const hd = heads[h];
158
+ if (!hd || typeof hd !== "object")
159
+ return null;
160
+ const w = hd["weights"];
161
+ if (!Array.isArray(w))
162
+ return null;
163
+ weights[h] = Float32Array.from(w);
164
+ if (Number(hd["dim"] ?? 0) !== ENCODER_HEAD_DIMS[h])
165
+ return null;
166
+ temperatures[h] = Number(hd["temperature"] ?? 1);
167
+ if (!Number.isFinite(dims[h]))
168
+ return null;
169
+ }
170
+ const table = {
171
+ schema: "trained-heads-v1",
172
+ seed: Number(r["seed"]),
173
+ trunkDim,
174
+ dims: { semantic: 384, dependency: 128, contradiction: 128, cacheStability: 64, payloadRouting: 32 },
175
+ weights: weights,
176
+ temperatures: temperatures,
177
+ };
178
+ if (!headsShapeValid(table))
179
+ return null;
180
+ return table;
181
+ }
182
+ /**
183
+ * Project a trunk embedding through a trained head's real weights, applying the
184
+ * row-major matrix then L2-normalizing (all-zero on zero norm). Returns a
185
+ * `HeadVector` of the head's normative dimension.
186
+ */
187
+ export function projectHeadFromTrunk(head, trunk, table) {
188
+ const dim = ENCODER_HEAD_DIMS[head];
189
+ const W = table.weights[head];
190
+ const t = table.trunkDim;
191
+ const out = new Float32Array(dim);
192
+ for (let i = 0; i < dim; i++) {
193
+ let acc = 0;
194
+ for (let j = 0; j < t; j++)
195
+ acc += W[i * t + j] * (trunk[j] ?? 0);
196
+ out[i] = acc;
197
+ }
198
+ return { head, dim, values: l2Normalize(out) };
199
+ }
113
200
  export { ENCODER_HEAD_ORDER, ENCODER_HEAD_DIMS, ENCODER_HEAD_LOSS_SUM, ENCODER_SEED, NOOP_VC2B_REPORTER };
@@ -33,6 +33,8 @@
33
33
  * `any` (PREVENT-011).
34
34
  */
35
35
  import { createHash } from "node:crypto";
36
+ import { ML5A_ENABLED } from "../../config/vector-cortex.js";
37
+ import { loadHeadProjections } from "./heads.js";
36
38
  import { ENC_QUALIFICATION_FAIL, EVALUATION_THRESHOLDS, } from "./types.js";
37
39
  import { createEncoderQualificationReporter, } from "./emit-vc2c.js";
38
40
  /** Canonical digest over a CalibrationV1's stable identity (split digest, heads,
@@ -118,6 +120,14 @@ export function selectQualifiedEncoder(candidate, options = {}) {
118
120
  }
119
121
  // Atomic: collect EVERY failed field across asset + all heads + reconstruction.
120
122
  const failed = [];
123
+ // ML5-A: real trained-head weights must load for mode A. When the gate is on
124
+ // and a trained-heads path is pinned, an unloadable/wrong-seed/malformed
125
+ // artifact is a qualification failure (any failed field demotes ALL of A).
126
+ if (ML5A_ENABLED() && candidate.trainedHeadsPath !== undefined) {
127
+ if (loadHeadProjections(candidate.trainedHeadsPath) === null) {
128
+ failed.push("head.weights.trainedHeadsPath");
129
+ }
130
+ }
121
131
  assetPasses(candidate.asset, failed);
122
132
  const heads = ["semantic", "dependency", "contradiction", "cacheStability", "payloadRouting"];
123
133
  for (const h of heads) {
@@ -22,6 +22,8 @@
22
22
  * (PREVENT-011).
23
23
  */
24
24
  import { createHash } from "node:crypto";
25
+ import { readFileSync } from "node:fs";
26
+ import { ML5A_ENABLED } from "../../config/vector-cortex.js";
25
27
  import { ENCODER_HEAD_ORDER, ENCODER_SEED, ENC_QUALIFICATION_FAIL, } from "./types.js";
26
28
  /** Canonical digests of a sorted stable representation (order-invariant). */
27
29
  function digestStrings(values) {
@@ -172,3 +174,56 @@ export function fitCalibration(examples, options = {}) {
172
174
  };
173
175
  return { ok: true, calibration };
174
176
  }
177
+ /**
178
+ * Load a persisted `CalibrationV1` artifact (schema "calibration-v1") from disk.
179
+ * ML5-A: gated on MEGACOMPACT_ML5_A; flag-off, absent file, malformed JSON,
180
+ * wrong schema, non-canonical five-head order, or non-finite temp/threshold each
181
+ * return null (non-fatal, never throws). Deterministic, local (PREVENT-PI-004).
182
+ */
183
+ export function loadCalibrationV1(path) {
184
+ if (!ML5A_ENABLED())
185
+ return null;
186
+ let raw;
187
+ try {
188
+ raw = readFileSync(path, "utf8");
189
+ }
190
+ catch {
191
+ return null;
192
+ }
193
+ let parsed;
194
+ try {
195
+ parsed = JSON.parse(raw);
196
+ }
197
+ catch {
198
+ return null;
199
+ }
200
+ const r = parsed;
201
+ if (!r || r["schema"] !== "calibration-v1")
202
+ return null;
203
+ const order = r["headOrder"];
204
+ if (!Array.isArray(order))
205
+ return null;
206
+ if (order.length !== ENCODER_HEAD_ORDER.length || !ENCODER_HEAD_ORDER.every((h, i) => order[i] === h)) {
207
+ return null;
208
+ }
209
+ const temperatures = r["temperatures"];
210
+ const thresholds = r["thresholds"];
211
+ const splitDigest = r["calibrationSplitDigest"];
212
+ if (!temperatures || !thresholds || typeof splitDigest !== "string" || splitDigest.length !== 64)
213
+ return null;
214
+ for (const h of ENCODER_HEAD_ORDER) {
215
+ const t = Number(temperatures[h]);
216
+ const th = Number(thresholds[h]);
217
+ if (!Number.isFinite(t) || !Number.isFinite(th))
218
+ return null;
219
+ }
220
+ return {
221
+ schema: "calibration-v1",
222
+ headOrder: [...ENCODER_HEAD_ORDER],
223
+ calibrationSplitDigest: splitDigest,
224
+ fittedOnCalibrationOnly: true,
225
+ temperatures: { ...temperatures },
226
+ thresholds: { ...thresholds },
227
+ seed: Number(r["seed"] ?? ENCODER_SEED),
228
+ };
229
+ }
@@ -19,6 +19,8 @@
19
19
  *
20
20
  * Pi-agnostic, zero network (PREVENT-PI-004), no `any` (PREVENT-011).
21
21
  */
22
+ import { readFileSync } from "node:fs";
23
+ import { ML5A_ENABLED } from "../../config/vector-cortex.js";
22
24
  import { ENCODER_HEAD_DIMS, ENCODER_HEAD_ORDER, ENCODER_HEAD_LOSS_WEIGHTS, ENCODER_HEAD_LOSS_SUM, ENCODER_SEED, } from "./types.js";
23
25
  import { createEncoderHeadsReporter, NOOP_VC2B_REPORTER, } from "./emit-vc2b.js";
24
26
  /** The stable head index of a head name (its position in ENCODER_HEAD_ORDER). */
@@ -110,4 +112,89 @@ export function encodeVectorSet(tokens, options = {}) {
110
112
  export function headLossWeights() {
111
113
  return { ...ENCODER_HEAD_LOSS_WEIGHTS };
112
114
  }
115
+ /** True when every head's output dim + weight length matches the contract. */
116
+ export function headsShapeValid(t) {
117
+ return ENCODER_HEAD_ORDER.every((h) => t.dims[h] === ENCODER_HEAD_DIMS[h] && t.weights[h].length === ENCODER_HEAD_DIMS[h] * t.trunkDim);
118
+ }
119
+ /**
120
+ * Load a `trained-heads-v1` artifact into a `HeadProjectionTable`. Gated on
121
+ * MEGACOMPACT_ML5_A: flag-off, absent file, malformed JSON, wrong schema,
122
+ * wrong seed, or a shape mismatch each return null (non-fatal). Deterministic
123
+ * and local (PREVENT-PI-004).
124
+ */
125
+ export function loadHeadProjections(path) {
126
+ if (!ML5A_ENABLED())
127
+ return null;
128
+ let raw;
129
+ try {
130
+ raw = readFileSync(path, "utf8");
131
+ }
132
+ catch {
133
+ return null;
134
+ }
135
+ let parsed;
136
+ try {
137
+ parsed = JSON.parse(raw);
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ const r = parsed;
143
+ if (!r || r["schema"] !== "trained-heads-v1")
144
+ return null;
145
+ if (r["seed"] !== ENCODER_SEED)
146
+ return null;
147
+ const dims = r["dims"];
148
+ const heads = r["heads"];
149
+ if (!dims || !heads)
150
+ return null;
151
+ const trunkDim = Number(r["trunkDim"] ?? 0);
152
+ if (!Number.isFinite(trunkDim) || trunkDim <= 0)
153
+ return null;
154
+ const weights = {};
155
+ const temperatures = {};
156
+ for (const h of ENCODER_HEAD_ORDER) {
157
+ const hd = heads[h];
158
+ if (!hd || typeof hd !== "object")
159
+ return null;
160
+ const w = hd["weights"];
161
+ if (!Array.isArray(w))
162
+ return null;
163
+ weights[h] = Float32Array.from(w);
164
+ if (Number(hd["dim"] ?? 0) !== ENCODER_HEAD_DIMS[h])
165
+ return null;
166
+ temperatures[h] = Number(hd["temperature"] ?? 1);
167
+ if (!Number.isFinite(dims[h]))
168
+ return null;
169
+ }
170
+ const table = {
171
+ schema: "trained-heads-v1",
172
+ seed: Number(r["seed"]),
173
+ trunkDim,
174
+ dims: { semantic: 384, dependency: 128, contradiction: 128, cacheStability: 64, payloadRouting: 32 },
175
+ weights: weights,
176
+ temperatures: temperatures,
177
+ };
178
+ if (!headsShapeValid(table))
179
+ return null;
180
+ return table;
181
+ }
182
+ /**
183
+ * Project a trunk embedding through a trained head's real weights, applying the
184
+ * row-major matrix then L2-normalizing (all-zero on zero norm). Returns a
185
+ * `HeadVector` of the head's normative dimension.
186
+ */
187
+ export function projectHeadFromTrunk(head, trunk, table) {
188
+ const dim = ENCODER_HEAD_DIMS[head];
189
+ const W = table.weights[head];
190
+ const t = table.trunkDim;
191
+ const out = new Float32Array(dim);
192
+ for (let i = 0; i < dim; i++) {
193
+ let acc = 0;
194
+ for (let j = 0; j < t; j++)
195
+ acc += W[i * t + j] * (trunk[j] ?? 0);
196
+ out[i] = acc;
197
+ }
198
+ return { head, dim, values: l2Normalize(out) };
199
+ }
113
200
  export { ENCODER_HEAD_ORDER, ENCODER_HEAD_DIMS, ENCODER_HEAD_LOSS_SUM, ENCODER_SEED, NOOP_VC2B_REPORTER };
@@ -33,6 +33,8 @@
33
33
  * `any` (PREVENT-011).
34
34
  */
35
35
  import { createHash } from "node:crypto";
36
+ import { ML5A_ENABLED } from "../../config/vector-cortex.js";
37
+ import { loadHeadProjections } from "./heads.js";
36
38
  import { ENC_QUALIFICATION_FAIL, EVALUATION_THRESHOLDS, } from "./types.js";
37
39
  import { createEncoderQualificationReporter, } from "./emit-vc2c.js";
38
40
  /** Canonical digest over a CalibrationV1's stable identity (split digest, heads,
@@ -118,6 +120,14 @@ export function selectQualifiedEncoder(candidate, options = {}) {
118
120
  }
119
121
  // Atomic: collect EVERY failed field across asset + all heads + reconstruction.
120
122
  const failed = [];
123
+ // ML5-A: real trained-head weights must load for mode A. When the gate is on
124
+ // and a trained-heads path is pinned, an unloadable/wrong-seed/malformed
125
+ // artifact is a qualification failure (any failed field demotes ALL of A).
126
+ if (ML5A_ENABLED() && candidate.trainedHeadsPath !== undefined) {
127
+ if (loadHeadProjections(candidate.trainedHeadsPath) === null) {
128
+ failed.push("head.weights.trainedHeadsPath");
129
+ }
130
+ }
121
131
  assetPasses(candidate.asset, failed);
122
132
  const heads = ["semantic", "dependency", "contradiction", "cacheStability", "payloadRouting"];
123
133
  for (const h of heads) {
@@ -222,5 +222,11 @@ export const VECTOR_CORTEX_SETTINGS: SettingGroup = {
222
222
  "Dashboard per-turn prompt-cache visibility: surfaces the per-turn stable-prefix ratio trend (GET /api/prefix-stability) in the CacheTab PrefixStabilityCard. Reads aggregate ratios/counts from the local monitoring events log only — no payload bytes. OFF = byte-identical predecessor (PC-B-era): /api/prefix-stability returns 404 and the CacheTab omits the PrefixStabilityCard.",
223
223
  true,
224
224
  ),
225
+ boolDirect(
226
+ "MEGACOMPACT_ML5_A",
227
+ "ML5-A Five-Head Training Load",
228
+ "ML5-A real trained-head loading: loadHeadProjections (trained-heads-v1) feeds selectQualifiedEncoder (trainedHeadsPath atomic demotion) + loadCalibrationV1. ON (default) = a pinned trained-heads path must load for mode A. OFF = loaders return null and selection ignores trainedHeadsPath — byte-identical to the placeholder-weighted VC2C path.",
229
+ true,
230
+ ),
225
231
  ],
226
232
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.20.35",
3
+ "version": "0.20.36",
4
4
  "description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
5
5
  "type": "module",
6
6
  "license": "BSD-3-Clause",
@@ -0,0 +1,30 @@
1
+ /**
2
+ * config/vector-cortex-ml5a.ts — ML5-A five-head training + calibration flag.
3
+ *
4
+ * Extracted from vector-cortex.ts so that file stays under the 300-line soft
5
+ * limit (soft-as-hard gate), exactly as vector-cortex-vc9a.ts..vector-cortex-vc9d.ts
6
+ * were. This is the ML5 training sprint flag. vector-cortex.ts re-exports the
7
+ * ENUM below and root src/config.ts re-exports it, so no consumer import path
8
+ * changes.
9
+ *
10
+ * The split is purely mechanical: ML5A_ENABLED is byte-identical in name,
11
+ * semantics, and default to the definition it replaces, and vector-cortex.ts
12
+ * re-exports it so every existing `from "./config/vector-cortex.js"` import
13
+ * keeps resolving unchanged.
14
+ *
15
+ * Pi-agnostic, dependency-free (PREVENT-PI-004 / PREVENT-011).
16
+ */
17
+
18
+ import { sprintFlag } from "./vector-cortex-flag.js";
19
+
20
+ /**
21
+ * ML5-A — five-head training + calibrated onnx asset. Default ON.
22
+ * `MEGACOMPACT_ML5_A=0` disables and is byte-identical to the placeholder
23
+ * predecessor (VC2C-era): `calibrate.ts`/`heads.ts` keep serving the LCG fake
24
+ * projections and the placeholder `fitTemperature`/`fitThreshold`, mode B
25
+ * trigram continues serving, and no trained artifact is loaded (a fresh/no
26
+ * corpus also no-ops gracefully — asset_emitted:false, placeholder behavior,
27
+ * byte-identical). This flag MUST also be a dashboard SETTINGS toggle (visible
28
+ * in config UI, never in EXCLUDED_SETTINGS), mirroring VC4A..VC9D.
29
+ */
30
+ export const ML5A_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_ML5_A");
@@ -269,18 +269,17 @@ export const VC8A_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC8A");
269
269
  */
270
270
  export const VC8B_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC8B");
271
271
 
272
- // VC8C (canary selection + external Rust parity) extracted to
273
- // vector-cortex-vc8c.ts to keep this file under the 300-line soft limit.
274
- // Re-exported here so every existing `from "./config/vector-cortex.js"`
275
- // import keeps resolving unchanged.
272
+ // VC8C (canary selection + Rust parity) extracted to vector-cortex-vc8c.ts;
273
+ // re-exported so existing `./config/vector-cortex.js` imports keep resolving.
276
274
  export { VC8C_ENABLED } from "./vector-cortex-vc8c.js";
277
275
 
278
- // VC9A/VC9B/VC9C/VC9D split to vector-cortex-vc9{a,b,c,d}.ts to stay under the 300-line soft limit.
276
+ // VC9A/VC9B/VC9C/VC9D/PCC/ML5A split to sibling files to stay under the 300-line soft limit.
279
277
  export { VC9A_ENABLED } from "./vector-cortex-vc9a.js";
280
278
  export { VC9B_ENABLED } from "./vector-cortex-vc9b.js";
281
279
  export { VC9C_ENABLED } from "./vector-cortex-vc9c.js";
282
280
  export { VC9D_ENABLED } from "./vector-cortex-vc9d.js";
283
281
  export { PCC_ENABLED } from "./vector-cortex-pcc.js";
282
+ export { ML5A_ENABLED } from "./vector-cortex-ml5a.js";
284
283
 
285
284
  // Breaker constants (TRIAD_RESILIENCE.md §breaker) extracted to vector-cortex-breakers.ts.
286
285
  export {
package/src/config.ts CHANGED
@@ -183,6 +183,7 @@ export {
183
183
  VC9C_ENABLED,
184
184
  VC9D_ENABLED,
185
185
  PCC_ENABLED,
186
+ ML5A_ENABLED,
186
187
  BREAKER_WINDOW_MS,
187
188
  BREAKER_MIN_ATTEMPTS,
188
189
  BREAKER_PERF_FAILURES,
@@ -23,6 +23,8 @@
23
23
  */
24
24
 
25
25
  import { createHash } from "node:crypto";
26
+ import { readFileSync } from "node:fs";
27
+ import { ML5A_ENABLED } from "../../config/vector-cortex.js";
26
28
  import {
27
29
  ENCODER_HEAD_ORDER,
28
30
  ENCODER_SEED,
@@ -223,3 +225,50 @@ export function fitCalibration(
223
225
  };
224
226
  return { ok: true, calibration };
225
227
  }
228
+
229
+ /**
230
+ * Load a persisted `CalibrationV1` artifact (schema "calibration-v1") from disk.
231
+ * ML5-A: gated on MEGACOMPACT_ML5_A; flag-off, absent file, malformed JSON,
232
+ * wrong schema, non-canonical five-head order, or non-finite temp/threshold each
233
+ * return null (non-fatal, never throws). Deterministic, local (PREVENT-PI-004).
234
+ */
235
+ export function loadCalibrationV1(path: string): CalibrationV1 | null {
236
+ if (!ML5A_ENABLED()) return null;
237
+ let raw: string;
238
+ try {
239
+ raw = readFileSync(path, "utf8");
240
+ } catch {
241
+ return null;
242
+ }
243
+ let parsed: unknown;
244
+ try {
245
+ parsed = JSON.parse(raw);
246
+ } catch {
247
+ return null;
248
+ }
249
+ const r = parsed as Record<string, unknown> | null;
250
+ if (!r || r["schema"] !== "calibration-v1") return null;
251
+ const order = r["headOrder"];
252
+ if (!Array.isArray(order)) return null;
253
+ if (order.length !== ENCODER_HEAD_ORDER.length || !ENCODER_HEAD_ORDER.every((h, i) => order[i] === h)) {
254
+ return null;
255
+ }
256
+ const temperatures = r["temperatures"] as Record<string, unknown> | undefined;
257
+ const thresholds = r["thresholds"] as Record<string, unknown> | undefined;
258
+ const splitDigest = r["calibrationSplitDigest"];
259
+ if (!temperatures || !thresholds || typeof splitDigest !== "string" || splitDigest.length !== 64) return null;
260
+ for (const h of ENCODER_HEAD_ORDER) {
261
+ const t = Number(temperatures[h]);
262
+ const th = Number(thresholds[h]);
263
+ if (!Number.isFinite(t) || !Number.isFinite(th)) return null;
264
+ }
265
+ return {
266
+ schema: "calibration-v1",
267
+ headOrder: [...ENCODER_HEAD_ORDER],
268
+ calibrationSplitDigest: splitDigest,
269
+ fittedOnCalibrationOnly: true,
270
+ temperatures: { ...(temperatures as Record<EncoderHeadName, number>) },
271
+ thresholds: { ...(thresholds as Record<EncoderHeadName, number>) },
272
+ seed: Number(r["seed"] ?? ENCODER_SEED),
273
+ };
274
+ }
@@ -20,6 +20,8 @@
20
20
  * Pi-agnostic, zero network (PREVENT-PI-004), no `any` (PREVENT-011).
21
21
  */
22
22
 
23
+ import { readFileSync } from "node:fs";
24
+ import { ML5A_ENABLED } from "../../config/vector-cortex.js";
23
25
  import {
24
26
  ENCODER_HEAD_DIMS,
25
27
  ENCODER_HEAD_ORDER,
@@ -139,4 +141,108 @@ export function headLossWeights(): Readonly<Record<EncoderHeadName, number>> {
139
141
  return { ...ENCODER_HEAD_LOSS_WEIGHTS };
140
142
  }
141
143
 
144
+ // ---------------------------------------------------------------------------
145
+ // ML5-A real trained-head loading. Produces the projection matrices that the
146
+ // deterministic placeholder `projectHead` approximates: row-major
147
+ // `weights[h]` of length `headDim * trunkDim`, applied `W[i*t+j]*trunk[j]`,
148
+ // L2-normalized. Loaded from the `trained-heads-v1` JSON that
149
+ // `training/vector-cortex/train.py` emits, under the MEGACOMPACT_ML5_A gate.
150
+ // Non-fatal: any violation (flag off, absent, malformed, wrong seed, wrong
151
+ // shape) yields null, never a throw (all loaders return null on violation).
152
+ // ---------------------------------------------------------------------------
153
+
154
+ /** The real loadable form of the trained five-head projection table. */
155
+ export interface HeadProjectionTable {
156
+ readonly schema: "trained-heads-v1";
157
+ readonly seed: number;
158
+ /** Input/trunk embedding dimension every head projects from (uniform 384). */
159
+ readonly trunkDim: number;
160
+ /** Per-head OUTPUT dimension (semantic 384 / ... / payloadRouting 32). */
161
+ readonly dims: Readonly<Record<EncoderHeadName, number>>;
162
+ /** Row-major `[headDim * trunkDim]` projection matrix per head. */
163
+ readonly weights: Readonly<Record<EncoderHeadName, Float32Array>>;
164
+ readonly temperatures: Readonly<Record<EncoderHeadName, number>>;
165
+ }
166
+
167
+ /** True when every head's output dim + weight length matches the contract. */
168
+ export function headsShapeValid(t: HeadProjectionTable): boolean {
169
+ return ENCODER_HEAD_ORDER.every(
170
+ (h) => t.dims[h] === ENCODER_HEAD_DIMS[h] && t.weights[h].length === ENCODER_HEAD_DIMS[h] * t.trunkDim,
171
+ );
172
+ }
173
+
174
+ /**
175
+ * Load a `trained-heads-v1` artifact into a `HeadProjectionTable`. Gated on
176
+ * MEGACOMPACT_ML5_A: flag-off, absent file, malformed JSON, wrong schema,
177
+ * wrong seed, or a shape mismatch each return null (non-fatal). Deterministic
178
+ * and local (PREVENT-PI-004).
179
+ */
180
+ export function loadHeadProjections(path: string): HeadProjectionTable | null {
181
+ if (!ML5A_ENABLED()) return null;
182
+ let raw: string;
183
+ try {
184
+ raw = readFileSync(path, "utf8");
185
+ } catch {
186
+ return null;
187
+ }
188
+ let parsed: unknown;
189
+ try {
190
+ parsed = JSON.parse(raw);
191
+ } catch {
192
+ return null;
193
+ }
194
+ const r = parsed as Record<string, unknown> | null;
195
+ if (!r || r["schema"] !== "trained-heads-v1") return null;
196
+ if (r["seed"] !== ENCODER_SEED) return null;
197
+ const dims = r["dims"] as Record<string, unknown> | undefined;
198
+ const heads = r["heads"] as Record<string, unknown> | undefined;
199
+ if (!dims || !heads) return null;
200
+ const trunkDim = Number(r["trunkDim"] ?? 0);
201
+ if (!Number.isFinite(trunkDim) || trunkDim <= 0) return null;
202
+ const weights: Record<string, Float32Array> = {};
203
+ const temperatures: Record<string, number> = {};
204
+ for (const h of ENCODER_HEAD_ORDER) {
205
+ const hd = heads[h] as Record<string, unknown> | undefined;
206
+ if (!hd || typeof hd !== "object") return null;
207
+ const w = hd["weights"];
208
+ if (!Array.isArray(w)) return null;
209
+ weights[h] = Float32Array.from(w as number[]);
210
+ if (Number(hd["dim"] ?? 0) !== ENCODER_HEAD_DIMS[h]) return null;
211
+ temperatures[h] = Number(hd["temperature"] ?? 1);
212
+ if (!Number.isFinite(dims[h])) return null;
213
+ }
214
+ const table: HeadProjectionTable = {
215
+ schema: "trained-heads-v1",
216
+ seed: Number(r["seed"]),
217
+ trunkDim,
218
+ dims: { semantic: 384, dependency: 128, contradiction: 128, cacheStability: 64, payloadRouting: 32 } as unknown as Record<EncoderHeadName, number>,
219
+ weights: weights as unknown as Record<EncoderHeadName, Float32Array>,
220
+ temperatures: temperatures as unknown as Record<EncoderHeadName, number>,
221
+ };
222
+ if (!headsShapeValid(table)) return null;
223
+ return table;
224
+ }
225
+
226
+ /**
227
+ * Project a trunk embedding through a trained head's real weights, applying the
228
+ * row-major matrix then L2-normalizing (all-zero on zero norm). Returns a
229
+ * `HeadVector` of the head's normative dimension.
230
+ */
231
+ export function projectHeadFromTrunk(
232
+ head: EncoderHeadName,
233
+ trunk: Float32Array,
234
+ table: HeadProjectionTable,
235
+ ): HeadVector {
236
+ const dim = ENCODER_HEAD_DIMS[head];
237
+ const W = table.weights[head];
238
+ const t = table.trunkDim;
239
+ const out = new Float32Array(dim);
240
+ for (let i = 0; i < dim; i++) {
241
+ let acc = 0;
242
+ for (let j = 0; j < t; j++) acc += W[i * t + j]! * (trunk[j] ?? 0);
243
+ out[i] = acc;
244
+ }
245
+ return { head, dim, values: l2Normalize(out) };
246
+ }
247
+
142
248
  export { ENCODER_HEAD_ORDER, ENCODER_HEAD_DIMS, ENCODER_HEAD_LOSS_SUM, ENCODER_SEED, NOOP_VC2B_REPORTER };
@@ -34,6 +34,8 @@
34
34
  */
35
35
 
36
36
  import { createHash } from "node:crypto";
37
+ import { ML5A_ENABLED } from "../../config/vector-cortex.js";
38
+ import { loadHeadProjections } from "./heads.js";
37
39
  import {
38
40
  ENC_QUALIFICATION_FAIL,
39
41
  EVALUATION_THRESHOLDS,
@@ -83,6 +85,13 @@ export interface QualificationCandidate {
83
85
  * corrupt-qualification-manifest injection (ENC_QUALIFICATION_DIGEST_MISMATCH).
84
86
  */
85
87
  readonly expectedQualificationManifestDigest?: string;
88
+ /**
89
+ * ML5-A: path to the `trained-heads-v1` artifact. When supplied AND the
90
+ * MEGACOMPACT_ML5_A gate is on, the candidate qualifies to mode A only if the
91
+ * real trained head weights load; a load failure atomically demotes all of A
92
+ * to B (non-fatal, reported as THRESHOLD_FAILED / head.weights.trainedHeadsPath).
93
+ */
94
+ readonly trainedHeadsPath?: string;
86
95
  }
87
96
 
88
97
  /**
@@ -183,6 +192,14 @@ export function selectQualifiedEncoder(
183
192
 
184
193
  // Atomic: collect EVERY failed field across asset + all heads + reconstruction.
185
194
  const failed: string[] = [];
195
+ // ML5-A: real trained-head weights must load for mode A. When the gate is on
196
+ // and a trained-heads path is pinned, an unloadable/wrong-seed/malformed
197
+ // artifact is a qualification failure (any failed field demotes ALL of A).
198
+ if (ML5A_ENABLED() && candidate.trainedHeadsPath !== undefined) {
199
+ if (loadHeadProjections(candidate.trainedHeadsPath) === null) {
200
+ failed.push("head.weights.trainedHeadsPath");
201
+ }
202
+ }
186
203
  assetPasses(candidate.asset, failed);
187
204
  const heads: EncoderHeadName[] = ["semantic", "dependency", "contradiction", "cacheStability", "payloadRouting"];
188
205
  for (const h of heads) {