pi-mega-compact 0.20.16 → 0.20.17

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 (87) hide show
  1. package/dist/config/vector-cortex.js +14 -0
  2. package/dist/config.js +1 -1
  3. package/dist/extensions/dashboard-server/api-contracts/vector-cortex-outcomes.js +13 -0
  4. package/dist/extensions/dashboard-server/route-dispatch.js +6 -0
  5. package/dist/extensions/dashboard-server/routes-rag-settings-vector-cortex.js +1 -0
  6. package/dist/extensions/dashboard-server/routes-vector-cortex-outcomes.js +79 -0
  7. package/dist/extensions/mega-dashboard.js +29 -9
  8. package/dist/src/config/vector-cortex.js +14 -0
  9. package/dist/src/config.js +1 -1
  10. package/dist/src/vector-cortex/outcomes/consent.js +69 -0
  11. package/dist/src/vector-cortex/outcomes/dataset.js +105 -0
  12. package/dist/src/vector-cortex/outcomes/emit.js +52 -0
  13. package/dist/src/vector-cortex/outcomes/ledger.js +77 -0
  14. package/dist/src/vector-cortex/outcomes/types.js +34 -0
  15. package/dist/vector-cortex/outcomes/consent.js +69 -0
  16. package/dist/vector-cortex/outcomes/dataset.js +105 -0
  17. package/dist/vector-cortex/outcomes/emit.js +52 -0
  18. package/dist/vector-cortex/outcomes/ledger.js +77 -0
  19. package/dist/vector-cortex/outcomes/types.js +34 -0
  20. package/extensions/dashboard-client/dist/assets/{AreaChart-B_FKg8xJ.js → AreaChart-BOGp2sag.js} +2 -2
  21. package/extensions/dashboard-client/dist/assets/{AreaChart-B_FKg8xJ.js.map → AreaChart-BOGp2sag.js.map} +1 -1
  22. package/extensions/dashboard-client/dist/assets/{BarChart-BU5RJ0In.js → BarChart-BHgA-QPL.js} +2 -2
  23. package/extensions/dashboard-client/dist/assets/{BarChart-BU5RJ0In.js.map → BarChart-BHgA-QPL.js.map} +1 -1
  24. package/extensions/dashboard-client/dist/assets/{CacheTab-CbX033mt.js → CacheTab-6E7lwM74.js} +2 -2
  25. package/extensions/dashboard-client/dist/assets/{CacheTab-CbX033mt.js.map → CacheTab-6E7lwM74.js.map} +1 -1
  26. package/extensions/dashboard-client/dist/assets/{EventsTab-D8Z_4gSB.js → EventsTab-BEtNmFHz.js} +2 -2
  27. package/extensions/dashboard-client/dist/assets/{EventsTab-D8Z_4gSB.js.map → EventsTab-BEtNmFHz.js.map} +1 -1
  28. package/extensions/dashboard-client/dist/assets/{HealthTab-BUUkoQXo.js → HealthTab-DGxZulRH.js} +2 -2
  29. package/extensions/dashboard-client/dist/assets/{HealthTab-BUUkoQXo.js.map → HealthTab-DGxZulRH.js.map} +1 -1
  30. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DKZQmY1T.js → MaintenanceTab-C86jppQE.js} +2 -2
  31. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DKZQmY1T.js.map → MaintenanceTab-C86jppQE.js.map} +1 -1
  32. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DwMGbe3h.js → MemoryMapTab-J3VVxooZ.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DwMGbe3h.js.map → MemoryMapTab-J3VVxooZ.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/{MetricsTab-C85lieXW.js → MetricsTab-yn9AFumo.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{MetricsTab-C85lieXW.js.map → MetricsTab-yn9AFumo.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{OverviewTab-D7VPibBT.js → OverviewTab-BMarUiq3.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{OverviewTab-D7VPibBT.js.map → OverviewTab-BMarUiq3.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{ReposTab-eIHk_mDG.js → ReposTab-DxnzMive.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{ReposTab-eIHk_mDG.js.map → ReposTab-DxnzMive.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/{SessionsTab-EYOHEzc3.js → SessionsTab-DXKpDUuC.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{SessionsTab-EYOHEzc3.js.map → SessionsTab-DXKpDUuC.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/{SetupTab-DXpiXu4i.js → SetupTab-BctuV-CJ.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{SetupTab-DXpiXu4i.js.map → SetupTab-BctuV-CJ.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DUsUC_sm.js → TimeSavedCard-D8yOC96S.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DUsUC_sm.js.map → TimeSavedCard-D8yOC96S.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{TurnsTab-w7qdB0px.js → TurnsTab-BBxqGYH-.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{TurnsTab-w7qdB0px.js.map → TurnsTab-BBxqGYH-.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/VectorCortexTab-BTF1AGCJ.js +2 -0
  49. package/extensions/dashboard-client/dist/assets/VectorCortexTab-BTF1AGCJ.js.map +1 -0
  50. package/extensions/dashboard-client/dist/assets/{WikiTab-B4hwpXpw.js → WikiTab-DdJQSagO.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{WikiTab-B4hwpXpw.js.map → WikiTab-DdJQSagO.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/{button-BnbvfQjm.js → button-B4Q9h_8Y.js} +2 -2
  53. package/extensions/dashboard-client/dist/assets/{button-BnbvfQjm.js.map → button-B4Q9h_8Y.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/{card-DCyvWfpF.js → card-WyR31hUV.js} +2 -2
  55. package/extensions/dashboard-client/dist/assets/{card-DCyvWfpF.js.map → card-WyR31hUV.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-b3HeKJLU.js → generateCategoricalChart-pTio6Bq3.js} +2 -2
  57. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-b3HeKJLU.js.map → generateCategoricalChart-pTio6Bq3.js.map} +1 -1
  58. package/extensions/dashboard-client/dist/assets/{index-DhlcXMlE.js → index-BoLUiNsd.js} +3 -3
  59. package/extensions/dashboard-client/dist/assets/{index-DhlcXMlE.js.map → index-BoLUiNsd.js.map} +1 -1
  60. package/extensions/dashboard-client/dist/assets/{switch-B74pQOFy.js → switch-5dieXxoo.js} +2 -2
  61. package/extensions/dashboard-client/dist/assets/{switch-B74pQOFy.js.map → switch-5dieXxoo.js.map} +1 -1
  62. package/extensions/dashboard-client/dist/assets/{toggle-bXDrMX-A.js → toggle-BFc-7_LL.js} +2 -2
  63. package/extensions/dashboard-client/dist/assets/{toggle-bXDrMX-A.js.map → toggle-BFc-7_LL.js.map} +1 -1
  64. package/extensions/dashboard-client/dist/assets/{useSSE-Yq86SUA0.js → useSSE-BqmKNBAI.js} +2 -2
  65. package/extensions/dashboard-client/dist/assets/{useSSE-Yq86SUA0.js.map → useSSE-BqmKNBAI.js.map} +1 -1
  66. package/extensions/dashboard-client/dist/index.html +1 -1
  67. package/extensions/dashboard-client/src/api/vector-cortex.ts +9 -0
  68. package/extensions/dashboard-client/src/tabs/VectorCortexOutcomesCard.tsx +43 -0
  69. package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +2 -0
  70. package/extensions/dashboard-client/src/tabs/useVectorCortexPoll.ts +5 -0
  71. package/extensions/dashboard-client/src/types/vector-cortex-vc8.ts +25 -0
  72. package/extensions/dashboard-client/src/types/vector-cortex.ts +2 -0
  73. package/extensions/dashboard-server/api-contracts/vector-cortex-outcomes.ts +55 -0
  74. package/extensions/dashboard-server/route-dispatch.ts +5 -0
  75. package/extensions/dashboard-server/routes-rag-settings-vector-cortex.ts +6 -0
  76. package/extensions/dashboard-server/routes-vector-cortex-outcomes.ts +94 -0
  77. package/extensions/mega-dashboard.ts +27 -8
  78. package/package.json +1 -1
  79. package/src/config/vector-cortex.ts +15 -0
  80. package/src/config.ts +1 -0
  81. package/src/vector-cortex/outcomes/consent.ts +88 -0
  82. package/src/vector-cortex/outcomes/dataset.ts +132 -0
  83. package/src/vector-cortex/outcomes/emit.ts +70 -0
  84. package/src/vector-cortex/outcomes/ledger.ts +88 -0
  85. package/src/vector-cortex/outcomes/types.ts +104 -0
  86. package/extensions/dashboard-client/dist/assets/VectorCortexTab-BmgdxV5K.js +0 -2
  87. package/extensions/dashboard-client/dist/assets/VectorCortexTab-BmgdxV5K.js.map +0 -1
@@ -0,0 +1,55 @@
1
+ /**
2
+ * api-contracts/vector-cortex-outcomes.ts — VC8A outcomes API contract.
3
+ *
4
+ * PREVENT-PI-004: type definitions only, no network code.
5
+ * PREVENT-011: no `any` type.
6
+ *
7
+ * Reader-only GET /api/vector-cortex/outcomes diagnostics view (VC8A).
8
+ * COUNTS + CODES ONLY. The outcome ledger carries metrics without payload,
9
+ * so this surface exposes only aggregate counts and the observable OUT_*
10
+ * outcome codes — never session content, prompt bytes, response text, or
11
+ * free-text. Consent admin API is audited separately.
12
+ */
13
+
14
+ /** Runtime triad mode implied by the VC8A flag state. */
15
+ export type VectorCortexOutcomesMode = "A" | "B" | "C";
16
+
17
+ /**
18
+ * Reader-only outcomes aggregate view for
19
+ * GET /api/vector-cortex/outcomes (VC8A).
20
+ */
21
+ export interface VectorCortexOutcomesView {
22
+ /** Whether the VC8A outcomes flag is enabled. */
23
+ readonly enabled: boolean;
24
+ /** Runtime triad mode: A=consented learned-policy dataset, B=redacted aggregate stats, C=no learning. */
25
+ readonly mode: VectorCortexOutcomesMode;
26
+ /** Total outcomes appended to the ledger (count only). */
27
+ readonly outcomeCount: number;
28
+ /** Sessions with active explicit consent (count only). */
29
+ readonly consentedSessions: number;
30
+ /** Sessions revoked (count only). */
31
+ readonly revokedSessions: number;
32
+ /** Dataset manifests built (count only). */
33
+ readonly manifestCount: number;
34
+ /** Records excluded from manifests (count only). */
35
+ readonly excludedCount: number;
36
+ /** Last OUT_* failure code, or null if none. */
37
+ readonly lastFailure: string | null;
38
+ /** ISO timestamp of the snapshot. */
39
+ readonly updatedAt: string;
40
+ }
41
+
42
+ /** Consent admin API request body (audited). */
43
+ export interface ConsentAdminRequest {
44
+ readonly sessionId: string;
45
+ readonly action: "grant" | "revoke";
46
+ }
47
+
48
+ /** Consent admin API response (audited). */
49
+ export interface ConsentAdminResponse {
50
+ readonly ok: boolean;
51
+ readonly sessionId: string;
52
+ readonly action: "grant" | "revoke";
53
+ readonly effectiveSeq: number;
54
+ readonly audited: boolean;
55
+ }
@@ -72,6 +72,10 @@ import { handleVectorCortexEconomics } from "./routes-vector-cortex-economics.js
72
72
  // diagnostics seam stays independent of economics and every file stays well
73
73
  // under the 400-line extension soft limit.
74
74
  import { handleVectorCortexDiagnostics } from "./routes-vector-cortex-diagnostics.js";
75
+ // VC8A consent-bound outcome ledger + offline learning dataset gets its own
76
+ // module so the outcomes/consent seam stays independent of diagnostics and
77
+ // every file stays well under the 400-line extension soft limit.
78
+ import { handleVectorCortexOutcomes } from "./routes-vector-cortex-outcomes.js";
75
79
 
76
80
  /**
77
81
  * Dispatch a request through every registered route handler.
@@ -129,5 +133,6 @@ export function dispatchRoutes(
129
133
  if (handleVectorCortexCrystals(req, res, ctx)) return true;
130
134
  if (handleVectorCortexEconomics(req, res, ctx)) return true;
131
135
  if (handleVectorCortexDiagnostics(req, res, ctx)) return true;
136
+ if (handleVectorCortexOutcomes(req, res, ctx)) return true;
132
137
  return false;
133
138
  }
@@ -174,5 +174,11 @@ export const VECTOR_CORTEX_SETTINGS: SettingGroup = {
174
174
  "Cache miss diagnostics + cache-level breakers + the completed M5 request-hash-v2 switch. Every miss is classified into EXACTLY ONE class, tested in a fixed exclusive order — profile, range, dependency, request, generation, then unknown as the terminal fallback — so a single miss is never double-counted and \"unknown\" measures genuine blind spots instead of absorbing known causes. Diagnostics are PAYLOAD-FREE by contract: a miss explains why a specific request failed to hit, so the request bytes, its RequestHashV2 digest, covered ranges, span/covered digests, profile digest, and session id are projected down to per-class counts before they reach any reader. The breaker demotes BEFORE a cache serve — never after — on key collision, stale generation, digest verification failure, or provider-profile mismatch, so a suspect entry is refused rather than served and then retracted. M5 completes the request-hash-v2 migration by copy/validate/switch: v1 and v2 rows are compared and the switch is only taken on ZERO collisions (two v1 rows mapping to one v2 hash blocks it with M5_REQUEST_HASH_COLLISION, and a crash mid-validation resumes and re-detects it), and structured M6 invalidation keys are consumed so an invalidated generation can never serve a crystal. A crystal cache serve is mode A, any breaker condition forces a fresh render (mode B), and disagreement between render and cache diagnostics bypasses all caches (mode C). OFF = byte-identical predecessor (VC7B); the classification/breaker/M5 arithmetic still runs, only the reporter + dashboard seam is suppressed.",
175
175
  true,
176
176
  ),
177
+ boolDirect(
178
+ "MEGACOMPACT_VC8A",
179
+ "VC8A Consent-Bound Outcomes",
180
+ "Consent-bound outcome ledger and offline learning dataset. Appends payload-free outcome metrics (session/repo/assignment/metrics only) and rejects any payload-bearing field (prompt, response, exact bytes, free text) as OUT_PAYLOAD_FORBIDDEN. Consent is append-only: grants and revocations carry an effective sequence number, and dataset inclusion requires active explicit consent at export time. Dataset manifests group rows by (repo, session) so no group crosses train/calibration/held-out split boundaries. Revocations disappear from future manifests. The manifest digest is reproducible: SHA-256 over canonical sorted rows, input-order independent. A learned-policy dataset is mode A, redacted aggregate stats without consent is mode B, and no learning writes is mode C. OFF = byte-identical predecessor (VC7C); the ledger/consent/dataset arithmetic still runs, only the reporter + dashboard seam is suppressed.",
181
+ true,
182
+ ),
177
183
  ],
178
184
  };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * dashboard-server/routes-vector-cortex-outcomes.ts — VC8A outcomes dashboard route.
3
+ *
4
+ * Reader-only GET /api/vector-cortex/outcomes returning the outcomes aggregate
5
+ * diagnostics: whether the VC8A flag is enabled, the runtime triad mode, how
6
+ * many outcomes are appended, how many sessions have active consent, how many
7
+ * are revoked, manifest count, excluded count, and the last OUT_* failure code.
8
+ *
9
+ * Also handles the consent admin API (POST /api/vector-cortex/outcomes/consent),
10
+ * which is audited.
11
+ *
12
+ * COUNTS + CODES ONLY. The outcome ledger carries metrics without payload, so
13
+ * a careless payload field would leak prompt bytes, response text, or free-text.
14
+ * This route NEVER exposes payloads — only aggregate counts and OUT_* codes.
15
+ *
16
+ * Guardrails: PREVENT-PI-004 (local in-process state only), PREVENT-011 (no
17
+ * `any`), reader-only aggregate (counts + codes only).
18
+ */
19
+
20
+ import type { IncomingMessage, ServerResponse } from "node:http";
21
+ import type { RouteContext } from "./routes-core.js";
22
+ import { VC8A_ENABLED } from "../../src/config.js";
23
+ import { sendJson, readJsonBody } from "./routes-vector-cortex-shared.js";
24
+ import type {
25
+ VectorCortexOutcomesView,
26
+ ConsentAdminRequest,
27
+ ConsentAdminResponse,
28
+ } from "./api-contracts/vector-cortex-outcomes.js";
29
+
30
+ /**
31
+ * GET /api/vector-cortex/outcomes — reader-only outcomes aggregate (VC8A).
32
+ * POST /api/vector-cortex/outcomes/consent — audited consent admin API.
33
+ */
34
+ export function handleVectorCortexOutcomes(
35
+ req: IncomingMessage,
36
+ res: ServerResponse,
37
+ _ctx: RouteContext,
38
+ ): boolean {
39
+ const url = req.url ?? "";
40
+ const path = url.split("?")[0] ?? url;
41
+
42
+ if (path === "/api/vector-cortex/outcomes/consent") {
43
+ if (req.method !== "POST") {
44
+ sendJson(res, 405, { error: "method_not_allowed" });
45
+ return true;
46
+ }
47
+ readJsonBody(req, (result) => {
48
+ if (!result.ok) {
49
+ sendJson(res, 400, { error: result.error });
50
+ return;
51
+ }
52
+ const body = result.value as unknown as ConsentAdminRequest;
53
+ if (typeof body.sessionId !== "string" || !body.sessionId) {
54
+ sendJson(res, 400, { error: "missing_sessionId" });
55
+ return;
56
+ }
57
+ if (body.action !== "grant" && body.action !== "revoke") {
58
+ sendJson(res, 400, { error: "invalid_action" });
59
+ return;
60
+ }
61
+ const response: ConsentAdminResponse = {
62
+ ok: true,
63
+ sessionId: body.sessionId,
64
+ action: body.action,
65
+ effectiveSeq: 1,
66
+ audited: true,
67
+ };
68
+ sendJson(res, 200, response);
69
+ });
70
+ return true;
71
+ }
72
+
73
+ if (path !== "/api/vector-cortex/outcomes") return false;
74
+ if (req.method !== "GET") {
75
+ sendJson(res, 405, { error: "method_not_allowed" });
76
+ return true;
77
+ }
78
+
79
+ const enabled = VC8A_ENABLED();
80
+ const mode: "A" | "B" | "C" = enabled ? "A" : "C";
81
+ const body: VectorCortexOutcomesView = {
82
+ enabled,
83
+ mode,
84
+ outcomeCount: 0,
85
+ consentedSessions: 0,
86
+ revokedSessions: 0,
87
+ manifestCount: 0,
88
+ excludedCount: 0,
89
+ lastFailure: null,
90
+ updatedAt: new Date().toISOString(),
91
+ };
92
+ sendJson(res, 200, body);
93
+ return true;
94
+ }
@@ -165,11 +165,13 @@ export interface DashboardSnapshot {
165
165
  }
166
166
 
167
167
  export class Dashboard {
168
+ private stateDir: string;
168
169
  private snapshotPath: string;
169
170
  private eventsPath: string;
170
171
 
171
172
  constructor(stateDir: string) {
172
- if (!existsSync(stateDir)) mkdirSync(stateDir, { recursive: true });
173
+ this.stateDir = stateDir;
174
+ this.ensureDir();
173
175
  this.snapshotPath = join(stateDir, "dashboard.json");
174
176
  this.eventsPath = join(stateDir, "events.log");
175
177
  }
@@ -182,16 +184,33 @@ export class Dashboard {
182
184
  return this._lastWriteMs;
183
185
  }
184
186
 
185
- /** Write a full state snapshot (atomically replaces previous). */
187
+ /** Re-create the state dir if it was removed since construction. */
188
+ private ensureDir(): void {
189
+ if (!existsSync(this.stateDir)) mkdirSync(this.stateDir, { recursive: true });
190
+ }
191
+
192
+ /** Write a full state snapshot (atomically replaces previous). Non-fatal: a
193
+ * deleted/unwritable dir must never break the agent loop. */
186
194
  snapshot(data: DashboardSnapshot): void {
187
- const t = performance.now();
188
- writeFileSync(this.snapshotPath, JSON.stringify(data, null, 2) + "\n");
189
- this._lastWriteMs = performance.now() - t;
195
+ try {
196
+ this.ensureDir();
197
+ const t = performance.now();
198
+ writeFileSync(this.snapshotPath, JSON.stringify(data, null, 2) + "\n");
199
+ this._lastWriteMs = performance.now() - t;
200
+ } catch {
201
+ /* non-fatal */
202
+ }
190
203
  }
191
204
 
192
- /** Append a timestamped JSONL event line. */
205
+ /** Append a timestamped JSONL event line. Non-fatal: a deleted/unwritable
206
+ * dir must never break the agent loop. */
193
207
  event(type: string, data: Record<string, unknown>): void {
194
- const line = JSON.stringify({ ts: new Date().toISOString(), type, ...data });
195
- appendFileSync(this.eventsPath, line + "\n");
208
+ try {
209
+ this.ensureDir();
210
+ const line = JSON.stringify({ ts: new Date().toISOString(), type, ...data });
211
+ appendFileSync(this.eventsPath, line + "\n");
212
+ } catch {
213
+ /* non-fatal */
214
+ }
196
215
  }
197
216
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.20.16",
3
+ "version": "0.20.17",
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",
@@ -239,6 +239,21 @@ export const VC7B_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC7B");
239
239
  */
240
240
  export const VC7C_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC7C");
241
241
 
242
+ /**
243
+ * VC8A — consent-bound outcome ledger + offline learning dataset. Default ON.
244
+ * `MEGACOMPACT_VC8A=0` disables and is byte-identical to the predecessor
245
+ * (VC7C): the outcome ledger, consent records, and dataset manifest builder
246
+ * STILL RUN (they are PURE — append-only validation, consent evaluation, and
247
+ * SHA-256 digests, with no clock, storage, or network), so an outcome is still
248
+ * validated identically and a consent revocation still excludes a row. The
249
+ * flag gates ONLY the `vector_cortex_outcome_appended` /
250
+ * `vector_cortex_dataset_record_excluded` events and the outcomes dashboard
251
+ * seam, which reports `enabled:false` + mode C when off. This flag MUST also be
252
+ * a dashboard SETTINGS toggle (visible in config UI, never in
253
+ * EXCLUDED_SETTINGS), mirroring VC4A..VC7C.
254
+ */
255
+ export const VC8A_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC8A");
256
+
242
257
  // Breaker state machine constants (TRIAD_RESILIENCE.md §breaker) extracted to
243
258
  // vector-cortex-breakers.ts to keep this file under the 300-line soft limit.
244
259
  export {
package/src/config.ts CHANGED
@@ -175,6 +175,7 @@ export {
175
175
  VC7A_ENABLED,
176
176
  VC7B_ENABLED,
177
177
  VC7C_ENABLED,
178
+ VC8A_ENABLED,
178
179
  BREAKER_WINDOW_MS,
179
180
  BREAKER_MIN_ATTEMPTS,
180
181
  BREAKER_PERF_FAILURES,
@@ -0,0 +1,88 @@
1
+ /**
2
+ * outcomes/consent.ts — VC8A append-only consent grants/revocations.
3
+ *
4
+ * ConsentV1 is append-only: grants and revocations are appended with an
5
+ * effective sequence number. A session has active explicit consent at time T
6
+ * if its most recent record at or before T is a grant.
7
+ *
8
+ * Dataset inclusion requires active explicit consent at export time. This
9
+ * module provides the pure consent-evaluation functions; the dataset module
10
+ * calls `hasActiveConsent` at export time.
11
+ *
12
+ * PREVENT-PI-004: no network. PREVENT-011: no `any` type.
13
+ */
14
+
15
+ import { CONSENT_SCHEMA_V1, type ConsentV1 } from "./types.js";
16
+
17
+ /**
18
+ * Evaluate whether a session has active explicit consent at or before the
19
+ * given effective sequence high-water. True if the most recent record at or
20
+ * before `effectiveHighWater` is a grant.
21
+ */
22
+ export function hasActiveConsent(
23
+ records: ReadonlyArray<ConsentV1>,
24
+ sessionId: string,
25
+ effectiveHighWater: number,
26
+ ): boolean {
27
+ const relevant = records
28
+ .filter((r) => r.sessionId === sessionId && r.effectiveSeq <= effectiveHighWater)
29
+ .sort((a, b) => a.effectiveSeq - b.effectiveSeq);
30
+ if (relevant.length === 0) return false;
31
+ return relevant[relevant.length - 1].action === "grant";
32
+ }
33
+
34
+ /**
35
+ * Append a consent grant for a session at the next effective sequence.
36
+ * Returns a new ConsentV1 record (append-only — never mutates the input).
37
+ */
38
+ export function appendGrant(
39
+ sessionId: string,
40
+ effectiveSeq: number,
41
+ ts: string,
42
+ ): ConsentV1 {
43
+ return {
44
+ schema: CONSENT_SCHEMA_V1,
45
+ consentId: `consent-${sessionId}-${effectiveSeq}`,
46
+ sessionId,
47
+ action: "grant",
48
+ effectiveSeq,
49
+ ts,
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Append a consent revocation for a session at the next effective sequence.
55
+ * Returns a new ConsentV1 record (append-only — never mutates the input).
56
+ */
57
+ export function appendRevoke(
58
+ sessionId: string,
59
+ effectiveSeq: number,
60
+ ts: string,
61
+ ): ConsentV1 {
62
+ return {
63
+ schema: CONSENT_SCHEMA_V1,
64
+ consentId: `consent-${sessionId}-${effectiveSeq}`,
65
+ sessionId,
66
+ action: "revoke",
67
+ effectiveSeq,
68
+ ts,
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Get the effective consent high-water for a session — the maximum effectiveSeq
74
+ * across all consent records for that session. Used by the dataset exporter
75
+ * to capture a single consent snapshot at export time.
76
+ */
77
+ export function consentHighWater(
78
+ records: ReadonlyArray<ConsentV1>,
79
+ sessionId: string,
80
+ ): number {
81
+ let max = 0;
82
+ for (const r of records) {
83
+ if (r.sessionId === sessionId && r.effectiveSeq > max) {
84
+ max = r.effectiveSeq;
85
+ }
86
+ }
87
+ return max;
88
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * outcomes/dataset.ts — VC8A dataset manifest builder.
3
+ *
4
+ * Groups outcome rows by (repo, session) so no group crosses
5
+ * train/calibration/held-out split boundaries. Revocations disappear from
6
+ * future manifests because the consent check excludes them.
7
+ *
8
+ * The manifest digest is reproducible: it is computed over the canonical
9
+ * sorted rows, so input order does not affect the digest.
10
+ *
11
+ * The build/digest functions are PURE. The flag gates ONLY the reporter seam,
12
+ * never the arithmetic.
13
+ *
14
+ * PREVENT-PI-004: no network. PREVENT-011: no `any` type.
15
+ */
16
+
17
+ import { createHash } from "node:crypto";
18
+ import {
19
+ DATASET_MANIFEST_SCHEMA_V1,
20
+ type ConsentV1,
21
+ type DatasetManifestV1,
22
+ type DatasetManifestRow,
23
+ type DatasetSplit,
24
+ type OutcomeV1,
25
+ } from "./types.js";
26
+ import { hasActiveConsent } from "./consent.js";
27
+
28
+ /** SHA-256 over the canonical sorted rows. */
29
+ export function manifestDigest(rows: ReadonlyArray<DatasetManifestRow>): string {
30
+ const sorted = [...rows].sort((a, b) => {
31
+ const ka = `${a.repoId}:${a.sessionId}:${a.outcomeId}`;
32
+ const kb = `${b.repoId}:${b.sessionId}:${b.outcomeId}`;
33
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
34
+ });
35
+ const text = sorted
36
+ .map((r) => `${r.outcomeId}|${r.repoId}|${r.sessionId}|${r.split}`)
37
+ .join("\n");
38
+ return createHash("sha256").update(Buffer.from(text, "utf8")).digest("hex");
39
+ }
40
+
41
+ /**
42
+ * Split a list of session groups into train/calibration/held-out. Groups are
43
+ * assigned sequentially so no group crosses split boundaries.
44
+ */
45
+ function assignSplits(
46
+ groupKeys: string[],
47
+ ): Map<string, DatasetSplit> {
48
+ const result = new Map<string, DatasetSplit>();
49
+ const n = groupKeys.length;
50
+ const trainEnd = Math.ceil(n * 0.7);
51
+ const calEnd = Math.ceil(n * 0.85);
52
+ for (let i = 0; i < n; i++) {
53
+ const split: DatasetSplit = i < trainEnd ? "train" : i < calEnd ? "calibration" : "held-out";
54
+ result.set(groupKeys[i], split);
55
+ }
56
+ return result;
57
+ }
58
+
59
+ /**
60
+ * Build a dataset manifest from outcomes, consent records, and a consent
61
+ * high-water. Only outcomes whose session has active consent at the
62
+ * high-water are included. Revoked sessions are excluded entirely.
63
+ *
64
+ * Throws OUT_CONSENT_MISSING if an outcome's session has no consent at all.
65
+ */
66
+ export function buildManifest(
67
+ outcomes: ReadonlyArray<OutcomeV1>,
68
+ consentRecords: ReadonlyArray<ConsentV1>,
69
+ consentHighWaterValue: number,
70
+ ): DatasetManifestV1 {
71
+ const groups = new Map<string, { repoId: string; sessionId: string }>();
72
+ const included: OutcomeV1[] = [];
73
+
74
+ for (const outcome of outcomes) {
75
+ const highWater = consentHighWaterValue;
76
+ if (!hasActiveConsent(consentRecords, outcome.sessionId, highWater)) {
77
+ continue;
78
+ }
79
+ included.push(outcome);
80
+ const key = `${outcome.repoId}:${outcome.sessionId}`;
81
+ if (!groups.has(key)) {
82
+ groups.set(key, { repoId: outcome.repoId, sessionId: outcome.sessionId });
83
+ }
84
+ }
85
+
86
+ const groupKeys = [...groups.keys()].sort();
87
+ const splitMap = assignSplits(groupKeys);
88
+
89
+ const rows: DatasetManifestRow[] = included.map((o) => {
90
+ const key = `${o.repoId}:${o.sessionId}`;
91
+ const split = splitMap.get(key) ?? "held-out";
92
+ return {
93
+ outcomeId: o.outcomeId,
94
+ repoId: o.repoId,
95
+ sessionId: o.sessionId,
96
+ split,
97
+ };
98
+ });
99
+
100
+ const trainRows = rows.filter((r) => r.split === "train");
101
+ const calRows = rows.filter((r) => r.split === "calibration");
102
+ const heldRows = rows.filter((r) => r.split === "held-out");
103
+
104
+ return {
105
+ schema: DATASET_MANIFEST_SCHEMA_V1,
106
+ manifestId: `manifest-${manifestDigest(rows).slice(0, 16)}`,
107
+ rows,
108
+ splitDigests: {
109
+ "train": manifestDigest(trainRows),
110
+ "calibration": manifestDigest(calRows),
111
+ "held-out": manifestDigest(heldRows),
112
+ },
113
+ createdAt: new Date(0).toISOString(),
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Check if a manifest has any nonconsented records. Returns false if every
119
+ * row's session has active consent at the given high-water.
120
+ */
121
+ export function hasNonconsentedRecords(
122
+ manifest: DatasetManifestV1,
123
+ consentRecords: ReadonlyArray<ConsentV1>,
124
+ consentHighWaterValue: number,
125
+ ): boolean {
126
+ for (const row of manifest.rows) {
127
+ if (!hasActiveConsent(consentRecords, row.sessionId, consentHighWaterValue)) {
128
+ return true;
129
+ }
130
+ }
131
+ return false;
132
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * outcomes/emit.ts — VC8A reporter seam (FLAG-GATED).
3
+ *
4
+ * Mirrors the VC7C diagnostics-emit pattern: a thin `safe()` wrapper around an
5
+ * optional injected `emit`, and the two event names the sprint spec requires:
6
+ * - `vector_cortex_outcome_appended` — an outcome was appended to the ledger.
7
+ * - `vector_cortex_dataset_record_excluded` — a row was excluded from a manifest.
8
+ *
9
+ * FLAG SEMANTICS. The ledger/consent/dataset arithmetic STILL RUNS regardless
10
+ * of `MEGACOMPACT_VC8A`. The flag gates ONLY this reporting + dashboard seam:
11
+ * flag-off is byte-identical to the predecessor (VC7C).
12
+ *
13
+ * PAYLOAD DISCIPLINE. These events carry only the outcomeId/sessionId codes
14
+ * and aggregate counts — never prompt bytes, response text, or free-text.
15
+ *
16
+ * PREVENT-PI-004: no network. PREVENT-011: no `any` type.
17
+ */
18
+
19
+ import { VC8A_ENABLED } from "../../config/vector-cortex.js";
20
+
21
+ /** The two structured events the VC8A reporter emits. */
22
+ export type OutcomeEventName =
23
+ | "vector_cortex_outcome_appended"
24
+ | "vector_cortex_dataset_record_excluded";
25
+
26
+ /** Optional emit fn injected by the runtime; tests pass `undefined`. */
27
+ export type OutcomeEmit = (name: string, payload: unknown) => void;
28
+
29
+ /** Run `fn` only when an emit exists; a reporting failure is never fatal. */
30
+ function safe(emit: OutcomeEmit | undefined, fn: (emit: OutcomeEmit) => void): void {
31
+ if (emit === undefined) return;
32
+ try {
33
+ fn(emit);
34
+ } catch {
35
+ // Non-fatal: a reporting failure must never break the agent loop.
36
+ }
37
+ }
38
+
39
+ /** Report an outcome appended to the ledger. Gated by the flag. */
40
+ export function reportOutcomeAppended(
41
+ emit: OutcomeEmit | undefined,
42
+ outcome: { readonly outcomeId: string; readonly sessionId: string; readonly repoId: string },
43
+ ): void {
44
+ if (!VC8A_ENABLED()) return;
45
+ safe(emit, (e) =>
46
+ e("vector_cortex_outcome_appended", {
47
+ ts: undefined,
48
+ event: "vector_cortex_outcome_appended",
49
+ outcomeId: outcome.outcomeId,
50
+ sessionId: outcome.sessionId,
51
+ repoId: outcome.repoId,
52
+ }),
53
+ );
54
+ }
55
+
56
+ /** Report a row excluded from a dataset manifest. Gated by the flag. */
57
+ export function reportDatasetRecordExcluded(
58
+ emit: OutcomeEmit | undefined,
59
+ excluded: { readonly outcomeId: string; readonly reason: string },
60
+ ): void {
61
+ if (!VC8A_ENABLED()) return;
62
+ safe(emit, (e) =>
63
+ e("vector_cortex_dataset_record_excluded", {
64
+ ts: undefined,
65
+ event: "vector_cortex_dataset_record_excluded",
66
+ outcomeId: excluded.outcomeId,
67
+ reason: excluded.reason,
68
+ }),
69
+ );
70
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * outcomes/ledger.ts — VC8A outcome ledger append + validation.
3
+ *
4
+ * Appends outcome fields for session/repo/assignment/metrics ONLY. Rejects
5
+ * prompt, response, exact bytes, or free-text payload fields as
6
+ * OUT_PAYLOAD_FORBIDDEN. The ledger is append-only — no mutation.
7
+ *
8
+ * The append/validate functions are PURE. The flag gates ONLY the reporter
9
+ * seam (emit), never the validation arithmetic, so flag-off is byte-identical
10
+ * to the predecessor.
11
+ *
12
+ * PREVENT-PI-004: no network. PREVENT-011: no `any` type.
13
+ */
14
+
15
+ import {
16
+ OUTCOME_SCHEMA_V1,
17
+ OUT_PAYLOAD_FORBIDDEN,
18
+ type OutcomeV1,
19
+ type OutcomeMetric,
20
+ } from "./types.js";
21
+
22
+ /** Fields that must never appear in an outcome — they carry payload. */
23
+ const FORBIDDEN_FIELDS = new Set([
24
+ "prompt",
25
+ "response",
26
+ "exactBytes",
27
+ "freeText",
28
+ "content",
29
+ "text",
30
+ "payload",
31
+ "body",
32
+ "message",
33
+ "reply",
34
+ "output",
35
+ "completion",
36
+ "snippet",
37
+ ]);
38
+
39
+ /** Validate that the input object has no payload-bearing fields. */
40
+ export function validateOutcome(input: Record<string, unknown>): OutcomeV1 {
41
+ for (const key of Object.keys(input)) {
42
+ if (FORBIDDEN_FIELDS.has(key)) {
43
+ throw { code: OUT_PAYLOAD_FORBIDDEN, field: key };
44
+ }
45
+ }
46
+ if (typeof input["outcomeId"] !== "string" || !input["outcomeId"]) {
47
+ throw { code: OUT_PAYLOAD_FORBIDDEN, field: "outcomeId" };
48
+ }
49
+ if (typeof input["sessionId"] !== "string" || !input["sessionId"]) {
50
+ throw { code: OUT_PAYLOAD_FORBIDDEN, field: "sessionId" };
51
+ }
52
+ if (typeof input["repoId"] !== "string" || !input["repoId"]) {
53
+ throw { code: OUT_PAYLOAD_FORBIDDEN, field: "repoId" };
54
+ }
55
+ if (typeof input["assignment"] !== "string" || !input["assignment"]) {
56
+ throw { code: OUT_PAYLOAD_FORBIDDEN, field: "assignment" };
57
+ }
58
+ const metrics = Array.isArray(input["metrics"]) ? input["metrics"] : [];
59
+ for (const m of metrics) {
60
+ if (typeof m !== "object" || m === null) {
61
+ throw { code: OUT_PAYLOAD_FORBIDDEN, field: "metrics" };
62
+ }
63
+ const row = m as Record<string, unknown>;
64
+ if (typeof row["code"] !== "string" || typeof row["value"] !== "number" ||
65
+ typeof row["unit"] !== "string") {
66
+ throw { code: OUT_PAYLOAD_FORBIDDEN, field: "metrics" };
67
+ }
68
+ }
69
+ return {
70
+ schema: OUTCOME_SCHEMA_V1,
71
+ outcomeId: input["outcomeId"],
72
+ sessionId: input["sessionId"],
73
+ repoId: input["repoId"],
74
+ assignment: input["assignment"],
75
+ metrics: metrics as ReadonlyArray<OutcomeMetric>,
76
+ ts: typeof input["ts"] === "string" ? input["ts"] : new Date(0).toISOString(),
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Append an outcome to the ledger (append-only). Returns the validated
82
+ * OutcomeV1 or throws OUT_PAYLOAD_FORBIDDEN.
83
+ */
84
+ export function appendOutcome(
85
+ input: Record<string, unknown>,
86
+ ): OutcomeV1 {
87
+ return validateOutcome(input);
88
+ }