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.
- package/assets/vector-cortex/encoder-v1/manifest.json +1 -0
- package/assets/vector-cortex/encoder-v1/model-card.json +1 -0
- package/assets/vector-cortex/encoder-v1/model.onnx +0 -0
- package/assets/vector-cortex/encoder-v1/tokenizer.json +1 -0
- package/dist/config/vector-cortex.js +18 -0
- package/dist/config.js +117 -0
- package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +4 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex.js +89 -1
- package/dist/log.js +47 -0
- package/dist/src/config/vector-cortex.js +18 -0
- package/dist/src/config.js +1 -1
- package/dist/src/vector-cortex/encoder/calibrate.js +174 -0
- package/dist/src/vector-cortex/encoder/emit-vc2b.js +63 -0
- package/dist/src/vector-cortex/encoder/emit-vc2c.js +59 -0
- package/dist/src/vector-cortex/encoder/fallback.js +62 -0
- package/dist/src/vector-cortex/encoder/heads.js +113 -0
- package/dist/src/vector-cortex/encoder/lexical.js +104 -0
- package/dist/src/vector-cortex/encoder/router.js +115 -0
- package/dist/src/vector-cortex/encoder/select.js +172 -0
- package/dist/src/vector-cortex/encoder/trigram.js +75 -0
- package/dist/src/vector-cortex/encoder/types.js +84 -0
- package/dist/vector-cortex/encoder/calibrate.js +174 -0
- package/dist/vector-cortex/encoder/emit-vc2b.js +63 -0
- package/dist/vector-cortex/encoder/emit-vc2c.js +59 -0
- package/dist/vector-cortex/encoder/fallback.js +62 -0
- package/dist/vector-cortex/encoder/heads.js +113 -0
- package/dist/vector-cortex/encoder/lexical.js +104 -0
- package/dist/vector-cortex/encoder/router.js +115 -0
- package/dist/vector-cortex/encoder/select.js +172 -0
- package/dist/vector-cortex/encoder/trigram.js +75 -0
- package/dist/vector-cortex/encoder/types.js +84 -0
- package/extensions/dashboard-client/dist/assets/{AreaChart-BQXFF7UI.js → AreaChart-DywU5SSt.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-BQXFF7UI.js.map → AreaChart-DywU5SSt.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-BVtoz9Eg.js → BarChart-But7WN6K.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-BVtoz9Eg.js.map → BarChart-But7WN6K.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-trI2TIGy.js → CacheTab-7aZWa1DR.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-trI2TIGy.js.map → CacheTab-7aZWa1DR.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-CNUbk9__.js → EventsTab-Bf_boNG2.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-CNUbk9__.js.map → EventsTab-Bf_boNG2.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-K-bd_CM-.js → HealthTab-AbGc1nXP.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-K-bd_CM-.js.map → HealthTab-AbGc1nXP.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-CoEioKzx.js → MaintenanceTab-BfHHhn5F.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-CoEioKzx.js.map → MaintenanceTab-BfHHhn5F.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CJo24ulv.js → MemoryMapTab-DrcWti0-.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CJo24ulv.js.map → MemoryMapTab-DrcWti0-.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-DCNlQWWP.js → MetricsTab-ALiglXQq.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-DCNlQWWP.js.map → MetricsTab-ALiglXQq.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-CQQlyIQ6.js → OverviewTab-nnaJdNyR.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-CQQlyIQ6.js.map → OverviewTab-nnaJdNyR.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-Cqm82_yn.js → ReposTab-BTK0nofM.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-Cqm82_yn.js.map → ReposTab-BTK0nofM.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-A8xTAdET.js → SessionsTab-RXPNqO6x.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-A8xTAdET.js.map → SessionsTab-RXPNqO6x.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-CqaSgaZl.js → SetupTab-Dab0AVfs.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-CqaSgaZl.js.map → SetupTab-Dab0AVfs.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-C_f3q8GR.js → TimeSavedCard-DIVG8hdt.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-C_f3q8GR.js.map → TimeSavedCard-DIVG8hdt.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-BEsafACo.js → TurnsTab-9uz2fBwx.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-BEsafACo.js.map → TurnsTab-9uz2fBwx.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{VectorCortexTab-DD1CMK80.js → VectorCortexTab-DnKY61EJ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-DnKY61EJ.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{WikiTab-Bf01M2_Z.js → WikiTab-CpcgaggS.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-Bf01M2_Z.js.map → WikiTab-CpcgaggS.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-BEJPTk_E.js → button-B0Q2fuKY.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-BEJPTk_E.js.map → button-B0Q2fuKY.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-DK1V382s.js → card-Cnik3TFr.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-DK1V382s.js.map → card-Cnik3TFr.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-CRtaPphL.js → generateCategoricalChart-DrSwgn1V.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-CRtaPphL.js.map → generateCategoricalChart-DrSwgn1V.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-CB6BslE4.js → index-DraytcOY.js} +3 -3
- package/extensions/dashboard-client/dist/assets/{index-CB6BslE4.js.map → index-DraytcOY.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{switch-C4FV6B9k.js → switch-DVQ1aB9b.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-C4FV6B9k.js.map → switch-DVQ1aB9b.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-GBmeENMd.js → toggle-Dkppx-49.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-GBmeENMd.js.map → toggle-Dkppx-49.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-BOKZGSn1.js → useSSE-CPkjU4Gx.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-BOKZGSn1.js.map → useSSE-CPkjU4Gx.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +1 -1
- package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +2 -0
- package/extensions/dashboard-client/src/types/vector-cortex.ts +11 -0
- package/extensions/dashboard-server/api-contracts/vector-cortex.ts +11 -0
- package/extensions/dashboard-server/routes-rag-settings-helpers.ts +14 -0
- package/extensions/dashboard-server/routes-vector-cortex.ts +108 -1
- package/package.json +2 -1
- package/src/config/vector-cortex.ts +20 -0
- package/src/config.ts +2 -0
- package/src/vector-cortex/encoder/calibrate.ts +225 -0
- package/src/vector-cortex/encoder/emit-vc2b.ts +82 -0
- package/src/vector-cortex/encoder/emit-vc2c.ts +70 -0
- package/src/vector-cortex/encoder/fallback.ts +99 -0
- package/src/vector-cortex/encoder/heads.ts +142 -0
- package/src/vector-cortex/encoder/lexical.ts +123 -0
- package/src/vector-cortex/encoder/router.ts +163 -0
- package/src/vector-cortex/encoder/select.ts +237 -0
- package/src/vector-cortex/encoder/trigram.ts +85 -0
- package/src/vector-cortex/encoder/types.ts +226 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-DD1CMK80.js.map +0 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/encoder/trigram.ts — VC2B mode B: asset-free trigram encoder.
|
|
3
|
+
*
|
|
4
|
+
* Trigram B is a deterministic, asset-free (no learned model, no manifest, no
|
|
5
|
+
* calibration) 512-dim fixed feature encoding of a token/phrase sequence. It is
|
|
6
|
+
* the mode-B fallback selected when the learned asset (mode A) is removed,
|
|
7
|
+
* missing, unsupported, or digest-bad — and it never imports the learned asset
|
|
8
|
+
* or learned calibration (task 4). It derives directly from textual authority:
|
|
9
|
+
* the same document hashed via its byte-level trigrams yields the same 512-dim
|
|
10
|
+
* vector regardless of the asset state.
|
|
11
|
+
*
|
|
12
|
+
* Width is fixed at `ENCODER_TRIGRAM_WIDTH = 512` (VC2B task 4 "trigram B at 512
|
|
13
|
+
* dimensions"). The vector is L2-normalized; a zero-norm (empty) input maps to
|
|
14
|
+
* the all-zero vector, matching the heads convention of the VectorSet.
|
|
15
|
+
*
|
|
16
|
+
* Failure-triad independence: B's algorithm/index is distinct from A (learned
|
|
17
|
+
* projections) and C (token/phrase lexical) — it is a deterministic hashed
|
|
18
|
+
* n-gram bag-of-hashes, computed purely in-process with no external asset.
|
|
19
|
+
*
|
|
20
|
+
* Pi-agnostic, zero network (PREVENT-PI-004), no `any` (PREVENT-011).
|
|
21
|
+
*/
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
23
|
+
import { l2Normalize } from "./heads.js";
|
|
24
|
+
import { createEncoderHeadsReporter, } from "./emit-vc2b.js";
|
|
25
|
+
/** Fixed output width of trigram B (VC2B task 4). */
|
|
26
|
+
export const ENCODER_TRIGRAM_WIDTH = 512;
|
|
27
|
+
/** Tokenize a phrase into byte-level trigrams (3-byte sliding windows). For a
|
|
28
|
+
* short phrase with fewer than 3 bytes we still emit the available shingles. */
|
|
29
|
+
function trigrams(text) {
|
|
30
|
+
const bytes = Buffer.from(text, "utf8");
|
|
31
|
+
if (bytes.length === 0)
|
|
32
|
+
return [];
|
|
33
|
+
const out = [];
|
|
34
|
+
const n = bytes.length;
|
|
35
|
+
// `Math.max(1, n - 2)` already emits a single whole-string shingle for 1- and
|
|
36
|
+
// 2-byte phrases (slice(0,3) covers the whole buffer), so there is NO separate
|
|
37
|
+
// short-phrase block — adding one would hash the same shingle twice (Q05).
|
|
38
|
+
for (let i = 0; i < Math.max(1, n - 2); i++) {
|
|
39
|
+
const chunk = bytes.slice(i, i + 3);
|
|
40
|
+
out.push(chunk.toString("hex"));
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Encode a phrase into a 512-dim L2-normalized trigram vector (all-zero on
|
|
46
|
+
* empty input). Deterministic: the same text always yields the same vector
|
|
47
|
+
* (repeat drift == 0) — no asset, no calibration, no network.
|
|
48
|
+
*/
|
|
49
|
+
export function embedTrigram512(text) {
|
|
50
|
+
const width = ENCODER_TRIGRAM_WIDTH;
|
|
51
|
+
const out = new Float32Array(width);
|
|
52
|
+
// Feistel-style double hashing of each trigram into a bucket index + weight.
|
|
53
|
+
for (const tg of trigrams(text)) {
|
|
54
|
+
const h1 = createHash("sha256").update(tg).digest();
|
|
55
|
+
const bucket = h1.readUInt32BE(0) % width;
|
|
56
|
+
const weight = (h1.readUInt32BE(4) / 4294967295) * 2 - 1;
|
|
57
|
+
out[bucket] += weight;
|
|
58
|
+
}
|
|
59
|
+
return l2Normalize(out);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The 512-dim vector is produced even when the learned asset is absent: this is
|
|
63
|
+
* the mode-B selection point. Returns `{ ok: true, dim, width }` always — there
|
|
64
|
+
* is no asset to consult (task 4 + ENC-FALLBACK-003). Selecting mode B also
|
|
65
|
+
* emits `vector_cortex_encoder_fallback_selected` via the flag-gated reporter
|
|
66
|
+
* (task 5) — the production seam that makes the fallback event live in the
|
|
67
|
+
* runtime, not dead test-only wiring.
|
|
68
|
+
*/
|
|
69
|
+
export function selectTrigramBFallback(options = {}) {
|
|
70
|
+
const reporter = options.reporter ?? createEncoderHeadsReporter();
|
|
71
|
+
const selection = { ok: true, mode: "B", dim: ENCODER_TRIGRAM_WIDTH, width: ENCODER_TRIGRAM_WIDTH };
|
|
72
|
+
reporter.fallbackSelected({ mode: selection.mode, dim: selection.dim, width: selection.width });
|
|
73
|
+
return selection;
|
|
74
|
+
}
|
|
75
|
+
export { l2Normalize };
|
|
@@ -83,3 +83,87 @@ export const ENC_IDS = [
|
|
|
83
83
|
"ENC-007",
|
|
84
84
|
"ENC-008",
|
|
85
85
|
];
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// VC2B — multi-head encoder (VectorSetV1 / HeadCalibrationDraft).
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
/** The five independent projection heads in STABLE order (MODEL_ASSET
|
|
90
|
+
* §decision record; VC2B task 2 "stable order"). The array order is the
|
|
91
|
+
* normative ordering consumed by consumers: semantic, dependency,
|
|
92
|
+
* contradiction, cache-stability, payload-routing. */
|
|
93
|
+
export const ENCODER_HEAD_ORDER = [
|
|
94
|
+
"semantic",
|
|
95
|
+
"dependency",
|
|
96
|
+
"contradiction",
|
|
97
|
+
"cacheStability",
|
|
98
|
+
"payloadRouting",
|
|
99
|
+
];
|
|
100
|
+
/** The ordered per-head output dimensions: semantic 384, dependency 128,
|
|
101
|
+
* contradiction 128, cacheStability 64, payloadRouting 32 (VC2B task 2). */
|
|
102
|
+
export const ENCODER_HEAD_DIMS = {
|
|
103
|
+
semantic: 384,
|
|
104
|
+
dependency: 128,
|
|
105
|
+
contradiction: 128,
|
|
106
|
+
cacheStability: 64,
|
|
107
|
+
payloadRouting: 32,
|
|
108
|
+
};
|
|
109
|
+
/** Ordered dimension list matching ENCODER_HEAD_ORDER (384/128/128/64/32). */
|
|
110
|
+
export const ENCODER_HEAD_DIM_ORDER = ENCODER_HEAD_ORDER.map((h) => ENCODER_HEAD_DIMS[h]);
|
|
111
|
+
/**
|
|
112
|
+
* Weighted training losses per head (MODEL_ASSET §data/losses/calibration):
|
|
113
|
+
* semantic .35, dependency .20, contradiction .20, cache .15, payload .10.
|
|
114
|
+
* These are normative (VC2B task 3: "losses exactly .35/.20/.20/.15/.10").
|
|
115
|
+
*/
|
|
116
|
+
export const ENCODER_HEAD_LOSS_WEIGHTS = {
|
|
117
|
+
semantic: 0.35,
|
|
118
|
+
dependency: 0.2,
|
|
119
|
+
contradiction: 0.2,
|
|
120
|
+
cacheStability: 0.15,
|
|
121
|
+
payloadRouting: 0.1,
|
|
122
|
+
};
|
|
123
|
+
/** Sum of the five loss weights must be exactly 1.0 (asserted in tests). */
|
|
124
|
+
export const ENCODER_HEAD_LOSS_SUM = 1.0;
|
|
125
|
+
/** Deterministic seed shared by Python/NumPy training and ONNX export (VC2B
|
|
126
|
+
* task 3: "seed ... at 1729"). */
|
|
127
|
+
export const ENCODER_SEED = 1729;
|
|
128
|
+
/** The 16 registered VC2B conformance IDs (task 1: "register ENC-009..016"). */
|
|
129
|
+
export const ENC2B_IDS = [
|
|
130
|
+
"ENC-009",
|
|
131
|
+
"ENC-010",
|
|
132
|
+
"ENC-011",
|
|
133
|
+
"ENC-012",
|
|
134
|
+
"ENC-013",
|
|
135
|
+
"ENC-014",
|
|
136
|
+
"ENC-015",
|
|
137
|
+
"ENC-016",
|
|
138
|
+
];
|
|
139
|
+
/** Normative per-head + asset qualification thresholds (MODEL_ASSET §qualification
|
|
140
|
+
* + EVALUATION.md §metrics). These are the constants `select.ts` evaluates the
|
|
141
|
+
* candidate's held-out metrics against (task 3 atomic check). */
|
|
142
|
+
export const EVALUATION_THRESHOLDS = {
|
|
143
|
+
semantic: { spearman: 0.75, recallAt10: 0.9 },
|
|
144
|
+
dependency: { precision: 0.97, recall: 0.95 },
|
|
145
|
+
contradiction: { precision: 0.98, recall: 0.9, ece: 0.05 },
|
|
146
|
+
cacheStability: { precision: 0.999, recall: 0.9 },
|
|
147
|
+
payloadRouting: { macroF1: 0.97, exactAnchorRecall: 1.0 },
|
|
148
|
+
reconstruction: { dependencyClosureRecall: 1.0 },
|
|
149
|
+
asset: { maxTokens: 512, maxLatencyP95Ms: 40, maxRssDeltaMib: 150 },
|
|
150
|
+
};
|
|
151
|
+
/** VC2C-specific qualification failure codes (returned, never thrown). */
|
|
152
|
+
export const ENC_QUALIFICATION_FAIL = {
|
|
153
|
+
/** The qualification manifest hash does not match the calibration that was fit
|
|
154
|
+
* (corrupt qualification manifest after calibration before selection). */
|
|
155
|
+
DIGEST_MISMATCH: "ENC_QUALIFICATION_DIGEST_MISMATCH",
|
|
156
|
+
/** One or more per-head EVALUATION thresholds failed (demotes all of A). */
|
|
157
|
+
THRESHOLD_FAILED: "ENC_QUALIFICATION_THRESHOLD_FAILED",
|
|
158
|
+
/** An asset-field qualification check (asset/latency/RSS) failed. */
|
|
159
|
+
ASSET_FAILED: "ENC_QUALIFICATION_ASSET_FAILED",
|
|
160
|
+
/** Calibration was attempted using held-out labels (fit prohibition). */
|
|
161
|
+
HELD_OUT_IN_FIT: "ENC_QUALIFICATION_HELD_OUT_IN_FIT",
|
|
162
|
+
};
|
|
163
|
+
/** The 4 registered VC2C conformance IDs (task 1: "register ENC-017..020"). */
|
|
164
|
+
export const ENC2C_IDS = [
|
|
165
|
+
"ENC-017",
|
|
166
|
+
"ENC-018",
|
|
167
|
+
"ENC-019",
|
|
168
|
+
"ENC-020",
|
|
169
|
+
];
|
|
@@ -0,0 +1,174 @@
|
|
|
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
|
+
import { createHash } from "node:crypto";
|
|
25
|
+
import { ENCODER_HEAD_ORDER, ENCODER_SEED, ENC_QUALIFICATION_FAIL, } from "./types.js";
|
|
26
|
+
/** Canonical digests of a sorted stable representation (order-invariant). */
|
|
27
|
+
function digestStrings(values) {
|
|
28
|
+
const sorted = [...values].sort();
|
|
29
|
+
return createHash("sha256").update(sorted.join("\n")).digest("hex");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Compute the calibration split digest for a set of (repository, session) groups
|
|
33
|
+
* present in the fit. The group list is canonicalized (sorted, deduped) so the
|
|
34
|
+
* digest is invariant to row order within the input. A caller that seeds WHOLE
|
|
35
|
+
* groups into the calibration split guarantees no group crosses a boundary.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Render one (repository, session) group as an injective canonical string.
|
|
39
|
+
* Each field is length-prefixed (`<len>:<value>`), so two distinct pairs can
|
|
40
|
+
* never collide to the same rendering — e.g. `{r:"a", s:"b::c"}` renders as
|
|
41
|
+
* `1:a3:b::c` while `{r:"a::b", s:"c"}` renders as `4:a::b1:c`. A plain
|
|
42
|
+
* `repository::session` join would conflate those when identifiers happen to
|
|
43
|
+
* contain "::"; length-prefixing makes the split digest sound for arbitrary
|
|
44
|
+
* repository/session identifiers.
|
|
45
|
+
*/
|
|
46
|
+
function renderGroup(g) {
|
|
47
|
+
return `${g.repository.length}:${g.repository}${g.session.length}:${g.session}`;
|
|
48
|
+
}
|
|
49
|
+
export function calibrationSplitDigest(groups) {
|
|
50
|
+
const rendered = new Set();
|
|
51
|
+
for (const g of groups)
|
|
52
|
+
rendered.add(renderGroup(g));
|
|
53
|
+
return digestStrings([...rendered]);
|
|
54
|
+
}
|
|
55
|
+
/** A deterministic 32-bit LCG step (matches the heads/runtime projectors). */
|
|
56
|
+
function nextState(state) {
|
|
57
|
+
return (state * 1664525 + 1013904223) >>> 0;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Deterministic per-head temperature in a stable, healthy range (e.g. 0.8..1.5).
|
|
61
|
+
*
|
|
62
|
+
* The caller-supplied `seed` is mixed into the LCG state through independent
|
|
63
|
+
* steps so it ALWAYS affects the temperature — even when it equals ENCODER_SEED.
|
|
64
|
+
* (A naive `ENCODER_SEED ^ head.length ^ seed` cancels the two seed terms when
|
|
65
|
+
* `seed === ENCODER_SEED`, leaving a pure function of the head — the default
|
|
66
|
+
* path would make the seed option inert. Here the seed seeds the PRNG first,
|
|
67
|
+
* then the head name is folded in, so both vary the fit independently.) The fit
|
|
68
|
+
* stays deterministic for a fixed (seed, head) across processes.
|
|
69
|
+
*/
|
|
70
|
+
function fitTemperature(head, seed) {
|
|
71
|
+
let state = (seed >>> 0) ^ 0x9e3779b9;
|
|
72
|
+
state = nextState(state);
|
|
73
|
+
state = (state ^ (head.length >>> 0)) >>> 0;
|
|
74
|
+
state = nextState(state);
|
|
75
|
+
state = (state ^ 0x85ebca6b) >>> 0;
|
|
76
|
+
state = nextState(state);
|
|
77
|
+
const r = (state / 4294967296) % 1;
|
|
78
|
+
return 0.8 + r * 0.7; // 0.8 .. 1.5
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Deterministic per-head decision threshold derived from the calibration
|
|
82
|
+
* distribution. For a head with BOTH classes present the threshold is the
|
|
83
|
+
* midpoint between the highest-scoring negative (label 0) and the lowest-scoring
|
|
84
|
+
* positive (label 1) — a true between-class balance point that NEVER lands on a
|
|
85
|
+
* negative example's own score (code-review Q03): a future inference at the
|
|
86
|
+
* highest calibration negative is still classified negative, and one at the
|
|
87
|
+
* lowest calibration positive is still classified positive. Degenerate heads with
|
|
88
|
+
* a single class fall back conservatively (no positives -> just above the top
|
|
89
|
+
* observed score; no negatives -> just below the lowest observed positive) and an
|
|
90
|
+
* empty head defaults to 0.5. Scoring is order-invariant: ties in score resolve by
|
|
91
|
+
* item ID bytewise (stable score/id ties), never by arrival order, so the fit is
|
|
92
|
+
* invariant to row order. This frozen threshold is a normative placeholder (real
|
|
93
|
+
* trained weights land later).
|
|
94
|
+
*/
|
|
95
|
+
function fitThreshold(head, examples) {
|
|
96
|
+
const headEx = examples
|
|
97
|
+
.filter((e) => e.head === head)
|
|
98
|
+
.slice()
|
|
99
|
+
.sort((a, b) => (a.score - b.score) || (a.itemId < b.itemId ? -1 : a.itemId > b.itemId ? 1 : 0));
|
|
100
|
+
if (headEx.length === 0)
|
|
101
|
+
return 0.5;
|
|
102
|
+
let highestNeg = -Infinity;
|
|
103
|
+
let lowestPos = Infinity;
|
|
104
|
+
for (const e of headEx) {
|
|
105
|
+
if (e.label === 0)
|
|
106
|
+
highestNeg = Math.max(highestNeg, e.score);
|
|
107
|
+
else
|
|
108
|
+
lowestPos = Math.min(lowestPos, e.score);
|
|
109
|
+
}
|
|
110
|
+
if (lowestPos === Infinity) {
|
|
111
|
+
// Only negatives observed — no positive class to balance against. Set the
|
|
112
|
+
// threshold just above the top observed score so no observed negative is
|
|
113
|
+
// re-admitted (a genuine future positive must exceed all calibration negatives).
|
|
114
|
+
return Math.max(0.5, highestNeg + 0.05);
|
|
115
|
+
}
|
|
116
|
+
if (highestNeg === -Infinity) {
|
|
117
|
+
// Only positives observed — set the threshold just below the lowest observed
|
|
118
|
+
// positive so every observed positive is admitted.
|
|
119
|
+
return Math.max(0, lowestPos - 0.05);
|
|
120
|
+
}
|
|
121
|
+
// Both classes present: the midpoint strictly between the highest negative and
|
|
122
|
+
// the lowest positive is the between-class balance point (Q03).
|
|
123
|
+
return (highestNeg + lowestPos) / 2;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Fit `CalibrationV1` over the calibration split only (task 2).
|
|
127
|
+
*
|
|
128
|
+
* - rejects any item whose `itemId` is in `heldOutIds` (held-out labels are
|
|
129
|
+
* prohibited from fit inputs).
|
|
130
|
+
* - treats `groups` as the calibration split units; the emitted split digest
|
|
131
|
+
* covers only the groups ACTUALLY present in the fit examples (plus the
|
|
132
|
+
* declared `groups`, when supplied — see below).
|
|
133
|
+
* - stable score/id ties (never arrival order).
|
|
134
|
+
*
|
|
135
|
+
* Held-out labels are prohibited by construction: the caller passes the full set
|
|
136
|
+
* of held-out item IDs, and the fit fails loudly if any calibration input is
|
|
137
|
+
* actually a held-out item — the fit can never silently learn from review labels.
|
|
138
|
+
*/
|
|
139
|
+
export function fitCalibration(examples, options = {}) {
|
|
140
|
+
const seed = options.seed ?? ENCODER_SEED;
|
|
141
|
+
const heldOut = new Set(options.heldOutIds ?? []);
|
|
142
|
+
for (const e of examples) {
|
|
143
|
+
if (heldOut.has(e.itemId)) {
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
code: ENC_QUALIFICATION_FAIL.HELD_OUT_IN_FIT,
|
|
147
|
+
reason: `held-out item ${e.itemId} leaked into calibration fit`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Stable order by (score, itemId) — arrival order never affects the digest or
|
|
152
|
+
// the fit; ties resolve by item ID bytewise (stable score/id ties).
|
|
153
|
+
const stable = examples
|
|
154
|
+
.slice()
|
|
155
|
+
.sort((a, b) => (a.score - b.score) || (a.itemId < b.itemId ? -1 : a.itemId > b.itemId ? 1 : 0));
|
|
156
|
+
const usedGroups = options.groups ?? stable.map((e) => ({ repository: e.repository, session: e.session }));
|
|
157
|
+
const splitDigest = calibrationSplitDigest(usedGroups);
|
|
158
|
+
const temperatures = {};
|
|
159
|
+
const thresholds = {};
|
|
160
|
+
for (const head of ENCODER_HEAD_ORDER) {
|
|
161
|
+
temperatures[head] = fitTemperature(head, seed);
|
|
162
|
+
thresholds[head] = fitThreshold(head, stable);
|
|
163
|
+
}
|
|
164
|
+
const calibration = {
|
|
165
|
+
schema: "calibration-v1",
|
|
166
|
+
headOrder: [...ENCODER_HEAD_ORDER],
|
|
167
|
+
calibrationSplitDigest: splitDigest,
|
|
168
|
+
fittedOnCalibrationOnly: true,
|
|
169
|
+
temperatures,
|
|
170
|
+
thresholds,
|
|
171
|
+
seed,
|
|
172
|
+
};
|
|
173
|
+
return { ok: true, calibration };
|
|
174
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/encoder/emit-vc2b.ts — VC2B observability seam.
|
|
3
|
+
*
|
|
4
|
+
* Owns the two VC2B events (task 5), gated on `MEGACOMPACT_VC2B` so the
|
|
5
|
+
* flag-OFF path emits zero events (mode C parity, byte-identical predecessor):
|
|
6
|
+
*
|
|
7
|
+
* vector_cortex_encoder_heads_emitted — a multi-head VectorSetV1 produced
|
|
8
|
+
* vector_cortex_encoder_fallback_selected — a mode B/C fallback selected
|
|
9
|
+
*
|
|
10
|
+
* No dashboard or API change is necessary for this internal sprint (task 5).
|
|
11
|
+
* Every event is a JSON line with `ts` + `event` (ENGINEERING_PRACTICES §8); the
|
|
12
|
+
* emitters are non-fatal (never break the agent loop). Pi-agnostic, zero network
|
|
13
|
+
* (PREVENT-PI-004), no `any` (PREVENT-011).
|
|
14
|
+
*/
|
|
15
|
+
import { VC2B_ENABLED } from "../../config/vector-cortex.js";
|
|
16
|
+
import { Logger } from "../../log.js";
|
|
17
|
+
/** A flag-gated no-op reporter (zero emissions, structural no-op). */
|
|
18
|
+
export const NOOP_VC2B_REPORTER = {
|
|
19
|
+
headsEmitted: () => { },
|
|
20
|
+
fallbackSelected: () => { },
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The default emitter: routes both VC2B events into the append-only structured
|
|
24
|
+
* logger (`src/log.ts`) as JSON lines with `ts` + `event`. Supplying `emit:` to
|
|
25
|
+
* `createEncoderHeadsReporter` replaces this with a caller-provided sink (used
|
|
26
|
+
* by tests and downstream consumers). Making the default a REAL producer means a
|
|
27
|
+
* caller that just invokes the producer seam (`encodeOrFallback`, `encodeVectorSet`,
|
|
28
|
+
* `selectTrigramBFallback`, `selectLexicalC`) without injecting an emitter still
|
|
29
|
+
* yields structured telemetry instead of silently dropping every event (task 5,
|
|
30
|
+
* code-review Q01). Best-effort: the logger swallows all I/O errors.
|
|
31
|
+
*/
|
|
32
|
+
function defaultEmitFor(logPath) {
|
|
33
|
+
const logger = new Logger(logPath === undefined ? {} : { path: logPath });
|
|
34
|
+
return (event, fields) => {
|
|
35
|
+
logger.info(event, fields);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Flag-gated emit, defaulting to a real logger-backed sink. The returned
|
|
40
|
+
* reporter is itself flag-gated (`VC2B_ENABLED`), so wiring it into a producer
|
|
41
|
+
* seam yields zero emissions when `MEGACOMPACT_VC2B=0` (byte-identical to the
|
|
42
|
+
* predecessor). Pass an explicit `emit` to route elsewhere (tests, downstream
|
|
43
|
+
* consumers); omit it to emit real structured log lines (Q01: the default is a
|
|
44
|
+
* live producer, not a silent no-op). `opts.logPath` only redirects the default
|
|
45
|
+
* sink and is ignored when `emit` is supplied.
|
|
46
|
+
*/
|
|
47
|
+
export function createEncoderHeadsReporter(emit, opts = {}) {
|
|
48
|
+
const sink = emit ?? defaultEmitFor(opts.logPath);
|
|
49
|
+
const fire = (event, fields) => {
|
|
50
|
+
if (!VC2B_ENABLED())
|
|
51
|
+
return;
|
|
52
|
+
try {
|
|
53
|
+
sink(event, { ...fields, ts: new Date().toISOString() });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* non-fatal observability */
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
headsEmitted: (fields) => fire("vector_cortex_encoder_heads_emitted", fields),
|
|
61
|
+
fallbackSelected: (fields) => fire("vector_cortex_encoder_fallback_selected", fields),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
import { VC2C_ENABLED } from "../../config/vector-cortex.js";
|
|
18
|
+
import { Logger } from "../../log.js";
|
|
19
|
+
/**
|
|
20
|
+
* The default emitter: routes both VC2C events into the append-only structured
|
|
21
|
+
* logger (`src/log.ts`) as JSON lines with `event` (the logger injects the
|
|
22
|
+
* numeric `ts`). Supplying `emit:` to `createEncoderQualificationReporter`
|
|
23
|
+
* replaces this with a caller-provided sink (used by tests and downstream
|
|
24
|
+
* consumers). Making the default a REAL producer means a caller that just
|
|
25
|
+
* invokes the producer seam (`selectQualifiedEncoder`) without injecting an
|
|
26
|
+
* emitter still yields structured telemetry instead of silently dropping every
|
|
27
|
+
* event (task 5). Best-effort: the logger swallows all I/O errors.
|
|
28
|
+
*/
|
|
29
|
+
function defaultEmitFor() {
|
|
30
|
+
const logger = new Logger();
|
|
31
|
+
return (event, fields) => {
|
|
32
|
+
logger.info(event, fields);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Flag-gated emit, defaulting to a real logger-backed sink. The returned
|
|
37
|
+
* reporter is itself flag-gated (`VC2C_ENABLED`), so wiring it into a producer
|
|
38
|
+
* seam yields zero emissions when `MEGACOMPACT_VC2C=0` (byte-identical to the
|
|
39
|
+
* predecessor). Pass an explicit `emit` to route elsewhere (tests, downstream
|
|
40
|
+
* consumers); omit it to emit real structured log lines. The `ts` is set by the
|
|
41
|
+
* underlying sink (numeric epoch-ms from `Logger`), not overridden here (Q04).
|
|
42
|
+
*/
|
|
43
|
+
export function createEncoderQualificationReporter(emit) {
|
|
44
|
+
const sink = emit ?? defaultEmitFor();
|
|
45
|
+
const fire = (event, fields) => {
|
|
46
|
+
if (!VC2C_ENABLED())
|
|
47
|
+
return;
|
|
48
|
+
try {
|
|
49
|
+
sink(event, fields);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
/* non-fatal observability */
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
qualificationPassed: (fields) => fire("vector_cortex_encoder_qualification_passed", fields),
|
|
57
|
+
qualificationDemoted: (fields) => fire("vector_cortex_encoder_qualification_demoted", fields),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
import { createEncoderHeadsReporter } from "./emit-vc2b.js";
|
|
22
|
+
import { embedTrigram512, selectTrigramBFallback, ENCODER_TRIGRAM_WIDTH } from "./trigram.js";
|
|
23
|
+
import { embedLexical, selectLexicalC, ENCODER_LEXICAL_WIDTH, ENCODER_LEXICAL_LIMITATION } from "./lexical.js";
|
|
24
|
+
/** Deterministic text derived from an int token sequence (mirrors router.ts). */
|
|
25
|
+
function textFromTokens(tokens) {
|
|
26
|
+
return tokens.join("-");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Select the B/C fallback for a failed qualification and produce the encode
|
|
30
|
+
* vector. When `injectBError` or `forceC` is set, selection lands on lexical C
|
|
31
|
+
* (absent A + injected B error -> C); otherwise a qualification THRESHOLD or
|
|
32
|
+
* DIGEST mismatch selects trigram B. Never returns without an explicit verdict.
|
|
33
|
+
*/
|
|
34
|
+
export function selectQualificationFallback(qualificationCode, tokens, options = {}) {
|
|
35
|
+
const reporter = options.reporter ?? createEncoderHeadsReporter();
|
|
36
|
+
const tokensText = textFromTokens(tokens);
|
|
37
|
+
// A absent + injected/forced B error -> C (lexical) with the documented
|
|
38
|
+
// semantic-context limitation.
|
|
39
|
+
if (options.forceC || options.injectBError) {
|
|
40
|
+
selectLexicalC({ reporter });
|
|
41
|
+
const vector = embedLexical(tokensText);
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
mode: "C",
|
|
45
|
+
vector,
|
|
46
|
+
width: ENCODER_LEXICAL_WIDTH,
|
|
47
|
+
code: options.injectBError ? "ENC_B_INJECTED_ERROR" : qualificationCode,
|
|
48
|
+
limitation: ENCODER_LEXICAL_LIMITATION,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// A qualification demotion selects the independently initialized trigram B.
|
|
52
|
+
selectTrigramBFallback({ reporter });
|
|
53
|
+
const vector = embedTrigram512(tokensText);
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
mode: "B",
|
|
57
|
+
vector,
|
|
58
|
+
width: ENCODER_TRIGRAM_WIDTH,
|
|
59
|
+
code: qualificationCode,
|
|
60
|
+
limitation: null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
import { ENCODER_HEAD_DIMS, ENCODER_HEAD_ORDER, ENCODER_HEAD_LOSS_WEIGHTS, ENCODER_HEAD_LOSS_SUM, ENCODER_SEED, } from "./types.js";
|
|
23
|
+
import { createEncoderHeadsReporter, NOOP_VC2B_REPORTER, } from "./emit-vc2b.js";
|
|
24
|
+
/** The stable head index of a head name (its position in ENCODER_HEAD_ORDER). */
|
|
25
|
+
const HEAD_INDEX = {
|
|
26
|
+
semantic: 0,
|
|
27
|
+
dependency: 1,
|
|
28
|
+
contradiction: 2,
|
|
29
|
+
cacheStability: 3,
|
|
30
|
+
payloadRouting: 4,
|
|
31
|
+
};
|
|
32
|
+
/** Deterministic 32-bit LCG step (matches runtime.ts projectSemantic). */
|
|
33
|
+
function nextState(state) {
|
|
34
|
+
return (state * 1664525 + 1013904223) >>> 0;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* L2-normalize a float vector in place semantics (returns a new Float32Array).
|
|
38
|
+
* A zero-norm (or empty) input maps to an all-zero vector of the same length
|
|
39
|
+
* (task 2: "mapping zero norm to an all-zero vector"). All finite.
|
|
40
|
+
*/
|
|
41
|
+
export function l2Normalize(values) {
|
|
42
|
+
const out = new Float32Array(values.length);
|
|
43
|
+
let sum = 0;
|
|
44
|
+
for (const v of values)
|
|
45
|
+
sum += v * v;
|
|
46
|
+
const norm = Math.sqrt(sum);
|
|
47
|
+
if (!(norm > 0))
|
|
48
|
+
return out; // zero norm -> all-zero
|
|
49
|
+
for (let i = 0; i < values.length; i++)
|
|
50
|
+
out[i] = values[i] / norm;
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/** L2 norm of a Float32Array (0 for empty/all-zero). */
|
|
54
|
+
export function l2Norm(values) {
|
|
55
|
+
let sum = 0;
|
|
56
|
+
for (const v of values)
|
|
57
|
+
sum += v * v;
|
|
58
|
+
return Math.sqrt(sum);
|
|
59
|
+
}
|
|
60
|
+
/** A deterministic per-head projection over the token sequence, pre-normalization. */
|
|
61
|
+
function projectRaw(head, tokens, seed) {
|
|
62
|
+
const dim = ENCODER_HEAD_DIMS[head];
|
|
63
|
+
const out = new Float32Array(dim);
|
|
64
|
+
// An EMPTY token sequence has no signal: the raw projection is the zero vector,
|
|
65
|
+
// so after L2 normalization it maps to the all-zero vector (task 2: "mapping
|
|
66
|
+
// zero norm to an all-zero vector"; ENC-ZERO-002). This keeps empty input
|
|
67
|
+
// finite and zero-norm instead of seeding spurious unit-norm noise.
|
|
68
|
+
if (tokens.length === 0)
|
|
69
|
+
return out;
|
|
70
|
+
// Mix the stable head index + ENCODER_SEED + seed into a per-head state so
|
|
71
|
+
// each head is a distinct independent projection (failure-triad independence).
|
|
72
|
+
let state = (((ENCODER_SEED ^ HEAD_INDEX[head]) >>> 0) ^ (seed >>> 0)) ^ 0x9e3779b9;
|
|
73
|
+
for (const t of tokens)
|
|
74
|
+
state = nextState(state ^ ((t >>> 0) * 2654435761));
|
|
75
|
+
for (let i = 0; i < dim; i++) {
|
|
76
|
+
state = nextState(state ^ seed);
|
|
77
|
+
out[i] = (state / 4294967296) * 2 - 1;
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Compute one head's L2-normalized vector (all-zero on zero norm) for a token
|
|
83
|
+
* sequence. Deterministic for a given seed (repeat drift == 0).
|
|
84
|
+
*/
|
|
85
|
+
export function projectHead(head, tokens, seed = ENCODER_SEED) {
|
|
86
|
+
const raw = projectRaw(head, tokens, seed);
|
|
87
|
+
return { head, dim: ENCODER_HEAD_DIMS[head], values: l2Normalize(raw) };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Encode a token sequence into a `VectorSetV1`: the five heads in stable order,
|
|
91
|
+
* each L2-normalized (all-zero on zero norm). Emits `heads_emitted` via the
|
|
92
|
+
* reporter (non-fatal, flag-gated). Deterministic for a given seed.
|
|
93
|
+
*/
|
|
94
|
+
export function encodeVectorSet(tokens, options = {}) {
|
|
95
|
+
const seed = options.seed ?? ENCODER_SEED;
|
|
96
|
+
const reporter = options.reporter ?? createEncoderHeadsReporter();
|
|
97
|
+
const heads = ENCODER_HEAD_ORDER.map((h) => projectHead(h, tokens, seed));
|
|
98
|
+
reporter.headsEmitted({
|
|
99
|
+
heads: heads.length,
|
|
100
|
+
dims: heads.map((h) => h.dim).join("/"),
|
|
101
|
+
normalized: true,
|
|
102
|
+
tokens: tokens.length,
|
|
103
|
+
});
|
|
104
|
+
return { schema: "vector-set-v1", inputTokens: [...tokens], heads, normalized: true };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The per-head loss weights (must sum to ENCODER_HEAD_LOSS_SUM exactly).
|
|
108
|
+
* Exposed for training/tests to assert the normative .35/.20/.20/.15/.10 split.
|
|
109
|
+
*/
|
|
110
|
+
export function headLossWeights() {
|
|
111
|
+
return { ...ENCODER_HEAD_LOSS_WEIGHTS };
|
|
112
|
+
}
|
|
113
|
+
export { ENCODER_HEAD_ORDER, ENCODER_HEAD_DIMS, ENCODER_HEAD_LOSS_SUM, ENCODER_SEED, NOOP_VC2B_REPORTER };
|