kld-sdd 2.6.21 → 2.7.8
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/README.md +12 -5
- package/USABILITY.md +84 -0
- package/bin/kld-sdd-init.js +3 -9
- package/kld-sdd-guide.html +16 -4
- package/lib/hook-gate-core.js +327 -0
- package/lib/init.js +241 -94
- package/lib/scale-thresholds.json +19 -0
- package/lib/skills-bundle.js +2 -1
- package/package.json +5 -3
- package/skywalk-sdd/context-client.cjs +50 -30
- package/skywalk-sdd/index.cjs +49 -16
- package/skywalk-sdd/kb-sync-identity.cjs +99 -451
- package/skywalk-sdd/kb-upload.cjs +67 -44
- package/skywalk-sdd/lib/git-identity.cjs +270 -0
- package/skywalk-sdd/lib/usage-contract.cjs +13 -81
- package/skywalk-sdd/lib/usage-reporter.cjs +235 -104
- package/skywalk-sdd/lib/user-config.cjs +21 -46
- package/skywalk-sdd/metrics-v3.cjs +2 -2
- package/skywalk-sdd/ontology/active-changes.cjs +2 -1
- package/skywalk-sdd/ontology/archive-package.cjs +1 -1
- package/skywalk-sdd/ontology/artifact-parser.cjs +8 -6
- package/skywalk-sdd/ontology/id.cjs +5 -4
- package/skywalk-sdd/ontology/identity-index.cjs +4 -3
- package/skywalk-sdd/ontology/list-changes.cjs +1 -1
- package/skywalk-sdd/ontology/runtime.cjs +7 -3
- package/skywalk-sdd/ontology/schema.cjs +2 -0
- package/skywalk-sdd/ontology/traceability-validator.cjs +74 -4
- package/skywalk-sdd/ontology/workspace-layout.cjs +25 -5
- package/skywalk-sdd/reporting/change-report-model.cjs +4 -3
- package/templates/git-hooks/commit-msg +71 -27
- package/templates/git-hooks/consistency-check-core.cjs +1087 -0
- package/templates/git-hooks/hooks.config +38 -0
- package/templates/git-hooks/pre-commit +62 -27
- package/templates/git-hooks/pre-commit-consistency-check.cjs +29 -332
- package/templates/git-hooks/pre-commit-sdd-check.cjs +98 -0
- package/templates/git-hooks/pre-push +70 -27
- package/templates/git-hooks/pre-push-consistency-check.cjs +61 -299
- package/templates/hooks/codebuddy/hooks/sdd-tdd-rhythm-gate.cjs +1 -1
- package/templates/openspec/tasks.md +3 -3
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +43 -6
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +1 -1
- package/templates/skills/kld-sdd/opsx-apply/reference.md +1 -1
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +9 -13
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +54 -328
- package/templates/skills/kld-sdd/opsx-check/checklist.md +7 -4
- package/templates/skills/kld-sdd/opsx-check/reference.md +58 -0
- package/templates/skills/kld-sdd/opsx-check/result-template.json +52 -0
- package/templates/skills/kld-sdd/opsx-check/reviewer.md +100 -0
- package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +82 -451
- package/templates/skills/kld-sdd/opsx-consistency-check/{reference.md → references/reference.md} +0 -1
- package/templates/skills/kld-sdd/opsx-consistency-check/scripts/scripts.cjs +517 -0
- package/templates/skills/kld-sdd/opsx-design/SKILL.md +10 -30
- package/templates/skills/kld-sdd/opsx-design/checklist.md +3 -4
- package/templates/skills/kld-sdd/opsx-design/reference.md +1 -1
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +12 -74
- package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +7 -5
- package/templates/skills/kld-sdd/opsx-ontology-query/phase-1-prechange.md +2 -2
- package/templates/skills/kld-sdd/opsx-ontology-query/reference.md +1 -1
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +28 -73
- package/templates/skills/kld-sdd/opsx-propose/checklist.md +8 -8
- package/templates/skills/kld-sdd/opsx-propose/interaction-policy.md +28 -0
- package/templates/skills/kld-sdd/opsx-propose/reference.md +12 -46
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +14 -61
- package/templates/skills/kld-sdd/opsx-spec/checklist.md +3 -3
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +38 -32
- package/templates/skills/kld-sdd/opsx-task/checklist.md +5 -6
- package/templates/skills/kld-sdd/opsx-test/SKILL.md +2 -0
- package/templates/skills/kld-sdd/tdd-rules/rules/tdd-strategy-selection.md +1 -1
- package/lib/device-auth-cli.js +0 -130
- package/lib/device-auth.js +0 -345
- package/lib/init-account-binding.js +0 -108
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# KLD SDD Hook 插件配置文件
|
|
2
|
+
# 每行一个脚本名,空行和 # 开头的行会被忽略
|
|
3
|
+
# 注释某行 = 禁用该插件,取消注释 = 启用该插件
|
|
4
|
+
|
|
5
|
+
[commit-msg]
|
|
6
|
+
# 自动写入 Spec-Change / Spec-Revision Trailer 到 commit message
|
|
7
|
+
commit-msg-sdd-trailer.cjs
|
|
8
|
+
|
|
9
|
+
[pre-commit]
|
|
10
|
+
# 检查 tasks.md 中是否有未完成的归档任务
|
|
11
|
+
pre-commit-sdd-check.cjs
|
|
12
|
+
# 检查 Spec 一致性校验报告(置信度、过期、人工确认等)
|
|
13
|
+
pre-commit-consistency-check.cjs
|
|
14
|
+
|
|
15
|
+
[pre-push]
|
|
16
|
+
# 运行 doctor 完整诊断(配置、Trailer 协议、活动变更等)
|
|
17
|
+
pre-push-sdd-check.cjs
|
|
18
|
+
# 检查待推送范围的一致性报告(存在性、过期;置信度由 pre-commit 负责)
|
|
19
|
+
pre-push-consistency-check.cjs
|
|
20
|
+
|
|
21
|
+
[consistency]
|
|
22
|
+
# 一致性校验的业务代码扩展名
|
|
23
|
+
# 语义:整行替换式配置,以本清单为准
|
|
24
|
+
# Go / Kotlin 等项目按需修改:
|
|
25
|
+
# 注意:新增语言的语言特定构建文件(如 Rust 的 build.rs)不在内置排除规则内,需自行确认
|
|
26
|
+
# 本配置同时被 git hook(isBusinessCode)与 opsx-consistency-check skill 读取,单一来源
|
|
27
|
+
# ⚠️ 清空本行 = 不拦截任何业务代码文件(一致性门禁对提交/推送失效,仅剩报告存在性/过期检查)
|
|
28
|
+
business_code_exts = .java,.ts,.tsx,.js,.jsx,.vue,.py
|
|
29
|
+
|
|
30
|
+
# 一致性校验模式(项目级默认值,环境变量优先覆盖;三个互斥,只能启用一个)
|
|
31
|
+
# 环境变量:SDD_SKIP_CONSISTENCY_CHECK=1 / SDD_STRICT_CONSISTENCY=1 / SDD_WARN_ONLY=1
|
|
32
|
+
# - skip = 1:跳过一致性校验(紧急绕过,如 hotfix 仓不需要 spec 校验)
|
|
33
|
+
# - strict = 1:严格模式,medium 置信度也阻止提交(仅 pre-commit 生效,pre-push 忽略)
|
|
34
|
+
# - warn_only = 1:仅告警不拦截(CI 调试期,问题全列但不阻断提交/推送)
|
|
35
|
+
# 三项均不设或均设为 0 = 默认模式(low 阻止,medium 放行,high 放行)
|
|
36
|
+
skip = 0
|
|
37
|
+
strict = 0
|
|
38
|
+
warn_only = 0
|
|
@@ -1,28 +1,27 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
# KLD SDD Pre-Commit Hook
|
|
3
|
-
#
|
|
4
|
-
#
|
|
3
|
+
# 在下方 SCRIPTS 列表中声明要加载的 .cjs 脚本
|
|
4
|
+
# 删除某行 = 禁用该插件
|
|
5
5
|
# marker: KLD SDD quality gate
|
|
6
6
|
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
15
|
-
spec_path
|
|
16
|
-
if [ -n "$spec_path" ] && [ -d "$spec_path/skywalk-sdd/git-hooks" ]; then
|
|
13
|
+
spec_path=$(git -C "$git_root" config --local sdd.specPath 2>/dev/null)
|
|
14
|
+
if [ -n "$spec_path" ]; then
|
|
15
|
+
case "$spec_path" in /*) ;; *) spec_path="$git_root/$spec_path" ;; esac
|
|
17
16
|
hooks_dir="$spec_path/skywalk-sdd/git-hooks"
|
|
17
|
+
elif [ -f "$git_root/.sdd-spec-root" ]; then
|
|
18
|
+
spec_rel=$(head -1 "$git_root/.sdd-spec-root" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
19
|
+
case "$spec_rel" in /*) hooks_dir="$spec_rel/skywalk-sdd/git-hooks" ;; *) hooks_dir="$git_root/$spec_rel/skywalk-sdd/git-hooks" ;; esac
|
|
18
20
|
fi
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
21
|
+
if [ -n "$hooks_dir" ] && [ ! -d "$hooks_dir" ]; then
|
|
22
|
+
echo "[SDD] 已配置的 Spec 路径失效:$hooks_dir"
|
|
23
|
+
echo "[SDD] 请恢复 Spec 仓,或修正 sdd.specPath / .sdd-spec-root 后重试"
|
|
24
|
+
exit 1
|
|
26
25
|
fi
|
|
27
26
|
|
|
28
27
|
# 方法3: 从 git 根向上搜索 skywalk-sdd/git-hooks(spec 仓本身或父级)
|
|
@@ -38,20 +37,56 @@ if [ -z "$hooks_dir" ]; then
|
|
|
38
37
|
fi
|
|
39
38
|
|
|
40
39
|
if [ -z "$hooks_dir" ]; then
|
|
41
|
-
echo "[SDD] 未找到 spec 仓的 skywalk-sdd/git-hooks 目录,跳过 pre-commit
|
|
40
|
+
echo "[SDD] 未找到 spec 仓的 skywalk-sdd/git-hooks 目录,跳过 pre-commit"
|
|
42
41
|
exit 0
|
|
43
42
|
fi
|
|
44
43
|
|
|
45
44
|
echo "[SDD] 执行提交前检查..."
|
|
46
45
|
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
# 从配置文件读取插件列表
|
|
47
|
+
config_file="$hooks_dir/hooks.config"
|
|
48
|
+
if [ ! -f "$config_file" ]; then
|
|
49
|
+
echo "[SDD] ️ 配置文件不存在:hooks.config,使用默认插件"
|
|
50
|
+
SCRIPTS="pre-commit-sdd-check.cjs pre-commit-consistency-check.cjs"
|
|
51
|
+
else
|
|
52
|
+
# 读取 [pre-commit] 段落的配置
|
|
53
|
+
in_section=0
|
|
54
|
+
SCRIPTS=""
|
|
55
|
+
CR=$(printf '\r')
|
|
56
|
+
while IFS= read -r line; do
|
|
57
|
+
line=${line%"$CR"}
|
|
58
|
+
case "$line" in
|
|
59
|
+
"[pre-commit]") in_section=1 ;;
|
|
60
|
+
"["*) in_section=0 ;;
|
|
61
|
+
"#"*|"") ;;
|
|
62
|
+
*)
|
|
63
|
+
if [ "$in_section" -eq 1 ]; then
|
|
64
|
+
SCRIPTS="$SCRIPTS $line"
|
|
65
|
+
fi
|
|
66
|
+
;;
|
|
67
|
+
esac
|
|
68
|
+
done < "$config_file"
|
|
50
69
|
fi
|
|
51
70
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
71
|
+
executed=0
|
|
72
|
+
failed=0
|
|
73
|
+
for script_name in $SCRIPTS; do
|
|
74
|
+
script="$hooks_dir/$script_name"
|
|
75
|
+
if [ ! -f "$script" ]; then
|
|
76
|
+
echo "[SDD] ❌ 已配置的插件不存在:$script_name"
|
|
77
|
+
failed=$((failed + 1))
|
|
78
|
+
continue
|
|
79
|
+
fi
|
|
80
|
+
executed=$((executed + 1))
|
|
81
|
+
echo "[SDD] 执行 pre-commit 插件:$script_name"
|
|
82
|
+
node "$script" --project="$git_root" "$@" || failed=$((failed + 1))
|
|
83
|
+
done
|
|
56
84
|
|
|
57
|
-
|
|
85
|
+
if [ "$failed" -gt 0 ]; then
|
|
86
|
+
echo "[SDD] ❌ 共执行 $executed 个 pre-commit 插件,$failed 个失败"
|
|
87
|
+
exit 1
|
|
88
|
+
elif [ "$executed" -eq 0 ]; then
|
|
89
|
+
echo "[SDD] 配置已禁用 pre-commit 插件"
|
|
90
|
+
else
|
|
91
|
+
echo "[SDD] ✅ 共执行 $executed 个 pre-commit 插件,全部通过"
|
|
92
|
+
fi
|
|
@@ -2,347 +2,44 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* pre-commit-consistency-check.cjs
|
|
4
4
|
*
|
|
5
|
-
* Pre-commit
|
|
6
|
-
* -
|
|
5
|
+
* Pre-commit 门禁:检查本次提交 diff 涉及的 Spec 一致性校验报告。
|
|
6
|
+
* - diff 驱动:仅检查 diff 文件与报告 coveredFiles 有交集的 change,无关 change 跳过
|
|
7
|
+
* - 报告存在性:活跃 change 无报告 → 阻止提交
|
|
8
|
+
* - 报告过期:代码在报告生成后被修改 → 阻止提交
|
|
9
|
+
* - 置信度门禁:confidence=low → 阻止提交;strict 模式下 medium 也阻止
|
|
7
10
|
*
|
|
8
11
|
* 通过环境变量控制:
|
|
9
12
|
* - SDD_SKIP_CONSISTENCY_CHECK=1 → 跳过校验
|
|
10
13
|
* - SDD_STRICT_CONSISTENCY=1 → 严格模式(medium 也阻止)
|
|
11
14
|
* - SDD_WARN_ONLY=1 → 仅告警不拦截
|
|
12
15
|
* - 注意:三个环境变量互斥,只能启用一个
|
|
16
|
+
*
|
|
17
|
+
* 共享逻辑见同目录 consistency-check-core.cjs。
|
|
13
18
|
*/
|
|
14
19
|
'use strict';
|
|
15
20
|
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const { execFileSync } = require('child_process');
|
|
19
|
-
|
|
20
|
-
const readArg = (name) => {
|
|
21
|
-
const prefix = `--${name}=`;
|
|
22
|
-
const found = process.argv.find(a => a.startsWith(prefix));
|
|
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, '..', '..');
|
|
31
|
-
}
|
|
32
|
-
|
|
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
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// --- 报告查找与解析 ---
|
|
44
|
-
|
|
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);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// 如果两种模式的报告都存在,返回错误标记
|
|
57
|
-
if (found.length > 1) {
|
|
58
|
-
return { error: 'duplicate', paths: found };
|
|
59
|
-
}
|
|
60
|
-
|
|
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
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// --- 变更文件采集(基于配置文件精准定位) ---
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* 简易 YAML 解析器:仅提取 code_repos 列表
|
|
82
|
-
*/
|
|
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;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
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 {
|
|
103
|
-
return [];
|
|
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
|
-
}
|
|
123
|
-
|
|
124
|
-
// 单仓模式 / 兜底:直接对 projectRoot 做 diff
|
|
125
|
-
return gitDiff(projectRoot);
|
|
126
|
-
}
|
|
21
|
+
const core = require('./consistency-check-core.cjs');
|
|
22
|
+
const { gitDiffNameStatus } = core;
|
|
127
23
|
|
|
128
24
|
/**
|
|
129
|
-
*
|
|
25
|
+
* 单仓 diff 采集:工作区相对 HEAD 的变更(含 staged 与未 staged)。
|
|
26
|
+
* files=修改/新增(排除删除);deleted=删除文件。
|
|
27
|
+
* 空仓(无任何 commit)时 HEAD 不存在:降级用 staged 文件列表。
|
|
28
|
+
* @returns {{files: string[], deleted: string[], ok: boolean}} ok=false 表示无法确定变更范围
|
|
130
29
|
*/
|
|
131
|
-
function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const reportTime = getReportTime(jsonPath);
|
|
148
|
-
|
|
149
|
-
// 排除报告文件本身
|
|
150
|
-
const reportFiles = [
|
|
151
|
-
'consistency-report.md',
|
|
152
|
-
'consistency-report-self-review.md',
|
|
153
|
-
'consistency-report-result.json',
|
|
154
|
-
'consistency-report-self-review-result.json',
|
|
155
|
-
];
|
|
156
|
-
|
|
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}`));
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
lines.push(
|
|
219
|
-
'',
|
|
220
|
-
' 💡 建议:新开对话窗口执行校验,以获得独立、客观的审核结果',
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
|
|
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
|
-
}
|
|
235
|
-
|
|
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
|
-
);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
lines.push('==========================================', '');
|
|
246
|
-
|
|
247
|
-
return lines.join('\n');
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// --- 主流程 ---
|
|
251
|
-
|
|
252
|
-
function main() {
|
|
253
|
-
const projectRoot = path.resolve(readArg('project') || process.env.SDD_PROJECT || process.cwd());
|
|
254
|
-
|
|
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);
|
|
269
|
-
}
|
|
270
|
-
|
|
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);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const modifiedFiles = getModifiedFiles(projectRoot);
|
|
282
|
-
const missing = [];
|
|
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
|
-
}
|
|
294
|
-
|
|
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);
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const report = parseReport(rp);
|
|
304
|
-
if (report.parseError) {
|
|
305
|
-
console.warn(`[SDD commit-consistency-check] ⚠️ ${cn} 报告解析失败`);
|
|
306
|
-
parseError.push(cn);
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (isReportStale(specRoot, cn, modifiedFiles, projectRoot)) {
|
|
311
|
-
console.log(`[SDD commit-consistency-check] ⚠️ ${cn}: 报告已过期`);
|
|
312
|
-
stale.push(cn);
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// 自审模式需要人工确认
|
|
317
|
-
if (report.reviewMode === 'self-review' && !report.humanConfirmed) {
|
|
318
|
-
console.log(`[SDD commit-consistency-check] ⚠️ ${cn}: 自审模式需人工确认`);
|
|
319
|
-
needsConfirmation.push(cn);
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
|
|
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;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
console.log(`[SDD commit-consistency-check] ✅ ${cn}: 置信度 ${c}`);
|
|
331
|
-
}
|
|
332
|
-
|
|
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);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
process.exit(1);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
console.log('\n[SDD commit-consistency-check] ✅ 所有变更均已通过一致性校验,放行提交');
|
|
345
|
-
process.exit(0);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
main();
|
|
30
|
+
function gitDiffHead(repoRoot) {
|
|
31
|
+
const bases = [['diff', 'HEAD'], ['diff', '--cached']];
|
|
32
|
+
for (const base of bases) {
|
|
33
|
+
const r = gitDiffNameStatus(repoRoot, base);
|
|
34
|
+
if (r.ok) return r;
|
|
35
|
+
}
|
|
36
|
+
return { files: [], deleted: [], ok: false };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
core.runConsistencyCheck({
|
|
40
|
+
label: '[SDD commit-consistency-check]',
|
|
41
|
+
actionNoun: '提交',
|
|
42
|
+
diffFn: gitDiffHead,
|
|
43
|
+
checkConfidence: true, // 置信度校验由 pre-commit 完成
|
|
44
|
+
includeUntracked: true, // 新文件将随本次提交入库,需检查
|
|
45
|
+
});
|
|
@@ -30,6 +30,95 @@ function normalizePath(filePath) {
|
|
|
30
30
|
return filePath.replace(/\\/g, '/');
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function isCodeFile(filePath) {
|
|
34
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
35
|
+
return ['.js', '.ts', '.java', '.py', '.go', '.rs', '.c', '.cpp', '.h', '.cs', '.rb', '.php', '.swift', '.kt'].includes(ext);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isSddDocument(filePath) {
|
|
39
|
+
const normalized = normalizePath(filePath);
|
|
40
|
+
return normalized.includes('openspec/changes/') &&
|
|
41
|
+
(normalized.endsWith('proposal.md') ||
|
|
42
|
+
normalized.endsWith('spec.md') ||
|
|
43
|
+
normalized.endsWith('design.md') ||
|
|
44
|
+
normalized.endsWith('tasks.md'));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getCurrentBranch(projectRoot) {
|
|
48
|
+
try {
|
|
49
|
+
return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
50
|
+
cwd: projectRoot,
|
|
51
|
+
encoding: 'utf8',
|
|
52
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
53
|
+
}).trim();
|
|
54
|
+
} catch {
|
|
55
|
+
return 'unknown';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getStagedLineCount(projectRoot) {
|
|
60
|
+
try {
|
|
61
|
+
const output = execFileSync('git', ['diff', '--cached', '--numstat'], {
|
|
62
|
+
cwd: projectRoot,
|
|
63
|
+
encoding: 'utf8',
|
|
64
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
65
|
+
});
|
|
66
|
+
let total = 0;
|
|
67
|
+
for (const line of output.split(/\r?\n/).filter(Boolean)) {
|
|
68
|
+
const parts = line.split(/\t/);
|
|
69
|
+
if (parts.length >= 2) {
|
|
70
|
+
total += (parseInt(parts[0], 10) || 0) + (parseInt(parts[1], 10) || 0);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return total;
|
|
74
|
+
} catch {
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function recordBypassTelemetry(projectRoot, branch, files, lines) {
|
|
80
|
+
try {
|
|
81
|
+
const logCli = path.join(projectRoot, 'skywalk-sdd', 'log.cjs');
|
|
82
|
+
if (!fs.existsSync(logCli)) {
|
|
83
|
+
console.warn('SDD pre-commit: log.cjs not found, bypass telemetry skipped.');
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const safeBranch = branch.replace(/[^a-z0-9\-]/gi, '-').toLowerCase();
|
|
88
|
+
const details = JSON.stringify({
|
|
89
|
+
bypass_detected: {
|
|
90
|
+
branch: branch,
|
|
91
|
+
files_changed: files.length,
|
|
92
|
+
lines_changed: lines,
|
|
93
|
+
staged_files: files.slice(0, 20),
|
|
94
|
+
reason: 'code_commit_without_sdd_documents',
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
execFileSync('node', [
|
|
99
|
+
logCli,
|
|
100
|
+
'record',
|
|
101
|
+
'--strict',
|
|
102
|
+
'--type=bypass_detected',
|
|
103
|
+
'--command=pre-commit',
|
|
104
|
+
'--project=.',
|
|
105
|
+
`--change=${safeBranch}`,
|
|
106
|
+
'--agent=git-hook',
|
|
107
|
+
'--source=pre-commit-hook',
|
|
108
|
+
`--details-json=${details}`,
|
|
109
|
+
], {
|
|
110
|
+
cwd: projectRoot,
|
|
111
|
+
encoding: 'utf8',
|
|
112
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
113
|
+
});
|
|
114
|
+
console.log(`SDD pre-commit: bypass-detected event recorded for ${safeBranch} (${files.length} files, ${lines} lines).`);
|
|
115
|
+
return true;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.warn('SDD pre-commit: bypass telemetry recording failed:', err.message);
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
33
122
|
function getChangeNameFromPath(filePath) {
|
|
34
123
|
const normalized = normalizePath(filePath);
|
|
35
124
|
const match = normalized.match(/^openspec\/changes\/([^/]+)\//);
|
|
@@ -119,6 +208,15 @@ function main() {
|
|
|
119
208
|
const changes = discoverChanges(projectRoot, explicitChange, archiveMode);
|
|
120
209
|
|
|
121
210
|
if (changes.length === 0) {
|
|
211
|
+
// Bypass detection: if code files are staged without SDD documents, record telemetry
|
|
212
|
+
const stagedFiles = getStagedFiles(projectRoot);
|
|
213
|
+
const hasCodeFiles = stagedFiles.some(isCodeFile);
|
|
214
|
+
const hasSddDocs = stagedFiles.some(isSddDocument);
|
|
215
|
+
if (hasCodeFiles && !hasSddDocs) {
|
|
216
|
+
const branch = getCurrentBranch(projectRoot);
|
|
217
|
+
const lines = getStagedLineCount(projectRoot);
|
|
218
|
+
recordBypassTelemetry(projectRoot, branch, stagedFiles, lines);
|
|
219
|
+
}
|
|
122
220
|
console.log('SDD pre-commit: no archive intent detected; tasks gate skipped.');
|
|
123
221
|
return;
|
|
124
222
|
}
|