dsh-continual-evolve 0.2.0 → 0.3.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.
Files changed (62) hide show
  1. package/README.md +45 -7
  2. package/README.zh.md +44 -7
  3. package/lib/apply.js +1 -1
  4. package/lib/approval.d.ts +6 -0
  5. package/lib/approval.js +9 -1
  6. package/lib/auto.d.ts +38 -4
  7. package/lib/auto.js +58 -5
  8. package/lib/benchmark-command.d.ts +9 -0
  9. package/lib/benchmark-command.js +331 -0
  10. package/lib/benchmark.d.ts +70 -0
  11. package/lib/benchmark.js +107 -1
  12. package/lib/command.js +25 -442
  13. package/lib/evaluate.d.ts +7 -0
  14. package/lib/evaluate.js +22 -7
  15. package/lib/evolve-event.d.ts +38 -0
  16. package/lib/evolve-event.js +49 -0
  17. package/lib/failures.d.ts +39 -0
  18. package/lib/failures.js +170 -0
  19. package/lib/fate.d.ts +3 -1
  20. package/lib/fate.js +8 -4
  21. package/lib/goal-command.d.ts +7 -0
  22. package/lib/goal-command.js +37 -0
  23. package/lib/index.d.ts +29 -25
  24. package/lib/index.js +14 -0
  25. package/lib/inject.d.ts +8 -0
  26. package/lib/inject.js +51 -4
  27. package/lib/llm-text.d.ts +30 -0
  28. package/lib/llm-text.js +49 -0
  29. package/lib/mount-command.d.ts +10 -0
  30. package/lib/mount-command.js +48 -0
  31. package/lib/plan.js +5 -0
  32. package/lib/planner.d.ts +1 -1
  33. package/lib/planner.js +13 -39
  34. package/lib/render.d.ts +1 -3
  35. package/lib/render.js +0 -4
  36. package/lib/review.d.ts +4 -1
  37. package/lib/review.js +10 -38
  38. package/lib/rollback.d.ts +1 -3
  39. package/lib/rollback.js +0 -8
  40. package/lib/score.d.ts +15 -0
  41. package/lib/score.js +74 -5
  42. package/lib/service.d.ts +2 -2
  43. package/lib/service.js +5 -2
  44. package/lib/skill-render.d.ts +15 -0
  45. package/lib/skill-render.js +30 -0
  46. package/lib/skill.d.ts +2 -5
  47. package/lib/skill.js +2 -29
  48. package/lib/skillquality.d.ts +1 -2
  49. package/lib/skillquality.js +2 -2
  50. package/lib/store.d.ts +1 -3
  51. package/lib/store.js +0 -7
  52. package/lib/tool.js +22 -1
  53. package/lib/types.d.ts +8 -0
  54. package/lib/usage.d.ts +32 -0
  55. package/lib/usage.js +84 -0
  56. package/lib/validate.d.ts +12 -2
  57. package/lib/validate.js +26 -1
  58. package/lib/wrapup-command.d.ts +8 -0
  59. package/lib/wrapup-command.js +211 -0
  60. package/lib/wrapup.d.ts +14 -9
  61. package/lib/wrapup.js +24 -36
  62. package/package.json +8 -8
@@ -0,0 +1,331 @@
1
+ import { formatHarnessStateForPrompt } from "./render.js";
2
+ import { stripAngleBrackets } from "./command.js";
3
+ import { addCase, caseCheckProblems, createBenchmark, listBenchmarks, listCases, loadBenchmark, loadCaseMeta, loadScoreboard, rollbackRejectedCandidate, saveCaseMeta, saveScoreboard, transitionCaseStatus } from "./benchmark.js";
4
+ import { decide, decisionReport, entryFromCells, flagMaterialDrift } from "./score.js";
5
+ import { evaluateState } from "./evaluate.js";
6
+ function success(text) {
7
+ return { kind: "success", text };
8
+ }
9
+ function error(text) {
10
+ return { kind: "error", text };
11
+ }
12
+ function parsePositiveInt(value, what) {
13
+ const n = Number(value);
14
+ if (!Number.isInteger(n) || n < 1) {
15
+ throw new Error(`${what} must be a positive integer, got "${value}"`);
16
+ }
17
+ return n;
18
+ }
19
+ /** " (N failed)" suffix when an evaluation entry carries failed cells. */
20
+ function failedTextOf(entry) {
21
+ const failed = entry.cells.filter((cell) => cell.status === "failed").length;
22
+ return failed > 0 ? ` (${failed} failed)` : "";
23
+ }
24
+ const BENCHMARK_USAGE = `Usage:
25
+ /evolve benchmark new <title> create a benchmark (runs=1)
26
+ /evolve benchmark add-case <bid> <title> <statement> <rubric>
27
+ /evolve benchmark list list benchmarks + reference status
28
+ /evolve benchmark status <bid> show scoreboard + decisions
29
+ /evolve benchmark reset <bid> clear the scoreboard (fresh reference)
30
+ /evolve benchmark run <bid> evaluate current state as the reference
31
+ /evolve benchmark run <bid> candidate <refinementId> evaluate the post-refinement state and decide
32
+ /evolve benchmark casecheck <bid> quality-gate check all cases
33
+ /evolve benchmark pilot <bid> <cid> single pilot run for calibration
34
+ /evolve benchmark freeze <bid> <cid> freeze a case as formal baseline
35
+ /evolve benchmark meta <bid> <cid> [field value ...] set case metadata (capability/distinguisher/shortcuts)`;
36
+ export async function executeBenchmarkCommand(ctx, engine, invocation, rest, runtime) {
37
+ const sub = rest[0] ?? "";
38
+ const args = rest.slice(1);
39
+ const sessionId = invocation.agent.id;
40
+ const baseDir = engine.baseDir;
41
+ switch (sub) {
42
+ case "":
43
+ case "help":
44
+ return success(BENCHMARK_USAGE);
45
+ case "new": {
46
+ const title = args[0] ?? "";
47
+ if (!title) {
48
+ return error(`benchmark new requires a title.\n${BENCHMARK_USAGE}`);
49
+ }
50
+ const runs = args[1] !== undefined ? parsePositiveInt(args[1], "runs") : undefined;
51
+ const definition = createBenchmark(baseDir, { title, ...(runs !== undefined ? { runs } : {}) });
52
+ return success(`benchmark ${definition.id} created (runs=${definition.runs}, passThreshold=${definition.passThreshold})\nAdd cases with: /evolve benchmark add-case ${definition.id} "<title>" "<statement>" "<rubric>"`);
53
+ }
54
+ case "list": {
55
+ const benchmarks = listBenchmarks(baseDir);
56
+ if (benchmarks.length === 0) {
57
+ return success("(no benchmarks yet — use /evolve benchmark new <title>)");
58
+ }
59
+ const lines = benchmarks.map((b) => {
60
+ const cases = listCases(baseDir, b.id);
61
+ const board = loadScoreboard(baseDir, b.id);
62
+ const ref = board.reference ? ` ref=${board.reference.overall ?? "?"}` : " no-reference";
63
+ return `- ${b.id} (${cases.length} cases, runs=${b.runs})${ref}`;
64
+ });
65
+ return success(lines.join("\n"));
66
+ }
67
+ case "add-case": {
68
+ const bid = stripAngleBrackets(args[0] ?? "");
69
+ const title = args[1] ?? "";
70
+ const statement = args[2] ?? "";
71
+ const rubric = args[3] ?? "";
72
+ if (!bid || !title || !statement || !rubric) {
73
+ return error(`benchmark add-case needs <bid> <title> <statement> <rubric>.\n${BENCHMARK_USAGE}`);
74
+ }
75
+ if (!loadBenchmark(baseDir, bid)) {
76
+ return error(`benchmark ${bid} not found`);
77
+ }
78
+ const caseItem = addCase(baseDir, bid, title, statement, rubric, runtime.rubricKey);
79
+ return success(`case ${caseItem.id} added to ${bid} (status: draft)`);
80
+ }
81
+ case "reset": {
82
+ const bid = stripAngleBrackets(args[0] ?? "");
83
+ if (!bid) {
84
+ return error(`benchmark reset needs a <bid>.\n${BENCHMARK_USAGE}`);
85
+ }
86
+ if (!loadBenchmark(baseDir, bid)) {
87
+ return error(`benchmark ${bid} not found`);
88
+ }
89
+ saveScoreboard(baseDir, bid, { candidates: [], decisions: [] });
90
+ return success(`scoreboard reset for ${bid} — run /evolve benchmark run ${bid} to record a fresh reference`);
91
+ }
92
+ case "status": {
93
+ const bid = stripAngleBrackets(args[0] ?? "");
94
+ const board = loadScoreboard(baseDir, bid);
95
+ const lines = [];
96
+ if (board.reference) {
97
+ lines.push(`reference "${board.reference.label}": overall=${board.reference.overall ?? "?"} cells=${board.reference.cells.length}${failedTextOf(board.reference)}`);
98
+ }
99
+ else {
100
+ lines.push("(no reference evaluation yet)");
101
+ }
102
+ for (const c of board.candidates) {
103
+ lines.push(`candidate "${c.label}": overall=${c.overall ?? "?"} cells=${c.cells.length}${failedTextOf(c)}${c.refinementId ? ` (${c.refinementId})` : ""}`);
104
+ }
105
+ for (const d of board.decisions) {
106
+ lines.push(`decision: ${d.accepted ? "ACCEPTED" : "rejected"} ${d.candidateLabel} — ${d.reasons.join("; ") || "ok"}`);
107
+ }
108
+ return success(lines.join("\n") || "(empty scoreboard)");
109
+ }
110
+ case "run": {
111
+ const bid = stripAngleBrackets(args[0] ?? "");
112
+ const candidateId = args.includes("candidate") ? stripAngleBrackets(args[args.indexOf("candidate") + 1] ?? "") : undefined;
113
+ const definition = loadBenchmark(baseDir, bid);
114
+ if (!definition) {
115
+ return error(`benchmark ${bid} not found`);
116
+ }
117
+ const cases = listCases(baseDir, bid);
118
+ if (cases.length === 0) {
119
+ return error(`benchmark ${bid} has no cases — use /evolve benchmark add-case`);
120
+ }
121
+ const board = loadScoreboard(baseDir, bid);
122
+ const label = candidateId ? `candidate:${candidateId}` : "reference";
123
+ if (!candidateId && board.reference) {
124
+ return error(`reference already evaluated (${board.reference.overall ?? "?"}); evaluate a candidate instead: /evolve benchmark run ${bid} candidate <refinementId>`);
125
+ }
126
+ const overview = formatHarnessStateForPrompt(engine.load("local", sessionId));
127
+ const outcome = await evaluateState(ctx, invocation.agent, {
128
+ cases,
129
+ rubricKey: runtime.rubricKey,
130
+ runs: definition.runs,
131
+ passThreshold: definition.passThreshold,
132
+ harnessOverview: overview,
133
+ label,
134
+ signal: invocation.signal,
135
+ });
136
+ // Gap A3 (version_changed semantics): when a reference exists, re-check
137
+ // the candidate's cells for material drift — a case whose statement/rubric
138
+ // hash differs from the reference run is re-marked failed (never counted
139
+ // as a score, can reject the round via the failure-cell protocol).
140
+ const cells = candidateId && board.reference ? flagMaterialDrift(board.reference, outcome.cells) : outcome.cells;
141
+ const entry = entryFromCells(label, cells, candidateId);
142
+ const failedCells = cells.filter((cell) => cell.status === "failed").length;
143
+ const lines = [
144
+ `evaluation "${label}": ${outcome.cells.length} cells${failedCells > 0 ? `, ${failedCells} failed` : ""}, overall=${entry.overall ?? "?"}`,
145
+ ...Object.entries(entry.aggregate)
146
+ .filter(([key]) => key !== "overall" && key !== "failed" && key !== "total")
147
+ .map(([key, value]) => ` ${key}: ${value ?? "?"}`),
148
+ ];
149
+ if (failedCells > 0) {
150
+ for (const cell of cells.filter((cell) => cell.status === "failed")) {
151
+ lines.push(` [failed] ${cell.caseId} r${cell.run}: ${cell.notes}`);
152
+ }
153
+ }
154
+ if (candidateId) {
155
+ if (!board.reference) {
156
+ lines.push("(no reference yet — this run only recorded the candidate)");
157
+ board.candidates.push(entry);
158
+ }
159
+ else {
160
+ const decision = decide(board.reference, entry, {
161
+ passThreshold: definition.passThreshold,
162
+ regressionTolerance: 0,
163
+ maxFailedCells: 0,
164
+ });
165
+ board.candidates.push(entry);
166
+ board.decisions.push({
167
+ candidateLabel: label,
168
+ refinementId: candidateId,
169
+ accepted: decision.accepted,
170
+ reasons: decision.reasons,
171
+ createdAt: new Date().toISOString(),
172
+ });
173
+ lines.push(...decisionReport(board.reference, entry, decision));
174
+ if (!decision.accepted) {
175
+ lines.push(`Consider rolling back the candidate: /evolve rollback <${candidateId}>`);
176
+ if (runtime.autoRollbackOnReject) {
177
+ const outcome = rollbackRejectedCandidate(engine, sessionId, candidateId);
178
+ lines.push(outcome.message);
179
+ }
180
+ }
181
+ }
182
+ }
183
+ else {
184
+ board.reference = entry;
185
+ lines.push("reference evaluation recorded as the baseline");
186
+ }
187
+ saveScoreboard(baseDir, bid, board);
188
+ return success(lines.join("\n"));
189
+ }
190
+ case "casecheck": {
191
+ const bid = stripAngleBrackets(args[0] ?? "");
192
+ if (!bid) {
193
+ return error(`benchmark casecheck needs a <bid>.\n${BENCHMARK_USAGE}`);
194
+ }
195
+ const definition = loadBenchmark(baseDir, bid);
196
+ if (!definition) {
197
+ return error(`benchmark ${bid} not found`);
198
+ }
199
+ const cases = listCases(baseDir, bid);
200
+ if (cases.length === 0) {
201
+ return error(`benchmark ${bid} has no cases`);
202
+ }
203
+ const lines = [];
204
+ let totalProblems = 0;
205
+ for (const c of cases) {
206
+ const problems = caseCheckProblems(baseDir, bid, c.id);
207
+ const meta = loadCaseMeta(baseDir, bid, c.id);
208
+ const status = meta?.status ?? "draft";
209
+ if (problems.length === 0) {
210
+ lines.push(` ${c.id} [${status}] ✓`);
211
+ }
212
+ else {
213
+ lines.push(` ${c.id} [${status}] ✗ (${problems.length} problem${problems.length > 1 ? "s" : ""}):`);
214
+ for (const p of problems) {
215
+ lines.push(` - ${p}`);
216
+ }
217
+ totalProblems += problems.length;
218
+ }
219
+ }
220
+ const verdict = totalProblems === 0 ? "✓ all cases pass quality gate" : `✗ ${totalProblems} problem${totalProblems > 1 ? "s" : ""} found`;
221
+ return success(`casecheck ${bid}: ${verdict}\n${cases.length} cases checked\n${lines.join("\n")}`);
222
+ }
223
+ case "pilot": {
224
+ const bid = stripAngleBrackets(args[0] ?? "");
225
+ const cid = stripAngleBrackets(args[1] ?? "");
226
+ if (!bid || !cid) {
227
+ return error(`benchmark pilot needs <bid> <cid>.\n${BENCHMARK_USAGE}`);
228
+ }
229
+ const definition = loadBenchmark(baseDir, bid);
230
+ if (!definition) {
231
+ return error(`benchmark ${bid} not found`);
232
+ }
233
+ const cases = listCases(baseDir, bid);
234
+ const target = cases.find((c) => c.id === cid);
235
+ if (!target) {
236
+ return error(`case ${cid} not found in ${bid}`);
237
+ }
238
+ // Transition to calibrating if currently draft.
239
+ const meta = loadCaseMeta(baseDir, bid, cid);
240
+ if (meta?.status === "frozen") {
241
+ return error(`case ${cid} is frozen and cannot be calibrated`);
242
+ }
243
+ if (meta?.status !== "calibrating") {
244
+ transitionCaseStatus(baseDir, bid, cid, "calibrating");
245
+ }
246
+ // Run a single evaluation on just this case (1 run).
247
+ const overview = formatHarnessStateForPrompt(engine.load("local", sessionId));
248
+ const outcome = await evaluateState(ctx, invocation.agent, {
249
+ cases: [target],
250
+ rubricKey: runtime.rubricKey,
251
+ runs: 1,
252
+ passThreshold: definition.passThreshold,
253
+ harnessOverview: overview,
254
+ label: `pilot:${cid}`,
255
+ signal: invocation.signal,
256
+ });
257
+ const cell = outcome.cells[0];
258
+ const scoreText = cell?.status === "ok" ? `${cell.score} (${cell.passed ? "passed" : "below threshold"})` : `failed: ${cell?.notes ?? "unknown"}`;
259
+ // Record in calibration history.
260
+ const updatedMeta = loadCaseMeta(baseDir, bid, cid) ?? { status: "calibrating", capability: "", distinguisher: "", shortcuts: "", calibrationHistory: [] };
261
+ updatedMeta.calibrationHistory.push({
262
+ runAt: new Date().toISOString(),
263
+ score: cell?.status === "ok" ? cell.score : 0,
264
+ passed: cell?.passed ?? false,
265
+ notes: cell?.notes ?? "",
266
+ modified: false,
267
+ });
268
+ saveCaseMeta(baseDir, bid, cid, updatedMeta);
269
+ const lines = [
270
+ `pilot ${cid}: ${scoreText}`,
271
+ `status: calibrating`,
272
+ `calibration runs: ${updatedMeta.calibrationHistory.length}`,
273
+ ];
274
+ if (cell?.status === "ok" && cell.sessionId) {
275
+ lines.push(`session: ${cell.sessionId}`);
276
+ }
277
+ lines.push(`next: set meta fields, then /evolve benchmark freeze ${bid} ${cid}`);
278
+ return success(lines.join("\n"));
279
+ }
280
+ case "freeze": {
281
+ const bid = stripAngleBrackets(args[0] ?? "");
282
+ const cid = stripAngleBrackets(args[1] ?? "");
283
+ if (!bid || !cid) {
284
+ return error(`benchmark freeze needs <bid> <cid>.\n${BENCHMARK_USAGE}`);
285
+ }
286
+ const definition = loadBenchmark(baseDir, bid);
287
+ if (!definition) {
288
+ return error(`benchmark ${bid} not found`);
289
+ }
290
+ const meta = loadCaseMeta(baseDir, bid, cid);
291
+ if (!meta) {
292
+ return error(`case ${cid} not found in ${bid}`);
293
+ }
294
+ if (meta.status === "frozen") {
295
+ return error(`case ${cid} is already frozen`);
296
+ }
297
+ // Require quality check to pass before freezing.
298
+ const problems = caseCheckProblems(baseDir, bid, cid);
299
+ if (problems.length > 0) {
300
+ return error(`case ${cid} has ${problems.length} quality problem${problems.length > 1 ? "s" : ""}:\n${problems.map((p) => ` - ${p}`).join("\n")}\nFix these before freezing.`);
301
+ }
302
+ transitionCaseStatus(baseDir, bid, cid, "frozen");
303
+ return success(`case ${cid} frozen as formal baseline (immutable)`);
304
+ }
305
+ case "meta": {
306
+ const bid = stripAngleBrackets(args[0] ?? "");
307
+ const cid = stripAngleBrackets(args[1] ?? "");
308
+ const field = args[2] ?? "";
309
+ const value = args.slice(3).join(" ");
310
+ if (!bid || !cid || !field || !value) {
311
+ return error(`benchmark meta needs <bid> <cid> <field> <value> (fields: capability, distinguisher, shortcuts).\n${BENCHMARK_USAGE}`);
312
+ }
313
+ const meta = loadCaseMeta(baseDir, bid, cid);
314
+ if (!meta) {
315
+ return error(`case ${cid} not found in ${bid}`);
316
+ }
317
+ if (meta.status === "frozen") {
318
+ return error(`case ${cid} is frozen and cannot be modified`);
319
+ }
320
+ if (field !== "capability" && field !== "distinguisher" && field !== "shortcuts") {
321
+ return error(`unknown meta field "${field}" — valid fields: capability, distinguisher, shortcuts`);
322
+ }
323
+ meta[field] = value;
324
+ saveCaseMeta(baseDir, bid, cid, meta);
325
+ return success(`case ${cid} ${field} updated`);
326
+ }
327
+ default:
328
+ return error(`unknown benchmark subcommand: ${sub}\n${BENCHMARK_USAGE}`);
329
+ }
330
+ }
331
+ //# sourceMappingURL=benchmark-command.js.map
@@ -4,6 +4,37 @@ export interface BenchmarkCase {
4
4
  title: string;
5
5
  statement: string;
6
6
  rubric: string;
7
+ /**
8
+ * Case lifecycle state (gap A5): draft → calibrating → frozen.
9
+ * - draft: newly added, not yet quality-checked or calibrated
10
+ * - calibrating: pilot run in progress (case may be edited)
11
+ * - frozen: calibrated, locked as a formal baseline (immutable)
12
+ */
13
+ status?: "draft" | "calibrating" | "frozen";
14
+ }
15
+ /**
16
+ * Persistent per-case metadata (stored in `cases/<cid>/meta.json`).
17
+ * Carries quality-gate annotations and calibration history.
18
+ */
19
+ export interface CaseMeta {
20
+ status: "draft" | "calibrating" | "frozen";
21
+ /** What this case tests — the capability contract. */
22
+ capability: string;
23
+ /** What distinguishes a pass from a fail. */
24
+ distinguisher: string;
25
+ /** Known shortcuts the agent might use to game the rubric. */
26
+ shortcuts: string;
27
+ /** Calibration run history (appended on each pilot run). */
28
+ calibrationHistory: CalibrationRecord[];
29
+ }
30
+ /** One pilot-run record in the calibration history. */
31
+ export interface CalibrationRecord {
32
+ runAt: string;
33
+ score: number;
34
+ passed: boolean;
35
+ notes: string;
36
+ /** Whether the case was modified after this run. */
37
+ modified: boolean;
7
38
  }
8
39
  export interface BenchmarkDefinition {
9
40
  id: string;
@@ -33,6 +64,27 @@ export interface CellScore {
33
64
  * back to the exact session steps that earned it.
34
65
  */
35
66
  sessionId?: string;
67
+ /**
68
+ * Runtime evidence verification (gap A3): the actual provider and model
69
+ * used by the evaluation units — written from the host (not the model),
70
+ * so it reflects reality. Combined with `caseHash`, these make material
71
+ * and route drift between reference and candidate runs detectable:
72
+ * `score.flagMaterialDrift` re-marks a candidate cell failed when its
73
+ * case hash no longer matches the reference (version_changed semantics).
74
+ */
75
+ provider?: string;
76
+ model?: string;
77
+ /**
78
+ * Material hash: SHA-256 prefix of the case statement + rubric envelope,
79
+ * so a material change between reference and candidate runs is detectable.
80
+ * absence means pre-A3 cell (backward compatible).
81
+ */
82
+ caseHash?: string;
83
+ /**
84
+ * Gap C3: wall-clock duration of this cell's evaluation (both executor +
85
+ * reviewer subagents) in milliseconds. Absent means pre-C3 cell.
86
+ */
87
+ durationMs?: number;
36
88
  }
37
89
  export interface EvaluationEntry {
38
90
  label: string;
@@ -82,5 +134,23 @@ export declare function addCase(baseDir: string, bid: string, title: string, sta
82
134
  export declare function listCases(baseDir: string, bid: string): BenchmarkCase[];
83
135
  export declare function loadScoreboard(baseDir: string, bid: string): Scoreboard;
84
136
  export declare function saveScoreboard(baseDir: string, bid: string, board: Scoreboard): void;
137
+ export declare function loadCaseMeta(baseDir: string, bid: string, cid: string): CaseMeta | undefined;
138
+ export declare function saveCaseMeta(baseDir: string, bid: string, cid: string, meta: CaseMeta): void;
139
+ /** List case metas for all cases in a benchmark (missing meta → defaults). */
140
+ export declare function listCaseMetas(baseDir: string, bid: string): Map<string, CaseMeta>;
141
+ /** Check whether a case is frozen (immutable). */
142
+ export declare function isCaseFrozen(baseDir: string, bid: string, cid: string): boolean;
143
+ /**
144
+ * Transition a case's lifecycle state. Throws on illegal transitions.
145
+ * draft → calibrating (start pilot)
146
+ * calibrating → draft (abandon calibration)
147
+ * calibrating → frozen (lock baseline)
148
+ */
149
+ export declare function transitionCaseStatus(baseDir: string, bid: string, cid: string, to: "draft" | "calibrating" | "frozen"): CaseMeta;
150
+ /**
151
+ * Quality-check a case: mechanical validation without LLM calls.
152
+ * Returns human-readable problems; empty array means the case passes.
153
+ */
154
+ export declare function caseCheckProblems(baseDir: string, bid: string, cid: string): string[];
85
155
  export declare function removeBenchmark(baseDir: string, bid: string): void;
86
156
  //# sourceMappingURL=benchmark.d.ts.map
package/lib/benchmark.js CHANGED
@@ -106,6 +106,8 @@ export function addCase(baseDir, bid, title, statement, rubric, rubricKey) {
106
106
  // resolveRubricKey) and the dev key here is only a defensive last resort.
107
107
  const stored = rubricKey ? encryptRubric(rubric, rubricKey) : encryptRubric(rubric, deriveKey(DEV_RUBRIC_KEY));
108
108
  writeFileSync(join(caseDir, "rubric.json"), `${JSON.stringify(stored, null, 2)}\n`, "utf8");
109
+ // A5: initialize case metadata for quality gate.
110
+ saveCaseMeta(baseDir, bid, id, defaultCaseMeta());
109
111
  return { id, title: title.trim(), statement, rubric };
110
112
  }
111
113
  export function listCases(baseDir, bid) {
@@ -121,7 +123,8 @@ export function listCases(baseDir, bid) {
121
123
  try {
122
124
  const statement = readFileSync(statementPath, "utf8");
123
125
  const rubric = JSON.parse(readFileSync(rubricPath, "utf8"));
124
- return { id, title: id, statement, rubric };
126
+ const meta = loadCaseMeta(baseDir, bid, id);
127
+ return { id, title: id, statement, rubric, ...(meta ? { status: meta.status } : {}) };
125
128
  }
126
129
  catch {
127
130
  return undefined;
@@ -149,6 +152,109 @@ export function loadScoreboard(baseDir, bid) {
149
152
  export function saveScoreboard(baseDir, bid, board) {
150
153
  writeFileSync(join(benchmarkDir(baseDir, bid), "scoreboard.json"), `${JSON.stringify(board, null, 2)}\n`, "utf8");
151
154
  }
155
+ // ── Case meta (gap A5: quality gate + calibration history) ────────────
156
+ export function loadCaseMeta(baseDir, bid, cid) {
157
+ const path = join(benchmarkDir(baseDir, bid), "cases", cid, "meta.json");
158
+ if (!existsSync(path))
159
+ return undefined;
160
+ try {
161
+ return JSON.parse(readFileSync(path, "utf8"));
162
+ }
163
+ catch {
164
+ return undefined;
165
+ }
166
+ }
167
+ export function saveCaseMeta(baseDir, bid, cid, meta) {
168
+ const metaDir = join(benchmarkDir(baseDir, bid), "cases", cid);
169
+ mkdirSync(metaDir, { recursive: true });
170
+ writeFileSync(join(metaDir, "meta.json"), `${JSON.stringify(meta, null, 2)}\n`, "utf8");
171
+ }
172
+ /** List case metas for all cases in a benchmark (missing meta → defaults). */
173
+ export function listCaseMetas(baseDir, bid) {
174
+ const result = new Map();
175
+ const cases = listCases(baseDir, bid);
176
+ for (const c of cases) {
177
+ const meta = loadCaseMeta(baseDir, bid, c.id);
178
+ if (meta) {
179
+ result.set(c.id, meta);
180
+ }
181
+ }
182
+ return result;
183
+ }
184
+ /** Check whether a case is frozen (immutable). */
185
+ export function isCaseFrozen(baseDir, bid, cid) {
186
+ const meta = loadCaseMeta(baseDir, bid, cid);
187
+ return meta?.status === "frozen";
188
+ }
189
+ /**
190
+ * Transition a case's lifecycle state. Throws on illegal transitions.
191
+ * draft → calibrating (start pilot)
192
+ * calibrating → draft (abandon calibration)
193
+ * calibrating → frozen (lock baseline)
194
+ */
195
+ export function transitionCaseStatus(baseDir, bid, cid, to) {
196
+ const meta = loadCaseMeta(baseDir, bid, cid) ?? defaultCaseMeta();
197
+ const from = meta.status;
198
+ const valid = (from === "draft" && to === "calibrating") ||
199
+ (from === "calibrating" && to === "draft") ||
200
+ (from === "calibrating" && to === "frozen");
201
+ if (!valid) {
202
+ throw new Error(`illegal case status transition: ${from} → ${to}`);
203
+ }
204
+ meta.status = to;
205
+ saveCaseMeta(baseDir, bid, cid, meta);
206
+ return meta;
207
+ }
208
+ function defaultCaseMeta() {
209
+ return {
210
+ status: "draft",
211
+ capability: "",
212
+ distinguisher: "",
213
+ shortcuts: "",
214
+ calibrationHistory: [],
215
+ };
216
+ }
217
+ /**
218
+ * Quality-check a case: mechanical validation without LLM calls.
219
+ * Returns human-readable problems; empty array means the case passes.
220
+ */
221
+ export function caseCheckProblems(baseDir, bid, cid) {
222
+ const problems = [];
223
+ const statementPath = join(benchmarkDir(baseDir, bid), "cases", cid, "statement.md");
224
+ if (!existsSync(statementPath)) {
225
+ problems.push("statement.md missing");
226
+ return problems;
227
+ }
228
+ const statement = readFileSync(statementPath, "utf8").trim();
229
+ if (statement.length < 20) {
230
+ problems.push(`statement too short (${statement.length} chars, minimum 20)`);
231
+ }
232
+ const rubricPath = join(benchmarkDir(baseDir, bid), "cases", cid, "rubric.json");
233
+ if (!existsSync(rubricPath)) {
234
+ problems.push("rubric.json missing");
235
+ return problems;
236
+ }
237
+ try {
238
+ const raw = readFileSync(rubricPath, "utf8");
239
+ JSON.parse(raw);
240
+ }
241
+ catch {
242
+ problems.push("rubric.json is not valid JSON");
243
+ }
244
+ const meta = loadCaseMeta(baseDir, bid, cid);
245
+ if (!meta) {
246
+ problems.push("meta.json missing (run /evolve benchmark casecheck to initialize)");
247
+ }
248
+ else {
249
+ if (!meta.capability)
250
+ problems.push("capability contract is empty");
251
+ if (!meta.distinguisher)
252
+ problems.push("distinguisher is empty");
253
+ if (!meta.shortcuts)
254
+ problems.push("shortcuts annotation is empty");
255
+ }
256
+ return problems;
257
+ }
152
258
  export function removeBenchmark(baseDir, bid) {
153
259
  const dir = benchmarkDir(baseDir, bid);
154
260
  if (existsSync(dir)) {