@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.
- package/.claude/skills/mutagent-evaluator/SKILL.md +538 -0
- package/.claude/skills/mutagent-evaluator/assets/agents/audit-executor.md +169 -0
- package/.claude/skills/mutagent-evaluator/assets/agents/dataset-builder.md +160 -0
- package/.claude/skills/mutagent-evaluator/assets/agents/discovery.md +425 -0
- package/.claude/skills/mutagent-evaluator/assets/agents/evaluator.md +1044 -0
- package/.claude/skills/mutagent-evaluator/assets/brand/theme.css +213 -0
- package/.claude/skills/mutagent-evaluator/assets/brand/wordmark.html +9 -0
- package/.claude/skills/mutagent-evaluator/lenses/context-flow-lens.md +85 -0
- package/.claude/skills/mutagent-evaluator/lenses/data-lens.md +47 -0
- package/.claude/skills/mutagent-evaluator/lenses/decision-lens.md +48 -0
- package/.claude/skills/mutagent-evaluator/lenses/methodology-critic-lens.md +53 -0
- package/.claude/skills/mutagent-evaluator/lenses/trajectory-lens.md +44 -0
- package/.claude/skills/mutagent-evaluator/references/build-review-interface.md +83 -0
- package/.claude/skills/mutagent-evaluator/references/edd-loop.md +134 -0
- package/.claude/skills/mutagent-evaluator/references/error-analysis.md +113 -0
- package/.claude/skills/mutagent-evaluator/references/eval-audit.md +154 -0
- package/.claude/skills/mutagent-evaluator/references/eval-stage.md +168 -0
- package/.claude/skills/mutagent-evaluator/references/generate-synthetic-data.md +81 -0
- package/.claude/skills/mutagent-evaluator/references/grounded-adjudication.md +221 -0
- package/.claude/skills/mutagent-evaluator/references/memory-format.md +65 -0
- package/.claude/skills/mutagent-evaluator/references/methodology.md +201 -0
- package/.claude/skills/mutagent-evaluator/references/operation-inventory.md +196 -0
- package/.claude/skills/mutagent-evaluator/references/validate-evaluator.md +125 -0
- package/.claude/skills/mutagent-evaluator/references/workflows/orchestrator-protocol.md +287 -0
- package/.claude/skills/mutagent-evaluator/references/write-judge-prompt.md +123 -0
- package/.claude/skills/mutagent-evaluator/schemas/behavior-tree.schema.yaml +73 -0
- package/.claude/skills/mutagent-evaluator/schemas/dataset.schema.yaml +66 -0
- package/.claude/skills/mutagent-evaluator/schemas/edd-change-request.schema.yaml +114 -0
- package/.claude/skills/mutagent-evaluator/schemas/eval-matrix.schema.yaml +74 -0
- package/.claude/skills/mutagent-evaluator/schemas/flow-graph.schema.yaml +69 -0
- package/.claude/skills/mutagent-evaluator/schemas/flow-profile.schema.yaml +49 -0
- package/.claude/skills/mutagent-evaluator/schemas/methodology-review.schema.yaml +40 -0
- package/.claude/skills/mutagent-evaluator/schemas/scorecard.schema.yaml +85 -0
- package/.claude/skills/mutagent-evaluator/scripts/agent-dispatch.ts +0 -0
- package/.claude/skills/mutagent-evaluator/scripts/aggregate-discover.ts +543 -0
- package/.claude/skills/mutagent-evaluator/scripts/artifact-paths.ts +99 -0
- package/.claude/skills/mutagent-evaluator/scripts/assemble-scorecard.ts +172 -0
- package/.claude/skills/mutagent-evaluator/scripts/build-dataset.ts +186 -0
- package/.claude/skills/mutagent-evaluator/scripts/build-evals.ts +93 -0
- package/.claude/skills/mutagent-evaluator/scripts/build-review-ui.ts +393 -0
- package/.claude/skills/mutagent-evaluator/scripts/check-method-router.ts +170 -0
- package/.claude/skills/mutagent-evaluator/scripts/cli/aggregate.ts +112 -0
- package/.claude/skills/mutagent-evaluator/scripts/cli/audit-run.ts +175 -0
- package/.claude/skills/mutagent-evaluator/scripts/cli/doctor.ts +211 -0
- package/.claude/skills/mutagent-evaluator/scripts/cli/dogfood.ts +133 -0
- package/.claude/skills/mutagent-evaluator/scripts/cli/init.ts +601 -0
- package/.claude/skills/mutagent-evaluator/scripts/cli/methodology-review.ts +122 -0
- package/.claude/skills/mutagent-evaluator/scripts/cli/prep.ts +279 -0
- package/.claude/skills/mutagent-evaluator/scripts/cli/profile-subject.ts +165 -0
- package/.claude/skills/mutagent-evaluator/scripts/cli/run.sh +56 -0
- package/.claude/skills/mutagent-evaluator/scripts/cli/variance-check.ts +105 -0
- package/.claude/skills/mutagent-evaluator/scripts/code-eval.ts +248 -0
- package/.claude/skills/mutagent-evaluator/scripts/codegen-evals.ts +173 -0
- package/.claude/skills/mutagent-evaluator/scripts/cold-start-project.ts +151 -0
- package/.claude/skills/mutagent-evaluator/scripts/cold-start-sampler.ts +267 -0
- package/.claude/skills/mutagent-evaluator/scripts/config/load.ts +307 -0
- package/.claude/skills/mutagent-evaluator/scripts/config/schema.ts +325 -0
- package/.claude/skills/mutagent-evaluator/scripts/contracts/agentspec-evals.ts +85 -0
- package/.claude/skills/mutagent-evaluator/scripts/contracts/dataset.ts +149 -0
- package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-engine.ts +118 -0
- package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-matrix.ts +577 -0
- package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-types.ts +1074 -0
- package/.claude/skills/mutagent-evaluator/scripts/contracts/flow-graph.ts +194 -0
- package/.claude/skills/mutagent-evaluator/scripts/contracts/types.ts +303 -0
- package/.claude/skills/mutagent-evaluator/scripts/contracts/validation.ts +193 -0
- package/.claude/skills/mutagent-evaluator/scripts/derive-dataset.ts +152 -0
- package/.claude/skills/mutagent-evaluator/scripts/determine-outcome.ts +219 -0
- package/.claude/skills/mutagent-evaluator/scripts/diff-discriminate.ts +160 -0
- package/.claude/skills/mutagent-evaluator/scripts/discover-criteria.ts +348 -0
- package/.claude/skills/mutagent-evaluator/scripts/edd/change-request.ts +232 -0
- package/.claude/skills/mutagent-evaluator/scripts/edd/edd-types.ts +210 -0
- package/.claude/skills/mutagent-evaluator/scripts/edd/variance-gate.ts +186 -0
- package/.claude/skills/mutagent-evaluator/scripts/emit-completeness.ts +111 -0
- package/.claude/skills/mutagent-evaluator/scripts/eval-engine.ts +160 -0
- package/.claude/skills/mutagent-evaluator/scripts/evaluate.ts +333 -0
- package/.claude/skills/mutagent-evaluator/scripts/flow-graph.ts +230 -0
- package/.claude/skills/mutagent-evaluator/scripts/judge-prompt-template.ts +253 -0
- package/.claude/skills/mutagent-evaluator/scripts/judge-provider.ts +135 -0
- package/.claude/skills/mutagent-evaluator/scripts/lint-grounding.ts +229 -0
- package/.claude/skills/mutagent-evaluator/scripts/lint-uniformity.ts +168 -0
- package/.claude/skills/mutagent-evaluator/scripts/living-suite.ts +109 -0
- package/.claude/skills/mutagent-evaluator/scripts/load-bundle.ts +203 -0
- package/.claude/skills/mutagent-evaluator/scripts/load-profile-vocab.ts +64 -0
- package/.claude/skills/mutagent-evaluator/scripts/load-profile.ts +106 -0
- package/.claude/skills/mutagent-evaluator/scripts/mask.ts +138 -0
- package/.claude/skills/mutagent-evaluator/scripts/materialize-dataset.ts +113 -0
- package/.claude/skills/mutagent-evaluator/scripts/matrix-judge.ts +750 -0
- package/.claude/skills/mutagent-evaluator/scripts/memory/append.ts +215 -0
- package/.claude/skills/mutagent-evaluator/scripts/memory/read.ts +168 -0
- package/.claude/skills/mutagent-evaluator/scripts/prep-tasks.ts +125 -0
- package/.claude/skills/mutagent-evaluator/scripts/profile-subject.ts +310 -0
- package/.claude/skills/mutagent-evaluator/scripts/publish-report.ts +131 -0
- package/.claude/skills/mutagent-evaluator/scripts/read-unitf-traces.ts +81 -0
- package/.claude/skills/mutagent-evaluator/scripts/render-build-cards.ts +195 -0
- package/.claude/skills/mutagent-evaluator/scripts/render-discover-report.ts +1640 -0
- package/.claude/skills/mutagent-evaluator/scripts/render-eval-report.ts +3823 -0
- package/.claude/skills/mutagent-evaluator/scripts/render-report.ts +212 -0
- package/.claude/skills/mutagent-evaluator/scripts/resolve-credential.ts +110 -0
- package/.claude/skills/mutagent-evaluator/scripts/resolve-ref.ts +98 -0
- package/.claude/skills/mutagent-evaluator/scripts/result-verify.ts +129 -0
- package/.claude/skills/mutagent-evaluator/scripts/route-failures.ts +320 -0
- package/.claude/skills/mutagent-evaluator/scripts/run-deterministic.ts +271 -0
- package/.claude/skills/mutagent-evaluator/scripts/run-evaluate.ts +715 -0
- package/.claude/skills/mutagent-evaluator/scripts/run-judge.ts +155 -0
- package/.claude/skills/mutagent-evaluator/scripts/run-pipeline.ts +175 -0
- package/.claude/skills/mutagent-evaluator/scripts/sample-traces.ts +210 -0
- package/.claude/skills/mutagent-evaluator/scripts/self-audit.ts +387 -0
- package/.claude/skills/mutagent-evaluator/scripts/source-map.ts +106 -0
- package/.claude/skills/mutagent-evaluator/scripts/subject-profile.ts +134 -0
- package/.claude/skills/mutagent-evaluator/scripts/substrate.ts +162 -0
- package/.claude/skills/mutagent-evaluator/scripts/ui-slots.ts +119 -0
- package/.claude/skills/mutagent-evaluator/scripts/unitf-to-evaltrace.ts +284 -0
- package/.claude/skills/mutagent-evaluator/scripts/validate-judge.ts +358 -0
- package/.claude/skills/mutagent-evaluator/scripts/variance-compare.ts +177 -0
- package/.claude/skills/mutagent-evaluator/subjects/mutagent-diagnostics/behavior-tree.yaml +140 -0
- package/.claude/skills/mutagent-evaluator/subjects/mutagent-diagnostics/eval-matrix.yaml +1270 -0
- package/.claude/skills/mutagent-evaluator/subjects/mutagent-diagnostics/methodology-review.yaml +105 -0
- package/.claude/skills/mutagent-evaluator/workflows/audit.workflow.js +82 -0
- package/.claude/skills/mutagent-evaluator/workflows/data-leak.workflow.js +236 -0
- package/.claude/skills/mutagent-evaluator/workflows/variance.workflow.js +163 -0
- package/LICENSE +201 -0
- package/NOTICE +23 -0
- package/README.md +90 -0
- package/bin/mutagent-cli.mjs +9263 -0
- package/bin/mutagent-evaluator.mjs +96 -0
- 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
|