crewhaus 0.1.7 → 0.2.0
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 +172 -0
- package/dist/dataset-mine.js +403 -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 +246 -0
- package/dist/feedback.js +612 -0
- 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 +11727 -740
- 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/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 +173 -0
- package/dist/security-digest.js +650 -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 +298 -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 +89 -64
package/dist/propose.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item 59 (half 2) — PR-based spec-change proposals. `crewhaus propose
|
|
3
|
+
* <proposed-spec.yaml>` is the team-governance wrapper around the
|
|
4
|
+
* write-back verbs (`optimize --write-back`, `advise`, `model-scan`): instead
|
|
5
|
+
* of mutating the live spec in place, it packages the proposed change into a
|
|
6
|
+
* REVIEW artifact — a `patch.json` field diff, a rendered changelog entry, the
|
|
7
|
+
* eval score delta, and provenance — and opens a GitHub PR so the team reviews
|
|
8
|
+
* a prompt/model change like code. It NEVER auto-merges: the PR is the human
|
|
9
|
+
* gate (mirroring the flywheel workflow's "never add an auto-merge step").
|
|
10
|
+
*
|
|
11
|
+
* Factored out of the entry file `index.ts` (which runs a top-level argv
|
|
12
|
+
* switch and so cannot be imported by a test without executing the CLI).
|
|
13
|
+
* Assembly is pure; git/gh live behind an injected `GitPrDriver` seam, so the
|
|
14
|
+
* artifact bundle + PR body are unit-tested without a network or a real repo.
|
|
15
|
+
*
|
|
16
|
+
* INPUTS the review bundle carries:
|
|
17
|
+
* - patch.json: the structural field diff (`diffSpecYaml`) between the
|
|
18
|
+
* CURRENT spec and the PROPOSED spec — the reviewable "what changed";
|
|
19
|
+
* - CHANGELOG entry: `renderChangelogEntry` over the same before/after (+
|
|
20
|
+
* any optimizer provenance the proposed dir carries), the human-readable
|
|
21
|
+
* summary;
|
|
22
|
+
* - eval delta: when a baseline + candidate eval score are supplied
|
|
23
|
+
* (`--score-before`/`--score-after`, or read from the optimize run), the
|
|
24
|
+
* measured improvement that justifies the change;
|
|
25
|
+
* - provenance: the source verb (`optimize`/`advise`/`manual`), the content
|
|
26
|
+
* hash of the proposed spec, and the run id when present.
|
|
27
|
+
*
|
|
28
|
+
* The PR is opened on a fresh branch carrying ONLY the spec change + the
|
|
29
|
+
* review bundle under `.crewhaus/proposals/<id>/`. The proposal is itself
|
|
30
|
+
* audit-logged (`governance_proposal`) so its provenance is evidenced before
|
|
31
|
+
* any human acts on it.
|
|
32
|
+
*/
|
|
33
|
+
import { createHash } from "node:crypto";
|
|
34
|
+
import { diffSpecYaml } from "@crewhaus/spec-patch";
|
|
35
|
+
import { renderChangelogEntry } from "./spec-changelog";
|
|
36
|
+
/** Thrown for operational failures (unreadable spec, no change, driver
|
|
37
|
+
* failure). The CLI routes the message through `die()`; tests assert on
|
|
38
|
+
* `.message`. */
|
|
39
|
+
export class ProposeError extends Error {
|
|
40
|
+
name = "ProposeError";
|
|
41
|
+
}
|
|
42
|
+
/** Where a proposal's review bundle lands within the repo. */
|
|
43
|
+
export const PROPOSALS_RELDIR = ".crewhaus/proposals";
|
|
44
|
+
/** sha256 hex of a spec's bytes — the patch identity carried into provenance. */
|
|
45
|
+
export function specContentHash(yaml) {
|
|
46
|
+
return createHash("sha256").update(yaml).digest("hex");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Assemble the review bundle from current + proposed spec YAML. Pure — no
|
|
50
|
+
* filesystem, no git. Throws when the proposed spec is byte-identical to the
|
|
51
|
+
* current one (nothing to propose); a structurally-empty-but-reformatted
|
|
52
|
+
* change is allowed through with a note (comments matter to reviewers).
|
|
53
|
+
*/
|
|
54
|
+
export function assembleProposal(opts) {
|
|
55
|
+
if (opts.proposedYaml === opts.currentYaml) {
|
|
56
|
+
throw new ProposeError("proposed spec is byte-identical to the current spec — nothing to propose");
|
|
57
|
+
}
|
|
58
|
+
let diff;
|
|
59
|
+
try {
|
|
60
|
+
diff = diffSpecYaml(opts.currentYaml, opts.proposedYaml);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
throw new ProposeError(`could not diff the specs: ${err.message}`);
|
|
64
|
+
}
|
|
65
|
+
const generatedAt = (opts.now?.() ?? new Date()).toISOString();
|
|
66
|
+
const provenance = {
|
|
67
|
+
source: opts.source,
|
|
68
|
+
specName: opts.specName,
|
|
69
|
+
patchHash: specContentHash(opts.proposedYaml),
|
|
70
|
+
...(opts.runId !== undefined ? { runId: opts.runId } : {}),
|
|
71
|
+
generatedAt,
|
|
72
|
+
};
|
|
73
|
+
const patch = {
|
|
74
|
+
specName: opts.specName,
|
|
75
|
+
diff,
|
|
76
|
+
provenance,
|
|
77
|
+
...(opts.evalDelta !== undefined ? { evalDelta: opts.evalDelta } : {}),
|
|
78
|
+
};
|
|
79
|
+
const patchJson = `${JSON.stringify(patch, null, 2)}\n`;
|
|
80
|
+
const changelogEntry = renderChangelogEntry({
|
|
81
|
+
version: opts.proposedVersion,
|
|
82
|
+
yaml: opts.proposedYaml,
|
|
83
|
+
previousYaml: opts.currentYaml,
|
|
84
|
+
...(opts.optimizeRootDir !== undefined ? { optimizeRootDir: opts.optimizeRootDir } : {}),
|
|
85
|
+
...(opts.patchJsonPath !== undefined ? { patchJsonPath: opts.patchJsonPath } : {}),
|
|
86
|
+
...(opts.now !== undefined ? { now: opts.now() } : {}),
|
|
87
|
+
});
|
|
88
|
+
const prTitle = `propose: ${opts.specName} ${opts.proposedVersion} (${opts.source})`;
|
|
89
|
+
const prBody = buildPrBody({
|
|
90
|
+
specName: opts.specName,
|
|
91
|
+
version: opts.proposedVersion,
|
|
92
|
+
source: opts.source,
|
|
93
|
+
diff,
|
|
94
|
+
...(opts.evalDelta !== undefined ? { evalDelta: opts.evalDelta } : {}),
|
|
95
|
+
...(opts.runId !== undefined ? { runId: opts.runId } : {}),
|
|
96
|
+
patchHash: provenance.patchHash,
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
patch,
|
|
100
|
+
patchJson,
|
|
101
|
+
changelogEntry,
|
|
102
|
+
prTitle,
|
|
103
|
+
prBody,
|
|
104
|
+
hasStructuralChange: diff.length > 0,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function buildPrBody(args) {
|
|
108
|
+
const lines = [];
|
|
109
|
+
lines.push(`Proposed change to **${args.specName}** (${args.version}), source: \`${args.source}\`.`);
|
|
110
|
+
lines.push("");
|
|
111
|
+
lines.push("This PR was opened by `crewhaus propose` — the spec was NOT modified in place.");
|
|
112
|
+
lines.push("Review it like code; merging is the human gate (there is no auto-merge).");
|
|
113
|
+
lines.push("");
|
|
114
|
+
if (args.evalDelta !== undefined) {
|
|
115
|
+
const d = args.evalDelta;
|
|
116
|
+
const arrow = d.scoreAfter >= d.scoreBefore ? "↑" : "↓";
|
|
117
|
+
lines.push(`**Eval delta**: ${d.scoreBefore.toFixed(3)} → ${d.scoreAfter.toFixed(3)} ${arrow}${d.datasetName !== undefined ? ` (${d.datasetName})` : ""}`);
|
|
118
|
+
lines.push("");
|
|
119
|
+
}
|
|
120
|
+
lines.push("**Structural diff**");
|
|
121
|
+
if (args.diff.length === 0) {
|
|
122
|
+
lines.push("- no structural changes (comments/formatting only)");
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
for (const d of args.diff)
|
|
126
|
+
lines.push(diffLine(d));
|
|
127
|
+
}
|
|
128
|
+
lines.push("");
|
|
129
|
+
if (args.runId !== undefined)
|
|
130
|
+
lines.push(`Run: \`${args.runId}\``);
|
|
131
|
+
lines.push(`Patch hash: \`${args.patchHash.slice(0, 16)}…\``);
|
|
132
|
+
lines.push("");
|
|
133
|
+
lines.push(`The full review bundle (\`patch.json\` + changelog entry) is in \`${PROPOSALS_RELDIR}/\`.`);
|
|
134
|
+
return `${lines.join("\n")}\n`;
|
|
135
|
+
}
|
|
136
|
+
function diffLine(d) {
|
|
137
|
+
switch (d.kind) {
|
|
138
|
+
case "added":
|
|
139
|
+
return `- added \`${d.path}\`: ${d.after ?? ""}`;
|
|
140
|
+
case "removed":
|
|
141
|
+
return `- removed \`${d.path}\`: ${d.before ?? ""}`;
|
|
142
|
+
case "changed":
|
|
143
|
+
return `- changed \`${d.path}\`: ${d.before ?? ""} → ${d.after ?? ""}`;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Slug-safe proposal id: `<spec>-<version>-<shorthash>-<stamp>`. */
|
|
147
|
+
export function proposalId(specName, version, patchHash, now) {
|
|
148
|
+
const slug = specName.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
149
|
+
const stamp = now.toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
150
|
+
return `${slug || "spec"}-${version}-${patchHash.slice(0, 8)}-${stamp}`;
|
|
151
|
+
}
|
|
152
|
+
/** Assemble the branch/commit/PR plan + the review-bundle files. Pure. */
|
|
153
|
+
export function buildProposalPrPlan(opts) {
|
|
154
|
+
const now = opts.now?.() ?? new Date();
|
|
155
|
+
const id = proposalId(opts.assembled.patch.specName, opts.proposedVersion, opts.assembled.patch.provenance.patchHash, now);
|
|
156
|
+
const bundleDir = `${PROPOSALS_RELDIR}/${id}`;
|
|
157
|
+
const files = {
|
|
158
|
+
[opts.specRelPath]: opts.proposedYaml,
|
|
159
|
+
[`${bundleDir}/patch.json`]: opts.assembled.patchJson,
|
|
160
|
+
[`${bundleDir}/CHANGELOG-entry.md`]: opts.assembled.changelogEntry,
|
|
161
|
+
[`${bundleDir}/PR-body.md`]: opts.assembled.prBody,
|
|
162
|
+
};
|
|
163
|
+
const plan = {
|
|
164
|
+
branch: `propose/${id}`,
|
|
165
|
+
title: opts.assembled.prTitle,
|
|
166
|
+
body: opts.assembled.prBody,
|
|
167
|
+
files,
|
|
168
|
+
commitMessage: `propose(${opts.assembled.patch.specName}): ${opts.proposedVersion} for review`,
|
|
169
|
+
};
|
|
170
|
+
return { plan, proposalId: id };
|
|
171
|
+
}
|
|
172
|
+
export function buildProposalAuditPayload(assembled, opened, proposedVersion, now) {
|
|
173
|
+
return {
|
|
174
|
+
source: assembled.patch.provenance.source,
|
|
175
|
+
specName: assembled.patch.specName,
|
|
176
|
+
patchHash: assembled.patch.provenance.patchHash,
|
|
177
|
+
proposedVersion,
|
|
178
|
+
...(assembled.patch.evalDelta !== undefined ? { evalDelta: assembled.patch.evalDelta } : {}),
|
|
179
|
+
...(opened.prNumber !== undefined ? { prNumber: opened.prNumber } : {}),
|
|
180
|
+
prUrl: opened.url,
|
|
181
|
+
branch: opened.branch,
|
|
182
|
+
ts: now(),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Sample } from "@crewhaus/eval-dataset";
|
|
2
|
+
import { type FeedbackRecord, type SessionTurn } from "./feedback";
|
|
3
|
+
/** Thrown on malformed flags / unusable inputs. The CLI entry file routes it
|
|
4
|
+
* through `die()`; tests assert on `.message`. */
|
|
5
|
+
export declare class RefreshGoldensError extends Error {
|
|
6
|
+
readonly name = "RefreshGoldensError";
|
|
7
|
+
}
|
|
8
|
+
/** Default rating threshold above which a turn is "up-rated" (mirrors distill). */
|
|
9
|
+
export declare const DEFAULT_REFRESH_MIN_SCORE = 0.7;
|
|
10
|
+
/** Similarity at/above which a feedback turn matches a stored sample by input. */
|
|
11
|
+
export declare const INPUT_MATCH_SIMILARITY = 0.6;
|
|
12
|
+
export type GoldEvidence = "correction" | "up-rated-divergence";
|
|
13
|
+
/** One proposed gold update for a stored sample. */
|
|
14
|
+
export type GoldProposal = {
|
|
15
|
+
readonly sampleId: string;
|
|
16
|
+
/** Content hash of the CURRENT stored sample (registry hashSample). */
|
|
17
|
+
readonly currentHash: string;
|
|
18
|
+
readonly input: string;
|
|
19
|
+
/** The stored gold (may be undefined — a sample with no expected_output). */
|
|
20
|
+
readonly currentGold?: string;
|
|
21
|
+
/** The proposed new gold, drawn from the correction/up-rated answer. */
|
|
22
|
+
readonly proposedGold: string;
|
|
23
|
+
readonly evidence: GoldEvidence;
|
|
24
|
+
/** Where the proposed gold came from (sessionId#turn). */
|
|
25
|
+
readonly sourceRef: string;
|
|
26
|
+
/** True when the run-history index shows this sample failing across runs. */
|
|
27
|
+
readonly stale: boolean;
|
|
28
|
+
};
|
|
29
|
+
export type RefreshResult = {
|
|
30
|
+
readonly proposals: ReadonlyArray<GoldProposal>;
|
|
31
|
+
/** Feedback turns that matched no stored sample (informational). */
|
|
32
|
+
readonly unmatched: number;
|
|
33
|
+
/** Number of samples inspected. */
|
|
34
|
+
readonly sampleCount: number;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Find the stored sample a feedback turn's input reconciles with: exact
|
|
38
|
+
* (normalized) input equality first, then the best token-overlap match at/above
|
|
39
|
+
* {@link INPUT_MATCH_SIMILARITY}. Returns undefined when nothing is close
|
|
40
|
+
* enough — a genuinely new input is not a gold refresh (that is `dataset mine`).
|
|
41
|
+
*/
|
|
42
|
+
export declare function matchSampleByInput(input: string, samples: ReadonlyArray<Sample>): Sample | undefined;
|
|
43
|
+
/** Minimal per-run sample outcome the staleness check reads. */
|
|
44
|
+
export type RunSampleOutcome = {
|
|
45
|
+
sampleId: string;
|
|
46
|
+
passed: boolean;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* A sample is STALE when it FAILED in every run that included it AND was seen
|
|
50
|
+
* in at least `minRuns` runs — a persistent eval failure. Callers pair this
|
|
51
|
+
* with a live up-rating to conclude the GOLD is wrong, not the agent. A sample
|
|
52
|
+
* absent from the run history carries no stale signal (returns false) — we
|
|
53
|
+
* never fabricate one.
|
|
54
|
+
*/
|
|
55
|
+
export declare function isStaleSample(sampleId: string, runs: ReadonlyArray<ReadonlyArray<RunSampleOutcome>>, minRuns?: number): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Reconcile merged feedback against the stored samples, proposing gold updates
|
|
58
|
+
* where a correction or up-rated answer diverges from the stored
|
|
59
|
+
* `expected_output`. `turns` supplies each feedback record's input + observed
|
|
60
|
+
* answer (keyed sessionId#turn). `runOutcomes` (from the run-history index)
|
|
61
|
+
* powers the stale flag; pass [] to skip staleness.
|
|
62
|
+
*
|
|
63
|
+
* A proposal is emitted only when the proposed gold is non-empty and actually
|
|
64
|
+
* DIFFERS from the stored gold — a correction that matches the current gold is
|
|
65
|
+
* already reconciled.
|
|
66
|
+
*/
|
|
67
|
+
export declare function reconcileGoldens(opts: {
|
|
68
|
+
samples: ReadonlyArray<Sample>;
|
|
69
|
+
turns: ReadonlyArray<SessionTurn>;
|
|
70
|
+
records: ReadonlyArray<FeedbackRecord>;
|
|
71
|
+
minScore: number;
|
|
72
|
+
runOutcomes?: ReadonlyArray<ReadonlyArray<RunSampleOutcome>>;
|
|
73
|
+
}): RefreshResult;
|
|
74
|
+
/**
|
|
75
|
+
* Apply proposals to the stored samples, producing the NEW sample array to
|
|
76
|
+
* register as a fresh version. Never mutates the input; a proposed sample gets
|
|
77
|
+
* its `expected_output` replaced and a provenance note added to metadata.
|
|
78
|
+
* Samples with no proposal pass through unchanged.
|
|
79
|
+
*/
|
|
80
|
+
export declare function applyProposals(samples: ReadonlyArray<Sample>, proposals: ReadonlyArray<GoldProposal>): Sample[];
|
|
81
|
+
/** The review diff — a per-proposal old→new gold report (printed by default). */
|
|
82
|
+
export declare function renderProposals(result: RefreshResult, datasetLabel: string): string;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item 5 — `crewhaus dataset refresh-goldens`: reconcile the human
|
|
3
|
+
* corrections + up-rated turns that accumulated in production against an
|
|
4
|
+
* existing dataset's stored gold answers. When a user's accepted/corrected
|
|
5
|
+
* output diverges from a sample's `expected_output`, PROPOSE a gold update
|
|
6
|
+
* (never in-place — the write is always a NEW dataset-registry version).
|
|
7
|
+
*
|
|
8
|
+
* Two evidence classes drive a proposal:
|
|
9
|
+
* - CORRECTION: a `user_feedback` record carrying a `correction` (the user
|
|
10
|
+
* typed a better answer) on a turn whose input matches a stored sample.
|
|
11
|
+
* - UP-RATED DIVERGENCE: an up-rated turn (normalized rating ≥ min-score)
|
|
12
|
+
* whose observed answer differs from the stored gold — the live behavior
|
|
13
|
+
* the user liked is not what the dataset asserts.
|
|
14
|
+
*
|
|
15
|
+
* Matching is input-equality first, then token-overlap similarity (the same
|
|
16
|
+
* similarity the sibling flywheel features use), so a lightly-reworded prompt
|
|
17
|
+
* still reconciles. Samples that FAIL consistently across eval runs yet are
|
|
18
|
+
* repeatedly up-rated live are flagged STALE using the run-history index; a
|
|
19
|
+
* sample with no cross-run history simply carries no stale signal (we never
|
|
20
|
+
* fabricate one).
|
|
21
|
+
*
|
|
22
|
+
* Provenance: each proposal records the stored sample's content hash (via the
|
|
23
|
+
* registry's own `hashSample`, so hashes line up with what `put` recorded) and
|
|
24
|
+
* the evidence class. Kept in a side-effect-free module mirroring
|
|
25
|
+
* `feedback.ts` / `graders-suggest.ts`; all filesystem access + the registry
|
|
26
|
+
* version write live in `apps/cli/src/index.ts`.
|
|
27
|
+
*/
|
|
28
|
+
import { hashSample } from "@crewhaus/dataset-registry";
|
|
29
|
+
import { mergeFeedback, normalizeRating } from "./feedback";
|
|
30
|
+
import { normalizeEvidenceTokens } from "./graders-suggest";
|
|
31
|
+
/** Thrown on malformed flags / unusable inputs. The CLI entry file routes it
|
|
32
|
+
* through `die()`; tests assert on `.message`. */
|
|
33
|
+
export class RefreshGoldensError extends Error {
|
|
34
|
+
name = "RefreshGoldensError";
|
|
35
|
+
}
|
|
36
|
+
/** Default rating threshold above which a turn is "up-rated" (mirrors distill). */
|
|
37
|
+
export const DEFAULT_REFRESH_MIN_SCORE = 0.7;
|
|
38
|
+
/** Similarity at/above which a feedback turn matches a stored sample by input. */
|
|
39
|
+
export const INPUT_MATCH_SIMILARITY = 0.6;
|
|
40
|
+
// -------- similarity --------
|
|
41
|
+
function tokenSet(text) {
|
|
42
|
+
return new Set(normalizeEvidenceTokens(text));
|
|
43
|
+
}
|
|
44
|
+
function jaccard(a, b) {
|
|
45
|
+
if (a.size === 0 && b.size === 0)
|
|
46
|
+
return 0;
|
|
47
|
+
let intersection = 0;
|
|
48
|
+
for (const t of a)
|
|
49
|
+
if (b.has(t))
|
|
50
|
+
intersection += 1;
|
|
51
|
+
const union = a.size + b.size - intersection;
|
|
52
|
+
return union === 0 ? 0 : intersection / union;
|
|
53
|
+
}
|
|
54
|
+
/** Normalize an input for equality matching (whitespace-collapsed, trimmed). */
|
|
55
|
+
function normInput(s) {
|
|
56
|
+
return s.replace(/\s+/g, " ").trim();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Find the stored sample a feedback turn's input reconciles with: exact
|
|
60
|
+
* (normalized) input equality first, then the best token-overlap match at/above
|
|
61
|
+
* {@link INPUT_MATCH_SIMILARITY}. Returns undefined when nothing is close
|
|
62
|
+
* enough — a genuinely new input is not a gold refresh (that is `dataset mine`).
|
|
63
|
+
*/
|
|
64
|
+
export function matchSampleByInput(input, samples) {
|
|
65
|
+
const target = normInput(input);
|
|
66
|
+
for (const s of samples) {
|
|
67
|
+
if (normInput(s.input) === target)
|
|
68
|
+
return s;
|
|
69
|
+
}
|
|
70
|
+
const targetTokens = tokenSet(input);
|
|
71
|
+
if (targetTokens.size === 0)
|
|
72
|
+
return undefined;
|
|
73
|
+
let best;
|
|
74
|
+
let bestOverlap = INPUT_MATCH_SIMILARITY;
|
|
75
|
+
for (const s of samples) {
|
|
76
|
+
const overlap = jaccard(targetTokens, tokenSet(s.input));
|
|
77
|
+
if (overlap >= bestOverlap) {
|
|
78
|
+
// Strictly-greater keeps the FIRST best on ties (stable across runs);
|
|
79
|
+
// use > so a later equal match doesn't flip the winner.
|
|
80
|
+
if (overlap > bestOverlap || best === undefined) {
|
|
81
|
+
bestOverlap = overlap;
|
|
82
|
+
best = s;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return best;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* A sample is STALE when it FAILED in every run that included it AND was seen
|
|
90
|
+
* in at least `minRuns` runs — a persistent eval failure. Callers pair this
|
|
91
|
+
* with a live up-rating to conclude the GOLD is wrong, not the agent. A sample
|
|
92
|
+
* absent from the run history carries no stale signal (returns false) — we
|
|
93
|
+
* never fabricate one.
|
|
94
|
+
*/
|
|
95
|
+
export function isStaleSample(sampleId, runs, minRuns = 2) {
|
|
96
|
+
let seen = 0;
|
|
97
|
+
let failedEvery = true;
|
|
98
|
+
for (const run of runs) {
|
|
99
|
+
const outcome = run.find((o) => o.sampleId === sampleId);
|
|
100
|
+
if (outcome === undefined)
|
|
101
|
+
continue;
|
|
102
|
+
seen += 1;
|
|
103
|
+
if (outcome.passed)
|
|
104
|
+
failedEvery = false;
|
|
105
|
+
}
|
|
106
|
+
return seen >= minRuns && failedEvery;
|
|
107
|
+
}
|
|
108
|
+
// -------- reconciliation --------
|
|
109
|
+
/**
|
|
110
|
+
* Reconcile merged feedback against the stored samples, proposing gold updates
|
|
111
|
+
* where a correction or up-rated answer diverges from the stored
|
|
112
|
+
* `expected_output`. `turns` supplies each feedback record's input + observed
|
|
113
|
+
* answer (keyed sessionId#turn). `runOutcomes` (from the run-history index)
|
|
114
|
+
* powers the stale flag; pass [] to skip staleness.
|
|
115
|
+
*
|
|
116
|
+
* A proposal is emitted only when the proposed gold is non-empty and actually
|
|
117
|
+
* DIFFERS from the stored gold — a correction that matches the current gold is
|
|
118
|
+
* already reconciled.
|
|
119
|
+
*/
|
|
120
|
+
export function reconcileGoldens(opts) {
|
|
121
|
+
const turnByKey = new Map();
|
|
122
|
+
for (const t of opts.turns)
|
|
123
|
+
turnByKey.set(`${t.sessionId}#${t.turnNumber}`, t);
|
|
124
|
+
const runs = opts.runOutcomes ?? [];
|
|
125
|
+
const bySample = new Map();
|
|
126
|
+
let unmatched = 0;
|
|
127
|
+
for (const fb of mergeFeedback(opts.records)) {
|
|
128
|
+
const turn = turnByKey.get(`${fb.sessionId}#${fb.turnNumber}`);
|
|
129
|
+
if (turn === undefined) {
|
|
130
|
+
unmatched += 1;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const score = normalizeRating(fb);
|
|
134
|
+
const upRated = score !== undefined && score >= opts.minScore;
|
|
135
|
+
// The candidate better-answer: an explicit correction, else the up-rated
|
|
136
|
+
// live answer.
|
|
137
|
+
const proposed = fb.correction !== undefined && fb.correction.trim() !== ""
|
|
138
|
+
? fb.correction
|
|
139
|
+
: upRated && turn.output.trim() !== ""
|
|
140
|
+
? turn.output
|
|
141
|
+
: undefined;
|
|
142
|
+
if (proposed === undefined)
|
|
143
|
+
continue;
|
|
144
|
+
const sample = matchSampleByInput(turn.input, opts.samples);
|
|
145
|
+
if (sample === undefined) {
|
|
146
|
+
unmatched += 1;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const currentGold = sample.expected_output;
|
|
150
|
+
if (currentGold !== undefined && normInput(currentGold) === normInput(proposed)) {
|
|
151
|
+
continue; // already reconciled
|
|
152
|
+
}
|
|
153
|
+
const evidence = fb.correction !== undefined ? "correction" : "up-rated-divergence";
|
|
154
|
+
const proposal = {
|
|
155
|
+
sampleId: sample.id,
|
|
156
|
+
currentHash: hashSample(sample),
|
|
157
|
+
input: sample.input,
|
|
158
|
+
...(currentGold !== undefined ? { currentGold } : {}),
|
|
159
|
+
proposedGold: proposed,
|
|
160
|
+
evidence,
|
|
161
|
+
sourceRef: `${fb.sessionId}#${fb.turnNumber}`,
|
|
162
|
+
stale: isStaleSample(sample.id, runs),
|
|
163
|
+
};
|
|
164
|
+
// A correction outranks an up-rated divergence for the same sample; on a
|
|
165
|
+
// tie the first-seen wins (deterministic given merged-feedback order).
|
|
166
|
+
const existing = bySample.get(sample.id);
|
|
167
|
+
if (existing === undefined ||
|
|
168
|
+
(existing.evidence !== "correction" && evidence === "correction")) {
|
|
169
|
+
bySample.set(sample.id, proposal);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const proposals = [...bySample.values()].sort((a, b) => a.sampleId.localeCompare(b.sampleId));
|
|
173
|
+
return { proposals, unmatched, sampleCount: opts.samples.length };
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Apply proposals to the stored samples, producing the NEW sample array to
|
|
177
|
+
* register as a fresh version. Never mutates the input; a proposed sample gets
|
|
178
|
+
* its `expected_output` replaced and a provenance note added to metadata.
|
|
179
|
+
* Samples with no proposal pass through unchanged.
|
|
180
|
+
*/
|
|
181
|
+
export function applyProposals(samples, proposals) {
|
|
182
|
+
const byId = new Map(proposals.map((p) => [p.sampleId, p]));
|
|
183
|
+
return samples.map((s) => {
|
|
184
|
+
const p = byId.get(s.id);
|
|
185
|
+
if (p === undefined)
|
|
186
|
+
return s;
|
|
187
|
+
const metadata = { ...(s.metadata ?? {}) };
|
|
188
|
+
metadata["gold_refreshed"] = {
|
|
189
|
+
from: p.currentGold ?? null,
|
|
190
|
+
evidence: p.evidence,
|
|
191
|
+
source: p.sourceRef,
|
|
192
|
+
prevHash: p.currentHash,
|
|
193
|
+
};
|
|
194
|
+
return { ...s, expected_output: p.proposedGold, metadata };
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
// -------- rendering --------
|
|
198
|
+
const clip = (s, max) => (s.length > max ? `${s.slice(0, max)}…` : s);
|
|
199
|
+
/** The review diff — a per-proposal old→new gold report (printed by default). */
|
|
200
|
+
export function renderProposals(result, datasetLabel) {
|
|
201
|
+
const lines = [];
|
|
202
|
+
lines.push(`refresh-goldens: ${result.proposals.length} proposed gold update(s) for ${datasetLabel} (${result.sampleCount} sample(s))`);
|
|
203
|
+
if (result.unmatched > 0) {
|
|
204
|
+
lines.push(` (${result.unmatched} feedback turn(s) matched no stored sample — skipped)`);
|
|
205
|
+
}
|
|
206
|
+
lines.push("");
|
|
207
|
+
if (result.proposals.length === 0) {
|
|
208
|
+
lines.push("no gold updates — stored golds already agree with corrections and up-rated turns.");
|
|
209
|
+
return `${lines.join("\n")}\n`;
|
|
210
|
+
}
|
|
211
|
+
for (const p of result.proposals) {
|
|
212
|
+
lines.push(`● ${p.sampleId} [${p.evidence}${p.stale ? ", STALE across runs" : ""}]`);
|
|
213
|
+
lines.push(` input: ${clip(p.input, 100)}`);
|
|
214
|
+
lines.push(` - old: ${p.currentGold !== undefined ? clip(p.currentGold, 120) : "(none)"}`);
|
|
215
|
+
lines.push(` + new: ${clip(p.proposedGold, 120)}`);
|
|
216
|
+
lines.push(` source: ${p.sourceRef}`);
|
|
217
|
+
}
|
|
218
|
+
lines.push("");
|
|
219
|
+
lines.push("re-run with --apply to write these as a NEW dataset version (never in-place).");
|
|
220
|
+
return `${lines.join("\n")}\n`;
|
|
221
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { type DatasetRegistry } from "@crewhaus/dataset-registry";
|
|
2
|
+
import type { Sample } from "@crewhaus/eval-dataset";
|
|
3
|
+
/** The per-spec regression suite's registry name. */
|
|
4
|
+
export declare function regressionSuiteName(specName: string): string;
|
|
5
|
+
export declare function isRegistrySafeName(name: string): boolean;
|
|
6
|
+
export type PinRegressionsResult = {
|
|
7
|
+
readonly suiteName: string;
|
|
8
|
+
/** Newly pinned samples (0 → nothing was written). */
|
|
9
|
+
readonly pinned: number;
|
|
10
|
+
/** The new registry version; present only when `pinned > 0`. */
|
|
11
|
+
readonly version?: string;
|
|
12
|
+
};
|
|
13
|
+
export type PinRecoveredSamplesOptions = {
|
|
14
|
+
readonly registry: DatasetRegistry;
|
|
15
|
+
readonly specName: string;
|
|
16
|
+
/** The recovered (fail→pass) samples to pin. */
|
|
17
|
+
readonly samples: ReadonlyArray<Sample>;
|
|
18
|
+
/** Name of the dataset the samples came from (provenance). */
|
|
19
|
+
readonly sourceDataset: string;
|
|
20
|
+
/** The run that produced the pin (the optimize runId on the post-accept
|
|
21
|
+
* path; the eval runId on the failure-arbiter path). */
|
|
22
|
+
readonly optimizeRunId: string;
|
|
23
|
+
/** Provenance tag naming WHAT produced the pin (e.g. "failure-arbiter").
|
|
24
|
+
* Recorded as `metadata.regression_pin.source` when present; absent on
|
|
25
|
+
* the optimize post-accept path (byte-identical to the pre-item-7 pin). */
|
|
26
|
+
readonly source?: string;
|
|
27
|
+
/** Warning sink for the registry-unsafe-name no-op; defaults to stderr. */
|
|
28
|
+
readonly warn?: (line: string) => void;
|
|
29
|
+
/** Clock override for deterministic tests. */
|
|
30
|
+
readonly now?: () => Date;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Append recovered samples to the per-spec regression suite as a new
|
|
34
|
+
* auto-bumped version: union of the previous latest version's samples and
|
|
35
|
+
* the new ones, deduped by sample id (existing pins win, so re-pinning the
|
|
36
|
+
* same recovery is idempotent and keeps its original provenance). All
|
|
37
|
+
* samples live in the `train` split — the suite is a single-split dataset.
|
|
38
|
+
* Provenance (optimize runId, pin date, source dataset) is recorded in each
|
|
39
|
+
* newly pinned sample's `metadata.regression_pin` (SampleSchema carries an
|
|
40
|
+
* open `metadata` record). No new samples → no-op: no version is written.
|
|
41
|
+
*/
|
|
42
|
+
export declare function pinRecoveredSamples(opts: PinRecoveredSamplesOptions): Promise<PinRegressionsResult>;
|
|
43
|
+
export type PinRecoveriesAfterOptimizeOptions = {
|
|
44
|
+
readonly registry: DatasetRegistry;
|
|
45
|
+
readonly specName: string;
|
|
46
|
+
/** `!--no-pin-regressions` — false → skip entirely (returns undefined). */
|
|
47
|
+
readonly pin: boolean;
|
|
48
|
+
/** Baseline (candidate-0) eval-run dir; undefined → nothing to diff. */
|
|
49
|
+
readonly baselineRunDir?: string;
|
|
50
|
+
/** Winning candidate's eval-run dir; undefined → nothing to diff. */
|
|
51
|
+
readonly candidateRunDir?: string;
|
|
52
|
+
/** The dev-set samples by id — recovered sampleIds are looked up here. */
|
|
53
|
+
readonly samplesById: ReadonlyMap<string, Sample>;
|
|
54
|
+
readonly sourceDataset: string;
|
|
55
|
+
readonly optimizeRunId: string;
|
|
56
|
+
readonly now?: () => Date;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The optimize success path: diff the baseline eval run against the winning
|
|
60
|
+
* candidate's, extract `recoveries` (fail→pass sampleIds), look those up in
|
|
61
|
+
* the dataset that was used, and pin them via {@link pinRecoveredSamples}.
|
|
62
|
+
* Returns undefined when pinning is disabled or either run dir is missing
|
|
63
|
+
* (e.g. a fitness fn that doesn't report `runDir`).
|
|
64
|
+
*/
|
|
65
|
+
export declare function pinRecoveriesAfterOptimize(opts: PinRecoveriesAfterOptimizeOptions): Promise<PinRegressionsResult | undefined>;
|
|
66
|
+
export type RegressionUnionResult = {
|
|
67
|
+
/** Primary samples first, then suite additions (dedupe: primary wins). */
|
|
68
|
+
readonly samples: Sample[];
|
|
69
|
+
/** `<primaryName>+regressions@vX` when the union ADDED samples; the
|
|
70
|
+
* untouched primary name when it added none (keyset unchanged → the
|
|
71
|
+
* run-index/baseline lineage must stay comparable, see below). */
|
|
72
|
+
readonly datasetName: string;
|
|
73
|
+
/** Primary hash with the suite's content hash folded in when the union
|
|
74
|
+
* added samples; the untouched primary hash otherwise. */
|
|
75
|
+
readonly datasetHash: string;
|
|
76
|
+
/** Suite samples actually added (collisions with the primary excluded). */
|
|
77
|
+
readonly added: number;
|
|
78
|
+
readonly suiteName: string;
|
|
79
|
+
readonly suiteVersion: string;
|
|
80
|
+
};
|
|
81
|
+
export type ApplyRegressionUnionOptions = {
|
|
82
|
+
readonly registry: DatasetRegistry;
|
|
83
|
+
readonly specName: string;
|
|
84
|
+
/** `!--no-regressions` — false → no union (returns undefined). */
|
|
85
|
+
readonly includeRegressions: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Registry name of the primary dataset when it came from a `registry:`
|
|
88
|
+
* ref. Evaling the regression suite itself must not union it into
|
|
89
|
+
* itself (and must not suffix its own name), so that case is skipped.
|
|
90
|
+
*/
|
|
91
|
+
readonly primaryRegistryName?: string;
|
|
92
|
+
/** Lazy primary samples — only materialized once a suite is found, so
|
|
93
|
+
* the streaming file-dataset path stays streaming when there is none. */
|
|
94
|
+
readonly loadPrimarySamples: () => Promise<ReadonlyArray<Sample>>;
|
|
95
|
+
readonly datasetName: string;
|
|
96
|
+
readonly datasetHash: string;
|
|
97
|
+
/** Warning sink for the registry-unsafe-name no-op; defaults to stderr. */
|
|
98
|
+
readonly warn?: (line: string) => void;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Union the per-spec regression suite into an eval's dataset (dedupe by
|
|
102
|
+
* sample id; the primary dataset wins on collision). Returns undefined when
|
|
103
|
+
* disabled, when no suite exists, or when the primary IS the suite (a
|
|
104
|
+
* `registry:` ref to it, or a file whose basename matches it — an exported
|
|
105
|
+
* copy must not union the suite into itself).
|
|
106
|
+
*
|
|
107
|
+
* Item-3 interaction, by design: a union that ADDS samples changes the
|
|
108
|
+
* run's sample keyset, so the returned `datasetName` is suffixed
|
|
109
|
+
* `+regressions@vX` and the returned `datasetHash` folds the suite's
|
|
110
|
+
* content hash into the primary's. Run-index entries and (spec, dataset)
|
|
111
|
+
* baselines then key on the honest union lineage — the first ADDING union
|
|
112
|
+
* starts a new baseline lineage instead of tripping diffReports'
|
|
113
|
+
* keyset-mismatch guard against the pre-union baseline. A union that adds
|
|
114
|
+
* NOTHING (every suite sample already in the primary) keeps the primary
|
|
115
|
+
* identity untouched: the keyset didn't change, so rewriting the key would
|
|
116
|
+
* orphan the existing baseline and silently disarm the gate.
|
|
117
|
+
*/
|
|
118
|
+
export declare function applyRegressionUnion(opts: ApplyRegressionUnionOptions): Promise<RegressionUnionResult | undefined>;
|
|
119
|
+
export type GuardedUnionOutcome = {
|
|
120
|
+
/** Dataset identity to record: the union's when one was applied, the
|
|
121
|
+
* untouched primary's otherwise. */
|
|
122
|
+
readonly datasetName: string;
|
|
123
|
+
readonly datasetHash: string;
|
|
124
|
+
/** Samples to run. Always safe to iterate: when the union attempt threw
|
|
125
|
+
* AFTER consuming the primary stream, this is the materialized primary
|
|
126
|
+
* array — never an exhausted iterable. */
|
|
127
|
+
readonly samples: AsyncIterable<Sample>;
|
|
128
|
+
/** Present when a union was applied (its `added` may be 0). */
|
|
129
|
+
readonly union?: RegressionUnionResult;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* `applyRegressionUnion` with the failure semantics `crewhaus eval` needs:
|
|
133
|
+
* best-effort (a broken suite record warns and falls back to the primary
|
|
134
|
+
* dataset) AND stream-loss-proof. The primary samples are captured as they
|
|
135
|
+
* are materialized for the union attempt, so a throw AFTER the one-shot
|
|
136
|
+
* primary stream was consumed (e.g. a suite record that passes the shape
|
|
137
|
+
* check but breaks the hash fold) falls back to the captured array instead
|
|
138
|
+
* of handing the runner an exhausted iterable — which would silently run a
|
|
139
|
+
* 0-sample eval.
|
|
140
|
+
*/
|
|
141
|
+
export declare function applyRegressionUnionGuarded(opts: {
|
|
142
|
+
readonly registry: DatasetRegistry;
|
|
143
|
+
readonly specName: string;
|
|
144
|
+
readonly includeRegressions: boolean;
|
|
145
|
+
readonly primaryRegistryName?: string;
|
|
146
|
+
readonly primary: {
|
|
147
|
+
readonly name: string;
|
|
148
|
+
readonly samples: AsyncIterable<Sample>;
|
|
149
|
+
};
|
|
150
|
+
readonly datasetHash: string;
|
|
151
|
+
/** Warning sink; defaults to stderr. */
|
|
152
|
+
readonly warn?: (line: string) => void;
|
|
153
|
+
}): Promise<GuardedUnionOutcome>;
|
|
154
|
+
/**
|
|
155
|
+
* One stable sha256 folding the regression suite's content hash into the
|
|
156
|
+
* primary dataset's, with domain separators (mirrors `overallDatasetHash`'s
|
|
157
|
+
* style). Keeps RunIndexEntry.datasetHash honest for unioned runs: it
|
|
158
|
+
* changes when EITHER the primary bytes or the suite content change.
|
|
159
|
+
*/
|
|
160
|
+
export declare function foldDatasetHash(primaryHash: string, suiteHash: string): string;
|