@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.
- package/.claude/skills/mutagent-evaluator/SKILL.md +28 -30
- package/.claude/skills/mutagent-evaluator/assets/agents/dataset-builder.md +6 -5
- package/.claude/skills/mutagent-evaluator/assets/agents/evaluator.md +93 -30
- package/.claude/skills/mutagent-evaluator/assets/code-quality-criteria.yaml +75 -0
- 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/lenses/methodology-critic-lens.md +1 -1
- package/.claude/skills/mutagent-evaluator/references/data-registry.md +43 -0
- package/.claude/skills/mutagent-evaluator/references/edd-loop.md +6 -6
- package/.claude/skills/mutagent-evaluator/references/eval-layers.md +52 -0
- package/.claude/skills/mutagent-evaluator/references/eval-stage.md +37 -4
- package/.claude/skills/mutagent-evaluator/references/memory-format.md +1 -1
- package/.claude/skills/mutagent-evaluator/schemas/methodology-review.schema.yaml +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/agent-dispatch.ts +0 -0
- 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/build-review-ui.ts +320 -6
- package/.claude/skills/mutagent-evaluator/scripts/cli/prep.ts +32 -2
- package/.claude/skills/mutagent-evaluator/scripts/cli/profile-subject.ts +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/code-eval-library.ts +201 -0
- package/.claude/skills/mutagent-evaluator/scripts/code-quality-verdict.ts +60 -68
- package/.claude/skills/mutagent-evaluator/scripts/config/schema.ts +3 -3
- package/.claude/skills/mutagent-evaluator/scripts/contracts/agentspec-evals.ts +101 -39
- package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-matrix.ts +326 -3
- package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-types.ts +30 -4
- package/.claude/skills/mutagent-evaluator/scripts/determine-outcome.ts +8 -0
- package/.claude/skills/mutagent-evaluator/scripts/edd/change-request.ts +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/edd/edd-types.ts +2 -2
- package/.claude/skills/mutagent-evaluator/scripts/judge-prompt-template.ts +144 -15
- 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/memory/append.ts +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/memory/ratify.ts +165 -0
- package/.claude/skills/mutagent-evaluator/scripts/merge-criteria.ts +849 -0
- package/.claude/skills/mutagent-evaluator/scripts/prep-tasks.ts +15 -4
- package/.claude/skills/mutagent-evaluator/scripts/read-manifest.ts +120 -0
- package/.claude/skills/mutagent-evaluator/scripts/read-unitf-traces.ts +34 -2
- package/.claude/skills/mutagent-evaluator/scripts/render-build-cards.ts +37 -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 +11 -92
- package/.claude/skills/mutagent-evaluator/scripts/render-review-report-v3.ts +691 -0
- package/.claude/skills/mutagent-evaluator/scripts/report-fragments.ts +173 -0
- package/.claude/skills/mutagent-evaluator/scripts/route-failures.ts +12 -9
- package/.claude/skills/mutagent-evaluator/scripts/run-evaluate.ts +46 -1
- package/.claude/skills/mutagent-evaluator/scripts/run-pipeline.ts +39 -4
- package/.claude/skills/mutagent-evaluator/scripts/run-review.ts +266 -0
- package/.claude/skills/mutagent-evaluator/scripts/subject-profile.ts +82 -0
- package/.claude/skills/mutagent-evaluator/scripts/sync-eval-criteria.ts +2 -2
- package/.claude/skills/mutagent-evaluator/scripts/unitf-to-evaltrace.ts +64 -21
- package/.claude/skills/mutagent-evaluator/scripts/verify-render.ts +728 -0
- package/bin/mutagent-cli.mjs +9 -5
- 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
|
-
* ⑤
|
|
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).
|
|
60
|
-
*
|
|
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
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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
|
|
79
|
-
* on any CRITICAL/HIGH criterion blocks the quality-pass; MEDIUM is advisory.
|
|
80
|
-
*
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
{
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
{
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
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;
|