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
@@ -0,0 +1,124 @@
1
+ import { type DatasetRecord, type DatasetRegistry, type DatasetSplit } from "@crewhaus/dataset-registry";
2
+ import type { Sample } from "@crewhaus/eval-dataset";
3
+ /** `--dataset` values with this prefix resolve via the registry, not a file. */
4
+ export declare const REGISTRY_PREFIX = "registry:";
5
+ /** Thrown on a malformed ref / split-spec or an unresolvable registry lookup.
6
+ * The CLI entry file catches it and routes the message through `die()`;
7
+ * tests assert on `.message` without the process exiting. */
8
+ export declare class DatasetRefError extends Error {
9
+ readonly name = "DatasetRefError";
10
+ }
11
+ export declare function isDatasetSplit(s: string): s is DatasetSplit;
12
+ /** Registry root shared with the emitted eval-bundle harness (see
13
+ * target-eval-bundle): `.crewhaus/datasets` under the cwd, or the
14
+ * CREWHAUS_DATASETS_DIR override. */
15
+ export declare function defaultDatasetsRoot(): string;
16
+ export type RegistryRef = {
17
+ readonly name: string;
18
+ /** Omitted → resolve to the latest version at lookup time. */
19
+ readonly version?: string;
20
+ /** Omitted → the union of every split present. */
21
+ readonly split?: DatasetSplit;
22
+ };
23
+ /** Parse `<name>[@version]` (the `datasets get` positional). */
24
+ export declare function parseNameVersion(refStr: string): {
25
+ name: string;
26
+ version?: string;
27
+ };
28
+ /**
29
+ * Parse a `--dataset` value. Not `registry:`-prefixed → undefined (the caller
30
+ * falls through to `loadDataset` — bare file paths keep working exactly as
31
+ * before). Prefixed but malformed → DatasetRefError.
32
+ */
33
+ export declare function parseRegistryRef(value: string): RegistryRef | undefined;
34
+ export type SplitSpec = {
35
+ readonly train: number;
36
+ readonly dev: number;
37
+ readonly test: number;
38
+ };
39
+ /** Default promotion split for `datasets put` / `distill --register`. */
40
+ export declare const DEFAULT_SPLIT_SPEC: SplitSpec;
41
+ /** Parse `--split-spec`: `train/dev` or `train/dev/test` integer percentages
42
+ * summing to 100 (e.g. `70/15/15`, `80/20`). */
43
+ export declare function parseSplitSpec(s: string): SplitSpec;
44
+ /**
45
+ * Deterministic split assignment. Samples are ordered by sha256(id) (stable
46
+ * pseudo-shuffle, independent of input order; ties break on the raw id), then
47
+ * cut at the cumulative percentage boundaries: train takes
48
+ * `[0, floor(n·train%))`, dev the next slice, test the remainder — so the
49
+ * three always partition the input exactly. NOT random: the same ids map to
50
+ * the same splits on every run, which is what makes registry versions
51
+ * reproducible. Tiny inputs can leave dev/test empty (floor); consumers fall
52
+ * back accordingly (see the optimize `registry:` path).
53
+ */
54
+ export declare function splitSamples(samples: ReadonlyArray<Sample>, spec: SplitSpec): {
55
+ train: Sample[];
56
+ dev: Sample[];
57
+ test: Sample[];
58
+ };
59
+ /**
60
+ * Auto-bump: `v<N+1>` where N is the highest existing `v<digits>` version
61
+ * (0 when there is none → `v1`). Versions outside that grammar (a
62
+ * hand-imported "1.0.0") are ignored rather than guessed at — the CLI's own
63
+ * promotion lineage always lives in the vN namespace, so this can't collide.
64
+ */
65
+ export declare function nextVersion(existing: ReadonlyArray<string>): string;
66
+ /** The splits a record actually carries (train/dev are always present keys;
67
+ * test is optional), in canonical train→dev→test order. */
68
+ export declare function splitsPresent(record: DatasetRecord): DatasetSplit[];
69
+ /** `<name>@<version>[#split]` — the datasetName recorded in the run-history
70
+ * index, so runs against different splits key to different baselines. */
71
+ export declare function registryDatasetName(name: string, version: string, split?: DatasetSplit): string;
72
+ /** The selected splits' samples, concatenated in canonical train→dev→test
73
+ * order (deterministic regardless of the `splits` array order). */
74
+ export declare function samplesForSplits(record: DatasetRecord, splits: ReadonlyArray<DatasetSplit>): Sample[];
75
+ /**
76
+ * One stable sha256 over the per-sample content hashes of the selected
77
+ * splits. Split names are folded in as domain separators (train vs dev with
78
+ * identical samples still hash apart) and splits are visited in canonical
79
+ * order, so neither the caller's array order nor record key order can change
80
+ * the digest. Feeds RunIndexEntry.datasetHash for `registry:` datasets.
81
+ */
82
+ export declare function overallDatasetHash(record: DatasetRecord, splits: ReadonlyArray<DatasetSplit>): string;
83
+ export type ResolvedRegistryRef = {
84
+ readonly record: DatasetRecord;
85
+ readonly version: string;
86
+ /** The splits the ref selects: the explicit `#split`, or every split present. */
87
+ readonly splits: DatasetSplit[];
88
+ /** `<name>@<version>[#split]` — see {@link registryDatasetName}. */
89
+ readonly datasetName: string;
90
+ /** Union of the selected splits' samples (canonical train→dev→test order). */
91
+ readonly samples: Sample[];
92
+ /** Content hash of the selected splits — see {@link overallDatasetHash}. */
93
+ readonly datasetHash: string;
94
+ };
95
+ /**
96
+ * Resolve a parsed `registry:` ref against a registry: pick the latest
97
+ * version when none was pinned, load the record, and derive the run-index
98
+ * keying fields (datasetName + datasetHash). Registry lookups may also throw
99
+ * DatasetRegistryError (a CrewhausError) — callers route both through die().
100
+ */
101
+ export declare function resolveRegistryRef(registry: DatasetRegistry, ref: RegistryRef): Promise<ResolvedRegistryRef>;
102
+ export type RegisterDatasetOptions = {
103
+ readonly registry: DatasetRegistry;
104
+ readonly name: string;
105
+ readonly samples: ReadonlyArray<Sample>;
106
+ /** Percentage split (default {@link DEFAULT_SPLIT_SPEC}); ignored with `split`. */
107
+ readonly splitSpec?: SplitSpec;
108
+ /** Put every sample into this single named split instead of splitting. */
109
+ readonly split?: DatasetSplit;
110
+ };
111
+ /**
112
+ * Import samples as a new auto-bumped version (v1, v2, …) of `name`, either
113
+ * deterministically split per the spec (test key omitted when the spec gives
114
+ * test 0%) or wholesale into one named split. Returns the persisted record.
115
+ */
116
+ export declare function registerDataset(opts: RegisterDatasetOptions): Promise<DatasetRecord>;
117
+ /**
118
+ * Samples as JSONL for stdout piping. With `split` given: that split's
119
+ * samples verbatim (SampleSchema-valid — feed straight back into
120
+ * `--dataset <file>.jsonl`). Without: all splits merged, each line tagged
121
+ * with a top-level `split` column (SampleSchema strips unknown keys on load,
122
+ * so the merged form round-trips too).
123
+ */
124
+ export declare function recordToJsonl(record: DatasetRecord, split?: DatasetSplit): string;
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Item 12 — dataset-registry plumbing for the CLI: the `crewhaus datasets`
3
+ * subcommand family, `distill --register` versioned promotion, and the
4
+ * `--dataset registry:<name>[@version][#split]` shorthand shared by `eval`
5
+ * and `optimize`. Factored out of the entry file `index.ts` (which runs a
6
+ * top-level argv switch and so cannot be imported by a test without
7
+ * executing the CLI). Side-effect-free — all filesystem access goes through
8
+ * an injected `DatasetRegistry` — mirroring `eval-history.ts` / `feedback.ts`.
9
+ *
10
+ * Determinism contract (the reason none of this uses an RNG):
11
+ * - Split assignment orders samples by sha256(sample.id) — a stable
12
+ * pseudo-shuffle independent of file order — then cuts at the cumulative
13
+ * percentage boundaries. Re-importing the same ids always lands each
14
+ * sample in the same split.
15
+ * - `overallDatasetHash` folds the registry record's per-sample content
16
+ * hashes (computed at `put`) into one sha256, so the run-history index and
17
+ * (spec, dataset) baselines from item 3 key on dataset CONTENT — the
18
+ * registry analogue of `hashDatasetFile`.
19
+ */
20
+ import { createHash } from "node:crypto";
21
+ import { resolve } from "node:path";
22
+ import { latestVersion, } from "@crewhaus/dataset-registry";
23
+ /** `--dataset` values with this prefix resolve via the registry, not a file. */
24
+ export const REGISTRY_PREFIX = "registry:";
25
+ const SPLIT_ORDER = ["train", "dev", "test"];
26
+ /** Thrown on a malformed ref / split-spec or an unresolvable registry lookup.
27
+ * The CLI entry file catches it and routes the message through `die()`;
28
+ * tests assert on `.message` without the process exiting. */
29
+ export class DatasetRefError extends Error {
30
+ name = "DatasetRefError";
31
+ }
32
+ export function isDatasetSplit(s) {
33
+ return SPLIT_ORDER.includes(s);
34
+ }
35
+ /** Registry root shared with the emitted eval-bundle harness (see
36
+ * target-eval-bundle): `.crewhaus/datasets` under the cwd, or the
37
+ * CREWHAUS_DATASETS_DIR override. */
38
+ export function defaultDatasetsRoot() {
39
+ return process.env["CREWHAUS_DATASETS_DIR"] ?? resolve(process.cwd(), ".crewhaus", "datasets");
40
+ }
41
+ /** Parse `<name>[@version]` (the `datasets get` positional). */
42
+ export function parseNameVersion(refStr) {
43
+ const at = refStr.indexOf("@");
44
+ if (at === -1)
45
+ return { name: refStr };
46
+ const name = refStr.slice(0, at);
47
+ const version = refStr.slice(at + 1);
48
+ if (version === "")
49
+ throw new DatasetRefError(`empty version in dataset ref "${refStr}"`);
50
+ return { name, version };
51
+ }
52
+ /**
53
+ * Parse a `--dataset` value. Not `registry:`-prefixed → undefined (the caller
54
+ * falls through to `loadDataset` — bare file paths keep working exactly as
55
+ * before). Prefixed but malformed → DatasetRefError.
56
+ */
57
+ export function parseRegistryRef(value) {
58
+ if (!value.startsWith(REGISTRY_PREFIX))
59
+ return undefined;
60
+ let rest = value.slice(REGISTRY_PREFIX.length);
61
+ let split;
62
+ const hash = rest.indexOf("#");
63
+ if (hash !== -1) {
64
+ const s = rest.slice(hash + 1);
65
+ rest = rest.slice(0, hash);
66
+ if (!isDatasetSplit(s)) {
67
+ throw new DatasetRefError(`invalid split "#${s}" in "${value}" — expected #train, #dev, or #test`);
68
+ }
69
+ split = s;
70
+ }
71
+ const { name, version } = parseNameVersion(rest);
72
+ if (name === "") {
73
+ throw new DatasetRefError(`missing dataset name in "${value}" — expected registry:<name>[@version][#split]`);
74
+ }
75
+ return {
76
+ name,
77
+ ...(version !== undefined ? { version } : {}),
78
+ ...(split !== undefined ? { split } : {}),
79
+ };
80
+ }
81
+ /** Default promotion split for `datasets put` / `distill --register`. */
82
+ export const DEFAULT_SPLIT_SPEC = { train: 70, dev: 15, test: 15 };
83
+ /** Parse `--split-spec`: `train/dev` or `train/dev/test` integer percentages
84
+ * summing to 100 (e.g. `70/15/15`, `80/20`). */
85
+ export function parseSplitSpec(s) {
86
+ const parts = s.split("/");
87
+ if (parts.length !== 2 && parts.length !== 3) {
88
+ throw new DatasetRefError(`invalid split spec "${s}" — expected train/dev or train/dev/test percentages (e.g. 70/15/15)`);
89
+ }
90
+ const nums = parts.map((p) => {
91
+ if (!/^\d+$/.test(p)) {
92
+ throw new DatasetRefError(`invalid split spec "${s}" — "${p}" is not a whole number`);
93
+ }
94
+ return Number.parseInt(p, 10);
95
+ });
96
+ const train = nums[0] ?? 0;
97
+ const dev = nums[1] ?? 0;
98
+ const test = nums[2] ?? 0;
99
+ if (train + dev + test !== 100) {
100
+ throw new DatasetRefError(`invalid split spec "${s}" — must sum to 100 (got ${train + dev + test})`);
101
+ }
102
+ return { train, dev, test };
103
+ }
104
+ function idHash(id) {
105
+ return createHash("sha256").update(id).digest("hex");
106
+ }
107
+ /**
108
+ * Deterministic split assignment. Samples are ordered by sha256(id) (stable
109
+ * pseudo-shuffle, independent of input order; ties break on the raw id), then
110
+ * cut at the cumulative percentage boundaries: train takes
111
+ * `[0, floor(n·train%))`, dev the next slice, test the remainder — so the
112
+ * three always partition the input exactly. NOT random: the same ids map to
113
+ * the same splits on every run, which is what makes registry versions
114
+ * reproducible. Tiny inputs can leave dev/test empty (floor); consumers fall
115
+ * back accordingly (see the optimize `registry:` path).
116
+ */
117
+ export function splitSamples(samples, spec) {
118
+ const ordered = [...samples].sort((a, b) => {
119
+ const ha = idHash(a.id);
120
+ const hb = idHash(b.id);
121
+ if (ha !== hb)
122
+ return ha < hb ? -1 : 1;
123
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
124
+ });
125
+ const n = ordered.length;
126
+ const trainEnd = Math.floor((n * spec.train) / 100);
127
+ const devEnd = Math.floor((n * (spec.train + spec.dev)) / 100);
128
+ return {
129
+ train: ordered.slice(0, trainEnd),
130
+ dev: ordered.slice(trainEnd, devEnd),
131
+ test: ordered.slice(devEnd),
132
+ };
133
+ }
134
+ // -------- versioning --------
135
+ /**
136
+ * Auto-bump: `v<N+1>` where N is the highest existing `v<digits>` version
137
+ * (0 when there is none → `v1`). Versions outside that grammar (a
138
+ * hand-imported "1.0.0") are ignored rather than guessed at — the CLI's own
139
+ * promotion lineage always lives in the vN namespace, so this can't collide.
140
+ */
141
+ export function nextVersion(existing) {
142
+ let max = 0;
143
+ for (const v of existing) {
144
+ const m = /^v(\d+)$/.exec(v);
145
+ if (m === null)
146
+ continue;
147
+ const n = Number.parseInt(m[1], 10);
148
+ if (n > max)
149
+ max = n;
150
+ }
151
+ return `v${max + 1}`;
152
+ }
153
+ // -------- registry resolution (the `registry:` shorthand) --------
154
+ /** The splits a record actually carries (train/dev are always present keys;
155
+ * test is optional), in canonical train→dev→test order. */
156
+ export function splitsPresent(record) {
157
+ return SPLIT_ORDER.filter((s) => record.splits[s] !== undefined);
158
+ }
159
+ /** `<name>@<version>[#split]` — the datasetName recorded in the run-history
160
+ * index, so runs against different splits key to different baselines. */
161
+ export function registryDatasetName(name, version, split) {
162
+ return `${name}@${version}${split !== undefined ? `#${split}` : ""}`;
163
+ }
164
+ /** The selected splits' samples, concatenated in canonical train→dev→test
165
+ * order (deterministic regardless of the `splits` array order). */
166
+ export function samplesForSplits(record, splits) {
167
+ const wanted = new Set(splits);
168
+ const out = [];
169
+ for (const s of SPLIT_ORDER) {
170
+ if (wanted.has(s))
171
+ out.push(...(record.splits[s] ?? []));
172
+ }
173
+ return out;
174
+ }
175
+ /**
176
+ * One stable sha256 over the per-sample content hashes of the selected
177
+ * splits. Split names are folded in as domain separators (train vs dev with
178
+ * identical samples still hash apart) and splits are visited in canonical
179
+ * order, so neither the caller's array order nor record key order can change
180
+ * the digest. Feeds RunIndexEntry.datasetHash for `registry:` datasets.
181
+ */
182
+ export function overallDatasetHash(record, splits) {
183
+ const wanted = new Set(splits);
184
+ const h = createHash("sha256");
185
+ for (const s of SPLIT_ORDER) {
186
+ if (!wanted.has(s) || record.splits[s] === undefined)
187
+ continue;
188
+ h.update(`${s}:${(record.sampleHashes[s] ?? []).join(",")}\n`);
189
+ }
190
+ return h.digest("hex");
191
+ }
192
+ /**
193
+ * Resolve a parsed `registry:` ref against a registry: pick the latest
194
+ * version when none was pinned, load the record, and derive the run-index
195
+ * keying fields (datasetName + datasetHash). Registry lookups may also throw
196
+ * DatasetRegistryError (a CrewhausError) — callers route both through die().
197
+ */
198
+ export async function resolveRegistryRef(registry, ref) {
199
+ const version = ref.version ?? (await latestVersion(registry, ref.name));
200
+ if (version === undefined) {
201
+ throw new DatasetRefError(`dataset "${ref.name}" has no versions in the registry — import one with \`crewhaus datasets put\``);
202
+ }
203
+ const record = await registry.getRecord(ref.name, version);
204
+ if (ref.split !== undefined && record.splits[ref.split] === undefined) {
205
+ throw new DatasetRefError(`split "${ref.split}" not present in "${ref.name}@${version}"`);
206
+ }
207
+ const splits = ref.split !== undefined ? [ref.split] : splitsPresent(record);
208
+ return {
209
+ record,
210
+ version,
211
+ splits,
212
+ datasetName: registryDatasetName(ref.name, version, ref.split),
213
+ samples: samplesForSplits(record, splits),
214
+ datasetHash: overallDatasetHash(record, splits),
215
+ };
216
+ }
217
+ /**
218
+ * Import samples as a new auto-bumped version (v1, v2, …) of `name`, either
219
+ * deterministically split per the spec (test key omitted when the spec gives
220
+ * test 0%) or wholesale into one named split. Returns the persisted record.
221
+ */
222
+ export async function registerDataset(opts) {
223
+ const version = nextVersion(await opts.registry.list(opts.name));
224
+ let splits;
225
+ if (opts.split !== undefined) {
226
+ splits = {
227
+ train: opts.split === "train" ? [...opts.samples] : [],
228
+ dev: opts.split === "dev" ? [...opts.samples] : [],
229
+ ...(opts.split === "test" ? { test: [...opts.samples] } : {}),
230
+ };
231
+ }
232
+ else {
233
+ const spec = opts.splitSpec ?? DEFAULT_SPLIT_SPEC;
234
+ const { train, dev, test } = splitSamples(opts.samples, spec);
235
+ splits = { train, dev, ...(spec.test > 0 ? { test } : {}) };
236
+ }
237
+ return opts.registry.put({ name: opts.name, version, splits });
238
+ }
239
+ // -------- serialization (datasets get) --------
240
+ /**
241
+ * Samples as JSONL for stdout piping. With `split` given: that split's
242
+ * samples verbatim (SampleSchema-valid — feed straight back into
243
+ * `--dataset <file>.jsonl`). Without: all splits merged, each line tagged
244
+ * with a top-level `split` column (SampleSchema strips unknown keys on load,
245
+ * so the merged form round-trips too).
246
+ */
247
+ export function recordToJsonl(record, split) {
248
+ const lines = [];
249
+ if (split !== undefined) {
250
+ for (const s of record.splits[split] ?? [])
251
+ lines.push(JSON.stringify(s));
252
+ }
253
+ else {
254
+ for (const sp of splitsPresent(record)) {
255
+ for (const s of record.splits[sp] ?? [])
256
+ lines.push(JSON.stringify({ ...s, split: sp }));
257
+ }
258
+ }
259
+ return lines.length === 0 ? "" : `${lines.join("\n")}\n`;
260
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Item 29 — `crewhaus deploy canary <spec> <version> ...`: an eval-gated
3
+ * unattended ramp with auto-rollback, wiring the REAL `regression-runner`
4
+ * gate into the `canary-controller` (which ships dependency-inverted with an
5
+ * always-pass stub only for tests — see `makeRegressionGate`).
6
+ *
7
+ * This module is deliberately side-effect-free (the CLI entry file runs an
8
+ * argv switch on import, so it can't be imported by a test): all I/O — the
9
+ * per-version eval, registry pins, audit appends, and stdout — is injected.
10
+ * `apps/cli/src/index.ts` supplies the real implementations; the tests inject
11
+ * seeded `EvalRunSummary` results plus the real `gate()` and a real
12
+ * file-backed registry + audit log.
13
+ *
14
+ * The ramp drives the DECLARED traffic steps in order (e.g. 5,25,50,100). At
15
+ * each step it evals BOTH the pinned baseline version and the candidate
16
+ * version against the same dataset+graders, feeds the two summaries into
17
+ * `regression-runner.gate()` (pass-rate + p95-latency thresholds), and:
18
+ * - on PASS at every step, auto-promotes the env pin to the candidate via
19
+ * the deployment path (`canary-controller.evaluate` re-pins on pass) and
20
+ * audit-logs a `deployment_action`;
21
+ * - on the FIRST failing step, auto-rolls-back (re-pins to the baseline)
22
+ * and audit-logs the regression reason, then stops the ramp.
23
+ *
24
+ * v1 CAVEAT (honest scope): `crewhaus eval` supports `target: cli`, and the
25
+ * canary-controller's `route()` has no serving-path consumer, so the ramp %
26
+ * gates eval SAMPLING/PROMOTION, not a real production traffic split. Each
27
+ * step evals the FULL dataset against both versions and gates promotion on
28
+ * the result — the percentages sequence the confidence ramp, they do not
29
+ * split live requests. A real traffic split matters only for gateway/managed
30
+ * shapes with a serving-path `route()` consumer; that is out of scope here
31
+ * and called out in the CLI `--help`.
32
+ */
33
+ import type { RegressionGate } from "@crewhaus/canary-controller";
34
+ import type { EvalRunSummary } from "@crewhaus/eval-runner";
35
+ import { type GateThresholds } from "@crewhaus/regression-runner";
36
+ /** Parse a `--traffic 5,25,50,100` list into validated, ordered steps. */
37
+ export declare function parseTrafficSteps(value: string): number[];
38
+ export declare class CanaryRampError extends Error {
39
+ readonly name = "CanaryRampError";
40
+ }
41
+ /**
42
+ * Build the {@link RegressionGate} closure the controller calls at each step:
43
+ * eval both versions with the injected `evalVersion`, then run the real
44
+ * `regression-runner.gate()` over the two summaries (baseline = prev,
45
+ * candidate = next). Latency gating is ON here (unlike the pass-rate/flip-only
46
+ * eval-history gate): a canary that regresses p95 latency is a real
47
+ * production signal. Thresholds default to regression-runner's own
48
+ * (5-point pass-rate drop, +5000ms p95) and are overridable via CLI flags.
49
+ */
50
+ export declare function makeCanaryEvalGate(opts: {
51
+ readonly evalVersion: (version: string) => Promise<EvalRunSummary>;
52
+ readonly thresholds?: GateThresholds;
53
+ readonly write?: (line: string) => void;
54
+ }): RegressionGate;
55
+ export type CanaryRampStepResult = {
56
+ readonly trafficPercent: number;
57
+ readonly verdict: "pass" | "fail";
58
+ readonly reason?: string;
59
+ readonly action?: "promote" | "rollback";
60
+ };
61
+ export type CanaryRampResult = {
62
+ /** True only when every declared step passed and the candidate was promoted. */
63
+ readonly promoted: boolean;
64
+ readonly steps: ReadonlyArray<CanaryRampStepResult>;
65
+ /** The step that failed (and triggered rollback), when any. */
66
+ readonly failedAt?: number;
67
+ };
68
+ /**
69
+ * Drive the declared traffic steps. `evaluateStep` is
70
+ * `canary-controller.evaluate` bound to a config whose `trafficPercent` this
71
+ * function sets per step; on pass it promotes the env pin, on fail it
72
+ * rolls back. The ramp stops at the first failing step (the controller has
73
+ * already rolled the env pin back to the baseline by then).
74
+ */
75
+ export declare function driveCanaryRamp(opts: {
76
+ readonly steps: ReadonlyArray<number>;
77
+ readonly evaluateStep: (trafficPercent: number) => Promise<{
78
+ verdict: "pass" | "fail";
79
+ reason?: string;
80
+ action?: "promote" | "rollback";
81
+ }>;
82
+ readonly write?: (line: string) => void;
83
+ }): Promise<CanaryRampResult>;
@@ -0,0 +1,87 @@
1
+ import { gate } from "@crewhaus/regression-runner";
2
+ /** Parse a `--traffic 5,25,50,100` list into validated, ordered steps. */
3
+ export function parseTrafficSteps(value) {
4
+ const raw = value
5
+ .split(",")
6
+ .map((s) => s.trim())
7
+ .filter((s) => s !== "");
8
+ if (raw.length === 0) {
9
+ throw new CanaryRampError("--traffic must list at least one percentage (e.g. 5,25,50,100)");
10
+ }
11
+ const steps = [];
12
+ let last = 0;
13
+ for (const token of raw) {
14
+ const n = Number(token);
15
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0 || n > 100) {
16
+ throw new CanaryRampError(`--traffic step "${token}" must be an integer in 1..100 (e.g. 5,25,50,100)`);
17
+ }
18
+ if (n <= last) {
19
+ throw new CanaryRampError(`--traffic steps must strictly increase; "${n}" does not exceed the previous "${last}"`);
20
+ }
21
+ steps.push(n);
22
+ last = n;
23
+ }
24
+ return steps;
25
+ }
26
+ export class CanaryRampError extends Error {
27
+ name = "CanaryRampError";
28
+ }
29
+ /**
30
+ * Build the {@link RegressionGate} closure the controller calls at each step:
31
+ * eval both versions with the injected `evalVersion`, then run the real
32
+ * `regression-runner.gate()` over the two summaries (baseline = prev,
33
+ * candidate = next). Latency gating is ON here (unlike the pass-rate/flip-only
34
+ * eval-history gate): a canary that regresses p95 latency is a real
35
+ * production signal. Thresholds default to regression-runner's own
36
+ * (5-point pass-rate drop, +5000ms p95) and are overridable via CLI flags.
37
+ */
38
+ export function makeCanaryEvalGate(opts) {
39
+ const write = opts.write ?? (() => { });
40
+ return async ({ fromVersion, toVersion }) => {
41
+ write(`[canary] evaluating baseline ${fromVersion} and candidate ${toVersion}`);
42
+ // Baseline first, then candidate — sequential so a low-TPM provider tier
43
+ // isn't hit by two concurrent eval runs (the same reason the eval CI
44
+ // scaffold pins --concurrency 1).
45
+ const baseline = await opts.evalVersion(fromVersion);
46
+ const candidate = await opts.evalVersion(toVersion);
47
+ const verdict = gate(baseline, candidate, opts.thresholds ?? {});
48
+ write(`[canary] baseline pass_rate=${(baseline.aggregates.passRate * 100).toFixed(1)}% ` +
49
+ `p95=${baseline.aggregates.p95LatencyMs}ms · ` +
50
+ `candidate pass_rate=${(candidate.aggregates.passRate * 100).toFixed(1)}% ` +
51
+ `p95=${candidate.aggregates.p95LatencyMs}ms`);
52
+ if (verdict.verdict === "fail") {
53
+ return { verdict: "fail", reason: verdict.reason ?? "regression gate failed" };
54
+ }
55
+ return { verdict: "pass" };
56
+ };
57
+ }
58
+ /**
59
+ * Drive the declared traffic steps. `evaluateStep` is
60
+ * `canary-controller.evaluate` bound to a config whose `trafficPercent` this
61
+ * function sets per step; on pass it promotes the env pin, on fail it
62
+ * rolls back. The ramp stops at the first failing step (the controller has
63
+ * already rolled the env pin back to the baseline by then).
64
+ */
65
+ export async function driveCanaryRamp(opts) {
66
+ const write = opts.write ?? (() => { });
67
+ const results = [];
68
+ for (const trafficPercent of opts.steps) {
69
+ write(`[canary] ramp step: ${trafficPercent}% traffic`);
70
+ const outcome = await opts.evaluateStep(trafficPercent);
71
+ const step = {
72
+ trafficPercent,
73
+ verdict: outcome.verdict,
74
+ ...(outcome.reason !== undefined ? { reason: outcome.reason } : {}),
75
+ ...(outcome.action !== undefined ? { action: outcome.action } : {}),
76
+ };
77
+ results.push(step);
78
+ if (outcome.verdict === "fail") {
79
+ write(`[canary] step ${trafficPercent}% FAILED — ${outcome.reason ?? "regression"}`);
80
+ write("[canary] auto-rolled-back to baseline; ramp aborted");
81
+ return { promoted: false, steps: results, failedAt: trafficPercent };
82
+ }
83
+ write(`[canary] step ${trafficPercent}% passed`);
84
+ }
85
+ write("[canary] all ramp steps passed — candidate promoted");
86
+ return { promoted: true, steps: results };
87
+ }
@@ -1,3 +1,4 @@
1
+ import type { IrChannels } from "@crewhaus/ir";
1
2
  /**
2
3
  * Model-aware credential checks for `crewhaus doctor`, factored out of the
3
4
  * entry file `index.ts` (which runs a top-level argv switch and so cannot be
@@ -32,6 +33,18 @@ export declare function extractSpecModel(yamlText: string): string | undefined;
32
33
  * run will need. Returns undefined for unparseable strings.
33
34
  */
34
35
  export declare function selectedProvider(model: string): DoctorProviderId | undefined;
36
+ /**
37
+ * Item 13 — online/offline mode selection for `crewhaus scaffold-evals`:
38
+ * true when the provider `model` routes to has visibly satisfied credentials
39
+ * (shares `statusFor` with the doctor checks, so the two never disagree).
40
+ * Unparseable model strings return false — the scaffold then stays on its
41
+ * deterministic template path instead of attempting a doomed call. Note the
42
+ * bedrock check is env-visible only (the AWS SDK's default chain may still
43
+ * work); scaffold prefers the safe offline mode in that ambiguity.
44
+ */
45
+ export declare function providerCredentialsSatisfied(model: string, env: NodeJS.ProcessEnv): boolean;
46
+ /** Env var names `doctor --fix` should stub for a selected provider. */
47
+ export declare function providerEnvStubs(provider: DoctorProviderId): readonly string[];
35
48
  /**
36
49
  * Build the credential section of `crewhaus doctor`.
37
50
  *
@@ -43,3 +56,23 @@ export declare function selectedProvider(model: string): DoctorProviderId | unde
43
56
  * env is clearly set (the operator is plainly not on Anthropic).
44
57
  */
45
58
  export declare function buildCredentialChecks(specModel: string | undefined, env: NodeJS.ProcessEnv): DoctorCredentialCheck[];
59
+ /**
60
+ * Item 61 — tolerantly extract the lowered channels block from a
61
+ * crewhaus.yaml text. Returns undefined when the spec doesn't parse/lower or
62
+ * is not a channel target, mirroring {@link extractSpecModel}'s tolerance —
63
+ * doctor must never fail because the cwd spec is a different shape. Lowering
64
+ * (not raw parsing) is deliberate: `lower()` is where `$VAR_NAME` strings
65
+ * become env-refs, so the check shares the compiled daemon's exact env
66
+ * semantics instead of re-implementing the regex.
67
+ */
68
+ export declare function extractChannelIr(yamlText: string): IrChannels | undefined;
69
+ /**
70
+ * Item 61 — channel-target doctor checks: one per configured platform
71
+ * (slack/telegram/discord), asserting that every secret env-ref the compiled
72
+ * daemon requires at boot (target-channel-bot's startup check exits 2 on the
73
+ * same set) is actually set. Returns [] for non-channel specs, so the check
74
+ * only appears when the cwd spec is a channel target. Offline by design —
75
+ * live platform probes (granted scopes, webhook state) belong to
76
+ * `crewhaus channel verify`, which the failure reason points at.
77
+ */
78
+ export declare function buildChannelEnvChecks(yamlText: string, env: NodeJS.ProcessEnv): DoctorCredentialCheck[];