pi-mega-compact 0.20.2 → 0.20.4

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 (103) hide show
  1. package/dist/config/vector-cortex.js +13 -0
  2. package/dist/config.js +1 -1
  3. package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +1 -0
  4. package/dist/extensions/dashboard-server/routes-vector-cortex-plans.js +49 -0
  5. package/dist/extensions/dashboard-server/routes-vector-cortex.js +1 -0
  6. package/dist/extensions/dashboard-server/routes.js +1 -1
  7. package/dist/extensions/dashboard-server/server.js +29 -22
  8. package/dist/src/config/vector-cortex.js +13 -0
  9. package/dist/src/config.js +1 -1
  10. package/dist/src/vector-cortex/planner/manifest.js +66 -0
  11. package/dist/src/vector-cortex/planner/portfolio.js +0 -0
  12. package/dist/src/vector-cortex/planner/types.js +41 -0
  13. package/dist/src/vector-cortex/prompt-dag/_acceptance-dag.js +347 -0
  14. package/dist/src/vector-cortex/prompt-dag/_acceptance-fixture.js +59 -0
  15. package/dist/src/vector-cortex/prompt-dag/_acceptance-helpers.js +17 -0
  16. package/dist/src/vector-cortex/prompt-dag/_acceptance-planner.js +153 -0
  17. package/dist/src/vector-cortex/prompt-dag/_acceptance-shuffle.js +24 -0
  18. package/dist/src/vector-cortex/prompt-dag/builder.js +171 -0
  19. package/dist/src/vector-cortex/prompt-dag/types.js +33 -0
  20. package/dist/src/vector-cortex/prompt-dag/validator.js +0 -0
  21. package/dist/vector-cortex/planner/manifest.js +66 -0
  22. package/dist/vector-cortex/planner/portfolio.js +0 -0
  23. package/dist/vector-cortex/planner/types.js +41 -0
  24. package/dist/vector-cortex/prompt-dag/_acceptance-dag.js +347 -0
  25. package/dist/vector-cortex/prompt-dag/_acceptance-fixture.js +59 -0
  26. package/dist/vector-cortex/prompt-dag/_acceptance-helpers.js +17 -0
  27. package/dist/vector-cortex/prompt-dag/_acceptance-planner.js +153 -0
  28. package/dist/vector-cortex/prompt-dag/_acceptance-shuffle.js +24 -0
  29. package/dist/vector-cortex/prompt-dag/builder.js +171 -0
  30. package/dist/vector-cortex/prompt-dag/types.js +33 -0
  31. package/dist/vector-cortex/prompt-dag/validator.js +0 -0
  32. package/extensions/dashboard-client/dist/assets/{AreaChart-XF3jPpvn.js → AreaChart-Cam9-nhT.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{AreaChart-XF3jPpvn.js.map → AreaChart-Cam9-nhT.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/{BarChart-DnYYn0GF.js → BarChart-DzIY8aM1.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{BarChart-DnYYn0GF.js.map → BarChart-DzIY8aM1.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{CacheTab-D3bVskCL.js → CacheTab-CKIiYsG_.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{CacheTab-D3bVskCL.js.map → CacheTab-CKIiYsG_.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{EventsTab-CX_8myfc.js → EventsTab-xaaBJLiK.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{EventsTab-CX_8myfc.js.map → EventsTab-xaaBJLiK.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/{HealthTab-CnVzBQvN.js → HealthTab-BGtUKOqG.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{HealthTab-CnVzBQvN.js.map → HealthTab-BGtUKOqG.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-Cao--dtV.js → MaintenanceTab-ABgQnTZ9.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-Cao--dtV.js.map → MaintenanceTab-ABgQnTZ9.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DZdI-9ig.js → MemoryMapTab-ChR2GAN-.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DZdI-9ig.js.map → MemoryMapTab-ChR2GAN-.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{MetricsTab-SlpSmNU4.js → MetricsTab-DdCLAgcR.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{MetricsTab-SlpSmNU4.js.map → MetricsTab-DdCLAgcR.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/{OverviewTab-C_-_4au9.js → OverviewTab-DtemzJt6.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{OverviewTab-C_-_4au9.js.map → OverviewTab-DtemzJt6.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/assets/{ReposTab-CyWedWZC.js → ReposTab-B9mvD9Ep.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{ReposTab-CyWedWZC.js.map → ReposTab-B9mvD9Ep.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/{SessionsTab-wqPBMIJZ.js → SessionsTab-BcO38XLS.js} +2 -2
  53. package/extensions/dashboard-client/dist/assets/{SessionsTab-wqPBMIJZ.js.map → SessionsTab-BcO38XLS.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/{SetupTab-MojXmuUf.js → SetupTab-D_TBNP09.js} +2 -2
  55. package/extensions/dashboard-client/dist/assets/{SetupTab-MojXmuUf.js.map → SetupTab-D_TBNP09.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BxcplEOJ.js → TimeSavedCard-TjHpbUOo.js} +2 -2
  57. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BxcplEOJ.js.map → TimeSavedCard-TjHpbUOo.js.map} +1 -1
  58. package/extensions/dashboard-client/dist/assets/{TurnsTab-D8_qWbjY.js → TurnsTab-DprxndCF.js} +2 -2
  59. package/extensions/dashboard-client/dist/assets/{TurnsTab-D8_qWbjY.js.map → TurnsTab-DprxndCF.js.map} +1 -1
  60. package/extensions/dashboard-client/dist/assets/VectorCortexTab-Bym21yVQ.js +2 -0
  61. package/extensions/dashboard-client/dist/assets/VectorCortexTab-Bym21yVQ.js.map +1 -0
  62. package/extensions/dashboard-client/dist/assets/{WikiTab-Cfd-QZYz.js → WikiTab-OKgSo15G.js} +2 -2
  63. package/extensions/dashboard-client/dist/assets/{WikiTab-Cfd-QZYz.js.map → WikiTab-OKgSo15G.js.map} +1 -1
  64. package/extensions/dashboard-client/dist/assets/{button-tX5ht0bb.js → button-D_Qz4q7L.js} +2 -2
  65. package/extensions/dashboard-client/dist/assets/{button-tX5ht0bb.js.map → button-D_Qz4q7L.js.map} +1 -1
  66. package/extensions/dashboard-client/dist/assets/{card-BT3Z5dDg.js → card-CsI6Jm3p.js} +2 -2
  67. package/extensions/dashboard-client/dist/assets/{card-BT3Z5dDg.js.map → card-CsI6Jm3p.js.map} +1 -1
  68. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-B6p2WnkY.js → generateCategoricalChart-CvHKUdhX.js} +2 -2
  69. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-B6p2WnkY.js.map → generateCategoricalChart-CvHKUdhX.js.map} +1 -1
  70. package/extensions/dashboard-client/dist/assets/{index-CFLekP12.js → index-0Ye_un22.js} +3 -3
  71. package/extensions/dashboard-client/dist/assets/{index-CFLekP12.js.map → index-0Ye_un22.js.map} +1 -1
  72. package/extensions/dashboard-client/dist/assets/{switch-B52oAMSw.js → switch-VQEoJl8Q.js} +2 -2
  73. package/extensions/dashboard-client/dist/assets/{switch-B52oAMSw.js.map → switch-VQEoJl8Q.js.map} +1 -1
  74. package/extensions/dashboard-client/dist/assets/{toggle-VxxVc5Tp.js → toggle-CAPm4HJo.js} +2 -2
  75. package/extensions/dashboard-client/dist/assets/{toggle-VxxVc5Tp.js.map → toggle-CAPm4HJo.js.map} +1 -1
  76. package/extensions/dashboard-client/dist/assets/{useSSE-DAVdZnG7.js → useSSE-pxsNTVkh.js} +2 -2
  77. package/extensions/dashboard-client/dist/assets/{useSSE-DAVdZnG7.js.map → useSSE-pxsNTVkh.js.map} +1 -1
  78. package/extensions/dashboard-client/dist/index.html +1 -1
  79. package/extensions/dashboard-client/src/api/vector-cortex.ts +9 -0
  80. package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +35 -0
  81. package/extensions/dashboard-client/src/types/vector-cortex.ts +20 -0
  82. package/extensions/dashboard-server/api-contracts/vector-cortex.ts +38 -0
  83. package/extensions/dashboard-server/routes-rag-settings-helpers.ts +6 -0
  84. package/extensions/dashboard-server/routes-vector-cortex-plans.ts +58 -0
  85. package/extensions/dashboard-server/routes-vector-cortex.ts +1 -0
  86. package/extensions/dashboard-server/routes.ts +1 -0
  87. package/extensions/dashboard-server/server.ts +31 -22
  88. package/package.json +1 -1
  89. package/src/config/vector-cortex.ts +14 -0
  90. package/src/config.ts +1 -0
  91. package/src/vector-cortex/planner/manifest.ts +83 -0
  92. package/src/vector-cortex/planner/portfolio.ts +0 -0
  93. package/src/vector-cortex/planner/types.ts +190 -0
  94. package/src/vector-cortex/prompt-dag/_acceptance-dag.ts +392 -0
  95. package/src/vector-cortex/prompt-dag/_acceptance-fixture.ts +132 -0
  96. package/src/vector-cortex/prompt-dag/_acceptance-helpers.ts +39 -0
  97. package/src/vector-cortex/prompt-dag/_acceptance-planner.ts +188 -0
  98. package/src/vector-cortex/prompt-dag/_acceptance-shuffle.ts +26 -0
  99. package/src/vector-cortex/prompt-dag/builder.ts +189 -0
  100. package/src/vector-cortex/prompt-dag/types.ts +151 -0
  101. package/src/vector-cortex/prompt-dag/validator.ts +0 -0
  102. package/extensions/dashboard-client/dist/assets/VectorCortexTab-B3yisAyB.js +0 -2
  103. package/extensions/dashboard-client/dist/assets/VectorCortexTab-B3yisAyB.js.map +0 -1
@@ -0,0 +1,58 @@
1
+ /**
2
+ * dashboard-server/routes-vector-cortex-plans.ts — VC5A PromptDagV1 + budgeted
3
+ * planner dashboard route.
4
+ *
5
+ * Reader-only GET /api/vector-cortex/plans returning ONLY plan manifests — the
6
+ * registered PromptDagV1 (DAG-001..030) and budgeted-planner (PLN-001..020)
7
+ * identifier counts, plus a reader-only plans array. NEVER exposes session
8
+ * payloads, prompt text, byte spans, or source bytes (reader-only,
9
+ * SECURITY_PRIVACY). Flag-gated on MEGACOMPACT_VC5A: `enabled:false` when off
10
+ * (byte-identical to the pre-VC5A predecessor).
11
+ *
12
+ * The VC5A planner is PURE IN-MEMORY logic in this sprint (it has no durable plan
13
+ * store yet), so the per-run plan outputs are not persisted. The route reports
14
+ * the registered manifest layout truthfully: `enabled` reflects the flag, the
15
+ * dag/planner counts come from the registered conformance ID range, and `plans`
16
+ * is empty until a future sprint persists selected plans. Non-fatal: an internal
17
+ * error degrades to `enabled:false` with empty arrays.
18
+ *
19
+ * Guardrails: PREVENT-PI-004 (local filesystem / in-process state only),
20
+ * PREVENT-011 (no `any`), reader-only aggregate (counts + manifests only).
21
+ */
22
+
23
+ import type { IncomingMessage, ServerResponse } from "node:http";
24
+ import type { RouteContext } from "./routes-core.js";
25
+ import { VC5A_ENABLED } from "../../src/config.js";
26
+ import { DAG_IDS } from "../../src/vector-cortex/prompt-dag/types.js";
27
+ import { PLN_IDS } from "../../src/vector-cortex/planner/types.js";
28
+ import { sendJson } from "./routes-vector-cortex-shared.js";
29
+ import type { VectorCortexPlansView } from "./api-contracts/vector-cortex.js";
30
+
31
+ /**
32
+ * Reader-only GET /api/vector-cortex/plans (VC5A).
33
+ */
34
+ export function handleVectorCortexPlans(
35
+ req: IncomingMessage,
36
+ res: ServerResponse,
37
+ _ctx: RouteContext,
38
+ ): boolean {
39
+ const url = req.url ?? "";
40
+ const path = url.split("?")[0] ?? url;
41
+ if (path !== "/api/vector-cortex/plans") return false;
42
+ if (req.method !== "GET") {
43
+ // Reader-only path: cannot be "off" without a GET; it is genuinely read-only.
44
+ sendJson(res, 405, { error: "method_not_allowed" });
45
+ return true;
46
+ }
47
+
48
+ const enabled = VC5A_ENABLED();
49
+ const body: VectorCortexPlansView = {
50
+ enabled,
51
+ dagCount: enabled ? DAG_IDS.length : 0,
52
+ plannerCount: enabled ? PLN_IDS.length : 0,
53
+ plans: [],
54
+ updatedAt: new Date().toISOString(),
55
+ };
56
+ sendJson(res, 200, body);
57
+ return true;
58
+ }
@@ -22,3 +22,4 @@ export { handleVectorCortexQuery } from "./routes-vector-cortex-query.js";
22
22
  export { handleVectorCortexShards } from "./routes-vector-cortex-shards.js";
23
23
  export { handleVectorCortexResidual } from "./routes-vector-cortex-residual.js";
24
24
  export { handleVectorCortexReconstruct } from "./routes-vector-cortex-reconstruct.js";
25
+ export { handleVectorCortexPlans } from "./routes-vector-cortex-plans.js";
@@ -50,4 +50,5 @@ export {
50
50
  handleVectorCortexShards,
51
51
  handleVectorCortexResidual,
52
52
  handleVectorCortexReconstruct,
53
+ handleVectorCortexPlans,
53
54
  } from "./routes-vector-cortex.js";
@@ -66,6 +66,7 @@ import {
66
66
  handleVectorCortexShards,
67
67
  handleVectorCortexResidual,
68
68
  handleVectorCortexReconstruct,
69
+ handleVectorCortexPlans,
69
70
  handleStatic,
70
71
  } from "./routes.js";
71
72
 
@@ -235,15 +236,16 @@ export async function launchDashboardServer(
235
236
  detectCrossRepoDrift,
236
237
  });
237
238
 
238
- // Bind host resolution: explicit MEGACOMPACT_DASHBOARD_HOST override wins;
239
- // otherwise auto-bind to the Tailscale interface (tailnet access); fall back
240
- // to loopback when Tailscale is absent. 0.0.0.0 means "all interfaces" and
241
- // opts into permissive CORS below.
242
- const host =
243
- process.env.MEGACOMPACT_DASHBOARD_HOST ??
244
- detectTailscaleIP() ??
245
- "127.0.0.1";
239
+ // Bind host resolution. Default: ALL interfaces (0.0.0.0) so the dashboard
240
+ // is reachable on loopback (localhost — the launcher's health check + the
241
+ // local browser), the tailnet (Tailscale), and any LAN interface. The
242
+ // network-denial gate was removed (the user opted into open access);
243
+ // MEGACOMPACT_DASHBOARD_HOST overrides for anyone who wants to restrict to
244
+ // a single interface. The tailnet IP is still detected so we can surface it
245
+ // as the remote access address. 0.0.0.0 opts into permissive CORS below.
246
+ const host = process.env.MEGACOMPACT_DASHBOARD_HOST ?? "0.0.0.0";
246
247
  const allInterfaces = host === "0.0.0.0";
248
+ const tailscaleIP = allInterfaces ? detectTailscaleIP() : null;
247
249
 
248
250
  // guardrails-allow PREVENT-PI-004: optional, user-triggered /dashboard server (tailnet-local via Tailscale mesh or loopback); no remote outbound calls.
249
251
  const server = createServer((req: IncomingMessage, res: ServerResponse) => {
@@ -310,6 +312,7 @@ export async function launchDashboardServer(
310
312
  if (handleVectorCortexShards(req, res, ctx)) return;
311
313
  if (handleVectorCortexResidual(req, res, ctx)) return;
312
314
  if (handleVectorCortexReconstruct(req, res, ctx)) return;
315
+ if (handleVectorCortexPlans(req, res, ctx)) return;
313
316
  handleStatic(req, res, ctx);
314
317
  });
315
318
 
@@ -334,27 +337,33 @@ export async function launchDashboardServer(
334
337
  });
335
338
 
336
339
  server.listen(port, host, () => {
337
- // When bound to the loopback, keep the legacy localhost URL so the
338
- // user's browser (which resolves localhost ::1 or 127.0.0.1) works.
339
- // When bound to a tailnet/tailscale IP, surface that address instead.
340
+ // Surface localhost for local reach (the launcher health-checks
341
+ // localhost + the local browser uses it), and when bound to all
342
+ // interfaces with a tailnet present — also surface the tailnet
343
+ // address for remote devices on the mesh.
344
+ const localUrl = `http://localhost:${port}`; // guardrails-allow PREVENT-PI-004: dashboard URL (loopback/all-interfaces, user-opted open access)
345
+ const tailnetUrl =
346
+ allInterfaces && tailscaleIP ? `http://${tailscaleIP}:${port}` : null; // guardrails-allow PREVENT-PI-004: dashboard URL (tailnet, user-opted open access)
340
347
  const url =
341
348
  host === "127.0.0.1"
342
- ? `http://localhost:${port}`
343
- : `http://${host}:${port}`; // guardrails-allow PREVENT-PI-004: dashboard URL (tailnet-local via Tailscale mesh or loopback)
344
- log("server running", { url, host });
349
+ ? localUrl
350
+ : tailnetUrl ?? `http://${host}:${port}`;
351
+ log("server running", { url, host, tailnetUrl });
345
352
  // eslint-disable-next-line no-console
346
- console.log(`[mega-compact] dashboard server running: ${url}`);
353
+ console.log(
354
+ `[mega-compact] dashboard server running: ${url}` +
355
+ (tailnetUrl ? ` (also ${localUrl})` : ""),
356
+ );
347
357
 
348
- // v0.8.2: also bind the IPv6 loopback (::1) when bound to the IPv4
349
- // loopback. On many systems `localhost` resolves to ::1 first (see
350
- // /etc/hosts), so an IPv4-only bind makes the browser hit ::1:port and
351
- // get connection refused. PREVENT-PI-004 (loopback-only) means BOTH
352
- // 127.0.0.1 and ::1. When bound to a tailscale/0.0.0.0 host, we do NOT
353
- // bind ::1 — the tailnet address is what the user reaches. Non-fatal:
358
+ // v0.8.2: also bind the IPv6 loopback (::1) when localhost must work.
359
+ // On many systems `localhost` resolves to ::1 first (see /etc/hosts),
360
+ // so an IPv4-only bind makes the browser hit ::1:port and get refused.
361
+ // Bind ::1 for the explicit loopback bind AND the all-interfaces bind
362
+ // (a browser resolving localhost → ::1 must still connect). Non-fatal:
354
363
  // IPv4-only hosts or a ::1 already in use just skip the mirror.
355
364
  let v6: ReturnType<typeof createServer> | undefined;
356
365
  const v4Handler = server.listeners("request")[0];
357
- if (host === "127.0.0.1" && v4Handler) {
366
+ if ((host === "127.0.0.1" || allInterfaces) && v4Handler) {
358
367
  v6 = createServer((r, s) =>
359
368
  (v4Handler as (a: IncomingMessage, b: ServerResponse) => void).call(
360
369
  server,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.20.2",
3
+ "version": "0.20.4",
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",
@@ -174,6 +174,20 @@ export const VC4B_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC4B");
174
174
  */
175
175
  export const VC4C_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC4C");
176
176
 
177
+ /**
178
+ * VC5A — PromptDagV1 + budgeted portfolio planner. Default ON.
179
+ * `MEGACOMPACT_VC5A=0` disables and is byte-identical to the predecessor
180
+ * (VC4C): no prompt DAG is built or validated, no budgeted plan is selected,
181
+ * the `vector_cortex_plan_selected` / `vector_cortex_plan_mandatory_overflow`
182
+ * events are never emitted, and the prompt continues to be built by the
183
+ * predecessor VC4C closure/reconstruction path (its goldens are unchanged).
184
+ * The builder/validator/portfolio functions are PURE — flag OFF gates the
185
+ * reporter + dashboard seam, never the arithmetic. This flag MUST also be a
186
+ * dashboard SETTINGS toggle (visible in config UI, never in EXCLUDED_SETTINGS),
187
+ * mirroring VC4A/VC4B/VC4C.
188
+ */
189
+ export const VC5A_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC5A");
190
+
177
191
  // ---------------------------------------------------------------------------
178
192
  // Breaker state machine constants (TRIAD_RESILIENCE.md §breaker).
179
193
  // Rolled numbers for one 60s window; VC0C consumes these at its breaker seam.
package/src/config.ts CHANGED
@@ -166,6 +166,7 @@ export {
166
166
  VC4A_ENABLED,
167
167
  VC4B_ENABLED,
168
168
  VC4C_ENABLED,
169
+ VC5A_ENABLED,
169
170
  BREAKER_WINDOW_MS,
170
171
  BREAKER_MIN_ATTEMPTS,
171
172
  BREAKER_PERF_FAILURES,
@@ -0,0 +1,83 @@
1
+ /**
2
+ * vector-cortex/planner/manifest.ts — plan manifest identity + pre-provider
3
+ * revalidation (VC5A).
4
+ *
5
+ * Split from `portfolio.ts` (which owns SELECTION) so each file keeps one
6
+ * concern and stays under the 300-line soft limit: this module owns the plan's
7
+ * IDENTITY and the last gate before a provider call.
8
+ *
9
+ * The manifest digest deliberately covers PER-NODE TOKEN COUNTS, which the DAG
10
+ * digest does not: token counts are a planner input rather than DAG structure.
11
+ * That is exactly what makes the sprint's unique failure injection detectable —
12
+ * mutating a node's token count after planning but before validation changes the
13
+ * recomputed manifest digest, so `validatePlanManifest` returns
14
+ * `PLN_MANIFEST_DIGEST_MISMATCH` and the plan never reaches the provider.
15
+ *
16
+ * Pure/deterministic: no storage, no console, no network (PREVENT-PI-004).
17
+ */
18
+
19
+ import { createHash } from "node:crypto";
20
+
21
+ import type { PlanCandidate, PlanV1 } from "./types.js";
22
+
23
+ /**
24
+ * Deterministic digest over a plan AND the token counts/utilities it was
25
+ * selected with. Hashing walks `selectedNodeIds` in its stored (sorted) order,
26
+ * so the digest is a pure function of the plan and the candidate facts.
27
+ *
28
+ * A node missing from `candidates` hashes as `-1`, so dropping a candidate is
29
+ * itself a detectable mutation rather than a silently-skipped field.
30
+ */
31
+ export function planManifestDigest(
32
+ plan: PlanV1,
33
+ candidates: readonly PlanCandidate[],
34
+ ): string {
35
+ const h = createHash("sha256");
36
+ h.update(plan.schema);
37
+ h.update(" ");
38
+ h.update(plan.dagDigest);
39
+ h.update(" ");
40
+ h.update(String(plan.tokenBudget));
41
+ h.update(" ");
42
+ h.update(String(plan.tokenTotal));
43
+ h.update(" ");
44
+ h.update(String(plan.dependencyHighWater));
45
+ const byId = new Map(candidates.map((c) => [c.nodeId, c]));
46
+ for (const id of plan.selectedNodeIds) {
47
+ const c = byId.get(id);
48
+ h.update("");
49
+ h.update(id);
50
+ h.update(" ");
51
+ // Token count is part of the identity — a post-plan mutation breaks it.
52
+ h.update(String(c?.tokenEstimate ?? -1));
53
+ h.update(" ");
54
+ h.update(String(c?.utility ?? -1));
55
+ }
56
+ return h.digest("hex");
57
+ }
58
+
59
+ /** The verdict of the pre-provider manifest revalidation. */
60
+ export type PlanManifestValidation =
61
+ | { readonly ok: true }
62
+ | { readonly ok: false; readonly code: "PLN_MANIFEST_DIGEST_MISMATCH" };
63
+
64
+ /**
65
+ * Re-validate a plan against the candidates AS THEY STAND NOW, immediately
66
+ * before a provider call. Returns `PLN_MANIFEST_DIGEST_MISMATCH` when the
67
+ * recomputed manifest digest disagrees with the digest pinned at planning time.
68
+ *
69
+ * This is the sprint's unique failure injection: a node token count mutated
70
+ * after planning but before validation is caught here, BEFORE the provider is
71
+ * invoked, rather than producing a prompt whose real cost differs from the
72
+ * admitted budget.
73
+ */
74
+ export function validatePlanManifest(
75
+ plan: PlanV1,
76
+ candidates: readonly PlanCandidate[],
77
+ pinnedDigest: string,
78
+ ): PlanManifestValidation {
79
+ if (planManifestDigest(plan, candidates) !== pinnedDigest) {
80
+ return { ok: false, code: "PLN_MANIFEST_DIGEST_MISMATCH" };
81
+ }
82
+ return { ok: true };
83
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * vector-cortex/planner/types.ts — `PlanV1` + the budget admission contract
3
+ * (VC5A, task 1).
4
+ *
5
+ * VC5A EXCLUSIVELY OWNS FRAMING + BUDGET ADMISSION (CONTRACTS §plan and
6
+ * closure). VC4C hands over `ClosureResult.mandatoryTokenEstimate`, a
7
+ * CONTENT-ONLY count with no prompt framing, no role tags and no separators, and
8
+ * VC4C never truncates a mandatory node nor reasons about a budget. This module
9
+ * adds the framing cost on top of that content estimate and decides admission:
10
+ *
11
+ * framed(node) = tokenEstimate + framingPerNode
12
+ * mandatoryCost = mandatoryTokenEstimate + framingPerNode * |mandatory|
13
+ * + framingOverhead
14
+ *
15
+ * If `mandatoryCost > tokenBudget` the planner returns
16
+ * `MANDATORY_CLOSURE_OVER_BUDGET` and the adapter demotes to C. Crucially it
17
+ * does so WITHOUT DROPPING EVIDENCE: the mandatory set is returned intact on the
18
+ * failure so the caller can report exactly what did not fit (the sprint bar:
19
+ * "over-budget mandatory closure never drops evidence and always demotes").
20
+ *
21
+ * The framing constants are CONFIGURABLE, never invented magic numbers: a caller
22
+ * supplies the profile it is actually rendering with, and the defaults below are
23
+ * documented as a conservative baseline rather than a measured provider fact.
24
+ *
25
+ * Pure types + registered conformance IDs: no storage, no console, no network
26
+ * (PREVENT-PI-004 / PREVENT-011).
27
+ */
28
+
29
+ /**
30
+ * The prompt framing cost model. These are the ONLY places framing enters the
31
+ * budget, keeping the VC4C content-only estimate cleanly separable.
32
+ *
33
+ * `perNode` is the per-node envelope (role tag + separator) a renderer adds
34
+ * around one node's content. `overhead` is the whole-prompt fixed cost (system
35
+ * preamble scaffolding, closing delimiters) charged once.
36
+ *
37
+ * Both DEFAULT to a conservative baseline and are overridable per call, so a
38
+ * provider profile with a measured framing cost (VC5B) supplies its own numbers
39
+ * rather than inheriting a guess.
40
+ */
41
+ export interface FramingProfile {
42
+ /** Tokens added around each selected node (role tag + separator). */
43
+ readonly perNode: number;
44
+ /** Fixed whole-prompt framing tokens charged once. */
45
+ readonly overhead: number;
46
+ }
47
+
48
+ /**
49
+ * Conservative default framing baseline. Documented as a BASELINE, not a
50
+ * measured provider constant: a real provider profile (VC5B) overrides it. Kept
51
+ * small and explicit so a default-driven plan is never silently over-optimistic.
52
+ */
53
+ export const DEFAULT_FRAMING: FramingProfile = { perNode: 4, overhead: 8 };
54
+
55
+ /**
56
+ * One candidate offered to the 0/1 portfolio. `mandatory` candidates are the
57
+ * closed set from VC4C and are admitted before any optional selection; optional
58
+ * candidates compete for the REMAINING budget.
59
+ *
60
+ * `sourceSeq` is the source ordering fact used as the FIRST tie-break after
61
+ * utility-per-token, so two equally efficient candidates resolve by source
62
+ * position (earlier wins) and then by ID bytes — a total, deterministic order.
63
+ */
64
+ export interface PlanCandidate {
65
+ readonly nodeId: string;
66
+ /** CONTENT-ONLY token estimate (framing is added by the planner). */
67
+ readonly tokenEstimate: number;
68
+ /** Selection value; higher is better. Ratio is `utility / framedTokens`. */
69
+ readonly utility: number;
70
+ /** Source sequence position, the first tie-break after the ratio. */
71
+ readonly sourceSeq: bigint;
72
+ /** True when the candidate is part of the mandatory closure. */
73
+ readonly mandatory: boolean;
74
+ }
75
+
76
+ /** Why a candidate was left out of the plan (recorded, never silent). */
77
+ export interface PlanOmission {
78
+ readonly nodeId: string;
79
+ readonly reason: "over-budget" | "incompatible" | "zero-utility";
80
+ }
81
+
82
+ /** Planner failure codes (registered PLN codes). */
83
+ export type PlanFailureCode =
84
+ /**
85
+ * The mandatory closure alone (with framing) exceeds `tokenBudget`. Evidence
86
+ * is NOT dropped — the mandatory set is returned intact and the adapter
87
+ * demotes to C.
88
+ */
89
+ | "MANDATORY_CLOSURE_OVER_BUDGET"
90
+ /** A candidate names a node absent from the DAG. */
91
+ | "PLN_UNKNOWN_NODE"
92
+ /** Two selected nodes declare mutual incompatibility. */
93
+ | "PLN_INCOMPATIBLE_SELECTION"
94
+ /** The DAG digest recorded in the plan no longer matches the DAG. */
95
+ | "PLN_MANIFEST_DIGEST_MISMATCH"
96
+ /** The budget itself is invalid (negative or non-finite). */
97
+ | "PLN_INVALID_BUDGET";
98
+
99
+ /**
100
+ * The accepted plan (CONTRACTS §plan and closure). `selectedNodeIds` is sorted
101
+ * for a stable manifest; `tokenTotal` is the FRAMED total and is guaranteed
102
+ * `<= tokenBudget` for every accepted plan.
103
+ */
104
+ export interface PlanV1 {
105
+ readonly schema: "plan-v1";
106
+ /** Digest of the DAG this plan was selected over (binds plan to structure). */
107
+ readonly dagDigest: string;
108
+ /** The selected node IDs, sorted by ID bytes. */
109
+ readonly selectedNodeIds: readonly string[];
110
+ /** The budget this plan was admitted against. */
111
+ readonly tokenBudget: number;
112
+ /** Framed token total of the selection; always `<= tokenBudget`. */
113
+ readonly tokenTotal: number;
114
+ /** Summed utility of the selection. */
115
+ readonly utilityTotal: number;
116
+ /** The durable authority high-water the plan's evidence depends on. */
117
+ readonly dependencyHighWater: bigint;
118
+ /** Candidates deliberately left out, with the reason. */
119
+ readonly omissions: readonly PlanOmission[];
120
+ }
121
+
122
+ /**
123
+ * The planner verdict. On failure the mandatory set is preserved so an
124
+ * over-budget closure can be reported WITHOUT dropping evidence.
125
+ */
126
+ export type PlanResult =
127
+ | { readonly ok: true; readonly plan: PlanV1 }
128
+ | {
129
+ readonly ok: false;
130
+ readonly code: PlanFailureCode;
131
+ /** The intact mandatory node IDs (never dropped on failure). */
132
+ readonly mandatory: readonly string[];
133
+ /** The framed cost of the mandatory set that could not be admitted. */
134
+ readonly mandatoryCost: number;
135
+ /** The budget the mandatory cost was measured against. */
136
+ readonly tokenBudget: number;
137
+ };
138
+
139
+ /**
140
+ * The triad mode VC5A selects (TRIAD_RESILIENCE). A/B/C are INDEPENDENT
141
+ * algorithms:
142
+ *
143
+ * A = the 0/1 portfolio optimizer (ratio-ordered admission);
144
+ * B = a stable greedy closed planner, forced by an A exception — it shares no
145
+ * ratio ordering with A and admits strictly in source order;
146
+ * C = the predecessor prompt, forced by mandatory overflow; it states its loss
147
+ * of old semantic context (continuity, NOT completeness).
148
+ */
149
+ export type PlanMode = "A" | "B" | "C";
150
+
151
+ /** The two structured events the VC5A reporter emits. */
152
+ export type PlanEventName =
153
+ | "vector_cortex_plan_selected"
154
+ | "vector_cortex_plan_mandatory_overflow";
155
+
156
+ /** Injected emit callback — same (event, fields) shape as the other VC seams. */
157
+ export type PlanEmitter = (
158
+ event: PlanEventName,
159
+ fields: Record<string, unknown>,
160
+ ) => void;
161
+
162
+ /** Typed, best-effort reporter bound to the two plan event names. */
163
+ export interface PlanReporter {
164
+ readonly planSelected: (fields: Record<string, unknown>) => void;
165
+ readonly planMandatoryOverflow: (fields: Record<string, unknown>) => void;
166
+ }
167
+
168
+ /**
169
+ * Aggregate-only plan metrics for the dashboard (counts/tokens only, never
170
+ * prompt text or node payloads).
171
+ */
172
+ export interface PlanMetricsV1 {
173
+ readonly plansSelected: number;
174
+ readonly mandatoryOverflows: number;
175
+ readonly nodesSelected: number;
176
+ readonly tokenTotal: number;
177
+ }
178
+
179
+ /**
180
+ * Registered PLN conformance ID range (PLN-001..020). The acceptance aggregator
181
+ * reads these rows from the v2 manifest and asserts each returns its manifest
182
+ * `ok`/`code`.
183
+ */
184
+ export const PLN_IDS: readonly string[] = Array.from(
185
+ { length: 20 },
186
+ (_v, i) => `PLN-${String(i + 1).padStart(3, "0")}`,
187
+ );
188
+
189
+ /** Named VC5A planner conformance assertions (the sprint's headline rows). */
190
+ export const PLAN_NAMED_IDS = ["PLN-MANDATORY-002", "PLN-TIE-003"] as const;