@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
@@ -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
- const vpath = join(opts.verdictDir, verdictFileName(system, user));
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
- traces.push(projectUnitfToEvalTrace(rec));
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
  }