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.
Files changed (37) hide show
  1. package/bin/kld-sdd-init.js +39 -3
  2. package/lib/init.js +320 -45
  3. package/lib/workspace-layout.js +2 -0
  4. package/package.json +2 -2
  5. package/skywalk-sdd/context-client.cjs +12 -1
  6. package/skywalk-sdd/ontology/active-changes.cjs +297 -0
  7. package/skywalk-sdd/ontology/archive-package.cjs +95 -6
  8. package/skywalk-sdd/ontology/artifact-parser.cjs +50 -7
  9. package/skywalk-sdd/ontology/change-key.cjs +241 -0
  10. package/skywalk-sdd/ontology/cli.cjs +154 -0
  11. package/skywalk-sdd/ontology/external-key.cjs +159 -0
  12. package/skywalk-sdd/ontology/list-changes.cjs +110 -0
  13. package/skywalk-sdd/ontology/modules.cjs +167 -0
  14. package/skywalk-sdd/ontology/naming-diagnose.cjs +594 -0
  15. package/skywalk-sdd/ontology/normalizer.cjs +4 -0
  16. package/skywalk-sdd/ontology/schema.cjs +5 -0
  17. package/skywalk-sdd/ontology/sdd-config.cjs +335 -0
  18. package/skywalk-sdd/ontology/traceability-validator.cjs +175 -0
  19. package/skywalk-sdd/ontology/workspace-layout.cjs +194 -0
  20. package/templates/dot-sdd.yaml +8 -0
  21. package/templates/git-hooks/commit-msg-sdd-trailer.cjs +224 -0
  22. package/templates/modules.yaml +13 -0
  23. package/templates/openspec/proposal.md +12 -3
  24. package/templates/openspec/spec.md +3 -3
  25. package/templates/sdd.config.yaml +12 -0
  26. package/templates/skills/kld-sdd/opsx-apply/SKILL.md +3 -3
  27. package/templates/skills/kld-sdd/opsx-apply/checklist.md +1 -1
  28. package/templates/skills/kld-sdd/opsx-archive/SKILL.md +10 -0
  29. package/templates/skills/kld-sdd/opsx-check/SKILL.md +47 -3
  30. package/templates/skills/kld-sdd/opsx-explore/SKILL.md +37 -17
  31. package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +13 -14
  32. package/templates/skills/kld-sdd/opsx-kb-ingest/reference.md +13 -0
  33. package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +26 -15
  34. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +61 -23
  35. package/templates/skills/kld-sdd/opsx-spec/SKILL.md +8 -2
  36. package/templates/skills/kld-sdd/opsx-task/SKILL.md +1 -0
  37. 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 externalId = String(ref.external_id || ref.externalId || '').trim();
111
- if (!system || !objectType || !externalId) continue;
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
- result.push({
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' && inheritedRequirementRefs.length > 0) {
134
- externalRefs = normalizeExternalRefs([...externalRefs, ...inheritedRequirementRefs]);
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
- requirementRefs.push({
210
+ const type = String(objectType).toLowerCase();
211
+ const entry = {
209
212
  system,
210
- object_type: String(objectType).toLowerCase(),
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
- ...structured.requirementRefs,
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 })),