@xulthekl/team-flow 0.32.2 → 0.34.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 (50) hide show
  1. package/.claude/always/phase-guard.md +1 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/.cursor-plugin/marketplace.json +1 -1
  6. package/.cursor-plugin/plugin.json +1 -1
  7. package/.github/plugin/marketplace.json +2 -2
  8. package/AGENTS.md +2 -0
  9. package/CHANGELOG.md +56 -0
  10. package/CONTRIBUTING.md +44 -0
  11. package/GEMINI.md +1 -1
  12. package/INSTALL.md +1 -1
  13. package/README.md +1 -1
  14. package/agents/architecture-design.md +1 -34
  15. package/agents/architecture-reviewer.md +1 -42
  16. package/agents/bug-investigator.md +1 -37
  17. package/agents/build-executor.md +1 -22
  18. package/agents/change-split-auditor.md +1 -42
  19. package/agents/code-reviewer.md +1 -42
  20. package/agents/contract-builder.md +1 -22
  21. package/agents/cross-change-consistency-checker.md +2 -43
  22. package/agents/need-explorer.md +1 -22
  23. package/agents/prd-completeness-reviewer.md +1 -47
  24. package/agents/prototype-builder.md +1 -41
  25. package/agents/prototype-env-scout.md +1 -26
  26. package/agents/prototype-reviewer.md +1 -41
  27. package/agents/release-archivist.md +1 -22
  28. package/agents/spec-writer.md +1 -22
  29. package/docs/README_en.md +1 -1
  30. package/docs/solutions/INDEX.md +1 -0
  31. package/docs/solutions/cross-phase/2026-08-04-no-summary.md +17 -0
  32. package/gemini-extension.json +1 -1
  33. package/hooks/session-start +2 -2
  34. package/llms.txt +1 -1
  35. package/package.json +5 -4
  36. package/plugin.json +1 -1
  37. package/scripts/lib/conventions-generator.mjs +350 -0
  38. package/scripts/lib/test-record.mjs +65 -2
  39. package/skills/e2e/SKILL.md +1 -1
  40. package/skills/test-strategy/SKILL.md +38 -1
  41. package/skills/test-strategy/references/integration-test-contracts.md +237 -0
  42. package/skills/test-strategy/references/integration-test-isolation.md +346 -0
  43. package/skills/test-strategy/references/test-quality-rules.md +292 -0
  44. package/skills/workflow-bootstrap/SKILL.md +40 -3
  45. package/templates/agent-template.md +41 -0
  46. package/templates/conventions/_manifest.json +39 -0
  47. package/templates/conventions/glaf4-compliant/java-testing.md +367 -0
  48. package/templates/conventions/glaf4-compliant/spring-patterns.md +415 -0
  49. package/templates/conventions/js-testing.md +261 -0
  50. package/templates/conventions/python-testing.md +333 -0
@@ -0,0 +1,350 @@
1
+ #!/usr/bin/env node
2
+ // scripts/lib/conventions-generator.mjs — conventions 生成器(v0.34.0 新增)
3
+ //
4
+ // 根据项目技术栈自动生成测试规范 conventions,符合 glaf4-test 要求。
5
+ // 支持存量项目(自动识别)和全新项目(交互式引导)。
6
+ //
7
+ // 使用方式:
8
+ // node scripts/lib/conventions-generator.mjs --root <项目根目录> [--interactive]
9
+ //
10
+ // 设计来源:test-capability-enhancement-design v1.0 §四
11
+
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
13
+ import path, { join } from 'node:path';
14
+ import { parseArgs } from 'node:util';
15
+
16
+ // ── 常量 ─────────────────────────────────────────────────────────────────────
17
+
18
+ const TEMPLATE_DIR = join(import.meta.dirname, '../../templates/conventions');
19
+ const GLAF4_TEMPLATE_DIR = join(TEMPLATE_DIR, 'glaf4-compliant');
20
+
21
+ // 技术栈检测特征
22
+ const TECH_STACK_SIGNATURES = {
23
+ java: ['pom.xml', 'build.gradle', 'build.gradle.kts'],
24
+ javascript: ['package.json', 'yarn.lock', 'pnpm-lock.yaml'],
25
+ python: ['requirements.txt', 'pyproject.toml', 'setup.py', 'Pipfile'],
26
+ };
27
+
28
+ // 测试框架检测特征
29
+ const TEST_FRAMEWORK_SIGNATURES = {
30
+ 'junit5-mockito': { file: 'pom.xml', content: /junit-jupiter|org\.junit\.jupiter/ },
31
+ jest: { file: 'package.json', content: /"jest"|"@jest\/|vitest/ },
32
+ pytest: { file: 'requirements.txt', content: /pytest/ },
33
+ };
34
+
35
+ // 应用框架检测特征
36
+ const APP_FRAMEWORK_SIGNATURES = {
37
+ 'spring-boot': { file: 'pom.xml', content: /spring-boot|org\.springframework\.boot/ },
38
+ express: { file: 'package.json', content: /"express"/ },
39
+ django: { file: 'requirements.txt', content: /django/ },
40
+ };
41
+
42
+ // ── 技术栈检测 ─────────────────────────────────────────────────────────────
43
+
44
+ /**
45
+ * 检测项目技术栈
46
+ * @param {string} root 项目根目录
47
+ * @returns {{ language: string|null, buildTool: string|null, testFramework: string|null, appFramework: string|null }}
48
+ */
49
+ export function detectTechStack(root) {
50
+ const result = {
51
+ language: null,
52
+ buildTool: null,
53
+ testFramework: null,
54
+ appFramework: null,
55
+ };
56
+
57
+ // 检测语言
58
+ for (const [lang, files] of Object.entries(TECH_STACK_SIGNATURES)) {
59
+ if (files.some(f => existsSync(join(root, f)))) {
60
+ result.language = lang;
61
+ break;
62
+ }
63
+ }
64
+
65
+ // 检测构建工具
66
+ if (existsSync(join(root, 'pom.xml'))) result.buildTool = 'maven';
67
+ else if (existsSync(join(root, 'build.gradle')) || existsSync(join(root, 'build.gradle.kts'))) result.buildTool = 'gradle';
68
+ else if (existsSync(join(root, 'package.json'))) result.buildTool = 'npm';
69
+ else if (existsSync(join(root, 'yarn.lock'))) result.buildTool = 'yarn';
70
+ else if (existsSync(join(root, 'pnpm-lock.yaml'))) result.buildTool = 'pnpm';
71
+ else if (existsSync(join(root, 'requirements.txt')) || existsSync(join(root, 'pyproject.toml'))) result.buildTool = 'pip';
72
+ else if (existsSync(join(root, 'Pipfile'))) result.buildTool = 'pipenv';
73
+
74
+ // 检测测试框架
75
+ for (const [fw, sig] of Object.entries(TEST_FRAMEWORK_SIGNATURES)) {
76
+ const filePath = join(root, sig.file);
77
+ if (existsSync(filePath)) {
78
+ const content = readFileSync(filePath, 'utf-8');
79
+ if (sig.content.test(content)) {
80
+ result.testFramework = fw;
81
+ break;
82
+ }
83
+ }
84
+ }
85
+
86
+ // 检测应用框架
87
+ for (const [fw, sig] of Object.entries(APP_FRAMEWORK_SIGNATURES)) {
88
+ const filePath = join(root, sig.file);
89
+ if (existsSync(filePath)) {
90
+ const content = readFileSync(filePath, 'utf-8');
91
+ if (sig.content.test(content)) {
92
+ result.appFramework = fw;
93
+ break;
94
+ }
95
+ }
96
+ }
97
+
98
+ return result;
99
+ }
100
+
101
+ // ── 模板选择 ─────────────────────────────────────────────────────────────
102
+
103
+ /**
104
+ * 根据技术栈选择 conventions 模板
105
+ * @param {{ language: string|null, buildTool: string|null, testFramework: string|null, appFramework: string|null }} techStack
106
+ * @returns {string[]} 模板文件名列表
107
+ */
108
+ export function selectTemplates(techStack) {
109
+ const templates = [];
110
+
111
+ if (techStack.language === 'java') {
112
+ // Java 项目:使用 glaf4-compliant 模板
113
+ if (existsSync(join(GLAF4_TEMPLATE_DIR, 'java-testing.md'))) {
114
+ templates.push('glaf4-compliant/java-testing.md');
115
+ }
116
+ if (techStack.appFramework === 'spring-boot') {
117
+ if (existsSync(join(GLAF4_TEMPLATE_DIR, 'spring-patterns.md'))) {
118
+ templates.push('glaf4-compliant/spring-patterns.md');
119
+ }
120
+ }
121
+ } else if (techStack.language === 'javascript') {
122
+ templates.push('js-testing.md');
123
+ } else if (techStack.language === 'python') {
124
+ templates.push('python-testing.md');
125
+ }
126
+
127
+ return templates;
128
+ }
129
+
130
+ // ── conventions 生成 ─────────────────────────────────────────────────────
131
+
132
+ /**
133
+ * 生成 conventions 文件
134
+ * @param {string} root 项目根目录
135
+ * @param {{ language: string|null, buildTool: string|null, testFramework: string|null, appFramework: string|null }} techStack
136
+ * @param {string[]} templates 模板文件名列表
137
+ * @returns {{ created: string[], skipped: string[] }}
138
+ */
139
+ export function generateConventions(root, techStack, templates) {
140
+ const conventionsDir = join(root, '.team-flow', 'conventions');
141
+ const versionsFile = join(conventionsDir, '.versions.json');
142
+
143
+ // 创建目录
144
+ mkdirSync(conventionsDir, { recursive: true });
145
+
146
+ // 读取现有版本信息
147
+ let versions = {};
148
+ if (existsSync(versionsFile)) {
149
+ try {
150
+ versions = JSON.parse(readFileSync(versionsFile, 'utf-8'));
151
+ } catch {
152
+ versions = {};
153
+ }
154
+ }
155
+ if (!versions.conventions) versions.conventions = {};
156
+
157
+ const created = [];
158
+ const skipped = [];
159
+
160
+ for (const template of templates) {
161
+ const templatePath = join(TEMPLATE_DIR, template);
162
+ const fileName = path.basename(template);
163
+ const targetPath = join(conventionsDir, fileName);
164
+
165
+ // 检查是否已存在且未自定义
166
+ if (existsSync(targetPath)) {
167
+ const existingVersion = versions.conventions[fileName];
168
+ if (existingVersion && !existingVersion.customized) {
169
+ // 读取模板版本
170
+ const templateContent = readFileSync(templatePath, 'utf-8');
171
+ const versionMatch = templateContent.match(/version:\s*(\d+\.\d+\.\d+)/);
172
+ const templateVersion = versionMatch ? versionMatch[1] : '1.0.0';
173
+
174
+ if (existingVersion.source_version === templateVersion) {
175
+ skipped.push(fileName);
176
+ continue;
177
+ }
178
+ }
179
+ }
180
+
181
+ // 复制模板
182
+ const templateContent = readFileSync(templatePath, 'utf-8');
183
+ writeFileSync(targetPath, templateContent, 'utf-8');
184
+
185
+ // 更新版本信息
186
+ const versionMatch = templateContent.match(/version:\s*(\d+\.\d+\.\d+)/);
187
+ const templateVersion = versionMatch ? versionMatch[1] : '1.0.0';
188
+
189
+ versions.conventions[fileName] = {
190
+ source_version: templateVersion,
191
+ installed_at: new Date().toISOString().split('T')[0],
192
+ customized: false,
193
+ last_checked: new Date().toISOString().split('T')[0],
194
+ source: template,
195
+ };
196
+
197
+ created.push(fileName);
198
+ }
199
+
200
+ // 写入版本信息
201
+ writeFileSync(versionsFile, JSON.stringify(versions, null, 2), 'utf-8');
202
+
203
+ return { created, skipped };
204
+ }
205
+
206
+ // ── 更新 team-flow.config.json ─────────────────────────────────────────
207
+
208
+ /**
209
+ * 更新 team-flow.config.json 的 conventions 字段
210
+ * @param {string} root 项目根目录
211
+ * @param {string[]} conventionsFiles conventions 文件名列表
212
+ */
213
+ export function updateConfig(root, conventionsFiles) {
214
+ const teamFlowDir = join(root, '.team-flow');
215
+ const configPath = join(teamFlowDir, 'team-flow.config.json');
216
+
217
+ // 确保目录存在
218
+ mkdirSync(teamFlowDir, { recursive: true });
219
+
220
+ let config = {};
221
+ if (existsSync(configPath)) {
222
+ try {
223
+ config = JSON.parse(readFileSync(configPath, 'utf-8'));
224
+ } catch {
225
+ config = {};
226
+ }
227
+ }
228
+
229
+ if (!config.conventions) config.conventions = {};
230
+
231
+ for (const file of conventionsFiles) {
232
+ const key = file.replace(/\.md$/, '').replace(/\//g, '-');
233
+ config.conventions[key] = `.team-flow/conventions/${file}`;
234
+ }
235
+
236
+ writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
237
+ }
238
+
239
+ // ── 交互式引导 ─────────────────────────────────────────────────────────────
240
+
241
+ /**
242
+ * 交互式引导用户选择技术栈(全新项目)
243
+ * @returns {Promise<{ language: string, buildTool: string, testFramework: string, appFramework: string }>}
244
+ */
245
+ export async function interactiveSetup() {
246
+ // 注意:在实际使用中,这里需要使用 readline 或 inquirer 等库
247
+ // 这里提供默认值,实际实现需要集成到 workflow-bootstrap 的交互流程中
248
+ console.log('\n=== 全新项目技术栈配置 ===\n');
249
+ console.log('由于当前为脚本模式,使用默认配置:');
250
+ console.log(' - 编程语言:Java');
251
+ console.log(' - 构建工具:Maven');
252
+ console.log(' - 测试框架:JUnit 5 + Mockito');
253
+ console.log(' - 应用框架:Spring Boot');
254
+ console.log('\n如需自定义配置,请在 workflow-bootstrap 中使用交互式模式。\n');
255
+
256
+ return {
257
+ language: 'java',
258
+ buildTool: 'maven',
259
+ testFramework: 'junit5-mockito',
260
+ appFramework: 'spring-boot',
261
+ };
262
+ }
263
+
264
+ // ── CLI ─────────────────────────────────────────────────────────────────────
265
+
266
+ export async function run(args) {
267
+ const { positionals, values } = parseArgs({
268
+ args,
269
+ options: {
270
+ root: { type: 'string', default: '.' },
271
+ interactive: { type: 'boolean', default: false },
272
+ json: { type: 'boolean', default: false },
273
+ },
274
+ allowPositionals: true,
275
+ });
276
+
277
+ const root = path.resolve(values.root);
278
+
279
+ // 检查项目根目录
280
+ if (!existsSync(root)) {
281
+ console.error(`项目根目录不存在: ${root}`);
282
+ process.exit(1);
283
+ }
284
+
285
+ // 检查 conventions 是否已存在
286
+ const conventionsDir = join(root, '.team-flow', 'conventions');
287
+ if (existsSync(conventionsDir) && readdirSync(conventionsDir).length > 0) {
288
+ console.log('⚠️ conventions 已存在,跳过生成');
289
+ console.log(` 路径: ${conventionsDir}`);
290
+ process.exit(0);
291
+ }
292
+
293
+ // 检测技术栈
294
+ let techStack = detectTechStack(root);
295
+
296
+ // 如果未检测到技术栈且启用交互式模式
297
+ if (!techStack.language && values.interactive) {
298
+ techStack = await interactiveSetup();
299
+ }
300
+
301
+ // 如果仍未检测到技术栈
302
+ if (!techStack.language) {
303
+ console.error('❌ 未检测到技术栈特征');
304
+ console.error(' 请指定项目根目录,或使用 --interactive 模式手动配置');
305
+ process.exit(1);
306
+ }
307
+
308
+ // 选择模板
309
+ const templates = selectTemplates(techStack);
310
+
311
+ if (templates.length === 0) {
312
+ console.error('❌ 未找到匹配的 conventions 模板');
313
+ console.error(` 技术栈: ${JSON.stringify(techStack)}`);
314
+ process.exit(1);
315
+ }
316
+
317
+ // 生成 conventions
318
+ const result = generateConventions(root, techStack, templates);
319
+
320
+ // 更新配置
321
+ updateConfig(root, result.created);
322
+
323
+ // 输出结果
324
+ if (values.json) {
325
+ console.log(JSON.stringify({
326
+ ok: true,
327
+ techStack,
328
+ templates,
329
+ created: result.created,
330
+ skipped: result.skipped,
331
+ conventionsDir,
332
+ }));
333
+ } else {
334
+ console.log('✅ conventions 生成完成');
335
+ console.log(` 技术栈: ${techStack.language} + ${techStack.testFramework}`);
336
+ console.log(` 创建: ${result.created.join(', ')}`);
337
+ if (result.skipped.length > 0) {
338
+ console.log(` 跳过: ${result.skipped.join(', ')}`);
339
+ }
340
+ console.log(` 路径: ${conventionsDir}`);
341
+ }
342
+ }
343
+
344
+ // 直接运行
345
+ if (import.meta.url === `file://${process.argv[1]}`) {
346
+ run(process.argv.slice(2)).catch(err => {
347
+ console.error(err);
348
+ process.exit(1);
349
+ });
350
+ }
@@ -93,6 +93,55 @@ const PARSERS = {
93
93
  pytest: parsePytest,
94
94
  };
95
95
 
96
+ // ── Failure Analysis 分类法(v0.13 §57,来自 glaf4-test analyze-surefire-failures.py)────────
97
+
98
+ /** 测试失败分类:assertion_failure / runtime_error / framework_error / compile_error / dependency_blocker / unknown */
99
+ export function classifyFailure(text) {
100
+ const lower = text.toLowerCase();
101
+
102
+ // 1. 编译错误
103
+ if (lower.includes('compilation error') || lower.includes('cannot find symbol') ||
104
+ lower.includes('incompatible types') || lower.includes('method does not override') ||
105
+ lower.includes('syntaxerror') || lower.includes('indentationerror')) {
106
+ return 'compile_error';
107
+ }
108
+
109
+ // 2. 依赖阻塞
110
+ if (lower.includes('connection refused') || lower.includes('connection timed out') ||
111
+ lower.includes('unknownhostexception') || lower.includes('could not resolve host') ||
112
+ lower.includes('no such file or directory') || lower.includes('module not found') ||
113
+ lower.includes('package does not exist') || lower.includes('dependency') && lower.includes('failed')) {
114
+ return 'dependency_blocker';
115
+ }
116
+
117
+ // 3. 框架错误
118
+ if (lower.includes('applicationcontextexception') || lower.includes('beandefinitionstoreexception') ||
119
+ lower.includes('nosuchbeandefinitionexception') || lower.includes('unsatisfieddependencyexception') ||
120
+ lower.includes('spring') && lower.includes('failed to load') ||
121
+ lower.includes('contextconfiguration') || lower.includes('testcontext')) {
122
+ return 'framework_error';
123
+ }
124
+
125
+ // 4. 运行时异常
126
+ if (lower.includes('nullpointerexception') || lower.includes('illegalargumentexception') ||
127
+ lower.includes('indexoutofboundsexception') || lower.includes('classcastexception') ||
128
+ lower.includes('numberformatexception') || lower.includes('stackoverflowerror') ||
129
+ lower.includes('outofmemoryerror') || lower.includes('runtimeerror') ||
130
+ lower.includes('typeerror') || lower.includes('referenceerror')) {
131
+ return 'runtime_error';
132
+ }
133
+
134
+ // 5. 断言失败
135
+ if (lower.includes('assertionerror') || lower.includes('expected') && lower.includes('but was') ||
136
+ lower.includes('assertionfailederror') || lower.includes('expected :') && lower.includes('actual :') ||
137
+ lower.includes('expect(') || lower.includes('assert') && lower.includes('failed')) {
138
+ return 'assertion_failure';
139
+ }
140
+
141
+ // 6. 未知
142
+ return 'unknown';
143
+ }
144
+
96
145
  /** auto 识别:按内容特征匹配 runner。 */
97
146
  export function detectRunner(text) {
98
147
  const trimmed = text.trim();
@@ -180,7 +229,18 @@ export async function run(args) {
180
229
  // 判定:failed==0 且 total>0 才算 pass(total=0 空真拒绝,v0.13 §50.2)
181
230
  const verdict = stats.failed === 0 && stats.total > 0 ? 'pass' : 'fail';
182
231
  const ts = new Date().toISOString();
183
- const record = `${verdict}: total=${stats.total} passed=${stats.passed} failed=${stats.failed} skipped=${stats.skipped} runner=${runner} recorded-by=tf-test-record ts=${ts}`;
232
+
233
+ // Failure Analysis 分类(v0.13 §57,来自 glaf4-test)
234
+ let failureClass = null;
235
+ if (verdict === 'fail') {
236
+ const rawText = fromStat.isFile()
237
+ ? readFileSync(fromPath, 'utf-8')
238
+ : '';
239
+ failureClass = classifyFailure(rawText);
240
+ }
241
+
242
+ const failureInfo = failureClass ? ` failure_class=${failureClass}` : '';
243
+ const record = `${verdict}: total=${stats.total} passed=${stats.passed} failed=${stats.failed} skipped=${stats.skipped} runner=${runner} recorded-by=tf-test-record ts=${ts}${failureInfo}`;
184
244
 
185
245
  // 原始证据落盘(tests-passing 门禁与 doctor 巡检都要求该文件存在)
186
246
  const evidenceDir = join(changeDir, '.superpowers', 'test-evidence');
@@ -200,11 +260,14 @@ export async function run(args) {
200
260
  writeState(changeDir, state);
201
261
 
202
262
  if (values.json) {
203
- console.log(JSON.stringify({ ok: true, verdict, ...stats, runner, test_result: record, test_evidence_path: evidenceRel }));
263
+ const jsonResult = { ok: true, verdict, ...stats, runner, test_result: record, test_evidence_path: evidenceRel };
264
+ if (failureClass) jsonResult.failure_class = failureClass;
265
+ console.log(JSON.stringify(jsonResult));
204
266
  } else {
205
267
  console.log(`${verdict === 'pass' ? '✅' : '❌'} test_result recorded (${runner}): total=${stats.total} passed=${stats.passed} failed=${stats.failed} skipped=${stats.skipped}`);
206
268
  console.log(` evidence: ${evidenceRel}`);
207
269
  if (verdict === 'fail') {
270
+ console.log(` failure_class: ${failureClass}`);
208
271
  console.log(' closing 将被 tests-passing 门禁阻断:修复失败后重新运行测试套件并再次 tf test record。');
209
272
  }
210
273
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: e2e
3
- description: 从 spec 验收场景生成 Playwright E2E 测试、执行并审计覆盖率(可选 overlay)。用法: /e2e [原型|集成|验收] [spec路径]
3
+ description: "从 spec 验收场景生成 Playwright E2E 测试、执行并审计覆盖率(可选 overlay)。用法: /e2e [原型|集成|验收] [spec路径]"
4
4
  version: 0.1.0
5
5
  user-invocable: true
6
6
  ---
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: test-strategy
3
- description: 测试设计方法论 skill——design_method 选择、分层策略、对抗验证、复杂度分级。build-executor 通过 skills: 预加载。
3
+ description: 测试设计方法论 skill——design_method 选择、分层策略、对抗验证、复杂度分级。build-executor / contract-builder 通过 skills 字段预加载。
4
4
  user-invocable: false
5
5
  ---
6
6
 
@@ -68,3 +68,40 @@ user-invocable: false
68
68
  | **TDD** | 新功能/新行为 | RED→GREEN→REFACTOR |
69
69
  | **CHARACTERIZATION** | 遗留代码行为捕获 | 只写不改(不改生产代码) |
70
70
  | **REGRESSION** | 缺陷复现+修复 | 先复现再修 |
71
+
72
+ ## 8. 组合覆盖声明
73
+
74
+ > 来源:glaf4-test design-worker 的 combination_coverage 机制(v0.13 §55)
75
+
76
+ 当目标方法满足以下条件之一时,矩阵中**必须声明**组合覆盖策略:
77
+
78
+ ### 触发条件
79
+
80
+ 1. **多参数方法**(param_count > 1):声明 `pairwise`,要求至少一个 equivalence/boundary 用例覆盖参数组合
81
+ 2. **有分支逻辑**(if/case/switch):声明 `branch`,要求至少一个 state/path 用例覆盖各分支
82
+
83
+ ### 声明格式
84
+
85
+ 在 test-matrix.md 的 description 列中声明:
86
+
87
+ - `[pairwise] 已覆盖参数组合 A×B, A×C`
88
+ - `[branch] 已覆盖 true/false 分支`
89
+ - `[not_applicable] 单参数无分支,无需组合覆盖`
90
+
91
+ ### 对账规则
92
+
93
+ contract-builder 生成矩阵后,必须校验:
94
+ - 声明 `pairwise` 的目标,必须有至少 1 个 equivalence 或 boundary 用例覆盖参数组合
95
+ - 声明 `branch` 的目标,必须有至少 1 个 state 或 path 用例覆盖各分支
96
+ - 声明 `not_applicable` 的目标,必须提供理由(如"单参数无分支")
97
+
98
+ ### 示例
99
+
100
+ ```markdown
101
+ | ID | work_mode | test_tier | design_method | description | priority |
102
+ |----|-----------|-----------|---------------|-------------|----------|
103
+ | TC-001 | TDD | unit | equivalence | [pairwise] 正常查询:page=1, size=10 | P0 |
104
+ | TC-002 | TDD | unit | boundary | [pairwise] 边界:page=0, size=10 | P1 |
105
+ | TC-003 | TDD | unit | boundary | [pairwise] 边界:page=1, size=0 | P1 |
106
+ | TC-004 | TDD | unit | error | [not_applicable] 单参数无分支,无需组合覆盖 | P1 |
107
+ ```