@mutagent/evaluator 0.1.0-alpha.4 → 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.
@@ -23,7 +23,7 @@ license: Proprietary. LICENSE.txt has complete terms.
23
23
  compatibility: Designed for Claude Code, Codex, Cursor, OpenCode and similar coding-agent runtimes; works with git, gh CLI, jq, curl, and Bun/pnpm/npm runtimes.
24
24
  metadata:
25
25
  author: mutagent
26
- version: "0.1.0-alpha.2"
26
+ version: "0.1.0-alpha.4"
27
27
  # allowed-tools: OMITTED — agent uses all native tools per host runtime
28
28
  ---
29
29
 
@@ -276,6 +276,7 @@ applies, EV-053/EQ5):
276
276
  | `scripts/aggregate-discover.ts` | P2 (§5a) + §9.4 (T6) | code | The real `*discover-evals` AGGREGATE half: parse the leaf's per-trace verdict files + per-batch mining report → `TraceAnnotation[]` (FAIL-LOUD via `missingVerdictKeys`; THROW on an undispatched-trace category ref) → `deriveMinedCriteria` (§5b metadata + §5c DR-2) → `growLivingSuite` (append-only, monotonic, saturation-stop). **T6:** `collectDatasetCandidates` (failure/uncertain → `DatasetCase[]` via the derive-dataset selectors) + `unmatchedDetectionCandidates` (the `*evaluate` judge's node-2.5 handoff) → consumable by `*build-dataset`. `DiscoverMiningReport` contract. No hand-rolled annotations. |
277
277
  | `scripts/code-eval.ts` | P3 (EX-2) | code | The v2 CODE-track primitive library — generic PURE `(trace) → {result, detail}` checks: presence · string-equality · format-validity · schema-conformance · ref-integrity (cross-stage). SV-1 dotted-path read incl. `obs:<name>.<path>`. Exhaustive `runCodeEval` (unknown primitive → THROW). NO LLM, byte-identical. Disjoint from the v1 `run-deterministic.ts` audit world. |
278
278
  | `scripts/check-method-router.ts` | P3 (EX-2) | hybrid | THE load-bearing §5b `check_method` router: `deterministic`→CODE-EXEC (judge NEVER called) · `llm-judge`→JUDGE seam · `hybrid`→code pre-filter that GATES the judge. Unknown → THROW (exhaustive). `RoutedCriterionVerdict` = `CriterionVerdict` + provenance `{producedBy, codeResult, judgeReached}` (structural superset → `evaluate.ts` rollup unchanged). `extractCodeEval(registry)` fail-loud (a code-class criterion with no spec THROWS — never silently judge). "Code before judge" lives INSIDE the agent. |
279
+ | `scripts/code-quality-verdict.ts` | W2I1 (⑤ IMPROVE) | hybrid | The **code-quality JUDGE mode** (`#mode-judge-code-quality`) — the (b) half of the ⑤ IMPROVE code-target BOTH-gate. Scores a `code` subject's QUALITY (spec-faithfulness · addresses-diagnosis · no-regression · error-handling · maintainability) BEYOND tests-green → severity-gated binary PASS/FAIL. `buildCodeQualityPrompt` (pure renderer, critique-before-verdict) + `codeQualityGatePass` (pure aggregate) + `runCodeQualityJudge` (C-PIN run wrapper on the injected JudgeInvoke seam). Judge-only (EV-051), host-runtime, pinned model + temp 0. **DISTINCT from `code-eval.ts`** (deterministic primitives — no LLM). `DEFAULT_CODE_QUALITY_CRITERIA` + its aggregation rule are FLAGGED-for-operator-sign-off. |
279
280
  | `scripts/run-evaluate.ts` | P5 + §9.4 (T1·T3·T4·T5) | hybrid | The real-engine `*evaluate` end-to-end COMPOSER: `tier0Plan` (T1 — code rows decided in the pre-pass, residual judge rows only in packets) → `prepMatrixPackets` (residual-only) → `adaptivePacketPlan` (T5 overload guard, default 1:1) → [dispatch #mode-judge-trajectory] → `aggregateEvaluate` (FAIL-LOUD readiness gate over DISPATCHED ids → `aggregateMatrixScorecard` folding code+judge + T3 `independentVerify` downgrade-only on gating fails → GATE + variance → T4 `consolidateByLocus` lociClusters → EV-051 `routeFailures`→validated HandoverBundle). `maskedScorecard` = the C-PIN byte-identity artifact. |
280
281
  | `scripts/render-eval-report.ts` | P4 (D-1/2/3) | code | The v2 reporting (v1 `render-report.ts` untouched): `renderEvalCards` (D-1 terminal per-criterion cards) + `buildEvalReportInput` (derives the rich tabs from real run data — ledger · coverage · cohorts · gating rolls · top-findings · judge-health) + `renderEvalReport` (the operator-APPROVED 5 tabs: ① Overview KPIs+coverage+gating-table+top-findings · ② Trajectory‖Judge ledger + click-row Target-Agent‖Judge side-by-side · ③ Eval Scorecard cohort heatmap + nested subcards + inline calibration · ④ Findings verbatim-evidence + judge chain + agree/revise/refute · ⑤ Self-Eval [INTERNAL, strip-marked]). Consumes the §9.4 judge-walk (`judge_steps[]` + dense map) when present, degrades to the per-trajectory scorecard otherwise. Tabs are `<button>` (no `<s>` strikethrough). Unified design-system tokens (`assets/brand/theme.css`). byte-identical after the `generatedAt` mask. `autoOpenCommand` cross-platform helper (orchestrator fires it). |
281
282
  | `scripts/contracts/eval-types.ts` *(extended)* | P2/P2b | code | + `MinedCriterion` (additive subtype of `DiscoveredCriterion`) carrying the §5b `MetricMetadata` (generality·dimension·level·check_method [3-value SF-2 router]·substrate·applies_to·severity·judge_inputs·flag) + the §5c `DiscoveryRationale` (DR-2: targets·why·evidence{grounding observed\|inferred·prevalence k/n·refs}·assumptions·expected_impact). `parseMinedCriterion` enforces the evidence-first gate (OBSERVED ⇒ refs + k>0). |
@@ -508,6 +508,7 @@ resolution_contract: |
508
508
  mode: judge, axis: criterion => run #mode-judge-criterion (preserves eval-judge); input is a .task.json.
509
509
  mode: verify => run #mode-verify (GA-5 result-verifier; reviewer≠judge, downgrade-only); input is a decided verdict + its situation.
510
510
  mode: improve => run #mode-improve (EDD ③ loop — F19 variance-first then F18 request-amend-reeval, bounded). JUDGE-ONLY: REQUEST, never patch.
511
+ mode: judge, axis: code-quality => run #mode-judge-code-quality (Wave-2 W2I1 — score a CODE subject's QUALITY for the ⑤ IMPROVE code-target BOTH-gate). JUDGE-ONLY; binary quality PASS/FAIL. Rubric mirrored in scripts/code-quality-verdict.ts.
511
512
  4. DISPATCH-INPUT DISAMBIGUATION — the parent hands you files:
512
513
  a *.packet.json (MatrixPacket) => judge/trajectory, ALWAYS (matrix-judge path; no unit field).
513
514
  a *.task.json (JudgeTaskSpec) => unit.kind: "discover" => discover; else unit.axis: "criterion" => judge/criterion.
@@ -1029,6 +1030,69 @@ never re-implements, the runner. The interface this mode ASSUMES (documented ver
1029
1030
  C-PIN (pinned model + temperature 0).
1030
1031
  - **Additive to the runner:** consumes the assumed eval-runner interface; rebuilds nothing.
1031
1032
 
1033
+ ## Mode: judge — axis code-quality ← the ⑤ IMPROVE code-target quality verdict (Wave-2 W2I1)
1034
+
1035
+ The **(b) half of the ⑤ IMPROVE loop's code-target BOTH-gate.** For a `code`-kind subject, "converged"
1036
+ requires BOTH (a) the subject's OWN test suite green — a hard DETERMINISTIC gate the ai-engineer
1037
+ reports and the optimize oracle records as `testsGreen` — AND (b) THIS code-quality verdict passing.
1038
+ This mode is (b): score the QUALITY of the applied code change, BEYOND tests-green (a deterministic
1039
+ primitive cannot decide spec-faithfulness, scope, error-handling, maintainability — a judge must).
1040
+
1041
+ ### What it does (per dispatch)
1042
+
1043
+ Judge the CODE subject (the amend diff / scaffold against the agentspec definition it must realize)
1044
+ on the DRAFT criteria set — **critique-BEFORE-verdict, strictly BINARY** per criterion — and emit a
1045
+ severity-gated binary quality **PASS/FAIL**. The authoritative rubric + the drafted default criteria
1046
+ live in `scripts/code-quality-verdict.ts` (`DEFAULT_CODE_QUALITY_CRITERIA` — Q1 spec-faithful [crit] ·
1047
+ Q2 addresses-diagnosis [high] · Q3 no-regression [high] · Q4 error-handling [high] · Q5 maintainable
1048
+ [med]); this mode is that rubric's dispatched form (the provider-callable mirror is the same file,
1049
+ mirroring how the other judge prompts mirror their agent modes).
1050
+
1051
+ > **⚠️ FLAGGED-FOR-OPERATOR-SIGN-OFF.** The exact acceptance criteria + the severity-gated aggregation
1052
+ > rule (CRIT/HIGH `fail`|`uncertain` blocks; MEDIUM advisory) are a DRAFT default, implemented behind
1053
+ > a clearly-named constant so the operator can confirm/adjust WITHOUT a loop code change. Do not treat
1054
+ > the current set as final.
1055
+
1056
+ ### Boundaries
1057
+
1058
+ - **Judge-only (EV-051):** score the quality; NEVER propose or write a fix (the ⑤ loop's ai-engineer
1059
+ amends). A FAIL just keeps the loop iterating.
1060
+ - **Host-runtime, no provider key; C-PIN:** pinned model + temperature 0 — byte-identical reruns.
1061
+ - **Separate from tests-green:** you judge quality, NOT whether tests pass; the deterministic test
1062
+ gate is ai-engineer's report, ANDed by the oracle — never fold it into your quality verdict.
1063
+
1064
+ ## Eval-leg reconcile — criteria MAINTENANCE, not judging (Wave-2 W2I5 · KP-003)
1065
+
1066
+ The evaluator owns the **eval leg** of the def → impl → eval triad — the third leg of `#sync-spec`. When
1067
+ an impl amends (the ⑤ IMPROVE loop's ai-engineer, or brownfield drift), the eval criteria that ground the
1068
+ subject's evaluation can go stale. The reconcile REASONING — which criteria a changed impl makes stale,
1069
+ what new criterion it needs — is the **ai-architect's** (`#sync-spec`, session-dispatched, Model-B). The
1070
+ evaluator exposes only the deterministic CONTRACT the reconcile reads/writes, in
1071
+ `scripts/sync-eval-criteria.ts`:
1072
+
1073
+ - `evalLegForSubjectKind(kind)` — the shape resolver: agent/skill/composite → the **eval-suite criteria**
1074
+ (this cell's discovered/maintained criteria); code → the **code-quality criteria**
1075
+ (`DEFAULT_CODE_QUALITY_CRITERIA` / `#mode-judge-code-quality`, Wave-2 W2I1).
1076
+ - `flagEvalLegDrift(...)` / `assessEvalLeg(...)` — a pure freshness FLAG (mirrors the builder probe).
1077
+ - `reconcileEvalCriteria(request)` — the COMPUTE: upsert the ai-architect-proposed criteria delta by id,
1078
+ append the novel, and NEVER drop a criterion (EV-053, via `assertMonotonicGrowth`). It returns the grown
1079
+ set as DATA (`result.criteria`) — it does **not** touch disk; the reconcile hook COMPUTES, it is not the write.
1080
+ - `persistEvalCriteria(result, { cwd, updatedAt? })` (`scripts/persist-eval-criteria.ts`) — the WRITE: the
1081
+ evaluator session PERSISTS `result.criteria` back to the located criteria artifact
1082
+ (`.mutagent/evaluator/living-suite/<leg>.criteria.json`, per-leg — eval-suite for an agent/skill/composite
1083
+ subject, code-quality for a code subject). That write is what BUMPS the artifact's freshness (its mtime — and
1084
+ the optional `updatedAt` marker the builder probe reads), which returns the eval leg to `in-sync`: the loop
1085
+ only closes once the reconciled criteria are on disk, not when they are merely computed.
1086
+
1087
+ > **Judge-only PRESERVED (EV-051).** This is criteria MAINTENANCE — growing/revising the criteria SET to
1088
+ > match a changed impl — NOT judging a subject. `sync-eval-criteria.ts` scores nothing and decides no
1089
+ > pass/fail; it is pure set algebra + a freshness flag (no clock/random/network — C-PIN byte-identical). The
1090
+ > `persist-eval-criteria.ts` WRITER is the IO layer (it is the one place that touches disk, mirroring how
1091
+ > `artifact-paths.ts` does IO while `living-suite.ts` stays pure); its serialization is still deterministic
1092
+ > (stable key order, no clock unless the caller passes `updatedAt`), so a persisted artifact round-trips
1093
+ > byte-faithful. The evaluator never mutates the impl and never fixes the subject; it only maintains — and now
1094
+ > persists — its own criteria.
1095
+
1032
1096
  ## Boundaries (shared)
1033
1097
 
1034
1098
  judge-only (EV-051) · model-intent-sacred · critique-before-verdict · binary-not-Likert ·
@@ -76,6 +76,7 @@ makes NO pass/fail decision; the LLM/HITL halves are Type B (below).
76
76
  | `*build-dataset` expand (EV-046) | `scripts/build-dataset.ts` | `buildCase` · cartesian dimension×value expand · deterministic token-Jaccard near-dup removal · content-derived id · `mergeCases` MONOTONIC merge. The agent PROPOSES realism; the script ENFORCES non-redundancy. Code half of the 3-way `*build-dataset` Hybrid. |
77
77
  | `*discover-dataset` distill (EV-047) | `scripts/derive-dataset.ts` | Distill a living regression set from past ✓/✗ — reuses EV-052 `sample-traces.ts` selectors (failure/outlier/uncertainty) + EV-046 monotonic merge; trace→`DatasetCase` whose query IS the real input. Labels already on traces (from `*discover-evals`); makes NO new decision. Type A Code-only. |
78
78
  | Living-suite writer (EV-053) | `scripts/living-suite.ts` | `appendOnly` + `assertMonotonicGrowth` — generic-over-`T` append-only writer (a living artifact NEVER shrinks) shared by datasets + criteria. Pure-counter provenance (no clock → byte-identity, C-PIN). Pure set algebra w/ fail-loud monotonicity guard. |
79
+ | Eval-leg reconcile contract (W2I5 · KP-003) | `scripts/sync-eval-criteria.ts` | The eval leg of `#sync-spec` (def → impl → eval triad). `evalLegForSubjectKind` (agent/skill → eval-suite · code → code-quality) · `flagEvalLegDrift`/`assessEvalLeg` (pure freshness flag) · `reconcileEvalCriteria` (upsert-by-id criteria MAINTENANCE via `assertMonotonicGrowth` — never drops, EV-053). Criteria maintenance, NOT judging (EV-051): scores nothing, decides no pass/fail. The reconcile REASONING is `ai-architect #sync-spec`'s (Model-B); this holds only the deterministic contract. Pure (no clock/random/network, C-PIN). |
79
80
  | W2 dataset contract | `scripts/contracts/dataset.ts` | TypeBox `Dimension`/`DatasetTuple`/`DatasetCase`/`Dataset` (companion to `schemas/dataset.schema.yaml`) + `tupleKey`. NEW W2-OWN file — does NOT touch shared `contracts/eval-types.ts`. Data contract only. |
80
81
  | W2 trust contract | `scripts/contracts/validation.ts` | TypeBox `HumanLabel` (produced by `*review`, consumed by `*validate`) + the confusion-matrix / validation-result shapes. NEW W2-OWN file; disjoint from shared types. Data contract only. |
81
82
 
@@ -26,7 +26,7 @@ import {
26
26
  resolveProfilePaths,
27
27
  } from "../load-profile.ts";
28
28
  import { loadBundle } from "../load-bundle.ts";
29
- import { runDeterministic } from "../run-deterministic.ts";
29
+ import { runDeterministic, reexecDeterministicCheck } from "../run-deterministic.ts";
30
30
  import { assembleScorecard } from "../assemble-scorecard.ts";
31
31
  import { renderReport } from "../render-report.ts";
32
32
  import { maskedCanonicalJson } from "../mask.ts";
@@ -107,8 +107,11 @@ export function main(argvIn?: string[]): number {
107
107
  // 2. Run-bundle
108
108
  const { bundle } = loadBundle(args.bundleDir, args.runId);
109
109
 
110
- // 3. Deterministic pass
111
- const det = runDeterministic(matrix, bundle);
110
+ // 3. Deterministic pass — WIRE the real live re-exec (Item #4): every mapped
111
+ // deterministic row runs an actual checkMethod-keyed check over the produced
112
+ // artifact instead of a presence proxy. In-cell, no sub-agents (PR-ORCH-01),
113
+ // judge-only (EV-051), C-PIN byte-stable.
114
+ const det = runDeterministic(matrix, bundle, { liveReexec: reexecDeterministicCheck });
112
115
 
113
116
  // 4. Two-track scorecard
114
117
  const scorecard = assembleScorecard({
@@ -0,0 +1,285 @@
1
+ /**
2
+ * scripts/code-quality-verdict.ts — the CODE-QUALITY judge mode (Wave-2 W2I1).
3
+ * ---------------------------------------------------------------------------
4
+ * A JUDGE mode that scores a CODE subject's QUALITY (the amended/scaffolded
5
+ * implementation of a `code`-kind subject) and emits a binary PASS/FAIL quality
6
+ * verdict with a critique-before-verdict per criterion. It is the (b) half of the
7
+ * ⑤ IMPROVE loop's code-target BOTH-gate: "converged" for a `code` subject = the
8
+ * subject's OWN test suite green (a deterministic gate the session records as
9
+ * `testsGreen`) AND this code-quality verdict PASSes. Neither alone converges.
10
+ *
11
+ * DISTINCT FROM `code-eval.ts`. That file is the DETERMINISTIC code-track primitive
12
+ * library (presence/string-equality/... over an EvalTrace — no LLM, byte-identical).
13
+ * This file is an LLM JUDGE mode: it scores the irreducibly-subjective quality of a
14
+ * code implementation (spec-faithfulness, maintainability, error-handling, ...) that
15
+ * a deterministic primitive cannot decide.
16
+ *
17
+ * INVARIANTS (consistent with the existing evaluator cell):
18
+ * - JUDGE-ONLY (EV-051). It scores; it NEVER fixes. Failures route to the ⑤ loop's
19
+ * ai-engineer amend (via the loop, not from here).
20
+ * - HOST-RUNTIME. Under the DEFAULT agent-dispatch substrate the authoritative
21
+ * rubric lives in the evaluator agent def (`#mode-judge-code-quality`), and the
22
+ * verdict comes from a dispatched subagent — this file's prompt is the
23
+ * provider-callable MIRROR for the OPTIONAL in-house / export substrate (same
24
+ * operator-named exception as judge-prompt-template.ts). No provider key on the
25
+ * default path.
26
+ * - PINNED model + temperature 0 (C-PIN). `runCodeQualityJudge` THROWS if the pin
27
+ * is not (modelId present AND temperature === 0) — reruns must be byte-identical.
28
+ * - PURE renderer + a thin injected-seam run wrapper. No clock, no random, no
29
+ * network; the LLM call is the injected `JudgeInvoke` seam so tests drive it.
30
+ */
31
+ import { parseCritiqueVerdict, type JudgeInvoke } from "./determine-outcome.ts";
32
+ import type { JudgePin } from "./build-evals.ts";
33
+ import type { CritiqueVerdict } from "./contracts/eval-types.ts";
34
+
35
+ /** Severity of a code-quality criterion — gates the aggregate (CRIT/HIGH block). */
36
+ export const CodeQualitySeverity = {
37
+ Critical: "critical",
38
+ High: "high",
39
+ Medium: "medium",
40
+ } as const;
41
+ export type CodeQualitySeverityValue =
42
+ (typeof CodeQualitySeverity)[keyof typeof CodeQualitySeverity];
43
+
44
+ /** One binary code-quality criterion the judge scores. */
45
+ export interface CodeQualityCriterion {
46
+ /** Stable id (byte-identity key + report anchor). */
47
+ id: string;
48
+ /** The one-line binary statement the judge decides pass/fail on. */
49
+ statement: string;
50
+ /** What a PASS looks like (concrete, so the judge is not guessing). */
51
+ passDefinition: string;
52
+ /** What a FAIL looks like. */
53
+ failDefinition: string;
54
+ /** Gating weight — a `fail`/`uncertain` on a CRIT/HIGH criterion blocks the gate. */
55
+ severity: CodeQualitySeverityValue;
56
+ }
57
+
58
+ /**
59
+ * ⚠️ FLAGGED-FOR-OPERATOR-SIGN-OFF (Wave-2 W2I1). This is the DRAFT default set of
60
+ * acceptance criteria that define "code quality-pass" BEYOND tests-green. It is
61
+ * implemented behind this clearly-named default so the operator can confirm/adjust
62
+ * the criteria + their severities WITHOUT a code change to the loop. The picks
63
+ * below are a conservative, subject-agnostic starting point — NOT a final contract.
64
+ *
65
+ * The five drafted criteria (each BINARY, critique-before-verdict):
66
+ * Q1 spec-faithful (critical) — the impl realizes the agentspec DEFINITION the
67
+ * amend targeted; no dropped tool / JTBD / silently
68
+ * altered system_prompt (def→impl cascade held).
69
+ * Q2 addresses-diagnosis (high) — the change actually targets the diagnosed
70
+ * root-cause locus, not an adjacent symptom.
71
+ * Q3 no-regression (high) — no unrelated behavior removed/broken; scope stayed
72
+ * on what the remedy named (scope discipline).
73
+ * Q4 error-handling (high) — failure paths are handled, not silently dropped
74
+ * (recovery/observability preserved).
75
+ * Q5 maintainable (medium) — readable, framework-idiomatic, no dead code /
76
+ * copy-paste / obvious footgun introduced.
77
+ *
78
+ * Aggregation (also FLAGGED): the gate is severity-gated — a `fail` OR `uncertain`
79
+ * on any CRITICAL/HIGH criterion blocks the quality-pass; MEDIUM is advisory. The
80
+ * operator may instead choose strict-all-pass or a different severity map.
81
+ */
82
+ export const DEFAULT_CODE_QUALITY_CRITERIA: readonly CodeQualityCriterion[] = [
83
+ {
84
+ id: "Q1-spec-faithful",
85
+ statement: "The implementation realizes the agentspec definition the amend targeted.",
86
+ passDefinition:
87
+ "PASS: every spec-declared tool / job-to-be-done is present and the operative system_prompt is unchanged except where the amend intended; the def→impl cascade held.",
88
+ failDefinition:
89
+ "FAIL: a spec-declared tool or JTBD is missing, or the system_prompt was altered beyond the amend intent (the impl forked from the SSoT).",
90
+ severity: CodeQualitySeverity.Critical,
91
+ },
92
+ {
93
+ id: "Q2-addresses-diagnosis",
94
+ statement: "The change targets the diagnosed root-cause locus, not an adjacent symptom.",
95
+ passDefinition:
96
+ "PASS: the edit is at (or directly causes a fix at) the RCA-named locus / the failing-case root.",
97
+ failDefinition:
98
+ "FAIL: the change edits an unrelated place or patches a symptom while the root cause remains.",
99
+ severity: CodeQualitySeverity.High,
100
+ },
101
+ {
102
+ id: "Q3-no-regression",
103
+ statement: "The change introduces no unrelated regression and stays in scope.",
104
+ passDefinition:
105
+ "PASS: only the remedy-named surface changed; no unrelated behavior was removed or broken.",
106
+ failDefinition:
107
+ "FAIL: unrelated code was changed/removed, or a previously-working path was broken.",
108
+ severity: CodeQualitySeverity.High,
109
+ },
110
+ {
111
+ id: "Q4-error-handling",
112
+ statement: "Failure paths are handled, not silently dropped.",
113
+ passDefinition:
114
+ "PASS: error/failure branches surface, recover, or log per the subject's contract; no new silent drop.",
115
+ failDefinition:
116
+ "FAIL: a failure is swallowed with no recovery/notify/observability (a silent-drop regression).",
117
+ severity: CodeQualitySeverity.High,
118
+ },
119
+ {
120
+ id: "Q5-maintainable",
121
+ statement: "The code is readable, framework-idiomatic, and free of obvious footguns.",
122
+ passDefinition:
123
+ "PASS: clear structure, idiomatic to the target framework, no dead code / copy-paste / obvious footgun introduced.",
124
+ failDefinition:
125
+ "FAIL: unreadable/duplicated/dead code or an obvious maintainability footgun was introduced.",
126
+ severity: CodeQualitySeverity.Medium,
127
+ },
128
+ ] as const;
129
+
130
+ /** The code subject under quality review (minimal, subject-agnostic). */
131
+ export interface CodeQualitySubject {
132
+ /** Subject id (spec_id / target name) — provenance only. */
133
+ subjectId: string;
134
+ /** The target framework/harness the impl was built into (context for the judge). */
135
+ framework: string;
136
+ /** A compact summary of the agentspec DEFINITION the impl must realize (the SSoT). */
137
+ specSummary: string;
138
+ /** The applied change under review — the amend diff, or the scaffold summary. */
139
+ appliedChange: string;
140
+ /**
141
+ * The diagnosed root-cause / remedy locus this change was meant to address (feeds
142
+ * Q2). Empty for a fresh scaffold with no prior diagnosis.
143
+ */
144
+ diagnosisLocus?: string;
145
+ }
146
+
147
+ /** One criterion's judged result (critique-before-verdict, binary). */
148
+ export interface CodeQualityCriterionVerdict {
149
+ criterionId: string;
150
+ severity: CodeQualitySeverityValue;
151
+ result: CritiqueVerdict["result"]; // pass | fail | uncertain
152
+ critique: string;
153
+ confidence: number;
154
+ }
155
+
156
+ /** The aggregated code-quality verdict for one subject. */
157
+ export interface CodeQualityVerdict {
158
+ subjectId: string;
159
+ /** The binary quality gate — the (b) half of the ⑤ code-target BOTH-gate. */
160
+ pass: boolean;
161
+ perCriterion: CodeQualityCriterionVerdict[];
162
+ /** C-PIN provenance stamped on the verdict (byte-identity guarantee). */
163
+ pinned: JudgePin;
164
+ }
165
+
166
+ /**
167
+ * Render the code-quality judge prompt for ONE criterion — the provider-callable
168
+ * mirror of the `#mode-judge-code-quality` rubric. Critique-BEFORE-verdict, strictly
169
+ * BINARY (no Likert), judge-only. No decision is made here.
170
+ */
171
+ export function buildCodeQualityPrompt(
172
+ criterion: CodeQualityCriterion,
173
+ subject: CodeQualitySubject,
174
+ ): { system: string; user: string } {
175
+ const system = [
176
+ "You are a BINARY Pass/Fail code-QUALITY judge for ONE criterion of a CODE",
177
+ "subject (an agent/tooling implementation). Judge exactly this criterion and",
178
+ "nothing else. You are a JUDGE ONLY — you NEVER propose or write a fix.",
179
+ "",
180
+ "Quality is assessed BEYOND the subject's test suite (tests-green is a separate",
181
+ "deterministic gate). Judge the code as written against the criterion.",
182
+ "",
183
+ `Criterion (${criterion.id}, severity ${criterion.severity}): ${criterion.statement}`,
184
+ criterion.passDefinition,
185
+ criterion.failDefinition,
186
+ "",
187
+ "Outcomes are strictly BINARY: pass or fail (use uncertain ONLY when the change",
188
+ "genuinely lacks the evidence to decide). NO Likert scales, NO 1-5 / letter",
189
+ "grades, NO partial credit.",
190
+ "",
191
+ "Output STRICT JSON with the critique BEFORE the verdict (reason first, then",
192
+ "commit):",
193
+ '{ "critique": "<evidence-citing assessment>", "result": "pass"|"fail"|"uncertain",',
194
+ ' "confidence": <0..1> }',
195
+ ].join("\n");
196
+
197
+ const user = [
198
+ `Subject: ${subject.subjectId} · target framework: ${subject.framework}`,
199
+ "",
200
+ "Agentspec definition the implementation must realize (the SSoT):",
201
+ subject.specSummary,
202
+ "",
203
+ subject.diagnosisLocus !== undefined && subject.diagnosisLocus.length > 0
204
+ ? `Diagnosed root-cause / remedy locus (for the addresses-diagnosis check):\n${subject.diagnosisLocus}\n`
205
+ : "",
206
+ "Applied change under review (diff or scaffold summary):",
207
+ subject.appliedChange,
208
+ ]
209
+ .filter((l) => l !== "")
210
+ .join("\n");
211
+
212
+ return { system, user };
213
+ }
214
+
215
+ /**
216
+ * The PURE aggregation: severity-gate the per-criterion verdicts into ONE binary
217
+ * quality-pass. FLAGGED default rule — a `fail` OR `uncertain` on any CRITICAL/HIGH
218
+ * criterion blocks the pass; MEDIUM is advisory (never blocks). Deterministic.
219
+ */
220
+ export function codeQualityGatePass(
221
+ perCriterion: readonly CodeQualityCriterionVerdict[],
222
+ ): boolean {
223
+ for (const v of perCriterion) {
224
+ const gating =
225
+ v.severity === CodeQualitySeverity.Critical || v.severity === CodeQualitySeverity.High;
226
+ if (gating && v.result !== "pass") return false;
227
+ }
228
+ return true;
229
+ }
230
+
231
+ /**
232
+ * Run the code-quality judge over the criteria set under a PINNED model. THROWS if
233
+ * the pin is not (modelId present AND temperature === 0) — C-PIN (byte-identical
234
+ * reruns). Renders each criterion → the injected `JudgeInvoke` seam → parses
235
+ * (critique-before-verdict) → severity-gated aggregate. Under the DEFAULT
236
+ * agent-dispatch substrate the verdicts instead come from a dispatched
237
+ * `#mode-judge-code-quality` subagent (verdict files) and this wrapper is not used.
238
+ * Deterministic given (subject, judge, pin, criteria).
239
+ */
240
+ export async function runCodeQualityJudge(
241
+ subject: CodeQualitySubject,
242
+ judge: JudgeInvoke,
243
+ pin: JudgePin,
244
+ criteria: readonly CodeQualityCriterion[] = DEFAULT_CODE_QUALITY_CRITERIA,
245
+ ): Promise<CodeQualityVerdict> {
246
+ if (typeof pin.modelId !== "string" || pin.modelId.length === 0) {
247
+ throw new Error(
248
+ "runCodeQualityJudge: judge is not pinned (missing modelId). MODEL INTENT IS " +
249
+ "SACRED — a non-pinned judge can never produce a verdict (C-PIN).",
250
+ );
251
+ }
252
+ if (pin.temperature !== 0) {
253
+ throw new Error(
254
+ `runCodeQualityJudge: judge temperature=${pin.temperature} (!= 0) — not pinned. ` +
255
+ "MODEL INTENT IS SACRED: reruns must be byte-identical (C-PIN); refusing.",
256
+ );
257
+ }
258
+ if (criteria.length === 0) {
259
+ throw new Error(
260
+ "runCodeQualityJudge: empty criteria set — a code-quality verdict with no " +
261
+ "criteria would vacuously pass (a false-green). Refusing.",
262
+ );
263
+ }
264
+
265
+ const perCriterion: CodeQualityCriterionVerdict[] = [];
266
+ for (const criterion of criteria) {
267
+ const { system, user } = buildCodeQualityPrompt(criterion, subject);
268
+ const raw = await judge(system, user);
269
+ const v = parseCritiqueVerdict(raw);
270
+ perCriterion.push({
271
+ criterionId: criterion.id,
272
+ severity: criterion.severity,
273
+ result: v.result,
274
+ critique: v.critique,
275
+ confidence: v.confidence,
276
+ });
277
+ }
278
+
279
+ return {
280
+ subjectId: subject.subjectId,
281
+ pass: codeQualityGatePass(perCriterion),
282
+ perCriterion,
283
+ pinned: { modelId: pin.modelId, temperature: pin.temperature },
284
+ };
285
+ }
@@ -0,0 +1,232 @@
1
+ /**
2
+ * scripts/persist-eval-criteria.ts — the EVAL-LEG on-disk WRITER (Wave-2 FU57 · KP-003).
3
+ * ---------------------------------------------------------------------------
4
+ * The IO half of the eval-leg reconcile. `sync-eval-criteria.ts` COMPUTES the grown
5
+ * criteria set as DATA (`reconcileEvalCriteria` — pure upsert-by-id, monotonic, never
6
+ * drops); it never touches disk. This module is the deterministic WRITER the evaluator
7
+ * SESSION calls to PERSIST that computed set back to the located eval-suite criteria
8
+ * artifact — the write that bumps the artifact's freshness and thereby returns the eval
9
+ * leg of `#sync-spec` to `in-sync` (the builder freshness probe reads the artifact's
10
+ * mtime / `updatedAt`; a fresh write makes the eval criteria as new as the amended impl).
11
+ *
12
+ * SEPARATION OF CONCERNS (mirrors `artifact-paths.ts` + `aggregate-discover.ts` doing IO
13
+ * while `living-suite.ts` stays pure): the reconcile COMPUTE is pure/C-PIN; this WRITE is
14
+ * the IO layer. The SERIALIZATION here is still deterministic (stable key order, no clock
15
+ * embedded unless the caller passes an explicit `updatedAt`), so a persisted artifact
16
+ * round-trips byte-faithful.
17
+ *
18
+ * JUDGE-ONLY PRESERVED (EV-051): this file writes CRITERIA (the evaluator maintaining its
19
+ * own eval-suite / code-quality criteria set) — it NEVER scores a subject, NEVER decides a
20
+ * pass/fail, and NEVER dispatches an agent (Model-B: code is a deterministic writer; the
21
+ * reconcile REASONING is the ai-architect's, session-dispatched).
22
+ *
23
+ * LOCATION + FORMAT (mirror the living-suite persistence; FLAGGED-for-operator, see
24
+ * `EVAL_CRITERIA_ARTIFACT_FILENAMES`): the artifact lands under the evaluator's namespaced
25
+ * `.mutagent/evaluator/living-suite/` root (`artifact-paths.ts` `livingSuiteDir`, auto-
26
+ * gitignored, no-spillover-guarded) as one JSON file per LEG — the same dir + extension the
27
+ * builder `check-sync-spec.ts` `locateEvalCriteria` auto-discovers.
28
+ */
29
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
30
+ import { dirname, join } from "node:path";
31
+ import { type Static, Type } from "@sinclair/typebox";
32
+ import { Value } from "@sinclair/typebox/value";
33
+
34
+ import { assertUnderRoot, livingSuiteDir } from "./artifact-paths.ts";
35
+ import {
36
+ EvalCriterionSchema,
37
+ EvalLegKind,
38
+ reconcileEvalCriteria,
39
+ type EvalCriteriaReconcileRequest,
40
+ type EvalCriteriaReconcileResult,
41
+ type EvalCriterion,
42
+ type EvalLegKindValue,
43
+ } from "./sync-eval-criteria.ts";
44
+
45
+ // ── the on-disk artifact shape (mirror LivingSuite `{ …, provenance }`) ───────
46
+
47
+ /**
48
+ * The persisted eval-criteria artifact — one per LEG. Mirrors the living-suite shape
49
+ * (a keyed collection + provenance), specialized to the reconcile result: the maintained
50
+ * `criteria` set, the reconcile `provenance` counters, and an OPTIONAL `updatedAt`
51
+ * freshness marker. `updatedAt` is the ONE clock-bearing field and it is NEVER generated
52
+ * here — the caller (the session, Model-B) passes it when it wants a content-level
53
+ * freshness stamp the builder probe's `parseEvalUpdatedAt` can read; omitted, freshness
54
+ * rides on the file's mtime alone (write bumps it). `additionalProperties: false` keeps the
55
+ * header tight while each criterion stays forward-parsing (EvalCriterionSchema is open).
56
+ */
57
+ export const EvalCriteriaArtifactSchema = Type.Object(
58
+ {
59
+ subjectId: Type.String({ minLength: 1 }),
60
+ leg: Type.Union([
61
+ Type.Literal(EvalLegKind.EvalSuite),
62
+ Type.Literal(EvalLegKind.CodeQuality),
63
+ ]),
64
+ criteria: Type.Array(EvalCriterionSchema),
65
+ provenance: Type.Object(
66
+ { added: Type.Number(), updated: Type.Number(), total: Type.Number() },
67
+ { additionalProperties: false },
68
+ ),
69
+ updatedAt: Type.Optional(Type.String({ minLength: 1 })),
70
+ },
71
+ { additionalProperties: false },
72
+ );
73
+ export type EvalCriteriaArtifact = Static<typeof EvalCriteriaArtifactSchema>;
74
+
75
+ /**
76
+ * FLAGGED-for-operator: the per-leg artifact FILENAME under `.mutagent/evaluator/living-suite/`.
77
+ * The sensible default that mirrors the living-suite location AND the builder probe's
78
+ * conventional fallback names (`eval-criteria` / `code-quality-criteria`), one file per leg
79
+ * so the two legs never collide. Both are `.json` under the dir `check-sync-spec.ts`
80
+ * `locateEvalCriteria` walks (`*.{yaml,yml,json}`), so a written artifact is auto-discovered
81
+ * as the eval-leg freshness anchor with no extra wiring. (A workspace evaluates ONE subject,
82
+ * so only its leg's file exists; multi-subject-per-workspace would want a `<subjectId>/`
83
+ * sub-dir — deferred, flagged.)
84
+ */
85
+ export const EVAL_CRITERIA_ARTIFACT_FILENAMES: Readonly<Record<EvalLegKindValue, string>> = {
86
+ [EvalLegKind.EvalSuite]: "eval-suite.criteria.json",
87
+ [EvalLegKind.CodeQuality]: "code-quality.criteria.json",
88
+ };
89
+
90
+ /**
91
+ * Resolve the located eval-criteria artifact path for a leg:
92
+ * `<cwd>/.mutagent/evaluator/living-suite/<leg>.criteria.json`. Routes through the
93
+ * `artifact-paths.ts` no-spillover guard (`assertUnderRoot`), so the writer can NEVER
94
+ * escape the evaluator's namespaced root. `cwd` passed in (pure path construction).
95
+ */
96
+ export function evalCriteriaArtifactPath(leg: EvalLegKindValue, cwd?: string): string {
97
+ return assertUnderRoot(join(livingSuiteDir(cwd), EVAL_CRITERIA_ARTIFACT_FILENAMES[leg]), cwd);
98
+ }
99
+
100
+ // ── serialize / build (pure, deterministic — no clock) ───────────────────────
101
+
102
+ /** Build the on-disk artifact from a reconcile RESULT. Pure. `updatedAt` passed in. */
103
+ export function evalCriteriaArtifactFromResult(
104
+ result: EvalCriteriaReconcileResult,
105
+ updatedAt?: string,
106
+ ): EvalCriteriaArtifact {
107
+ return {
108
+ subjectId: result.subjectId,
109
+ leg: result.leg,
110
+ criteria: result.criteria,
111
+ provenance: result.provenance,
112
+ ...(updatedAt !== undefined ? { updatedAt } : {}),
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Serialize an artifact to its on-disk JSON — DETERMINISTIC: explicit key order, 2-space
118
+ * indent, trailing newline. Same artifact → byte-identical string (round-trip faithful);
119
+ * no clock unless the artifact already carries a caller-supplied `updatedAt`.
120
+ */
121
+ export function serializeEvalCriteriaArtifact(artifact: EvalCriteriaArtifact): string {
122
+ const ordered: EvalCriteriaArtifact = {
123
+ subjectId: artifact.subjectId,
124
+ leg: artifact.leg,
125
+ criteria: artifact.criteria,
126
+ provenance: artifact.provenance,
127
+ ...(artifact.updatedAt !== undefined ? { updatedAt: artifact.updatedAt } : {}),
128
+ };
129
+ return `${JSON.stringify(ordered, null, 2)}\n`;
130
+ }
131
+
132
+ /** Guarded parse of a persisted artifact. THROWS with the first schema error. PURE. */
133
+ export function parseEvalCriteriaArtifact(value: unknown): EvalCriteriaArtifact {
134
+ if (!Value.Check(EvalCriteriaArtifactSchema, value)) {
135
+ const first = [...Value.Errors(EvalCriteriaArtifactSchema, value)][0];
136
+ throw new Error(
137
+ `parseEvalCriteriaArtifact: schema violation at '${first?.path ?? "(root)"}': ` +
138
+ `${first?.message ?? "invalid eval-criteria artifact"}`,
139
+ );
140
+ }
141
+ return value;
142
+ }
143
+
144
+ // ── the WRITER + reader (the IO layer) ───────────────────────────────────────
145
+
146
+ /** The result of a persist: WHERE it landed + WHAT was written (artifact + bytes). */
147
+ export interface PersistEvalCriteriaResult {
148
+ /** the located artifact path the criteria were written to. */
149
+ path: string;
150
+ /** the artifact that was serialized. */
151
+ artifact: EvalCriteriaArtifact;
152
+ /** the exact bytes written (for a byte-faithful round-trip assertion). */
153
+ json: string;
154
+ }
155
+
156
+ /**
157
+ * PERSIST a reconcile result's `criteria` back to the located eval-suite criteria artifact
158
+ * — the WRITE that returns the eval leg to `in-sync`. Resolves the per-leg path under
159
+ * `.mutagent/evaluator/living-suite/` (no-spillover-guarded via `artifact-paths.ts`),
160
+ * ensures the dir, and writes the deterministic JSON. Handles BOTH legs by `result.leg`:
161
+ * the eval-suite leg (agent/skill/composite subject) and the code-quality leg (a code
162
+ * subject, incl. an operator-overridden code-quality set). The freshness bump is the file
163
+ * write itself (mtime advances → the builder probe reads the eval leg as fresh); pass
164
+ * `options.updatedAt` (ISO 8601) to ALSO stamp a content-level freshness marker the probe's
165
+ * `parseEvalUpdatedAt` reads. IO — NOT pure (that is its job); serialization is deterministic.
166
+ */
167
+ export function persistEvalCriteria(
168
+ result: EvalCriteriaReconcileResult,
169
+ options: { cwd?: string; updatedAt?: string } = {},
170
+ ): PersistEvalCriteriaResult {
171
+ const artifact = evalCriteriaArtifactFromResult(result, options.updatedAt);
172
+ const path = evalCriteriaArtifactPath(result.leg, options.cwd);
173
+ const json = serializeEvalCriteriaArtifact(artifact);
174
+ mkdirSync(dirname(path), { recursive: true });
175
+ writeFileSync(path, json);
176
+ return { path, artifact, json };
177
+ }
178
+
179
+ /** Read + validate a persisted artifact (round-trip). THROWS on a schema violation. */
180
+ export function readEvalCriteriaArtifact(path: string): EvalCriteriaArtifact {
181
+ return parseEvalCriteriaArtifact(JSON.parse(readFileSync(path, "utf8")) as unknown);
182
+ }
183
+
184
+ /**
185
+ * Load the CURRENTLY-persisted criteria for a leg (the `existing` set the next reconcile
186
+ * grows). Returns `[]` when no artifact exists yet (the cold / first-reconcile case) so a
187
+ * caller can feed it straight into an `EvalCriteriaReconcileRequest.existing`. Reads only.
188
+ */
189
+ export function loadExistingEvalCriteria(leg: EvalLegKindValue, cwd?: string): EvalCriterion[] {
190
+ const path = evalCriteriaArtifactPath(leg, cwd);
191
+ if (!existsSync(path)) return [];
192
+ return readEvalCriteriaArtifact(path).criteria;
193
+ }
194
+
195
+ /**
196
+ * The eval-criteria artifact's effective freshness epoch (seconds) — the evaluator-side
197
+ * MIRROR of the builder `check-sync-spec.ts` eval-leg compare: `max(updatedAt, mtime)`
198
+ * (git omitted — kept in-package + git-independent). `null` when the artifact is absent.
199
+ * Feed this as `evalCriteriaEpoch` to `flagEvalLegDrift` to confirm a fresh write closed
200
+ * the loop (`in-sync`). Reads mtime (IO).
201
+ */
202
+ export function evalCriteriaEffectiveEpoch(path: string): number | null {
203
+ if (!existsSync(path)) return null;
204
+ const epochs: number[] = [];
205
+ try {
206
+ const parsed = readEvalCriteriaArtifact(path);
207
+ if (parsed.updatedAt !== undefined) {
208
+ const ms = Date.parse(parsed.updatedAt);
209
+ if (Number.isFinite(ms)) epochs.push(Math.floor(ms / 1000));
210
+ }
211
+ } catch {
212
+ // a corrupt artifact still has an mtime — fall back to it for freshness.
213
+ }
214
+ epochs.push(Math.floor(statSync(path).mtimeMs / 1000));
215
+ return Math.max(...epochs);
216
+ }
217
+
218
+ /**
219
+ * Convenience for the session: COMPUTE the reconcile (pure) THEN PERSIST it (IO) in one
220
+ * call — the exact "reconcile → write → in-sync" step the `#sync-spec` eval leg performs.
221
+ * Still Model-B: the criteria DELTA (`request.proposed`) is the ai-architect's reasoning;
222
+ * this only applies it deterministically and writes it. Returns the compute result + the
223
+ * persist result together.
224
+ */
225
+ export function reconcileAndPersistEvalCriteria(
226
+ request: EvalCriteriaReconcileRequest,
227
+ options: { cwd?: string; updatedAt?: string } = {},
228
+ ): { result: EvalCriteriaReconcileResult; persisted: PersistEvalCriteriaResult } {
229
+ const result = reconcileEvalCriteria(request);
230
+ const persisted = persistEvalCriteria(result, options);
231
+ return { result, persisted };
232
+ }