@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.
- package/.claude/skills/mutagent-evaluator/SKILL.md +28 -30
- package/.claude/skills/mutagent-evaluator/assets/agents/dataset-builder.md +6 -5
- package/.claude/skills/mutagent-evaluator/assets/agents/evaluator.md +93 -30
- package/.claude/skills/mutagent-evaluator/assets/code-quality-criteria.yaml +75 -0
- package/.claude/skills/mutagent-evaluator/assets/templates/discover-report.template.html +397 -0
- package/.claude/skills/mutagent-evaluator/assets/templates/eval-report.template.html +594 -0
- package/.claude/skills/mutagent-evaluator/assets/templates/review-report.template.html +560 -0
- package/.claude/skills/mutagent-evaluator/golden/judge-trajectory.prose.md +69 -0
- package/.claude/skills/mutagent-evaluator/lenses/methodology-critic-lens.md +1 -1
- package/.claude/skills/mutagent-evaluator/references/data-registry.md +43 -0
- package/.claude/skills/mutagent-evaluator/references/edd-loop.md +6 -6
- package/.claude/skills/mutagent-evaluator/references/eval-layers.md +52 -0
- package/.claude/skills/mutagent-evaluator/references/eval-stage.md +37 -4
- package/.claude/skills/mutagent-evaluator/references/memory-format.md +1 -1
- package/.claude/skills/mutagent-evaluator/schemas/methodology-review.schema.yaml +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/agent-dispatch.ts +0 -0
- package/.claude/skills/mutagent-evaluator/scripts/aggregate-discover.ts +254 -9
- package/.claude/skills/mutagent-evaluator/scripts/audience.ts +84 -0
- package/.claude/skills/mutagent-evaluator/scripts/build-review-ui.ts +320 -6
- package/.claude/skills/mutagent-evaluator/scripts/cli/prep.ts +32 -2
- package/.claude/skills/mutagent-evaluator/scripts/cli/profile-subject.ts +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/code-eval-library.ts +201 -0
- package/.claude/skills/mutagent-evaluator/scripts/code-quality-verdict.ts +60 -68
- package/.claude/skills/mutagent-evaluator/scripts/config/schema.ts +3 -3
- package/.claude/skills/mutagent-evaluator/scripts/contracts/agentspec-evals.ts +101 -39
- package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-matrix.ts +326 -3
- package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-types.ts +30 -4
- package/.claude/skills/mutagent-evaluator/scripts/determine-outcome.ts +8 -0
- package/.claude/skills/mutagent-evaluator/scripts/edd/change-request.ts +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/edd/edd-types.ts +2 -2
- package/.claude/skills/mutagent-evaluator/scripts/judge-prompt-template.ts +144 -15
- package/.claude/skills/mutagent-evaluator/scripts/materialize-dataset.ts +130 -68
- package/.claude/skills/mutagent-evaluator/scripts/matrix-judge.ts +219 -1
- package/.claude/skills/mutagent-evaluator/scripts/memory/append.ts +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/memory/ratify.ts +165 -0
- package/.claude/skills/mutagent-evaluator/scripts/merge-criteria.ts +849 -0
- package/.claude/skills/mutagent-evaluator/scripts/prep-tasks.ts +15 -4
- package/.claude/skills/mutagent-evaluator/scripts/read-manifest.ts +120 -0
- package/.claude/skills/mutagent-evaluator/scripts/read-unitf-traces.ts +34 -2
- package/.claude/skills/mutagent-evaluator/scripts/render-build-cards.ts +37 -0
- package/.claude/skills/mutagent-evaluator/scripts/render-discover-report-v3.ts +1155 -0
- package/.claude/skills/mutagent-evaluator/scripts/render-eval-report-v3.ts +610 -0
- package/.claude/skills/mutagent-evaluator/scripts/render-eval-report.ts +11 -92
- package/.claude/skills/mutagent-evaluator/scripts/render-review-report-v3.ts +691 -0
- package/.claude/skills/mutagent-evaluator/scripts/report-fragments.ts +173 -0
- package/.claude/skills/mutagent-evaluator/scripts/route-failures.ts +12 -9
- package/.claude/skills/mutagent-evaluator/scripts/run-evaluate.ts +46 -1
- package/.claude/skills/mutagent-evaluator/scripts/run-pipeline.ts +39 -4
- package/.claude/skills/mutagent-evaluator/scripts/run-review.ts +266 -0
- package/.claude/skills/mutagent-evaluator/scripts/subject-profile.ts +82 -0
- package/.claude/skills/mutagent-evaluator/scripts/sync-eval-criteria.ts +2 -2
- package/.claude/skills/mutagent-evaluator/scripts/unitf-to-evaltrace.ts +64 -21
- package/.claude/skills/mutagent-evaluator/scripts/verify-render.ts +728 -0
- package/bin/mutagent-cli.mjs +9 -5
- 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
|
+
}
|
|
@@ -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
|
+
}
|