kld-sdd 2.6.7 → 2.6.9
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 +39 -3
- package/lib/init.js +320 -45
- package/lib/workspace-layout.js +2 -0
- package/package.json +2 -2
- package/skywalk-sdd/context-client.cjs +59 -5
- package/skywalk-sdd/ontology/active-changes.cjs +297 -0
- package/skywalk-sdd/ontology/change-key.cjs +241 -0
- package/skywalk-sdd/ontology/cli.cjs +135 -0
- package/skywalk-sdd/ontology/list-changes.cjs +110 -0
- package/skywalk-sdd/ontology/modules.cjs +167 -0
- package/skywalk-sdd/ontology/naming-diagnose.cjs +594 -0
- package/skywalk-sdd/ontology/sdd-config.cjs +335 -0
- package/skywalk-sdd/ontology/workspace-layout.cjs +194 -0
- package/templates/dot-sdd.yaml +8 -0
- package/templates/git-hooks/commit-msg-sdd-trailer.cjs +224 -0
- package/templates/modules.yaml +13 -0
- package/templates/openspec/proposal.md +7 -1
- package/templates/sdd.config.yaml +12 -0
- package/templates/skills/kld-sdd/openspec-sync-specs/SKILL.md +148 -0
- package/templates/skills/kld-sdd/openspec-update-change/SKILL.md +86 -0
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +3 -3
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +1 -1
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +11 -1
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +73 -3
- package/templates/skills/kld-sdd/opsx-design/SKILL.md +9 -0
- package/templates/skills/kld-sdd/opsx-explore/SKILL.md +37 -17
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +9 -14
- package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +83 -109
- package/templates/skills/kld-sdd/opsx-ontology-query/phase-1-prechange.md +276 -0
- package/templates/skills/kld-sdd/opsx-ontology-query/phase-2-during.md +354 -0
- package/templates/skills/kld-sdd/opsx-ontology-query/phase-3-postchange.md +223 -0
- package/templates/skills/kld-sdd/opsx-ontology-query/phase-4-explore.md +240 -0
- package/templates/skills/kld-sdd/opsx-ontology-query/phase-5-governance.md +232 -0
- package/templates/skills/kld-sdd/opsx-ontology-query/reference.md +92 -4
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +87 -16
- package/templates/skills/kld-sdd/opsx-propose/checklist.md +1 -0
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +33 -3
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +10 -0
- package/templates/skills/kld-sdd/opsx-tdd-core/checklist.md +1 -1
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* sdd.config.yaml —— spec 仓库根目录的活动变更登记表。
|
|
5
|
+
*
|
|
6
|
+
* 唯一职责:让所有已接入代码仓(经 sdd.specPath 读到同一份 spec clone)
|
|
7
|
+
* 拿到当前活动 change 的 key / 中文标题 / 摘要,供 AI 参考,
|
|
8
|
+
* 并供 commit-msg Hook 写入 Spec-Change Trailer(可多条)。
|
|
9
|
+
*
|
|
10
|
+
* 不承载:代码仓清单、路径 glob、CI 配置、本地绝对路径。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const changeKey = require('./change-key.cjs');
|
|
16
|
+
|
|
17
|
+
const FILE_NAME = 'sdd.config.yaml';
|
|
18
|
+
|
|
19
|
+
const CODES = {
|
|
20
|
+
MISSING: 'SDD_CONFIG_MISSING',
|
|
21
|
+
FORMAT_INVALID: 'SDD_CONFIG_FORMAT_INVALID',
|
|
22
|
+
CHANGE_KEY_INVALID: 'SDD_CONFIG_CHANGE_KEY_INVALID',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const FIELD_ALIASES = {
|
|
26
|
+
'change-key': 'changeKey',
|
|
27
|
+
'change-id': 'changeId',
|
|
28
|
+
title: 'title',
|
|
29
|
+
module: 'module',
|
|
30
|
+
summary: 'summary',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function stripQuotes(value) {
|
|
34
|
+
const text = String(value == null ? '' : value).trim();
|
|
35
|
+
if (
|
|
36
|
+
(text.startsWith('"') && text.endsWith('"') && text.length >= 2) ||
|
|
37
|
+
(text.startsWith("'") && text.endsWith("'") && text.length >= 2)
|
|
38
|
+
) {
|
|
39
|
+
return text.slice(1, -1);
|
|
40
|
+
}
|
|
41
|
+
return text;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function needsQuotes(value) {
|
|
45
|
+
return /^[\s]|[:#]|[\s]$/.test(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function quoteIfNeeded(value) {
|
|
49
|
+
const text = String(value == null ? '' : value);
|
|
50
|
+
if (!text) return '""';
|
|
51
|
+
if (needsQuotes(text)) return `"${text.replace(/"/g, '\\"')}"`;
|
|
52
|
+
return text;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Minimal parser for the constrained schema:
|
|
57
|
+
*
|
|
58
|
+
* version: 1
|
|
59
|
+
* active_changes:
|
|
60
|
+
* - change-key: fi-260727-account-doc-head-create
|
|
61
|
+
* change-id: CHG-FI-260727-ACCOUNT-DOC-HEAD-CREATE
|
|
62
|
+
* title: 会计凭证头创建
|
|
63
|
+
* module: fi
|
|
64
|
+
* summary: 支持凭证头创建与校验
|
|
65
|
+
*/
|
|
66
|
+
function parseSddConfigYaml(text) {
|
|
67
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
68
|
+
const activeChanges = [];
|
|
69
|
+
let version = null;
|
|
70
|
+
let inActive = false;
|
|
71
|
+
let current = null;
|
|
72
|
+
|
|
73
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
74
|
+
const raw = lines[index];
|
|
75
|
+
const line = raw.replace(/#.*$/, '').replace(/\s+$/, '');
|
|
76
|
+
if (!line.trim()) continue;
|
|
77
|
+
|
|
78
|
+
const versionMatch = /^version:\s*(\d+)\s*$/.exec(line);
|
|
79
|
+
if (versionMatch) {
|
|
80
|
+
version = Number(versionMatch[1]);
|
|
81
|
+
inActive = false;
|
|
82
|
+
current = null;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (/^active_changes:\s*(\[\s*\])?\s*$/.test(line)) {
|
|
87
|
+
inActive = true;
|
|
88
|
+
current = null;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!inActive) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
code: CODES.FORMAT_INVALID,
|
|
96
|
+
message: `${FILE_NAME} 第 ${index + 1} 行无法解析: ${raw}`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const itemMatch = /^ {2}-\s+([a-z-]+):\s*(.*)$/.exec(line);
|
|
101
|
+
if (itemMatch) {
|
|
102
|
+
const field = FIELD_ALIASES[itemMatch[1]];
|
|
103
|
+
if (!field) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
code: CODES.FORMAT_INVALID,
|
|
107
|
+
message: `${FILE_NAME} 第 ${index + 1} 行存在未知字段: ${itemMatch[1]}`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
current = {};
|
|
111
|
+
current[field] = stripQuotes(itemMatch[2]);
|
|
112
|
+
activeChanges.push(current);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const fieldMatch = /^ {4}([a-z-]+):\s*(.*)$/.exec(line);
|
|
117
|
+
if (fieldMatch && current) {
|
|
118
|
+
const field = FIELD_ALIASES[fieldMatch[1]];
|
|
119
|
+
if (!field) {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
code: CODES.FORMAT_INVALID,
|
|
123
|
+
message: `${FILE_NAME} 第 ${index + 1} 行存在未知字段: ${fieldMatch[1]}`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
current[field] = stripQuotes(fieldMatch[2]);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
code: CODES.FORMAT_INVALID,
|
|
133
|
+
message: `${FILE_NAME} 第 ${index + 1} 行无法解析: ${raw}`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (version == null) {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
code: CODES.FORMAT_INVALID,
|
|
141
|
+
message: `${FILE_NAME} 缺少 version`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (version !== 1) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
code: CODES.FORMAT_INVALID,
|
|
148
|
+
message: `不支持的 ${FILE_NAME} version: ${version}`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const entry of activeChanges) {
|
|
153
|
+
if (!entry.changeKey) {
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
code: CODES.FORMAT_INVALID,
|
|
157
|
+
message: `${FILE_NAME} 存在缺少 change-key 的条目`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const validated = changeKey.validate(entry.changeKey);
|
|
161
|
+
if (!validated.ok) {
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
code: CODES.CHANGE_KEY_INVALID,
|
|
165
|
+
message: `${FILE_NAME} change-key 不合规: ${validated.message}`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
entry.changeKey = validated.normalized;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { ok: true, version, activeChanges };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function configPath(specRoot) {
|
|
175
|
+
return path.join(specRoot, FILE_NAME);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function loadSddConfig(specRoot) {
|
|
179
|
+
const filePath = configPath(specRoot);
|
|
180
|
+
if (!fs.existsSync(filePath)) {
|
|
181
|
+
return {
|
|
182
|
+
ok: false,
|
|
183
|
+
code: CODES.MISSING,
|
|
184
|
+
message: `${FILE_NAME} 不存在: ${filePath}`,
|
|
185
|
+
path: filePath,
|
|
186
|
+
activeChanges: [],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const parsed = parseSddConfigYaml(fs.readFileSync(filePath, 'utf8'));
|
|
190
|
+
return { ...parsed, path: filePath, activeChanges: parsed.activeChanges || [] };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function serializeSddConfig(activeChanges) {
|
|
194
|
+
const header = [
|
|
195
|
+
'# 活动变更登记表(spec 仓库根目录)',
|
|
196
|
+
'# 由 opsx-propose 隐式写入、opsx-archive 隐式移除,供代码仓 AI 与 commit-msg Hook 读取。',
|
|
197
|
+
'# 不登记代码仓清单、路径 glob 或 CI 配置。',
|
|
198
|
+
'version: 1',
|
|
199
|
+
'',
|
|
200
|
+
'active_changes:',
|
|
201
|
+
];
|
|
202
|
+
if (!activeChanges || activeChanges.length === 0) {
|
|
203
|
+
return `${header.join('\n')}\n`;
|
|
204
|
+
}
|
|
205
|
+
const body = [];
|
|
206
|
+
for (const entry of activeChanges) {
|
|
207
|
+
body.push(` - change-key: ${entry.changeKey}`);
|
|
208
|
+
if (entry.changeId) body.push(` change-id: ${entry.changeId}`);
|
|
209
|
+
if (entry.title) body.push(` title: ${quoteIfNeeded(entry.title)}`);
|
|
210
|
+
if (entry.module) body.push(` module: ${entry.module}`);
|
|
211
|
+
if (entry.summary) body.push(` summary: ${quoteIfNeeded(entry.summary)}`);
|
|
212
|
+
}
|
|
213
|
+
return `${header.concat(body).join('\n')}\n`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function writeSddConfig(specRoot, activeChanges) {
|
|
217
|
+
const filePath = configPath(specRoot);
|
|
218
|
+
fs.writeFileSync(filePath, serializeSddConfig(activeChanges), 'utf8');
|
|
219
|
+
return { ok: true, path: filePath, count: activeChanges.length };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Upsert one active change (idempotent by change-key).
|
|
224
|
+
*/
|
|
225
|
+
function registerActiveChange(specRoot, entry) {
|
|
226
|
+
const validated = changeKey.validate(entry && entry.changeKey);
|
|
227
|
+
if (!validated.ok) {
|
|
228
|
+
return { ok: false, code: CODES.CHANGE_KEY_INVALID, message: validated.message };
|
|
229
|
+
}
|
|
230
|
+
const key = validated.normalized;
|
|
231
|
+
|
|
232
|
+
const existing = loadSddConfig(specRoot);
|
|
233
|
+
if (!existing.ok && existing.code !== CODES.MISSING) {
|
|
234
|
+
return existing;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const next = {
|
|
238
|
+
changeKey: key,
|
|
239
|
+
changeId: entry.changeId || changeKey.toChangeId(key),
|
|
240
|
+
title: entry.title || '',
|
|
241
|
+
module: entry.module || changeKey.parse(key).module,
|
|
242
|
+
summary: entry.summary || '',
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const list = existing.activeChanges.slice();
|
|
246
|
+
const at = list.findIndex((item) => item.changeKey === key);
|
|
247
|
+
if (at >= 0) {
|
|
248
|
+
list[at] = { ...list[at], ...next };
|
|
249
|
+
} else {
|
|
250
|
+
list.push(next);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const written = writeSddConfig(specRoot, list);
|
|
254
|
+
return {
|
|
255
|
+
ok: true,
|
|
256
|
+
path: written.path,
|
|
257
|
+
registered: next,
|
|
258
|
+
activeChanges: list,
|
|
259
|
+
updated: at >= 0,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function removeActiveChange(specRoot, keyInput) {
|
|
264
|
+
const key = String(keyInput || '').trim().toLowerCase();
|
|
265
|
+
const existing = loadSddConfig(specRoot);
|
|
266
|
+
if (!existing.ok && existing.code !== CODES.MISSING) {
|
|
267
|
+
return existing;
|
|
268
|
+
}
|
|
269
|
+
const list = existing.activeChanges.filter((item) => item.changeKey !== key);
|
|
270
|
+
const removed = list.length !== existing.activeChanges.length;
|
|
271
|
+
if (removed) {
|
|
272
|
+
writeSddConfig(specRoot, list);
|
|
273
|
+
}
|
|
274
|
+
return { ok: true, removed, activeChanges: list, path: configPath(specRoot) };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Ordered change-keys for Spec-Change trailers (may be multiple).
|
|
279
|
+
*/
|
|
280
|
+
function activeChangeKeys(specRoot) {
|
|
281
|
+
const config = loadSddConfig(specRoot);
|
|
282
|
+
if (!config.ok) return [];
|
|
283
|
+
return config.activeChanges.map((entry) => entry.changeKey);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
module.exports = {
|
|
287
|
+
FILE_NAME,
|
|
288
|
+
CODES,
|
|
289
|
+
parseSddConfigYaml,
|
|
290
|
+
serializeSddConfig,
|
|
291
|
+
loadSddConfig,
|
|
292
|
+
writeSddConfig,
|
|
293
|
+
registerActiveChange,
|
|
294
|
+
removeActiveChange,
|
|
295
|
+
activeChangeKeys,
|
|
296
|
+
configPath,
|
|
297
|
+
};
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Change Key grammar for multi-repo association.
|
|
5
|
+
* Authority: 本体架构设计方案/10-最小化多仓库关联与变更命名方案.md §2.1 / §2.2
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const CHANGE_KEY_REGEX = /^([a-z][a-z0-9]{1,7})-([0-9]{6})-([a-z0-9]+(?:-[a-z0-9]+)*)$/;
|
|
9
|
+
const MODULE_CODE_REGEX = /^[a-z][a-z0-9]{1,7}$/;
|
|
10
|
+
const SLUG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
11
|
+
const SLUG_MAX_LEN = 48;
|
|
12
|
+
const RESERVED_CROSS = 'cross';
|
|
13
|
+
|
|
14
|
+
const CODES = {
|
|
15
|
+
FORMAT_INVALID: 'CHANGE_KEY_FORMAT_INVALID',
|
|
16
|
+
DATE_INVALID: 'CHANGE_KEY_DATE_INVALID',
|
|
17
|
+
MODULE_INVALID: 'CHANGE_KEY_MODULE_INVALID',
|
|
18
|
+
SLUG_INVALID: 'CHANGE_KEY_SLUG_INVALID',
|
|
19
|
+
CONFLICT: 'CHANGE_KEY_CONFLICT',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function pad2(value) {
|
|
23
|
+
return String(value).padStart(2, '0');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function formatYymmdd(date = new Date()) {
|
|
27
|
+
const year = date.getFullYear() % 100;
|
|
28
|
+
const month = date.getMonth() + 1;
|
|
29
|
+
const day = date.getDate();
|
|
30
|
+
return `${pad2(year)}${pad2(month)}${pad2(day)}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isValidCalendarYymmdd(yymmdd) {
|
|
34
|
+
if (!/^[0-9]{6}$/.test(String(yymmdd || ''))) return false;
|
|
35
|
+
const year = 2000 + Number(yymmdd.slice(0, 2));
|
|
36
|
+
const month = Number(yymmdd.slice(2, 4));
|
|
37
|
+
const day = Number(yymmdd.slice(4, 6));
|
|
38
|
+
if (month < 1 || month > 12 || day < 1 || day > 31) return false;
|
|
39
|
+
const dt = new Date(year, month - 1, day);
|
|
40
|
+
return (
|
|
41
|
+
dt.getFullYear() === year &&
|
|
42
|
+
dt.getMonth() === month - 1 &&
|
|
43
|
+
dt.getDate() === day
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeSlug(raw) {
|
|
48
|
+
if (raw == null) return '';
|
|
49
|
+
let slug = String(raw)
|
|
50
|
+
.trim()
|
|
51
|
+
.toLowerCase()
|
|
52
|
+
.replace(/[_\s.]+/g, '-')
|
|
53
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
54
|
+
.replace(/-+/g, '-')
|
|
55
|
+
.replace(/^-+|-+$/g, '');
|
|
56
|
+
if (slug.length > SLUG_MAX_LEN) {
|
|
57
|
+
slug = slug.slice(0, SLUG_MAX_LEN).replace(/-+$/g, '');
|
|
58
|
+
}
|
|
59
|
+
return slug;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeModule(raw) {
|
|
63
|
+
if (raw == null) return '';
|
|
64
|
+
return String(raw).trim().toLowerCase();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parse(changeKey) {
|
|
68
|
+
const key = String(changeKey || '').trim().toLowerCase();
|
|
69
|
+
const match = CHANGE_KEY_REGEX.exec(key);
|
|
70
|
+
if (!match) return null;
|
|
71
|
+
return {
|
|
72
|
+
changeKey: key,
|
|
73
|
+
module: match[1],
|
|
74
|
+
yymmdd: match[2],
|
|
75
|
+
slug: match[3],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function validate(changeKey, options = {}) {
|
|
80
|
+
const key = String(changeKey || '').trim();
|
|
81
|
+
if (!key) {
|
|
82
|
+
return { ok: false, code: CODES.FORMAT_INVALID, message: 'change-key 为空' };
|
|
83
|
+
}
|
|
84
|
+
if (key !== key.toLowerCase()) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
code: CODES.FORMAT_INVALID,
|
|
88
|
+
message: `change-key 必须全小写: ${changeKey}`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (/[_\s.]/.test(key) || key.includes('--')) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
code: CODES.FORMAT_INVALID,
|
|
95
|
+
message: `change-key 禁止下划线、空格、点号或连续连字符: ${changeKey}`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (!CHANGE_KEY_REGEX.test(key)) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
code: CODES.FORMAT_INVALID,
|
|
102
|
+
message: `change-key 格式不合规: ${changeKey}`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const parts = parse(key);
|
|
106
|
+
if (!parts) {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
code: CODES.FORMAT_INVALID,
|
|
110
|
+
message: `change-key 无法解析: ${changeKey}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (!isValidCalendarYymmdd(parts.yymmdd)) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
code: CODES.DATE_INVALID,
|
|
117
|
+
message: `change-key 日期不是真实日历日: ${parts.yymmdd}`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (parts.slug.length > SLUG_MAX_LEN) {
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
code: CODES.SLUG_INVALID,
|
|
124
|
+
message: `slug 超过 ${SLUG_MAX_LEN} 字符: ${parts.slug}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const registeredModules = options.registeredModules;
|
|
128
|
+
if (registeredModules && typeof registeredModules.has === 'function') {
|
|
129
|
+
if (!registeredModules.has(parts.module)) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
code: CODES.MODULE_INVALID,
|
|
133
|
+
message: `模块代号未注册: ${parts.module}`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
} else if (Array.isArray(registeredModules)) {
|
|
137
|
+
if (!registeredModules.includes(parts.module)) {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
code: CODES.MODULE_INVALID,
|
|
141
|
+
message: `模块代号未注册: ${parts.module}`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { ok: true, code: null, message: null, normalized: key, parts };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function toChangeId(changeKey) {
|
|
149
|
+
const result = validate(changeKey);
|
|
150
|
+
if (!result.ok) {
|
|
151
|
+
throw new Error(result.message);
|
|
152
|
+
}
|
|
153
|
+
return `CHG-${result.normalized.toUpperCase()}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildChangeKey(moduleCode, slug, options = {}) {
|
|
157
|
+
const module = normalizeModule(moduleCode);
|
|
158
|
+
const normalizedSlug = normalizeSlug(slug);
|
|
159
|
+
const yymmdd = options.yymmdd || formatYymmdd(options.date || new Date());
|
|
160
|
+
|
|
161
|
+
if (!MODULE_CODE_REGEX.test(module)) {
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
code: CODES.MODULE_INVALID,
|
|
165
|
+
message: `模块代号不合规: ${moduleCode}`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (!SLUG_REGEX.test(normalizedSlug)) {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
code: CODES.SLUG_INVALID,
|
|
172
|
+
message: `slug 不合规: ${slug} → ${normalizedSlug}`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (!isValidCalendarYymmdd(yymmdd)) {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
code: CODES.DATE_INVALID,
|
|
179
|
+
message: `日期不是真实日历日: ${yymmdd}`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let candidate = `${module}-${yymmdd}-${normalizedSlug}`;
|
|
184
|
+
const existing = options.existingKeys || new Set();
|
|
185
|
+
const existingSet = existing instanceof Set
|
|
186
|
+
? existing
|
|
187
|
+
: new Set(Array.from(existing || []).map((item) => String(item).toLowerCase()));
|
|
188
|
+
|
|
189
|
+
if (!existingSet.has(candidate)) {
|
|
190
|
+
const checked = validate(candidate, options);
|
|
191
|
+
if (!checked.ok) return checked;
|
|
192
|
+
return {
|
|
193
|
+
ok: true,
|
|
194
|
+
changeKey: candidate,
|
|
195
|
+
changeId: toChangeId(candidate),
|
|
196
|
+
module,
|
|
197
|
+
yymmdd,
|
|
198
|
+
slug: normalizedSlug,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (let suffix = 2; suffix <= 99; suffix += 1) {
|
|
203
|
+
candidate = `${module}-${yymmdd}-${normalizedSlug}-${suffix}`;
|
|
204
|
+
if (existingSet.has(candidate)) continue;
|
|
205
|
+
const checked = validate(candidate, options);
|
|
206
|
+
if (!checked.ok) return checked;
|
|
207
|
+
return {
|
|
208
|
+
ok: true,
|
|
209
|
+
changeKey: candidate,
|
|
210
|
+
changeId: toChangeId(candidate),
|
|
211
|
+
module,
|
|
212
|
+
yymmdd,
|
|
213
|
+
slug: `${normalizedSlug}-${suffix}`,
|
|
214
|
+
collided: true,
|
|
215
|
+
suffix,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
code: CODES.CONFLICT,
|
|
222
|
+
message: `无法为 ${module}-${yymmdd}-${normalizedSlug} 分配可用序号`,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = {
|
|
227
|
+
CHANGE_KEY_REGEX,
|
|
228
|
+
MODULE_CODE_REGEX,
|
|
229
|
+
SLUG_REGEX,
|
|
230
|
+
SLUG_MAX_LEN,
|
|
231
|
+
RESERVED_CROSS,
|
|
232
|
+
CODES,
|
|
233
|
+
formatYymmdd,
|
|
234
|
+
isValidCalendarYymmdd,
|
|
235
|
+
normalizeSlug,
|
|
236
|
+
normalizeModule,
|
|
237
|
+
parse,
|
|
238
|
+
validate,
|
|
239
|
+
toChangeId,
|
|
240
|
+
buildChangeKey,
|
|
241
|
+
};
|
|
@@ -11,6 +11,12 @@ const {
|
|
|
11
11
|
const { observeChangeArtifacts } = require('./artifact-observer.cjs');
|
|
12
12
|
const { allocateIdentity } = require('./id.cjs');
|
|
13
13
|
const externalKey = require('./external-key.cjs');
|
|
14
|
+
const changeKey = require('./change-key.cjs');
|
|
15
|
+
const modules = require('./modules.cjs');
|
|
16
|
+
const namingDiagnose = require('./naming-diagnose.cjs');
|
|
17
|
+
const listChanges = require('./list-changes.cjs');
|
|
18
|
+
const sddConfig = require('./sdd-config.cjs');
|
|
19
|
+
const activeChanges = require('./active-changes.cjs');
|
|
14
20
|
|
|
15
21
|
function parseArgs(argv) {
|
|
16
22
|
const result = { _: [] };
|
|
@@ -55,6 +61,15 @@ function showHelp() {
|
|
|
55
61
|
node skywalk-sdd/ontology/cli.cjs identity --delta-state=modified --entity-id=<uuid> --predecessor-version=<uuid>
|
|
56
62
|
node skywalk-sdd/ontology/cli.cjs identity --delta-state=unchanged --entity-id=<uuid> --version-id=<uuid>
|
|
57
63
|
node skywalk-sdd/ontology/cli.cjs external-key --validate <id> --type <requirement|feature|scenario>
|
|
64
|
+
node skywalk-sdd/ontology/cli.cjs change-key --validate <key>
|
|
65
|
+
node skywalk-sdd/ontology/cli.cjs change-key --generate --module=<code> --slug=<slug> [--date=yymmdd]
|
|
66
|
+
node skywalk-sdd/ontology/cli.cjs modules --validate [--project=.]
|
|
67
|
+
node skywalk-sdd/ontology/cli.cjs diagnose-naming [--project=.] [--change=<key>] [--mode=spec-repo|code-repo]
|
|
68
|
+
node skywalk-sdd/ontology/cli.cjs list-changes [--project=.] [--json]
|
|
69
|
+
node skywalk-sdd/ontology/cli.cjs link-spec --path=<spec-clone> [--project=.]
|
|
70
|
+
node skywalk-sdd/ontology/cli.cjs active-change --register --change=<key> [--title=] [--summary=] [--module=]
|
|
71
|
+
node skywalk-sdd/ontology/cli.cjs active-change --remove --change=<key>
|
|
72
|
+
node skywalk-sdd/ontology/cli.cjs active-change --list [--json]
|
|
58
73
|
node skywalk-sdd/ontology/cli.cjs reconcile --project=. --change=<name> [--profile=...]
|
|
59
74
|
node skywalk-sdd/ontology/cli.cjs check --project=. --change=<name> [--profile=...]
|
|
60
75
|
node skywalk-sdd/ontology/cli.cjs status --project=. --change=<name>
|
|
@@ -92,6 +107,126 @@ function main(argv = process.argv.slice(2)) {
|
|
|
92
107
|
if (!result.ok) process.exitCode = 1;
|
|
93
108
|
return;
|
|
94
109
|
}
|
|
110
|
+
if (command === 'change-key') {
|
|
111
|
+
if (args.generate || args._[1] === 'generate') {
|
|
112
|
+
const projectRoot = path.resolve(args.project || '.');
|
|
113
|
+
const registry = modules.loadModulesYaml(projectRoot);
|
|
114
|
+
const existing = listChanges.listActiveChanges(projectRoot);
|
|
115
|
+
const built = changeKey.buildChangeKey(args.module, args.slug, {
|
|
116
|
+
yymmdd: args.date || args.yymmdd,
|
|
117
|
+
existingKeys: existing,
|
|
118
|
+
registeredModules: registry.ok ? registry.codes : null,
|
|
119
|
+
});
|
|
120
|
+
console.log(JSON.stringify(built, null, 2));
|
|
121
|
+
if (!built.ok) process.exitCode = 1;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const key = args.validate || args._[1];
|
|
125
|
+
if (!key) throw new Error('用法: change-key --validate <key> | --generate --module= --slug=');
|
|
126
|
+
const projectRoot = path.resolve(args.project || '.');
|
|
127
|
+
const registry = modules.loadModulesYaml(projectRoot);
|
|
128
|
+
const result = changeKey.validate(key, {
|
|
129
|
+
registeredModules: registry.ok ? registry.codes : null,
|
|
130
|
+
});
|
|
131
|
+
console.log(JSON.stringify({
|
|
132
|
+
ok: result.ok,
|
|
133
|
+
code: result.code,
|
|
134
|
+
message: result.message,
|
|
135
|
+
normalized: result.normalized || null,
|
|
136
|
+
changeId: result.ok ? changeKey.toChangeId(result.normalized) : null,
|
|
137
|
+
parts: result.parts || null,
|
|
138
|
+
}, null, 2));
|
|
139
|
+
if (!result.ok) process.exitCode = 1;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (command === 'modules') {
|
|
143
|
+
const projectRoot = path.resolve(args.project || '.');
|
|
144
|
+
const result = modules.loadModulesYaml(projectRoot);
|
|
145
|
+
console.log(JSON.stringify({
|
|
146
|
+
ok: result.ok,
|
|
147
|
+
code: result.code || null,
|
|
148
|
+
message: result.message || null,
|
|
149
|
+
path: result.path,
|
|
150
|
+
version: result.version || null,
|
|
151
|
+
modules: result.modules || null,
|
|
152
|
+
}, null, 2));
|
|
153
|
+
if (!result.ok) process.exitCode = 1;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (command === 'diagnose-naming') {
|
|
157
|
+
const projectRoot = path.resolve(args.project || '.');
|
|
158
|
+
const result = namingDiagnose.diagnose(projectRoot, {
|
|
159
|
+
change: args.change,
|
|
160
|
+
mode: args.mode,
|
|
161
|
+
});
|
|
162
|
+
if (args.json) {
|
|
163
|
+
console.log(JSON.stringify(result, null, 2));
|
|
164
|
+
} else {
|
|
165
|
+
console.log(namingDiagnose.formatReport(result));
|
|
166
|
+
console.log('');
|
|
167
|
+
console.log(JSON.stringify(result.summary, null, 2));
|
|
168
|
+
}
|
|
169
|
+
if (result.summary.overall === 'FAIL') process.exitCode = 1;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (command === 'list-changes') {
|
|
173
|
+
const projectRoot = path.resolve(args.project || '.');
|
|
174
|
+
const result = listChanges.groupChanges(projectRoot);
|
|
175
|
+
if (args.json) {
|
|
176
|
+
console.log(JSON.stringify(result, null, 2));
|
|
177
|
+
} else {
|
|
178
|
+
console.log(listChanges.formatGroupedText(result) || '(无活动 change)');
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (command === 'link-spec') {
|
|
183
|
+
const projectRoot = path.resolve(args.project || '.');
|
|
184
|
+
const specPath = args.path || args._[1];
|
|
185
|
+
if (!specPath) throw new Error('用法: link-spec --path=<spec-clone> [--project=.]');
|
|
186
|
+
const result = sddConfig.setSpecPath(projectRoot, specPath);
|
|
187
|
+
console.log(JSON.stringify(result, null, 2));
|
|
188
|
+
if (!result.ok) process.exitCode = 1;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (command === 'active-change') {
|
|
192
|
+
const specRoot = path.resolve(args.project || '.');
|
|
193
|
+
if (args.list || args._[1] === 'list') {
|
|
194
|
+
const config = activeChanges.loadSddConfig(specRoot);
|
|
195
|
+
if (args.json) {
|
|
196
|
+
console.log(JSON.stringify(config, null, 2));
|
|
197
|
+
} else if (!config.ok) {
|
|
198
|
+
console.log(config.message);
|
|
199
|
+
} else if (config.activeChanges.length === 0) {
|
|
200
|
+
console.log('(无活动 change)');
|
|
201
|
+
} else {
|
|
202
|
+
for (const entry of config.activeChanges) {
|
|
203
|
+
console.log(`● ${entry.title || '(无标题)'}`);
|
|
204
|
+
console.log(` ${entry.changeKey}${entry.module ? ` [${entry.module}]` : ''}`);
|
|
205
|
+
if (entry.summary) console.log(` ${entry.summary}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (!config.ok && config.code !== activeChanges.CODES.MISSING) process.exitCode = 1;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const key = args.change || args._[1];
|
|
212
|
+
if (!key) throw new Error('用法: active-change --register|--remove --change=<key> [--project=<spec>]');
|
|
213
|
+
if (args.remove) {
|
|
214
|
+
const result = activeChanges.removeActiveChange(specRoot, key);
|
|
215
|
+
console.log(JSON.stringify(result, null, 2));
|
|
216
|
+
if (!result.ok) process.exitCode = 1;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const result = activeChanges.registerActiveChange(specRoot, {
|
|
220
|
+
changeKey: key,
|
|
221
|
+
changeId: args['change-id'] || args.changeId,
|
|
222
|
+
title: args.title,
|
|
223
|
+
module: args.module,
|
|
224
|
+
summary: args.summary,
|
|
225
|
+
});
|
|
226
|
+
console.log(JSON.stringify(result, null, 2));
|
|
227
|
+
if (!result.ok) process.exitCode = 1;
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
95
230
|
const projectRoot = path.resolve(args.project || '.');
|
|
96
231
|
const changeName = args.change;
|
|
97
232
|
if (!changeName) throw new Error('缺少 --change 参数');
|