pi-mega-compact 0.20.48 → 0.20.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/config/vector-cortex-enc0g.js +37 -0
  2. package/dist/config/vector-cortex-enc1a.js +39 -0
  3. package/dist/config/vector-cortex.js +2 -0
  4. package/dist/config.js +1 -1
  5. package/dist/extensions/dashboard-server/qualification-record.js +71 -0
  6. package/dist/extensions/dashboard-server/routes-rag-settings-vector-cortex.js +2 -0
  7. package/dist/extensions/dashboard-server/routes-setup-cortex-actions.js +49 -3
  8. package/dist/extensions/dashboard-server/routes-setup-cortex.js +65 -16
  9. package/dist/extensions/dashboard-server/routes-setup-enc1a.js +157 -0
  10. package/dist/extensions/dashboard-server/routes-setup.js +21 -0
  11. package/dist/extensions/dashboard-server/setup-cortex-actions.js +21 -4
  12. package/dist/extensions/dashboard-server/setup-cortex-blockers.js +3 -69
  13. package/dist/src/config/vector-cortex-enc0g.js +37 -0
  14. package/dist/src/config/vector-cortex-enc1a.js +39 -0
  15. package/dist/src/config/vector-cortex.js +2 -0
  16. package/dist/src/config.js +1 -1
  17. package/dist/src/vector-cortex/setup-cortex-blockers-compute.js +160 -0
  18. package/dist/vector-cortex/setup-cortex-blockers-compute.js +160 -0
  19. package/extensions/dashboard-client/dist/assets/{AreaChart-COnj8FHi.js → AreaChart-CeVnaV-0.js} +2 -2
  20. package/extensions/dashboard-client/dist/assets/{AreaChart-COnj8FHi.js.map → AreaChart-CeVnaV-0.js.map} +1 -1
  21. package/extensions/dashboard-client/dist/assets/{BarChart-BYqt8c5-.js → BarChart-DoNd35sw.js} +2 -2
  22. package/extensions/dashboard-client/dist/assets/{BarChart-BYqt8c5-.js.map → BarChart-DoNd35sw.js.map} +1 -1
  23. package/extensions/dashboard-client/dist/assets/{CacheTab-PLRjMrZV.js → CacheTab-BROshdFf.js} +2 -2
  24. package/extensions/dashboard-client/dist/assets/{CacheTab-PLRjMrZV.js.map → CacheTab-BROshdFf.js.map} +1 -1
  25. package/extensions/dashboard-client/dist/assets/{EventsTab-DXKSft6w.js → EventsTab-DAD6NeBR.js} +2 -2
  26. package/extensions/dashboard-client/dist/assets/{EventsTab-DXKSft6w.js.map → EventsTab-DAD6NeBR.js.map} +1 -1
  27. package/extensions/dashboard-client/dist/assets/{HealthTab-BgVIsrde.js → HealthTab-BBVMM54F.js} +2 -2
  28. package/extensions/dashboard-client/dist/assets/{HealthTab-BgVIsrde.js.map → HealthTab-BBVMM54F.js.map} +1 -1
  29. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DNgFC8KZ.js → MaintenanceTab-f9VhlFco.js} +2 -2
  30. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DNgFC8KZ.js.map → MaintenanceTab-f9VhlFco.js.map} +1 -1
  31. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-B6yY47sz.js → MemoryMapTab-BaxfyzN2.js} +2 -2
  32. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-B6yY47sz.js.map → MemoryMapTab-BaxfyzN2.js.map} +1 -1
  33. package/extensions/dashboard-client/dist/assets/{MetricsTab-BrhJ6rkx.js → MetricsTab-DkUvoeS_.js} +2 -2
  34. package/extensions/dashboard-client/dist/assets/{MetricsTab-BrhJ6rkx.js.map → MetricsTab-DkUvoeS_.js.map} +1 -1
  35. package/extensions/dashboard-client/dist/assets/{OverviewTab-RFHU9L4x.js → OverviewTab-BCpK1vgt.js} +2 -2
  36. package/extensions/dashboard-client/dist/assets/{OverviewTab-RFHU9L4x.js.map → OverviewTab-BCpK1vgt.js.map} +1 -1
  37. package/extensions/dashboard-client/dist/assets/{ReposTab-UFIWKkJM.js → ReposTab-cJ5neYIG.js} +2 -2
  38. package/extensions/dashboard-client/dist/assets/{ReposTab-UFIWKkJM.js.map → ReposTab-cJ5neYIG.js.map} +1 -1
  39. package/extensions/dashboard-client/dist/assets/{SessionsTab-BHFYsmK8.js → SessionsTab-8DyQv1Ry.js} +2 -2
  40. package/extensions/dashboard-client/dist/assets/{SessionsTab-BHFYsmK8.js.map → SessionsTab-8DyQv1Ry.js.map} +1 -1
  41. package/extensions/dashboard-client/dist/assets/SetupTab-BQGPf9CI.js +3 -0
  42. package/extensions/dashboard-client/dist/assets/SetupTab-BQGPf9CI.js.map +1 -0
  43. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-aPG5Knqn.js → TimeSavedCard-8N7Vvlch.js} +2 -2
  44. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-aPG5Knqn.js.map → TimeSavedCard-8N7Vvlch.js.map} +1 -1
  45. package/extensions/dashboard-client/dist/assets/{TurnsTab-fGm9n-Tk.js → TurnsTab-CCijkQoH.js} +2 -2
  46. package/extensions/dashboard-client/dist/assets/{TurnsTab-fGm9n-Tk.js.map → TurnsTab-CCijkQoH.js.map} +1 -1
  47. package/extensions/dashboard-client/dist/assets/{VcStatusBadge-BtmSNH8X.js → VcStatusBadge-D9a7By8x.js} +2 -2
  48. package/extensions/dashboard-client/dist/assets/{VcStatusBadge-BtmSNH8X.js.map → VcStatusBadge-D9a7By8x.js.map} +1 -1
  49. package/extensions/dashboard-client/dist/assets/{VectorCortexTab-meALMMOZ.js → VectorCortexTab-pviD6CCy.js} +2 -2
  50. package/extensions/dashboard-client/dist/assets/{VectorCortexTab-meALMMOZ.js.map → VectorCortexTab-pviD6CCy.js.map} +1 -1
  51. package/extensions/dashboard-client/dist/assets/{WikiTab-0TK6q6g1.js → WikiTab-DLyicguz.js} +2 -2
  52. package/extensions/dashboard-client/dist/assets/{WikiTab-0TK6q6g1.js.map → WikiTab-DLyicguz.js.map} +1 -1
  53. package/extensions/dashboard-client/dist/assets/{button-CKEnMZ98.js → button-D8nKit8Y.js} +2 -2
  54. package/extensions/dashboard-client/dist/assets/{button-CKEnMZ98.js.map → button-D8nKit8Y.js.map} +1 -1
  55. package/extensions/dashboard-client/dist/assets/{card-BVVhm3sL.js → card-Xf5wHHBL.js} +2 -2
  56. package/extensions/dashboard-client/dist/assets/{card-BVVhm3sL.js.map → card-Xf5wHHBL.js.map} +1 -1
  57. package/extensions/dashboard-client/dist/assets/{client-extra-BvGxKZpm.js → client-extra-CvxDLpQr.js} +2 -2
  58. package/extensions/dashboard-client/dist/assets/{client-extra-BvGxKZpm.js.map → client-extra-CvxDLpQr.js.map} +1 -1
  59. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-B1PeCct1.js → generateCategoricalChart-DhQquRa8.js} +2 -2
  60. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-B1PeCct1.js.map → generateCategoricalChart-DhQquRa8.js.map} +1 -1
  61. package/extensions/dashboard-client/dist/assets/{index-DuThOA-d.js → index-BUP3MK5S.js} +3 -3
  62. package/extensions/dashboard-client/dist/assets/{index-DuThOA-d.js.map → index-BUP3MK5S.js.map} +1 -1
  63. package/extensions/dashboard-client/dist/assets/{switch-7qLS7ajc.js → switch-B13rb8Z2.js} +2 -2
  64. package/extensions/dashboard-client/dist/assets/{switch-7qLS7ajc.js.map → switch-B13rb8Z2.js.map} +1 -1
  65. package/extensions/dashboard-client/dist/assets/{toggle-D7mgOVxz.js → toggle-4h6DUEB5.js} +2 -2
  66. package/extensions/dashboard-client/dist/assets/{toggle-D7mgOVxz.js.map → toggle-4h6DUEB5.js.map} +1 -1
  67. package/extensions/dashboard-client/dist/assets/{useSSE-ChpyZVeX.js → useSSE-Djom4z1K.js} +2 -2
  68. package/extensions/dashboard-client/dist/assets/{useSSE-ChpyZVeX.js.map → useSSE-Djom4z1K.js.map} +1 -1
  69. package/extensions/dashboard-client/dist/index.html +1 -1
  70. package/extensions/dashboard-client/src/tabs/SetupTab/CustomEndpointSection.tsx +57 -2
  71. package/extensions/dashboard-client/src/tabs/SetupTab/EmbedderSetup.tsx +9 -39
  72. package/extensions/dashboard-client/src/tabs/SetupTab/EmbedderSetupHelpers.tsx +50 -0
  73. package/extensions/dashboard-server/api-contracts/setup-cortex.ts +6 -2
  74. package/extensions/dashboard-server/api-contracts/setup.ts +12 -0
  75. package/extensions/dashboard-server/qualification-record.ts +70 -0
  76. package/extensions/dashboard-server/routes-rag-settings-vector-cortex.ts +12 -0
  77. package/extensions/dashboard-server/routes-setup-cortex-actions.ts +50 -3
  78. package/extensions/dashboard-server/routes-setup-cortex.ts +92 -16
  79. package/extensions/dashboard-server/routes-setup-enc1a.ts +184 -0
  80. package/extensions/dashboard-server/routes-setup.ts +21 -0
  81. package/extensions/dashboard-server/setup-cortex-actions.ts +23 -4
  82. package/extensions/dashboard-server/setup-cortex-blockers.ts +3 -97
  83. package/package.json +1 -1
  84. package/src/config/vector-cortex-enc0g.ts +39 -0
  85. package/src/config/vector-cortex-enc1a.ts +41 -0
  86. package/src/config/vector-cortex.ts +2 -0
  87. package/src/config.ts +2 -0
  88. package/src/vector-cortex/setup-cortex-blockers-compute.ts +216 -0
  89. package/extensions/dashboard-client/dist/assets/SetupTab-C3dnKa8y.js +0 -3
  90. package/extensions/dashboard-client/dist/assets/SetupTab-C3dnKa8y.js.map +0 -1
@@ -0,0 +1,50 @@
1
+ /**
2
+ * SetupTab/EmbedderSetupHelpers.tsx — extracted render helpers for EmbedderSetup.
3
+ *
4
+ * Split from EmbedderSetup.tsx to respect the extensions/ 400-line soft limit
5
+ * (ENC-1a pushed the embedder card over its cap, so the helpers were extracted).
6
+ */
7
+ import type React from "react";
8
+ import type { SetupStatusResponse } from "@contracts";
9
+ import { styles } from "./EmbedderSetupStyles";
10
+
11
+ /** Human label for an embedder identifier. */
12
+ export function embedderLabel(e: string): string {
13
+ switch (e) {
14
+ case "trigram":
15
+ return "TrigramEmbedder (heuristic, default)";
16
+ case "http":
17
+ return "HTTP Embedder (BYO localhost)";
18
+ case "minilm":
19
+ return "MiniLM (experimental)";
20
+ default:
21
+ return "Unknown";
22
+ }
23
+ }
24
+
25
+ /** Warn when the active embedder is the heuristic trigram default. */
26
+ export function trigramWarning(status: SetupStatusResponse | null): React.ReactElement | null {
27
+ if (status?.currentEmbedder !== "trigram") return null;
28
+ return (
29
+ <div style={styles.warning}>
30
+ <strong>Note:</strong> TrigramEmbedder is a heuristic-strength default
31
+ embedder. It works without any setup but recall quality may be lower
32
+ than a dedicated embedding backend. If you notice poor recall results,
33
+ consider installing Ollama and running the{" "}
34
+ <code>/megasetup</code> command, or set{" "}
35
+ <code>MEGACOMPACT_EMBEDDING_URL</code> to point at your own localhost
36
+ embedding server.
37
+ </div>
38
+ );
39
+ }
40
+
41
+ /** Installed / not-found badge dot for a detection row. */
42
+ export function detectBadge(installed: boolean): React.ReactElement {
43
+ const bg = installed ? "#1a5a1a" : "#3a1a1a";
44
+ const color = installed ? "#4caf50" : "#f44336";
45
+ return (
46
+ <span style={{ ...styles.badge, background: bg, color }}>
47
+ {installed ? "Detected" : "Not found"}
48
+ </span>
49
+ );
50
+ }
@@ -21,8 +21,12 @@ export interface BlockerV1 {
21
21
  readonly title: string;
22
22
  /** Severity: blocker | high | medium. */
23
23
  readonly severity: "blocker" | "high" | "medium";
24
- /** Lifecycle state — always "open" (hard gates not closed in-workstream). */
25
- readonly status: "open";
24
+ /**
25
+ * Lifecycle state. "open" (hard gate unresolved), "closed" (re-derived by
26
+ * the ENC-0g computed blocker list, e.g. HG-1 once five heads exist), or
27
+ * "superseded" (an open question with no live measurement on this device).
28
+ */
29
+ readonly status: "open" | "closed" | "superseded";
26
30
  /** Optional candidate resolution surfaced for the user / controller. */
27
31
  readonly resolution?: string;
28
32
  }
@@ -27,6 +27,12 @@ export interface SetupStatusResponse {
27
27
  readonly embedCache: string | null;
28
28
  /** Whether MEGACOMPACT_MINILM is set to a truthy value. */
29
29
  readonly minilm: boolean;
30
+ /** ENC-1a: the external embedder endpoint URL persisted in the per-repo
31
+ * `.mega-compact.env`, when one is configured (additive, flag-gated). */
32
+ readonly embeddingEndpointUrl?: string;
33
+ /** ENC-1a: whether an external embedder API key is configured. The raw key is
34
+ * NEVER returned — only this boolean presence marker (additive, flag-gated). */
35
+ readonly embeddingApiKeySet?: boolean;
30
36
  }
31
37
 
32
38
  /**
@@ -72,6 +78,12 @@ export interface SetupConfigureRequest {
72
78
  readonly embedder: "ollama" | "llama" | "trigram" | "custom" | "onnx";
73
79
  /** Override URL (required for "custom"; optional defaults for ollama/llama/onnx). */
74
80
  readonly url?: string;
81
+ /** ENC-1a: external embedder endpoint URL, persisted to the per-repo
82
+ * `.mega-compact.env` (additive, flag-gated). */
83
+ readonly embeddingEndpointUrl?: string;
84
+ /** ENC-1a: external embedder API key, persisted to the per-repo
85
+ * `.mega-compact.env` only — never returned by any GET (additive, flag-gated). */
86
+ readonly embeddingApiKey?: string;
75
87
  }
76
88
 
77
89
  /** Response for POST /api/setup-configure. */
@@ -0,0 +1,70 @@
1
+ /**
2
+ * dashboard-server/qualification-record.ts — ENC-0g QualificationV1 reader.
3
+ *
4
+ * Read-only reader for the ENC-0f qualification record at
5
+ * <stateDir>/encoder-qualification.json. The computed-blocker + action-gating
6
+ * functions live in setup-cortex-blockers.ts (Worker A's canonical module).
7
+ *
8
+ * Reader-only: local filesystem read only (PREVENT-PI-004), no `any`
9
+ * (PREVENT-011), never payloads/prompts/ledger (EVAL-REDACT-002).
10
+ */
11
+
12
+ import { homedir } from "node:os";
13
+ import { readFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import type { QualificationV1 } from "../../src/vector-cortex/encoder/qualify.js";
16
+ import { QUALIFICATION_RECORD_UNAVAILABLE } from "../../src/vector-cortex/setup-cortex-blockers-compute.js";
17
+
18
+ /** Sentinel appended to thresholdFailures when no valid record is readable (canonical def in setup-cortex-blockers-compute). */
19
+ export { QUALIFICATION_RECORD_UNAVAILABLE };
20
+
21
+ /**
22
+ * Resolve the encoder state dir (the record's home) — same precedence as
23
+ * scripts/encoder/gate-qualify.mjs: MEGACOMPACT_STATE_DIR first, else
24
+ * ~/.pi/mega-compact-encoder.
25
+ */
26
+ export function encoderStateDir(): string {
27
+ const env = process.env.MEGACOMPACT_STATE_DIR;
28
+ return env !== undefined && env.length > 0
29
+ ? env
30
+ : join(homedir(), ".pi", "mega-compact-encoder");
31
+ }
32
+
33
+ /**
34
+ * Read + minimally validate the QualificationV1 record. Returns null on a
35
+ * missing/unreadable file, a JSON parse failure, or a shape mismatch (the
36
+ * route degrades to verify-only — never crashes). Read-only (PREVENT-PI-004).
37
+ */
38
+ export function readQualificationRecord(stateDir: string): QualificationV1 | null {
39
+ let raw: string;
40
+ try {
41
+ // guardrails-allow PREVENT-PI-004: local qualification-record filesystem read (loopback)
42
+ raw = readFileSync(join(stateDir, "encoder-qualification.json"), "utf8");
43
+ } catch {
44
+ return null;
45
+ }
46
+ let parsed: unknown;
47
+ try {
48
+ parsed = JSON.parse(raw) as unknown;
49
+ } catch {
50
+ return null;
51
+ }
52
+ const rec = parsed as Record<string, unknown> | null;
53
+ if (rec === null || typeof rec !== "object") return null;
54
+ if (rec["schema"] !== "qualification-v1") return null;
55
+ if (typeof rec["verdict"] !== "string") return null;
56
+ if (!Array.isArray(rec["reasons"])) return null;
57
+ if (typeof rec["platform"] !== "string") return null;
58
+ const reasons = rec["reasons"].map((r) => String(r));
59
+ return {
60
+ schema: "qualification-v1",
61
+ verdict: rec["verdict"] === "qualified" ? "qualified" : "failed",
62
+ reasons,
63
+ platform: rec["platform"],
64
+ p95Ms: typeof rec["p95Ms"] === "number" ? rec["p95Ms"] : 0,
65
+ rssMib: typeof rec["rssMib"] === "number" ? rec["rssMib"] : 0,
66
+ opset: typeof rec["opset"] === "number" ? rec["opset"] : 0,
67
+ digest: typeof rec["digest"] === "string" ? rec["digest"] : "",
68
+ };
69
+ }
70
+
@@ -288,5 +288,17 @@ export const VECTOR_CORTEX_SETTINGS: SettingGroup = {
288
288
  "ENC-0f p95 + marginal-RSS qualification gate (real trained asset → mode A): runs the ENC-0d-promoted trained asset through the bench under --expose-gc, asserts p95 ≤ 40 ms @ 512/4 threads, marginal RSS ≤ 150 MiB (baseline-subtracted), determinism, and opset-21; on pass emits QualificationV1 + flips runtime to qualified mode A, on failure asset stays demoted to mode B. OFF = no qualification gate runs, no QualificationV1 written, runtime keeps serving the ENC-0d survivor (byte-identical predecessor).",
289
289
  true,
290
290
  ),
291
+ boolDirect(
292
+ "MEGACOMPACT_ENC_0G",
293
+ "ENC-0g Setup Cortex honest state (verdict override + live blockers)",
294
+ "ENC-0g makes the Setup Cortex status route honest: reads the ENC-0f QualificationV1 record (when ENC_0F is ON and a record exists) and lets its verdict override the structural verify for the qualification field; computes the blocker list from (platform, record, manifest head-count) with corrected HG statuses/wording; re-derives VC9B action gating from live blockers. OFF = verdict from verifyEncoderAsset alone, static SETUP_CORTEX_BLOCKERS, static action gating (byte-identical ENC-0f predecessor).",
295
+ true,
296
+ ),
297
+ boolDirect(
298
+ "MEGACOMPACT_ENC_1A",
299
+ "ENC-1a external embedder API key + endpoint Settings fields",
300
+ "ENC-1a adds a Settings-visible field pair for the external embedder endpoint URL + optional Bearer API key, persisted by the setup-configure writer branch to the per-repo .mega-compact.env (MEGACOMPACT_EMBEDDING_URL / MEGACOMPACT_EMBEDDING_KEY). The GET status route echoes the endpoint URL and reports embeddingApiKeySet as a boolean ONLY — the raw key is never returned. OFF = no new GET fields, no writer branch, no new Settings text rows (byte-identical ENC-0g predecessor).",
301
+ true,
302
+ ),
291
303
  ],
292
304
  };
@@ -19,10 +19,18 @@
19
19
  */
20
20
 
21
21
  import type { IncomingMessage, ServerResponse } from "node:http";
22
+ import { existsSync } from "node:fs";
23
+ import { join, dirname } from "node:path";
24
+ import { fileURLToPath } from "node:url";
22
25
  import type { RouteContext } from "./routes-core.js";
23
26
  import { sendJson, readJsonBody } from "./routes-vector-cortex-shared.js";
24
- import { VC9B_ENABLED } from "../../src/config.js";
25
- import { setupCortexActionBlockers } from "./setup-cortex-blockers.js";
27
+ import { VC9B_ENABLED, ENC_0G_ENABLED, ENC_0F_ENABLED } from "../../src/config.js";
28
+ import { setupCortexActionBlockers, computeSetupCortexBlockers } from "./setup-cortex-blockers.js";
29
+ import { readEncoderManifest, detectPlatform } from "../../src/vector-cortex/encoder/asset.js";
30
+ import {
31
+ readQualificationRecord,
32
+ encoderStateDir,
33
+ } from "./qualification-record.js";
26
34
  import {
27
35
  runSetupCortexAction,
28
36
  readActionLogTail,
@@ -39,6 +47,42 @@ function isActionKind(v: unknown): v is SetupCortexActionKind {
39
47
  return typeof v === "string" && ACTION_KINDS.has(v as SetupCortexActionKind);
40
48
  }
41
49
 
50
+ /** The encoder asset dir used by the action gate (mirrors routes-setup-cortex.ts). */
51
+ function encoderAssetDir(): string | null {
52
+ let dir = dirname(fileURLToPath(import.meta.url));
53
+ const rel = join("assets", "vector-cortex", "encoder-v1");
54
+ for (let i = 0; i < 8; i++) {
55
+ const candidate = join(dir, rel);
56
+ if (existsSync(join(candidate, "manifest.json"))) return candidate;
57
+ const next = dirname(dir);
58
+ if (next === dir) break;
59
+ dir = next;
60
+ }
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * ENC-0g: the live computed blockers for the action gate. When both gates are
66
+ * ON, read the QualificationV1 record + verified manifest head-count and derive
67
+ * the live list (so a closed HG-1 no longer gates fetch-model/bench). Otherwise
68
+ * return null and let setupCortexActionBlockers fall back to the static base.
69
+ */
70
+ function liveActionBlockers(): ReturnType<typeof computeSetupCortexBlockers> | null {
71
+ if (!ENC_0G_ENABLED() || !ENC_0F_ENABLED()) return null;
72
+ const record = readQualificationRecord(encoderStateDir());
73
+ let headCount: number | null = null;
74
+ const dir = encoderAssetDir();
75
+ if (dir !== null) {
76
+ const manifest = readEncoderManifest(dir);
77
+ if (manifest !== null) headCount = Object.keys(manifest.heads).length;
78
+ }
79
+ return computeSetupCortexBlockers({
80
+ platform: detectPlatform(),
81
+ qualification: record,
82
+ headCount,
83
+ });
84
+ }
85
+
42
86
  /** Flag-off response, byte-identical regardless of request (VC9B absent). */
43
87
  function sendDisabled(res: ServerResponse): void {
44
88
  sendJson(res, 404, { error: "disabled" });
@@ -79,7 +123,10 @@ export function handleSetupCortexAction(
79
123
  }
80
124
  // Hard-gate check: when an OPEN hard-gate item gates this action, do NOT
81
125
  // spawn — surface the blocker ids so the client highlights the matching rows.
82
- const blockers = setupCortexActionBlockers(action);
126
+ // ENC-0g: re-derive against the LIVE computed blockers (a closed HG-1 no
127
+ // longer blocks), falling back to the static base when the gates are off.
128
+ const live = liveActionBlockers();
129
+ const blockers = live !== null ? setupCortexActionBlockers(action, live) : setupCortexActionBlockers(action);
83
130
  if (blockers.length > 0) {
84
131
  sendJson(res, 423, {
85
132
  error: "action_blocked_by_open_item",
@@ -25,12 +25,18 @@ import { readFileSync, statSync } from "node:fs";
25
25
  import { join, dirname } from "node:path";
26
26
  import { fileURLToPath } from "node:url";
27
27
  import type { RouteContext } from "./routes-core.js";
28
- import { VC9A_ENABLED, ENC_0E_ENABLED } from "../../src/config.js";
28
+ import { VC9A_ENABLED, ENC_0E_ENABLED, ENC_0F_ENABLED, ENC_0G_ENABLED } from "../../src/config.js";
29
29
  import { readEncoderManifest, verifyEncoderAsset, detectPlatform } from "../../src/vector-cortex/encoder/asset.js";
30
+ import type { QualificationV1 } from "../../src/vector-cortex/encoder/qualify.js";
30
31
  import { selectRuntimeBackend } from "../../src/vector-cortex/encoder/runtime-select.js";
31
32
  import { sendJson } from "./routes-vector-cortex-shared.js";
32
33
  import { deriveVcStatus } from "./vc-status.js";
33
- import { SETUP_CORTEX_BLOCKERS } from "./setup-cortex-blockers.js";
34
+ import { SETUP_CORTEX_BLOCKERS, computeSetupCortexBlockers } from "./setup-cortex-blockers.js";
35
+ import {
36
+ readQualificationRecord,
37
+ encoderStateDir,
38
+ QUALIFICATION_RECORD_UNAVAILABLE,
39
+ } from "./qualification-record.js";
34
40
  import type {
35
41
  SetupCortexStatusResponse,
36
42
  BlockerV1,
@@ -62,6 +68,8 @@ interface SetupCortexFacts {
62
68
  assetDigestPrefix: string | null;
63
69
  verdict: "qualified" | "demoted" | "unavailable";
64
70
  thresholdFailures: string[];
71
+ /** Number of projection heads the verified manifest declares, or null. */
72
+ headCount: number | null;
65
73
  }
66
74
 
67
75
  interface SetupCortexFactCache {
@@ -72,7 +80,13 @@ interface SetupCortexFactCache {
72
80
  let factCache: SetupCortexFactCache | null = null;
73
81
 
74
82
  /** Stable cache key: manifest bytes + (size,mtime) of the asset files + platform. */
75
- function factCacheKey(dir: string, manifestPath: string): string | null {
83
+ function factCacheKey(
84
+ dir: string,
85
+ manifestPath: string,
86
+ stateDir: string,
87
+ record: QualificationV1 | null,
88
+ recordGate: boolean,
89
+ ): string | null {
76
90
  const platform = detectPlatform();
77
91
  if (!platform) return null;
78
92
  const parts: string[] = [platform];
@@ -86,16 +100,56 @@ function factCacheKey(dir: string, manifestPath: string): string | null {
86
100
  } catch {
87
101
  return null;
88
102
  }
103
+ // ENC-0g: fold the qualification-record gate + (mtime, content sha) into the
104
+ // key so a re-run gate invalidates memoized facts (no stale verdict served).
105
+ try {
106
+ const s = statSync(join(stateDir, "encoder-qualification.json"));
107
+ parts.push(`g:${recordGate ? "on" : "off"}`, `q:${s.size}:${s.mtimeMs}`);
108
+ parts.push(createHash("sha256").update(JSON.stringify(record)).digest("hex"));
109
+ } catch {
110
+ parts.push(`g:${recordGate ? "on" : "off"}`, "q:absent");
111
+ }
89
112
  return parts.join("|");
90
113
  }
91
114
 
92
- function computeSetupCortexFacts(dir: string): SetupCortexFacts | null {
115
+ function applyQualificationOverride(
116
+ base: SetupCortexFacts,
117
+ record: QualificationV1 | null,
118
+ recordGate: boolean,
119
+ ): SetupCortexFacts {
120
+ // Flag-off (recordGate false): byte-identical verify-derived facts.
121
+ if (!recordGate) return base;
122
+ if (record !== null) {
123
+ // The ENC-0f QualificationV1 failure maps onto the contract "demoted": a
124
+ // structurally-valid asset whose real-asset gate measured failure is honest
125
+ // as demoted, NOT qualified. mode stays verify-derived (A).
126
+ return {
127
+ ...base,
128
+ verdict: record.verdict === "qualified" ? "qualified" : "demoted",
129
+ thresholdFailures: [...record.reasons],
130
+ };
131
+ }
132
+ // No record + gate on: keep the verify verdict but surface the missing-record
133
+ // sentinel (never a fabricated pass, never a bare silent fallback).
134
+ return {
135
+ ...base,
136
+ thresholdFailures: [...base.thresholdFailures, QUALIFICATION_RECORD_UNAVAILABLE],
137
+ };
138
+ }
139
+
140
+ function computeSetupCortexFacts(
141
+ dir: string,
142
+ stateDir: string,
143
+ record: QualificationV1 | null,
144
+ recordGate: boolean,
145
+ ): SetupCortexFacts | null {
93
146
  const manifest = readEncoderManifest(dir);
94
147
  if (manifest === null) {
95
- return { mode: "C", assetDigestPrefix: null, verdict: "unavailable", thresholdFailures: [] };
148
+ return { mode: "C", assetDigestPrefix: null, verdict: "unavailable", thresholdFailures: [], headCount: null };
96
149
  }
150
+ const headCount = Object.keys(manifest.heads).length;
97
151
  const manifestPath = join(dir, "manifest.json");
98
- const key = factCacheKey(dir, manifestPath);
152
+ const key = factCacheKey(dir, manifestPath, stateDir, record, recordGate);
99
153
  if (key === null) return null;
100
154
  if (factCache !== null && factCache.key === key) return factCache.facts;
101
155
  let digest: string;
@@ -103,26 +157,31 @@ function computeSetupCortexFacts(dir: string): SetupCortexFacts | null {
103
157
  // guardrails-allow PREVENT-PI-004: local manifest filesystem read (loopback)
104
158
  digest = createHash("sha256").update(readFileSync(manifestPath)).digest("hex");
105
159
  } catch {
106
- const facts: SetupCortexFacts = { mode: "B", assetDigestPrefix: null, verdict: "demoted", thresholdFailures: ["ENC_ASSET_UNREADABLE"] };
160
+ const base: SetupCortexFacts = { mode: "B", assetDigestPrefix: null, verdict: "demoted", thresholdFailures: ["ENC_ASSET_UNREADABLE"], headCount };
161
+ const facts = applyQualificationOverride(base, record, recordGate);
107
162
  factCache = { key, facts };
108
163
  return facts;
109
164
  }
110
165
  const verify = verifyEncoderAsset(dir, manifest, detectPlatform());
111
166
  const prefix = digest.slice(0, 12);
112
- const facts: SetupCortexFacts = verify.ok
113
- ? { mode: "A", assetDigestPrefix: prefix, verdict: "qualified", thresholdFailures: [] }
114
- : { mode: "B", assetDigestPrefix: prefix, verdict: "demoted", thresholdFailures: [verify.code] };
167
+ const base: SetupCortexFacts = verify.ok
168
+ ? { mode: "A", assetDigestPrefix: prefix, verdict: "qualified", thresholdFailures: [], headCount }
169
+ : { mode: "B", assetDigestPrefix: prefix, verdict: "demoted", thresholdFailures: [verify.code], headCount };
170
+ const facts = applyQualificationOverride(base, record, recordGate);
115
171
  factCache = { key, facts };
116
172
  return facts;
117
173
  }
118
174
 
119
- function setupCortexFacts(): SetupCortexFacts {
175
+ function setupCortexFacts(
176
+ record: QualificationV1 | null,
177
+ recordGate: boolean,
178
+ ): SetupCortexFacts {
120
179
  const dir = encoderAssetDir();
121
180
  if (dir === null) {
122
- return { mode: "C", assetDigestPrefix: null, verdict: "unavailable", thresholdFailures: [] };
181
+ return { mode: "C", assetDigestPrefix: null, verdict: "unavailable", thresholdFailures: [], headCount: null };
123
182
  }
124
- const facts = computeSetupCortexFacts(dir);
125
- return facts ?? { mode: "C", assetDigestPrefix: null, verdict: "unavailable", thresholdFailures: [] };
183
+ const facts = computeSetupCortexFacts(dir, encoderStateDir(), record, recordGate);
184
+ return facts ?? { mode: "C", assetDigestPrefix: null, verdict: "unavailable", thresholdFailures: [], headCount: null };
126
185
  }
127
186
 
128
187
  /**
@@ -145,8 +204,25 @@ export function handleSetupCortexStatus(
145
204
  }
146
205
 
147
206
  const enabled = VC9A_ENABLED();
148
- const facts = enabled ? setupCortexFacts() : null;
149
- const blocks: BlockerV1[] = enabled ? [...SETUP_CORTEX_BLOCKERS] : [];
207
+
208
+ // ENC-0g: when both gates are ON, read the QualificationV1 record and let its
209
+ // verdict override the structural verify for the `qualification` field. When
210
+ // ENC_0G is OFF, nothing below reads the record (byte-identical to ENC-0f-era).
211
+ const enc0g = enabled && ENC_0G_ENABLED();
212
+ const recordGate = enc0g && ENC_0F_ENABLED();
213
+ const record = recordGate ? readQualificationRecord(encoderStateDir()) : null;
214
+
215
+ const facts = enabled ? setupCortexFacts(record, recordGate) : null;
216
+
217
+ const blocks: BlockerV1[] = enabled
218
+ ? enc0g
219
+ ? [...computeSetupCortexBlockers({
220
+ platform: detectPlatform(),
221
+ qualification: record,
222
+ headCount: facts ? facts.headCount : null,
223
+ })]
224
+ : [...SETUP_CORTEX_BLOCKERS]
225
+ : [];
150
226
 
151
227
  // ENC-0e: surface the darwin-x64 demotion reason additively (reader-only GET,
152
228
  // no new route). The platform is read locally; the selection is pure. On a
@@ -0,0 +1,184 @@
1
+ /**
2
+ * dashboard-server/routes-setup-enc1a.ts — ENC-1a external-embedder Settings
3
+ * read/write + the additive writer branch.
4
+ *
5
+ * Extracted out of routes-setup.ts (which hovers at the 300-line source soft
6
+ * cap) so the ENC-1a additive read/write of the per-repo `.mega-compact.env`
7
+ * keys AND the pure external-embedder configure branch live in a sibling impl
8
+ * file. The two keys are `MEGACOMPACT_EMBEDDING_URL` and
9
+ * `MEGACOMPACT_EMBEDDING_KEY` — the exact names `embeddingConfigFromEnv`
10
+ * (src/httpEmbedder.ts) reads at runtime.
11
+ *
12
+ * The write is create-or-append: it upserts the two keys into the existing
13
+ * per-repo `.mega-compact.env`, preserving every unrelated line and never
14
+ * deleting other keys. The read reports the endpoint URL (echoed) and an
15
+ * `apiKeySet` boolean ONLY — the raw API key is never returned, logged, or
16
+ * emitted (redaction invariant).
17
+ *
18
+ * Guardrails: PREVENT-PI-004 (local filesystem writes only, zero network),
19
+ * PREVENT-001 (null-safe JSON), PREVENT-011 (no `any`).
20
+ */
21
+
22
+ import { writeFileSync, mkdirSync, readFileSync, existsSync } from "node:fs";
23
+ import { join } from "node:path";
24
+ import type { ServerResponse } from "node:http";
25
+ import type { RouteContext } from "./routes-core.js";
26
+ import { ENC_1A_ENABLED } from "../../src/config.js";
27
+ import type {
28
+ SetupConfigureRequest,
29
+ SetupConfigureResponse,
30
+ } from "./api-contracts/setup.js";
31
+
32
+ const URL_KEY = "MEGACOMPACT_EMBEDDING_URL";
33
+ const API_KEY = "MEGACOMPACT_EMBEDDING_KEY";
34
+
35
+ function envPath(stateDir: string): string {
36
+ return join(stateDir, ".mega-compact.env");
37
+ }
38
+
39
+ /** Extract the value of `export MEGACOMPACT_X="..."` (or unquoted) from a line. */
40
+ function lineValue(line: string, key: string): string | null {
41
+ const m = line.match(new RegExp(`^export\\s+${key}=(.*)$`));
42
+ if (!m) return null;
43
+ const rest = m[1].trim();
44
+ const q = rest.match(/^"([^"]*)"$/);
45
+ return q ? q[1] : rest;
46
+ }
47
+
48
+ /** True when the payload carries the ENC-1a keys (flag-gated). */
49
+ export function wantsEnc1a(body: SetupConfigureRequest): boolean {
50
+ return (
51
+ ENC_1A_ENABLED() &&
52
+ (typeof body.embeddingEndpointUrl === "string" ||
53
+ typeof body.embeddingApiKey === "string")
54
+ );
55
+ }
56
+
57
+ /** Additive GET status fields for ENC-1a (both omitted when flag-off): the
58
+ * endpoint URL (echoed) + an apiKeySet boolean (true/false) ONLY — never the
59
+ * raw key. The boolean is always present when the flag is on so an absent key
60
+ * is reported as `embeddingApiKeySet:false` (non-fatal, triad B). */
61
+ export function enc1aStatusFields(stateDir: string): {
62
+ embeddingEndpointUrl?: string;
63
+ embeddingApiKeySet?: boolean;
64
+ } {
65
+ if (!ENC_1A_ENABLED()) return {};
66
+ const enc1a = readEnc1aEnv(stateDir);
67
+ return {
68
+ ...(enc1a.endpointUrl !== null ? { embeddingEndpointUrl: enc1a.endpointUrl } : {}),
69
+ embeddingApiKeySet: enc1a.apiKeySet,
70
+ };
71
+ }
72
+
73
+ /** Read the ENC-1a keys from the per-repo `.mega-compact.env` (never the key
74
+ * itself to callers — only its presence). */
75
+ export function readEnc1aEnv(stateDir: string): {
76
+ endpointUrl: string | null;
77
+ apiKeySet: boolean;
78
+ } {
79
+ try {
80
+ const p = envPath(stateDir);
81
+ if (!existsSync(p)) return { endpointUrl: null, apiKeySet: false };
82
+ const content = readFileSync(p, "utf8");
83
+ const lines = content.split(/\r?\n/);
84
+ let endpointUrl: string | null = null;
85
+ let apiKeySet = false;
86
+ for (const line of lines) {
87
+ const url = lineValue(line, URL_KEY);
88
+ if (url !== null) endpointUrl = url;
89
+ const key = lineValue(line, API_KEY);
90
+ if (key !== null && key.length > 0) apiKeySet = true;
91
+ }
92
+ return { endpointUrl, apiKeySet };
93
+ } catch {
94
+ return { endpointUrl: null, apiKeySet: false };
95
+ }
96
+ }
97
+
98
+ /** Upsert the ENC-1a keys into the per-repo `.mega-compact.env`. Creates the
99
+ * file if absent; preserves every unrelated line; never deletes other keys.
100
+ * Passing `null` for a key leaves that line untouched. */
101
+ export function writeEnc1aEnv(
102
+ stateDir: string,
103
+ entries: { endpointUrl?: string | null; apiKey?: string | null },
104
+ ): string {
105
+ const p = envPath(stateDir);
106
+ const existingLines: string[] = existsSync(p)
107
+ ? readFileSync(p, "utf8").split(/\r?\n/)
108
+ : [];
109
+ const out: string[] = [];
110
+ let urlWritten = false;
111
+ let keyWritten = false;
112
+ for (const line of existingLines) {
113
+ if (entries.endpointUrl !== null && lineValue(line, URL_KEY) !== null) {
114
+ out.push(`export ${URL_KEY}="${entries.endpointUrl ?? ""}"`);
115
+ urlWritten = true;
116
+ continue;
117
+ }
118
+ if (entries.apiKey !== null && lineValue(line, API_KEY) !== null) {
119
+ out.push(`export ${API_KEY}="${entries.apiKey ?? ""}"`);
120
+ keyWritten = true;
121
+ continue;
122
+ }
123
+ out.push(line);
124
+ }
125
+ if (entries.endpointUrl !== null && !urlWritten) {
126
+ out.push(`export ${URL_KEY}="${entries.endpointUrl}"`);
127
+ }
128
+ if (entries.apiKey !== null && !keyWritten) {
129
+ out.push(`export ${API_KEY}="${entries.apiKey ?? ""}"`);
130
+ }
131
+ mkdirSync(stateDir, { recursive: true });
132
+ writeFileSync(p, out.join("\n"), "utf-8");
133
+ return p;
134
+ }
135
+
136
+ /** If the payload is a pure ENC-1a configure (new keys, no valid embedder
137
+ * selection), write them additively to the per-repo env and reply. Returns
138
+ * true when it fully handled the request. */
139
+ export function tryEnc1aConfigure(
140
+ body: SetupConfigureRequest,
141
+ res: ServerResponse,
142
+ ctx: RouteContext,
143
+ ): boolean {
144
+ if (!wantsEnc1a(body)) return false;
145
+ const embedder = body.embedder;
146
+ const embedderValid =
147
+ !!embedder &&
148
+ (embedder === "ollama" || embedder === "llama" || embedder === "trigram" ||
149
+ embedder === "custom" || embedder === "onnx");
150
+ if (embedderValid) return false; // combined payload — integrated after the embedder write
151
+ try {
152
+ const envPath = writeEnc1aEnv(ctx.stateDir, {
153
+ endpointUrl:
154
+ typeof body.embeddingEndpointUrl === "string" && body.embeddingEndpointUrl
155
+ ? body.embeddingEndpointUrl
156
+ : null,
157
+ apiKey: typeof body.embeddingApiKey === "string" ? body.embeddingApiKey : null,
158
+ });
159
+ const currentUrl = process.env["MEGACOMPACT_EMBEDDING_URL"];
160
+ const alreadyActive =
161
+ typeof body.embeddingEndpointUrl === "string" &&
162
+ currentUrl === body.embeddingEndpointUrl;
163
+ const resp: SetupConfigureResponse = {
164
+ embedder: "custom",
165
+ url:
166
+ typeof body.embeddingEndpointUrl === "string"
167
+ ? body.embeddingEndpointUrl
168
+ : null,
169
+ envPath,
170
+ restartRequired: !alreadyActive,
171
+ alreadyActive,
172
+ };
173
+ // guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
174
+ res.writeHead(200, { "Content-Type": "application/json" });
175
+ res.end(JSON.stringify(resp));
176
+ return true;
177
+ } catch (e) {
178
+ res.writeHead(500, { "Content-Type": "application/json" });
179
+ res.end(
180
+ JSON.stringify({ error: `write_failed: ${e instanceof Error ? e.message : String(e)}` }),
181
+ );
182
+ return true;
183
+ }
184
+ }
@@ -14,6 +14,7 @@ import { join } from "node:path";
14
14
  import type { IncomingMessage, ServerResponse } from "node:http";
15
15
  import type { RouteContext } from "./routes-core.js";
16
16
  import { VC9D_ENABLED } from "../../src/config.js";
17
+ import { enc1aStatusFields, writeEnc1aEnv, tryEnc1aConfigure, wantsEnc1a } from "./routes-setup-enc1a.js";
17
18
  import {
18
19
  detectOllama,
19
20
  detectLlamaCpp,
@@ -77,6 +78,9 @@ export function handleSetupStatus(
77
78
  }
78
79
  const configured = detectConfiguredEmbedder(ctx.stateDir);
79
80
  const active = detectCurrentEmbedder();
81
+ // ENC-1a (flag-gated, additive): echo the persisted endpoint URL and an
82
+ // embeddingApiKeySet boolean ONLY — the raw API key is never returned. When
83
+ // the flag is off (or neither is set) both fields are simply omitted.
80
84
  const body: SetupStatusResponse = {
81
85
  currentEmbedder: active,
82
86
  configuredEmbedder: configured.embedder,
@@ -87,6 +91,7 @@ export function handleSetupStatus(
87
91
  minilm: ["1", "true", "yes"].includes(
88
92
  (process.env["MEGACOMPACT_MINILM"] ?? "").trim().toLowerCase(),
89
93
  ),
94
+ ...enc1aStatusFields(ctx.stateDir),
90
95
  };
91
96
  // guardrails-allow PREVENT-PI-004: loopback dashboard response (local)
92
97
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -205,6 +210,11 @@ export function handleSetupConfigure(
205
210
  return;
206
211
  }
207
212
  const body = parsed.value as unknown as SetupConfigureRequest;
213
+ // ENC-1a (flag-gated, additive): a pure external-embedder configure
214
+ // (new keys, no embedder selection) is handled by the sibling writer.
215
+ // Flag-off = the keys are simply not recognized, falling through to the
216
+ // pre-ENC-1a embedder path below (byte-identical predecessor).
217
+ if (tryEnc1aConfigure(body, res, ctx)) return;
208
218
  const embedder = body.embedder;
209
219
  if (embedder !== "ollama" && embedder !== "llama" && embedder !== "trigram" && embedder !== "custom" && embedder !== "onnx") {
210
220
  res.writeHead(400, { "Content-Type": "application/json" });
@@ -258,6 +268,17 @@ export function handleSetupConfigure(
258
268
  res.end(JSON.stringify({ error: `write_failed: ${e instanceof Error ? e.message : String(e)}` }));
259
269
  return;
260
270
  }
271
+ // ENC-1a: a combined payload (valid embedder + new keys) also upserts
272
+ // the endpoint/key lines onto the freshly-written env file.
273
+ if (wantsEnc1a(body)) {
274
+ writeEnc1aEnv(stateDir, {
275
+ endpointUrl:
276
+ typeof body.embeddingEndpointUrl === "string" && body.embeddingEndpointUrl
277
+ ? body.embeddingEndpointUrl
278
+ : null,
279
+ apiKey: typeof body.embeddingApiKey === "string" ? body.embeddingApiKey : null,
280
+ });
281
+ }
261
282
  // Detect if the new config matches what's already active (no restart needed).
262
283
  const currentUrl = process.env["MEGACOMPACT_EMBEDDING_URL"];
263
284
  const alreadyActive = (resolvedUrl === null && !currentUrl) || (resolvedUrl !== null && currentUrl === resolvedUrl);