@mutagent/evaluator 0.1.0-alpha.3 → 0.1.0-alpha.5

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 (23) hide show
  1. package/.claude/skills/mutagent-evaluator/SKILL.md +19 -7
  2. package/.claude/skills/mutagent-evaluator/assets/agents/discovery.md +2 -2
  3. package/.claude/skills/mutagent-evaluator/assets/agents/evaluator.md +65 -1
  4. package/.claude/skills/mutagent-evaluator/references/operation-inventory.md +1 -0
  5. package/.claude/skills/mutagent-evaluator/references/workflows/orchestrator-protocol.md +20 -7
  6. package/.claude/skills/mutagent-evaluator/scripts/aggregate-discover.ts +1 -1
  7. package/.claude/skills/mutagent-evaluator/scripts/artifact-paths.ts +16 -0
  8. package/.claude/skills/mutagent-evaluator/scripts/cli/audit-run.ts +14 -7
  9. package/.claude/skills/mutagent-evaluator/scripts/cli/prep.ts +18 -2
  10. package/.claude/skills/mutagent-evaluator/scripts/code-quality-verdict.ts +285 -0
  11. package/.claude/skills/mutagent-evaluator/scripts/config/load.ts +45 -9
  12. package/.claude/skills/mutagent-evaluator/scripts/config/schema.ts +63 -8
  13. package/.claude/skills/mutagent-evaluator/scripts/load-bundle.ts +1 -1
  14. package/.claude/skills/mutagent-evaluator/scripts/persist-eval-criteria.ts +232 -0
  15. package/.claude/skills/mutagent-evaluator/scripts/render-discover-report.ts +1 -1
  16. package/.claude/skills/mutagent-evaluator/scripts/render-eval-report.ts +21 -6
  17. package/.claude/skills/mutagent-evaluator/scripts/route-failures.ts +12 -2
  18. package/.claude/skills/mutagent-evaluator/scripts/run-deterministic.ts +114 -18
  19. package/.claude/skills/mutagent-evaluator/scripts/run-evaluate.ts +6 -2
  20. package/.claude/skills/mutagent-evaluator/scripts/sync-eval-criteria.ts +244 -0
  21. package/.claude/skills/mutagent-evaluator/workflows/audit.workflow.js +1 -1
  22. package/bin/mutagent-cli.mjs +698 -7
  23. package/package.json +3 -3
@@ -32,6 +32,7 @@ import {
32
32
  type EvalMatrix,
33
33
  type RowResultValue,
34
34
  type ScorecardCriterion,
35
+ CheckMethod,
35
36
  RowResult,
36
37
  Track,
37
38
  trackForCheckMethod,
@@ -84,14 +85,96 @@ function isNonEmpty(value: unknown): boolean {
84
85
  return true;
85
86
  }
86
87
 
88
+ /** A STRUCTURED, non-empty value: a non-empty object or array. A present-but-
89
+ * scalar/empty artifact is NOT structured — the typebox-schema re-exec fails it
90
+ * (a hollow presence-pass under the old proxy). PURE. */
91
+ function isStructuredNonEmpty(value: unknown): boolean {
92
+ if (Array.isArray(value)) return value.length > 0;
93
+ if (value !== null && typeof value === "object") return Object.keys(value).length > 0;
94
+ return false;
95
+ }
96
+
97
+ /** Scan a produced artifact for an EXPLICIT gate-failure marker: a falsy pass
98
+ * flag (`pass`/`ok`/`passed` === false) or a failing `status` stamp
99
+ * (`fail`/`failed`/`error`). Conservative + shallow so it stays deterministic
100
+ * and does not false-fail on unrelated substrings. PURE. */
101
+ function hasFailureMarker(value: unknown): boolean {
102
+ if (value === null || typeof value !== "object") return false;
103
+ const obj = value as Record<string, unknown>;
104
+ for (const flag of ["pass", "ok", "passed"] as const) {
105
+ if (obj[flag] === false) return true;
106
+ }
107
+ const status = obj["status"];
108
+ if (typeof status === "string") {
109
+ const s = status.toLowerCase();
110
+ if (s === "fail" || s === "failed" || s === "error") return true;
111
+ }
112
+ return false;
113
+ }
114
+
115
+ /**
116
+ * Item #4 — the REAL live re-execution of a deterministic row. Replaces the
117
+ * presence-proxy (`the artifact exists → mark it covered`) with an ACTUAL
118
+ * checkMethod-keyed predicate over the artifact's LIVE content:
119
+ * - typebox-schema : the produced artifact must be a STRUCTURED, non-empty
120
+ * object/array — a present-but-malformed scalar now FAILS
121
+ * instead of hollow-passing (real shape conformance).
122
+ * - gate : the evidence must be present AND carry NO explicit
123
+ * failure marker (fail-loud gate — `hasFailureMarker`).
124
+ * - deterministic-script: the produced artifact must be present + non-empty
125
+ * (the script actually emitted its output).
126
+ * Returns pass/fail when the criterion maps to a live bundle artifact; returns
127
+ * `null` when NO artifact answers it offline — the caller then records the
128
+ * documented seam-skip. A genuinely un-runnable row would require executing the
129
+ * SUBJECT'S OWN script, which the judge-only cell never does (EV-051) and which
130
+ * dispatches NO sub-agents (PR-ORCH-01). PURE + deterministic (no clock/random/
131
+ * network) ⇒ C-PIN byte-identical reruns.
132
+ */
133
+ export function reexecDeterministicCheck(
134
+ criterion: Criterion,
135
+ bundle: RunBundle,
136
+ ): RowResultValue | null {
137
+ const artifactKey = evidenceArtifactFor(criterion, bundle);
138
+ if (artifactKey == null) return null; // no live artifact → not re-executable here
139
+ const value = bundle.data[artifactKey];
140
+ // HONESTY on the depth of each re-exec (do NOT mistake these for deep checks):
141
+ // - typebox-schema : a real SHAPE upgrade over presence (rejects a present-but-
142
+ // scalar), but it is NOT a full TypeBox `Value.Check` against
143
+ // the produced artifact's declared schema — it asserts
144
+ // "structured + non-empty", not field-level conformance.
145
+ // - gate : `hasFailureMarker` is TOP-LEVEL-ONLY (scans the object's own
146
+ // pass/ok/passed/status keys) — it does NOT recurse into nested
147
+ // gate evidence, so a failure buried in a child object is missed.
148
+ // - deterministic-script : semantically ≡ the OLD presence proxy (present +
149
+ // non-empty). It is re-run against LIVE data (so it is a real
150
+ // re-execution, not a cached verdict), but carries no deeper
151
+ // predicate than presence. Deepening any of these to a true
152
+ // re-run of the subject's own script is the documented seam
153
+ // (out of scope for a judge-only / no-sub-agent cell).
154
+ switch (criterion.checkMethod) {
155
+ case CheckMethod.TypeboxSchema:
156
+ return isStructuredNonEmpty(value) ? RowResult.Pass : RowResult.Fail;
157
+ case CheckMethod.Gate:
158
+ return isNonEmpty(value) && !hasFailureMarker(value) ? RowResult.Pass : RowResult.Fail;
159
+ case CheckMethod.DeterministicScript:
160
+ default:
161
+ return isNonEmpty(value) ? RowResult.Pass : RowResult.Fail;
162
+ }
163
+ }
164
+
87
165
  export interface EvaluateOptions {
88
166
  /**
89
- * When false (default), criteria that need a LIVE re-exec of the subject's own
90
- * script (beyond bundle presence/shape) return `skip` with a documented seam
91
- * note instead of false-passing. A future integration may set this true and
92
- * provide a re-exec callback.
167
+ * Item #4 the LIVE RE-EXEC executor. When supplied it RUNS the real
168
+ * deterministic check against live bundle data (see `reexecDeterministicCheck`,
169
+ * the default) and its pass/fail return is authoritative replacing the
170
+ * presence-proxy that marked a row covered from mere artifact existence. A
171
+ * `null` return means "this executor cannot answer this row" ⇒ the legacy
172
+ * (byte-stable) path runs. ABSENT ⇒ no re-exec wired: fully back-compatible,
173
+ * byte-identical to before (the presence proxy + documented seam-skip). The
174
+ * executor is PURE + dispatches NO sub-agents (PR-ORCH-01) + never fixes the
175
+ * subject (EV-051).
93
176
  */
94
- liveReexec?: (criterion: Criterion) => RowResultValue | null;
177
+ liveReexec?: (criterion: Criterion, bundle: RunBundle) => RowResultValue | null;
95
178
  }
96
179
 
97
180
  /**
@@ -104,21 +187,31 @@ export function evaluateDeterministic(
104
187
  opts: EvaluateOptions = {},
105
188
  ): ScorecardCriterion {
106
189
  const track = Track.Deterministic;
190
+
191
+ // Item #4 — LIVE RE-EXEC FIRST. When an executor is wired it RUNS the real
192
+ // deterministic check against live data; a pass/fail is authoritative and
193
+ // REPLACES the presence-proxy (no more "an artifact exists → covered"). A null
194
+ // return means the executor could not answer this row ⇒ fall through to the
195
+ // (byte-stable) legacy path below.
196
+ const live = opts.liveReexec?.(criterion, bundle) ?? null;
197
+ if (live === RowResult.Pass || live === RowResult.Fail || live === RowResult.Incomplete) {
198
+ return {
199
+ dimension: criterion.dimension,
200
+ severity: criterion.severity,
201
+ checkMethod: criterion.checkMethod,
202
+ track,
203
+ result: live,
204
+ detail: `live re-exec (${criterion.checkMethod}): real deterministic check over the produced artifact → ${live}`,
205
+ };
206
+ }
207
+
107
208
  const artifactKey = evidenceArtifactFor(criterion, bundle);
108
209
 
109
210
  if (artifactKey == null) {
110
- // No bundle artifact answers this row offline -> live-reexec seam.
111
- const live = opts.liveReexec?.(criterion) ?? null;
112
- if (live) {
113
- return {
114
- dimension: criterion.dimension,
115
- severity: criterion.severity,
116
- checkMethod: criterion.checkMethod,
117
- track,
118
- result: live,
119
- detail: "evaluated via live-reexec callback",
120
- };
121
- }
211
+ // No bundle artifact answers this row offline -> documented seam-skip. NOT
212
+ // silent: the coverage-honesty warning (computeCoverage) surfaces it, and
213
+ // fully executing it would require running the subject's OWN script — which
214
+ // the judge-only cell never does (EV-051) and which dispatches no sub-agents.
122
215
  return {
123
216
  dimension: criterion.dimension,
124
217
  severity: criterion.severity,
@@ -130,6 +223,9 @@ export function evaluateDeterministic(
130
223
  };
131
224
  }
132
225
 
226
+ // LEGACY presence proxy — reached ONLY when no live-reexec executor is wired
227
+ // (kept for byte-identity with pre-Item-#4 callers). With the default executor
228
+ // wired (audit-run.ts) this branch is never taken for a mapped row.
133
229
  const present = isNonEmpty(bundle.data[artifactKey]);
134
230
  return {
135
231
  dimension: criterion.dimension,
@@ -138,7 +234,7 @@ export function evaluateDeterministic(
138
234
  track,
139
235
  result: present ? RowResult.Pass : RowResult.Fail,
140
236
  detail: present
141
- ? `evidence artifact '${artifactKey}' present + non-empty`
237
+ ? `evidence artifact '${artifactKey}' present + non-empty (presence proxy — no live re-exec wired)`
142
238
  : `evidence artifact '${artifactKey}' MISSING/empty in bundle (fail-loud)`,
143
239
  };
144
240
  }
@@ -542,7 +542,7 @@ export function readRunVerdictFiles(input: EvaluateRunInput): MatrixVerdictFile[
542
542
  * REPORT — build the rich `EvalReportInput` from the REAL run outputs (the folded
543
543
  * scorecard + per-criterion verdicts + the per-trajectory Judge-Agent verdict
544
544
  * files + the matrix criteria + the EV-051 handover) and render the 5-tab HTML
545
- * eval-report into `<reportDir>/report.html`. This is the wiring D-2/D-3 had been
545
+ * eval-report into `<reportDir>/evaluation-report.html`. This is the wiring D-2/D-3 had been
546
546
  * MISSING: `*evaluate` produced a scorecard but emitted no report. The report
547
547
  * consumes the §9.4 judge-walk fields when present (side-by-side) and degrades to
548
548
  * the per-trajectory scorecard otherwise. Returns the written report path.
@@ -555,6 +555,9 @@ export function writeRunReport(
555
555
  result: EvaluateRunResult,
556
556
  runId: string,
557
557
  cwd?: string,
558
+ // PRD R-4 — render audience (resolved by the caller: explicit flag → config.default_audience →
559
+ // "client"). Omitted ⇒ the renderer defaults to "client" (leak-safe). "internal" shows §5.
560
+ audience?: "client" | "internal",
558
561
  ): string {
559
562
  const matrixVerdictFiles = readRunVerdictFiles(input);
560
563
  const reportInput = buildEvalReportInput({
@@ -586,9 +589,10 @@ export function writeRunReport(
586
589
  cPin: true,
587
590
  },
588
591
  });
592
+ if (audience) reportInput.audience = audience; // else renderEvalReport defaults to "client"
589
593
  const dir = reportDir(runId, cwd);
590
594
  mkdirSync(dir, { recursive: true });
591
- const outPath = join(dir, "report.html");
595
+ const outPath = join(dir, "evaluation-report.html");
592
596
  writeFileSync(outPath, renderEvalReport(reportInput));
593
597
  return outPath;
594
598
  }
@@ -0,0 +1,244 @@
1
+ /**
2
+ * scripts/sync-eval-criteria.ts — the EVAL-LEG reconcile CONTRACT (Wave-2 W2I5 · KP-003).
3
+ * ---------------------------------------------------------------------------
4
+ * The THIRD leg of the def → impl → eval triad. Today `#sync-spec` reconciles two
5
+ * legs — spec ↔ impl. When an implementation amends (the ⑤ IMPROVE loop's ai-engineer,
6
+ * or a brownfield drift), the eval criteria that GROUND the subject's evaluation can go
7
+ * stale w.r.t. the changed impl. This module is the EVALUATOR-SIDE CONTRACT that the
8
+ * reconcile (ai-architect REASONING, session-dispatched — Model-B) READS and WRITES:
9
+ *
10
+ * 1. WHICH eval leg applies — the subject-kind resolver. The eval leg's shape differs
11
+ * by subject kind (this is why W2I5 was gated behind W2I1's code-quality leg):
12
+ * - agent / skill / composite subject → the EVAL-SUITE criteria (the evaluator's
13
+ * discovered / maintained criteria for that subject).
14
+ * - code subject → the CODE-QUALITY criteria (W2I1's `DEFAULT_CODE_QUALITY_CRITERIA`
15
+ * / `#mode-judge-code-quality`).
16
+ * 2. A deterministic STALENESS FLAG predicate — the eval-leg half of the drift trigger
17
+ * (mirrors the builder freshness probe's spec/code compare; same 3-value semantics).
18
+ * 3. The monotonic APPLY primitive — the ai-architect reasons a criteria delta; this
19
+ * applies it as criteria MAINTENANCE: upsert-by-id, append the novel, and NEVER drop
20
+ * an existing criterion (EV-053, guarded by `assertMonotonicGrowth`).
21
+ *
22
+ * JUDGE-ONLY PRESERVED (EV-051): this file NEVER scores a subject and NEVER decides a
23
+ * subject pass/fail. It FLAGS freshness (pure epoch compare) and GROWS a criteria set
24
+ * (pure set algebra). The reconcile REASONING — which criteria a changed impl makes stale,
25
+ * what new criterion it needs — is the ai-architect's, not this code's (Model-B: code =
26
+ * drift predicates + deterministic maintenance only, never agent dispatch).
27
+ *
28
+ * PURE + deterministic (C-PIN): no clock / random / network; same inputs → byte-identical.
29
+ */
30
+ import { type Static, Type } from "@sinclair/typebox";
31
+ import { Value } from "@sinclair/typebox/value";
32
+
33
+ import { assertMonotonicGrowth } from "./living-suite.ts";
34
+
35
+ // ── subject kind → eval leg ──────────────────────────────────────────────────
36
+
37
+ /** The subject kinds the triad reconciles. `agent|skill|composite` come from the
38
+ * agentspec `definition.identity.kind`; `code` is the ⑤ IMPROVE code-target subject. */
39
+ export const EvalSubjectKind = {
40
+ Agent: "agent",
41
+ Skill: "skill",
42
+ Composite: "composite",
43
+ Code: "code",
44
+ } as const;
45
+ export type EvalSubjectKindValue = (typeof EvalSubjectKind)[keyof typeof EvalSubjectKind];
46
+
47
+ /** Which eval leg grounds a subject's evaluation — the W2I5 core rule. */
48
+ export const EvalLegKind = {
49
+ /** agent / skill / composite → the evaluator's discovered/maintained eval-suite criteria. */
50
+ EvalSuite: "eval-suite",
51
+ /** code → W2I1's code-quality criteria (`DEFAULT_CODE_QUALITY_CRITERIA`). */
52
+ CodeQuality: "code-quality",
53
+ } as const;
54
+ export type EvalLegKindValue = (typeof EvalLegKind)[keyof typeof EvalLegKind];
55
+
56
+ /**
57
+ * Resolve WHICH eval leg a subject kind reconciles against. Code subjects use the
58
+ * code-quality criteria (a deterministic-test-gate PLUS a quality judge); every other
59
+ * kind uses the discovered eval-suite criteria. This single mapping is the shape
60
+ * differentiator the whole triad turns on.
61
+ */
62
+ export function evalLegForSubjectKind(kind: EvalSubjectKindValue): EvalLegKindValue {
63
+ return kind === EvalSubjectKind.Code ? EvalLegKind.CodeQuality : EvalLegKind.EvalSuite;
64
+ }
65
+
66
+ // ── the staleness flag predicate (eval-leg half of the drift trigger) ────────
67
+
68
+ /** The eval-leg freshness verdict — mirrors the builder freshness probe's 3-value spec/code semantics. */
69
+ export const EvalLegDrift = {
70
+ /** no eval-criteria artifact yet — the eval leg must be CONSTRUCTED (cold). */
71
+ MissingEval: "missing-eval",
72
+ /** the eval criteria are as fresh as the impl — no reconcile write needed. */
73
+ InSync: "in-sync",
74
+ /** the impl is newer than the eval criteria — the eval leg is flagged for RECONCILE. */
75
+ NeedsSync: "needs-sync",
76
+ } as const;
77
+ export type EvalLegDriftValue = (typeof EvalLegDrift)[keyof typeof EvalLegDrift];
78
+
79
+ /** Effective freshness epochs (seconds) the flag predicate compares. `null` = unknown/absent. */
80
+ export interface EvalLegFreshnessInput {
81
+ /** effective freshness epoch of the eval-criteria artifact; `null` = artifact absent. */
82
+ evalCriteriaEpoch: number | null;
83
+ /** effective freshness epoch of the implementation; `null` = unknown. */
84
+ implEpoch: number | null;
85
+ }
86
+
87
+ /**
88
+ * FLAG whether the eval leg is stale w.r.t. the impl. Deterministic — a pure epoch
89
+ * compare, identical in spirit to the builder `check-sync-spec` spec/code compare so the
90
+ * two legs agree on "impl is newer → reconcile". This is a FLAG only: it decides nothing
91
+ * about the subject and proposes no criteria — the reconcile reasoning is the agent's.
92
+ */
93
+ export function flagEvalLegDrift(input: EvalLegFreshnessInput): EvalLegDriftValue {
94
+ if (input.evalCriteriaEpoch == null) return EvalLegDrift.MissingEval;
95
+ if (input.implEpoch != null && input.implEpoch > input.evalCriteriaEpoch) {
96
+ return EvalLegDrift.NeedsSync;
97
+ }
98
+ return EvalLegDrift.InSync;
99
+ }
100
+
101
+ /** The resolved eval leg for a subject: which criteria kind + the current staleness flag. */
102
+ export interface EvalLegAssessment {
103
+ leg: EvalLegKindValue;
104
+ drift: EvalLegDriftValue;
105
+ }
106
+
107
+ /**
108
+ * Convenience: resolve the eval leg for a subject kind AND flag its staleness in one
109
+ * call — the exact pair the reconcile reads ("which leg, is it stale?").
110
+ */
111
+ export function assessEvalLeg(
112
+ subjectKind: EvalSubjectKindValue,
113
+ freshness: EvalLegFreshnessInput,
114
+ ): EvalLegAssessment {
115
+ return { leg: evalLegForSubjectKind(subjectKind), drift: flagEvalLegDrift(freshness) };
116
+ }
117
+
118
+ // ── the criteria representation + the monotonic reconcile-apply ──────────────
119
+
120
+ /**
121
+ * One eval criterion — subject-kind-agnostic (a stable `id` key + the binary `statement`,
122
+ * plus the `leg` it belongs to and an optional severity). Serves BOTH legs: a code-quality
123
+ * `Qn` criterion (`severity` ∈ critical|high|medium) and an eval-suite discovered criterion
124
+ * flow through the same shape. `additionalProperties: true` keeps a richer criterion (e.g.
125
+ * a code-quality `passDefinition`/`failDefinition`, an eval-suite `checkMethod`) forward-parsing.
126
+ */
127
+ export const EvalCriterionSchema = Type.Object(
128
+ {
129
+ id: Type.String({ minLength: 1 }),
130
+ statement: Type.String({ minLength: 1 }),
131
+ leg: Type.Union([Type.Literal(EvalLegKind.EvalSuite), Type.Literal(EvalLegKind.CodeQuality)]),
132
+ severity: Type.Optional(Type.String({ minLength: 1 })),
133
+ },
134
+ { additionalProperties: true },
135
+ );
136
+ export type EvalCriterion = Static<typeof EvalCriterionSchema>;
137
+
138
+ /**
139
+ * The reconcile REQUEST the ai-architect emits: the persisted criteria `existing`, plus the
140
+ * agent-reasoned `proposed` additions/updates. `proposed` is an UPSERT set (append the novel,
141
+ * revise a same-id criterion's wording for the changed impl) — it is NEVER a delete list; the
142
+ * monotonic-growth invariant (EV-053) forbids dropping a criterion.
143
+ */
144
+ export const EvalCriteriaReconcileRequestSchema = Type.Object(
145
+ {
146
+ subjectId: Type.String({ minLength: 1 }),
147
+ subjectKind: Type.Union([
148
+ Type.Literal(EvalSubjectKind.Agent),
149
+ Type.Literal(EvalSubjectKind.Skill),
150
+ Type.Literal(EvalSubjectKind.Composite),
151
+ Type.Literal(EvalSubjectKind.Code),
152
+ ]),
153
+ leg: Type.Union([Type.Literal(EvalLegKind.EvalSuite), Type.Literal(EvalLegKind.CodeQuality)]),
154
+ existing: Type.Array(EvalCriterionSchema),
155
+ proposed: Type.Array(EvalCriterionSchema),
156
+ },
157
+ { additionalProperties: false },
158
+ );
159
+ export type EvalCriteriaReconcileRequest = Static<typeof EvalCriteriaReconcileRequestSchema>;
160
+
161
+ /** Provenance for a reconcile — pure counters (NO clock, for byte-identity / C-PIN). */
162
+ export interface EvalCriteriaReconcileProvenance {
163
+ /** how many genuinely-novel criteria were appended. */
164
+ added: number;
165
+ /** how many existing criteria (same id) had their content revised. */
166
+ updated: number;
167
+ /** total criteria after the reconcile. */
168
+ total: number;
169
+ }
170
+
171
+ /** The reconcile RESULT — the grown/maintained criteria set + provenance. */
172
+ export interface EvalCriteriaReconcileResult {
173
+ subjectId: string;
174
+ leg: EvalLegKindValue;
175
+ criteria: EvalCriterion[];
176
+ provenance: EvalCriteriaReconcileProvenance;
177
+ }
178
+
179
+ /** Parse + narrow a reconcile request (guarded). THROWS on schema violation. PURE. */
180
+ export function parseEvalCriteriaReconcileRequest(value: unknown): EvalCriteriaReconcileRequest {
181
+ if (!Value.Check(EvalCriteriaReconcileRequestSchema, value)) {
182
+ const first = [...Value.Errors(EvalCriteriaReconcileRequestSchema, value)][0];
183
+ throw new Error(
184
+ `parseEvalCriteriaReconcileRequest: schema violation at '${first?.path ?? "(root)"}': ` +
185
+ `${first?.message ?? "invalid eval-criteria reconcile request"}`,
186
+ );
187
+ }
188
+ return value;
189
+ }
190
+
191
+ const criterionId = (c: EvalCriterion): string => c.id;
192
+
193
+ /**
194
+ * APPLY the reconcile: maintain the eval-criteria set for a changed impl. Criteria
195
+ * MAINTENANCE, not judging (EV-051): upsert each `proposed` criterion by `id` (revise a
196
+ * same-id criterion's content in place, keeping the id), append the genuinely-novel, and
197
+ * assert the id-set NEVER shrank (EV-053, via the shared `assertMonotonicGrowth`). The
198
+ * `leg` on every proposed criterion must match the request's `leg` — mixing a code-quality
199
+ * criterion into an eval-suite reconcile (or vice versa) is a hard error, not a silent
200
+ * accept. DETERMINISTIC + PURE: existing order preserved, novel appended in proposal order;
201
+ * no clock / random. Same (existing, proposed) → byte-identical result.
202
+ */
203
+ export function reconcileEvalCriteria(
204
+ request: EvalCriteriaReconcileRequest,
205
+ ): EvalCriteriaReconcileResult {
206
+ const req = parseEvalCriteriaReconcileRequest(request);
207
+
208
+ for (const p of req.proposed) {
209
+ if (p.leg !== req.leg) {
210
+ throw new Error(
211
+ `reconcileEvalCriteria: proposed criterion '${p.id}' has leg '${p.leg}' but the ` +
212
+ `reconcile targets leg '${req.leg}'. An eval-suite and a code-quality criterion ` +
213
+ "are different legs — refusing to mix them (subject-kind integrity).",
214
+ );
215
+ }
216
+ }
217
+
218
+ // Insertion-ordered upsert: existing order first (content possibly revised), novel appended.
219
+ const byId = new Map<string, EvalCriterion>();
220
+ for (const c of req.existing) byId.set(c.id, c);
221
+
222
+ let added = 0;
223
+ let updated = 0;
224
+ for (const p of req.proposed) {
225
+ if (byId.has(p.id)) {
226
+ byId.set(p.id, p); // revise content, id (and thus the key/order) retained
227
+ updated += 1;
228
+ } else {
229
+ byId.set(p.id, p); // novel → appended after the existing keys
230
+ added += 1;
231
+ }
232
+ }
233
+
234
+ const criteria = [...byId.values()];
235
+ // EV-053: a maintained criteria set may GROW or revise, but never DROP a criterion.
236
+ assertMonotonicGrowth(req.existing, criteria, criterionId);
237
+
238
+ return {
239
+ subjectId: req.subjectId,
240
+ leg: req.leg,
241
+ criteria,
242
+ provenance: { added, updated, total: criteria.length },
243
+ };
244
+ }
@@ -47,7 +47,7 @@ phase('Deterministic spine')
47
47
  const spine = await agent(
48
48
  `Run the DETERMINISTIC spine for the master audit. Use Bash:\n` +
49
49
  ` bash ${PKG}/scripts/cli/run.sh ${PKG}/scripts/cli/audit-run.ts ${RUN_ID} --subject ${SUBJECT} --bundle-dir ${BUNDLE_DIR} --subjects-root ${PKG}/subjects --generated-at ${GEN}\n` +
50
- `This writes <bundle>/audit/scorecard.json (two-track GATE+TREND) + <bundle>/audit/master-audit.html (the base report). Return the parsed JSON the script prints (subject, runId, criteria, deterministicRows, judgeRows, gateRunPass, scorecard, report).`,
50
+ `This writes the master-audit under the unified evaluator reports root: .mutagent/evaluator/reports/<runId>/master-audit.html + master-audit.scorecard.json (two-track GATE+TREND). Return the parsed JSON the script prints (subject, runId, criteria, deterministicRows, judgeRows, gateRunPass, scorecard, report).`,
51
51
  { label: 'spine:audit-run', phase: 'Deterministic spine' },
52
52
  )
53
53
  log(`spine: ${spine && spine.criteria} criteria, ${spine && spine.deterministicRows} deterministic + ${spine && spine.judgeRows} judge rows; gateRunPass=${spine && spine.gateRunPass}`)