@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
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* code-eval-library — the L0 CODE-CHECKS layer of the v3 layered walk (W2 · D4=A).
|
|
3
|
+
*
|
|
4
|
+
* The growing library of code-detectable failure patterns, run FIRST as the cheap
|
|
5
|
+
* early filter before any trace is read by the LLM judge. Each entry is a STATIC
|
|
6
|
+
* check over the MatrixPacket's trajectory — NO LLM, no content semantics. A
|
|
7
|
+
* pattern FIRING flags a CANDIDATE for the deeper layers; it is NOT a verdict
|
|
8
|
+
* (code-detectable ≠ incorrect — the meta-judge fence). The judge records each
|
|
9
|
+
* hit as `codeEvalHits[{pattern, anchor, detail}]` (E-NEW-5) — evidence, never
|
|
10
|
+
* a layer verdict on its own.
|
|
11
|
+
*
|
|
12
|
+
* TS re-implementation of the Labs seeds (spec-first absorption, D4=A). The
|
|
13
|
+
* Python originals stay in Labs as measurement provenance (catch 1.0 / FP 0.0
|
|
14
|
+
* held-out on the aliased benchmark — a maturity fence, not a claim about any
|
|
15
|
+
* new subject). Origins:
|
|
16
|
+
* CE-P1 fault-passthrough ← M-tool-fault-passthrough (fail at k → DIFFERENT tool at k+1)
|
|
17
|
+
* CE-P2 error-output ← M-tool-output-check (completeness arm: any error output)
|
|
18
|
+
* CE-P3 malformed-structure ← M-tool-output-check (structural arm; schema-less here: truncation only)
|
|
19
|
+
* CE-P4 unguarded-send ← M-expected-outcome-map (required-review-missing invariant)
|
|
20
|
+
* CE-P5 out-of-order-gate ← M-behavioral-tree-divergence (send precedes the present review)
|
|
21
|
+
*
|
|
22
|
+
* GENERIC fence: the send/review effect classifier is name-heuristic SEED logic
|
|
23
|
+
* and is INJECTABLE — a subject profile may carry better effect classes; nothing
|
|
24
|
+
* subject-specific is hard-coded. Deterministic — no clock, no random, no network.
|
|
25
|
+
*
|
|
26
|
+
* CLI (how the judge runs L0 per the mode doc):
|
|
27
|
+
* bun scripts/code-eval-library.ts <packet.json> → {"hits":[{pattern,anchor,detail}]} on stdout
|
|
28
|
+
*/
|
|
29
|
+
import { readFileSync } from "node:fs";
|
|
30
|
+
|
|
31
|
+
/** One L0 hit — mirrors the CodeEvalHit contract (eval-matrix.ts). */
|
|
32
|
+
export interface CodeEvalHit {
|
|
33
|
+
pattern: string;
|
|
34
|
+
anchor: string;
|
|
35
|
+
detail: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The minimal step shape the library reads (the packet's trajectory[N]). */
|
|
39
|
+
export interface CodeEvalStep {
|
|
40
|
+
name: string;
|
|
41
|
+
input?: unknown;
|
|
42
|
+
output?: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** send/review effect classes for the gate patterns (CE-P4/P5). */
|
|
46
|
+
export type EffectClass = "send" | "review" | "other";
|
|
47
|
+
|
|
48
|
+
/** Heuristic SEED classifier — injectable; a subject profile may override. */
|
|
49
|
+
export function defaultEffectClassifier(step: CodeEvalStep): EffectClass {
|
|
50
|
+
const n = step.name.toLowerCase();
|
|
51
|
+
if (/send|dispatch|publish|reply|post/.test(n)) return "send";
|
|
52
|
+
if (/review|approv|guard|verify|check|confirm/.test(n)) return "review";
|
|
53
|
+
return "other";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Did this tool output fail? success:false OR an error-shaped payload. */
|
|
57
|
+
function outputFailed(step: CodeEvalStep): boolean {
|
|
58
|
+
const out = step.output;
|
|
59
|
+
if (out === null || out === undefined || typeof out !== "object") return false;
|
|
60
|
+
const rec = out as Record<string, unknown>;
|
|
61
|
+
if (rec["success"] === false) return true;
|
|
62
|
+
if (typeof rec["error"] === "string" && rec["error"].length > 0) return true;
|
|
63
|
+
if (rec["status"] === "error" || rec["status"] === "failed") return true;
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Was this output truncated? (the packet marks it; absent = false). */
|
|
68
|
+
function outputTruncated(step: CodeEvalStep): boolean {
|
|
69
|
+
const out = step.output;
|
|
70
|
+
if (out === null || out === undefined || typeof out !== "object") return false;
|
|
71
|
+
return (out as Record<string, unknown>)["truncated"] === true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* CE-P1 fault-passthrough: a tool fails at step k and step k+1 is a DIFFERENT
|
|
76
|
+
* tool (not a same-tool retry, not run-end). Strict-local ⇒ an UPPER BOUND —
|
|
77
|
+
* handling that arrives >1 step later is under-credited (the origin's fence).
|
|
78
|
+
*/
|
|
79
|
+
function ceP1(steps: CodeEvalStep[]): CodeEvalHit | null {
|
|
80
|
+
for (let k = 0; k < steps.length; k++) {
|
|
81
|
+
const s = steps[k]!;
|
|
82
|
+
if (!outputFailed(s)) continue;
|
|
83
|
+
if (k + 1 >= steps.length) continue; // run ended at the fault → handled-by-termination
|
|
84
|
+
const nxt = steps[k + 1]!;
|
|
85
|
+
if (nxt.name === s.name) continue; // same-tool retry → handled
|
|
86
|
+
return {
|
|
87
|
+
pattern: "CE-P1 fault-passthrough",
|
|
88
|
+
anchor: `trajectory.${k}`,
|
|
89
|
+
detail: `${s.name} failed; the chronologically NEXT step is a DIFFERENT tool (${nxt.name}) — proceeded on a broken result (strict-local upper bound).`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** CE-P2 error-output: ANY error tool output (completeness arm; a later-handled
|
|
96
|
+
* error still fires — separating handled is CE-P1's job, the declared cost). */
|
|
97
|
+
function ceP2(steps: CodeEvalStep[]): CodeEvalHit | null {
|
|
98
|
+
const k = steps.findIndex(outputFailed);
|
|
99
|
+
if (k < 0) return null;
|
|
100
|
+
return {
|
|
101
|
+
pattern: "CE-P2 error-output",
|
|
102
|
+
anchor: `trajectory.${k}`,
|
|
103
|
+
detail: `${steps[k]!.name} returned an error-shaped output (success:false / error payload).`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** CE-P3 malformed-structure (schema-less form): a TRUNCATED payload is missing
|
|
108
|
+
* its trailing fields — structural. Full schema conformance needs a derived
|
|
109
|
+
* clean-corpus schema (not available generically) — declared scope limit. */
|
|
110
|
+
function ceP3(steps: CodeEvalStep[]): CodeEvalHit | null {
|
|
111
|
+
for (let k = 0; k < steps.length; k++) {
|
|
112
|
+
if (outputFailed(steps[k]!)) continue; // error outputs are CE-P2's domain (disjoint scoping)
|
|
113
|
+
if (outputTruncated(steps[k]!)) {
|
|
114
|
+
return {
|
|
115
|
+
pattern: "CE-P3 malformed-structure",
|
|
116
|
+
anchor: `trajectory.${k}`,
|
|
117
|
+
detail: `${steps[k]!.name} output marked truncated — trailing fields missing (structural).`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** CE-P4 unguarded-send: a send effect with NO review effect anywhere. */
|
|
125
|
+
function ceP4(seq: { cls: EffectClass; k: number; name: string }[]): CodeEvalHit | null {
|
|
126
|
+
const send = seq.find((e) => e.cls === "send");
|
|
127
|
+
if (send === undefined) return null;
|
|
128
|
+
if (seq.some((e) => e.cls === "review")) return null; // review present → CE-P5's domain
|
|
129
|
+
return {
|
|
130
|
+
pattern: "CE-P4 unguarded-send",
|
|
131
|
+
anchor: `trajectory.${send.k}`,
|
|
132
|
+
detail: `${send.name} (send effect) with NO review-class step anywhere in the trajectory.`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** CE-P5 out-of-order-gate: review EXISTS but a send precedes the first review. */
|
|
137
|
+
function ceP5(seq: { cls: EffectClass; k: number; name: string }[]): CodeEvalHit | null {
|
|
138
|
+
const firstReview = seq.findIndex((e) => e.cls === "review");
|
|
139
|
+
if (firstReview < 0) return null; // no review at all → CE-P4's domain, not a reorder
|
|
140
|
+
const early = seq.find((e, i) => e.cls === "send" && i < firstReview);
|
|
141
|
+
if (early === undefined) return null;
|
|
142
|
+
return {
|
|
143
|
+
pattern: "CE-P5 out-of-order-gate",
|
|
144
|
+
anchor: `trajectory.${early.k}`,
|
|
145
|
+
detail: `${early.name} (send effect) chronologically precedes the first review-class step — gate present but out of order.`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The step order of the input array. CE-P1/CE-P5 are ORDER-BEARING; a
|
|
150
|
+
* MatrixPacket's `trajectory` preserves the trace's observations order, which
|
|
151
|
+
* is REVERSE-chronological in this intake (the judge brief says the same:
|
|
152
|
+
* "the observations array is reverse-chronological"). Default matches the
|
|
153
|
+
* packet so `bun … <packet.json>` is correct out of the box. */
|
|
154
|
+
export type StepOrder = "chronological" | "reverse-chronological";
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Run the whole library over a packet's trajectory steps. Returns every fired
|
|
158
|
+
* hit (empty = no pattern fired — which is NOT a clean-bill verdict, only "no
|
|
159
|
+
* code-detectable candidate"). Order-bearing checks (CE-P1/CE-P5) run over the
|
|
160
|
+
* CHRONOLOGICAL sequence; anchors always cite the ORIGINAL input index (so a
|
|
161
|
+
* `trajectory.N` anchor re-resolves against the packet as handed). PURE +
|
|
162
|
+
* deterministic.
|
|
163
|
+
*/
|
|
164
|
+
export function runCodeEvalLibrary(
|
|
165
|
+
steps: CodeEvalStep[],
|
|
166
|
+
classify: (s: CodeEvalStep) => EffectClass = defaultEffectClassifier,
|
|
167
|
+
order: StepOrder = "reverse-chronological",
|
|
168
|
+
): CodeEvalHit[] {
|
|
169
|
+
// normalize to chronological, remembering each step's ORIGINAL packet index.
|
|
170
|
+
const indexed = steps.map((s, k) => ({ s, k }));
|
|
171
|
+
const chrono = order === "reverse-chronological" ? [...indexed].reverse() : indexed;
|
|
172
|
+
const chronoSteps = chrono.map((e) => e.s);
|
|
173
|
+
const seq = chrono.map((e, i) => ({ cls: classify(e.s), k: e.k, name: e.s.name, i }));
|
|
174
|
+
// re-anchor a hit produced against chrono positions back to the packet index.
|
|
175
|
+
const reanchor = (h: CodeEvalHit | null): CodeEvalHit | null => {
|
|
176
|
+
if (h === null) return h;
|
|
177
|
+
const m = /^trajectory\.(\d+)$/.exec(h.anchor);
|
|
178
|
+
if (m === null) return h;
|
|
179
|
+
const orig = chrono[Number.parseInt(m[1]!, 10)];
|
|
180
|
+
return orig === undefined ? h : { ...h, anchor: `trajectory.${orig.k}` };
|
|
181
|
+
};
|
|
182
|
+
return [
|
|
183
|
+
reanchor(ceP1(chronoSteps)),
|
|
184
|
+
reanchor(ceP2(chronoSteps)),
|
|
185
|
+
reanchor(ceP3(chronoSteps)),
|
|
186
|
+
ceP4(seq), // seq entries already carry the original index k
|
|
187
|
+
ceP5(seq),
|
|
188
|
+
].filter((h): h is CodeEvalHit => h !== null);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── CLI: bun scripts/code-eval-library.ts <packet.json> ──────────────────────
|
|
192
|
+
if (import.meta.main) {
|
|
193
|
+
const packetPath = process.argv[2];
|
|
194
|
+
if (packetPath === undefined) {
|
|
195
|
+
console.error("usage: bun scripts/code-eval-library.ts <packet.json>");
|
|
196
|
+
process.exit(2);
|
|
197
|
+
}
|
|
198
|
+
const packet = JSON.parse(readFileSync(packetPath, "utf8")) as { trajectory?: CodeEvalStep[] };
|
|
199
|
+
const hits = runCodeEvalLibrary(packet.trajectory ?? []);
|
|
200
|
+
console.info(JSON.stringify({ hits }, null, 2));
|
|
201
|
+
}
|
|
@@ -1,84 +1,146 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* scripts/contracts/agentspec-evals.ts — the MINIMAL
|
|
2
|
+
* scripts/contracts/agentspec-evals.ts — the MINIMAL `spec.evaluation` slice the
|
|
3
3
|
* EVAL stage consumes (standalone — NEVER imports the agentspec skill's schema).
|
|
4
4
|
* ---------------------------------------------------------------------------
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* AgentSpec 0.3.0 (D18/D19) replaces the flat 0.2 `definition.evals` shape
|
|
6
|
+
* (`dataset_categories[] × edge_cases[]`) with `spec.evaluation`, a universal
|
|
7
|
+
* closure of THREE arrays (each may be empty, F05):
|
|
7
8
|
*
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - scenarios[]
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
9
|
+
* - criteria[] — `{ id, description, type, goal }` — binary-actionable pass/fail
|
|
10
|
+
* criteria (type ∈ llm-judge | code-check). (0.2 `success_criteria[]`, with
|
|
11
|
+
* `criterion` → `description`.)
|
|
12
|
+
* - scenarios[] — `{ id, description, expectedBehavior, edgeCase? }` — representative
|
|
13
|
+
* situations + the correct behavior. Scenario↔dataset linkage now lives on the
|
|
14
|
+
* dataset ITEM (`scenarioRef`), not on the scenario.
|
|
15
|
+
* - datasets[] — `{ id, description, mapsTo?, categories[], caseDimensions?,
|
|
16
|
+
* items[]|itemsRef? }` — REAL, already-materialized dataset rows (D18):
|
|
17
|
+
* · mapsTo — direct job/scenario/criterion references this dataset exercises.
|
|
18
|
+
* · categories[] — DATASET-LOCAL taxonomy (`{ id, description, generationGuidance? }`),
|
|
19
|
+
* no longer a top-level global list (D18).
|
|
20
|
+
* · caseDimensions — a MAP `name → { description?, values[] }`; the independent
|
|
21
|
+
* variation axes each item's `case` assigns over.
|
|
22
|
+
* · items[] — each `{ id, category?, scenarioRef?, case?, input?, expected? }`
|
|
23
|
+
* carrying a KIND-SPECIFIC `input`/`expected` payload (opaque data — we read the
|
|
24
|
+
* shape structurally, never interpret the kind).
|
|
16
25
|
*
|
|
17
26
|
* We DECLARE only the fields we read, with `additionalProperties: true` on the
|
|
18
|
-
* objects so a richer agentspec still parses (forward-compatible). The cross-skill
|
|
27
|
+
* objects so a richer 0.3.x agentspec still parses (forward-compatible). The cross-skill
|
|
19
28
|
* import ban (coding-rules "Sealed-Sibling" + standalone discipline) is why this is
|
|
20
29
|
* a local re-declaration, not an import. PURE — no clock / random / network.
|
|
21
30
|
*/
|
|
22
31
|
import { type Static, Type } from "@sinclair/typebox";
|
|
23
32
|
import { Value } from "@sinclair/typebox/value";
|
|
24
33
|
|
|
25
|
-
/** A
|
|
26
|
-
export const
|
|
34
|
+
/** A binary-actionable SUCCESS CRITERION (0.3 `criteria[]`). */
|
|
35
|
+
export const SpecEvalCriterionSchema = Type.Object(
|
|
27
36
|
{
|
|
28
37
|
id: Type.String({ minLength: 1 }),
|
|
29
38
|
description: Type.String({ minLength: 1 }),
|
|
30
|
-
|
|
39
|
+
type: Type.Union([Type.Literal("llm-judge"), Type.Literal("code-check")]),
|
|
40
|
+
goal: Type.String({ minLength: 1 }),
|
|
31
41
|
},
|
|
32
42
|
{ additionalProperties: true },
|
|
33
43
|
);
|
|
34
|
-
export type
|
|
44
|
+
export type SpecEvalCriterion = Static<typeof SpecEvalCriterionSchema>;
|
|
35
45
|
|
|
36
46
|
/** A representative SCENARIO (the situation + correct behavior). */
|
|
37
|
-
export const
|
|
47
|
+
export const SpecEvalScenarioSchema = Type.Object(
|
|
38
48
|
{
|
|
39
49
|
id: Type.String({ minLength: 1 }),
|
|
40
50
|
description: Type.String({ minLength: 1 }),
|
|
41
|
-
|
|
51
|
+
expectedBehavior: Type.String({ minLength: 1 }),
|
|
52
|
+
edgeCase: Type.Optional(Type.Boolean()),
|
|
53
|
+
},
|
|
54
|
+
{ additionalProperties: true },
|
|
55
|
+
);
|
|
56
|
+
export type SpecEvalScenario = Static<typeof SpecEvalScenarioSchema>;
|
|
57
|
+
|
|
58
|
+
/** A DATASET-LOCAL category slice (D18) — the taxonomy is owned by its dataset. */
|
|
59
|
+
export const SpecEvalDatasetCategorySchema = Type.Object(
|
|
60
|
+
{
|
|
61
|
+
id: Type.String({ minLength: 1 }),
|
|
62
|
+
description: Type.String({ minLength: 1 }),
|
|
63
|
+
generationGuidance: Type.Optional(Type.String()),
|
|
64
|
+
},
|
|
65
|
+
{ additionalProperties: true },
|
|
66
|
+
);
|
|
67
|
+
export type SpecEvalDatasetCategory = Static<typeof SpecEvalDatasetCategorySchema>;
|
|
68
|
+
|
|
69
|
+
/** One independent variation AXIS in a dataset's `caseDimensions` map (the map VALUE). */
|
|
70
|
+
export const SpecEvalCaseDimensionSchema = Type.Object(
|
|
71
|
+
{
|
|
72
|
+
description: Type.Optional(Type.String()),
|
|
73
|
+
values: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
|
|
74
|
+
},
|
|
75
|
+
{ additionalProperties: true },
|
|
76
|
+
);
|
|
77
|
+
export type SpecEvalCaseDimension = Static<typeof SpecEvalCaseDimensionSchema>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A single, already-materialized dataset ITEM. `case` assigns a value per
|
|
81
|
+
* `caseDimensions` axis; `input`/`expected` are opaque kind-specific payloads we
|
|
82
|
+
* carry structurally but never interpret.
|
|
83
|
+
*/
|
|
84
|
+
export const SpecEvalDatasetItemSchema = Type.Object(
|
|
85
|
+
{
|
|
86
|
+
id: Type.String({ minLength: 1 }),
|
|
42
87
|
category: Type.Optional(Type.String()),
|
|
43
|
-
|
|
88
|
+
scenarioRef: Type.Optional(Type.String()),
|
|
89
|
+
case: Type.Optional(Type.Record(Type.String({ minLength: 1 }), Type.String())),
|
|
90
|
+
input: Type.Optional(Type.Unknown()),
|
|
91
|
+
expected: Type.Optional(Type.Unknown()),
|
|
44
92
|
},
|
|
45
93
|
{ additionalProperties: true },
|
|
46
94
|
);
|
|
47
|
-
export type
|
|
95
|
+
export type SpecEvalDatasetItem = Static<typeof SpecEvalDatasetItemSchema>;
|
|
48
96
|
|
|
49
|
-
/**
|
|
50
|
-
export const
|
|
97
|
+
/** Direct job/scenario/criterion references a dataset exercises (all optional). */
|
|
98
|
+
export const SpecEvalDatasetMapsToSchema = Type.Object(
|
|
99
|
+
{
|
|
100
|
+
jobs: Type.Optional(Type.Array(Type.String())),
|
|
101
|
+
scenarios: Type.Optional(Type.Array(Type.String())),
|
|
102
|
+
criteria: Type.Optional(Type.Array(Type.String())),
|
|
103
|
+
},
|
|
104
|
+
{ additionalProperties: true },
|
|
105
|
+
);
|
|
106
|
+
export type SpecEvalDatasetMapsTo = Static<typeof SpecEvalDatasetMapsToSchema>;
|
|
107
|
+
|
|
108
|
+
/** A dataset: local categories + variation axes + the real materialized `items[]`. */
|
|
109
|
+
export const SpecEvalDatasetSchema = Type.Object(
|
|
51
110
|
{
|
|
52
111
|
id: Type.String({ minLength: 1 }),
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
112
|
+
description: Type.String({ minLength: 1 }),
|
|
113
|
+
mapsTo: Type.Optional(SpecEvalDatasetMapsToSchema),
|
|
114
|
+
categories: Type.Optional(Type.Array(SpecEvalDatasetCategorySchema)),
|
|
115
|
+
caseDimensions: Type.Optional(Type.Record(Type.String({ minLength: 1 }), SpecEvalCaseDimensionSchema)),
|
|
116
|
+
items: Type.Optional(Type.Array(SpecEvalDatasetItemSchema)),
|
|
117
|
+
itemsRef: Type.Optional(Type.String()),
|
|
56
118
|
},
|
|
57
119
|
{ additionalProperties: true },
|
|
58
120
|
);
|
|
59
|
-
export type
|
|
121
|
+
export type SpecEvalDataset = Static<typeof SpecEvalDatasetSchema>;
|
|
60
122
|
|
|
61
|
-
/** The `
|
|
62
|
-
export const
|
|
123
|
+
/** The `spec.evaluation` closure. Each array is required but may be empty (F05). */
|
|
124
|
+
export const SpecEvaluationSchema = Type.Object(
|
|
63
125
|
{
|
|
64
|
-
|
|
65
|
-
scenarios: Type.Array(
|
|
66
|
-
|
|
126
|
+
criteria: Type.Array(SpecEvalCriterionSchema),
|
|
127
|
+
scenarios: Type.Array(SpecEvalScenarioSchema),
|
|
128
|
+
datasets: Type.Array(SpecEvalDatasetSchema),
|
|
67
129
|
},
|
|
68
130
|
{ additionalProperties: true },
|
|
69
131
|
);
|
|
70
|
-
export type
|
|
132
|
+
export type SpecEvaluation = Static<typeof SpecEvaluationSchema>;
|
|
71
133
|
|
|
72
134
|
/**
|
|
73
|
-
* Parse + narrow the
|
|
74
|
-
* a malformed
|
|
135
|
+
* Parse + narrow the `spec.evaluation` slice (guarded). THROWS on schema violation —
|
|
136
|
+
* a malformed contract must never silently reach materialization. PURE.
|
|
75
137
|
*/
|
|
76
|
-
export function
|
|
77
|
-
if (!Value.Check(
|
|
78
|
-
const first = [...Value.Errors(
|
|
138
|
+
export function parseSpecEvaluation(value: unknown): SpecEvaluation {
|
|
139
|
+
if (!Value.Check(SpecEvaluationSchema, value)) {
|
|
140
|
+
const first = [...Value.Errors(SpecEvaluationSchema, value)][0];
|
|
79
141
|
throw new Error(
|
|
80
|
-
`
|
|
81
|
-
`${first?.message ?? "invalid
|
|
142
|
+
`parseSpecEvaluation: schema violation at '${first?.path ?? "(root)"}': ` +
|
|
143
|
+
`${first?.message ?? "invalid spec.evaluation slice"}`,
|
|
82
144
|
);
|
|
83
145
|
}
|
|
84
146
|
return value;
|