pi-mega-compact 0.18.0 → 0.19.0

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 (97) 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 +18 -0
  6. package/dist/config.js +117 -0
  7. package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +4 -0
  8. package/dist/extensions/dashboard-server/routes-vector-cortex.js +89 -1
  9. package/dist/log.js +47 -0
  10. package/dist/src/config/vector-cortex.js +18 -0
  11. package/dist/src/config.js +1 -1
  12. package/dist/src/vector-cortex/encoder/calibrate.js +174 -0
  13. package/dist/src/vector-cortex/encoder/emit-vc2b.js +63 -0
  14. package/dist/src/vector-cortex/encoder/emit-vc2c.js +59 -0
  15. package/dist/src/vector-cortex/encoder/fallback.js +62 -0
  16. package/dist/src/vector-cortex/encoder/heads.js +113 -0
  17. package/dist/src/vector-cortex/encoder/lexical.js +104 -0
  18. package/dist/src/vector-cortex/encoder/router.js +115 -0
  19. package/dist/src/vector-cortex/encoder/select.js +172 -0
  20. package/dist/src/vector-cortex/encoder/trigram.js +75 -0
  21. package/dist/src/vector-cortex/encoder/types.js +84 -0
  22. package/dist/vector-cortex/encoder/calibrate.js +174 -0
  23. package/dist/vector-cortex/encoder/emit-vc2b.js +63 -0
  24. package/dist/vector-cortex/encoder/emit-vc2c.js +59 -0
  25. package/dist/vector-cortex/encoder/fallback.js +62 -0
  26. package/dist/vector-cortex/encoder/heads.js +113 -0
  27. package/dist/vector-cortex/encoder/lexical.js +104 -0
  28. package/dist/vector-cortex/encoder/router.js +115 -0
  29. package/dist/vector-cortex/encoder/select.js +172 -0
  30. package/dist/vector-cortex/encoder/trigram.js +75 -0
  31. package/dist/vector-cortex/encoder/types.js +84 -0
  32. package/extensions/dashboard-client/dist/assets/{AreaChart-BQXFF7UI.js → AreaChart-DywU5SSt.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{AreaChart-BQXFF7UI.js.map → AreaChart-DywU5SSt.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/{BarChart-BVtoz9Eg.js → BarChart-But7WN6K.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{BarChart-BVtoz9Eg.js.map → BarChart-But7WN6K.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{CacheTab-trI2TIGy.js → CacheTab-7aZWa1DR.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{CacheTab-trI2TIGy.js.map → CacheTab-7aZWa1DR.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{EventsTab-CNUbk9__.js → EventsTab-Bf_boNG2.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{EventsTab-CNUbk9__.js.map → EventsTab-Bf_boNG2.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/{HealthTab-K-bd_CM-.js → HealthTab-AbGc1nXP.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{HealthTab-K-bd_CM-.js.map → HealthTab-AbGc1nXP.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-CoEioKzx.js → MaintenanceTab-BfHHhn5F.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-CoEioKzx.js.map → MaintenanceTab-BfHHhn5F.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CJo24ulv.js → MemoryMapTab-DrcWti0-.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CJo24ulv.js.map → MemoryMapTab-DrcWti0-.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{MetricsTab-DCNlQWWP.js → MetricsTab-ALiglXQq.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{MetricsTab-DCNlQWWP.js.map → MetricsTab-ALiglXQq.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/{OverviewTab-CQQlyIQ6.js → OverviewTab-nnaJdNyR.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{OverviewTab-CQQlyIQ6.js.map → OverviewTab-nnaJdNyR.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/assets/{ReposTab-Cqm82_yn.js → ReposTab-BTK0nofM.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{ReposTab-Cqm82_yn.js.map → ReposTab-BTK0nofM.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/{SessionsTab-A8xTAdET.js → SessionsTab-RXPNqO6x.js} +2 -2
  53. package/extensions/dashboard-client/dist/assets/{SessionsTab-A8xTAdET.js.map → SessionsTab-RXPNqO6x.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/{SetupTab-CqaSgaZl.js → SetupTab-Dab0AVfs.js} +2 -2
  55. package/extensions/dashboard-client/dist/assets/{SetupTab-CqaSgaZl.js.map → SetupTab-Dab0AVfs.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-C_f3q8GR.js → TimeSavedCard-DIVG8hdt.js} +2 -2
  57. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-C_f3q8GR.js.map → TimeSavedCard-DIVG8hdt.js.map} +1 -1
  58. package/extensions/dashboard-client/dist/assets/{TurnsTab-BEsafACo.js → TurnsTab-9uz2fBwx.js} +2 -2
  59. package/extensions/dashboard-client/dist/assets/{TurnsTab-BEsafACo.js.map → TurnsTab-9uz2fBwx.js.map} +1 -1
  60. package/extensions/dashboard-client/dist/assets/{VectorCortexTab-DD1CMK80.js → VectorCortexTab-DnKY61EJ.js} +2 -2
  61. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DnKY61EJ.js.map +1 -0
  62. package/extensions/dashboard-client/dist/assets/{WikiTab-Bf01M2_Z.js → WikiTab-CpcgaggS.js} +2 -2
  63. package/extensions/dashboard-client/dist/assets/{WikiTab-Bf01M2_Z.js.map → WikiTab-CpcgaggS.js.map} +1 -1
  64. package/extensions/dashboard-client/dist/assets/{button-BEJPTk_E.js → button-B0Q2fuKY.js} +2 -2
  65. package/extensions/dashboard-client/dist/assets/{button-BEJPTk_E.js.map → button-B0Q2fuKY.js.map} +1 -1
  66. package/extensions/dashboard-client/dist/assets/{card-DK1V382s.js → card-Cnik3TFr.js} +2 -2
  67. package/extensions/dashboard-client/dist/assets/{card-DK1V382s.js.map → card-Cnik3TFr.js.map} +1 -1
  68. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-CRtaPphL.js → generateCategoricalChart-DrSwgn1V.js} +2 -2
  69. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-CRtaPphL.js.map → generateCategoricalChart-DrSwgn1V.js.map} +1 -1
  70. package/extensions/dashboard-client/dist/assets/{index-CB6BslE4.js → index-DraytcOY.js} +3 -3
  71. package/extensions/dashboard-client/dist/assets/{index-CB6BslE4.js.map → index-DraytcOY.js.map} +1 -1
  72. package/extensions/dashboard-client/dist/assets/{switch-C4FV6B9k.js → switch-DVQ1aB9b.js} +2 -2
  73. package/extensions/dashboard-client/dist/assets/{switch-C4FV6B9k.js.map → switch-DVQ1aB9b.js.map} +1 -1
  74. package/extensions/dashboard-client/dist/assets/{toggle-GBmeENMd.js → toggle-Dkppx-49.js} +2 -2
  75. package/extensions/dashboard-client/dist/assets/{toggle-GBmeENMd.js.map → toggle-Dkppx-49.js.map} +1 -1
  76. package/extensions/dashboard-client/dist/assets/{useSSE-BOKZGSn1.js → useSSE-CPkjU4Gx.js} +2 -2
  77. package/extensions/dashboard-client/dist/assets/{useSSE-BOKZGSn1.js.map → useSSE-CPkjU4Gx.js.map} +1 -1
  78. package/extensions/dashboard-client/dist/index.html +1 -1
  79. package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +2 -0
  80. package/extensions/dashboard-client/src/types/vector-cortex.ts +11 -0
  81. package/extensions/dashboard-server/api-contracts/vector-cortex.ts +11 -0
  82. package/extensions/dashboard-server/routes-rag-settings-helpers.ts +14 -0
  83. package/extensions/dashboard-server/routes-vector-cortex.ts +108 -1
  84. package/package.json +2 -1
  85. package/src/config/vector-cortex.ts +20 -0
  86. package/src/config.ts +2 -0
  87. package/src/vector-cortex/encoder/calibrate.ts +225 -0
  88. package/src/vector-cortex/encoder/emit-vc2b.ts +82 -0
  89. package/src/vector-cortex/encoder/emit-vc2c.ts +70 -0
  90. package/src/vector-cortex/encoder/fallback.ts +99 -0
  91. package/src/vector-cortex/encoder/heads.ts +142 -0
  92. package/src/vector-cortex/encoder/lexical.ts +123 -0
  93. package/src/vector-cortex/encoder/router.ts +163 -0
  94. package/src/vector-cortex/encoder/select.ts +237 -0
  95. package/src/vector-cortex/encoder/trigram.ts +85 -0
  96. package/src/vector-cortex/encoder/types.ts +226 -0
  97. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DD1CMK80.js.map +0 -1
@@ -0,0 +1,142 @@
1
+ /**
2
+ * vector-cortex/encoder/heads.ts — VC2B multi-head encoder (tasks 1–2).
3
+ *
4
+ * Produces a `VectorSetV1`: five independent L2-normalized projection heads in
5
+ * STABLE order — semantic 384, dependency 128, contradiction 128, cacheStability
6
+ * 64, payloadRouting 32 (MODEL_ASSET §decision record). Each head L2-normalizes
7
+ * its raw projection; a zero-norm projection maps to an all-zero vector (task 2).
8
+ *
9
+ * The raw per-head projection is a deterministic seeded compression of the input
10
+ * token sequence (seeded by `ENCODER_SEED` and the head's stable index), which
11
+ * mirrors the VC2A `projectSemantic` placeholder pattern: the contract, shape
12
+ * gating, normalization, zero-norm mapping, ordering and loss/seed constants are
13
+ * all normative here; real trained weights are substituted in VC2C. This keeps
14
+ * the mode-A multi-head path fully testable end-to-end today with zero network.
15
+ *
16
+ * The VC2B emit seam (task 5) is wired: producing a VectorSetV1 emits
17
+ * `vector_cortex_encoder_heads_emitted`; selecting a mode B/C fallback emits
18
+ * `vector_cortex_encoder_fallback_selected` — both gated on MEGACOMPACT_VC2B.
19
+ *
20
+ * Pi-agnostic, zero network (PREVENT-PI-004), no `any` (PREVENT-011).
21
+ */
22
+
23
+ import {
24
+ ENCODER_HEAD_DIMS,
25
+ ENCODER_HEAD_ORDER,
26
+ ENCODER_HEAD_LOSS_WEIGHTS,
27
+ ENCODER_HEAD_LOSS_SUM,
28
+ ENCODER_SEED,
29
+ type EncoderHeadName,
30
+ type HeadVector,
31
+ type VectorSetV1,
32
+ } from "./types.js";
33
+ import {
34
+ createEncoderHeadsReporter,
35
+ NOOP_VC2B_REPORTER,
36
+ type EncoderHeadsReporter,
37
+ } from "./emit-vc2b.js";
38
+
39
+ /** The stable head index of a head name (its position in ENCODER_HEAD_ORDER). */
40
+ const HEAD_INDEX: Readonly<Record<EncoderHeadName, number>> = {
41
+ semantic: 0,
42
+ dependency: 1,
43
+ contradiction: 2,
44
+ cacheStability: 3,
45
+ payloadRouting: 4,
46
+ };
47
+
48
+ /** Deterministic 32-bit LCG step (matches runtime.ts projectSemantic). */
49
+ function nextState(state: number): number {
50
+ return (state * 1664525 + 1013904223) >>> 0;
51
+ }
52
+
53
+ export interface HeadProjectionOptions {
54
+ readonly seed?: number;
55
+ readonly reporter?: EncoderHeadsReporter;
56
+ }
57
+
58
+ /**
59
+ * L2-normalize a float vector in place semantics (returns a new Float32Array).
60
+ * A zero-norm (or empty) input maps to an all-zero vector of the same length
61
+ * (task 2: "mapping zero norm to an all-zero vector"). All finite.
62
+ */
63
+ export function l2Normalize(values: Float32Array): Float32Array {
64
+ const out = new Float32Array(values.length);
65
+ let sum = 0;
66
+ for (const v of values) sum += v * v;
67
+ const norm = Math.sqrt(sum);
68
+ if (!(norm > 0)) return out; // zero norm -> all-zero
69
+ for (let i = 0; i < values.length; i++) out[i] = values[i]! / norm;
70
+ return out;
71
+ }
72
+
73
+ /** L2 norm of a Float32Array (0 for empty/all-zero). */
74
+ export function l2Norm(values: Float32Array): number {
75
+ let sum = 0;
76
+ for (const v of values) sum += v * v;
77
+ return Math.sqrt(sum);
78
+ }
79
+
80
+ /** A deterministic per-head projection over the token sequence, pre-normalization. */
81
+ function projectRaw(head: EncoderHeadName, tokens: readonly number[], seed: number): Float32Array {
82
+ const dim = ENCODER_HEAD_DIMS[head];
83
+ const out = new Float32Array(dim);
84
+ // An EMPTY token sequence has no signal: the raw projection is the zero vector,
85
+ // so after L2 normalization it maps to the all-zero vector (task 2: "mapping
86
+ // zero norm to an all-zero vector"; ENC-ZERO-002). This keeps empty input
87
+ // finite and zero-norm instead of seeding spurious unit-norm noise.
88
+ if (tokens.length === 0) return out;
89
+ // Mix the stable head index + ENCODER_SEED + seed into a per-head state so
90
+ // each head is a distinct independent projection (failure-triad independence).
91
+ let state = (((ENCODER_SEED ^ HEAD_INDEX[head]) >>> 0) ^ (seed >>> 0)) ^ 0x9e3779b9;
92
+ for (const t of tokens) state = nextState(state ^ ((t >>> 0) * 2654435761));
93
+ for (let i = 0; i < dim; i++) {
94
+ state = nextState(state ^ seed);
95
+ out[i] = (state / 4294967296) * 2 - 1;
96
+ }
97
+ return out;
98
+ }
99
+
100
+ /**
101
+ * Compute one head's L2-normalized vector (all-zero on zero norm) for a token
102
+ * sequence. Deterministic for a given seed (repeat drift == 0).
103
+ */
104
+ export function projectHead(
105
+ head: EncoderHeadName,
106
+ tokens: readonly number[],
107
+ seed: number = ENCODER_SEED,
108
+ ): HeadVector {
109
+ const raw = projectRaw(head, tokens, seed);
110
+ return { head, dim: ENCODER_HEAD_DIMS[head], values: l2Normalize(raw) };
111
+ }
112
+
113
+ /**
114
+ * Encode a token sequence into a `VectorSetV1`: the five heads in stable order,
115
+ * each L2-normalized (all-zero on zero norm). Emits `heads_emitted` via the
116
+ * reporter (non-fatal, flag-gated). Deterministic for a given seed.
117
+ */
118
+ export function encodeVectorSet(
119
+ tokens: readonly number[],
120
+ options: HeadProjectionOptions = {},
121
+ ): VectorSetV1 {
122
+ const seed = options.seed ?? ENCODER_SEED;
123
+ const reporter = options.reporter ?? createEncoderHeadsReporter();
124
+ const heads: HeadVector[] = ENCODER_HEAD_ORDER.map((h) => projectHead(h, tokens, seed));
125
+ reporter.headsEmitted({
126
+ heads: heads.length,
127
+ dims: heads.map((h) => h.dim).join("/"),
128
+ normalized: true,
129
+ tokens: tokens.length,
130
+ });
131
+ return { schema: "vector-set-v1", inputTokens: [...tokens], heads, normalized: true };
132
+ }
133
+
134
+ /**
135
+ * The per-head loss weights (must sum to ENCODER_HEAD_LOSS_SUM exactly).
136
+ * Exposed for training/tests to assert the normative .35/.20/.20/.15/.10 split.
137
+ */
138
+ export function headLossWeights(): Readonly<Record<EncoderHeadName, number>> {
139
+ return { ...ENCODER_HEAD_LOSS_WEIGHTS };
140
+ }
141
+
142
+ export { ENCODER_HEAD_ORDER, ENCODER_HEAD_DIMS, ENCODER_HEAD_LOSS_SUM, ENCODER_SEED, NOOP_VC2B_REPORTER };
@@ -0,0 +1,123 @@
1
+ /**
2
+ * vector-cortex/encoder/lexical.ts — VC2B mode C: token/phrase lexical encoder.
3
+ *
4
+ * Lexical C is a token/phrase lexical feature generator used when both mode A
5
+ * (learned asset) and mode B (trigram) are unavailable or fail. It is continuity,
6
+ * NOT semantic completeness: C operates on exact current tokens/phrases only and
7
+ * MUST state that it has lost old semantic context (task 4 + TRIAD_RESILIENCE.
8
+ * "C is continuity, not semantic completeness: it may omit old context and must
9
+ * report that limitation").
10
+ *
11
+ * C never imports the learned asset or learned calibration (task 4): it is a
12
+ * pure token/phrase lexical projection (token counts + phrase hashes) computed
13
+ * from the exact input. It is independently implemented from B (which hashes
14
+ * byte-level trigrams) — C works at the token/phrase level, B at the byte-ngram
15
+ * level, so the two share no algorithm.
16
+ *
17
+ * Authority outage freezes derived high-water: C never advances any derived
18
+ * frontier; it is purely a local reconstruction from the exact present tokens.
19
+ *
20
+ * Pi-agnostic, zero network (PREVENT-PI-004), no `any` (PREVENT-011).
21
+ */
22
+
23
+ import { createHash } from "node:crypto";
24
+ import { l2Normalize } from "./heads.js";
25
+ import {
26
+ createEncoderHeadsReporter,
27
+ type EncoderHeadsReporter,
28
+ } from "./emit-vc2b.js";
29
+
30
+ /** Fixed output width of lexical C. */
31
+ export const ENCODER_LEXICAL_WIDTH = 256;
32
+
33
+ /** The documented limitation lexical C reports (continuity, not semantics). */
34
+ export const ENCODER_LEXICAL_LIMITATION =
35
+ "lexical C: token/phrase-level continuity only; old semantic context is omitted";
36
+
37
+ export function tokenizeLexical(text: string): string[] {
38
+ // Split into lowercase token/phrase units on non-alphanumeric boundaries.
39
+ return text.toLowerCase().split(/[^a-z0-9_]+/).filter((t) => t.length > 0);
40
+ }
41
+
42
+ /**
43
+ * Normalize a single token/phrase unit the same way `tokenizeLexical` does for
44
+ * the string form: lowercase and strip leading/trailing non-alphanumeric runs.
45
+ * Applied to array-form tokens so both accepted input forms of `embedLexical`
46
+ * hash identical conceptual content to identical buckets, regardless of how the
47
+ * caller chose to pass it (code-review Q03).
48
+ */
49
+ function normalizeToken(tok: string): string {
50
+ return tok.toLowerCase().split(/[^a-z0-9_]+/).join("");
51
+ }
52
+
53
+ /**
54
+ * Resolve either accepted input form to a normalized token sequence: the string
55
+ * form routes through `tokenizeLexical` (split on non-alphanumeric boundaries,
56
+ * fragmented tokens discarded); the array form applies the same per-token
57
+ * lowercase/strip normalization and drops tokens that normalize to empty. Both
58
+ * paths therefore agree on hash buckets for the same conceptual content.
59
+ */
60
+ function resolveTokens(tokensOrText: readonly string[] | string): string[] {
61
+ if (typeof tokensOrText === "string") return tokenizeLexical(tokensOrText);
62
+ const out: string[] = [];
63
+ for (const raw of tokensOrText) {
64
+ const norm = normalizeToken(raw);
65
+ if (norm.length > 0) out.push(norm);
66
+ }
67
+ return out;
68
+ }
69
+
70
+ /**
71
+ * Encode a token/phrase sequence into a 256-dim L2-normalized lexical vector
72
+ * (all-zero on empty input). Features: exact token count + token id-hash sums +
73
+ * phrase-adjacency hashes. Deterministic (repeat drift == 0). Pure local compute.
74
+ */
75
+ export function embedLexical(tokensOrText: readonly string[] | string): Float32Array {
76
+ const width = ENCODER_LEXICAL_WIDTH;
77
+ const out = new Float32Array(width);
78
+ const tokens = resolveTokens(tokensOrText);
79
+ for (let i = 0; i < tokens.length; i++) {
80
+ const tok = tokens[i]!;
81
+ const h = createHash("sha256").update(`t:${tok}`).digest();
82
+ const bucket = h.readUInt32BE(0) % width;
83
+ const weight = (h.readUInt32BE(4) / 4294967295) * 2 - 1;
84
+ out[bucket] += weight;
85
+ // Phrase adjacency: bigram hash blended in so semantic-free ordering matters.
86
+ if (i > 0) {
87
+ const pair = createHash("sha256").update(`p:${tokens[i - 1]}:${tok}`).digest();
88
+ const pb = pair.readUInt32BE(0) % width;
89
+ const pw = (pair.readUInt32BE(4) / 4294967295) * 2 - 1;
90
+ out[pb] += pw;
91
+ }
92
+ }
93
+ return l2Normalize(out);
94
+ }
95
+
96
+ /** The documented limitation string, surfaced when lexical C is selected. */
97
+ export function selectLexicalC(
98
+ options: { readonly reporter?: EncoderHeadsReporter } = {},
99
+ ): {
100
+ ok: true;
101
+ mode: "C";
102
+ dim: number;
103
+ width: number;
104
+ limitation: string;
105
+ } {
106
+ const reporter = options.reporter ?? createEncoderHeadsReporter();
107
+ const selection = {
108
+ ok: true as const,
109
+ mode: "C" as const,
110
+ dim: ENCODER_LEXICAL_WIDTH,
111
+ width: ENCODER_LEXICAL_WIDTH,
112
+ limitation: ENCODER_LEXICAL_LIMITATION,
113
+ };
114
+ reporter.fallbackSelected({
115
+ mode: selection.mode,
116
+ dim: selection.dim,
117
+ width: selection.width,
118
+ limitation: selection.limitation,
119
+ });
120
+ return selection;
121
+ }
122
+
123
+ export { l2Normalize };
@@ -0,0 +1,163 @@
1
+ /**
2
+ * vector-cortex/encoder/router.ts — VC2B encode-or-fallback router (S2).
3
+ *
4
+ * The single production seam that catches a real VC2A `load()` failure and
5
+ * hands off to the independently initialized VC2B fallbacks:
6
+ *
7
+ * - mode A: the learned multi-head projection — an EncoderRuntime that
8
+ * verifies+loads a local qualified ONNX (VC2A) plus `encodeVectorSet`
9
+ * producing VectorSetV1, emitting `vector_cortex_encoder_heads_emitted`;
10
+ * - when that `load()` fails for ANY reason (removed model ->
11
+ * ENC_ASSET_UNREADABLE, digest mismatch -> ENC_DIGEST_MISMATCH, missing
12
+ * manifest -> ENC_MANIFEST_INVALID, unsupported platform, RSS over budget,
13
+ * ...) the router catches the real failure and selects the independently
14
+ * initialized asset-free trigram B (`selectTrigramBFallback`) — or lexical C
15
+ * when the runtime reports B-unavailable / the caller forces mode C. The
16
+ * `vector_cortex_encoder_fallback_selected` event fires from the REAL
17
+ * producer seam, not test wiring (task 5 + code-review S1).
18
+ *
19
+ * Best-effort and non-fatal: every branch returns an explicit verdict and never
20
+ * throws across the boundary into the agent loop. Flag-OFF parity: with
21
+ * `MEGACOMPACT_VC2B=0` the VC2B reporter is a no-op (zero emissions), and the
22
+ * mode-A path is governed by the VC2A runtime itself — the router only adds the
23
+ * fallback handoff and changes no producer bytes.
24
+ *
25
+ * Pi-agnostic, zero network (PREVENT-PI-004), no `any` (PREVENT-011).
26
+ */
27
+
28
+ import { createEncoderRuntime } from "./runtime.js";
29
+ import { encodeVectorSet, type HeadProjectionOptions } from "./heads.js";
30
+ import { embedTrigram512, selectTrigramBFallback } from "./trigram.js";
31
+ import { embedLexical, selectLexicalC } from "./lexical.js";
32
+ import { createEncoderHeadsReporter, type EncoderHeadsReporter } from "./emit-vc2b.js";
33
+ import {
34
+ ENC_FAIL,
35
+ type EncoderInput,
36
+ type EncoderLoadResult,
37
+ type EncoderRuntime,
38
+ type VectorSetV1,
39
+ } from "./types.js";
40
+
41
+ /** A resolved encode decision: a qualified mode-A VectorSet, an explicit B/C
42
+ * fallback vector, or a hard failure. */
43
+ export type RouterVerdict =
44
+ | { readonly ok: true; readonly mode: "A"; readonly vectorSet: VectorSetV1; readonly code: null }
45
+ | {
46
+ readonly ok: true;
47
+ readonly mode: "B" | "C";
48
+ readonly vector: Float32Array;
49
+ readonly width: number;
50
+ /** Semantic-context limitation (mode C) or null (mode B). */
51
+ readonly limitation: string | null;
52
+ /** The VC2A failure code that triggered the fallback (e.g.
53
+ * ENC_ASSET_UNREADABLE), or null when the caller forced the mode. */
54
+ readonly code: string | null;
55
+ }
56
+ | { readonly ok: false; readonly mode: "B" | "C"; readonly code: string };
57
+
58
+ export interface RouterOptions extends HeadProjectionOptions {
59
+ /** The VC2A runtime (mode A path). Defaults to a freshly created runtime. */
60
+ readonly runtime?: EncoderRuntime;
61
+ /** Forced fallback: skips the A load and selects the named VC2B fallback
62
+ * (used to exercise C when A and B are both disabled). Optional. */
63
+ readonly forceFallback?: "B" | "C";
64
+ }
65
+
66
+ /** Deterministic text derived from an int token sequence so the asset-free
67
+ * fallback producers operate on the same authority the learned path encoded. */
68
+ function textFromTokens(tokens: readonly number[]): string {
69
+ return tokens.join("-");
70
+ }
71
+
72
+ /**
73
+ * Try to produce an encoding for an input token sequence. Mode A: verify+load
74
+ * the local learned asset via the EncoderRuntime; on a real `load()` failure
75
+ * (removed model, digest mismatch, missing manifest, ...) the router catches it
76
+ * and selects the independently initialized trigram B or lexical C, emitting
77
+ * `vector_cortex_encoder_fallback_selected` (and `heads_emitted` when A wins).
78
+ */
79
+ export function encodeOrFallback(
80
+ input: EncoderInput,
81
+ assetDir: string,
82
+ options: RouterOptions = {},
83
+ ): RouterVerdict {
84
+ const reporter = options.reporter ?? createEncoderHeadsReporter();
85
+ const tokens = Array.isArray(input?.tokens) ? input.tokens : [];
86
+ const runtime = options.runtime ?? createEncoderRuntime();
87
+
88
+ // A caller that explicitly forces a fallback mode wins even over the empty-
89
+ // input degenerate case: the forced-mode contract must hold for ANY input, so
90
+ // handle forceFallback before the empty-input selection below (Q02). A forced
91
+ // B/C is NOT a rollback and NOT a demotion — it is an intentional selection
92
+ // for capacity/testing — so the verdict carries `code: null`, never
93
+ // ENC_FAIL.ROLLBACK, so a consumer that reads `verdict.code` as "what
94
+ // triggered the fallback" won't misread a deliberately forced mode as a
95
+ // rollback and take rollback-specific action (code-review Q04).
96
+ if (options.forceFallback !== undefined) {
97
+ if (options.forceFallback === "C") {
98
+ const sel = selectLexicalC({ reporter });
99
+ const vector = embedLexical(textFromTokens(tokens));
100
+ return { ok: true, mode: "C", vector, width: sel.width, limitation: sel.limitation, code: null };
101
+ }
102
+ const sel = selectTrigramBFallback({ reporter });
103
+ const vector = embedTrigram512(textFromTokens(tokens));
104
+ return { ok: true, mode: "B", vector, width: sel.width, limitation: null, code: null };
105
+ }
106
+
107
+ // Empty input yields the asset-free fallback (finite, deterministic zero
108
+ // vector, ENC-ZERO-002). This is legitimate degenerate behavior, NOT a shape
109
+ // failure, so the verdict carries no failure code (code === null) — a consumer
110
+ // that interprets `code` as "what went wrong" must not misread a valid all-zero
111
+ // B vector as a shape rejection (Q05). Only reached when no mode is forced, so
112
+ // a forced B/C is never subverted by empty tokens.
113
+ if (tokens.length === 0) {
114
+ const sel = selectTrigramBFallback({ reporter });
115
+ const vector = embedTrigram512("");
116
+ return { ok: true, mode: "B", vector, width: sel.width, limitation: null, code: null };
117
+ }
118
+
119
+ // Mode A attempt — reached only when no mode is forced (the forceFallback
120
+ // guard at the top already returned), so `options.forceFallback` is undefined here.
121
+ const loaded = runtime.load(assetDir);
122
+ if (!loaded.ok) {
123
+ return fallbackFromLoad(loaded, reporter, tokens);
124
+ }
125
+ // Q01/Q03: a qualified mode-A load is not enough — the verified per-manifest
126
+ // token capacity (maxTokens, <= global 512) must also be enforced before we
127
+ // produce a VectorSetV1. run inference over the input; an over-cap sequence
128
+ // (e.g. 100 tokens against a verified maxTokens=64 manifest) or an
129
+ // over-budget inference is rejected here and routed to the B/C fallback with
130
+ // the real failure code, rather than silently emitting an ok:true mode-A
131
+ // set whose inputTokens breach the model's declared capacity.
132
+ const inferred = runtime.infer({ tokens });
133
+ if (!inferred.ok) {
134
+ return fallbackFromLoad({ ok: false, mode: "B", code: inferred.code }, reporter, tokens);
135
+ }
136
+ const vectorSet = encodeVectorSet(tokens, { reporter, seed: options.seed });
137
+ return { ok: true, mode: "A", vectorSet, code: null };
138
+ }
139
+
140
+ /**
141
+ * Catch a (real or forced) A load failure (ok === false only) and select the
142
+ * B/C fallback that emits `vector_cortex_encoder_fallback_selected` from the
143
+ * production seam. The parameter is narrowed to the failed-load variant because
144
+ * the router hands off here only on a non-A/failed load — a qualified mode-A
145
+ * success never reaches this function (Q04).
146
+ */
147
+ function fallbackFromLoad(
148
+ loaded: Extract<EncoderLoadResult, { ok: false }>,
149
+ reporter: EncoderHeadsReporter,
150
+ tokens: readonly number[],
151
+ ): RouterVerdict {
152
+ if (loaded.mode === "C") {
153
+ const sel = selectLexicalC({ reporter });
154
+ const vector = embedLexical(textFromTokens(tokens));
155
+ return { ok: true, mode: "C", vector, width: sel.width, limitation: sel.limitation, code: loaded.code };
156
+ }
157
+ const sel = selectTrigramBFallback({ reporter });
158
+ const vector = embedTrigram512(textFromTokens(tokens));
159
+ return { ok: true, mode: "B", vector, width: sel.width, limitation: null, code: loaded.code };
160
+ }
161
+
162
+ export { ENC_FAIL };
163
+ export type { EncoderRuntime };
@@ -0,0 +1,237 @@
1
+ /**
2
+ * vector-cortex/encoder/select.ts — VC2C atomic qualification selection (task 3).
3
+ *
4
+ * Selects mode A (a `QualifiedEncoderV1`) ONLY when EVERY MODEL_ASSET
5
+ * constraint AND every per-head EVALUATION threshold passes — the eligibility
6
+ * check is ATOMIC: any single failed field demotes the ENTIRE candidate to
7
+ * mode B/C (never a partial A). The failed field is reported so the caller/UI
8
+ * can inspect which threshold tripped.
9
+ *
10
+ * - asset fields (MODEL_ASSET §qualification): maxTokens <= 512, p95 latency
11
+ * <= 40 ms, encoder RSS delta <= 150 MiB. When an asset field fails, the
12
+ * demotion code is `ENC_QUALIFICATION_ASSET_FAILED`; per-head / reconstruction
13
+ * failures report `ENC_QUALIFICATION_THRESHOLD_FAILED`. Either way the check is
14
+ * atomic — the field that tripped is surfaced via `failedField`.
15
+ * - per-head EVALUATION thresholds (EVALUATION.md §metrics):
16
+ * semantic spearman >= .75, recall@10 >= .90
17
+ * dependency precision >= .97, recall >= .95
18
+ * contradiction precision >= .98, recall >= .90, ECE <= .05
19
+ * cacheStability precision >= .999, recall >= .90
20
+ * payloadRouting macro-F1 >= .97, exact/anchor recall = 1.0
21
+ * - reconstruction: zero causal/tool/anchor/exact violations, dependency
22
+ * closure recall = 1.0, task-success non-inferior to C.
23
+ *
24
+ * Unique failure injection: when a candidate supplies an expected
25
+ * `qualificationManifestDigest` (the digest of the qualification manifest
26
+ * sealed at calibration time) and the presented calibration digest differs,
27
+ * selection returns ENC_QUALIFICATION_DIGEST_MISMATCH and demotes A — the
28
+ * "corrupt qualification manifest after calibration but before selection" case.
29
+ *
30
+ * Emits `vector_cortex_encoder_qualification_passed` on success and
31
+ * `vector_cortex_encoder_qualification_demoted` on any demotion, via the
32
+ * flag-gated VC2C reporter. Pi-agnostic, zero network (PREVENT-PI-004), no
33
+ * `any` (PREVENT-011).
34
+ */
35
+
36
+ import { createHash } from "node:crypto";
37
+ import {
38
+ ENC_QUALIFICATION_FAIL,
39
+ EVALUATION_THRESHOLDS,
40
+ type CalibrationV1,
41
+ type EncoderHeldOutMetrics,
42
+ type EncoderHeadName,
43
+ type QualifiedEncoderV1,
44
+ } from "./types.js";
45
+ import {
46
+ createEncoderQualificationReporter,
47
+ type EncoderQualificationReporter,
48
+ } from "./emit-vc2c.js";
49
+
50
+ /** The MODEL_ASSET asset constraints a candidate must satisfy (subset that
51
+ * select.ts evaluates; the rest — supported matrix, opset, batch, digest — are
52
+ * enforced by the VC2A asset verification seam before a candidate exists). */
53
+ export interface CandidateAssetFacts {
54
+ /** Declared/verified per-manifest token capacity (<= 512). */
55
+ readonly maxTokens: number;
56
+ /** Measured p95 inference latency (ms, <= 40). */
57
+ readonly latencyP95Ms: number;
58
+ /** Measured encoder RSS delta (MiB, <= 150). */
59
+ readonly rssDeltaMib: number;
60
+ }
61
+
62
+ /** Inputs to the atomic eligibility check. */
63
+ export interface QualificationCandidate {
64
+ readonly modelVersion: string;
65
+ readonly asset: CandidateAssetFacts;
66
+ /** The verified ONNX digest of the candidate asset. */
67
+ readonly onnxDigest: string;
68
+ /**
69
+ * SHA-256 of the asset manifest bytes (ModelManifestV1) that qualified — the
70
+ * same digest the dashboard health card reports as `encoderAssetDigest`
71
+ * (SHA-256 of the committed manifest.json bytes). select.ts stamps this into
72
+ * `QualifiedEncoderV1.assetDigest` verbatim so the record pins the true
73
+ * asset-manifest digest, not a calibration-derived hash.
74
+ */
75
+ readonly assetManifestDigest: string;
76
+ /** Calibration digest the asset is qualified against (from `CalibrationV1`). */
77
+ readonly calibration: CalibrationV1;
78
+ /** Held-out metrics as evidence (EVALUATION.md). */
79
+ readonly heldOut: EncoderHeldOutMetrics;
80
+ /**
81
+ * The qualification manifest digest sealed at calibration time. When supplied,
82
+ * it must equal SHA-256 over the calibration manifest bytes; a mismatch is the
83
+ * corrupt-qualification-manifest injection (ENC_QUALIFICATION_DIGEST_MISMATCH).
84
+ */
85
+ readonly expectedQualificationManifestDigest?: string;
86
+ }
87
+
88
+ /**
89
+ * The qualification verdict. A successful candidate is mode A; ANY demotion here
90
+ * is mode B — select.ts never produces mode C. Mode C (lexical/exact) is
91
+ * exclusively the output of the separate fallback seam (`fallback.ts`, selected
92
+ * only when A is absent AND B itself errors or the caller forces C). The type is
93
+ * narrowed to "B" on failure to match the implementation: demoting a candidate
94
+ * is the same "asset-free, non-learned" fallback regardless of which field
95
+ * tripped; the lexical C demotion is not a property of qualification selection.
96
+ */
97
+ export type QualificationVerdict =
98
+ | { ok: true; mode: "A"; qualified: QualifiedEncoderV1; code: null }
99
+ | { ok: false; mode: "B"; code: string; failedField: string | null };
100
+
101
+ /** Canonical digest over a CalibrationV1's stable identity (split digest, heads,
102
+ * temps, thresholds, seed) — the "qualification manifest digest". */
103
+ export function qualificationManifestDigest(calibration: CalibrationV1): string {
104
+ const payload = JSON.stringify({
105
+ headOrder: [...calibration.headOrder].sort(),
106
+ splitDigest: calibration.calibrationSplitDigest,
107
+ temps: calibration.temperatures,
108
+ thresholds: calibration.thresholds,
109
+ seed: calibration.seed,
110
+ });
111
+ return createHash("sha256").update(payload).digest("hex");
112
+ }
113
+
114
+ /** True when one per-head metric row clears its normative EVALUATION threshold. */
115
+ function headPasses(head: EncoderHeadName, m: EncoderHeldOutMetrics): boolean {
116
+ switch (head) {
117
+ case "semantic":
118
+ return m.semantic.spearman >= EVALUATION_THRESHOLDS.semantic.spearman &&
119
+ m.semantic.recallAt10 >= EVALUATION_THRESHOLDS.semantic.recallAt10;
120
+ case "dependency":
121
+ return m.dependency.precision >= EVALUATION_THRESHOLDS.dependency.precision &&
122
+ m.dependency.recall >= EVALUATION_THRESHOLDS.dependency.recall;
123
+ case "contradiction":
124
+ return m.contradiction.precision >= EVALUATION_THRESHOLDS.contradiction.precision &&
125
+ m.contradiction.recall >= EVALUATION_THRESHOLDS.contradiction.recall &&
126
+ m.contradiction.ece <= EVALUATION_THRESHOLDS.contradiction.ece;
127
+ case "cacheStability":
128
+ return m.cacheStability.precision >= EVALUATION_THRESHOLDS.cacheStability.precision &&
129
+ m.cacheStability.recall >= EVALUATION_THRESHOLDS.cacheStability.recall;
130
+ case "payloadRouting":
131
+ return m.payloadRouting.macroF1 >= EVALUATION_THRESHOLDS.payloadRouting.macroF1 &&
132
+ m.payloadRouting.exactAnchorRecall >= EVALUATION_THRESHOLDS.payloadRouting.exactAnchorRecall;
133
+ }
134
+ }
135
+
136
+ function reconstructionPasses(m: EncoderHeldOutMetrics): boolean {
137
+ return (
138
+ m.reconstruction.votesOk &&
139
+ m.reconstruction.dependencyClosureRecall >= EVALUATION_THRESHOLDS.reconstruction.dependencyClosureRecall &&
140
+ m.reconstruction.taskSuccessNonInferior
141
+ );
142
+ }
143
+
144
+ function assetPasses(a: CandidateAssetFacts, failed: string[]): boolean {
145
+ let ok = true;
146
+ if (a.maxTokens > EVALUATION_THRESHOLDS.asset.maxTokens) { failed.push("asset.maxTokens"); ok = false; }
147
+ if (a.latencyP95Ms > EVALUATION_THRESHOLDS.asset.maxLatencyP95Ms) { failed.push("asset.latencyP95Ms"); ok = false; }
148
+ if (a.rssDeltaMib > EVALUATION_THRESHOLDS.asset.maxRssDeltaMib) { failed.push("asset.rssDeltaMib"); ok = false; }
149
+ return ok;
150
+ }
151
+
152
+ /**
153
+ * Atomic eligibility check across every MODEL_ASSET + per-head EVALUATION
154
+ * threshold (task 3). Any single failed field demotes ALL of A (returns
155
+ * `ok:false`); there is no partial qualification. On success returns a
156
+ * `QualifiedEncoderV1` pinning asset/calibration digests + held-out evidence.
157
+ */
158
+ export function selectQualifiedEncoder(
159
+ candidate: QualificationCandidate,
160
+ options: { reporter?: EncoderQualificationReporter } = {},
161
+ ): QualificationVerdict {
162
+ const reporter = options.reporter ?? createEncoderQualificationReporter();
163
+
164
+ // Unique failure injection: corrupt qualification manifest between calibration
165
+ // and selection. The sealed manifest digest must match the presented
166
+ // calibration's digest; a mismatch demotes A to B/C with the dedicated code.
167
+ if (candidate.expectedQualificationManifestDigest !== undefined) {
168
+ const presented = qualificationManifestDigest(candidate.calibration);
169
+ if (presented !== candidate.expectedQualificationManifestDigest) {
170
+ reporter.qualificationDemoted({
171
+ reason: ENC_QUALIFICATION_FAIL.DIGEST_MISMATCH,
172
+ modelVersion: candidate.modelVersion,
173
+ mode: "B",
174
+ });
175
+ return {
176
+ ok: false,
177
+ mode: "B",
178
+ code: ENC_QUALIFICATION_FAIL.DIGEST_MISMATCH,
179
+ failedField: "qualificationManifestDigest",
180
+ };
181
+ }
182
+ }
183
+
184
+ // Atomic: collect EVERY failed field across asset + all heads + reconstruction.
185
+ const failed: string[] = [];
186
+ assetPasses(candidate.asset, failed);
187
+ const heads: EncoderHeadName[] = ["semantic", "dependency", "contradiction", "cacheStability", "payloadRouting"];
188
+ for (const h of heads) {
189
+ if (!headPasses(h, candidate.heldOut)) failed.push(`head.${h}`);
190
+ }
191
+ if (!reconstructionPasses(candidate.heldOut)) failed.push("reconstruction");
192
+
193
+ if (failed.length > 0) {
194
+ // Any failed field demotes all of A — no partial A. Route the demotion code
195
+ // by the failed-field class: an asset-field failure (maxTokens/latency/RSS)
196
+ // reports ENC_QUALIFICATION_ASSET_FAILED; a per-head/reconstruction failure
197
+ // reports ENC_QUALIFICATION_THRESHOLD_FAILED. `failedField` always carries the
198
+ // specific field that tripped, so the code + field together fully identify it.
199
+ const assetFailed = failed.some((f) => f.startsWith("asset."));
200
+ const code = assetFailed
201
+ ? ENC_QUALIFICATION_FAIL.ASSET_FAILED
202
+ : ENC_QUALIFICATION_FAIL.THRESHOLD_FAILED;
203
+ reporter.qualificationDemoted({
204
+ reason: code,
205
+ failed: failed.join("/"),
206
+ modelVersion: candidate.modelVersion,
207
+ mode: "B",
208
+ });
209
+ return {
210
+ ok: false,
211
+ mode: "B",
212
+ code,
213
+ failedField: failed[0] ?? null,
214
+ };
215
+ }
216
+
217
+ const qualified: QualifiedEncoderV1 = {
218
+ schema: "qualified-encoder-v1",
219
+ modelVersion: candidate.modelVersion,
220
+ mode: "A",
221
+ // The REAL ModelManifestV1 asset-manifest digest (passed through the
222
+ // candidate), not a calibration-derived hash — matches the documented
223
+ // contract and the dashboard health card's encoderAssetDigest.
224
+ assetDigest: candidate.assetManifestDigest,
225
+ calibrationDigest: candidate.calibration.calibrationSplitDigest,
226
+ onnxDigest: candidate.onnxDigest,
227
+ heldOut: candidate.heldOut,
228
+ calibration: candidate.calibration,
229
+ };
230
+ reporter.qualificationPassed({
231
+ modelVersion: candidate.modelVersion,
232
+ mode: "A",
233
+ assetDigest: qualified.assetDigest.slice(0, 12),
234
+ calibrationDigest: qualified.calibrationDigest.slice(0, 12),
235
+ });
236
+ return { ok: true, mode: "A", qualified, code: null };
237
+ }