@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.
Files changed (41) 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/CHANGELOG.md +27 -0
  9. package/GEMINI.md +1 -1
  10. package/INSTALL.md +1 -1
  11. package/README.md +1 -1
  12. package/agents/architecture-reviewer.md +16 -6
  13. package/agents/change-split-auditor.md +11 -6
  14. package/agents/code-reviewer.md +7 -6
  15. package/agents/cross-change-consistency-checker.md +11 -6
  16. package/agents/prd-completeness-reviewer.md +11 -6
  17. package/agents/prototype-reviewer.md +11 -6
  18. package/docs/README_en.md +1 -1
  19. package/docs/solutions/INDEX.md +1 -0
  20. package/docs/solutions/cross-phase/2026-08-06-no-summary.md +17 -0
  21. package/gemini-extension.json +1 -1
  22. package/hooks/session-start +2 -2
  23. package/llms.txt +1 -1
  24. package/package.json +1 -1
  25. package/plugin.json +1 -1
  26. package/scripts/lib/test-matrix-export.mjs +5 -0
  27. package/scripts/lib/test-merge.mjs +130 -93
  28. package/skills/build-executor/implementer-prompt.md +3 -1
  29. package/skills/ce-brainstorm/references/prototype-loop.md +2 -2
  30. package/skills/code-reviewer/SKILL.md +4 -3
  31. package/skills/code-reviewer/code-reviewer-prompt.md +2 -2
  32. package/skills/contract-builder/SKILL.md +6 -4
  33. package/skills/prototype/SKILL.md +3 -3
  34. package/skills/prototype/references/orchestration-flow.md +2 -2
  35. package/skills/release-archivist/SKILL.md +16 -6
  36. package/skills/spec-writer/SKILL.md +1 -1
  37. package/skills/test-strategy/SKILL.md +2 -1
  38. package/skills/test-strategy/references/design-methods-detail.md +1 -0
  39. package/skills/test-strategy/references/test-quality-rules.md +1 -0
  40. package/skills/workflow-orchestrator/references/s2-prd-prototype-loop.md +2 -2
  41. 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 defMatch = line.match(/Deferred items:\s*(\d+)/i);
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()).filter(Boolean);
235
- if (cells.length >= 10) {
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(增量更新 Current Cases + 追加 Evolution Log)
275
+ * 合并现有 baselineCurrent 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
- // 插入 Current Cases header,然后用新数据替换表格
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 cells = cl.split('|').map(c => c.trim()).filter(Boolean);
292
- if (cells.length >= 10) {
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
- lines.push(line);
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
- lines.push('');
303
- const caseCount = sectionContent.split('\n').filter(l =>
304
- l.startsWith('|') && !l.match(/^\|\s*[-:]+/) && !l.match(/\|\s*case_id/)
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
- let content = readFileSync(fp, 'utf-8');
331
- let changed = false;
346
+ const content = readFileSync(fp, 'utf-8');
347
+ const lines = content.split('\n');
332
348
 
333
- // 移除 Deferred Items 中被新 case 覆盖的行
334
- for (const caseId of newCaseIds) {
335
- const regex = new RegExp(`\\|\\s*${escapeRegex(caseId)}\\s*\\|[^\\n]*\\n`, 'g');
336
- if (regex.test(content)) {
337
- content = content.replace(regex, '');
338
- changed = true;
339
- resolved++;
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, content, 'utf-8');
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(/unit/gi) || []).length;
404
- const intCount = (content.match(/integration/gi) || []).length;
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
- console.log(`✅ Step 2: mergeBaselines — ${Object.keys(mergeResults).length} modules processed`);
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()).filter(Boolean);
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(`✅ Step 3: resolveDeferred — ${deferredResult.resolved} items resolved`);
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(`✅ Step 4: appendChangelog — ${relative(projectRoot, changelogPath)}`);
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(`✅ Step 5: rewriteIndex — ${indexResult.modules} modules, ${indexResult.totalCases} total cases`);
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('✅ Step 6: gitCommit');
565
+ console.log(`${mark} Step 6: gitCommit`);
533
566
 
534
- console.log(`\n🎉 test-merge complete for ${changeName}`);
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` 子代理(只读,独立上下文),评审 PRD「是否完整到能支撑后续 plan/spec 实施」(区别于 Phase 2.6 claim verifier——后者管"说得对不对",本评审管"说得全不全")。
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
- - 子代理按 §18.1 交接协议返回。报告落盘到 `prd/{ITERATION_VERSION}/prd-completeness-review.md`。
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 cases: 70-80% of total
157
- - integration cases: ≤30% of total
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` 派生(unit/integration)。
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(只读,PRD 6 维度+P0 grep+craft 4 席 rubric,v0.18.0
26
+ ④ 原型评审 → sub: prototype-reviewerPRD 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,只读,含 craft rubric) | 无(报告写 response,主代理落盘) |
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`(只读,独立上下文),**传入:`prd_path`、`prototype_path`(及当前轮次 N)**,执行:
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
- 报告写 response,主代理落盘到 `prd/vN/prototype-auto-review.md`。
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. **Candidate Coverage Ledger audit**: any `decision=deferred` without reasonable reason?
50
- 3. **Complexity tier check**: cases per module meet minimum (trivial≥3 / medium≥5 / complex≥7)?
51
- 4. **Verdict**:
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 (optional, v0.4)
86
- If an `e2e/` suite exists for the change, fold its report into verification (see design spec ch.16):
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:由 e2e skill 独立覆盖,不计入本矩阵
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(只读审查员,对齐 code-reviewer 模式)。
110
- - tools: Read, Bash, Grep, Glob(**无 Write**,报告写在 final response,编排层落盘)
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