bmad-method-test-architecture-enterprise 2.0.4-next.0 → 2.0.4-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,7 +31,7 @@
31
31
  "name": "bmad-method-test-architecture-enterprise",
32
32
  "source": "./",
33
33
  "description": "Master Test Architect module for quality strategy, test generation in red and expand modes with automatic run-and-heal, combined framework and CI setup, CI/CD quality gates, and structured testing education. The CI and ATDD commands remain available with existing customizations. Part of the BMad Method ecosystem.",
34
- "version": "2.0.4-next.0",
34
+ "version": "2.0.4-next.1",
35
35
  "author": {
36
36
  "name": "Murat K Ozcan (TEA Creator) & Brian (BMad) Madison"
37
37
  },
@@ -230,6 +230,7 @@ function coercePactMcp(value, source = CONFIG_RELATIVE_PATH) {
230
230
  throw configError(`tea_pact_mcp from ${source} must be one of ${PACT_MCP_VALUES.join(' | ')}, got ${JSON.stringify(value)}`);
231
231
  }
232
232
 
233
+ /** Accept configuration tables while excluding arrays, dates and null. */
233
234
  function isPlainObject(value) {
234
235
  return value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date);
235
236
  }
@@ -270,20 +270,53 @@ function readRisks(tables) {
270
270
  // one had the second half vanish with nothing reported. They are counted so the
271
271
  // caller can fail on them.
272
272
  const unscored = [];
273
+ const residualReferences = new Map();
273
274
  for (const table of tables) {
274
275
  if (table.reference) continue;
275
276
  const idColumn = columnIndex(table.header, ['Risk ID', 'RiskID', 'ID']);
276
277
  const scoreColumn = columnIndex(table.header, ['Score', 'Risk Score']);
278
+ const hasRiskFields = ['Category', 'Risk Category', 'Probability', 'Impact'].some((name) => columnIndex(table.header, [name]) !== -1);
279
+ const statesRiskRegister = /\brisk(?:s| register| assessment)\b/i.test(table.headings.at(-1) ?? '');
277
280
  if (
278
281
  idColumn !== -1 &&
279
282
  scoreColumn === -1 &&
280
- table.rows.some((row) => RISK_ID_PATTERN.test(String(row[idColumn] ?? '').toUpperCase()))
283
+ table.rows.some((row) => {
284
+ const id = String(row[idColumn] ?? '').trim();
285
+ return (
286
+ RISK_ID_PATTERN.test(id.toUpperCase()) ||
287
+ ((hasRiskFields || statesRiskRegister) && id !== '' && !/^\{.*\}$/.test(id) && !/^none$/i.test(id))
288
+ );
289
+ })
281
290
  ) {
282
- unscored.push({ headings: table.headings, rows: table.rows.length });
291
+ const entry = { headings: table.headings, rows: table.rows.length };
292
+ unscored.push(entry);
293
+ const normalized = table.header.map((header) =>
294
+ String(header)
295
+ .toLowerCase()
296
+ .replaceAll(/[^a-z]/g, ''),
297
+ );
298
+ if (
299
+ String(table.headings.at(-1) ?? '')
300
+ .toLowerCase()
301
+ .replaceAll(/[^a-z]/g, '')
302
+ .startsWith('residualrisk') &&
303
+ normalized.some((header) => header.startsWith('residualrisk')) &&
304
+ normalized.includes('acceptancecondition') &&
305
+ !normalized.some((header) => ['category', 'riskcategory', 'probability', 'impact'].includes(header))
306
+ ) {
307
+ residualReferences.set(
308
+ entry,
309
+ table.rows.map((row) =>
310
+ String(row[idColumn] ?? '')
311
+ .trim()
312
+ .toUpperCase(),
313
+ ),
314
+ );
315
+ }
283
316
  }
284
317
  if (idColumn === -1 || scoreColumn === -1) continue;
285
318
  const categoryColumn = columnIndex(table.header, ['Category', 'Risk Category']);
286
- const descriptionColumn = columnIndex(table.header, ['Description', 'Risk', 'Summary']);
319
+ const descriptionColumn = columnIndex(table.header, ['Description', 'Description and source evidence', 'Risk', 'Summary']);
287
320
  const probabilityColumn = columnIndex(table.header, ['Probability']);
288
321
  const impactColumn = columnIndex(table.header, ['Impact']);
289
322
  for (const row of table.rows) {
@@ -291,6 +324,16 @@ function readRisks(tables) {
291
324
  // A template row still carrying its own placeholder describes nothing, and
292
325
  // counting it would make an unfilled template score as a register.
293
326
  if (rawId === '' || /^\{.*\}$/.test(rawId)) continue;
327
+ // An explicitly empty band can carry a human-readable absence marker.
328
+ // Require all numeric cells empty so a scored or incomplete real risk
329
+ // cannot disappear through this exception.
330
+ if (
331
+ /^none$/i.test(rawId) &&
332
+ /^n\/a$/i.test(row[categoryColumn] ?? '') &&
333
+ /^no evidence-supported risk scored in this band\.?$/i.test(row[descriptionColumn] ?? '') &&
334
+ [probabilityColumn, impactColumn, scoreColumn].every((column) => column !== -1 && (row[column] ?? '').trim() === '')
335
+ )
336
+ continue;
294
337
  const id = rawId.toUpperCase();
295
338
  risks.push({
296
339
  id,
@@ -304,7 +347,14 @@ function readRisks(tables) {
304
347
  });
305
348
  }
306
349
  }
307
- risks.unscoredTables = unscored;
350
+ const knownIds = new Set(risks.map((risk) => risk.id));
351
+ // A residual acceptance table may only reference IDs in the scored register.
352
+ // Its exact heading and columns distinguish it from an additional register;
353
+ // an unknown ID still reports an unscored table to every caller.
354
+ risks.unscoredTables = unscored.filter((entry) => {
355
+ const references = residualReferences.get(entry);
356
+ return !references || references.length === 0 || references.some((id) => !knownIds.has(id));
357
+ });
308
358
  return risks;
309
359
  }
310
360
 
@@ -332,7 +382,9 @@ function readCoverage(tables) {
332
382
  if (level === '' || /^\{.*\}$/.test(level)) continue;
333
383
  const linkCell = linkColumn === -1 ? '' : (row[linkColumn] ?? '');
334
384
  rows.push({
335
- level,
385
+ // These observed qualifiers retain the E2E test level. Unknown values
386
+ // stay unchanged so validators and coverage oracles still reject them.
387
+ level: ['E2E performance', 'E2E operational'].includes(level) ? 'E2E' : level,
336
388
  priority,
337
389
  riskIds: [...String(linkCell).toUpperCase().matchAll(RISK_REFERENCE_PATTERN)].map((match) => match[0]),
338
390
  linkCell,
@@ -8,6 +8,7 @@ const { runAgent } = require('./run-agent');
8
8
  const { assertAgentReady } = require('./agent-presence');
9
9
  const { classOfAgentError } = require('./runner-exit-codes');
10
10
 
11
+ /** A classified workflow failure used by caller exit-code policies. */
11
12
  class WorkflowError extends Error {
12
13
  constructor(failureClass, message, options) {
13
14
  super(message, options);
@@ -15,12 +16,14 @@ class WorkflowError extends Error {
15
16
  }
16
17
  }
17
18
  const collect = (value, previous) => [...previous, value];
19
+ /** Parse a bounded whole decimal without accepting partial or unsafe values. */
18
20
  function integer(value, name, { min = 1, max = Number.MAX_SAFE_INTEGER } = {}) {
19
21
  if (!/^\d+$/.test(String(value)) || !Number.isSafeInteger(Number(value)) || Number(value) < min || Number(value) > max) {
20
22
  throw new WorkflowError('usage', `${name} must be an integer from ${min} to ${max}`);
21
23
  }
22
24
  return Number(value);
23
25
  }
26
+ /** Add the shared vendor, model, environment and execution controls. */
24
27
  function addAgentOptions(program) {
25
28
  return program
26
29
  .requiredOption(
@@ -34,6 +37,7 @@ function addAgentOptions(program) {
34
37
  .option('--timeout-ms <n>', 'wall-clock timeout per attempt in milliseconds', '1200000')
35
38
  .option('--retries <n>', 'additional attempts for transport failures or timeouts, from 0 to 3', '1');
36
39
  }
40
+ /** Validate command-line controls and resolve the requested adapter invocation. */
37
41
  function agentOptions(options, projectRoot) {
38
42
  const timeout = integer(options.timeoutMs ?? '1200000', '--timeout-ms', { max: 2_147_483_647 });
39
43
  const retries = integer(options.retries ?? '1', '--retries', { min: 0, max: 3 });
@@ -63,6 +67,7 @@ function agentOptions(options, projectRoot) {
63
67
  cwd: projectRoot,
64
68
  };
65
69
  }
70
+ /** Determine whether a resolved path is contained by the supplied root. */
66
71
  function inside(root, candidate) {
67
72
  const relative = path.relative(root, candidate);
68
73
  return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
@@ -85,6 +90,7 @@ function projectPath(projectRoot, value, label = 'path') {
85
90
  if (!inside(root, resolved)) throw new WorkflowError('usage', `${label} resolves outside --project-root`);
86
91
  return resolved;
87
92
  }
93
+ /** Resolve a readable regular input file confined to the consuming project. */
88
94
  function readInput(projectRoot, value, label = 'input') {
89
95
  const target = projectPath(projectRoot, value, label);
90
96
  let stat;
@@ -108,6 +114,7 @@ function resolveWorkflowSkill({ projectRoot, skillName, skillRoot }) {
108
114
  throw new WorkflowError('environment-configuration', `${skillName} is unavailable at ${root}: ${error.message}`, { cause: error });
109
115
  }
110
116
  }
117
+ /** Activate the actual skill using authoritative config and caller run inputs. */
111
118
  function headlessPrompt({ skillRoot, projectRoot, resolvedConfig, operation = 'create', requestLines = [] }) {
112
119
  return [
113
120
  `Run the TEA skill at ${JSON.stringify(skillRoot)} for the project at ${JSON.stringify(projectRoot)}.`,
@@ -125,6 +132,7 @@ function headlessPrompt({ skillRoot, projectRoot, resolvedConfig, operation = 'c
125
132
  ...requestLines,
126
133
  ].join('\n');
127
134
  }
135
+ /** Create a fresh readable JSON evidence record without overwriting a prior one. */
128
136
  function jsonFile(target, value) {
129
137
  fs.writeFileSync(target, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx' });
130
138
  }
@@ -232,6 +240,7 @@ function runWithEvidence({
232
240
  lastError.runDir = runDir;
233
241
  throw lastError;
234
242
  }
243
+ /** Print a retained-evidence diagnostic and map failures to public CLI codes. */
235
244
  function mainError(name, error) {
236
245
  process.stderr.write(`${name}: ${error.message}${error.runDir ? `; evidence: ${error.runDir}` : ''}\n`);
237
246
  return ['environment-transport', 'environment-timeout', 'environment-parser'].includes(error.failureClass) ? 3 : 2;
@@ -0,0 +1,410 @@
1
+ #!/usr/bin/env node
2
+ /** Generate a project test plan through the packaged test-design skill. */
3
+ 'use strict';
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const crypto = require('node:crypto');
7
+ const yaml = require('js-yaml');
8
+ const MarkdownIt = require('markdown-it');
9
+ const { Command } = require('commander');
10
+ const { resolveTeaConfig } = require('./lib/resolve-tea-config');
11
+ const { readDesign, RISK_ID_PATTERN, bandFor } = require('./lib/test-design-parser');
12
+ const {
13
+ WorkflowError,
14
+ addAgentOptions,
15
+ readInput,
16
+ projectPath,
17
+ resolveWorkflowSkill,
18
+ headlessPrompt,
19
+ runWithEvidence,
20
+ mainError,
21
+ } = require('./lib/workflow-cli');
22
+ const collect = (value, previous) => [...previous, value];
23
+ const sectionMarkdown = new MarkdownIt();
24
+ const digest = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
25
+ /** Resolve existing parents so publication checks include symlink aliases. */
26
+ function canonicalDestination(file) {
27
+ if (fs.existsSync(file)) return fs.realpathSync(file);
28
+ return path.join(canonicalDestination(path.dirname(file)), path.basename(file));
29
+ }
30
+ /** Refuse publication onto a supplied input or another deliverable. */
31
+ function checkPublication(destinations, inputs) {
32
+ const canonical = destinations.map(canonicalDestination);
33
+ const aliases = (a, b) => {
34
+ if (a === b) return true;
35
+ if (!fs.existsSync(a) || !fs.existsSync(b)) return false;
36
+ const left = fs.statSync(a);
37
+ const right = fs.statSync(b);
38
+ return left.dev === right.dev && left.ino === right.ino;
39
+ };
40
+ if (canonical.some((file) => inputs.some((input) => aliases(file, input))))
41
+ throw new WorkflowError('usage', 'a published artifact aliases an input document; select a separate --output-dir');
42
+ if (canonical.some((file, index) => canonical.slice(0, index).some((other) => aliases(file, other))))
43
+ throw new WorkflowError('usage', 'published artifacts alias each other; select a separate --output-dir');
44
+ }
45
+ /** Require populated workflow sections and reject unresolved template scaffolding. */
46
+ function validateSections(text, sections, label) {
47
+ const lines = text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
48
+ const offsets = [0];
49
+ for (const line of lines) offsets.push(offsets.at(-1) + line.length);
50
+ const headings = [];
51
+ let quoted = 0;
52
+ let listed = 0;
53
+ for (const token of sectionMarkdown.parse(text, {})) {
54
+ if (token.type === 'blockquote_open') quoted += 1;
55
+ if (token.type === 'blockquote_close') quoted -= 1;
56
+ if (token.type === 'list_item_open') listed += 1;
57
+ if (token.type === 'list_item_close') listed -= 1;
58
+ if (token.type !== 'heading_open' || quoted || listed || !/^#{2,3}$/.test(token.markup)) continue;
59
+ const level = Number.parseInt(token.tag.slice(1), 10);
60
+ const [start, end] = token.map;
61
+ const source = lines[start].replace(/\r?\n$/, '');
62
+ const match = /^#{2,3}[ \t]+(.+?)[ \t]*#*[ \t]*$/.exec(source);
63
+ if (match) headings.push({ level, index: offsets[start], end: offsets[end], title: match[1] });
64
+ }
65
+ const equivalentHeading = (value) => value.replaceAll(/\band\b/gi, '&');
66
+ const normalizeHeading = (value) =>
67
+ equivalentHeading(value)
68
+ .replace(/^[\s\p{Extended_Pictographic}\p{Emoji_Presentation}\p{Symbol}\d.)\-:]+/u, '')
69
+ .replaceAll(/\s+/g, ' ')
70
+ .trim()
71
+ .toLowerCase();
72
+ for (const section of sections) {
73
+ const normalizedSection = normalizeHeading(section);
74
+ const escaped = equivalentHeading(section).replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
75
+ const heading =
76
+ headings.find((entry) => normalizeHeading(entry.title) === normalizedSection) ??
77
+ headings.find((entry) => new RegExp(`(?:^|\\b)${escaped}(?:\\b|$)`, 'i').test(equivalentHeading(entry.title)));
78
+ const stop = heading && headings.find((entry) => entry.index > heading.index && entry.level <= heading.level);
79
+ const body = heading && text.slice(heading.end, stop?.index ?? text.length);
80
+ const content = body
81
+ ?.replaceAll(/<!--[^]*?-->/g, '')
82
+ .replaceAll(/^#{1,6}[^\n]*$|^[-*_]{3,}\s*$/gm, '')
83
+ .trim();
84
+ if (!content) throw new WorkflowError('environment-parser', `${label} is missing populated section: ${section}`);
85
+ }
86
+ if (/<!--\s*TEA will populate|\{(?:Feature Name|project_name|timestamp|test_design_path)\}/i.test(text))
87
+ throw new WorkflowError('environment-parser', `${label} contains unresolved template placeholders`);
88
+ }
89
+ /** Read a fresh regular artifact confined to the current attempt. */
90
+ function readArtifact(attemptDir, file) {
91
+ const target = projectPath(attemptDir, file, 'generated artifact');
92
+ if (!fs.existsSync(target) || !fs.statSync(target).isFile())
93
+ throw new WorkflowError('environment-parser', `required artifact was not written: ${file}`);
94
+ const text = fs.readFileSync(target, 'utf8');
95
+ if (!text.trim()) throw new WorkflowError('environment-parser', `required artifact is empty: ${file}`);
96
+ return { path: target, text };
97
+ }
98
+ /** Validate completion, preserved inputs, risk arithmetic and coverage references. */
99
+ function validateDesign(context) {
100
+ const { attemptDir, artifactFiles, checkpointFile, runKey, inputDigests, planFile } = context;
101
+ for (const [file, expected] of inputDigests)
102
+ if (digest(file) !== expected) throw new WorkflowError('environment-parser', `the agent changed an input: ${file}`);
103
+ const artifacts = artifactFiles.map((file) => readArtifact(attemptDir, file));
104
+ if (context.runScope === 'system') {
105
+ const architecture = artifacts.find((artifact) => path.basename(artifact.path) === 'test-design-architecture.md');
106
+ const handoff = artifacts.find((artifact) => artifact.path.endsWith('-handoff.md'));
107
+ validateSections(
108
+ architecture.text,
109
+ [
110
+ 'Executive Summary',
111
+ 'Risk Assessment',
112
+ 'NFR Testability Requirements',
113
+ 'Testability Concerns and Architectural Gaps',
114
+ 'Risk Mitigation Plans',
115
+ 'Assumptions and Dependencies',
116
+ ],
117
+ 'architecture report',
118
+ );
119
+ validateSections(
120
+ handoff.text,
121
+ [
122
+ 'Purpose',
123
+ 'TEA Artifacts Inventory',
124
+ 'Epic-Level Integration Guidance',
125
+ 'Story-Level Integration Guidance',
126
+ 'Risk-to-Story Mapping',
127
+ 'Recommended BMAD',
128
+ 'Phase Transition Quality Gates',
129
+ ],
130
+ 'handoff report',
131
+ );
132
+ }
133
+ const checkpoint = readArtifact(attemptDir, checkpointFile);
134
+ const match = checkpoint.text.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
135
+ if (!match) throw new WorkflowError('environment-parser', 'the progress checkpoint has no YAML frontmatter');
136
+ const progress = yaml.load(match[1]);
137
+ if (
138
+ progress?.workflowStatus !== 'completed' ||
139
+ progress.runKey !== runKey ||
140
+ progress.runScope !== context.runScope ||
141
+ !Array.isArray(progress.stepsCompleted) ||
142
+ [
143
+ 'step-01-detect-mode',
144
+ 'step-02-load-context',
145
+ 'step-03-risk-and-testability',
146
+ 'step-04-coverage-plan',
147
+ 'step-05-generate-output',
148
+ ].some((step) => !progress.stepsCompleted.includes(step)) ||
149
+ progress.lastStep !== 'step-05-generate-output' ||
150
+ progress.nextStep !== '' ||
151
+ progress.totalSteps !== 5
152
+ ) {
153
+ throw new WorkflowError('environment-parser', 'the progress checkpoint does not confirm completion of this run');
154
+ }
155
+ const report = artifacts.find((artifact) => artifact.path === path.join(attemptDir, planFile));
156
+ const planSections =
157
+ context.runScope === 'system'
158
+ ? [
159
+ 'Executive Summary',
160
+ 'Not in Scope',
161
+ 'Dependencies & Test Blockers',
162
+ 'Risk Assessment',
163
+ 'NFR Test Coverage Plan',
164
+ 'Entry Criteria',
165
+ 'Exit Criteria',
166
+ 'Test Coverage Plan',
167
+ 'Execution Strategy',
168
+ 'QA Effort Estimate',
169
+ 'Interworking & Regression',
170
+ 'Appendix A: Code Examples & Tagging',
171
+ 'Appendix B: Knowledge Base References',
172
+ ]
173
+ : [
174
+ 'Executive Summary',
175
+ 'Not in Scope',
176
+ 'Risk Assessment',
177
+ 'NFR Planning',
178
+ 'Entry Criteria',
179
+ 'Exit Criteria',
180
+ 'Test Coverage Plan',
181
+ 'Execution Strategy',
182
+ 'Resource Estimates',
183
+ 'Quality Gate Criteria',
184
+ 'Mitigation Plans',
185
+ 'Assumptions and Dependencies',
186
+ 'Follow-on Workflows',
187
+ 'Approval',
188
+ 'Interworking & Regression',
189
+ 'Appendix',
190
+ ];
191
+ validateSections(report.text, planSections, context.runScope === 'system' ? 'QA plan' : 'epic plan');
192
+ const parsed = readDesign({ kind: 'text', value: report.text });
193
+ if (!parsed.ok) throw new WorkflowError('environment-parser', parsed.reason);
194
+ const { risks, coverage } = parsed.design;
195
+ if (parsed.design.unscoredTables.length > 0) throw new WorkflowError('environment-parser', 'the report contains unscored risk tables');
196
+ const ids = new Set();
197
+ for (const risk of risks) {
198
+ if (
199
+ !risk.description.trim() ||
200
+ !RISK_ID_PATTERN.test(risk.rawId) ||
201
+ ids.has(risk.id) ||
202
+ !['TECH', 'SEC', 'PERF', 'DATA', 'BUS', 'OPS'].includes(risk.category) ||
203
+ ![1, 2, 3].includes(risk.probability) ||
204
+ ![1, 2, 3].includes(risk.impact) ||
205
+ risk.score !== risk.probability * risk.impact
206
+ ) {
207
+ throw new WorkflowError('environment-parser', `invalid or duplicate scored risk row: ${risk.rawId}`);
208
+ }
209
+ const declared = bandFor(risk.headings);
210
+ if (!declared || risk.score < declared.band.min || risk.score > declared.band.max)
211
+ throw new WorkflowError('environment-parser', `scored risk ${risk.rawId} has a missing or contradictory score band`);
212
+ ids.add(risk.id);
213
+ }
214
+ if (
215
+ coverage.length === 0 ||
216
+ coverage.some(
217
+ (row) =>
218
+ !['P0', 'P1', 'P2', 'P3'].includes(row.priority) ||
219
+ !['E2E', 'API', 'Component', 'Integration', 'Unit'].includes(row.level) ||
220
+ row.riskIds.some((id) => !ids.has(id)),
221
+ ) ||
222
+ risks.some((risk) => !coverage.some((row) => row.riskIds.includes(risk.id)))
223
+ )
224
+ throw new WorkflowError(
225
+ 'environment-parser',
226
+ 'the coverage plan is empty, omits valid priorities, uses invalid levels, carries dangling references or leaves risks uncovered',
227
+ );
228
+ return { artifacts: [...artifacts, checkpoint], riskCount: risks.length, coverageCount: coverage.length };
229
+ }
230
+ /** Stage all deliverables and restore prior reports if a replacement fails. */
231
+ function publishDesign(artifacts, { projectRoot, outputDir, runDir }, io = fs) {
232
+ const staged = [];
233
+ let keepBackups = false;
234
+ try {
235
+ for (const artifact of artifacts) {
236
+ const destination = projectPath(projectRoot, path.join(outputDir, 'test-design', path.basename(artifact.path)), 'published artifact');
237
+ const target = canonicalDestination(destination);
238
+ io.mkdirSync(path.dirname(target), { recursive: true });
239
+ const directory = io.mkdtempSync(path.join(path.dirname(target), '.tea-publish-'));
240
+ const item = {
241
+ destination,
242
+ target,
243
+ directory,
244
+ next: path.join(directory, 'next'),
245
+ previous: path.join(directory, 'previous'),
246
+ existed: io.existsSync(target),
247
+ installed: false,
248
+ };
249
+ staged.push(item);
250
+ if (item.existed) {
251
+ if (!io.statSync(target).isFile()) throw new Error(`publication destination is not a regular file: ${destination}`);
252
+ io.copyFileSync(target, item.previous);
253
+ }
254
+ io.copyFileSync(artifact.path, item.next);
255
+ if (item.existed) io.chmodSync(item.next, io.statSync(target).mode);
256
+ }
257
+ for (const item of staged) {
258
+ io.renameSync(item.next, item.target);
259
+ item.installed = true;
260
+ }
261
+ return staged.map((item) => item.destination);
262
+ } catch (error_) {
263
+ const rollbackErrors = [];
264
+ for (const item of staged.filter((entry) => entry.installed).toReversed()) {
265
+ try {
266
+ if (item.existed) io.renameSync(item.previous, item.target);
267
+ else io.unlinkSync(item.target);
268
+ } catch (error) {
269
+ rollbackErrors.push(`${item.destination}: ${error.message}`);
270
+ }
271
+ }
272
+ keepBackups = rollbackErrors.length > 0;
273
+ const status = keepBackups
274
+ ? `publication may contain mixed reports; recovery backups: ${staged.map((item) => item.directory).join(', ')}; rollback errors: ${rollbackErrors.join('; ')}`
275
+ : 'previous reports restored; no partial publication remains';
276
+ const error = new WorkflowError('environment-configuration', `could not publish artifacts: ${error_.message}; ${status}`, {
277
+ cause: error_,
278
+ });
279
+ error.runDir = runDir;
280
+ throw error;
281
+ } finally {
282
+ if (!keepBackups) {
283
+ for (const item of staged) {
284
+ try {
285
+ io.rmSync(item.directory, { recursive: true, force: true });
286
+ } catch (error) {
287
+ process.stderr.write(`publication staging cleanup failed at ${item.directory}: ${error.message}\n`);
288
+ }
289
+ }
290
+ }
291
+ }
292
+ }
293
+ /** Resolve user inputs, invoke the actual skill and publish verified deliverables. */
294
+ function run(argv) {
295
+ const program = addAgentOptions(
296
+ new Command().name('tea-test-design').description('Generate and verify an epic or system test plan with the TEA test-design skill.'),
297
+ );
298
+ program
299
+ .option('--project-root <dir>', 'consuming project root', process.cwd())
300
+ .option('--skill-root <dir>', 'explicit trusted skill directory; defaults to the packaged skill')
301
+ .requiredOption('--input <path>', 'requirements or architecture document in the project (repeatable)', collect, [])
302
+ .option('--scope <name>', 'epic or system', 'epic')
303
+ .option('--epic <id>', 'epic number or stable slug; required for epic scope')
304
+ .option('--output-dir <dir>', 'published artifact root inside the project; defaults to TEA test_artifacts')
305
+ .option('--evidence-dir <dir>', 'retained prompts, raw streams and fresh artifacts inside the project', '.tea-runs')
306
+ .setOptionValue('retries', '0')
307
+ .exitOverride()
308
+ .configureOutput({ writeErr: () => {} });
309
+ try {
310
+ program.parse(argv);
311
+ } catch (error) {
312
+ if (error.exitCode === 0) return;
313
+ throw new WorkflowError('usage', error.message);
314
+ }
315
+ const options = program.opts();
316
+ const projectRoot = fs.realpathSync(path.resolve(options.projectRoot));
317
+ if (!['epic', 'system'].includes(options.scope)) throw new WorkflowError('usage', '--scope must be epic or system');
318
+ if (options.scope === 'epic' && (!options.epic || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(options.epic)))
319
+ throw new WorkflowError('usage', 'epic scope requires --epic with a number or stable lowercase slug');
320
+ if (options.scope === 'system' && options.epic) throw new WorkflowError('usage', '--epic applies to epic scope');
321
+ if (options.scope === 'system' && options.input.length < 2)
322
+ throw new WorkflowError('usage', 'system scope requires PRD and architecture inputs, supplied with at least two --input options');
323
+ const inputs = [...new Set(options.input.map((file) => readInput(projectRoot, file, '--input')))];
324
+ if (options.scope === 'system' && inputs.length < 2)
325
+ throw new WorkflowError('usage', 'system scope requires two distinct input documents');
326
+ const inputDigests = inputs.map((file) => [file, digest(file)]);
327
+ const skillRoot = resolveWorkflowSkill({ projectRoot, skillName: 'bmad-testarch-test-design', skillRoot: options.skillRoot });
328
+ const resolvedConfig = resolveTeaConfig({ projectRoot, skillRoot, skillName: 'bmad-testarch-test-design' });
329
+ const outputValue =
330
+ options.outputDir || resolvedConfig.configSnapshot.modules.tea.test_artifacts.replaceAll('{project-root}', projectRoot);
331
+ const outputDir = projectPath(projectRoot, outputValue, '--output-dir');
332
+ const runKey = options.scope === 'epic' ? `epic-${options.epic}` : 'system';
333
+ const projectName =
334
+ path
335
+ .basename(projectRoot)
336
+ .toLowerCase()
337
+ .replaceAll(/[^a-z0-9]+/g, '-')
338
+ .replaceAll(/^-|-$/g, '') || 'project';
339
+ const planName = options.scope === 'epic' ? `test-design-epic-${options.epic}.md` : 'test-design-qa.md';
340
+ const publishedNames = options.scope === 'epic' ? [planName] : ['test-design-architecture.md', planName, `${projectName}-handoff.md`];
341
+ const destinations = [...publishedNames, `test-design-progress-${runKey}.md`].map((name) =>
342
+ projectPath(projectRoot, path.join(outputDir, 'test-design', name), 'published artifact'),
343
+ );
344
+ checkPublication(destinations, inputs);
345
+ const result = runWithEvidence({
346
+ name: 'tea-test-design',
347
+ projectRoot,
348
+ evidenceRoot: options.evidenceDir,
349
+ options,
350
+ prepare({ attemptDir }) {
351
+ const artifactsRoot = path.join(attemptDir, 'artifacts');
352
+ const config = {
353
+ ...resolvedConfig,
354
+ configSnapshot: {
355
+ core: { ...resolvedConfig.configSnapshot.core, project_name: projectName },
356
+ modules: { tea: { ...resolvedConfig.configSnapshot.modules.tea, test_artifacts: artifactsRoot } },
357
+ },
358
+ };
359
+ const artifactFiles = publishedNames.map((file) => path.join('artifacts', 'test-design', file));
360
+ const checkpointFile = path.join('artifacts', 'test-design', `test-design-progress-${runKey}.md`);
361
+ const requestLines = [
362
+ `Start Create mode at ${JSON.stringify(path.join(skillRoot, 'steps-c', 'step-01-detect-mode.md'))}.`,
363
+ `mode=${options.scope}-level; run_scope=${options.scope}; run_key=${runKey}; design_level=full`,
364
+ ...(options.scope === 'epic' ? [`epic_num=${options.epic}`] : []),
365
+ `project_name=${projectName}; test_artifacts=${JSON.stringify(artifactsRoot)}; tea_browser_automation=none`,
366
+ `Inputs are exactly this JSON list: ${JSON.stringify(inputs)}. Read them as the supplied requirements and architecture context.`,
367
+ 'Do not discover other requirement documents. Do not generate tests or modify any input document.',
368
+ `Produce these deliverables: ${JSON.stringify(artifactFiles.map((file) => path.join(attemptDir, file)))}`,
369
+ `Save a completed progress checkpoint at ${JSON.stringify(path.join(attemptDir, checkpointFile))}.`,
370
+ 'Use the workflow template and checklist. Complete all five steps and preserve its scored risk and coverage table schema.',
371
+ ];
372
+ return {
373
+ prompt: headlessPrompt({ skillRoot, projectRoot, resolvedConfig: config, requestLines }),
374
+ artifactFiles,
375
+ checkpointFile,
376
+ runKey,
377
+ runScope: options.scope,
378
+ inputDigests,
379
+ planFile: path.join('artifacts', 'test-design', planName),
380
+ };
381
+ },
382
+ validate: validateDesign,
383
+ });
384
+ if (result.dryRun) {
385
+ process.stdout.write(
386
+ `${JSON.stringify({ mode: 'prompt-only', evidence: result.runDir, prompt: path.join(result.attemptDir, 'prompt.txt') })}\n`,
387
+ );
388
+ return;
389
+ }
390
+ // Recheck after execution: the consuming project is writable during generation.
391
+ checkPublication(
392
+ destinations.map((file) => projectPath(projectRoot, file, 'published artifact')),
393
+ inputs,
394
+ );
395
+ const published = publishDesign(result.value.artifacts, { projectRoot, outputDir, runDir: result.runDir });
396
+ process.stdout.write(
397
+ `${JSON.stringify({ mode: 'live', runKey, evidence: result.runDir, artifacts: published, riskCount: result.value.riskCount, coverageCount: result.value.coverageCount })}\n`,
398
+ );
399
+ }
400
+ /** Convert configuration and agent failures to the documented CLI exit codes. */
401
+ function main(argv) {
402
+ try {
403
+ run(argv);
404
+ return 0;
405
+ } catch (error) {
406
+ return mainError('tea-test-design', error);
407
+ }
408
+ }
409
+ if (require.main === module) process.exitCode = main(process.argv);
410
+ module.exports = { main, validateDesign, publishDesign };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-method-test-architecture-enterprise",
4
- "version": "2.0.4-next.0",
4
+ "version": "2.0.4-next.1",
5
5
  "description": "Master Test Architect for quality strategy, test automation, and release gates",
6
6
  "keywords": [
7
7
  "bmad",
@@ -40,6 +40,7 @@
40
40
  "tea-nfr-runner": "cli/nfr-runner.js",
41
41
  "tea-routing-runner": "cli/routing-runner.js",
42
42
  "tea-skill-runner": "cli/skill-runner.js",
43
+ "tea-test-design": "cli/test-design.js",
43
44
  "tea-test-design-runner": "cli/test-design-runner.js",
44
45
  "tea-test-review": "cli/test-review.js",
45
46
  "tea-trace": "cli/trace.js",
@@ -90,7 +91,7 @@
90
91
  "release:minor": "gh workflow run publish.yaml -f channel=latest -f bump=minor",
91
92
  "release:next": "gh workflow run publish.yaml -f channel=next",
92
93
  "release:patch": "gh workflow run publish.yaml -f channel=latest -f bump=patch",
93
- "test": "npm run test:conflict-markers && npm run test:schemas && npm run test:install && npm run test:evaluate-guidance && npm run test:evaluate-authoring && npm run test:evaluate-gap-loop && npm run test:evaluate-dogfood && npm run test:evaluate-check && npm run test:evaluate-compare && npm run test:evaluate-before-state && npm run test:evaluate-ci-gates && npm run test:evaluate-ci-tiers && npm run test:evaluate-ci-repositories:tagged-release-pr && npm run test:evaluate-ci-repositories:tagged-release-merge && npm run test:evaluate-ci-repositories:tagged-release-release && npm run test:evaluate-ci-repositories:nightly-deploy-pr && npm run test:evaluate-ci-repositories:nightly-deploy-merge && npm run test:evaluate-ci-repositories:nightly-deploy-scheduled && npm run test:evaluate-ci-repositories:nightly-deploy-release && npm run test:evaluate-ci-render && npm run test:evaluate-pr-mcp && npm run test:evaluate-pr-api && npm run test:evaluate-pr-suite && npm run test:evaluate-pr-workflow && npm run test:evaluate-pr-tool-use && npm run test:evaluate-pr-promptfoo && npm run test:evaluate-pr-ai-feature && npm run test:evaluate-pr-test-review && npm run test:evaluate-pr-gap-loop && npm run test:evaluate-pr-learn && npm run test:evaluate-pr-tutorial && npm run test:evaluate-partitions && npm run test:evaluate-partition-plans-views-and-check && npm run test:evaluate-partition-plans-preflight-and-run && npm run test:evaluate-partition-plans-score && npm run test:evaluate-partition-plans-attempts-seal-and-mappings && npm run test:evaluate-partition-plans-attempts-records && npm run test:evaluate-partition-plans-attempts-gameability && npm run test:evaluate-interpret && npm run test:evaluate-calibration && npm run test:evaluate-boundaries && npm run test:evaluate-preflight && npm run test:evaluate-isolation-conformance && npm run test:evaluate-mutation:qualification-basics && npm run test:evaluate-mutation:qualification-guards && npm run test:evaluate-mutation:recovery && npm run test:evaluate-mutation:journals && npm run test:evaluate-run && npm run test:evaluate-progress && npm run test:evaluate-aggregate && npm run test:evaluate-confinement && npm run test:evaluate-confinement-audit && npm run test:evaluate-confinement-history && npm run test:evaluate-confinement-git-state && npm run test:evaluate-confinement-network && npm run test:evaluate-held-inputs && npm run test:evaluate-arms-historical && npm run test:evaluate-arms-deployments && npm run test:evaluate-evaluators && npm run test:evaluate-evaluators-agent-version && npm run test:evaluate-agents && npm run test:evaluate-agents-importers && npm run test:evaluate-agents-sealed-brief && npm run test:evaluate-held-attempts && npm run test:evaluate-private && npm run test:evaluate-records && npm run test:evaluate-mcp && npm run test:evaluate-api && npm run test:evaluate-workflow && npm run test:evaluate-tool-use && npm run test:evaluate-promptfoo && npm run test:evaluate-learned-framework && npm run test:docs-tutorial && npm run test:knowledge && npm run test:criteria-fragments && npm run test:doc-count-sources && npm run test:planning-doc-sources && npm run test:doc-counts && npm run test:doc-claim-sources && npm run test:doc-claims && npm run test:doc-invocation-entry && npm run test:doc-invocations && npm run test:doc-link-anchors && npm run test:docs-build-names && npm run test:docs-evaluate-sidebar && npm run test:docs-gap-reader && npm run test:enforce-hook && npm run test:framework-scaffold && npm run test:isolation-primitives && npm run test:atdd-isolation && npm run test:atdd-net-guard && npm run test:atdd-scoring && npm run test:atdd-workflow-guidance && npm run test:generation-healing && npm run test:framework-scaffold-install-isolation && npm run test:transcript-harness && npm run test:suite-manifest && npm run test:teach-me-testing-scoring && npm run test:eval-data && npm run test:eval-atdd-data && npm run test:eval-ci-data && npm run test:eval-framework-scaffold-data && npm run test:eval-nfr-data && npm run test:nfr-validate-corpus && npm run test:nfr-evidence-guidance && npm run test:eval-test-design-data && npm run test:eval-trace-data && npm run test:eval-routing-data && npm run test:eval-routing-boundaries && npm run test:eval-routing-evidence && npm run test:eval-transcript-data && npm run test:eval-teach-me-testing-data && npm run test:eval-schemas && npm run test:eval-diagnostics && npm run test:eval-replay && npm run test:eval-quality-corpus && npm run test:schema-versions && npm run test:contract-sources && npm run test:contracts && npm run test:contract-oracles && npm run test:probe-sources && npm run test:test-design-qualification && npm run test:test-review-qualification && npm run test:trace-qualification && npm run test:nfr-qualification && npm run test:ci-qualification && npm run test:probe-corpus && npm run test:compare-dominance && npm run test:compare-eval-runs && npm run test:trial-set-scoring && npm run test:automate-cli && npm run test:automate-generation-evidence && npm run test:automate-eval-fixture && npm run test:eval-automate-data && npm run test:clock-port && npm run test:probe-conformance && npm run test:corpus-conformance && npm run test:file-system-conformance && npm run test:file-system-port && npm run test:probe-targets && npm run test:port-totality && npm run test:ci-coverage && npm run test:ci-coverage-filters && npm run test:install-actionlint && npm run test:workflows-lint && npm run test:install-linux-sandbox && npm run test:shards && npm run test:groups && npm run test:cli && npm run test:test-review-pr-scope && npm run test:test-review-report && npm run test:test-review-rubric && npm run test:bmad-output-gated && npm run test:release-metadata && npm run test:release-notes && npm run test:lockfile-age && npm run test:lockfile-age-cache && npm run test:licences && npm run test:supply-chain && npm run test:boundary && npm run test:direction && npm run test:lineage && npm run test:guard-publish && npm run test:layering-boundary-lineage && npm run test:changelog && npm run test:tea-workflow-descriptions && npm run test:docs-mermaid && npm run test:llms-headroom && npm run validate:schemas && npm run lint && npm run lint:md && npm run format:check",
94
+ "test": "npm run test:conflict-markers && npm run test:schemas && npm run test:install && npm run test:evaluate-guidance && npm run test:evaluate-authoring && npm run test:evaluate-gap-loop && npm run test:evaluate-dogfood && npm run test:evaluate-check && npm run test:evaluate-compare && npm run test:evaluate-before-state && npm run test:evaluate-ci-gates && npm run test:evaluate-ci-tiers && npm run test:evaluate-ci-repositories:tagged-release-pr && npm run test:evaluate-ci-repositories:tagged-release-merge && npm run test:evaluate-ci-repositories:tagged-release-release && npm run test:evaluate-ci-repositories:nightly-deploy-pr && npm run test:evaluate-ci-repositories:nightly-deploy-merge && npm run test:evaluate-ci-repositories:nightly-deploy-scheduled && npm run test:evaluate-ci-repositories:nightly-deploy-release && npm run test:evaluate-ci-render && npm run test:evaluate-pr-mcp && npm run test:evaluate-pr-api && npm run test:evaluate-pr-suite && npm run test:evaluate-pr-workflow && npm run test:evaluate-pr-tool-use && npm run test:evaluate-pr-promptfoo && npm run test:evaluate-pr-ai-feature && npm run test:evaluate-pr-test-review && npm run test:evaluate-pr-gap-loop && npm run test:evaluate-pr-learn && npm run test:evaluate-pr-tutorial && npm run test:evaluate-partitions && npm run test:evaluate-partition-plans-views-and-check && npm run test:evaluate-partition-plans-preflight-and-run && npm run test:evaluate-partition-plans-score && npm run test:evaluate-partition-plans-attempts-seal-and-mappings && npm run test:evaluate-partition-plans-attempts-records && npm run test:evaluate-partition-plans-attempts-gameability && npm run test:evaluate-interpret && npm run test:evaluate-calibration && npm run test:evaluate-boundaries && npm run test:evaluate-preflight && npm run test:evaluate-isolation-conformance && npm run test:evaluate-mutation:qualification-basics && npm run test:evaluate-mutation:qualification-guards && npm run test:evaluate-mutation:recovery && npm run test:evaluate-mutation:journals && npm run test:evaluate-run && npm run test:evaluate-progress && npm run test:evaluate-aggregate && npm run test:evaluate-confinement && npm run test:evaluate-confinement-audit && npm run test:evaluate-confinement-history && npm run test:evaluate-confinement-git-state && npm run test:evaluate-confinement-network && npm run test:evaluate-held-inputs && npm run test:evaluate-arms-historical && npm run test:evaluate-arms-deployments && npm run test:evaluate-evaluators && npm run test:evaluate-evaluators-agent-version && npm run test:evaluate-agents && npm run test:evaluate-agents-importers && npm run test:evaluate-agents-sealed-brief && npm run test:evaluate-held-attempts && npm run test:evaluate-private && npm run test:evaluate-records && npm run test:evaluate-mcp && npm run test:evaluate-api && npm run test:evaluate-workflow && npm run test:evaluate-tool-use && npm run test:evaluate-promptfoo && npm run test:evaluate-learned-framework && npm run test:docs-tutorial && npm run test:knowledge && npm run test:criteria-fragments && npm run test:doc-count-sources && npm run test:planning-doc-sources && npm run test:doc-counts && npm run test:doc-claim-sources && npm run test:doc-claims && npm run test:doc-invocation-entry && npm run test:doc-invocations && npm run test:doc-link-anchors && npm run test:docs-build-names && npm run test:docs-evaluate-sidebar && npm run test:docs-gap-reader && npm run test:enforce-hook && npm run test:framework-scaffold && npm run test:isolation-primitives && npm run test:atdd-isolation && npm run test:atdd-net-guard && npm run test:atdd-scoring && npm run test:atdd-workflow-guidance && npm run test:generation-healing && npm run test:framework-scaffold-install-isolation && npm run test:transcript-harness && npm run test:suite-manifest && npm run test:teach-me-testing-scoring && npm run test:eval-data && npm run test:eval-atdd-data && npm run test:eval-ci-data && npm run test:eval-framework-scaffold-data && npm run test:eval-nfr-data && npm run test:nfr-validate-corpus && npm run test:nfr-evidence-guidance && npm run test:eval-test-design-data && npm run test:eval-trace-data && npm run test:eval-routing-data && npm run test:eval-routing-boundaries && npm run test:eval-routing-evidence && npm run test:eval-transcript-data && npm run test:eval-teach-me-testing-data && npm run test:eval-schemas && npm run test:eval-diagnostics && npm run test:eval-replay && npm run test:eval-quality-corpus && npm run test:schema-versions && npm run test:contract-sources && npm run test:contracts && npm run test:contract-oracles && npm run test:probe-sources && npm run test:test-design-qualification && npm run test:test-review-qualification && npm run test:trace-qualification && npm run test:nfr-qualification && npm run test:ci-qualification && npm run test:probe-corpus && npm run test:compare-dominance && npm run test:compare-eval-runs && npm run test:trial-set-scoring && npm run test:automate-cli && npm run test:automate-generation-evidence && npm run test:automate-eval-fixture && npm run test:eval-automate-data && npm run test:clock-port && npm run test:probe-conformance && npm run test:corpus-conformance && npm run test:file-system-conformance && npm run test:file-system-port && npm run test:probe-targets && npm run test:port-totality && npm run test:ci-coverage && npm run test:ci-coverage-filters && npm run test:install-actionlint && npm run test:workflows-lint && npm run test:install-linux-sandbox && npm run test:shards && npm run test:groups && npm run test:cli && npm run test:workflow-cli && npm run test:test-review-pr-scope && npm run test:test-review-report && npm run test:test-review-rubric && npm run test:bmad-output-gated && npm run test:release-metadata && npm run test:release-notes && npm run test:lockfile-age && npm run test:lockfile-age-cache && npm run test:licences && npm run test:supply-chain && npm run test:boundary && npm run test:direction && npm run test:lineage && npm run test:guard-publish && npm run test:layering-boundary-lineage && npm run test:changelog && npm run test:tea-workflow-descriptions && npm run test:docs-mermaid && npm run test:llms-headroom && npm run validate:schemas && npm run lint && npm run lint:md && npm run format:check",
94
95
  "test:atdd-isolation": "node test/test-atdd-isolation.js",
95
96
  "test:atdd-net-guard": "node test/test-atdd-net-guard.js",
96
97
  "test:atdd-scoring": "node test/test-atdd-scoring.js",
@@ -263,6 +264,7 @@
263
264
  "test:trace-qualification": "node test/test-trace-qualification.js",
264
265
  "test:transcript-harness": "node test/test-transcript-harness.js",
265
266
  "test:trial-set-scoring": "node test/test-trial-set-scoring.js",
267
+ "test:workflow-cli": "node test/test-workflow-cli.js",
266
268
  "test:workflows-lint": "node test/test-workflows-lint.js",
267
269
  "validate:schemas": "node tools/validate-agent-schema.js"
268
270
  },
@@ -62,9 +62,15 @@ Using `risk-governance.md` and `probability-impact.md` (if loaded):
62
62
  - Identify real risks (not just features)
63
63
  - Ground every risk in an explicit statement from the supplied epic or a named artifact loaded for this run. Record the supporting statement in the risk description or mitigation notes so another reader can trace the row.
64
64
  - Keep the register bounded by the scope supported by the supplied epic or a named artifact loaded for this run. Do not add plausible risks merely because they are common in other systems. If a concern has no supporting statement, record it as an assumption or clarification outside the risk register.
65
+ - Treat controls explicitly described as already satisfied as scope constraints. Verify them with regression assertions in the coverage plan. A hypothetical implementation violating a stated read-only, local-only, privacy, or rollout constraint needs evidence of a new exposure before it becomes a scored risk.
66
+ - Consolidate conditions that share a failure mechanism, consequence, and mitigation into one risk. Keep individual trigger and boundary cases in the coverage plan. For example, a stale cached timestamp and a missed refresh event can be one display-freshness risk; absent-value and disabled-flag rendering can share one conditional-visibility risk when the same visibility guard controls both.
67
+ - Preserve the risk table columns `Risk ID`, `Category`, `Description`, `Probability`, `Impact`, and `Score`. Add a separate source-evidence column when useful. State the failure consequence in `Description` and cite the supporting input.
68
+ - Assign unique canonical IDs `R-001`, `R-002`, and so on. Preserve those IDs in coverage, mitigations, summaries, and handoffs. Keep epic or story identity in the document run key.
69
+ - Preserve explicit score ranges in risk-band headings: high score 6 or greater, medium score 3 to 4, and low score 1 to 2. For a band with no identified risk, state that absence outside the table and leave its table body empty.
65
70
  - Classify by category: TECH / SEC / PERF / DATA / BUS / OPS
66
71
  - Score Probability (1–3) and Impact (1–3)
67
72
  - Calculate Risk Score (P × I)
73
+ - Put only a single bare integer in each Probability, Impact, and Score table cell. Keep likelihood and consequence explanations in the Description, Mitigation, or nearby prose. A cell such as `2: possible boundary error` is invalid even when its leading number is in range.
68
74
  - Flag high risks (score ≥ 6)
69
75
  - Define mitigation, owner, and timeline
70
76
 
@@ -46,6 +46,7 @@ For each requirement or risk-driven scenario:
46
46
  - Assign priorities (P0–P3) using `test-priorities-matrix.md`
47
47
  - Map NFR-derived risks to planned validation scenarios and evidence sources
48
48
  - Use the exact `Risk ID` assigned in the risk register in every `Risk Link` cell. A prose description alone is not a traceable mapping.
49
+ - Each `Test Level` cell contains exactly one canonical value: E2E, API, Component, Integration, or Unit. Put performance, operational, device, and other qualifiers in scenario descriptions or notes.
49
50
  - Give every material risk at least one coverage row whose `Test Level` is suitable for that risk under `test-levels-framework.md`. Explain the suitability in the row notes when the level is not obvious. Preserve the risk ID when rows are split across priorities or test levels.
50
51
 
51
52
  **Priority rules:** decide each priority on business impact, user reach, and workaround
@@ -57,6 +58,8 @@ Walk the Priority Decision Tree in `test-priorities-matrix.md` for each scenario
57
58
  - P2: Secondary behavior with narrower user reach and an acceptable workaround
58
59
  - P3: Rare, cosmetic, or experimental behavior with minimal impact and an easy workaround
59
60
 
61
+ Record the failure consequence and the source-supported workaround in the primary coverage row's notes. Preserve these four priority criteria in the published plan. Evaluate a performance budget or an operational control through its documented business consequence before assigning its test priority. Assess a workaround against the original failure consequence, before proposed mitigations. A valid workaround preserves essential business operation and resolves the affected state. Pausing the application, leaving a core user action unresolved, or suppressing a symptom needs a source-backed recovery path before it can justify a lower priority. Record the evidence for recoverability; mark it unresolved when the source provides none. Separate an acceptance threshold from the consequence of missing it. For a latency or throughput target, state whether the source establishes a critical business deadline, permanent loss, blocked essential operation, or bounded degradation while work remains recoverable. Record an unstated business consequence as a clarification. Assign P0 when the documented consequence meets the critical-impact criterion. A release acceptance target can still have P1 coverage when the documented consequence is material degradation. An empty priority band is valid; explain why the planned scenarios belong to the assigned bands. When every primary risk has the same priority, justify how each consequence meets that criterion.
62
+
60
63
  ---
61
64
 
62
65
  ## 2. NFR Coverage and Evidence Plan
@@ -185,7 +185,8 @@ Before finalizing, review the complete output document for quality:
185
185
  1. **Remove duplication**: Progressive-append workflow may have created repeated sections — consolidate
186
186
  2. **Verify consistency**: Ensure terminology, risk scores, and references are consistent throughout
187
187
  3. **Check completeness**: All template sections should be populated or explicitly marked N/A
188
- 4. **Format cleanup**: Ensure markdown formatting is clean (tables aligned, headers consistent, no orphaned references)
188
+ 4. **Table schema**: Preserve the canonical risk and coverage column names from the templates. Verify every scored risk has one unique `R-<three digits>` ID, every coverage link preserves that exact ID, and every risk-band heading states its score range. Probability, Impact, and Score cells contain only bare integers; explain each judgment in prose and verify Score equals Probability × Impact. Empty bands contain no risk rows; describe the absence in prose outside the table. Keep each risk description readable and traceable to its source.
189
+ 5. **Format cleanup**: Ensure markdown formatting is clean (tables aligned, headers consistent, no orphaned references)
189
190
 
190
191
  ---
191
192
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  [bmod]
4
4
  code = "tea"
5
- version = "2.0.4-next.0"
5
+ version = "2.0.4-next.1"
6
6
  update_source = "github:bmad-code-org/bmad-method-test-architecture-enterprise/skills"
7
7
  skills = [
8
8
  "bmad-tea",