@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
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
import { existsSync, readFileSync } from "node:fs";
|
|
28
28
|
import { join } from "node:path";
|
|
29
29
|
import { extractOutcomeSignals, type JudgeInvoke } from "./determine-outcome.ts";
|
|
30
|
-
import { buildOutcomePrompt } from "./judge-prompt-template.ts";
|
|
30
|
+
import { buildOutcomePrompt, type SubjectFrame } from "./judge-prompt-template.ts";
|
|
31
31
|
import {
|
|
32
32
|
writeJudgeTask,
|
|
33
33
|
verdictFileName,
|
|
@@ -53,19 +53,27 @@ const PREP_PLACEHOLDER = JSON.stringify({
|
|
|
53
53
|
*/
|
|
54
54
|
export function prepDeterminerTasks(
|
|
55
55
|
traces: EvalTrace[],
|
|
56
|
-
opts: { dir: string; pin: PinnedEnvelope; vocab: SubjectVocab },
|
|
56
|
+
opts: { dir: string; pin: PinnedEnvelope; vocab: SubjectVocab; subject?: SubjectFrame },
|
|
57
57
|
): JudgeTaskSpec[] {
|
|
58
58
|
return traces.map((trace) => {
|
|
59
|
+
// EV-1 — the subject frame MUST match what the Stage-B pipeline renders (byte-
|
|
60
|
+
// identity for the cross-stage cache), so the caller derives it from the SAME
|
|
61
|
+
// full trace batch buildSubjectProfile sees in run-pipeline.
|
|
59
62
|
const { system, user } = buildOutcomePrompt(
|
|
60
63
|
trace,
|
|
61
64
|
extractOutcomeSignals(trace, opts.vocab),
|
|
62
65
|
opts.vocab,
|
|
66
|
+
opts.subject,
|
|
63
67
|
);
|
|
64
68
|
return writeJudgeTask(opts.dir, {
|
|
65
69
|
unit: { kind: "discover", traceId: trace.id },
|
|
66
70
|
system,
|
|
67
71
|
user,
|
|
68
72
|
pin: opts.pin,
|
|
73
|
+
// INF-3 — salt the determiner cache key with the trace id so two sessions with
|
|
74
|
+
// an identical (e.g. empty) determiner prompt never collapse onto one verdict.
|
|
75
|
+
// AGGREGATE reads the stored `spec.verdictFile`, so this needs no re-derive there.
|
|
76
|
+
keyId: trace.id,
|
|
69
77
|
});
|
|
70
78
|
});
|
|
71
79
|
}
|
|
@@ -84,8 +92,11 @@ export function createCapturingJudge(opts: {
|
|
|
84
92
|
pin: PinnedEnvelope;
|
|
85
93
|
}): { judge: JudgeInvoke; captured: JudgeTaskSpec[] } {
|
|
86
94
|
const captured: JudgeTaskSpec[] = [];
|
|
87
|
-
const judge: JudgeInvoke = (system, user) => {
|
|
88
|
-
|
|
95
|
+
const judge: JudgeInvoke = (system, user, keyId) => {
|
|
96
|
+
// INF-3 — the determiner passes its trace id (keyId) so the salted stage-A
|
|
97
|
+
// verdict file is found; criterion-judge prompts pass no keyId ⇒ unsalted,
|
|
98
|
+
// and are CAPTURED as build-evals tasks below (byte-identical key, C-PIN).
|
|
99
|
+
const vpath = join(opts.verdictDir, verdictFileName(system, user, keyId));
|
|
89
100
|
if (existsSync(vpath)) return Promise.resolve(readFileSync(vpath, "utf8"));
|
|
90
101
|
captured.push(
|
|
91
102
|
// D4 — the criterion judging axis (*build-evals / Step-2b). `kind` now matches the
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scripts/read-manifest.ts — EV-6: the sibling TraceManifest leg of the Discovery handoff.
|
|
3
|
+
* ---------------------------------------------------------------------------
|
|
4
|
+
* The Discovery handoff is a PATH PAIR — the UniTF `.jsonl` PLUS a sibling
|
|
5
|
+
* `TraceManifest` that reports the slice (count · format · truncation · coverage).
|
|
6
|
+
* Diagnostics reads BOTH and audits the slice BEFORE it judges; the evaluator used
|
|
7
|
+
* to read only the JSONL (a bare `--traces` path) and never opened the manifest —
|
|
8
|
+
* so a truncated or partially-malformed export was judged silently as if complete
|
|
9
|
+
* (EV-6). This wires the manifest leg into the evaluator intake, WARN-not-fail, the
|
|
10
|
+
* way diagnostics' `read-unitf.ts` `validateUniTFManifest` does.
|
|
11
|
+
*
|
|
12
|
+
* STANDALONE — the manifest shape is VENDORED (a minimal structural subset of
|
|
13
|
+
* `@mutagent/tools` `trace-manifest.ts`), NOT imported: the standalone-publish
|
|
14
|
+
* guard forbids the evaluator reaching into `@mutagent/tools`
|
|
15
|
+
* (MIGRATION-diagnostics-evaluator.md:417). Only the fields the audit reads are
|
|
16
|
+
* mirrored; diagnostics vendors its own identical copy.
|
|
17
|
+
*
|
|
18
|
+
* PURE except for the effectful `validateSiblingManifest` (one fs read of a sibling
|
|
19
|
+
* path). No clock / random / network.
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
22
|
+
|
|
23
|
+
/** The manifest `format` marker that pairs with the UniTF version. */
|
|
24
|
+
export const UNITF_MANIFEST_FORMAT = "unitf@0.1" as const;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The sibling TraceManifest (frozen `TraceManifest v0.1.0`), structural subset —
|
|
28
|
+
* only the fields the slice audit inspects. Every field is optional so a partial
|
|
29
|
+
* or older manifest degrades to fewer warnings rather than throwing.
|
|
30
|
+
*/
|
|
31
|
+
export interface UnitfTraceManifest {
|
|
32
|
+
manifest_version?: string;
|
|
33
|
+
count?: number;
|
|
34
|
+
truncated?: boolean;
|
|
35
|
+
truncationReason?: string;
|
|
36
|
+
format?: string;
|
|
37
|
+
coverage?: {
|
|
38
|
+
fetched?: number;
|
|
39
|
+
malformed?: number;
|
|
40
|
+
deduped?: number;
|
|
41
|
+
};
|
|
42
|
+
warnings?: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The sibling manifest path for a handed-over traces file: `<path>.manifest.json`,
|
|
47
|
+
* with a `.jsonl` suffix REPLACED (`traces.jsonl` → `traces.manifest.json`) —
|
|
48
|
+
* mirrors `@mutagent/tools` `manifestPathFor`. A `.gz` suffix is stripped first
|
|
49
|
+
* (the evaluator loads `.jsonl.gz` too) so `traces.jsonl.gz` also resolves to
|
|
50
|
+
* `traces.manifest.json`. PURE.
|
|
51
|
+
*/
|
|
52
|
+
export function manifestPathForTraces(tracesPath: string): string {
|
|
53
|
+
const base = tracesPath.endsWith(".gz") ? tracesPath.slice(0, -".gz".length) : tracesPath;
|
|
54
|
+
return base.endsWith(".jsonl")
|
|
55
|
+
? base.slice(0, -".jsonl".length) + ".manifest.json"
|
|
56
|
+
: base + ".manifest.json";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Validate a parsed manifest against the parsed trace count. Returns WARNINGS
|
|
61
|
+
* (never throws, never aborts) — the migration's "surface warnings but do NOT
|
|
62
|
+
* abort a partial export" rule. Empty array = clean / no manifest. Mirrors
|
|
63
|
+
* diagnostics `validateUniTFManifest`.
|
|
64
|
+
*/
|
|
65
|
+
export function validateUnitfManifest(
|
|
66
|
+
manifest: UnitfTraceManifest | undefined,
|
|
67
|
+
actualTraceCount: number,
|
|
68
|
+
): string[] {
|
|
69
|
+
const warnings: string[] = [];
|
|
70
|
+
if (!manifest) return warnings;
|
|
71
|
+
if (manifest.format !== undefined && manifest.format !== UNITF_MANIFEST_FORMAT) {
|
|
72
|
+
warnings.push(`manifest.format is "${manifest.format}" — expected "${UNITF_MANIFEST_FORMAT}"`);
|
|
73
|
+
}
|
|
74
|
+
if (manifest.count !== undefined && manifest.count !== actualTraceCount) {
|
|
75
|
+
warnings.push(
|
|
76
|
+
`manifest.count (${manifest.count}) != parsed trace count (${actualTraceCount}) — ` +
|
|
77
|
+
`the slice is incomplete (a truncated/partially-malformed export judged as if complete)`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (manifest.truncated === true) {
|
|
81
|
+
warnings.push(
|
|
82
|
+
`export is truncated${manifest.truncationReason ? ` (${manifest.truncationReason})` : ""} — ` +
|
|
83
|
+
`judging a PARTIAL slice`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
// Coverage cross-check: fetched should account for the parsed + malformed + deduped
|
|
87
|
+
// records; a fetched count that exceeds what landed on disk is a silent drop.
|
|
88
|
+
const cov = manifest.coverage;
|
|
89
|
+
if (cov?.fetched !== undefined) {
|
|
90
|
+
const accountedFor = actualTraceCount + (cov.malformed ?? 0) + (cov.deduped ?? 0);
|
|
91
|
+
if (cov.fetched !== accountedFor) {
|
|
92
|
+
warnings.push(
|
|
93
|
+
`manifest.coverage.fetched (${cov.fetched}) != parsed(${actualTraceCount}) + ` +
|
|
94
|
+
`malformed(${cov.malformed ?? 0}) + deduped(${cov.deduped ?? 0}) = ${accountedFor} — ` +
|
|
95
|
+
`records went missing between fetch and hand-off`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (manifest.warnings) warnings.push(...manifest.warnings);
|
|
100
|
+
return warnings;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Read + validate the sibling manifest for a handed-over traces file. Effectful
|
|
105
|
+
* (one fs read). NON-fatal throughout: an ABSENT manifest returns [] (the handoff
|
|
106
|
+
* may predate the manifest leg); an UNREADABLE / unparseable manifest yields a
|
|
107
|
+
* single warning and proceeds. Returns the human-readable warning list the caller
|
|
108
|
+
* surfaces on stderr.
|
|
109
|
+
*/
|
|
110
|
+
export function validateSiblingManifest(tracesPath: string, actualTraceCount: number): string[] {
|
|
111
|
+
const manifestPath = manifestPathForTraces(tracesPath);
|
|
112
|
+
if (!existsSync(manifestPath)) return []; // no manifest leg — nothing to audit
|
|
113
|
+
let manifest: UnitfTraceManifest | undefined;
|
|
114
|
+
try {
|
|
115
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as UnitfTraceManifest;
|
|
116
|
+
} catch {
|
|
117
|
+
return [`sibling manifest '${manifestPath}' failed to read/parse — proceeding without slice audit`];
|
|
118
|
+
}
|
|
119
|
+
return validateUnitfManifest(manifest, actualTraceCount);
|
|
120
|
+
}
|
|
@@ -23,6 +23,30 @@ export interface ParsedUnitfTraces {
|
|
|
23
23
|
traces: EvalTrace[];
|
|
24
24
|
/** lines that failed to parse OR lacked the minimal UniTF shape (surfaced). */
|
|
25
25
|
skipped: number;
|
|
26
|
+
/**
|
|
27
|
+
* INF-1 — traceIds of records that carry a prompt-bearing span (a user/system
|
|
28
|
+
* turn, or an assistant/agent GENERATION span) yet projected to an EMPTY
|
|
29
|
+
* `input.prompt`. This is the exact silent-empty symptom that read 21/21 blank
|
|
30
|
+
* on Claude Code traces: the reader must WARN, never emit empty in silence. The
|
|
31
|
+
* effectful reader/CLI surfaces this list; a non-empty list is a projection miss.
|
|
32
|
+
*/
|
|
33
|
+
emptyPromptTraceIds: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Does this record carry a span that SHOULD yield a prompt — an explicit user/
|
|
38
|
+
* system turn, or a GENERATION span (kind llm/agent, the Langfuse shape whose
|
|
39
|
+
* `input` holds the prompt)? Used to WARN when such a record still projects an
|
|
40
|
+
* empty `input.prompt` (the INF-1 silent-empty class). PURE.
|
|
41
|
+
*/
|
|
42
|
+
function hasPromptBearingSpan(rec: UnifiedTraceLike): boolean {
|
|
43
|
+
return rec.spans.some(
|
|
44
|
+
(s) =>
|
|
45
|
+
s.role === "user" ||
|
|
46
|
+
s.role === "system" ||
|
|
47
|
+
s.kind === "llm" ||
|
|
48
|
+
s.kind === "agent",
|
|
49
|
+
);
|
|
26
50
|
}
|
|
27
51
|
|
|
28
52
|
/**
|
|
@@ -45,6 +69,7 @@ function asUnitfRecord(value: unknown): UnifiedTraceLike | null {
|
|
|
45
69
|
*/
|
|
46
70
|
export function parseUnitfJsonl(text: string): ParsedUnitfTraces {
|
|
47
71
|
const traces: EvalTrace[] = [];
|
|
72
|
+
const emptyPromptTraceIds: string[] = [];
|
|
48
73
|
let skipped = 0;
|
|
49
74
|
for (const line of text.split("\n")) {
|
|
50
75
|
const trimmed = line.trim();
|
|
@@ -61,9 +86,16 @@ export function parseUnitfJsonl(text: string): ParsedUnitfTraces {
|
|
|
61
86
|
skipped += 1;
|
|
62
87
|
continue;
|
|
63
88
|
}
|
|
64
|
-
|
|
89
|
+
const trace = projectUnitfToEvalTrace(rec);
|
|
90
|
+
traces.push(trace);
|
|
91
|
+
// INF-1 — WARN, never silently emit empty: a record with a prompt-bearing span
|
|
92
|
+
// that still projected an empty prompt is a projection miss (the Claude Code
|
|
93
|
+
// 21/21-blank class). Recorded here; the CLI/reader surfaces it.
|
|
94
|
+
if (hasPromptBearingSpan(rec) && (trace.input?.prompt ?? "").length === 0) {
|
|
95
|
+
emptyPromptTraceIds.push(trace.id);
|
|
96
|
+
}
|
|
65
97
|
}
|
|
66
|
-
return { traces, skipped };
|
|
98
|
+
return { traces, skipped, emptyPromptTraceIds };
|
|
67
99
|
}
|
|
68
100
|
|
|
69
101
|
/**
|
|
@@ -83,6 +83,33 @@ export function renderBuildEvalsProgressCard(p: BuildEvalsProgress): string {
|
|
|
83
83
|
].join("\n");
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// ── EVAL-1 — "what this stage will produce" notice (up-front, at stage start) ─
|
|
87
|
+
export interface StageProducesNotice {
|
|
88
|
+
subject: string;
|
|
89
|
+
/** the command about to run (e.g. "*build-dataset" | "*build-evals" | "*eval"). */
|
|
90
|
+
command: string;
|
|
91
|
+
/** the named deliverables this stage emits (dataset · criteria · which files). */
|
|
92
|
+
produces: string[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* EVAL-1 (1) — render the up-front notice that names WHAT this stage will produce,
|
|
97
|
+
* so a build never runs silently with the operator unaware a dataset/criteria are
|
|
98
|
+
* coming. Emitted at Step 0, BEFORE the build. PURE. NOT an approval gate — a
|
|
99
|
+
* notification only (operator constraint).
|
|
100
|
+
*/
|
|
101
|
+
export function renderStageProducesNotice(n: StageProducesNotice): string {
|
|
102
|
+
const lines = [
|
|
103
|
+
TOP,
|
|
104
|
+
row(`▷ ${n.command} — ${n.subject}`),
|
|
105
|
+
MID,
|
|
106
|
+
row("this stage will produce:"),
|
|
107
|
+
];
|
|
108
|
+
for (const p of n.produces) lines.push(row(` • ${p}`));
|
|
109
|
+
lines.push(BOT);
|
|
110
|
+
return lines.join("\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
86
113
|
// ── F22 — dataset entity card (verbose, after *build-dataset) ─────────────────
|
|
87
114
|
export interface DatasetEntity {
|
|
88
115
|
subject: string;
|
|
@@ -99,6 +126,12 @@ export function renderDatasetEntityCard(e: DatasetEntity): string {
|
|
|
99
126
|
MID,
|
|
100
127
|
row(`categories (${e.categories.length}) · ${e.totalItems} items total`),
|
|
101
128
|
];
|
|
129
|
+
// EVAL-1 — a build that produced NOTHING must WARN, never render a blank card that
|
|
130
|
+
// reads as a silent success (the dogfood symptom: a build that reported and a build
|
|
131
|
+
// that stayed silent looked identical).
|
|
132
|
+
if (e.categories.length === 0 || e.totalItems === 0) {
|
|
133
|
+
lines.push(row("⚠ EMPTY — this build produced no dataset items (nothing to evaluate)."));
|
|
134
|
+
}
|
|
102
135
|
for (const c of e.categories) {
|
|
103
136
|
lines.push(row(` • ${c.id}: ${c.items} items (edge: ${c.edgeItems})`));
|
|
104
137
|
}
|
|
@@ -129,6 +162,10 @@ export function renderEvalsEntityCard(e: EvalsEntity): string {
|
|
|
129
162
|
row(`output sink: ${e.outputSink}`),
|
|
130
163
|
row(`criteria (${e.criteria.length}):`),
|
|
131
164
|
];
|
|
165
|
+
// EVAL-1 — an eval suite with zero criteria is a silent no-op build; WARN loudly.
|
|
166
|
+
if (e.criteria.length === 0) {
|
|
167
|
+
lines.push(row("⚠ EMPTY — this build produced no criteria (the suite judges nothing)."));
|
|
168
|
+
}
|
|
132
169
|
for (const c of e.criteria) {
|
|
133
170
|
lines.push(row(` • ${c.id} [${c.severity}·${c.kind}]`));
|
|
134
171
|
}
|