@mutagent/evaluator 0.1.0-alpha.5 → 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.
Files changed (55) hide show
  1. package/.claude/skills/mutagent-evaluator/SKILL.md +28 -30
  2. package/.claude/skills/mutagent-evaluator/assets/agents/dataset-builder.md +6 -5
  3. package/.claude/skills/mutagent-evaluator/assets/agents/evaluator.md +93 -30
  4. package/.claude/skills/mutagent-evaluator/assets/code-quality-criteria.yaml +75 -0
  5. package/.claude/skills/mutagent-evaluator/assets/templates/discover-report.template.html +397 -0
  6. package/.claude/skills/mutagent-evaluator/assets/templates/eval-report.template.html +594 -0
  7. package/.claude/skills/mutagent-evaluator/assets/templates/review-report.template.html +560 -0
  8. package/.claude/skills/mutagent-evaluator/golden/judge-trajectory.prose.md +69 -0
  9. package/.claude/skills/mutagent-evaluator/lenses/methodology-critic-lens.md +1 -1
  10. package/.claude/skills/mutagent-evaluator/references/data-registry.md +43 -0
  11. package/.claude/skills/mutagent-evaluator/references/edd-loop.md +6 -6
  12. package/.claude/skills/mutagent-evaluator/references/eval-layers.md +52 -0
  13. package/.claude/skills/mutagent-evaluator/references/eval-stage.md +37 -4
  14. package/.claude/skills/mutagent-evaluator/references/memory-format.md +1 -1
  15. package/.claude/skills/mutagent-evaluator/schemas/methodology-review.schema.yaml +1 -1
  16. package/.claude/skills/mutagent-evaluator/scripts/agent-dispatch.ts +0 -0
  17. package/.claude/skills/mutagent-evaluator/scripts/aggregate-discover.ts +254 -9
  18. package/.claude/skills/mutagent-evaluator/scripts/audience.ts +84 -0
  19. package/.claude/skills/mutagent-evaluator/scripts/build-review-ui.ts +320 -6
  20. package/.claude/skills/mutagent-evaluator/scripts/cli/prep.ts +32 -2
  21. package/.claude/skills/mutagent-evaluator/scripts/cli/profile-subject.ts +1 -1
  22. package/.claude/skills/mutagent-evaluator/scripts/code-eval-library.ts +201 -0
  23. package/.claude/skills/mutagent-evaluator/scripts/code-quality-verdict.ts +60 -68
  24. package/.claude/skills/mutagent-evaluator/scripts/config/schema.ts +3 -3
  25. package/.claude/skills/mutagent-evaluator/scripts/contracts/agentspec-evals.ts +101 -39
  26. package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-matrix.ts +326 -3
  27. package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-types.ts +30 -4
  28. package/.claude/skills/mutagent-evaluator/scripts/determine-outcome.ts +8 -0
  29. package/.claude/skills/mutagent-evaluator/scripts/edd/change-request.ts +1 -1
  30. package/.claude/skills/mutagent-evaluator/scripts/edd/edd-types.ts +2 -2
  31. package/.claude/skills/mutagent-evaluator/scripts/judge-prompt-template.ts +144 -15
  32. package/.claude/skills/mutagent-evaluator/scripts/materialize-dataset.ts +130 -68
  33. package/.claude/skills/mutagent-evaluator/scripts/matrix-judge.ts +219 -1
  34. package/.claude/skills/mutagent-evaluator/scripts/memory/append.ts +1 -1
  35. package/.claude/skills/mutagent-evaluator/scripts/memory/ratify.ts +165 -0
  36. package/.claude/skills/mutagent-evaluator/scripts/merge-criteria.ts +849 -0
  37. package/.claude/skills/mutagent-evaluator/scripts/prep-tasks.ts +15 -4
  38. package/.claude/skills/mutagent-evaluator/scripts/read-manifest.ts +120 -0
  39. package/.claude/skills/mutagent-evaluator/scripts/read-unitf-traces.ts +34 -2
  40. package/.claude/skills/mutagent-evaluator/scripts/render-build-cards.ts +37 -0
  41. package/.claude/skills/mutagent-evaluator/scripts/render-discover-report-v3.ts +1155 -0
  42. package/.claude/skills/mutagent-evaluator/scripts/render-eval-report-v3.ts +610 -0
  43. package/.claude/skills/mutagent-evaluator/scripts/render-eval-report.ts +11 -92
  44. package/.claude/skills/mutagent-evaluator/scripts/render-review-report-v3.ts +691 -0
  45. package/.claude/skills/mutagent-evaluator/scripts/report-fragments.ts +173 -0
  46. package/.claude/skills/mutagent-evaluator/scripts/route-failures.ts +12 -9
  47. package/.claude/skills/mutagent-evaluator/scripts/run-evaluate.ts +46 -1
  48. package/.claude/skills/mutagent-evaluator/scripts/run-pipeline.ts +39 -4
  49. package/.claude/skills/mutagent-evaluator/scripts/run-review.ts +266 -0
  50. package/.claude/skills/mutagent-evaluator/scripts/subject-profile.ts +82 -0
  51. package/.claude/skills/mutagent-evaluator/scripts/sync-eval-criteria.ts +2 -2
  52. package/.claude/skills/mutagent-evaluator/scripts/unitf-to-evaltrace.ts +64 -21
  53. package/.claude/skills/mutagent-evaluator/scripts/verify-render.ts +728 -0
  54. package/bin/mutagent-cli.mjs +9 -5
  55. 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
+ }
@@ -4,7 +4,7 @@
4
4
  * A JUDGE mode that scores a CODE subject's QUALITY (the amended/scaffolded
5
5
  * implementation of a `code`-kind subject) and emits a binary PASS/FAIL quality
6
6
  * verdict with a critique-before-verdict per criterion. It is the (b) half of the
7
- * ⑤ IMPROVE loop's code-target BOTH-gate: "converged" for a `code` subject = the
7
+ * ⑤ OPTIMIZE loop's code-target BOTH-gate: "converged" for a `code` subject = the
8
8
  * subject's OWN test suite green (a deterministic gate the session records as
9
9
  * `testsGreen`) AND this code-quality verdict PASSes. Neither alone converges.
10
10
  *
@@ -28,6 +28,9 @@
28
28
  * - PURE renderer + a thin injected-seam run wrapper. No clock, no random, no
29
29
  * network; the LLM call is the injected `JudgeInvoke` seam so tests drive it.
30
30
  */
31
+ import { readFileSync } from "node:fs";
32
+ import { fileURLToPath, URL } from "node:url";
33
+ import { parse as parseYaml } from "yaml";
31
34
  import { parseCritiqueVerdict, type JudgeInvoke } from "./determine-outcome.ts";
32
35
  import type { JudgePin } from "./build-evals.ts";
33
36
  import type { CritiqueVerdict } from "./contracts/eval-types.ts";
@@ -56,76 +59,65 @@ export interface CodeQualityCriterion {
56
59
  }
57
60
 
58
61
  /**
59
- * ⚠️ FLAGGED-FOR-OPERATOR-SIGN-OFF (Wave-2 W2I1). This is the DRAFT default set of
60
- * acceptance criteria that define "code quality-pass" BEYOND tests-green. It is
61
- * implemented behind this clearly-named default so the operator can confirm/adjust
62
- * the criteria + their severities WITHOUT a code change to the loop. The picks
63
- * below are a conservative, subject-agnostic starting point — NOT a final contract.
62
+ * ⚠️ FLAGGED-FOR-OPERATOR-SIGN-OFF (Wave-2 W2I1). The DRAFT default set of acceptance
63
+ * criteria that define "code quality-pass" BEYOND tests-green.
64
64
  *
65
- * The five drafted criteria (each BINARY, critique-before-verdict):
66
- * Q1 spec-faithful (critical) — the impl realizes the agentspec DEFINITION the
67
- * amend targeted; no dropped tool / JTBD / silently
68
- * altered system_prompt (def→impl cascade held).
69
- * Q2 addresses-diagnosis (high) — the change actually targets the diagnosed
70
- * root-cause locus, not an adjacent symptom.
71
- * Q3 no-regression (high) — no unrelated behavior removed/broken; scope stayed
72
- * on what the remedy named (scope discipline).
73
- * Q4 error-handling (high) — failure paths are handled, not silently dropped
74
- * (recovery/observability preserved).
75
- * Q5 maintainable (medium) — readable, framework-idiomatic, no dead code /
76
- * copy-paste / obvious footgun introduced.
65
+ * The rubric itself now lives in `../assets/code-quality-criteria.yaml` — a versioned,
66
+ * operator-editable ARTIFACT so the criteria + severities can be tuned WITHOUT a code
67
+ * change (FU-61.1). This module only LOADS and VALIDATES it; the shape/aggregation stay
68
+ * in code. Ids/statements/severities are unchanged from the original inline set.
77
69
  *
78
- * Aggregation (also FLAGGED): the gate is severity-gated — a `fail` OR `uncertain`
79
- * on any CRITICAL/HIGH criterion blocks the quality-pass; MEDIUM is advisory. The
80
- * operator may instead choose strict-all-pass or a different severity map.
70
+ * Aggregation (also FLAGGED, still in code): severity-gated — a `fail` OR `uncertain`
71
+ * on any CRITICAL/HIGH criterion blocks the quality-pass; MEDIUM is advisory.
72
+ *
73
+ * Fail-loud: a malformed/missing rubric THROWS at import. There is no silent fallback —
74
+ * a broken rubric must never quietly degrade the gate.
81
75
  */
82
- export const DEFAULT_CODE_QUALITY_CRITERIA: readonly CodeQualityCriterion[] = [
83
- {
84
- id: "Q1-spec-faithful",
85
- statement: "The implementation realizes the agentspec definition the amend targeted.",
86
- passDefinition:
87
- "PASS: every spec-declared tool / job-to-be-done is present and the operative system_prompt is unchanged except where the amend intended; the def→impl cascade held.",
88
- failDefinition:
89
- "FAIL: a spec-declared tool or JTBD is missing, or the system_prompt was altered beyond the amend intent (the impl forked from the SSoT).",
90
- severity: CodeQualitySeverity.Critical,
91
- },
92
- {
93
- id: "Q2-addresses-diagnosis",
94
- statement: "The change targets the diagnosed root-cause locus, not an adjacent symptom.",
95
- passDefinition:
96
- "PASS: the edit is at (or directly causes a fix at) the RCA-named locus / the failing-case root.",
97
- failDefinition:
98
- "FAIL: the change edits an unrelated place or patches a symptom while the root cause remains.",
99
- severity: CodeQualitySeverity.High,
100
- },
101
- {
102
- id: "Q3-no-regression",
103
- statement: "The change introduces no unrelated regression and stays in scope.",
104
- passDefinition:
105
- "PASS: only the remedy-named surface changed; no unrelated behavior was removed or broken.",
106
- failDefinition:
107
- "FAIL: unrelated code was changed/removed, or a previously-working path was broken.",
108
- severity: CodeQualitySeverity.High,
109
- },
110
- {
111
- id: "Q4-error-handling",
112
- statement: "Failure paths are handled, not silently dropped.",
113
- passDefinition:
114
- "PASS: error/failure branches surface, recover, or log per the subject's contract; no new silent drop.",
115
- failDefinition:
116
- "FAIL: a failure is swallowed with no recovery/notify/observability (a silent-drop regression).",
117
- severity: CodeQualitySeverity.High,
118
- },
119
- {
120
- id: "Q5-maintainable",
121
- statement: "The code is readable, framework-idiomatic, and free of obvious footguns.",
122
- passDefinition:
123
- "PASS: clear structure, idiomatic to the target framework, no dead code / copy-paste / obvious footgun introduced.",
124
- failDefinition:
125
- "FAIL: unreadable/duplicated/dead code or an obvious maintainability footgun was introduced.",
126
- severity: CodeQualitySeverity.Medium,
127
- },
128
- ] as const;
76
+ const CODE_QUALITY_CRITERIA_PATH = fileURLToPath(
77
+ new URL("../assets/code-quality-criteria.yaml", import.meta.url),
78
+ );
79
+
80
+ const VALID_SEVERITIES: readonly string[] = Object.values(CodeQualitySeverity);
81
+
82
+ /** Parse + validate the rubric artifact. PURE given the file; THROWS on any defect. */
83
+ export function parseCodeQualityCriteria(raw: string, source = CODE_QUALITY_CRITERIA_PATH): readonly CodeQualityCriterion[] {
84
+ // NOTE: a function DECLARATION (not a const arrow) so TS narrows through the
85
+ // never-returning calls below — control-flow analysis requires this form.
86
+ function bad(msg: string): never {
87
+ throw new Error(`code-quality rubric invalid (${source}): ${msg}`);
88
+ }
89
+ const doc = parseYaml(raw) as { version?: unknown; criteria?: unknown } | null;
90
+ if (doc === null || typeof doc !== "object") bad("not a YAML mapping");
91
+ if (typeof doc.version !== "number") bad("`version` must be a number");
92
+ if (!Array.isArray(doc.criteria) || doc.criteria.length === 0) bad("`criteria` must be a non-empty list");
93
+
94
+ const seen = new Set<string>();
95
+ const out = (doc.criteria as unknown[]).map((c, i) => {
96
+ const at = `criteria[${i}]`;
97
+ if (c === null || typeof c !== "object") return bad(`${at} is not a mapping`);
98
+ const r = c as Record<string, unknown>;
99
+ for (const k of ["id", "statement", "passDefinition", "failDefinition", "severity"]) {
100
+ if (typeof r[k] !== "string" || (r[k] as string).trim() === "") bad(`${at}.${k} must be a non-empty string`);
101
+ }
102
+ const id = r.id as string;
103
+ if (seen.has(id)) bad(`${at}.id "${id}" is duplicated`);
104
+ seen.add(id);
105
+ if (!VALID_SEVERITIES.includes(r.severity as string)) {
106
+ bad(`${at}.severity "${String(r.severity)}" must be one of ${VALID_SEVERITIES.join(" | ")}`);
107
+ }
108
+ return {
109
+ id,
110
+ statement: r.statement as string,
111
+ passDefinition: r.passDefinition as string,
112
+ failDefinition: r.failDefinition as string,
113
+ severity: r.severity as CodeQualitySeverityValue,
114
+ } satisfies CodeQualityCriterion;
115
+ });
116
+ return Object.freeze(out);
117
+ }
118
+
119
+ export const DEFAULT_CODE_QUALITY_CRITERIA: readonly CodeQualityCriterion[] =
120
+ parseCodeQualityCriteria(readFileSync(CODE_QUALITY_CRITERIA_PATH, "utf8"));
129
121
 
130
122
  /** The code subject under quality review (minimal, subject-agnostic). */
131
123
  export interface CodeQualitySubject {
@@ -235,9 +235,9 @@ export type GlobalSource = Static<typeof GlobalSourceSchema>;
235
235
 
236
236
  /**
237
237
  * A precise link to an Agent / Tooling definition file in a code target.
238
- * RESERVED — consumed by Build/Improve (future). The evaluator is JUDGE-ONLY (EV-051)
238
+ * RESERVED — consumed by Build/Optimize (future). The evaluator is JUDGE-ONLY (EV-051)
239
239
  * and never writes a target; `targets[].code_refs` is ported for shape parity only and
240
- * has NO evaluator consumer today. PURPOSE (future): tells BUILD/IMPROVE which files
240
+ * has NO evaluator consumer today. PURPOSE (future): tells BUILD/OPTIMIZE which files
241
241
  * realize the agent so a fix lands on the right def.
242
242
  */
243
243
  export const CodeRefSchema = Type.Object(
@@ -253,7 +253,7 @@ export type CodeRef = Static<typeof CodeRefSchema>;
253
253
  * HOW a fix is applied for a target. `kind` gates the report-only branch.
254
254
  * CONSUMER: the diagnostics APPLY path (not the evaluator — judge-only). PURPOSE:
255
255
  * selects the write mechanism for a fix.
256
- * versioning / pr — RESERVED, consumed by Build/Improve (future). No evaluator
256
+ * versioning / pr — RESERVED, consumed by Build/Optimize (future). No evaluator
257
257
  * consumer; ported for parity. PURPOSE (future): toggle spec-bump /
258
258
  * open-a-PR on apply.
259
259
  */
@@ -1,84 +1,146 @@
1
1
  /**
2
- * scripts/contracts/agentspec-evals.ts — the MINIMAL agentspec.evals slice the
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
- * The ADL `*build` stage emits an agentspec whose `definition.evals` block carries
6
- * the SEED material the evaluator materializes into a real dataset (F8) + criteria:
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
- * - dataset_categories[] — `{ id, description, edge_cases[] }` — the golden
9
- * eval-suite slices + the explicit edge_cases each must exercise (the dataset
10
- * DEFINITION the spec hands forward; "seed, don't duplicate").
11
- * - scenarios[] — `{ id, description, expected_behavior, category?,
12
- * edge_case? }` — representative situations + the correct behavior (extra seed
13
- * material, optionally tagged into a category).
14
- * - success_criteria[] — `{ id, criterion, type, goal }` — binary-actionable
15
- * pass/fail criteria (type llm-judge | code-check).
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 dataset CATEGORY slice + the edge-cases it must exercise. */
26
- export const AgentspecDatasetCategorySchema = Type.Object(
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
- edge_cases: Type.Array(Type.String()),
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 AgentspecDatasetCategory = Static<typeof AgentspecDatasetCategorySchema>;
44
+ export type SpecEvalCriterion = Static<typeof SpecEvalCriterionSchema>;
35
45
 
36
46
  /** A representative SCENARIO (the situation + correct behavior). */
37
- export const AgentspecScenarioSchema = Type.Object(
47
+ export const SpecEvalScenarioSchema = Type.Object(
38
48
  {
39
49
  id: Type.String({ minLength: 1 }),
40
50
  description: Type.String({ minLength: 1 }),
41
- expected_behavior: Type.String({ minLength: 1 }),
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
- edge_case: Type.Optional(Type.Boolean()),
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 AgentspecScenario = Static<typeof AgentspecScenarioSchema>;
95
+ export type SpecEvalDatasetItem = Static<typeof SpecEvalDatasetItemSchema>;
48
96
 
49
- /** A binary-actionable SUCCESS CRITERION. */
50
- export const AgentspecSuccessCriterionSchema = Type.Object(
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
- criterion: Type.String({ minLength: 1 }),
54
- type: Type.Union([Type.Literal("llm-judge"), Type.Literal("code-check")]),
55
- goal: Type.String({ minLength: 1 }),
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 AgentspecSuccessCriterion = Static<typeof AgentspecSuccessCriterionSchema>;
121
+ export type SpecEvalDataset = Static<typeof SpecEvalDatasetSchema>;
60
122
 
61
- /** The `definition.evals` slice. Each array may be empty structurally. */
62
- export const AgentspecEvalsSchema = Type.Object(
123
+ /** The `spec.evaluation` closure. Each array is required but may be empty (F05). */
124
+ export const SpecEvaluationSchema = Type.Object(
63
125
  {
64
- success_criteria: Type.Array(AgentspecSuccessCriterionSchema),
65
- scenarios: Type.Array(AgentspecScenarioSchema),
66
- dataset_categories: Type.Array(AgentspecDatasetCategorySchema),
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 AgentspecEvals = Static<typeof AgentspecEvalsSchema>;
132
+ export type SpecEvaluation = Static<typeof SpecEvaluationSchema>;
71
133
 
72
134
  /**
73
- * Parse + narrow the agentspec.evals slice (guarded). THROWS on schema violation —
74
- * a malformed seed must never silently reach materialization. PURE.
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 parseAgentspecEvals(value: unknown): AgentspecEvals {
77
- if (!Value.Check(AgentspecEvalsSchema, value)) {
78
- const first = [...Value.Errors(AgentspecEvalsSchema, value)][0];
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
- `parseAgentspecEvals: schema violation at '${first?.path ?? "(root)"}': ` +
81
- `${first?.message ?? "invalid agentspec evals slice"}`,
142
+ `parseSpecEvaluation: schema violation at '${first?.path ?? "(root)"}': ` +
143
+ `${first?.message ?? "invalid spec.evaluation slice"}`,
82
144
  );
83
145
  }
84
146
  return value;