@xulthekl/team-flow 0.33.0 → 0.34.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.
Files changed (34) 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 +57 -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/docs/README_en.md +1 -1
  15. package/docs/solutions/INDEX.md +1 -0
  16. package/docs/solutions/cross-phase/2026-08-04-no-summary.md +17 -0
  17. package/gemini-extension.json +1 -1
  18. package/hooks/session-start +2 -2
  19. package/llms.txt +1 -1
  20. package/package.json +1 -1
  21. package/plugin.json +1 -1
  22. package/scripts/lib/conventions-generator.mjs +512 -0
  23. package/scripts/lib/test-record.mjs +65 -2
  24. package/skills/test-strategy/SKILL.md +37 -0
  25. package/skills/test-strategy/references/integration-test-contracts.md +237 -0
  26. package/skills/test-strategy/references/integration-test-isolation.md +346 -0
  27. package/skills/test-strategy/references/test-quality-rules.md +292 -0
  28. package/skills/workflow-bootstrap/SKILL.md +40 -3
  29. package/templates/agent-template.md +41 -0
  30. package/templates/conventions/_manifest.json +39 -0
  31. package/templates/conventions/glaf4-compliant/java-testing.md +367 -0
  32. package/templates/conventions/glaf4-compliant/spring-patterns.md +415 -0
  33. package/templates/conventions/js-testing.md +261 -0
  34. package/templates/conventions/python-testing.md +333 -0
@@ -0,0 +1,512 @@
1
+ #!/usr/bin/env node
2
+ // scripts/lib/conventions-generator.mjs — conventions 生成器(v0.34.0 新增,v0.34.1 工作空间支持)
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 §四 + §55.4
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
+ * 扫描工作空间中的所有子项目
105
+ * @param {string} root 工作空间根目录
106
+ * @returns {Array<{ path: string, name: string, techStack: object }>}
107
+ */
108
+ export function scanSubProjects(root) {
109
+ const subProjects = [];
110
+
111
+ // 扫描一级子目录
112
+ const entries = readdirSync(root, { withFileTypes: true });
113
+
114
+ for (const entry of entries) {
115
+ if (!entry.isDirectory()) continue;
116
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
117
+
118
+ const subProjectPath = join(root, entry.name);
119
+ const techStack = detectTechStack(subProjectPath);
120
+
121
+ // 如果检测到技术栈,记录为子项目
122
+ if (techStack.language) {
123
+ subProjects.push({
124
+ path: subProjectPath,
125
+ name: entry.name,
126
+ techStack,
127
+ });
128
+ }
129
+ }
130
+
131
+ return subProjects;
132
+ }
133
+
134
+ /**
135
+ * 汇总所有子项目的技术栈
136
+ * @param {Array<{ path: string, name: string, techStack: object }>} subProjects
137
+ * @returns {{ language: string|null, buildTool: string|null, testFramework: string|null, appFramework: string|null, languages: string[], appFrameworks: string[] }}
138
+ */
139
+ export function aggregateTechStacks(subProjects) {
140
+ const languages = new Set();
141
+ const buildTools = new Set();
142
+ const testFrameworks = new Set();
143
+ const appFrameworks = new Set();
144
+
145
+ for (const { techStack } of subProjects) {
146
+ if (techStack.language) languages.add(techStack.language);
147
+ if (techStack.buildTool) buildTools.add(techStack.buildTool);
148
+ if (techStack.testFramework) testFrameworks.add(techStack.testFramework);
149
+ if (techStack.appFramework) appFrameworks.add(techStack.appFramework);
150
+ }
151
+
152
+ return {
153
+ // 主技术栈(第一个子项目)
154
+ language: subProjects[0]?.techStack.language || null,
155
+ buildTool: subProjects[0]?.techStack.buildTool || null,
156
+ testFramework: subProjects[0]?.techStack.testFramework || null,
157
+ appFramework: subProjects[0]?.techStack.appFramework || null,
158
+ // 汇总列表
159
+ languages: [...languages],
160
+ appFrameworks: [...appFrameworks],
161
+ };
162
+ }
163
+
164
+ /**
165
+ * 为工作空间选择 conventions 模板
166
+ * @param {{ languages: string[], appFrameworks: string[] }} aggregatedTechStack
167
+ * @returns {string[]} 模板文件名列表
168
+ */
169
+ export function selectTemplatesForWorkspace(aggregatedTechStack) {
170
+ const templates = [];
171
+
172
+ // Java 相关
173
+ if (aggregatedTechStack.languages.includes('java')) {
174
+ if (existsSync(join(GLAF4_TEMPLATE_DIR, 'java-testing.md'))) {
175
+ templates.push('glaf4-compliant/java-testing.md');
176
+ }
177
+ if (aggregatedTechStack.appFrameworks.includes('spring-boot')) {
178
+ if (existsSync(join(GLAF4_TEMPLATE_DIR, 'spring-patterns.md'))) {
179
+ templates.push('glaf4-compliant/spring-patterns.md');
180
+ }
181
+ }
182
+ }
183
+
184
+ // JavaScript 相关
185
+ if (aggregatedTechStack.languages.includes('javascript')) {
186
+ templates.push('js-testing.md');
187
+ }
188
+
189
+ // Python 相关
190
+ if (aggregatedTechStack.languages.includes('python')) {
191
+ templates.push('python-testing.md');
192
+ }
193
+
194
+ return templates;
195
+ }
196
+
197
+ // ── 模板选择 ─────────────────────────────────────────────────────────────
198
+
199
+ /**
200
+ * 根据技术栈选择 conventions 模板
201
+ * @param {{ language: string|null, buildTool: string|null, testFramework: string|null, appFramework: string|null }} techStack
202
+ * @returns {string[]} 模板文件名列表
203
+ */
204
+ export function selectTemplates(techStack) {
205
+ const templates = [];
206
+
207
+ if (techStack.language === 'java') {
208
+ // Java 项目:使用 glaf4-compliant 模板
209
+ if (existsSync(join(GLAF4_TEMPLATE_DIR, 'java-testing.md'))) {
210
+ templates.push('glaf4-compliant/java-testing.md');
211
+ }
212
+ if (techStack.appFramework === 'spring-boot') {
213
+ if (existsSync(join(GLAF4_TEMPLATE_DIR, 'spring-patterns.md'))) {
214
+ templates.push('glaf4-compliant/spring-patterns.md');
215
+ }
216
+ }
217
+ } else if (techStack.language === 'javascript') {
218
+ templates.push('js-testing.md');
219
+ } else if (techStack.language === 'python') {
220
+ templates.push('python-testing.md');
221
+ }
222
+
223
+ return templates;
224
+ }
225
+
226
+ // ── conventions 生成 ─────────────────────────────────────────────────────
227
+
228
+ /**
229
+ * 生成 conventions 文件
230
+ * @param {string} root 项目根目录
231
+ * @param {{ language: string|null, buildTool: string|null, testFramework: string|null, appFramework: string|null }} techStack
232
+ * @param {string[]} templates 模板文件名列表
233
+ * @returns {{ created: string[], skipped: string[] }}
234
+ */
235
+ export function generateConventions(root, techStack, templates) {
236
+ const conventionsDir = join(root, '.team-flow', 'conventions');
237
+ const versionsFile = join(conventionsDir, '.versions.json');
238
+
239
+ // 创建目录
240
+ mkdirSync(conventionsDir, { recursive: true });
241
+
242
+ // 读取现有版本信息
243
+ let versions = {};
244
+ if (existsSync(versionsFile)) {
245
+ try {
246
+ versions = JSON.parse(readFileSync(versionsFile, 'utf-8'));
247
+ } catch {
248
+ versions = {};
249
+ }
250
+ }
251
+ if (!versions.conventions) versions.conventions = {};
252
+
253
+ const created = [];
254
+ const skipped = [];
255
+
256
+ for (const template of templates) {
257
+ const templatePath = join(TEMPLATE_DIR, template);
258
+ const fileName = path.basename(template);
259
+ const targetPath = join(conventionsDir, fileName);
260
+
261
+ // 检查是否已存在且未自定义
262
+ if (existsSync(targetPath)) {
263
+ const existingVersion = versions.conventions[fileName];
264
+ if (existingVersion && !existingVersion.customized) {
265
+ // 读取模板版本
266
+ const templateContent = readFileSync(templatePath, 'utf-8');
267
+ const versionMatch = templateContent.match(/version:\s*(\d+\.\d+\.\d+)/);
268
+ const templateVersion = versionMatch ? versionMatch[1] : '1.0.0';
269
+
270
+ if (existingVersion.source_version === templateVersion) {
271
+ skipped.push(fileName);
272
+ continue;
273
+ }
274
+ }
275
+ }
276
+
277
+ // 复制模板
278
+ const templateContent = readFileSync(templatePath, 'utf-8');
279
+ writeFileSync(targetPath, templateContent, 'utf-8');
280
+
281
+ // 更新版本信息
282
+ const versionMatch = templateContent.match(/version:\s*(\d+\.\d+\.\d+)/);
283
+ const templateVersion = versionMatch ? versionMatch[1] : '1.0.0';
284
+
285
+ versions.conventions[fileName] = {
286
+ source_version: templateVersion,
287
+ installed_at: new Date().toISOString().split('T')[0],
288
+ customized: false,
289
+ last_checked: new Date().toISOString().split('T')[0],
290
+ source: template,
291
+ };
292
+
293
+ created.push(fileName);
294
+ }
295
+
296
+ // 写入版本信息
297
+ writeFileSync(versionsFile, JSON.stringify(versions, null, 2), 'utf-8');
298
+
299
+ return { created, skipped };
300
+ }
301
+
302
+ // ── 更新 team-flow.config.json ─────────────────────────────────────────
303
+
304
+ /**
305
+ * 更新 team-flow.config.json 的 conventions 字段
306
+ * @param {string} root 项目根目录
307
+ * @param {string[]} conventionsFiles conventions 文件名列表
308
+ */
309
+ export function updateConfig(root, conventionsFiles) {
310
+ const teamFlowDir = join(root, '.team-flow');
311
+ const configPath = join(teamFlowDir, 'team-flow.config.json');
312
+
313
+ // 确保目录存在
314
+ mkdirSync(teamFlowDir, { recursive: true });
315
+
316
+ let config = {};
317
+ if (existsSync(configPath)) {
318
+ try {
319
+ config = JSON.parse(readFileSync(configPath, 'utf-8'));
320
+ } catch {
321
+ config = {};
322
+ }
323
+ }
324
+
325
+ if (!config.conventions) config.conventions = {};
326
+
327
+ for (const file of conventionsFiles) {
328
+ const key = file.replace(/\.md$/, '').replace(/\//g, '-');
329
+ config.conventions[key] = `.team-flow/conventions/${file}`;
330
+ }
331
+
332
+ writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
333
+ }
334
+
335
+ // ── 交互式引导 ─────────────────────────────────────────────────────────────
336
+
337
+ /**
338
+ * 交互式引导用户选择技术栈(全新项目)
339
+ * @returns {Promise<{ language: string, buildTool: string, testFramework: string, appFramework: string }>}
340
+ */
341
+ export async function interactiveSetup() {
342
+ // 注意:在实际使用中,这里需要使用 readline 或 inquirer 等库
343
+ // 这里提供默认值,实际实现需要集成到 workflow-bootstrap 的交互流程中
344
+ console.log('\n=== 全新项目技术栈配置 ===\n');
345
+ console.log('由于当前为脚本模式,使用默认配置:');
346
+ console.log(' - 编程语言:Java');
347
+ console.log(' - 构建工具:Maven');
348
+ console.log(' - 测试框架:JUnit 5 + Mockito');
349
+ console.log(' - 应用框架:Spring Boot');
350
+ console.log('\n如需自定义配置,请在 workflow-bootstrap 中使用交互式模式。\n');
351
+
352
+ return {
353
+ language: 'java',
354
+ buildTool: 'maven',
355
+ testFramework: 'junit5-mockito',
356
+ appFramework: 'spring-boot',
357
+ };
358
+ }
359
+
360
+ // ── CLI ─────────────────────────────────────────────────────────────────────
361
+
362
+ export async function run(args) {
363
+ const { positionals, values } = parseArgs({
364
+ args,
365
+ options: {
366
+ root: { type: 'string', default: '.' },
367
+ interactive: { type: 'boolean', default: false },
368
+ json: { type: 'boolean', default: false },
369
+ },
370
+ allowPositionals: true,
371
+ });
372
+
373
+ const root = path.resolve(values.root);
374
+
375
+ // 检查项目根目录
376
+ if (!existsSync(root)) {
377
+ console.error(`项目根目录不存在: ${root}`);
378
+ process.exit(1);
379
+ }
380
+
381
+ // 检查 conventions 是否已存在
382
+ const conventionsDir = join(root, '.team-flow', 'conventions');
383
+ if (existsSync(conventionsDir) && readdirSync(conventionsDir).length > 0) {
384
+ console.log('⚠️ conventions 已存在,跳过生成');
385
+ console.log(` 路径: ${conventionsDir}`);
386
+ process.exit(0);
387
+ }
388
+
389
+ // 检测是否为工作空间(包含多个子项目)
390
+ const subProjects = scanSubProjects(root);
391
+
392
+ if (subProjects.length > 1) {
393
+ // 工作空间模式:扫描所有子项目
394
+ console.log(`\n=== 检测到工作空间(${subProjects.length} 个子项目)===\n`);
395
+
396
+ for (const { name, techStack } of subProjects) {
397
+ console.log(` - ${name}: ${techStack.language}`);
398
+ }
399
+
400
+ // 汇总技术栈
401
+ const aggregatedTechStack = aggregateTechStacks(subProjects);
402
+
403
+ // 选择模板
404
+ const templates = selectTemplatesForWorkspace(aggregatedTechStack);
405
+
406
+ if (templates.length === 0) {
407
+ console.error('❌ 未找到匹配的 conventions 模板');
408
+ console.error(` 检测到的语言: ${aggregatedTechStack.languages.join(', ')}`);
409
+ process.exit(1);
410
+ }
411
+
412
+ // 生成 conventions
413
+ const result = generateConventions(root, aggregatedTechStack, templates);
414
+
415
+ // 更新配置
416
+ updateConfig(root, result.created);
417
+
418
+ // 输出结果
419
+ if (values.json) {
420
+ console.log(JSON.stringify({
421
+ ok: true,
422
+ mode: 'workspace',
423
+ subProjects: subProjects.map(p => ({ name: p.name, language: p.techStack.language })),
424
+ aggregatedTechStack: {
425
+ languages: aggregatedTechStack.languages,
426
+ appFrameworks: aggregatedTechStack.appFrameworks,
427
+ },
428
+ templates,
429
+ created: result.created,
430
+ skipped: result.skipped,
431
+ conventionsDir,
432
+ }));
433
+ } else {
434
+ console.log('\n✅ 工作空间级 conventions 生成完成');
435
+ console.log(` 检测到的语言:${aggregatedTechStack.languages.join(', ')}`);
436
+ console.log(` 创建:${result.created.join(', ')}`);
437
+ if (result.skipped.length > 0) {
438
+ console.log(` 跳过:${result.skipped.join(', ')}`);
439
+ }
440
+ console.log(` 路径:${conventionsDir}`);
441
+ }
442
+
443
+ } else {
444
+ // 单项目模式
445
+ let techStack;
446
+
447
+ if (subProjects.length === 1) {
448
+ // 从子项目检测
449
+ techStack = subProjects[0].techStack;
450
+ console.log(`\n=== 检测到单项目:${subProjects[0].name} ===\n`);
451
+ } else {
452
+ // 从根目录检测
453
+ techStack = detectTechStack(root);
454
+ }
455
+
456
+ // 如果未检测到技术栈且启用交互式模式
457
+ if (!techStack.language && values.interactive) {
458
+ techStack = await interactiveSetup();
459
+ }
460
+
461
+ // 如果仍未检测到技术栈
462
+ if (!techStack.language) {
463
+ console.error('❌ 未检测到技术栈特征');
464
+ console.error(' 请指定项目根目录,或使用 --interactive 模式手动配置');
465
+ process.exit(1);
466
+ }
467
+
468
+ // 选择模板
469
+ const templates = selectTemplates(techStack);
470
+
471
+ if (templates.length === 0) {
472
+ console.error('❌ 未找到匹配的 conventions 模板');
473
+ console.error(` 技术栈: ${JSON.stringify(techStack)}`);
474
+ process.exit(1);
475
+ }
476
+
477
+ // 生成 conventions
478
+ const result = generateConventions(root, techStack, templates);
479
+
480
+ // 更新配置
481
+ updateConfig(root, result.created);
482
+
483
+ // 输出结果
484
+ if (values.json) {
485
+ console.log(JSON.stringify({
486
+ ok: true,
487
+ mode: 'single',
488
+ techStack,
489
+ templates,
490
+ created: result.created,
491
+ skipped: result.skipped,
492
+ conventionsDir,
493
+ }));
494
+ } else {
495
+ console.log('✅ conventions 生成完成');
496
+ console.log(` 技术栈: ${techStack.language} + ${techStack.testFramework}`);
497
+ console.log(` 创建: ${result.created.join(', ')}`);
498
+ if (result.skipped.length > 0) {
499
+ console.log(` 跳过: ${result.skipped.join(', ')}`);
500
+ }
501
+ console.log(` 路径: ${conventionsDir}`);
502
+ }
503
+ }
504
+ }
505
+
506
+ // 直接运行
507
+ if (import.meta.url === `file://${process.argv[1]}`) {
508
+ run(process.argv.slice(2)).catch(err => {
509
+ console.error(err);
510
+ process.exit(1);
511
+ });
512
+ }
@@ -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
  }
@@ -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
+ ```