kld-sdd 2.6.17 → 2.6.21

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.
@@ -3,191 +3,346 @@
3
3
  * pre-commit-consistency-check.cjs
4
4
  *
5
5
  * Pre-commit 门禁:检查活跃变更的 Spec 一致性校验报告。
6
- * confidence=low 则阻止提交;缺少报告则警告但放行(向后兼容)。
6
+ * - 默认:缺少报告 / confidence=low → 阻止提交;strict 模式下 medium 也阻止
7
7
  *
8
- * 用法: node pre-commit-consistency-check.cjs --project=<project-root> [--change=<change-name>]
8
+ * 通过环境变量控制:
9
+ * - SDD_SKIP_CONSISTENCY_CHECK=1 → 跳过校验
10
+ * - SDD_STRICT_CONSISTENCY=1 → 严格模式(medium 也阻止)
11
+ * - SDD_WARN_ONLY=1 → 仅告警不拦截
12
+ * - 注意:三个环境变量互斥,只能启用一个
9
13
  */
10
-
11
14
  'use strict';
12
15
 
13
16
  const fs = require('fs');
14
17
  const path = require('path');
18
+ const { execFileSync } = require('child_process');
15
19
 
16
- function readArg(name) {
20
+ const readArg = (name) => {
17
21
  const prefix = `--${name}=`;
18
- const found = process.argv.find(arg => arg.startsWith(prefix));
22
+ const found = process.argv.find(a => a.startsWith(prefix));
19
23
  return found ? found.slice(prefix.length) : '';
24
+ };
25
+
26
+ // --- Spec 根路径解析 ---
27
+
28
+ function resolveSpecRoot() {
29
+ // .cjs 位于 <spec-root>/skywalk-sdd/git-hooks/,直接用 __dirname 推算
30
+ return path.resolve(__dirname, '..', '..');
20
31
  }
21
32
 
22
- function hasFlag(name) {
23
- return process.argv.includes(`--${name}`);
33
+ function discoverActiveChanges(specRoot) {
34
+ const changesDir = path.join(specRoot, 'openspec', 'changes');
35
+ if (!fs.existsSync(changesDir)) return [];
36
+
37
+ return fs.readdirSync(changesDir).filter(name => {
38
+ const p = path.join(changesDir, name);
39
+ return fs.statSync(p).isDirectory() && !name.startsWith('.') && name !== 'archive';
40
+ });
24
41
  }
25
42
 
26
- /**
27
- * 从 .sdd-spec-root 文件读取 spec 包裹包相对路径(单仓 mono 布局)
28
- */
29
- function resolveSpecRoot(projectRoot) {
30
- // 1. 检查 .sdd-spec-root(单仓 mono 模式)
31
- const hintPath = path.join(projectRoot, '.sdd-spec-root');
32
- if (fs.existsSync(hintPath)) {
33
- const rel = fs.readFileSync(hintPath, 'utf8').trim();
34
- if (rel) {
35
- const specAbs = path.resolve(projectRoot, rel);
36
- if (fs.existsSync(specAbs)) {
37
- return specAbs;
38
- }
39
- }
40
- }
43
+ // --- 报告查找与解析 ---
41
44
 
42
- // 2. 查找 *-sdd-specs 子目录(多仓工作区模式)
43
- const entries = fs.existsSync(projectRoot) ? fs.readdirSync(projectRoot) : [];
44
- for (const name of entries) {
45
- if (/-sdd-specs$/i.test(name)) {
46
- const candidate = path.join(projectRoot, name);
47
- if (fs.statSync(candidate).isDirectory() && fs.existsSync(path.join(candidate, 'openspec'))) {
48
- return candidate;
49
- }
50
- }
45
+ function findReport(specRoot, changeName, type) {
46
+ const names = type === 'json'
47
+ ? ['consistency-report-result.json', 'consistency-report-self-review-result.json']
48
+ : ['consistency-report.md', 'consistency-report-self-review.md'];
49
+
50
+ const found = [];
51
+ for (const n of names) {
52
+ const p = path.join(specRoot, 'openspec', 'changes', changeName, n);
53
+ if (fs.existsSync(p)) found.push(p);
51
54
  }
52
55
 
53
- // 3. 项目根本身就是 spec 仓
54
- if (fs.existsSync(path.join(projectRoot, 'openspec', 'changes'))) {
55
- return projectRoot;
56
+ // 如果两种模式的报告都存在,返回错误标记
57
+ if (found.length > 1) {
58
+ return { error: 'duplicate', paths: found };
56
59
  }
57
60
 
58
- return null;
61
+ return found.length === 1 ? found[0] : null;
62
+ }
63
+
64
+ function parseReport(reportPath) {
65
+ try {
66
+ const data = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
67
+ return {
68
+ confidence: (data.confidence || '').toLowerCase(),
69
+ reviewMode: data.reviewMode || 'independent-review',
70
+ humanConfirmed: data.humanConfirmed === true,
71
+ reportPath,
72
+ };
73
+ } catch (err) {
74
+ return { confidence: '', reviewMode: '', humanConfirmed: false, reportPath, parseError: err.message };
75
+ }
59
76
  }
60
77
 
78
+ // --- 变更文件采集(基于配置文件精准定位) ---
79
+
61
80
  /**
62
- * 发现活跃变更列表
81
+ * 简易 YAML 解析器:仅提取 code_repos 列表
63
82
  */
64
- function discoverActiveChanges(specRoot, explicitChange) {
65
- if (explicitChange) {
66
- return [explicitChange];
83
+ function parseCodeRepos(yamlPath) {
84
+ try {
85
+ const content = fs.readFileSync(yamlPath, 'utf8');
86
+ const match = content.match(/code_repos:\s*\n((?:\s+-\s+\S+\n?)+)/);
87
+ if (!match) return null;
88
+ return match[1]
89
+ .split('\n')
90
+ .map(line => line.replace(/^\s+-\s+/, '').trim())
91
+ .filter(Boolean);
92
+ } catch {
93
+ return null;
67
94
  }
95
+ }
68
96
 
69
- const changesDir = path.join(specRoot, 'openspec', 'changes');
70
- if (!fs.existsSync(changesDir)) {
97
+ function gitDiff(repoRoot) {
98
+ try {
99
+ return execFileSync('git', ['diff', 'HEAD', '--name-only'], {
100
+ cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
101
+ }).split(/\r?\n/).filter(Boolean);
102
+ } catch {
71
103
  return [];
72
104
  }
105
+ }
106
+
107
+ function getModifiedFiles(projectRoot) {
108
+ // 工作区模式:读 .sdd-workspace.yaml 的 code_repos
109
+ const wsYaml = path.join(projectRoot, '.sdd-workspace.yaml');
110
+ if (fs.existsSync(wsYaml)) {
111
+ const repos = parseCodeRepos(wsYaml);
112
+ if (repos && repos.length > 0) {
113
+ const files = [];
114
+ for (const repo of repos) {
115
+ const repoRoot = path.join(projectRoot, repo);
116
+ for (const f of gitDiff(repoRoot)) {
117
+ files.push(path.join(repo, f).replace(/\\/g, '/'));
118
+ }
119
+ }
120
+ return [...new Set(files)];
121
+ }
122
+ }
73
123
 
74
- return fs.readdirSync(changesDir)
75
- .filter(name => {
76
- const fullPath = path.join(changesDir, name);
77
- return fs.statSync(fullPath).isDirectory()
78
- && !name.startsWith('.')
79
- && name !== 'archive';
80
- });
124
+ // 单仓模式 / 兜底:直接对 projectRoot 做 diff
125
+ return gitDiff(projectRoot);
81
126
  }
82
127
 
83
128
  /**
84
- * 查找变更的一致性报告 JSON
129
+ * 获取报告生成时间:优先用 JSON 中的 generatedAt,降级用文件 mtime
85
130
  */
86
- function findConsistencyReport(specRoot, changeName) {
87
- const patterns = [
131
+ function getReportTime(jsonPath) {
132
+ try {
133
+ const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
134
+ if (data.generatedAt) {
135
+ const t = new Date(data.generatedAt).getTime();
136
+ if (!isNaN(t)) return t;
137
+ }
138
+ } catch {}
139
+ // 降级:用文件 mtime
140
+ return fs.statSync(jsonPath).mtimeMs;
141
+ }
142
+
143
+ function isReportStale(specRoot, changeName, modifiedFiles, projectRoot) {
144
+ const jsonPath = findReport(specRoot, changeName, 'json');
145
+ if (!jsonPath) return false;
146
+
147
+ const reportTime = getReportTime(jsonPath);
148
+
149
+ // 排除报告文件本身
150
+ const reportFiles = [
151
+ 'consistency-report.md',
152
+ 'consistency-report-self-review.md',
88
153
  'consistency-report-result.json',
89
154
  'consistency-report-self-review-result.json',
90
155
  ];
91
156
 
92
- for (const pattern of patterns) {
93
- const reportPath = path.join(specRoot, 'openspec', 'changes', changeName, pattern);
94
- if (fs.existsSync(reportPath)) {
95
- return reportPath;
157
+ return modifiedFiles.some(file => {
158
+ if (reportFiles.includes(path.basename(file))) return false;
159
+ const fp = path.join(projectRoot, file);
160
+ return fs.existsSync(fp) && fs.statSync(fp).mtimeMs > reportTime;
161
+ });
162
+ }
163
+
164
+ // --- 提示信息 ---
165
+
166
+ function generateInstructions(missing, parseError, lowConfidence, stale, needsConfirmation, warnOnly) {
167
+ const lines = ['', '=========================================='];
168
+ const needsRegenerate = [...new Set([...missing, ...parseError, ...lowConfidence, ...stale])];
169
+
170
+ if (warnOnly) {
171
+ lines.push('[SDD commit-consistency-check] ⚠️ 仅告警模式(SDD_WARN_ONLY=1),以下问题不会阻止提交:');
172
+ } else {
173
+ lines.push('[SDD commit-consistency-check] 以下变更存在问题,拒绝提交:');
174
+ }
175
+
176
+ lines.push('');
177
+
178
+ if (missing.length) {
179
+ lines.push(' 缺少一致性报告:');
180
+ missing.forEach(c => lines.push(` - ${c}`));
181
+ }
182
+
183
+ if (parseError.length) {
184
+ lines.push(' 报告解析失败(JSON 格式损坏):');
185
+ parseError.forEach(c => lines.push(` - ${c}`));
186
+ }
187
+
188
+ if (lowConfidence.length) {
189
+ lines.push(' 报告置信度不达标:');
190
+ lowConfidence.forEach(c => lines.push(` - ${c}`));
191
+ }
192
+
193
+ if (stale.length) {
194
+ lines.push(' 报告已过期(代码在报告生成后被修改):');
195
+ stale.forEach(c => lines.push(` - ${c}`));
196
+ }
197
+
198
+ if (needsConfirmation.length) {
199
+ lines.push(' 自审模式需人工确认:');
200
+ needsConfirmation.forEach(c => lines.push(` - ${c}`));
201
+ }
202
+
203
+ // 只有需要重新生成报告时才显示 skill 执行建议
204
+ if (needsRegenerate.length > 0) {
205
+ lines.push('');
206
+ lines.push(warnOnly
207
+ ? '建议执行 opsx-consistency-check skill 更新报告:'
208
+ : '请执行 opsx-consistency-check skill 更新报告后再提交:');
209
+ lines.push('');
210
+
211
+ if (needsRegenerate.length === 1) {
212
+ lines.push(` skill: opsx-consistency-check ${needsRegenerate[0]}`);
213
+ } else {
214
+ lines.push(' skill(逐个执行):');
215
+ needsRegenerate.forEach(c => lines.push(` opsx-consistency-check ${c}`));
96
216
  }
217
+
218
+ lines.push(
219
+ '',
220
+ ' 💡 建议:新开对话窗口执行校验,以获得独立、客观的审核结果',
221
+ );
97
222
  }
98
223
 
99
- return null;
100
- }
224
+ // 自审模式人工确认指引
225
+ if (needsConfirmation.length > 0) {
226
+ lines.push('');
227
+ lines.push(' 人工确认步骤:');
228
+ lines.push(' 1. 打开 consistency-report-self-review.md,找到「人工确认(自审模式)」章节');
229
+ lines.push(' 2. 逐条核对断言,在「人工确认」列填写 ✅ 已确认 或 ❌ 有误');
230
+ lines.push(' 3. 确认无误后,编辑 consistency-report-self-review-result.json,将 humanConfirmed 从 false 改为 true');
231
+ lines.push('');
232
+ lines.push(' 报告路径: openspec/changes/<change-name>/consistency-report-self-review.md');
233
+ lines.push(' JSON 路径: openspec/changes/<change-name>/consistency-report-self-review-result.json');
234
+ }
101
235
 
102
- /**
103
- * 解析一致性报告,返回置信度
104
- */
105
- function parseReport(reportPath) {
106
- try {
107
- const content = fs.readFileSync(reportPath, 'utf8');
108
- const data = JSON.parse(content);
109
- return {
110
- confidence: (data.confidence || '').toLowerCase(),
111
- overallResult: (data.overallResult || '').toLowerCase(),
112
- changeName: data.changeName || '',
113
- reportPath,
114
- };
115
- } catch (err) {
116
- return { confidence: '', overallResult: '', changeName: '', reportPath, parseError: err.message };
236
+ // 如果没有需要重新生成的报告,但有其他问题,显示默认报告路径
237
+ if (needsRegenerate.length > 0 && needsConfirmation.length === 0) {
238
+ lines.push(
239
+ '',
240
+ ' 报告路径: openspec/changes/<change-name>/consistency-report.md',
241
+ ' JSON 路径: openspec/changes/<change-name>/consistency-report-result.json',
242
+ );
117
243
  }
244
+
245
+ lines.push('==========================================', '');
246
+
247
+ return lines.join('\n');
118
248
  }
119
249
 
250
+ // --- 主流程 ---
251
+
120
252
  function main() {
121
253
  const projectRoot = path.resolve(readArg('project') || process.env.SDD_PROJECT || process.cwd());
122
- const explicitChange = readArg('change') || process.env.SDD_CHANGE || process.env.OPENSPEC_CHANGE || '';
123
- const strictMode = hasFlag('strict') || process.env.SDD_STRICT_CONSISTENCY === '1';
124
254
 
125
- const specRoot = resolveSpecRoot(projectRoot);
126
- if (!specRoot) {
127
- console.log('SDD consistency-check: 未找到 spec 仓库,跳过一致性校验');
128
- return;
255
+ const skip = process.env.SDD_SKIP_CONSISTENCY_CHECK === '1';
256
+ const strict = process.env.SDD_STRICT_CONSISTENCY === '1';
257
+ const warnOnly = process.env.SDD_WARN_ONLY === '1';
258
+
259
+ // 互斥检查
260
+ const activeModes = [skip, strict, warnOnly].filter(Boolean).length;
261
+ if (activeModes > 1) {
262
+ console.error('[SDD commit-consistency-check] 错误:SDD_SKIP_CONSISTENCY_CHECK、SDD_STRICT_CONSISTENCY、SDD_WARN_ONLY 三个环境变量互斥,只能启用一个');
263
+ process.exit(1);
264
+ }
265
+
266
+ if (skip) {
267
+ console.log('[SDD commit-consistency-check] 跳过校验');
268
+ process.exit(0);
129
269
  }
130
270
 
131
- const changes = discoverActiveChanges(specRoot, explicitChange);
132
- if (changes.length === 0) {
133
- console.log('SDD consistency-check: 无活跃变更,跳过一致性校验');
134
- return;
271
+ if (warnOnly) console.log('[SDD commit-consistency-check] 仅告警模式');
272
+
273
+ const specRoot = resolveSpecRoot();
274
+
275
+ const changes = discoverActiveChanges(specRoot);
276
+ if (!changes.length) {
277
+ console.log('[SDD commit-consistency-check] 无活跃变更,跳过');
278
+ process.exit(0);
135
279
  }
136
280
 
137
- const blockers = [];
281
+ const modifiedFiles = getModifiedFiles(projectRoot);
138
282
  const missing = [];
139
- const passed = [];
283
+ const parseError = [];
284
+ const lowConfidence = [];
285
+ const stale = [];
286
+ const needsConfirmation = [];
287
+
288
+ for (const cn of changes) {
289
+ const rp = findReport(specRoot, cn, 'json');
290
+ if (!rp) {
291
+ missing.push(cn);
292
+ continue;
293
+ }
140
294
 
141
- for (const changeName of changes) {
142
- const reportPath = findConsistencyReport(specRoot, changeName);
143
- if (!reportPath) {
144
- missing.push(changeName);
295
+ // 检查是否存在两种模式的报告
296
+ if (rp.error === 'duplicate') {
297
+ console.warn(`[SDD commit-consistency-check] ⚠️ ${cn} 存在两种模式的报告,请删除其中一个:`);
298
+ rp.paths.forEach(p => console.warn(` - ${p}`));
299
+ parseError.push(cn);
145
300
  continue;
146
301
  }
147
302
 
148
- const report = parseReport(reportPath);
303
+ const report = parseReport(rp);
149
304
  if (report.parseError) {
150
- console.warn(`SDD consistency-check: ⚠️ ${changeName} 报告解析失败: ${report.parseError}`);
151
- missing.push(changeName);
305
+ console.warn(`[SDD commit-consistency-check] ⚠️ ${cn} 报告解析失败`);
306
+ parseError.push(cn);
152
307
  continue;
153
308
  }
154
309
 
155
- if (report.confidence === 'low' || report.overallResult === 'fail') {
156
- blockers.push({ changeName, report });
157
- } else {
158
- passed.push({ changeName, report });
310
+ if (isReportStale(specRoot, cn, modifiedFiles, projectRoot)) {
311
+ console.log(`[SDD commit-consistency-check] ⚠️ ${cn}: 报告已过期`);
312
+ stale.push(cn);
313
+ continue;
159
314
  }
160
- }
161
315
 
162
- // 输出摘要
163
- if (passed.length > 0) {
164
- for (const item of passed) {
165
- console.log(`SDD consistency-check: ✅ ${item.changeName} 置信度=${item.report.confidence}`);
316
+ // 自审模式需要人工确认
317
+ if (report.reviewMode === 'self-review' && !report.humanConfirmed) {
318
+ console.log(`[SDD commit-consistency-check] ⚠️ ${cn}: 自审模式需人工确认`);
319
+ needsConfirmation.push(cn);
320
+ continue;
166
321
  }
167
- }
168
322
 
169
- if (missing.length > 0) {
170
- console.warn(`SDD consistency-check: ⚠️ 以下变更缺少一致性校验报告(允许提交,建议执行 opsx-consistency-check):`);
171
- for (const name of missing) {
172
- console.warn(` - ${name}`);
323
+ const c = report.confidence;
324
+ if (c === 'low' || (c === 'medium' && strict)) {
325
+ console.log(`[SDD commit-consistency-check] ${cn}: 置信度 ${c}`);
326
+ lowConfidence.push(cn);
327
+ continue;
173
328
  }
329
+
330
+ console.log(`[SDD commit-consistency-check] ✅ ${cn}: 置信度 ${c}`);
174
331
  }
175
332
 
176
- if (blockers.length > 0) {
177
- console.error(`SDD consistency-check: 以下变更的一致性校验未通过(confidence=low),阻止提交:`);
178
- for (const item of blockers) {
179
- console.error(` - ${item.changeName} (confidence=${item.report.confidence}, result=${item.report.overallResult})`);
180
- console.error(` 报告路径: ${item.report.reportPath}`);
333
+ if (missing.length || parseError.length || lowConfidence.length || stale.length || needsConfirmation.length) {
334
+ console.log(generateInstructions(missing, parseError, lowConfidence, stale, needsConfirmation, warnOnly));
335
+
336
+ if (warnOnly) {
337
+ console.log('[SDD commit-consistency-check] 仅告警模式,放行提交');
338
+ process.exit(0);
181
339
  }
182
- console.error('');
183
- console.error('请修复不一致项后重新执行 opsx-consistency-check,或使用 --no-verify 跳过(不推荐)。');
184
- process.exit(1);
185
- }
186
340
 
187
- if (strictMode && missing.length > 0) {
188
- console.error('SDD consistency-check: ❌ strict 模式已启用,缺少报告的变更不允许提交。');
189
341
  process.exit(1);
190
342
  }
343
+
344
+ console.log('\n[SDD commit-consistency-check] ✅ 所有变更均已通过一致性校验,放行提交');
345
+ process.exit(0);
191
346
  }
192
347
 
193
348
  main();