pi-mega-compact 0.20.15 → 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-diagnostics.js +35 -0
- package/dist/extensions/dashboard-server/api-contracts/vector-cortex-outcomes.js +13 -0
- package/dist/extensions/dashboard-server/route-dispatch.js +12 -1
- package/dist/extensions/dashboard-server/routes-rag-settings-vector-cortex.js +1 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex-diagnostics.js +68 -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/cache/_diagnostics-fixture.js +31 -0
- package/dist/src/vector-cortex/cache/breaker.js +110 -0
- package/dist/src/vector-cortex/cache/diagnostics-emit.js +64 -0
- package/dist/src/vector-cortex/cache/diagnostics-types.js +15 -0
- package/dist/src/vector-cortex/cache/diagnostics.js +97 -0
- package/dist/src/vector-cortex/migrations/request-hash-v2-ops.js +17 -6
- 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/cache/_diagnostics-fixture.js +31 -0
- package/dist/vector-cortex/cache/breaker.js +110 -0
- package/dist/vector-cortex/cache/diagnostics-emit.js +64 -0
- package/dist/vector-cortex/cache/diagnostics-types.js +15 -0
- package/dist/vector-cortex/cache/diagnostics.js +97 -0
- package/dist/vector-cortex/migrations/request-hash-v2-ops.js +17 -6
- 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-CV8WqMmt.js → AreaChart-BOGp2sag.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-CV8WqMmt.js.map → AreaChart-BOGp2sag.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-M-HEBq9l.js → BarChart-BHgA-QPL.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-M-HEBq9l.js.map → BarChart-BHgA-QPL.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-DJ_yPAhP.js → CacheTab-6E7lwM74.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-DJ_yPAhP.js.map → CacheTab-6E7lwM74.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-DNVlKzt6.js → EventsTab-BEtNmFHz.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-DNVlKzt6.js.map → EventsTab-BEtNmFHz.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-B5yjuInb.js → HealthTab-DGxZulRH.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-B5yjuInb.js.map → HealthTab-DGxZulRH.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DoQedESF.js → MaintenanceTab-C86jppQE.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DoQedESF.js.map → MaintenanceTab-C86jppQE.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CuL5CzdH.js → MemoryMapTab-J3VVxooZ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CuL5CzdH.js.map → MemoryMapTab-J3VVxooZ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CZSTaCpK.js → MetricsTab-yn9AFumo.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CZSTaCpK.js.map → MetricsTab-yn9AFumo.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-vkZlXRW6.js → OverviewTab-BMarUiq3.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-vkZlXRW6.js.map → OverviewTab-BMarUiq3.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-DgPD96rF.js → ReposTab-DxnzMive.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-DgPD96rF.js.map → ReposTab-DxnzMive.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-XPbWh7XN.js → SessionsTab-DXKpDUuC.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-XPbWh7XN.js.map → SessionsTab-DXKpDUuC.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-Dz74XAtR.js → SetupTab-BctuV-CJ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-Dz74XAtR.js.map → SetupTab-BctuV-CJ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-FynCpJdx.js → TimeSavedCard-D8yOC96S.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-FynCpJdx.js.map → TimeSavedCard-D8yOC96S.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-C7m3GDy0.js → TurnsTab-BBxqGYH-.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-C7m3GDy0.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-C6viLa0u.js → WikiTab-DdJQSagO.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-C6viLa0u.js.map → WikiTab-DdJQSagO.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-mBEgbBVU.js → button-B4Q9h_8Y.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-mBEgbBVU.js.map → button-B4Q9h_8Y.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-CalUQAKG.js → card-WyR31hUV.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-CalUQAKG.js.map → card-WyR31hUV.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-CMmkuwj5.js → generateCategoricalChart-pTio6Bq3.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-CMmkuwj5.js.map → generateCategoricalChart-pTio6Bq3.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-Dz8GPBOE.js → index-BoLUiNsd.js} +3 -3
- package/extensions/dashboard-client/dist/assets/{index-Dz8GPBOE.js.map → index-BoLUiNsd.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{switch-8QKO6CTU.js → switch-5dieXxoo.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-8QKO6CTU.js.map → switch-5dieXxoo.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-9eCkWI4q.js → toggle-BFc-7_LL.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-9eCkWI4q.js.map → toggle-BFc-7_LL.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-s6IqOKoD.js → useSSE-BqmKNBAI.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-s6IqOKoD.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/VectorCortexDiagnosticsCard.tsx +46 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexOutcomesCard.tsx +43 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +4 -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-diagnostics.ts +76 -0
- package/extensions/dashboard-server/api-contracts/vector-cortex-outcomes.ts +55 -0
- package/extensions/dashboard-server/route-dispatch.ts +10 -1
- package/extensions/dashboard-server/routes-rag-settings-vector-cortex.ts +6 -0
- package/extensions/dashboard-server/routes-vector-cortex-diagnostics.ts +77 -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/cache/_diagnostics-fixture.ts +32 -0
- package/src/vector-cortex/cache/breaker.ts +124 -0
- package/src/vector-cortex/cache/diagnostics-emit.ts +90 -0
- package/src/vector-cortex/cache/diagnostics-types.ts +87 -0
- package/src/vector-cortex/cache/diagnostics.ts +119 -0
- package/src/vector-cortex/migrations/request-hash-v2-ops.ts +15 -9
- 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-jRd_Iztk.js +0 -2
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-jRd_Iztk.js.map +0 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cache/diagnostics-emit.ts — VC7C reporter seam (FLAG-GATED).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `./economics-emit.ts`: a thin `safe()` wrapper around an optional
|
|
5
|
+
* injected `emit`, and the two event names the sprint spec requires verbatim:
|
|
6
|
+
* - `vector_cortex_cache_miss_classified` — a miss was classified.
|
|
7
|
+
* - `vector_cortex_cache_serve_blocked` — a cache serve was blocked by a
|
|
8
|
+
* breaker condition before it could answer from a stale/invalid identity.
|
|
9
|
+
*
|
|
10
|
+
* FLAG SEMANTICS. `classifyMiss` (`./diagnostics.ts`) is PURE and runs REGARDLESS
|
|
11
|
+
* of `MEGACOMPACT_VC7C`. The flag gates ONLY this reporting + dashboard seam:
|
|
12
|
+
* with the flag off the classifier still returns the SAME class, the breaker
|
|
13
|
+
* still blocks the SAME serve — we just do not announce them under the VC7C
|
|
14
|
+
* event namespace, and the dashboard reports `enabled:false` + mode C. That is
|
|
15
|
+
* what makes flag-off byte-identical to the predecessor (VC7B): the arithmetic
|
|
16
|
+
* and the correctness decision are never skipped, only the emission. (NOTE the
|
|
17
|
+
* asymmetry vs VC7B: VC7B gated pure telemetry; here the blocked-serve DECISION
|
|
18
|
+
* is a correctness behavior and is never flag-gated — only its announcement is.)
|
|
19
|
+
*
|
|
20
|
+
* PAYLOAD DISCIPLINE. These events carry the `missClass` and the payload-free
|
|
21
|
+
* `evidence` booleans/counts (see `./diagnostics-types.ts`) — never a session
|
|
22
|
+
* id, never a covered range, never a request or covered digest. The classifier
|
|
23
|
+
* is payload-free by construction, so there is no slot to leak into
|
|
24
|
+
* (SECURITY_PRIVACY — the exact ledger is not diagnostic data).
|
|
25
|
+
*
|
|
26
|
+
* No console, no storage, no network (PREVENT-PI-004 / PREVENT-011). Every line
|
|
27
|
+
* is a structured JSON event with `ts` + `event`.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { VC7C_ENABLED } from "../../config/vector-cortex.js";
|
|
31
|
+
import type { CacheDiagnosticV1, MissClass } from "./diagnostics-types.js";
|
|
32
|
+
|
|
33
|
+
/** The two structured events the VC7C reporter emits. */
|
|
34
|
+
export type CacheDiagnosticEventName =
|
|
35
|
+
| "vector_cortex_cache_miss_classified"
|
|
36
|
+
| "vector_cortex_cache_serve_blocked";
|
|
37
|
+
|
|
38
|
+
/** Optional emit fn injected by the runtime; tests pass `undefined`. */
|
|
39
|
+
export type CacheDiagnosticEmit = (name: string, payload: unknown) => void;
|
|
40
|
+
|
|
41
|
+
/** Run `fn` only when an emit exists; a reporting failure is never fatal. */
|
|
42
|
+
function safe(
|
|
43
|
+
emit: CacheDiagnosticEmit | undefined,
|
|
44
|
+
fn: (emit: CacheDiagnosticEmit) => void,
|
|
45
|
+
): void {
|
|
46
|
+
if (emit === undefined) return;
|
|
47
|
+
try {
|
|
48
|
+
fn(emit);
|
|
49
|
+
} catch {
|
|
50
|
+
// Non-fatal: a reporting failure must never break the agent loop.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Report a classified miss. Gated only by the reporter/dashboard flag. */
|
|
55
|
+
export function reportCacheMissClassified(
|
|
56
|
+
emit: CacheDiagnosticEmit | undefined,
|
|
57
|
+
diagnostic: CacheDiagnosticV1,
|
|
58
|
+
): void {
|
|
59
|
+
if (!VC7C_ENABLED()) return;
|
|
60
|
+
safe(emit, (e) =>
|
|
61
|
+
e("vector_cortex_cache_miss_classified", {
|
|
62
|
+
ts: undefined,
|
|
63
|
+
event: "vector_cortex_cache_miss_classified",
|
|
64
|
+
schema: diagnostic.schema,
|
|
65
|
+
missClass: diagnostic.missClass,
|
|
66
|
+
evidence: diagnostic.evidence,
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Report a cache serve that was blocked before answering. Gated by the flag. */
|
|
72
|
+
export function reportCacheServeBlocked(
|
|
73
|
+
emit: CacheDiagnosticEmit | undefined,
|
|
74
|
+
blocked: {
|
|
75
|
+
readonly missClass: MissClass;
|
|
76
|
+
readonly triadState: string;
|
|
77
|
+
readonly reason: string;
|
|
78
|
+
},
|
|
79
|
+
): void {
|
|
80
|
+
if (!VC7C_ENABLED()) return;
|
|
81
|
+
safe(emit, (e) =>
|
|
82
|
+
e("vector_cortex_cache_serve_blocked", {
|
|
83
|
+
ts: undefined,
|
|
84
|
+
event: "vector_cortex_cache_serve_blocked",
|
|
85
|
+
missClass: blocked.missClass,
|
|
86
|
+
triadState: blocked.triadState,
|
|
87
|
+
reason: blocked.reason,
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cache/diagnostics-types.ts — VC7C cache miss-diagnostic contract types.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for the miss-classification vocabulary. Kept separate
|
|
5
|
+
* from the arithmetic (`diagnostics.ts`) so the dashboard client, the emit layer
|
|
6
|
+
* and the acceptance test all import ONE definition. No logic here; pure types
|
|
7
|
+
* + the registered ID lists. PREVENT-011 honored (no `any`).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** The exclusive miss classes, in the order the classifier ranks them. */
|
|
11
|
+
export type MissClass =
|
|
12
|
+
| "profile"
|
|
13
|
+
| "range"
|
|
14
|
+
| "dependency"
|
|
15
|
+
| "request"
|
|
16
|
+
| "generation"
|
|
17
|
+
| "unknown";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Payload-free by construction. Every field is a boolean or a bounded,
|
|
21
|
+
* non-negative count — there is no string slot into which a request body, a
|
|
22
|
+
* session id, or a covered range of user content could be placed. See
|
|
23
|
+
* SECURITY_PRIVACY: a diagnostic that carried the offending payload would leak
|
|
24
|
+
* it to the dashboard.
|
|
25
|
+
*/
|
|
26
|
+
export interface MissEvidence {
|
|
27
|
+
readonly profileMismatch: boolean;
|
|
28
|
+
readonly rangeMismatch: boolean;
|
|
29
|
+
readonly dependencyAdvanced: boolean;
|
|
30
|
+
readonly requestMismatch: boolean;
|
|
31
|
+
readonly generationInvalidated: boolean;
|
|
32
|
+
/** Ranges the caller asked for (always observable, even on a cold key). */
|
|
33
|
+
readonly requestedRangeCount: number;
|
|
34
|
+
/** Ranges the cache could serve; 0 on a cold key (no crystal). */
|
|
35
|
+
readonly cachedRangeCount: number;
|
|
36
|
+
/** max(0, requestHighWater - cachedHighWater), saturated at MAX_SAFE_INTEGER. */
|
|
37
|
+
readonly dependencyDelta: number;
|
|
38
|
+
/** True when nothing was cached for this key (null cached fields). */
|
|
39
|
+
readonly absent: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A single observation fed to the classifier. A `null` cached field means that
|
|
44
|
+
* piece of state was never persisted — "absence is not a mismatch". Digests
|
|
45
|
+
* follow convention: `coveredDigest` is `sha256:<hex>` (WITH prefix);
|
|
46
|
+
* `requestDigest` is BARE lowercase hex (NO prefix).
|
|
47
|
+
*/
|
|
48
|
+
export interface MissObservation {
|
|
49
|
+
readonly requestProfileId: string;
|
|
50
|
+
readonly requestProfileVersion: string;
|
|
51
|
+
readonly cachedProfileId: string | null;
|
|
52
|
+
readonly cachedProfileVersion: string | null;
|
|
53
|
+
readonly requestCoveredDigest: string;
|
|
54
|
+
readonly cachedCoveredDigest: string | null;
|
|
55
|
+
readonly requestedRangeCount: number;
|
|
56
|
+
readonly cachedRangeCount: number | null;
|
|
57
|
+
readonly requestDigest: string;
|
|
58
|
+
readonly cachedRequestDigest: string | null;
|
|
59
|
+
/** Dependency validation high-water (monotonic frontier), as a bigint. */
|
|
60
|
+
readonly requestDependencyHighWater: bigint;
|
|
61
|
+
readonly cachedDependencyHighWater: bigint | null;
|
|
62
|
+
readonly generationInvalidated: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The versioned diagnostic record emitted for every classified miss. */
|
|
66
|
+
export interface CacheDiagnosticV1 {
|
|
67
|
+
readonly schema: "cache-diagnostic-v1";
|
|
68
|
+
readonly missClass: MissClass;
|
|
69
|
+
readonly evidence: MissEvidence;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Registered conformance IDs (CACHE-016..030). */
|
|
73
|
+
export const CACHE_DIAGNOSTIC_IDS: readonly string[] = Array.from(
|
|
74
|
+
{ length: 15 },
|
|
75
|
+
(_v, i) => `CACHE-${String(i + 16).padStart(3, "0")}`,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
/** Named headlines surfaced by the conformance corpus. */
|
|
79
|
+
export const CACHE_DIAGNOSTIC_NAMED_IDS = [
|
|
80
|
+
"CACHE-MISS-001",
|
|
81
|
+
"CACHE-STALE-003",
|
|
82
|
+
] as const;
|
|
83
|
+
|
|
84
|
+
/** Event names this subsystem emits (gated by MEGACOMPACT_VC7C). */
|
|
85
|
+
export type CacheDiagnosticEventName =
|
|
86
|
+
| "vector_cortex_cache_miss_classified"
|
|
87
|
+
| "vector_cortex_cache_serve_blocked";
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cache/diagnostics.ts — VC7C pure miss classification (flag-independent).
|
|
3
|
+
*
|
|
4
|
+
* The classifier is PURE arithmetic: given a `MissObservation` it returns
|
|
5
|
+
* exactly ONE `MissClass` and the payload-free `MissEvidence`. It is NOT gated by
|
|
6
|
+
* MEGACOMPACT_VC7C — flag-off must be byte-identical to the predecessor, so the
|
|
7
|
+
* SAME class a user sees today, they see tomorrow. The flag gates only the
|
|
8
|
+
* reporter/dashboard seam (see `diagnostics-emit.ts`).
|
|
9
|
+
*
|
|
10
|
+
* Exclusive ranking: profile -> range -> dependency -> request -> generation ->
|
|
11
|
+
* unknown. First match wins; the ranking itself is the contract and is pinned by
|
|
12
|
+
* CACHE-016..030. "Absence is not a mismatch": a cold key (null cached fields)
|
|
13
|
+
* classifies `unknown`, never `profile`. PREVENT-002/011/PI-004 honored.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
CacheDiagnosticV1,
|
|
18
|
+
MissClass,
|
|
19
|
+
MissEvidence,
|
|
20
|
+
MissObservation,
|
|
21
|
+
} from "./diagnostics-types.js";
|
|
22
|
+
|
|
23
|
+
/** A cached field is present iff it is non-null. */
|
|
24
|
+
function isPresent(value: string | number | bigint | null): boolean {
|
|
25
|
+
return value !== null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Clamp the dependency delta to a non-negative, safe-integer count. A request
|
|
30
|
+
* BEHIND the cached frontier is not an advance (delta 0); a huge advance
|
|
31
|
+
* saturates rather than losing precision.
|
|
32
|
+
*/
|
|
33
|
+
function advanceDelta(
|
|
34
|
+
request: bigint,
|
|
35
|
+
cached: bigint | null,
|
|
36
|
+
): { advanced: boolean; delta: number } {
|
|
37
|
+
if (cached === null) return { advanced: false, delta: 0 };
|
|
38
|
+
if (request <= cached) return { advanced: false, delta: 0 };
|
|
39
|
+
const diff = request - cached;
|
|
40
|
+
if (diff > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
41
|
+
return { advanced: true, delta: Number.MAX_SAFE_INTEGER };
|
|
42
|
+
}
|
|
43
|
+
return { advanced: true, delta: Number(diff) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Collect the payload-free evidence from one observation. */
|
|
47
|
+
export function collectEvidence(o: MissObservation): MissEvidence {
|
|
48
|
+
const profileMismatch =
|
|
49
|
+
isPresent(o.cachedProfileId) &&
|
|
50
|
+
isPresent(o.cachedProfileVersion) &&
|
|
51
|
+
(o.cachedProfileId !== o.requestProfileId ||
|
|
52
|
+
o.cachedProfileVersion !== o.requestProfileVersion);
|
|
53
|
+
|
|
54
|
+
const rangeMismatch =
|
|
55
|
+
(isPresent(o.cachedCoveredDigest) &&
|
|
56
|
+
o.cachedCoveredDigest !== o.requestCoveredDigest) ||
|
|
57
|
+
(isPresent(o.cachedRangeCount) &&
|
|
58
|
+
o.cachedRangeCount !== o.requestedRangeCount);
|
|
59
|
+
|
|
60
|
+
const dep = advanceDelta(
|
|
61
|
+
o.requestDependencyHighWater,
|
|
62
|
+
o.cachedDependencyHighWater,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const requestMismatch =
|
|
66
|
+
isPresent(o.cachedRequestDigest) &&
|
|
67
|
+
o.cachedRequestDigest !== o.requestDigest;
|
|
68
|
+
|
|
69
|
+
const absent =
|
|
70
|
+
!isPresent(o.cachedProfileId) &&
|
|
71
|
+
!isPresent(o.cachedCoveredDigest) &&
|
|
72
|
+
!isPresent(o.cachedRequestDigest) &&
|
|
73
|
+
!isPresent(o.cachedDependencyHighWater);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
profileMismatch,
|
|
77
|
+
rangeMismatch,
|
|
78
|
+
dependencyAdvanced: dep.advanced,
|
|
79
|
+
requestMismatch,
|
|
80
|
+
generationInvalidated: o.generationInvalidated,
|
|
81
|
+
requestedRangeCount: o.requestedRangeCount,
|
|
82
|
+
// Nothing was cached when the observation is absent, so a stale
|
|
83
|
+
// cachedRangeCount on the observation is not real — report 0.
|
|
84
|
+
cachedRangeCount:
|
|
85
|
+
o.cachedRangeCount === null || absent ? 0 : o.cachedRangeCount,
|
|
86
|
+
dependencyDelta: dep.delta,
|
|
87
|
+
absent,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The exclusive ranking: first true cause wins. */
|
|
92
|
+
export function classFor(e: MissEvidence): MissClass {
|
|
93
|
+
if (e.profileMismatch) return "profile";
|
|
94
|
+
if (e.rangeMismatch) return "range";
|
|
95
|
+
if (e.dependencyAdvanced) return "dependency";
|
|
96
|
+
if (e.requestMismatch) return "request";
|
|
97
|
+
if (e.generationInvalidated) return "generation";
|
|
98
|
+
return "unknown";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Classify one observation: exactly one deterministic class. */
|
|
102
|
+
export function classifyMiss(o: MissObservation): CacheDiagnosticV1 {
|
|
103
|
+
const evidence = collectEvidence(o);
|
|
104
|
+
return {
|
|
105
|
+
schema: "cache-diagnostic-v1",
|
|
106
|
+
missClass: classFor(evidence),
|
|
107
|
+
evidence,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A miss a caller can self-heal on the next turn (re-fetch under a fresh
|
|
113
|
+
* profile / a new generation), so a triad recovery path may treat it as
|
|
114
|
+
* transient. Only profile + generation qualify; the rest are hard mismatches.
|
|
115
|
+
* An `unknown` miss is NEVER transient.
|
|
116
|
+
*/
|
|
117
|
+
export function isTransientMiss(d: CacheDiagnosticV1): boolean {
|
|
118
|
+
return d.missClass === "profile" || d.missClass === "generation";
|
|
119
|
+
}
|
|
@@ -75,16 +75,17 @@ function generationFromEconomics(economicsVersion: string): bigint {
|
|
|
75
75
|
|
|
76
76
|
export function detectCollision(host: M5Host): boolean {
|
|
77
77
|
const byHash = new Map<string, string>();
|
|
78
|
+
const seenIdentities = new Set<string>();
|
|
78
79
|
for (const v1 of host.v1Rows()) {
|
|
79
80
|
const row = deriveRequestHashRow(host, v1);
|
|
81
|
+
const id = identity(v1.profileId, v1.requestDigest);
|
|
82
|
+
// Two distinct v1 rows (different v1 hash) with the same v2 hash collide,
|
|
83
|
+
// even if they share an identity — the v2 table cannot distinguish them.
|
|
84
|
+
if (seenIdentities.has(id)) return true;
|
|
85
|
+
seenIdentities.add(id);
|
|
80
86
|
const prior = byHash.get(row.hash);
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
prior !== identity(v1.profileId, v1.requestDigest)
|
|
84
|
-
) {
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
byHash.set(row.hash, identity(v1.profileId, v1.requestDigest));
|
|
87
|
+
if (prior !== undefined && prior !== id) return true;
|
|
88
|
+
byHash.set(row.hash, id);
|
|
88
89
|
}
|
|
89
90
|
return false;
|
|
90
91
|
}
|
|
@@ -148,6 +149,9 @@ export function m5Verify(host: M5Host): M5ValidateResult {
|
|
|
148
149
|
const expected = new Set<string>();
|
|
149
150
|
for (const v1 of v1Rows) {
|
|
150
151
|
const id = identity(v1.profileId, v1.requestDigest);
|
|
152
|
+
// Dead-generation rows are intentionally skipped by copy; do not flag.
|
|
153
|
+
const fresh = deriveRequestHashRow(host, v1);
|
|
154
|
+
if (isGenerationInvalidated(host, fresh)) continue;
|
|
151
155
|
expected.add(id);
|
|
152
156
|
const n = counts.get(id) ?? 0;
|
|
153
157
|
if (n === 0) codes.push(M5_FAIL.COPY_PARTIAL);
|
|
@@ -181,10 +185,12 @@ export function m5Switch(host: M5Host): M5ValidateResult {
|
|
|
181
185
|
if (host.activeVersion() !== REQUEST_HASH_LEGACY_VERSION) {
|
|
182
186
|
return { ok: false, codes: [M5_FAIL.NOT_ON_LEGACY] };
|
|
183
187
|
}
|
|
184
|
-
|
|
185
|
-
|
|
188
|
+
// Collision check runs before copy-completeness verify: a structural
|
|
189
|
+
// collision is a switch-blocker regardless of copy state.
|
|
186
190
|
if (detectCollision(host))
|
|
187
191
|
return { ok: false, codes: [M5_FAIL.REQUEST_HASH_COLLISION] };
|
|
192
|
+
const verify = m5Verify(host);
|
|
193
|
+
if (!verify.ok) return verify;
|
|
188
194
|
host.switchToV2();
|
|
189
195
|
return { ok: true, codes: [] };
|
|
190
196
|
}
|
|
@@ -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
|
+
}
|