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
|
@@ -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;
|
package/dist/datasets.js
ADDED
|
@@ -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
|
+
}
|
package/dist/doctor-checks.d.ts
CHANGED
|
@@ -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[];
|