@xulthekl/team-flow 0.29.2 → 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 +3 -3
- 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 +2 -2
- package/.github/plugin/marketplace.json +2 -2
- package/AGENTS.md +10 -4
- package/CHANGELOG.md +88 -0
- package/GEMINI.md +1 -1
- package/HANDOFF.md +4 -4
- package/INSTALL.md +1 -1
- package/README.md +5 -5
- package/agents/architecture-design.md +1 -0
- package/agents/build-executor.md +81 -0
- package/agents/contract-builder.md +78 -0
- package/agents/cross-change-consistency-checker.md +1 -1
- package/agents/need-explorer.md +67 -0
- package/agents/release-archivist.md +82 -0
- package/agents/spec-writer.md +83 -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 +2 -2
- package/plugin.json +2 -2
- package/scripts/ensure-branch.mjs +200 -42
- package/scripts/guard/checks/test-matrix-complete.mjs +67 -0
- package/scripts/guard/guard.mjs +5 -1
- package/scripts/lib/cmd-doctor.mjs +40 -1
- package/scripts/lib/cmd-state.mjs +33 -13
- package/scripts/lib/hash.mjs +11 -0
- package/scripts/lib/state-loader.mjs +34 -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/SKILL.md +14 -4
- 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 +78 -0
- package/skills/need-explorer/SKILL.md +2 -0
- package/skills/release-archivist/SKILL.md +37 -2
- package/skills/spec-writer/SKILL.md +5 -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/SKILL.md +29 -15
- package/skills/workflow-start/references/routing-rules.md +29 -6
- package/tests/lib/cmd-install-workbuddy.test.mjs +1 -1
- package/tests/lib/ensure-branch.test.mjs +52 -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)
|
|
@@ -27,17 +27,27 @@ Branch/worktree preflight before ANY implementation edit (mandatory — do not s
|
|
|
27
27
|
```bash
|
|
28
28
|
tf isolate <change-dir>
|
|
29
29
|
```
|
|
30
|
-
This script enforces git isolation
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
This script enforces git isolation. In a **multi-repo workspace** (a workspace root
|
|
31
|
+
whose direct subdirectories are independent code repositories, e.g. `bff-*`/`ui-*`/
|
|
32
|
+
`prototype/`, while the root itself is often a near-empty git repo), it creates one git
|
|
33
|
+
worktree per code repo under `<workspace>/.worktrees/<change>/<repo>` — each based on
|
|
34
|
+
that repo's current branch — so the implementation subagent enters that aggregated
|
|
35
|
+
directory and sees reference code from every repo with the same layout as the main
|
|
36
|
+
workspace. In a **single code repository** it falls back to creating one git worktree
|
|
37
|
+
(preferred) or a new branch. It exits non-zero if it cannot isolate and you have not
|
|
38
|
+
approved `--force`.
|
|
33
39
|
2. If `tf isolate` exits non-zero: STOP. Do not edit `main`/`master` in place.
|
|
34
40
|
Ask the user for explicit approval (and re-run with `tf isolate <change-dir> --force`
|
|
35
41
|
only after they approve).
|
|
36
|
-
3. If it succeeds, report the chosen branch/worktree
|
|
42
|
+
3. If it succeeds, report the chosen branch/worktree — in a multi-repo workspace, the
|
|
43
|
+
aggregated `<workspace>/.worktrees/<change>/` directory — and make all implementation
|
|
37
44
|
edits there.
|
|
38
45
|
|
|
39
46
|
## Core Laws
|
|
40
47
|
|
|
48
|
+
### Law 0: State Field Boundary (v0.30.0)
|
|
49
|
+
本 skill 仅通过 `tf state set` 写自己的 `dp_5_*` 决策字段;**MUST NOT** 修改 `.team-flow.yaml` 的 `state`/`workflow` 核心字段——状态转换是主代理(workflow-start)经 `tf state transition` 的专有职责(来源:workflow-feedback 2026-08-01)。
|
|
50
|
+
|
|
41
51
|
### Law 1: Contract First
|
|
42
52
|
The execution contract is the approved handoff artifact, not chat history.
|
|
43
53
|
|
|
@@ -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
|