@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,267 @@
1
+ /**
2
+ * scripts/cold-start-sampler.ts — EV-5.3 the balanced COLD-START sampler.
3
+ * ---------------------------------------------------------------------------
4
+ * PRE-SUITE, there is NO LLM judge yet: the EV-042 determiner (`determine-
5
+ * outcome.ts`) needs a criteria suite to run, and the suite doesn't exist yet.
6
+ * So the ONLY ✓/✗ labels available at cold start are:
7
+ * (a) the agent/skill DEFINITION's success ORACLE, and
8
+ * (b) deterministic MECHANICAL signals — the four cold-label families
9
+ * ERROR · FEEDBACK · SCORE · TERMINAL (a subset of `ext.signals` plus the
10
+ * §9.4.2 terminal/`incomplete` marker). No provider, no judge tokens.
11
+ *
12
+ * This is DISTINCT from `sample-traces.ts` `balancedSample`, which balances over
13
+ * the LLM-determiner's `OutcomeVerdict` labels (a WARM, post-judge input). This
14
+ * module is the COLD front-door: it manufactures a balanced ✓/✗ bootstrap suite
15
+ * from mechanical labels alone so the first judge pass is trained on BOTH classes.
16
+ *
17
+ * The pipeline (in order):
18
+ * stratify → SL-5 prevalence floor → worthiness/scenario dedup →
19
+ * MIN-BOTH (a guaranteed minimum from BOTH pools — never a success-only suite) →
20
+ * oversample the ✗-pool when rare (and flag the suite LOW-CONFIDENCE).
21
+ *
22
+ * PURE + deterministic: no `Math.random`, no clock, no network. A given
23
+ * (traces, opts) always yields the identical suite (reproducible cold suites —
24
+ * C-PIN-adjacent). Never fabricates a trace: a scarcer class contributes all it
25
+ * has and no more.
26
+ */
27
+
28
+ // ── Mechanical cold labels (NO LLM judge) ───────────────────────────────────
29
+
30
+ /** The cold outcome — the ONLY two pools a balanced suite is built from. */
31
+ export const ColdOutcome = { Pass: "pass", Fail: "fail" } as const;
32
+ export type ColdOutcomeValue = (typeof ColdOutcome)[keyof typeof ColdOutcome];
33
+
34
+ /**
35
+ * The four MECHANICAL cold-label signal families available pre-suite. Mirrors the
36
+ * subset of UniTF `ext.signals` (`hasError`/`hasApiErrors`/`hasChatFeedback`/
37
+ * `negativeReaction`/`hasScore`/`minScore`) plus the §9.4.2 terminal marker
38
+ * (`incomplete`). All optional/additive: an absent field is UNKNOWN, never a
39
+ * false — the oracle reads only what is present.
40
+ */
41
+ export interface MechanicalSignals {
42
+ /** ERROR family — a run/tool error was recorded. */
43
+ hasError?: boolean;
44
+ /** ERROR family — provider/API errors were recorded. */
45
+ hasApiErrors?: boolean;
46
+ /** FEEDBACK family — a user chat reaction exists on the trace. */
47
+ hasChatFeedback?: boolean;
48
+ /** FEEDBACK family — that reaction was negative (frustration/swear hit). */
49
+ negativeReaction?: boolean;
50
+ /** SCORE family — a numeric score exists. */
51
+ hasScore?: boolean;
52
+ /** SCORE family — the lowest score seen (compared to `scoreThreshold`). */
53
+ minScore?: number;
54
+ /** TERMINAL family — the session was cut off / reached no terminal event. */
55
+ incomplete?: boolean;
56
+ }
57
+
58
+ /** One candidate trace at cold start: its id + mechanical signals + dedup keys. */
59
+ export interface ColdTrace {
60
+ id: string;
61
+ signals: MechanicalSignals;
62
+ /** `ext.classification.scenario` — the dedup key (one rep per scenario). */
63
+ scenario?: string;
64
+ /** `ext.signals.worthinessScore` [0,1] — dedup keeps the worthiest rep. */
65
+ worthiness?: number;
66
+ }
67
+
68
+ /**
69
+ * The DEFINITION success oracle: maps a cold trace → ✓/✗, or `null` when the
70
+ * mechanical signals are insufficient to decide. A `null` (UNKNOWN) trace is kept
71
+ * for coverage but NEVER forced into a pool — it can't help balance and must not
72
+ * fabricate a label. The default is {@link defaultMechanicalOracle}; a subject
73
+ * whose DEFINITION encodes a richer success rule injects its own here.
74
+ */
75
+ export type SuccessOracle = (t: ColdTrace) => ColdOutcomeValue | null;
76
+
77
+ /** Default SCORE-family pass/fail cut. */
78
+ export const DEFAULT_SCORE_THRESHOLD = 0.5;
79
+
80
+ /**
81
+ * The default MECHANICAL oracle over the four cold-label families. A trace is ✗
82
+ * when ANY negative mechanical signal fired — an error, a NEGATIVE user reaction,
83
+ * a below-threshold score, or a non-terminal/truncated session — and ✓ otherwise.
84
+ * PURE + total (never returns `null`): a subject that needs an UNKNOWN verdict
85
+ * injects its own oracle. No LLM, no provider.
86
+ */
87
+ export function defaultMechanicalOracle(
88
+ scoreThreshold: number = DEFAULT_SCORE_THRESHOLD,
89
+ ): SuccessOracle {
90
+ return (t) => {
91
+ const s = t.signals;
92
+ const errored = s.hasError === true || s.hasApiErrors === true;
93
+ const negFeedback = s.hasChatFeedback === true && s.negativeReaction === true;
94
+ const lowScore =
95
+ s.hasScore === true && typeof s.minScore === "number" && s.minScore < scoreThreshold;
96
+ const truncated = s.incomplete === true;
97
+ return errored || negFeedback || lowScore || truncated ? ColdOutcome.Fail : ColdOutcome.Pass;
98
+ };
99
+ }
100
+
101
+ // ── stratify ────────────────────────────────────────────────────────────────
102
+
103
+ /** The ✓/✗ pools plus the UNKNOWN remainder (oracle abstained). Order-preserving. */
104
+ export interface Stratified {
105
+ pass: ColdTrace[];
106
+ fail: ColdTrace[];
107
+ unknown: ColdTrace[];
108
+ }
109
+
110
+ /**
111
+ * Partition traces into the ✓/✗ pools via the oracle (UNKNOWN → `unknown`).
112
+ * PURE + deterministic; preserves input order within each pool.
113
+ */
114
+ export function stratify(traces: ColdTrace[], oracle: SuccessOracle): Stratified {
115
+ const pass: ColdTrace[] = [];
116
+ const fail: ColdTrace[] = [];
117
+ const unknown: ColdTrace[] = [];
118
+ for (const t of traces) {
119
+ const v = oracle(t);
120
+ if (v === ColdOutcome.Pass) pass.push(t);
121
+ else if (v === ColdOutcome.Fail) fail.push(t);
122
+ else unknown.push(t);
123
+ }
124
+ return { pass, fail, unknown };
125
+ }
126
+
127
+ // ── worthiness / scenario dedup ─────────────────────────────────────────────
128
+
129
+ /**
130
+ * Keep ONE representative per `scenario` — the highest `worthiness` (ties → the
131
+ * first seen). Traces with NO scenario have no dedup key, so they are all kept
132
+ * (they can't be proven duplicates). Deterministic + order-preserving over the
133
+ * surviving representatives. Never reorders by worthiness — only DROPS lower-
134
+ * worthiness same-scenario duplicates.
135
+ */
136
+ export function dedupByScenario(pool: ColdTrace[]): ColdTrace[] {
137
+ // First pass: pick the winning id per scenario (highest worthiness, first-seen tie).
138
+ const winner = new Map<string, ColdTrace>();
139
+ for (const t of pool) {
140
+ if (t.scenario === undefined) continue;
141
+ const cur = winner.get(t.scenario);
142
+ if (cur === undefined || (t.worthiness ?? 0) > (cur.worthiness ?? 0)) {
143
+ winner.set(t.scenario, t);
144
+ }
145
+ }
146
+ // Second pass: emit in original order, keeping non-scenario traces + each
147
+ // scenario's winner exactly once.
148
+ const emittedScenario = new Set<string>();
149
+ const out: ColdTrace[] = [];
150
+ for (const t of pool) {
151
+ if (t.scenario === undefined) {
152
+ out.push(t);
153
+ continue;
154
+ }
155
+ if (winner.get(t.scenario) === t && !emittedScenario.has(t.scenario)) {
156
+ emittedScenario.add(t.scenario);
157
+ out.push(t);
158
+ }
159
+ }
160
+ return out;
161
+ }
162
+
163
+ // ── coldStartSample — the balanced cold suite ───────────────────────────────
164
+
165
+ export interface ColdStartOptions {
166
+ /** Target suite size (the sampler stays at or under this, MIN-BOTH excepted). */
167
+ size: number;
168
+ /**
169
+ * MIN-BOTH — the guaranteed minimum drawn from EACH non-empty pool (default 1).
170
+ * When honoring both minimums would exceed `size`, MIN-BOTH WINS: a balanced
171
+ * bootstrap suite is worth more than a hard size cap, and a success-only suite
172
+ * is never acceptable.
173
+ */
174
+ minBoth?: number;
175
+ /**
176
+ * SL-5 PREVALENCE FLOOR — the minimum number of ✗-pool traces retained even when
177
+ * the ✗ class is scarce (default = `minBoth`). The fail budget never drops below
178
+ * `min(prevalenceFloor, availableFails)`, so a rare-but-present failure signal is
179
+ * never balanced away.
180
+ */
181
+ prevalenceFloor?: number;
182
+ /** SCORE-family pass/fail cut for the default oracle (default 0.5). */
183
+ scoreThreshold?: number;
184
+ /** The DEFINITION success oracle (default `defaultMechanicalOracle(scoreThreshold)`). */
185
+ oracle?: SuccessOracle;
186
+ /** Worthiness/scenario dedup (default true). */
187
+ dedup?: boolean;
188
+ }
189
+
190
+ export interface ColdStartSuite {
191
+ /** The selected cold suite (deduped, balanced): ✓ representatives then ✗. */
192
+ selected: ColdTrace[];
193
+ passCount: number;
194
+ failCount: number;
195
+ /** true when the ✗-pool was too rare to fill its balanced half → all fails taken. */
196
+ oversampledFailPool: boolean;
197
+ /**
198
+ * true when the suite rests on TOO-FEW negatives — the ✗-pool was oversampled or
199
+ * couldn't fill its balanced half (or is empty). Downstream MUST treat the first
200
+ * cold judge pass as LOW-CONFIDENCE and re-balance once real verdicts exist.
201
+ */
202
+ lowConfidence: boolean;
203
+ /**
204
+ * The MIN-BOTH invariant: BOTH non-empty pools are represented in `selected`.
205
+ * A success-only suite (or a fail-only suite) ⇒ false. When both pools have
206
+ * members this is ALWAYS true.
207
+ */
208
+ minBothHeld: boolean;
209
+ }
210
+
211
+ /**
212
+ * Build a balanced cold-start suite from mechanical labels. Runs the full EV-5.3
213
+ * pipeline: stratify → SL-5 prevalence floor → scenario/worthiness dedup →
214
+ * MIN-BOTH → oversample-when-rare. PURE + deterministic; never fabricates.
215
+ */
216
+ export function coldStartSample(traces: ColdTrace[], opts: ColdStartOptions): ColdStartSuite {
217
+ const size = Math.max(0, Math.floor(opts.size));
218
+ const minBoth = Math.max(0, Math.floor(opts.minBoth ?? 1));
219
+ const oracle = opts.oracle ?? defaultMechanicalOracle(opts.scoreThreshold);
220
+
221
+ const strat = stratify(traces, oracle);
222
+ const pass = opts.dedup === false ? strat.pass : dedupByScenario(strat.pass);
223
+ const fail = opts.dedup === false ? strat.fail : dedupByScenario(strat.fail);
224
+
225
+ const perSide = Math.floor(size / 2);
226
+ // SL-5 prevalence floor: fails we must keep even when scarce (capped at availability).
227
+ const prevalenceFloor = Math.max(0, Math.floor(opts.prevalenceFloor ?? minBoth));
228
+ const failFloor = Math.min(Math.max(prevalenceFloor, minBoth), fail.length);
229
+
230
+ let takeFail: number;
231
+ let takePass: number;
232
+ let oversampledFailPool = false;
233
+
234
+ if (fail.length > 0 && fail.length < perSide) {
235
+ // ✗-pool RARE — oversample: take EVERY available fail, fill the rest with ✓.
236
+ oversampledFailPool = true;
237
+ takeFail = fail.length;
238
+ takePass = Math.min(pass.length, Math.max(0, size - takeFail));
239
+ } else {
240
+ // Balanced take, but never below the SL-5 floor for fails.
241
+ takeFail = Math.min(fail.length, Math.max(perSide, failFloor));
242
+ takePass = Math.min(pass.length, Math.max(0, size - takeFail));
243
+ // If ✓ is scarce and budget remains, let ✗ backfill up to `size`.
244
+ if (takePass + takeFail < size) {
245
+ takeFail = Math.min(fail.length, size - takePass);
246
+ }
247
+ }
248
+
249
+ // MIN-BOTH enforcement — never drop a NON-EMPTY pool below min(minBoth, available).
250
+ // This is the "never a success-only suite" guarantee; it can push the total over
251
+ // `size` only in the degenerate small-`size` case, which is intentional.
252
+ if (fail.length > 0) takeFail = Math.max(takeFail, Math.min(minBoth, fail.length));
253
+ if (pass.length > 0) takePass = Math.max(takePass, Math.min(minBoth, pass.length));
254
+
255
+ const selectedPass = pass.slice(0, takePass);
256
+ const selectedFail = fail.slice(0, takeFail);
257
+ const selected = [...selectedPass, ...selectedFail];
258
+
259
+ const passCount = selectedPass.length;
260
+ const failCount = selectedFail.length;
261
+ const minBothHeld = passCount > 0 && failCount > 0;
262
+ // Low-confidence when negatives are thin: oversampled, or the ✗-pool couldn't
263
+ // fill its balanced half, or there are no negatives at all.
264
+ const lowConfidence = oversampledFailPool || fail.length === 0 || failCount < perSide;
265
+
266
+ return { selected, passCount, failCount, oversampledFailPool, lowConfidence, minBothHeld };
267
+ }
@@ -0,0 +1,307 @@
1
+ /**
2
+ * scripts/config/load.ts — the evaluator's config-loader (v0.2.0).
3
+ * ---------------------------------------------------------------------------
4
+ * Read + YAML-parse the UNIFIED local `.mutagent/config.yaml` and RESOLVE the
5
+ * two things the evaluator needs to run:
6
+ *
7
+ * source — bound BY ROLE. The evaluator is a source-CONSUMER, so it binds
8
+ * `global.sources`. A SINGLE entry auto-binds; MULTIPLE ⇒
9
+ * disambiguation deferred (Fork A) → returned as `multiple` (NOT a
10
+ * hard fail — DISCOVER needs a source, code/dataset runs don't).
11
+ * judge — the pinned judge MODEL, resolved from `global.models.judge_model`
12
+ * ?? `global.models.default`. `judge_runtime` (renamed from
13
+ * `substrate` in v0.2.0) selects HOW judges run.
14
+ *
15
+ * These feed the CLI's config→flag precedence (an explicit `--model` /
16
+ * `--traces` flag WINS; the config fills the gap). This module does NOT apply
17
+ * that precedence — it exposes the resolved config values; the caller (cli/prep.ts)
18
+ * merges them under flag precedence.
19
+ *
20
+ * LEGACY (Fork B — hard-cut): a pre-v0.2.0 config (wrong `config_version` OR a
21
+ * legacy top-level key `shared`/`stages`/top-level `diagnostics`/`evaluator`) is
22
+ * DETECTED, never parsed at runtime — the loader returns
23
+ * `{ status: "migration-required", … }` so the caller routes to the migration
24
+ * DIRECTIVE. No old-shape parse ever happens.
25
+ *
26
+ * Determinism (coding-rules): `loadEvaluatorConfig` reads an INJECTED path —
27
+ * never resolved here. Path resolution (the LOCAL `.mutagent/`) happens only in
28
+ * the thin CLI, so tests stay deterministic against committed fixtures. Pure:
29
+ * one fs read + a YAML parse; no clock, no random, no network. Never throws.
30
+ *
31
+ * SECRETS: config holds only credential REF names (never values); resolution is
32
+ * a separate use-time step (resolve-credential.ts).
33
+ */
34
+ import { existsSync, readFileSync } from "node:fs";
35
+ import { resolve } from "node:path";
36
+ import { parse as parseYaml } from "yaml";
37
+ import {
38
+ CONFIG_VERSION,
39
+ LEGACY_CONFIG_VERSIONS,
40
+ type GlobalSource,
41
+ type GlobalTarget,
42
+ type ContextLink,
43
+ validateGlobal,
44
+ validateLifecycleEvaluator,
45
+ } from "./schema.ts";
46
+
47
+ /** The unified config path under a project root: `<root>/.mutagent/config.yaml`. */
48
+ export function configPathFor(projectRoot: string): string {
49
+ return resolve(projectRoot, ".mutagent", "config.yaml");
50
+ }
51
+
52
+ /** The v0.1.0 top-level keys that must NOT appear in a v0.2.0 config. */
53
+ const LEGACY_TOP_LEVEL_KEYS = [
54
+ "shared",
55
+ "stages",
56
+ "diagnostics",
57
+ "evaluator",
58
+ ] as const;
59
+
60
+ /**
61
+ * Detect a legacy (pre-v0.2.0) config WITHOUT parsing its old shape. Two signals,
62
+ * either one fires: a wrong/absent `config_version`, or a legacy top-level key
63
+ * (`shared`/`stages`/a top-level `diagnostics`/`evaluator` skill section). Pure;
64
+ * tolerant of a non-object input (returns []). Mirrors the orchestrator's
65
+ * detectLegacyConfig (parity port — no cross-import).
66
+ */
67
+ export function detectLegacyConfig(obj: unknown): string[] {
68
+ if (obj === null || typeof obj !== "object") return [];
69
+ const o = obj as Record<string, unknown>;
70
+ const markers: string[] = [];
71
+
72
+ const version = o.config_version;
73
+ if (version !== CONFIG_VERSION) {
74
+ if (
75
+ typeof version === "string" &&
76
+ (LEGACY_CONFIG_VERSIONS as readonly string[]).includes(version)
77
+ ) {
78
+ markers.push(
79
+ `config_version: "${version}" (legacy — expected "${CONFIG_VERSION}")`,
80
+ );
81
+ } else if (version === undefined) {
82
+ markers.push(`config_version: absent (expected "${CONFIG_VERSION}")`);
83
+ }
84
+ // A non-legacy, non-current version (e.g. a future "0.3.0") is NOT a legacy
85
+ // marker — structural validation reports it instead.
86
+ }
87
+
88
+ for (const key of LEGACY_TOP_LEVEL_KEYS) {
89
+ if (key in o) markers.push(`legacy top-level key '${key}'`);
90
+ }
91
+ return markers;
92
+ }
93
+
94
+ /**
95
+ * How the evaluator's source was bound (or why it couldn't be). The SELECTION
96
+ * contract (v0.2.1 — parity with orchestrator + diagnostics):
97
+ * bound — exactly one source resolved (single entry · `default:true` · `--source` hit).
98
+ * none — zero sources (DISCOVER needs a pick; code/dataset runs don't need a source).
99
+ * needs-selection — >1 source, none `default:true`, no `--source` ⇒ the caller must PROMPT the
100
+ * operator to pick (NON-fatal — the source EXISTS, only the pick is missing).
101
+ * multiple-defaults— >1 source with `default:true` — an AMBIGUOUS config the operator must fix.
102
+ * unknown-name — an explicit `--source <name>` that matched no catalog entry.
103
+ */
104
+ export type SourceBinding =
105
+ | { kind: "bound"; source: GlobalSource }
106
+ | { kind: "none" }
107
+ | { kind: "needs-selection"; sources: GlobalSource[] }
108
+ | { kind: "multiple-defaults"; sources: GlobalSource[] }
109
+ | { kind: "unknown-name"; name: string };
110
+
111
+ /** How the judge model resolved (model-intent-sacred: never a silent swap). */
112
+ export type JudgeModelResolution =
113
+ | { kind: "resolved"; model: string; from: "judge_model" | "default" }
114
+ | { kind: "unresolved" };
115
+
116
+ export interface EvaluatorConfigResolved {
117
+ /** the raw config_version literal read from the file. */
118
+ configVersion: string;
119
+ /** source bound BY ROLE from `global.sources` (single auto-binds). */
120
+ source: SourceBinding;
121
+ /** the pinned judge model from `global.models.{judge_model,default}`. */
122
+ judgeModel: JudgeModelResolution;
123
+ /** `lifecycle.evaluator.judge_runtime` (renamed from `substrate`), if set. */
124
+ judgeRuntime: string | undefined;
125
+ /** `global.context[]` — PROJECT-WIDE context links (every run loads these). */
126
+ globalContext: ContextLink[];
127
+ /** `lifecycle.evaluator.context[]` — the evaluator's stage-specific links. */
128
+ evaluatorContext: ContextLink[];
129
+ /** present targets (evaluator is judge-only — carried for completeness only). */
130
+ targets: GlobalTarget[];
131
+ }
132
+
133
+ export type LoadEvaluatorConfigResult =
134
+ | { status: "ok"; exists: true; config: EvaluatorConfigResolved }
135
+ | { status: "absent"; exists: false }
136
+ | { status: "migration-required"; exists: true; markers: string[]; error: string }
137
+ | { status: "invalid"; exists: true; errors: string[] };
138
+
139
+ /**
140
+ * Bind a source by role from `global.sources`. Precedence (SELECTION contract —
141
+ * EXACT parity with orchestrator + diagnostics):
142
+ *
143
+ * 1. explicit `selectName` (`--source`) WINS — find by name; miss ⇒ `unknown-name`.
144
+ * 2. 0 sources ⇒ `none`.
145
+ * 3. 1 source ⇒ `bound` (auto-bind).
146
+ * 4. >1 sources:
147
+ * exactly one `default:true` ⇒ `bound` (silent default).
148
+ * >1 `default:true` ⇒ `multiple-defaults` (ambiguous — operator fixes config).
149
+ * 0 `default:true` ⇒ `needs-selection` (caller PROMPTS the operator).
150
+ *
151
+ * Pure; no clock/random/network. NON-fatal on `needs-selection` — the source
152
+ * exists, only the disambiguation pick is deferred to the run-start ASK.
153
+ */
154
+ function bindSourceByRole(
155
+ sources: GlobalSource[],
156
+ selectName?: string,
157
+ ): SourceBinding {
158
+ // (1) Explicit override — `--source <name>` WINS over any default / auto-bind.
159
+ if (selectName !== undefined && selectName !== "") {
160
+ const hit = sources.find((s) => s.name === selectName);
161
+ return hit !== undefined
162
+ ? { kind: "bound", source: hit }
163
+ : { kind: "unknown-name", name: selectName };
164
+ }
165
+ // (2) / (3) — zero or a single entry.
166
+ if (sources.length === 0) return { kind: "none" };
167
+ if (sources.length === 1) return { kind: "bound", source: sources[0] as GlobalSource };
168
+ // (4) — multiple entries; disambiguate by `default:true`.
169
+ const defaults = sources.filter((s) => s.default === true);
170
+ if (defaults.length === 1) return { kind: "bound", source: defaults[0] as GlobalSource };
171
+ if (defaults.length > 1) return { kind: "multiple-defaults", sources };
172
+ return { kind: "needs-selection", sources };
173
+ }
174
+
175
+ /**
176
+ * Resolve the judge model (model-intent-sacred): `judge_model` wins, else
177
+ * `default`, else unresolved. The CALLER refuses (or falls back to `--model`)
178
+ * on `unresolved` — this pure resolver never invents a model.
179
+ */
180
+ function resolveJudgeModel(models: {
181
+ default?: string;
182
+ judge_model?: string;
183
+ }): JudgeModelResolution {
184
+ if (models.judge_model !== undefined && models.judge_model !== "") {
185
+ return { kind: "resolved", model: models.judge_model, from: "judge_model" };
186
+ }
187
+ if (models.default !== undefined && models.default !== "") {
188
+ return { kind: "resolved", model: models.default, from: "default" };
189
+ }
190
+ return { kind: "unresolved" };
191
+ }
192
+
193
+ /** Options for {@link loadEvaluatorConfig}. */
194
+ export interface LoadEvaluatorConfigOptions {
195
+ /**
196
+ * Run-time source override (`--source <name>`). WINS over `default:true` /
197
+ * auto-bind; a miss ⇒ `{ kind: "unknown-name" }`. Absent ⇒ default precedence.
198
+ */
199
+ selectSourceName?: string;
200
+ }
201
+
202
+ /**
203
+ * Load + resolve the evaluator's config from an INJECTED path. Guarded (mirrors
204
+ * the orchestrator loadConfig): a missing file → `absent`; malformed YAML or a
205
+ * failed structural check → `invalid`; a LEGACY config → `migration-required`
206
+ * (detected BEFORE structural validation — never parse the old shape). Otherwise
207
+ * `ok` with the resolved source + judge model + context links. Never throws.
208
+ *
209
+ * `opts.selectSourceName` (`--source`) threads the run-time source override into
210
+ * `bindSourceByRole` (SELECTION contract) — it WINS over `default:true`.
211
+ */
212
+ export function loadEvaluatorConfig(
213
+ configPath: string,
214
+ opts: LoadEvaluatorConfigOptions = {},
215
+ ): LoadEvaluatorConfigResult {
216
+ if (!existsSync(configPath)) {
217
+ return { status: "absent", exists: false };
218
+ }
219
+
220
+ let raw: string;
221
+ try {
222
+ raw = readFileSync(configPath, "utf8");
223
+ } catch (err) {
224
+ return { status: "invalid", exists: true, errors: [`cannot read ${configPath}: ${String(err)}`] };
225
+ }
226
+
227
+ let parsed: unknown;
228
+ try {
229
+ parsed = parseYaml(raw);
230
+ } catch (err) {
231
+ return { status: "invalid", exists: true, errors: [`malformed YAML in ${configPath}: ${String(err)}`] };
232
+ }
233
+
234
+ // Legacy detection FIRST — never parse the old shape at runtime (Fork B).
235
+ const legacyMarkers = detectLegacyConfig(parsed);
236
+ if (legacyMarkers.length > 0) {
237
+ return {
238
+ status: "migration-required",
239
+ exists: true,
240
+ markers: legacyMarkers,
241
+ error:
242
+ `legacy config detected in ${configPath} — migration required ` +
243
+ `(see references/config-migration.md): ${legacyMarkers.join("; ")}`,
244
+ };
245
+ }
246
+
247
+ const root = (parsed ?? {}) as Record<string, unknown>;
248
+ const globalBlock = (root.global ?? {}) as Record<string, unknown>;
249
+ const lifecycle = (root.lifecycle ?? {}) as Record<string, unknown>;
250
+ const evaluatorSection = (lifecycle.evaluator ?? {}) as Record<string, unknown>;
251
+
252
+ // Structural validation of the two blocks the evaluator reads.
253
+ const gRes = validateGlobal(globalBlock);
254
+ if (!gRes.ok) return { status: "invalid", exists: true, errors: gRes.errors.map((e) => `global${e}`) };
255
+ const eRes = validateLifecycleEvaluator(evaluatorSection);
256
+ if (!eRes.ok) {
257
+ return { status: "invalid", exists: true, errors: eRes.errors.map((e) => `lifecycle.evaluator${e}`) };
258
+ }
259
+
260
+ const sources = (globalBlock.sources as GlobalSource[] | undefined) ?? [];
261
+ const targets = (globalBlock.targets as GlobalTarget[] | undefined) ?? [];
262
+ const models = (globalBlock.models as { default?: string; judge_model?: string } | undefined) ?? {};
263
+ const globalContext = (globalBlock.context as ContextLink[] | undefined) ?? [];
264
+ const evaluatorContext = (evaluatorSection.context as ContextLink[] | undefined) ?? [];
265
+ const judgeRuntime =
266
+ typeof evaluatorSection.judge_runtime === "string" && evaluatorSection.judge_runtime !== ""
267
+ ? evaluatorSection.judge_runtime
268
+ : undefined;
269
+
270
+ return {
271
+ status: "ok",
272
+ exists: true,
273
+ config: {
274
+ configVersion: typeof root.config_version === "string" ? root.config_version : "",
275
+ source: bindSourceByRole(sources, opts.selectSourceName),
276
+ judgeModel: resolveJudgeModel(models),
277
+ judgeRuntime,
278
+ globalContext,
279
+ evaluatorContext,
280
+ targets,
281
+ },
282
+ };
283
+ }
284
+
285
+ // ── CLI — thin wrapper (resolve the LOCAL config; print the resolution) ────────
286
+ // bun scripts/config/load.ts [project-root] [--source <name>] (root defaults to cwd)
287
+ // Exit 0 = ok / absent; exit 1 = invalid / migration-required.
288
+
289
+ function runCli(argv: string[]): number {
290
+ const sourceIdx = argv.indexOf("--source");
291
+ const selectSourceName =
292
+ sourceIdx >= 0 && sourceIdx + 1 < argv.length ? argv[sourceIdx + 1] : undefined;
293
+ // First positional after the script path that isn't the --source value.
294
+ const positional = argv[2] !== undefined && argv[2] !== "--source" ? argv[2] : undefined;
295
+ const projectRoot = positional ?? process.cwd();
296
+ const result = loadEvaluatorConfig(configPathFor(projectRoot), { selectSourceName });
297
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
298
+ return result.status === "invalid" || result.status === "migration-required" ? 1 : 0;
299
+ }
300
+
301
+ const isMain =
302
+ typeof import.meta !== "undefined" &&
303
+ (import.meta as unknown as { main?: boolean }).main === true;
304
+ if (isMain) {
305
+ const argv = typeof Bun !== "undefined" ? Bun.argv : process.argv;
306
+ process.exit(runCli(argv));
307
+ }