@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.
- package/.claude/skills/mutagent-evaluator/SKILL.md +2 -1
- package/.claude/skills/mutagent-evaluator/assets/agents/evaluator.md +64 -0
- package/.claude/skills/mutagent-evaluator/references/operation-inventory.md +1 -0
- package/.claude/skills/mutagent-evaluator/scripts/cli/audit-run.ts +6 -3
- package/.claude/skills/mutagent-evaluator/scripts/code-quality-verdict.ts +285 -0
- package/.claude/skills/mutagent-evaluator/scripts/persist-eval-criteria.ts +232 -0
- package/.claude/skills/mutagent-evaluator/scripts/route-failures.ts +12 -2
- package/.claude/skills/mutagent-evaluator/scripts/run-deterministic.ts +114 -18
- package/.claude/skills/mutagent-evaluator/scripts/sync-eval-criteria.ts +244 -0
- package/bin/mutagent-cli.mjs +650 -3
- package/package.json +2 -2
|
@@ -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.
|
|
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
|
-
|
|
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
|
+
}
|