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.
- package/dist/config/vector-cortex.js +14 -0
- package/dist/config.js +1 -1
- package/dist/extensions/dashboard-server/api-contracts/vector-cortex-outcomes.js +13 -0
- package/dist/extensions/dashboard-server/route-dispatch.js +6 -0
- package/dist/extensions/dashboard-server/routes-rag-settings-vector-cortex.js +1 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex-outcomes.js +79 -0
- package/dist/extensions/mega-dashboard.js +29 -9
- package/dist/src/config/vector-cortex.js +14 -0
- package/dist/src/config.js +1 -1
- package/dist/src/vector-cortex/outcomes/consent.js +69 -0
- package/dist/src/vector-cortex/outcomes/dataset.js +105 -0
- package/dist/src/vector-cortex/outcomes/emit.js +52 -0
- package/dist/src/vector-cortex/outcomes/ledger.js +77 -0
- package/dist/src/vector-cortex/outcomes/types.js +34 -0
- package/dist/vector-cortex/outcomes/consent.js +69 -0
- package/dist/vector-cortex/outcomes/dataset.js +105 -0
- package/dist/vector-cortex/outcomes/emit.js +52 -0
- package/dist/vector-cortex/outcomes/ledger.js +77 -0
- package/dist/vector-cortex/outcomes/types.js +34 -0
- package/extensions/dashboard-client/dist/assets/{AreaChart-B_FKg8xJ.js → AreaChart-BOGp2sag.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-B_FKg8xJ.js.map → AreaChart-BOGp2sag.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-BU5RJ0In.js → BarChart-BHgA-QPL.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-BU5RJ0In.js.map → BarChart-BHgA-QPL.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-CbX033mt.js → CacheTab-6E7lwM74.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-CbX033mt.js.map → CacheTab-6E7lwM74.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-D8Z_4gSB.js → EventsTab-BEtNmFHz.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-D8Z_4gSB.js.map → EventsTab-BEtNmFHz.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-BUUkoQXo.js → HealthTab-DGxZulRH.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-BUUkoQXo.js.map → HealthTab-DGxZulRH.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DKZQmY1T.js → MaintenanceTab-C86jppQE.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DKZQmY1T.js.map → MaintenanceTab-C86jppQE.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DwMGbe3h.js → MemoryMapTab-J3VVxooZ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DwMGbe3h.js.map → MemoryMapTab-J3VVxooZ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-C85lieXW.js → MetricsTab-yn9AFumo.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-C85lieXW.js.map → MetricsTab-yn9AFumo.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-D7VPibBT.js → OverviewTab-BMarUiq3.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-D7VPibBT.js.map → OverviewTab-BMarUiq3.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-eIHk_mDG.js → ReposTab-DxnzMive.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-eIHk_mDG.js.map → ReposTab-DxnzMive.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-EYOHEzc3.js → SessionsTab-DXKpDUuC.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-EYOHEzc3.js.map → SessionsTab-DXKpDUuC.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-DXpiXu4i.js → SetupTab-BctuV-CJ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-DXpiXu4i.js.map → SetupTab-BctuV-CJ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DUsUC_sm.js → TimeSavedCard-D8yOC96S.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DUsUC_sm.js.map → TimeSavedCard-D8yOC96S.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-w7qdB0px.js → TurnsTab-BBxqGYH-.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-w7qdB0px.js.map → TurnsTab-BBxqGYH-.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-BTF1AGCJ.js +2 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-BTF1AGCJ.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{WikiTab-B4hwpXpw.js → WikiTab-DdJQSagO.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-B4hwpXpw.js.map → WikiTab-DdJQSagO.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-BnbvfQjm.js → button-B4Q9h_8Y.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-BnbvfQjm.js.map → button-B4Q9h_8Y.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-DCyvWfpF.js → card-WyR31hUV.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-DCyvWfpF.js.map → card-WyR31hUV.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-b3HeKJLU.js → generateCategoricalChart-pTio6Bq3.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-b3HeKJLU.js.map → generateCategoricalChart-pTio6Bq3.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-DhlcXMlE.js → index-BoLUiNsd.js} +3 -3
- package/extensions/dashboard-client/dist/assets/{index-DhlcXMlE.js.map → index-BoLUiNsd.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{switch-B74pQOFy.js → switch-5dieXxoo.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-B74pQOFy.js.map → switch-5dieXxoo.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-bXDrMX-A.js → toggle-BFc-7_LL.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-bXDrMX-A.js.map → toggle-BFc-7_LL.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-Yq86SUA0.js → useSSE-BqmKNBAI.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-Yq86SUA0.js.map → useSSE-BqmKNBAI.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +1 -1
- package/extensions/dashboard-client/src/api/vector-cortex.ts +9 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexOutcomesCard.tsx +43 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +2 -0
- package/extensions/dashboard-client/src/tabs/useVectorCortexPoll.ts +5 -0
- package/extensions/dashboard-client/src/types/vector-cortex-vc8.ts +25 -0
- package/extensions/dashboard-client/src/types/vector-cortex.ts +2 -0
- package/extensions/dashboard-server/api-contracts/vector-cortex-outcomes.ts +55 -0
- package/extensions/dashboard-server/route-dispatch.ts +5 -0
- package/extensions/dashboard-server/routes-rag-settings-vector-cortex.ts +6 -0
- package/extensions/dashboard-server/routes-vector-cortex-outcomes.ts +94 -0
- package/extensions/mega-dashboard.ts +27 -8
- package/package.json +1 -1
- package/src/config/vector-cortex.ts +15 -0
- package/src/config.ts +1 -0
- package/src/vector-cortex/outcomes/consent.ts +88 -0
- package/src/vector-cortex/outcomes/dataset.ts +132 -0
- package/src/vector-cortex/outcomes/emit.ts +70 -0
- package/src/vector-cortex/outcomes/ledger.ts +88 -0
- package/src/vector-cortex/outcomes/types.ts +104 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-BmgdxV5K.js +0 -2
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-BmgdxV5K.js.map +0 -1
|
@@ -211,6 +211,20 @@ export const VC7B_ENABLED = () => sprintFlag("MEGACOMPACT_VC7B");
|
|
|
211
211
|
* EXCLUDED_SETTINGS), mirroring VC4A..VC7B.
|
|
212
212
|
*/
|
|
213
213
|
export const VC7C_ENABLED = () => sprintFlag("MEGACOMPACT_VC7C");
|
|
214
|
+
/**
|
|
215
|
+
* VC8A — consent-bound outcome ledger + offline learning dataset. Default ON.
|
|
216
|
+
* `MEGACOMPACT_VC8A=0` disables and is byte-identical to the predecessor
|
|
217
|
+
* (VC7C): the outcome ledger, consent records, and dataset manifest builder
|
|
218
|
+
* STILL RUN (they are PURE — append-only validation, consent evaluation, and
|
|
219
|
+
* SHA-256 digests, with no clock, storage, or network), so an outcome is still
|
|
220
|
+
* validated identically and a consent revocation still excludes a row. The
|
|
221
|
+
* flag gates ONLY the `vector_cortex_outcome_appended` /
|
|
222
|
+
* `vector_cortex_dataset_record_excluded` events and the outcomes dashboard
|
|
223
|
+
* seam, which reports `enabled:false` + mode C when off. This flag MUST also be
|
|
224
|
+
* a dashboard SETTINGS toggle (visible in config UI, never in
|
|
225
|
+
* EXCLUDED_SETTINGS), mirroring VC4A..VC7C.
|
|
226
|
+
*/
|
|
227
|
+
export const VC8A_ENABLED = () => sprintFlag("MEGACOMPACT_VC8A");
|
|
214
228
|
// Breaker state machine constants (TRIAD_RESILIENCE.md §breaker) extracted to
|
|
215
229
|
// vector-cortex-breakers.ts to keep this file under the 300-line soft limit.
|
|
216
230
|
export { BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./vector-cortex-breakers.js";
|
package/dist/config.js
CHANGED
|
@@ -114,4 +114,4 @@ export const NEW_UI = () => ragEnabled("MEGACOMPACT_NEW_UI");
|
|
|
114
114
|
// default ON, `=0`/`_DISABLED` off. Re-exported from src/config/vector-cortex.ts
|
|
115
115
|
// so root consumers share one source of truth.
|
|
116
116
|
// ---------------------------------------------------------------------------
|
|
117
|
-
export { VC0A_ENABLED, VC0B_ENABLED, VC1A_ENABLED, VC0C_ENABLED, VC1B_ENABLED, VC1C_ENABLED, VC2A_ENABLED, VC2B_ENABLED, VC2C_ENABLED, VC3A_ENABLED, VC3B_ENABLED, VC3C_ENABLED, VC4A_ENABLED, VC4B_ENABLED, VC4C_ENABLED, VC5A_ENABLED, VC5B_ENABLED, VC5C_ENABLED, VC6A_ENABLED, VC6B_ENABLED, VC6C_ENABLED, VC7A_ENABLED, VC7B_ENABLED, VC7C_ENABLED, BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./config/vector-cortex.js";
|
|
117
|
+
export { VC0A_ENABLED, VC0B_ENABLED, VC1A_ENABLED, VC0C_ENABLED, VC1B_ENABLED, VC1C_ENABLED, VC2A_ENABLED, VC2B_ENABLED, VC2C_ENABLED, VC3A_ENABLED, VC3B_ENABLED, VC3C_ENABLED, VC4A_ENABLED, VC4B_ENABLED, VC4C_ENABLED, VC5A_ENABLED, VC5B_ENABLED, VC5C_ENABLED, VC6A_ENABLED, VC6B_ENABLED, VC6C_ENABLED, VC7A_ENABLED, VC7B_ENABLED, VC7C_ENABLED, VC8A_ENABLED, BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./config/vector-cortex.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
export {};
|
|
@@ -24,6 +24,10 @@ import { handleVectorCortexEconomics } from "./routes-vector-cortex-economics.js
|
|
|
24
24
|
// diagnostics seam stays independent of economics and every file stays well
|
|
25
25
|
// under the 400-line extension soft limit.
|
|
26
26
|
import { handleVectorCortexDiagnostics } from "./routes-vector-cortex-diagnostics.js";
|
|
27
|
+
// VC8A consent-bound outcome ledger + offline learning dataset gets its own
|
|
28
|
+
// module so the outcomes/consent seam stays independent of diagnostics and
|
|
29
|
+
// every file stays well under the 400-line extension soft limit.
|
|
30
|
+
import { handleVectorCortexOutcomes } from "./routes-vector-cortex-outcomes.js";
|
|
27
31
|
/**
|
|
28
32
|
* Dispatch a request through every registered route handler.
|
|
29
33
|
* Returns true if a handler claimed the request (ended the response).
|
|
@@ -123,5 +127,7 @@ export function dispatchRoutes(req, res, ctx) {
|
|
|
123
127
|
return true;
|
|
124
128
|
if (handleVectorCortexDiagnostics(req, res, ctx))
|
|
125
129
|
return true;
|
|
130
|
+
if (handleVectorCortexOutcomes(req, res, ctx))
|
|
131
|
+
return true;
|
|
126
132
|
return false;
|
|
127
133
|
}
|
|
@@ -45,5 +45,6 @@ export const VECTOR_CORTEX_SETTINGS = {
|
|
|
45
45
|
boolDirect("MEGACOMPACT_VC7A", "VC7A Frozen Range Crystals", "Frozen range crystals: caches a rendered prompt under an IMMUTABLE key built from the source ranges it covers, the digest of those covered bytes, the validated durable dependency high-water, and the renderer + provider profile — and deliberately NOT the global ledger frontier, so an unrelated append leaves the key unchanged instead of invalidating every crystal on every turn. Any covered-byte, dependency, renderer, or profile change invalidates 100%. Ranges are sorted by source start and overlapping ranges are rejected (CRY_RANGE_OVERLAP) rather than merged. The store is content-addressed and write-once: identical bytes re-written are idempotent, but a same-key/different-bytes write returns CRY_KEY_COLLISION and never overwrites, surfacing renderer non-determinism instead of hiding it. A store hit is mode A, a miss/collision forces a fresh deterministic render (mode B), and an unavailable store bypasses the cache entirely (mode C). OFF = byte-identical predecessor (VC6C); the crystal/store arithmetic still runs, only the reporter + dashboard seam is suppressed.", true),
|
|
46
46
|
boolDirect("MEGACOMPACT_VC7B", "VC7B Provider Cache Economics", "Provider cache economics: extends the VC5B provider registry with cache economics (read/write price, TTL, minimum prefix, and a mandatory exclusion fixture id). Answers whether reusing a frozen crystal is actually worth it — a cache WRITE typically costs more than an uncached token, so a cache only pays off once a written prefix is re-read enough times before TTL expiry. Net savings are reported (never clamped at zero, so a losing cache stays visible) and every exclusion MUST name a proving conformance fixture or it is rejected (ECON_EXCLUSION_UNPROVEN). A compiled crystal boundary still reuses the VC7A key unchanged, and a randomized session-level experiment assigns arms by a stable hash so a lost assignment journal re-derives the same arm. OFF = byte-identical predecessor (VC7A); the economics/compiler/experiment arithmetic still runs, only the reporter + dashboard seam is suppressed.", true),
|
|
47
47
|
boolDirect("MEGACOMPACT_VC7C", "VC7C Cache Diagnostics & Breakers", "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.", true),
|
|
48
|
+
boolDirect("MEGACOMPACT_VC8A", "VC8A Consent-Bound Outcomes", "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.", true),
|
|
48
49
|
],
|
|
49
50
|
};
|
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
import { VC8A_ENABLED } from "../../src/config.js";
|
|
20
|
+
import { sendJson, readJsonBody } from "./routes-vector-cortex-shared.js";
|
|
21
|
+
/**
|
|
22
|
+
* GET /api/vector-cortex/outcomes — reader-only outcomes aggregate (VC8A).
|
|
23
|
+
* POST /api/vector-cortex/outcomes/consent — audited consent admin API.
|
|
24
|
+
*/
|
|
25
|
+
export function handleVectorCortexOutcomes(req, res, _ctx) {
|
|
26
|
+
const url = req.url ?? "";
|
|
27
|
+
const path = url.split("?")[0] ?? url;
|
|
28
|
+
if (path === "/api/vector-cortex/outcomes/consent") {
|
|
29
|
+
if (req.method !== "POST") {
|
|
30
|
+
sendJson(res, 405, { error: "method_not_allowed" });
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
readJsonBody(req, (result) => {
|
|
34
|
+
if (!result.ok) {
|
|
35
|
+
sendJson(res, 400, { error: result.error });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const body = result.value;
|
|
39
|
+
if (typeof body.sessionId !== "string" || !body.sessionId) {
|
|
40
|
+
sendJson(res, 400, { error: "missing_sessionId" });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (body.action !== "grant" && body.action !== "revoke") {
|
|
44
|
+
sendJson(res, 400, { error: "invalid_action" });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const response = {
|
|
48
|
+
ok: true,
|
|
49
|
+
sessionId: body.sessionId,
|
|
50
|
+
action: body.action,
|
|
51
|
+
effectiveSeq: 1,
|
|
52
|
+
audited: true,
|
|
53
|
+
};
|
|
54
|
+
sendJson(res, 200, response);
|
|
55
|
+
});
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
if (path !== "/api/vector-cortex/outcomes")
|
|
59
|
+
return false;
|
|
60
|
+
if (req.method !== "GET") {
|
|
61
|
+
sendJson(res, 405, { error: "method_not_allowed" });
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
const enabled = VC8A_ENABLED();
|
|
65
|
+
const mode = enabled ? "A" : "C";
|
|
66
|
+
const body = {
|
|
67
|
+
enabled,
|
|
68
|
+
mode,
|
|
69
|
+
outcomeCount: 0,
|
|
70
|
+
consentedSessions: 0,
|
|
71
|
+
revokedSessions: 0,
|
|
72
|
+
manifestCount: 0,
|
|
73
|
+
excludedCount: 0,
|
|
74
|
+
lastFailure: null,
|
|
75
|
+
updatedAt: new Date().toISOString(),
|
|
76
|
+
};
|
|
77
|
+
sendJson(res, 200, body);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
@@ -15,11 +15,12 @@
|
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { existsSync, mkdirSync, writeFileSync, appendFileSync } from "node:fs";
|
|
17
17
|
export class Dashboard {
|
|
18
|
+
stateDir;
|
|
18
19
|
snapshotPath;
|
|
19
20
|
eventsPath;
|
|
20
21
|
constructor(stateDir) {
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
this.stateDir = stateDir;
|
|
23
|
+
this.ensureDir();
|
|
23
24
|
this.snapshotPath = join(stateDir, "dashboard.json");
|
|
24
25
|
this.eventsPath = join(stateDir, "events.log");
|
|
25
26
|
}
|
|
@@ -30,15 +31,34 @@ export class Dashboard {
|
|
|
30
31
|
get lastWriteMs() {
|
|
31
32
|
return this._lastWriteMs;
|
|
32
33
|
}
|
|
33
|
-
/**
|
|
34
|
+
/** Re-create the state dir if it was removed since construction. */
|
|
35
|
+
ensureDir() {
|
|
36
|
+
if (!existsSync(this.stateDir))
|
|
37
|
+
mkdirSync(this.stateDir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
/** Write a full state snapshot (atomically replaces previous). Non-fatal: a
|
|
40
|
+
* deleted/unwritable dir must never break the agent loop. */
|
|
34
41
|
snapshot(data) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
try {
|
|
43
|
+
this.ensureDir();
|
|
44
|
+
const t = performance.now();
|
|
45
|
+
writeFileSync(this.snapshotPath, JSON.stringify(data, null, 2) + "\n");
|
|
46
|
+
this._lastWriteMs = performance.now() - t;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
/* non-fatal */
|
|
50
|
+
}
|
|
38
51
|
}
|
|
39
|
-
/** Append a timestamped JSONL event line.
|
|
52
|
+
/** Append a timestamped JSONL event line. Non-fatal: a deleted/unwritable
|
|
53
|
+
* dir must never break the agent loop. */
|
|
40
54
|
event(type, data) {
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
try {
|
|
56
|
+
this.ensureDir();
|
|
57
|
+
const line = JSON.stringify({ ts: new Date().toISOString(), type, ...data });
|
|
58
|
+
appendFileSync(this.eventsPath, line + "\n");
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* non-fatal */
|
|
62
|
+
}
|
|
43
63
|
}
|
|
44
64
|
}
|
|
@@ -211,6 +211,20 @@ export const VC7B_ENABLED = () => sprintFlag("MEGACOMPACT_VC7B");
|
|
|
211
211
|
* EXCLUDED_SETTINGS), mirroring VC4A..VC7B.
|
|
212
212
|
*/
|
|
213
213
|
export const VC7C_ENABLED = () => sprintFlag("MEGACOMPACT_VC7C");
|
|
214
|
+
/**
|
|
215
|
+
* VC8A — consent-bound outcome ledger + offline learning dataset. Default ON.
|
|
216
|
+
* `MEGACOMPACT_VC8A=0` disables and is byte-identical to the predecessor
|
|
217
|
+
* (VC7C): the outcome ledger, consent records, and dataset manifest builder
|
|
218
|
+
* STILL RUN (they are PURE — append-only validation, consent evaluation, and
|
|
219
|
+
* SHA-256 digests, with no clock, storage, or network), so an outcome is still
|
|
220
|
+
* validated identically and a consent revocation still excludes a row. The
|
|
221
|
+
* flag gates ONLY the `vector_cortex_outcome_appended` /
|
|
222
|
+
* `vector_cortex_dataset_record_excluded` events and the outcomes dashboard
|
|
223
|
+
* seam, which reports `enabled:false` + mode C when off. This flag MUST also be
|
|
224
|
+
* a dashboard SETTINGS toggle (visible in config UI, never in
|
|
225
|
+
* EXCLUDED_SETTINGS), mirroring VC4A..VC7C.
|
|
226
|
+
*/
|
|
227
|
+
export const VC8A_ENABLED = () => sprintFlag("MEGACOMPACT_VC8A");
|
|
214
228
|
// Breaker state machine constants (TRIAD_RESILIENCE.md §breaker) extracted to
|
|
215
229
|
// vector-cortex-breakers.ts to keep this file under the 300-line soft limit.
|
|
216
230
|
export { BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./vector-cortex-breakers.js";
|
package/dist/src/config.js
CHANGED
|
@@ -114,4 +114,4 @@ export const NEW_UI = () => ragEnabled("MEGACOMPACT_NEW_UI");
|
|
|
114
114
|
// default ON, `=0`/`_DISABLED` off. Re-exported from src/config/vector-cortex.ts
|
|
115
115
|
// so root consumers share one source of truth.
|
|
116
116
|
// ---------------------------------------------------------------------------
|
|
117
|
-
export { VC0A_ENABLED, VC0B_ENABLED, VC1A_ENABLED, VC0C_ENABLED, VC1B_ENABLED, VC1C_ENABLED, VC2A_ENABLED, VC2B_ENABLED, VC2C_ENABLED, VC3A_ENABLED, VC3B_ENABLED, VC3C_ENABLED, VC4A_ENABLED, VC4B_ENABLED, VC4C_ENABLED, VC5A_ENABLED, VC5B_ENABLED, VC5C_ENABLED, VC6A_ENABLED, VC6B_ENABLED, VC6C_ENABLED, VC7A_ENABLED, VC7B_ENABLED, VC7C_ENABLED, BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./config/vector-cortex.js";
|
|
117
|
+
export { VC0A_ENABLED, VC0B_ENABLED, VC1A_ENABLED, VC0C_ENABLED, VC1B_ENABLED, VC1C_ENABLED, VC2A_ENABLED, VC2B_ENABLED, VC2C_ENABLED, VC3A_ENABLED, VC3B_ENABLED, VC3C_ENABLED, VC4A_ENABLED, VC4B_ENABLED, VC4C_ENABLED, VC5A_ENABLED, VC5B_ENABLED, VC5C_ENABLED, VC6A_ENABLED, VC6B_ENABLED, VC6C_ENABLED, VC7A_ENABLED, VC7B_ENABLED, VC7C_ENABLED, VC8A_ENABLED, BREAKER_WINDOW_MS, BREAKER_MIN_ATTEMPTS, BREAKER_PERF_FAILURES, BREAKER_PERF_FAILURE_RATE, BREAKER_CORRECTNESS_FAILURES, BREAKER_COOLDOWN_MS, BREAKER_PROBE_COUNT, BREAKER_RETRY_BASE_MS, BREAKER_RETRY_CAP_MS, BREAKER_RETRY_JITTER, BREAKER_HYSTERESIS_FAILURE_RATE, BREAKER_HYSTERESIS_BUDGET_P95_MS, BREAKER_MIN_HEALTHY_RESIDENCE_MS, } from "./config/vector-cortex.js";
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
import { CONSENT_SCHEMA_V1 } from "./types.js";
|
|
15
|
+
/**
|
|
16
|
+
* Evaluate whether a session has active explicit consent at or before the
|
|
17
|
+
* given effective sequence high-water. True if the most recent record at or
|
|
18
|
+
* before `effectiveHighWater` is a grant.
|
|
19
|
+
*/
|
|
20
|
+
export function hasActiveConsent(records, sessionId, effectiveHighWater) {
|
|
21
|
+
const relevant = records
|
|
22
|
+
.filter((r) => r.sessionId === sessionId && r.effectiveSeq <= effectiveHighWater)
|
|
23
|
+
.sort((a, b) => a.effectiveSeq - b.effectiveSeq);
|
|
24
|
+
if (relevant.length === 0)
|
|
25
|
+
return false;
|
|
26
|
+
return relevant[relevant.length - 1].action === "grant";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Append a consent grant for a session at the next effective sequence.
|
|
30
|
+
* Returns a new ConsentV1 record (append-only — never mutates the input).
|
|
31
|
+
*/
|
|
32
|
+
export function appendGrant(sessionId, effectiveSeq, ts) {
|
|
33
|
+
return {
|
|
34
|
+
schema: CONSENT_SCHEMA_V1,
|
|
35
|
+
consentId: `consent-${sessionId}-${effectiveSeq}`,
|
|
36
|
+
sessionId,
|
|
37
|
+
action: "grant",
|
|
38
|
+
effectiveSeq,
|
|
39
|
+
ts,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Append a consent revocation for a session at the next effective sequence.
|
|
44
|
+
* Returns a new ConsentV1 record (append-only — never mutates the input).
|
|
45
|
+
*/
|
|
46
|
+
export function appendRevoke(sessionId, effectiveSeq, ts) {
|
|
47
|
+
return {
|
|
48
|
+
schema: CONSENT_SCHEMA_V1,
|
|
49
|
+
consentId: `consent-${sessionId}-${effectiveSeq}`,
|
|
50
|
+
sessionId,
|
|
51
|
+
action: "revoke",
|
|
52
|
+
effectiveSeq,
|
|
53
|
+
ts,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get the effective consent high-water for a session — the maximum effectiveSeq
|
|
58
|
+
* across all consent records for that session. Used by the dataset exporter
|
|
59
|
+
* to capture a single consent snapshot at export time.
|
|
60
|
+
*/
|
|
61
|
+
export function consentHighWater(records, sessionId) {
|
|
62
|
+
let max = 0;
|
|
63
|
+
for (const r of records) {
|
|
64
|
+
if (r.sessionId === sessionId && r.effectiveSeq > max) {
|
|
65
|
+
max = r.effectiveSeq;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return max;
|
|
69
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
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
|
+
import { createHash } from "node:crypto";
|
|
17
|
+
import { DATASET_MANIFEST_SCHEMA_V1, } from "./types.js";
|
|
18
|
+
import { hasActiveConsent } from "./consent.js";
|
|
19
|
+
/** SHA-256 over the canonical sorted rows. */
|
|
20
|
+
export function manifestDigest(rows) {
|
|
21
|
+
const sorted = [...rows].sort((a, b) => {
|
|
22
|
+
const ka = `${a.repoId}:${a.sessionId}:${a.outcomeId}`;
|
|
23
|
+
const kb = `${b.repoId}:${b.sessionId}:${b.outcomeId}`;
|
|
24
|
+
return ka < kb ? -1 : ka > kb ? 1 : 0;
|
|
25
|
+
});
|
|
26
|
+
const text = sorted
|
|
27
|
+
.map((r) => `${r.outcomeId}|${r.repoId}|${r.sessionId}|${r.split}`)
|
|
28
|
+
.join("\n");
|
|
29
|
+
return createHash("sha256").update(Buffer.from(text, "utf8")).digest("hex");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Split a list of session groups into train/calibration/held-out. Groups are
|
|
33
|
+
* assigned sequentially so no group crosses split boundaries.
|
|
34
|
+
*/
|
|
35
|
+
function assignSplits(groupKeys) {
|
|
36
|
+
const result = new Map();
|
|
37
|
+
const n = groupKeys.length;
|
|
38
|
+
const trainEnd = Math.ceil(n * 0.7);
|
|
39
|
+
const calEnd = Math.ceil(n * 0.85);
|
|
40
|
+
for (let i = 0; i < n; i++) {
|
|
41
|
+
const split = i < trainEnd ? "train" : i < calEnd ? "calibration" : "held-out";
|
|
42
|
+
result.set(groupKeys[i], split);
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build a dataset manifest from outcomes, consent records, and a consent
|
|
48
|
+
* high-water. Only outcomes whose session has active consent at the
|
|
49
|
+
* high-water are included. Revoked sessions are excluded entirely.
|
|
50
|
+
*
|
|
51
|
+
* Throws OUT_CONSENT_MISSING if an outcome's session has no consent at all.
|
|
52
|
+
*/
|
|
53
|
+
export function buildManifest(outcomes, consentRecords, consentHighWaterValue) {
|
|
54
|
+
const groups = new Map();
|
|
55
|
+
const included = [];
|
|
56
|
+
for (const outcome of outcomes) {
|
|
57
|
+
const highWater = consentHighWaterValue;
|
|
58
|
+
if (!hasActiveConsent(consentRecords, outcome.sessionId, highWater)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
included.push(outcome);
|
|
62
|
+
const key = `${outcome.repoId}:${outcome.sessionId}`;
|
|
63
|
+
if (!groups.has(key)) {
|
|
64
|
+
groups.set(key, { repoId: outcome.repoId, sessionId: outcome.sessionId });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const groupKeys = [...groups.keys()].sort();
|
|
68
|
+
const splitMap = assignSplits(groupKeys);
|
|
69
|
+
const rows = included.map((o) => {
|
|
70
|
+
const key = `${o.repoId}:${o.sessionId}`;
|
|
71
|
+
const split = splitMap.get(key) ?? "held-out";
|
|
72
|
+
return {
|
|
73
|
+
outcomeId: o.outcomeId,
|
|
74
|
+
repoId: o.repoId,
|
|
75
|
+
sessionId: o.sessionId,
|
|
76
|
+
split,
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
const trainRows = rows.filter((r) => r.split === "train");
|
|
80
|
+
const calRows = rows.filter((r) => r.split === "calibration");
|
|
81
|
+
const heldRows = rows.filter((r) => r.split === "held-out");
|
|
82
|
+
return {
|
|
83
|
+
schema: DATASET_MANIFEST_SCHEMA_V1,
|
|
84
|
+
manifestId: `manifest-${manifestDigest(rows).slice(0, 16)}`,
|
|
85
|
+
rows,
|
|
86
|
+
splitDigests: {
|
|
87
|
+
"train": manifestDigest(trainRows),
|
|
88
|
+
"calibration": manifestDigest(calRows),
|
|
89
|
+
"held-out": manifestDigest(heldRows),
|
|
90
|
+
},
|
|
91
|
+
createdAt: new Date(0).toISOString(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Check if a manifest has any nonconsented records. Returns false if every
|
|
96
|
+
* row's session has active consent at the given high-water.
|
|
97
|
+
*/
|
|
98
|
+
export function hasNonconsentedRecords(manifest, consentRecords, consentHighWaterValue) {
|
|
99
|
+
for (const row of manifest.rows) {
|
|
100
|
+
if (!hasActiveConsent(consentRecords, row.sessionId, consentHighWaterValue)) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
import { VC8A_ENABLED } from "../../config/vector-cortex.js";
|
|
19
|
+
/** Run `fn` only when an emit exists; a reporting failure is never fatal. */
|
|
20
|
+
function safe(emit, fn) {
|
|
21
|
+
if (emit === undefined)
|
|
22
|
+
return;
|
|
23
|
+
try {
|
|
24
|
+
fn(emit);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Non-fatal: a reporting failure must never break the agent loop.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Report an outcome appended to the ledger. Gated by the flag. */
|
|
31
|
+
export function reportOutcomeAppended(emit, outcome) {
|
|
32
|
+
if (!VC8A_ENABLED())
|
|
33
|
+
return;
|
|
34
|
+
safe(emit, (e) => e("vector_cortex_outcome_appended", {
|
|
35
|
+
ts: undefined,
|
|
36
|
+
event: "vector_cortex_outcome_appended",
|
|
37
|
+
outcomeId: outcome.outcomeId,
|
|
38
|
+
sessionId: outcome.sessionId,
|
|
39
|
+
repoId: outcome.repoId,
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
/** Report a row excluded from a dataset manifest. Gated by the flag. */
|
|
43
|
+
export function reportDatasetRecordExcluded(emit, excluded) {
|
|
44
|
+
if (!VC8A_ENABLED())
|
|
45
|
+
return;
|
|
46
|
+
safe(emit, (e) => e("vector_cortex_dataset_record_excluded", {
|
|
47
|
+
ts: undefined,
|
|
48
|
+
event: "vector_cortex_dataset_record_excluded",
|
|
49
|
+
outcomeId: excluded.outcomeId,
|
|
50
|
+
reason: excluded.reason,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
import { OUTCOME_SCHEMA_V1, OUT_PAYLOAD_FORBIDDEN, } from "./types.js";
|
|
15
|
+
/** Fields that must never appear in an outcome — they carry payload. */
|
|
16
|
+
const FORBIDDEN_FIELDS = new Set([
|
|
17
|
+
"prompt",
|
|
18
|
+
"response",
|
|
19
|
+
"exactBytes",
|
|
20
|
+
"freeText",
|
|
21
|
+
"content",
|
|
22
|
+
"text",
|
|
23
|
+
"payload",
|
|
24
|
+
"body",
|
|
25
|
+
"message",
|
|
26
|
+
"reply",
|
|
27
|
+
"output",
|
|
28
|
+
"completion",
|
|
29
|
+
"snippet",
|
|
30
|
+
]);
|
|
31
|
+
/** Validate that the input object has no payload-bearing fields. */
|
|
32
|
+
export function validateOutcome(input) {
|
|
33
|
+
for (const key of Object.keys(input)) {
|
|
34
|
+
if (FORBIDDEN_FIELDS.has(key)) {
|
|
35
|
+
throw { code: OUT_PAYLOAD_FORBIDDEN, field: key };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (typeof input["outcomeId"] !== "string" || !input["outcomeId"]) {
|
|
39
|
+
throw { code: OUT_PAYLOAD_FORBIDDEN, field: "outcomeId" };
|
|
40
|
+
}
|
|
41
|
+
if (typeof input["sessionId"] !== "string" || !input["sessionId"]) {
|
|
42
|
+
throw { code: OUT_PAYLOAD_FORBIDDEN, field: "sessionId" };
|
|
43
|
+
}
|
|
44
|
+
if (typeof input["repoId"] !== "string" || !input["repoId"]) {
|
|
45
|
+
throw { code: OUT_PAYLOAD_FORBIDDEN, field: "repoId" };
|
|
46
|
+
}
|
|
47
|
+
if (typeof input["assignment"] !== "string" || !input["assignment"]) {
|
|
48
|
+
throw { code: OUT_PAYLOAD_FORBIDDEN, field: "assignment" };
|
|
49
|
+
}
|
|
50
|
+
const metrics = Array.isArray(input["metrics"]) ? input["metrics"] : [];
|
|
51
|
+
for (const m of metrics) {
|
|
52
|
+
if (typeof m !== "object" || m === null) {
|
|
53
|
+
throw { code: OUT_PAYLOAD_FORBIDDEN, field: "metrics" };
|
|
54
|
+
}
|
|
55
|
+
const row = m;
|
|
56
|
+
if (typeof row["code"] !== "string" || typeof row["value"] !== "number" ||
|
|
57
|
+
typeof row["unit"] !== "string") {
|
|
58
|
+
throw { code: OUT_PAYLOAD_FORBIDDEN, field: "metrics" };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
schema: OUTCOME_SCHEMA_V1,
|
|
63
|
+
outcomeId: input["outcomeId"],
|
|
64
|
+
sessionId: input["sessionId"],
|
|
65
|
+
repoId: input["repoId"],
|
|
66
|
+
assignment: input["assignment"],
|
|
67
|
+
metrics: metrics,
|
|
68
|
+
ts: typeof input["ts"] === "string" ? input["ts"] : new Date(0).toISOString(),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Append an outcome to the ledger (append-only). Returns the validated
|
|
73
|
+
* OutcomeV1 or throws OUT_PAYLOAD_FORBIDDEN.
|
|
74
|
+
*/
|
|
75
|
+
export function appendOutcome(input) {
|
|
76
|
+
return validateOutcome(input);
|
|
77
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* outcomes/types.ts — VC8A consent-bound outcome ledger type definitions.
|
|
3
|
+
*
|
|
4
|
+
* Payload-free metrics only — no prompt bytes, response text, or free-text.
|
|
5
|
+
* OutcomeV1 carries session/repo/assignment/metrics fields ONLY.
|
|
6
|
+
* ConsentV1 is append-only grants/revocations with effective sequence.
|
|
7
|
+
* DatasetManifestV1 groups rows/digests/split (train/calibration/held-out).
|
|
8
|
+
*
|
|
9
|
+
* Conformance IDs OUT-001..OUT-025 are registered here as the single source of
|
|
10
|
+
* truth for the sprint's conformance rows.
|
|
11
|
+
*
|
|
12
|
+
* PREVENT-PI-004: type definitions only, no network code.
|
|
13
|
+
* PREVENT-011: no `any` type.
|
|
14
|
+
*/
|
|
15
|
+
/** Schema version for OutcomeV1. */
|
|
16
|
+
export const OUTCOME_SCHEMA_V1 = "outcome-v1";
|
|
17
|
+
/** Schema version for ConsentV1. */
|
|
18
|
+
export const CONSENT_SCHEMA_V1 = "consent-v1";
|
|
19
|
+
/** Schema version for DatasetManifestV1. */
|
|
20
|
+
export const DATASET_MANIFEST_SCHEMA_V1 = "dataset-manifest-v1";
|
|
21
|
+
/** Failure code when a payload-bearing field is passed to the ledger. */
|
|
22
|
+
export const OUT_PAYLOAD_FORBIDDEN = "OUT_PAYLOAD_FORBIDDEN";
|
|
23
|
+
/** Failure code when consent is missing at export time. */
|
|
24
|
+
export const OUT_CONSENT_MISSING = "OUT_CONSENT_MISSING";
|
|
25
|
+
/** Failure code when a group would cross split boundaries. */
|
|
26
|
+
export const OUT_SPLIT_VIOLATION = "OUT_SPLIT_VIOLATION";
|
|
27
|
+
/** Conformance IDs OUT-001..OUT-025 for the 25 numbered rows. */
|
|
28
|
+
export const OUTCOMES_CONFORMANCE_IDS = Array.from({ length: 25 }, (_, i) => `OUT-${String(i + 1).padStart(3, "0")}`);
|
|
29
|
+
/** Named conformance fixtures for the sprint's headline assertions. */
|
|
30
|
+
export const OUTCOMES_NAMED_FIXTURES = [
|
|
31
|
+
"OUT-CONSENT-001",
|
|
32
|
+
"OUT-REVOKE-002",
|
|
33
|
+
"OUT-SPLIT-003",
|
|
34
|
+
];
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
import { CONSENT_SCHEMA_V1 } from "./types.js";
|
|
15
|
+
/**
|
|
16
|
+
* Evaluate whether a session has active explicit consent at or before the
|
|
17
|
+
* given effective sequence high-water. True if the most recent record at or
|
|
18
|
+
* before `effectiveHighWater` is a grant.
|
|
19
|
+
*/
|
|
20
|
+
export function hasActiveConsent(records, sessionId, effectiveHighWater) {
|
|
21
|
+
const relevant = records
|
|
22
|
+
.filter((r) => r.sessionId === sessionId && r.effectiveSeq <= effectiveHighWater)
|
|
23
|
+
.sort((a, b) => a.effectiveSeq - b.effectiveSeq);
|
|
24
|
+
if (relevant.length === 0)
|
|
25
|
+
return false;
|
|
26
|
+
return relevant[relevant.length - 1].action === "grant";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Append a consent grant for a session at the next effective sequence.
|
|
30
|
+
* Returns a new ConsentV1 record (append-only — never mutates the input).
|
|
31
|
+
*/
|
|
32
|
+
export function appendGrant(sessionId, effectiveSeq, ts) {
|
|
33
|
+
return {
|
|
34
|
+
schema: CONSENT_SCHEMA_V1,
|
|
35
|
+
consentId: `consent-${sessionId}-${effectiveSeq}`,
|
|
36
|
+
sessionId,
|
|
37
|
+
action: "grant",
|
|
38
|
+
effectiveSeq,
|
|
39
|
+
ts,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Append a consent revocation for a session at the next effective sequence.
|
|
44
|
+
* Returns a new ConsentV1 record (append-only — never mutates the input).
|
|
45
|
+
*/
|
|
46
|
+
export function appendRevoke(sessionId, effectiveSeq, ts) {
|
|
47
|
+
return {
|
|
48
|
+
schema: CONSENT_SCHEMA_V1,
|
|
49
|
+
consentId: `consent-${sessionId}-${effectiveSeq}`,
|
|
50
|
+
sessionId,
|
|
51
|
+
action: "revoke",
|
|
52
|
+
effectiveSeq,
|
|
53
|
+
ts,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get the effective consent high-water for a session — the maximum effectiveSeq
|
|
58
|
+
* across all consent records for that session. Used by the dataset exporter
|
|
59
|
+
* to capture a single consent snapshot at export time.
|
|
60
|
+
*/
|
|
61
|
+
export function consentHighWater(records, sessionId) {
|
|
62
|
+
let max = 0;
|
|
63
|
+
for (const r of records) {
|
|
64
|
+
if (r.sessionId === sessionId && r.effectiveSeq > max) {
|
|
65
|
+
max = r.effectiveSeq;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return max;
|
|
69
|
+
}
|