crewhaus 0.1.8 → 0.2.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 +10 -3
- package/dist/advice-apply.d.ts +182 -0
- package/dist/advice-apply.js +286 -0
- package/dist/advise-rules.d.ts +348 -0
- package/dist/advise-rules.js +905 -0
- package/dist/alert-sink.d.ts +48 -0
- package/dist/alert-sink.js +86 -0
- package/dist/approval-gate.d.ts +127 -0
- package/dist/approval-gate.js +254 -0
- package/dist/audit-verify.d.ts +69 -0
- package/dist/audit-verify.js +97 -0
- package/dist/autodistill.d.ts +113 -0
- package/dist/autodistill.js +256 -0
- package/dist/channel-provision.d.ts +360 -0
- package/dist/channel-provision.js +881 -0
- package/dist/ci-scaffold.d.ts +31 -0
- package/dist/ci-scaffold.js +343 -0
- package/dist/compile-check.d.ts +90 -0
- package/dist/compile-check.js +285 -0
- package/dist/compliance-schedule.d.ts +35 -0
- package/dist/compliance-schedule.js +36 -0
- package/dist/context-pressure.d.ts +80 -0
- package/dist/context-pressure.js +166 -0
- package/dist/dataset-mine.d.ts +173 -0
- package/dist/dataset-mine.js +404 -0
- package/dist/datasets.d.ts +124 -0
- package/dist/datasets.js +260 -0
- package/dist/deploy-canary.d.ts +83 -0
- package/dist/deploy-canary.js +87 -0
- package/dist/doctor-checks.d.ts +33 -0
- package/dist/doctor-checks.js +92 -0
- package/dist/doctor-detect.d.ts +108 -0
- package/dist/doctor-detect.js +214 -0
- package/dist/doctor-fix.d.ts +81 -0
- package/dist/doctor-fix.js +164 -0
- package/dist/egress-triage.d.ts +121 -0
- package/dist/egress-triage.js +261 -0
- package/dist/eval-bridge.d.ts +114 -0
- package/dist/eval-bridge.js +158 -0
- package/dist/eval-coverage.d.ts +140 -0
- package/dist/eval-coverage.js +428 -0
- package/dist/eval-history.d.ts +48 -0
- package/dist/eval-history.js +157 -0
- package/dist/eval-matrix.d.ts +80 -0
- package/dist/eval-matrix.js +182 -0
- package/dist/eval-sentinel.d.ts +65 -0
- package/dist/eval-sentinel.js +132 -0
- package/dist/faq.d.ts +68 -0
- package/dist/faq.js +168 -0
- package/dist/feedback.d.ts +9 -2
- package/dist/feedback.js +17 -7
- package/dist/fewshot.d.ts +83 -0
- package/dist/fewshot.js +158 -0
- package/dist/fleet.d.ts +207 -0
- package/dist/fleet.js +488 -0
- package/dist/flywheel.d.ts +193 -0
- package/dist/flywheel.js +519 -0
- package/dist/graders-suggest.d.ts +186 -0
- package/dist/graders-suggest.js +658 -0
- package/dist/incident.d.ts +99 -0
- package/dist/incident.js +217 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +11664 -963
- package/dist/init-interactive.d.ts +105 -0
- package/dist/init-interactive.js +208 -0
- package/dist/intents.d.ts +105 -0
- package/dist/intents.js +292 -0
- package/dist/judge-calibrate.d.ts +137 -0
- package/dist/judge-calibrate.js +247 -0
- package/dist/justification-calibrate.d.ts +150 -0
- package/dist/justification-calibrate.js +262 -0
- package/dist/justification-gate.d.ts +27 -6
- package/dist/justification-gate.js +30 -6
- package/dist/knowledge-sync.d.ts +179 -0
- package/dist/knowledge-sync.js +551 -0
- package/dist/lessons.d.ts +87 -0
- package/dist/lessons.js +207 -0
- package/dist/lint.d.ts +127 -0
- package/dist/lint.js +226 -0
- package/dist/loadtest.d.ts +114 -0
- package/dist/loadtest.js +196 -0
- package/dist/marketplace-cli.d.ts +110 -0
- package/dist/marketplace-cli.js +250 -0
- package/dist/mcp-doctor.d.ts +121 -0
- package/dist/mcp-doctor.js +249 -0
- package/dist/model-scan.d.ts +116 -0
- package/dist/model-scan.js +226 -0
- package/dist/onchain-tune.d.ts +164 -0
- package/dist/onchain-tune.js +346 -0
- package/dist/permissions-suggest.d.ts +126 -0
- package/dist/permissions-suggest.js +333 -0
- package/dist/pii-tune.d.ts +107 -0
- package/dist/pii-tune.js +122 -0
- package/dist/propose.d.ts +117 -0
- package/dist/propose.js +184 -0
- package/dist/refresh-goldens.d.ts +82 -0
- package/dist/refresh-goldens.js +221 -0
- package/dist/regression-pin.d.ts +160 -0
- package/dist/regression-pin.js +281 -0
- package/dist/retention.d.ts +193 -0
- package/dist/retention.js +607 -0
- package/dist/retire.d.ts +118 -0
- package/dist/retire.js +291 -0
- package/dist/right-size.d.ts +100 -0
- package/dist/right-size.js +123 -0
- package/dist/route.d.ts +19 -0
- package/dist/route.js +90 -0
- package/dist/scaffold-evals.d.ts +138 -0
- package/dist/scaffold-evals.js +410 -0
- package/dist/scope-audit-drift.d.ts +139 -0
- package/dist/scope-audit-drift.js +260 -0
- package/dist/security-corpus.d.ts +237 -0
- package/dist/security-corpus.js +516 -0
- package/dist/security-digest.d.ts +174 -0
- package/dist/security-digest.js +656 -0
- package/dist/sessions-index.d.ts +27 -0
- package/dist/sessions-index.js +51 -0
- package/dist/slo-doctor.d.ts +67 -0
- package/dist/slo-doctor.js +119 -0
- package/dist/slo-sink.d.ts +96 -0
- package/dist/slo-sink.js +107 -0
- package/dist/spec-changelog.d.ts +102 -0
- package/dist/spec-changelog.js +237 -0
- package/dist/state-backup.d.ts +223 -0
- package/dist/state-backup.js +648 -0
- package/dist/tools-cli.d.ts +170 -0
- package/dist/tools-cli.js +300 -0
- package/dist/triage.d.ts +202 -0
- package/dist/triage.js +403 -0
- package/dist/upgrade.d.ts +57 -0
- package/dist/upgrade.js +113 -0
- package/dist/version.d.ts +6 -0
- package/dist/version.js +27 -0
- package/dist/voice-eval.d.ts +138 -0
- package/dist/voice-eval.js +309 -0
- package/dist/watch.d.ts +58 -0
- package/dist/watch.js +97 -0
- package/package.json +90 -65
package/dist/triage.js
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item 7 — wire the Track B failure arbiter (eval-optimizer-orchestrator/
|
|
3
|
+
* src/failure-arbiter.ts) into `crewhaus eval` and `crewhaus optimize`.
|
|
4
|
+
*
|
|
5
|
+
* Post-eval (`finishEvalTriage`): every failing sample is classified into
|
|
6
|
+
* bug / spec-gap / noise / contract-ambiguity via `arbitrate()`, the run
|
|
7
|
+
* roll-up comes from `aggregate()`, and the verdicts persist ADDITIVELY as
|
|
8
|
+
* `verdicts.json` NEXT TO `results.json` (a sibling file, not a section
|
|
9
|
+
* inside it: results.json is eval-runner's single-writer artifact, parsed
|
|
10
|
+
* as a typed `EvalRunSummary` by eval-report's loadRun/diffReports —
|
|
11
|
+
* rewriting it post-hoc from the CLI would make it double-writer and let
|
|
12
|
+
* foreign keys drift into the runner's snapshot). Bug-class verdicts carry
|
|
13
|
+
* `ArbiterAction.promoteRegression: true` ("fix impl; promote regression"),
|
|
14
|
+
* and such samples MAY be pinned into the per-spec `<specName>-regressions`
|
|
15
|
+
* suite via regression-pin's `pinRecoveredSamples` with `source:
|
|
16
|
+
* "failure-arbiter"` provenance — but the pin is heavily guarded:
|
|
17
|
+
*
|
|
18
|
+
* - Samples already in the run's OWN dataset are never pinned. Pinning
|
|
19
|
+
* exists to capture samples so later dataset pruning can't lose them; a
|
|
20
|
+
* sample the run just evaled is already covered, so pinning it is pure
|
|
21
|
+
* suite-version churn. (Unguarded, that churn armed a gate-disarm loop:
|
|
22
|
+
* fail → pin → the next run's union suffix rewrote the baseline key →
|
|
23
|
+
* "first run" pinned the FAILING run as the new baseline.)
|
|
24
|
+
* - Samples whose failure is an ERROR (`SampleResult.error` /
|
|
25
|
+
* `.graderError`) are never pinned — an outage is not a regression to
|
|
26
|
+
* guard, even when the arbiter's default rule labels it "bug".
|
|
27
|
+
* - When the whole run looks infrastructure-failed (every failing sample
|
|
28
|
+
* errored, or the error rate is past {@link INFRA_FAILURE_ERROR_RATE}),
|
|
29
|
+
* pinning is skipped entirely with a warning — mirroring eval-matrix's
|
|
30
|
+
* `cellCrashReason` "the cell never really ran" heuristic.
|
|
31
|
+
*
|
|
32
|
+
* Optimize (`triageFitnessSamples`): the same classification runs after
|
|
33
|
+
* each candidate's fitness eval, and noise (flaky infra) samples plus
|
|
34
|
+
* contract-ambiguity verdicts BACKED BY STRUCTURED GRADER EVIDENCE
|
|
35
|
+
* (`graderOutput.acceptable` / `.multipleAcceptable` — bad gold, confirmed)
|
|
36
|
+
* are withheld from the failure signal the mutator sees
|
|
37
|
+
* (`OptimizerState.bestGrades`) — mutating the prompt against them wastes
|
|
38
|
+
* budget. Contract-ambiguity verdicts from the arbiter's NO-REFERENCE
|
|
39
|
+
* heuristic (score > 0 with no gold answer) are NOT excluded: on
|
|
40
|
+
* judge-graded / ratings-distilled datasets no sample has a reference, so
|
|
41
|
+
* the heuristic fires for most failures and a sticky exclusion would
|
|
42
|
+
* starve the mutator of its entire failure signal. Both kinds surface in
|
|
43
|
+
* the printed "dataset-fix queue" at the end of the run; only the
|
|
44
|
+
* evidence-backed ids stay sticky-excluded across iterations.
|
|
45
|
+
*
|
|
46
|
+
* Field mapping caveat, faithful to the arbiter's source: the arbiter
|
|
47
|
+
* reads `sample.reference` (via cast — not part of the dataset schema) as
|
|
48
|
+
* "the unique correct answer, when present"; the dataset schema spells
|
|
49
|
+
* that field `expected_output`, so {@link toFailingSample} surfaces it AS
|
|
50
|
+
* `reference`. The rules keyed on `graderOutput` (`acceptable`,
|
|
51
|
+
* `multipleAcceptable`, `addressedByImpl`/`inContract`) cannot fire today
|
|
52
|
+
* because `GradeResult` carries no structured output — the field is left
|
|
53
|
+
* undefined rather than fabricated, and those rules light up as soon as a
|
|
54
|
+
* grader supplies structured output.
|
|
55
|
+
*
|
|
56
|
+
* Kept in a side-effect-free module (the CLI entry file runs an argv
|
|
57
|
+
* switch on import) mirroring `eval-history.ts` / `regression-pin.ts`:
|
|
58
|
+
* placement here (not in eval-optimizer-orchestrator) is deliberate — the
|
|
59
|
+
* orchestrator is intentionally decoupled from eval-runner, while this
|
|
60
|
+
* wiring joins eval-runner artifacts, dataset Samples, and the CLI-owned
|
|
61
|
+
* pinning helper. `finishEvalTriage` is best-effort BY CONSTRUCTION: every
|
|
62
|
+
* stage warns and continues instead of throwing, so a triage failure can
|
|
63
|
+
* never break an otherwise successful eval.
|
|
64
|
+
*/
|
|
65
|
+
import { writeFileSync } from "node:fs";
|
|
66
|
+
import { join } from "node:path";
|
|
67
|
+
import { aggregate as aggregateVerdicts, arbitrate, } from "@crewhaus/eval-optimizer-orchestrator";
|
|
68
|
+
import { pinRecoveredSamples } from "./regression-pin";
|
|
69
|
+
/** Fixed class order for every printed/rendered counts line. */
|
|
70
|
+
export const FAILURE_CLASS_ORDER = [
|
|
71
|
+
"bug",
|
|
72
|
+
"spec-gap",
|
|
73
|
+
"noise",
|
|
74
|
+
"contract-ambiguity",
|
|
75
|
+
];
|
|
76
|
+
/** A failing result: errored, or graded and not passed (errors also grade
|
|
77
|
+
* `passed: false`, but the explicit disjunction keeps the intent legible). */
|
|
78
|
+
export function isFailing(result) {
|
|
79
|
+
return result.error !== undefined || !result.grades.overall.passed;
|
|
80
|
+
}
|
|
81
|
+
/** True when the failure is an ERROR — the invoker failed or a grader
|
|
82
|
+
* threw — rather than an honest graded verdict on the agent's output. */
|
|
83
|
+
export function isErroredResult(result) {
|
|
84
|
+
return result.error !== undefined || result.graderError !== undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Join one failing `SampleResult` back to its dataset `Sample` and shape
|
|
88
|
+
* the pair into the arbiter's `FailingSample` input. See the module doc
|
|
89
|
+
* for the `expected_output` → `reference` mapping and the (deliberately)
|
|
90
|
+
* absent `graderOutput`. A grader-infra failure (`graderError`, e.g. a
|
|
91
|
+
* judge 429 on both attempts) surfaces as `errorMessage` too, so the
|
|
92
|
+
* arbiter's transient-marker rule can classify it as noise. A missing
|
|
93
|
+
* dataset sample (should not happen — the CLI tees every sample the runner
|
|
94
|
+
* sees) degrades to a minimal stand-in with no reference rather than
|
|
95
|
+
* skipping the sample.
|
|
96
|
+
*/
|
|
97
|
+
export function toFailingSample(result, sample) {
|
|
98
|
+
const base = sample ?? {
|
|
99
|
+
id: result.sampleId,
|
|
100
|
+
input: "(sample not found in dataset)",
|
|
101
|
+
};
|
|
102
|
+
const arbSample = base.expected_output !== undefined ? { ...base, reference: base.expected_output } : base;
|
|
103
|
+
const errorMessage = result.error ?? result.graderError;
|
|
104
|
+
return {
|
|
105
|
+
sample: arbSample,
|
|
106
|
+
actual: result.agentOutput,
|
|
107
|
+
score: result.grades.overall.score,
|
|
108
|
+
...(errorMessage !== undefined ? { errorMessage } : {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Classify every failing sample of a completed eval run. Returns undefined
|
|
113
|
+
* when nothing failed — there is nothing to arbitrate, and persisting
|
|
114
|
+
* `aggregate([])`'s degenerate roll-up (dominant contract-ambiguity over
|
|
115
|
+
* zero samples) would mislead.
|
|
116
|
+
*/
|
|
117
|
+
export function triageEvalRun(opts) {
|
|
118
|
+
const failing = opts.samples.filter(isFailing);
|
|
119
|
+
if (failing.length === 0)
|
|
120
|
+
return undefined;
|
|
121
|
+
const inputs = failing.map((r) => toFailingSample(r, opts.samplesById.get(r.sampleId)));
|
|
122
|
+
const verdicts = failing.map((r, i) => {
|
|
123
|
+
const v = arbitrate(inputs[i]);
|
|
124
|
+
return { sampleId: r.sampleId, class: v.class, action: v.action, reason: v.reason };
|
|
125
|
+
});
|
|
126
|
+
const agg = aggregateVerdicts(inputs);
|
|
127
|
+
return {
|
|
128
|
+
source: "failure-arbiter",
|
|
129
|
+
runId: opts.runId,
|
|
130
|
+
arbitratedAt: (opts.now?.() ?? new Date()).toISOString(),
|
|
131
|
+
total: agg.total,
|
|
132
|
+
counts: agg.counts,
|
|
133
|
+
dominantClass: agg.dominantClass,
|
|
134
|
+
recommendedAction: agg.recommendedAction,
|
|
135
|
+
verdicts,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/** `triage: 2 bug, 1 spec-gap, 3 noise, 1 contract-ambiguity` — all four
|
|
139
|
+
* classes always print (stable shape for log scrapers), in fixed order. */
|
|
140
|
+
export function formatTriageSummary(counts) {
|
|
141
|
+
return `triage: ${FAILURE_CLASS_ORDER.map((c) => `${counts[c]} ${c}`).join(", ")}`;
|
|
142
|
+
}
|
|
143
|
+
/** Persist verdicts as `<outDir>/verdicts.json`; returns the path. */
|
|
144
|
+
export function writeRunVerdicts(outDir, verdicts) {
|
|
145
|
+
const path = join(outDir, "verdicts.json");
|
|
146
|
+
writeFileSync(path, JSON.stringify(verdicts, null, 2));
|
|
147
|
+
return path;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The post-eval triage flow: arbitrate → persist verdicts.json → print the
|
|
151
|
+
* one-line summary → pin the promoteRegression (bug-class) samples. Never
|
|
152
|
+
* throws — each stage is isolated so e.g. an unwritable run dir still
|
|
153
|
+
* leaves the verdicts available for the HTML report, and a broken registry
|
|
154
|
+
* only skips the pin. Returns the verdicts (for the report renderer), or
|
|
155
|
+
* undefined when nothing failed or classification itself broke.
|
|
156
|
+
*/
|
|
157
|
+
export async function finishEvalTriage(opts) {
|
|
158
|
+
const write = opts.write ?? ((line) => process.stdout.write(`${line}\n`));
|
|
159
|
+
const warn = opts.warn ?? ((line) => process.stderr.write(`${line}\n`));
|
|
160
|
+
let verdicts;
|
|
161
|
+
try {
|
|
162
|
+
verdicts = triageEvalRun({
|
|
163
|
+
samples: opts.samples,
|
|
164
|
+
samplesById: opts.samplesById,
|
|
165
|
+
runId: opts.runId,
|
|
166
|
+
...(opts.now !== undefined ? { now: opts.now } : {}),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
warn(`[eval] triage skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
if (verdicts === undefined)
|
|
174
|
+
return undefined;
|
|
175
|
+
write(`[eval] ${formatTriageSummary(verdicts.counts)}`);
|
|
176
|
+
try {
|
|
177
|
+
write(`[eval] triage verdicts: ${writeRunVerdicts(opts.outDir, verdicts)}`);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
warn(`[eval] triage: verdicts.json not written (${err instanceof Error ? err.message : String(err)})`);
|
|
181
|
+
}
|
|
182
|
+
// promoteRegression wiring: the arbiter's bug verdicts carry
|
|
183
|
+
// `{ kind: "fix-impl", promoteRegression: true }` — "fix the impl and
|
|
184
|
+
// promote this sample to the regression suite". Promote exactly the
|
|
185
|
+
// samples whose action carries the flag (checked structurally, so a
|
|
186
|
+
// future arbiter attaching it to another class is honored too) — MINUS
|
|
187
|
+
// the guards the module doc describes: no run-level infrastructure
|
|
188
|
+
// failure, no errored samples, and no samples the run's own dataset
|
|
189
|
+
// already contains (which, on this path, is every sample the tee saw —
|
|
190
|
+
// the pin stays wired for provenance and for callers whose triaged
|
|
191
|
+
// samples aren't dataset members, but it must never churn the suite with
|
|
192
|
+
// samples the eval already guards; see the F1 gate-disarm loop).
|
|
193
|
+
if (opts.pin !== false && opts.registry !== undefined) {
|
|
194
|
+
const infraReason = runLooksInfrastructureFailed(opts.samples);
|
|
195
|
+
if (infraReason !== undefined) {
|
|
196
|
+
warn(`[eval] triage: run looks infrastructure-failed (${infraReason}) — skipping triage pinning`);
|
|
197
|
+
return verdicts;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const pin = await promoteArbiterSamples({
|
|
201
|
+
registry: opts.registry,
|
|
202
|
+
specName: opts.specName,
|
|
203
|
+
verdicts,
|
|
204
|
+
samplesById: opts.samplesById,
|
|
205
|
+
sourceDataset: opts.sourceDataset,
|
|
206
|
+
runId: opts.runId,
|
|
207
|
+
datasetMemberIds: new Set(opts.samplesById.keys()),
|
|
208
|
+
erroredIds: new Set(opts.samples.filter(isErroredResult).map((s) => s.sampleId)),
|
|
209
|
+
warn,
|
|
210
|
+
...(opts.now !== undefined ? { now: opts.now } : {}),
|
|
211
|
+
});
|
|
212
|
+
if (pin.pinned > 0) {
|
|
213
|
+
write(`[eval] triage: pinned ${pin.pinned} bug sample(s) → ${pin.suiteName}@${pin.version} (failure-arbiter)`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
warn(`[eval] triage: regression pinning skipped (${err instanceof Error ? err.message : String(err)})`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return verdicts;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Error-rate threshold past which a run is presumed infrastructure-failed
|
|
224
|
+
* (credentials revoked mid-run, provider outage) even when some samples
|
|
225
|
+
* still failed on grades — a mass of "bug" verdicts from an outage must not
|
|
226
|
+
* feed the regression pin.
|
|
227
|
+
*/
|
|
228
|
+
export const INFRA_FAILURE_ERROR_RATE = 0.5;
|
|
229
|
+
/**
|
|
230
|
+
* Run-level outage guard, mirroring eval-matrix's `cellCrashReason`: when
|
|
231
|
+
* every FAILING sample is an error (invoker or grader), or the run's error
|
|
232
|
+
* rate exceeds {@link INFRA_FAILURE_ERROR_RATE}, the run looks
|
|
233
|
+
* infrastructure-failed and triage must not pin anything from it. Returns
|
|
234
|
+
* the human-readable reason, or undefined when the run looks real.
|
|
235
|
+
*/
|
|
236
|
+
export function runLooksInfrastructureFailed(samples) {
|
|
237
|
+
const failing = samples.filter(isFailing);
|
|
238
|
+
if (failing.length === 0)
|
|
239
|
+
return undefined;
|
|
240
|
+
const errored = samples.filter(isErroredResult);
|
|
241
|
+
if (failing.every(isErroredResult)) {
|
|
242
|
+
return `all ${failing.length} failing sample(s) errored`;
|
|
243
|
+
}
|
|
244
|
+
if (errored.length / samples.length > INFRA_FAILURE_ERROR_RATE) {
|
|
245
|
+
return `${errored.length}/${samples.length} sample(s) errored`;
|
|
246
|
+
}
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
/** True when an arbiter action asks for regression promotion. */
|
|
250
|
+
export function actionPromotesRegression(action) {
|
|
251
|
+
return "promoteRegression" in action && action.promoteRegression === true;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Pin every sample whose verdict action carries `promoteRegression: true`
|
|
255
|
+
* into the per-spec regression suite, tagging the pin's provenance with
|
|
256
|
+
* `source: "failure-arbiter"` (see regression-pin's metadata.regression_pin).
|
|
257
|
+
* Samples the dataset tee somehow missed are skipped — a pin must append
|
|
258
|
+
* the sample as it lives in the source dataset, or not at all.
|
|
259
|
+
*
|
|
260
|
+
* Two candidate filters (see the module doc):
|
|
261
|
+
* - `datasetMemberIds` — samples the run's own dataset already contains are
|
|
262
|
+
* never pinned: zero coverage gain, and the resulting suite-version churn
|
|
263
|
+
* is what armed the fail→pin→retry gate-disarm loop.
|
|
264
|
+
* - `erroredIds` — samples whose failure is an ERROR are never pinned: an
|
|
265
|
+
* outage (e.g. "401 invalid x-api-key", which the arbiter's default rule
|
|
266
|
+
* labels "bug") is not a regression to guard.
|
|
267
|
+
*/
|
|
268
|
+
export async function promoteArbiterSamples(opts) {
|
|
269
|
+
const promoted = [];
|
|
270
|
+
for (const v of opts.verdicts.verdicts) {
|
|
271
|
+
if (!actionPromotesRegression(v.action))
|
|
272
|
+
continue;
|
|
273
|
+
if (opts.datasetMemberIds?.has(v.sampleId) === true)
|
|
274
|
+
continue;
|
|
275
|
+
if (opts.erroredIds?.has(v.sampleId) === true)
|
|
276
|
+
continue;
|
|
277
|
+
const sample = opts.samplesById.get(v.sampleId);
|
|
278
|
+
if (sample !== undefined)
|
|
279
|
+
promoted.push(sample);
|
|
280
|
+
}
|
|
281
|
+
return pinRecoveredSamples({
|
|
282
|
+
registry: opts.registry,
|
|
283
|
+
specName: opts.specName,
|
|
284
|
+
samples: promoted,
|
|
285
|
+
sourceDataset: opts.sourceDataset,
|
|
286
|
+
optimizeRunId: opts.runId,
|
|
287
|
+
source: "failure-arbiter",
|
|
288
|
+
...(opts.warn !== undefined ? { warn: opts.warn } : {}),
|
|
289
|
+
...(opts.now !== undefined ? { now: opts.now } : {}),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
// -------- optimize pre-filter --------
|
|
293
|
+
/** The exclusion policy for the mutator's failure signal: noise (flaky
|
|
294
|
+
* infra) and contract-ambiguity (bad gold) waste mutation budget; bug and
|
|
295
|
+
* spec-gap are real prompt-fixable signal and stay. NOTE: a
|
|
296
|
+
* contract-ambiguity verdict is only actually excluded when it is backed
|
|
297
|
+
* by structured grader evidence — see {@link hasStructuredAmbiguityEvidence}
|
|
298
|
+
* and the module doc (the no-reference heuristic fires for MOST failures
|
|
299
|
+
* on judge-graded datasets, and excluding those would starve the mutator). */
|
|
300
|
+
export function isExcludedClass(cls) {
|
|
301
|
+
return cls === "noise" || cls === "contract-ambiguity";
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* True when a contract-ambiguity verdict is backed by the grader's OWN
|
|
305
|
+
* structured output (`acceptable` / `multipleAcceptable`) rather than the
|
|
306
|
+
* arbiter's no-reference heuristic. Today `toFailingSample` never fabricates
|
|
307
|
+
* `graderOutput` (GradeResult carries no structured output), so this lights
|
|
308
|
+
* up as soon as a grader supplies it — until then every contract-ambiguity
|
|
309
|
+
* verdict in the optimize loop is heuristic and stays in the mutator signal.
|
|
310
|
+
*/
|
|
311
|
+
export function hasStructuredAmbiguityEvidence(failing) {
|
|
312
|
+
return (failing.graderOutput?.["acceptable"] === true ||
|
|
313
|
+
failing.graderOutput?.["multipleAcceptable"] === true);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Arbitrate one fitness call's results for the optimize loop. Ids already
|
|
317
|
+
* in `alreadyAmbiguous` (the caller's sticky exclusion set — evidence-backed
|
|
318
|
+
* contract-ambiguity only) are excluded WITHOUT re-arbitration ("exclude
|
|
319
|
+
* them from mutation targets across iterations"): a different candidate's
|
|
320
|
+
* output could flip the class, but the underlying dataset/contract problem
|
|
321
|
+
* is unchanged, so the exclusion is sticky.
|
|
322
|
+
*/
|
|
323
|
+
export function triageFitnessSamples(opts) {
|
|
324
|
+
const counts = {
|
|
325
|
+
bug: 0,
|
|
326
|
+
"spec-gap": 0,
|
|
327
|
+
noise: 0,
|
|
328
|
+
"contract-ambiguity": 0,
|
|
329
|
+
};
|
|
330
|
+
const excluded = new Set();
|
|
331
|
+
const ambiguous = [];
|
|
332
|
+
let carried = 0;
|
|
333
|
+
for (const r of opts.samples) {
|
|
334
|
+
if (opts.alreadyAmbiguous.has(r.sampleId)) {
|
|
335
|
+
excluded.add(r.sampleId);
|
|
336
|
+
carried += 1;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (!isFailing(r))
|
|
340
|
+
continue;
|
|
341
|
+
const failing = toFailingSample(r, opts.samplesById.get(r.sampleId));
|
|
342
|
+
const v = arbitrate(failing);
|
|
343
|
+
counts[v.class] += 1;
|
|
344
|
+
if (!isExcludedClass(v.class))
|
|
345
|
+
continue;
|
|
346
|
+
if (v.class === "contract-ambiguity") {
|
|
347
|
+
const fromGraderEvidence = hasStructuredAmbiguityEvidence(failing);
|
|
348
|
+
ambiguous.push({ sampleId: r.sampleId, reason: v.reason, fromGraderEvidence });
|
|
349
|
+
// Heuristic (no-reference) ambiguity keeps feeding the mutator: on
|
|
350
|
+
// judge-graded / no-gold datasets it IS the failure signal (F5).
|
|
351
|
+
if (!fromGraderEvidence)
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
excluded.add(r.sampleId);
|
|
355
|
+
}
|
|
356
|
+
return { excluded, counts, ambiguous, carried };
|
|
357
|
+
}
|
|
358
|
+
/** One observable log line per fitness call that excluded or queued
|
|
359
|
+
* anything; undefined when the triage was a no-op (keep the log quiet). */
|
|
360
|
+
export function formatFitnessTriageLine(t) {
|
|
361
|
+
if (t.excluded.size === 0 && t.ambiguous.length === 0)
|
|
362
|
+
return undefined;
|
|
363
|
+
const excludedAmbiguous = t.ambiguous.filter((a) => a.fromGraderEvidence).length;
|
|
364
|
+
const heuristicAmbiguous = t.ambiguous.length - excludedAmbiguous;
|
|
365
|
+
const queued = t.carried > 0 ? ` (+${t.carried} queued)` : "";
|
|
366
|
+
const heuristic = heuristicAmbiguous > 0
|
|
367
|
+
? `, ${heuristicAmbiguous} heuristic contract-ambiguity (queued for dataset fix, kept in signal)`
|
|
368
|
+
: "";
|
|
369
|
+
return (`triage: excluded ${t.counts.noise} noise, ${excludedAmbiguous} contract-ambiguity${queued} ` +
|
|
370
|
+
`from mutation signal; kept ${t.counts.bug} bug, ${t.counts["spec-gap"]} spec-gap${heuristic}`);
|
|
371
|
+
}
|
|
372
|
+
/** Render the end-of-optimize dataset-fix queue (contract-ambiguity ids +
|
|
373
|
+
* arbiter rationale). Empty queue → no lines (print nothing). */
|
|
374
|
+
export function formatDatasetFixQueue(queue) {
|
|
375
|
+
if (queue.size === 0)
|
|
376
|
+
return [];
|
|
377
|
+
const lines = [
|
|
378
|
+
`dataset-fix queue: ${queue.size} contract-ambiguity sample(s) — fix the dataset/contract, not the prompt:`,
|
|
379
|
+
];
|
|
380
|
+
for (const [sampleId, reason] of queue) {
|
|
381
|
+
lines.push(` - ${sampleId}: ${reason}`);
|
|
382
|
+
}
|
|
383
|
+
return lines;
|
|
384
|
+
}
|
|
385
|
+
// -------- sample tee --------
|
|
386
|
+
/**
|
|
387
|
+
* Tee an async sample stream into `sink` (first occurrence of each id
|
|
388
|
+
* wins, matching the union's primary-wins dedupe) while passing every
|
|
389
|
+
* sample through unchanged. Lets the eval flow hand the runner a stream
|
|
390
|
+
* AND retain the Sample objects triage/pinning need afterwards — the
|
|
391
|
+
* runner materializes the stream anyway, so this adds no memory ceiling.
|
|
392
|
+
*/
|
|
393
|
+
export function tapSamples(source, sink) {
|
|
394
|
+
return {
|
|
395
|
+
async *[Symbol.asyncIterator]() {
|
|
396
|
+
for await (const s of source) {
|
|
397
|
+
if (!sink.has(s.id))
|
|
398
|
+
sink.set(s.id, s);
|
|
399
|
+
yield s;
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { MigrationEngine, SpecObject } from "@crewhaus/migration-engine";
|
|
2
|
+
import { type SpecDiffEntry } from "@crewhaus/spec-patch";
|
|
3
|
+
/**
|
|
4
|
+
* Item 43 — `crewhaus upgrade`. The single-spec upgrade assistant: detect the
|
|
5
|
+
* cwd spec's schema version, run the migration-engine chain up to the CLI's
|
|
6
|
+
* current spec version, validate the migrated spec, and show a diff. `--write`
|
|
7
|
+
* applies.
|
|
8
|
+
*
|
|
9
|
+
* Distinct from `crewhaus migrate-all` (which walks the whole spec-REGISTRY):
|
|
10
|
+
* `upgrade` operates on the working spec FILE the author edits. Crucially it
|
|
11
|
+
* passes a `validate` callback — the CLI's `migrate-all` passed none, so a
|
|
12
|
+
* migrated spec was written UNCHECKED; here every migrated spec must parse
|
|
13
|
+
* before it can be written.
|
|
14
|
+
*
|
|
15
|
+
* Side-effect-free: `planUpgrade` is a pure function over the spec text + an
|
|
16
|
+
* injected engine + validator. The CLI wrapper reads/writes the file and prints.
|
|
17
|
+
*/
|
|
18
|
+
export type UpgradeAction = "up-to-date" | "upgrade" | "ahead" | "validate-fail";
|
|
19
|
+
export type UpgradePlan = {
|
|
20
|
+
readonly action: UpgradeAction;
|
|
21
|
+
/** The spec's current schema version (`version ?? 0`). */
|
|
22
|
+
readonly fromVersion: number;
|
|
23
|
+
/** The CLI's current spec-schema version (engine.latestVersion()). */
|
|
24
|
+
readonly toVersion: number;
|
|
25
|
+
/** The migrated YAML — present only for `action: "upgrade"`. */
|
|
26
|
+
readonly migratedYaml?: string;
|
|
27
|
+
/** Field-level diff (old → migrated) — present for `action: "upgrade"`. */
|
|
28
|
+
readonly diff?: ReadonlyArray<SpecDiffEntry>;
|
|
29
|
+
/** Failure detail for `action: "validate-fail"`. */
|
|
30
|
+
readonly error?: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Compute the upgrade plan for a spec's YAML against `engine`.
|
|
34
|
+
*
|
|
35
|
+
* - `up-to-date` : the spec is already at the current version → no-op.
|
|
36
|
+
* - `ahead` : the spec's version is HIGHER than the CLI supports
|
|
37
|
+
* (a newer-CLI-authored spec) → refuse, don't downgrade.
|
|
38
|
+
* - `upgrade` : migrate up, validate, and produce the diff.
|
|
39
|
+
* - `validate-fail`: the migrated spec failed the injected validator (the
|
|
40
|
+
* migration produced an invalid spec) — never written.
|
|
41
|
+
*
|
|
42
|
+
* `validate` receives the migrated spec object + throws on invalid (the CLI
|
|
43
|
+
* wires `parseSpec(stringifyYaml(spec))`). Migrations that throw are reported
|
|
44
|
+
* as `validate-fail` too, so a broken migration chain never crashes upgrade.
|
|
45
|
+
*/
|
|
46
|
+
export declare function planUpgrade(yamlText: string, engine: MigrationEngine, validate: (spec: SpecObject) => void): UpgradePlan;
|
|
47
|
+
/**
|
|
48
|
+
* Build the validate callback the CLI passes to {@link planUpgrade}: it
|
|
49
|
+
* stringifies the migrated spec object and runs it through the injected
|
|
50
|
+
* `parse` (the CLI wires `parseSpec`). Kept here so the CLI wrapper does not
|
|
51
|
+
* need its own `yaml` import — the fix for `migrate-all` writing migrated specs
|
|
52
|
+
* unchecked is one shared, tested helper.
|
|
53
|
+
*/
|
|
54
|
+
export declare function makeSpecValidator(parse: (yaml: string) => unknown): (spec: SpecObject) => void;
|
|
55
|
+
/** Render an upgrade plan as the human-readable report. `write` toggles the
|
|
56
|
+
* "would apply" vs "applied" wording; returns the block. */
|
|
57
|
+
export declare function formatUpgradePlan(plan: UpgradePlan, write: boolean): string;
|
package/dist/upgrade.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { diffSpecYaml } from "@crewhaus/spec-patch";
|
|
2
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
3
|
+
/**
|
|
4
|
+
* Compute the upgrade plan for a spec's YAML against `engine`.
|
|
5
|
+
*
|
|
6
|
+
* - `up-to-date` : the spec is already at the current version → no-op.
|
|
7
|
+
* - `ahead` : the spec's version is HIGHER than the CLI supports
|
|
8
|
+
* (a newer-CLI-authored spec) → refuse, don't downgrade.
|
|
9
|
+
* - `upgrade` : migrate up, validate, and produce the diff.
|
|
10
|
+
* - `validate-fail`: the migrated spec failed the injected validator (the
|
|
11
|
+
* migration produced an invalid spec) — never written.
|
|
12
|
+
*
|
|
13
|
+
* `validate` receives the migrated spec object + throws on invalid (the CLI
|
|
14
|
+
* wires `parseSpec(stringifyYaml(spec))`). Migrations that throw are reported
|
|
15
|
+
* as `validate-fail` too, so a broken migration chain never crashes upgrade.
|
|
16
|
+
*/
|
|
17
|
+
export function planUpgrade(yamlText, engine, validate) {
|
|
18
|
+
const toVersion = engine.latestVersion();
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = parseYaml(yamlText);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
return {
|
|
25
|
+
action: "validate-fail",
|
|
26
|
+
fromVersion: 0,
|
|
27
|
+
toVersion,
|
|
28
|
+
error: `spec is not parseable YAML: ${err.message}`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const fromVersion = (parsed?.version ?? 0) | 0;
|
|
32
|
+
if (fromVersion === toVersion) {
|
|
33
|
+
return { action: "up-to-date", fromVersion, toVersion };
|
|
34
|
+
}
|
|
35
|
+
if (fromVersion > toVersion) {
|
|
36
|
+
return { action: "ahead", fromVersion, toVersion };
|
|
37
|
+
}
|
|
38
|
+
let migrated;
|
|
39
|
+
try {
|
|
40
|
+
migrated = engine.migrate(parsed, toVersion);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
return {
|
|
44
|
+
action: "validate-fail",
|
|
45
|
+
fromVersion,
|
|
46
|
+
toVersion,
|
|
47
|
+
error: `migration failed: ${err.message}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// Validate the migrated spec BEFORE offering it for write (the fix for the
|
|
51
|
+
// "migrated specs written unchecked" gap).
|
|
52
|
+
try {
|
|
53
|
+
validate(migrated);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
return {
|
|
57
|
+
action: "validate-fail",
|
|
58
|
+
fromVersion,
|
|
59
|
+
toVersion,
|
|
60
|
+
error: `migrated spec failed validation: ${err.message}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const migratedYaml = stringifyYaml(migrated);
|
|
64
|
+
const diff = diffSpecYaml(yamlText, migratedYaml);
|
|
65
|
+
return { action: "upgrade", fromVersion, toVersion, migratedYaml, diff };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Build the validate callback the CLI passes to {@link planUpgrade}: it
|
|
69
|
+
* stringifies the migrated spec object and runs it through the injected
|
|
70
|
+
* `parse` (the CLI wires `parseSpec`). Kept here so the CLI wrapper does not
|
|
71
|
+
* need its own `yaml` import — the fix for `migrate-all` writing migrated specs
|
|
72
|
+
* unchecked is one shared, tested helper.
|
|
73
|
+
*/
|
|
74
|
+
export function makeSpecValidator(parse) {
|
|
75
|
+
return (spec) => {
|
|
76
|
+
parse(stringifyYaml(spec));
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Render an upgrade plan as the human-readable report. `write` toggles the
|
|
80
|
+
* "would apply" vs "applied" wording; returns the block. */
|
|
81
|
+
export function formatUpgradePlan(plan, write) {
|
|
82
|
+
switch (plan.action) {
|
|
83
|
+
case "up-to-date":
|
|
84
|
+
return `upgrade: spec is already at the current version (v${plan.toVersion}) — nothing to do.\n`;
|
|
85
|
+
case "ahead":
|
|
86
|
+
return `upgrade: spec version v${plan.fromVersion} is NEWER than this CLI supports (v${plan.toVersion}).\n Upgrade the CLI (\`brew upgrade crewhaus\` / \`npm i -g crewhaus\`) rather than downgrading the spec.\n`;
|
|
87
|
+
case "validate-fail":
|
|
88
|
+
return `upgrade: cannot migrate — ${plan.error}\n`;
|
|
89
|
+
case "upgrade": {
|
|
90
|
+
const lines = [];
|
|
91
|
+
lines.push(`upgrade: v${plan.fromVersion} → v${plan.toVersion}`);
|
|
92
|
+
const diff = plan.diff ?? [];
|
|
93
|
+
if (diff.length === 0) {
|
|
94
|
+
lines.push(" (no field-level changes — a version stamp only)");
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
for (const d of diff) {
|
|
98
|
+
if (d.kind === "added")
|
|
99
|
+
lines.push(` + ${d.path}: ${d.after}`);
|
|
100
|
+
else if (d.kind === "removed")
|
|
101
|
+
lines.push(` - ${d.path}: ${d.before}`);
|
|
102
|
+
else
|
|
103
|
+
lines.push(` ~ ${d.path}: ${d.before} → ${d.after}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
lines.push("");
|
|
107
|
+
lines.push(write
|
|
108
|
+
? " applied — spec rewritten in place."
|
|
109
|
+
: " dry-run — re-run with --write to apply.");
|
|
110
|
+
return `${lines.join("\n")}\n`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI's own version: the build-time embedded constant when compiled to a
|
|
3
|
+
* standalone binary, else the version in apps/cli/package.json. `undefined`
|
|
4
|
+
* only when neither is available (a broken installation).
|
|
5
|
+
*/
|
|
6
|
+
export declare function cliVersion(): string | undefined;
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI version resolution, shared by `crewhaus version` (index.ts) and the
|
|
3
|
+
* `compile --check` dependency pinning (compile-check.ts). Side-effect-free
|
|
4
|
+
* so it is importable from unit-testable modules (the CLI entry file runs an
|
|
5
|
+
* argv switch on import).
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
/**
|
|
9
|
+
* The CLI's own version: the build-time embedded constant when compiled to a
|
|
10
|
+
* standalone binary, else the version in apps/cli/package.json. `undefined`
|
|
11
|
+
* only when neither is available (a broken installation).
|
|
12
|
+
*/
|
|
13
|
+
export function cliVersion() {
|
|
14
|
+
if (typeof CREWHAUS_EMBEDDED_VERSION === "string") {
|
|
15
|
+
return CREWHAUS_EMBEDDED_VERSION;
|
|
16
|
+
}
|
|
17
|
+
// The package ships src/ directly (bin → src/index.ts) and tsc -b also
|
|
18
|
+
// emits dist/, so resolve package.json relative to this module — one level
|
|
19
|
+
// up lands on apps/cli/package.json from either tree, and on
|
|
20
|
+
// node_modules/crewhaus/package.json when installed.
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8")).version;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|