kld-sdd 2.6.15 → 2.6.17
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/lib/init.js +174 -76
- package/lib/skills-bundle.js +23 -1
- package/lib/tool-profiles.js +8 -0
- package/package.json +6 -1
- package/skywalk-sdd/apply-worktree-finish.cjs +2 -23
- package/skywalk-sdd/context-client.cjs +38 -87
- package/skywalk-sdd/index.cjs +719 -108
- package/skywalk-sdd/kb-sync-identity.cjs +780 -0
- package/skywalk-sdd/kb-upload.cjs +505 -0
- package/skywalk-sdd/lib/shared.cjs +663 -0
- package/skywalk-sdd/metrics-v3.cjs +138 -8
- package/skywalk-sdd/ontology/archive-package.cjs +19 -34
- package/skywalk-sdd/ontology/change-lock.cjs +3 -7
- package/skywalk-sdd/ontology/external-key.cjs +18 -4
- package/skywalk-sdd/ontology/id.cjs +26 -5
- package/skywalk-sdd/ontology/identity-index.cjs +3 -7
- package/skywalk-sdd/ontology/resolve-spec-root.cjs +20 -6
- package/skywalk-sdd/ontology/runtime.cjs +16 -12
- package/skywalk-sdd/ontology/traceability-validator.cjs +7 -4
- package/skywalk-sdd/openspec-shim.cjs +48 -0
- package/skywalk-sdd/reporting/change-report-markdown.cjs +137 -19
- package/skywalk-sdd/reporting/change-report-model.cjs +993 -14
- package/skywalk-sdd/reporting/change-report-renderer.cjs +106 -41
- package/skywalk-sdd/reporting/change-report-view-model.cjs +272 -43
- package/skywalk-sdd/reporting/core-metric-definitions.cjs +192 -0
- package/skywalk-sdd/spec-root.cjs +31 -0
- package/templates/git-hooks/commit-msg +46 -0
- package/templates/git-hooks/pre-commit +57 -0
- package/templates/git-hooks/pre-commit-consistency-check.cjs +193 -0
- package/templates/git-hooks/pre-push +57 -0
- package/templates/git-hooks/pre-push-consistency-check.cjs +197 -0
- package/templates/hooks/codebuddy/hooks/hook-gate-core.cjs +696 -0
- package/templates/hooks/codebuddy/hooks/sdd-apply-test-gate.cjs +86 -0
- package/templates/hooks/codebuddy/hooks/sdd-mid-checkpoint.cjs +165 -0
- package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +36 -1
- package/templates/hooks/codebuddy/hooks/sdd-tdd-rhythm-gate.cjs +296 -0
- package/templates/hooks/codebuddy/settings.json +8 -0
- package/templates/openspec/proposal.md +7 -3
- package/templates/openspec/spec.md +3 -3
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +21 -6
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +17 -0
- package/templates/skills/kld-sdd/opsx-apply/reference.md +70 -7
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +6 -5
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +48 -16
- package/templates/skills/kld-sdd/opsx-check/checklist.md +4 -2
- package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +592 -0
- package/templates/skills/kld-sdd/opsx-design/SKILL.md +2 -2
- package/templates/skills/kld-sdd/opsx-explore/SKILL.md +2 -2
- package/templates/skills/kld-sdd/opsx-kb-config/SKILL.md +164 -0
- package/templates/skills/kld-sdd/opsx-kb-config/reference.md +116 -0
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +218 -53
- package/templates/skills/kld-sdd/opsx-kb-ingest/reference.md +51 -9
- package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +12 -50
- package/templates/skills/kld-sdd/opsx-ontology-query/phase-3-postchange.md +2 -2
- package/templates/skills/kld-sdd/opsx-ontology-query/reference.md +1 -1
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +35 -23
- package/templates/skills/kld-sdd/opsx-propose/checklist.md +2 -0
- package/templates/skills/kld-sdd/opsx-propose/reference.md +22 -17
- package/templates/skills/kld-sdd/opsx-rules/SKILL.md +2 -2
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +19 -15
- package/templates/skills/kld-sdd/opsx-spec/checklist.md +2 -0
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +2 -4
- package/templates/skills/kld-sdd/opsx-test/SKILL.md +2 -2
- package/templates/skills/kld-sdd/tdd-core/reference.md +1 -1
- package/templates/skills/kld-sdd/tdd-rules/SKILL.md +1 -0
- package/templates/skills/kld-sdd/tdd-rules/rules/tdd-rhythm-enforcement.md +101 -0
- package/templates/skills/kld-sdd/tdd-rules/rules/test-skeleton-telemetry.md +1 -1
- package/templates/skills/kld-sdd/opsx-kb-ingest/state.example.json +0 -7
- package/templates/skills/kld-sdd/opsx-ontology-query/state.example.json +0 -7
|
@@ -341,6 +341,53 @@ function hasExistingWarning(events, code) {
|
|
|
341
341
|
);
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
+
/** B3: 检测 TDD 时间异常
|
|
345
|
+
* B1/B2 改进:降低阈值,分两级(30s 阻断 / 60s 警告),最少 3 任务触发
|
|
346
|
+
* 返回 { anomaly, severity, perTask, totalTasks, totalDuration, testEvidence, threshold } */
|
|
347
|
+
function detectTimingAnomaly(events, sinceTimestamp) {
|
|
348
|
+
const since = sinceTimestamp ? new Date(sinceTimestamp).getTime() : 0;
|
|
349
|
+
const filtered = (events || []).filter(
|
|
350
|
+
(e) => new Date(e.timestamp || 0).getTime() >= since
|
|
351
|
+
);
|
|
352
|
+
const taskUpdates = filtered.filter(
|
|
353
|
+
(e) => e.type === 'task_update' && e.status === 'completed'
|
|
354
|
+
);
|
|
355
|
+
if (taskUpdates.length < 3) return null; // B2: 10 → 3
|
|
356
|
+
const timestamps = filtered
|
|
357
|
+
.map((e) => new Date(e.timestamp || 0).getTime())
|
|
358
|
+
.filter((t) => t > 0);
|
|
359
|
+
if (timestamps.length === 0) return null;
|
|
360
|
+
const firstTs = Math.min(...timestamps);
|
|
361
|
+
const lastTs = Math.max(...timestamps);
|
|
362
|
+
const totalDurationSec = (lastTs - firstTs) / 1000;
|
|
363
|
+
const perTaskSec = totalDurationSec / taskUpdates.length;
|
|
364
|
+
const BLOCK_THRESHOLD = 30; // B1: < 30s/任务 → block
|
|
365
|
+
const WARN_THRESHOLD = 60; // B1: < 60s/任务 → warn
|
|
366
|
+
if (perTaskSec < BLOCK_THRESHOLD) {
|
|
367
|
+
return {
|
|
368
|
+
anomaly: true,
|
|
369
|
+
severity: 'critical',
|
|
370
|
+
perTask: Math.round(perTaskSec),
|
|
371
|
+
totalTasks: taskUpdates.length,
|
|
372
|
+
totalDuration: Math.round(totalDurationSec),
|
|
373
|
+
testEvidence: filtered.filter((e) => e.type === 'test_result').length,
|
|
374
|
+
threshold: BLOCK_THRESHOLD,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
if (perTaskSec < WARN_THRESHOLD) {
|
|
378
|
+
return {
|
|
379
|
+
anomaly: true,
|
|
380
|
+
severity: 'warning',
|
|
381
|
+
perTask: Math.round(perTaskSec),
|
|
382
|
+
totalTasks: taskUpdates.length,
|
|
383
|
+
totalDuration: Math.round(totalDurationSec),
|
|
384
|
+
testEvidence: filtered.filter((e) => e.type === 'test_result').length,
|
|
385
|
+
threshold: WARN_THRESHOLD,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
|
|
344
391
|
// ── 主逻辑(仅直接执行时运行;被 require 时跳过,便于单元测试) ──
|
|
345
392
|
if (require.main === module) {
|
|
346
393
|
const parsed = parseHookInput(readStdin(), { strict: true });
|
|
@@ -390,6 +437,44 @@ if (require.main === module) {
|
|
|
390
437
|
'tdd 策略下 apply 阶段未检测到失败的测试结果(红灯),测试骨架可能未经 RED 验证即标记完成');
|
|
391
438
|
}
|
|
392
439
|
}
|
|
440
|
+
// B3: TDD 时间异常检测(B1 改进:critical 级阻断)
|
|
441
|
+
const timingAnomaly = detectTimingAnomaly(warningEvents, activeApply.timestamp);
|
|
442
|
+
if (timingAnomaly && !hasExistingWarning(warningEvents, 'tdd_timing_anomaly')) {
|
|
443
|
+
const msg = 'TDD 时间异常:' + timingAnomaly.totalTasks + ' 个任务在 ' + timingAnomaly.totalDuration +
|
|
444
|
+
' 秒内完成(每任务 ' + timingAnomaly.perTask + ' 秒,阈值 ' + timingAnomaly.threshold +
|
|
445
|
+
' 秒),测试证据 ' + timingAnomaly.testEvidence + ' 条。可能存在批量执行或跳过 TDD 红绿循环的情况。';
|
|
446
|
+
if (timingAnomaly.severity === 'critical') {
|
|
447
|
+
// B1: critical 级直接阻断
|
|
448
|
+
recordWarning(projectRoot, warningCtx, 'tdd_timing_anomaly', msg);
|
|
449
|
+
block('block',
|
|
450
|
+
`⛔ [SDD Apply Test Gate] TDD 时间异常阻断\n\n${msg}\n\n` +
|
|
451
|
+
'每任务平均耗时 < 30 秒,不可能完成真实的 RED→GREEN 循环。\n' +
|
|
452
|
+
'请回退并逐对重新执行 TDD 循环。',
|
|
453
|
+
{ test_strategy: warningCtx.test_strategy, change: activeApply.change }
|
|
454
|
+
);
|
|
455
|
+
} else if (!hasExistingWarning(warningEvents, 'tdd_timing_anomaly')) {
|
|
456
|
+
recordWarning(projectRoot, warningCtx, 'tdd_timing_anomaly', msg);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// B3: TDD 批量执行模式检测(E2 极速配对 + E3 时间戳聚类 + 连续 RED + 交错违规)
|
|
460
|
+
const batchPattern = core.detectTddBatchPattern(
|
|
461
|
+
warningEvents,
|
|
462
|
+
activeApply.timestamp,
|
|
463
|
+
null // 无 tasks.md 内容,退化为编号推断
|
|
464
|
+
);
|
|
465
|
+
if (batchPattern.has_violation && batchPattern.severity === 'critical') {
|
|
466
|
+
const criticalMsgs = batchPattern.violations
|
|
467
|
+
.filter(v => v.severity === 'critical')
|
|
468
|
+
.map(v => `【${v.type}】${v.message}`)
|
|
469
|
+
.join('\n');
|
|
470
|
+
recordWarning(projectRoot, warningCtx, 'tdd_batch_execution',
|
|
471
|
+
`TDD 批量执行检测:${criticalMsgs}`);
|
|
472
|
+
block('block',
|
|
473
|
+
`⛔ [SDD Apply Test Gate] TDD 批量执行违规\n\n${criticalMsgs}\n\n` +
|
|
474
|
+
'请回退并逐对重新执行 TDD 循环。',
|
|
475
|
+
{ test_strategy: warningCtx.test_strategy, change: activeApply.change }
|
|
476
|
+
);
|
|
477
|
+
}
|
|
393
478
|
} catch {
|
|
394
479
|
// 警告检测失败不阻断主流程
|
|
395
480
|
}
|
|
@@ -418,6 +503,7 @@ module.exports = {
|
|
|
418
503
|
detectReusedTestResults,
|
|
419
504
|
detectProcessNoteGap,
|
|
420
505
|
detectMissingTddRed,
|
|
506
|
+
detectTimingAnomaly,
|
|
421
507
|
hasExistingWarning,
|
|
422
508
|
isRealTestDetails,
|
|
423
509
|
isStrictTestEvidence,
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SDD Mid-Apply Checkpoint Hook
|
|
6
|
+
*
|
|
7
|
+
* 每完成 5 个 TDD 任务后强制检查点,验证 telemetry 证据链完整性。
|
|
8
|
+
* C1 改进:证据比率 < 50% 或检测到 critical 批量违规时阻断。
|
|
9
|
+
* C2 改进:检查点间隔从 10 降为 5。
|
|
10
|
+
* C3 改进:新增连续 RED + 极速配对检测。
|
|
11
|
+
*
|
|
12
|
+
* 触发点:PostToolUse(Bash) - 当检测到测试命令执行后
|
|
13
|
+
* 退出码:0 放行 | 2 阻断(C1 改进:严重违规时阻断)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const core = require('./hook-gate-core.cjs');
|
|
19
|
+
|
|
20
|
+
const CHECKPOINT_INTERVAL = 5; // C2: 10 → 5
|
|
21
|
+
|
|
22
|
+
function loadChangeEvents(projectRoot, changeName) {
|
|
23
|
+
const skywalk = core.resolveSkywalkDir(projectRoot);
|
|
24
|
+
const safe = core.safeChangeName(changeName);
|
|
25
|
+
const dir = skywalk
|
|
26
|
+
? path.join(skywalk, 'events', safe)
|
|
27
|
+
: path.join(projectRoot, 'skywalk-sdd', 'events', safe);
|
|
28
|
+
if (!fs.existsSync(dir)) return [];
|
|
29
|
+
const events = [];
|
|
30
|
+
for (const file of fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'))) {
|
|
31
|
+
try {
|
|
32
|
+
const lines = fs.readFileSync(path.join(dir, file), 'utf8').split('\n').filter(Boolean);
|
|
33
|
+
for (const line of lines) { try { events.push(JSON.parse(line)); } catch {} }
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
return events;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function latestActiveStage(projectRoot) {
|
|
40
|
+
const skywalk = core.resolveSkywalkDir(projectRoot);
|
|
41
|
+
const stateDir = skywalk
|
|
42
|
+
? path.join(skywalk, 'state')
|
|
43
|
+
: path.join(projectRoot, 'skywalk-sdd', 'state');
|
|
44
|
+
if (!fs.existsSync(stateDir)) return null;
|
|
45
|
+
return fs.readdirSync(stateDir)
|
|
46
|
+
.filter(f => f.endsWith('.json'))
|
|
47
|
+
.map(f => { try { return JSON.parse(fs.readFileSync(path.join(stateDir, f), 'utf8')).event; } catch { return null; } })
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.filter(e => e.command === 'apply')
|
|
50
|
+
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())[0] || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (require.main === module) {
|
|
54
|
+
const parsed = core.parseHookInput(core.readStdin(), { strict: false });
|
|
55
|
+
const input = core.normalizeHookInput(parsed.input || {});
|
|
56
|
+
const toolInput = input.tool_input || input.toolInput || {};
|
|
57
|
+
const command = String(toolInput.command || '');
|
|
58
|
+
|
|
59
|
+
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)) {
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const projectRoot = core.getProjectRoot(input, 'codebuddy');
|
|
64
|
+
const activeStage = latestActiveStage(projectRoot);
|
|
65
|
+
if (!activeStage) process.exit(0);
|
|
66
|
+
|
|
67
|
+
const events = loadChangeEvents(projectRoot, activeStage.change);
|
|
68
|
+
const since = new Date(activeStage.timestamp).getTime();
|
|
69
|
+
|
|
70
|
+
const taskUpdates = events.filter(e =>
|
|
71
|
+
e.type === 'task_update' && e.status === 'completed' &&
|
|
72
|
+
new Date(e.timestamp || 0).getTime() >= since);
|
|
73
|
+
const completedCount = taskUpdates.length;
|
|
74
|
+
|
|
75
|
+
if (completedCount === 0 || completedCount % CHECKPOINT_INTERVAL !== 0) {
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const testResults = events.filter(e =>
|
|
80
|
+
e.type === 'test_result' &&
|
|
81
|
+
new Date(e.timestamp || 0).getTime() >= since);
|
|
82
|
+
|
|
83
|
+
const redCount = testResults.filter(e =>
|
|
84
|
+
e.result === 'failure' && e.details && e.details.test_results &&
|
|
85
|
+
e.details.test_results.tdd_phase === 'red').length;
|
|
86
|
+
const greenCount = testResults.filter(e =>
|
|
87
|
+
e.result === 'success' && e.details && e.details.test_results &&
|
|
88
|
+
e.details.test_results.tdd_phase === 'green').length;
|
|
89
|
+
|
|
90
|
+
const expectedMin = Math.floor(completedCount * 0.8);
|
|
91
|
+
const actualEvidence = redCount + greenCount;
|
|
92
|
+
const evidenceRatio = completedCount > 0 ? actualEvidence / completedCount : 1;
|
|
93
|
+
|
|
94
|
+
// C3: TDD 批量执行模式检测
|
|
95
|
+
const batchPattern = core.detectTddBatchPattern(
|
|
96
|
+
events,
|
|
97
|
+
activeStage.timestamp,
|
|
98
|
+
null // 无 tasks.md 内容,退化为编号推断
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// C1: critical 批量违规 → 阻断
|
|
102
|
+
if (batchPattern.has_violation && batchPattern.severity === 'critical') {
|
|
103
|
+
const criticalMsgs = batchPattern.violations
|
|
104
|
+
.filter(v => v.severity === 'critical')
|
|
105
|
+
.map(v => `【${v.type}】${v.message}`)
|
|
106
|
+
.join('\n');
|
|
107
|
+
console.log(JSON.stringify({
|
|
108
|
+
decision: 'block',
|
|
109
|
+
reason: '⛔ [SDD Mid-Checkpoint] TDD 批量执行违规(' + completedCount + ' 任务已完成)\n\n' +
|
|
110
|
+
criticalMsgs + '\n\n' +
|
|
111
|
+
'请回退并逐对重新执行 TDD 循环。',
|
|
112
|
+
}));
|
|
113
|
+
process.exit(2);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// C1: 证据比率 < 50% → 阻断
|
|
117
|
+
if (evidenceRatio < 0.5) {
|
|
118
|
+
console.log(JSON.stringify({
|
|
119
|
+
decision: 'block',
|
|
120
|
+
reason: '⛔ [SDD Mid-Checkpoint] 测试证据严重不足(' + completedCount + ' 任务已完成)\n' +
|
|
121
|
+
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
|
|
122
|
+
' task_update 事件: ' + completedCount + '\n' +
|
|
123
|
+
' test_result 事件: ' + actualEvidence + ' (RED: ' + redCount + ', GREEN: ' + greenCount + ')\n' +
|
|
124
|
+
' 证据比率: ' + Math.round(evidenceRatio * 100) + '%(阈值 50%)\n\n' +
|
|
125
|
+
' 请确认你仍在遵循严格 RED→GREEN 循环。\n' +
|
|
126
|
+
' 如果最近的任务跳过了 TDD,请回退并补做。\n' +
|
|
127
|
+
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
|
|
128
|
+
}));
|
|
129
|
+
process.exit(2);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 证据比率 < 80% → 警告(不阻断)
|
|
133
|
+
if (evidenceRatio < 0.8) {
|
|
134
|
+
console.log(JSON.stringify({
|
|
135
|
+
decision: 'allow',
|
|
136
|
+
message: '\n⚠️ TDD 中途检查点 [' + completedCount + ' 任务已完成]\n' +
|
|
137
|
+
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
|
|
138
|
+
' task_update 事件: ' + completedCount + '\n' +
|
|
139
|
+
' test_result 事件: ' + actualEvidence + ' (RED: ' + redCount + ', GREEN: ' + greenCount + ')\n' +
|
|
140
|
+
' 证据比率: ' + Math.round(evidenceRatio * 100) + '%\n\n' +
|
|
141
|
+
' ⚠️ 测试证据不足!请确认你仍在遵循严格 RED→GREEN 循环。\n' +
|
|
142
|
+
' 如果最近的任务跳过了 TDD,请回退并补做。\n' +
|
|
143
|
+
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n',
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// C3: warning 级批量违规 → 警告(不阻断)
|
|
148
|
+
if (batchPattern.has_violation && batchPattern.severity === 'warning') {
|
|
149
|
+
const warningMsgs = batchPattern.violations
|
|
150
|
+
.filter(v => v.severity === 'warning')
|
|
151
|
+
.map(v => `【${v.type}】${v.message}`)
|
|
152
|
+
.join('\n');
|
|
153
|
+
console.log(JSON.stringify({
|
|
154
|
+
decision: 'allow',
|
|
155
|
+
message: '\n⚠️ TDD 中途检查点 — 批量执行警告\n' +
|
|
156
|
+
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
|
|
157
|
+
warningMsgs + '\n' +
|
|
158
|
+
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n',
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
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 ||
|
|
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,296 @@
|
|
|
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: 批量更新检测(E1 修正:不再提前退出,模式检查继续执行)
|
|
157
|
+
var skipPerTaskCheck = false;
|
|
158
|
+
if (changes.length >= 2 && hasTddTasks) {
|
|
159
|
+
// 安全网 2: 全部有 task_update 遥测 → check-task 合法更新,跳过逐个检查
|
|
160
|
+
const allHaveTaskUpdate = changes.every(change =>
|
|
161
|
+
events.some(e => e.type === 'task_update' && e.task_id === change.taskId));
|
|
162
|
+
if (!allHaveTaskUpdate) {
|
|
163
|
+
core.blockWithReason(
|
|
164
|
+
`[SDD TDD Rhythm Gate] 检测到单次写入中 ${changes.length} 个任务状态变更,且无对应 task_update 遥测。\n\n` +
|
|
165
|
+
'⛔ 禁止批量修改任务状态。必须逐个通过 task_update → check-task 流程更新。\n' +
|
|
166
|
+
`批量变更的任务: ${changes.map(c => c.taskId).join(', ')}`);
|
|
167
|
+
}
|
|
168
|
+
// E1 修正:有 task_update → 跳过逐个证据检查,但模式检查继续执行
|
|
169
|
+
skipPerTaskCheck = true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 逐个检查(E1 修正:批量安全网触发时跳过逐个检查,但模式检查仍执行)
|
|
173
|
+
if (!skipPerTaskCheck) {
|
|
174
|
+
for (const change of changes) {
|
|
175
|
+
const { taskId, taskType } = change;
|
|
176
|
+
|
|
177
|
+
if (taskType === 'red') {
|
|
178
|
+
const hasRedEvidence = events.some(e =>
|
|
179
|
+
e.type === 'test_result' && e.task_id === taskId &&
|
|
180
|
+
e.result === 'failure' &&
|
|
181
|
+
e.details && e.details.test_results && e.details.test_results.tdd_phase === 'red');
|
|
182
|
+
if (!hasRedEvidence) {
|
|
183
|
+
core.blockWithReason(
|
|
184
|
+
`[SDD TDD Rhythm Gate] 任务 ${taskId} 是 RED 任务,标记完成前必须先运行测试并确认失败。\n\n` +
|
|
185
|
+
'未检测到对应的 test_result(result=failure, tdd_phase=red) 遥测事件。\n' +
|
|
186
|
+
'请先运行测试确认 RED 失败,记录 test_result 后再标记任务完成。');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (taskType === 'green') {
|
|
191
|
+
const hasGreenEvidence = events.some(e =>
|
|
192
|
+
e.type === 'test_result' && e.task_id === taskId &&
|
|
193
|
+
e.result === 'success' &&
|
|
194
|
+
e.details && e.details.test_results && e.details.test_results.tdd_phase === 'green');
|
|
195
|
+
if (!hasGreenEvidence) {
|
|
196
|
+
core.blockWithReason(
|
|
197
|
+
`[SDD TDD Rhythm Gate] 任务 ${taskId} 是 GREEN 任务,标记完成前必须先运行测试并确认通过。\n\n` +
|
|
198
|
+
'未检测到对应的 test_result(result=success, tdd_phase=green) 遥测事件。\n' +
|
|
199
|
+
'请先运行测试确认 GREEN 通过,记录 test_result 后再标记任务完成。');
|
|
200
|
+
}
|
|
201
|
+
// G5: RED→GREEN 间隔检查
|
|
202
|
+
const pairedRedTaskId = findPairedRedTaskId(newContent, taskId);
|
|
203
|
+
if (pairedRedTaskId) {
|
|
204
|
+
const redEvent = events.find(e =>
|
|
205
|
+
e.type === 'test_result' && e.task_id === pairedRedTaskId && e.result === 'failure');
|
|
206
|
+
const greenEvent = events.find(e =>
|
|
207
|
+
e.type === 'test_result' && e.task_id === taskId && e.result === 'success');
|
|
208
|
+
if (redEvent && greenEvent) {
|
|
209
|
+
const gap = (new Date(greenEvent.timestamp).getTime() -
|
|
210
|
+
new Date(redEvent.timestamp).getTime()) / 1000;
|
|
211
|
+
if (gap < 60) {
|
|
212
|
+
core.blockWithReason(
|
|
213
|
+
`[SDD TDD Rhythm Gate] TDD 节奏异常:\n\n` +
|
|
214
|
+
`任务 ${taskId}(GREEN) 与 ${pairedRedTaskId}(RED) 的测试执行间隔仅 ${gap} 秒,建议 >60 秒。\n` +
|
|
215
|
+
'可能存在批量执行或跳过 RED 阶段的情况。');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (taskType === 'refactor') {
|
|
222
|
+
const hasRefactorEvidence = events.some(e =>
|
|
223
|
+
e.type === 'test_result' && e.task_id === taskId &&
|
|
224
|
+
e.result === 'success' &&
|
|
225
|
+
e.details && e.details.test_results && e.details.test_results.tdd_phase === 'refactor');
|
|
226
|
+
if (!hasRefactorEvidence) {
|
|
227
|
+
core.blockWithReason(
|
|
228
|
+
`[SDD TDD Rhythm Gate] 任务 ${taskId} 是 REFACTOR 任务,` +
|
|
229
|
+
'标记完成前必须先运行测试确认全部通过(测试仍绿)。\n\n' +
|
|
230
|
+
'未检测到对应的 test_result(result=success, tdd_phase=refactor) 遥测事件。');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 安全网 3: unknown 类型降级为警告
|
|
235
|
+
if (taskType === 'unknown') {
|
|
236
|
+
console.log(JSON.stringify({
|
|
237
|
+
decision: 'allow',
|
|
238
|
+
message: `⚠️ [SDD TDD Rhythm Gate] 任务 ${taskId} 的类型字段无法识别,已放行。` +
|
|
239
|
+
'建议补全 **类型** 字段,可选值:测试-RED | 实现-GREEN | 重构-REFACTOR | 配置 | 数据层 | 接口层 | 接口测试',
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
// non-tdd 类型:直接放行
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ═══ E1/E2/E3: 全局 TDD 批量执行模式检测(总是执行,不受 skipPerTaskCheck 影响) ═══
|
|
247
|
+
const batchPattern = core.detectTddBatchPattern(
|
|
248
|
+
events,
|
|
249
|
+
activeStage.timestamp,
|
|
250
|
+
newContent
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
if (batchPattern.has_violation && batchPattern.severity === 'critical') {
|
|
254
|
+
const criticalViolations = batchPattern.violations.filter(v => v.severity === 'critical');
|
|
255
|
+
const violationMessages = criticalViolations.map(v =>
|
|
256
|
+
`【${v.type}】${v.message}`
|
|
257
|
+
).join('\n');
|
|
258
|
+
|
|
259
|
+
// 构建恢复引导
|
|
260
|
+
const tddTaskIds = batchPattern.tdd_events
|
|
261
|
+
.map(e => e.task_id)
|
|
262
|
+
.filter((v, i, a) => a.indexOf(v) === i);
|
|
263
|
+
|
|
264
|
+
core.blockWithReason(
|
|
265
|
+
`⛔ [SDD TDD Rhythm Gate] 检测到 TDD 批量执行违规!\n\n` +
|
|
266
|
+
`违规详情:\n${violationMessages}\n\n` +
|
|
267
|
+
`涉及任务:${tddTaskIds.join(', ')}\n\n` +
|
|
268
|
+
`━━━ 恢复步骤 ━━━\n` +
|
|
269
|
+
`1. 回退相关源代码到第一个违规任务之前的状态\n` +
|
|
270
|
+
`2. 逐对重新执行 RED→GREEN 循环(每对间隔 > 60 秒)\n` +
|
|
271
|
+
`3. 每次运行测试后记录 test_result 事件\n` +
|
|
272
|
+
`4. 全部重新执行后,再次更新 tasks.md\n\n` +
|
|
273
|
+
`注意:重新执行产生的新 test_result 事件会覆盖旧的批量事件。\n` +
|
|
274
|
+
`Hook 检查每个任务的最新事件,不会卡死。\n` +
|
|
275
|
+
`━━━━━━━━━━━━`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (batchPattern.has_violation && batchPattern.severity === 'warning') {
|
|
279
|
+
const warningViolations = batchPattern.violations.filter(v => v.severity === 'warning');
|
|
280
|
+
const warningMessages = warningViolations.map(v =>
|
|
281
|
+
`【${v.type}】${v.message}`
|
|
282
|
+
).join('\n');
|
|
283
|
+
console.log(JSON.stringify({
|
|
284
|
+
decision: 'allow',
|
|
285
|
+
message: `⚠️ [SDD TDD Rhythm Gate] TDD 节奏警告:\n${warningMessages}\n` +
|
|
286
|
+
'建议检查是否批量执行了测试。',
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
process.exit(0);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = {
|
|
294
|
+
detectStatusChanges, detectTaskType, findPairedRedTaskId,
|
|
295
|
+
loadChangeEvents, readTestStrategy,
|
|
296
|
+
};
|
|
@@ -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
|
}
|
|
@@ -13,12 +13,16 @@ delta-state: "added"
|
|
|
13
13
|
predecessor-version: "" # added 留空;modified/removed 指向直接前序版本
|
|
14
14
|
mode: "" # full=分 Capability 产物,simple=根目录精简产物
|
|
15
15
|
test-strategy: "" # tdd=测试先行, impl-first=实现优先, none=无测试
|
|
16
|
-
#
|
|
16
|
+
change-type: "" # config | transaction | report | composite | unknown,由 Agent 自动分类
|
|
17
|
+
change-type-source: "agent-inferred"
|
|
18
|
+
change-type-confidence: "" # high | medium | low
|
|
19
|
+
change-type-reason: "" # 一句中文分类理由
|
|
20
|
+
# 外部需求键(默认必填 ≥1 个;编号格式:大写字母+数字+连字符+下划线,例 KLERP-001、REQ-FI-2024-001;探索性/纯内部重构可走 numbering-waiver)
|
|
17
21
|
requirement-refs:
|
|
18
22
|
- system: requirement-mgmt
|
|
19
23
|
object-type: requirement
|
|
20
|
-
external-id:
|
|
21
|
-
feature-id:
|
|
24
|
+
external-id: "<外部需求编号>" # 例 KLERP-001, REQ-FI-2024-001, 1010000
|
|
25
|
+
feature-id: "<外部功能编号>" # 可选,需求所属功能(例 FEAT-FI-012, KLERP_F-001)
|
|
22
26
|
# numbering-waiver:
|
|
23
27
|
# reason: "探索性原型,本轮不种桥"
|
|
24
28
|
# Continuity:字段一律来自知识库 resolve,禁止本地 archive 文件夹名
|