kld-sdd 2.6.17 → 2.7.3

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.
@@ -2,196 +2,435 @@
2
2
  /**
3
3
  * pre-push-consistency-check.cjs
4
4
  *
5
- * Pre-push 门禁:检查活跃变更的 Spec 一致性校验报告。
6
- * pre-commit-consistency-check.cjs 类似,但更严格:
7
- * - confidence=low → 阻止推送
8
- * - 缺少报告 → 警告(默认放行);strict 模式下阻止推送
5
+ * Pre-push 轻量门禁:仅检查报告是否存在、是否过期。
6
+ * 完整的置信度校验由 pre-commit 完成,push 阶段不再重复。
9
7
  *
10
- * 用法: node pre-push-consistency-check.cjs --project=<project-root> [--change=<change-name>]
8
+ * 通过环境变量控制:
9
+ * - SDD_SKIP_CONSISTENCY_CHECK=1 → 跳过校验
10
+ * - SDD_WARN_ONLY=1 → 仅告警不拦截
11
+ * - 注意:两个环境变量互斥,只能启用一个
11
12
  */
12
-
13
13
  'use strict';
14
14
 
15
15
  const fs = require('fs');
16
16
  const path = require('path');
17
+ const { execFileSync } = require('child_process');
17
18
 
18
- function readArg(name) {
19
+ const readArg = (name) => {
19
20
  const prefix = `--${name}=`;
20
- const found = process.argv.find(arg => arg.startsWith(prefix));
21
+ const found = process.argv.find(a => a.startsWith(prefix));
21
22
  return found ? found.slice(prefix.length) : '';
23
+ };
24
+
25
+ // --- Spec 根路径解析 ---
26
+
27
+ function resolveSpecRoot() {
28
+ // .cjs 位于 <spec-root>/skywalk-sdd/git-hooks/,直接用 __dirname 推算
29
+ return path.resolve(__dirname, '..', '..');
22
30
  }
23
31
 
24
- function hasFlag(name) {
25
- return process.argv.includes(`--${name}`);
32
+ function discoverActiveChanges(specRoot) {
33
+ const changesDir = path.join(specRoot, 'openspec', 'changes');
34
+ if (!fs.existsSync(changesDir)) return [];
35
+
36
+ return fs.readdirSync(changesDir).filter(name => {
37
+ const p = path.join(changesDir, name);
38
+ return fs.statSync(p).isDirectory() && !name.startsWith('.') && name !== 'archive';
39
+ });
26
40
  }
27
41
 
42
+ // --- Push 范围收敛 ---
43
+
28
44
  /**
29
- * .sdd-spec-root 文件读取 spec 包裹包相对路径(单仓 mono 布局)
45
+ * 读取 pre-push 引用范围。
46
+ * 优先从 shell hook 通过环境变量 SDD_PRE_PUSH_REFS 传入(shell hook 已消费 stdin),
47
+ * 降级时尝试直接读 stdin。
30
48
  */
31
- function resolveSpecRoot(projectRoot) {
32
- // 1. 检查 .sdd-spec-root(单仓 mono 模式)
33
- const hintPath = path.join(projectRoot, '.sdd-spec-root');
34
- if (fs.existsSync(hintPath)) {
35
- const rel = fs.readFileSync(hintPath, 'utf8').trim();
36
- if (rel) {
37
- const specAbs = path.resolve(projectRoot, rel);
38
- if (fs.existsSync(specAbs)) {
39
- return specAbs;
40
- }
41
- }
49
+ function readPushRefLines() {
50
+ const envRefs = process.env.SDD_PRE_PUSH_REFS;
51
+ if (envRefs) {
52
+ // shell hook 用 \n 字面量拼接,也可能含真实换行符
53
+ return envRefs.split(/\\n|\n/).filter(Boolean);
42
54
  }
43
55
 
44
- // 2. 查找 *-sdd-specs 子目录(多仓工作区模式)
45
- const entries = fs.existsSync(projectRoot) ? fs.readdirSync(projectRoot) : [];
46
- for (const name of entries) {
47
- if (/-sdd-specs$/i.test(name)) {
48
- const candidate = path.join(projectRoot, name);
49
- if (fs.statSync(candidate).isDirectory() && fs.existsSync(path.join(candidate, 'openspec'))) {
50
- return candidate;
56
+ try {
57
+ if (process.stdin.isTTY) return [];
58
+ const buffer = fs.readFileSync(0, 'utf8'); // fd 0 = stdin
59
+ return buffer.split(/\r?\n/).filter(Boolean);
60
+ } catch {
61
+ return [];
62
+ }
63
+ }
64
+
65
+ /**
66
+ * 从 pre-push stdin 行解析 commit 范围,获取本次 push 实际变更的文件列表
67
+ */
68
+ function getPushRangeFiles(projectRoot) {
69
+ const lines = readPushRefLines();
70
+ if (lines.length === 0) return null;
71
+
72
+ const files = new Set();
73
+ for (const line of lines) {
74
+ const parts = line.split(/\s+/);
75
+ if (parts.length < 4) continue;
76
+ const localSha = parts[1];
77
+ const remoteSha = parts[3];
78
+
79
+ // 删除分支:localSha 为全 0
80
+ if (localSha === '0000000000000000000000000000000000000000') continue;
81
+
82
+ try {
83
+ let diffFiles;
84
+ if (remoteSha === '0000000000000000000000000000000000000000') {
85
+ // 新建分支:用 git show 获取该 commit 引入的文件
86
+ diffFiles = execFileSync('git', ['show', '--name-only', '--pretty=format:', localSha], {
87
+ cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
88
+ }).split(/\r?\n/).filter(Boolean);
89
+ } else {
90
+ // 已有分支:用 git diff 获取范围变更
91
+ diffFiles = execFileSync('git', ['diff', `${remoteSha}..${localSha}`, '--name-only'], {
92
+ cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
93
+ }).split(/\r?\n/).filter(Boolean);
51
94
  }
95
+ for (const f of diffFiles) files.add(f.replace(/\\/g, '/'));
96
+ } catch {
97
+ // 单个 ref 失败不阻断,继续处理其他 ref
52
98
  }
53
99
  }
54
100
 
55
- // 3. 项目根本身就是 spec
56
- if (fs.existsSync(path.join(projectRoot, 'openspec', 'changes'))) {
57
- return projectRoot;
58
- }
101
+ return files.size > 0 ? Array.from(files) : null;
102
+ }
103
+
104
+ function normalizePath(filePath) {
105
+ return filePath.replace(/\\/g, '/');
106
+ }
59
107
 
60
- return null;
108
+ function getChangeNameFromPath(filePath) {
109
+ const normalized = normalizePath(filePath);
110
+ const match = normalized.match(/^openspec\/changes\/([^/]+)\//);
111
+ return match ? match[1] : '';
61
112
  }
62
113
 
63
114
  /**
64
- * 发现活跃变更列表
115
+ * 从 push 的变更文件列表中提取涉及的 change name
65
116
  */
66
- function discoverActiveChanges(specRoot, explicitChange) {
67
- if (explicitChange) {
68
- return [explicitChange];
117
+ function discoverChangesFromPushFiles(projectRoot, specRoot) {
118
+ const pushFiles = getPushRangeFiles(projectRoot);
119
+ if (!pushFiles) return null; // 降级信号
120
+
121
+ const changes = new Set();
122
+ for (const file of pushFiles) {
123
+ const changeName = getChangeNameFromPath(file);
124
+ if (changeName) changes.add(changeName);
69
125
  }
70
126
 
71
- const changesDir = path.join(specRoot, 'openspec', 'changes');
72
- if (!fs.existsSync(changesDir)) {
73
- return [];
127
+ if (changes.size === 0) {
128
+ console.log('[SDD push-consistency-check] 本次 push 不涉及 openspec 变更,跳过');
129
+ process.exit(0);
74
130
  }
75
131
 
76
- return fs.readdirSync(changesDir)
77
- .filter(name => {
78
- const fullPath = path.join(changesDir, name);
79
- return fs.statSync(fullPath).isDirectory()
80
- && !name.startsWith('.')
81
- && name !== 'archive';
82
- });
132
+ // 过滤掉已归档的 change
133
+ const activeChanges = new Set(discoverActiveChanges(specRoot));
134
+ return Array.from(changes).filter(cn => activeChanges.has(cn));
83
135
  }
84
136
 
85
- /**
86
- * 查找变更的一致性报告 JSON
87
- */
88
- function findConsistencyReport(specRoot, changeName) {
89
- const patterns = [
90
- 'consistency-report-result.json',
91
- 'consistency-report-self-review-result.json',
92
- ];
137
+ // --- 报告查找与解析 ---
93
138
 
94
- for (const pattern of patterns) {
95
- const reportPath = path.join(specRoot, 'openspec', 'changes', changeName, pattern);
96
- if (fs.existsSync(reportPath)) {
97
- return reportPath;
98
- }
139
+ function findReport(specRoot, changeName, type) {
140
+ const names = type === 'json'
141
+ ? ['consistency-report-result.json', 'consistency-report-self-review-result.json']
142
+ : ['consistency-report.md', 'consistency-report-self-review.md'];
143
+
144
+ const found = [];
145
+ for (const n of names) {
146
+ const p = path.join(specRoot, 'openspec', 'changes', changeName, n);
147
+ if (fs.existsSync(p)) found.push(p);
148
+ }
149
+
150
+ // 如果两种模式的报告都存在,返回错误标记
151
+ if (found.length > 1) {
152
+ return { error: 'duplicate', paths: found };
99
153
  }
100
154
 
101
- return null;
155
+ return found.length === 1 ? found[0] : null;
102
156
  }
103
157
 
104
- /**
105
- * 解析一致性报告,返回置信度
106
- */
107
158
  function parseReport(reportPath) {
108
159
  try {
109
- const content = fs.readFileSync(reportPath, 'utf8');
110
- const data = JSON.parse(content);
160
+ const data = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
111
161
  return {
112
162
  confidence: (data.confidence || '').toLowerCase(),
113
- overallResult: (data.overallResult || '').toLowerCase(),
114
- changeName: data.changeName || '',
163
+ reviewMode: data.reviewMode || 'independent-review',
164
+ humanConfirmed: data.humanConfirmed === true,
115
165
  reportPath,
116
166
  };
117
167
  } catch (err) {
118
- return { confidence: '', overallResult: '', changeName: '', reportPath, parseError: err.message };
168
+ return { confidence: '', reviewMode: '', humanConfirmed: false, reportPath, parseError: err.message };
119
169
  }
120
170
  }
121
171
 
172
+ // --- 变更文件采集(基于配置文件精准定位) ---
173
+
174
+ /**
175
+ * 简易 YAML 解析器:仅提取 code_repos 列表
176
+ */
177
+ function parseCodeRepos(yamlPath) {
178
+ try {
179
+ const content = fs.readFileSync(yamlPath, 'utf8');
180
+ const match = content.match(/code_repos:\s*\n((?:\s+-\s+\S+\n?)+)/);
181
+ if (!match) return null;
182
+ return match[1]
183
+ .split('\n')
184
+ .map(line => line.replace(/^\s+-\s+/, '').trim())
185
+ .filter(Boolean);
186
+ } catch {
187
+ return null;
188
+ }
189
+ }
190
+
191
+ function gitDiff(repoRoot) {
192
+ try {
193
+ return execFileSync('git', ['diff', 'HEAD', '--name-only'], {
194
+ cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
195
+ }).split(/\r?\n/).filter(Boolean);
196
+ } catch {
197
+ return [];
198
+ }
199
+ }
200
+
201
+ function getModifiedFiles(projectRoot) {
202
+ // 工作区模式:读 .sdd-workspace.yaml 的 code_repos
203
+ const wsYaml = path.join(projectRoot, '.sdd-workspace.yaml');
204
+ if (fs.existsSync(wsYaml)) {
205
+ const repos = parseCodeRepos(wsYaml);
206
+ if (repos && repos.length > 0) {
207
+ const files = [];
208
+ for (const repo of repos) {
209
+ const repoRoot = path.join(projectRoot, repo);
210
+ for (const f of gitDiff(repoRoot)) {
211
+ files.push(path.join(repo, f).replace(/\\/g, '/'));
212
+ }
213
+ }
214
+ return [...new Set(files)];
215
+ }
216
+ }
217
+
218
+ // 单仓模式 / 兜底:直接对 projectRoot 做 diff
219
+ return gitDiff(projectRoot);
220
+ }
221
+
222
+ /**
223
+ * 获取报告生成时间:优先用 JSON 中的 generatedAt,降级用文件 mtime
224
+ */
225
+ function getReportTime(jsonPath) {
226
+ try {
227
+ const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
228
+ if (data.generatedAt) {
229
+ const t = new Date(data.generatedAt).getTime();
230
+ if (!isNaN(t)) return t;
231
+ }
232
+ } catch {}
233
+ // 降级:用文件 mtime
234
+ return fs.statSync(jsonPath).mtimeMs;
235
+ }
236
+
237
+ function isReportStale(specRoot, changeName, modifiedFiles, projectRoot) {
238
+ const jsonPath = findReport(specRoot, changeName, 'json');
239
+ if (!jsonPath) return false;
240
+
241
+ const reportTime = getReportTime(jsonPath);
242
+
243
+ // 排除报告文件本身
244
+ const reportFiles = [
245
+ 'consistency-report.md',
246
+ 'consistency-report-self-review.md',
247
+ 'consistency-report-result.json',
248
+ 'consistency-report-self-review-result.json',
249
+ ];
250
+
251
+ return modifiedFiles.some(file => {
252
+ if (reportFiles.includes(path.basename(file))) return false;
253
+ const fp = path.join(projectRoot, file);
254
+ return fs.existsSync(fp) && fs.statSync(fp).mtimeMs > reportTime;
255
+ });
256
+ }
257
+
258
+ // --- 提示信息 ---
259
+
260
+ function generateInstructions(missing, stale, needsConfirmation, warnOnly) {
261
+ const lines = ['', '=========================================='];
262
+ const needsRegenerate = [...new Set([...missing, ...stale])];
263
+
264
+ if (warnOnly) {
265
+ lines.push('[SDD push-consistency-check] ⚠️ 仅告警模式(SDD_WARN_ONLY=1),以下问题不会阻止推送:');
266
+ } else {
267
+ lines.push('[SDD push-consistency-check] 以下变更存在问题,拒绝推送:');
268
+ }
269
+
270
+ lines.push('');
271
+
272
+ if (missing.length) {
273
+ lines.push(' 缺少一致性报告:');
274
+ missing.forEach(c => lines.push(` - ${c}`));
275
+ }
276
+
277
+ if (stale.length) {
278
+ lines.push(' 报告已过期(代码在报告生成后被修改):');
279
+ stale.forEach(c => lines.push(` - ${c}`));
280
+ }
281
+
282
+ if (needsConfirmation.length) {
283
+ lines.push(' 自审模式需人工确认:');
284
+ needsConfirmation.forEach(c => lines.push(` - ${c}`));
285
+ }
286
+
287
+ // 只有需要重新生成报告时才显示 skill 执行建议
288
+ if (needsRegenerate.length > 0) {
289
+ lines.push('');
290
+ lines.push(warnOnly
291
+ ? '建议执行 opsx-consistency-check skill 更新报告:'
292
+ : '请执行 opsx-consistency-check skill 更新报告后再推送:');
293
+ lines.push('');
294
+
295
+ if (needsRegenerate.length === 1) {
296
+ lines.push(` skill: opsx-consistency-check ${needsRegenerate[0]}`);
297
+ } else {
298
+ lines.push(' skill(逐个执行):');
299
+ needsRegenerate.forEach(c => lines.push(` opsx-consistency-check ${c}`));
300
+ }
301
+
302
+ lines.push(
303
+ '',
304
+ ' 💡 建议:新开对话窗口执行校验,以获得独立、客观的审核结果',
305
+ );
306
+ }
307
+
308
+ // 自审模式人工确认指引
309
+ if (needsConfirmation.length > 0) {
310
+ lines.push('');
311
+ lines.push(' 人工确认步骤:');
312
+ lines.push(' 1. 打开 consistency-report-self-review.md,找到「人工确认(自审模式)」章节');
313
+ lines.push(' 2. 逐条核对断言,在「人工确认」列填写 ✅ 已确认 或 ❌ 有误');
314
+ lines.push(' 3. 确认无误后,编辑 consistency-report-self-review-result.json,将 humanConfirmed 从 false 改为 true');
315
+ lines.push('');
316
+ lines.push(' 报告路径: openspec/changes/<change-name>/consistency-report-self-review.md');
317
+ lines.push(' JSON 路径: openspec/changes/<change-name>/consistency-report-self-review-result.json');
318
+ }
319
+
320
+ // 如果没有需要重新生成的报告,但有其他问题,显示默认报告路径
321
+ if (needsRegenerate.length > 0 && needsConfirmation.length === 0) {
322
+ lines.push(
323
+ '',
324
+ ' 报告路径: openspec/changes/<change-name>/consistency-report.md',
325
+ ' JSON 路径: openspec/changes/<change-name>/consistency-report-result.json',
326
+ );
327
+ }
328
+
329
+ lines.push('==========================================', '');
330
+
331
+ return lines.join('\n');
332
+ }
333
+
334
+ // --- 主流程 ---
335
+
122
336
  function main() {
123
337
  const projectRoot = path.resolve(readArg('project') || process.env.SDD_PROJECT || process.cwd());
124
- const explicitChange = readArg('change') || process.env.SDD_CHANGE || process.env.OPENSPEC_CHANGE || '';
125
- const strictMode = hasFlag('strict') || process.env.SDD_STRICT_CONSISTENCY === '1';
126
- const looseMode = hasFlag('no-strict') || process.env.SDD_LOOSE_CONSISTENCY === '1';
127
338
 
128
- const specRoot = resolveSpecRoot(projectRoot);
129
- if (!specRoot) {
130
- console.log('SDD consistency-check (pre-push): 未找到 spec 仓库,跳过一致性校验');
131
- return;
339
+ const skip = process.env.SDD_SKIP_CONSISTENCY_CHECK === '1';
340
+ const warnOnly = process.env.SDD_WARN_ONLY === '1';
341
+
342
+ // 互斥检查
343
+ if (skip && warnOnly) {
344
+ console.error('[SDD push-consistency-check] 错误:SDD_SKIP_CONSISTENCY_CHECK 与 SDD_WARN_ONLY 互斥,只能启用一个');
345
+ process.exit(1);
346
+ }
347
+
348
+ if (skip) {
349
+ console.log('[SDD push-consistency-check] 跳过校验');
350
+ process.exit(0);
351
+ }
352
+
353
+ if (warnOnly) console.log('[SDD push-consistency-check] 仅告警模式');
354
+
355
+ const specRoot = resolveSpecRoot();
356
+
357
+ // 优先尝试按本次 push 范围收敛变更
358
+ let changes = discoverChangesFromPushFiles(projectRoot, specRoot);
359
+ let modifiedFiles = null;
360
+
361
+ if (changes === null) {
362
+ // 降级:stdin 读取失败,回退到现有行为
363
+ console.log('[SDD push-consistency-check] 无法读取 push 范围,降级为全量检查');
364
+ changes = discoverActiveChanges(specRoot);
365
+ modifiedFiles = getModifiedFiles(projectRoot);
366
+ } else if (changes.length === 0) {
367
+ console.log('[SDD push-consistency-check] 无相关活跃变更,跳过');
368
+ process.exit(0);
369
+ } else {
370
+ // 收敛成功:用 push 范围文件做过期判定
371
+ modifiedFiles = getPushRangeFiles(projectRoot) || [];
132
372
  }
133
373
 
134
- const changes = discoverActiveChanges(specRoot, explicitChange);
135
- if (changes.length === 0) {
136
- console.log('SDD consistency-check (pre-push): 无活跃变更,跳过一致性校验');
137
- return;
374
+ if (!changes.length) {
375
+ console.log('[SDD push-consistency-check] 无活跃变更,跳过');
376
+ process.exit(0);
138
377
  }
139
378
 
140
- const blockers = [];
141
379
  const missing = [];
142
- const passed = [];
380
+ const stale = [];
381
+ const needsConfirmation = [];
143
382
 
144
- for (const changeName of changes) {
145
- const reportPath = findConsistencyReport(specRoot, changeName);
146
- if (!reportPath) {
147
- missing.push(changeName);
383
+ for (const cn of changes) {
384
+ const rp = findReport(specRoot, cn, 'json');
385
+ if (!rp) {
386
+ missing.push(cn);
148
387
  continue;
149
388
  }
150
389
 
151
- const report = parseReport(reportPath);
152
- if (report.parseError) {
153
- console.warn(`SDD consistency-check (pre-push): ⚠️ ${changeName} 报告解析失败: ${report.parseError}`);
154
- missing.push(changeName);
390
+ // 检查是否存在两种模式的报告
391
+ if (rp.error === 'duplicate') {
392
+ console.warn(`[SDD push-consistency-check] ⚠️ ${cn} 存在两种模式的报告,请删除其中一个:`);
393
+ rp.paths.forEach(p => console.warn(` - ${p}`));
394
+ missing.push(cn);
155
395
  continue;
156
396
  }
157
397
 
158
- if (report.confidence === 'low' || report.overallResult === 'fail') {
159
- blockers.push({ changeName, report });
160
- } else {
161
- passed.push({ changeName, report });
398
+ const report = parseReport(rp);
399
+ if (report.parseError) {
400
+ console.warn(`[SDD push-consistency-check] ⚠️ ${cn}: 报告解析失败`);
401
+ missing.push(cn);
402
+ continue;
162
403
  }
163
- }
164
404
 
165
- // 输出摘要
166
- if (passed.length > 0) {
167
- for (const item of passed) {
168
- console.log(`SDD consistency-check (pre-push): ✅ ${item.changeName} 置信度=${item.report.confidence}`);
405
+ if (isReportStale(specRoot, cn, modifiedFiles, projectRoot)) {
406
+ console.log(`[SDD push-consistency-check] ⚠️ ${cn}: 报告已过期`);
407
+ stale.push(cn);
408
+ continue;
169
409
  }
170
- }
171
410
 
172
- if (missing.length > 0) {
173
- console.warn(`SDD consistency-check (pre-push): ⚠️ 以下变更缺少一致性校验报告:`);
174
- for (const name of missing) {
175
- console.warn(` - ${name}`);
176
- }
177
- if (strictMode && !looseMode) {
178
- console.error('SDD consistency-check (pre-push): ❌ strict 模式已启用,缺少报告的变更不允许推送。');
179
- console.error('请先执行 opsx-consistency-check 生成报告,或使用 --no-verify 跳过(不推荐)。');
180
- process.exit(1);
411
+ // 自审模式需要人工确认
412
+ if (report.reviewMode === 'self-review' && !report.humanConfirmed) {
413
+ console.log(`[SDD push-consistency-check] ⚠️ ${cn}: 自审模式需人工确认`);
414
+ needsConfirmation.push(cn);
415
+ continue;
181
416
  }
182
- console.warn(' → 推送已放行,建议尽快执行 opsx-consistency-check');
417
+
418
+ console.log(`[SDD push-consistency-check] ✅ ${cn}: 报告有效`);
183
419
  }
184
420
 
185
- if (blockers.length > 0) {
186
- console.error(`SDD consistency-check (pre-push): ❌ 以下变更的一致性校验未通过(confidence=low),阻止推送:`);
187
- for (const item of blockers) {
188
- console.error(` - ${item.changeName} (confidence=${item.report.confidence}, result=${item.report.overallResult})`);
189
- console.error(` 报告路径: ${item.report.reportPath}`);
421
+ if (missing.length || stale.length || needsConfirmation.length) {
422
+ console.log(generateInstructions(missing, stale, needsConfirmation, warnOnly));
423
+
424
+ if (warnOnly) {
425
+ console.log('[SDD push-consistency-check] 仅告警模式,放行推送');
426
+ process.exit(0);
190
427
  }
191
- console.error('');
192
- console.error('请修复不一致项后重新执行 opsx-consistency-check,或使用 --no-verify 跳过(不推荐)。');
428
+
193
429
  process.exit(1);
194
430
  }
431
+
432
+ console.log('\n[SDD push-consistency-check] ✅ 所有变更均已通过一致性校验,放行推送');
433
+ process.exit(0);
195
434
  }
196
435
 
197
436
  main();
@@ -4,10 +4,10 @@ description: "质量检查技能 - 验证文档完整性、一致性、算法正
4
4
  argument-hint: "[change-name] [上下文文件...]"
5
5
  license: MIT
6
6
  compatibility: Requires openspec CLI; depends on opsx-ontology-query for external-key validation and baseline checks.
7
- depends-on: opsx-ontology-query
8
7
  metadata:
9
8
  author: sdd-team
10
9
  version: "3.0"
10
+ depends-on: opsx-ontology-query
11
11
  allowed-tools:
12
12
  - Bash
13
13
  - Read