kld-sdd 2.6.6 → 2.6.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/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 +12 -1
- package/skywalk-sdd/ontology/active-changes.cjs +297 -0
- package/skywalk-sdd/ontology/archive-package.cjs +95 -6
- package/skywalk-sdd/ontology/artifact-parser.cjs +50 -7
- package/skywalk-sdd/ontology/change-key.cjs +241 -0
- package/skywalk-sdd/ontology/cli.cjs +154 -0
- package/skywalk-sdd/ontology/external-key.cjs +159 -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/normalizer.cjs +4 -0
- package/skywalk-sdd/ontology/schema.cjs +5 -0
- package/skywalk-sdd/ontology/sdd-config.cjs +335 -0
- package/skywalk-sdd/ontology/traceability-validator.cjs +175 -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 +12 -3
- package/templates/openspec/spec.md +3 -3
- package/templates/sdd.config.yaml +12 -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 +10 -0
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +47 -3
- package/templates/skills/kld-sdd/opsx-explore/SKILL.md +37 -17
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +13 -14
- package/templates/skills/kld-sdd/opsx-kb-ingest/reference.md +13 -0
- package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +26 -15
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +61 -23
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +8 -2
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +1 -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
|
+
};
|
|
@@ -101,26 +101,107 @@ function requiredText(value, field, owner) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
function normalizeExternalRefs(refs) {
|
|
104
|
+
const { normalize } = require('./external-key.cjs');
|
|
104
105
|
if (!Array.isArray(refs)) return [];
|
|
105
106
|
const seen = new Set();
|
|
106
107
|
const result = [];
|
|
107
108
|
for (const ref of refs) {
|
|
108
109
|
const system = String(ref.system || ref.system_name || '').trim();
|
|
109
110
|
const objectType = String(ref.object_type || ref.objectType || '').trim().toLowerCase();
|
|
110
|
-
const
|
|
111
|
-
if (!system || !objectType || !
|
|
111
|
+
const rawExternalId = String(ref.external_id || ref.externalId || '').trim();
|
|
112
|
+
if (!system || !objectType || !rawExternalId) continue;
|
|
113
|
+
const externalId = normalize(rawExternalId, objectType) || rawExternalId;
|
|
112
114
|
const key = `${system}\0${objectType}\0${externalId}`;
|
|
113
115
|
if (seen.has(key)) continue;
|
|
114
116
|
seen.add(key);
|
|
115
|
-
|
|
117
|
+
const entry = {
|
|
116
118
|
system,
|
|
117
119
|
object_type: objectType,
|
|
118
120
|
external_id: externalId,
|
|
119
|
-
}
|
|
121
|
+
};
|
|
122
|
+
const rawFeatureId = ref.feature_id || ref.featureId;
|
|
123
|
+
if (rawFeatureId && objectType === 'requirement') {
|
|
124
|
+
entry.feature_id = normalize(String(rawFeatureId), 'feature') || String(rawFeatureId).trim();
|
|
125
|
+
}
|
|
126
|
+
result.push(entry);
|
|
120
127
|
}
|
|
121
128
|
return result;
|
|
122
129
|
}
|
|
123
130
|
|
|
131
|
+
function expandRequirementRefsWithFeatures(requirementRefs) {
|
|
132
|
+
const expanded = [];
|
|
133
|
+
for (const ref of requirementRefs) {
|
|
134
|
+
expanded.push(ref);
|
|
135
|
+
if (ref.object_type === 'requirement' && ref.feature_id) {
|
|
136
|
+
expanded.push({
|
|
137
|
+
system: ref.system,
|
|
138
|
+
object_type: 'feature',
|
|
139
|
+
external_id: ref.feature_id,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return normalizeExternalRefs(expanded);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function assertExternalKeyFinalGate(entities) {
|
|
147
|
+
const {
|
|
148
|
+
validate,
|
|
149
|
+
normalize,
|
|
150
|
+
parseScenario,
|
|
151
|
+
CODES,
|
|
152
|
+
} = require('./external-key.cjs');
|
|
153
|
+
const packageRequirementIds = new Set();
|
|
154
|
+
for (const entity of entities) {
|
|
155
|
+
for (const ref of entity.external_refs || []) {
|
|
156
|
+
if (ref.object_type === 'requirement') {
|
|
157
|
+
packageRequirementIds.add(normalize(ref.external_id, 'requirement'));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const entity of entities) {
|
|
162
|
+
const declaredFeatures = new Set();
|
|
163
|
+
const featureBindings = new Set();
|
|
164
|
+
for (const ref of entity.external_refs || []) {
|
|
165
|
+
const format = validate(ref.external_id, ref.object_type);
|
|
166
|
+
if (!format.ok) {
|
|
167
|
+
throw new Error(`${CODES.FORMAT_INVALID}: ${format.message} @ ${entity.anchor_id}`);
|
|
168
|
+
}
|
|
169
|
+
if (ref.feature_id) {
|
|
170
|
+
const featureFormat = validate(ref.feature_id, 'feature');
|
|
171
|
+
if (!featureFormat.ok) {
|
|
172
|
+
throw new Error(`${CODES.FORMAT_INVALID}: requirement.feature_id 不合规: ${featureFormat.message} @ ${entity.anchor_id}`);
|
|
173
|
+
}
|
|
174
|
+
declaredFeatures.add(normalize(ref.feature_id, 'feature'));
|
|
175
|
+
}
|
|
176
|
+
if (ref.object_type === 'feature') {
|
|
177
|
+
featureBindings.add(normalize(ref.external_id, 'feature'));
|
|
178
|
+
}
|
|
179
|
+
if (ref.object_type === 'scenario') {
|
|
180
|
+
const parts = parseScenario(ref.external_id);
|
|
181
|
+
if (!parts || !packageRequirementIds.has(parts.requirementId)) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`${CODES.SCOPE_MISMATCH}: 场景键 REQ 前缀不在同包 requirement 绑定集合中: ${ref.external_id} @ ${entity.anchor_id}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
for (const featureId of featureBindings) {
|
|
189
|
+
if (!declaredFeatures.has(featureId)) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`${CODES.FEATURE_UNDECLARED}: feature 绑定无法与同实体 requirement.feature_id 配对: ${featureId} @ ${entity.anchor_id}`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
for (const featureId of declaredFeatures) {
|
|
196
|
+
if (!featureBindings.has(featureId)) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`${CODES.FEATURE_UNDECLARED}: requirement 申报了 feature_id 但缺少对应 feature 绑定: ${featureId} @ ${entity.anchor_id}`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
124
205
|
function canonicalEntity(archiveDir, entity, inheritedRequirementRefs = []) {
|
|
125
206
|
const anchorId = requiredText(entity.anchor_id || entity.id, 'anchor_id', '本体实体').toUpperCase();
|
|
126
207
|
const versionId = requiredText(
|
|
@@ -130,8 +211,15 @@ function canonicalEntity(archiveDir, entity, inheritedRequirementRefs = []) {
|
|
|
130
211
|
).toLowerCase();
|
|
131
212
|
const entityType = requiredText(entity.type, 'type', anchorId);
|
|
132
213
|
let externalRefs = normalizeExternalRefs(entity.external_refs || entity.externalRefs || []);
|
|
133
|
-
if (entityType === 'Capability'
|
|
134
|
-
|
|
214
|
+
if (entityType === 'Capability') {
|
|
215
|
+
const requirementSources = [
|
|
216
|
+
...externalRefs.filter((ref) => ref.object_type === 'requirement'),
|
|
217
|
+
...inheritedRequirementRefs,
|
|
218
|
+
];
|
|
219
|
+
externalRefs = normalizeExternalRefs([
|
|
220
|
+
...externalRefs,
|
|
221
|
+
...expandRequirementRefsWithFeatures(requirementSources),
|
|
222
|
+
]);
|
|
135
223
|
}
|
|
136
224
|
return {
|
|
137
225
|
anchor_id: anchorId,
|
|
@@ -237,6 +325,7 @@ function buildCanonicalFacts(archiveDir, snapshot, projectId, archiveId) {
|
|
|
237
325
|
if (entities.length === 0) {
|
|
238
326
|
throw new Error('archive-ontology.json 没有可导出的实体');
|
|
239
327
|
}
|
|
328
|
+
assertExternalKeyFinalGate(entities);
|
|
240
329
|
const entityByAnchor = new Map(entities.map((entity) => [entity.anchor_id, entity]));
|
|
241
330
|
const warningCounts = new Map();
|
|
242
331
|
const seenRelations = new Set();
|
|
@@ -201,15 +201,28 @@ function identityFromValues(values = {}) {
|
|
|
201
201
|
|
|
202
202
|
function flushRequirementRef(requirementRefs, currentRef) {
|
|
203
203
|
if (!currentRef) return null;
|
|
204
|
+
const { normalize, validate } = require('./external-key.cjs');
|
|
204
205
|
const system = currentRef.system;
|
|
205
206
|
const objectType = currentRef['object-type'] || currentRef.object_type;
|
|
206
207
|
const externalId = currentRef['external-id'] || currentRef.external_id;
|
|
208
|
+
const featureId = currentRef['feature-id'] || currentRef.feature_id;
|
|
207
209
|
if (system && objectType && externalId) {
|
|
208
|
-
|
|
210
|
+
const type = String(objectType).toLowerCase();
|
|
211
|
+
const entry = {
|
|
209
212
|
system,
|
|
210
|
-
object_type:
|
|
211
|
-
external_id: externalId,
|
|
212
|
-
}
|
|
213
|
+
object_type: type,
|
|
214
|
+
external_id: normalize(externalId, type) || String(externalId).trim(),
|
|
215
|
+
};
|
|
216
|
+
if (featureId) {
|
|
217
|
+
const featureCheck = validate(featureId, 'feature');
|
|
218
|
+
if (!featureCheck.ok) {
|
|
219
|
+
entry.feature_id_invalid = featureCheck.message;
|
|
220
|
+
entry.feature_id_raw = featureId;
|
|
221
|
+
} else {
|
|
222
|
+
entry.feature_id = normalize(featureId, 'feature');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
requirementRefs.push(entry);
|
|
213
226
|
}
|
|
214
227
|
return null;
|
|
215
228
|
}
|
|
@@ -218,8 +231,10 @@ function parseStructuredFrontmatter(lines) {
|
|
|
218
231
|
const flat = {};
|
|
219
232
|
const requirementRefs = [];
|
|
220
233
|
const continuity = {};
|
|
234
|
+
const numberingWaiver = {};
|
|
235
|
+
let numberingGate = false;
|
|
221
236
|
if (!lines.length || lines[0].trim() !== '---') {
|
|
222
|
-
return { flat, requirementRefs, continuity };
|
|
237
|
+
return { flat, requirementRefs, continuity, numberingWaiver, numberingGate };
|
|
223
238
|
}
|
|
224
239
|
let section = null;
|
|
225
240
|
let currentRef = null;
|
|
@@ -238,6 +253,13 @@ function parseStructuredFrontmatter(lines) {
|
|
|
238
253
|
flat[section] = value;
|
|
239
254
|
currentCap = null;
|
|
240
255
|
if (section === 'continuity' && value) continuity.kind = value;
|
|
256
|
+
if (section === 'numbering-waiver') {
|
|
257
|
+
numberingGate = true;
|
|
258
|
+
if (value) numberingWaiver.reason = value;
|
|
259
|
+
}
|
|
260
|
+
if (section === 'requirement-refs') {
|
|
261
|
+
numberingGate = true;
|
|
262
|
+
}
|
|
241
263
|
continue;
|
|
242
264
|
}
|
|
243
265
|
|
|
@@ -253,6 +275,12 @@ function parseStructuredFrontmatter(lines) {
|
|
|
253
275
|
continue;
|
|
254
276
|
}
|
|
255
277
|
|
|
278
|
+
if (section === 'numbering-waiver' && nested) {
|
|
279
|
+
const key = nested[1].toLowerCase().replace(/-/g, '_');
|
|
280
|
+
numberingWaiver[key] = unwrapScalar(stripInlineYamlComment(nested[2]));
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
256
284
|
if (section === 'continuity' && nested) {
|
|
257
285
|
const key = nested[1].toLowerCase().replace(/-/g, '_');
|
|
258
286
|
const value = unwrapScalar(stripInlineYamlComment(nested[2]));
|
|
@@ -280,7 +308,7 @@ function parseStructuredFrontmatter(lines) {
|
|
|
280
308
|
}
|
|
281
309
|
}
|
|
282
310
|
flushRequirementRef(requirementRefs, currentRef);
|
|
283
|
-
return { flat, requirementRefs, continuity };
|
|
311
|
+
return { flat, requirementRefs, continuity, numberingWaiver, numberingGate };
|
|
284
312
|
}
|
|
285
313
|
|
|
286
314
|
function parseIdentityBlock(lines, startIndex, endIndex = lines.length) {
|
|
@@ -324,6 +352,8 @@ function parseProposal(target, file, lines, frontmatter) {
|
|
|
324
352
|
target.proposalMode = String(mergedFrontmatter.mode || '').trim().toLowerCase();
|
|
325
353
|
target.requirementRefs = structured.requirementRefs;
|
|
326
354
|
target.continuity = structured.continuity;
|
|
355
|
+
target.numberingWaiver = structured.numberingWaiver || {};
|
|
356
|
+
target.numberingGate = Boolean(structured.numberingGate);
|
|
327
357
|
const changeId = String(mergedFrontmatter['change-id'] || '').trim().toUpperCase();
|
|
328
358
|
if (changeId) {
|
|
329
359
|
addEntity(
|
|
@@ -355,9 +385,20 @@ function parseProposal(target, file, lines, frontmatter) {
|
|
|
355
385
|
const capabilityId = match[1].toUpperCase();
|
|
356
386
|
const identity = parseIdentityBlock(lines, index + 1);
|
|
357
387
|
if (structured.requirementRefs.length > 0) {
|
|
388
|
+
const expanded = [];
|
|
389
|
+
for (const ref of structured.requirementRefs) {
|
|
390
|
+
expanded.push(ref);
|
|
391
|
+
if (ref.feature_id) {
|
|
392
|
+
expanded.push({
|
|
393
|
+
system: ref.system,
|
|
394
|
+
object_type: 'feature',
|
|
395
|
+
external_id: ref.feature_id,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
358
399
|
identity.external_refs = [
|
|
359
400
|
...(identity.external_refs || []),
|
|
360
|
-
...
|
|
401
|
+
...expanded,
|
|
361
402
|
];
|
|
362
403
|
}
|
|
363
404
|
const slug = unwrapScalar(match[2]);
|
|
@@ -683,6 +724,8 @@ function parseChangeArtifacts(projectRoot, changeName, options = {}) {
|
|
|
683
724
|
profile: 'simple',
|
|
684
725
|
proposalMode: '',
|
|
685
726
|
requirementRefs: [],
|
|
727
|
+
numberingWaiver: {},
|
|
728
|
+
numberingGate: false,
|
|
686
729
|
continuity: {},
|
|
687
730
|
artifacts: [],
|
|
688
731
|
files: files.map((file) => ({ path: file.relativePath, content_hash: file.contentHash })),
|