kld-sdd 2.7.8-2 → 2.7.8-4
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 +2 -0
- package/USABILITY.md +52 -0
- package/bin/kld-sdd-init.js +700 -34
- package/lib/command-bridge.js +26 -17
- package/lib/deploy-codebuddy-hooks.js +19 -26
- package/lib/deploy-strategy.js +293 -0
- package/lib/hook-gate-core.js +7 -8
- package/lib/hook-install.js +75 -0
- package/lib/init.js +450 -711
- package/lib/managed-hook-hashes.json +24 -0
- package/lib/settings-merge.js +102 -25
- package/lib/skills-bundle.js +2 -4
- package/lib/spec-choice.js +85 -0
- package/lib/tool-profiles.js +2 -2
- package/lib/uninstall.js +764 -0
- package/package.json +3 -3
- package/skywalk-sdd/index.cjs +1 -1
- package/skywalk-sdd/kb-upload.cjs +64 -49
- package/skywalk-sdd/lib/shared.cjs +62 -41
- package/skywalk-sdd/lib/test-execution.cjs +20 -0
- package/skywalk-sdd/lib/usage-reporter.cjs +17 -8
- package/skywalk-sdd/ontology/resolve-spec-root.cjs +49 -0
- package/skywalk-sdd/ontology/sdd-config.cjs +33 -5
- package/skywalk-sdd/ontology/spec-detect.cjs +421 -0
- package/skywalk-sdd/ontology/workspace-layout.cjs +46 -16
- package/templates/git-hooks/commit-msg +17 -99
- package/templates/git-hooks/commit-msg-runner.cjs +44 -0
- package/templates/git-hooks/commit-msg-sdd-trailer.cjs +60 -69
- package/templates/git-hooks/consistency-check-core.cjs +61 -240
- package/templates/git-hooks/delivery-check.cjs +231 -0
- package/templates/git-hooks/delivery-snapshot.cjs +181 -0
- package/templates/git-hooks/hooks.config +8 -6
- package/templates/git-hooks/pre-commit-consistency-check.cjs +13 -31
- package/templates/git-hooks/pre-push-consistency-check.cjs +36 -83
- package/templates/hooks/codebuddy/hooks/hook-gate-core.cjs +7 -8
- package/templates/openspec/proposal.md +1 -1
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +24 -22
- package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +34 -112
- package/templates/skills/kld-sdd/opsx-consistency-check/scripts/scripts.cjs +15 -14
- package/templates/skills/kld-sdd/opsx-kb-config/SKILL.md +157 -11
- package/index.js +0 -13
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* spec 目录识别 —— **唯一判据入口**。
|
|
5
|
+
*
|
|
6
|
+
* 本模块只回答两个问题,不负责定位(定位在 resolve-spec-root.cjs):
|
|
7
|
+
*
|
|
8
|
+
* 1. 「这个目录是不是 spec 包?」 → classifySpecDir / isSpecCandidate
|
|
9
|
+
* 2. 「候选有哪些、优先级如何?」 → detectSpecCandidates
|
|
10
|
+
*
|
|
11
|
+
* 为什么需要分级而不是单一布尔值:两类调用方的松紧要求本来就不同。
|
|
12
|
+
*
|
|
13
|
+
* - **检测**("这个目录是不是当前项目的 spec 仓")→ 严,避免误报。
|
|
14
|
+
* 例:`sddmult-sdd-specs` 是个只有 .git 的空壳,若仅凭目录名就认定,
|
|
15
|
+
* 会被当成 spec 包采纳(init 往里灌内容、sync-repos 把它的路径写进代码仓
|
|
16
|
+
* git config)。故名称判定必须附带"至少一项 SDD 产物"。
|
|
17
|
+
* - **校验**("用户显式给的路径能不能当 spec 用")→ 宽,接受半成品。
|
|
18
|
+
* 例:`--skip-openspec` 时 `openspec/changes` 永不创建,四结构必然不齐,
|
|
19
|
+
* 但包裹包是合法的。
|
|
20
|
+
*
|
|
21
|
+
* 因此不是"同一个布尔值到处用",而是**同一个函数返回等级,调用方按需取阈值**。
|
|
22
|
+
*
|
|
23
|
+
* 等级(tier 越小优先级越高):
|
|
24
|
+
*
|
|
25
|
+
* | tier | 来源 | 判据 |
|
|
26
|
+
* |------|-----------------|---------------------------------------------------|
|
|
27
|
+
* | 0 | 显式声明 | `.sdd-spec-root` / `git config` / `.sdd.yaml mono` |
|
|
28
|
+
* | | | / `.sdd-workspace.yaml spec_path` |
|
|
29
|
+
* | 1 | 四结构齐全 | modules.yaml + sdd.config.yaml + openspec/changes |
|
|
30
|
+
* | | | + openspec/specs/overview.md |
|
|
31
|
+
* | 2 | 目录名 | `*-sdd-specs`(**含预填空壳**:用户先建目录后填充) |
|
|
32
|
+
* | — | 非候选 | 无结构证据且目录名不匹配 |
|
|
33
|
+
*
|
|
34
|
+
* 依赖方向:本模块是叶子节点,加载期不 require 任何其它 ontology 模块
|
|
35
|
+
* (需要 sdd-config / workspace-layout 时在函数内惰性 require),
|
|
36
|
+
* 因此 workspace-layout / resolve-spec-root / sdd-config 都可以安全依赖它。
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const fs = require('fs');
|
|
40
|
+
const path = require('path');
|
|
41
|
+
|
|
42
|
+
/** tier 常量 */
|
|
43
|
+
const TIER = Object.freeze({
|
|
44
|
+
/** 显式声明:.sdd-spec-root / git config sdd.specPath / .sdd.yaml layout: mono */
|
|
45
|
+
DECLARED: 0,
|
|
46
|
+
/** 四结构齐全 */
|
|
47
|
+
STRUCTURE: 1,
|
|
48
|
+
/** 目录名 `*-sdd-specs`(含预填空壳) */
|
|
49
|
+
NAME: 2,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 四结构标记 —— tier 1 判据,**四项必须同时满足**。
|
|
54
|
+
*
|
|
55
|
+
* 与"名称判定"的关键区别:四结构不看目录名,因此用户自定义命名的包裹包
|
|
56
|
+
* (如 `erp-specs`)只要内容齐全也能被识别。
|
|
57
|
+
*/
|
|
58
|
+
const SPEC_STRUCTURE_MARKS = Object.freeze([
|
|
59
|
+
'modules.yaml',
|
|
60
|
+
'sdd.config.yaml',
|
|
61
|
+
'openspec/changes',
|
|
62
|
+
'openspec/specs/overview.md',
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 半成品标记 —— 用于「显式给定路径」的宽松校验(任一满足即可)。
|
|
67
|
+
*
|
|
68
|
+
* 为什么比四结构宽:`--skip-openspec` 下 `openspec/changes` 永不创建
|
|
69
|
+
*(openspec init 是唯一创建者),但包裹包合法。此判据只用于
|
|
70
|
+
* `validateSpecPath({ allowNestedPackage: true })` —— 路径是用户显式给的,
|
|
71
|
+
* 只需确认"内容像 spec 包",无需确认"是不是当前项目的 spec 仓"。
|
|
72
|
+
*/
|
|
73
|
+
const SPEC_ARTIFACT_MARKS = Object.freeze([
|
|
74
|
+
'modules.yaml',
|
|
75
|
+
'sdd.config.yaml',
|
|
76
|
+
'openspec/changes',
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 候选来源的中文标签(供 init / uninstall / sync-repos 展示,唯一真相源)。
|
|
81
|
+
*
|
|
82
|
+
* **来源表示「怎么找到的」,判据强度由候选的 `tier` 字段承载**:
|
|
83
|
+
* - hint / git / mono 显式声明(权威)
|
|
84
|
+
* - workspace 工作区探测(子仓目录被识别为 spec 包)
|
|
85
|
+
* - scan 裸目录扫描命中(结构或命名,具体见 `evidence`)
|
|
86
|
+
*
|
|
87
|
+
* ⚠️ 历史文案 `workspace: '工作区探测:子仓结构判据命中'` 有误 —— 该来源
|
|
88
|
+
* 实际也可能由**目录名** `*-sdd-specs` 命中(空壳子仓即如此)。故标签不再
|
|
89
|
+
* 声称具体判据,避免与 `tier` 冲突。
|
|
90
|
+
*/
|
|
91
|
+
const SPEC_SOURCE_LABELS = Object.freeze({
|
|
92
|
+
hint: '权威:.sdd-spec-root 指向',
|
|
93
|
+
git: '权威:git config sdd.specPath',
|
|
94
|
+
mono: '权威:.sdd.yaml layout: mono',
|
|
95
|
+
'workspace-yaml': '权威:.sdd-workspace.yaml spec_path',
|
|
96
|
+
scan: '推测:目录内容或命名命中',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
/** tier 内来源优先级(数值越小越靠前) */
|
|
100
|
+
const SOURCE_RANK = Object.freeze({
|
|
101
|
+
hint: 0,
|
|
102
|
+
git: 1,
|
|
103
|
+
mono: 2,
|
|
104
|
+
'workspace-yaml': 3,
|
|
105
|
+
scan: 4,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 目录名是否符合 `*-sdd-specs` 命名约定。
|
|
110
|
+
*
|
|
111
|
+
* 单独导出是因为「名称命中」与「结构命中」是两个不同强度的证据,
|
|
112
|
+
* 调用方有时需要区分(如提示文案、诊断)。
|
|
113
|
+
*
|
|
114
|
+
* @param {string} dir
|
|
115
|
+
* @returns {boolean}
|
|
116
|
+
*/
|
|
117
|
+
function isSpecDirName(dir) {
|
|
118
|
+
if (!dir) return false;
|
|
119
|
+
const name = path.basename(path.resolve(dir)).toLowerCase();
|
|
120
|
+
return /-sdd-specs$/.test(name) || name.endsWith('sdd-specs');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 列出实际存在的标记(相对路径用 posix 形式,便于展示与断言) */
|
|
124
|
+
function listExistingMarks(dir, marks) {
|
|
125
|
+
return marks.filter((mark) => {
|
|
126
|
+
try {
|
|
127
|
+
return fs.existsSync(path.join(dir, ...mark.split('/')));
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 对目录做分级判定 —— **本模块的核心,全仓唯一的 spec 判据**。
|
|
136
|
+
*
|
|
137
|
+
* @param {string} dir 候选目录
|
|
138
|
+
* @returns {{tier: number, source: 'structure'|'name', evidence: string[]}|null}
|
|
139
|
+
* null 表示不是 spec 候选
|
|
140
|
+
*/
|
|
141
|
+
function classifySpecDir(dir) {
|
|
142
|
+
if (!dir) return null;
|
|
143
|
+
const abs = path.resolve(dir);
|
|
144
|
+
let st;
|
|
145
|
+
try {
|
|
146
|
+
st = fs.statSync(abs);
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
if (!st.isDirectory()) return null;
|
|
151
|
+
|
|
152
|
+
// tier 1:四结构齐全(不看目录名 —— 支持用户自定义命名)
|
|
153
|
+
const structure = listExistingMarks(abs, SPEC_STRUCTURE_MARKS);
|
|
154
|
+
if (structure.length === SPEC_STRUCTURE_MARKS.length) {
|
|
155
|
+
return { tier: TIER.STRUCTURE, source: 'structure', evidence: structure };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// tier 2:目录名命中。**空壳(evidence 为空)仍是候选** ——
|
|
159
|
+
// 典型场景是用户预建目录待填充;由调用方按 tier 排序决定取舍。
|
|
160
|
+
if (!isSpecDirName(abs)) return null;
|
|
161
|
+
return {
|
|
162
|
+
tier: TIER.NAME,
|
|
163
|
+
source: 'name',
|
|
164
|
+
evidence: listExistingMarks(abs, SPEC_ARTIFACT_MARKS),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 严格判据:是否可作为 spec 候选(检测用)。
|
|
170
|
+
*
|
|
171
|
+
* 与历史 `looksLikeSpecRepo` 语义等价 —— 名称命中**或**四结构齐全。
|
|
172
|
+
* 保留"名称即命中"是刻意的:空壳 `*-sdd-specs` 是预填场景,必须入选。
|
|
173
|
+
*
|
|
174
|
+
* @param {string} dir
|
|
175
|
+
* @returns {boolean}
|
|
176
|
+
*/
|
|
177
|
+
function isSpecCandidate(dir) {
|
|
178
|
+
return classifySpecDir(dir) !== null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 校验判据:目录是否可作为 kb-state.json 的宿主 / spec 根。
|
|
183
|
+
*
|
|
184
|
+
* 比 `isSpecCandidate` 多一条 `openspec/` 兜底 —— 半成品包裹包
|
|
185
|
+
* (`--skip-openspec` 产物,只有 openspec/ 目录树)也是合法的 spec 根。
|
|
186
|
+
*
|
|
187
|
+
* @param {string} dir
|
|
188
|
+
* @returns {boolean}
|
|
189
|
+
*/
|
|
190
|
+
function isSpecPackageRoot(dir) {
|
|
191
|
+
if (!dir) return false;
|
|
192
|
+
const abs = path.resolve(dir);
|
|
193
|
+
if (classifySpecDir(abs)) return true;
|
|
194
|
+
try {
|
|
195
|
+
return fs.existsSync(path.join(abs, 'openspec'));
|
|
196
|
+
} catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 宽松判据:目录内容是否"像 spec 包"(任一产物标记存在)。
|
|
203
|
+
*
|
|
204
|
+
* 仅用于用户**显式给定**路径的校验(`validateSpecPath` 的 allowNestedPackage)。
|
|
205
|
+
* 不可用于检测 —— 单仓代码仓根可能有 modules.yaml 而无 openspec。
|
|
206
|
+
*
|
|
207
|
+
* @param {string} dir
|
|
208
|
+
* @returns {boolean}
|
|
209
|
+
*/
|
|
210
|
+
function hasSpecPackageContent(dir) {
|
|
211
|
+
if (!dir) return false;
|
|
212
|
+
const abs = path.resolve(dir);
|
|
213
|
+
try {
|
|
214
|
+
if (!fs.statSync(abs).isDirectory()) return false;
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
return listExistingMarks(abs, SPEC_ARTIFACT_MARKS).length > 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** 向上逐级读取 `.sdd-spec-root` 指针(内容为相对路径) */
|
|
222
|
+
function resolveHintUpward(anchorRoot) {
|
|
223
|
+
let current = path.resolve(anchorRoot);
|
|
224
|
+
const stop = path.parse(current).root;
|
|
225
|
+
for (;;) {
|
|
226
|
+
const hintPath = path.join(current, '.sdd-spec-root');
|
|
227
|
+
if (fs.existsSync(hintPath)) {
|
|
228
|
+
try {
|
|
229
|
+
const rel = String(fs.readFileSync(hintPath, 'utf8') || '')
|
|
230
|
+
.trim()
|
|
231
|
+
.split(/\r?\n/)[0]
|
|
232
|
+
.trim();
|
|
233
|
+
if (rel) {
|
|
234
|
+
const abs = path.resolve(current, rel);
|
|
235
|
+
if (fs.existsSync(abs)) return abs;
|
|
236
|
+
}
|
|
237
|
+
} catch { /* 指针损坏:继续向上,不阻断 */ }
|
|
238
|
+
}
|
|
239
|
+
if (current === stop) break;
|
|
240
|
+
current = path.dirname(current);
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** 向上逐级查找 `.sdd.yaml` 的 `layout: mono` 声明 */
|
|
246
|
+
function resolveMonoUpward(anchorRoot) {
|
|
247
|
+
let current = path.resolve(anchorRoot);
|
|
248
|
+
const stop = path.parse(current).root;
|
|
249
|
+
for (;;) {
|
|
250
|
+
try {
|
|
251
|
+
const sddConfig = require('./sdd-config.cjs');
|
|
252
|
+
if (sddConfig.isMonoLayout(current)) {
|
|
253
|
+
const resolved = sddConfig.resolveMonoSpecRoot(current);
|
|
254
|
+
if (resolved && fs.existsSync(resolved)) return resolved;
|
|
255
|
+
}
|
|
256
|
+
} catch { /* sdd-config 不可用时跳过该来源 */ }
|
|
257
|
+
if (current === stop) break;
|
|
258
|
+
current = path.dirname(current);
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 向上逐级查找 `.sdd-workspace.yaml` 的 `spec_path` 声明(多仓布局)。
|
|
265
|
+
*
|
|
266
|
+
* 与 `resolveMonoUpward` 的区别:`.sdd.yaml layout: mono` 描述**单仓内**的包裹包,
|
|
267
|
+
* `.sdd-workspace.yaml` 描述**多仓工作区**的编排(spec 是兄弟目录的独立 Git 仓)。
|
|
268
|
+
* 二者都是显式声明,属权威来源。
|
|
269
|
+
*
|
|
270
|
+
* 解析委托 `workspace-layout.parseWorkspaceYaml`(唯一解析实现),避免此处
|
|
271
|
+
* 再写一遍正则 —— 历史上 skills 的 `opsx-consistency-check/scripts.cjs` 就是
|
|
272
|
+
* 自己解析该文件,导致「权威链不认这个来源、技能自己认」的不一致。
|
|
273
|
+
*
|
|
274
|
+
* @param {string} anchorRoot 搜索起点
|
|
275
|
+
* @returns {string|null} spec 根绝对路径;未声明 / 指向不存在时 null
|
|
276
|
+
*/
|
|
277
|
+
function resolveWorkspaceYamlUpward(anchorRoot) {
|
|
278
|
+
let current = path.resolve(anchorRoot);
|
|
279
|
+
const stop = path.parse(current).root;
|
|
280
|
+
for (;;) {
|
|
281
|
+
try {
|
|
282
|
+
const workspaceLayout = require('./workspace-layout.cjs');
|
|
283
|
+
const loaded = workspaceLayout.loadWorkspaceFile(current);
|
|
284
|
+
if (loaded.ok && loaded.data && loaded.data.spec_path) {
|
|
285
|
+
// spec_path 相对清单所在目录解析(清单是工作区级文件)
|
|
286
|
+
const abs = path.resolve(current, loaded.data.spec_path);
|
|
287
|
+
if (fs.existsSync(abs)) return abs;
|
|
288
|
+
}
|
|
289
|
+
} catch { /* 解析异常:继续向上,不阻断 */ }
|
|
290
|
+
if (current === stop) break;
|
|
291
|
+
current = path.dirname(current);
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* 枚举 spec 候选集合,按优先级排序。
|
|
298
|
+
*
|
|
299
|
+
* 这是 init / uninstall / sync-repos 三个 CLI 场景**共用**的入口:
|
|
300
|
+
* 三处都要求「多候选时让用户单选」,故必须返回集合而非单值。
|
|
301
|
+
*
|
|
302
|
+
* 来源与 tier:
|
|
303
|
+
* - hint / git / mono → tier 0(显式声明,权威)
|
|
304
|
+
* - 子仓 / 子目录 / 根自身命中 → tier 1(四结构)或 tier 2(名称)
|
|
305
|
+
*
|
|
306
|
+
* 排序:tier 升序 → 来源优先级 → 名称升序(保证输出稳定、可断言)。
|
|
307
|
+
*
|
|
308
|
+
* @param {string} anchorRoot 探测起点(工作目录 / 工作区根)
|
|
309
|
+
* @param {{gitSpecPath?: string, includeRoot?: boolean}} [options]
|
|
310
|
+
* gitSpecPath 由调用方读取后传入,避免本模块反向依赖 sdd-config 的 git 逻辑
|
|
311
|
+
* @returns {Array<{abs: string, name: string, source: string, tier: number, evidence: string[]}>}
|
|
312
|
+
*/
|
|
313
|
+
function detectSpecCandidates(anchorRoot, options = {}) {
|
|
314
|
+
const root = path.resolve(anchorRoot || process.cwd());
|
|
315
|
+
const includeRoot = options.includeRoot !== false;
|
|
316
|
+
const out = [];
|
|
317
|
+
const seen = new Set();
|
|
318
|
+
|
|
319
|
+
const push = (candidate, source, tier, evidence) => {
|
|
320
|
+
if (!candidate) return;
|
|
321
|
+
const abs = path.resolve(candidate);
|
|
322
|
+
if (seen.has(abs)) return;
|
|
323
|
+
let st;
|
|
324
|
+
try {
|
|
325
|
+
st = fs.statSync(abs);
|
|
326
|
+
} catch {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (!st.isDirectory()) return;
|
|
330
|
+
seen.add(abs);
|
|
331
|
+
out.push({
|
|
332
|
+
abs,
|
|
333
|
+
name: path.basename(abs),
|
|
334
|
+
source,
|
|
335
|
+
tier,
|
|
336
|
+
evidence: evidence || [],
|
|
337
|
+
});
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// ── tier 0:显式声明(权威)──────────────────────────────
|
|
341
|
+
push(resolveHintUpward(root), 'hint', TIER.DECLARED, ['.sdd-spec-root']);
|
|
342
|
+
push(options.gitSpecPath, 'git', TIER.DECLARED, ['git config sdd.specPath']);
|
|
343
|
+
push(resolveMonoUpward(root), 'mono', TIER.DECLARED, ['.sdd.yaml layout: mono']);
|
|
344
|
+
push(resolveWorkspaceYamlUpward(root), 'workspace-yaml', TIER.DECLARED,
|
|
345
|
+
['.sdd-workspace.yaml spec_path']);
|
|
346
|
+
|
|
347
|
+
// ── tier 1/2:结构 / 名称判据扫描(来源统一记 'scan')──────
|
|
348
|
+
// 判据强度由 `tier` 承载,来源只表示「怎么找到的」,不表示判据类型 ——
|
|
349
|
+
// 否则 UI 无法解释「同一来源却强度不同」。具体命中判据见 `evidence`。
|
|
350
|
+
const pushClassified = (dir) => {
|
|
351
|
+
const classified = classifySpecDir(dir);
|
|
352
|
+
if (!classified) return;
|
|
353
|
+
push(dir, 'scan', classified.tier, classified.evidence);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// 根自身
|
|
357
|
+
if (includeRoot) pushClassified(root);
|
|
358
|
+
|
|
359
|
+
// 一级子目录(含非 Git 目录 —— 单仓包裹包不是独立 Git)
|
|
360
|
+
try {
|
|
361
|
+
const workspaceLayout = require('./workspace-layout.cjs');
|
|
362
|
+
for (const child of workspaceLayout.listChildDirectories(root)) {
|
|
363
|
+
pushClassified(child.abs);
|
|
364
|
+
}
|
|
365
|
+
} catch { /* workspace-layout 不可用时退化为仅根目录 */ }
|
|
366
|
+
|
|
367
|
+
out.sort((a, b) => {
|
|
368
|
+
if (a.tier !== b.tier) return a.tier - b.tier;
|
|
369
|
+
const rankA = SOURCE_RANK[a.source] ?? 99;
|
|
370
|
+
const rankB = SOURCE_RANK[b.source] ?? 99;
|
|
371
|
+
if (rankA !== rankB) return rankA - rankB;
|
|
372
|
+
return a.name.localeCompare(b.name);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
return out;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* 枚举候选集合,并自动读取 `git config --local sdd.specPath` 作为 tier 0 来源。
|
|
380
|
+
*
|
|
381
|
+
* 这是 CLI 三个场景(init / uninstall / sync-repos)**共用的唯一枚举入口**。
|
|
382
|
+
* 历史上 init 与 uninstall 各写了一份完全相同的实现(都只是「读 git config
|
|
383
|
+
* 后调 listSpecCandidates」),本函数把它收敛为一处。
|
|
384
|
+
*
|
|
385
|
+
* 为什么 git 读取放在这里而不是让调用方传入:读取逻辑需要 sdd-config,
|
|
386
|
+
* 而 sdd-config 顶层 require 本模块 —— 放在函数内惰性 require 可避免加载期
|
|
387
|
+
* 循环依赖(本模块顶层只依赖 fs/path)。
|
|
388
|
+
*
|
|
389
|
+
* @param {string} anchorRoot 探测起点(工作目录 / 工作区根)
|
|
390
|
+
* @returns {Array<{abs: string, name: string, source: string, tier: number, evidence: string[]}>}
|
|
391
|
+
*/
|
|
392
|
+
function detectSpecCandidatesWithGit(anchorRoot) {
|
|
393
|
+
const root = path.resolve(anchorRoot || process.cwd());
|
|
394
|
+
let gitSpecPath = '';
|
|
395
|
+
try {
|
|
396
|
+
gitSpecPath = require('./sdd-config.cjs').gitConfig(root, 'sdd.specPath') || '';
|
|
397
|
+
} catch { /* 非 Git 仓或 git 不可用:跳过该来源 */ }
|
|
398
|
+
return detectSpecCandidates(root, { gitSpecPath });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** 取候选来源的中文标签;未知来源原样返回 */
|
|
402
|
+
function specSourceLabel(source) {
|
|
403
|
+
return SPEC_SOURCE_LABELS[source] || source || '未知来源';
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
module.exports = {
|
|
407
|
+
TIER,
|
|
408
|
+
SPEC_STRUCTURE_MARKS,
|
|
409
|
+
SPEC_ARTIFACT_MARKS,
|
|
410
|
+
SPEC_SOURCE_LABELS,
|
|
411
|
+
SOURCE_RANK,
|
|
412
|
+
isSpecDirName,
|
|
413
|
+
classifySpecDir,
|
|
414
|
+
isSpecCandidate,
|
|
415
|
+
isSpecPackageRoot,
|
|
416
|
+
hasSpecPackageContent,
|
|
417
|
+
resolveWorkspaceYamlUpward,
|
|
418
|
+
detectSpecCandidates,
|
|
419
|
+
detectSpecCandidatesWithGit,
|
|
420
|
+
specSourceLabel,
|
|
421
|
+
};
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
|
+
// spec 判据的唯一真相源。spec-detect 对 workspace-layout 的依赖是惰性(函数内
|
|
7
|
+
// require),故此处顶层 require 不构成循环依赖。
|
|
8
|
+
const specDetect = require('./spec-detect.cjs');
|
|
9
|
+
|
|
6
10
|
const WORKSPACE_FILE = '.sdd-workspace.yaml';
|
|
7
11
|
const IGNORE_DIR_NAMES = new Set([
|
|
8
12
|
'node_modules',
|
|
@@ -24,19 +28,19 @@ function isGitRepo(dir) {
|
|
|
24
28
|
return fs.existsSync(path.join(dir, '.git'));
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
/**
|
|
32
|
+
* 是否像 SDD spec 包裹包。
|
|
33
|
+
*
|
|
34
|
+
* ⚠️ 判据已收敛到 `spec-detect.cjs#isSpecCandidate`(全仓唯一判据入口),
|
|
35
|
+
* 本函数保留仅为向后兼容既有调用点,**不得再在此处新增判据**。
|
|
36
|
+
*
|
|
37
|
+
* 语义(与 spec-detect 一致):
|
|
38
|
+
* - 四结构齐全(modules.yaml + sdd.config.yaml + openspec/changes
|
|
39
|
+
* + openspec/specs/overview.md)→ 命中,**不看目录名**
|
|
40
|
+
* - 目录名 `*-sdd-specs` → 命中(含预填空壳,由 tier 排序决定取舍)
|
|
41
|
+
*/
|
|
27
42
|
function looksLikeSpecRepo(dir) {
|
|
28
|
-
|
|
29
|
-
// 命名约定:*-sdd-specs 目录名直接命中
|
|
30
|
-
if (/-sdd-specs$/.test(name) || name.endsWith('sdd-specs')) return true;
|
|
31
|
-
// 结构判据(四项同时满足):
|
|
32
|
-
// modules.yaml(模块登记)+ sdd.config.yaml(活动变更登记)
|
|
33
|
-
// + openspec/changes(变更结构)+ openspec/specs/overview.md(全局契约)
|
|
34
|
-
return (
|
|
35
|
-
fs.existsSync(path.join(dir, 'modules.yaml'))
|
|
36
|
-
&& fs.existsSync(path.join(dir, 'sdd.config.yaml'))
|
|
37
|
-
&& fs.existsSync(path.join(dir, 'openspec', 'changes'))
|
|
38
|
-
&& fs.existsSync(path.join(dir, 'openspec', 'specs', 'overview.md'))
|
|
39
|
-
);
|
|
43
|
+
return specDetect.isSpecCandidate(dir);
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
function listChildGitRepos(workspaceRoot) {
|
|
@@ -88,7 +92,6 @@ function detectWorkspaceLayout(workspaceRoot = process.cwd()) {
|
|
|
88
92
|
rootIsGit,
|
|
89
93
|
specRepo: spec,
|
|
90
94
|
codeRepos,
|
|
91
|
-
allChildGitRepos: children,
|
|
92
95
|
};
|
|
93
96
|
}
|
|
94
97
|
|
|
@@ -228,6 +231,22 @@ function findNestedSpecPackage(parentDir) {
|
|
|
228
231
|
);
|
|
229
232
|
}
|
|
230
233
|
|
|
234
|
+
/**
|
|
235
|
+
* 取候选来源的中文标签;未知来源原样返回。
|
|
236
|
+
*
|
|
237
|
+
* ⚠️ 标签表与候选枚举均已收敛到 `spec-detect.cjs`(唯一真相源):
|
|
238
|
+
* - 枚举用 `detectSpecCandidates` / `detectSpecCandidatesWithGit`
|
|
239
|
+
* - 标签用 `spec-detect#specSourceLabel`
|
|
240
|
+
*
|
|
241
|
+
* 本模块曾另有一份 `listSpecCandidates`(多出一个 'workspace' 来源标签),
|
|
242
|
+
* 现已删除 —— 它只是给「扫描已能找到的同一个目录」换个标签,对候选集合
|
|
243
|
+
* 无增量,反而造成「同一目录两种来源说法」。判据强度统一由候选的 `tier`
|
|
244
|
+
* 承载(0=显式声明 / 1=四结构 / 2=名称含空壳)。
|
|
245
|
+
*/
|
|
246
|
+
function specSourceLabel(source) {
|
|
247
|
+
return specDetect.specSourceLabel(source);
|
|
248
|
+
}
|
|
249
|
+
|
|
231
250
|
/**
|
|
232
251
|
* 确保父目录下存在统一的 SDD 包裹包目录。
|
|
233
252
|
* @param {{ name?: string, initGit?: boolean }} options
|
|
@@ -278,18 +297,29 @@ function ensureSpecPackage(parentDir, options = {}) {
|
|
|
278
297
|
return { name, abs, created: !existed, isGit };
|
|
279
298
|
}
|
|
280
299
|
|
|
300
|
+
/**
|
|
301
|
+
* 公开 API —— 只导出「有跨模块消费者」的符号。
|
|
302
|
+
*
|
|
303
|
+
* 刻意不导出(函数体保留,模块内部使用):
|
|
304
|
+
* - `WORKSPACE_FILE` 仅本模块 loadWorkspaceFile / writeWorkspaceFile 使用
|
|
305
|
+
* - `listChildGitRepos` 仅 detectWorkspaceLayout 使用
|
|
306
|
+
* - `parseWorkspaceYaml` 仅 loadWorkspaceFile 使用
|
|
307
|
+
*
|
|
308
|
+
* 标签表 `SPEC_SOURCE_LABELS` 已迁至 `spec-detect.cjs`(唯一真相源),
|
|
309
|
+
* 本模块仅转发 `specSourceLabel()`;如需直接读表请从 spec-detect 引入。
|
|
310
|
+
*
|
|
311
|
+
* 如需外部使用请重新加入本表,而不是依赖 `module.exports` 的隐式宽松。
|
|
312
|
+
*/
|
|
281
313
|
module.exports = {
|
|
282
|
-
WORKSPACE_FILE,
|
|
283
314
|
isGitRepo,
|
|
284
315
|
looksLikeSpecRepo,
|
|
285
|
-
listChildGitRepos,
|
|
286
316
|
listChildDirectories,
|
|
287
317
|
detectWorkspaceLayout,
|
|
288
|
-
parseWorkspaceYaml,
|
|
289
318
|
loadWorkspaceFile,
|
|
290
319
|
writeWorkspaceFile,
|
|
291
320
|
findNewCodeRepos,
|
|
292
321
|
deriveSpecPackageName,
|
|
293
322
|
findNestedSpecPackage,
|
|
323
|
+
specSourceLabel,
|
|
294
324
|
ensureSpecPackage,
|
|
295
325
|
};
|
|
@@ -1,104 +1,22 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
# KLD SDD
|
|
3
|
-
# 在下方 SCRIPTS 列表中声明要加载的 .cjs 脚本
|
|
4
|
-
# 删除某行 = 禁用该插件
|
|
2
|
+
# KLD SDD provenance hook v2
|
|
5
3
|
# marker: KLD SDD quality gate
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
hooks_dir=""
|
|
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"
|
|
4
|
+
# Ordinary commits save work; delivery-check verifies readiness at Apply completion.
|
|
5
|
+
git_root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
|
|
6
|
+
spec_path=$(git config --local sdd.specPath 2>/dev/null)
|
|
7
|
+
if [ -z "$spec_path" ] && [ -f "$git_root/.sdd-spec-root" ]; then
|
|
8
|
+
IFS= read -r spec_path < "$git_root/.sdd-spec-root"
|
|
31
9
|
fi
|
|
32
|
-
if [ -
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if [ -
|
|
40
|
-
|
|
41
|
-
while [ -n "$search_dir" ] && [ "$search_dir" != "/" ]; do
|
|
42
|
-
if [ -d "$search_dir/skywalk-sdd/git-hooks" ]; then
|
|
43
|
-
hooks_dir="$search_dir/skywalk-sdd/git-hooks"
|
|
44
|
-
break
|
|
45
|
-
fi
|
|
46
|
-
# Windows 盘符根(P:/)dirname 后不变,需显式收敛,避免死循环
|
|
47
|
-
parent_dir="$(dirname "$search_dir")"
|
|
48
|
-
[ "$parent_dir" = "$search_dir" ] && break
|
|
49
|
-
search_dir="$parent_dir"
|
|
50
|
-
done
|
|
51
|
-
fi
|
|
52
|
-
|
|
53
|
-
if [ -z "$hooks_dir" ]; then
|
|
54
|
-
echo "[SDD] 未找到 spec 仓的 skywalk-sdd/git-hooks 目录,跳过 commit-msg"
|
|
55
|
-
exit 0
|
|
56
|
-
fi
|
|
57
|
-
|
|
58
|
-
# 从配置文件读取插件列表
|
|
59
|
-
config_file="$hooks_dir/hooks.config"
|
|
60
|
-
if [ ! -f "$config_file" ]; then
|
|
61
|
-
echo "[SDD] ⚠️ 配置文件不存在:hooks.config,使用默认插件"
|
|
62
|
-
SCRIPTS="commit-msg-sdd-trailer.cjs"
|
|
63
|
-
else
|
|
64
|
-
# 读取 [commit-msg] 段落的配置
|
|
65
|
-
in_section=0
|
|
66
|
-
SCRIPTS=""
|
|
67
|
-
CR=$(printf '\r')
|
|
68
|
-
while IFS= read -r line; do
|
|
69
|
-
line=${line%"$CR"}
|
|
70
|
-
case "$line" in
|
|
71
|
-
"[commit-msg]") in_section=1 ;;
|
|
72
|
-
"["*) in_section=0 ;;
|
|
73
|
-
"#"*|"") ;;
|
|
74
|
-
*)
|
|
75
|
-
if [ "$in_section" -eq 1 ]; then
|
|
76
|
-
SCRIPTS="$SCRIPTS $line"
|
|
77
|
-
fi
|
|
78
|
-
;;
|
|
79
|
-
esac
|
|
80
|
-
done < "$config_file"
|
|
81
|
-
fi
|
|
82
|
-
|
|
83
|
-
executed=0
|
|
84
|
-
failed=0
|
|
85
|
-
for script_name in $SCRIPTS; do
|
|
86
|
-
script="$hooks_dir/$script_name"
|
|
87
|
-
if [ ! -f "$script" ]; then
|
|
88
|
-
echo "[SDD] ❌ 已配置的插件不存在:$script_name"
|
|
89
|
-
failed=$((failed + 1))
|
|
90
|
-
continue
|
|
91
|
-
fi
|
|
92
|
-
executed=$((executed + 1))
|
|
93
|
-
echo "[SDD] 执行 commit-msg 插件:$script_name"
|
|
94
|
-
node "$script" "$1" --project="$git_root" || failed=$((failed + 1))
|
|
95
|
-
done
|
|
96
|
-
|
|
97
|
-
if [ "$failed" -gt 0 ]; then
|
|
98
|
-
echo "[SDD] ❌ 共执行 $executed 个 commit-msg 插件,$failed 个失败"
|
|
99
|
-
exit 1
|
|
100
|
-
elif [ "$executed" -eq 0 ]; then
|
|
101
|
-
echo "[SDD] 配置已禁用 commit-msg 插件"
|
|
10
|
+
if [ -z "$spec_path" ]; then spec_path="$git_root"; fi
|
|
11
|
+
spec_path=$(printf '%s' "$spec_path" | tr '\\' '/' | tr -d '\r')
|
|
12
|
+
case "$spec_path" in
|
|
13
|
+
/*|[A-Za-z]:*) ;;
|
|
14
|
+
*) spec_path="$git_root/$spec_path" ;;
|
|
15
|
+
esac
|
|
16
|
+
runner="$spec_path/skywalk-sdd/git-hooks/commit-msg-runner.cjs"
|
|
17
|
+
if [ -f "$runner" ]; then
|
|
18
|
+
node "$runner" "$1" --project="$git_root" || echo '[SDD] 关联待补充,本次提交继续' >&2
|
|
102
19
|
else
|
|
103
|
-
echo
|
|
20
|
+
echo '[SDD] Spec 运行时未就绪,关联待补充;可重装或修正 sdd.specPath,本次提交继续' >&2
|
|
104
21
|
fi
|
|
22
|
+
exit 0
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
// Only the managed provenance worker is advisory. User hooks are never run here.
|
|
9
|
+
const original = process.argv[2];
|
|
10
|
+
let tempDir;
|
|
11
|
+
try {
|
|
12
|
+
const config = path.join(__dirname, 'hooks.config');
|
|
13
|
+
if (fs.existsSync(config)) {
|
|
14
|
+
const text = fs.readFileSync(config, 'utf8');
|
|
15
|
+
const section = text.split(/\[commit-msg\]\s*\r?\n/)[1]?.split(/^\[/m)[0] || '';
|
|
16
|
+
if (!section.split(/\r?\n/).some(line => line.trim() === 'commit-msg-sdd-trailer.cjs')) process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
if (!String(process.env.KLD_SDD_CHANGE || '').trim()
|
|
19
|
+
&& !fs.existsSync(path.join(__dirname, '../state/delivery-associations'))) {
|
|
20
|
+
// No explicit or recorded association exists. Avoid starting a second Node
|
|
21
|
+
// process and loading ontology just to discover that there is nothing to add.
|
|
22
|
+
const message = fs.readFileSync(original, 'utf8').replace(/^Spec-(?:Change|Revision):[^\n]*\n?/gim, '');
|
|
23
|
+
fs.writeFileSync(original, message);
|
|
24
|
+
console.error('[SDD] 本次提交的 Change 待关联;保存不受影响');
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sdd-msg-'));
|
|
28
|
+
const copy = path.join(tempDir, 'message');
|
|
29
|
+
fs.copyFileSync(original, copy);
|
|
30
|
+
const result = spawnSync(process.execPath, [path.join(__dirname, 'commit-msg-sdd-trailer.cjs'), copy, ...process.argv.slice(3)], {
|
|
31
|
+
encoding: 'utf8', timeout: 1500, maxBuffer: 256 * 1024,
|
|
32
|
+
});
|
|
33
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
34
|
+
if (result.status === 0 && !result.error) fs.copyFileSync(copy, original);
|
|
35
|
+
else {
|
|
36
|
+
const message = fs.readFileSync(original, 'utf8').replace(/^Spec-(?:Change|Revision):[^\n]*\n?/gim, '');
|
|
37
|
+
fs.writeFileSync(original, message);
|
|
38
|
+
console.error('[SDD] 关联待补充(超时或运行失败),本次提交继续');
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error(`[SDD] 关联待补充:${error.message};本次提交继续`);
|
|
42
|
+
} finally {
|
|
43
|
+
if (tempDir) fs.rmSync(tempDir, { recursive: true, force: true });
|
|
44
|
+
}
|