bmad-method-test-architecture-enterprise 3.2.0 → 3.4.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 (31) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/cli/evaluate.js +20 -0
  3. package/cli/lib/agent-adapters.js +46 -0
  4. package/cli/lib/evaluate/configuration-experiment.js +3401 -0
  5. package/cli/lib/evaluate/experiments.js +1299 -0
  6. package/cli/lib/evaluate/retain-skill-artifacts.js +78 -0
  7. package/cli/lib/evaluate/test-design-corpus/clean/docs/epics/epic-9-last-sync-indicator.md +47 -0
  8. package/cli/lib/evaluate/test-design-corpus/ground-truth.json +222 -0
  9. package/cli/lib/evaluate/test-design-corpus/native-identity.json +4 -0
  10. package/cli/lib/evaluate/test-design-corpus/seeded/docs/epics/epic-7-offline-order-capture.md +75 -0
  11. package/cli/lib/evaluate/test-design-replay.js +453 -0
  12. package/cli/lib/run-agent.js +18 -2
  13. package/cli/lib/test-design-staged-config.js +60 -0
  14. package/cli/lib/test-design-staging.js +229 -0
  15. package/cli/test-design-runner.js +245 -11
  16. package/package.json +4 -3
  17. package/skills/bmad-tea-automate/red/atdd-checklist-template.md +1 -1
  18. package/skills/bmad-tea-automate/red/resources/atdd-checklist-2-4-guest-checkout.example.md +1 -1
  19. package/skills/bmad-tea-automate/red/steps-c/step-01-preflight-and-context.md +1 -1
  20. package/skills/bmad-tea-automate/red/steps-c/step-05-validate-and-complete.md +1 -1
  21. package/skills/bmad-tea-evaluate/SKILL.md +11 -0
  22. package/skills/bmad-tea-evaluate/references/configuration-experiments.md +37 -0
  23. package/skills/bmad-tea-test-design/checklist.md +39 -8
  24. package/skills/bmad-tea-test-design/steps-c/step-03-risk-and-testability.md +1 -1
  25. package/skills/bmad-tea-test-design/steps-c/step-04-coverage-plan.md +10 -3
  26. package/skills/bmad-tea-test-design/steps-c/step-05-generate-output.md +69 -21
  27. package/skills/bmad-tea-test-design/steps-e/step-02-apply-edit.md +3 -0
  28. package/skills/bmad-tea-test-design/steps-v/step-01-validate.md +1 -0
  29. package/skills/bmad-tea-test-design/test-design-qa-template.md +1 -1
  30. package/skills/bmod-tea/bmod.toml +1 -1
  31. package/skills/bmod-tea/help/help.md +1 -1
@@ -0,0 +1,1299 @@
1
+ /** Explicit opt-in experiments. Offline imports never load an execution mechanism. */
2
+ 'use strict';
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const crypto = require('node:crypto');
6
+ const reader = require('./configuration-experiment');
7
+ const scoring = require('./test-design-replay');
8
+ const staging = require('../test-design-staging');
9
+ const { digest, digestPrompts } = require('./digest');
10
+ const { decimalNumber } = require('./usage-report');
11
+ const usdText = (value) => (typeof value === 'string' ? value : decimalNumber(value));
12
+ const usdParts = (value) => {
13
+ const text = usdText(value);
14
+ const [whole, fraction = ''] = text.split('.');
15
+ return { units: BigInt(whole + fraction), places: fraction.length };
16
+ };
17
+ function usdSum(values) {
18
+ const parts = values.map(usdParts);
19
+ const places = Math.max(0, ...parts.map((part) => part.places));
20
+ const units = parts.reduce((sum, part) => sum + part.units * 10n ** BigInt(places - part.places), 0n);
21
+ const digits = String(units).padStart(places + 1, '0');
22
+ return places === 0 ? digits : `${digits.slice(0, -places)}.${digits.slice(-places)}`;
23
+ }
24
+ function usdGreater(left, right) {
25
+ const a = usdParts(left);
26
+ const b = usdParts(right);
27
+ const places = Math.max(a.places, b.places);
28
+ return a.units * 10n ** BigInt(places - a.places) > b.units * 10n ** BigInt(places - b.places);
29
+ }
30
+ function usdMultiply(value, count) {
31
+ const part = usdParts(value);
32
+ const digits = String(part.units * BigInt(count)).padStart(part.places + 1, '0');
33
+ return part.places === 0 ? digits : `${digits.slice(0, -part.places)}.${digits.slice(-part.places)}`;
34
+ }
35
+ function stageCases(declaration) {
36
+ const memberships = declaration.scope?.memberships ?? {};
37
+ return declaration.stage === 'confirmation'
38
+ ? []
39
+ : declaration.stage === 'selection'
40
+ ? [...(memberships.development ?? []), ...(memberships.validation ?? [])]
41
+ : (memberships.development ?? []);
42
+ }
43
+ const NATIVE_IDENTITY = nativeSuiteIdentity();
44
+ const PACKAGE_ROOT = path.resolve(__dirname, '../../..');
45
+ const sha = (bytes) => `sha256:${crypto.createHash('sha256').update(bytes).digest('hex')}`;
46
+ const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
47
+ const now = () => new Date().toISOString();
48
+ function writeJson(file, value) {
49
+ const fd = fs.openSync(file, 'wx');
50
+ try {
51
+ fs.writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`);
52
+ fs.fsyncSync(fd);
53
+ } finally {
54
+ fs.closeSync(fd);
55
+ }
56
+ }
57
+ function confinedPath(root, relative) {
58
+ if (
59
+ typeof relative !== 'string' ||
60
+ path.isAbsolute(relative) ||
61
+ relative !== path.normalize(relative) ||
62
+ relative === '..' ||
63
+ relative.startsWith(`..${path.sep}`)
64
+ )
65
+ throw new Error('Evidence path must be relative and confined to its archive');
66
+ let file = root;
67
+ for (const part of relative.split(path.sep)) {
68
+ file = path.join(file, part);
69
+ if (fs.lstatSync(file).isSymbolicLink()) throw new Error('Evidence path contains a symbolic link');
70
+ }
71
+ return file;
72
+ }
73
+ function regularBytes(root, relative) {
74
+ const file = confinedPath(root, relative);
75
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
76
+ try {
77
+ const held = fs.fstatSync(fd);
78
+ const named = fs.lstatSync(file);
79
+ if (!held.isFile() || held.nlink !== 1 || named.dev !== held.dev || named.ino !== held.ino || named.nlink !== 1)
80
+ throw new Error('Evidence must be a regular file with one link');
81
+ return fs.readFileSync(fd);
82
+ } finally {
83
+ fs.closeSync(fd);
84
+ }
85
+ }
86
+ function runnerSource(workspace, observation) {
87
+ let result;
88
+ try {
89
+ result = (observation.stdout?.kind === 'json' ? observation.stdout.value : JSON.parse(observation.stdout?.value ?? '')).sourceCapture;
90
+ if (!['verified', 'rejected'].includes(result?.status) || path.basename(result.path) !== result.path)
91
+ throw new Error('runner supplied no trusted capture result');
92
+ if (result.status === 'verified' && result.path !== 'source-capture.json') throw new Error('unexpected source receipt path');
93
+ if (result.status === 'rejected' && !/^source-capture-recovery-[\da-f-]+\.json$/.test(result.path))
94
+ throw new Error('unexpected recovery receipt path');
95
+ const bytes = regularBytes(workspace.dir, result.path);
96
+ if (sha(bytes) !== result.sha256) throw new Error('source receipt differs from runner digest');
97
+ return { status: result.status, source: JSON.parse(bytes), result };
98
+ } catch (error) {
99
+ return { status: 'unverified', source: null, result: result ?? null, error: error.message };
100
+ }
101
+ }
102
+
103
+ function filesUnder(root) {
104
+ const result = [];
105
+ const walk = (directory) => {
106
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
107
+ if (entry.isSymbolicLink()) continue;
108
+ const file = path.join(directory, entry.name);
109
+ if (entry.isDirectory()) {
110
+ if (!['node_modules', '.git', 'workspace'].includes(entry.name)) walk(file);
111
+ } else if (entry.isFile()) result.push(file);
112
+ }
113
+ };
114
+ walk(path.resolve(root));
115
+ return result;
116
+ }
117
+ function nativeSuiteIdentity() {
118
+ const files = [];
119
+ const corpus = path.join(__dirname, 'test-design-corpus');
120
+ for (const set of scoring.loadGroundTruth().fixtureSets) {
121
+ const root = path.join(corpus, set.root);
122
+ for (const file of filesUnder(root))
123
+ files.push({ relative: `test/fixtures/test-design-eval/${set.root}/${path.relative(root, file).split(path.sep).join('/')}`, file });
124
+ }
125
+ const parts = files
126
+ .sort((a, b) => a.relative.localeCompare(b.relative))
127
+ .flatMap((entry) => [entry.relative, fs.readFileSync(entry.file)]);
128
+ return { suiteDigest: digest(parts), promptDigest: digestPrompts(staging.caseIndex(scoring.loadGroundTruth().fixtureSets)) };
129
+ }
130
+ function verifyManifest(directory, required = []) {
131
+ const manifest = JSON.parse(regularBytes(directory, 'manifest.json'));
132
+ if (!Array.isArray(manifest.files)) throw new Error('manifest.files must be an array');
133
+ const seen = new Set();
134
+ const verifiedBytes = new Map();
135
+ for (const entry of manifest.files) {
136
+ const relative = path.normalize(entry.path);
137
+ if (path.isAbsolute(relative) || relative === '..' || relative.startsWith(`..${path.sep}`) || seen.has(relative))
138
+ throw new Error('invalid or duplicate manifest path');
139
+ seen.add(relative);
140
+ let file = directory;
141
+ for (const part of relative.split(path.sep)) {
142
+ file = path.join(file, part);
143
+ if (fs.lstatSync(file).isSymbolicLink()) throw new Error('manifest contains a symbolic link');
144
+ }
145
+ const bytes = regularBytes(directory, relative);
146
+ if (entry.bytes !== bytes.length || entry.sha256 !== sha(bytes).slice(7)) throw new Error(`integrity failure: ${entry.path}`);
147
+ verifiedBytes.set(relative, bytes);
148
+ }
149
+ for (const relative of required)
150
+ if (!seen.has(path.normalize(relative))) throw new Error(`manifest does not list consumed evidence: ${relative}`);
151
+ return { ...manifest, verifiedBytes };
152
+ }
153
+ function sealAttemptArchive(directory) {
154
+ const files = [];
155
+ const walk = (dir, prefix = '') => {
156
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
157
+ if (entry.name === 'manifest.json' && !prefix) continue;
158
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
159
+ if (entry.isSymbolicLink()) throw new Error('archive contains a symbolic link');
160
+ if (entry.isDirectory()) walk(path.join(dir, entry.name), relative);
161
+ else {
162
+ const bytes = fs.readFileSync(path.join(dir, entry.name));
163
+ files.push({ path: relative, bytes: bytes.length, sha256: sha(bytes).slice(7) });
164
+ }
165
+ }
166
+ };
167
+ walk(directory);
168
+ writeJson(path.join(directory, 'manifest.json'), { files });
169
+ }
170
+ function discoverSaved({ searchRoot, saved = [], skipped = [] }) {
171
+ if (saved.length > 0) return saved.map((file) => path.resolve(file));
172
+ if (!searchRoot) throw new Error('Name a saved run or a search root for the latest compatible Test Design records');
173
+ const directories = [
174
+ ...new Set(
175
+ filesUnder(searchRoot)
176
+ .filter((file) => ['result.json', 'provenance.json', 'plan.json'].includes(path.basename(file)))
177
+ .map((file) => path.dirname(file)),
178
+ ),
179
+ ];
180
+ const groups = [];
181
+ const experimentRoots = [];
182
+ const candidate = (directory, read) => {
183
+ try {
184
+ return read();
185
+ } catch (error) {
186
+ skipped.push({ directories: [directory], reason: error.message });
187
+ return null;
188
+ }
189
+ };
190
+ const stamp = (value, fallback) => (Number.isFinite(Date.parse(value)) ? Date.parse(value) : fs.statSync(fallback).mtimeMs);
191
+ for (const dir of directories.filter(
192
+ (dir) => fs.existsSync(path.join(dir, 'plan.json')) && fs.existsSync(path.join(dir, 'journal.jsonl')),
193
+ )) {
194
+ experimentRoots.push(dir);
195
+ const plan = candidate(dir, () => readJson(path.join(dir, 'plan.json')));
196
+ if (!plan) continue;
197
+ if (
198
+ plan.declaration?.scope?.workflow !== 'bmad-tea-test-design' ||
199
+ plan.declaration?.identity?.fixed?.suiteDigest !== NATIVE_IDENTITY.suiteDigest
200
+ )
201
+ continue;
202
+ const journal = candidate(dir, () => readJournal(dir));
203
+ if (!journal) continue;
204
+ if (!journal.some((entry) => entry.kind === 'sealed' || entry.kind === 'completion')) continue;
205
+ groups.push({
206
+ kind: 'experiment',
207
+ directories: [dir],
208
+ at: Math.max(...journal.filter((entry) => entry.kind === 'completion').map((entry) => stamp(entry.at, dir))),
209
+ });
210
+ }
211
+ const insideExperiment = (dir) => experimentRoots.some((root) => dir.startsWith(`${root}${path.sep}`));
212
+ for (const dir of directories.filter(
213
+ (dir) => !insideExperiment(dir) && fs.existsSync(path.join(dir, 'result.json')) && fs.existsSync(path.join(dir, 'manifest.json')),
214
+ )) {
215
+ const result = candidate(dir, () => readJson(path.join(dir, 'result.json')));
216
+ if (!result || result.runners?.length !== 1) continue;
217
+ if (result.suite?.fixtureDigest !== NATIVE_IDENTITY.suiteDigest) continue;
218
+ groups.push({ kind: 'legacy', directories: [dir], at: stamp(result.generatedAt, dir) });
219
+ }
220
+ const retainedGroups = new Map();
221
+ for (const dir of directories.filter(
222
+ (dir) =>
223
+ !insideExperiment(dir) &&
224
+ fs.existsSync(path.join(dir, 'provenance.json')) &&
225
+ fs.existsSync(path.join(dir, 'prompt.txt')) &&
226
+ fs.existsSync(path.join(dir, 'observation.json')),
227
+ )) {
228
+ const provenance = candidate(dir, () => readJson(path.join(dir, 'provenance.json')));
229
+ if (!provenance) continue;
230
+ // An unbound collection is a discovery scope only; its parent establishes no invocation identity.
231
+ const groupId = `${path.dirname(dir)}:${provenance.evaluationId ?? provenance.invocationId ?? 'historical-unbound-siblings'}`;
232
+ const group = retainedGroups.get(groupId) ?? { kind: 'retained', directories: [], at: 0 };
233
+ group.directories.push(dir);
234
+ group.at = Math.max(group.at, stamp(provenance.capturedAt, dir));
235
+ retainedGroups.set(groupId, group);
236
+ }
237
+ groups.push(...retainedGroups.values());
238
+ if (groups.length === 0)
239
+ throw new Error(
240
+ `No compatible Test Design records found. Expected native suite identity ${NATIVE_IDENTITY.suiteDigest}, or retained native cases with prompt/observation/workspace and matching companion ground truth`,
241
+ );
242
+ const groundTruth = scoring.loadGroundTruth();
243
+ for (const group of groups.sort((a, b) => b.at - a.at || a.directories[0].localeCompare(b.directories[0]))) {
244
+ try {
245
+ if (group.kind === 'retained') {
246
+ const groupRows = [];
247
+ for (const dir of group.directories) {
248
+ const rows = retainedRows(dir, [], groundTruth);
249
+ groupRows.push(...rows);
250
+ }
251
+ if (!groupRows.some((row) => row.provenance.replayCompatible))
252
+ throw new Error('retained prompt/workspace input identity is incompatible or missing');
253
+ } else if (group.kind === 'legacy') {
254
+ const loaded = reader.loadRecordedTestDesignRun({ dir: group.directories[0] });
255
+ if (
256
+ loaded.integrity.problems.length > 0 ||
257
+ !loaded.rows.every((row) => {
258
+ const set = groundTruth.fixtureSets.find((entry) => entry.id === row.caseId);
259
+ return set && row.inputSha256 === sha(staging.buildPrompt(set));
260
+ })
261
+ )
262
+ throw new Error('legacy integrity or prompt identity is incompatible');
263
+ } else {
264
+ validateFrozen(group.directories[0]);
265
+ for (const entry of readJournal(group.directories[0]).filter((entry) => entry.kind === 'sealed')) {
266
+ const dir = confinedPath(group.directories[0], entry.directory);
267
+ if (sha(fs.readFileSync(path.join(dir, 'manifest.json'))) !== entry.manifestSha256)
268
+ throw new Error('sealed manifest differs from journal');
269
+ const loaded = reader.loadSealedTestDesignAttempt({ dir });
270
+ if (Object.values(loaded.records).some((record) => record.manifestSha256 !== entry.manifestSha256))
271
+ throw new Error('Sealed manifest differs from controller journal');
272
+ for (const row of loaded.rows)
273
+ if (
274
+ !retainedInputCompatible(
275
+ dir,
276
+ groundTruth.fixtureSets.find((set) => set.id === row.caseId),
277
+ loaded.verifiedBytes.get('prompt.txt'),
278
+ loaded.verifiedBytes,
279
+ )
280
+ )
281
+ throw new Error('experiment staged input identity is incompatible');
282
+ }
283
+ }
284
+ return group.directories.sort();
285
+ } catch (error) {
286
+ skipped.push({ directories: group.directories, reason: error.message });
287
+ }
288
+ }
289
+ throw new Error(`No compatible Test Design evidence group: ${skipped.map((entry) => entry.reason).join('; ')}`);
290
+ }
291
+
292
+ function retainedInputCompatible(directory, set, prompt, checkedBytes = null) {
293
+ if (!set || prompt.toString('utf8') !== staging.buildPrompt(set)) return false;
294
+ const corpus = path.join(__dirname, 'test-design-corpus', set.root);
295
+ const workspace = path.join(directory, 'workspace', set.projectRoot);
296
+ const manifest = !checkedBytes && fs.existsSync(path.join(directory, 'manifest.json')) ? verifyManifest(directory) : null;
297
+ const bytes = checkedBytes ?? manifest?.verifiedBytes;
298
+ for (const source of filesUnder(corpus)) {
299
+ const retained = path.join(workspace, path.relative(corpus, source));
300
+ if (bytes && !bytes.has(path.relative(directory, retained)))
301
+ throw new Error(`manifest does not list consumed evidence: ${path.relative(directory, retained)}`);
302
+ const retainedBytes =
303
+ bytes?.get(path.relative(directory, retained)) ??
304
+ (fs.existsSync(retained) ? regularBytes(directory, path.relative(directory, retained)) : null);
305
+ if (!retainedBytes || !fs.readFileSync(source).equals(retainedBytes)) return false;
306
+ }
307
+ return true;
308
+ }
309
+ function retainedRows(directory, limitations, groundTruth) {
310
+ const required = ['provenance.json', 'observation.json', 'prompt.txt'];
311
+ if (fs.existsSync(path.join(directory, 'diagnostic.json'))) required.push('diagnostic.json');
312
+ const sealed = fs.existsSync(path.join(directory, 'manifest.json'));
313
+ const manifest = sealed ? verifyManifest(directory, required) : null;
314
+ const read = (relative) => (manifest ? manifest.verifiedBytes.get(path.normalize(relative)) : regularBytes(directory, relative));
315
+ const provenance = JSON.parse(read('provenance.json'));
316
+ const observation = JSON.parse(read('observation.json'));
317
+ try {
318
+ if (
319
+ (observation.stdout?.kind === 'json' ? observation.stdout.value : JSON.parse(observation.stdout?.value ?? '{}')).sourceCapture
320
+ ?.status === 'rejected'
321
+ )
322
+ throw new Error('Rejected source capture cannot be imported as authentic saved evidence');
323
+ } catch (error) {
324
+ if (error.message.startsWith('Rejected source capture')) throw error;
325
+ }
326
+ const prompt = read('prompt.txt');
327
+ const set = groundTruth.fixtureSets.find((set) => set.id === provenance.caseId);
328
+ const compatible = retainedInputCompatible(directory, set, prompt, manifest?.verifiedBytes);
329
+ const diagnostic =
330
+ manifest?.verifiedBytes.has('diagnostic.json') || (!manifest && fs.existsSync(path.join(directory, 'diagnostic.json')))
331
+ ? JSON.parse(read('diagnostic.json'))
332
+ : null;
333
+ const text = observation.artifacts?.design?.value ?? null;
334
+ if (!compatible)
335
+ limitations.push(
336
+ `${directory}: input identity is incompatible or missing. Expected native Test Design case, exact prompt and matching retained input bytes; supply the original matching ground truth for scorer replay`,
337
+ );
338
+ if (!provenance.evaluationId && !provenance.invocationId)
339
+ limitations.push(
340
+ `${directory}: historical retention has no evaluation invocation identity; all retained siblings under this root are reported without asserting a common invocation`,
341
+ );
342
+ if (diagnostic)
343
+ limitations.push(
344
+ `${directory}: companion diagnostic has no verified source/native binding. Native stability and qualification are unknown; supply the original manifest-bound native result.json or source-captured attempt directory for ${directory}`,
345
+ );
346
+ else limitations.push(`${directory}: no companion diagnostic. Native stability and qualification are unknown`);
347
+ const replayed = compatible && text !== null ? reader.testDesignReplay({ groundTruth })(text, { caseId: provenance.caseId }) : null;
348
+ const projected = diagnostic
349
+ ? reader.metricsFromContributions(diagnostic.metricContributions ?? {})
350
+ : { metrics: replayed?.metrics ?? {}, counts: replayed?.counts ?? {}, notApplicable: replayed?.notApplicable ?? [] };
351
+ if (!diagnostic && replayed)
352
+ limitations.push(
353
+ `${directory}: metric spread uses the current deterministic scorer on compatible saved bytes; the native diagnostic remains unknown`,
354
+ );
355
+ return [
356
+ {
357
+ attemptId: directory,
358
+ runId: provenance.evaluationId ?? provenance.invocationId ?? null,
359
+ arm: 'A',
360
+ stage: 'baseline',
361
+ caseId: provenance.caseId ?? 'unknown',
362
+ clusterId: provenance.caseId ?? 'unknown',
363
+ repetition: provenance.repetition ?? 1,
364
+ attempt: provenance.attempt ?? 1,
365
+ status: provenance.status ?? (observation.exitCode === 0 && text !== null ? 'completed' : 'failed'),
366
+ error: observation.fault ?? null,
367
+ outputText: text,
368
+ outputSha256: text === null ? null : sha(text),
369
+ inputSha256: sha(prompt),
370
+ // Historical capture fields are copied exactly. No fresh receipt is generated while importing.
371
+ receipt: provenance.receipt ?? null,
372
+ observed: provenance.observed ?? null,
373
+ // These are provenance hints for descriptive observations, with no applied-configuration proof.
374
+ historicalContext: { agent: provenance.agent ?? null, requestedModel: provenance.requestedModel ?? null },
375
+ blockId: provenance.blockId ?? null,
376
+ usage: provenance.usage ?? null,
377
+ cost: provenance.cost ?? null,
378
+ latency: provenance.latency ?? null,
379
+ provenance: { ...provenance, replayCompatible: compatible },
380
+ ...projected,
381
+ },
382
+ ];
383
+ }
384
+ function nativeQualification(contributions) {
385
+ for (const [metric, minimum] of Object.entries(scoring.THRESHOLDS)) {
386
+ if (metric.startsWith('max')) continue;
387
+ const denominator = contributions[`${metric}.denominator`];
388
+ if (denominator === 0 && contributions[`${metric}.expected`] === 0) continue;
389
+ if (!(denominator > 0) || contributions[`${metric}.numerator`] / denominator < minimum) return 'failed';
390
+ }
391
+ for (const [value, maximum] of [
392
+ ['ungroundedRisks', 'maxUngroundedRisks'],
393
+ ['unscoredRiskTables', 'maxUnscoredRiskTables'],
394
+ ['topSeverityMissed', 'maxTopSeverityMissed'],
395
+ ['riskCeilingExcess', 'maxRiskCeilingExcess'],
396
+ ['fixtureMutations', 'maxFixtureMutations'],
397
+ ])
398
+ if (!(contributions[value] <= scoring.THRESHOLDS[maximum])) return 'failed';
399
+ return 'passed';
400
+ }
401
+ function nativeIdentity(workspace, set) {
402
+ const inputs = [];
403
+ for (const [prefix, root] of [
404
+ ['corpus', path.join(__dirname, 'test-design-corpus')],
405
+ ['skill', path.join(PACKAGE_ROOT, 'skills/bmad-tea-test-design')],
406
+ ['knowledge', path.join(PACKAGE_ROOT, 'skills/bmod-tea')],
407
+ ]) {
408
+ for (const file of filesUnder(root).sort()) {
409
+ const relative = path.relative(root, file);
410
+ let observedFile = file;
411
+ if (workspace) {
412
+ if (prefix === 'skill') observedFile = path.join(workspace.dir, 'skill', relative);
413
+ if (prefix === 'knowledge') observedFile = path.join(workspace.dir, 'bmod-tea', relative);
414
+ if (prefix === 'corpus' && relative.startsWith(`${set.root}${path.sep}`))
415
+ observedFile = path.join(workspace.projectDir, path.relative(set.root, relative));
416
+ }
417
+ inputs.push([`${prefix}/${relative}`, fs.existsSync(observedFile) ? sha(fs.readFileSync(observedFile)) : 'missing']);
418
+ }
419
+ if (workspace && prefix !== 'corpus') {
420
+ const stagedRoot = path.join(workspace.dir, prefix === 'skill' ? 'skill' : 'bmod-tea');
421
+ for (const file of filesUnder(stagedRoot).sort()) {
422
+ const relative = path.relative(stagedRoot, file);
423
+ if (!fs.existsSync(path.join(root, relative))) inputs.push([`${prefix}/${relative}`, sha(fs.readFileSync(file))]);
424
+ }
425
+ }
426
+ }
427
+ if (workspace) {
428
+ for (const relative of staging.filesUnder(workspace.projectDir)) {
429
+ if (
430
+ !relative.startsWith(`test-artifacts${path.sep}`) &&
431
+ !relative.startsWith(`${path.dirname(staging.TEA_CONFIG_RELATIVE_PATH)}${path.sep}`) &&
432
+ !workspace.corpusFiles.includes(relative)
433
+ )
434
+ inputs.push([`corpus/${set.root}/${relative}`, sha(fs.readFileSync(path.join(workspace.projectDir, relative)))]);
435
+ }
436
+ }
437
+ const config = workspace ? fs.readFileSync(path.join(workspace.projectDir, staging.TEA_CONFIG_RELATIVE_PATH)) : staging.configToml();
438
+ inputs.push(['config', sha(config)]);
439
+ return {
440
+ ...NATIVE_IDENTITY,
441
+ groundTruthDigest: sha(fs.readFileSync(path.join(__dirname, 'test-design-corpus/ground-truth.json'))),
442
+ scorerDigest: digest([
443
+ 'test-design-replay.js',
444
+ fs.readFileSync(path.join(__dirname, 'test-design-replay.js')),
445
+ 'test-design-parser.js',
446
+ fs.readFileSync(path.join(__dirname, '../test-design-parser.js')),
447
+ ]),
448
+ analysisDigest: digest([
449
+ 'configuration-experiment.js',
450
+ fs.readFileSync(path.join(__dirname, 'configuration-experiment.js')),
451
+ 'experiments.js',
452
+ fs.readFileSync(__filename),
453
+ ]),
454
+ executionDigest: digest(
455
+ [
456
+ 'test-design-runner.js',
457
+ 'lib/run-agent.js',
458
+ 'lib/agent-adapters.js',
459
+ 'lib/agent-supervisor.js',
460
+ 'lib/test-design-staging.js',
461
+ 'lib/test-design-staged-config.js',
462
+ 'lib/evaluate/retain-skill-artifacts.js',
463
+ ].flatMap((file) => [file, fs.readFileSync(path.join(PACKAGE_ROOT, 'cli', file))]),
464
+ ),
465
+ thresholdsDigest: reader.digestOf(scoring.THRESHOLDS),
466
+ rubricDigest: 'not-applicable',
467
+ inputTreeDigest: reader.digestOf(inputs),
468
+ environment: { cacheRegime: 'deterministic double; process and workspace fresh' },
469
+ };
470
+ }
471
+ function baselineDeclaration(rows, experimentId) {
472
+ const cases = [...new Set(rows.map((row) => row.caseId))];
473
+ return {
474
+ schemaVersion: '1.0.0',
475
+ experimentId,
476
+ stage: 'baseline',
477
+ scope: {
478
+ workflow: 'bmad-tea-test-design',
479
+ owner: 'saved-record analysis',
480
+ claim: 'fixed-suite',
481
+ judge: 'none',
482
+ baseline: { id: 'saved-records' },
483
+ memberships: { development: cases, validation: [], confirmation: [] },
484
+ },
485
+ identity: { fixed: {} },
486
+ design: { repeats: Math.max(1, ...rows.map((row) => row.repetition)) },
487
+ gates: { success: { metrics: {} } },
488
+ resources: { authorization: null },
489
+ };
490
+ }
491
+ function analyzeSaved({ searchRoot, saved, out }) {
492
+ const skipped = [];
493
+ const selected = discoverSaved({ searchRoot, saved, skipped });
494
+ const groundTruth = scoring.loadGroundTruth();
495
+ const limitations = [];
496
+ const attempts = [];
497
+ const records = {};
498
+ for (const directory of selected) {
499
+ if (fs.existsSync(path.join(directory, 'plan.json'))) {
500
+ const result = analyzeExperiment(directory);
501
+ result.report.skippedSavedGroups = skipped;
502
+ result.report.sourceLimitations.push(
503
+ ...skipped.map((entry) => `Skipped saved group ${entry.directories.join(', ')}: ${entry.reason}`),
504
+ );
505
+ if (out) {
506
+ fs.mkdirSync(out, { recursive: true });
507
+ writeJson(path.join(out, 'saved-inputs.json'), { selected });
508
+ reader.writeOutputs(result, out);
509
+ }
510
+ return result;
511
+ }
512
+ if (fs.existsSync(path.join(directory, 'result.json'))) {
513
+ const suite = readJson(path.join(directory, 'result.json')).suite;
514
+ if (suite?.fixtureDigest !== NATIVE_IDENTITY.suiteDigest)
515
+ throw new Error(
516
+ `Unsupported Test Design corpus: expected suite fixture identity ${NATIVE_IDENTITY.suiteDigest}. Supply its original matching ground truth and reader input; this route ships only the native two-case corpus`,
517
+ );
518
+ if (readJson(path.join(directory, 'result.json')).runners?.length !== 1)
519
+ throw new Error(
520
+ 'Legacy Test Design ingestion supports one runner per saved directory. Supply the corresponding per-attempt retained directories for a multi-runner result',
521
+ );
522
+ const loaded = reader.loadRecordedTestDesignRun({ dir: directory });
523
+ if (loaded.integrity.problems.length > 0) throw new Error(loaded.integrity.problems.join('\n'));
524
+ for (const row of loaded.rows) {
525
+ const set = groundTruth.fixtureSets.find((set) => set.id === row.caseId);
526
+ const compatible = Boolean(set) && row.inputSha256 === sha(staging.buildPrompt(set));
527
+ row.provenance.replayCompatible = compatible;
528
+ if (!compatible)
529
+ limitations.push(`${row.attemptId}: historical prompt identity differs from the shipped corpus; scorer replay unavailable`);
530
+ }
531
+ attempts.push(...loaded.rows);
532
+ Object.assign(records, loaded.records);
533
+ } else if (
534
+ fs.existsSync(path.join(directory, 'manifest.json')) &&
535
+ fs.existsSync(path.join(directory, 'workspace/source-capture.json')) &&
536
+ fs.existsSync(path.join(directory, 'diagnostic.json'))
537
+ ) {
538
+ const loaded = reader.loadSealedTestDesignAttempt({ dir: directory });
539
+ for (const row of loaded.rows) {
540
+ const set = groundTruth.fixtureSets.find((set) => set.id === row.caseId);
541
+ row.provenance.replayCompatible = retainedInputCompatible(
542
+ directory,
543
+ set,
544
+ loaded.verifiedBytes.get('prompt.txt'),
545
+ loaded.verifiedBytes,
546
+ );
547
+ row.stage = 'baseline';
548
+ row.arm = 'A';
549
+ }
550
+ attempts.push(...loaded.rows);
551
+ Object.assign(records, loaded.records);
552
+ } else attempts.push(...retainedRows(directory, limitations, groundTruth));
553
+ }
554
+ const declaration = baselineDeclaration(attempts, `saved-${crypto.randomUUID()}`);
555
+ const replay = reader.testDesignReplay({ groundTruth });
556
+ const result = reader.readExperiment({
557
+ declaration,
558
+ attempts,
559
+ nativeRecords: records,
560
+ replay: (text, row) => (row.provenance?.replayCompatible ? replay(text, row) : { unmeasurable: 'incompatible-input-identity' }),
561
+ });
562
+ // No business success threshold was authorized for this retrospective reading.
563
+ for (const group of result.report.descriptive?.groups ?? [])
564
+ for (const entry of group.cases) {
565
+ entry.successes = null;
566
+ entry.successFraction = null;
567
+ }
568
+ result.report.sourceLimitations.push(
569
+ ...limitations,
570
+ ...skipped.map((entry) => `Skipped saved group ${entry.directories.join(', ')}: ${entry.reason}`),
571
+ 'No recorded experiment success thresholds were supplied. Success fractions are unavailable; counts, spread, native signatures and deterministic scorer replay remain descriptive.',
572
+ feasibility().limitation,
573
+ );
574
+ result.report.savedInputs = selected;
575
+ result.report.skippedSavedGroups = skipped;
576
+ result.report.offlineAnalysis = { freshAttempts: 0, modelCalls: 0, judgeCalls: 0 };
577
+ result.report.sourceLimitations.push(
578
+ 'Every selected record is saved historical evidence. A stored fresh-workspace receipt describes its original capture; this analysis dispatched no fresh trial.',
579
+ );
580
+ if (attempts.some((row) => row.provenance?.label === 'deterministic-test-double'))
581
+ result.report.sourceLimitations.push(
582
+ 'These saved records contain deterministic test doubles and establish no provider quality or savings.',
583
+ );
584
+ if (out) {
585
+ fs.mkdirSync(out, { recursive: true });
586
+ writeJson(path.join(out, 'saved-inputs.json'), { selected });
587
+ writeJson(path.join(out, 'declaration.json'), declaration);
588
+ reader.writeOutputs(result, out);
589
+ }
590
+ return result;
591
+ }
592
+ function feasibility() {
593
+ const independentCases = scoring.loadGroundTruth().fixtureSets.length;
594
+ return {
595
+ independentCases,
596
+ selectionFeasible: independentCases >= 2,
597
+ confirmationFeasible: false,
598
+ limitation: `The native corpus has ${independentCases} independent cases. Selection needs two clusters; no genuinely unseen cases remain for confirmation. Repetitions cannot add independent cases or produce a confirmed recommendation.`,
599
+ };
600
+ }
601
+ function liveBlockers(agent) {
602
+ return [
603
+ `${agent ?? 'native'}: applied reasoning effort is unavailable`,
604
+ `${agent ?? 'native'}: an enforceable verified all-in USD bound including tools, retries and overhead is unavailable`,
605
+ ...(agent === 'codex' ? ['codex: verified money telemetry is unavailable'] : []),
606
+ ];
607
+ }
608
+ function makeProposal({ declaration, agent = 'codex', maxUsd, timeoutMs = 1_200_000, testDouble = null }) {
609
+ declaration = structuredClone(declaration);
610
+ if (declaration.design && declaration.design.maxAttempts === undefined) {
611
+ const frozenValid = !declaration.frozen || declaration.frozen.digest === reader.declarationDigest(declaration);
612
+ declaration.design.maxAttempts = 1;
613
+ if (declaration.frozen && frozenValid) declaration.frozen = { digest: reader.declarationDigest(declaration) };
614
+ }
615
+ const validation = reader.validateDeclaration(declaration);
616
+ const ids = stageCases(declaration);
617
+ const attempts =
618
+ ids.length * (declaration.scope?.candidate ? 2 : 1) * (declaration.design?.repeats ?? 0) * (declaration.design?.maxAttempts ?? 1);
619
+ const blockers = validation.problems.map((entry) => `${entry.where}: ${entry.message}`);
620
+ if (!Number.isSafeInteger(declaration.design?.maxAttempts) || declaration.design.maxAttempts < 1)
621
+ blockers.push('maxAttempts must be a positive safe integer');
622
+ if (declaration.scope?.candidate) blockers.push(...validation.pairedProblems.map((entry) => `${entry.where}: ${entry.message}`));
623
+ if (declaration.stage === 'confirmation') blockers.push(feasibility().limitation);
624
+ if (declaration.scope?.workflow !== 'bmad-tea-test-design') blockers.push('Only native Test Design is supported');
625
+ if (!testDouble) blockers.push(...liveBlockers(agent));
626
+ const actualFixed = nativeIdentity();
627
+ if (testDouble) {
628
+ const overhead = declaration.resources?.overhead;
629
+ if (
630
+ !Array.isArray(overhead) ||
631
+ !overhead.every((entry) => entry.currency === 'USD' && Number.isFinite(entry.amount) && entry.amount >= 0) ||
632
+ !Number.isFinite(testDouble.overheadUsd ?? 0) ||
633
+ (testDouble.overheadUsd ?? 0) < 0 ||
634
+ usdGreater(usdSum(overhead.map((entry) => entry.amount)), testDouble.overheadUsd ?? 0) ||
635
+ usdGreater(testDouble.overheadUsd ?? 0, usdSum(overhead.map((entry) => entry.amount)))
636
+ )
637
+ blockers.push('All overhead requires a verified USD amount matching the reserved test-double bound');
638
+ if (
639
+ !Number.isFinite(testDouble.boundUsd) ||
640
+ testDouble.boundUsd < 0 ||
641
+ !Number.isFinite(testDouble.overheadUsd ?? 0) ||
642
+ (testDouble.overheadUsd ?? 0) < 0
643
+ )
644
+ blockers.push('Test double money bounds must be finite nonnegative amounts');
645
+ if (reader.canonicalJson(declaration.identity?.fixed) !== reader.canonicalJson(actualFixed))
646
+ blockers.push('Fixed input/scorer identities must match nativeIdentity() before approval');
647
+ if (declaration.scope?.candidate) {
648
+ const factor = declaration.scope.changedFactor;
649
+ const placeholder =
650
+ factor?.kind === 'model' ? '{model}' : factor?.kind === 'effort' && factor.setting === 'effort' ? '{effort}' : null;
651
+ if (!placeholder || !(testDouble.args ?? []).some((arg) => arg.includes(placeholder)))
652
+ blockers.push(
653
+ 'This double supports model or effort comparisons only when the changed value is passed through its frozen argv placeholder',
654
+ );
655
+ }
656
+ }
657
+ if (!Number.isFinite(maxUsd) || maxUsd < 0) blockers.push('A verified USD spending cap is required');
658
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) blockers.push('A positive elapsed bound is required');
659
+ return {
660
+ kind: 'evaluate-experiment-proposal',
661
+ declaration,
662
+ agent,
663
+ testDouble: testDouble
664
+ ? {
665
+ command: path.resolve(testDouble.command),
666
+ args: testDouble.args ?? [],
667
+ executableSha256: testDouble.executableSha256,
668
+ boundUsd: testDouble.boundUsd,
669
+ overheadUsd: testDouble.overheadUsd ?? 0,
670
+ }
671
+ : null,
672
+ maxUsd: maxUsd ?? null,
673
+ timeoutMs,
674
+ maxAttempts: attempts,
675
+ estimatedElapsedMs: attempts * (timeoutMs + 60_000),
676
+ elapsedEstimateIncludes: 'per-attempt target timeout plus one minute for native runner, staging and archive overhead',
677
+ money:
678
+ testDouble &&
679
+ Number.isSafeInteger(attempts) &&
680
+ attempts >= 0 &&
681
+ Number.isFinite(testDouble.boundUsd) &&
682
+ testDouble.boundUsd >= 0 &&
683
+ Number.isFinite(testDouble.overheadUsd ?? 0) &&
684
+ (testDouble.overheadUsd ?? 0) >= 0
685
+ ? {
686
+ label: 'deterministic-test-double',
687
+ upperBoundUsd: Number(usdSum([usdMultiply(testDouble.boundUsd, attempts), testDouble.overheadUsd ?? 0])),
688
+ upperBoundUsdExact: usdSum([usdMultiply(testDouble.boundUsd, attempts), testDouble.overheadUsd ?? 0]),
689
+ }
690
+ : null,
691
+ feasibility: feasibility(),
692
+ blockers,
693
+ requiresApproval: [
694
+ 'frozen declaration and thresholds',
695
+ 'execution scope and attempt/retry schedule',
696
+ 'elapsed estimate',
697
+ 'verified all-in money cap, confirmation and overhead',
698
+ ],
699
+ automaticAdoption: false,
700
+ };
701
+ }
702
+ function appendJournal(directory, event) {
703
+ const entries = readJournal(directory);
704
+ const payload = { sequence: entries.length + 1, previous: entries.at(-1)?.hash ?? null, at: now(), ...event };
705
+ const entry = { ...payload, hash: reader.digestOf(payload) };
706
+ const fd = fs.openSync(path.join(directory, 'journal.jsonl'), 'a');
707
+ try {
708
+ fs.writeFileSync(fd, `${JSON.stringify(entry)}\n`);
709
+ fs.fsyncSync(fd);
710
+ } finally {
711
+ fs.closeSync(fd);
712
+ }
713
+ return entry;
714
+ }
715
+ function readJournal(directory) {
716
+ const file = path.join(directory, 'journal.jsonl');
717
+ if (!fs.existsSync(file)) return [];
718
+ const entries = fs
719
+ .readFileSync(file, 'utf8')
720
+ .trim()
721
+ .split('\n')
722
+ .filter(Boolean)
723
+ .map((line) => JSON.parse(line));
724
+ let previous = null;
725
+ let approved = false;
726
+ let finished = false;
727
+ const kinds = new Set([
728
+ 'frozen',
729
+ 'approved',
730
+ 'reserved',
731
+ 'exposure',
732
+ 'dispatch',
733
+ 'completion',
734
+ 'sealed',
735
+ 'settled',
736
+ 'archive-failed',
737
+ 'stopped',
738
+ 'finished',
739
+ ]);
740
+ const states = new Map();
741
+ for (const [index, entry] of entries.entries()) {
742
+ const { hash, ...payload } = entry;
743
+ if (entry.previous !== previous || entry.sequence !== index + 1 || hash !== reader.digestOf(payload))
744
+ throw new Error('Controller journal integrity failure');
745
+ if (!kinds.has(entry.kind)) throw new Error('Unknown controller journal event');
746
+ if ((index === 0 && entry.kind !== 'frozen') || (index > 0 && entry.kind === 'frozen') || finished)
747
+ throw new Error('Controller journal must have one frozen first event and no events after finish');
748
+ if (entry.kind === 'approved') {
749
+ if (approved || index !== 1) throw new Error('Controller approval must occur once immediately after freeze');
750
+ approved = true;
751
+ }
752
+ if (['reserved', 'dispatch', 'completion', 'sealed', 'settled', 'archive-failed'].includes(entry.kind)) {
753
+ if (!approved || !entry.attemptId) throw new Error('Controller attempt requires prior approval');
754
+ const prior = states.get(entry.attemptId);
755
+ const expected = {
756
+ reserved: undefined,
757
+ dispatch: 'reserved',
758
+ completion: 'dispatch',
759
+ sealed: 'completion',
760
+ settled: 'sealed',
761
+ 'archive-failed': 'completion',
762
+ };
763
+ if (prior !== expected[entry.kind]) throw new Error(`Controller ${entry.kind} ordering failure for ${entry.attemptId}`);
764
+ states.set(entry.attemptId, entry.kind);
765
+ }
766
+ if (entry.kind === 'finished') {
767
+ if (!approved || [...states.values()].some((state) => state !== 'settled'))
768
+ throw new Error('Controller finished with unresolved attempts');
769
+ finished = true;
770
+ }
771
+ if (entry.kind === 'exposure' && (!approved || states.get(entry.entry?.attemptId) !== 'reserved'))
772
+ throw new Error('Exposure requires an approved reserved attempt');
773
+ if (['stopped', 'archive-failed'].includes(entry.kind)) finished = true;
774
+ previous = hash;
775
+ }
776
+ return entries;
777
+ }
778
+ function freezeProposal(proposal, directory) {
779
+ const repeats = proposal.declaration?.design?.repeats;
780
+ const maxAttempts = proposal.declaration?.design?.maxAttempts;
781
+ const development = proposal.declaration?.scope?.memberships?.development ?? [];
782
+ const validation = proposal.declaration?.scope?.memberships?.validation ?? [];
783
+ const confirmation = proposal.declaration?.scope?.memberships?.confirmation ?? [];
784
+ const arms = proposal.declaration?.scope?.candidate ? ['A', 'B'] : ['A'];
785
+ const cases =
786
+ Array.isArray(development) && Array.isArray(validation) && Array.isArray(confirmation) ? stageCases(proposal.declaration) : [];
787
+ const totalAttempts = cases.length * arms.length * repeats * maxAttempts;
788
+ const estimatedElapsedMs = totalAttempts * (proposal.timeoutMs + 60_000);
789
+ const nativeCases = new Set(scoring.loadGroundTruth().fixtureSets.map((set) => set.id));
790
+ if (
791
+ !Array.isArray(development) ||
792
+ !Array.isArray(validation) ||
793
+ !Array.isArray(confirmation) ||
794
+ !Number.isSafeInteger(repeats) ||
795
+ repeats < 1 ||
796
+ repeats > 1000 ||
797
+ !Number.isSafeInteger(maxAttempts) ||
798
+ maxAttempts < 1 ||
799
+ !Number.isSafeInteger(totalAttempts) ||
800
+ (totalAttempts < 1 && proposal.declaration.stage !== 'confirmation') ||
801
+ !Number.isSafeInteger(proposal.timeoutMs) ||
802
+ proposal.timeoutMs < 1 ||
803
+ !Number.isSafeInteger(estimatedElapsedMs) ||
804
+ !cases.every((caseId) => nativeCases.has(caseId)) ||
805
+ new Set(cases).size !== cases.length
806
+ )
807
+ throw new Error(
808
+ 'Proposal schedule requires native case arrays, repeats from 1 through 1000, positive safe-integer retries and safe finite attempt/elapsed bounds',
809
+ );
810
+ // Fixed interleaving is supported; randomized dispatch needs a declared and implemented RNG.
811
+ if (proposal.declaration.design.order === 'randomized')
812
+ throw new Error('Randomized live dispatch is unavailable; propose interleaved order');
813
+ const schedule = [];
814
+ for (let repetition = 1; repetition <= repeats; repetition++)
815
+ for (const caseId of cases) for (const arm of arms) schedule.push({ caseId, repetition, arm, blockId: `${caseId}-run-${repetition}` });
816
+ fs.mkdirSync(directory, { recursive: false });
817
+ const plan = {
818
+ ...proposal,
819
+ exposureScope: 'this-experiment-only; prior exposure unknown',
820
+ schedule,
821
+ createdAt: now(),
822
+ planId: crypto.randomUUID(),
823
+ };
824
+ writeJson(path.join(directory, 'plan.json'), plan);
825
+ writeJson(path.join(directory, 'declaration.json'), proposal.declaration);
826
+ writeJson(path.join(directory, 'experiment-exposure-initial.json'), []);
827
+ appendJournal(directory, {
828
+ kind: 'frozen',
829
+ planDigest: reader.digestOf(plan),
830
+ declarationDigest: reader.declarationDigest(plan.declaration),
831
+ });
832
+ return plan;
833
+ }
834
+ function approvePlan(directory, approval) {
835
+ const plan = readJson(path.join(directory, 'plan.json'));
836
+ if (plan.blockers.length > 0) throw new Error(`Live experiment blocked: ${plan.blockers.join('; ')}`);
837
+ if (plan.money && usdGreater(plan.money.upperBoundUsdExact, plan.maxUsd))
838
+ throw new Error('Verified all-in money bound exceeds approved cap');
839
+ if (
840
+ approval.planDigest !== reader.digestOf(plan) ||
841
+ approval.maxUsd !== plan.maxUsd ||
842
+ !approval.approvedBy ||
843
+ approval.approved !== true
844
+ )
845
+ throw new Error('Approval must bind the exact frozen plan, thresholds, scope, attempts, elapsed estimate and USD cap');
846
+ writeJson(path.join(directory, 'approval.json'), approval);
847
+ appendJournal(directory, { kind: 'approved', planDigest: approval.planDigest, approvalDigest: reader.digestOf(approval) });
848
+ }
849
+ function recoveryReport(directory) {
850
+ const journal = readJournal(directory);
851
+ validateFrozen(directory, { requireApproval: journal.some((entry) => ['reserved', 'dispatch'].includes(entry.kind)) });
852
+ const reservations = journal.filter((entry) => entry.kind === 'reserved');
853
+ const unsettled = reservations.filter(
854
+ (entry) => !journal.some((later) => later.kind === 'settled' && later.attemptId === entry.attemptId),
855
+ );
856
+ return {
857
+ status: unsettled.length > 0 ? 'recovery-required' : 'settled',
858
+ unresolvedReservations: unsettled,
859
+ settlements: journal.filter((entry) => entry.kind === 'settled'),
860
+ stopped: journal.findLast((entry) => ['stopped', 'archive-failed'].includes(entry.kind)) ?? null,
861
+ message:
862
+ unsettled.length > 0
863
+ ? 'Dispatched or reserved attempts remain unresolved. Preserve original workspaces and partial archives. Do not rerun or release their reservations without verified settlement.'
864
+ : 'Every recorded reservation is settled',
865
+ };
866
+ }
867
+ function validateFrozen(directory, { requireApproval = true } = {}) {
868
+ const plan = readJson(path.join(directory, 'plan.json'));
869
+ const entries = readJournal(directory);
870
+ if (
871
+ entries[0]?.kind !== 'frozen' ||
872
+ entries[0].planDigest !== reader.digestOf(plan) ||
873
+ entries[0].declarationDigest !== reader.declarationDigest(plan.declaration) ||
874
+ entries.some((entry) => entry.kind === 'dispatch' && entry.planDigest !== reader.digestOf(plan)) ||
875
+ reader.canonicalJson(readJson(path.join(directory, 'declaration.json'))) !== reader.canonicalJson(plan.declaration)
876
+ )
877
+ throw new Error('Frozen plan/declaration integrity failure');
878
+ if (!fs.existsSync(path.join(directory, 'approval.json'))) {
879
+ if (requireApproval) throw new Error('Explicit approval of the frozen plan is required before dispatch');
880
+ return plan;
881
+ }
882
+ const approval = readJson(path.join(directory, 'approval.json'));
883
+ if (
884
+ !entries.some(
885
+ (entry) =>
886
+ entry.kind === 'approved' && entry.approvalDigest === reader.digestOf(approval) && entry.planDigest === approval.planDigest,
887
+ ) ||
888
+ approval.planDigest !== reader.digestOf(plan) ||
889
+ approval.approved !== true ||
890
+ !approval.approvedBy ||
891
+ approval.maxUsd !== plan.maxUsd
892
+ )
893
+ throw new Error('Approval binding integrity failure');
894
+ return plan;
895
+ }
896
+ async function executePlan(directory, { testDouble = null, archive = null, afterDispatch = null } = {}) {
897
+ const plan = validateFrozen(directory);
898
+ if (plan.blockers.length > 0) throw new Error(`Live experiment blocked: ${plan.blockers.join('; ')}`);
899
+ if (!testDouble || plan.money?.label !== 'deterministic-test-double') throw new Error(liveBlockers(plan.agent).join('; '));
900
+ if (!Number.isFinite(testDouble.boundUsd) || testDouble.boundUsd < 0 || usdGreater(plan.money.upperBoundUsdExact, plan.maxUsd))
901
+ throw new Error('Verified all-in money bound exceeds approved cap');
902
+ if (recoveryReport(directory).status !== 'settled') throw new Error('Recovery required before any dispatch');
903
+ if (readJournal(directory).some((entry) => entry.kind === 'dispatch'))
904
+ throw new Error('A frozen plan executes once. Resume through recovery; attempts are never rerun');
905
+ const normalizedDouble = {
906
+ command: path.resolve(testDouble.command),
907
+ args: testDouble.args ?? [],
908
+ executableSha256: testDouble.executableSha256,
909
+ boundUsd: testDouble.boundUsd,
910
+ overheadUsd: testDouble.overheadUsd ?? 0,
911
+ };
912
+ if (reader.canonicalJson(normalizedDouble) !== reader.canonicalJson(plan.testDouble))
913
+ throw new Error('Test double execution settings differ from the approved plan');
914
+ const executable = path.resolve(testDouble.command);
915
+ if (sha(fs.readFileSync(executable)) !== testDouble.executableSha256) throw new Error('Test double executable identity mismatch');
916
+ const { createRegistry, probeRequest } = require('./registry');
917
+ const { gitAccessOf } = require('./workspace');
918
+ const { retainSkillArtifacts } = require('./retain-skill-artifacts');
919
+ const registry = createRegistry(
920
+ [
921
+ {
922
+ interfaceId: 'tea-test-design-runner',
923
+ executable: 'tea-test-design-runner',
924
+ target: 'cli/test-design-runner.js',
925
+ subcommandPaths: [[]],
926
+ artifacts: {},
927
+ environmentKeys: [],
928
+ maxElapsedMs: plan.timeoutMs + 60_000,
929
+ infrastructureExitCodes: [1, 3, 4, 5, 6],
930
+ },
931
+ ],
932
+ { root: PACKAGE_ROOT },
933
+ );
934
+ if (reader.canonicalJson(plan.declaration.identity.fixed) !== reader.canonicalJson(nativeIdentity()))
935
+ throw new Error('Native inputs or scorer changed after approval');
936
+ const groundTruth = scoring.loadGroundTruth();
937
+ const attemptRoot = path.join(directory, 'attempts');
938
+ fs.mkdirSync(attemptRoot);
939
+ let spent = usdText(testDouble.overheadUsd ?? 0);
940
+ let ledger = [];
941
+ for (const slot of plan.schedule) {
942
+ for (let attempt = 1; attempt <= plan.declaration.design.maxAttempts; attempt++) {
943
+ if (usdGreater(usdSum([spent, testDouble.boundUsd]), plan.maxUsd)) {
944
+ appendJournal(directory, { kind: 'stopped', reason: 'approved cap exhausted' });
945
+ return recoveryReport(directory);
946
+ }
947
+ const set = groundTruth.fixtureSets.find((set) => set.id === slot.caseId);
948
+ if (!set) throw new Error(`Unsupported native Test Design case ${slot.caseId}`);
949
+ const workspace = await staging.stageWorkspace(set);
950
+ const leaked = await staging.assertGroundTruthAbsent(workspace.dir);
951
+ if (leaked.length > 0) {
952
+ fs.rmSync(workspace.dir, { recursive: true, force: true });
953
+ throw new Error(leaked.join('; '));
954
+ }
955
+ const attemptId = `${slot.caseId}/${slot.repetition}/${slot.arm}/${attempt}`;
956
+ appendJournal(directory, { kind: 'reserved', attemptId, boundUsd: testDouble.boundUsd, originalWorkspace: workspace.dir });
957
+ if (plan.declaration.scope.candidate && !ledger.some((entry) => entry.caseId === slot.caseId)) {
958
+ ledger = reader.appendExposure(ledger, {
959
+ kind: 'selection-exposure',
960
+ candidateId: plan.declaration.scope.candidate.id,
961
+ caseId: slot.caseId,
962
+ stage: plan.declaration.stage,
963
+ attemptId,
964
+ at: now(),
965
+ });
966
+ appendJournal(directory, { kind: 'exposure', entry: ledger.at(-1) });
967
+ }
968
+ appendJournal(directory, {
969
+ kind: 'dispatch',
970
+ attemptId,
971
+ ...slot,
972
+ attempt,
973
+ planDigest: reader.digestOf(plan),
974
+ originalWorkspace: workspace.dir,
975
+ });
976
+ const prompt = staging.buildPrompt(set);
977
+ let observation;
978
+ try {
979
+ const { port } = await registry.createProbePort({
980
+ cwd: workspace.dir,
981
+ git: gitAccessOf(workspace),
982
+ privateRoot: registry.privateRoot,
983
+ });
984
+ observation = await port.probe(
985
+ probeRequest({
986
+ probeId: attemptId.toLowerCase().replaceAll('/', '-'),
987
+ interfaceId: 'tea-test-design-runner',
988
+ operationId: 'design-fixture-set',
989
+ option: {
990
+ agent: 'custom',
991
+ 'agent-cmd': executable,
992
+ 'agent-arg': (testDouble.args ?? []).map((arg) =>
993
+ arg
994
+ .replaceAll('{model}', plan.declaration.identity?.arms?.[slot.arm]?.model?.requested ?? '')
995
+ .replaceAll('{effort}', plan.declaration.identity?.arms?.[slot.arm]?.effort?.requested ?? ''),
996
+ ),
997
+ model: plan.declaration.identity?.arms?.[slot.arm]?.model?.requested ?? 'deterministic-test-double',
998
+ 'design-path': staging.designArtifactPaths(set).design,
999
+ 'timeout-ms': String(plan.timeoutMs),
1000
+ 'capture-path': 'source-capture.json',
1001
+ 'capture-test-double': true,
1002
+ },
1003
+ environment: {},
1004
+ stdin: { kind: 'text', value: prompt },
1005
+ }),
1006
+ new AbortController().signal,
1007
+ );
1008
+ } catch (error) {
1009
+ observation = { fault: { name: error.name, code: error.code ?? null, message: error.message } };
1010
+ }
1011
+ appendJournal(directory, { kind: 'completion', attemptId, exitCode: observation.exitCode ?? null, fault: observation.fault ?? null });
1012
+ if (afterDispatch) await afterDispatch({ directory, workspace, observation, attemptId });
1013
+ const capturedSource = runnerSource(workspace, observation);
1014
+ if (capturedSource.status !== 'verified') {
1015
+ let partialArchive = null;
1016
+ try {
1017
+ partialArchive = await (archive ?? retainSkillArtifacts)({
1018
+ preserveOnFailure: true,
1019
+ artifactsDir: attemptRoot,
1020
+ workspace,
1021
+ caseId: slot.caseId,
1022
+ agent: 'custom',
1023
+ model: null,
1024
+ repetition: slot.repetition,
1025
+ attempt,
1026
+ prompt,
1027
+ observation,
1028
+ provenance: { status: 'failed', sourceIntegrity: capturedSource.status },
1029
+ });
1030
+ writeJson(path.join(partialArchive, 'runner-source-result.json'), capturedSource);
1031
+ } catch (error) {
1032
+ partialArchive = partialArchive ?? error.partialArchive ?? null;
1033
+ }
1034
+ appendJournal(directory, {
1035
+ kind: 'archive-failed',
1036
+ attemptId,
1037
+ originalWorkspace: workspace.dir,
1038
+ partialArchive,
1039
+ message: `Source capture integrity ${capturedSource.status}: ${capturedSource.error ?? 'runner rejected modified capture'}`,
1040
+ });
1041
+ return recoveryReport(directory);
1042
+ }
1043
+ const source = capturedSource.source;
1044
+ const text =
1045
+ observation.artifacts?.design?.value ??
1046
+ (fs.existsSync(path.join(workspace.dir, staging.designArtifactPaths(set).design))
1047
+ ? fs.readFileSync(path.join(workspace.dir, staging.designArtifactPaths(set).design), 'utf8')
1048
+ : null);
1049
+ // The registry reads both deliverable and source receipt as artifacts; also retain exact workspace bytes on faults.
1050
+ observation.artifacts ??= {};
1051
+ observation.artifacts.design = text === null ? { kind: 'absent' } : { kind: 'text', value: text };
1052
+ const status =
1053
+ source?.status === 'completed' && observation.exitCode === 0
1054
+ ? 'completed'
1055
+ : source?.status === 'timeout'
1056
+ ? 'timeout'
1057
+ : source?.status === 'canceled'
1058
+ ? 'canceled'
1059
+ : 'failed';
1060
+ const replayed = text === null ? null : reader.testDesignReplay({ groundTruth })(text, { caseId: slot.caseId });
1061
+ const diagnostic = {
1062
+ caseId: slot.caseId,
1063
+ repetition: slot.repetition,
1064
+ completionState: status,
1065
+ signature: replayed?.signature ?? null,
1066
+ metricContributions: {},
1067
+ failureClass: status === 'completed' ? 'none' : (source.failureClass ?? 'environment-transport'),
1068
+ };
1069
+ const read = text === null ? null : scoring.readDesign({ kind: 'text', value: text });
1070
+ const mutations = await staging.corpusMutations(workspace);
1071
+ if (read?.ok) {
1072
+ const scored = scoring.scoreRun(set, read.design, new Set(groundTruth.riskCategories));
1073
+ diagnostic.metricContributions = scoring.numericContributions(scoring.testDesignDiagnosticProjection(scored, mutations));
1074
+ diagnostic.signature = scoring.signatureOf(scored, mutations);
1075
+ }
1076
+ diagnostic.metricContributions.fixtureMutations = mutations;
1077
+ diagnostic.metricContributions.maxFixtureMutations = scoring.THRESHOLDS.maxFixtureMutations;
1078
+ if (mutations > 0) diagnostic.failureClass = 'environment-configuration';
1079
+ diagnostic.qualification = status === 'completed' ? nativeQualification(diagnostic.metricContributions) : 'failed';
1080
+ if (diagnostic.qualification === 'failed' && diagnostic.failureClass === 'none') diagnostic.failureClass = 'quality';
1081
+ const observed = {
1082
+ fixed: nativeIdentity(workspace, set),
1083
+ arm: {
1084
+ model: { requested: source?.requested?.model ?? null, resolved: source?.observed?.resolvedModel ?? null },
1085
+ effort: {
1086
+ requested: plan.declaration.identity?.arms?.[slot.arm]?.effort?.requested ?? null,
1087
+ applied: source?.observed?.appliedEffort ?? null,
1088
+ },
1089
+ },
1090
+ };
1091
+ const provenance = {
1092
+ evaluationId: plan.planId,
1093
+ originalWorkspace: source?.workspace ?? workspace.dir,
1094
+ label: 'deterministic-test-double',
1095
+ stage: plan.declaration.stage,
1096
+ arm: slot.arm,
1097
+ status,
1098
+ blockId: slot.blockId,
1099
+ observed,
1100
+ receipt: source?.endedAt
1101
+ ? {
1102
+ kind: 'fresh-workspace',
1103
+ workspaceId: source.workspaceId,
1104
+ startedAt: source.startedAt,
1105
+ endedAt: source.endedAt,
1106
+ artifactSha256: source.artifactSha256 ?? null,
1107
+ }
1108
+ : null,
1109
+ usage: source?.usage ?? null,
1110
+ cost: source?.cost ?? null,
1111
+ latency: source?.endedAt ? { wallMs: Date.parse(source.endedAt) - Date.parse(source.startedAt) } : null,
1112
+ };
1113
+ let retained;
1114
+ try {
1115
+ retained = await (archive ?? retainSkillArtifacts)({
1116
+ preserveOnFailure: true,
1117
+ artifactsDir: attemptRoot,
1118
+ workspace,
1119
+ caseId: slot.caseId,
1120
+ agent: 'custom',
1121
+ model: source?.requested?.model ?? null,
1122
+ repetition: slot.repetition,
1123
+ attempt,
1124
+ prompt,
1125
+ observation,
1126
+ provenance,
1127
+ });
1128
+ writeJson(path.join(retained, 'diagnostic.json'), diagnostic);
1129
+ sealAttemptArchive(retained);
1130
+ appendJournal(directory, {
1131
+ kind: 'sealed',
1132
+ attemptId,
1133
+ directory: path.relative(directory, retained),
1134
+ manifestSha256: sha(fs.readFileSync(path.join(retained, 'manifest.json'))),
1135
+ });
1136
+ } catch (error) {
1137
+ appendJournal(directory, {
1138
+ kind: 'archive-failed',
1139
+ attemptId,
1140
+ originalWorkspace: workspace.dir,
1141
+ partialArchive: retained ?? error.partialArchive ?? null,
1142
+ message: error.message,
1143
+ });
1144
+ return recoveryReport(directory);
1145
+ }
1146
+ const cost = source?.cost;
1147
+ const charge =
1148
+ cost?.currency === 'USD' &&
1149
+ Object.keys(cost.components ?? {}).includes('target') &&
1150
+ Object.keys(cost.components ?? {}).includes('tools') &&
1151
+ Object.values(cost.components).every((value) => Number.isFinite(value) && value >= 0)
1152
+ ? usdSum(Object.values(cost.components))
1153
+ : null;
1154
+ if (
1155
+ charge === null ||
1156
+ usdGreater(charge, testDouble.boundUsd) ||
1157
+ !source?.observed?.resolvedModel ||
1158
+ !source?.observed?.appliedEffort
1159
+ ) {
1160
+ appendJournal(directory, {
1161
+ kind: 'stopped',
1162
+ attemptId,
1163
+ reason: 'required source identity/cost missing or reserved bound violated',
1164
+ });
1165
+ return recoveryReport(directory);
1166
+ }
1167
+ spent = usdSum([spent, charge]);
1168
+ appendJournal(directory, {
1169
+ kind: 'settled',
1170
+ attemptId,
1171
+ chargeUsd: Number(charge),
1172
+ totalUsd: Number(spent),
1173
+ chargeUsdExact: charge,
1174
+ totalUsdExact: spent,
1175
+ });
1176
+ fs.rmSync(workspace.dir, { recursive: true, force: true });
1177
+ if (status === 'completed') break;
1178
+ }
1179
+ }
1180
+ writeJson(path.join(directory, 'exposure.json'), ledger);
1181
+ appendJournal(directory, { kind: 'finished', totalUsd: Number(spent), totalUsdExact: spent });
1182
+ return analyzeExperiment(directory, path.join(directory, 'analysis'));
1183
+ }
1184
+ function analyzeExperiment(directory, out) {
1185
+ const journal = readJournal(directory);
1186
+ const captured = journal.some((entry) => ['dispatch', 'completion', 'sealed'].includes(entry.kind));
1187
+ const plan = validateFrozen(directory, { requireApproval: captured });
1188
+ const attempts = [];
1189
+ const limitations = [];
1190
+ const groundTruth = scoring.loadGroundTruth();
1191
+ const nativeRecords = {};
1192
+ for (const entry of journal.filter((entry) => entry.kind === 'sealed')) {
1193
+ const archive = confinedPath(directory, entry.directory);
1194
+ if (sha(fs.readFileSync(path.join(archive, 'manifest.json'))) !== entry.manifestSha256)
1195
+ throw new Error('Sealed manifest differs from controller journal');
1196
+ const loaded = reader.loadSealedTestDesignAttempt({ dir: archive, runId: plan.planId });
1197
+ if (Object.values(loaded.records).some((record) => record.manifestSha256 !== entry.manifestSha256))
1198
+ throw new Error('Sealed manifest differs from controller journal');
1199
+ attempts.push(...loaded.rows);
1200
+ Object.assign(nativeRecords, loaded.records);
1201
+ }
1202
+ for (const entry of journal.filter(
1203
+ (entry) => entry.kind === 'dispatch' && !journal.some((later) => later.kind === 'sealed' && later.attemptId === entry.attemptId),
1204
+ )) {
1205
+ attempts.push({
1206
+ attemptId: `${plan.planId}/${entry.attemptId}`,
1207
+ runId: plan.planId,
1208
+ stage: plan.declaration.stage,
1209
+ arm: entry.arm,
1210
+ caseId: entry.caseId,
1211
+ clusterId: entry.caseId,
1212
+ repetition: entry.repetition,
1213
+ attempt: entry.attempt,
1214
+ blockId: entry.blockId,
1215
+ status: 'failed',
1216
+ outputText: null,
1217
+ outputSha256: null,
1218
+ receipt: null,
1219
+ observed: null,
1220
+ cost: null,
1221
+ usage: null,
1222
+ latency: null,
1223
+ metrics: {},
1224
+ counts: {},
1225
+ error: 'Dispatched attempt has no verified sealed source evidence; its reservation remains unresolved',
1226
+ });
1227
+ }
1228
+ const ledger = journal.filter((entry) => entry.kind === 'exposure').map((entry) => entry.entry);
1229
+ if (
1230
+ fs.existsSync(path.join(directory, 'exposure.json')) &&
1231
+ reader.canonicalJson(readJson(path.join(directory, 'exposure.json'))) !== reader.canonicalJson(ledger)
1232
+ )
1233
+ throw new Error('Exposure history differs from durable controller journal');
1234
+ const result = reader.readExperiment({
1235
+ declaration: plan.declaration,
1236
+ attempts,
1237
+ nativeRecords,
1238
+ replay: reader.testDesignReplay({ groundTruth }),
1239
+ ledger,
1240
+ });
1241
+ result.report.savedInputs = [directory];
1242
+ result.report.offlineAnalysis = { freshAttempts: 0, modelCalls: 0, judgeCalls: 0 };
1243
+ result.report.controller = {
1244
+ status: captured ? 'captured-experiment' : 'proposal-without-evidence',
1245
+ proposalBlockers: plan.blockers,
1246
+ label: plan.money?.label ?? 'native',
1247
+ journalEntries: journal.length,
1248
+ recovery: recoveryReport(directory),
1249
+ attempts: attempts.length,
1250
+ automaticAdoption: false,
1251
+ };
1252
+ if (!captured)
1253
+ result.report.sourceLimitations.push(
1254
+ 'This frozen proposal has no captured attempts. Offline inspection needs no execution or spending approval and dispatches nothing.',
1255
+ );
1256
+ result.report.sourceLimitations.push(
1257
+ ...limitations,
1258
+ feasibility().limitation,
1259
+ 'Deterministic doubles verify orchestration and accounting only. They establish no provider quality or savings.',
1260
+ 'The local unsigned journal checks retained sequence and bindings. It cannot prove tail completeness, resist wholesale rechaining, or establish independent wall-clock ordering.',
1261
+ 'Exposure history covers this experiment only. Prior candidate exposure outside these records is unknown; native confirmation remains unavailable.',
1262
+ );
1263
+ if (out) reader.writeOutputs(result, out);
1264
+ return result;
1265
+ }
1266
+ async function experimentCommand(options) {
1267
+ if (['saved', 'searchRoot', 'propose', 'recover'].filter((key) => options[key] !== undefined).length > 1)
1268
+ throw Object.assign(new Error('Choose exactly one experiment operation: --saved, --search-root, --propose or --recover'), {
1269
+ code: 'EXPERIMENT_USAGE',
1270
+ });
1271
+ let result;
1272
+ if (options.saved || options.searchRoot)
1273
+ result = analyzeSaved({ saved: options.saved ? [options.saved] : [], searchRoot: options.searchRoot, out: options.out });
1274
+ else if (options.recover) result = recoveryReport(path.resolve(options.recover));
1275
+ else if (options.propose) {
1276
+ const declaration = readJson(options.propose);
1277
+ result = makeProposal({ declaration, agent: options.agent, maxUsd: options.maxUsd === undefined ? undefined : Number(options.maxUsd) });
1278
+ if (options.out) freezeProposal(result, path.resolve(options.out));
1279
+ } else throw new Error('Use --saved <path>, --search-root <path>, --propose <declaration.json>, or --recover <experiment directory>');
1280
+ process.stdout.write(`${JSON.stringify(result.report ?? result, null, 2)}\n`);
1281
+ return 0;
1282
+ }
1283
+ module.exports = {
1284
+ nativeIdentity,
1285
+ runnerSource,
1286
+ discoverSaved,
1287
+ analyzeSaved,
1288
+ feasibility,
1289
+ liveBlockers,
1290
+ makeProposal,
1291
+ freezeProposal,
1292
+ approvePlan,
1293
+ executePlan,
1294
+ analyzeExperiment,
1295
+ readJournal,
1296
+ recoveryReport,
1297
+ experimentCommand,
1298
+ sealAttemptArchive,
1299
+ };