pi-mega-compact 0.20.44 → 0.20.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/vector-cortex-enc0c.js +35 -0
- package/dist/config/vector-cortex-enc0d.js +39 -0
- package/dist/config/vector-cortex.js +2 -0
- package/dist/config.js +1 -1
- package/dist/extensions/dashboard-server/routes-rag-settings-vector-cortex.js +2 -0
- package/dist/src/config/vector-cortex-enc0c.js +35 -0
- package/dist/src/config/vector-cortex-enc0d.js +39 -0
- package/dist/src/config/vector-cortex.js +2 -0
- package/dist/src/config.js +1 -1
- package/dist/src/vector-cortex/encoder/heads-candidate.js +127 -0
- package/dist/src/vector-cortex/encoder/heads.js +4 -0
- package/dist/src/vector-cortex/encoder/promotion-emit.js +40 -0
- package/dist/src/vector-cortex/encoder/promotion.js +64 -0
- package/dist/vector-cortex/encoder/heads-candidate.js +127 -0
- package/dist/vector-cortex/encoder/heads.js +4 -0
- package/dist/vector-cortex/encoder/promotion-emit.js +40 -0
- package/dist/vector-cortex/encoder/promotion.js +64 -0
- package/extensions/dashboard-server/routes-rag-settings-vector-cortex.ts +12 -0
- package/package.json +1 -1
- package/src/config/vector-cortex-enc0c.ts +37 -0
- package/src/config/vector-cortex-enc0d.ts +41 -0
- package/src/config/vector-cortex.ts +2 -2
- package/src/config.ts +2 -0
- package/src/vector-cortex/encoder/heads-candidate.ts +148 -0
- package/src/vector-cortex/encoder/heads.ts +14 -0
- package/src/vector-cortex/encoder/promotion-emit.ts +63 -0
- package/src/vector-cortex/encoder/promotion.ts +95 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config/vector-cortex-enc0c.ts — ENC-0c five-head supervision transfer 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-enc0a.ts / enc0b.ts and
|
|
6
|
+
* the VC8C/VC9A-D/ML5A-E/DEDUP_ATTR siblings were. vector-cortex.ts re-exports
|
|
7
|
+
* the flag below and root src/config.ts re-exports it, so no consumer import
|
|
8
|
+
* path changes.
|
|
9
|
+
*
|
|
10
|
+
* ENC-0c trains the five real heads onto the frozen ENC-0b bge-small trunk via
|
|
11
|
+
* supervision transfer, and stages a qualified candidate under
|
|
12
|
+
* ~/.pi/mega-compact-encoder/candidates/ (only when the developer trains it —
|
|
13
|
+
* the extension never stages one by itself). When the flag is ON and a
|
|
14
|
+
* qualified candidate exists, `loadHeadCandidate` (encoder/heads.ts seam) serves
|
|
15
|
+
* the trained head weights; when the flag is OFF (MEGACOMPACT_ENC_0C=0) or no
|
|
16
|
+
* candidate is staged, the heads keep serving the ENC-0b survivor exactly —
|
|
17
|
+
* byte-identical, no weight change. The flag gates ONLY the candidate-load seam;
|
|
18
|
+
* the survivor path is untouched.
|
|
19
|
+
*
|
|
20
|
+
* The split is purely mechanical: ENC_0C_ENABLED follows ENC_0B_ENABLED in name,
|
|
21
|
+
* semantics, and default, and vector-cortex.ts re-exports it so every existing
|
|
22
|
+
* `from "./config/vector-cortex.js"` import keeps resolving unchanged.
|
|
23
|
+
*
|
|
24
|
+
* Pi-agnostic, dependency-free (PREVENT-PI-004 / PREVENT-011).
|
|
25
|
+
*/
|
|
26
|
+
import { sprintFlag } from "./vector-cortex-flag.js";
|
|
27
|
+
/**
|
|
28
|
+
* ENC-0c — five-head supervision transfer on the frozen bge-small trunk.
|
|
29
|
+
* Default ON. `MEGACOMPACT_ENC_0C=0` disables and is byte-identical to the
|
|
30
|
+
* predecessor (ENC-0b): no head candidate is loaded and the heads keep serving
|
|
31
|
+
* the ENC-0b survivor defaults exactly as before. This flag MUST also be a
|
|
32
|
+
* dashboard SETTINGS toggle (visible in config UI, never in EXCLUDED_SETTINGS),
|
|
33
|
+
* mirroring ENC_0A and ENC_0B.
|
|
34
|
+
*/
|
|
35
|
+
export const ENC_0C_ENABLED = () => sprintFlag("MEGACOMPACT_ENC_0C");
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config/vector-cortex-enc0d.ts — ENC-0d promotion gate over real trained assets.
|
|
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-enc0a.ts / enc0b.ts /
|
|
6
|
+
* enc0c.ts and the VC8C/VC9A-D/ML5A-E/DEDUP_ATTR siblings were. vector-cortex.ts
|
|
7
|
+
* re-exports the flag below and root src/config.ts re-exports it, so no
|
|
8
|
+
* consumer import path changes.
|
|
9
|
+
*
|
|
10
|
+
* ENC-0d turns the ML5-E promotion gate into the real-asset promotion path:
|
|
11
|
+
* it accepts a `{color}` real candidate manifest (the ENC-0c trained head
|
|
12
|
+
* weights + the ENC-0b trunk, staged under
|
|
13
|
+
* ~/.pi/mega-compact-encoder/candidates/), digest-verifies every staged byte
|
|
14
|
+
* before any swap, and performs an atomic asset swap with rollback-to-previous
|
|
15
|
+
* on qualification failure. The extension itself never stages candidate assets —
|
|
16
|
+
* only the operator trains them. When the flag is ON and a digest-verified
|
|
17
|
+
* green candidate exists, an atomic swap is performed and the runtime flips into
|
|
18
|
+
* qualified mode A; a red qualification (or any verification failure) keeps the
|
|
19
|
+
* prior asset live and emits a demotion.
|
|
20
|
+
*
|
|
21
|
+
* `MEGACOMPACT_ENC_0D=0` accepts no candidate, swaps nothing, emits nothing, and
|
|
22
|
+
* the shipped manifest stays at the ENC-0c survivor — byte-identical predecessor.
|
|
23
|
+
*
|
|
24
|
+
* The split is purely mechanical: ENC_0D_ENABLED follows ENC_0C_ENABLED in name,
|
|
25
|
+
* semantics, and default, and vector-cortex.ts re-exports it so every existing
|
|
26
|
+
* `from "./config/vector-cortex.js"` import keeps resolving unchanged.
|
|
27
|
+
*
|
|
28
|
+
* Pi-agnostic, dependency-free (PREVENT-PI-004 / PREVENT-011).
|
|
29
|
+
*/
|
|
30
|
+
import { sprintFlag } from "./vector-cortex-flag.js";
|
|
31
|
+
/**
|
|
32
|
+
* ENC-0d — atomic real-asset promotion over trained candidates. Default ON.
|
|
33
|
+
* `MEGACOMPACT_ENC_0D=0` disables and is byte-identical to the predecessor
|
|
34
|
+
* (ENC-0c): no candidate is accepted, no swap is performed, no promote/
|
|
35
|
+
* demote/rollback events are emitted, and the shipped manifest stays at the
|
|
36
|
+
* ENC-0c survivor. This flag MUST also be a dashboard SETTINGS toggle (visible
|
|
37
|
+
* in config UI, never in EXCLUDED_SETTINGS), mirroring ENC_0A/ENC_0B/ENC_0C.
|
|
38
|
+
*/
|
|
39
|
+
export const ENC_0D_ENABLED = () => sprintFlag("MEGACOMPACT_ENC_0D");
|
|
@@ -251,5 +251,7 @@ export { ML5E_ENABLED } from "./vector-cortex-ml5e.js";
|
|
|
251
251
|
export { DEDUP_ATTR_ENABLED } from "./vector-cortex-dedup-attr.js";
|
|
252
252
|
export { ENC_0A_ENABLED } from "./vector-cortex-enc0a.js";
|
|
253
253
|
export { ENC_0B_ENABLED } from "./vector-cortex-enc0b.js";
|
|
254
|
+
export { ENC_0C_ENABLED } from "./vector-cortex-enc0c.js";
|
|
255
|
+
export { ENC_0D_ENABLED } from "./vector-cortex-enc0d.js";
|
|
254
256
|
// Breaker constants (TRIAD_RESILIENCE.md §breaker) extracted to vector-cortex-breakers.ts.
|
|
255
257
|
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, ML5A_ENABLED, ML5B_ENABLED, ML5C_ENABLED, ML5D_ENABLED, ML5E_ENABLED, DEDUP_ATTR_ENABLED, ENC_0A_ENABLED, ENC_0B_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, ML5B_ENABLED, ML5C_ENABLED, ML5D_ENABLED, ML5E_ENABLED, DEDUP_ATTR_ENABLED, ENC_0A_ENABLED, ENC_0B_ENABLED, ENC_0C_ENABLED, ENC_0D_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";
|
|
@@ -60,5 +60,7 @@ export const VECTOR_CORTEX_SETTINGS = {
|
|
|
60
60
|
boolDirect("MEGACOMPACT_DEDUP_ATTR", "Dedup Tier Attribution Rollup", "Dedup tier-attribution rollup: per-tier dedup catch shares (L0/L1/L2/new percent of dedup decisions) read from the local events.log dedup_audit stream (GET /api/dedup-tier-attribution). OFF = 404 + no cache file, byte-identical predecessor.", true),
|
|
61
61
|
boolDirect("MEGACOMPACT_ENC_0A", "ENC-0a Encoder Backend Decision", "ENC-0a learned-encoder backend-decision lock: records the transformers.js/WASM vs onnxruntime-node choice, per-platform install matrix, opset-21 baseline and pinned digests in docs/vector-cortex/encoder-backend-decision.md. OFF = no decision record written / no resolver runs, mode B trigram byte-identical predecessor.", true),
|
|
62
62
|
boolDirect("MEGACOMPACT_ENC_0B", "ENC-0b Real Trunk Fetch & Gated Path", "ENC-0b real ONNX trunk fetch + gated inference: replaces the LCG placeholder with the real bge-small int8 model through an ONNX InferenceSession (onnxruntime-web WASM). OFF = LCG placeholder serves byte-identical predecessor output, no ONNX session built.", true),
|
|
63
|
+
boolDirect("MEGACOMPACT_ENC_0C", "ENC-0c Five-Head Supervision Transfer", "ENC-0c five-head supervision transfer on the frozen bge-small trunk: when a developer-trained head candidate is staged under ~/.pi/mega-compact-encoder/candidates/, the five heads serve the trained weights. OFF = no candidate is loaded, the heads serve byte-identical ENC-0b survivor defaults.", true),
|
|
64
|
+
boolDirect("MEGACOMPACT_ENC_0D", "ENC-0d Real-Asset Promotion Gate", "ENC-0d promotion gate over real trained assets: accepts a {color} real candidate manifest (the ENC-0c trained head weights + the ENC-0b trunk), digest-verifies every staged byte, and atomically swaps the shipped manifest to a green candidate with rollback-to-previous on qualification failure. OFF = accepts no candidate, swaps nothing, emits nothing, shipped manifest stays at the ENC-0c survivor.", true),
|
|
63
65
|
],
|
|
64
66
|
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config/vector-cortex-enc0c.ts — ENC-0c five-head supervision transfer 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-enc0a.ts / enc0b.ts and
|
|
6
|
+
* the VC8C/VC9A-D/ML5A-E/DEDUP_ATTR siblings were. vector-cortex.ts re-exports
|
|
7
|
+
* the flag below and root src/config.ts re-exports it, so no consumer import
|
|
8
|
+
* path changes.
|
|
9
|
+
*
|
|
10
|
+
* ENC-0c trains the five real heads onto the frozen ENC-0b bge-small trunk via
|
|
11
|
+
* supervision transfer, and stages a qualified candidate under
|
|
12
|
+
* ~/.pi/mega-compact-encoder/candidates/ (only when the developer trains it —
|
|
13
|
+
* the extension never stages one by itself). When the flag is ON and a
|
|
14
|
+
* qualified candidate exists, `loadHeadCandidate` (encoder/heads.ts seam) serves
|
|
15
|
+
* the trained head weights; when the flag is OFF (MEGACOMPACT_ENC_0C=0) or no
|
|
16
|
+
* candidate is staged, the heads keep serving the ENC-0b survivor exactly —
|
|
17
|
+
* byte-identical, no weight change. The flag gates ONLY the candidate-load seam;
|
|
18
|
+
* the survivor path is untouched.
|
|
19
|
+
*
|
|
20
|
+
* The split is purely mechanical: ENC_0C_ENABLED follows ENC_0B_ENABLED in name,
|
|
21
|
+
* semantics, and default, and vector-cortex.ts re-exports it so every existing
|
|
22
|
+
* `from "./config/vector-cortex.js"` import keeps resolving unchanged.
|
|
23
|
+
*
|
|
24
|
+
* Pi-agnostic, dependency-free (PREVENT-PI-004 / PREVENT-011).
|
|
25
|
+
*/
|
|
26
|
+
import { sprintFlag } from "./vector-cortex-flag.js";
|
|
27
|
+
/**
|
|
28
|
+
* ENC-0c — five-head supervision transfer on the frozen bge-small trunk.
|
|
29
|
+
* Default ON. `MEGACOMPACT_ENC_0C=0` disables and is byte-identical to the
|
|
30
|
+
* predecessor (ENC-0b): no head candidate is loaded and the heads keep serving
|
|
31
|
+
* the ENC-0b survivor defaults exactly as before. This flag MUST also be a
|
|
32
|
+
* dashboard SETTINGS toggle (visible in config UI, never in EXCLUDED_SETTINGS),
|
|
33
|
+
* mirroring ENC_0A and ENC_0B.
|
|
34
|
+
*/
|
|
35
|
+
export const ENC_0C_ENABLED = () => sprintFlag("MEGACOMPACT_ENC_0C");
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config/vector-cortex-enc0d.ts — ENC-0d promotion gate over real trained assets.
|
|
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-enc0a.ts / enc0b.ts /
|
|
6
|
+
* enc0c.ts and the VC8C/VC9A-D/ML5A-E/DEDUP_ATTR siblings were. vector-cortex.ts
|
|
7
|
+
* re-exports the flag below and root src/config.ts re-exports it, so no
|
|
8
|
+
* consumer import path changes.
|
|
9
|
+
*
|
|
10
|
+
* ENC-0d turns the ML5-E promotion gate into the real-asset promotion path:
|
|
11
|
+
* it accepts a `{color}` real candidate manifest (the ENC-0c trained head
|
|
12
|
+
* weights + the ENC-0b trunk, staged under
|
|
13
|
+
* ~/.pi/mega-compact-encoder/candidates/), digest-verifies every staged byte
|
|
14
|
+
* before any swap, and performs an atomic asset swap with rollback-to-previous
|
|
15
|
+
* on qualification failure. The extension itself never stages candidate assets —
|
|
16
|
+
* only the operator trains them. When the flag is ON and a digest-verified
|
|
17
|
+
* green candidate exists, an atomic swap is performed and the runtime flips into
|
|
18
|
+
* qualified mode A; a red qualification (or any verification failure) keeps the
|
|
19
|
+
* prior asset live and emits a demotion.
|
|
20
|
+
*
|
|
21
|
+
* `MEGACOMPACT_ENC_0D=0` accepts no candidate, swaps nothing, emits nothing, and
|
|
22
|
+
* the shipped manifest stays at the ENC-0c survivor — byte-identical predecessor.
|
|
23
|
+
*
|
|
24
|
+
* The split is purely mechanical: ENC_0D_ENABLED follows ENC_0C_ENABLED in name,
|
|
25
|
+
* semantics, and default, and vector-cortex.ts re-exports it so every existing
|
|
26
|
+
* `from "./config/vector-cortex.js"` import keeps resolving unchanged.
|
|
27
|
+
*
|
|
28
|
+
* Pi-agnostic, dependency-free (PREVENT-PI-004 / PREVENT-011).
|
|
29
|
+
*/
|
|
30
|
+
import { sprintFlag } from "./vector-cortex-flag.js";
|
|
31
|
+
/**
|
|
32
|
+
* ENC-0d — atomic real-asset promotion over trained candidates. Default ON.
|
|
33
|
+
* `MEGACOMPACT_ENC_0D=0` disables and is byte-identical to the predecessor
|
|
34
|
+
* (ENC-0c): no candidate is accepted, no swap is performed, no promote/
|
|
35
|
+
* demote/rollback events are emitted, and the shipped manifest stays at the
|
|
36
|
+
* ENC-0c survivor. This flag MUST also be a dashboard SETTINGS toggle (visible
|
|
37
|
+
* in config UI, never in EXCLUDED_SETTINGS), mirroring ENC_0A/ENC_0B/ENC_0C.
|
|
38
|
+
*/
|
|
39
|
+
export const ENC_0D_ENABLED = () => sprintFlag("MEGACOMPACT_ENC_0D");
|
|
@@ -251,5 +251,7 @@ export { ML5E_ENABLED } from "./vector-cortex-ml5e.js";
|
|
|
251
251
|
export { DEDUP_ATTR_ENABLED } from "./vector-cortex-dedup-attr.js";
|
|
252
252
|
export { ENC_0A_ENABLED } from "./vector-cortex-enc0a.js";
|
|
253
253
|
export { ENC_0B_ENABLED } from "./vector-cortex-enc0b.js";
|
|
254
|
+
export { ENC_0C_ENABLED } from "./vector-cortex-enc0c.js";
|
|
255
|
+
export { ENC_0D_ENABLED } from "./vector-cortex-enc0d.js";
|
|
254
256
|
// Breaker constants (TRIAD_RESILIENCE.md §breaker) extracted to vector-cortex-breakers.ts.
|
|
255
257
|
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/src/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, ML5A_ENABLED, ML5B_ENABLED, ML5C_ENABLED, ML5D_ENABLED, ML5E_ENABLED, DEDUP_ATTR_ENABLED, ENC_0A_ENABLED, ENC_0B_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, ML5B_ENABLED, ML5C_ENABLED, ML5D_ENABLED, ML5E_ENABLED, DEDUP_ATTR_ENABLED, ENC_0A_ENABLED, ENC_0B_ENABLED, ENC_0C_ENABLED, ENC_0D_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";
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* encoder/heads-candidate.ts — ENC-0c head-candidate load seam (delegate impl).
|
|
3
|
+
*
|
|
4
|
+
* Loads/validates a `head-candidate-v1` five-head candidate staged under
|
|
5
|
+
* `~/.pi/mega-compact-encoder/candidates/<version>/` after training on the
|
|
6
|
+
* frozen ENC-0b bge-small trunk. Flag-off / absent / malformed / wrong dims /
|
|
7
|
+
* non-finite / digest / trunk mismatch each return null or {ok:false} — a bad
|
|
8
|
+
* candidate is NEVER force-loaded; the runtime keeps the ENC-0b survivor
|
|
9
|
+
* byte-identical. No `any` (PREVENT-011), local-only, structured logging.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { ENC_0C_ENABLED } from "../../config/vector-cortex.js";
|
|
15
|
+
import { ENCODER_HEAD_DIMS, ENCODER_HEAD_ORDER } from "./types.js";
|
|
16
|
+
export const HEAD_CANDIDATE_SCHEMA = "head-candidate-v1";
|
|
17
|
+
const MANIFEST_FILE = "manifest.json";
|
|
18
|
+
export const HEAD_CANDIDATE_FAIL = {
|
|
19
|
+
INVALID: "ENC0C_CANDIDATE_INVALID",
|
|
20
|
+
TRUNK_MISMATCH: "ENC0C_TRUNK_MISMATCH",
|
|
21
|
+
DIM_MISMATCH: "ENC0C_DIM_MISMATCH",
|
|
22
|
+
NON_FINITE: "ENC0C_NON_FINITE",
|
|
23
|
+
DIGEST_MISMATCH: "ENC0C_DIGEST_MISMATCH",
|
|
24
|
+
};
|
|
25
|
+
function sha256(buf) { return createHash("sha256").update(buf).digest("hex"); }
|
|
26
|
+
function finiteDim(values, dim) {
|
|
27
|
+
if (values.length !== dim)
|
|
28
|
+
return false;
|
|
29
|
+
for (const v of values)
|
|
30
|
+
if (!Number.isFinite(v))
|
|
31
|
+
return false;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
function parseManifest(m) {
|
|
35
|
+
if (!m || m["schema"] !== HEAD_CANDIDATE_SCHEMA)
|
|
36
|
+
return null;
|
|
37
|
+
const version = typeof m["version"] === "string" ? m["version"] : "";
|
|
38
|
+
const trunkDigest = typeof m["trunkDigest"] === "string" ? m["trunkDigest"] : "";
|
|
39
|
+
if (!version || !trunkDigest || !Array.isArray(m["heads"]))
|
|
40
|
+
return null;
|
|
41
|
+
const heads = [];
|
|
42
|
+
for (const item of m["heads"]) {
|
|
43
|
+
const rec = item;
|
|
44
|
+
if (!rec || typeof rec !== "object")
|
|
45
|
+
return null;
|
|
46
|
+
const name = rec["name"];
|
|
47
|
+
if (typeof name !== "string" || !ENCODER_HEAD_ORDER.includes(name))
|
|
48
|
+
return null;
|
|
49
|
+
heads.push({
|
|
50
|
+
name: name,
|
|
51
|
+
dim: Number(rec["dim"] ?? 0),
|
|
52
|
+
sha256: typeof rec["sha256"] === "string" ? rec["sha256"] : "",
|
|
53
|
+
bytes: Number(rec["bytes"] ?? 0),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return { version, trunkDigest, heads };
|
|
57
|
+
}
|
|
58
|
+
/** All 5 dims match, weights finite, digests hold. */
|
|
59
|
+
export function validateHeadCandidate(candidate) {
|
|
60
|
+
for (const h of ENCODER_HEAD_ORDER) {
|
|
61
|
+
const dim = candidate.dims[h];
|
|
62
|
+
if (dim !== ENCODER_HEAD_DIMS[h])
|
|
63
|
+
return { ok: false, code: HEAD_CANDIDATE_FAIL.DIM_MISMATCH };
|
|
64
|
+
const w = candidate.weights[h];
|
|
65
|
+
if (!w || !finiteDim(w, dim))
|
|
66
|
+
return { ok: false, code: HEAD_CANDIDATE_FAIL.NON_FINITE };
|
|
67
|
+
const buf = new Uint8Array(w.buffer, w.byteOffset, w.byteLength);
|
|
68
|
+
if (sha256(buf) !== candidate.digests[h])
|
|
69
|
+
return { ok: false, code: HEAD_CANDIDATE_FAIL.DIGEST_MISMATCH };
|
|
70
|
+
}
|
|
71
|
+
return { ok: true };
|
|
72
|
+
}
|
|
73
|
+
/** Load a candidate against the frozen trunk; null on any violation, never throws. */
|
|
74
|
+
export function loadHeadCandidate(candidateDir, manifest) {
|
|
75
|
+
if (!ENC_0C_ENABLED())
|
|
76
|
+
return null;
|
|
77
|
+
let raw;
|
|
78
|
+
try {
|
|
79
|
+
raw = readFileSync(join(candidateDir, MANIFEST_FILE), "utf8");
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
let parsed;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(raw);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const m = parseManifest(parsed);
|
|
92
|
+
if (!m || m.trunkDigest !== manifest.onnx.sha256)
|
|
93
|
+
return null;
|
|
94
|
+
const weights = {};
|
|
95
|
+
const digests = {};
|
|
96
|
+
const dims = {};
|
|
97
|
+
for (const rec of m.heads) {
|
|
98
|
+
const want = rec.dim * 4;
|
|
99
|
+
if (!Number.isInteger(want) || want <= 0 || rec.dim !== ENCODER_HEAD_DIMS[rec.name])
|
|
100
|
+
return null;
|
|
101
|
+
let bytes;
|
|
102
|
+
try {
|
|
103
|
+
bytes = readFileSync(join(candidateDir, `${rec.name}.bin`));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const f = new Float32Array(bytes.buffer, bytes.byteOffset, rec.dim);
|
|
109
|
+
const d = sha256(bytes);
|
|
110
|
+
if (bytes.length !== want || d !== rec.sha256)
|
|
111
|
+
return null;
|
|
112
|
+
weights[rec.name] = f.slice();
|
|
113
|
+
digests[rec.name] = d;
|
|
114
|
+
dims[rec.name] = rec.dim;
|
|
115
|
+
}
|
|
116
|
+
for (const h of ENCODER_HEAD_ORDER) {
|
|
117
|
+
if (weights[h] === undefined || digests[h] === undefined || dims[h] === undefined)
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const candidate = {
|
|
121
|
+
schema: HEAD_CANDIDATE_SCHEMA, version: m.version, trunkDigest: m.trunkDigest,
|
|
122
|
+
dims: dims,
|
|
123
|
+
weights: weights,
|
|
124
|
+
digests: digests,
|
|
125
|
+
};
|
|
126
|
+
return validateHeadCandidate(candidate).ok ? candidate : null;
|
|
127
|
+
}
|
|
@@ -198,3 +198,7 @@ export function projectHeadFromTrunk(head, trunk, table) {
|
|
|
198
198
|
return { head, dim, values: l2Normalize(out) };
|
|
199
199
|
}
|
|
200
200
|
export { ENCODER_HEAD_ORDER, ENCODER_HEAD_DIMS, ENCODER_HEAD_LOSS_SUM, ENCODER_SEED, NOOP_VC2B_REPORTER };
|
|
201
|
+
// ENC-0c five-head candidate seam (delegate-shell): the load/validate impl
|
|
202
|
+
// lives in heads-candidate.ts; these re-exports keep the public import path
|
|
203
|
+
// stable at heads.ts without growing this survivor file over the soft limit.
|
|
204
|
+
export { HEAD_CANDIDATE_SCHEMA, HEAD_CANDIDATE_FAIL, loadHeadCandidate, validateHeadCandidate, } from "./heads-candidate.js";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/encoder/promotion-emit.ts — ENC-0d promotion event writers.
|
|
3
|
+
*
|
|
4
|
+
* Writes the three real-asset promotion events to the monitoring `events.log`
|
|
5
|
+
* as structured append-only JSON lines via the existing `logBenchEvent` seam
|
|
6
|
+
* (`src/monitoring.ts`):
|
|
7
|
+
*
|
|
8
|
+
* - `vector_cortex_asset_promoted` (green digest-verified swap)
|
|
9
|
+
* - `vector_cortex_asset_demoted` (red qualification / digest failure)
|
|
10
|
+
* - `vector_cortex_asset_rollback_back` (restore of a prior stack digest)
|
|
11
|
+
*
|
|
12
|
+
* Non-fatal and log-and-swallow: a write failure never breaks the agent loop
|
|
13
|
+
* (best-effort store/write contract). Each line is `{ts, event, ...fields}`
|
|
14
|
+
* shaped so the dashboard live-stream tail and evidence tooling parse it
|
|
15
|
+
* identically to every other monitoring event. Events carry digests, colors
|
|
16
|
+
* and verdicts ONLY — never message content (EVAL-REDACT-002).
|
|
17
|
+
*
|
|
18
|
+
* The path defaults to the standard state-dir events.log (mirrors
|
|
19
|
+
* defaultEventsPath). Zero network, no `any` (PREVENT-PI-004 / PREVENT-011).
|
|
20
|
+
*/
|
|
21
|
+
import { defaultEventsPath, logBenchEvent } from "../../monitoring.js";
|
|
22
|
+
import { getStateDir } from "../../store.js";
|
|
23
|
+
/** Default events.log beside the state dir (mirrors bench.ts). */
|
|
24
|
+
export function promotionEventsPath(stateDir = getStateDir()) {
|
|
25
|
+
return defaultEventsPath(stateDir);
|
|
26
|
+
}
|
|
27
|
+
const EVENT_NAME = {
|
|
28
|
+
promoted: "vector_cortex_asset_promoted",
|
|
29
|
+
demoted: "vector_cortex_asset_demoted",
|
|
30
|
+
rollback_back: "vector_cortex_asset_rollback_back",
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Append one promotion event to events.log (best-effort, non-fatal). The line
|
|
34
|
+
* is `{ts, event, ...fields}` shaped, written append-only via the monitoring
|
|
35
|
+
* logBenchEvent seam. On any write failure the event is swallowed — the agent
|
|
36
|
+
* loop is never broken.
|
|
37
|
+
*/
|
|
38
|
+
export function appendPromotionEvent(path, kind, fields) {
|
|
39
|
+
logBenchEvent(path, EVENT_NAME[kind], { ...fields });
|
|
40
|
+
}
|
|
@@ -14,6 +14,15 @@
|
|
|
14
14
|
* an atomic digest swap (flip the committed pointer to the prior entry's
|
|
15
15
|
* digest in one step) — no partial state is possible because the swap is a
|
|
16
16
|
* single value assignment.
|
|
17
|
+
*
|
|
18
|
+
* ENC-0d extends the ledger to the real-asset promotion path: `PromotionV1`
|
|
19
|
+
* carries `{color, assetDigestStack}` — a promotion color from gate
|
|
20
|
+
* qualification ("green" atomically swaps to the trained asset, "red" keeps
|
|
21
|
+
* the prior asset live) and a LIFO of prior shipped digest for O(1)-by-sha256
|
|
22
|
+
* rollback. The on-disk manifest byte-swap itself (write-temp-then-rename,
|
|
23
|
+
* never in-place partial) is the promotion gate script's job; these helpers
|
|
24
|
+
* stay pure and return the restorable digest + the updated manifest/stack so
|
|
25
|
+
* the script can perform the swap in one atomic step.
|
|
17
26
|
*/
|
|
18
27
|
// ---------------------------------------------------------------------------
|
|
19
28
|
// PromotionV1 — the ledger row recording one nightly training + promotion run.
|
|
@@ -42,6 +51,61 @@ export function rollbackTo(manifest, digest) {
|
|
|
42
51
|
};
|
|
43
52
|
}
|
|
44
53
|
// ---------------------------------------------------------------------------
|
|
54
|
+
// ENC-0d LIFO rollback stack + atomic swap. Pure pointer arithmetic; the
|
|
55
|
+
// on-disk write-temp-then-rename of the swapped manifest bytes is the gate
|
|
56
|
+
// script's job so no partial state can ever be observed (PREVENT-PI-004).
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
/** Push a freshly-shipped digest onto the LIFO rollback stack (top = latest
|
|
59
|
+
* prior shipped digest). Non-mutating: returns a new array. */
|
|
60
|
+
export function pushAssetDigest(stack, digest) {
|
|
61
|
+
return [...stack, digest];
|
|
62
|
+
}
|
|
63
|
+
/** Pop the top (most recent prior) digest off the LIFO rollback stack without
|
|
64
|
+
* mutating it. Returns the prior digest (null when the stack is empty) plus
|
|
65
|
+
* the remaining stack for the caller to commit (atomic card-swap fallback:
|
|
66
|
+
* a green swap pushes the incumbent digest; a rollback pops it). */
|
|
67
|
+
export function popAssetDigest(stack) {
|
|
68
|
+
if (stack.length === 0)
|
|
69
|
+
return { prior: null, rest: stack };
|
|
70
|
+
const rest = stack.slice(0, -1);
|
|
71
|
+
return { prior: stack[stack.length - 1], rest };
|
|
72
|
+
}
|
|
73
|
+
/** Atomically swap the committed manifest to a real trained asset's digest.
|
|
74
|
+
* Green: append the new entry (append-only — the incumbent stays in the
|
|
75
|
+
* ledger, never overwritten), commit to it, and record the INCUMBENT digest
|
|
76
|
+
* onto the LIFO stack for O(1) rollback (rollback restores the incumbent, so
|
|
77
|
+
* the stack must hold the pre-swap committed digest — pushing the swapped-in
|
|
78
|
+
* digest would point rollback back at the very asset being rolled back). Red
|
|
79
|
+
* (or no candidate digest): return the manifest and stack untouched — no
|
|
80
|
+
* swap, prior asset stays live. */
|
|
81
|
+
export function atomicSwap(manifest, entry, color, stack) {
|
|
82
|
+
if (color !== "green" || !entry.assetDigest) {
|
|
83
|
+
return { manifest, stack, swapped: false };
|
|
84
|
+
}
|
|
85
|
+
const incumbent = manifest.committed;
|
|
86
|
+
const next = appendAsset(manifest, entry);
|
|
87
|
+
return {
|
|
88
|
+
manifest: next,
|
|
89
|
+
stack: incumbent === null ? stack : pushAssetDigest(stack, incumbent),
|
|
90
|
+
swapped: true,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** Roll back to the previous shipped asset by SHA-256 (O(1) via the LIFO
|
|
94
|
+
* stack): restores the most recent prior digest in the manifest and pops that
|
|
95
|
+
* entry off the stack. Returns null when no prior digest exists or the prior
|
|
96
|
+
* digest is no longer in the append-only manifest (defensive: append-only
|
|
97
|
+
* means it should always be findable). Never leaves a partial state — the
|
|
98
|
+
* committed pointer flip and the stack pop happen together or not at all. */
|
|
99
|
+
export function assetRollback(manifest, stack) {
|
|
100
|
+
const { prior, rest } = popAssetDigest(stack);
|
|
101
|
+
if (prior === null)
|
|
102
|
+
return null;
|
|
103
|
+
const restored = rollbackTo(manifest, prior);
|
|
104
|
+
if (!restored)
|
|
105
|
+
return null;
|
|
106
|
+
return { manifest: restored, stack: rest, swapped: true };
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
45
109
|
// Pure decision rules.
|
|
46
110
|
// ---------------------------------------------------------------------------
|
|
47
111
|
/** Evaluate the promotion gate: promote only when all five heads pass AND the
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* encoder/heads-candidate.ts — ENC-0c head-candidate load seam (delegate impl).
|
|
3
|
+
*
|
|
4
|
+
* Loads/validates a `head-candidate-v1` five-head candidate staged under
|
|
5
|
+
* `~/.pi/mega-compact-encoder/candidates/<version>/` after training on the
|
|
6
|
+
* frozen ENC-0b bge-small trunk. Flag-off / absent / malformed / wrong dims /
|
|
7
|
+
* non-finite / digest / trunk mismatch each return null or {ok:false} — a bad
|
|
8
|
+
* candidate is NEVER force-loaded; the runtime keeps the ENC-0b survivor
|
|
9
|
+
* byte-identical. No `any` (PREVENT-011), local-only, structured logging.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { ENC_0C_ENABLED } from "../../config/vector-cortex.js";
|
|
15
|
+
import { ENCODER_HEAD_DIMS, ENCODER_HEAD_ORDER } from "./types.js";
|
|
16
|
+
export const HEAD_CANDIDATE_SCHEMA = "head-candidate-v1";
|
|
17
|
+
const MANIFEST_FILE = "manifest.json";
|
|
18
|
+
export const HEAD_CANDIDATE_FAIL = {
|
|
19
|
+
INVALID: "ENC0C_CANDIDATE_INVALID",
|
|
20
|
+
TRUNK_MISMATCH: "ENC0C_TRUNK_MISMATCH",
|
|
21
|
+
DIM_MISMATCH: "ENC0C_DIM_MISMATCH",
|
|
22
|
+
NON_FINITE: "ENC0C_NON_FINITE",
|
|
23
|
+
DIGEST_MISMATCH: "ENC0C_DIGEST_MISMATCH",
|
|
24
|
+
};
|
|
25
|
+
function sha256(buf) { return createHash("sha256").update(buf).digest("hex"); }
|
|
26
|
+
function finiteDim(values, dim) {
|
|
27
|
+
if (values.length !== dim)
|
|
28
|
+
return false;
|
|
29
|
+
for (const v of values)
|
|
30
|
+
if (!Number.isFinite(v))
|
|
31
|
+
return false;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
function parseManifest(m) {
|
|
35
|
+
if (!m || m["schema"] !== HEAD_CANDIDATE_SCHEMA)
|
|
36
|
+
return null;
|
|
37
|
+
const version = typeof m["version"] === "string" ? m["version"] : "";
|
|
38
|
+
const trunkDigest = typeof m["trunkDigest"] === "string" ? m["trunkDigest"] : "";
|
|
39
|
+
if (!version || !trunkDigest || !Array.isArray(m["heads"]))
|
|
40
|
+
return null;
|
|
41
|
+
const heads = [];
|
|
42
|
+
for (const item of m["heads"]) {
|
|
43
|
+
const rec = item;
|
|
44
|
+
if (!rec || typeof rec !== "object")
|
|
45
|
+
return null;
|
|
46
|
+
const name = rec["name"];
|
|
47
|
+
if (typeof name !== "string" || !ENCODER_HEAD_ORDER.includes(name))
|
|
48
|
+
return null;
|
|
49
|
+
heads.push({
|
|
50
|
+
name: name,
|
|
51
|
+
dim: Number(rec["dim"] ?? 0),
|
|
52
|
+
sha256: typeof rec["sha256"] === "string" ? rec["sha256"] : "",
|
|
53
|
+
bytes: Number(rec["bytes"] ?? 0),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return { version, trunkDigest, heads };
|
|
57
|
+
}
|
|
58
|
+
/** All 5 dims match, weights finite, digests hold. */
|
|
59
|
+
export function validateHeadCandidate(candidate) {
|
|
60
|
+
for (const h of ENCODER_HEAD_ORDER) {
|
|
61
|
+
const dim = candidate.dims[h];
|
|
62
|
+
if (dim !== ENCODER_HEAD_DIMS[h])
|
|
63
|
+
return { ok: false, code: HEAD_CANDIDATE_FAIL.DIM_MISMATCH };
|
|
64
|
+
const w = candidate.weights[h];
|
|
65
|
+
if (!w || !finiteDim(w, dim))
|
|
66
|
+
return { ok: false, code: HEAD_CANDIDATE_FAIL.NON_FINITE };
|
|
67
|
+
const buf = new Uint8Array(w.buffer, w.byteOffset, w.byteLength);
|
|
68
|
+
if (sha256(buf) !== candidate.digests[h])
|
|
69
|
+
return { ok: false, code: HEAD_CANDIDATE_FAIL.DIGEST_MISMATCH };
|
|
70
|
+
}
|
|
71
|
+
return { ok: true };
|
|
72
|
+
}
|
|
73
|
+
/** Load a candidate against the frozen trunk; null on any violation, never throws. */
|
|
74
|
+
export function loadHeadCandidate(candidateDir, manifest) {
|
|
75
|
+
if (!ENC_0C_ENABLED())
|
|
76
|
+
return null;
|
|
77
|
+
let raw;
|
|
78
|
+
try {
|
|
79
|
+
raw = readFileSync(join(candidateDir, MANIFEST_FILE), "utf8");
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
let parsed;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(raw);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const m = parseManifest(parsed);
|
|
92
|
+
if (!m || m.trunkDigest !== manifest.onnx.sha256)
|
|
93
|
+
return null;
|
|
94
|
+
const weights = {};
|
|
95
|
+
const digests = {};
|
|
96
|
+
const dims = {};
|
|
97
|
+
for (const rec of m.heads) {
|
|
98
|
+
const want = rec.dim * 4;
|
|
99
|
+
if (!Number.isInteger(want) || want <= 0 || rec.dim !== ENCODER_HEAD_DIMS[rec.name])
|
|
100
|
+
return null;
|
|
101
|
+
let bytes;
|
|
102
|
+
try {
|
|
103
|
+
bytes = readFileSync(join(candidateDir, `${rec.name}.bin`));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const f = new Float32Array(bytes.buffer, bytes.byteOffset, rec.dim);
|
|
109
|
+
const d = sha256(bytes);
|
|
110
|
+
if (bytes.length !== want || d !== rec.sha256)
|
|
111
|
+
return null;
|
|
112
|
+
weights[rec.name] = f.slice();
|
|
113
|
+
digests[rec.name] = d;
|
|
114
|
+
dims[rec.name] = rec.dim;
|
|
115
|
+
}
|
|
116
|
+
for (const h of ENCODER_HEAD_ORDER) {
|
|
117
|
+
if (weights[h] === undefined || digests[h] === undefined || dims[h] === undefined)
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const candidate = {
|
|
121
|
+
schema: HEAD_CANDIDATE_SCHEMA, version: m.version, trunkDigest: m.trunkDigest,
|
|
122
|
+
dims: dims,
|
|
123
|
+
weights: weights,
|
|
124
|
+
digests: digests,
|
|
125
|
+
};
|
|
126
|
+
return validateHeadCandidate(candidate).ok ? candidate : null;
|
|
127
|
+
}
|
|
@@ -198,3 +198,7 @@ export function projectHeadFromTrunk(head, trunk, table) {
|
|
|
198
198
|
return { head, dim, values: l2Normalize(out) };
|
|
199
199
|
}
|
|
200
200
|
export { ENCODER_HEAD_ORDER, ENCODER_HEAD_DIMS, ENCODER_HEAD_LOSS_SUM, ENCODER_SEED, NOOP_VC2B_REPORTER };
|
|
201
|
+
// ENC-0c five-head candidate seam (delegate-shell): the load/validate impl
|
|
202
|
+
// lives in heads-candidate.ts; these re-exports keep the public import path
|
|
203
|
+
// stable at heads.ts without growing this survivor file over the soft limit.
|
|
204
|
+
export { HEAD_CANDIDATE_SCHEMA, HEAD_CANDIDATE_FAIL, loadHeadCandidate, validateHeadCandidate, } from "./heads-candidate.js";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/encoder/promotion-emit.ts — ENC-0d promotion event writers.
|
|
3
|
+
*
|
|
4
|
+
* Writes the three real-asset promotion events to the monitoring `events.log`
|
|
5
|
+
* as structured append-only JSON lines via the existing `logBenchEvent` seam
|
|
6
|
+
* (`src/monitoring.ts`):
|
|
7
|
+
*
|
|
8
|
+
* - `vector_cortex_asset_promoted` (green digest-verified swap)
|
|
9
|
+
* - `vector_cortex_asset_demoted` (red qualification / digest failure)
|
|
10
|
+
* - `vector_cortex_asset_rollback_back` (restore of a prior stack digest)
|
|
11
|
+
*
|
|
12
|
+
* Non-fatal and log-and-swallow: a write failure never breaks the agent loop
|
|
13
|
+
* (best-effort store/write contract). Each line is `{ts, event, ...fields}`
|
|
14
|
+
* shaped so the dashboard live-stream tail and evidence tooling parse it
|
|
15
|
+
* identically to every other monitoring event. Events carry digests, colors
|
|
16
|
+
* and verdicts ONLY — never message content (EVAL-REDACT-002).
|
|
17
|
+
*
|
|
18
|
+
* The path defaults to the standard state-dir events.log (mirrors
|
|
19
|
+
* defaultEventsPath). Zero network, no `any` (PREVENT-PI-004 / PREVENT-011).
|
|
20
|
+
*/
|
|
21
|
+
import { defaultEventsPath, logBenchEvent } from "../../monitoring.js";
|
|
22
|
+
import { getStateDir } from "../../store.js";
|
|
23
|
+
/** Default events.log beside the state dir (mirrors bench.ts). */
|
|
24
|
+
export function promotionEventsPath(stateDir = getStateDir()) {
|
|
25
|
+
return defaultEventsPath(stateDir);
|
|
26
|
+
}
|
|
27
|
+
const EVENT_NAME = {
|
|
28
|
+
promoted: "vector_cortex_asset_promoted",
|
|
29
|
+
demoted: "vector_cortex_asset_demoted",
|
|
30
|
+
rollback_back: "vector_cortex_asset_rollback_back",
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Append one promotion event to events.log (best-effort, non-fatal). The line
|
|
34
|
+
* is `{ts, event, ...fields}` shaped, written append-only via the monitoring
|
|
35
|
+
* logBenchEvent seam. On any write failure the event is swallowed — the agent
|
|
36
|
+
* loop is never broken.
|
|
37
|
+
*/
|
|
38
|
+
export function appendPromotionEvent(path, kind, fields) {
|
|
39
|
+
logBenchEvent(path, EVENT_NAME[kind], { ...fields });
|
|
40
|
+
}
|
|
@@ -14,6 +14,15 @@
|
|
|
14
14
|
* an atomic digest swap (flip the committed pointer to the prior entry's
|
|
15
15
|
* digest in one step) — no partial state is possible because the swap is a
|
|
16
16
|
* single value assignment.
|
|
17
|
+
*
|
|
18
|
+
* ENC-0d extends the ledger to the real-asset promotion path: `PromotionV1`
|
|
19
|
+
* carries `{color, assetDigestStack}` — a promotion color from gate
|
|
20
|
+
* qualification ("green" atomically swaps to the trained asset, "red" keeps
|
|
21
|
+
* the prior asset live) and a LIFO of prior shipped digest for O(1)-by-sha256
|
|
22
|
+
* rollback. The on-disk manifest byte-swap itself (write-temp-then-rename,
|
|
23
|
+
* never in-place partial) is the promotion gate script's job; these helpers
|
|
24
|
+
* stay pure and return the restorable digest + the updated manifest/stack so
|
|
25
|
+
* the script can perform the swap in one atomic step.
|
|
17
26
|
*/
|
|
18
27
|
// ---------------------------------------------------------------------------
|
|
19
28
|
// PromotionV1 — the ledger row recording one nightly training + promotion run.
|
|
@@ -42,6 +51,61 @@ export function rollbackTo(manifest, digest) {
|
|
|
42
51
|
};
|
|
43
52
|
}
|
|
44
53
|
// ---------------------------------------------------------------------------
|
|
54
|
+
// ENC-0d LIFO rollback stack + atomic swap. Pure pointer arithmetic; the
|
|
55
|
+
// on-disk write-temp-then-rename of the swapped manifest bytes is the gate
|
|
56
|
+
// script's job so no partial state can ever be observed (PREVENT-PI-004).
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
/** Push a freshly-shipped digest onto the LIFO rollback stack (top = latest
|
|
59
|
+
* prior shipped digest). Non-mutating: returns a new array. */
|
|
60
|
+
export function pushAssetDigest(stack, digest) {
|
|
61
|
+
return [...stack, digest];
|
|
62
|
+
}
|
|
63
|
+
/** Pop the top (most recent prior) digest off the LIFO rollback stack without
|
|
64
|
+
* mutating it. Returns the prior digest (null when the stack is empty) plus
|
|
65
|
+
* the remaining stack for the caller to commit (atomic card-swap fallback:
|
|
66
|
+
* a green swap pushes the incumbent digest; a rollback pops it). */
|
|
67
|
+
export function popAssetDigest(stack) {
|
|
68
|
+
if (stack.length === 0)
|
|
69
|
+
return { prior: null, rest: stack };
|
|
70
|
+
const rest = stack.slice(0, -1);
|
|
71
|
+
return { prior: stack[stack.length - 1], rest };
|
|
72
|
+
}
|
|
73
|
+
/** Atomically swap the committed manifest to a real trained asset's digest.
|
|
74
|
+
* Green: append the new entry (append-only — the incumbent stays in the
|
|
75
|
+
* ledger, never overwritten), commit to it, and record the INCUMBENT digest
|
|
76
|
+
* onto the LIFO stack for O(1) rollback (rollback restores the incumbent, so
|
|
77
|
+
* the stack must hold the pre-swap committed digest — pushing the swapped-in
|
|
78
|
+
* digest would point rollback back at the very asset being rolled back). Red
|
|
79
|
+
* (or no candidate digest): return the manifest and stack untouched — no
|
|
80
|
+
* swap, prior asset stays live. */
|
|
81
|
+
export function atomicSwap(manifest, entry, color, stack) {
|
|
82
|
+
if (color !== "green" || !entry.assetDigest) {
|
|
83
|
+
return { manifest, stack, swapped: false };
|
|
84
|
+
}
|
|
85
|
+
const incumbent = manifest.committed;
|
|
86
|
+
const next = appendAsset(manifest, entry);
|
|
87
|
+
return {
|
|
88
|
+
manifest: next,
|
|
89
|
+
stack: incumbent === null ? stack : pushAssetDigest(stack, incumbent),
|
|
90
|
+
swapped: true,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** Roll back to the previous shipped asset by SHA-256 (O(1) via the LIFO
|
|
94
|
+
* stack): restores the most recent prior digest in the manifest and pops that
|
|
95
|
+
* entry off the stack. Returns null when no prior digest exists or the prior
|
|
96
|
+
* digest is no longer in the append-only manifest (defensive: append-only
|
|
97
|
+
* means it should always be findable). Never leaves a partial state — the
|
|
98
|
+
* committed pointer flip and the stack pop happen together or not at all. */
|
|
99
|
+
export function assetRollback(manifest, stack) {
|
|
100
|
+
const { prior, rest } = popAssetDigest(stack);
|
|
101
|
+
if (prior === null)
|
|
102
|
+
return null;
|
|
103
|
+
const restored = rollbackTo(manifest, prior);
|
|
104
|
+
if (!restored)
|
|
105
|
+
return null;
|
|
106
|
+
return { manifest: restored, stack: rest, swapped: true };
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
45
109
|
// Pure decision rules.
|
|
46
110
|
// ---------------------------------------------------------------------------
|
|
47
111
|
/** Evaluate the promotion gate: promote only when all five heads pass AND the
|
|
@@ -264,5 +264,17 @@ export const VECTOR_CORTEX_SETTINGS: SettingGroup = {
|
|
|
264
264
|
"ENC-0b real ONNX trunk fetch + gated inference: replaces the LCG placeholder with the real bge-small int8 model through an ONNX InferenceSession (onnxruntime-web WASM). OFF = LCG placeholder serves byte-identical predecessor output, no ONNX session built.",
|
|
265
265
|
true,
|
|
266
266
|
),
|
|
267
|
+
boolDirect(
|
|
268
|
+
"MEGACOMPACT_ENC_0C",
|
|
269
|
+
"ENC-0c Five-Head Supervision Transfer",
|
|
270
|
+
"ENC-0c five-head supervision transfer on the frozen bge-small trunk: when a developer-trained head candidate is staged under ~/.pi/mega-compact-encoder/candidates/, the five heads serve the trained weights. OFF = no candidate is loaded, the heads serve byte-identical ENC-0b survivor defaults.",
|
|
271
|
+
true,
|
|
272
|
+
),
|
|
273
|
+
boolDirect(
|
|
274
|
+
"MEGACOMPACT_ENC_0D",
|
|
275
|
+
"ENC-0d Real-Asset Promotion Gate",
|
|
276
|
+
"ENC-0d promotion gate over real trained assets: accepts a {color} real candidate manifest (the ENC-0c trained head weights + the ENC-0b trunk), digest-verifies every staged byte, and atomically swaps the shipped manifest to a green candidate with rollback-to-previous on qualification failure. OFF = accepts no candidate, swaps nothing, emits nothing, shipped manifest stays at the ENC-0c survivor.",
|
|
277
|
+
true,
|
|
278
|
+
),
|
|
267
279
|
],
|
|
268
280
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mega-compact",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.46",
|
|
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,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config/vector-cortex-enc0c.ts — ENC-0c five-head supervision transfer 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-enc0a.ts / enc0b.ts and
|
|
6
|
+
* the VC8C/VC9A-D/ML5A-E/DEDUP_ATTR siblings were. vector-cortex.ts re-exports
|
|
7
|
+
* the flag below and root src/config.ts re-exports it, so no consumer import
|
|
8
|
+
* path changes.
|
|
9
|
+
*
|
|
10
|
+
* ENC-0c trains the five real heads onto the frozen ENC-0b bge-small trunk via
|
|
11
|
+
* supervision transfer, and stages a qualified candidate under
|
|
12
|
+
* ~/.pi/mega-compact-encoder/candidates/ (only when the developer trains it —
|
|
13
|
+
* the extension never stages one by itself). When the flag is ON and a
|
|
14
|
+
* qualified candidate exists, `loadHeadCandidate` (encoder/heads.ts seam) serves
|
|
15
|
+
* the trained head weights; when the flag is OFF (MEGACOMPACT_ENC_0C=0) or no
|
|
16
|
+
* candidate is staged, the heads keep serving the ENC-0b survivor exactly —
|
|
17
|
+
* byte-identical, no weight change. The flag gates ONLY the candidate-load seam;
|
|
18
|
+
* the survivor path is untouched.
|
|
19
|
+
*
|
|
20
|
+
* The split is purely mechanical: ENC_0C_ENABLED follows ENC_0B_ENABLED in name,
|
|
21
|
+
* semantics, and default, and vector-cortex.ts re-exports it so every existing
|
|
22
|
+
* `from "./config/vector-cortex.js"` import keeps resolving unchanged.
|
|
23
|
+
*
|
|
24
|
+
* Pi-agnostic, dependency-free (PREVENT-PI-004 / PREVENT-011).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { sprintFlag } from "./vector-cortex-flag.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* ENC-0c — five-head supervision transfer on the frozen bge-small trunk.
|
|
31
|
+
* Default ON. `MEGACOMPACT_ENC_0C=0` disables and is byte-identical to the
|
|
32
|
+
* predecessor (ENC-0b): no head candidate is loaded and the heads keep serving
|
|
33
|
+
* the ENC-0b survivor defaults exactly as before. This flag MUST also be a
|
|
34
|
+
* dashboard SETTINGS toggle (visible in config UI, never in EXCLUDED_SETTINGS),
|
|
35
|
+
* mirroring ENC_0A and ENC_0B.
|
|
36
|
+
*/
|
|
37
|
+
export const ENC_0C_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_ENC_0C");
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config/vector-cortex-enc0d.ts — ENC-0d promotion gate over real trained assets.
|
|
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-enc0a.ts / enc0b.ts /
|
|
6
|
+
* enc0c.ts and the VC8C/VC9A-D/ML5A-E/DEDUP_ATTR siblings were. vector-cortex.ts
|
|
7
|
+
* re-exports the flag below and root src/config.ts re-exports it, so no
|
|
8
|
+
* consumer import path changes.
|
|
9
|
+
*
|
|
10
|
+
* ENC-0d turns the ML5-E promotion gate into the real-asset promotion path:
|
|
11
|
+
* it accepts a `{color}` real candidate manifest (the ENC-0c trained head
|
|
12
|
+
* weights + the ENC-0b trunk, staged under
|
|
13
|
+
* ~/.pi/mega-compact-encoder/candidates/), digest-verifies every staged byte
|
|
14
|
+
* before any swap, and performs an atomic asset swap with rollback-to-previous
|
|
15
|
+
* on qualification failure. The extension itself never stages candidate assets —
|
|
16
|
+
* only the operator trains them. When the flag is ON and a digest-verified
|
|
17
|
+
* green candidate exists, an atomic swap is performed and the runtime flips into
|
|
18
|
+
* qualified mode A; a red qualification (or any verification failure) keeps the
|
|
19
|
+
* prior asset live and emits a demotion.
|
|
20
|
+
*
|
|
21
|
+
* `MEGACOMPACT_ENC_0D=0` accepts no candidate, swaps nothing, emits nothing, and
|
|
22
|
+
* the shipped manifest stays at the ENC-0c survivor — byte-identical predecessor.
|
|
23
|
+
*
|
|
24
|
+
* The split is purely mechanical: ENC_0D_ENABLED follows ENC_0C_ENABLED in name,
|
|
25
|
+
* semantics, and default, and vector-cortex.ts re-exports it so every existing
|
|
26
|
+
* `from "./config/vector-cortex.js"` import keeps resolving unchanged.
|
|
27
|
+
*
|
|
28
|
+
* Pi-agnostic, dependency-free (PREVENT-PI-004 / PREVENT-011).
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { sprintFlag } from "./vector-cortex-flag.js";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* ENC-0d — atomic real-asset promotion over trained candidates. Default ON.
|
|
35
|
+
* `MEGACOMPACT_ENC_0D=0` disables and is byte-identical to the predecessor
|
|
36
|
+
* (ENC-0c): no candidate is accepted, no swap is performed, no promote/
|
|
37
|
+
* demote/rollback events are emitted, and the shipped manifest stays at the
|
|
38
|
+
* ENC-0c survivor. This flag MUST also be a dashboard SETTINGS toggle (visible
|
|
39
|
+
* in config UI, never in EXCLUDED_SETTINGS), mirroring ENC_0A/ENC_0B/ENC_0C.
|
|
40
|
+
*/
|
|
41
|
+
export const ENC_0D_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_ENC_0D");
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Breaker/triad constants (TRIAD_RESILIENCE.md) live here; pi-agnostic, dep-free.
|
|
9
9
|
*/
|
|
10
|
-
|
|
11
10
|
import { sprintFlag } from "./vector-cortex-flag.js";
|
|
12
11
|
|
|
13
12
|
// VC0/VC1/VC2 foundation-phase flags in vector-cortex-early.ts; re-exported
|
|
@@ -281,7 +280,8 @@ export { ML5E_ENABLED } from "./vector-cortex-ml5e.js";
|
|
|
281
280
|
export { DEDUP_ATTR_ENABLED } from "./vector-cortex-dedup-attr.js";
|
|
282
281
|
export { ENC_0A_ENABLED } from "./vector-cortex-enc0a.js";
|
|
283
282
|
export { ENC_0B_ENABLED } from "./vector-cortex-enc0b.js";
|
|
284
|
-
|
|
283
|
+
export { ENC_0C_ENABLED } from "./vector-cortex-enc0c.js";
|
|
284
|
+
export { ENC_0D_ENABLED } from "./vector-cortex-enc0d.js";
|
|
285
285
|
// Breaker constants (TRIAD_RESILIENCE.md §breaker) extracted to vector-cortex-breakers.ts.
|
|
286
286
|
export {
|
|
287
287
|
BREAKER_WINDOW_MS,
|
package/src/config.ts
CHANGED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* encoder/heads-candidate.ts — ENC-0c head-candidate load seam (delegate impl).
|
|
3
|
+
*
|
|
4
|
+
* Loads/validates a `head-candidate-v1` five-head candidate staged under
|
|
5
|
+
* `~/.pi/mega-compact-encoder/candidates/<version>/` after training on the
|
|
6
|
+
* frozen ENC-0b bge-small trunk. Flag-off / absent / malformed / wrong dims /
|
|
7
|
+
* non-finite / digest / trunk mismatch each return null or {ok:false} — a bad
|
|
8
|
+
* candidate is NEVER force-loaded; the runtime keeps the ENC-0b survivor
|
|
9
|
+
* byte-identical. No `any` (PREVENT-011), local-only, structured logging.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
import { ENC_0C_ENABLED } from "../../config/vector-cortex.js";
|
|
17
|
+
import { ENCODER_HEAD_DIMS, ENCODER_HEAD_ORDER } from "./types.js";
|
|
18
|
+
import type { EncoderHeadName, ModelManifestV1 } from "./types.js";
|
|
19
|
+
|
|
20
|
+
export const HEAD_CANDIDATE_SCHEMA = "head-candidate-v1" as const;
|
|
21
|
+
const MANIFEST_FILE = "manifest.json";
|
|
22
|
+
|
|
23
|
+
export interface HeadCandidateHeadDigest {
|
|
24
|
+
readonly name: EncoderHeadName;
|
|
25
|
+
readonly dim: number;
|
|
26
|
+
readonly sha256: string;
|
|
27
|
+
readonly bytes: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** On-disk candidate contract; `trunkDigest` pins the frozen trunk. */
|
|
31
|
+
export interface HeadCandidateManifest {
|
|
32
|
+
readonly schema: typeof HEAD_CANDIDATE_SCHEMA;
|
|
33
|
+
readonly version: string;
|
|
34
|
+
readonly trunkDigest: string;
|
|
35
|
+
readonly heads: readonly HeadCandidateHeadDigest[];
|
|
36
|
+
readonly totalBytes: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Loaded candidate: parsed Float32Array weights per head. */
|
|
40
|
+
export interface HeadCandidate {
|
|
41
|
+
readonly schema: typeof HEAD_CANDIDATE_SCHEMA;
|
|
42
|
+
readonly version: string;
|
|
43
|
+
readonly trunkDigest: string;
|
|
44
|
+
readonly dims: Readonly<Record<EncoderHeadName, number>>;
|
|
45
|
+
readonly weights: Readonly<Record<EncoderHeadName, Float32Array>>;
|
|
46
|
+
readonly digests: Readonly<Record<EncoderHeadName, string>>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type HeadCandidateValidation = { readonly ok: true } | { readonly ok: false; readonly code: string };
|
|
50
|
+
|
|
51
|
+
export const HEAD_CANDIDATE_FAIL = {
|
|
52
|
+
INVALID: "ENC0C_CANDIDATE_INVALID",
|
|
53
|
+
TRUNK_MISMATCH: "ENC0C_TRUNK_MISMATCH",
|
|
54
|
+
DIM_MISMATCH: "ENC0C_DIM_MISMATCH",
|
|
55
|
+
NON_FINITE: "ENC0C_NON_FINITE",
|
|
56
|
+
DIGEST_MISMATCH: "ENC0C_DIGEST_MISMATCH",
|
|
57
|
+
} as const;
|
|
58
|
+
|
|
59
|
+
function sha256(buf: Uint8Array): string { return createHash("sha256").update(buf).digest("hex"); }
|
|
60
|
+
function finiteDim(values: Float32Array, dim: number): boolean {
|
|
61
|
+
if (values.length !== dim) return false;
|
|
62
|
+
for (const v of values) if (!Number.isFinite(v)) return false;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Parse the candidate manifest object, or null if invalid. */
|
|
67
|
+
type ParsedManifest = { version: string; trunkDigest: string; heads: HeadCandidateHeadDigest[] };
|
|
68
|
+
function parseManifest(m: Record<string, unknown> | null): ParsedManifest | null {
|
|
69
|
+
if (!m || m["schema"] !== HEAD_CANDIDATE_SCHEMA) return null;
|
|
70
|
+
const version = typeof m["version"] === "string" ? m["version"] : "";
|
|
71
|
+
const trunkDigest = typeof m["trunkDigest"] === "string" ? m["trunkDigest"] : "";
|
|
72
|
+
if (!version || !trunkDigest || !Array.isArray(m["heads"])) return null;
|
|
73
|
+
const heads: HeadCandidateHeadDigest[] = [];
|
|
74
|
+
for (const item of m["heads"] as unknown[]) {
|
|
75
|
+
const rec = item as Record<string, unknown> | null;
|
|
76
|
+
if (!rec || typeof rec !== "object") return null;
|
|
77
|
+
const name = rec["name"];
|
|
78
|
+
if (typeof name !== "string" || !ENCODER_HEAD_ORDER.includes(name as EncoderHeadName)) return null;
|
|
79
|
+
heads.push({
|
|
80
|
+
name: name as EncoderHeadName,
|
|
81
|
+
dim: Number(rec["dim"] ?? 0),
|
|
82
|
+
sha256: typeof rec["sha256"] === "string" ? rec["sha256"] : "",
|
|
83
|
+
bytes: Number(rec["bytes"] ?? 0),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return { version, trunkDigest, heads };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** All 5 dims match, weights finite, digests hold. */
|
|
90
|
+
export function validateHeadCandidate(candidate: HeadCandidate): HeadCandidateValidation {
|
|
91
|
+
for (const h of ENCODER_HEAD_ORDER) {
|
|
92
|
+
const dim = candidate.dims[h];
|
|
93
|
+
if (dim !== ENCODER_HEAD_DIMS[h]) return { ok: false, code: HEAD_CANDIDATE_FAIL.DIM_MISMATCH };
|
|
94
|
+
const w = candidate.weights[h];
|
|
95
|
+
if (!w || !finiteDim(w, dim)) return { ok: false, code: HEAD_CANDIDATE_FAIL.NON_FINITE };
|
|
96
|
+
const buf = new Uint8Array(w.buffer, w.byteOffset, w.byteLength);
|
|
97
|
+
if (sha256(buf) !== candidate.digests[h]) return { ok: false, code: HEAD_CANDIDATE_FAIL.DIGEST_MISMATCH };
|
|
98
|
+
}
|
|
99
|
+
return { ok: true };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Load a candidate against the frozen trunk; null on any violation, never throws. */
|
|
103
|
+
export function loadHeadCandidate(candidateDir: string, manifest: ModelManifestV1): HeadCandidate | null {
|
|
104
|
+
if (!ENC_0C_ENABLED()) return null;
|
|
105
|
+
let raw: string;
|
|
106
|
+
try {
|
|
107
|
+
raw = readFileSync(join(candidateDir, MANIFEST_FILE), "utf8");
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
let parsed: unknown;
|
|
112
|
+
try {
|
|
113
|
+
parsed = JSON.parse(raw);
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const m = parseManifest(parsed as Record<string, unknown> | null);
|
|
118
|
+
if (!m || m.trunkDigest !== manifest.onnx.sha256) return null;
|
|
119
|
+
const weights: Partial<Record<EncoderHeadName, Float32Array>> = {};
|
|
120
|
+
const digests: Partial<Record<EncoderHeadName, string>> = {};
|
|
121
|
+
const dims: Partial<Record<EncoderHeadName, number>> = {};
|
|
122
|
+
for (const rec of m.heads) {
|
|
123
|
+
const want = rec.dim * 4;
|
|
124
|
+
if (!Number.isInteger(want) || want <= 0 || rec.dim !== ENCODER_HEAD_DIMS[rec.name]) return null;
|
|
125
|
+
let bytes: Buffer;
|
|
126
|
+
try {
|
|
127
|
+
bytes = readFileSync(join(candidateDir, `${rec.name}.bin`));
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const f = new Float32Array(bytes.buffer, bytes.byteOffset, rec.dim);
|
|
132
|
+
const d = sha256(bytes);
|
|
133
|
+
if (bytes.length !== want || d !== rec.sha256) return null;
|
|
134
|
+
weights[rec.name] = f.slice();
|
|
135
|
+
digests[rec.name] = d;
|
|
136
|
+
dims[rec.name] = rec.dim;
|
|
137
|
+
}
|
|
138
|
+
for (const h of ENCODER_HEAD_ORDER) {
|
|
139
|
+
if (weights[h] === undefined || digests[h] === undefined || dims[h] === undefined) return null;
|
|
140
|
+
}
|
|
141
|
+
const candidate: HeadCandidate = {
|
|
142
|
+
schema: HEAD_CANDIDATE_SCHEMA, version: m.version, trunkDigest: m.trunkDigest,
|
|
143
|
+
dims: dims as Record<EncoderHeadName, number>,
|
|
144
|
+
weights: weights as Record<EncoderHeadName, Float32Array>,
|
|
145
|
+
digests: digests as Record<EncoderHeadName, string>,
|
|
146
|
+
};
|
|
147
|
+
return validateHeadCandidate(candidate).ok ? candidate : null;
|
|
148
|
+
}
|
|
@@ -246,3 +246,17 @@ export function projectHeadFromTrunk(
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
export { ENCODER_HEAD_ORDER, ENCODER_HEAD_DIMS, ENCODER_HEAD_LOSS_SUM, ENCODER_SEED, NOOP_VC2B_REPORTER };
|
|
249
|
+
|
|
250
|
+
// ENC-0c five-head candidate seam (delegate-shell): the load/validate impl
|
|
251
|
+
// lives in heads-candidate.ts; these re-exports keep the public import path
|
|
252
|
+
// stable at heads.ts without growing this survivor file over the soft limit.
|
|
253
|
+
export {
|
|
254
|
+
HEAD_CANDIDATE_SCHEMA,
|
|
255
|
+
HEAD_CANDIDATE_FAIL,
|
|
256
|
+
loadHeadCandidate,
|
|
257
|
+
validateHeadCandidate,
|
|
258
|
+
type HeadCandidate,
|
|
259
|
+
type HeadCandidateManifest,
|
|
260
|
+
type HeadCandidateHeadDigest,
|
|
261
|
+
type HeadCandidateValidation,
|
|
262
|
+
} from "./heads-candidate.js";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/encoder/promotion-emit.ts — ENC-0d promotion event writers.
|
|
3
|
+
*
|
|
4
|
+
* Writes the three real-asset promotion events to the monitoring `events.log`
|
|
5
|
+
* as structured append-only JSON lines via the existing `logBenchEvent` seam
|
|
6
|
+
* (`src/monitoring.ts`):
|
|
7
|
+
*
|
|
8
|
+
* - `vector_cortex_asset_promoted` (green digest-verified swap)
|
|
9
|
+
* - `vector_cortex_asset_demoted` (red qualification / digest failure)
|
|
10
|
+
* - `vector_cortex_asset_rollback_back` (restore of a prior stack digest)
|
|
11
|
+
*
|
|
12
|
+
* Non-fatal and log-and-swallow: a write failure never breaks the agent loop
|
|
13
|
+
* (best-effort store/write contract). Each line is `{ts, event, ...fields}`
|
|
14
|
+
* shaped so the dashboard live-stream tail and evidence tooling parse it
|
|
15
|
+
* identically to every other monitoring event. Events carry digests, colors
|
|
16
|
+
* and verdicts ONLY — never message content (EVAL-REDACT-002).
|
|
17
|
+
*
|
|
18
|
+
* The path defaults to the standard state-dir events.log (mirrors
|
|
19
|
+
* defaultEventsPath). Zero network, no `any` (PREVENT-PI-004 / PREVENT-011).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { defaultEventsPath, logBenchEvent } from "../../monitoring.js";
|
|
23
|
+
import { getStateDir } from "../../store.js";
|
|
24
|
+
|
|
25
|
+
/** Default events.log beside the state dir (mirrors bench.ts). */
|
|
26
|
+
export function promotionEventsPath(stateDir: string = getStateDir()): string {
|
|
27
|
+
return defaultEventsPath(stateDir);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The promotion event kinds this emitter can write. */
|
|
31
|
+
export type PromotionEventKind =
|
|
32
|
+
| "promoted"
|
|
33
|
+
| "demoted"
|
|
34
|
+
| "rollback_back";
|
|
35
|
+
|
|
36
|
+
const EVENT_NAME: Record<PromotionEventKind, string> = {
|
|
37
|
+
promoted: "vector_cortex_asset_promoted",
|
|
38
|
+
demoted: "vector_cortex_asset_demoted",
|
|
39
|
+
rollback_back: "vector_cortex_asset_rollback_back",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Fields carried by a promotion event: digests, colors and verdicts only.
|
|
43
|
+
* Never any payload or message content (EVAL-REDACT-002). */
|
|
44
|
+
export interface PromotionEventFields {
|
|
45
|
+
readonly color: "green" | "red";
|
|
46
|
+
readonly assetDigest: string | null;
|
|
47
|
+
readonly priorAssetDigest: string | null;
|
|
48
|
+
readonly verdict: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Append one promotion event to events.log (best-effort, non-fatal). The line
|
|
53
|
+
* is `{ts, event, ...fields}` shaped, written append-only via the monitoring
|
|
54
|
+
* logBenchEvent seam. On any write failure the event is swallowed — the agent
|
|
55
|
+
* loop is never broken.
|
|
56
|
+
*/
|
|
57
|
+
export function appendPromotionEvent(
|
|
58
|
+
path: string,
|
|
59
|
+
kind: PromotionEventKind,
|
|
60
|
+
fields: PromotionEventFields,
|
|
61
|
+
): void {
|
|
62
|
+
logBenchEvent(path, EVENT_NAME[kind], { ...fields });
|
|
63
|
+
}
|
|
@@ -14,6 +14,15 @@
|
|
|
14
14
|
* an atomic digest swap (flip the committed pointer to the prior entry's
|
|
15
15
|
* digest in one step) — no partial state is possible because the swap is a
|
|
16
16
|
* single value assignment.
|
|
17
|
+
*
|
|
18
|
+
* ENC-0d extends the ledger to the real-asset promotion path: `PromotionV1`
|
|
19
|
+
* carries `{color, assetDigestStack}` — a promotion color from gate
|
|
20
|
+
* qualification ("green" atomically swaps to the trained asset, "red" keeps
|
|
21
|
+
* the prior asset live) and a LIFO of prior shipped digest for O(1)-by-sha256
|
|
22
|
+
* rollback. The on-disk manifest byte-swap itself (write-temp-then-rename,
|
|
23
|
+
* never in-place partial) is the promotion gate script's job; these helpers
|
|
24
|
+
* stay pure and return the restorable digest + the updated manifest/stack so
|
|
25
|
+
* the script can perform the swap in one atomic step.
|
|
17
26
|
*/
|
|
18
27
|
|
|
19
28
|
// ---------------------------------------------------------------------------
|
|
@@ -54,6 +63,13 @@ export interface PromotionV1 {
|
|
|
54
63
|
readonly verdict: "promoted" | "demoted" | "noop";
|
|
55
64
|
/** Present only when verdict is "demoted": the demotion event name. */
|
|
56
65
|
readonly demotedEvent: "demoted_new_asset" | null;
|
|
66
|
+
/** ENC-0d promotion color from gate qualification. "green" atomically swaps
|
|
67
|
+
* the shipped manifest to the trained asset; "red" keeps the prior asset
|
|
68
|
+
* live and emits a demotion. */
|
|
69
|
+
readonly color: "green" | "red";
|
|
70
|
+
/** ENC-0d LIFO of prior shipped asset digests for O(1)-by-sha256 rollback.
|
|
71
|
+
* The most recent prior digest (top of the stack) is the rollback target. */
|
|
72
|
+
readonly assetDigestStack: readonly string[];
|
|
57
73
|
}
|
|
58
74
|
|
|
59
75
|
// ---------------------------------------------------------------------------
|
|
@@ -104,6 +120,85 @@ export function rollbackTo(
|
|
|
104
120
|
};
|
|
105
121
|
}
|
|
106
122
|
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// ENC-0d LIFO rollback stack + atomic swap. Pure pointer arithmetic; the
|
|
125
|
+
// on-disk write-temp-then-rename of the swapped manifest bytes is the gate
|
|
126
|
+
// script's job so no partial state can ever be observed (PREVENT-PI-004).
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
/** Push a freshly-shipped digest onto the LIFO rollback stack (top = latest
|
|
130
|
+
* prior shipped digest). Non-mutating: returns a new array. */
|
|
131
|
+
export function pushAssetDigest(
|
|
132
|
+
stack: readonly string[],
|
|
133
|
+
digest: string,
|
|
134
|
+
): readonly string[] {
|
|
135
|
+
return [...stack, digest];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Pop the top (most recent prior) digest off the LIFO rollback stack without
|
|
139
|
+
* mutating it. Returns the prior digest (null when the stack is empty) plus
|
|
140
|
+
* the remaining stack for the caller to commit (atomic card-swap fallback:
|
|
141
|
+
* a green swap pushes the incumbent digest; a rollback pops it). */
|
|
142
|
+
export function popAssetDigest(
|
|
143
|
+
stack: readonly string[],
|
|
144
|
+
): { prior: string | null; rest: readonly string[] } {
|
|
145
|
+
if (stack.length === 0) return { prior: null, rest: stack };
|
|
146
|
+
const rest = stack.slice(0, -1);
|
|
147
|
+
return { prior: stack[stack.length - 1]!, rest };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Outcome of an atomic promotion swap (pure): the (possibly restored)
|
|
151
|
+
* manifest, the updated rollback stack, and whether a digest swap occurred. */
|
|
152
|
+
export interface AtomicSwapOutcome {
|
|
153
|
+
readonly manifest: AssetManifest;
|
|
154
|
+
readonly stack: readonly string[];
|
|
155
|
+
/** True when the committed pointer changed (green swap or rollback). */
|
|
156
|
+
readonly swapped: boolean;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Atomically swap the committed manifest to a real trained asset's digest.
|
|
160
|
+
* Green: append the new entry (append-only — the incumbent stays in the
|
|
161
|
+
* ledger, never overwritten), commit to it, and record the INCUMBENT digest
|
|
162
|
+
* onto the LIFO stack for O(1) rollback (rollback restores the incumbent, so
|
|
163
|
+
* the stack must hold the pre-swap committed digest — pushing the swapped-in
|
|
164
|
+
* digest would point rollback back at the very asset being rolled back). Red
|
|
165
|
+
* (or no candidate digest): return the manifest and stack untouched — no
|
|
166
|
+
* swap, prior asset stays live. */
|
|
167
|
+
export function atomicSwap(
|
|
168
|
+
manifest: AssetManifest,
|
|
169
|
+
entry: AssetManifestEntry,
|
|
170
|
+
color: "green" | "red",
|
|
171
|
+
stack: readonly string[],
|
|
172
|
+
): AtomicSwapOutcome {
|
|
173
|
+
if (color !== "green" || !entry.assetDigest) {
|
|
174
|
+
return { manifest, stack, swapped: false };
|
|
175
|
+
}
|
|
176
|
+
const incumbent = manifest.committed;
|
|
177
|
+
const next = appendAsset(manifest, entry);
|
|
178
|
+
return {
|
|
179
|
+
manifest: next,
|
|
180
|
+
stack: incumbent === null ? stack : pushAssetDigest(stack, incumbent),
|
|
181
|
+
swapped: true,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Roll back to the previous shipped asset by SHA-256 (O(1) via the LIFO
|
|
186
|
+
* stack): restores the most recent prior digest in the manifest and pops that
|
|
187
|
+
* entry off the stack. Returns null when no prior digest exists or the prior
|
|
188
|
+
* digest is no longer in the append-only manifest (defensive: append-only
|
|
189
|
+
* means it should always be findable). Never leaves a partial state — the
|
|
190
|
+
* committed pointer flip and the stack pop happen together or not at all. */
|
|
191
|
+
export function assetRollback(
|
|
192
|
+
manifest: AssetManifest,
|
|
193
|
+
stack: readonly string[],
|
|
194
|
+
): AtomicSwapOutcome | null {
|
|
195
|
+
const { prior, rest } = popAssetDigest(stack);
|
|
196
|
+
if (prior === null) return null;
|
|
197
|
+
const restored = rollbackTo(manifest, prior);
|
|
198
|
+
if (!restored) return null;
|
|
199
|
+
return { manifest: restored, stack: rest, swapped: true };
|
|
200
|
+
}
|
|
201
|
+
|
|
107
202
|
// ---------------------------------------------------------------------------
|
|
108
203
|
// Pure decision rules.
|
|
109
204
|
// ---------------------------------------------------------------------------
|