@xulthekl/team-flow 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/always/phase-guard.md +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/marketplace.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.github/plugin/marketplace.json +2 -2
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +38 -0
- package/GEMINI.md +1 -1
- package/INSTALL.md +1 -1
- package/README.md +2 -2
- package/agents/build-executor.md +1 -0
- package/docs/README_en.md +1 -1
- package/gemini-extension.json +1 -1
- package/hooks/session-start +2 -2
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/plugin.json +2 -2
- package/scripts/guard/checks/test-matrix-complete.mjs +67 -0
- package/scripts/guard/guard.mjs +5 -1
- package/scripts/lib/cmd-state.mjs +9 -4
- package/scripts/lib/hash.mjs +11 -0
- package/scripts/lib/state-loader.mjs +12 -1
- package/scripts/lib/test-matrix-export.mjs +231 -0
- package/scripts/lib/test-merge.mjs +540 -0
- package/scripts/team-flow.mjs +6 -0
- package/skills/build-executor/implementer-prompt.md +38 -3
- package/skills/code-reviewer/SKILL.md +28 -1
- package/skills/code-reviewer/code-reviewer-prompt.md +10 -0
- package/skills/contract-builder/SKILL.md +76 -0
- package/skills/release-archivist/SKILL.md +35 -2
- package/skills/spec-writer/SKILL.md +3 -1
- package/skills/test-strategy/SKILL.md +70 -0
- package/skills/test-strategy/references/adversarial-patterns.md +0 -0
- package/skills/test-strategy/references/complexity-grading.md +137 -0
- package/skills/test-strategy/references/design-methods-detail.md +183 -0
- package/skills/workflow-orchestrator/references/s1-path-router.md +4 -0
- package/skills/workflow-start/references/routing-rules.md +17 -0
- package/tests/lib/cmd-install-workbuddy.test.mjs +1 -1
|
@@ -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
|
+
});
|
package/scripts/team-flow.mjs
CHANGED
|
@@ -34,6 +34,8 @@ const COMMANDS = {
|
|
|
34
34
|
'install-zcode': () => import('./lib/cmd-install-zcode.mjs'),
|
|
35
35
|
'prototype-sync': () => import('./lib/prototype-sync.mjs'),
|
|
36
36
|
'arch-merge': () => import('./lib/arch-merge.mjs'),
|
|
37
|
+
'test-merge': () => import('./lib/test-merge.mjs'),
|
|
38
|
+
'test-matrix-export': () => import('./lib/test-matrix-export.mjs'),
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
const HELP = `team-flow (tf) — Unified workflow plugin CLI
|
|
@@ -50,6 +52,10 @@ Commands:
|
|
|
50
52
|
Merge UX delta into global prototype/ + design-system.md
|
|
51
53
|
arch-merge <change-dir> [--project-root <path>] [--dry-run]
|
|
52
54
|
Merge architecture delta into global docs/architecture/
|
|
55
|
+
test-merge <change-dir> [--project-root <path>] [--dry-run]
|
|
56
|
+
Merge test matrix results into global docs/test-ledger/
|
|
57
|
+
test-matrix-export <input.json> <output.md> [--change-id <id>]
|
|
58
|
+
Convert glaf4 test-matrix.json to team-flow test-matrix.md
|
|
53
59
|
config [options] Display or modify configuration
|
|
54
60
|
config --resolve-model <profile> Resolve a configured model profile without switching models
|
|
55
61
|
state <sub> <dir> Manage .team-flow.yaml state (init|check|transition|get|rebuild)
|
|
@@ -40,13 +40,42 @@ Subagent (general-purpose):
|
|
|
40
40
|
## Your Job
|
|
41
41
|
|
|
42
42
|
Once you're clear on requirements:
|
|
43
|
-
1.
|
|
44
|
-
2.
|
|
45
|
-
3. Verify implementation works
|
|
43
|
+
1. Read test-matrix.md for this task's module section (if it exists)
|
|
44
|
+
2. Implement exactly what the task specifies, following the Test Matrix Protocol below
|
|
45
|
+
3. Verify implementation works (all matrix cases pass)
|
|
46
46
|
4. Commit your work
|
|
47
47
|
5. Self-review (see below)
|
|
48
48
|
6. Report back
|
|
49
49
|
|
|
50
|
+
## Test Matrix Protocol (v0.12 §44.2)
|
|
51
|
+
|
|
52
|
+
If `test-matrix.md` exists, read the module section relevant to your task.
|
|
53
|
+
For each case in the matrix, follow the protocol matching its `work_mode`:
|
|
54
|
+
|
|
55
|
+
**work_mode=TDD** (new behavior):
|
|
56
|
+
1. RED: Write failing test matching `case_id` + `test_method_name`, confirm failure
|
|
57
|
+
2. GREEN: Implement minimum code to pass
|
|
58
|
+
3. REFACTOR: Clean up, suite stays green
|
|
59
|
+
4. Report TDD Evidence: RED command + failure output, GREEN command + pass output
|
|
60
|
+
|
|
61
|
+
**work_mode=CHARACTERIZATION** (legacy behavior capture):
|
|
62
|
+
1. Write test capturing current behavior — do NOT change production code
|
|
63
|
+
2. Report: `current_behavior_note` + test output
|
|
64
|
+
3. `production_change_allowed=false` — if test reveals a bug, report it but do NOT fix
|
|
65
|
+
|
|
66
|
+
**work_mode=REGRESSION** (defect reproduction + fix):
|
|
67
|
+
1. Write failing test reproducing the defect
|
|
68
|
+
2. Fix production code, confirm green
|
|
69
|
+
3. Report: defect reproduction + fix evidence
|
|
70
|
+
|
|
71
|
+
After all cases: self-check matrix coverage = passed cases / total cases in matrix for your module.
|
|
72
|
+
|
|
73
|
+
**两不原则 (Two Prohibitions)**:
|
|
74
|
+
- ⛔ DO NOT write production code and test code simultaneously — RED first, then GREEN
|
|
75
|
+
- ⛔ DO NOT skip the RED failure confirmation — you must see the test fail before implementing
|
|
76
|
+
|
|
77
|
+
If `test-matrix.md` does NOT exist, fall back to standard TDD (Law 2): write tests first, then implement.
|
|
78
|
+
|
|
50
79
|
Work from: [directory]
|
|
51
80
|
|
|
52
81
|
**While you work:** If you encounter something unexpected or unclear, **ask questions**.
|
|
@@ -109,6 +138,8 @@ Subagent (general-purpose):
|
|
|
109
138
|
- Did I follow TDD if required?
|
|
110
139
|
- Are tests comprehensive?
|
|
111
140
|
- Is the test output pristine (no stray warnings or noise)?
|
|
141
|
+
- If test-matrix.md exists: matrix coverage = passed cases / total cases for my module
|
|
142
|
+
- Each case's `design_method` matches the actual test approach (boundary case uses real boundary values, not happy-path disguised as boundary)
|
|
112
143
|
|
|
113
144
|
If you find issues during self-review, fix them now before reporting.
|
|
114
145
|
|
|
@@ -126,6 +157,10 @@ Subagent (general-purpose):
|
|
|
126
157
|
- **TDD Evidence** (if TDD was required for this task):
|
|
127
158
|
- RED: command run, relevant failing output before implementation, and why the failure was expected
|
|
128
159
|
- GREEN: command run and relevant passing output after implementation
|
|
160
|
+
- **Test Matrix Coverage** (if test-matrix.md exists):
|
|
161
|
+
- Cases implemented: N/M (list case_ids)
|
|
162
|
+
- Cases deferred: N (list case_ids + reason)
|
|
163
|
+
- Any case where expected_red did not match actual failure — report mismatch
|
|
129
164
|
- Files changed
|
|
130
165
|
- Self-review findings (if any)
|
|
131
166
|
- Any issues or concerns
|
|
@@ -134,6 +134,31 @@ Check for:
|
|
|
134
134
|
- **Edge cases**: Tests cover boundary conditions, error cases
|
|
135
135
|
- **Test quality**: Tests are meaningful (not just "it works"), assertions are specific
|
|
136
136
|
|
|
137
|
+
### Step 5b: Test Matrix Compliance (v0.12 §44.3)
|
|
138
|
+
|
|
139
|
+
If `test-matrix.md` exists in the change directory, audit the implementation against it:
|
|
140
|
+
|
|
141
|
+
1. **Case-by-case verification**: For each case in the matrix:
|
|
142
|
+
- Check if the test implementation exists (`test_file` + `test_method_name` match)
|
|
143
|
+
- Verify assertions match the matrix's `expected` output
|
|
144
|
+
- Verify `design_method` matches actual test approach (a `boundary` case must use real boundary values, not a happy-path disguised as boundary)
|
|
145
|
+
|
|
146
|
+
2. **Coverage calculation**:
|
|
147
|
+
- Matrix coverage = implemented cases / total cases in matrix
|
|
148
|
+
- Matrix coverage < 100% → **Critical finding** (test-matrix-gap)
|
|
149
|
+
- List missing case_ids
|
|
150
|
+
|
|
151
|
+
3. **Candidate Coverage Ledger audit**:
|
|
152
|
+
- Entries with `decision=deferred` have a reasonable `reason`?
|
|
153
|
+
- Entries with `decision=covered` actually have corresponding tests?
|
|
154
|
+
|
|
155
|
+
4. **Pyramid ratio check**:
|
|
156
|
+
- unit cases: 70-80% of total
|
|
157
|
+
- integration cases: ≤30% of total
|
|
158
|
+
- Significant deviation → **Important finding** (test-pyramid-imbalance)
|
|
159
|
+
|
|
160
|
+
If `test-matrix.md` does NOT exist, skip this step silently (legacy change compatibility).
|
|
161
|
+
|
|
137
162
|
### Step 6: Documentation Review
|
|
138
163
|
|
|
139
164
|
Check for:
|
|
@@ -156,7 +181,9 @@ Check for:
|
|
|
156
181
|
|---------|-----------|
|
|
157
182
|
| **PASS** | No Critical or Important findings |
|
|
158
183
|
| **PASS_WITH_WARNINGS** | No Critical, but Important findings exist |
|
|
159
|
-
| **FAIL** | Any Critical finding |
|
|
184
|
+
| **FAIL** | Any Critical finding (including Test Matrix Compliance gaps — v0.12 §44.3) |
|
|
185
|
+
|
|
186
|
+
Test Matrix Compliance Critical findings carry the same weight as Spec Compliance violations — matrix gaps are always Critical, never Important.
|
|
160
187
|
|
|
161
188
|
## Calibration Rules
|
|
162
189
|
|
|
@@ -68,6 +68,16 @@ Subagent (general-purpose):
|
|
|
68
68
|
- Integration tests where they matter?
|
|
69
69
|
- All tests passing?
|
|
70
70
|
|
|
71
|
+
**Test Matrix Compliance (v0.12 §44.3):**
|
|
72
|
+
If `test-matrix.md` exists, audit against it:
|
|
73
|
+
- Each case has a corresponding test implementation (test_file + test_method_name match)?
|
|
74
|
+
- Assertions match the matrix's `expected` output?
|
|
75
|
+
- `design_method` matches actual test approach (boundary case uses real boundary values)?
|
|
76
|
+
- Matrix coverage < 100% → Critical finding (test-matrix-gap)
|
|
77
|
+
- Candidate Coverage Ledger: deferred items have reasonable reasons?
|
|
78
|
+
- Pyramid ratio: unit 70-80%, integration ≤30%?
|
|
79
|
+
If `test-matrix.md` does NOT exist, skip this section silently.
|
|
80
|
+
|
|
71
81
|
**Production readiness:**
|
|
72
82
|
- Migration strategy if schema changed?
|
|
73
83
|
- Backward compatibility considered?
|