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.
Files changed (138) hide show
  1. package/README.md +10 -3
  2. package/dist/advice-apply.d.ts +182 -0
  3. package/dist/advice-apply.js +286 -0
  4. package/dist/advise-rules.d.ts +348 -0
  5. package/dist/advise-rules.js +905 -0
  6. package/dist/alert-sink.d.ts +48 -0
  7. package/dist/alert-sink.js +86 -0
  8. package/dist/approval-gate.d.ts +127 -0
  9. package/dist/approval-gate.js +254 -0
  10. package/dist/audit-verify.d.ts +69 -0
  11. package/dist/audit-verify.js +97 -0
  12. package/dist/autodistill.d.ts +113 -0
  13. package/dist/autodistill.js +256 -0
  14. package/dist/channel-provision.d.ts +360 -0
  15. package/dist/channel-provision.js +881 -0
  16. package/dist/ci-scaffold.d.ts +31 -0
  17. package/dist/ci-scaffold.js +343 -0
  18. package/dist/compile-check.d.ts +90 -0
  19. package/dist/compile-check.js +285 -0
  20. package/dist/compliance-schedule.d.ts +35 -0
  21. package/dist/compliance-schedule.js +36 -0
  22. package/dist/context-pressure.d.ts +80 -0
  23. package/dist/context-pressure.js +166 -0
  24. package/dist/dataset-mine.d.ts +173 -0
  25. package/dist/dataset-mine.js +404 -0
  26. package/dist/datasets.d.ts +124 -0
  27. package/dist/datasets.js +260 -0
  28. package/dist/deploy-canary.d.ts +83 -0
  29. package/dist/deploy-canary.js +87 -0
  30. package/dist/doctor-checks.d.ts +33 -0
  31. package/dist/doctor-checks.js +92 -0
  32. package/dist/doctor-detect.d.ts +108 -0
  33. package/dist/doctor-detect.js +214 -0
  34. package/dist/doctor-fix.d.ts +81 -0
  35. package/dist/doctor-fix.js +164 -0
  36. package/dist/egress-triage.d.ts +121 -0
  37. package/dist/egress-triage.js +261 -0
  38. package/dist/eval-bridge.d.ts +114 -0
  39. package/dist/eval-bridge.js +158 -0
  40. package/dist/eval-coverage.d.ts +140 -0
  41. package/dist/eval-coverage.js +428 -0
  42. package/dist/eval-history.d.ts +48 -0
  43. package/dist/eval-history.js +157 -0
  44. package/dist/eval-matrix.d.ts +80 -0
  45. package/dist/eval-matrix.js +182 -0
  46. package/dist/eval-sentinel.d.ts +65 -0
  47. package/dist/eval-sentinel.js +132 -0
  48. package/dist/faq.d.ts +68 -0
  49. package/dist/faq.js +168 -0
  50. package/dist/feedback.d.ts +9 -2
  51. package/dist/feedback.js +17 -7
  52. package/dist/fewshot.d.ts +83 -0
  53. package/dist/fewshot.js +158 -0
  54. package/dist/fleet.d.ts +207 -0
  55. package/dist/fleet.js +488 -0
  56. package/dist/flywheel.d.ts +193 -0
  57. package/dist/flywheel.js +519 -0
  58. package/dist/graders-suggest.d.ts +186 -0
  59. package/dist/graders-suggest.js +658 -0
  60. package/dist/incident.d.ts +99 -0
  61. package/dist/incident.js +217 -0
  62. package/dist/index.d.ts +9 -1
  63. package/dist/index.js +11664 -963
  64. package/dist/init-interactive.d.ts +105 -0
  65. package/dist/init-interactive.js +208 -0
  66. package/dist/intents.d.ts +105 -0
  67. package/dist/intents.js +292 -0
  68. package/dist/judge-calibrate.d.ts +137 -0
  69. package/dist/judge-calibrate.js +247 -0
  70. package/dist/justification-calibrate.d.ts +150 -0
  71. package/dist/justification-calibrate.js +262 -0
  72. package/dist/justification-gate.d.ts +27 -6
  73. package/dist/justification-gate.js +30 -6
  74. package/dist/knowledge-sync.d.ts +179 -0
  75. package/dist/knowledge-sync.js +551 -0
  76. package/dist/lessons.d.ts +87 -0
  77. package/dist/lessons.js +207 -0
  78. package/dist/lint.d.ts +127 -0
  79. package/dist/lint.js +226 -0
  80. package/dist/loadtest.d.ts +114 -0
  81. package/dist/loadtest.js +196 -0
  82. package/dist/marketplace-cli.d.ts +110 -0
  83. package/dist/marketplace-cli.js +250 -0
  84. package/dist/mcp-doctor.d.ts +121 -0
  85. package/dist/mcp-doctor.js +249 -0
  86. package/dist/model-scan.d.ts +116 -0
  87. package/dist/model-scan.js +226 -0
  88. package/dist/onchain-tune.d.ts +164 -0
  89. package/dist/onchain-tune.js +346 -0
  90. package/dist/permissions-suggest.d.ts +126 -0
  91. package/dist/permissions-suggest.js +333 -0
  92. package/dist/pii-tune.d.ts +107 -0
  93. package/dist/pii-tune.js +122 -0
  94. package/dist/propose.d.ts +117 -0
  95. package/dist/propose.js +184 -0
  96. package/dist/refresh-goldens.d.ts +82 -0
  97. package/dist/refresh-goldens.js +221 -0
  98. package/dist/regression-pin.d.ts +160 -0
  99. package/dist/regression-pin.js +281 -0
  100. package/dist/retention.d.ts +193 -0
  101. package/dist/retention.js +607 -0
  102. package/dist/retire.d.ts +118 -0
  103. package/dist/retire.js +291 -0
  104. package/dist/right-size.d.ts +100 -0
  105. package/dist/right-size.js +123 -0
  106. package/dist/route.d.ts +19 -0
  107. package/dist/route.js +90 -0
  108. package/dist/scaffold-evals.d.ts +138 -0
  109. package/dist/scaffold-evals.js +410 -0
  110. package/dist/scope-audit-drift.d.ts +139 -0
  111. package/dist/scope-audit-drift.js +260 -0
  112. package/dist/security-corpus.d.ts +237 -0
  113. package/dist/security-corpus.js +516 -0
  114. package/dist/security-digest.d.ts +174 -0
  115. package/dist/security-digest.js +656 -0
  116. package/dist/sessions-index.d.ts +27 -0
  117. package/dist/sessions-index.js +51 -0
  118. package/dist/slo-doctor.d.ts +67 -0
  119. package/dist/slo-doctor.js +119 -0
  120. package/dist/slo-sink.d.ts +96 -0
  121. package/dist/slo-sink.js +107 -0
  122. package/dist/spec-changelog.d.ts +102 -0
  123. package/dist/spec-changelog.js +237 -0
  124. package/dist/state-backup.d.ts +223 -0
  125. package/dist/state-backup.js +648 -0
  126. package/dist/tools-cli.d.ts +170 -0
  127. package/dist/tools-cli.js +300 -0
  128. package/dist/triage.d.ts +202 -0
  129. package/dist/triage.js +403 -0
  130. package/dist/upgrade.d.ts +57 -0
  131. package/dist/upgrade.js +113 -0
  132. package/dist/version.d.ts +6 -0
  133. package/dist/version.js +27 -0
  134. package/dist/voice-eval.d.ts +138 -0
  135. package/dist/voice-eval.js +309 -0
  136. package/dist/watch.d.ts +58 -0
  137. package/dist/watch.js +97 -0
  138. 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;
@@ -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;
@@ -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
+ }