@xulthekl/team-flow 0.37.0 → 0.38.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.
- package/.claude/always/phase-guard.md +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/marketplace.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.github/plugin/marketplace.json +2 -2
- package/CHANGELOG.md +27 -0
- package/GEMINI.md +1 -1
- package/INSTALL.md +1 -1
- package/README.md +1 -1
- package/agents/architecture-reviewer.md +16 -6
- package/agents/change-split-auditor.md +11 -6
- package/agents/code-reviewer.md +7 -6
- package/agents/cross-change-consistency-checker.md +11 -6
- package/agents/prd-completeness-reviewer.md +11 -6
- package/agents/prototype-reviewer.md +11 -6
- package/docs/README_en.md +1 -1
- package/docs/solutions/INDEX.md +1 -0
- package/docs/solutions/cross-phase/2026-08-06-no-summary.md +17 -0
- package/gemini-extension.json +1 -1
- package/hooks/session-start +2 -2
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/lib/test-matrix-export.mjs +5 -0
- package/scripts/lib/test-merge.mjs +130 -93
- package/skills/build-executor/implementer-prompt.md +3 -1
- package/skills/ce-brainstorm/references/prototype-loop.md +2 -2
- package/skills/code-reviewer/SKILL.md +4 -3
- package/skills/code-reviewer/code-reviewer-prompt.md +2 -2
- package/skills/contract-builder/SKILL.md +6 -4
- package/skills/prototype/SKILL.md +3 -3
- package/skills/prototype/references/orchestration-flow.md +2 -2
- package/skills/release-archivist/SKILL.md +16 -6
- package/skills/spec-writer/SKILL.md +1 -1
- package/skills/test-strategy/SKILL.md +2 -1
- package/skills/test-strategy/references/design-methods-detail.md +1 -0
- package/skills/test-strategy/references/test-quality-rules.md +1 -0
- package/skills/workflow-orchestrator/references/s2-prd-prototype-loop.md +2 -2
- package/skills/workflow-start/SKILL.md +1 -1
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
* 6. gitCommit — 单次原子提交
|
|
15
15
|
*
|
|
16
16
|
* 回写顺序:arch-merge → prototype-sync → test-merge → compound promotion
|
|
17
|
+
*
|
|
18
|
+
* v0.38.0(feedback 2026-08-05 修复 + E2E 层级):
|
|
19
|
+
* - resolveDeferred 只删 Deferred Items 段内被覆盖行(原全文件 regex 误删 Current Cases)
|
|
20
|
+
* - extractSummary 容忍 markdown bold(**87**)+ 识别 E2E cases 行
|
|
21
|
+
* - mergeExistingBaseline 覆盖式语义修正(原 startsWith('') 恒真导致旧行累积)
|
|
22
|
+
* - cells 提取按索引保留空列(原 filter(Boolean) 丢空列错位)
|
|
23
|
+
* - dry-run 真正跳过写盘(原仅 gitCommit 生效)
|
|
24
|
+
* - rewriteIndex 统计 test_tier=e2e 层级(unit/integration/e2e 三栏)
|
|
17
25
|
*/
|
|
18
26
|
|
|
19
27
|
import { readFileSync, writeFileSync, existsSync, cpSync, mkdirSync, readdirSync } from 'node:fs';
|
|
@@ -61,20 +69,24 @@ function preCheck(changeDir) {
|
|
|
61
69
|
|
|
62
70
|
/**
|
|
63
71
|
* 从 test-matrix.md 提取 Summary 统计信息
|
|
72
|
+
* 容忍 markdown bold(`**87**`)与普通数字两种格式(feedback 2026-08-05:C1 矩阵 LLM 手写加粗导致返回 0)
|
|
73
|
+
* 支持 E2E cases 行(v0.38.0:test_tier=e2e 记录,与复利 E2E 层级联动)
|
|
64
74
|
*/
|
|
65
|
-
function extractSummary(content) {
|
|
66
|
-
const summary = { totalCases: 0, modules: 0, unitCases: 0, integrationCases: 0, deferred: 0 };
|
|
75
|
+
export function extractSummary(content) {
|
|
76
|
+
const summary = { totalCases: 0, modules: 0, unitCases: 0, integrationCases: 0, e2eCases: 0, deferred: 0 };
|
|
67
77
|
const lines = content.split('\n');
|
|
68
78
|
for (const line of lines) {
|
|
69
|
-
const totalMatch = line.match(/Total cases:\s*(\d+)/i);
|
|
79
|
+
const totalMatch = line.match(/Total cases:\s*\*{0,2}\s*(\d+)/i);
|
|
70
80
|
if (totalMatch) summary.totalCases = parseInt(totalMatch[1], 10);
|
|
71
|
-
const modulesMatch = line.match(/Modules covered:\s*(\d+)/i);
|
|
81
|
+
const modulesMatch = line.match(/Modules covered:\s*\*{0,2}\s*(\d+)/i);
|
|
72
82
|
if (modulesMatch) summary.modules = parseInt(modulesMatch[1], 10);
|
|
73
|
-
const unitMatch = line.match(/Unit cases:\s*(\d+)/i);
|
|
83
|
+
const unitMatch = line.match(/Unit cases:\s*\*{0,2}\s*(\d+)/i);
|
|
74
84
|
if (unitMatch) summary.unitCases = parseInt(unitMatch[1], 10);
|
|
75
|
-
const intMatch = line.match(/Integration cases:\s*(\d+)/i);
|
|
85
|
+
const intMatch = line.match(/Integration cases:\s*\*{0,2}\s*(\d+)/i);
|
|
76
86
|
if (intMatch) summary.integrationCases = parseInt(intMatch[1], 10);
|
|
77
|
-
const
|
|
87
|
+
const e2eMatch = line.match(/E2E cases:\s*\*{0,2}\s*(\d+)/i);
|
|
88
|
+
if (e2eMatch) summary.e2eCases = parseInt(e2eMatch[1], 10);
|
|
89
|
+
const defMatch = line.match(/Deferred items:\s*\*{0,2}\s*(\d+)/i);
|
|
78
90
|
if (defMatch) summary.deferred = parseInt(defMatch[1], 10);
|
|
79
91
|
}
|
|
80
92
|
return summary;
|
|
@@ -173,10 +185,11 @@ function extractDeferredItems(content) {
|
|
|
173
185
|
|
|
174
186
|
/**
|
|
175
187
|
* Step 2: mergeBaselines — 增量合并到 baselines/{module}.md
|
|
188
|
+
* dryRun(feedback 2026-08-05):仅计算不写盘,返回 would-create/would-update 预览
|
|
176
189
|
*/
|
|
177
|
-
function mergeBaselines(ledgerDir, changeName, moduleSections, candidateLedger, deferredItems) {
|
|
190
|
+
export function mergeBaselines(ledgerDir, changeName, moduleSections, candidateLedger, deferredItems, dryRun) {
|
|
178
191
|
const baselinesDir = join(ledgerDir, 'baselines');
|
|
179
|
-
if (!existsSync(baselinesDir)) {
|
|
192
|
+
if (!existsSync(baselinesDir) && !dryRun) {
|
|
180
193
|
mkdirSync(baselinesDir, { recursive: true });
|
|
181
194
|
}
|
|
182
195
|
|
|
@@ -198,13 +211,13 @@ function mergeBaselines(ledgerDir, changeName, moduleSections, candidateLedger,
|
|
|
198
211
|
const existing = readFileSync(baselinePath, 'utf-8');
|
|
199
212
|
const updated = mergeExistingBaseline(existing, sectionContent, changeName, today, moduleCandidates, moduleDeferred);
|
|
200
213
|
if (!updated) continue; // no changes
|
|
201
|
-
writeFileSync(baselinePath, updated, 'utf-8');
|
|
202
|
-
results[moduleName] = 'updated';
|
|
214
|
+
if (!dryRun) writeFileSync(baselinePath, updated, 'utf-8');
|
|
215
|
+
results[moduleName] = dryRun ? 'would-update' : 'updated';
|
|
203
216
|
} else {
|
|
204
217
|
// 新建 baseline
|
|
205
218
|
const newBaseline = createNewBaseline(moduleName, sectionContent, changeName, today, moduleCandidates, moduleDeferred);
|
|
206
|
-
writeFileSync(baselinePath, newBaseline, 'utf-8');
|
|
207
|
-
results[moduleName] = 'created';
|
|
219
|
+
if (!dryRun) writeFileSync(baselinePath, newBaseline, 'utf-8');
|
|
220
|
+
results[moduleName] = dryRun ? 'would-create' : 'created';
|
|
208
221
|
}
|
|
209
222
|
}
|
|
210
223
|
return results;
|
|
@@ -213,7 +226,7 @@ function mergeBaselines(ledgerDir, changeName, moduleSections, candidateLedger,
|
|
|
213
226
|
/**
|
|
214
227
|
* 创建新 baseline 文件
|
|
215
228
|
*/
|
|
216
|
-
function createNewBaseline(moduleName, sectionContent, changeName, date, candidates, deferred) {
|
|
229
|
+
export function createNewBaseline(moduleName, sectionContent, changeName, date, candidates, deferred) {
|
|
217
230
|
const lines = [];
|
|
218
231
|
lines.push(`# ${moduleName} Test Baseline`);
|
|
219
232
|
lines.push('');
|
|
@@ -227,12 +240,13 @@ function createNewBaseline(moduleName, sectionContent, changeName, date, candida
|
|
|
227
240
|
lines.push('|---|---|---|---|---|---|---|---|---|');
|
|
228
241
|
|
|
229
242
|
// 从 sectionContent 提取 case 行并添加 source_change 列
|
|
243
|
+
// 空列保留(slice(1,-1) 去首尾空串),避免 filter(Boolean) 丢空列导致索引左移(feedback 2026-08-05)
|
|
230
244
|
const caseLines = sectionContent.split('\n').filter(l =>
|
|
231
245
|
l.startsWith('|') && !l.match(/^\|\s*[-:]+/) && !l.match(/\|\s*case_id/)
|
|
232
246
|
);
|
|
233
247
|
for (const line of caseLines) {
|
|
234
|
-
const cells = line.split('|').map(c => c.trim())
|
|
235
|
-
if (cells.length >=
|
|
248
|
+
const cells = line.split('|').slice(1, -1).map(c => c.trim());
|
|
249
|
+
if (cells.length >= 11) {
|
|
236
250
|
lines.push(`| ${cells[0]} | ${cells[1]} | ${cells[2]} | ${cells[5]} | ${cells[6]} | ${cells[8]} | ${cells[9]} | ${cells[10]} | ${changeName} |`);
|
|
237
251
|
}
|
|
238
252
|
}
|
|
@@ -258,57 +272,55 @@ function createNewBaseline(moduleName, sectionContent, changeName, date, candida
|
|
|
258
272
|
}
|
|
259
273
|
|
|
260
274
|
/**
|
|
261
|
-
* 合并现有 baseline
|
|
275
|
+
* 合并现有 baseline(Current Cases 覆盖式替换 + 追加 Evolution Log)
|
|
276
|
+
*
|
|
277
|
+
* feedback 2026-08-05 修复:
|
|
278
|
+
* 1. 原 `inFrontmatter && (line.startsWith('## ') || line.startsWith(''))` 中 startsWith('') 恒真 →
|
|
279
|
+
* Current Cases 旧行不被跳过而累积(覆盖式语义失效)。改为仅在 `## ` 标题退出。
|
|
280
|
+
* 2. cells 提取改用 slice(1,-1) 保留空列(filter(Boolean) 丢空列导致索引左移错位)。
|
|
281
|
+
* 3. Evolution Log 幂等:同 change 同日已记录则跳过 append。
|
|
262
282
|
*/
|
|
263
|
-
function mergeExistingBaseline(existing, sectionContent, changeName, date, candidates, deferred) {
|
|
283
|
+
export function mergeExistingBaseline(existing, sectionContent, changeName, date, candidates, deferred) {
|
|
264
284
|
const lines = [];
|
|
265
285
|
const existingLines = existing.split('\n');
|
|
286
|
+
const caseLines = sectionContent.split('\n').filter(l =>
|
|
287
|
+
l.startsWith('|') && !l.match(/^\|\s*[-:]+/) && !l.match(/\|\s*case_id/)
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const renderCaseRow = (line) => {
|
|
291
|
+
const cells = line.split('|').slice(1, -1).map(c => c.trim());
|
|
292
|
+
if (cells.length < 11) return null;
|
|
293
|
+
return `| ${cells[0]} | ${cells[1]} | ${cells[2]} | ${cells[5]} | ${cells[6]} | ${cells[8]} | ${cells[9]} | ${cells[10]} | ${changeName} |`;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
let inCases = false; // Current Cases 段内 → 跳过旧行(覆盖式)
|
|
297
|
+
const logPattern = new RegExp(`\\*\\*${date}\\*\\* \\[${changeName}\\]:`);
|
|
266
298
|
|
|
267
|
-
// 更新 frontmatter
|
|
268
|
-
let inFrontmatter = false;
|
|
269
|
-
let frontmatterDone = false;
|
|
270
299
|
for (const line of existingLines) {
|
|
271
300
|
if (line.startsWith('> last_updated_by_change:')) {
|
|
272
301
|
lines.push(`> last_updated_by_change: ${changeName}`);
|
|
273
302
|
} else if (line.startsWith('> last_updated:')) {
|
|
274
303
|
lines.push(`> last_updated: ${date}`);
|
|
275
304
|
} else if (line.startsWith('## Current Cases')) {
|
|
276
|
-
|
|
277
|
-
lines.push(line);
|
|
278
|
-
lines.push('');
|
|
279
|
-
lines.push('| case_id | behavior | design_method | test_kind | test_tier | work_mode | test_file | test_method_name | source_change |');
|
|
280
|
-
lines.push('|---|---|---|---|---|---|---|---|---|');
|
|
281
|
-
inFrontmatter = true;
|
|
282
|
-
// 跳过旧表格
|
|
283
|
-
continue;
|
|
284
|
-
} else if (inFrontmatter && (line.startsWith('## ') || line.startsWith(''))) {
|
|
285
|
-
inFrontmatter = false;
|
|
286
|
-
// 插入新的 case 行
|
|
287
|
-
const caseLines = sectionContent.split('\n').filter(l =>
|
|
288
|
-
l.startsWith('|') && !l.match(/^\|\s*[-:]+/) && !l.match(/\|\s*case_id/)
|
|
289
|
-
);
|
|
305
|
+
lines.push(line, '', '| case_id | behavior | design_method | test_kind | test_tier | work_mode | test_file | test_method_name | source_change |', '|---|---|---|---|---|---|---|---|---|');
|
|
290
306
|
for (const cl of caseLines) {
|
|
291
|
-
const
|
|
292
|
-
if (
|
|
293
|
-
lines.push(`| ${cells[0]} | ${cells[1]} | ${cells[2]} | ${cells[5]} | ${cells[6]} | ${cells[8]} | ${cells[9]} | ${cells[10]} | ${changeName} |`);
|
|
294
|
-
}
|
|
307
|
+
const row = renderCaseRow(cl);
|
|
308
|
+
if (row) lines.push(row);
|
|
295
309
|
}
|
|
296
|
-
|
|
297
|
-
} else if (inFrontmatter && line.startsWith('|')) {
|
|
298
|
-
// 跳过旧表格行(已替换)
|
|
299
|
-
continue;
|
|
310
|
+
inCases = true;
|
|
300
311
|
} else if (line.startsWith('## Evolution Log')) {
|
|
312
|
+
// 优先于 inCases 退出分支:Evolution Log 标题也以 ## 开头,需先处理 append
|
|
313
|
+
inCases = false;
|
|
314
|
+
lines.push(line, '');
|
|
315
|
+
if (!existingLines.some(l => logPattern.test(l))) {
|
|
316
|
+
lines.push(`- **${date}** [${changeName}]: 新增 ${caseLines.length} case, defer ${deferred.length}`);
|
|
317
|
+
}
|
|
318
|
+
} else if (inCases && line.startsWith('## ')) {
|
|
319
|
+
inCases = false;
|
|
301
320
|
lines.push(line);
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
).length;
|
|
306
|
-
lines.push(`- **${date}** [${changeName}]: 新增 ${caseCount} case, defer ${deferred.length}`);
|
|
307
|
-
frontmatterDone = true;
|
|
308
|
-
} else if (frontmatterDone && line.startsWith('- **')) {
|
|
309
|
-
// 保留旧的 evolution log entries
|
|
310
|
-
lines.push(line);
|
|
311
|
-
} else if (!inFrontmatter) {
|
|
321
|
+
} else if (inCases) {
|
|
322
|
+
continue; // 跳过 Current Cases 段内旧表行/分隔行/空行
|
|
323
|
+
} else {
|
|
312
324
|
lines.push(line);
|
|
313
325
|
}
|
|
314
326
|
}
|
|
@@ -318,8 +330,12 @@ function mergeExistingBaseline(existing, sectionContent, changeName, date, candi
|
|
|
318
330
|
|
|
319
331
|
/**
|
|
320
332
|
* Step 3: resolveDeferred — 新 case 覆盖了旧 deferred 项
|
|
333
|
+
*
|
|
334
|
+
* feedback 2026-08-05 修复:原实现用全文件 regex `\|<caseId>\|[^\n]*\n` 删除匹配行,
|
|
335
|
+
* 把 baselines `Current Cases` 表的正常 case 行也一并误删(表体清空、Evolution Log 计数保留)。
|
|
336
|
+
* 改为逐行扫描,仅删除 `## Deferred Items` 段(到下一个 `## ` 标题之间)内被新 case 覆盖的行。
|
|
321
337
|
*/
|
|
322
|
-
function resolveDeferred(ledgerDir, changeName, newCaseIds) {
|
|
338
|
+
export function resolveDeferred(ledgerDir, changeName, newCaseIds, dryRun) {
|
|
323
339
|
const baselinesDir = join(ledgerDir, 'baselines');
|
|
324
340
|
if (!existsSync(baselinesDir)) return { resolved: 0 };
|
|
325
341
|
|
|
@@ -327,47 +343,58 @@ function resolveDeferred(ledgerDir, changeName, newCaseIds) {
|
|
|
327
343
|
for (const file of readdirSync(baselinesDir)) {
|
|
328
344
|
if (!file.endsWith('.md')) continue;
|
|
329
345
|
const fp = join(baselinesDir, file);
|
|
330
|
-
|
|
331
|
-
|
|
346
|
+
const content = readFileSync(fp, 'utf-8');
|
|
347
|
+
const lines = content.split('\n');
|
|
332
348
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
349
|
+
let inDeferred = false;
|
|
350
|
+
const kept = [];
|
|
351
|
+
let changed = false;
|
|
352
|
+
for (const line of lines) {
|
|
353
|
+
if (line.startsWith('## Deferred Items')) {
|
|
354
|
+
inDeferred = true;
|
|
355
|
+
kept.push(line);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (inDeferred && line.startsWith('## ')) {
|
|
359
|
+
inDeferred = false;
|
|
360
|
+
kept.push(line);
|
|
361
|
+
continue;
|
|
340
362
|
}
|
|
363
|
+
if (inDeferred && line.startsWith('|') && !line.match(/^\|\s*[-:]/)) {
|
|
364
|
+
const cells = line.split('|').slice(1, -1).map(c => c.trim());
|
|
365
|
+
if (cells.length && newCaseIds.includes(cells[0])) {
|
|
366
|
+
changed = true;
|
|
367
|
+
resolved++;
|
|
368
|
+
continue; // 被新 case 覆盖 → 删除该 deferred 行
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
kept.push(line);
|
|
341
372
|
}
|
|
342
373
|
|
|
343
|
-
if (changed) {
|
|
344
|
-
writeFileSync(fp,
|
|
374
|
+
if (changed && !dryRun) {
|
|
375
|
+
writeFileSync(fp, kept.join('\n'), 'utf-8');
|
|
345
376
|
}
|
|
346
377
|
}
|
|
347
378
|
return { resolved };
|
|
348
379
|
}
|
|
349
380
|
|
|
350
|
-
function escapeRegex(str) {
|
|
351
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
352
|
-
}
|
|
353
|
-
|
|
354
381
|
/**
|
|
355
382
|
* Step 4: appendChangelog — 归档 test-matrix.md
|
|
356
383
|
*/
|
|
357
|
-
function appendChangelog(ledgerDir, changeName, matrixContent) {
|
|
384
|
+
export function appendChangelog(ledgerDir, changeName, matrixContent, dryRun) {
|
|
358
385
|
const changelogDir = join(ledgerDir, 'changelog');
|
|
359
|
-
if (!existsSync(changelogDir)) {
|
|
386
|
+
if (!existsSync(changelogDir) && !dryRun) {
|
|
360
387
|
mkdirSync(changelogDir, { recursive: true });
|
|
361
388
|
}
|
|
362
389
|
const destPath = join(changelogDir, `${changeName}.md`);
|
|
363
|
-
writeFileSync(destPath, matrixContent, 'utf-8');
|
|
390
|
+
if (!dryRun) writeFileSync(destPath, matrixContent, 'utf-8');
|
|
364
391
|
return destPath;
|
|
365
392
|
}
|
|
366
393
|
|
|
367
394
|
/**
|
|
368
395
|
* Step 5: rewriteIndex — 重写 INDEX.md
|
|
369
396
|
*/
|
|
370
|
-
function rewriteIndex(ledgerDir) {
|
|
397
|
+
export function rewriteIndex(ledgerDir, dryRun) {
|
|
371
398
|
const baselinesDir = join(ledgerDir, 'baselines');
|
|
372
399
|
const indexPath = join(ledgerDir, 'INDEX.md');
|
|
373
400
|
const today = new Date().toISOString().slice(0, 10);
|
|
@@ -399,11 +426,12 @@ function rewriteIndex(ledgerDir) {
|
|
|
399
426
|
if (deferredCount > 0) status = '⚠️ partial';
|
|
400
427
|
if (caseCount === 0) status = '⏭️ not_applicable';
|
|
401
428
|
|
|
402
|
-
// test_tier_breakdown
|
|
403
|
-
const unitCount = (content.match(/
|
|
404
|
-
const intCount = (content.match(/
|
|
429
|
+
// test_tier_breakdown(v0.38.0:补 e2e 层级;\b 词边界防误匹配 community 等)
|
|
430
|
+
const unitCount = (content.match(/\bunit\b/gi) || []).length;
|
|
431
|
+
const intCount = (content.match(/\bintegration\b/gi) || []).length;
|
|
432
|
+
const e2eCount = (content.match(/\be2e\b/gi) || []).length;
|
|
405
433
|
|
|
406
|
-
modules.push({ moduleName, caseCount, deferredCount, lastChange, lastDate, status, unitCount, intCount });
|
|
434
|
+
modules.push({ moduleName, caseCount, deferredCount, lastChange, lastDate, status, unitCount, intCount, e2eCount });
|
|
407
435
|
}
|
|
408
436
|
}
|
|
409
437
|
|
|
@@ -421,10 +449,10 @@ function rewriteIndex(ledgerDir) {
|
|
|
421
449
|
lines.push('|---|---|---|---|---|---|---|');
|
|
422
450
|
|
|
423
451
|
for (const m of modules) {
|
|
424
|
-
lines.push(`| ${m.moduleName} | unit:${m.unitCount}, integration:${m.intCount} | ${m.caseCount} | ${m.deferredCount} | ${m.lastChange} | ${m.lastDate} | ${m.status} |`);
|
|
452
|
+
lines.push(`| ${m.moduleName} | unit:${m.unitCount}, integration:${m.intCount}, e2e:${m.e2eCount} | ${m.caseCount} | ${m.deferredCount} | ${m.lastChange} | ${m.lastDate} | ${m.status} |`);
|
|
425
453
|
}
|
|
426
454
|
|
|
427
|
-
writeFileSync(indexPath, lines.join('\n') + '\n', 'utf-8');
|
|
455
|
+
if (!dryRun) writeFileSync(indexPath, lines.join('\n') + '\n', 'utf-8');
|
|
428
456
|
return { modules: modules.length, totalCases, totalDeferred };
|
|
429
457
|
}
|
|
430
458
|
|
|
@@ -494,16 +522,21 @@ async function main(argv, projectRoot) {
|
|
|
494
522
|
const candidateLedger = extractCandidateLedger(matrixContent);
|
|
495
523
|
const deferredItems = extractDeferredItems(matrixContent);
|
|
496
524
|
|
|
497
|
-
console.log(` Summary: ${summary.totalCases} cases, ${summary.modules} modules, ${summary.deferred} deferred`);
|
|
525
|
+
console.log(` Summary: ${summary.totalCases} cases (unit ${summary.unitCases} + integration ${summary.integrationCases} + e2e ${summary.e2eCases}), ${summary.modules} modules, ${summary.deferred} deferred`);
|
|
498
526
|
|
|
499
|
-
// Ensure ledger directory exists
|
|
500
|
-
if (!existsSync(ledgerDir)) {
|
|
527
|
+
// Ensure ledger directory exists(dry-run 不建目录)
|
|
528
|
+
if (!existsSync(ledgerDir) && !dryRun) {
|
|
501
529
|
mkdirSync(ledgerDir, { recursive: true });
|
|
502
530
|
}
|
|
503
531
|
|
|
532
|
+
const mark = dryRun ? '[DRY-RUN]' : '✅';
|
|
533
|
+
|
|
504
534
|
// Step 2: mergeBaselines
|
|
505
|
-
const mergeResults = mergeBaselines(ledgerDir, changeName, moduleSections, candidateLedger, deferredItems);
|
|
506
|
-
|
|
535
|
+
const mergeResults = mergeBaselines(ledgerDir, changeName, moduleSections, candidateLedger, deferredItems, dryRun);
|
|
536
|
+
const mergeStates = Object.values(mergeResults);
|
|
537
|
+
const created = mergeStates.filter(s => s === 'created' || s === 'would-create').length;
|
|
538
|
+
const updated = mergeStates.filter(s => s === 'updated' || s === 'would-update').length;
|
|
539
|
+
console.log(`${mark} Step 2: mergeBaselines — ${Object.keys(mergeResults).length} modules processed (${created} created, ${updated} updated)`);
|
|
507
540
|
|
|
508
541
|
// Step 3: resolveDeferred
|
|
509
542
|
const newCaseIds = [];
|
|
@@ -511,27 +544,31 @@ async function main(argv, projectRoot) {
|
|
|
511
544
|
const lines = section.split('\n');
|
|
512
545
|
for (const line of lines) {
|
|
513
546
|
if (line.startsWith('|') && !line.match(/^\|\s*[-:]+/) && !line.match(/\|\s*case_id/)) {
|
|
514
|
-
const cells = line.split('|').map(c => c.trim())
|
|
547
|
+
const cells = line.split('|').slice(1, -1).map(c => c.trim());
|
|
515
548
|
if (cells[0]) newCaseIds.push(cells[0]);
|
|
516
549
|
}
|
|
517
550
|
}
|
|
518
551
|
}
|
|
519
|
-
const deferredResult = resolveDeferred(ledgerDir, changeName, newCaseIds);
|
|
520
|
-
console.log(
|
|
552
|
+
const deferredResult = resolveDeferred(ledgerDir, changeName, newCaseIds, dryRun);
|
|
553
|
+
console.log(`${mark} Step 3: resolveDeferred — ${deferredResult.resolved} items resolved`);
|
|
521
554
|
|
|
522
555
|
// Step 4: appendChangelog
|
|
523
|
-
const changelogPath = appendChangelog(ledgerDir, changeName, matrixContent);
|
|
524
|
-
console.log(
|
|
556
|
+
const changelogPath = appendChangelog(ledgerDir, changeName, matrixContent, dryRun);
|
|
557
|
+
console.log(`${mark} Step 4: appendChangelog — ${relative(projectRoot, changelogPath)}`);
|
|
525
558
|
|
|
526
559
|
// Step 5: rewriteIndex
|
|
527
|
-
const indexResult = rewriteIndex(ledgerDir);
|
|
528
|
-
console.log(
|
|
560
|
+
const indexResult = rewriteIndex(ledgerDir, dryRun);
|
|
561
|
+
console.log(`${mark} Step 5: rewriteIndex — ${indexResult.modules} modules, ${indexResult.totalCases} total cases`);
|
|
529
562
|
|
|
530
563
|
// Step 6: gitCommit
|
|
531
564
|
gitCommit(projectRoot, changeName, dryRun);
|
|
532
|
-
console.log(
|
|
565
|
+
console.log(`${mark} Step 6: gitCommit`);
|
|
533
566
|
|
|
534
|
-
|
|
567
|
+
if (dryRun) {
|
|
568
|
+
console.log(`\n🧪 test-merge DRY-RUN complete for ${changeName} — 未写入任何文件`);
|
|
569
|
+
} else {
|
|
570
|
+
console.log(`\n🎉 test-merge complete for ${changeName}`);
|
|
571
|
+
}
|
|
535
572
|
}
|
|
536
573
|
|
|
537
574
|
// 直接运行(v0.35.0 修复:main() 无参自调用崩溃 → 调用 run() 走正确参数解析)
|
|
@@ -52,6 +52,8 @@ Subagent (general-purpose):
|
|
|
52
52
|
If `test-matrix.md` exists, read the module section relevant to your task.
|
|
53
53
|
For each case in the matrix, follow the protocol matching its `work_mode`:
|
|
54
54
|
|
|
55
|
+
**test_tier=e2e 豁免(v0.38.0)**: `test_tier=e2e` 的 case 不在 implementer 职责内——由 e2e skill / release-archivist Step 5b 覆盖,跳过(不计入模块 coverage,不按 work_mode 走 TDD)。
|
|
56
|
+
|
|
55
57
|
**work_mode=TDD** (new behavior):
|
|
56
58
|
1. RED: Write failing test matching `case_id` + `test_method_name`, confirm failure
|
|
57
59
|
2. GREEN: Implement minimum code to pass
|
|
@@ -68,7 +70,7 @@ Subagent (general-purpose):
|
|
|
68
70
|
2. Fix production code, confirm green
|
|
69
71
|
3. Report: defect reproduction + fix evidence
|
|
70
72
|
|
|
71
|
-
After all cases: self-check matrix coverage = passed cases / total cases in matrix for your module
|
|
73
|
+
After all cases: self-check matrix coverage = passed cases / total cases in matrix for your module(分母排除 test_tier=e2e)。
|
|
72
74
|
|
|
73
75
|
**两不原则 (Two Prohibitions)**:
|
|
74
76
|
- ⛔ DO NOT write production code and test code simultaneously — RED first, then GREEN
|
|
@@ -39,10 +39,10 @@ PRD 文档写入后、Handoff 之前,执行原型内循环。原型是 PRD 的
|
|
|
39
39
|
|
|
40
40
|
## 3.5.5 PRD 完整性评审(冻结前门禁)
|
|
41
41
|
|
|
42
|
-
原型审查通过后、冻结前,派发 `prd-completeness-reviewer`
|
|
42
|
+
原型审查通过后、冻结前,派发 `prd-completeness-reviewer` 子代理(独立上下文),评审 PRD「是否完整到能支撑后续 plan/spec 实施」(区别于 Phase 2.6 claim verifier——后者管"说得对不对",本评审管"说得全不全")。
|
|
43
43
|
|
|
44
44
|
- 派发:按名派发插件 agent `prd-completeness-reviewer`(定义见插件 `agents/prd-completeness-reviewer.md`),传入 PRD 路径 + CONCEPTS.md 路径。
|
|
45
|
-
-
|
|
45
|
+
- agent 直接写审查报告到 `prd/{ITERATION_VERSION}/prd-completeness-review.md`。
|
|
46
46
|
- 判定(柔性):PASS / PASS_WITH_WARNINGS → 进入冻结;**FAIL(Critical>0)→ 回 Phase 1.3 补充**后重审。
|
|
47
47
|
- 5 维度:用户故事完整性(Critical)/验收标准(Critical)/边界与非功能(Important)/术语一致性(Minor)/范围闭环(Important)。
|
|
48
48
|
|
|
@@ -144,7 +144,7 @@ If `test-matrix.md` exists in the change directory, audit the implementation aga
|
|
|
144
144
|
- Verify `design_method` matches actual test approach (a `boundary` case must use real boundary values, not a happy-path disguised as boundary)
|
|
145
145
|
|
|
146
146
|
2. **Coverage calculation**:
|
|
147
|
-
- Matrix coverage = implemented cases / total cases in matrix
|
|
147
|
+
- Matrix coverage = implemented cases / total cases in matrix(**分母排除 test_tier=e2e**——E2E case 由 release-archivist Step 5b 对账,不在实施层判定)
|
|
148
148
|
- Matrix coverage < 100% → **Critical finding** (test-matrix-gap)
|
|
149
149
|
- List missing case_ids
|
|
150
150
|
|
|
@@ -153,8 +153,9 @@ If `test-matrix.md` exists in the change directory, audit the implementation aga
|
|
|
153
153
|
- Entries with `decision=covered` actually have corresponding tests?
|
|
154
154
|
|
|
155
155
|
4. **Pyramid ratio check**:
|
|
156
|
-
- unit
|
|
157
|
-
-
|
|
156
|
+
- 分母 = unit + integration(e2e 不计入金字塔配比,v0.38.0)
|
|
157
|
+
- unit cases: 70-80% of (unit+integration)
|
|
158
|
+
- integration cases: ≤30% of (unit+integration)
|
|
158
159
|
- Significant deviation → **Important finding** (test-pyramid-imbalance)
|
|
159
160
|
|
|
160
161
|
If `test-matrix.md` does NOT exist, skip this step silently (legacy change compatibility).
|
|
@@ -73,9 +73,9 @@ Subagent (general-purpose):
|
|
|
73
73
|
- Each case has a corresponding test implementation (test_file + test_method_name match)?
|
|
74
74
|
- Assertions match the matrix's `expected` output?
|
|
75
75
|
- `design_method` matches actual test approach (boundary case uses real boundary values)?
|
|
76
|
-
- Matrix coverage < 100% → Critical finding (test-matrix-gap)
|
|
76
|
+
- Matrix coverage < 100% → Critical finding (test-matrix-gap)(分母排除 test_tier=e2e,E2E case 由 release-archivist Step 5b 对账)
|
|
77
77
|
- Candidate Coverage Ledger: deferred items have reasonable reasons?
|
|
78
|
-
- Pyramid ratio: unit 70-80%, integration ≤30
|
|
78
|
+
- Pyramid ratio: unit 70-80%, integration ≤30%?(分母 = unit+integration,e2e 不计入)
|
|
79
79
|
If `test-matrix.md` does NOT exist, skip this section silently.
|
|
80
80
|
|
|
81
81
|
**Production readiness:**
|
|
@@ -11,7 +11,7 @@ Read before generating: `proposal.md`, `specs/`, `design.md`, `tasks.md`, then l
|
|
|
11
11
|
|
|
12
12
|
**Architecture Design Outputs (v0.9 §26)**: 若 `architecture/` 目录存在,同时读取 `architecture/architecture.md` / `database.md` / `api.md`,作为执行契约的架构约束补充输入——确保 execution-contract.md 的 Implementation Constraints 段包含架构设计的关键约束(聚合边界/CQRS 分流/API 契约/schema 变更)。`architecture/` 不存在时跳过。
|
|
13
13
|
|
|
14
|
-
**Test Ledger Injection (v0.12 §43.5)**: 若 `docs/test-ledger/` 存在,读取 `INDEX.md`(模块覆盖概览 coverage_status)+ `baselines/{module}.md`(已有 case 避免重复设计;Deferred Items 评估本次是否可解决),作为 test-matrix.md
|
|
14
|
+
**Test Ledger Injection (v0.12 §43.5)**: 若 `docs/test-ledger/` 存在,读取 `INDEX.md`(模块覆盖概览 coverage_status)+ `baselines/{module}.md`(已有 case 避免重复设计;Deferred Items 评估本次是否可解决),作为 test-matrix.md 生成的增量输入。不存在时跳过(首次使用或无历史数据)。**tier 语义(v0.38.0)**:baselines 中 `test_tier=e2e` 的行表示"端到端已覆盖",不参与 unit/integration 去重与金字塔配比——不得把它误判为单测已覆盖而省略低层级 case 设计。
|
|
15
15
|
|
|
16
16
|
## Artifact Mapping
|
|
17
17
|
|
|
@@ -57,19 +57,20 @@ When the change involves UI (design.md has a `## UI Contract` section), the exec
|
|
|
57
57
|
|
|
58
58
|
### Generation Protocol
|
|
59
59
|
|
|
60
|
-
1. **输入来源**:specs/(Scenario + Unit/Integration 标签)、tasks.md(batch + file structure)、test-strategy skill(design_method 规则)、test-ledger baselines(增量输入)
|
|
60
|
+
1. **输入来源**:specs/(Scenario + Unit/Integration/E2E 标签)、tasks.md(batch + file structure)、test-strategy skill(design_method 规则)、test-ledger baselines(增量输入)
|
|
61
61
|
2. **按模块分组**:为每个有业务逻辑的模块(Service/Controller/Repository 等)生成 case 列表
|
|
62
62
|
3. **复杂度分级**:每个模块标注 trivial/medium/complex,作为用例数下限判据(test-strategy §2)
|
|
63
63
|
4. **design_method 覆盖**:每个模块至少 1 个 `{boundary, equivalence}` case + 1 个 `{error, exception, reject}` case(test-strategy §6 schema 强制覆盖)
|
|
64
64
|
5. **对抗验证**:矩阵生成后执行三招回检(test-strategy §4),缺失则追加 adversarial case
|
|
65
65
|
6. **候选覆盖台账**:列出所有被测候选(方法/类),标注 decision(covered/deferred/not_applicable)
|
|
66
|
+
7. **E2E 层评估(v0.38.0)**:对 specs 中含 **UI 交互/跨层链路**的 Scenario(AC 涉及前端交互、跨服务/跨层数据流)显式评估 E2E 必要性——纳入 `test_tier=e2e` 的 case(标准 12 列,case_id `{Module}-e2e-{scene}-{NNN}`,`work_mode=E2E`(不参与 build-executor 的 TDD 路由),mock 列 `-`,test_kind 用 `playwright_prototype`/`playwright_integration`)或记录"E2E 排除 + 理由"(如无 UI/纯后端)。不得静默跳过。
|
|
66
67
|
|
|
67
68
|
### 12-Column Format
|
|
68
69
|
|
|
69
70
|
| case_id | behavior | design_method | input | expected | test_kind | test_tier | mock | work_mode | test_file | test_method_name | run_command |
|
|
70
71
|
|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
71
72
|
|
|
72
|
-
字段说明见 test-strategy skill 的 `references/design-methods-detail.md`。`test_tier` 从 `test_kind`
|
|
73
|
+
字段说明见 test-strategy skill 的 `references/design-methods-detail.md`。`test_tier` 从 `test_kind` 派生,可取 **unit / integration / e2e** 三值(v0.38.0:E2E case 的 test_kind 用 `playwright_prototype` / `playwright_integration`)。
|
|
73
74
|
|
|
74
75
|
### Structure
|
|
75
76
|
|
|
@@ -81,6 +82,7 @@ When the change involves UI (design.md has a `## UI Contract` section), the exec
|
|
|
81
82
|
- Modules covered: {N} (complexity: trivial×{n}, medium×{n}, complex×{n})
|
|
82
83
|
- Unit cases: {N} ({percent}%)
|
|
83
84
|
- Integration cases: {N} ({percent}%)
|
|
85
|
+
- E2E cases: {N} ({percent}%)
|
|
84
86
|
- Deferred items: {N}
|
|
85
87
|
- Matrix revision: 1
|
|
86
88
|
|
|
@@ -104,7 +106,7 @@ When the change involves UI (design.md has a `## UI Contract` section), the exec
|
|
|
104
106
|
```markdown
|
|
105
107
|
## Test Matrix
|
|
106
108
|
See `test-matrix.md` for the full test case matrix (附属产物, 独立 hash).
|
|
107
|
-
- Total cases: {N}, Unit: {N}, Integration: {N}, Deferred: {N}
|
|
109
|
+
- Total cases: {N}, Unit: {N}, Integration: {N}, E2E: {N}, Deferred: {N}
|
|
108
110
|
```
|
|
109
111
|
|
|
110
112
|
然后执行 `tf state rebuild <change-dir>` 更新 `test_matrix_hash`。
|
|
@@ -23,7 +23,7 @@ description: 本地 HTML 原型设计与维护 skill(零外部依赖、可离
|
|
|
23
23
|
① 环境探查 + 方案设计 → sub: prototype-env-scout(只读,产出环境简报+2-3 个差异化方向,v0.18.0)
|
|
24
24
|
② 方案评审 + 人工评审 → 主代理(方向选定+方案确认 → AskUserQuestion)
|
|
25
25
|
③ 原型绘制 → sub: prototype-builder(有 Write,种子优先:template.html+layouts.md 组合,v0.18.0)
|
|
26
|
-
④ 原型评审 → sub: prototype-reviewer
|
|
26
|
+
④ 原型评审 → sub: prototype-reviewer(PRD 6 维度+P0 grep+craft 4 席 rubric,v0.18.0,直接写审查报告)
|
|
27
27
|
⑤ 循环编排 → 主代理:FAIL→回③修正(≤3轮,收敛检测);PASS/PASS_WITH_WARNINGS→⑥
|
|
28
28
|
⑥ 人工评审路由 → 主代理 AskUserQuestion:
|
|
29
29
|
- PRD 有问题 → 回 orchestrator S2 修订 PRD(vN 内修订,非升版)→ 再更新原型
|
|
@@ -38,8 +38,8 @@ description: 本地 HTML 原型设计与维护 skill(零外部依赖、可离
|
|
|
38
38
|
| ①环境探查+方案设计 | sub `prototype-env-scout`(插件 agent,只读) | 无(简报+方向写 response) |
|
|
39
39
|
| ②方案/人工评审 | 主代理(含方向选定,v0.18.0) | 无 |
|
|
40
40
|
| ③绘制 | sub `prototype-builder`(插件 agent,种子优先) | prototype/ 全部 |
|
|
41
|
-
| ④评审 | sub `prototype-reviewer`(插件 agent
|
|
42
|
-
| ⑤循环编排 | 主代理 |
|
|
41
|
+
| ④评审 | sub `prototype-reviewer`(插件 agent,含 craft rubric) | 审查报告(直接写文件) |
|
|
42
|
+
| ⑤循环编排 | 主代理 | 无 |
|
|
43
43
|
| ⑥人工路由 | 主代理 | 无 |
|
|
44
44
|
|
|
45
45
|
## 内核(零外部依赖)
|
|
@@ -83,12 +83,12 @@ test -f <prototype_path>/index.html && test -s <prototype_path>/index.html && ec
|
|
|
83
83
|
|
|
84
84
|
## 步骤 ④ 原型评审(子代理)
|
|
85
85
|
|
|
86
|
-
派发插件 agent `prototype-reviewer
|
|
86
|
+
派发插件 agent `prototype-reviewer`(独立上下文),**传入:`prd_path`、`prototype_path`(及当前轮次 N)**,执行:
|
|
87
87
|
- PRD 6 维度(D1-D6,D6 含五状态契约)
|
|
88
88
|
- **P0 工艺机械检查(v0.18.0)**:裸 hex / 靛蓝黑名单 / emoji / 填充文案 / scrollIntoView / data-testid / accent 超限——P0 违规 = Critical
|
|
89
89
|
- **Craft Quality 4 席 rubric(v0.18.0)**:工艺(×0.4)+品牌(×0.2)+无障碍(×0.2)+文案(×0.2) 加权合成
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
agent 直接写审查报告到 `prd/vN/prototype-auto-review.md`。
|
|
92
92
|
|
|
93
93
|
判定(v0.18.0 升级):
|
|
94
94
|
- **PASS**:Critical=0 且 Important=0 且 craft 合成 ≥6.0
|
|
@@ -46,9 +46,11 @@ Compare contract batches against actual diff. Every SHALL/MUST must have impleme
|
|
|
46
46
|
|
|
47
47
|
If `test-matrix.md` exists:
|
|
48
48
|
1. **Statistics**: total cases / implemented / passed
|
|
49
|
-
2. **
|
|
50
|
-
3. **
|
|
51
|
-
4. **
|
|
49
|
+
2. **E2E 层豁免(v0.38.0)**:`test_tier=e2e` 的 case 从 coverage 分母豁免,单列统计 `e2e cases: implemented N/M(经 Step 5b 验证)`——E2E 证据由 Step 5b 对账,不参与本步骤 100%/90% 判定
|
|
50
|
+
3. **Candidate Coverage Ledger audit**: any `decision=deferred` without reasonable reason?
|
|
51
|
+
4. **Complexity tier check**: cases per module meet minimum (trivial≥3 / medium≥5 / complex≥7)?(E2E case 不参与模块用例数下限判定)
|
|
52
|
+
5. **分母为 0 兜底(v0.38.0)**:纯 e2e change(无 unit/integration case,分母为 0)时,本步骤以 Step 5b E2E 验证结果为准,不判 FAIL(E2E 不替代低层级测试,但边界场景不卡死)
|
|
53
|
+
6. **Verdict**:
|
|
52
54
|
- Matrix coverage = 100% AND all cases pass → **PASS**
|
|
53
55
|
- Matrix coverage ≥ 90% with reasonable deferred items → **CONDITIONAL (WARN)**
|
|
54
56
|
- Matrix coverage < 90% OR unexplained gaps → **FAIL**
|
|
@@ -82,12 +84,20 @@ Check for files modified outside scope fence, new dependencies not in design. Un
|
|
|
82
84
|
- CONDITIONAL → present WARNs, proceed only with user acceptance
|
|
83
85
|
- PASS → proceed to final checks
|
|
84
86
|
|
|
85
|
-
### Step 5b: E2E Verification (
|
|
86
|
-
|
|
87
|
+
### Step 5b: E2E Verification (conditional, v0.38.0 起由"存在 e2e/ 套件"改为"矩阵含 E2E case"触发)
|
|
88
|
+
|
|
89
|
+
检测 `test-matrix.md` 是否含 `test_tier=e2e` 的 case:
|
|
90
|
+
1. **无 E2E case** → 跳过(本步骤不执行)
|
|
91
|
+
2. **有 E2E case 且已有 e2e 报告**(`docs/statistics/YYYY-MM-DD-e2e-report.md` 存在)→ 折进验证(见下)
|
|
92
|
+
3. **有 E2E case 且无 e2e 报告** → **AskUserQuestion 阻塞确认**:"test-matrix 含 {N} 个 E2E case,是否执行 E2E 测试?"
|
|
93
|
+
- **是** → 引导执行 `/e2e`(e2e skill:AC 提取 → Playwright 脚本 → 执行 → 报告 `docs/statistics/YYYY-MM-DD-e2e-report.md`),报告产生后回填本步骤验证
|
|
94
|
+
- **否** → 记录 E2E skip + 理由(如环境不具备/本期不做),报告行 `| E2E | SKIP | [reason] |`(不 FAIL)
|
|
95
|
+
|
|
96
|
+
有 E2E 报告时的验证逻辑(4 级判定与门禁内联于此;design spec ch.16 仅概述):
|
|
87
97
|
- Read `docs/statistics/YYYY-MM-DD-e2e-report.md`: AC coverage / four-dimension coverage (EX/ST/BND) / type distribution / uncovered list / failure root-cause (test issue vs implementation issue).
|
|
88
98
|
- **4-level artifact verification** (L1 existence / L2 substance / L3 wiring / L4 dataflow → VERIFIED/HOLLOW/ORPHANED/STUB/MISSING): compare `architecture-design` outputs (API/DB design docs) against actual code; STUB/MISSING = BLOCKER, HOLLOW/ORPHANED = WARNING.
|
|
89
99
|
- **Graded gate**: prototype-phase AC≥80% (HP required, EX/ST/BND non-blocking if absent); integration-phase AC≥95% / EX≥80% / ST≥90% / BND≥75% (AC/EX = BLOCKER, ST/BND = WARNING).
|
|
90
|
-
- Add a report row: `| E2E | PASS/FAIL/WARN | [coverage summary] |`.
|
|
100
|
+
- Add a report row: `| E2E | PASS/FAIL/WARN/SKIP | [coverage summary or skip reason] |`.
|
|
91
101
|
- Note: `ce-proof` is the Proof markdown editor — unrelated to verification; do NOT route E2E there.
|
|
92
102
|
|
|
93
103
|
## Final Checks
|
|
@@ -92,7 +92,7 @@ Every requirement must be testable. Use SHALL or MUST. Every requirement must ha
|
|
|
92
92
|
|
|
93
93
|
Optional structured AC tags under a Scenario are supported for E2E extraction (v0.4, design spec ch.16): `##### Exception:` (→ EX AC, ×2), `##### State:` (→ ST AC, ×2), `##### Boundary:` (→ BND AC, ×1). These are **OPTIONAL** — omitting them does NOT fail validation. The `e2e` skill applies dimension-conditional gating (absent dimension = N/A, not 0%) plus keyword fallback (error/invalid/fail/边界), so no spec is forced to add tags.
|
|
94
94
|
|
|
95
|
-
Optional test-dimension tags under a Scenario are supported for test-matrix extraction (v0.12 §44.1): `##### Unit:` (→ unit test hints: equivalence classes, boundary values), `##### Integration:` (→ integration test hints: cross-module, transaction boundaries). These are **OPTIONAL** — omitting them does NOT fail validation. The `contract-builder` extracts matrix skeletons from these tags when present, but does not require them.
|
|
95
|
+
Optional test-dimension tags under a Scenario are supported for test-matrix extraction (v0.12 §44.1): `##### Unit:` (→ unit test hints: equivalence classes, boundary values), `##### Integration:` (→ integration test hints: cross-module, transaction boundaries), `##### E2E:` (→ e2e test hints: UI interaction / cross-layer flows; v0.38.0, test_tier=e2e). These are **OPTIONAL** — omitting them does NOT fail validation. The `contract-builder` extracts matrix skeletons from these tags when present, but does not require them.
|
|
96
96
|
|
|
97
97
|
### design.md
|
|
98
98
|
Must have: Context (current state, constraints, stakeholders), Goals, Decisions (Choice + Rationale + Alternatives considered), Risks And Trade-Offs.
|
|
@@ -42,7 +42,8 @@ user-invocable: false
|
|
|
42
42
|
|
|
43
43
|
- unit(test_tier=unit):70-80% 的 case
|
|
44
44
|
- integration(test_tier=integration):≤30% 的 case
|
|
45
|
-
- e2e
|
|
45
|
+
- e2e(test_tier=e2e,v0.38.0):含 **UI 交互/跨层链路**的 AC(如上下分栏联动、导出端到端、权限 403 端到端)必须显式评估 E2E 必要性——**纳入 E2E case 或记录排除理由**(不静默跳过)。E2E case 沿用标准 12 列格式,`test_tier=e2e`、case_id 用 `{Module}-e2e-{scene}-{NNN}`(3 连字符 + 数字结尾,满足复利台账计数)、mock 列填 `-`。
|
|
46
|
+
- **金字塔分母 = unit + integration**(e2e 不计入金字塔配比,避免稀释 unit 占比判定)。
|
|
46
47
|
|
|
47
48
|
## 4. 对抗验证三招(矩阵生成后必检)
|
|
48
49
|
|
|
@@ -179,5 +179,6 @@ CANCELLED ←──────────────┘ (仅 PAID 前可取
|
|
|
179
179
|
| service_social, repository_h2 | integration |
|
|
180
180
|
| rabbitmq_social, redis_social | integration |
|
|
181
181
|
| external_api_stub, test_infrastructure | integration |
|
|
182
|
+
| playwright_prototype, playwright_integration(v0.38.0) | e2e |
|
|
182
183
|
|
|
183
184
|
**用途**:test_tier 用于统计和 CI 分级;test_kind 用于路由到正确的编写模板。
|
|
@@ -226,6 +226,7 @@ class UserServiceCreateUserTest { ... }
|
|
|
226
226
|
|
|
227
227
|
**检查**:
|
|
228
228
|
- case 声明的 `test_tier` 与实际测试文件不符(如 unit case 写到了集成测试文件)
|
|
229
|
+
- `test_tier=e2e` 的 case 合法落点是 `e2e/` 目录(Playwright spec,v0.38.0)——落在普通单元/集成测试文件里算 mismatch
|
|
229
230
|
|
|
230
231
|
### 12. evidence-not-in-method
|
|
231
232
|
|
|
@@ -106,8 +106,8 @@ tf solutions capture --phase prd --domain <domain> --type pitfall --severity <se
|
|
|
106
106
|
|
|
107
107
|
## 原型自动评审(§17.10)
|
|
108
108
|
|
|
109
|
-
**实现形式**:独立 `prototype-reviewer` agent
|
|
110
|
-
- tools: Read, Bash, Grep, Glob
|
|
109
|
+
**实现形式**:独立 `prototype-reviewer` agent(对齐 code-reviewer 模式)。
|
|
110
|
+
- tools: Read, Bash, Grep, Glob, Write(直接写审查报告文件)
|
|
111
111
|
- 独立上下文运行,未参与 PRD/原型产出(规避锚定效应)
|
|
112
112
|
- 两阶段检查:Pre-check(Bash 结构化预检,D1/D4 机械化)+ Deep-check(LLM 六维度语义对比,D2/D3/D5/D6 标注为「建议级,误报可被人工推翻」)
|
|
113
113
|
|