kld-sdd 2.7.3 → 2.7.8-2

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 (74) hide show
  1. package/README.md +12 -5
  2. package/USABILITY.md +104 -0
  3. package/bin/kld-sdd-init.js +1 -1
  4. package/kld-sdd-guide.html +16 -4
  5. package/lib/hook-gate-core.js +327 -0
  6. package/lib/init.js +228 -37
  7. package/lib/scale-thresholds.json +19 -0
  8. package/lib/skills-bundle.js +2 -1
  9. package/package.json +5 -3
  10. package/skywalk-sdd/context-client.cjs +50 -30
  11. package/skywalk-sdd/index.cjs +159 -40
  12. package/skywalk-sdd/kb-sync-identity.cjs +99 -451
  13. package/skywalk-sdd/kb-upload.cjs +67 -44
  14. package/skywalk-sdd/lib/check-review.cjs +41 -0
  15. package/skywalk-sdd/lib/test-execution.cjs +110 -0
  16. package/skywalk-sdd/lib/usage-contract.cjs +3 -2
  17. package/skywalk-sdd/lib/usage-reporter.cjs +27 -2
  18. package/skywalk-sdd/metrics-v3.cjs +2 -2
  19. package/skywalk-sdd/ontology/active-changes.cjs +2 -1
  20. package/skywalk-sdd/ontology/archive-package.cjs +1 -1
  21. package/skywalk-sdd/ontology/artifact-parser.cjs +7 -4
  22. package/skywalk-sdd/ontology/id.cjs +5 -4
  23. package/skywalk-sdd/ontology/identity-index.cjs +9 -4
  24. package/skywalk-sdd/ontology/list-changes.cjs +1 -1
  25. package/skywalk-sdd/ontology/runtime.cjs +7 -3
  26. package/skywalk-sdd/ontology/schema.cjs +2 -0
  27. package/skywalk-sdd/ontology/traceability-validator.cjs +74 -4
  28. package/skywalk-sdd/ontology/workspace-layout.cjs +25 -5
  29. package/skywalk-sdd/reporting/change-report-model.cjs +4 -3
  30. package/templates/git-hooks/commit-msg +39 -24
  31. package/templates/git-hooks/consistency-check-core.cjs +1097 -0
  32. package/templates/git-hooks/hooks.config +20 -1
  33. package/templates/git-hooks/pre-commit +39 -24
  34. package/templates/git-hooks/pre-commit-consistency-check.cjs +29 -332
  35. package/templates/git-hooks/pre-commit-sdd-check.cjs +98 -0
  36. package/templates/git-hooks/pre-push +39 -24
  37. package/templates/git-hooks/pre-push-consistency-check.cjs +58 -406
  38. package/templates/hooks/claude/hooks/sdd-post-tool.cjs +2 -2
  39. package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +2 -2
  40. package/templates/hooks/codebuddy/hooks/sdd-tdd-rhythm-gate.cjs +1 -1
  41. package/templates/openspec/tasks.md +3 -3
  42. package/templates/skills/kld-sdd/opsx-apply/SKILL.md +44 -6
  43. package/templates/skills/kld-sdd/opsx-apply/checklist.md +1 -1
  44. package/templates/skills/kld-sdd/opsx-apply/reference.md +20 -2
  45. package/templates/skills/kld-sdd/opsx-archive/SKILL.md +19 -21
  46. package/templates/skills/kld-sdd/opsx-check/SKILL.md +55 -327
  47. package/templates/skills/kld-sdd/opsx-check/checklist.md +7 -4
  48. package/templates/skills/kld-sdd/opsx-check/reference.md +60 -0
  49. package/templates/skills/kld-sdd/opsx-check/result-template.json +52 -0
  50. package/templates/skills/kld-sdd/opsx-check/review-template.json +22 -0
  51. package/templates/skills/kld-sdd/opsx-check/reviewer.md +100 -0
  52. package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +82 -451
  53. package/templates/skills/kld-sdd/opsx-consistency-check/{reference.md → references/reference.md} +0 -1
  54. package/templates/skills/kld-sdd/opsx-consistency-check/scripts/scripts.cjs +517 -0
  55. package/templates/skills/kld-sdd/opsx-design/SKILL.md +10 -30
  56. package/templates/skills/kld-sdd/opsx-design/checklist.md +3 -4
  57. package/templates/skills/kld-sdd/opsx-design/reference.md +1 -1
  58. package/templates/skills/kld-sdd/opsx-kb-config/SKILL.md +29 -154
  59. package/templates/skills/kld-sdd/opsx-kb-config/reference.md +8 -11
  60. package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +12 -74
  61. package/templates/skills/kld-sdd/opsx-kb-ingest/reference.md +2 -2
  62. package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +7 -5
  63. package/templates/skills/kld-sdd/opsx-ontology-query/phase-1-prechange.md +2 -2
  64. package/templates/skills/kld-sdd/opsx-ontology-query/reference.md +1 -1
  65. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +29 -74
  66. package/templates/skills/kld-sdd/opsx-propose/checklist.md +8 -8
  67. package/templates/skills/kld-sdd/opsx-propose/interaction-policy.md +35 -0
  68. package/templates/skills/kld-sdd/opsx-propose/reference.md +12 -46
  69. package/templates/skills/kld-sdd/opsx-spec/SKILL.md +14 -61
  70. package/templates/skills/kld-sdd/opsx-spec/checklist.md +3 -3
  71. package/templates/skills/kld-sdd/opsx-task/SKILL.md +40 -34
  72. package/templates/skills/kld-sdd/opsx-task/checklist.md +5 -6
  73. package/templates/skills/kld-sdd/opsx-test/SKILL.md +2 -0
  74. package/templates/skills/kld-sdd/tdd-rules/rules/tdd-strategy-selection.md +1 -1
@@ -4,36 +4,49 @@
4
4
  # 删除某行 = 禁用该插件
5
5
  # marker: KLD SDD quality gate
6
6
 
7
- # hook 所在目录推算 git 根目录
8
- hook_dir="$(cd "$(dirname "$0")" && pwd)"
9
- git_dir="$(dirname "$hook_dir")"
10
- git_root="$(dirname "$git_dir")"
11
-
7
+ # Git supplies the working tree context; this also supports worktrees and core.hooksPath.
8
+ git_root=$(git rev-parse --show-toplevel 2>/dev/null) || {
9
+ echo "[SDD] 无法定位当前 Git 工作区"
10
+ exit 1
11
+ }
12
12
  hooks_dir=""
13
-
14
- # 方法1: git config sdd.specPath(多仓布局:spec 是独立 clone)
15
- spec_path=$(git config --local sdd.specPath 2>/dev/null)
16
- if [ -n "$spec_path" ] && [ -d "$spec_path/skywalk-sdd/git-hooks" ]; then
17
- hooks_dir="$spec_path/skywalk-sdd/git-hooks"
13
+ # 统一反斜杠为正斜杠,保证 Windows 盘符路径(P:\a\b → P:/a/b)能被 case 判定为绝对路径
14
+ normalize_path() {
15
+ printf '%s' "$1" | tr '\\' '/'
16
+ }
17
+ # 绝对路径(/ 开头或盘符 X: 开头)直接使用;相对路径基于 git 根拼接
18
+ resolve_spec_dir() {
19
+ raw=$(normalize_path "$1")
20
+ case "$raw" in
21
+ /*|[A-Za-z]:*) printf '%s' "$raw" ;;
22
+ *) printf '%s' "$git_root/$raw" ;;
23
+ esac
24
+ }
25
+ spec_path=$(git -C "$git_root" config --local sdd.specPath 2>/dev/null)
26
+ if [ -n "$spec_path" ]; then
27
+ hooks_dir="$(resolve_spec_dir "$spec_path")/skywalk-sdd/git-hooks"
28
+ elif [ -f "$git_root/.sdd-spec-root" ]; then
29
+ spec_rel=$(head -1 "$git_root/.sdd-spec-root" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
30
+ hooks_dir="$(resolve_spec_dir "$spec_rel")/skywalk-sdd/git-hooks"
18
31
  fi
19
-
20
- # 方法2: .sdd-spec-root(单仓 mono 布局:skywalk-sdd 在 spec 包裹包子目录内)
21
- if [ -z "$hooks_dir" ] && [ -f "$git_root/.sdd-spec-root" ]; then
22
- spec_rel=$(cat "$git_root/.sdd-spec-root" | head -1 | tr -d '[:space:]')
23
- if [ -n "$spec_rel" ] && [ -d "$git_root/$spec_rel/skywalk-sdd/git-hooks" ]; then
24
- hooks_dir="$git_root/$spec_rel/skywalk-sdd/git-hooks"
25
- fi
32
+ if [ -n "$hooks_dir" ] && [ ! -d "$hooks_dir" ]; then
33
+ echo "[SDD] 已配置的 Spec 路径失效:$hooks_dir"
34
+ echo "[SDD] 请恢复 Spec 仓,或修正 sdd.specPath / .sdd-spec-root 后重试"
35
+ exit 1
26
36
  fi
27
37
 
28
38
  # 方法3: 从 git 根向上搜索 skywalk-sdd/git-hooks(spec 仓本身或父级)
29
39
  if [ -z "$hooks_dir" ]; then
30
40
  search_dir="$git_root"
31
- while [ "$search_dir" != "/" ] && [ "$search_dir" != "" ]; do
41
+ while [ -n "$search_dir" ] && [ "$search_dir" != "/" ]; do
32
42
  if [ -d "$search_dir/skywalk-sdd/git-hooks" ]; then
33
43
  hooks_dir="$search_dir/skywalk-sdd/git-hooks"
34
44
  break
35
45
  fi
36
- search_dir="$(dirname "$search_dir")"
46
+ # Windows 盘符根(P:/)dirname 后不变,需显式收敛,避免死循环
47
+ parent_dir="$(dirname "$search_dir")"
48
+ [ "$parent_dir" = "$search_dir" ] && break
49
+ search_dir="$parent_dir"
37
50
  done
38
51
  fi
39
52
 
@@ -61,8 +74,9 @@ else
61
74
  # 读取 [pre-push] 段落的配置
62
75
  in_section=0
63
76
  SCRIPTS=""
77
+ CR=$(printf '\r')
64
78
  while IFS= read -r line; do
65
- line=$(printf '%s' "$line" | tr -d '\r')
79
+ line=${line%"$CR"}
66
80
  case "$line" in
67
81
  "[pre-push]") in_section=1 ;;
68
82
  "["*) in_section=0 ;;
@@ -81,7 +95,8 @@ failed=0
81
95
  for script_name in $SCRIPTS; do
82
96
  script="$hooks_dir/$script_name"
83
97
  if [ ! -f "$script" ]; then
84
- echo "[SDD] ⚠️ 插件不存在:$script_name"
98
+ echo "[SDD] ❌ 已配置的插件不存在:$script_name"
99
+ failed=$((failed + 1))
85
100
  continue
86
101
  fi
87
102
  executed=$((executed + 1))
@@ -89,11 +104,11 @@ for script_name in $SCRIPTS; do
89
104
  node "$script" --project="$git_root" "$@" || failed=$((failed + 1))
90
105
  done
91
106
 
92
- if [ "$executed" -eq 0 ]; then
93
- echo "[SDD] 无 pre-push 插件,跳过"
94
- elif [ "$failed" -gt 0 ]; then
107
+ if [ "$failed" -gt 0 ]; then
95
108
  echo "[SDD] ❌ 共执行 $executed 个 pre-push 插件,$failed 个失败"
96
109
  exit 1
110
+ elif [ "$executed" -eq 0 ]; then
111
+ echo "[SDD] 配置已禁用 pre-push 插件"
97
112
  else
98
113
  echo "[SDD] ✅ 共执行 $executed 个 pre-push 插件,全部通过"
99
114
  fi
@@ -2,435 +2,87 @@
2
2
  /**
3
3
  * pre-push-consistency-check.cjs
4
4
  *
5
- * Pre-push 轻量门禁:仅检查报告是否存在、是否过期。
6
- * 完整的置信度校验由 pre-commit 完成,push 阶段不再重复。
5
+ * Pre-push 轻量门禁:仅检查待推送范围涉及报告的存在性、是否过期。
6
+ * - diff 驱动:仅检查待推送文件与报告 coveredFiles 有交集的 change,无关 change 跳过
7
+ * - 完整的置信度校验由 pre-commit 完成,push 阶段不再重复。
8
+ * - 过期判断读取 HEAD 已提交内容(而非工作区),避免工作区未提交修改造成误判。
9
+ *
10
+ * 待推送范围采集(逐级降级):
11
+ * 1. git diff @{push}..HEAD(本地领先远程的提交)
12
+ * 2. git diff origin/HEAD..HEAD
13
+ * 3. 远程引用不存在(新分支/新仓库首次推送):git diff <空树>..HEAD,
14
+ * 语义 = 首推时 HEAD 全部文件都是新增。空树 SHA 是 git 内置常量,
15
+ * 不依赖任何远程引用,避免「首推因无远程引用被拦 → 引用永远建立不起来」的死锁
16
+ * 4. 连空树 diff 都失败(如空仓无 HEAD):返回 ok=false,由 core 拦截推送而非静默放行
7
17
  *
8
18
  * 通过环境变量控制:
9
19
  * - SDD_SKIP_CONSISTENCY_CHECK=1 → 跳过校验
10
20
  * - SDD_WARN_ONLY=1 → 仅告警不拦截
11
21
  * - 注意:两个环境变量互斥,只能启用一个
22
+ *
23
+ * 共享逻辑见同目录 consistency-check-core.cjs。
12
24
  */
13
25
  'use strict';
14
26
 
15
- const fs = require('fs');
16
- const path = require('path');
27
+ const core = require('./consistency-check-core.cjs');
28
+ const { gitDiffNameStatus } = core;
17
29
  const { execFileSync } = require('child_process');
18
30
 
19
- const readArg = (name) => {
20
- const prefix = `--${name}=`;
21
- const found = process.argv.find(a => a.startsWith(prefix));
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, '..', '..');
30
- }
31
-
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
- });
40
- }
41
-
42
- // --- Push 范围收敛 ---
31
+ // git 内置空树对象 SHA(SHA-1 仓库恒定不变)
32
+ const EMPTY_TREE_SHA = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
43
33
 
44
34
  /**
45
- * 读取 pre-push 引用范围。
46
- * 优先从 shell hook 通过环境变量 SDD_PRE_PUSH_REFS 传入(shell hook 已消费 stdin),
47
- * 降级时尝试直接读 stdin。
35
+ * 单仓待推送范围采集:本地领先远程的提交所涉及的文件。
36
+ * files=修改/新增(排除删除);deleted=删除文件。
37
+ * @returns {{files: string[], deleted: string[], ok: boolean}}
48
38
  */
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);
39
+ function gitPushRangeDiff(repoRoot) {
40
+ const ranges = ['@{push}..HEAD', 'origin/HEAD..HEAD'];
41
+ for (const range of ranges) {
42
+ const r = gitDiffNameStatus(repoRoot, ['diff', range]);
43
+ if (r.ok) return r;
54
44
  }
55
-
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 [];
45
+ // 首推降级:远程引用不存在(@{push}/origin/HEAD 均无法解析)时,
46
+ // 用 git 内置空树 SHA 做 diff —— 语义为「首推 = HEAD 全部文件都是新增」。
47
+ // 空树对象不依赖任何远程引用,打破「首推被拦 远程引用永远建立不起来」的死锁。
48
+ const emptyTree = gitEmptyTreeSha(repoRoot);
49
+ if (emptyTree) {
50
+ const r = gitDiffNameStatus(repoRoot, ['diff', `${emptyTree}..HEAD`]);
51
+ if (r.ok) return r;
62
52
  }
53
+ // 连空树 diff 都失败(如空仓无 HEAD):返回失败,由 core 拦截而非静默放行
54
+ return { files: [], deleted: [], ok: false };
63
55
  }
64
56
 
65
57
  /**
66
- * pre-push stdin 行解析 commit 范围,获取本次 push 实际变更的文件列表
58
+ * 读取 git 内置空树 SHA(4b825dc642cb6eb9a060e54bf8d69288fbee4904)。
59
+ * 优先用 git mktree 动态计算(兼容 SHA-256 仓库),失败时回退内置常量。
60
+ * @param {string} repoRoot
61
+ * @returns {string|null}
67
62
  */
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);
94
- }
95
- for (const f of diffFiles) files.add(f.replace(/\\/g, '/'));
96
- } catch {
97
- // 单个 ref 失败不阻断,继续处理其他 ref
98
- }
99
- }
100
-
101
- return files.size > 0 ? Array.from(files) : null;
102
- }
103
-
104
- function normalizePath(filePath) {
105
- return filePath.replace(/\\/g, '/');
106
- }
107
-
108
- function getChangeNameFromPath(filePath) {
109
- const normalized = normalizePath(filePath);
110
- const match = normalized.match(/^openspec\/changes\/([^/]+)\//);
111
- return match ? match[1] : '';
112
- }
113
-
114
- /**
115
- * 从 push 的变更文件列表中提取涉及的 change name
116
- */
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);
125
- }
126
-
127
- if (changes.size === 0) {
128
- console.log('[SDD push-consistency-check] 本次 push 不涉及 openspec 变更,跳过');
129
- process.exit(0);
130
- }
131
-
132
- // 过滤掉已归档的 change
133
- const activeChanges = new Set(discoverActiveChanges(specRoot));
134
- return Array.from(changes).filter(cn => activeChanges.has(cn));
135
- }
136
-
137
- // --- 报告查找与解析 ---
138
-
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 };
153
- }
154
-
155
- return found.length === 1 ? found[0] : null;
156
- }
157
-
158
- function parseReport(reportPath) {
159
- try {
160
- const data = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
161
- return {
162
- confidence: (data.confidence || '').toLowerCase(),
163
- reviewMode: data.reviewMode || 'independent-review',
164
- humanConfirmed: data.humanConfirmed === true,
165
- reportPath,
166
- };
167
- } catch (err) {
168
- return { confidence: '', reviewMode: '', humanConfirmed: false, reportPath, parseError: err.message };
169
- }
170
- }
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) {
63
+ function gitEmptyTreeSha(repoRoot) {
192
64
  try {
193
- return execFileSync('git', ['diff', 'HEAD', '--name-only'], {
194
- cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
195
- }).split(/\r?\n/).filter(Boolean);
65
+ const sha = execFileSync('git', ['mktree'], {
66
+ cwd: repoRoot, encoding: 'utf8', input: '', stdio: ['pipe', 'pipe', 'ignore'],
67
+ }).trim();
68
+ if (/^[0-9a-f]{40,64}$/.test(sha)) return sha;
196
69
  } 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
- );
70
+ // 降级到内置常量
327
71
  }
328
-
329
- lines.push('==========================================', '');
330
-
331
- return lines.join('\n');
72
+ return /^[0-9a-f]{40}$/.test(EMPTY_TREE_SHA) ? EMPTY_TREE_SHA : null;
332
73
  }
333
74
 
334
- // --- 主流程 ---
335
-
336
- function main() {
337
- const projectRoot = path.resolve(readArg('project') || process.env.SDD_PROJECT || process.cwd());
338
-
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) || [];
372
- }
373
-
374
- if (!changes.length) {
375
- console.log('[SDD push-consistency-check] 无活跃变更,跳过');
376
- process.exit(0);
377
- }
378
-
379
- const missing = [];
380
- const stale = [];
381
- const needsConfirmation = [];
382
-
383
- for (const cn of changes) {
384
- const rp = findReport(specRoot, cn, 'json');
385
- if (!rp) {
386
- missing.push(cn);
387
- continue;
388
- }
389
-
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);
395
- continue;
396
- }
397
-
398
- const report = parseReport(rp);
399
- if (report.parseError) {
400
- console.warn(`[SDD push-consistency-check] ⚠️ ${cn}: 报告解析失败`);
401
- missing.push(cn);
402
- continue;
403
- }
404
-
405
- if (isReportStale(specRoot, cn, modifiedFiles, projectRoot)) {
406
- console.log(`[SDD push-consistency-check] ⚠️ ${cn}: 报告已过期`);
407
- stale.push(cn);
408
- continue;
409
- }
410
-
411
- // 自审模式需要人工确认
412
- if (report.reviewMode === 'self-review' && !report.humanConfirmed) {
413
- console.log(`[SDD push-consistency-check] ⚠️ ${cn}: 自审模式需人工确认`);
414
- needsConfirmation.push(cn);
415
- continue;
416
- }
417
-
418
- console.log(`[SDD push-consistency-check] ✅ ${cn}: 报告有效`);
419
- }
420
-
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);
427
- }
428
-
429
- process.exit(1);
430
- }
431
-
432
- console.log('\n[SDD push-consistency-check] ✅ 所有变更均已通过一致性校验,放行推送');
433
- process.exit(0);
75
+ // strict 模式仅 pre-commit 支持:显式提示而非静默忽略
76
+ const _mode = core.loadConsistencyMode();
77
+ if (process.env.SDD_STRICT_CONSISTENCY === '1' || _mode.strict) {
78
+ console.log('[SDD push-consistency-check] ℹ️ SDD_STRICT_CONSISTENCY 仅在 pre-commit 阶段生效,push 阶段忽略');
434
79
  }
435
80
 
436
- main();
81
+ core.runConsistencyCheck({
82
+ label: '[SDD push-consistency-check]',
83
+ actionNoun: '推送',
84
+ diffFn: gitPushRangeDiff,
85
+ checkConfidence: false, // 置信度校验由 pre-commit 完成,push 阶段不重复
86
+ includeUntracked: false, // untracked 文件不在任何 commit 中,不会被推送
87
+ staleFromHead: true, // 过期判断读 HEAD 已提交内容,避免工作区未提交修改误判
88
+ });
@@ -171,8 +171,8 @@ function recordTelemetry(projectRoot, activeStage, record, result, exitCodeKnown
171
171
  skipped: counts.skipped,
172
172
  } : {}),
173
173
  duration_ms: Number.isFinite(toolResponse.duration_ms) ? toolResponse.duration_ms : 0,
174
- failure_type: result === 'failure' ? (exitCode == null ? 'infrastructure' : 'assertion') : 'none',
175
- expected_failure: tddPhase === 'red' && result === 'failure' && exitCode != null,
174
+ failure_type: result === 'failure' ? (exitCode != null && /ERR_ASSERTION|AssertionError|AssertionFailedError/.test(output) ? 'assertion' : 'infrastructure') : 'none',
175
+ expected_failure: tddPhase === 'red' && result === 'failure' && exitCode != null && /ERR_ASSERTION|AssertionError|AssertionFailedError/.test(output),
176
176
  };
177
177
  strictTestEvent = exitCode != null && Boolean(activeStage.task_id);
178
178
  runId = stableRunId(input || {}, activeStage, details.test_results.command);
@@ -160,8 +160,8 @@ function recordTelemetry(projectRoot, activeStage, record, result, exitCodeKnown
160
160
  skipped: counts.skipped,
161
161
  } : {}),
162
162
  duration_ms: Number.isFinite(toolResponse.duration_ms) ? toolResponse.duration_ms : 0,
163
- failure_type: result === 'failure' ? (exitCode == null ? 'infrastructure' : 'assertion') : 'none',
164
- expected_failure: tddPhase === 'red' && result === 'failure' && exitCode != null,
163
+ failure_type: result === 'failure' ? (exitCode != null && /ERR_ASSERTION|AssertionError|AssertionFailedError/.test(output) ? 'assertion' : 'infrastructure') : 'none',
164
+ expected_failure: tddPhase === 'red' && result === 'failure' && exitCode != null && /ERR_ASSERTION|AssertionError|AssertionFailedError/.test(output),
165
165
  };
166
166
  strictTestEvent = exitCode != null && Boolean(activeStage.task_id);
167
167
  runId = stableRunId(input || {}, activeStage, details.test_results.command);
@@ -154,7 +154,7 @@ if (require.main === module) {
154
154
  }
155
155
 
156
156
  // G4: 批量更新检测(E1 修正:不再提前退出,模式检查继续执行)
157
- var skipPerTaskCheck = false;
157
+ let skipPerTaskCheck = false;
158
158
  if (changes.length >= 2 && hasTddTasks) {
159
159
  // 安全网 2: 全部有 task_update 遥测 → check-task 合法更新,跳过逐个检查
160
160
  const allHaveTaskUpdate = changes.every(change =>
@@ -8,7 +8,7 @@ capability-id: "CAP-<CAPABILITY>" # 必须与对应 spec/design 一致
8
8
  >
9
9
  > **⚠️ 边界声明**:本任务清单仅服务于当前 Capability,严禁跨模块任务。
10
10
  >
11
- > **【质量红线】颗粒度必须达到"AI能在5分钟内实现";且拆解的任务和验证逻辑必须 100% 覆盖 spec 和 design
11
+ > **【质量红线】任务按可验证行为或交付单元拆分,不设固定分钟数;且拆解的任务和验证逻辑必须 100% 覆盖 spec 和 design
12
12
 
13
13
  ---
14
14
 
@@ -105,7 +105,7 @@ capability-id: "CAP-<CAPABILITY>" # 必须与对应 spec/design 一致
105
105
 
106
106
  ## 3. 原子任务清单
107
107
 
108
- <!-- 【质量红线】每个任务必须能在5分钟内由AI实现 -->
108
+ <!-- 【质量红线】每个任务应有可验证结果和明确依赖,避免按字段或命令机械拆分 -->
109
109
 
110
110
  ### 3.0 任务类型说明
111
111
 
@@ -299,7 +299,7 @@ capability-id: "CAP-<CAPABILITY>" # 必须与对应 spec/design 一致
299
299
  ---
300
300
 
301
301
  > **质量红线检查清单**
302
- > - [ ] 每个任务颗粒度符合"5分钟可实现"标准
302
+ > - [ ] 每个任务对应可验证结果,粒度与真实依赖相匹配
303
303
  > - [ ] 任务清单 100% 覆盖 spec.md 定义
304
304
  > - [ ] 任务清单 100% 覆盖 design.md 定义
305
305
  > - [ ] 每个任务都有明确的验收标准