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.
- package/bin/kld-sdd-init.js +4 -0
- package/kld-sdd-guide.html +22 -0
- package/lib/init.js +160 -73
- package/package.json +3 -3
- package/skywalk-sdd/index.cjs +155 -25
- package/skywalk-sdd/lib/git-identity.cjs +270 -0
- package/skywalk-sdd/lib/shared.cjs +150 -2
- package/skywalk-sdd/lib/usage-contract.cjs +207 -0
- package/skywalk-sdd/lib/usage-reporter.cjs +460 -0
- package/skywalk-sdd/lib/user-config.cjs +132 -0
- package/skywalk-sdd/ontology/artifact-parser.cjs +1 -2
- package/templates/git-hooks/commit-msg +58 -15
- package/templates/git-hooks/hooks.config +19 -0
- package/templates/git-hooks/pre-commit +45 -11
- package/templates/git-hooks/pre-commit-consistency-check.cjs +271 -116
- package/templates/git-hooks/pre-push +53 -11
- package/templates/git-hooks/pre-push-consistency-check.cjs +357 -118
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +1 -1
- package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +187 -257
- package/templates/skills/kld-sdd/opsx-consistency-check/reference.md +129 -0
- package/templates/skills/kld-sdd/opsx-kb-config/SKILL.md +27 -6
- package/templates/skills/kld-sdd/opsx-kb-config/reference.md +12 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// kld-T02 — ~/.kld-sdd/config.json 用户级配置管理
|
|
2
|
+
// 职责:读取、规范化、原子写、0600/Windows ACL。
|
|
3
|
+
// schema v2 = {version, server};使用统计按 git 身份上报,不再保存任何 token。
|
|
4
|
+
// 旧版含 token/userName/boundAt 的 config 读取时 sanitize 为 {version, server},回写即淘汰。
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const fs = require('node:fs');
|
|
8
|
+
const os = require('node:os');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
|
|
11
|
+
const USER_CONFIG_VERSION = 2;
|
|
12
|
+
const CONFIG_DIR_NAME = '.kld-sdd';
|
|
13
|
+
const CONFIG_FILE_NAME = 'config.json';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 返回用户配置目录路径。
|
|
17
|
+
* @param {string} [homeDir] — 注入的 home 目录(测试用);缺省取 env.KLD_SDD_HOME 或 os.homedir()
|
|
18
|
+
*/
|
|
19
|
+
function getConfigDir(homeDir) {
|
|
20
|
+
const envHome = typeof process !== 'undefined' && process && process.env
|
|
21
|
+
? process.env.KLD_SDD_HOME
|
|
22
|
+
: undefined;
|
|
23
|
+
const home = (typeof homeDir === 'string' && homeDir)
|
|
24
|
+
|| (typeof envHome === 'string' && envHome)
|
|
25
|
+
|| os.homedir();
|
|
26
|
+
return path.join(home, CONFIG_DIR_NAME);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 返回用户配置文件完整路径。
|
|
31
|
+
*/
|
|
32
|
+
function getConfigPath(homeDir) {
|
|
33
|
+
return path.join(getConfigDir(homeDir), CONFIG_FILE_NAME);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 将任意已解析对象规范化为 v2 config:只保留 {version, server}。
|
|
38
|
+
* @param {object} parsed
|
|
39
|
+
* @returns {{version: number, server?: string}}
|
|
40
|
+
*/
|
|
41
|
+
function sanitizeConfig(parsed) {
|
|
42
|
+
const config = { version: USER_CONFIG_VERSION };
|
|
43
|
+
if (typeof parsed.server === 'string') {
|
|
44
|
+
config.server = parsed.server;
|
|
45
|
+
}
|
|
46
|
+
return config;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 读取用户配置。
|
|
51
|
+
* - 文件不存在 / 损坏 JSON / 非对象 → 返回 null(不抛)
|
|
52
|
+
* - 旧版多余字段(token/userName/boundAt 等)读取即忽略,不回传
|
|
53
|
+
* - 返回新引用;调用方修改不影响后续读取
|
|
54
|
+
*
|
|
55
|
+
* @param {string} [homeDir]
|
|
56
|
+
* @returns {object|null}
|
|
57
|
+
*/
|
|
58
|
+
function readUserConfig(homeDir) {
|
|
59
|
+
const file = getConfigPath(homeDir);
|
|
60
|
+
let raw;
|
|
61
|
+
try {
|
|
62
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(raw);
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
|
|
73
|
+
return sanitizeConfig(parsed);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* POSIX 平台设置 0600/0700;Windows 平台尝试通过 icacls 收紧 ACL(best-effort)。
|
|
78
|
+
* @param {string} target — 文件或目录路径
|
|
79
|
+
* @param {'file'|'dir'} kind
|
|
80
|
+
*/
|
|
81
|
+
function hardenPermissions(target, kind) {
|
|
82
|
+
if (process.platform === 'win32') {
|
|
83
|
+
// Windows:依赖 Node 默认 ACL 通常已限当前用户,无需显式 icacls;
|
|
84
|
+
// 实际收紧在更高层以同步子进程完成,避免引入异步依赖。
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
fs.chmodSync(target, kind === 'dir' ? 0o700 : 0o600);
|
|
89
|
+
} catch {
|
|
90
|
+
// best-effort:无法 chmod 时静默(例如某些 FS 不支持)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 原子写入用户配置:先 temp 后 rename,目录与文件均设置权限。
|
|
96
|
+
* 只持久化 {version, server},多余字段写入即淘汰。
|
|
97
|
+
* @param {object} config — 必须含 version 字段
|
|
98
|
+
* @param {string} [homeDir]
|
|
99
|
+
* @throws {Error} config 非法 / 缺 version
|
|
100
|
+
*/
|
|
101
|
+
function writeUserConfig(config, homeDir) {
|
|
102
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
103
|
+
throw new Error('user config must be an object');
|
|
104
|
+
}
|
|
105
|
+
if (typeof config.version !== 'number') {
|
|
106
|
+
throw new Error('user config requires numeric version');
|
|
107
|
+
}
|
|
108
|
+
const dir = getConfigDir(homeDir);
|
|
109
|
+
const file = getConfigPath(homeDir);
|
|
110
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
111
|
+
hardenPermissions(dir, 'dir');
|
|
112
|
+
|
|
113
|
+
const tmp = path.join(dir, `.${CONFIG_FILE_NAME}.${process.pid}.${Date.now()}.tmp`);
|
|
114
|
+
const payload = JSON.stringify(sanitizeConfig(config), null, 2);
|
|
115
|
+
fs.writeFileSync(tmp, payload, { encoding: 'utf8', mode: 0o600 });
|
|
116
|
+
hardenPermissions(tmp, 'file');
|
|
117
|
+
try {
|
|
118
|
+
fs.renameSync(tmp, file);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
try { fs.unlinkSync(tmp); } catch { /* ignore */ }
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
hardenPermissions(file, 'file');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = {
|
|
127
|
+
USER_CONFIG_VERSION,
|
|
128
|
+
getConfigDir,
|
|
129
|
+
getConfigPath,
|
|
130
|
+
readUserConfig,
|
|
131
|
+
writeUserConfig,
|
|
132
|
+
};
|
|
@@ -200,7 +200,7 @@ function identityFromValues(values = {}) {
|
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
function flushRequirementRef(requirementRefs, currentRef) {
|
|
203
|
-
if (!currentRef) return
|
|
203
|
+
if (!currentRef) return;
|
|
204
204
|
const { normalize, validate } = require('./external-key.cjs');
|
|
205
205
|
const system = currentRef.system;
|
|
206
206
|
const objectType = currentRef['object-type'] || currentRef.object_type;
|
|
@@ -224,7 +224,6 @@ function flushRequirementRef(requirementRefs, currentRef) {
|
|
|
224
224
|
}
|
|
225
225
|
requirementRefs.push(entry);
|
|
226
226
|
}
|
|
227
|
-
return null;
|
|
228
227
|
}
|
|
229
228
|
|
|
230
229
|
function parseStructuredFrontmatter(lines) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
# KLD SDD Commit-MSG Hook
|
|
3
|
-
#
|
|
3
|
+
# 在下方 SCRIPTS 列表中声明要加载的 .cjs 脚本
|
|
4
|
+
# 删除某行 = 禁用该插件
|
|
4
5
|
# marker: KLD SDD quality gate
|
|
5
6
|
|
|
6
7
|
# 从 hook 所在目录推算 git 根目录
|
|
@@ -8,39 +9,81 @@ hook_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
|
8
9
|
git_dir="$(dirname "$hook_dir")"
|
|
9
10
|
git_root="$(dirname "$git_dir")"
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
hooks_dir=""
|
|
12
13
|
|
|
13
14
|
# 方法1: git config sdd.specPath(多仓布局:spec 是独立 clone)
|
|
14
15
|
spec_path=$(git config --local sdd.specPath 2>/dev/null)
|
|
15
|
-
if [ -n "$spec_path" ] && [ -
|
|
16
|
-
|
|
16
|
+
if [ -n "$spec_path" ] && [ -d "$spec_path/skywalk-sdd/git-hooks" ]; then
|
|
17
|
+
hooks_dir="$spec_path/skywalk-sdd/git-hooks"
|
|
17
18
|
fi
|
|
18
19
|
|
|
19
20
|
# 方法2: .sdd-spec-root(单仓 mono 布局:skywalk-sdd 在 spec 包裹包子目录内)
|
|
20
|
-
if [ -z "$
|
|
21
|
+
if [ -z "$hooks_dir" ] && [ -f "$git_root/.sdd-spec-root" ]; then
|
|
21
22
|
spec_rel=$(cat "$git_root/.sdd-spec-root" | head -1 | tr -d '[:space:]')
|
|
22
|
-
if [ -n "$spec_rel" ] && [ -
|
|
23
|
-
|
|
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"
|
|
24
25
|
fi
|
|
25
26
|
fi
|
|
26
27
|
|
|
27
28
|
# 方法3: 从 git 根向上搜索 skywalk-sdd/git-hooks(spec 仓本身或父级)
|
|
28
|
-
if [ -z "$
|
|
29
|
+
if [ -z "$hooks_dir" ]; then
|
|
29
30
|
search_dir="$git_root"
|
|
30
31
|
while [ "$search_dir" != "/" ] && [ "$search_dir" != "" ]; do
|
|
31
|
-
if [ -
|
|
32
|
-
|
|
32
|
+
if [ -d "$search_dir/skywalk-sdd/git-hooks" ]; then
|
|
33
|
+
hooks_dir="$search_dir/skywalk-sdd/git-hooks"
|
|
33
34
|
break
|
|
34
35
|
fi
|
|
35
36
|
search_dir="$(dirname "$search_dir")"
|
|
36
37
|
done
|
|
37
38
|
fi
|
|
38
39
|
|
|
39
|
-
if [ -z "$
|
|
40
|
-
echo "[SDD] 未找到
|
|
40
|
+
if [ -z "$hooks_dir" ]; then
|
|
41
|
+
echo "[SDD] 未找到 spec 仓的 skywalk-sdd/git-hooks 目录,跳过 commit-msg"
|
|
41
42
|
exit 0
|
|
42
43
|
fi
|
|
43
44
|
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
# 从配置文件读取插件列表
|
|
46
|
+
config_file="$hooks_dir/hooks.config"
|
|
47
|
+
if [ ! -f "$config_file" ]; then
|
|
48
|
+
echo "[SDD] ⚠️ 配置文件不存在:hooks.config,使用默认插件"
|
|
49
|
+
SCRIPTS="commit-msg-sdd-trailer.cjs"
|
|
50
|
+
else
|
|
51
|
+
# 读取 [commit-msg] 段落的配置
|
|
52
|
+
in_section=0
|
|
53
|
+
SCRIPTS=""
|
|
54
|
+
while IFS= read -r line; do
|
|
55
|
+
line=$(printf '%s' "$line" | tr -d '\r')
|
|
56
|
+
case "$line" in
|
|
57
|
+
"[commit-msg]") in_section=1 ;;
|
|
58
|
+
"["*) in_section=0 ;;
|
|
59
|
+
"#"*|"") ;;
|
|
60
|
+
*)
|
|
61
|
+
if [ "$in_section" -eq 1 ]; then
|
|
62
|
+
SCRIPTS="$SCRIPTS $line"
|
|
63
|
+
fi
|
|
64
|
+
;;
|
|
65
|
+
esac
|
|
66
|
+
done < "$config_file"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
executed=0
|
|
70
|
+
failed=0
|
|
71
|
+
for script_name in $SCRIPTS; do
|
|
72
|
+
script="$hooks_dir/$script_name"
|
|
73
|
+
if [ ! -f "$script" ]; then
|
|
74
|
+
echo "[SDD] ⚠️ 插件不存在:$script_name"
|
|
75
|
+
continue
|
|
76
|
+
fi
|
|
77
|
+
executed=$((executed + 1))
|
|
78
|
+
echo "[SDD] 执行 commit-msg 插件:$script_name"
|
|
79
|
+
node "$script" "$1" --project="$git_root" || failed=$((failed + 1))
|
|
80
|
+
done
|
|
81
|
+
|
|
82
|
+
if [ "$executed" -eq 0 ]; then
|
|
83
|
+
echo "[SDD] 无 commit-msg 插件,跳过"
|
|
84
|
+
elif [ "$failed" -gt 0 ]; then
|
|
85
|
+
echo "[SDD] ❌ 共执行 $executed 个 commit-msg 插件,$failed 个失败"
|
|
86
|
+
exit 1
|
|
87
|
+
else
|
|
88
|
+
echo "[SDD] ✅ 共执行 $executed 个 commit-msg 插件,全部通过"
|
|
89
|
+
fi
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
# 轻量检查一致性报告是否存在、是否过期
|
|
19
|
+
pre-push-consistency-check.cjs
|
|
@@ -1,7 +1,7 @@
|
|
|
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
7
|
# 从 hook 所在目录推算 git 根目录
|
|
@@ -38,20 +38,54 @@ if [ -z "$hooks_dir" ]; then
|
|
|
38
38
|
fi
|
|
39
39
|
|
|
40
40
|
if [ -z "$hooks_dir" ]; then
|
|
41
|
-
echo "[SDD] 未找到 spec 仓的 skywalk-sdd/git-hooks 目录,跳过 pre-commit
|
|
41
|
+
echo "[SDD] 未找到 spec 仓的 skywalk-sdd/git-hooks 目录,跳过 pre-commit"
|
|
42
42
|
exit 0
|
|
43
43
|
fi
|
|
44
44
|
|
|
45
45
|
echo "[SDD] 执行提交前检查..."
|
|
46
46
|
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
# 从配置文件读取插件列表
|
|
48
|
+
config_file="$hooks_dir/hooks.config"
|
|
49
|
+
if [ ! -f "$config_file" ]; then
|
|
50
|
+
echo "[SDD] ️ 配置文件不存在:hooks.config,使用默认插件"
|
|
51
|
+
SCRIPTS="pre-commit-sdd-check.cjs pre-commit-consistency-check.cjs"
|
|
52
|
+
else
|
|
53
|
+
# 读取 [pre-commit] 段落的配置
|
|
54
|
+
in_section=0
|
|
55
|
+
SCRIPTS=""
|
|
56
|
+
while IFS= read -r line; do
|
|
57
|
+
line=$(printf '%s' "$line" | tr -d '\r')
|
|
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
|
+
continue
|
|
78
|
+
fi
|
|
79
|
+
executed=$((executed + 1))
|
|
80
|
+
echo "[SDD] 执行 pre-commit 插件:$script_name"
|
|
81
|
+
node "$script" --project="$git_root" "$@" || failed=$((failed + 1))
|
|
82
|
+
done
|
|
56
83
|
|
|
57
|
-
|
|
84
|
+
if [ "$executed" -eq 0 ]; then
|
|
85
|
+
echo "[SDD] 无 pre-commit 插件,跳过"
|
|
86
|
+
elif [ "$failed" -gt 0 ]; then
|
|
87
|
+
echo "[SDD] ❌ 共执行 $executed 个 pre-commit 插件,$failed 个失败"
|
|
88
|
+
exit 1
|
|
89
|
+
else
|
|
90
|
+
echo "[SDD] ✅ 共执行 $executed 个 pre-commit 插件,全部通过"
|
|
91
|
+
fi
|