@mutagent/evaluator 0.1.0-alpha.5 → 0.2.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.claude/skills/mutagent-evaluator/SKILL.md +28 -30
  2. package/.claude/skills/mutagent-evaluator/assets/agents/dataset-builder.md +6 -5
  3. package/.claude/skills/mutagent-evaluator/assets/agents/evaluator.md +93 -30
  4. package/.claude/skills/mutagent-evaluator/assets/code-quality-criteria.yaml +75 -0
  5. package/.claude/skills/mutagent-evaluator/assets/templates/discover-report.template.html +397 -0
  6. package/.claude/skills/mutagent-evaluator/assets/templates/eval-report.template.html +594 -0
  7. package/.claude/skills/mutagent-evaluator/assets/templates/review-report.template.html +560 -0
  8. package/.claude/skills/mutagent-evaluator/golden/judge-trajectory.prose.md +69 -0
  9. package/.claude/skills/mutagent-evaluator/lenses/methodology-critic-lens.md +1 -1
  10. package/.claude/skills/mutagent-evaluator/references/data-registry.md +43 -0
  11. package/.claude/skills/mutagent-evaluator/references/edd-loop.md +6 -6
  12. package/.claude/skills/mutagent-evaluator/references/eval-layers.md +52 -0
  13. package/.claude/skills/mutagent-evaluator/references/eval-stage.md +37 -4
  14. package/.claude/skills/mutagent-evaluator/references/memory-format.md +1 -1
  15. package/.claude/skills/mutagent-evaluator/schemas/methodology-review.schema.yaml +1 -1
  16. package/.claude/skills/mutagent-evaluator/scripts/agent-dispatch.ts +0 -0
  17. package/.claude/skills/mutagent-evaluator/scripts/aggregate-discover.ts +254 -9
  18. package/.claude/skills/mutagent-evaluator/scripts/audience.ts +84 -0
  19. package/.claude/skills/mutagent-evaluator/scripts/build-review-ui.ts +320 -6
  20. package/.claude/skills/mutagent-evaluator/scripts/cli/prep.ts +32 -2
  21. package/.claude/skills/mutagent-evaluator/scripts/cli/profile-subject.ts +1 -1
  22. package/.claude/skills/mutagent-evaluator/scripts/code-eval-library.ts +201 -0
  23. package/.claude/skills/mutagent-evaluator/scripts/code-quality-verdict.ts +60 -68
  24. package/.claude/skills/mutagent-evaluator/scripts/config/schema.ts +3 -3
  25. package/.claude/skills/mutagent-evaluator/scripts/contracts/agentspec-evals.ts +101 -39
  26. package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-matrix.ts +326 -3
  27. package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-types.ts +30 -4
  28. package/.claude/skills/mutagent-evaluator/scripts/determine-outcome.ts +8 -0
  29. package/.claude/skills/mutagent-evaluator/scripts/edd/change-request.ts +1 -1
  30. package/.claude/skills/mutagent-evaluator/scripts/edd/edd-types.ts +2 -2
  31. package/.claude/skills/mutagent-evaluator/scripts/judge-prompt-template.ts +144 -15
  32. package/.claude/skills/mutagent-evaluator/scripts/materialize-dataset.ts +130 -68
  33. package/.claude/skills/mutagent-evaluator/scripts/matrix-judge.ts +219 -1
  34. package/.claude/skills/mutagent-evaluator/scripts/memory/append.ts +1 -1
  35. package/.claude/skills/mutagent-evaluator/scripts/memory/ratify.ts +165 -0
  36. package/.claude/skills/mutagent-evaluator/scripts/merge-criteria.ts +849 -0
  37. package/.claude/skills/mutagent-evaluator/scripts/prep-tasks.ts +15 -4
  38. package/.claude/skills/mutagent-evaluator/scripts/read-manifest.ts +120 -0
  39. package/.claude/skills/mutagent-evaluator/scripts/read-unitf-traces.ts +34 -2
  40. package/.claude/skills/mutagent-evaluator/scripts/render-build-cards.ts +37 -0
  41. package/.claude/skills/mutagent-evaluator/scripts/render-discover-report-v3.ts +1155 -0
  42. package/.claude/skills/mutagent-evaluator/scripts/render-eval-report-v3.ts +610 -0
  43. package/.claude/skills/mutagent-evaluator/scripts/render-eval-report.ts +11 -92
  44. package/.claude/skills/mutagent-evaluator/scripts/render-review-report-v3.ts +691 -0
  45. package/.claude/skills/mutagent-evaluator/scripts/report-fragments.ts +173 -0
  46. package/.claude/skills/mutagent-evaluator/scripts/route-failures.ts +12 -9
  47. package/.claude/skills/mutagent-evaluator/scripts/run-evaluate.ts +46 -1
  48. package/.claude/skills/mutagent-evaluator/scripts/run-pipeline.ts +39 -4
  49. package/.claude/skills/mutagent-evaluator/scripts/run-review.ts +266 -0
  50. package/.claude/skills/mutagent-evaluator/scripts/subject-profile.ts +82 -0
  51. package/.claude/skills/mutagent-evaluator/scripts/sync-eval-criteria.ts +2 -2
  52. package/.claude/skills/mutagent-evaluator/scripts/unitf-to-evaltrace.ts +64 -21
  53. package/.claude/skills/mutagent-evaluator/scripts/verify-render.ts +728 -0
  54. package/bin/mutagent-cli.mjs +9 -5
  55. package/package.json +2 -2
@@ -85,6 +85,18 @@ function transcriptOf(trace: EvalTrace): TranscriptTurn[] {
85
85
  return turns;
86
86
  }
87
87
 
88
+ /**
89
+ * The VERIFY-SURFACE view of a situation: each trace augmented with the SAME
90
+ * deterministic `trajectory`/`transcript` projections its packet was built from
91
+ * (`trajectoryOf`/`transcriptOf`). A judge cites refs against the packet it was
92
+ * handed — those paths must re-resolve at verify time, or a syntax/shape gap
93
+ * (not evidence weakness) silently downgrades a grounded verdict. Trace-shaped
94
+ * paths (`observations.N.…`) keep resolving via the spread. PURE.
95
+ */
96
+ export function verifySituationViews(traces: EvalTrace[]): EvalTrace[] {
97
+ return traces.map((t) => ({ ...t, trajectory: trajectoryOf(t), transcript: transcriptOf(t) }) as EvalTrace);
98
+ }
99
+
88
100
  // ── T1 (B-U3) tier-0 deterministic pre-pass ──────────────────────────────────
89
101
  //
90
102
  // THE real cost lever (§9.4.1): run the code-method rows FIRST (deterministic,
@@ -338,6 +350,7 @@ export function buildMatrixPacket(
338
350
  criteria: MatrixCriterion[],
339
351
  pin: { model: string; temperature: 0 },
340
352
  subjectProfile?: SubjectProfile,
353
+ layerScope?: string[],
341
354
  ): MatrixPacket {
342
355
  const packet: MatrixPacket = {
343
356
  subject,
@@ -349,6 +362,9 @@ export function buildMatrixPacket(
349
362
  // byte-stable legacy packet (the judge reconstructs the profile at reason-time).
350
363
  ...(subjectProfile !== undefined ? { subjectProfile } : {}),
351
364
  pin,
365
+ // v3 D1=B+ — the pipeable `*evaluate-<layer>` filter. ABSENT/empty ⇒ the FULL
366
+ // walk (byte-stable legacy packet — the field is omitted entirely).
367
+ ...(layerScope !== undefined && layerScope.length > 0 ? { layerScope } : {}),
352
368
  };
353
369
  assertMatrixPacket(packet);
354
370
  return packet;
@@ -604,7 +620,14 @@ export function aggregateMatrixScorecard(input: MatrixAggregateInput): MatrixAgg
604
620
  // proof that a DIFFERENT judge ran, not just "eligible for".
605
621
  const independentVerify: IndependentVerifyRecord[] = [];
606
622
  if (input.independentVerify !== undefined) {
607
- const { situation, signals } = input.independentVerify;
623
+ const { situation: rawSituation, signals } = input.independentVerify;
624
+ // The judge cites refs against the PACKET it was handed (trajectory[]/transcript[]
625
+ // projections), but the raw EvalTrace carries neither field — so a deterministic
626
+ // re-resolve against bare traces reads every packet-shaped ref as DEAD and
627
+ // silently downgrades a grounded fail to uncertain (observed live: gate flipped
628
+ // fail → incomplete on a verifier-UPHELD defect). Re-resolve against the trace
629
+ // AUGMENTED with the same deterministic projections the packet was built from.
630
+ const situation = verifySituationViews(rawSituation);
608
631
  const gatingFails = gatingFailVerdicts(verdicts, severityById, input.gatingSeverities);
609
632
  const gatingFailIds = new Set(gatingFails.map((v) => v.criterionId));
610
633
  for (const v of verdicts) {
@@ -748,3 +771,198 @@ export function readMatrixVerdictFiles(verdictDir: string, trajectoryIds: string
748
771
  }
749
772
  return raws;
750
773
  }
774
+
775
+ // ═══════════════════════════════════════════════════════════════════════════════
776
+ // v3 LAYERED WALK — aggregate-side folds (E-NEW-1/3/9 · D2-A). ALL PURE, ALL
777
+ // ADDITIVE: consumed by run-evaluate AFTER the (untouched) criteria aggregate;
778
+ // the GATE function is NOT edited — layer verdicts are diagnostic columns and
779
+ // cross-layer CONFLICTS are surfaced for a human, never auto-resolved (OT-1).
780
+ // ═══════════════════════════════════════════════════════════════════════════════
781
+
782
+ import { LAYER_IDS, type LayerVerdict, type WalkEmissions } from "./contracts/eval-matrix.ts";
783
+
784
+ /** One trajectory's layer row for the report's layer matrix (skipped ≠ undecidable). */
785
+ export interface LayerFoldRow {
786
+ trajectoryId: string;
787
+ /** layer id → verdict; layers the walk never mentioned are `absent` (pre-v3 file →
788
+ * NAMED degrade); `fired` = the L0 evidence-state (patterns fired, no verdict). */
789
+ byLayer: Record<string, "pass" | "fail" | "undecidable" | "skipped" | "fired" | "no-fires" | "absent">;
790
+ earlyExit?: string;
791
+ /** COERCED to a count (a live judge may emit the engaged layer-id LIST). */
792
+ layersEngaged?: number;
793
+ }
794
+
795
+ /** Per-layer fold buckets. `other` is the NaN-proof catch-all for any tolerant
796
+ * verdict value the buckets don't know yet — surfaced, never silently dropped
797
+ * (the raw value stays visible in the row's `byLayer`). */
798
+ export interface LayerFoldTotals {
799
+ pass: number;
800
+ fail: number;
801
+ undecidable: number;
802
+ skipped: number;
803
+ fired: number;
804
+ /** L0 clean-state (library ran, no pattern fired) — honest clean, never a pass. */
805
+ "no-fires": number;
806
+ absent: number;
807
+ other: number;
808
+ }
809
+
810
+ /** Coerce a tolerant `layersEngaged` (count OR layer-id list) to a count. */
811
+ export function layersEngagedCount(v: number | string[] | undefined): number | undefined {
812
+ return Array.isArray(v) ? v.length : v;
813
+ }
814
+
815
+ /** E-NEW-1 — fold per-walk layerVerdicts into per-trajectory rows + per-layer totals. */
816
+ export function foldLayerVerdicts(files: MatrixVerdictFile[]): {
817
+ rows: LayerFoldRow[];
818
+ totals: Record<string, LayerFoldTotals>;
819
+ } {
820
+ const totals: Record<string, LayerFoldTotals> = {};
821
+ for (const layer of LAYER_IDS)
822
+ totals[layer] = { pass: 0, fail: 0, undecidable: 0, skipped: 0, fired: 0, "no-fires": 0, absent: 0, other: 0 };
823
+ const rows: LayerFoldRow[] = files.map((file) => {
824
+ const byLayer: LayerFoldRow["byLayer"] = {};
825
+ for (const layer of LAYER_IDS) {
826
+ const lv = (file.layerVerdicts ?? []).find((x: LayerVerdict) => x.layer === layer);
827
+ const v = (lv?.verdict ?? "absent") as LayerFoldRow["byLayer"][string];
828
+ byLayer[layer] = v;
829
+ const bucket = totals[layer]![v as keyof LayerFoldTotals] !== undefined ? (v as keyof LayerFoldTotals) : "other";
830
+ totals[layer]![bucket] += 1;
831
+ }
832
+ return {
833
+ trajectoryId: file.trajectoryId,
834
+ byLayer,
835
+ earlyExit: file.earlyExit,
836
+ layersEngaged: layersEngagedCount(file.layersEngaged),
837
+ };
838
+ });
839
+ return { rows, totals };
840
+ }
841
+
842
+ /** E-NEW-3 · D2-A — one detected cross-layer disagreement. NEVER auto-resolved. */
843
+ export interface LayerConflict {
844
+ trajectoryId: string;
845
+ /** the disagreeing layers, e.g. ["L1","L2"]. */
846
+ layers: string[];
847
+ verdicts: Record<string, string>;
848
+ /** the judge's own note if it flagged the tension (honest emission). */
849
+ note?: string;
850
+ }
851
+
852
+ /**
853
+ * Detect outcome-vs-deep-layer disagreements (the OT-1 class): L1 pass with any
854
+ * of L2/L3/L4 fail, or L1 fail with ALL deep layers pass. Detection ONLY — the
855
+ * conflict routes to the human calibration queue; the GATE never reads it.
856
+ */
857
+ export function detectLayerConflicts(files: MatrixVerdictFile[]): LayerConflict[] {
858
+ const out: LayerConflict[] = [];
859
+ for (const file of files) {
860
+ const lvs = file.layerVerdicts ?? [];
861
+ const get = (layer: string): LayerVerdict | undefined => lvs.find((x: LayerVerdict) => x.layer === layer);
862
+ const l1 = get("L1");
863
+ if (!l1) continue;
864
+ const deep = ["L2", "L3", "L4"].map((l) => get(l)).filter((x): x is LayerVerdict => x !== undefined);
865
+ const deepFails = deep.filter((x) => x.verdict === "fail");
866
+ const deepDecided = deep.filter((x) => x.verdict === "pass" || x.verdict === "fail");
867
+ if (l1.verdict === "pass" && deepFails.length > 0) {
868
+ const layers = ["L1", ...deepFails.map((x) => x.layer)];
869
+ out.push({
870
+ trajectoryId: file.trajectoryId,
871
+ layers,
872
+ verdicts: Object.fromEntries([["L1", "pass"], ...deepFails.map((x) => [x.layer, x.verdict])]),
873
+ note: deepFails.map((x) => x.note).filter(Boolean).join(" · ") || l1.note,
874
+ });
875
+ } else if (l1.verdict === "fail" && deepDecided.length > 0 && deepFails.length === 0) {
876
+ out.push({
877
+ trajectoryId: file.trajectoryId,
878
+ layers: ["L1", ...deepDecided.map((x) => x.layer)],
879
+ verdicts: Object.fromEntries([["L1", "fail"], ...deepDecided.map((x) => [x.layer, x.verdict])]),
880
+ note: l1.note,
881
+ });
882
+ }
883
+ }
884
+ return out;
885
+ }
886
+
887
+ /**
888
+ * COMPLETENESS LAW (S1.2) — every (criterion × complete-fidelity trajectory) cell
889
+ * must be accounted for: decided, abstained (uncertain), or explicitly `na` in the
890
+ * denseMap. A silently-missing cell THROWS — a criterion may never go unverdicted
891
+ * because nothing bound. INCOMPLETE (fidelity) trajectories are exempt by design.
892
+ */
893
+ export function assertNoUnverdictedCriterion(criteria: MatrixCriterion[], files: MatrixVerdictFile[]): void {
894
+ const missing: string[] = [];
895
+ for (const file of files) {
896
+ if (file.fidelity && file.fidelity.complete === false) continue; // INCOMPLETE exempt
897
+ const judged = new Set(file.verdicts.map((v) => v.criterionId));
898
+ const dense = file.denseMap ?? {};
899
+ for (const c of criteria) {
900
+ if (judged.has(c.criterionId)) continue;
901
+ if (dense[c.criterionId] !== undefined) continue; // na / dense-mapped = accounted
902
+ missing.push(`${file.trajectoryId} × ${c.criterionId}`);
903
+ }
904
+ }
905
+ if (missing.length > 0) {
906
+ throw new Error(
907
+ `COMPLETENESS LAW: ${missing.length} (criterion × trajectory) cell(s) unaccounted — a criterion ` +
908
+ `may NEVER be silently unverdicted (decide, abstain-with-reason, or mark na in denseMap). ` +
909
+ `First offenders: ${missing.slice(0, 5).join(" · ")}`,
910
+ );
911
+ }
912
+ }
913
+
914
+ /** MR-5 (E-NEW-9b) — fold the walks' emissions self-manifests into the run
915
+ * data-completeness scorecard: expected-key coverage + every NAMED missing reason. */
916
+ export interface EmissionsScorecard {
917
+ walksWithManifest: number;
918
+ walksTotal: number;
919
+ /** expected emission keys of the v3 contract. */
920
+ expectedKeys: string[];
921
+ /** key → number of complete-fidelity walks that emitted it. */
922
+ emittedCounts: Record<string, number>;
923
+ /** every named degrade: {trajectoryId, key, reason}. */
924
+ namedMissing: { trajectoryId: string; key: string; reason: string }[];
925
+ /** walks that emitted NO manifest at all (pre-v3 / defect — a named degrade itself). */
926
+ manifestAbsent: string[];
927
+ }
928
+
929
+ export const V3_EXPECTED_EMISSIONS: readonly string[] = [
930
+ "layerVerdicts",
931
+ "understanding",
932
+ "expectedTrajectory",
933
+ "agentSteps",
934
+ "judgeSteps",
935
+ "emissions",
936
+ ];
937
+
938
+ export function foldEmissionsScorecard(files: MatrixVerdictFile[]): EmissionsScorecard {
939
+ const complete = files.filter((f) => !(f.fidelity && f.fidelity.complete === false));
940
+ const emittedCounts: Record<string, number> = {};
941
+ for (const k of V3_EXPECTED_EMISSIONS) emittedCounts[k] = 0;
942
+ const namedMissing: EmissionsScorecard["namedMissing"] = [];
943
+ const manifestAbsent: string[] = [];
944
+ for (const f of complete) {
945
+ // ground truth from the file itself (never trust-only the self-report):
946
+ const present = (k: string): boolean =>
947
+ (f as unknown as Record<string, unknown>)[k] !== undefined &&
948
+ (!Array.isArray((f as unknown as Record<string, unknown>)[k]) ||
949
+ ((f as unknown as Record<string, unknown>)[k] as unknown[]).length > 0);
950
+ for (const k of V3_EXPECTED_EMISSIONS) if (present(k)) emittedCounts[k]! += 1;
951
+ const manifest: WalkEmissions | undefined = f.emissions;
952
+ if (!manifest) {
953
+ manifestAbsent.push(f.trajectoryId);
954
+ continue;
955
+ }
956
+ for (const m of manifest.missing ?? []) {
957
+ namedMissing.push({ trajectoryId: f.trajectoryId, key: m.key, reason: m.reason });
958
+ }
959
+ }
960
+ return {
961
+ walksWithManifest: complete.length - manifestAbsent.length,
962
+ walksTotal: complete.length,
963
+ expectedKeys: [...V3_EXPECTED_EMISSIONS],
964
+ emittedCounts,
965
+ namedMissing,
966
+ manifestAbsent,
967
+ };
968
+ }
@@ -37,7 +37,7 @@ export const Lifecycle = {
37
37
  Build: "build",
38
38
  Evaluate: "evaluate",
39
39
  Diagnose: "diagnose",
40
- Improve: "improve",
40
+ Optimize: "optimize",
41
41
  General: "general",
42
42
  } as const;
43
43
  export type LifecycleValue = (typeof Lifecycle)[keyof typeof Lifecycle];
@@ -0,0 +1,165 @@
1
+ /**
2
+ * scripts/memory/ratify.ts — EV-4: persist ratify / do-not decisions + hold the verdict.
3
+ * ---------------------------------------------------------------------------
4
+ * The dominant dogfood finding ("router-as-doer") rested on a NORMATIVE call —
5
+ * what counts as correct routing — that only the operator can settle. The review
6
+ * UI already CAPTURES the operator's verify/eliminate decisions on each surfaced
7
+ * assumption (`build-review-ui.ts` `setCalib` → `calibration.json`), but wrote them
8
+ * NOWHERE: a verdict resting on an unratified standard still read as final.
9
+ *
10
+ * This is the LEAN wiring (decision EV-4 option a — NO new criterion schema):
11
+ * 1. ROUTE each calibration decision into `appendMemory` — the same store the
12
+ * next run's judges read at start (`memory/read.ts`), deduped by slug.
13
+ * 2. HOLD the verdict: a NORMATIVE criterion with no `verify` ratification in the
14
+ * decision set is `needs-ratification`; while any is pending the run's verdict
15
+ * is HELD (not final) — the value call must be settled first.
16
+ *
17
+ * PURE cores (deterministic; `now` injected). The only fs edge reuses `appendMemory`.
18
+ */
19
+ import { appendMemory, Lifecycle, MemoryType, type AppendResult } from "./append.ts";
20
+ import { AssumptionKind, type CriterionVerdict } from "../contracts/eval-types.ts";
21
+
22
+ /**
23
+ * The criterion ids whose verdict rests on a NORMATIVE assumption (a value/standard
24
+ * call — `AssumptionKind.Normative`), whether surfaced in `assumptions[]` or the
25
+ * `blockedBy` payload. These are the criteria that REQUIRE operator ratification
26
+ * before their verdict can be final. PURE.
27
+ */
28
+ export function normativeCriterionIds(verdicts: CriterionVerdict[]): string[] {
29
+ const ids = new Set<string>();
30
+ for (const v of verdicts) {
31
+ const inAssumptions = (v.assumptions ?? []).some((a) => a.kind === AssumptionKind.Normative);
32
+ const inBlock = v.blockedBy?.kind === AssumptionKind.Normative;
33
+ if (inAssumptions || inBlock) ids.add(v.criterionId);
34
+ }
35
+ return [...ids].sort();
36
+ }
37
+
38
+ /** The verify/eliminate action a review-UI calibration button emits (`data-action`). */
39
+ export const RatifyAction = {
40
+ /** the operator RATIFIES the surfaced assumption/standard (accept it). */
41
+ Verify: "verify",
42
+ /** the operator rejects it (do-not — drop this standard). */
43
+ Eliminate: "eliminate",
44
+ } as const;
45
+ export type RatifyActionValue = (typeof RatifyAction)[keyof typeof RatifyAction];
46
+
47
+ /**
48
+ * One calibration decision as the review UI exports it (`calibration.json`). Shape
49
+ * mirrors `build-review-ui.ts` `setCalib` EXACTLY so the operator's export routes
50
+ * with no transform.
51
+ */
52
+ export interface CalibrationDecision {
53
+ traceId: string;
54
+ criterionId: string;
55
+ assumptionIndex: number;
56
+ action: RatifyActionValue;
57
+ decidedAt?: string;
58
+ }
59
+
60
+ /** A stable slug for a ratification memory (dedupe: re-deciding the same pair updates). */
61
+ export function ratificationSlug(d: CalibrationDecision): string {
62
+ return `ratify-${d.criterionId}-a${d.assumptionIndex}`;
63
+ }
64
+
65
+ /**
66
+ * Project a calibration decision → an AutoMemory FeedbackInput. A `verify` is a
67
+ * standing "this standard IS ratified" fact the next run's judge honours; an
68
+ * `eliminate` records the operator dropped it. PURE.
69
+ */
70
+ export function calibrationToMemory(
71
+ d: CalibrationDecision,
72
+ criterionStatement?: string,
73
+ ): { title: string; description: string; body: string } {
74
+ const ratified = d.action === RatifyAction.Verify;
75
+ const what = criterionStatement !== undefined && criterionStatement.length > 0 ? criterionStatement : d.criterionId;
76
+ const title = ratificationSlug(d);
77
+ const verb = ratified ? "RATIFIED" : "REJECTED (do-not)";
78
+ return {
79
+ title,
80
+ description: `Normative ratification — criterion ${d.criterionId} assumption #${d.assumptionIndex} ${verb}`,
81
+ body:
82
+ `Operator ${verb} the normative standard for criterion \`${d.criterionId}\` ` +
83
+ `(assumption #${d.assumptionIndex}).\n\n` +
84
+ `**What:** ${what}\n` +
85
+ `**Why:** a normative criterion (what SHOULD count as good) is an operator value call — ` +
86
+ `the judge cannot settle it. This decision is the settled standard.\n` +
87
+ `**How to apply:** ${ratified
88
+ ? "treat this standard as ratified — the verdict on this criterion may now be final."
89
+ : "do NOT gate on this standard — the operator dropped it."}\n` +
90
+ (d.decidedAt !== undefined ? `\n(decided ${d.decidedAt})` : ""),
91
+ };
92
+ }
93
+
94
+ /**
95
+ * ROUTE calibration decisions into AutoMemory (EV-4). One `appendMemory` per
96
+ * decision, deduped by slug (re-deciding UPDATES). Returns the append results.
97
+ * `now` (YYYY-MM-DD) is INJECTED. `statements` optionally maps criterionId → its
98
+ * statement for a richer memory body.
99
+ */
100
+ export function persistCalibrations(
101
+ memoryDir: string,
102
+ decisions: CalibrationDecision[],
103
+ now: string,
104
+ statements: Record<string, string> = {},
105
+ ): AppendResult[] {
106
+ return decisions.map((d) => {
107
+ const m = calibrationToMemory(d, statements[d.criterionId]);
108
+ return appendMemory(
109
+ memoryDir,
110
+ {
111
+ title: m.title,
112
+ description: m.description,
113
+ lifecycle: Lifecycle.Evaluate,
114
+ type: MemoryType.Feedback, // a standing steer the next run's judge honours
115
+ feedback: { text: m.body },
116
+ },
117
+ now,
118
+ );
119
+ });
120
+ }
121
+
122
+ /** The ratification state of one normative criterion (EV-4). */
123
+ export interface RatificationStatus {
124
+ criterionId: string;
125
+ /** true when a `verify` decision ratified it; false ⇒ needs-ratification. */
126
+ ratified: boolean;
127
+ }
128
+
129
+ /**
130
+ * Given the NORMATIVE criterion ids (those resting on a normative assumption) and
131
+ * the operator's calibration decisions, report which are RATIFIED (a `verify`
132
+ * exists) vs still `needs-ratification`. An `eliminate` does NOT ratify — the
133
+ * standard was dropped, not settled-as-correct. PURE.
134
+ */
135
+ export function ratificationStatus(
136
+ normativeCriterionIds: string[],
137
+ decisions: CalibrationDecision[],
138
+ ): RatificationStatus[] {
139
+ const ratified = new Set(
140
+ decisions.filter((d) => d.action === RatifyAction.Verify).map((d) => d.criterionId),
141
+ );
142
+ return normativeCriterionIds.map((id) => ({ criterionId: id, ratified: ratified.has(id) }));
143
+ }
144
+
145
+ /**
146
+ * The criterion ids that HOLD the verdict (EV-4): normative criteria not yet
147
+ * ratified. While this list is non-empty the run's verdict is NOT final — the
148
+ * operator must settle the value call first. PURE.
149
+ */
150
+ export function pendingRatifications(
151
+ normativeCriterionIds: string[],
152
+ decisions: CalibrationDecision[],
153
+ ): string[] {
154
+ return ratificationStatus(normativeCriterionIds, decisions)
155
+ .filter((s) => !s.ratified)
156
+ .map((s) => s.criterionId);
157
+ }
158
+
159
+ /** Is the run's verdict HELD pending an operator ratification? (EV-4) */
160
+ export function isVerdictHeldForRatification(
161
+ normativeCriterionIds: string[],
162
+ decisions: CalibrationDecision[],
163
+ ): boolean {
164
+ return pendingRatifications(normativeCriterionIds, decisions).length > 0;
165
+ }