@mutagent/evaluator 0.1.0-alpha.2

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 (126) hide show
  1. package/.claude/skills/mutagent-evaluator/SKILL.md +538 -0
  2. package/.claude/skills/mutagent-evaluator/assets/agents/audit-executor.md +169 -0
  3. package/.claude/skills/mutagent-evaluator/assets/agents/dataset-builder.md +160 -0
  4. package/.claude/skills/mutagent-evaluator/assets/agents/discovery.md +425 -0
  5. package/.claude/skills/mutagent-evaluator/assets/agents/evaluator.md +1044 -0
  6. package/.claude/skills/mutagent-evaluator/assets/brand/theme.css +213 -0
  7. package/.claude/skills/mutagent-evaluator/assets/brand/wordmark.html +9 -0
  8. package/.claude/skills/mutagent-evaluator/lenses/context-flow-lens.md +85 -0
  9. package/.claude/skills/mutagent-evaluator/lenses/data-lens.md +47 -0
  10. package/.claude/skills/mutagent-evaluator/lenses/decision-lens.md +48 -0
  11. package/.claude/skills/mutagent-evaluator/lenses/methodology-critic-lens.md +53 -0
  12. package/.claude/skills/mutagent-evaluator/lenses/trajectory-lens.md +44 -0
  13. package/.claude/skills/mutagent-evaluator/references/build-review-interface.md +83 -0
  14. package/.claude/skills/mutagent-evaluator/references/edd-loop.md +134 -0
  15. package/.claude/skills/mutagent-evaluator/references/error-analysis.md +113 -0
  16. package/.claude/skills/mutagent-evaluator/references/eval-audit.md +154 -0
  17. package/.claude/skills/mutagent-evaluator/references/eval-stage.md +168 -0
  18. package/.claude/skills/mutagent-evaluator/references/generate-synthetic-data.md +81 -0
  19. package/.claude/skills/mutagent-evaluator/references/grounded-adjudication.md +221 -0
  20. package/.claude/skills/mutagent-evaluator/references/memory-format.md +65 -0
  21. package/.claude/skills/mutagent-evaluator/references/methodology.md +201 -0
  22. package/.claude/skills/mutagent-evaluator/references/operation-inventory.md +196 -0
  23. package/.claude/skills/mutagent-evaluator/references/validate-evaluator.md +125 -0
  24. package/.claude/skills/mutagent-evaluator/references/workflows/orchestrator-protocol.md +287 -0
  25. package/.claude/skills/mutagent-evaluator/references/write-judge-prompt.md +123 -0
  26. package/.claude/skills/mutagent-evaluator/schemas/behavior-tree.schema.yaml +73 -0
  27. package/.claude/skills/mutagent-evaluator/schemas/dataset.schema.yaml +66 -0
  28. package/.claude/skills/mutagent-evaluator/schemas/edd-change-request.schema.yaml +114 -0
  29. package/.claude/skills/mutagent-evaluator/schemas/eval-matrix.schema.yaml +74 -0
  30. package/.claude/skills/mutagent-evaluator/schemas/flow-graph.schema.yaml +69 -0
  31. package/.claude/skills/mutagent-evaluator/schemas/flow-profile.schema.yaml +49 -0
  32. package/.claude/skills/mutagent-evaluator/schemas/methodology-review.schema.yaml +40 -0
  33. package/.claude/skills/mutagent-evaluator/schemas/scorecard.schema.yaml +85 -0
  34. package/.claude/skills/mutagent-evaluator/scripts/agent-dispatch.ts +0 -0
  35. package/.claude/skills/mutagent-evaluator/scripts/aggregate-discover.ts +543 -0
  36. package/.claude/skills/mutagent-evaluator/scripts/artifact-paths.ts +99 -0
  37. package/.claude/skills/mutagent-evaluator/scripts/assemble-scorecard.ts +172 -0
  38. package/.claude/skills/mutagent-evaluator/scripts/build-dataset.ts +186 -0
  39. package/.claude/skills/mutagent-evaluator/scripts/build-evals.ts +93 -0
  40. package/.claude/skills/mutagent-evaluator/scripts/build-review-ui.ts +393 -0
  41. package/.claude/skills/mutagent-evaluator/scripts/check-method-router.ts +170 -0
  42. package/.claude/skills/mutagent-evaluator/scripts/cli/aggregate.ts +112 -0
  43. package/.claude/skills/mutagent-evaluator/scripts/cli/audit-run.ts +175 -0
  44. package/.claude/skills/mutagent-evaluator/scripts/cli/doctor.ts +211 -0
  45. package/.claude/skills/mutagent-evaluator/scripts/cli/dogfood.ts +133 -0
  46. package/.claude/skills/mutagent-evaluator/scripts/cli/init.ts +601 -0
  47. package/.claude/skills/mutagent-evaluator/scripts/cli/methodology-review.ts +122 -0
  48. package/.claude/skills/mutagent-evaluator/scripts/cli/prep.ts +279 -0
  49. package/.claude/skills/mutagent-evaluator/scripts/cli/profile-subject.ts +165 -0
  50. package/.claude/skills/mutagent-evaluator/scripts/cli/run.sh +56 -0
  51. package/.claude/skills/mutagent-evaluator/scripts/cli/variance-check.ts +105 -0
  52. package/.claude/skills/mutagent-evaluator/scripts/code-eval.ts +248 -0
  53. package/.claude/skills/mutagent-evaluator/scripts/codegen-evals.ts +173 -0
  54. package/.claude/skills/mutagent-evaluator/scripts/cold-start-project.ts +151 -0
  55. package/.claude/skills/mutagent-evaluator/scripts/cold-start-sampler.ts +267 -0
  56. package/.claude/skills/mutagent-evaluator/scripts/config/load.ts +307 -0
  57. package/.claude/skills/mutagent-evaluator/scripts/config/schema.ts +325 -0
  58. package/.claude/skills/mutagent-evaluator/scripts/contracts/agentspec-evals.ts +85 -0
  59. package/.claude/skills/mutagent-evaluator/scripts/contracts/dataset.ts +149 -0
  60. package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-engine.ts +118 -0
  61. package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-matrix.ts +577 -0
  62. package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-types.ts +1074 -0
  63. package/.claude/skills/mutagent-evaluator/scripts/contracts/flow-graph.ts +194 -0
  64. package/.claude/skills/mutagent-evaluator/scripts/contracts/types.ts +303 -0
  65. package/.claude/skills/mutagent-evaluator/scripts/contracts/validation.ts +193 -0
  66. package/.claude/skills/mutagent-evaluator/scripts/derive-dataset.ts +152 -0
  67. package/.claude/skills/mutagent-evaluator/scripts/determine-outcome.ts +219 -0
  68. package/.claude/skills/mutagent-evaluator/scripts/diff-discriminate.ts +160 -0
  69. package/.claude/skills/mutagent-evaluator/scripts/discover-criteria.ts +348 -0
  70. package/.claude/skills/mutagent-evaluator/scripts/edd/change-request.ts +232 -0
  71. package/.claude/skills/mutagent-evaluator/scripts/edd/edd-types.ts +210 -0
  72. package/.claude/skills/mutagent-evaluator/scripts/edd/variance-gate.ts +186 -0
  73. package/.claude/skills/mutagent-evaluator/scripts/emit-completeness.ts +111 -0
  74. package/.claude/skills/mutagent-evaluator/scripts/eval-engine.ts +160 -0
  75. package/.claude/skills/mutagent-evaluator/scripts/evaluate.ts +333 -0
  76. package/.claude/skills/mutagent-evaluator/scripts/flow-graph.ts +230 -0
  77. package/.claude/skills/mutagent-evaluator/scripts/judge-prompt-template.ts +253 -0
  78. package/.claude/skills/mutagent-evaluator/scripts/judge-provider.ts +135 -0
  79. package/.claude/skills/mutagent-evaluator/scripts/lint-grounding.ts +229 -0
  80. package/.claude/skills/mutagent-evaluator/scripts/lint-uniformity.ts +168 -0
  81. package/.claude/skills/mutagent-evaluator/scripts/living-suite.ts +109 -0
  82. package/.claude/skills/mutagent-evaluator/scripts/load-bundle.ts +203 -0
  83. package/.claude/skills/mutagent-evaluator/scripts/load-profile-vocab.ts +64 -0
  84. package/.claude/skills/mutagent-evaluator/scripts/load-profile.ts +106 -0
  85. package/.claude/skills/mutagent-evaluator/scripts/mask.ts +138 -0
  86. package/.claude/skills/mutagent-evaluator/scripts/materialize-dataset.ts +113 -0
  87. package/.claude/skills/mutagent-evaluator/scripts/matrix-judge.ts +750 -0
  88. package/.claude/skills/mutagent-evaluator/scripts/memory/append.ts +215 -0
  89. package/.claude/skills/mutagent-evaluator/scripts/memory/read.ts +168 -0
  90. package/.claude/skills/mutagent-evaluator/scripts/prep-tasks.ts +125 -0
  91. package/.claude/skills/mutagent-evaluator/scripts/profile-subject.ts +310 -0
  92. package/.claude/skills/mutagent-evaluator/scripts/publish-report.ts +131 -0
  93. package/.claude/skills/mutagent-evaluator/scripts/read-unitf-traces.ts +81 -0
  94. package/.claude/skills/mutagent-evaluator/scripts/render-build-cards.ts +195 -0
  95. package/.claude/skills/mutagent-evaluator/scripts/render-discover-report.ts +1640 -0
  96. package/.claude/skills/mutagent-evaluator/scripts/render-eval-report.ts +3823 -0
  97. package/.claude/skills/mutagent-evaluator/scripts/render-report.ts +212 -0
  98. package/.claude/skills/mutagent-evaluator/scripts/resolve-credential.ts +110 -0
  99. package/.claude/skills/mutagent-evaluator/scripts/resolve-ref.ts +98 -0
  100. package/.claude/skills/mutagent-evaluator/scripts/result-verify.ts +129 -0
  101. package/.claude/skills/mutagent-evaluator/scripts/route-failures.ts +320 -0
  102. package/.claude/skills/mutagent-evaluator/scripts/run-deterministic.ts +271 -0
  103. package/.claude/skills/mutagent-evaluator/scripts/run-evaluate.ts +715 -0
  104. package/.claude/skills/mutagent-evaluator/scripts/run-judge.ts +155 -0
  105. package/.claude/skills/mutagent-evaluator/scripts/run-pipeline.ts +175 -0
  106. package/.claude/skills/mutagent-evaluator/scripts/sample-traces.ts +210 -0
  107. package/.claude/skills/mutagent-evaluator/scripts/self-audit.ts +387 -0
  108. package/.claude/skills/mutagent-evaluator/scripts/source-map.ts +106 -0
  109. package/.claude/skills/mutagent-evaluator/scripts/subject-profile.ts +134 -0
  110. package/.claude/skills/mutagent-evaluator/scripts/substrate.ts +162 -0
  111. package/.claude/skills/mutagent-evaluator/scripts/ui-slots.ts +119 -0
  112. package/.claude/skills/mutagent-evaluator/scripts/unitf-to-evaltrace.ts +284 -0
  113. package/.claude/skills/mutagent-evaluator/scripts/validate-judge.ts +358 -0
  114. package/.claude/skills/mutagent-evaluator/scripts/variance-compare.ts +177 -0
  115. package/.claude/skills/mutagent-evaluator/subjects/mutagent-diagnostics/behavior-tree.yaml +140 -0
  116. package/.claude/skills/mutagent-evaluator/subjects/mutagent-diagnostics/eval-matrix.yaml +1270 -0
  117. package/.claude/skills/mutagent-evaluator/subjects/mutagent-diagnostics/methodology-review.yaml +105 -0
  118. package/.claude/skills/mutagent-evaluator/workflows/audit.workflow.js +82 -0
  119. package/.claude/skills/mutagent-evaluator/workflows/data-leak.workflow.js +236 -0
  120. package/.claude/skills/mutagent-evaluator/workflows/variance.workflow.js +163 -0
  121. package/LICENSE +201 -0
  122. package/NOTICE +23 -0
  123. package/README.md +90 -0
  124. package/bin/mutagent-cli.mjs +9263 -0
  125. package/bin/mutagent-evaluator.mjs +96 -0
  126. package/package.json +52 -0
@@ -0,0 +1,358 @@
1
+ /**
2
+ * scripts/validate-judge.ts — EV-044 `*validate` (Type A — Code-only, pure math).
3
+ * ---------------------------------------------------------------------------
4
+ * Calibrate a judge against human labels (the rubric is `references/validate-
5
+ * evaluator.md`; this is the deterministic STATS that implement it):
6
+ * - 2×2 confusion matrix → TPR / TNR (NOT raw accuracy — misleading under
7
+ * class imbalance);
8
+ * - split-disjointness + TEST-ONCE guards (dev for iteration, test measured
9
+ * exactly once — looking at test then iterating is data leakage);
10
+ * - Rogan-Gladen bias correction θ = (p_obs + TNR − 1) / (TPR + TNR − 1)
11
+ * with clip to [0,1] and `invalid` when the denominator ≈ 0 (judge no better
12
+ * than random);
13
+ * - a DETERMINISTIC bootstrap CI (seeded LCG resample — NO Math.random, so the
14
+ * interval is byte-identical across reruns, C-PIN-adjacent);
15
+ * - graceful degradation: a criterion with < MIN_LABELS human labels (or TPR/
16
+ * TNR below target) stays `unvalidated` and is reported BIAS-CORRECTED, never
17
+ * blocking — the autonomous loop has no human to wait on.
18
+ *
19
+ * Austerity: NO judge prompt, NO LLM — this PREPs/AGGREGATEs the numbers; the
20
+ * verdicts it consumes come from the dispatched eval-judge (read back from
21
+ * verdict files). The pinned judge model is recorded on the result (a model
22
+ * change invalidates the numbers — re-validate). PURE — no clock/random/network.
23
+ */
24
+ import { OutcomeVerdict, type OutcomeVerdictValue } from "./contracts/eval-types.ts";
25
+ import {
26
+ HumanVerdict,
27
+ ValidationStatus,
28
+ type ConfidenceInterval,
29
+ type ConfusionMatrix,
30
+ type HumanLabel,
31
+ type ValidationResult,
32
+ } from "./contracts/validation.ts";
33
+
34
+ // ── calibration thresholds (validate-evaluator.md) ───────────────────────────
35
+ /** Below this many labels the CIs get wide → the judge stays `unvalidated`. */
36
+ export const MIN_LABELS = 60;
37
+ /** Target TPR/TNR (validate-evaluator.md step 5); below → `unvalidated`. */
38
+ export const TARGET_TPR = 0.9;
39
+ export const TARGET_TNR = 0.9;
40
+ /** Rogan-Gladen denominator floor — below this the judge is ≈ random → invalid. */
41
+ export const RG_DENOM_MIN = 1e-6;
42
+
43
+ // ── binary class projection (defer / uncertain are excluded) ─────────────────
44
+ type BinaryClass = "pass" | "fail";
45
+
46
+ /** Project a human verdict to the binary class, or null for `defer`. */
47
+ function humanBinary(v: HumanLabel["label"]): BinaryClass | null {
48
+ if (v === HumanVerdict.Pass) return "pass";
49
+ if (v === HumanVerdict.Fail) return "fail";
50
+ return null; // defer — no ground truth
51
+ }
52
+
53
+ /** Project a judge verdict to the binary class, or null for `uncertain`. */
54
+ function judgeBinary(v: OutcomeVerdictValue): BinaryClass | null {
55
+ if (v === OutcomeVerdict.Pass) return "pass";
56
+ if (v === OutcomeVerdict.Fail) return "fail";
57
+ return null; // uncertain — excluded from confusion math
58
+ }
59
+
60
+ /** One judge prediction for a trace (read back from a verdict file). */
61
+ export interface JudgePred {
62
+ traceId: string;
63
+ result: OutcomeVerdictValue;
64
+ }
65
+
66
+ /** A paired human/judge binary judgment for one trace (defer/uncertain dropped). */
67
+ export interface LabeledPair {
68
+ traceId: string;
69
+ human: BinaryClass;
70
+ judge: BinaryClass;
71
+ }
72
+
73
+ // ── split-disjointness + test-once guards ────────────────────────────────────
74
+
75
+ /**
76
+ * Assert no traceId is assigned to more than one split (train/dev/test must
77
+ * partition cleanly — an overlap is leakage). THROWS on a conflict. PURE.
78
+ */
79
+ export function assertSplitsDisjoint(labels: HumanLabel[]): void {
80
+ const splitOf = new Map<string, string>();
81
+ for (const l of labels) {
82
+ if (l.split === undefined) continue;
83
+ const prior = splitOf.get(l.traceId);
84
+ if (prior !== undefined && prior !== l.split) {
85
+ throw new Error(
86
+ `assertSplitsDisjoint: trace '${l.traceId}' is in both '${prior}' and '${l.split}' ` +
87
+ "splits — train/dev/test must be disjoint (data leakage).",
88
+ );
89
+ }
90
+ splitOf.set(l.traceId, l.split);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Enforce the test-once rule: the held-out test split is measured EXACTLY once.
96
+ * THROWS if a prior test measurement is already recorded. The caller persists
97
+ * the flag in run state; this guards the transition. PURE.
98
+ */
99
+ export function assertTestUsedOnce(testAlreadyMeasured: boolean): void {
100
+ if (testAlreadyMeasured) {
101
+ throw new Error(
102
+ "assertTestUsedOnce: the test split was already measured — re-running on test after " +
103
+ "seeing results is data leakage (validate-evaluator.md step 6). Go back to dev.",
104
+ );
105
+ }
106
+ }
107
+
108
+ // ── pairing + confusion matrix ───────────────────────────────────────────────
109
+
110
+ /**
111
+ * Pair human labels with judge predictions by traceId, dropping `defer` /
112
+ * `uncertain` (no ground truth / no judge call). Optionally restrict to one
113
+ * split. DETERMINISTIC (ordered by traceId). PURE.
114
+ */
115
+ export function pairLabels(
116
+ human: HumanLabel[],
117
+ judge: JudgePred[],
118
+ split?: "train" | "dev" | "test",
119
+ ): LabeledPair[] {
120
+ const judgeById = new Map(judge.map((j) => [j.traceId, j.result]));
121
+ const out: LabeledPair[] = [];
122
+ for (const l of human) {
123
+ if (split !== undefined && l.split !== split) continue;
124
+ const h = humanBinary(l.label);
125
+ if (h === null) continue;
126
+ const jr = judgeById.get(l.traceId);
127
+ if (jr === undefined) continue;
128
+ const j = judgeBinary(jr);
129
+ if (j === null) continue;
130
+ out.push({ traceId: l.traceId, human: h, judge: j });
131
+ }
132
+ return out.sort((a, b) => a.traceId.localeCompare(b.traceId));
133
+ }
134
+
135
+ /** Build a 2×2 confusion matrix from paired judgments (Pass = positive). PURE. */
136
+ export function confusionMatrix(pairs: LabeledPair[]): ConfusionMatrix {
137
+ const cm: ConfusionMatrix = { tp: 0, fp: 0, tn: 0, fn: 0 };
138
+ for (const p of pairs) {
139
+ if (p.human === "pass" && p.judge === "pass") cm.tp++;
140
+ else if (p.human === "fail" && p.judge === "pass") cm.fp++;
141
+ else if (p.human === "fail" && p.judge === "fail") cm.tn++;
142
+ else cm.fn++; // human pass, judge fail
143
+ }
144
+ return cm;
145
+ }
146
+
147
+ /** TPR = tp / (tp + fn); null when there are no human-Pass examples. PURE. */
148
+ export function tprOf(cm: ConfusionMatrix): number | null {
149
+ const denom = cm.tp + cm.fn;
150
+ return denom === 0 ? null : cm.tp / denom;
151
+ }
152
+
153
+ /** TNR = tn / (tn + fp); null when there are no human-Fail examples. PURE. */
154
+ export function tnrOf(cm: ConfusionMatrix): number | null {
155
+ const denom = cm.tn + cm.fp;
156
+ return denom === 0 ? null : cm.tn / denom;
157
+ }
158
+
159
+ // ── Rogan-Gladen bias correction ─────────────────────────────────────────────
160
+
161
+ function clip01(x: number): number {
162
+ return x < 0 ? 0 : x > 1 ? 1 : x;
163
+ }
164
+
165
+ export interface RoganGladen {
166
+ /** the bias-corrected true rate, clipped to [0,1]; null when invalid. */
167
+ corrected: number | null;
168
+ /** false when TPR+TNR−1 ≈ 0 (judge no better than random → uncorrectable). */
169
+ valid: boolean;
170
+ }
171
+
172
+ /**
173
+ * Rogan-Gladen correction: θ = (p_obs + TNR − 1) / (TPR + TNR − 1). Clipped to
174
+ * [0,1]. INVALID (corrected=null) when the denominator's magnitude is below
175
+ * `denomMin` — the judge is no better than random, so the raw rate cannot be
176
+ * de-biased. PURE.
177
+ */
178
+ export function roganGladen(
179
+ pObs: number,
180
+ tpr: number,
181
+ tnr: number,
182
+ denomMin = RG_DENOM_MIN,
183
+ ): RoganGladen {
184
+ const denom = tpr + tnr - 1;
185
+ if (Math.abs(denom) < denomMin) return { corrected: null, valid: false };
186
+ return { corrected: clip01((pObs + tnr - 1) / denom), valid: true };
187
+ }
188
+
189
+ /** Observed judge Pass-rate over unlabeled predictions (p_obs); null if none. PURE. */
190
+ export function observedPassRate(preds: JudgePred[]): number | null {
191
+ let pass = 0;
192
+ let total = 0;
193
+ for (const p of preds) {
194
+ const b = judgeBinary(p.result);
195
+ if (b === null) continue;
196
+ total++;
197
+ if (b === "pass") pass++;
198
+ }
199
+ return total === 0 ? null : pass / total;
200
+ }
201
+
202
+ // ── deterministic bootstrap CI (seeded LCG — no Math.random) ─────────────────
203
+
204
+ /** A small seeded LCG → [0,1). Deterministic; NEVER Math.random (byte-identity). */
205
+ function lcg(seed: number): () => number {
206
+ let s = seed >>> 0;
207
+ return () => {
208
+ // Numerical Recipes LCG constants.
209
+ s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
210
+ return s / 0x100000000;
211
+ };
212
+ }
213
+
214
+ /** Percentile of an ASCENDING-sorted array (linear interpolation). PURE. */
215
+ export function percentile(sortedAsc: number[], p: number): number {
216
+ if (sortedAsc.length === 0) return NaN;
217
+ if (sortedAsc.length === 1) return sortedAsc[0];
218
+ const idx = clip01(p) * (sortedAsc.length - 1);
219
+ const lo = Math.floor(idx);
220
+ const hi = Math.ceil(idx);
221
+ if (lo === hi) return sortedAsc[lo];
222
+ return sortedAsc[lo] + (sortedAsc[hi] - sortedAsc[lo]) * (idx - lo);
223
+ }
224
+
225
+ export interface BootstrapOptions {
226
+ iterations?: number;
227
+ /** 2-sided level (default 0.95). */
228
+ level?: number;
229
+ /** LCG seed — fixed by default so the CI is reproducible (C-PIN). */
230
+ seed?: number;
231
+ }
232
+
233
+ /**
234
+ * Bootstrap a CI for the Rogan-Gladen corrected rate: resample the labeled pairs
235
+ * (with replacement, deterministic LCG) `iterations` times, recompute TPR/TNR/θ
236
+ * per resample, and take the level-tailed percentiles of the valid θ values.
237
+ * Returns undefined when too few valid resamples exist. `pObs` is fixed (the
238
+ * production observation), only the calibration sample is resampled. PURE.
239
+ */
240
+ export function bootstrapCorrectedCI(
241
+ pairs: LabeledPair[],
242
+ pObs: number,
243
+ opts: BootstrapOptions = {},
244
+ ): ConfidenceInterval | undefined {
245
+ const iterations = opts.iterations ?? 1000;
246
+ const level = opts.level ?? 0.95;
247
+ const n = pairs.length;
248
+ if (n < 2) return undefined;
249
+ const rand = lcg(opts.seed ?? 0x2545f491);
250
+ const thetas: number[] = [];
251
+ for (let it = 0; it < iterations; it++) {
252
+ const cm: ConfusionMatrix = { tp: 0, fp: 0, tn: 0, fn: 0 };
253
+ for (let k = 0; k < n; k++) {
254
+ const p = pairs[Math.floor(rand() * n)];
255
+ if (p.human === "pass" && p.judge === "pass") cm.tp++;
256
+ else if (p.human === "fail" && p.judge === "pass") cm.fp++;
257
+ else if (p.human === "fail" && p.judge === "fail") cm.tn++;
258
+ else cm.fn++;
259
+ }
260
+ const tpr = tprOf(cm);
261
+ const tnr = tnrOf(cm);
262
+ if (tpr === null || tnr === null) continue;
263
+ const rg = roganGladen(pObs, tpr, tnr);
264
+ if (rg.corrected !== null) thetas.push(rg.corrected);
265
+ }
266
+ if (thetas.length < 2) return undefined;
267
+ thetas.sort((a, b) => a - b);
268
+ const tail = (1 - level) / 2;
269
+ return { lo: percentile(thetas, tail), hi: percentile(thetas, 1 - tail), level };
270
+ }
271
+
272
+ // ── top-level orchestration ──────────────────────────────────────────────────
273
+
274
+ export interface ValidateJudgeInput {
275
+ criterionId: string;
276
+ /** the PINNED judge model these numbers belong to (required — C-PIN). */
277
+ judgeModel: string;
278
+ /** human labels (with split tags). */
279
+ humanLabels: HumanLabel[];
280
+ /** judge predictions on the labeled set (read back from verdict files). */
281
+ judgeVerdicts: JudgePred[];
282
+ /** judge predictions on UNLABELED production traces (for p_obs). */
283
+ unlabeledVerdicts?: JudgePred[];
284
+ /** has the test split already been measured? (test-once guard). */
285
+ testAlreadyMeasured?: boolean;
286
+ /** override the min-labels gate (default MIN_LABELS). */
287
+ minLabels?: number;
288
+ }
289
+
290
+ /**
291
+ * Validate ONE judge against human labels → a `ValidationResult`. Picks the
292
+ * final-measurement split (test if present — enforcing test-once — else dev,
293
+ * else all labeled), computes TPR/TNR, the Rogan-Gladen corrected rate from the
294
+ * unlabeled p_obs, and a bootstrap CI. DEGRADES GRACEFULLY: < minLabels labels or
295
+ * sub-target TPR/TNR → `unvalidated` (reported bias-corrected, never blocking).
296
+ * THROWS only on a contract breach (missing pinned model, split overlap,
297
+ * test-reuse). PURE.
298
+ */
299
+ export function validateJudge(input: ValidateJudgeInput): ValidationResult {
300
+ if (input.judgeModel.length === 0) {
301
+ throw new Error("validateJudge: judgeModel is required (C-PIN — the numbers are model-specific).");
302
+ }
303
+ assertSplitsDisjoint(input.humanLabels);
304
+ const minLabels = input.minLabels ?? MIN_LABELS;
305
+
306
+ // choose the final-measurement split: test (once) > dev > all.
307
+ const hasTest = input.humanLabels.some((l) => l.split === "test");
308
+ const hasDev = input.humanLabels.some((l) => l.split === "dev");
309
+ let split: "dev" | "test" | undefined;
310
+ if (hasTest) {
311
+ assertTestUsedOnce(input.testAlreadyMeasured ?? false);
312
+ split = "test";
313
+ } else if (hasDev) {
314
+ split = "dev";
315
+ } else {
316
+ split = undefined; // no split tags → use all labeled
317
+ }
318
+
319
+ const pairs = pairLabels(input.humanLabels, input.judgeVerdicts, split);
320
+ const cm = confusionMatrix(pairs);
321
+ const tpr = tprOf(cm);
322
+ const tnr = tnrOf(cm);
323
+ const labelCount = pairs.length;
324
+
325
+ const pObs = observedPassRate(input.unlabeledVerdicts ?? []);
326
+ const rg =
327
+ tpr !== null && tnr !== null && pObs !== null
328
+ ? roganGladen(pObs, tpr, tnr)
329
+ : { corrected: null, valid: false };
330
+ const ci =
331
+ pObs !== null && rg.valid ? bootstrapCorrectedCI(pairs, pObs) : undefined;
332
+
333
+ // status + note (graceful degradation).
334
+ const reasons: string[] = [];
335
+ if (labelCount < minLabels) reasons.push(`only ${labelCount} labels (< ${minLabels})`);
336
+ if (tpr === null) reasons.push("no human-Pass examples (TPR undefined)");
337
+ else if (tpr < TARGET_TPR) reasons.push(`TPR ${tpr.toFixed(2)} < ${TARGET_TPR}`);
338
+ if (tnr === null) reasons.push("no human-Fail examples (TNR undefined)");
339
+ else if (tnr < TARGET_TNR) reasons.push(`TNR ${tnr.toFixed(2)} < ${TARGET_TNR}`);
340
+ const status = reasons.length === 0 ? ValidationStatus.Validated : ValidationStatus.Unvalidated;
341
+
342
+ const result: ValidationResult = {
343
+ criterionId: input.criterionId,
344
+ judgeModel: input.judgeModel,
345
+ status,
346
+ labelCount,
347
+ tpr,
348
+ tnr,
349
+ observedPassRate: pObs,
350
+ correctedRate: rg.corrected,
351
+ correctionValid: rg.valid,
352
+ note:
353
+ status === ValidationStatus.Validated
354
+ ? `validated on ${labelCount} labels (split: ${split ?? "all"})`
355
+ : `unvalidated — ${reasons.join("; ")}. Reporting bias-corrected, not raw; loop not blocked.`,
356
+ };
357
+ return ci === undefined ? result : { ...result, ci };
358
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * scripts/variance-compare.ts
3
+ * ---------------------------------------------------------------------------
4
+ * The variant comparator — Track-2 TREND (R7, decisions #9/#12). A COORDINATOR
5
+ * (a role distinct from the audit executors) compares TWO run bundles on the
6
+ * fixed 15-dimension determinism scorecard. The byte-identity masking contract
7
+ * (mask.ts) is applied first: "byte-identical across runs" is only testable
8
+ * AFTER masking the declared injected fields (runId / timestamps / abs-paths).
9
+ *
10
+ * Each dimension has a Measure and a per-Phase Target. A dimension is:
11
+ * - identical : masked values byte-identical
12
+ * - within-target : differ but inside the phase target
13
+ * - diverged : differ beyond target (counts toward varianceScore)
14
+ * - not-evaluated : the inputs needed for this dimension were absent
15
+ *
16
+ * Pure + deterministic: same two bundles -> same trend, always. No clock/random.
17
+ */
18
+ import { type RunBundle, type TrendDimension } from "./contracts/types.ts";
19
+ import { maskedCanonicalJson } from "./mask.ts";
20
+
21
+ /**
22
+ * The FIXED 15 determinism dimensions (manual §11.1). Each carries its Measure
23
+ * and a default target. `extract` pulls the comparable value from a bundle; when
24
+ * it returns undefined the dimension is not-evaluated for that pair.
25
+ */
26
+ export interface VarianceDimensionSpec {
27
+ readonly name: string;
28
+ readonly measure: string;
29
+ readonly target: string;
30
+ readonly extract: (bundle: RunBundle) => unknown;
31
+ }
32
+
33
+ function path(obj: unknown, ...keys: string[]): unknown {
34
+ let cur: unknown = obj;
35
+ for (const k of keys) {
36
+ if (cur == null || typeof cur !== "object") return undefined;
37
+ cur = (cur as Record<string, unknown>)[k];
38
+ }
39
+ return cur;
40
+ }
41
+
42
+ export const VARIANCE_DIMENSIONS: readonly VarianceDimensionSpec[] = [
43
+ {
44
+ name: "headline-latency-p50",
45
+ measure: "primary-signal latency p50 magnitude",
46
+ target: "identical magnitude (no 10s<->54s flip)",
47
+ extract: (b) => path(b.data.renderInput, "bigStat", "p50"),
48
+ },
49
+ {
50
+ name: "render-crashes",
51
+ measure: "count of post-gate render TypeErrors",
52
+ target: "0",
53
+ extract: (b) => path(b.data.runMeta, "renderCrashes") ?? 0,
54
+ },
55
+ {
56
+ name: "gate-fail-loud-completeness",
57
+ measure: "all missing fields surfaced in ONE error",
58
+ target: "single-pass",
59
+ extract: (b) => path(b.data.runMeta, "gateFailMode"),
60
+ },
61
+ {
62
+ name: "analyzer-count",
63
+ measure: "(n, analyzerCount) dispatch sizing",
64
+ target: "identical across runs/machines",
65
+ extract: (b) => path(b.data.runMeta, "dispatch", "analyzerCount"),
66
+ },
67
+ {
68
+ name: "primary-signal",
69
+ measure: "runMeta.primarySignal.name",
70
+ target: "identical",
71
+ extract: (b) => path(b.data.runMeta, "primarySignal", "name"),
72
+ },
73
+ {
74
+ name: "finding-id-set",
75
+ measure: "set of findingIds after dedup+cluster",
76
+ target: "identical set",
77
+ extract: (b) => path(b.data.renderInput, "findingIds"),
78
+ },
79
+ {
80
+ name: "remedy-ranking",
81
+ measure: "star-recommended remedy id ordering",
82
+ target: "identical",
83
+ extract: (b) => path(b.data.renderInput, "recommendedRemedyId"),
84
+ },
85
+ {
86
+ name: "slice-plan",
87
+ measure: "SlicePlan trace assignment",
88
+ target: "byte-identical",
89
+ extract: (b) => path(b.data.runMeta, "slicePlan"),
90
+ },
91
+ {
92
+ name: "sampling-buckets",
93
+ measure: "representative sampler buckets",
94
+ target: "byte-identical",
95
+ extract: (b) => path(b.data.runMeta, "sampling", "buckets"),
96
+ },
97
+ {
98
+ name: "coverage-confidence",
99
+ measure: "coverageConfidence value",
100
+ target: "identical",
101
+ extract: (b) => path(b.data.runMeta, "coverageConfidence"),
102
+ },
103
+ {
104
+ name: "tier0-signals",
105
+ measure: "Tier0Report signal set",
106
+ target: "identical",
107
+ extract: (b) => path(b.data.runMeta, "tier0", "signals"),
108
+ },
109
+ {
110
+ name: "entity-identity",
111
+ measure: "diagnosedEntity name",
112
+ target: "identical",
113
+ extract: (b) => path(b.data.entityContext, "diagnosedEntity"),
114
+ },
115
+ {
116
+ name: "caps-firstToTrip",
117
+ measure: "enforceCaps firstToTrip",
118
+ target: "identical",
119
+ extract: (b) => path(b.data.runMeta, "caps", "firstToTrip"),
120
+ },
121
+ {
122
+ name: "awareness-fire",
123
+ measure: "awareness fired vs skipped decision",
124
+ target: "identical",
125
+ extract: (b) => path(b.data.runMeta, "awareness", "fired"),
126
+ },
127
+ {
128
+ name: "heatmap-cells",
129
+ measure: "24-cell hourly heatmap",
130
+ target: "byte-identical after generatedAt mask",
131
+ extract: (b) => path(b.data.renderInput, "hourlyHeatmap"),
132
+ },
133
+ ];
134
+
135
+ export interface VarianceCompareResult {
136
+ dimensions: TrendDimension[];
137
+ varianceScore: number;
138
+ runPair: { a: string; b: string };
139
+ }
140
+
141
+ /**
142
+ * Compare two bundles across the 15 dimensions. Each value is masked + canonical-
143
+ * serialized before comparison so injected fields never count as divergence.
144
+ */
145
+ export function varianceCompare(
146
+ a: RunBundle,
147
+ b: RunBundle,
148
+ ): VarianceCompareResult {
149
+ const dimensions: TrendDimension[] = VARIANCE_DIMENSIONS.map((spec) => {
150
+ const va = spec.extract(a);
151
+ const vb = spec.extract(b);
152
+ let divergence: TrendDimension["divergence"];
153
+ if (va === undefined && vb === undefined) {
154
+ divergence = "not-evaluated";
155
+ } else {
156
+ const ma = maskedCanonicalJson(va ?? null);
157
+ const mb = maskedCanonicalJson(vb ?? null);
158
+ divergence = ma === mb ? "identical" : "diverged";
159
+ }
160
+ return {
161
+ name: spec.name,
162
+ measure: spec.measure,
163
+ target: spec.target,
164
+ divergence,
165
+ };
166
+ });
167
+
168
+ const varianceScore = dimensions.filter(
169
+ (d) => d.divergence === "diverged",
170
+ ).length;
171
+
172
+ return {
173
+ dimensions,
174
+ varianceScore,
175
+ runPair: { a: a.runId, b: b.runId },
176
+ };
177
+ }
@@ -0,0 +1,140 @@
1
+ # subjects/mutagent-diagnostics/behavior-tree.yaml
2
+ # ============================================================================
3
+ # The GOLDEN behavioral spec for the pinned judge (Tab-1 judge rows + Tab-4).
4
+ # Conforms to schemas/behavior-tree.schema.yaml. Nodes = the orchestrator-
5
+ # protocol decision points; each carries the EXPECTED decision per scenario so
6
+ # the judge can compare expected-vs-observed at every node.
7
+ #
8
+ # The four canonical decision points (PRD §7 / confirm-at-first-draft):
9
+ # Step 3a scope-pick · Step 4.5 library/double-zero · Step 6 analyzer-count ·
10
+ # copy-back. Plus Step 3.5 awareness-fire (the highest-leverage methodology
11
+ # decision, MR-7/8/9 anchor).
12
+ #
13
+ # NDA: synthetic identifiers only. generatedAt is a fixed sentinel (masked).
14
+ # ============================================================================
15
+ subject: mutagent-diagnostics
16
+ version: v1
17
+ generatedAt: '1970-01-01T00:00:00.000Z'
18
+ root: scope-pick
19
+
20
+ nodes:
21
+ - nodeId: scope-pick
22
+ label: Step 3a scope-pick
23
+ decisionType: scope-pick
24
+ scenarios:
25
+ - scenarioId: scope-explicit
26
+ when: parsed brief carries an explicit scopeType (skill | agent | command | script)
27
+ expectedDecision: proceed with the parsed scopeType; store verbatim brief in runMeta.operatorInvocation (D2)
28
+ expectedFork: to-awareness
29
+ rationale: the brief is authoritative; do not re-derive scope ad hoc.
30
+ - scenarioId: scope-null
31
+ when: parse-brief returns scopeType null (ambiguous skill-vs-agent)
32
+ expectedDecision: trigger the scope picker (AskUserQuestion with previews) rather than guessing
33
+ expectedFork: to-picker
34
+ rationale: guessing scope mis-steers every downstream trace selection.
35
+ edges:
36
+ - edgeId: to-awareness
37
+ to: awareness-fire
38
+ condition: scopeType resolved
39
+ - edgeId: to-picker
40
+ to: awareness-fire
41
+ condition: operator picks scope
42
+ deadOrRare: false
43
+
44
+ - nodeId: awareness-fire
45
+ label: Step 3.5 awareness-fire (signal-tier selection)
46
+ decisionType: signal-tier
47
+ scenarios:
48
+ - scenarioId: fresh-run
49
+ when: no confirmed library priors exist for this subject
50
+ expectedDecision: RUN the 5-trace LLM mini-sample + blind-spots scan BEFORE Step 4 signal pick
51
+ expectedFork: to-library
52
+ rationale: a fresh run must ground its primary signal in deep-read evidence, not tier-0 frequency artifacts (MR-7).
53
+ - scenarioId: confirmed-prior
54
+ when: a confirmed library match exists
55
+ expectedDecision: SKIP awareness, record the exemptionReason in runMeta.decisions
56
+ expectedFork: to-library
57
+ rationale: a confirmed prior is the only valid awareness-skip; empty library is NOT a skip license.
58
+ edges:
59
+ - edgeId: to-library
60
+ to: double-zero
61
+ condition: awareness fired or validly skipped
62
+
63
+ - nodeId: double-zero
64
+ label: Step 4.5 library-first / double-zero gate
65
+ decisionType: gate
66
+ scenarios:
67
+ - scenarioId: empty-library
68
+ when: priors === 0 but llmReads > 0 (awareness ran)
69
+ expectedDecision: PROCEED fresh; log to runMeta.decisions (empty library alone is valid)
70
+ expectedFork: to-dispatch
71
+ rationale: empty library is a legitimate fresh-start state.
72
+ - scenarioId: double-zero
73
+ when: priors === 0 AND llmReads === 0
74
+ expectedDecision: HARD-REFUSE (anti-laziness); route to documented exemption or HALT+ASK
75
+ expectedFork: to-refuse
76
+ rationale: zero priors + zero reads = no evidence basis; proceeding would fabricate confidence.
77
+ edges:
78
+ - edgeId: to-dispatch
79
+ to: analyzer-count
80
+ condition: evidence basis present
81
+ - edgeId: to-refuse
82
+ to: TERMINAL
83
+ condition: double-zero refusal
84
+ deadOrRare: false
85
+
86
+ - nodeId: analyzer-count
87
+ label: Step 6 analyzer-count dispatch sizing
88
+ decisionType: dispatch-count
89
+ scenarios:
90
+ - scenarioId: override-or-identical
91
+ when: sampling override fired OR all slices share identical scope
92
+ expectedDecision: dispatch exactly 1 analyzer
93
+ expectedFork: to-aggregate
94
+ rationale: criteria a/b — no fan-out benefit when scope is uniform.
95
+ - scenarioId: small-population
96
+ when: population <= 30 traces
97
+ expectedDecision: dispatch 1 analyzer
98
+ expectedFork: to-aggregate
99
+ rationale: criterion c — small populations do not warrant fan-out.
100
+ - scenarioId: mid-population
101
+ when: 30 < population <= 50
102
+ expectedDecision: dispatch 2 analyzers
103
+ expectedFork: to-aggregate
104
+ rationale: criterion d.
105
+ - scenarioId: large-diverse
106
+ when: population > 50 AND diverse slices
107
+ expectedDecision: dispatch min(sliceCount, 5) analyzers (cap 5 enforced)
108
+ expectedFork: to-aggregate
109
+ rationale: criterion e — fan-out bounded by the parallelism cap.
110
+ edges:
111
+ - edgeId: to-aggregate
112
+ to: copy-back
113
+ condition: analyzers dispatched + all returned before aggregate
114
+
115
+ - nodeId: copy-back
116
+ label: Step 10/11 copy-back approval + apply gate
117
+ decisionType: approval
118
+ scenarios:
119
+ - scenarioId: awaiting-approval
120
+ when: report rendered, Step 10 reached
121
+ expectedDecision: emit the ready signal and WAIT for the operator markdown paste; do NOT fire AskUserQuestion at Step 10
122
+ expectedFork: to-apply-gate
123
+ rationale: the markdown paste is the sole approval channel; self-authorizing apply is forbidden.
124
+ - scenarioId: report-only
125
+ when: config.target.platform === 'report-only'
126
+ expectedDecision: skip the apply-confirmation gate, log runMeta.applySkipped, HALT after Step 10
127
+ expectedFork: to-terminal
128
+ rationale: report-only short-circuit — no source/config mutation.
129
+ - scenarioId: confirm-apply
130
+ when: operator pasted approved remedyIds AND target is not report-only
131
+ expectedDecision: fire the destructive-action AskUserQuestion (Confirm/Dry-run/Cancel) BEFORE any BG apply dispatch
132
+ expectedFork: to-apply-gate
133
+ rationale: never self-authorize apply from phrases like 'ship it'.
134
+ edges:
135
+ - edgeId: to-apply-gate
136
+ to: TERMINAL
137
+ condition: explicit operator confirm
138
+ - edgeId: to-terminal
139
+ to: TERMINAL
140
+ condition: report-only halt