@xulthekl/team-flow 0.30.0 → 0.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/.claude/always/phase-guard.md +1 -1
  2. package/.claude-plugin/marketplace.json +3 -3
  3. package/.claude-plugin/plugin.json +2 -2
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.cursor-plugin/marketplace.json +2 -2
  6. package/.cursor-plugin/plugin.json +2 -2
  7. package/.github/plugin/marketplace.json +2 -2
  8. package/AGENTS.md +5 -4
  9. package/CHANGELOG.md +85 -0
  10. package/GEMINI.md +1 -1
  11. package/HANDOFF.md +1 -1
  12. package/INSTALL.md +1 -1
  13. package/README.md +3 -3
  14. package/agents/build-executor.md +1 -0
  15. package/agents/contract-builder.md +6 -1
  16. package/docs/README_en.md +1 -1
  17. package/docs/solutions/INDEX.md +2 -0
  18. package/docs/solutions/cross-phase/2026-08-01-no-summary.md +17 -0
  19. package/docs/solutions/cross-phase/2026-08-03-no-summary.md +17 -0
  20. package/gemini-extension.json +2 -2
  21. package/hooks/session-start +2 -2
  22. package/llms.txt +1 -1
  23. package/package.json +6 -2
  24. package/plugin.json +2 -2
  25. package/scripts/guard/checks/test-gate-exemptions.mjs +53 -0
  26. package/scripts/guard/checks/test-matrix-complete.mjs +91 -0
  27. package/scripts/guard/checks/test-matrix-ready.mjs +60 -0
  28. package/scripts/guard/checks/tests-passing.mjs +112 -24
  29. package/scripts/guard/guard.mjs +10 -2
  30. package/scripts/lib/cmd-doctor.mjs +43 -1
  31. package/scripts/lib/cmd-execution.mjs +24 -1
  32. package/scripts/lib/cmd-state.mjs +33 -5
  33. package/scripts/lib/cmd-validate.mjs +42 -0
  34. package/scripts/lib/execution-plan.mjs +23 -0
  35. package/scripts/lib/hash.mjs +11 -0
  36. package/scripts/lib/state-loader.mjs +26 -1
  37. package/scripts/lib/test-matrix-export.mjs +231 -0
  38. package/scripts/lib/test-merge.mjs +540 -0
  39. package/scripts/lib/test-record.mjs +212 -0
  40. package/scripts/team-flow.mjs +9 -0
  41. package/skills/build-executor/implementer-prompt.md +38 -3
  42. package/skills/code-reviewer/SKILL.md +28 -1
  43. package/skills/code-reviewer/code-reviewer-prompt.md +10 -0
  44. package/skills/contract-builder/SKILL.md +75 -0
  45. package/skills/release-archivist/SKILL.md +50 -10
  46. package/skills/session-handoff/SKILL.md +1 -0
  47. package/skills/spec-writer/SKILL.md +3 -1
  48. package/skills/test-strategy/SKILL.md +70 -0
  49. package/skills/test-strategy/references/adversarial-patterns.md +0 -0
  50. package/skills/test-strategy/references/complexity-grading.md +137 -0
  51. package/skills/test-strategy/references/design-methods-detail.md +183 -0
  52. package/skills/workflow-orchestrator/references/s1-path-router.md +4 -0
  53. package/skills/workflow-start/SKILL.md +5 -3
  54. package/skills/workflow-start/references/routing-rules.md +21 -0
  55. package/tests/e2e.test.mjs +0 -266
  56. package/tests/lib/cmd-audit.test.mjs +0 -202
  57. package/tests/lib/cmd-checkpoint.test.mjs +0 -98
  58. package/tests/lib/cmd-config.test.mjs +0 -101
  59. package/tests/lib/cmd-doctor.test.mjs +0 -450
  60. package/tests/lib/cmd-execution.test.mjs +0 -647
  61. package/tests/lib/cmd-handoff.test.mjs +0 -137
  62. package/tests/lib/cmd-inject.test.mjs +0 -218
  63. package/tests/lib/cmd-install-workbuddy.test.mjs +0 -113
  64. package/tests/lib/cmd-install-zcode.test.mjs +0 -42
  65. package/tests/lib/cmd-isolate.test.mjs +0 -68
  66. package/tests/lib/cmd-list.test.mjs +0 -127
  67. package/tests/lib/cmd-runtime.test.mjs +0 -83
  68. package/tests/lib/cmd-state-missing.test.mjs +0 -65
  69. package/tests/lib/cmd-state.test.mjs +0 -365
  70. package/tests/lib/cmd-sync-paths.test.mjs +0 -84
  71. package/tests/lib/cmd-validate-paths.test.mjs +0 -90
  72. package/tests/lib/config-loader.test.mjs +0 -175
  73. package/tests/lib/ensure-branch.test.mjs +0 -110
  74. package/tests/lib/execution-control-plane.test.mjs +0 -168
  75. package/tests/lib/execution-plan.test.mjs +0 -474
  76. package/tests/lib/guard-compound-captured.test.mjs +0 -92
  77. package/tests/lib/guard-specs-merged.test.mjs +0 -110
  78. package/tests/lib/guard-tests-passing.test.mjs +0 -114
  79. package/tests/lib/guard-transitions.test.mjs +0 -262
  80. package/tests/lib/guard.test.mjs +0 -613
  81. package/tests/lib/hash.test.mjs +0 -138
  82. package/tests/lib/infer-workflow.test.mjs +0 -161
  83. package/tests/lib/install-git-hooks.test.mjs +0 -20
  84. package/tests/lib/marketplace-release-docs.test.mjs +0 -31
  85. package/tests/lib/minimality-discipline.test.mjs +0 -37
  86. package/tests/lib/model-profiles-docs.test.mjs +0 -33
  87. package/tests/lib/node20-compatibility.test.mjs +0 -73
  88. package/tests/lib/node20-test-entry.test.mjs +0 -17
  89. package/tests/lib/platform-runtime-distribution.test.mjs +0 -157
  90. package/tests/lib/raw-mode-smoke.test.mjs +0 -42
  91. package/tests/lib/sdd-overlay.test.mjs +0 -118
  92. package/tests/lib/solutions-capture.test.mjs +0 -108
  93. package/tests/lib/solutions-index-gen.test.mjs +0 -147
  94. package/tests/lib/solutions-inject.test.mjs +0 -115
  95. package/tests/lib/solutions-promote.test.mjs +0 -200
  96. package/tests/lib/spec-paths.test.mjs +0 -80
  97. package/tests/lib/state-loader.test.mjs +0 -251
  98. package/tests/lib/token-baseline.test.mjs +0 -72
  99. package/tests/lib/token-rules.test.mjs +0 -94
  100. package/tests/lib/verify-marketplace-release.test.mjs +0 -128
  101. package/tests/tsconfig.json +0 -12
@@ -0,0 +1,540 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * test-merge — change 完成后将测试增量合并回全局 docs/test-ledger/
4
+ *
5
+ * v0.12 §43.3 定义,v0.31.0 实现为 CLI 子命令
6
+ * 用法:tf test-merge <change-dir> [--project-root <path>] [--dry-run]
7
+ *
8
+ * 功能(6 步流程):
9
+ * 1. preCheck — 校验 test-matrix.md 存在且非空
10
+ * 2. mergeBaselines — 按模块提取 case,增量合并到 baselines/{module}.md
11
+ * 3. resolveDeferred — 新 case 覆盖了旧 deferred 项 → 从 Deferred Items 移除
12
+ * 4. appendChangelog — 归档 test-matrix.md → changelog/{change-id}.md
13
+ * 5. rewriteIndex — 统计模块数/case 数/deferred 数,重写 INDEX.md
14
+ * 6. gitCommit — 单次原子提交
15
+ *
16
+ * 回写顺序:arch-merge → prototype-sync → test-merge → compound promotion
17
+ */
18
+
19
+ import { readFileSync, writeFileSync, existsSync, cpSync, mkdirSync, readdirSync } from 'node:fs';
20
+ import { join, basename, relative, resolve } from 'node:path';
21
+ import { execSync } from 'node:child_process';
22
+
23
+ /**
24
+ * 解析 CLI 参数数组为结构化对象
25
+ */
26
+ function parseArgv(argv) {
27
+ const parsed = { _: [] };
28
+ for (let i = 0; i < argv.length; i++) {
29
+ if (argv[i] === '--project-root' && argv[i + 1] && !argv[i + 1].startsWith('--')) {
30
+ parsed.projectRoot = argv[++i];
31
+ } else if (argv[i] === '--dry-run') {
32
+ parsed.dryRun = true;
33
+ } else if (!argv[i].startsWith('--')) {
34
+ parsed._.push(argv[i]);
35
+ }
36
+ }
37
+ return parsed;
38
+ }
39
+
40
+ /**
41
+ * 从 change 目录提取 change name
42
+ */
43
+ function extractChangeName(changeDir) {
44
+ return basename(changeDir.replace(/\/$/, ''));
45
+ }
46
+
47
+ /**
48
+ * Step 1: 预检 — 校验 test-matrix.md 存在且非空
49
+ */
50
+ function preCheck(changeDir) {
51
+ const matrixPath = join(changeDir, 'test-matrix.md');
52
+ if (!existsSync(matrixPath)) {
53
+ return { pass: false, reason: 'test-matrix.md not found — skipping test-merge' };
54
+ }
55
+ const content = readFileSync(matrixPath, 'utf-8');
56
+ if (content.trim().length === 0) {
57
+ return { pass: false, reason: 'test-matrix.md is empty — skipping test-merge' };
58
+ }
59
+ return { pass: true, content };
60
+ }
61
+
62
+ /**
63
+ * 从 test-matrix.md 提取 Summary 统计信息
64
+ */
65
+ function extractSummary(content) {
66
+ const summary = { totalCases: 0, modules: 0, unitCases: 0, integrationCases: 0, deferred: 0 };
67
+ const lines = content.split('\n');
68
+ for (const line of lines) {
69
+ const totalMatch = line.match(/Total cases:\s*(\d+)/i);
70
+ if (totalMatch) summary.totalCases = parseInt(totalMatch[1], 10);
71
+ const modulesMatch = line.match(/Modules covered:\s*(\d+)/i);
72
+ if (modulesMatch) summary.modules = parseInt(modulesMatch[1], 10);
73
+ const unitMatch = line.match(/Unit cases:\s*(\d+)/i);
74
+ if (unitMatch) summary.unitCases = parseInt(unitMatch[1], 10);
75
+ const intMatch = line.match(/Integration cases:\s*(\d+)/i);
76
+ if (intMatch) summary.integrationCases = parseInt(intMatch[1], 10);
77
+ const defMatch = line.match(/Deferred items:\s*(\d+)/i);
78
+ if (defMatch) summary.deferred = parseInt(defMatch[1], 10);
79
+ }
80
+ return summary;
81
+ }
82
+
83
+ /**
84
+ * 从 test-matrix.md 提取模块分段(## Cases 下的 ### 子段)
85
+ * 返回 { moduleName: sectionContent } 映射
86
+ */
87
+ function extractModuleSections(content) {
88
+ const sections = {};
89
+ const lines = content.split('\n');
90
+ let currentModule = null;
91
+ let currentLines = [];
92
+
93
+ for (const line of lines) {
94
+ if (line.startsWith('### ') && !line.startsWith('### #')) {
95
+ if (currentModule) {
96
+ sections[currentModule] = currentLines.join('\n');
97
+ }
98
+ currentModule = line.replace(/^###\s+/, '').trim();
99
+ currentLines = [line];
100
+ } else if (currentModule) {
101
+ currentLines.push(line);
102
+ }
103
+ }
104
+ if (currentModule) {
105
+ sections[currentModule] = currentLines.join('\n');
106
+ }
107
+ return sections;
108
+ }
109
+
110
+ /**
111
+ * 从 test-matrix.md 提取 Candidate Coverage Ledger
112
+ */
113
+ function extractCandidateLedger(content) {
114
+ const candidates = [];
115
+ const lines = content.split('\n');
116
+ let inLedger = false;
117
+
118
+ for (const line of lines) {
119
+ if (line.match(/## Candidate Coverage Ledger/i)) {
120
+ inLedger = true;
121
+ continue;
122
+ }
123
+ if (inLedger && line.startsWith('## ')) {
124
+ break;
125
+ }
126
+ if (inLedger && line.startsWith('|') && !line.match(/^\|\s*[-:]+/)) {
127
+ const cells = line.split('|').map(c => c.trim()).filter(Boolean);
128
+ if (cells.length >= 3 && cells[0] !== 'candidate') {
129
+ candidates.push({
130
+ candidate: cells[0],
131
+ category: cells[1] || 'unknown',
132
+ decision: cells[2] || 'unknown',
133
+ caseIds: cells[3] || '',
134
+ reason: cells[4] || '',
135
+ });
136
+ }
137
+ }
138
+ }
139
+ return candidates;
140
+ }
141
+
142
+ /**
143
+ * 从 test-matrix.md 提取 Deferred Items
144
+ */
145
+ function extractDeferredItems(content) {
146
+ const items = [];
147
+ const lines = content.split('\n');
148
+ let inDeferred = false;
149
+
150
+ for (const line of lines) {
151
+ if (line.match(/## Deferred Items/i)) {
152
+ inDeferred = true;
153
+ continue;
154
+ }
155
+ if (inDeferred && line.startsWith('## ')) {
156
+ break;
157
+ }
158
+ if (inDeferred && line.startsWith('|') && !line.match(/^\|\s*[-:]+/)) {
159
+ const cells = line.split('|').map(c => c.trim()).filter(Boolean);
160
+ if (cells.length >= 3 && cells[0] !== 'case_id') {
161
+ items.push({
162
+ caseId: cells[0],
163
+ behavior: cells[1] || '',
164
+ designMethod: cells[2] || '',
165
+ reason: cells[3] || '',
166
+ deferredSince: cells[4] || new Date().toISOString().slice(0, 10),
167
+ });
168
+ }
169
+ }
170
+ }
171
+ return items;
172
+ }
173
+
174
+ /**
175
+ * Step 2: mergeBaselines — 增量合并到 baselines/{module}.md
176
+ */
177
+ function mergeBaselines(ledgerDir, changeName, moduleSections, candidateLedger, deferredItems) {
178
+ const baselinesDir = join(ledgerDir, 'baselines');
179
+ if (!existsSync(baselinesDir)) {
180
+ mkdirSync(baselinesDir, { recursive: true });
181
+ }
182
+
183
+ const results = {};
184
+ const today = new Date().toISOString().slice(0, 10);
185
+
186
+ for (const [moduleName, sectionContent] of Object.entries(moduleSections)) {
187
+ const safeName = moduleName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
188
+ const baselinePath = join(baselinesDir, `${safeName}.md`);
189
+ const moduleCandidates = candidateLedger.filter(c =>
190
+ c.candidate.toLowerCase().includes(moduleName.toLowerCase())
191
+ );
192
+ const moduleDeferred = deferredItems.filter(d =>
193
+ d.caseId.toLowerCase().startsWith(moduleName.toLowerCase().slice(0, 3))
194
+ );
195
+
196
+ if (existsSync(baselinePath)) {
197
+ // 增量合并:读取现有 baseline,追加新 case
198
+ const existing = readFileSync(baselinePath, 'utf-8');
199
+ const updated = mergeExistingBaseline(existing, sectionContent, changeName, today, moduleCandidates, moduleDeferred);
200
+ if (!updated) continue; // no changes
201
+ writeFileSync(baselinePath, updated, 'utf-8');
202
+ results[moduleName] = 'updated';
203
+ } else {
204
+ // 新建 baseline
205
+ const newBaseline = createNewBaseline(moduleName, sectionContent, changeName, today, moduleCandidates, moduleDeferred);
206
+ writeFileSync(baselinePath, newBaseline, 'utf-8');
207
+ results[moduleName] = 'created';
208
+ }
209
+ }
210
+ return results;
211
+ }
212
+
213
+ /**
214
+ * 创建新 baseline 文件
215
+ */
216
+ function createNewBaseline(moduleName, sectionContent, changeName, date, candidates, deferred) {
217
+ const lines = [];
218
+ lines.push(`# ${moduleName} Test Baseline`);
219
+ lines.push('');
220
+ lines.push(`> schema_version: 1`);
221
+ lines.push(`> last_updated_by_change: ${changeName}`);
222
+ lines.push(`> last_updated: ${date}`);
223
+ lines.push('');
224
+ lines.push('## Current Cases(当前态 — 覆盖式)');
225
+ lines.push('');
226
+ lines.push('| case_id | behavior | design_method | test_kind | test_tier | work_mode | test_file | test_method_name | source_change |');
227
+ lines.push('|---|---|---|---|---|---|---|---|---|');
228
+
229
+ // 从 sectionContent 提取 case 行并添加 source_change 列
230
+ const caseLines = sectionContent.split('\n').filter(l =>
231
+ l.startsWith('|') && !l.match(/^\|\s*[-:]+/) && !l.match(/\|\s*case_id/)
232
+ );
233
+ for (const line of caseLines) {
234
+ const cells = line.split('|').map(c => c.trim()).filter(Boolean);
235
+ if (cells.length >= 10) {
236
+ lines.push(`| ${cells[0]} | ${cells[1]} | ${cells[2]} | ${cells[5]} | ${cells[6]} | ${cells[8]} | ${cells[9]} | ${cells[10]} | ${changeName} |`);
237
+ }
238
+ }
239
+
240
+ if (deferred.length > 0) {
241
+ lines.push('');
242
+ lines.push('## Deferred Items');
243
+ lines.push('');
244
+ lines.push('| case_id | behavior | design_method | reason | deferred_since |');
245
+ lines.push('|---|---|---|---|---|');
246
+ for (const d of deferred) {
247
+ lines.push(`| ${d.caseId} | ${d.behavior} | ${d.designMethod} | ${d.reason} | ${d.deferredSince} |`);
248
+ }
249
+ }
250
+
251
+ lines.push('');
252
+ lines.push('## Evolution Log(append-only)');
253
+ lines.push('');
254
+ const caseCount = caseLines.length;
255
+ lines.push(`- **${date}** [${changeName}]: 新增 ${caseCount} case, defer ${deferred.length}`);
256
+
257
+ return lines.join('\n') + '\n';
258
+ }
259
+
260
+ /**
261
+ * 合并现有 baseline(增量更新 Current Cases + 追加 Evolution Log)
262
+ */
263
+ function mergeExistingBaseline(existing, sectionContent, changeName, date, candidates, deferred) {
264
+ const lines = [];
265
+ const existingLines = existing.split('\n');
266
+
267
+ // 更新 frontmatter
268
+ let inFrontmatter = false;
269
+ let frontmatterDone = false;
270
+ for (const line of existingLines) {
271
+ if (line.startsWith('> last_updated_by_change:')) {
272
+ lines.push(`> last_updated_by_change: ${changeName}`);
273
+ } else if (line.startsWith('> last_updated:')) {
274
+ lines.push(`> last_updated: ${date}`);
275
+ } 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
+ );
290
+ 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
+ }
295
+ }
296
+ lines.push(line);
297
+ } else if (inFrontmatter && line.startsWith('|')) {
298
+ // 跳过旧表格行(已替换)
299
+ continue;
300
+ } else if (line.startsWith('## Evolution Log')) {
301
+ 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) {
312
+ lines.push(line);
313
+ }
314
+ }
315
+
316
+ return lines.join('\n') + '\n';
317
+ }
318
+
319
+ /**
320
+ * Step 3: resolveDeferred — 新 case 覆盖了旧 deferred 项
321
+ */
322
+ function resolveDeferred(ledgerDir, changeName, newCaseIds) {
323
+ const baselinesDir = join(ledgerDir, 'baselines');
324
+ if (!existsSync(baselinesDir)) return { resolved: 0 };
325
+
326
+ let resolved = 0;
327
+ for (const file of readdirSync(baselinesDir)) {
328
+ if (!file.endsWith('.md')) continue;
329
+ const fp = join(baselinesDir, file);
330
+ let content = readFileSync(fp, 'utf-8');
331
+ let changed = false;
332
+
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++;
340
+ }
341
+ }
342
+
343
+ if (changed) {
344
+ writeFileSync(fp, content, 'utf-8');
345
+ }
346
+ }
347
+ return { resolved };
348
+ }
349
+
350
+ function escapeRegex(str) {
351
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
352
+ }
353
+
354
+ /**
355
+ * Step 4: appendChangelog — 归档 test-matrix.md
356
+ */
357
+ function appendChangelog(ledgerDir, changeName, matrixContent) {
358
+ const changelogDir = join(ledgerDir, 'changelog');
359
+ if (!existsSync(changelogDir)) {
360
+ mkdirSync(changelogDir, { recursive: true });
361
+ }
362
+ const destPath = join(changelogDir, `${changeName}.md`);
363
+ writeFileSync(destPath, matrixContent, 'utf-8');
364
+ return destPath;
365
+ }
366
+
367
+ /**
368
+ * Step 5: rewriteIndex — 重写 INDEX.md
369
+ */
370
+ function rewriteIndex(ledgerDir) {
371
+ const baselinesDir = join(ledgerDir, 'baselines');
372
+ const indexPath = join(ledgerDir, 'INDEX.md');
373
+ const today = new Date().toISOString().slice(0, 10);
374
+
375
+ const modules = [];
376
+ if (existsSync(baselinesDir)) {
377
+ for (const file of readdirSync(baselinesDir)) {
378
+ if (!file.endsWith('.md')) continue;
379
+ const content = readFileSync(join(baselinesDir, file), 'utf-8');
380
+ const moduleName = file.replace('.md', '').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
381
+
382
+ // 统计 case 数
383
+ const caseMatches = content.match(/\|\s*\S+-\S+-\S+-\d+/g) || [];
384
+ const caseCount = caseMatches.length;
385
+
386
+ // 统计 deferred 数
387
+ const deferredSection = content.split('## Deferred Items')[1] || '';
388
+ const deferredMatches = deferredSection.match(/\|\s*\S+-\S+-\S+-\d+/g) || [];
389
+ const deferredCount = deferredMatches.length;
390
+
391
+ // 提取 last_updated_by_change
392
+ const changeMatch = content.match(/last_updated_by_change:\s*(.+)/);
393
+ const lastChange = changeMatch ? changeMatch[1].trim() : 'unknown';
394
+ const dateMatch = content.match(/last_updated:\s*(.+)/);
395
+ const lastDate = dateMatch ? dateMatch[1].trim() : 'unknown';
396
+
397
+ // coverage_status
398
+ let status = '✅ baseline';
399
+ if (deferredCount > 0) status = '⚠️ partial';
400
+ if (caseCount === 0) status = '⏭️ not_applicable';
401
+
402
+ // test_tier_breakdown
403
+ const unitCount = (content.match(/unit/gi) || []).length;
404
+ const intCount = (content.match(/integration/gi) || []).length;
405
+
406
+ modules.push({ moduleName, caseCount, deferredCount, lastChange, lastDate, status, unitCount, intCount });
407
+ }
408
+ }
409
+
410
+ const totalCases = modules.reduce((sum, m) => sum + m.caseCount, 0);
411
+ const totalDeferred = modules.reduce((sum, m) => sum + m.deferredCount, 0);
412
+
413
+ const lines = [];
414
+ lines.push('# Test Ledger Index');
415
+ lines.push('');
416
+ lines.push('> Auto-generated by `tf test-merge`. Do not edit manually.');
417
+ lines.push('');
418
+ lines.push(`**Modules**: ${modules.length} | **Total Cases**: ${totalCases} | **Deferred**: ${totalDeferred} | **Last Updated**: ${today}`);
419
+ lines.push('');
420
+ lines.push('| module | test_tier_breakdown | case_count | deferred | last_change | last_date | coverage_status |');
421
+ lines.push('|---|---|---|---|---|---|---|');
422
+
423
+ 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} |`);
425
+ }
426
+
427
+ writeFileSync(indexPath, lines.join('\n') + '\n', 'utf-8');
428
+ return { modules: modules.length, totalCases, totalDeferred };
429
+ }
430
+
431
+ /**
432
+ * Step 6: gitCommit — 原子性提交
433
+ */
434
+ function gitCommit(projectRoot, changeName, dryRun) {
435
+ const msg = `test-ledger: merge test results from ${changeName}`;
436
+ if (dryRun) {
437
+ console.log(`[DRY-RUN] Would commit: git add docs/test-ledger/ && git commit -m "${msg}"`);
438
+ return;
439
+ }
440
+ try {
441
+ execSync('git add docs/test-ledger/', { cwd: projectRoot, stdio: 'pipe' });
442
+ execSync(`git commit -m "${msg}"`, { cwd: projectRoot, stdio: 'pipe' });
443
+ console.log(`✅ test-ledger committed: ${msg}`);
444
+ } catch (err) {
445
+ // Nothing to commit is acceptable (e.g., no changes)
446
+ if (err.message && err.message.includes('nothing to commit')) {
447
+ console.log('ℹ️ No changes to commit in test-ledger');
448
+ } else {
449
+ console.error(`⚠️ git commit warning: ${err.message}`);
450
+ }
451
+ }
452
+ }
453
+
454
+ /**
455
+ * CLI entry point — export as `run()` for registration
456
+ */
457
+ export async function run(args) {
458
+ const argv = parseArgv(args || process.argv.slice(2));
459
+ const projectRoot = resolve(argv.projectRoot || process.cwd());
460
+
461
+ // Delegate to main()
462
+ await main(argv, projectRoot);
463
+ }
464
+
465
+ /**
466
+ * 主入口
467
+ */
468
+ async function main(argv, projectRoot) {
469
+ const changeDir = argv._[0];
470
+ const dryRun = argv.dryRun || false;
471
+
472
+ if (!changeDir) {
473
+ console.error('Usage: tf test-merge <change-dir> [--project-root <path>] [--dry-run]');
474
+ process.exit(2);
475
+ }
476
+
477
+ const absChangeDir = resolve(changeDir);
478
+ const changeName = extractChangeName(absChangeDir);
479
+ const ledgerDir = join(projectRoot, 'docs', 'test-ledger');
480
+
481
+ console.log(`📋 test-merge: ${changeName} → ${ledgerDir}`);
482
+
483
+ // Step 1: preCheck
484
+ const preCheckResult = preCheck(absChangeDir);
485
+ if (!preCheckResult.pass) {
486
+ console.log(`⏭️ ${preCheckResult.reason}`);
487
+ process.exit(0);
488
+ }
489
+ console.log('✅ Step 1: preCheck passed');
490
+
491
+ const matrixContent = preCheckResult.content;
492
+ const summary = extractSummary(matrixContent);
493
+ const moduleSections = extractModuleSections(matrixContent);
494
+ const candidateLedger = extractCandidateLedger(matrixContent);
495
+ const deferredItems = extractDeferredItems(matrixContent);
496
+
497
+ console.log(` Summary: ${summary.totalCases} cases, ${summary.modules} modules, ${summary.deferred} deferred`);
498
+
499
+ // Ensure ledger directory exists
500
+ if (!existsSync(ledgerDir)) {
501
+ mkdirSync(ledgerDir, { recursive: true });
502
+ }
503
+
504
+ // Step 2: mergeBaselines
505
+ const mergeResults = mergeBaselines(ledgerDir, changeName, moduleSections, candidateLedger, deferredItems);
506
+ console.log(`✅ Step 2: mergeBaselines — ${Object.keys(mergeResults).length} modules processed`);
507
+
508
+ // Step 3: resolveDeferred
509
+ const newCaseIds = [];
510
+ for (const section of Object.values(moduleSections)) {
511
+ const lines = section.split('\n');
512
+ for (const line of lines) {
513
+ if (line.startsWith('|') && !line.match(/^\|\s*[-:]+/) && !line.match(/\|\s*case_id/)) {
514
+ const cells = line.split('|').map(c => c.trim()).filter(Boolean);
515
+ if (cells[0]) newCaseIds.push(cells[0]);
516
+ }
517
+ }
518
+ }
519
+ const deferredResult = resolveDeferred(ledgerDir, changeName, newCaseIds);
520
+ console.log(`✅ Step 3: resolveDeferred — ${deferredResult.resolved} items resolved`);
521
+
522
+ // Step 4: appendChangelog
523
+ const changelogPath = appendChangelog(ledgerDir, changeName, matrixContent);
524
+ console.log(`✅ Step 4: appendChangelog — ${relative(projectRoot, changelogPath)}`);
525
+
526
+ // Step 5: rewriteIndex
527
+ const indexResult = rewriteIndex(ledgerDir);
528
+ console.log(`✅ Step 5: rewriteIndex — ${indexResult.modules} modules, ${indexResult.totalCases} total cases`);
529
+
530
+ // Step 6: gitCommit
531
+ gitCommit(projectRoot, changeName, dryRun);
532
+ console.log('✅ Step 6: gitCommit');
533
+
534
+ console.log(`\n🎉 test-merge complete for ${changeName}`);
535
+ }
536
+
537
+ main().catch(err => {
538
+ console.error('test-merge error:', err.message);
539
+ process.exit(1);
540
+ });