@mutagent/evaluator 0.1.0-alpha.6 → 0.2.0-alpha.1
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/.claude/skills/mutagent-evaluator/SKILL.md +23 -25
- package/.claude/skills/mutagent-evaluator/assets/agents/dataset-builder.md +6 -5
- package/.claude/skills/mutagent-evaluator/assets/agents/evaluator.md +77 -14
- package/.claude/skills/mutagent-evaluator/assets/templates/discover-report.template.html +397 -0
- package/.claude/skills/mutagent-evaluator/assets/templates/eval-report.template.html +594 -0
- package/.claude/skills/mutagent-evaluator/assets/templates/review-report.template.html +560 -0
- package/.claude/skills/mutagent-evaluator/golden/judge-trajectory.prose.md +69 -0
- package/.claude/skills/mutagent-evaluator/references/data-registry.md +43 -0
- package/.claude/skills/mutagent-evaluator/references/eval-layers.md +52 -0
- package/.claude/skills/mutagent-evaluator/references/eval-stage.md +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/aggregate-discover.ts +254 -9
- package/.claude/skills/mutagent-evaluator/scripts/audience.ts +84 -0
- package/.claude/skills/mutagent-evaluator/scripts/code-eval-library.ts +201 -0
- package/.claude/skills/mutagent-evaluator/scripts/contracts/agentspec-evals.ts +101 -39
- package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-matrix.ts +287 -3
- package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-types.ts +30 -4
- package/.claude/skills/mutagent-evaluator/scripts/materialize-dataset.ts +130 -68
- package/.claude/skills/mutagent-evaluator/scripts/matrix-judge.ts +219 -1
- package/.claude/skills/mutagent-evaluator/scripts/merge-criteria.ts +849 -0
- package/.claude/skills/mutagent-evaluator/scripts/render-discover-report-v3.ts +1155 -0
- package/.claude/skills/mutagent-evaluator/scripts/render-eval-report-v3.ts +610 -0
- package/.claude/skills/mutagent-evaluator/scripts/render-eval-report.ts +2 -1
- package/.claude/skills/mutagent-evaluator/scripts/render-review-report-v3.ts +691 -0
- package/.claude/skills/mutagent-evaluator/scripts/route-failures.ts +10 -7
- package/.claude/skills/mutagent-evaluator/scripts/run-evaluate.ts +46 -1
- package/.claude/skills/mutagent-evaluator/scripts/run-review.ts +266 -0
- package/.claude/skills/mutagent-evaluator/scripts/verify-render.ts +728 -0
- package/package.json +2 -2
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* scripts/materialize-dataset.ts — F8:
|
|
3
|
-
*
|
|
2
|
+
* scripts/materialize-dataset.ts — F8: PROJECT the agentspec's real dataset items
|
|
3
|
+
* into runnable evaluator DatasetCases (Type A — PURE).
|
|
4
4
|
* ---------------------------------------------------------------------------
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* AgentSpec 0.3.0 (D18/D19) moved the dataset from a DEFINITION (0.2's
|
|
6
|
+
* `dataset_categories[] × edge_cases[]`, which the evaluator had to synthesize
|
|
7
|
+
* base+edge queries from) to `spec.evaluation.datasets[].items[]` — REAL,
|
|
8
|
+
* already-materialized rows, each carrying `category` / `scenarioRef` / a `case`
|
|
9
|
+
* assignment over the dataset's `caseDimensions`, plus kind-specific `input` /
|
|
10
|
+
* `expected` payloads.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
* - one
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* So materialization is now a faithful PROJECTION, not a synthesis:
|
|
13
|
+
* - one DatasetCase per spec item (the seed layer, source: "seed").
|
|
14
|
+
* - the case TUPLE = the item's `category` + its `case` assignment (the axes the
|
|
15
|
+
* dataset varies over — see `dimensionsFromAgentspec`).
|
|
16
|
+
* - the case QUERY = the item's opaque `input` payload rendered deterministically
|
|
17
|
+
* (raw string when `input` is a string; a stable key-sorted serialization
|
|
18
|
+
* otherwise), so a re-projection is byte-identical.
|
|
16
19
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
20
|
+
* The dataset-builder agent then EXPANDS these seeds synthetically; build-dataset.ts's
|
|
21
|
+
* deterministic id/dedup/merge dedups any overlap. Subject-agnostic (EV-002): axes
|
|
22
|
+
* and values come from the agentspec, never hard-coded. DETERMINISTIC — items and
|
|
23
|
+
* axes consumed in given order; no clock / random / network → re-projecting is
|
|
24
|
+
* byte-identical (C-PIN).
|
|
22
25
|
*/
|
|
23
26
|
import {
|
|
24
27
|
type Dataset,
|
|
@@ -26,88 +29,147 @@ import {
|
|
|
26
29
|
type DatasetTuple,
|
|
27
30
|
type Dimension,
|
|
28
31
|
} from "./contracts/dataset.ts";
|
|
29
|
-
import type {
|
|
32
|
+
import type {
|
|
33
|
+
SpecEvaluation,
|
|
34
|
+
SpecEvalDatasetItem,
|
|
35
|
+
} from "./contracts/agentspec-evals.ts";
|
|
30
36
|
import { buildCase, appendToDataset } from "./build-dataset.ts";
|
|
31
37
|
import { CaseSource } from "./contracts/dataset.ts";
|
|
32
38
|
|
|
33
|
-
/** The
|
|
34
|
-
const
|
|
35
|
-
const EDGE_FALSE = "false";
|
|
39
|
+
/** The tuple key under which an item's dataset-local category is recorded. */
|
|
40
|
+
const CATEGORY_DIM = "category";
|
|
36
41
|
|
|
37
42
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
43
|
+
* Deterministic, key-sorted serialization — a stable rendering of an opaque item
|
|
44
|
+
* payload so two projections of the same payload are byte-identical regardless of
|
|
45
|
+
* key insertion order. PURE.
|
|
41
46
|
*/
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
function stableStringify(value: unknown): string {
|
|
48
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
49
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
50
|
+
const record = value as Record<string, unknown>;
|
|
51
|
+
const body = Object.keys(record)
|
|
52
|
+
.sort()
|
|
53
|
+
.map((k) => `${JSON.stringify(k)}:${stableStringify(record[k])}`)
|
|
54
|
+
.join(",");
|
|
55
|
+
return `{${body}}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The dimensions the projected dataset varies over: `category` (values = the
|
|
60
|
+
* dataset-local category ids, first-seen order) + one dimension per distinct
|
|
61
|
+
* `caseDimensions` axis (values merged across datasets, first-seen order). Both are
|
|
62
|
+
* derived from the agentspec, never hard-coded. Subject-agnostic. PURE.
|
|
63
|
+
*/
|
|
64
|
+
export function dimensionsFromAgentspec(evaluation: SpecEvaluation): Dimension[] {
|
|
44
65
|
const dims: Dimension[] = [];
|
|
45
|
-
|
|
46
|
-
|
|
66
|
+
|
|
67
|
+
// category dimension — the union of dataset-local category ids (first-seen, deduped).
|
|
68
|
+
const categoryIds: string[] = [];
|
|
69
|
+
const seenCategory = new Set<string>();
|
|
70
|
+
for (const dataset of evaluation.datasets) {
|
|
71
|
+
for (const category of dataset.categories ?? []) {
|
|
72
|
+
if (seenCategory.has(category.id)) continue;
|
|
73
|
+
seenCategory.add(category.id);
|
|
74
|
+
categoryIds.push(category.id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (categoryIds.length > 0) {
|
|
78
|
+
dims.push({ name: CATEGORY_DIM, description: "the dataset-local category slice", values: categoryIds });
|
|
47
79
|
}
|
|
48
|
-
dims.push({ name: "edge_case", description: "is this an edge/adversarial case", values: [EDGE_FALSE, EDGE_TRUE] });
|
|
49
|
-
return dims;
|
|
50
|
-
}
|
|
51
80
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
81
|
+
// caseDimensions — each named axis, values merged across datasets (first-seen, deduped).
|
|
82
|
+
const axisOrder: string[] = [];
|
|
83
|
+
const axes = new Map<string, { description?: string; values: string[]; seen: Set<string> }>();
|
|
84
|
+
for (const dataset of evaluation.datasets) {
|
|
85
|
+
for (const [name, axis] of Object.entries(dataset.caseDimensions ?? {})) {
|
|
86
|
+
let entry = axes.get(name);
|
|
87
|
+
if (entry === undefined) {
|
|
88
|
+
entry = { values: [], seen: new Set<string>() };
|
|
89
|
+
if (axis.description !== undefined) entry.description = axis.description;
|
|
90
|
+
axes.set(name, entry);
|
|
91
|
+
axisOrder.push(name);
|
|
92
|
+
}
|
|
93
|
+
for (const value of axis.values) {
|
|
94
|
+
if (entry.seen.has(value)) continue;
|
|
95
|
+
entry.seen.add(value);
|
|
96
|
+
entry.values.push(value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const name of axisOrder) {
|
|
101
|
+
const entry = axes.get(name);
|
|
102
|
+
if (entry === undefined) continue;
|
|
103
|
+
const dim: Dimension = { name, values: entry.values };
|
|
104
|
+
if (entry.description !== undefined) dim.description = entry.description;
|
|
105
|
+
dims.push(dim);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return dims;
|
|
55
109
|
}
|
|
56
110
|
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
111
|
+
/**
|
|
112
|
+
* The variation tuple an item realizes — its dataset-local `category` (when present)
|
|
113
|
+
* plus its `case` axis assignments. Falls back to the item id when an item declares
|
|
114
|
+
* neither (a DatasetTuple must have ≥1 assignment). PURE.
|
|
115
|
+
*/
|
|
116
|
+
function tupleFromItem(item: SpecEvalDatasetItem): DatasetTuple {
|
|
117
|
+
const tuple: DatasetTuple = {};
|
|
118
|
+
if (item.category !== undefined && item.category.length > 0) tuple[CATEGORY_DIM] = item.category;
|
|
119
|
+
for (const [axis, value] of Object.entries(item.case ?? {})) tuple[axis] = value;
|
|
120
|
+
if (Object.keys(tuple).length === 0) tuple.item = item.id;
|
|
121
|
+
return tuple;
|
|
61
122
|
}
|
|
62
123
|
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
124
|
+
/**
|
|
125
|
+
* The natural-language query an item realizes — its opaque `input` payload rendered
|
|
126
|
+
* deterministically. A string input is used verbatim; any other payload is
|
|
127
|
+
* stable-serialized; a missing payload falls back to a deterministic descriptor so
|
|
128
|
+
* the query is always non-empty (DatasetCase requires it). PURE.
|
|
129
|
+
*/
|
|
130
|
+
function queryFromItem(item: SpecEvalDatasetItem): string {
|
|
131
|
+
const input = item.input;
|
|
132
|
+
if (typeof input === "string" && input.length > 0) return input;
|
|
133
|
+
if (input !== undefined) {
|
|
134
|
+
const rendered = stableStringify(input);
|
|
135
|
+
if (rendered.length > 0) return rendered;
|
|
136
|
+
}
|
|
137
|
+
const context = item.category ?? item.scenarioRef ?? item.id;
|
|
138
|
+
return `[${context}] ${item.id}`;
|
|
67
139
|
}
|
|
68
140
|
|
|
69
141
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
142
|
+
* PROJECT the agentspec's real dataset items (0.3.0 `spec.evaluation.datasets[].items[]`)
|
|
143
|
+
* into runnable DatasetCases (F8), one seed case per item. DETERMINISTIC, deduped by
|
|
144
|
+
* content id (a re-projection collides → drop). Returns [] when no dataset carries
|
|
145
|
+
* items. PURE.
|
|
74
146
|
*/
|
|
75
|
-
export function materializeFromAgentspec(
|
|
147
|
+
export function materializeFromAgentspec(evaluation: SpecEvaluation): DatasetCase[] {
|
|
76
148
|
const out: DatasetCase[] = [];
|
|
77
149
|
const seen = new Set<string>();
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
for (const cat of evals.dataset_categories) {
|
|
85
|
-
// base item — one real query per category.
|
|
86
|
-
push(
|
|
87
|
-
{ category: cat.id, edge_case: EDGE_FALSE },
|
|
88
|
-
baseQueryFor(evals, cat.id, cat.description),
|
|
89
|
-
);
|
|
90
|
-
// edge items — one real query per declared edge_case.
|
|
91
|
-
for (const edge of cat.edge_cases) {
|
|
92
|
-
if (edge.length === 0) continue;
|
|
93
|
-
push({ category: cat.id, edge_case: EDGE_TRUE }, edgeQueryFor(cat.id, edge));
|
|
150
|
+
for (const dataset of evaluation.datasets) {
|
|
151
|
+
for (const item of dataset.items ?? []) {
|
|
152
|
+
const c = buildCase(tupleFromItem(item), queryFromItem(item), CaseSource.Seed);
|
|
153
|
+
if (seen.has(c.id)) continue; // content-id dedup (re-project collides → drop)
|
|
154
|
+
seen.add(c.id);
|
|
155
|
+
out.push(c);
|
|
94
156
|
}
|
|
95
157
|
}
|
|
96
158
|
return out;
|
|
97
159
|
}
|
|
98
160
|
|
|
99
161
|
/**
|
|
100
|
-
* Assemble (or extend) a Dataset seeded with the
|
|
101
|
-
* `existing` is supplied, the
|
|
162
|
+
* Assemble (or extend) a Dataset seeded with the projected real items. When
|
|
163
|
+
* `existing` is supplied, the projected cases are merged MONOTONICALLY (no
|
|
102
164
|
* duplicates, version bumps). Subject-agnostic. DETERMINISTIC. PURE.
|
|
103
165
|
*/
|
|
104
166
|
export function materializeToDataset(
|
|
105
167
|
subject: string,
|
|
106
|
-
|
|
168
|
+
evaluation: SpecEvaluation,
|
|
107
169
|
existing?: Dataset,
|
|
108
170
|
): Dataset {
|
|
109
|
-
const dimensions = dimensionsFromAgentspec(
|
|
110
|
-
const cases = materializeFromAgentspec(
|
|
171
|
+
const dimensions = dimensionsFromAgentspec(evaluation);
|
|
172
|
+
const cases = materializeFromAgentspec(evaluation);
|
|
111
173
|
const base: Dataset = existing ?? { subject, dimensions, cases: [], version: 0 };
|
|
112
174
|
return appendToDataset(base, cases);
|
|
113
175
|
}
|
|
@@ -85,6 +85,18 @@ function transcriptOf(trace: EvalTrace): TranscriptTurn[] {
|
|
|
85
85
|
return turns;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* The VERIFY-SURFACE view of a situation: each trace augmented with the SAME
|
|
90
|
+
* deterministic `trajectory`/`transcript` projections its packet was built from
|
|
91
|
+
* (`trajectoryOf`/`transcriptOf`). A judge cites refs against the packet it was
|
|
92
|
+
* handed — those paths must re-resolve at verify time, or a syntax/shape gap
|
|
93
|
+
* (not evidence weakness) silently downgrades a grounded verdict. Trace-shaped
|
|
94
|
+
* paths (`observations.N.…`) keep resolving via the spread. PURE.
|
|
95
|
+
*/
|
|
96
|
+
export function verifySituationViews(traces: EvalTrace[]): EvalTrace[] {
|
|
97
|
+
return traces.map((t) => ({ ...t, trajectory: trajectoryOf(t), transcript: transcriptOf(t) }) as EvalTrace);
|
|
98
|
+
}
|
|
99
|
+
|
|
88
100
|
// ── T1 (B-U3) tier-0 deterministic pre-pass ──────────────────────────────────
|
|
89
101
|
//
|
|
90
102
|
// THE real cost lever (§9.4.1): run the code-method rows FIRST (deterministic,
|
|
@@ -338,6 +350,7 @@ export function buildMatrixPacket(
|
|
|
338
350
|
criteria: MatrixCriterion[],
|
|
339
351
|
pin: { model: string; temperature: 0 },
|
|
340
352
|
subjectProfile?: SubjectProfile,
|
|
353
|
+
layerScope?: string[],
|
|
341
354
|
): MatrixPacket {
|
|
342
355
|
const packet: MatrixPacket = {
|
|
343
356
|
subject,
|
|
@@ -349,6 +362,9 @@ export function buildMatrixPacket(
|
|
|
349
362
|
// byte-stable legacy packet (the judge reconstructs the profile at reason-time).
|
|
350
363
|
...(subjectProfile !== undefined ? { subjectProfile } : {}),
|
|
351
364
|
pin,
|
|
365
|
+
// v3 D1=B+ — the pipeable `*evaluate-<layer>` filter. ABSENT/empty ⇒ the FULL
|
|
366
|
+
// walk (byte-stable legacy packet — the field is omitted entirely).
|
|
367
|
+
...(layerScope !== undefined && layerScope.length > 0 ? { layerScope } : {}),
|
|
352
368
|
};
|
|
353
369
|
assertMatrixPacket(packet);
|
|
354
370
|
return packet;
|
|
@@ -604,7 +620,14 @@ export function aggregateMatrixScorecard(input: MatrixAggregateInput): MatrixAgg
|
|
|
604
620
|
// proof that a DIFFERENT judge ran, not just "eligible for".
|
|
605
621
|
const independentVerify: IndependentVerifyRecord[] = [];
|
|
606
622
|
if (input.independentVerify !== undefined) {
|
|
607
|
-
const { situation, signals } = input.independentVerify;
|
|
623
|
+
const { situation: rawSituation, signals } = input.independentVerify;
|
|
624
|
+
// The judge cites refs against the PACKET it was handed (trajectory[]/transcript[]
|
|
625
|
+
// projections), but the raw EvalTrace carries neither field — so a deterministic
|
|
626
|
+
// re-resolve against bare traces reads every packet-shaped ref as DEAD and
|
|
627
|
+
// silently downgrades a grounded fail to uncertain (observed live: gate flipped
|
|
628
|
+
// fail → incomplete on a verifier-UPHELD defect). Re-resolve against the trace
|
|
629
|
+
// AUGMENTED with the same deterministic projections the packet was built from.
|
|
630
|
+
const situation = verifySituationViews(rawSituation);
|
|
608
631
|
const gatingFails = gatingFailVerdicts(verdicts, severityById, input.gatingSeverities);
|
|
609
632
|
const gatingFailIds = new Set(gatingFails.map((v) => v.criterionId));
|
|
610
633
|
for (const v of verdicts) {
|
|
@@ -748,3 +771,198 @@ export function readMatrixVerdictFiles(verdictDir: string, trajectoryIds: string
|
|
|
748
771
|
}
|
|
749
772
|
return raws;
|
|
750
773
|
}
|
|
774
|
+
|
|
775
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
776
|
+
// v3 LAYERED WALK — aggregate-side folds (E-NEW-1/3/9 · D2-A). ALL PURE, ALL
|
|
777
|
+
// ADDITIVE: consumed by run-evaluate AFTER the (untouched) criteria aggregate;
|
|
778
|
+
// the GATE function is NOT edited — layer verdicts are diagnostic columns and
|
|
779
|
+
// cross-layer CONFLICTS are surfaced for a human, never auto-resolved (OT-1).
|
|
780
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
781
|
+
|
|
782
|
+
import { LAYER_IDS, type LayerVerdict, type WalkEmissions } from "./contracts/eval-matrix.ts";
|
|
783
|
+
|
|
784
|
+
/** One trajectory's layer row for the report's layer matrix (skipped ≠ undecidable). */
|
|
785
|
+
export interface LayerFoldRow {
|
|
786
|
+
trajectoryId: string;
|
|
787
|
+
/** layer id → verdict; layers the walk never mentioned are `absent` (pre-v3 file →
|
|
788
|
+
* NAMED degrade); `fired` = the L0 evidence-state (patterns fired, no verdict). */
|
|
789
|
+
byLayer: Record<string, "pass" | "fail" | "undecidable" | "skipped" | "fired" | "no-fires" | "absent">;
|
|
790
|
+
earlyExit?: string;
|
|
791
|
+
/** COERCED to a count (a live judge may emit the engaged layer-id LIST). */
|
|
792
|
+
layersEngaged?: number;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/** Per-layer fold buckets. `other` is the NaN-proof catch-all for any tolerant
|
|
796
|
+
* verdict value the buckets don't know yet — surfaced, never silently dropped
|
|
797
|
+
* (the raw value stays visible in the row's `byLayer`). */
|
|
798
|
+
export interface LayerFoldTotals {
|
|
799
|
+
pass: number;
|
|
800
|
+
fail: number;
|
|
801
|
+
undecidable: number;
|
|
802
|
+
skipped: number;
|
|
803
|
+
fired: number;
|
|
804
|
+
/** L0 clean-state (library ran, no pattern fired) — honest clean, never a pass. */
|
|
805
|
+
"no-fires": number;
|
|
806
|
+
absent: number;
|
|
807
|
+
other: number;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/** Coerce a tolerant `layersEngaged` (count OR layer-id list) to a count. */
|
|
811
|
+
export function layersEngagedCount(v: number | string[] | undefined): number | undefined {
|
|
812
|
+
return Array.isArray(v) ? v.length : v;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/** E-NEW-1 — fold per-walk layerVerdicts into per-trajectory rows + per-layer totals. */
|
|
816
|
+
export function foldLayerVerdicts(files: MatrixVerdictFile[]): {
|
|
817
|
+
rows: LayerFoldRow[];
|
|
818
|
+
totals: Record<string, LayerFoldTotals>;
|
|
819
|
+
} {
|
|
820
|
+
const totals: Record<string, LayerFoldTotals> = {};
|
|
821
|
+
for (const layer of LAYER_IDS)
|
|
822
|
+
totals[layer] = { pass: 0, fail: 0, undecidable: 0, skipped: 0, fired: 0, "no-fires": 0, absent: 0, other: 0 };
|
|
823
|
+
const rows: LayerFoldRow[] = files.map((file) => {
|
|
824
|
+
const byLayer: LayerFoldRow["byLayer"] = {};
|
|
825
|
+
for (const layer of LAYER_IDS) {
|
|
826
|
+
const lv = (file.layerVerdicts ?? []).find((x: LayerVerdict) => x.layer === layer);
|
|
827
|
+
const v = (lv?.verdict ?? "absent") as LayerFoldRow["byLayer"][string];
|
|
828
|
+
byLayer[layer] = v;
|
|
829
|
+
const bucket = totals[layer]![v as keyof LayerFoldTotals] !== undefined ? (v as keyof LayerFoldTotals) : "other";
|
|
830
|
+
totals[layer]![bucket] += 1;
|
|
831
|
+
}
|
|
832
|
+
return {
|
|
833
|
+
trajectoryId: file.trajectoryId,
|
|
834
|
+
byLayer,
|
|
835
|
+
earlyExit: file.earlyExit,
|
|
836
|
+
layersEngaged: layersEngagedCount(file.layersEngaged),
|
|
837
|
+
};
|
|
838
|
+
});
|
|
839
|
+
return { rows, totals };
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** E-NEW-3 · D2-A — one detected cross-layer disagreement. NEVER auto-resolved. */
|
|
843
|
+
export interface LayerConflict {
|
|
844
|
+
trajectoryId: string;
|
|
845
|
+
/** the disagreeing layers, e.g. ["L1","L2"]. */
|
|
846
|
+
layers: string[];
|
|
847
|
+
verdicts: Record<string, string>;
|
|
848
|
+
/** the judge's own note if it flagged the tension (honest emission). */
|
|
849
|
+
note?: string;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Detect outcome-vs-deep-layer disagreements (the OT-1 class): L1 pass with any
|
|
854
|
+
* of L2/L3/L4 fail, or L1 fail with ALL deep layers pass. Detection ONLY — the
|
|
855
|
+
* conflict routes to the human calibration queue; the GATE never reads it.
|
|
856
|
+
*/
|
|
857
|
+
export function detectLayerConflicts(files: MatrixVerdictFile[]): LayerConflict[] {
|
|
858
|
+
const out: LayerConflict[] = [];
|
|
859
|
+
for (const file of files) {
|
|
860
|
+
const lvs = file.layerVerdicts ?? [];
|
|
861
|
+
const get = (layer: string): LayerVerdict | undefined => lvs.find((x: LayerVerdict) => x.layer === layer);
|
|
862
|
+
const l1 = get("L1");
|
|
863
|
+
if (!l1) continue;
|
|
864
|
+
const deep = ["L2", "L3", "L4"].map((l) => get(l)).filter((x): x is LayerVerdict => x !== undefined);
|
|
865
|
+
const deepFails = deep.filter((x) => x.verdict === "fail");
|
|
866
|
+
const deepDecided = deep.filter((x) => x.verdict === "pass" || x.verdict === "fail");
|
|
867
|
+
if (l1.verdict === "pass" && deepFails.length > 0) {
|
|
868
|
+
const layers = ["L1", ...deepFails.map((x) => x.layer)];
|
|
869
|
+
out.push({
|
|
870
|
+
trajectoryId: file.trajectoryId,
|
|
871
|
+
layers,
|
|
872
|
+
verdicts: Object.fromEntries([["L1", "pass"], ...deepFails.map((x) => [x.layer, x.verdict])]),
|
|
873
|
+
note: deepFails.map((x) => x.note).filter(Boolean).join(" · ") || l1.note,
|
|
874
|
+
});
|
|
875
|
+
} else if (l1.verdict === "fail" && deepDecided.length > 0 && deepFails.length === 0) {
|
|
876
|
+
out.push({
|
|
877
|
+
trajectoryId: file.trajectoryId,
|
|
878
|
+
layers: ["L1", ...deepDecided.map((x) => x.layer)],
|
|
879
|
+
verdicts: Object.fromEntries([["L1", "fail"], ...deepDecided.map((x) => [x.layer, x.verdict])]),
|
|
880
|
+
note: l1.note,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return out;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* COMPLETENESS LAW (S1.2) — every (criterion × complete-fidelity trajectory) cell
|
|
889
|
+
* must be accounted for: decided, abstained (uncertain), or explicitly `na` in the
|
|
890
|
+
* denseMap. A silently-missing cell THROWS — a criterion may never go unverdicted
|
|
891
|
+
* because nothing bound. INCOMPLETE (fidelity) trajectories are exempt by design.
|
|
892
|
+
*/
|
|
893
|
+
export function assertNoUnverdictedCriterion(criteria: MatrixCriterion[], files: MatrixVerdictFile[]): void {
|
|
894
|
+
const missing: string[] = [];
|
|
895
|
+
for (const file of files) {
|
|
896
|
+
if (file.fidelity && file.fidelity.complete === false) continue; // INCOMPLETE exempt
|
|
897
|
+
const judged = new Set(file.verdicts.map((v) => v.criterionId));
|
|
898
|
+
const dense = file.denseMap ?? {};
|
|
899
|
+
for (const c of criteria) {
|
|
900
|
+
if (judged.has(c.criterionId)) continue;
|
|
901
|
+
if (dense[c.criterionId] !== undefined) continue; // na / dense-mapped = accounted
|
|
902
|
+
missing.push(`${file.trajectoryId} × ${c.criterionId}`);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (missing.length > 0) {
|
|
906
|
+
throw new Error(
|
|
907
|
+
`COMPLETENESS LAW: ${missing.length} (criterion × trajectory) cell(s) unaccounted — a criterion ` +
|
|
908
|
+
`may NEVER be silently unverdicted (decide, abstain-with-reason, or mark na in denseMap). ` +
|
|
909
|
+
`First offenders: ${missing.slice(0, 5).join(" · ")}`,
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/** MR-5 (E-NEW-9b) — fold the walks' emissions self-manifests into the run
|
|
915
|
+
* data-completeness scorecard: expected-key coverage + every NAMED missing reason. */
|
|
916
|
+
export interface EmissionsScorecard {
|
|
917
|
+
walksWithManifest: number;
|
|
918
|
+
walksTotal: number;
|
|
919
|
+
/** expected emission keys of the v3 contract. */
|
|
920
|
+
expectedKeys: string[];
|
|
921
|
+
/** key → number of complete-fidelity walks that emitted it. */
|
|
922
|
+
emittedCounts: Record<string, number>;
|
|
923
|
+
/** every named degrade: {trajectoryId, key, reason}. */
|
|
924
|
+
namedMissing: { trajectoryId: string; key: string; reason: string }[];
|
|
925
|
+
/** walks that emitted NO manifest at all (pre-v3 / defect — a named degrade itself). */
|
|
926
|
+
manifestAbsent: string[];
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
export const V3_EXPECTED_EMISSIONS: readonly string[] = [
|
|
930
|
+
"layerVerdicts",
|
|
931
|
+
"understanding",
|
|
932
|
+
"expectedTrajectory",
|
|
933
|
+
"agentSteps",
|
|
934
|
+
"judgeSteps",
|
|
935
|
+
"emissions",
|
|
936
|
+
];
|
|
937
|
+
|
|
938
|
+
export function foldEmissionsScorecard(files: MatrixVerdictFile[]): EmissionsScorecard {
|
|
939
|
+
const complete = files.filter((f) => !(f.fidelity && f.fidelity.complete === false));
|
|
940
|
+
const emittedCounts: Record<string, number> = {};
|
|
941
|
+
for (const k of V3_EXPECTED_EMISSIONS) emittedCounts[k] = 0;
|
|
942
|
+
const namedMissing: EmissionsScorecard["namedMissing"] = [];
|
|
943
|
+
const manifestAbsent: string[] = [];
|
|
944
|
+
for (const f of complete) {
|
|
945
|
+
// ground truth from the file itself (never trust-only the self-report):
|
|
946
|
+
const present = (k: string): boolean =>
|
|
947
|
+
(f as unknown as Record<string, unknown>)[k] !== undefined &&
|
|
948
|
+
(!Array.isArray((f as unknown as Record<string, unknown>)[k]) ||
|
|
949
|
+
((f as unknown as Record<string, unknown>)[k] as unknown[]).length > 0);
|
|
950
|
+
for (const k of V3_EXPECTED_EMISSIONS) if (present(k)) emittedCounts[k]! += 1;
|
|
951
|
+
const manifest: WalkEmissions | undefined = f.emissions;
|
|
952
|
+
if (!manifest) {
|
|
953
|
+
manifestAbsent.push(f.trajectoryId);
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
for (const m of manifest.missing ?? []) {
|
|
957
|
+
namedMissing.push({ trajectoryId: f.trajectoryId, key: m.key, reason: m.reason });
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return {
|
|
961
|
+
walksWithManifest: complete.length - manifestAbsent.length,
|
|
962
|
+
walksTotal: complete.length,
|
|
963
|
+
expectedKeys: [...V3_EXPECTED_EMISSIONS],
|
|
964
|
+
emittedCounts,
|
|
965
|
+
namedMissing,
|
|
966
|
+
manifestAbsent,
|
|
967
|
+
};
|
|
968
|
+
}
|