kld-sdd 2.6.14 → 2.6.16

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 (33) hide show
  1. package/README.md +7 -5
  2. package/lib/init.js +118 -73
  3. package/lib/skills-bundle.js +3 -0
  4. package/package.json +3 -2
  5. package/skywalk-sdd/index.cjs +936 -109
  6. package/skywalk-sdd/metrics-v3.cjs +103 -15
  7. package/skywalk-sdd/openspec-shim.cjs +48 -0
  8. package/skywalk-sdd/reporting/change-report-markdown.cjs +294 -0
  9. package/skywalk-sdd/reporting/change-report-model.cjs +170 -27
  10. package/skywalk-sdd/reporting/change-report-renderer.cjs +78 -154
  11. package/skywalk-sdd/reporting/change-report-view-model.cjs +340 -0
  12. package/templates/git-hooks/commit-msg +46 -0
  13. package/templates/git-hooks/pre-commit +57 -0
  14. package/templates/git-hooks/pre-commit-consistency-check.cjs +193 -0
  15. package/templates/git-hooks/pre-push +57 -0
  16. package/templates/git-hooks/pre-push-consistency-check.cjs +197 -0
  17. package/templates/hooks/codebuddy/hooks/hook-gate-core.cjs +369 -0
  18. package/templates/hooks/codebuddy/hooks/sdd-apply-test-gate.cjs +41 -0
  19. package/templates/hooks/codebuddy/hooks/sdd-mid-checkpoint.cjs +108 -0
  20. package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +36 -1
  21. package/templates/hooks/codebuddy/hooks/sdd-tdd-rhythm-gate.cjs +248 -0
  22. package/templates/hooks/codebuddy/settings.json +8 -0
  23. package/templates/skills/kld-sdd/opsx-apply/SKILL.md +13 -0
  24. package/templates/skills/kld-sdd/opsx-apply/checklist.md +15 -0
  25. package/templates/skills/kld-sdd/opsx-apply/reference.md +45 -2
  26. package/templates/skills/kld-sdd/opsx-archive/SKILL.md +1 -1
  27. package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +592 -0
  28. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +3 -3
  29. package/templates/skills/kld-sdd/opsx-propose/reference.md +6 -7
  30. package/templates/skills/kld-sdd/tdd-core/reference.md +3 -1
  31. package/templates/skills/kld-sdd/tdd-rules/SKILL.md +1 -0
  32. package/templates/skills/kld-sdd/tdd-rules/rules/tdd-rhythm-enforcement.md +101 -0
  33. package/templates/skills/kld-sdd/tdd-rules/rules/test-skeleton-telemetry.md +1 -1
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * SDD Mid-Apply Checkpoint Hook
6
+ *
7
+ * 每完成 10 个 TDD 任务后强制检查点,验证 telemetry 证据链完整性。
8
+ * 不阻断,仅输出警告提醒。
9
+ *
10
+ * 触发点:PostToolUse(Bash) - 当检测到测试命令执行后
11
+ * 退出码:0(不阻断)
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const core = require('./hook-gate-core.cjs');
17
+
18
+ const CHECKPOINT_INTERVAL = 10;
19
+
20
+ function loadChangeEvents(projectRoot, changeName) {
21
+ const skywalk = core.resolveSkywalkDir(projectRoot);
22
+ const safe = core.safeChangeName(changeName);
23
+ const dir = skywalk
24
+ ? path.join(skywalk, 'events', safe)
25
+ : path.join(projectRoot, 'skywalk-sdd', 'events', safe);
26
+ if (!fs.existsSync(dir)) return [];
27
+ const events = [];
28
+ for (const file of fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'))) {
29
+ try {
30
+ const lines = fs.readFileSync(path.join(dir, file), 'utf8').split('\n').filter(Boolean);
31
+ for (const line of lines) { try { events.push(JSON.parse(line)); } catch {} }
32
+ } catch {}
33
+ }
34
+ return events;
35
+ }
36
+
37
+ function latestActiveStage(projectRoot) {
38
+ const skywalk = core.resolveSkywalkDir(projectRoot);
39
+ const stateDir = skywalk
40
+ ? path.join(skywalk, 'state')
41
+ : path.join(projectRoot, 'skywalk-sdd', 'state');
42
+ if (!fs.existsSync(stateDir)) return null;
43
+ return fs.readdirSync(stateDir)
44
+ .filter(f => f.endsWith('.json'))
45
+ .map(f => { try { return JSON.parse(fs.readFileSync(path.join(stateDir, f), 'utf8')).event; } catch { return null; } })
46
+ .filter(Boolean)
47
+ .filter(e => e.command === 'apply')
48
+ .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())[0] || null;
49
+ }
50
+
51
+ if (require.main === module) {
52
+ const parsed = core.parseHookInput(core.readStdin(), { strict: false });
53
+ const input = core.normalizeHookInput(parsed.input || {});
54
+ const toolInput = input.tool_input || input.toolInput || {};
55
+ const command = String(toolInput.command || '');
56
+
57
+ if (!/\b(npm|pnpm|yarn)\s+(test|run\s+test)\b|\bpytest\b|\bmvn\b.*\btest\b|\bgo\s+test\b|\bcargo\s+test\b/i.test(command)) {
58
+ process.exit(0);
59
+ }
60
+
61
+ const projectRoot = core.getProjectRoot(input, 'codebuddy');
62
+ const activeStage = latestActiveStage(projectRoot);
63
+ if (!activeStage) process.exit(0);
64
+
65
+ const events = loadChangeEvents(projectRoot, activeStage.change);
66
+ const since = new Date(activeStage.timestamp).getTime();
67
+
68
+ const taskUpdates = events.filter(e =>
69
+ e.type === 'task_update' && e.status === 'completed' &&
70
+ new Date(e.timestamp || 0).getTime() >= since);
71
+ const completedCount = taskUpdates.length;
72
+
73
+ if (completedCount === 0 || completedCount % CHECKPOINT_INTERVAL !== 0) {
74
+ process.exit(0);
75
+ }
76
+
77
+ const testResults = events.filter(e =>
78
+ e.type === 'test_result' &&
79
+ new Date(e.timestamp || 0).getTime() >= since);
80
+
81
+ const redCount = testResults.filter(e =>
82
+ e.result === 'failure' && e.details && e.details.test_results &&
83
+ e.details.test_results.tdd_phase === 'red').length;
84
+ const greenCount = testResults.filter(e =>
85
+ e.result === 'success' && e.details && e.details.test_results &&
86
+ e.details.test_results.tdd_phase === 'green').length;
87
+
88
+ const expectedMin = Math.floor(completedCount * 0.8);
89
+ const actualEvidence = redCount + greenCount;
90
+
91
+ if (actualEvidence < expectedMin) {
92
+ console.log(JSON.stringify({
93
+ decision: 'allow',
94
+ message: '\n⚠️ TDD 中途检查点 [' + completedCount + ' 任务已完成]\n' +
95
+ '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
96
+ ' task_update 事件: ' + completedCount + '\n' +
97
+ ' test_result 事件: ' + actualEvidence + ' (RED: ' + redCount + ', GREEN: ' + greenCount + ')\n' +
98
+ ' 期望最少: ' + expectedMin + '\n\n' +
99
+ ' ⚠️ 测试证据不足!请确认你仍在遵循严格 RED→GREEN 循环。\n' +
100
+ ' 如果最近的任务跳过了 TDD,请回退并补做。\n' +
101
+ '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n',
102
+ }));
103
+ }
104
+
105
+ process.exit(0);
106
+ }
107
+
108
+ module.exports = { loadChangeEvents, latestActiveStage, CHECKPOINT_INTERVAL };
@@ -5,6 +5,7 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const crypto = require('crypto');
7
7
  const { execFileSync } = require('child_process');
8
+ const core = require('./hook-gate-core.cjs');
8
9
 
9
10
  function latestActiveStage(projectRoot) {
10
11
  const stateDir = path.join(projectRoot, 'skywalk-sdd', 'state');
@@ -76,6 +77,38 @@ function stableRunId(input, activeStage, command) {
76
77
  .slice(0, 20)}`;
77
78
  }
78
79
 
80
+ function inferTddPhase(projectRoot, activeStage, testResult) {
81
+ const taskId = activeStage.task_id;
82
+ if (!taskId) return 'regression';
83
+
84
+ const skywalk = core.resolveSkywalkDir(projectRoot);
85
+ const safe = core.safeChangeName(activeStage.change);
86
+ const tasksCandidates = [
87
+ path.join(projectRoot, 'openspec', 'changes', activeStage.change, 'tasks.md'),
88
+ path.join(projectRoot, 'changes', activeStage.change, 'tasks.md'),
89
+ ];
90
+ let tasksPath = null;
91
+ for (const p of tasksCandidates) {
92
+ if (fs.existsSync(p)) { tasksPath = p; break; }
93
+ }
94
+ if (!tasksPath) return 'regression';
95
+
96
+ try {
97
+ const content = fs.readFileSync(tasksPath, 'utf8');
98
+ const escaped = taskId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
99
+ const regex = new RegExp('### \\[' + escaped + '\\][\\s\\S]*?(?=### \\[TASK|$)');
100
+ const match = content.match(regex);
101
+ if (!match) return 'regression';
102
+ const block = match[0];
103
+ if (/测试-RED|test-red/i.test(block)) {
104
+ return testResult.result === 'failure' ? 'red' : 'red-passed-unexpectedly';
105
+ }
106
+ if (/实现-GREEN|impl-green/i.test(block)) return 'green';
107
+ if (/重构-REFACTOR|refactor/i.test(block)) return 'refactor';
108
+ } catch {}
109
+ return 'regression';
110
+ }
111
+
79
112
  function inferRecord(command) {
80
113
  if (/\b(log\.cjs|log\.js)\s+record\b/.test(command)) return null;
81
114
  if (/\b(npm|pnpm|yarn)\s+(test|run\s+test)\b|\bpytest\b|\bmvn\b.*\btest\b|\bgo\s+test\b|\bcargo\s+test\b/i.test(command)) {
@@ -114,7 +147,8 @@ function recordTelemetry(projectRoot, activeStage, record, result, exitCodeKnown
114
147
  : (typeof toolResponse.exitCode === 'number'
115
148
  ? toolResponse.exitCode
116
149
  : (typeof toolResponse.success === 'boolean' ? (toolResponse.success ? 0 : 1) : null));
117
- const tddPhase = toolInput.tdd_phase || input.tdd_phase || 'regression';
150
+ const tddPhase = toolInput.tdd_phase || input.tdd_phase ||
151
+ inferTddPhase(projectRoot, activeStage, inferred);
118
152
  details.test_results = {
119
153
  command: toolInput.command || record.details.command,
120
154
  tdd_phase: tddPhase,
@@ -194,6 +228,7 @@ if (require.main === module) {
194
228
  module.exports = {
195
229
  inferResult,
196
230
  inferRecord,
231
+ inferTddPhase,
197
232
  parseTestCounts,
198
233
  stableRunId,
199
234
  recordTelemetry,
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * SDD TDD Rhythm Gate Hook
6
+ *
7
+ * 当 agent 修改 tasks.md 中的任务状态时,校验 TDD 证据链。
8
+ * 仅在 TDD 模式的 apply 阶段对 RED/GREEN/REFACTOR 任务生效。
9
+ *
10
+ * 触发点:PreToolUse(Write|Edit)
11
+ * 退出码:0 放行 | 2 阻断
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const core = require('./hook-gate-core.cjs');
17
+
18
+ // ── 辅助函数 ──
19
+
20
+ function loadChangeEvents(projectRoot, changeName) {
21
+ const skywalk = core.resolveSkywalkDir(projectRoot);
22
+ const safe = core.safeChangeName(changeName);
23
+ const dir = skywalk
24
+ ? path.join(skywalk, 'events', safe)
25
+ : path.join(projectRoot, 'skywalk-sdd', 'events', safe);
26
+ if (!fs.existsSync(dir)) return [];
27
+ const events = [];
28
+ for (const file of fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'))) {
29
+ try {
30
+ const lines = fs.readFileSync(path.join(dir, file), 'utf8').split('\n').filter(Boolean);
31
+ for (const line of lines) {
32
+ try { events.push(JSON.parse(line)); } catch {}
33
+ }
34
+ } catch {}
35
+ }
36
+ return events;
37
+ }
38
+
39
+ function readTestStrategy(projectRoot, changeName) {
40
+ const candidates = [
41
+ path.join(projectRoot, 'openspec', 'changes', changeName, 'proposal.md'),
42
+ path.join(projectRoot, 'changes', changeName, 'proposal.md'),
43
+ ];
44
+ for (const p of candidates) {
45
+ if (!fs.existsSync(p)) continue;
46
+ try {
47
+ const content = fs.readFileSync(p, 'utf8');
48
+ const match = content.match(/test-strategy:\s*(\S+)/);
49
+ if (match) {
50
+ const strategy = match[1].toLowerCase().trim();
51
+ if (strategy === 'none' || strategy === 'impl-first') return strategy;
52
+ }
53
+ } catch {}
54
+ }
55
+ return 'tdd';
56
+ }
57
+
58
+ function detectTaskType(lines, taskLineIndex) {
59
+ for (let i = taskLineIndex; i < Math.min(taskLineIndex + 30, lines.length); i++) {
60
+ const line = lines[i];
61
+ if (/测试-RED|test-red/i.test(line)) return 'red';
62
+ if (/实现-GREEN|impl-green/i.test(line)) return 'green';
63
+ if (/重构-REFACTOR|refactor/i.test(line)) return 'refactor';
64
+ if (/配置|数据层|接口层|接口测试|scaffold|migration/i.test(line)) return 'non-tdd';
65
+ if (/^### \[TASK/.test(line) && i > taskLineIndex) break;
66
+ }
67
+ return 'unknown';
68
+ }
69
+
70
+ function findPairedRedTaskId(content, greenTaskId) {
71
+ const escaped = greenTaskId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
72
+ const regex = new RegExp(`### \\[${escaped}\\][\\s\\S]*?(?=### \\[TASK|$)`);
73
+ const match = content.match(regex);
74
+ if (!match) return null;
75
+ const depMatch = match[0].match(/- \*\*dependsOn\*\*:\s*(TASK-[A-Z]+-\d+)/);
76
+ return depMatch ? depMatch[1] : null;
77
+ }
78
+
79
+ function detectStatusChanges(oldContent, newContent) {
80
+ const changes = [];
81
+ const oldLines = oldContent.split('\n');
82
+ const newLines = newContent.split('\n');
83
+ let currentTaskId = null;
84
+ let currentTaskType = null;
85
+ for (let i = 0; i < Math.max(oldLines.length, newLines.length); i++) {
86
+ const oldLine = oldLines[i] || '';
87
+ const newLine = newLines[i] || '';
88
+ const taskMatch = newLine.match(/^### \[(TASK-[A-Z]+-\d+)\]/);
89
+ if (taskMatch) {
90
+ currentTaskId = taskMatch[1];
91
+ currentTaskType = detectTaskType(newLines, i);
92
+ continue;
93
+ }
94
+ if (oldLine.includes('**状态**: [ ] 未完成') &&
95
+ newLine.includes('**状态**: [x] 已完成')) {
96
+ changes.push({ taskId: currentTaskId, taskType: currentTaskType, line: i + 1 });
97
+ }
98
+ }
99
+ return changes;
100
+ }
101
+
102
+ // ── 主逻辑 ──
103
+
104
+ if (require.main === module) {
105
+ const parsed = core.parseHookInput(core.readStdin(), { strict: false });
106
+ if (!parsed.ok) process.exit(0);
107
+ const input = core.normalizeHookInput(parsed.input || {});
108
+ const toolInput = input.tool_input || input.toolInput || {};
109
+ const filePath = toolInput.file_path || toolInput.filePath || '';
110
+
111
+ // 只检查 tasks.md
112
+ if (!filePath.endsWith('tasks.md')) process.exit(0);
113
+
114
+ const projectRoot = core.getProjectRoot(input, 'codebuddy');
115
+
116
+ // 仅在有活跃 apply 阶段时触发
117
+ const activeStage = core.findActiveApplyStage(projectRoot);
118
+ if (!activeStage) process.exit(0);
119
+
120
+ // 安全网 0: 非 TDD 模式直接放行
121
+ const testStrategy = readTestStrategy(projectRoot, activeStage.change);
122
+ if (testStrategy !== 'tdd') process.exit(0);
123
+
124
+ // 读取旧内容
125
+ let oldContent = '';
126
+ try { oldContent = fs.readFileSync(filePath, 'utf8'); } catch { process.exit(0); }
127
+
128
+ // 获取新内容
129
+ let newContent = '';
130
+ if (toolInput.content) {
131
+ newContent = toolInput.content;
132
+ } else if (toolInput.new_str) {
133
+ newContent = oldContent.replace(toolInput.old_str || '', toolInput.new_str);
134
+ } else { process.exit(0); }
135
+
136
+ // 检测状态变更
137
+ const changes = detectStatusChanges(oldContent, newContent);
138
+ if (changes.length === 0) process.exit(0);
139
+
140
+ // 加载 telemetry 事件
141
+ const events = loadChangeEvents(projectRoot, activeStage.change);
142
+
143
+ // 安全网 1: 无任何 test_result → post-tool 可能失败,降级为警告
144
+ const totalTestResults = events.filter(e => e.type === 'test_result').length;
145
+ const hasTddTasks = changes.some(c =>
146
+ c.taskType === 'red' || c.taskType === 'green' || c.taskType === 'refactor');
147
+ if (totalTestResults === 0 && hasTddTasks) {
148
+ console.log(JSON.stringify({
149
+ decision: 'allow',
150
+ message: '⚠️ [SDD TDD Rhythm Gate] 未检测到任何 test_result 遥测事件。' +
151
+ 'sdd-post-tool.cjs 可能未正常工作。TDD 节奏检查降级为警告模式。',
152
+ }));
153
+ process.exit(0);
154
+ }
155
+
156
+ // G4: 批量更新检测
157
+ if (changes.length > 3) {
158
+ // 安全网 2: 全部有 task_update 遥测 → check-task 合法更新,放行
159
+ const allHaveTaskUpdate = changes.every(change =>
160
+ events.some(e => e.type === 'task_update' && e.task_id === change.taskId));
161
+ if (!allHaveTaskUpdate) {
162
+ core.blockWithReason(
163
+ `[SDD TDD Rhythm Gate] 检测到单次写入中 ${changes.length} 个任务状态变更,且无对应 task_update 遥测。\n\n` +
164
+ '⛔ 禁止批量修改任务状态。必须逐个通过 task_update → check-task 流程更新。\n' +
165
+ `批量变更的任务: ${changes.map(c => c.taskId).join(', ')}`);
166
+ }
167
+ process.exit(0); // 合法批量更新,放行
168
+ }
169
+
170
+ // 逐个检查
171
+ for (const change of changes) {
172
+ const { taskId, taskType } = change;
173
+
174
+ if (taskType === 'red') {
175
+ const hasRedEvidence = events.some(e =>
176
+ e.type === 'test_result' && e.task_id === taskId &&
177
+ e.result === 'failure' &&
178
+ e.details && e.details.test_results && e.details.test_results.tdd_phase === 'red');
179
+ if (!hasRedEvidence) {
180
+ core.blockWithReason(
181
+ `[SDD TDD Rhythm Gate] 任务 ${taskId} 是 RED 任务,标记完成前必须先运行测试并确认失败。\n\n` +
182
+ '未检测到对应的 test_result(result=failure, tdd_phase=red) 遥测事件。\n' +
183
+ '请先运行测试确认 RED 失败,记录 test_result 后再标记任务完成。');
184
+ }
185
+ }
186
+
187
+ if (taskType === 'green') {
188
+ const hasGreenEvidence = events.some(e =>
189
+ e.type === 'test_result' && e.task_id === taskId &&
190
+ e.result === 'success' &&
191
+ e.details && e.details.test_results && e.details.test_results.tdd_phase === 'green');
192
+ if (!hasGreenEvidence) {
193
+ core.blockWithReason(
194
+ `[SDD TDD Rhythm Gate] 任务 ${taskId} 是 GREEN 任务,标记完成前必须先运行测试并确认通过。\n\n` +
195
+ '未检测到对应的 test_result(result=success, tdd_phase=green) 遥测事件。\n' +
196
+ '请先运行测试确认 GREEN 通过,记录 test_result 后再标记任务完成。');
197
+ }
198
+ // G5: RED→GREEN 间隔检查
199
+ const pairedRedTaskId = findPairedRedTaskId(newContent, taskId);
200
+ if (pairedRedTaskId) {
201
+ const redEvent = events.find(e =>
202
+ e.type === 'test_result' && e.task_id === pairedRedTaskId && e.result === 'failure');
203
+ const greenEvent = events.find(e =>
204
+ e.type === 'test_result' && e.task_id === taskId && e.result === 'success');
205
+ if (redEvent && greenEvent) {
206
+ const gap = (new Date(greenEvent.timestamp).getTime() -
207
+ new Date(redEvent.timestamp).getTime()) / 1000;
208
+ if (gap < 60) {
209
+ core.blockWithReason(
210
+ `[SDD TDD Rhythm Gate] TDD 节奏异常:\n\n` +
211
+ `任务 ${taskId}(GREEN) 与 ${pairedRedTaskId}(RED) 的测试执行间隔仅 ${gap} 秒,建议 >60 秒。\n` +
212
+ '可能存在批量执行或跳过 RED 阶段的情况。');
213
+ }
214
+ }
215
+ }
216
+ }
217
+
218
+ if (taskType === 'refactor') {
219
+ const hasRefactorEvidence = events.some(e =>
220
+ e.type === 'test_result' && e.task_id === taskId &&
221
+ e.result === 'success' &&
222
+ e.details && e.details.test_results && e.details.test_results.tdd_phase === 'refactor');
223
+ if (!hasRefactorEvidence) {
224
+ core.blockWithReason(
225
+ `[SDD TDD Rhythm Gate] 任务 ${taskId} 是 REFACTOR 任务,` +
226
+ '标记完成前必须先运行测试确认全部通过(测试仍绿)。\n\n' +
227
+ '未检测到对应的 test_result(result=success, tdd_phase=refactor) 遥测事件。');
228
+ }
229
+ }
230
+
231
+ // 安全网 3: unknown 类型降级为警告
232
+ if (taskType === 'unknown') {
233
+ console.log(JSON.stringify({
234
+ decision: 'allow',
235
+ message: `⚠️ [SDD TDD Rhythm Gate] 任务 ${taskId} 的类型字段无法识别,已放行。` +
236
+ '建议补全 **类型** 字段,可选值:测试-RED | 实现-GREEN | 重构-REFACTOR | 配置 | 数据层 | 接口层 | 接口测试',
237
+ }));
238
+ }
239
+ // non-tdd 类型:直接放行
240
+ }
241
+
242
+ process.exit(0);
243
+ }
244
+
245
+ module.exports = {
246
+ detectStatusChanges, detectTaskType, findPairedRedTaskId,
247
+ loadChangeEvents, readTestStrategy,
248
+ };
@@ -39,6 +39,10 @@
39
39
  {
40
40
  "type": "command",
41
41
  "command": "node \"$CODEBUDDY_PROJECT_DIR/.codebuddy/hooks/sdd-apply-gate.cjs\""
42
+ },
43
+ {
44
+ "type": "command",
45
+ "command": "node \"$CODEBUDDY_PROJECT_DIR/.codebuddy/hooks/sdd-tdd-rhythm-gate.cjs\""
42
46
  }
43
47
  ]
44
48
  }
@@ -50,6 +54,10 @@
50
54
  {
51
55
  "type": "command",
52
56
  "command": "node \"$CODEBUDDY_PROJECT_DIR/.codebuddy/hooks/sdd-post-tool.cjs\""
57
+ },
58
+ {
59
+ "type": "command",
60
+ "command": "node \"$CODEBUDDY_PROJECT_DIR/.codebuddy/hooks/sdd-mid-checkpoint.cjs\""
53
61
  }
54
62
  ]
55
63
  }
@@ -215,6 +215,19 @@ g. **继续下一个层级** — 重新检查 DAG,找出依赖已满足的下
215
215
  > 异常路径覆盖门禁见 tdd-rules/rules/exception-path-coverage.md
216
216
  > TDD 节奏校验(执行后校验)见 `./checklist.md` §5e.1
217
217
 
218
+ **【S2.2 TDD 纪律上下文刷新】**(每 Layer 开始时强制):
219
+
220
+ 在开始每个新 Layer 的任务实施前,必须重新执行以下步骤:
221
+
222
+ 1. 重新读取本 SKILL.md §S2(TDD 红绿节奏 7 步骤)
223
+ 2. 重新读取 `./checklist.md` §5e.1(TDD 节奏校验)
224
+ 3. 显式声明:"我将在 Layer [X] 严格遵循 RED→GREEN 串行循环"
225
+ 4. 如果本 Layer 有 >5 个 TDD 对,显示额外警告:
226
+ "⚠️ 大量 TDD 任务([N] 对),注意保持节奏。每对必须:写测试→运行确认失败→中断声明→写实现→运行确认通过"
227
+
228
+ > 此步骤防止上下文窗口退化导致 TDD 纪律被"挤出"。
229
+ > 随着对话变长,早期读取的 TDD 规则可能被遗忘,必须定期刷新。
230
+
218
231
  **【S2.1 RED 测试质量标准】**(test-strategy=tdd 时强制):
219
232
 
220
233
  ⛔ RED 测试质量门禁见 `./checklist.md`「§5e.2 RED 测试质量门禁」,核心包括:
@@ -95,6 +95,21 @@ description: opsx-apply 的阶段强制检查点与自检清单。仅在执行 a
95
95
 
96
96
  ---
97
97
 
98
+ ### §5g 中途 TDD 节奏检查点(每完成 10 个 TDD 任务后强制执行)
99
+
100
+ > ⛔ 在 apply 执行过程中,每完成 10 个 TDD 任务后必须执行此检查点。
101
+ > 此检查点由 `sdd-mid-checkpoint.cjs` hook 自动触发,但 agent 也必须主动执行。
102
+
103
+ - [ ] 统计已完成的 `task_update` 事件数,与 tasks.md `[x]` 数量对比
104
+ - [ ] 统计已记录的 `test_result` 事件数(RED + GREEN),与已完成 TDD 对数 × 2 对比
105
+ - [ ] 如果 `task_update` 数量 < 已完成任务数:⛔ 阻断,补录缺失的 `task_update`
106
+ - [ ] 如果 `test_result` 数量 < 已完成 TDD 对数 × 2:⛔ 阻断,补录缺失的 `test_result`
107
+ - [ ] 显示警告:"已完成 [N] 个 TDD 任务。请确认你仍在遵循严格 RED→GREEN 循环。"
108
+ - [ ] 检查最近 3 对 RED→GREEN 的 `test_result` 时间戳间隔,均 >60 秒
109
+ - [ ] 如果发现异常,回退到上一个检查点重新执行
110
+
111
+ ---
112
+
98
113
  ## §6.0 单元测试真实执行自检(`test-strategy` 非 `none`)
99
114
 
100
115
  > 单元测试真实执行自检见 tdd-core/reference.md §9
@@ -17,10 +17,12 @@ description: opsx-apply 的详细模板:telemetry 命令、worktree 全套策
17
17
  ### task_update(每完成一个任务记录)
18
18
 
19
19
  ```bash
20
- node "$(cat .sdd-spec-root)/skywalk-sdd/log.cjs" record --strict --type=task_update --command=apply --project=. --change=<变更名称> --capability=<capability-name> --task-id=<TASK-ID> --run-id=<本次任务更新稳定ID> --agent=<Agent类型> --source=opsx-command --session-id=<会话ID> --status=completed --result=success --summary="<TASK-ID> 完成" --details-json="{\"task_update\":{\"test_event_id\":\"<同一change内成功test_result的event_id>\",\"tdd_required\":<true|false>},\"files_changed\":[]}"
20
+ node "$(cat .sdd-spec-root)/skywalk-sdd/log.cjs" record --strict --type=task_update --command=apply --project=. --change=<变更名称> --capability=<capability-name> --task-id=<TASK-ID> --run-id=<本次任务更新稳定ID> --agent=<Agent类型> --source=opsx-command --session-id=<会话ID> --status=completed --result=success --summary="<TASK-ID> 完成" --details-json="{\"task_update\":{\"test_event_id\":\"<同一change内成功test_result的event_id>\",\"tdd_required\":<true|false>,\"tdd_pair_id\":\"<pair-N>\",\"tdd_role\":\"green\"},\"files_changed\":[]}"
21
+
22
+ 也可互斥使用 `test_run_id`(同 change、覆盖当前任务、唯一严格完成候选);解析成功后会补齐规范化 `test_event_id`。
21
23
  ```
22
24
 
23
- **⚠️ 注意**:`--task-id=<TASK-ID>` 必须替换为实际任务 ID。完成任务必须引用同一 change 内、`tdd_phase` 为 green/refactor/regression、退出码为 0、`failure_type=none` 的 `test_result.event_id`,并明确填写 `tdd_required=true/false`;禁止把测试计数复制进 `task_update`。
25
+ **⚠️ 注意**:`--task-id=<TASK-ID>` 必须替换为实际任务 ID。完成任务必须引用同一 change 内、`tdd_phase` 为 green/refactor/regression、退出码为 0、`failure_type=none` 的 `test_result.event_id`(或可唯一解析的 `test_run_id`),并明确填写 `tdd_required=true/false`;TDD 任务应写同一 `tdd_pair_id` 与 `tdd_role`;禁止把测试计数复制进 `task_update`。
24
26
 
25
27
  只有文档、纯配置说明等确实不适用测试的任务,才能显式声明:
26
28
 
@@ -46,6 +48,47 @@ node "$(cat .sdd-spec-root)/skywalk-sdd/log.cjs" check-task --project=. --change
46
48
 
47
49
  **【严格证据口径】**:测试执行事实只存在于 `test_result`;`task_update` 只表达任务状态与证据引用。旧事件仍可读取,但新事件必须使用 `--strict`。
48
50
 
51
+ ### test_result(每次真实测试执行记录)
52
+
53
+ > **⚠️ key 名必须是 `test_results`(复数),不是 `test_result`(单数)。**
54
+ > `log.cjs` 的 `normalizeStrictTestResult` 校验器从 `details.test_results` 读取,单数 key 会导致 `TEST_RESULT_REQUIRED` 错误。
55
+ > 完整 schema 亦见 `tdd-core/reference.md` §8 和 `opsx-test/SKILL.md` §4a。
56
+
57
+ **RED 阶段(测试失败,预期红灯)**:
58
+
59
+ ```bash
60
+ node "$(cat .sdd-spec-root)/skywalk-sdd/log.cjs" record --strict --type=test_result --command=apply --project=. --change=<变更名称> --capability=<capability-name> --task-id=<TASK-ID> --run-id=<本次真实执行的稳定ID> --agent=<Agent类型> --source=opsx-command --session-id=<会话ID> --result=failure --summary="<TASK-ID> TDD 红灯符合预期" --details-json='{"test_results":{"tdd_phase":"red","verified_task_ids":["<TASK-ID>"],"command":"<实际测试命令>","exit_code":<非零退出码>,"counts_known":true,"passed":<通过数>,"failed":<失败数>,"skipped":<跳过数>,"duration_ms":<实测毫秒>,"failure_type":"assertion","expected_failure":true}}'
61
+ ```
62
+
63
+ **GREEN 阶段(测试通过)**:
64
+
65
+ ```bash
66
+ node "$(cat .sdd-spec-root)/skywalk-sdd/log.cjs" record --strict --type=test_result --command=apply --project=. --change=<变更名称> --capability=<capability-name> --task-id=<TASK-ID> --run-id=<本次真实执行的稳定ID> --agent=<Agent类型> --source=opsx-command --session-id=<会话ID> --result=success --summary="<TASK-ID> TDD 绿灯通过" --details-json='{"test_results":{"tdd_phase":"green","verified_task_ids":["<TASK-ID>"],"command":"<实际测试命令>","exit_code":0,"counts_known":true,"passed":<通过数>,"failed":0,"skipped":<跳过数>,"duration_ms":<实测毫秒>,"failure_type":"none","expected_failure":false}}'
67
+ ```
68
+
69
+ **必填字段**:
70
+
71
+ | 字段 | 类型 | 说明 | 约束 |
72
+ |------|------|------|------|
73
+ | `tdd_phase` | string | TDD 阶段 | `red`/`green`/`refactor`/`regression`/`not-applicable` |
74
+ | `command` | string | 实际测试命令 | 如 `mvn test -Dtest=...` |
75
+ | `result` | string | 测试结果 | `success`/`failure`(与 `exit_code` 一致) |
76
+ | `exit_code` | integer | 退出码 | `0`=成功,非 0=失败 |
77
+ | `counts_known` | boolean | 是否已知测试计数 | `true` 时必须填 `passed`/`failed`/`skipped` |
78
+ | `duration_ms` | integer | 执行耗时(毫秒) | 实测值 |
79
+ | `failure_type` | string | 失败类型 | `assertion`/`contract`/`compile`/`infrastructure`/`timeout`/`none` |
80
+ | `run_id` | string | 稳定执行 ID | 同一次执行重试时保持不变 |
81
+ | `task_id` | string | 关联任务 ID | 或用 `verified_task_ids` 数组 |
82
+ | `expected_failure` | boolean | 是否预期失败 | 仅 `tdd_phase=red` 时可为 `true` |
83
+
84
+ **交叉校验规则**:
85
+ - `exit_code=0` ↔ `result=success` ↔ `failure_type=none`
86
+ - `exit_code≠0` ↔ `result=failure` ↔ `failure_type≠none`
87
+ - `expected_failure=true` 仅允许 `tdd_phase=red`
88
+ - 有效 RED:`expected_failure=true` + `exit_code≠0` + `failure_type` 为 `assertion` 或 `contract`
89
+
90
+ > **自动记录**:`sdd-post-tool.cjs` hook 会在 agent 运行 `mvn test`/`npm test` 等命令时自动记录 `test_result`,使用正确的 `test_results` key。但自动记录依赖 `activeStage.task_id`,若未设置则 `tdd_phase` 默认为 `regression`。需要精确标注 RED/GREEN 时建议手动记录。
91
+
49
92
  ### process_note(阶段内过程事件:决策/范围变化/故障与恢复)
50
93
 
51
94
  记录阶段内过程信息,供 execution-log 叙事与报告“过程记录”板块统计。新事件使用 `--strict`,`details.kind` 必填,枚举:
@@ -112,7 +112,7 @@ node "$(cat .sdd-spec-root)/skywalk-sdd/log.cjs" tasks-status --project=. --chan
112
112
  ```json
113
113
  {
114
114
  "maturity_evidence": {
115
- "change_type": "config|document|report|composite|other",
115
+ "change_type": "config|transaction|report|composite",
116
116
  "e1_actual_ms": 500,
117
117
  "e1_target_ms": 600,
118
118
  "e1_type_target_met": true,