switchboard-cli 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -0
- package/bin/switchboard.mjs +49 -0
- package/calibration/engine/baseline.ts +93 -0
- package/calibration/engine/diagnosis.ts +191 -0
- package/calibration/engine/diff.ts +118 -0
- package/calibration/engine/escalation.ts +49 -0
- package/calibration/engine/ledger.ts +141 -0
- package/calibration/engine/trends.ts +141 -0
- package/calibration/external/rubric.yaml +32 -0
- package/calibration/external/scorer.ts +479 -0
- package/calibration/external/verdict-writer.ts +29 -0
- package/calibration/internal/harness.ts +697 -0
- package/calibration/internal/return-simulator.ts +270 -0
- package/calibration/internal/trace-collector.ts +78 -0
- package/calibration/internal/verdict-writer.ts +149 -0
- package/calibration/ledger/baselines/baseline-2026-04-09.yaml +23 -0
- package/calibration/ledger/history.yaml +18 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/diffs/adv-0bdc944b61d5.yaml +9 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/diffs/blind-16cdf0db1b43.yaml +9 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/diffs/blind-a6b2c8be67cc.yaml +9 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/manifest.yaml +8 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/seeds/adv-0bdc944b61d5.yaml +7 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/seeds/blind-16cdf0db1b43.yaml +8 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/seeds/blind-a6b2c8be67cc.yaml +8 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/summary.yaml +10 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/traces/adv-0bdc944b61d5.yaml +141 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/traces/blind-16cdf0db1b43.yaml +147 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/traces/blind-a6b2c8be67cc.yaml +147 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-a/adv-0bdc944b61d5.yaml +24 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-a/blind-16cdf0db1b43.yaml +24 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-a/blind-a6b2c8be67cc.yaml +25 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-b/adv-0bdc944b61d5.yaml +31 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-b/blind-16cdf0db1b43.yaml +32 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-b/blind-a6b2c8be67cc.yaml +32 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/diffs/adv-a0c9e2bfb0d6.yaml +9 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/diffs/blind-3e892f3a89ee.yaml +9 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/diffs/blind-958b2f9e6816.yaml +9 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/manifest.yaml +8 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/seeds/adv-a0c9e2bfb0d6.yaml +7 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/seeds/blind-3e892f3a89ee.yaml +8 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/seeds/blind-958b2f9e6816.yaml +8 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/summary.yaml +10 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/traces/adv-a0c9e2bfb0d6.yaml +141 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/traces/blind-3e892f3a89ee.yaml +147 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/traces/blind-958b2f9e6816.yaml +147 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-a/adv-a0c9e2bfb0d6.yaml +24 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-a/blind-3e892f3a89ee.yaml +23 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-a/blind-958b2f9e6816.yaml +25 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-b/adv-a0c9e2bfb0d6.yaml +31 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-b/blind-3e892f3a89ee.yaml +32 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-b/blind-958b2f9e6816.yaml +32 -0
- package/calibration/seeds/adversarial-generator.ts +159 -0
- package/calibration/seeds/blind-generator.ts +169 -0
- package/calibration/seeds/replay-loader.ts +117 -0
- package/calibration/skill/calibrate.ts +292 -0
- package/calibration/skill/cli-flags.ts +49 -0
- package/calibration/skill/report.ts +80 -0
- package/calibration/skill/review.ts +118 -0
- package/calibration/types.ts +292 -0
- package/package.json +46 -0
- package/src/commands/audit-codex.ts +266 -0
- package/src/commands/calibrate.ts +70 -0
- package/src/commands/compile.ts +117 -0
- package/src/commands/evaluate.ts +103 -0
- package/src/commands/ingest.ts +250 -0
- package/src/commands/init.ts +133 -0
- package/src/commands/packet.ts +408 -0
- package/src/commands/receipt.ts +305 -0
- package/src/commands/run-claude.ts +355 -0
- package/src/index.ts +43 -0
- package/src/lib/draft-return.ts +278 -0
- package/src/lib/drift-guard.ts +105 -0
- package/src/lib/errors.ts +61 -0
- package/src/lib/output.ts +43 -0
- package/src/lib/paths.ts +125 -0
- package/src/lib/proof.ts +262 -0
- package/src/lib/transport.ts +276 -0
- package/src/lib/yaml-io.ts +62 -0
- package/src/store/filesystem-store.ts +326 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { randomBytes } from "crypto";
|
|
3
|
+
import type {
|
|
4
|
+
CalibrationSeed,
|
|
5
|
+
CalibrateFlags,
|
|
6
|
+
DiagnosisCard,
|
|
7
|
+
EscalationItem,
|
|
8
|
+
LayerAVerdict,
|
|
9
|
+
LayerBVerdict,
|
|
10
|
+
PipelineTrace,
|
|
11
|
+
RunManifest,
|
|
12
|
+
RunSummary,
|
|
13
|
+
TrendEntry,
|
|
14
|
+
VerdictDiff,
|
|
15
|
+
} from "../types";
|
|
16
|
+
import { parseFlags } from "./cli-flags";
|
|
17
|
+
import { formatRunReport, formatTrendsReport } from "./report";
|
|
18
|
+
import { loadPendingEscalations, formatEscalationForReview, countPendingEscalations } from "./review";
|
|
19
|
+
|
|
20
|
+
// Seed generators
|
|
21
|
+
import { generateBlindSeeds } from "../seeds/blind-generator";
|
|
22
|
+
import { generateAdversarialSeeds } from "../seeds/adversarial-generator";
|
|
23
|
+
import { loadReplaySeeds } from "../seeds/replay-loader";
|
|
24
|
+
|
|
25
|
+
// Layer A
|
|
26
|
+
import { runPipeline } from "../internal/harness";
|
|
27
|
+
import { produceLayerAVerdict } from "../internal/verdict-writer";
|
|
28
|
+
|
|
29
|
+
// Layer B
|
|
30
|
+
import { produceLayerBVerdict } from "../external/verdict-writer";
|
|
31
|
+
|
|
32
|
+
// Engine
|
|
33
|
+
import {
|
|
34
|
+
initLedger,
|
|
35
|
+
appendRun,
|
|
36
|
+
writeSeed,
|
|
37
|
+
writeTrace,
|
|
38
|
+
writeVerdictA,
|
|
39
|
+
writeVerdictB,
|
|
40
|
+
writeDiff,
|
|
41
|
+
writeDiagnosis,
|
|
42
|
+
writeEscalation,
|
|
43
|
+
writeSummary,
|
|
44
|
+
appendHistory,
|
|
45
|
+
} from "../engine/ledger";
|
|
46
|
+
import { captureBaseline } from "../engine/baseline";
|
|
47
|
+
import { computeDiff } from "../engine/diff";
|
|
48
|
+
import { generateDiagnosis } from "../engine/diagnosis";
|
|
49
|
+
import { buildEscalationQueue } from "../engine/escalation";
|
|
50
|
+
import { computeTrends, checkSelfMonitoring } from "../engine/trends";
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Constants
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
const AGREEMENT_SAMPLE_RATE = 0.2;
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Main calibration runner
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
export interface CalibrationResult {
|
|
63
|
+
report: string;
|
|
64
|
+
summary: RunSummary;
|
|
65
|
+
warnings: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function runCalibration(
|
|
69
|
+
args: string,
|
|
70
|
+
projectRoot: string,
|
|
71
|
+
): Promise<CalibrationResult> {
|
|
72
|
+
const flags = parseFlags(args);
|
|
73
|
+
const ledgerPath = join(projectRoot, "calibration", "ledger");
|
|
74
|
+
const archivePath = join(projectRoot, ".switchboard", "archive");
|
|
75
|
+
|
|
76
|
+
// Special modes
|
|
77
|
+
if (flags.review) {
|
|
78
|
+
const pending = loadPendingEscalations(ledgerPath);
|
|
79
|
+
if (pending.length === 0) {
|
|
80
|
+
return {
|
|
81
|
+
report: "No pending escalations to review.",
|
|
82
|
+
summary: emptySummary(),
|
|
83
|
+
warnings: [],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const reports = pending.map(formatEscalationForReview);
|
|
87
|
+
return {
|
|
88
|
+
report: `${pending.length} pending escalation(s):\n\n${reports.join("\n\n---\n\n")}`,
|
|
89
|
+
summary: emptySummary(),
|
|
90
|
+
warnings: [],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (flags.trends) {
|
|
95
|
+
const trends = computeTrends(ledgerPath);
|
|
96
|
+
const warnings = checkSelfMonitoring(trends);
|
|
97
|
+
return {
|
|
98
|
+
report: formatTrendsReport(trends),
|
|
99
|
+
summary: emptySummary(),
|
|
100
|
+
warnings,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Initialize ledger
|
|
105
|
+
initLedger(ledgerPath);
|
|
106
|
+
|
|
107
|
+
// Capture baseline
|
|
108
|
+
const baseline = captureBaseline(ledgerPath);
|
|
109
|
+
|
|
110
|
+
// Generate run ID
|
|
111
|
+
const runId = new Date().toISOString().replace(/[:.]/g, "-");
|
|
112
|
+
|
|
113
|
+
// Generate seeds
|
|
114
|
+
const seeds: CalibrationSeed[] = [];
|
|
115
|
+
if (flags.blind > 0) seeds.push(...generateBlindSeeds(flags.blind));
|
|
116
|
+
if (flags.adversarial > 0) seeds.push(...generateAdversarialSeeds(flags.adversarial));
|
|
117
|
+
if (flags.replay > 0) seeds.push(...loadReplaySeeds(archivePath, flags.replay));
|
|
118
|
+
|
|
119
|
+
if (seeds.length === 0) {
|
|
120
|
+
return {
|
|
121
|
+
report: "No seeds generated. Check flags.",
|
|
122
|
+
summary: emptySummary(),
|
|
123
|
+
warnings: [],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Write manifest
|
|
128
|
+
const manifest: RunManifest = {
|
|
129
|
+
run_id: runId,
|
|
130
|
+
started_at: new Date().toISOString(),
|
|
131
|
+
seed_count: seeds.length,
|
|
132
|
+
blind_count: seeds.filter(s => s.source === "blind").length,
|
|
133
|
+
adversarial_count: seeds.filter(s => s.source === "adversarial").length,
|
|
134
|
+
replay_count: seeds.filter(s => s.source === "replay").length,
|
|
135
|
+
trigger: flags.retestOpen ? "retest" : "manual",
|
|
136
|
+
baseline_snapshot: baseline.baseline_id,
|
|
137
|
+
};
|
|
138
|
+
appendRun(ledgerPath, manifest);
|
|
139
|
+
|
|
140
|
+
// Process each seed
|
|
141
|
+
const traces: PipelineTrace[] = [];
|
|
142
|
+
const verdictsA: LayerAVerdict[] = [];
|
|
143
|
+
const verdictsB: LayerBVerdict[] = [];
|
|
144
|
+
const diffs: VerdictDiff[] = [];
|
|
145
|
+
const diagnoses: DiagnosisCard[] = [];
|
|
146
|
+
|
|
147
|
+
for (const seed of seeds) {
|
|
148
|
+
// Write seed
|
|
149
|
+
writeSeed(ledgerPath, runId, seed);
|
|
150
|
+
|
|
151
|
+
// Run pipeline (Layer A)
|
|
152
|
+
const pipelineResult = runPipeline(seed);
|
|
153
|
+
const trace = pipelineResult.trace;
|
|
154
|
+
traces.push(trace);
|
|
155
|
+
writeTrace(ledgerPath, runId, trace);
|
|
156
|
+
|
|
157
|
+
// Layer A verdict
|
|
158
|
+
const verdictA = produceLayerAVerdict(seed, trace);
|
|
159
|
+
verdictsA.push(verdictA);
|
|
160
|
+
writeVerdictA(ledgerPath, runId, verdictA);
|
|
161
|
+
|
|
162
|
+
// Layer B verdict (independent)
|
|
163
|
+
const verdictB = produceLayerBVerdict(
|
|
164
|
+
seed,
|
|
165
|
+
trace.compiled_spec,
|
|
166
|
+
trace.receipt_summary,
|
|
167
|
+
trace.trust_posture,
|
|
168
|
+
);
|
|
169
|
+
verdictsB.push(verdictB);
|
|
170
|
+
writeVerdictB(ledgerPath, runId, verdictB);
|
|
171
|
+
|
|
172
|
+
// Diff
|
|
173
|
+
const diff = computeDiff(verdictA, verdictB, trace.return_mode, AGREEMENT_SAMPLE_RATE);
|
|
174
|
+
diffs.push(diff);
|
|
175
|
+
writeDiff(ledgerPath, runId, diff);
|
|
176
|
+
|
|
177
|
+
// Diagnosis for disagreements
|
|
178
|
+
if (diff.is_disagreement) {
|
|
179
|
+
const diag = generateDiagnosis(seed, diff, trace, verdictA, verdictB);
|
|
180
|
+
diagnoses.push(diag);
|
|
181
|
+
writeDiagnosis(ledgerPath, runId, diag);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Build escalation queue
|
|
186
|
+
const escalations = buildEscalationQueue(diffs, diagnoses);
|
|
187
|
+
for (const esc of escalations) {
|
|
188
|
+
writeEscalation(ledgerPath, runId, esc);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Compute summary
|
|
192
|
+
const summary = computeRunSummary(runId, seeds, traces, verdictsA, verdictsB, diffs, diagnoses, escalations);
|
|
193
|
+
writeSummary(ledgerPath, runId, summary);
|
|
194
|
+
|
|
195
|
+
// Update manifest with completion
|
|
196
|
+
const completedManifest = { ...manifest, completed_at: new Date().toISOString() };
|
|
197
|
+
appendHistory(ledgerPath, completedManifest);
|
|
198
|
+
|
|
199
|
+
// Compute trends and check self-monitoring
|
|
200
|
+
const trends = computeTrends(ledgerPath);
|
|
201
|
+
const previousTrend = trends.length >= 2 ? trends[trends.length - 2] : undefined;
|
|
202
|
+
const warnings = checkSelfMonitoring(trends);
|
|
203
|
+
|
|
204
|
+
// Pending escalation reminder
|
|
205
|
+
const pendingCount = countPendingEscalations(ledgerPath);
|
|
206
|
+
if (pendingCount > 10) {
|
|
207
|
+
warnings.push(`${pendingCount} unreviewed escalations across runs. Human judgments are calibration fuel.`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const report = formatRunReport(summary, previousTrend);
|
|
211
|
+
|
|
212
|
+
return { report, summary, warnings };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// Summary computation
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
function computeRunSummary(
|
|
220
|
+
runId: string,
|
|
221
|
+
seeds: CalibrationSeed[],
|
|
222
|
+
traces: PipelineTrace[],
|
|
223
|
+
verdictsA: LayerAVerdict[],
|
|
224
|
+
verdictsB: LayerBVerdict[],
|
|
225
|
+
diffs: VerdictDiff[],
|
|
226
|
+
diagnoses: DiagnosisCard[],
|
|
227
|
+
escalations: EscalationItem[],
|
|
228
|
+
): RunSummary {
|
|
229
|
+
const driftSeeds = traces.filter(t => t.return_mode === "drift");
|
|
230
|
+
const honestSeeds = traces.filter(t => t.return_mode === "honest");
|
|
231
|
+
|
|
232
|
+
// Gate catch rate: % of drift-mode returns correctly flagged
|
|
233
|
+
const driftCaught = driftSeeds.filter(t => {
|
|
234
|
+
const va = verdictsA.find(v => v.seed_id === t.seed_id);
|
|
235
|
+
return va && va.verdict === "fail";
|
|
236
|
+
}).length;
|
|
237
|
+
const gate_catch_rate = driftSeeds.length > 0 ? driftCaught / driftSeeds.length : 1;
|
|
238
|
+
|
|
239
|
+
// False positive: % of honest returns incorrectly flagged
|
|
240
|
+
const honestFlagged = honestSeeds.filter(t => {
|
|
241
|
+
const va = verdictsA.find(v => v.seed_id === t.seed_id);
|
|
242
|
+
return va && va.verdict === "fail";
|
|
243
|
+
}).length;
|
|
244
|
+
const false_positive_rate = honestSeeds.length > 0 ? honestFlagged / honestSeeds.length : 0;
|
|
245
|
+
|
|
246
|
+
// False negative: % of drift returns that slipped through
|
|
247
|
+
const driftPassed = driftSeeds.filter(t => {
|
|
248
|
+
const va = verdictsA.find(v => v.seed_id === t.seed_id);
|
|
249
|
+
return va && va.verdict === "pass";
|
|
250
|
+
}).length;
|
|
251
|
+
const false_negative_rate = driftSeeds.length > 0 ? driftPassed / driftSeeds.length : 0;
|
|
252
|
+
|
|
253
|
+
// Layer agreement
|
|
254
|
+
const agreements = diffs.filter(d => !d.is_disagreement).length;
|
|
255
|
+
const layer_agreement_rate = diffs.length > 0 ? agreements / diffs.length : 1;
|
|
256
|
+
|
|
257
|
+
// Biggest miss
|
|
258
|
+
let biggest_miss = "";
|
|
259
|
+
if (diagnoses.length > 0) {
|
|
260
|
+
const worst = diagnoses.sort((a, b) =>
|
|
261
|
+
Math.abs(b.root_cause.threshold_margin) - Math.abs(a.root_cause.threshold_margin)
|
|
262
|
+
)[0]!;
|
|
263
|
+
biggest_miss = `${worst.root_cause.detail} (margin: ${worst.root_cause.threshold_margin.toFixed(4)})`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
run_id: runId,
|
|
268
|
+
seed_count: seeds.length,
|
|
269
|
+
gate_catch_rate,
|
|
270
|
+
false_positive_rate,
|
|
271
|
+
false_negative_rate,
|
|
272
|
+
layer_agreement_rate,
|
|
273
|
+
diagnosis_count: diagnoses.length,
|
|
274
|
+
escalation_count: escalations.length,
|
|
275
|
+
biggest_miss,
|
|
276
|
+
completed_at: new Date().toISOString(),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function emptySummary(): RunSummary {
|
|
281
|
+
return {
|
|
282
|
+
run_id: "",
|
|
283
|
+
seed_count: 0,
|
|
284
|
+
gate_catch_rate: 0,
|
|
285
|
+
false_positive_rate: 0,
|
|
286
|
+
false_negative_rate: 0,
|
|
287
|
+
layer_agreement_rate: 0,
|
|
288
|
+
diagnosis_count: 0,
|
|
289
|
+
escalation_count: 0,
|
|
290
|
+
completed_at: new Date().toISOString(),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { calibrateFlagsSchema, type CalibrateFlags } from "../types";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Parse /calibrate CLI flags
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
export function parseFlags(args: string): CalibrateFlags {
|
|
8
|
+
const tokens = args.trim().split(/\s+/);
|
|
9
|
+
const raw: Record<string, unknown> = {};
|
|
10
|
+
|
|
11
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
12
|
+
const tok = tokens[i]!;
|
|
13
|
+
|
|
14
|
+
if (tok === "--blind" && tokens[i + 1]) {
|
|
15
|
+
raw.blind = parseInt(tokens[++i]!, 10);
|
|
16
|
+
} else if (tok === "--adversarial" && tokens[i + 1]) {
|
|
17
|
+
raw.adversarial = parseInt(tokens[++i]!, 10);
|
|
18
|
+
} else if (tok === "--replay" && tokens[i + 1]) {
|
|
19
|
+
raw.replay = parseInt(tokens[++i]!, 10);
|
|
20
|
+
} else if (tok === "--adversarial-only") {
|
|
21
|
+
raw.adversarialOnly = true;
|
|
22
|
+
if (tokens[i + 1] && !tokens[i + 1]!.startsWith("--")) {
|
|
23
|
+
raw.adversarial = parseInt(tokens[++i]!, 10);
|
|
24
|
+
}
|
|
25
|
+
} else if (tok === "--replay-only") {
|
|
26
|
+
raw.replayOnly = true;
|
|
27
|
+
} else if (tok === "--retest") {
|
|
28
|
+
raw.retestOpen = true;
|
|
29
|
+
} else if (tok === "--review") {
|
|
30
|
+
raw.review = true;
|
|
31
|
+
} else if (tok === "--trends") {
|
|
32
|
+
raw.trends = true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Apply mode overrides
|
|
37
|
+
if (raw.adversarialOnly) {
|
|
38
|
+
raw.blind = 0;
|
|
39
|
+
raw.replay = 0;
|
|
40
|
+
if (!raw.adversarial) raw.adversarial = 8;
|
|
41
|
+
}
|
|
42
|
+
if (raw.replayOnly) {
|
|
43
|
+
raw.blind = 0;
|
|
44
|
+
raw.adversarial = 0;
|
|
45
|
+
if (!raw.replay) raw.replay = 10;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return calibrateFlagsSchema.parse(raw);
|
|
49
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { RunSummary, TrendEntry } from "../types";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Compact report formatter
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
function arrow(current: number, previous: number | undefined): string {
|
|
8
|
+
if (previous === undefined) return "";
|
|
9
|
+
if (current > previous + 0.01) return ` \u25B2 from ${(previous * 100).toFixed(0)}%`;
|
|
10
|
+
if (current < previous - 0.01) return ` \u25BC from ${(previous * 100).toFixed(0)}%`;
|
|
11
|
+
return " \u2500 stable";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatRunReport(summary: RunSummary, previousTrend?: TrendEntry): string {
|
|
15
|
+
const lines: string[] = [];
|
|
16
|
+
|
|
17
|
+
lines.push(`CALIBRATION RUN ${summary.completed_at}`);
|
|
18
|
+
lines.push(`Seeds: ${summary.seed_count}`);
|
|
19
|
+
lines.push("");
|
|
20
|
+
lines.push(`Gate catch rate: ${(summary.gate_catch_rate * 100).toFixed(0)}% (target: >90%)${arrow(summary.gate_catch_rate, previousTrend?.gate_catch_rate)}`);
|
|
21
|
+
lines.push(`False positive: ${(summary.false_positive_rate * 100).toFixed(0)}% (target: <10%)${arrow(summary.false_positive_rate, previousTrend?.false_positive_rate)}`);
|
|
22
|
+
lines.push(`False negative: ${(summary.false_negative_rate * 100).toFixed(0)}% (target: <5%)${arrow(summary.false_negative_rate, previousTrend?.false_negative_rate)}`);
|
|
23
|
+
lines.push(`Layer agreement: ${(summary.layer_agreement_rate * 100).toFixed(0)}%`);
|
|
24
|
+
lines.push("");
|
|
25
|
+
lines.push(`${summary.diagnosis_count} diagnosis card(s) generated`);
|
|
26
|
+
lines.push(`${summary.escalation_count} item(s) in escalation queue`);
|
|
27
|
+
|
|
28
|
+
if (summary.biggest_miss) {
|
|
29
|
+
lines.push("");
|
|
30
|
+
lines.push(`Biggest miss: ${summary.biggest_miss}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
lines.push("");
|
|
34
|
+
lines.push("Run: /calibrate --review to see escalation queue");
|
|
35
|
+
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function formatTrendsReport(trends: TrendEntry[]): string {
|
|
40
|
+
if (trends.length === 0) {
|
|
41
|
+
return "No calibration runs recorded yet.";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const lines: string[] = [];
|
|
45
|
+
lines.push("CALIBRATION TRENDS");
|
|
46
|
+
lines.push("==================");
|
|
47
|
+
lines.push("");
|
|
48
|
+
lines.push("Run ID | Catch | FP | FN | Agreement | Blind Pass");
|
|
49
|
+
lines.push("--- | --- | --- | --- | --- | ---");
|
|
50
|
+
|
|
51
|
+
for (const t of trends.slice(-10)) {
|
|
52
|
+
lines.push(
|
|
53
|
+
`${t.run_id.slice(0, 30).padEnd(32)}| ` +
|
|
54
|
+
`${(t.gate_catch_rate * 100).toFixed(0).padStart(3)}% | ` +
|
|
55
|
+
`${(t.false_positive_rate * 100).toFixed(0).padStart(3)}% | ` +
|
|
56
|
+
`${(t.false_negative_rate * 100).toFixed(0).padStart(3)}% | ` +
|
|
57
|
+
`${(t.layer_agreement_rate * 100).toFixed(0).padStart(7)}% | ` +
|
|
58
|
+
`${(t.pass_rate_blind * 100).toFixed(0).padStart(3)}%`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Direction summary
|
|
63
|
+
if (trends.length >= 2) {
|
|
64
|
+
const latest = trends[trends.length - 1]!;
|
|
65
|
+
const prev = trends[trends.length - 2]!;
|
|
66
|
+
lines.push("");
|
|
67
|
+
|
|
68
|
+
if (latest.false_negative_rate > prev.false_negative_rate) {
|
|
69
|
+
lines.push("WARNING: False negative rate is increasing. Machinery may be getting softer.");
|
|
70
|
+
}
|
|
71
|
+
if (latest.false_positive_rate > prev.false_positive_rate + 0.05) {
|
|
72
|
+
lines.push("WARNING: False positive rate spiked. Machinery may be getting paranoid.");
|
|
73
|
+
}
|
|
74
|
+
if (latest.pass_rate_blind > 0.9 && prev.pass_rate_blind > 0.9) {
|
|
75
|
+
lines.push("WARNING: Blind seed pass rate >90% for 2+ runs. Seeds may be too easy.");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return lines.join("\n");
|
|
80
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import type { EscalationItem } from "../types";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Escalation review interface
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Load all pending escalation items across all runs.
|
|
12
|
+
*/
|
|
13
|
+
export function loadPendingEscalations(basePath: string): EscalationItem[] {
|
|
14
|
+
const runsDir = join(basePath, "runs");
|
|
15
|
+
if (!existsSync(runsDir)) return [];
|
|
16
|
+
|
|
17
|
+
const pending: EscalationItem[] = [];
|
|
18
|
+
const runDirs = readdirSync(runsDir).sort();
|
|
19
|
+
|
|
20
|
+
for (const runDir of runDirs) {
|
|
21
|
+
const escDir = join(runsDir, runDir, "escalations");
|
|
22
|
+
if (!existsSync(escDir)) continue;
|
|
23
|
+
|
|
24
|
+
const files = readdirSync(escDir).filter(f => f.endsWith(".yaml"));
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
const content = readFileSync(join(escDir, file), "utf-8");
|
|
27
|
+
const item = YAML.parse(content) as EscalationItem;
|
|
28
|
+
if (item.human_judgment === "pending") {
|
|
29
|
+
pending.push(item);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return pending;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Format a single escalation item for human review.
|
|
39
|
+
*/
|
|
40
|
+
export function formatEscalationForReview(item: EscalationItem): string {
|
|
41
|
+
const lines: string[] = [];
|
|
42
|
+
|
|
43
|
+
lines.push(`ESCALATION: ${item.id}`);
|
|
44
|
+
lines.push(`Seed: ${item.seed_id}`);
|
|
45
|
+
lines.push("");
|
|
46
|
+
lines.push(`Layer A verdict: ${item.diff.layer_a_verdict}`);
|
|
47
|
+
lines.push(`Layer B verdict: ${item.diff.layer_b_verdict}`);
|
|
48
|
+
lines.push(`Return mode: ${item.diff.return_mode}`);
|
|
49
|
+
lines.push(`Category: ${item.diff.category}`);
|
|
50
|
+
lines.push("");
|
|
51
|
+
lines.push(`Reasoning: ${item.diff.reasoning}`);
|
|
52
|
+
|
|
53
|
+
if (item.diagnosis) {
|
|
54
|
+
lines.push("");
|
|
55
|
+
lines.push("DIAGNOSIS:");
|
|
56
|
+
lines.push(` Root cause: ${item.diagnosis.root_cause.detail}`);
|
|
57
|
+
if (item.diagnosis.root_cause.gate_that_should_have_fired) {
|
|
58
|
+
lines.push(` Gate that should have fired: ${item.diagnosis.root_cause.gate_that_should_have_fired}`);
|
|
59
|
+
lines.push(` Threshold margin: ${item.diagnosis.root_cause.threshold_margin.toFixed(4)}`);
|
|
60
|
+
}
|
|
61
|
+
lines.push(` Recommended fix: ${item.diagnosis.recommended_fix.type}`);
|
|
62
|
+
lines.push(` Justification: ${item.diagnosis.recommended_fix.justification}`);
|
|
63
|
+
if (item.diagnosis.recommended_fix.parameter) {
|
|
64
|
+
lines.push(` Parameter: ${item.diagnosis.recommended_fix.parameter} (${item.diagnosis.recommended_fix.current_value} -> ${item.diagnosis.recommended_fix.proposed_value})`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
lines.push("");
|
|
69
|
+
lines.push("Your judgment: agree_a | agree_b | neither");
|
|
70
|
+
|
|
71
|
+
return lines.join("\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Record a human judgment on an escalation item.
|
|
76
|
+
*/
|
|
77
|
+
export function recordJudgment(
|
|
78
|
+
basePath: string,
|
|
79
|
+
item: EscalationItem,
|
|
80
|
+
judgment: "agree_a" | "agree_b" | "neither",
|
|
81
|
+
reasoning: string,
|
|
82
|
+
): EscalationItem {
|
|
83
|
+
const updated: EscalationItem = {
|
|
84
|
+
...item,
|
|
85
|
+
human_judgment: judgment,
|
|
86
|
+
human_reasoning: reasoning,
|
|
87
|
+
reviewed_at: new Date().toISOString(),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Find and update the escalation file
|
|
91
|
+
const runsDir = join(basePath, "runs");
|
|
92
|
+
const runDirs = readdirSync(runsDir).sort();
|
|
93
|
+
|
|
94
|
+
for (const runDir of runDirs) {
|
|
95
|
+
const escDir = join(runsDir, runDir, "escalations");
|
|
96
|
+
if (!existsSync(escDir)) continue;
|
|
97
|
+
|
|
98
|
+
const files = readdirSync(escDir).filter(f => f.endsWith(".yaml"));
|
|
99
|
+
for (const file of files) {
|
|
100
|
+
const filePath = join(escDir, file);
|
|
101
|
+
const content = readFileSync(filePath, "utf-8");
|
|
102
|
+
const parsed = YAML.parse(content) as EscalationItem;
|
|
103
|
+
if (parsed.id === item.id) {
|
|
104
|
+
writeFileSync(filePath, YAML.stringify(updated), "utf-8");
|
|
105
|
+
return updated;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return updated;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Count pending escalations.
|
|
115
|
+
*/
|
|
116
|
+
export function countPendingEscalations(basePath: string): number {
|
|
117
|
+
return loadPendingEscalations(basePath).length;
|
|
118
|
+
}
|