@xulthekl/team-flow 0.36.4 → 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 (50) hide show
  1. package/.claude/always/phase-guard.md +1 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/.cursor-plugin/marketplace.json +1 -1
  6. package/.cursor-plugin/plugin.json +1 -1
  7. package/.github/plugin/marketplace.json +2 -2
  8. package/AGENTS.md +1 -0
  9. package/CHANGELOG.md +37 -0
  10. package/GEMINI.md +1 -1
  11. package/INSTALL.md +1 -1
  12. package/README.md +1 -1
  13. package/agents/architecture-reviewer.md +16 -6
  14. package/agents/change-split-auditor.md +11 -6
  15. package/agents/code-reviewer.md +7 -6
  16. package/agents/cross-change-consistency-checker.md +11 -6
  17. package/agents/prd-completeness-reviewer.md +11 -6
  18. package/agents/prototype-reviewer.md +11 -6
  19. package/docs/README_en.md +1 -1
  20. package/docs/solutions/INDEX.md +1 -0
  21. package/docs/solutions/cross-phase/2026-08-06-no-summary.md +17 -0
  22. package/gemini-extension.json +1 -1
  23. package/hooks/session-start +2 -2
  24. package/llms.txt +1 -1
  25. package/package.json +1 -1
  26. package/plugin.json +1 -1
  27. package/scripts/lib/cmd-prototype.mjs +227 -0
  28. package/scripts/lib/cmd-publish.mjs +254 -0
  29. package/scripts/lib/prototype-sync.mjs +41 -2
  30. package/scripts/lib/test-matrix-export.mjs +5 -0
  31. package/scripts/lib/test-merge.mjs +130 -93
  32. package/scripts/team-flow.mjs +8 -0
  33. package/skills/architecture-design/references/s3.5-product-architecture.md +9 -0
  34. package/skills/build-executor/SKILL.md +7 -0
  35. package/skills/build-executor/implementer-prompt.md +3 -1
  36. package/skills/ce-brainstorm/references/prototype-loop.md +2 -2
  37. package/skills/code-reviewer/SKILL.md +4 -3
  38. package/skills/code-reviewer/code-reviewer-prompt.md +2 -2
  39. package/skills/contract-builder/SKILL.md +19 -4
  40. package/skills/e2e/SKILL.md +2 -0
  41. package/skills/prototype/SKILL.md +4 -3
  42. package/skills/prototype/references/orchestration-flow.md +2 -2
  43. package/skills/release-archivist/SKILL.md +25 -6
  44. package/skills/spec-writer/SKILL.md +12 -8
  45. package/skills/test-strategy/SKILL.md +2 -1
  46. package/skills/test-strategy/references/design-methods-detail.md +1 -0
  47. package/skills/test-strategy/references/test-quality-rules.md +1 -0
  48. package/skills/workflow-orchestrator/references/s2-prd-prototype-loop.md +11 -2
  49. package/skills/workflow-orchestrator/references/s4-split-validate.md +9 -0
  50. package/skills/workflow-start/SKILL.md +10 -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() 走正确参数解析)
@@ -34,6 +34,8 @@ const COMMANDS = {
34
34
  'install-qoder': () => import('./lib/cmd-install-qoder.mjs'),
35
35
  'install-zcode': () => import('./lib/cmd-install-zcode.mjs'),
36
36
  'prototype-sync': () => import('./lib/prototype-sync.mjs'),
37
+ prototype: () => import('./lib/cmd-prototype.mjs'),
38
+ publish: () => import('./lib/cmd-publish.mjs'),
37
39
  'arch-merge': () => import('./lib/arch-merge.mjs'),
38
40
  arch: () => import('./lib/cmd-arch.mjs'),
39
41
  'test-merge': () => import('./lib/test-merge.mjs'),
@@ -53,6 +55,12 @@ Commands:
53
55
  sync <change-dir> Merge delta specs into main specs
54
56
  prototype-sync <change-dir> [--source <path>] [--prototype-dir <path>]
55
57
  Merge UX delta into global prototype/ + design-system.md
58
+ publish <--prd|--arch|--changes <dir>|--all> [--push] [--dry-run]
59
+ Commit stage artifacts (whitelist) + optional push (v0.37.0 §68.3)
60
+ prototype branch <prd-vN>
61
+ Create/reuse prototype version worktree (v0.37.0 §68.4)
62
+ prototype deisolate <prd-vN> [--merge] [--clean]
63
+ Version wrap-up: merge prototype branch back + clean worktree
56
64
  arch init [--mode reconstruction|design] [--baseline-ref <prd/vN/>]
57
65
  Stamp project-level arch_baseline into .team-flow/arch-state.json (v0.35.0 §59.4)
58
66
  arch show Show current project architecture baseline state
@@ -72,6 +72,15 @@
72
72
 
73
73
  逆向重建工具:`workflow-bootstrap` 的 recon-probe.sh(--ddl-out,路径 `${CLAUDE_PLUGIN_ROOT}/skills/workflow-bootstrap/scripts/recon-probe.sh`)+ codebase-recon-analyst + 新增 `arch-reverse-analyst`(v0.14 §63.2)。
74
74
 
75
+ ## 阶段产物同步门禁点(v0.37.0 §68.2 G2)
76
+
77
+ ARCH 完成(评审 PASS 或 skip 已物化)后,**阻塞确认**(AskUserQuestion)是否同步阶段产物(团队协作:架构决策是实施依据):
78
+ - **A 提交并推送**:`tf publish --arch --push`
79
+ - **B 仅提交不推送**:`tf publish --arch`
80
+ - **C 暂不同步**:继续 S4,后续可补
81
+
82
+ 同步对象:`docs/architecture/iterations/vN/`(架构快照)+ 评审 verdict(含首轮 `tf arch init` 打戳产物 `.team-flow/arch-state.json`)。
83
+
75
84
  ## 完成条件
76
85
 
77
86
  - `docs/architecture/iterations/vN/architecture.md` 已产出(6 产物,provenance 标注)
@@ -43,6 +43,13 @@ Branch/worktree preflight before ANY implementation edit (mandatory — do not s
43
43
  aggregated `<workspace>/.worktrees/<change>/` directory — and make all implementation
44
44
  edits there.
45
45
 
46
+ ### UI Contract Prototype Location (v0.37.0 §68.5)
47
+
48
+ When implementing UI tasks, the UI contract source is the **prototype version worktree**, not the change worktree's prototype copy:
49
+ - **定位**:读 `change-brief.md` 的 `upstream_plan_ref: prd/vN/plan.md`(或 change 目录名 `v{N}-` 前缀)→ PRD 版本 vN → 原型 worktree = `<workspace>/.worktrees/prd-vN/prototype/`
50
+ - **回退**:该 worktree 不存在时用 `<workspace>/prototype/`,并提示 `tf prototype branch <prd-vN>` 创建
51
+ - **引用**:execution-contract / design.md 中引用的原型页面(`prototype/pages/<page>.html` 等)以此为基准定位;**禁止**引用主干上其他版本的旧原型
52
+
46
53
  ## Core Laws
47
54
 
48
55
  ### Law 0: State Field Boundary (v0.30.0)
@@ -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
 
@@ -36,6 +36,19 @@ Before finalizing:
36
36
 
37
37
  Must make obvious: approved behavior, out-of-scope, constraints, batches, test obligations, review gates, and conditions that force a rewind to planning. Prefer compression over repeating planning details.
38
38
 
39
+ ## UI UX Delta Prototype Reference (v0.37.0 §68.5)
40
+
41
+ When the change involves UI (design.md has a `## UI Contract` section), the execution-contract's `## UX 增量` section (consumed by `tf prototype-sync`) must **state the prototype version worktree path** in its first line:
42
+
43
+ ```markdown
44
+ ## UX 增量
45
+ 原型目标:<workspace>/.worktrees/prd-vN/prototype/(PRD 版本 vN,从 change-brief `upstream_plan_ref` 推导)
46
+ [ADD] pages/xxx.html
47
+ [MODIFY] pages/yyy.html
48
+ ```
49
+
50
+ `[ADD]/[MODIFY]` relative paths stay prototype-root-relative; the stated worktree is what `prototype-sync` resolves to when `--prototype-dir` is absent (v0.37.0 §68.5 制品链路径版本化).
51
+
39
52
  ## Test Matrix Generation (v0.12 §42, v0.13 §52 B1 强制化)
40
53
 
41
54
  `test-matrix.md` 是 `execution-contract.md` 的**附属产物**(不是独立第 6 核心产物),在 contract 的 `## Test Matrix` 段引用。
@@ -44,19 +57,20 @@ Must make obvious: approved behavior, out-of-scope, constraints, batches, test o
44
57
 
45
58
  ### Generation Protocol
46
59
 
47
- 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(增量输入)
48
61
  2. **按模块分组**:为每个有业务逻辑的模块(Service/Controller/Repository 等)生成 case 列表
49
62
  3. **复杂度分级**:每个模块标注 trivial/medium/complex,作为用例数下限判据(test-strategy §2)
50
63
  4. **design_method 覆盖**:每个模块至少 1 个 `{boundary, equivalence}` case + 1 个 `{error, exception, reject}` case(test-strategy §6 schema 强制覆盖)
51
64
  5. **对抗验证**:矩阵生成后执行三招回检(test-strategy §4),缺失则追加 adversarial case
52
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/纯后端)。不得静默跳过。
53
67
 
54
68
  ### 12-Column Format
55
69
 
56
70
  | case_id | behavior | design_method | input | expected | test_kind | test_tier | mock | work_mode | test_file | test_method_name | run_command |
57
71
  |---|---|---|---|---|---|---|---|---|---|---|---|
58
72
 
59
- 字段说明见 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`)。
60
74
 
61
75
  ### Structure
62
76
 
@@ -68,6 +82,7 @@ Must make obvious: approved behavior, out-of-scope, constraints, batches, test o
68
82
  - Modules covered: {N} (complexity: trivial×{n}, medium×{n}, complex×{n})
69
83
  - Unit cases: {N} ({percent}%)
70
84
  - Integration cases: {N} ({percent}%)
85
+ - E2E cases: {N} ({percent}%)
71
86
  - Deferred items: {N}
72
87
  - Matrix revision: 1
73
88
 
@@ -91,7 +106,7 @@ Must make obvious: approved behavior, out-of-scope, constraints, batches, test o
91
106
  ```markdown
92
107
  ## Test Matrix
93
108
  See `test-matrix.md` for the full test case matrix (附属产物, 独立 hash).
94
- - Total cases: {N}, Unit: {N}, Integration: {N}, Deferred: {N}
109
+ - Total cases: {N}, Unit: {N}, Integration: {N}, E2E: {N}, Deferred: {N}
95
110
  ```
96
111
 
97
112
  然后执行 `tf state rebuild <change-dir>` 更新 `test_matrix_hash`。
@@ -52,5 +52,7 @@ user-invocable: true
52
52
  ## 复用机制
53
53
  单一 `e2e/` + `playwright.config.ts` `projects`(baseURL):`prototype` project → http-server 托管 HTML;`integration` project → 真实应用。脚本一致,仅切地址 + 少量选择器/断言适配(高重叠复用,非零触碰;原型绿 ≠ 集成绿)。
54
54
 
55
+ **原型期托管(v0.37.0 §68.5)**:`prototype` project 托管的原型 = 对应 PRD 版本 worktree 的 `prototype/`(`<workspace>/.worktrees/prd-vN/prototype/`,版本从 change-brief `upstream_plan_ref` 推导);baseURL 指向该 worktree 的 http-server,**不指向主干旧版本原型**。该 worktree 缺失时回退 `<workspace>/prototype/` 并提示 `tf prototype branch <prd-vN>` 创建(与 prototype-sync 回退语义一致)。
56
+
55
57
  ## 与 release-archivist 衔接
56
58
  e2e 报告 + 4 级制品比对(L1 存在 / L2 实质 / L3 接线 / L4 数据流)作为 closing 验收 gate 的验证维度,由 `release-archivist` 接入(`ce-proof` 是 Proof 编辑器,与此无关)。
@@ -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
  ## 内核(零外部依赖)
@@ -87,6 +87,7 @@ prototype/
87
87
  ## 分支约定
88
88
  - 原型随 **PRD 当前版本分支**维护(如 `prd-v2` 分支的 `prototype/` = v2 产品原型)。
89
89
  - change 引用其所在 PRD 版本分支的 `prototype/`。
90
+ - **版本 worktree 管理(v0.37.0 §68.4)**:原型为独立仓库(有远程),版本分支的 worktree 由 `tf prototype branch <prd-vN>` 创建/复用(隔离 + 团队拉取入口);版本收尾(change_dag 该版本全部 closing)由 `tf prototype deisolate <prd-vN> --merge --clean` merge 回主干 + 清理(方案 A:merge 时机 = 版本收尾)。原型 worktree 布局 `<workspace>/.worktrees/prd-vN/prototype/`(与代码实施 `.worktrees/` 统一隔离区)。
90
91
 
91
92
  ## prototype-sync(change 完成回写,release-archivist 自动触发,v0.24.0 升级)
92
93
 
@@ -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