kld-sdd 2.6.6 → 2.6.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kld-sdd",
3
- "version": "2.6.6",
3
+ "version": "2.6.7",
4
4
  "description": "KLD SDD OpenSpec 项目初始化工具 - 一键部署 SDD skills",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  "kld-sdd-init": "bin/kld-sdd-init.js"
9
9
  },
10
10
  "scripts": {
11
- "test": "node test/ontology-release-blockers.cjs && node test/ontology-semantic-core.cjs && node test/ontology-identity-versioning.cjs && node test/ontology-identity-continuity.cjs && node test/ontology-state-transaction.cjs && node test/ontology-process-concurrency.cjs && node test/ontology-observer-convergence.cjs && node test/ontology-working-runtime.cjs && node test/ontology-stage-materialization.cjs && node test/ontology-template-contract.cjs && node test/ontology-cli-archive.cjs && node test/archive-package-producer.cjs && node test/validate-skills-bundle.cjs && node test/tool-profiles.cjs && node test/settings-merge.cjs && node test/command-bridge.cjs && node test/codebuddy-hooks.cjs && node test/skill-content-contract.cjs && node test/init-agent-profiles.cjs"
11
+ "test": "node test/external-key.cjs && node test/ontology-release-blockers.cjs && node test/ontology-semantic-core.cjs && node test/ontology-identity-versioning.cjs && node test/ontology-identity-continuity.cjs && node test/ontology-state-transaction.cjs && node test/ontology-process-concurrency.cjs && node test/ontology-observer-convergence.cjs && node test/ontology-working-runtime.cjs && node test/ontology-stage-materialization.cjs && node test/ontology-template-contract.cjs && node test/ontology-cli-archive.cjs && node test/archive-package-producer.cjs && node test/validate-skills-bundle.cjs && node test/tool-profiles.cjs && node test/settings-merge.cjs && node test/command-bridge.cjs && node test/codebuddy-hooks.cjs && node test/skill-content-contract.cjs && node test/init-agent-profiles.cjs"
12
12
  },
13
13
  "keywords": [
14
14
  "kld",
@@ -132,7 +132,18 @@ function buildPayload(args, mode) {
132
132
  if (args['entity-type']) payload.entityType = args['entity-type'];
133
133
  if (args['external-system']) payload.externalSystem = args['external-system'];
134
134
  if (args['external-object-type']) payload.externalObjectType = args['external-object-type'];
135
- if (args['external-id']) payload.externalId = args['external-id'];
135
+ if (args['external-id']) {
136
+ const externalKey = require('./ontology/external-key.cjs');
137
+ const objectType = args['external-object-type'] || 'requirement';
138
+ const check = externalKey.validate(args['external-id'], objectType);
139
+ if (!check.ok) {
140
+ throw new Error(`${check.code}: ${check.message}`);
141
+ }
142
+ payload.externalId = externalKey.normalize(args['external-id'], objectType);
143
+ if (payload.externalObjectType) {
144
+ payload.externalObjectType = String(payload.externalObjectType).trim().toLowerCase();
145
+ }
146
+ }
136
147
  if (args['canonical-key']) payload.canonicalKey = args['canonical-key'];
137
148
  return payload;
138
149
  }
@@ -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 })),
@@ -10,6 +10,7 @@ const {
10
10
  } = require('./runtime.cjs');
11
11
  const { observeChangeArtifacts } = require('./artifact-observer.cjs');
12
12
  const { allocateIdentity } = require('./id.cjs');
13
+ const externalKey = require('./external-key.cjs');
13
14
 
14
15
  function parseArgs(argv) {
15
16
  const result = { _: [] };
@@ -53,6 +54,7 @@ function showHelp() {
53
54
  node skywalk-sdd/ontology/cli.cjs identity --delta-state=added
54
55
  node skywalk-sdd/ontology/cli.cjs identity --delta-state=modified --entity-id=<uuid> --predecessor-version=<uuid>
55
56
  node skywalk-sdd/ontology/cli.cjs identity --delta-state=unchanged --entity-id=<uuid> --version-id=<uuid>
57
+ node skywalk-sdd/ontology/cli.cjs external-key --validate <id> --type <requirement|feature|scenario>
56
58
  node skywalk-sdd/ontology/cli.cjs reconcile --project=. --change=<name> [--profile=...]
57
59
  node skywalk-sdd/ontology/cli.cjs check --project=. --change=<name> [--profile=...]
58
60
  node skywalk-sdd/ontology/cli.cjs status --project=. --change=<name>
@@ -73,6 +75,23 @@ function main(argv = process.argv.slice(2)) {
73
75
  console.log(JSON.stringify(allocateIdentity(args), null, 2));
74
76
  return;
75
77
  }
78
+ if (command === 'external-key') {
79
+ const id = args.validate || args._[1];
80
+ const type = args.type || args.t;
81
+ if (!id || !type) {
82
+ throw new Error('用法: external-key --validate <id> --type <requirement|feature|scenario>');
83
+ }
84
+ const result = externalKey.validate(id, type);
85
+ console.log(JSON.stringify({
86
+ ok: result.ok,
87
+ code: result.code,
88
+ message: result.message,
89
+ normalized: externalKey.normalize(id, type),
90
+ objectType: String(type).toLowerCase(),
91
+ }, null, 2));
92
+ if (!result.ok) process.exitCode = 1;
93
+ return;
94
+ }
76
95
  const projectRoot = path.resolve(args.project || '.');
77
96
  const changeName = args.change;
78
97
  if (!changeName) throw new Error('缺少 --change 参数');
@@ -0,0 +1,159 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * External key grammar for requirement / feature / scenario.
5
+ * Authority: kb-sdd docs/superpowers/specs/2026-07-24-external-numbering-kb-sdd-design.md §2.
6
+ * Regex and normalization MUST stay byte-for-byte aligned with kb-sdd ExternalKeyFormat.java.
7
+ */
8
+
9
+ const REGEX = {
10
+ requirement: /^REQ-[A-Z][A-Z0-9]{1,7}-[0-9]{4}-[0-9]{3,6}$/,
11
+ feature: /^FEAT-[A-Z][A-Z0-9]{1,7}-[0-9]{3,6}$/,
12
+ scenario: /^REQ-[A-Z][A-Z0-9]{1,7}-[0-9]{4}-[0-9]{3,6}:SCN-[a-z0-9]+(-[a-z0-9]+)*-[0-9]{3}$/,
13
+ };
14
+
15
+ const SCENARIO_PARTS =
16
+ /^(REQ-[A-Z][A-Z0-9]{1,7}-[0-9]{4}-[0-9]{3,6}):SCN-([a-z0-9]+(?:-[a-z0-9]+)*)-([0-9]{3})$/;
17
+
18
+ const SLUG_MAX_LEN = 40;
19
+ const SCENARIO_KEY_MAX_LEN = 120;
20
+ const SCN_SEQ_MAX = 999;
21
+
22
+ const CODES = {
23
+ FORMAT_INVALID: 'EXTERNAL_KEY_FORMAT_INVALID',
24
+ SCOPE_MISMATCH: 'EXTERNAL_KEY_SCOPE_MISMATCH',
25
+ FEATURE_UNDECLARED: 'EXTERNAL_KEY_FEATURE_UNDECLARED',
26
+ SEQ_REUSED: 'EXTERNAL_KEY_SEQ_REUSED',
27
+ WAIVER_ACTIVE: 'NUMBERING_WAIVER_ACTIVE',
28
+ };
29
+
30
+ function normalizeObjectType(objectType) {
31
+ if (objectType == null || String(objectType).trim() === '') return null;
32
+ return String(objectType).trim().toLowerCase();
33
+ }
34
+
35
+ function normalize(id, objectType) {
36
+ if (id == null) return null;
37
+ const trimmed = String(id).trim();
38
+ if (!trimmed) return trimmed;
39
+ const type = normalizeObjectType(objectType);
40
+ if (type === 'scenario') {
41
+ const colon = trimmed.indexOf(':');
42
+ if (colon < 0) return trimmed.toUpperCase();
43
+ const reqPart = trimmed.slice(0, colon).trim().toUpperCase();
44
+ const rest = trimmed.slice(colon + 1).trim();
45
+ if (/^scn-/i.test(rest)) {
46
+ const afterScn = rest.slice(4);
47
+ const lastDash = afterScn.lastIndexOf('-');
48
+ if (lastDash > 0) {
49
+ const slug = afterScn.slice(0, lastDash).toLowerCase();
50
+ const seq = afterScn.slice(lastDash + 1);
51
+ return `${reqPart}:SCN-${slug}-${seq}`;
52
+ }
53
+ }
54
+ return `${reqPart}:${rest.toLowerCase()}`;
55
+ }
56
+ return trimmed.toUpperCase();
57
+ }
58
+
59
+ function validate(id, objectType) {
60
+ const type = normalizeObjectType(objectType);
61
+ if (!type || !REGEX[type]) {
62
+ return {
63
+ ok: false,
64
+ code: CODES.FORMAT_INVALID,
65
+ message: `未知 object_type,无法校验编号: ${objectType}`,
66
+ };
67
+ }
68
+ const normalized = normalize(id, type);
69
+ if (!normalized) {
70
+ return { ok: false, code: CODES.FORMAT_INVALID, message: 'external_id 为空' };
71
+ }
72
+ if (!REGEX[type].test(normalized)) {
73
+ return {
74
+ ok: false,
75
+ code: CODES.FORMAT_INVALID,
76
+ message: `编号格式不合规 (${type}): ${id} → ${normalized}`,
77
+ };
78
+ }
79
+ if (type === 'scenario') {
80
+ const parts = parseScenario(normalized);
81
+ if (!parts) {
82
+ return {
83
+ ok: false,
84
+ code: CODES.FORMAT_INVALID,
85
+ message: `场景键无法解析: ${normalized}`,
86
+ };
87
+ }
88
+ if (parts.slug.length > SLUG_MAX_LEN) {
89
+ return {
90
+ ok: false,
91
+ code: CODES.FORMAT_INVALID,
92
+ message: `场景 SLUG 超过 ${SLUG_MAX_LEN} 字符: ${parts.slug}`,
93
+ };
94
+ }
95
+ if (normalized.length > SCENARIO_KEY_MAX_LEN) {
96
+ return {
97
+ ok: false,
98
+ code: CODES.FORMAT_INVALID,
99
+ message: `场景键总长超过 ${SCENARIO_KEY_MAX_LEN} 字符`,
100
+ };
101
+ }
102
+ }
103
+ return { ok: true, code: null, message: null, normalized };
104
+ }
105
+
106
+ function parseScenario(key) {
107
+ const normalized = normalize(key, 'scenario');
108
+ if (!normalized) return null;
109
+ const match = SCENARIO_PARTS.exec(normalized);
110
+ if (!match) return null;
111
+ return {
112
+ requirementId: match[1],
113
+ slug: match[2],
114
+ seq: Number(match[3]),
115
+ };
116
+ }
117
+
118
+ function composeScenario(reqId, slug, seq) {
119
+ const req = normalize(reqId, 'requirement');
120
+ const normalizedSlug = String(slug || '').trim().toLowerCase();
121
+ const n = Number(seq);
122
+ if (!Number.isInteger(n) || n < 1 || n > SCN_SEQ_MAX) {
123
+ throw new Error(`SCN 序号必须在 1..${SCN_SEQ_MAX} 之间: ${seq}`);
124
+ }
125
+ const composed = `${req}:SCN-${normalizedSlug}-${String(n).padStart(3, '0')}`;
126
+ const result = validate(composed, 'scenario');
127
+ if (!result.ok) throw new Error(result.message);
128
+ return composed;
129
+ }
130
+
131
+ function nextScenarioSeq(reqId, usedKeys) {
132
+ const req = normalize(reqId, 'requirement');
133
+ let max = 0;
134
+ for (const key of usedKeys || []) {
135
+ const parts = parseScenario(key);
136
+ if (!parts) continue;
137
+ if (parts.requirementId === req) {
138
+ max = Math.max(max, parts.seq);
139
+ }
140
+ }
141
+ if (max >= SCN_SEQ_MAX) {
142
+ throw new Error(`SCN 序号已用尽 (999),请回需求管理系统拆分需求: ${req}`);
143
+ }
144
+ return max + 1;
145
+ }
146
+
147
+ module.exports = {
148
+ REGEX,
149
+ CODES,
150
+ SLUG_MAX_LEN,
151
+ SCENARIO_KEY_MAX_LEN,
152
+ SCN_SEQ_MAX,
153
+ normalize,
154
+ validate,
155
+ parseScenario,
156
+ composeScenario,
157
+ nextScenarioSeq,
158
+ normalizeObjectType,
159
+ };
@@ -82,6 +82,10 @@ function normalizeFacts(parsed) {
82
82
  profile: parsed.profile,
83
83
  proposal_mode: parsed.proposalMode || undefined,
84
84
  requirement_refs: Array.isArray(parsed.requirementRefs) ? parsed.requirementRefs : undefined,
85
+ numbering_waiver: parsed.numberingWaiver && Object.keys(parsed.numberingWaiver).length > 0
86
+ ? parsed.numberingWaiver
87
+ : undefined,
88
+ numbering_gate: parsed.numberingGate ? true : undefined,
85
89
  continuity: parsed.continuity && Object.keys(parsed.continuity).length > 0
86
90
  ? parsed.continuity
87
91
  : undefined,
@@ -107,6 +107,11 @@ const DIAGNOSTIC_CODES = Object.freeze({
107
107
  EXTERNAL_REF_INVALID: 'EXTERNAL_REF_INVALID',
108
108
  EXTERNAL_REF_TYPE_MISMATCH: 'EXTERNAL_REF_TYPE_MISMATCH',
109
109
  EXTERNAL_REF_CONFLICT: 'EXTERNAL_REF_CONFLICT',
110
+ EXTERNAL_KEY_FORMAT_INVALID: 'EXTERNAL_KEY_FORMAT_INVALID',
111
+ EXTERNAL_KEY_SCOPE_MISMATCH: 'EXTERNAL_KEY_SCOPE_MISMATCH',
112
+ EXTERNAL_KEY_FEATURE_UNDECLARED: 'EXTERNAL_KEY_FEATURE_UNDECLARED',
113
+ EXTERNAL_KEY_SEQ_REUSED: 'EXTERNAL_KEY_SEQ_REUSED',
114
+ NUMBERING_WAIVER_ACTIVE: 'NUMBERING_WAIVER_ACTIVE',
110
115
  CONTINUITY_DECISION_REQUIRED: 'CONTINUITY_DECISION_REQUIRED',
111
116
  CONTINUITY_IDENTITY_MISMATCH: 'CONTINUITY_IDENTITY_MISMATCH',
112
117
  });
@@ -453,8 +453,94 @@ function validateContinuityAndExternalRefs(facts, diagnostics, options = {}) {
453
453
  }
454
454
  }
455
455
 
456
+ const {
457
+ validate,
458
+ normalize,
459
+ parseScenario,
460
+ } = require('./external-key.cjs');
461
+
462
+ // 仅当 frontmatter 显式出现 requirement-refs / numbering-waiver 时启用入场门禁
463
+ // (新模板默认带 requirement-refs;旧夹具无该节则不强制)
464
+ const numberingDeclared = Boolean(facts.numbering_gate)
465
+ || (facts.numbering_waiver && typeof facts.numbering_waiver === 'object');
466
+ const requirementRefs = Array.isArray(facts.requirement_refs) ? facts.requirement_refs : [];
467
+ const waiverReason = String(
468
+ (facts.numbering_waiver && (facts.numbering_waiver.reason || facts.numbering_waiver.Reason)) || '',
469
+ ).trim();
470
+ if (numberingDeclared) {
471
+ if (requirementRefs.length === 0 && !waiverReason) {
472
+ diagnostics.push(diagnostic(
473
+ DIAGNOSTIC_CODES.CONTINUITY_DECISION_REQUIRED,
474
+ 'error',
475
+ '缺少格式合规的 requirement-refs,且未提供 numbering-waiver.reason',
476
+ {
477
+ file: 'proposal.md',
478
+ suggestion: '申报 REQ 号,或填写 numbering-waiver.reason(探索性/纯内部重构)',
479
+ },
480
+ ));
481
+ }
482
+ if (requirementRefs.length === 0 && waiverReason) {
483
+ diagnostics.push(diagnostic(
484
+ DIAGNOSTIC_CODES.NUMBERING_WAIVER_ACTIVE,
485
+ 'warning',
486
+ `编号豁免生效:${waiverReason}(本轮不种桥)`,
487
+ { file: 'proposal.md' },
488
+ ));
489
+ }
490
+ if (requirementRefs.length > 0 && waiverReason) {
491
+ diagnostics.push(diagnostic(
492
+ DIAGNOSTIC_CODES.EXTERNAL_KEY_FORMAT_INVALID,
493
+ 'error',
494
+ 'numbering-waiver 与非空 requirement-refs 互斥',
495
+ { file: 'proposal.md', suggestion: '清空其一' },
496
+ ));
497
+ }
498
+ }
499
+
500
+ const declaredReqIds = new Set();
501
+ const featureByReq = new Map();
502
+ for (const ref of requirementRefs) {
503
+ const objectType = String(ref.object_type || 'requirement').toLowerCase();
504
+ const format = validate(ref.external_id, objectType);
505
+ if (!format.ok) {
506
+ diagnostics.push(diagnostic(
507
+ DIAGNOSTIC_CODES.EXTERNAL_KEY_FORMAT_INVALID,
508
+ 'error',
509
+ format.message,
510
+ { file: 'proposal.md', suggestion: '回需求管理系统核实/换发编号,禁止改写' },
511
+ ));
512
+ continue;
513
+ }
514
+ const normalizedReq = normalize(ref.external_id, objectType);
515
+ if (objectType === 'requirement') {
516
+ declaredReqIds.add(normalizedReq);
517
+ if (ref.feature_id_invalid) {
518
+ diagnostics.push(diagnostic(
519
+ DIAGNOSTIC_CODES.EXTERNAL_KEY_FORMAT_INVALID,
520
+ 'error',
521
+ `feature-id 不合规: ${ref.feature_id_invalid}`,
522
+ { file: 'proposal.md', suggestion: '回需求管理系统核实 FEAT 号,禁止静默丢弃' },
523
+ ));
524
+ } else if (ref.feature_id) {
525
+ if (featureByReq.has(normalizedReq) && featureByReq.get(normalizedReq) !== ref.feature_id) {
526
+ diagnostics.push(diagnostic(
527
+ DIAGNOSTIC_CODES.EXTERNAL_KEY_FEATURE_UNDECLARED,
528
+ 'error',
529
+ `同一 Change 内同一 REQ 申报了多个 feature-id: ${normalizedReq}`,
530
+ { file: 'proposal.md' },
531
+ ));
532
+ }
533
+ featureByReq.set(normalizedReq, normalize(ref.feature_id, 'feature'));
534
+ }
535
+ }
536
+ }
537
+
456
538
  const bindingKeys = new Map();
539
+ const scenarioKeysInChange = new Set();
540
+ const removedScenarioKeys = new Set();
457
541
  for (const entity of facts.entities || []) {
542
+ const declaredFeatures = new Set();
543
+ const featureBindings = new Set();
458
544
  for (const ref of entity.external_refs || []) {
459
545
  const objectType = String(ref.object_type || '').toLowerCase();
460
546
  const allowedTypes = allowed[objectType];
@@ -475,6 +561,59 @@ function validateContinuityAndExternalRefs(facts, diagnostics, options = {}) {
475
561
  { ...sourceContext(entity), entity_id: entity.id },
476
562
  ));
477
563
  }
564
+ const format = validate(ref.external_id, objectType);
565
+ if (!format.ok) {
566
+ diagnostics.push(diagnostic(
567
+ DIAGNOSTIC_CODES.EXTERNAL_KEY_FORMAT_INVALID,
568
+ 'error',
569
+ `${format.message} (${entity.id})`,
570
+ {
571
+ ...sourceContext(entity),
572
+ entity_id: entity.id,
573
+ suggestion: '编号不合规回需求系统换发;场景键回 opsx-spec 修正',
574
+ },
575
+ ));
576
+ }
577
+ if (ref.feature_id) {
578
+ const featureFormat = validate(ref.feature_id, 'feature');
579
+ if (!featureFormat.ok) {
580
+ diagnostics.push(diagnostic(
581
+ DIAGNOSTIC_CODES.EXTERNAL_KEY_FORMAT_INVALID,
582
+ 'error',
583
+ `requirement.feature_id 不合规: ${featureFormat.message} (${entity.id})`,
584
+ { ...sourceContext(entity), entity_id: entity.id },
585
+ ));
586
+ } else {
587
+ declaredFeatures.add(normalize(ref.feature_id, 'feature'));
588
+ }
589
+ }
590
+ if (objectType === 'feature') {
591
+ featureBindings.add(normalize(ref.external_id, 'feature'));
592
+ }
593
+ if (objectType === 'scenario') {
594
+ const normalizedScenario = normalize(ref.external_id, 'scenario');
595
+ const parts = parseScenario(normalizedScenario);
596
+ if (parts && declaredReqIds.size > 0 && !declaredReqIds.has(parts.requirementId)) {
597
+ diagnostics.push(diagnostic(
598
+ DIAGNOSTIC_CODES.EXTERNAL_KEY_SCOPE_MISMATCH,
599
+ 'error',
600
+ `场景键 REQ 前缀不在 frontmatter requirement-refs 申报集合: ${normalizedScenario}`,
601
+ { ...sourceContext(entity), entity_id: entity.id },
602
+ ));
603
+ }
604
+ if (scenarioKeysInChange.has(normalizedScenario)) {
605
+ diagnostics.push(diagnostic(
606
+ DIAGNOSTIC_CODES.EXTERNAL_KEY_SEQ_REUSED,
607
+ 'error',
608
+ `本 Change 内 SCN 键重复: ${normalizedScenario}`,
609
+ { ...sourceContext(entity), entity_id: entity.id },
610
+ ));
611
+ }
612
+ scenarioKeysInChange.add(normalizedScenario);
613
+ if (String(entity.delta_state || '').toLowerCase() === 'removed') {
614
+ removedScenarioKeys.add(normalizedScenario);
615
+ }
616
+ }
478
617
  const key = `${ref.system}|${objectType}|${ref.external_id}|${entity.anchor_id || entity.id}`;
479
618
  const prior = bindingKeys.get(key);
480
619
  if (prior && prior !== entity.entity_id) {
@@ -492,6 +631,42 @@ function validateContinuityAndExternalRefs(facts, diagnostics, options = {}) {
492
631
  bindingKeys.set(key, entity.entity_id);
493
632
  }
494
633
  }
634
+ for (const featureId of featureBindings) {
635
+ if (!declaredFeatures.has(featureId)) {
636
+ diagnostics.push(diagnostic(
637
+ DIAGNOSTIC_CODES.EXTERNAL_KEY_FEATURE_UNDECLARED,
638
+ 'error',
639
+ `feature 绑定无法与同实体 requirement.feature_id 配对: ${featureId} (${entity.id})`,
640
+ { ...sourceContext(entity), entity_id: entity.id },
641
+ ));
642
+ }
643
+ }
644
+ for (const featureId of declaredFeatures) {
645
+ if (!featureBindings.has(featureId)) {
646
+ diagnostics.push(diagnostic(
647
+ DIAGNOSTIC_CODES.EXTERNAL_KEY_FEATURE_UNDECLARED,
648
+ 'error',
649
+ `requirement 申报了 feature_id 但缺少对应 feature 绑定: ${featureId} (${entity.id})`,
650
+ { ...sourceContext(entity), entity_id: entity.id },
651
+ ));
652
+ }
653
+ }
654
+ }
655
+
656
+ for (const entity of facts.entities || []) {
657
+ if (String(entity.delta_state || '').toLowerCase() === 'removed') continue;
658
+ for (const ref of entity.external_refs || []) {
659
+ if (String(ref.object_type || '').toLowerCase() !== 'scenario') continue;
660
+ const key = normalize(ref.external_id, 'scenario');
661
+ if (removedScenarioKeys.has(key) && String(entity.delta_state || '').toLowerCase() !== 'removed') {
662
+ diagnostics.push(diagnostic(
663
+ DIAGNOSTIC_CODES.EXTERNAL_KEY_SEQ_REUSED,
664
+ 'error',
665
+ `复用了 removed 墓碑 SCN 号: ${key}`,
666
+ { ...sourceContext(entity), entity_id: entity.id },
667
+ ));
668
+ }
669
+ }
495
670
  }
496
671
  }
497
672
 
@@ -7,11 +7,14 @@ delta-state: "added"
7
7
  predecessor-version: "" # added 留空;modified/removed 指向直接前序版本
8
8
  mode: "" # full=分 Capability 产物,simple=根目录精简产物
9
9
  test-strategy: "" # tdd=测试先行, impl-first=实现优先, none=无测试
10
- # 外部需求键(首轮冷启动也建议写入,便于 ingest 种桥)
10
+ # 外部需求键(默认必填 ≥1 个合规 REQ;探索性/纯内部重构可走 numbering-waiver)
11
11
  requirement-refs:
12
12
  - system: requirement-mgmt
13
13
  object-type: requirement
14
- external-id: REQ-<DOMAIN>-<NNN>
14
+ external-id: REQ-<DOMAIN>-<YEAR>-<SEQ> # 例 REQ-FI-2024-001
15
+ feature-id: FEAT-<DOMAIN>-<SEQ> # 可选,需求所属功能(需求系统权威)
16
+ # numbering-waiver:
17
+ # reason: "探索性原型,本轮不种桥"
15
18
  # Continuity:字段一律来自知识库 resolve,禁止本地 archive 文件夹名
16
19
  continuity:
17
20
  kind: new # iteration | similar-reference | new
@@ -34,7 +34,7 @@ capability-id: "CAP-<CAPABILITY>" # 必须与 proposal.md 中的 Capability ID
34
34
  - **version-id**: <UUID>
35
35
  - **delta-state**: added
36
36
  - **predecessor-version**: 无
37
- - **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<NNN>:SCN-<slug>
37
+ - **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<YEAR>-<SEQ>:SCN-<slug>-<NNN>
38
38
  - **当** <!-- 触发条件 -->
39
39
  - **预期** <!-- 预期结果 -->
40
40
 
@@ -43,7 +43,7 @@ capability-id: "CAP-<CAPABILITY>" # 必须与 proposal.md 中的 Capability ID
43
43
  - **version-id**: <UUID>
44
44
  - **delta-state**: added
45
45
  - **predecessor-version**: 无
46
- - **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<NNN>:SCN-<slug>
46
+ - **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<YEAR>-<SEQ>:SCN-<slug>-<NNN>
47
47
  - **当** <!-- 触发条件 -->
48
48
  - **预期** <!-- 预期结果 -->
49
49
 
@@ -63,7 +63,7 @@ capability-id: "CAP-<CAPABILITY>" # 必须与 proposal.md 中的 Capability ID
63
63
  - **version-id**: <新 UUID>
64
64
  - **delta-state**: <modified|added>
65
65
  - **predecessor-version**: <modified 时填写;added 为无>
66
- - **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<NNN>:SCN-<slug>
66
+ - **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<YEAR>-<SEQ>:SCN-<slug>-<NNN>
67
67
  - **当** <!-- 触发条件 -->
68
68
  - **预期** <!-- 预期结果 -->
69
69
 
@@ -220,7 +220,7 @@ node skywalk-sdd/log.cjs record --type=conformance_review --command=check --proj
220
220
 
221
221
  ---
222
222
 
223
- ## Continuity / external_ref 确定性门禁
223
+ ## Continuity / external_ref / 编号确定性门禁
224
224
 
225
225
  `opsx-check` **不联网提问**。Agent 应在 propose/spec 已问完;本阶段只验证并入既有 apply 前门禁:
226
226
 
@@ -228,6 +228,13 @@ node skywalk-sdd/log.cjs record --type=conformance_review --command=check --proj
228
228
  - 已写 spec 的 Capability:场景 `external-ref` 与 `continuity-resolution.json` 决议一致;同 key+同锚点未偷偷换 entity_id
229
229
  - 用户选「原对象」却仍用新 id、或选「新对象」却仍共用旧锚点 → 失败(`CONTINUITY_IDENTITY_MISMATCH` / `EXTERNAL_REF_CONFLICT`)
230
230
  - 决议缺失 / pending / 与产物不一致 → `CONTINUITY_DECISION_REQUIRED`
231
+ - **编号诊断码**(并入五维报告与 apply gate):
232
+ - `EXTERNAL_KEY_FORMAT_INVALID` — external_id / feature_id 不合规
233
+ - `EXTERNAL_KEY_SCOPE_MISMATCH` — 场景键 REQ 前缀不在 `requirement-refs`
234
+ - `EXTERNAL_KEY_FEATURE_UNDECLARED` — feature↔requirement.`feature_id` 配对断裂
235
+ - `EXTERNAL_KEY_SEQ_REUSED` — 本 Change 内 SCN 重复或复用 removed 墓碑号
236
+ - `NUMBERING_WAIVER_ACTIVE`(warning)— 豁免生效,本轮不种桥
237
+ - `requirement-refs` 为空且无有效 waiver → `CONTINUITY_DECISION_REQUIRED` 级阻断
231
238
  - CI/非交互:失败即非零退出并打印修复说明,不挂起等待输入
232
239
 
233
240
  ## 本体语义关系门禁
@@ -5,24 +5,10 @@ description: >-
5
5
  (archive:ingest scope). Prompts for API key and multi-selects spaces/KBs into
6
6
  local skill state. Supports upload, job status query, job list, and retry.
7
7
  Use when ingesting new or updated knowledge archives into the ontology KB.
8
- argument-hint: "[path-to-archive.zip]"
9
- license: MIT
10
- compatibility: Requires Engineering KB API (API Key with archive:ingest).
11
- metadata:
12
- author: sdd-team
13
- version: "1.0"
14
- source: "kb-sdd/skills/opsx-kb-ingest"
15
- allowed-tools:
16
- - Bash
17
- - Read
18
- - Write
19
- - Edit
20
8
  ---
21
9
 
22
10
  # 本体知识库 · 入库
23
11
 
24
- > **部署说明**:本技能随 `kld-sdd-init` 安装到项目 skills 目录。权威源在工程知识库仓 `skills/opsx-kb-ingest`;`opsx-archive` **硬依赖**本技能完成收尾入库。
25
-
26
12
  只负责**入库**(zip 上传)。鉴权只用 **API Key**(`Authorization: Bearer sk_sdd_…`),**禁止**走手机号登录。
27
13
 
28
14
  > 控制台知识库页另有浮动 Ontology Agent(会话登录 + SSE);本 Skill 仍走 API Key,二者分开。
@@ -127,6 +113,9 @@ curl -sS -X POST "$API/v1/spaces/$SPACE_ID/knowledge-bases/$KB_ID/ingestions/$JO
127
113
  - object_type 与实体类型匹配:`requirement`/`feature`→Capability;`scenario`→SpecificationStatement|AcceptanceCriterion
128
114
  - 包内无重复 `(external key, entity_id)` 绑定对
129
115
  - `project_id` / `spaceKey` 既有规则保留
116
+ - **编号格式**:三条正则(见 reference)+ 场景 SLUG≤40 / 键总长≤120;先归一化再匹配
117
+ - **场景作用域**:scenario 键的 REQ 前缀必须出现在同包 requirement 绑定集合
118
+ - **功能配对**:同实体每条 feature 绑定必须能与某 requirement 条目的 `feature_id` 精确配对;反向同理
130
119
 
131
120
  详细字段说明 → [reference.md](reference.md)。
132
121
 
@@ -160,6 +149,16 @@ curl -sS -X POST "$API/v1/spaces/$SPACE_ID/knowledge-bases/$KB_ID/ingestions/$JO
160
149
  - **改为新锚点**:回到 kld-sdd,分配新锚点与新 entity_id,重新 check → 归档 → 入库
161
150
  3. **禁止**在 KB 内现场改绑或解绑。
162
151
 
152
+ 若编号门禁失败:
153
+
154
+ | errorCode | 指引 |
155
+ |-----------|------|
156
+ | `EXTERNAL_REFERENCE_FORMAT_INVALID` | 编号不合规 → **回需求管理系统换发**;不得手改编号硬闯 |
157
+ | `EXTERNAL_REFERENCE_SCOPE_MISMATCH` | 场景键 REQ 前缀不在申报集合 → 回 kld-sdd spec 修正 external-ref |
158
+ | `EXTERNAL_REFERENCE_FEATURE_UNBOUND` | feature↔requirement.`feature_id` 配对断裂 → 回 propose/archive 修正线缆字段 |
159
+
160
+ **禁止**手改包内编号绕过门禁。
161
+
163
162
  **硬规则**
164
163
 
165
164
  - 无 `apiKey` 不得猜密钥、不得改走 login。
@@ -141,8 +141,21 @@ POST {base}/ingestions/{jobId}/retry
141
141
  | `EXTERNAL_REF_CONFLICT` | 同外部键+同锚点已绑不同 entity_id;整包回滚;见 `report.details` |
142
142
  | `EXTERNAL_REFERENCE_TYPE_MISMATCH` | 包内 object_type 与实体类型不匹配 |
143
143
  | `EXTERNAL_REFERENCE_DUPLICATE_IN_PACKAGE` | 包内重复 `(external key, entity_id)` |
144
+ | `EXTERNAL_REFERENCE_FORMAT_INVALID` | 编号归一化后不合规(含 requirement.`feature_id`);回需求系统换发 |
145
+ | `EXTERNAL_REFERENCE_SCOPE_MISMATCH` | 场景键 REQ 前缀不在同包 requirement 集合;回 kld-sdd spec 修正 |
146
+ | `EXTERNAL_REFERENCE_FEATURE_UNBOUND` | feature 绑定与 requirement.`feature_id` 无法配对;回 propose/archive |
144
147
  | `FACTS_SCHEMA_UNSUPPORTED` | canonical-facts schema 非 v1/v2 |
145
148
 
149
+ ### 编号正则(与 KB 设计 §2 / ExternalKeyFormat 一致)
150
+
151
+ ```text
152
+ requirement: ^REQ-[A-Z][A-Z0-9]{1,7}-[0-9]{4}-[0-9]{3,6}$
153
+ feature: ^FEAT-[A-Z][A-Z0-9]{1,7}-[0-9]{3,6}$
154
+ scenario: ^REQ-[A-Z][A-Z0-9]{1,7}-[0-9]{4}-[0-9]{3,6}:SCN-[a-z0-9]+(-[a-z0-9]+)*-[0-9]{3}$
155
+ ```
156
+
157
+ requirement 条目可选 `feature_id`(FEAT 文法);配对关系随线缆走,KB 不反查 frontmatter。
158
+
146
159
  ### `EXTERNAL_REF_CONFLICT` 的 report.details
147
160
 
148
161
  ```json
@@ -5,24 +5,10 @@ description: >-
5
5
  entity/impact/ontology-view). Prompts for API key and multi-selects spaces/KBs into
6
6
  local skill state. Use when looking up Spec/design facts, Spec reuse, impact, or
7
7
  citation-backed answers from the ontology KB.
8
- argument-hint: "[query or continuity intent]"
9
- license: MIT
10
- compatibility: Requires Engineering KB API (API Key with context:read).
11
- metadata:
12
- author: sdd-team
13
- version: "1.0"
14
- source: "kb-sdd/skills/opsx-ontology-query"
15
- allowed-tools:
16
- - Bash
17
- - Read
18
- - Write
19
- - Edit
20
8
  ---
21
9
 
22
10
  # 本体知识库 · 查询
23
11
 
24
- > **部署说明**:本技能随 `kld-sdd-init` 安装到项目 skills 目录。权威源在工程知识库仓 `skills/opsx-ontology-query`;`opsx-propose` / `opsx-spec` 等流程技能**硬依赖**本技能,缺失时不得用本地 archive 兜底。
25
-
26
12
  只负责**查**。鉴权只用 **API Key**(`Authorization: Bearer sk_sdd_…`),**禁止**走手机号登录。
27
13
 
28
14
  > 控制台知识库页另有浮动 Ontology Agent(会话登录 + SSE);本 Skill 仍走 API Key,二者分开。
@@ -124,6 +110,31 @@ curl -sS -X POST "$API/v1/spaces/$SPACE_ID/knowledge-bases/$KB_ID/entities/resol
124
110
 
125
111
  命中时关注:`resolution=LINK_EXISTING` 且 `inheritanceAllowed=true`(可继承 n);`removedBindingCount`(已失效绑定 m);`matchType=HISTORICAL_ONLY` 表示仅有失效绑定,不可继承。
126
112
 
113
+ **按功能号圈能力(propose 前)**
114
+
115
+ ```bash
116
+ curl -sS -X POST "$API/v1/spaces/$SPACE_ID/knowledge-bases/$KB_ID/entities/resolve" \
117
+ -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
118
+ -d '{
119
+ "externalSystem":"requirement-mgmt",
120
+ "externalObjectType":"feature",
121
+ "externalId":"FEAT-FI-012",
122
+ "entityType":"Capability"
123
+ }'
124
+ ```
125
+
126
+ 展示该功能下存活 / 失效能力清单,辅助勾选本次 CAP 范围;此查询只作范围参考,不改变 Continuity 判定优先级。
127
+
128
+ ### 编号文法速查(权威在 KB 仓设计 §2)
129
+
130
+ ```text
131
+ requirement: ^REQ-[A-Z][A-Z0-9]{1,7}-[0-9]{4}-[0-9]{3,6}$
132
+ feature: ^FEAT-[A-Z][A-Z0-9]{1,7}-[0-9]{3,6}$
133
+ scenario: ^REQ-…:SCN-[a-z0-9]+(-[a-z0-9]+)*-[0-9]{3}$
134
+ ```
135
+
136
+ 归一化:trim;REQ/FEAT 段大写;SCN slug 小写。编号五律摘要:REQ/FEAT 仅需求系统铸号;SCN 由 kld-sdd/opsx-spec 铸号;编号≠身份;归属权威在需求系统;历史绑定 append-only。
137
+
127
138
  **match-requirement**
128
139
 
129
140
  ```bash
@@ -152,7 +163,7 @@ curl -sS -X POST "$API/v1/spaces/$SPACE_ID/knowledge-bases/$KB_ID/context/match-
152
163
 
153
164
  ### 命中
154
165
  1. **{displayName}**({entityType})@ {kbName}
155
- - id / version / matchType / externalRefs
166
+ - id / version / matchType / externalRefs(归一化形态)…
156
167
  - 若外部键命中:可继承 n / 已失效绑定 m
157
168
  ```
158
169
 
@@ -162,13 +162,21 @@ openspec instructions proposal --change "<name>" --json
162
162
 
163
163
  > 完整性检查(问题描述/目标/模块/约束 4 项)与缺失补充机制见 `./checklist.md`「§6 需求完整性检查」。发现缺失时主动询问用户补充。
164
164
 
165
- ### 6.5 Continuity】需求 / Capability 身份(只到 CAP,不做场景)
165
+ ### 6.5 【编号入场 + Continuity】需求 / Capability 身份(只到 CAP,不做场景)
166
166
 
167
- 在创建变更目录之后、写 proposal 能力列表之前(或紧接 CAP 编号分配前):
167
+ 在创建变更目录之后、写 proposal 能力列表之前(或紧接 CAP 编号分配前)。**验号必须在 resolve 之前**:
168
168
 
169
- 1. 提取 / 询问外部需求号 `REQ-*`(没有则问一次)。
170
- 2. **先加载依赖技能**:确认 `${AGENT_SKILL_DIR}/opsx-ontology-query/SKILL.md`(或当前编辑器等价 skills 路径)存在并 Read;按该技能完成 API Key / 空间与 KB 选择(写入其 `.local/state.json`)。未安装则停止本步。
171
- 3. 调用知识库 **`opsx-ontology-query`**(权威);可用薄封装,但契约以该技能为准:
169
+ 1. 收集 REQ 号(含可选 `feature-id`);没有则问一次。
170
+ 2. 逐个验号(确定性入口,禁止肉眼判正则):
171
+ ```bash
172
+ node skywalk-sdd/ontology/cli.cjs external-key --validate "<REQ-...>" --type requirement
173
+ # 若有 feature-id:
174
+ node skywalk-sdd/ontology/cli.cjs external-key --validate "<FEAT-...>" --type feature
175
+ ```
176
+ - REQ/FEAT 不合规 → **拒绝进入 resolve**,告知「编号不合规,请回需求管理系统核实/换发」;Agent 不得猜测、补位、改写。
177
+ - 用户明确说「没有外部需求号」→ 走 `numbering-waiver.reason`(必填理由);`requirement-refs` 必须为空;提示本轮不种桥。
178
+ 3. **先加载依赖技能**:确认 `${AGENT_SKILL_DIR}/opsx-ontology-query/SKILL.md` 存在并 Read;按该技能完成 API Key / 空间与 KB 选择。未安装则停止本步。
179
+ 4. 验号通过后才 resolve:
172
180
  ```bash
173
181
  node skywalk-sdd/context-client.cjs --mode=resolve \
174
182
  --external-system=requirement-mgmt \
@@ -178,11 +186,12 @@ node skywalk-sdd/context-client.cjs --mode=resolve \
178
186
  --space-id="$ENGINEERING_KB_SPACE_ID" \
179
187
  --kb-id="$ENGINEERING_KB_KB_ID"
180
188
  ```
181
- 4. KB 结果确认 Continuity:`iteration` / `similar-reference` / `new`;勾选本次涉及的 CAP
182
- 5. 写入 proposal frontmatter:`requirement-refs` + `continuity`(字段来自 KB:`kb-space-id` / `kb-id` / `base-capabilities[].entity-id` / `current-version-id`)。**禁止**写本地 archive 文件夹名作为 `base-archive`。
183
- 6. CAP 级「同 key + 同锚点、不同 entity_id」当场问 A/B/C;决议写入 `openspec/changes/<name>/continuity-resolution.json` `capabilities[]`。
184
- 7. KB 不可用 `degraded` 继续,**禁止**扫本地 `archive/` UUID。预期:恢复后同锚点入库可能触发 `EXTERNAL_REF_CONFLICT`。
185
- 8. **不得**在本阶段生成 STMT/AC/场景或裁决场景身份。
189
+ 5. 若申报了 feature-id:额外 `objectType=feature` resolve 一次,展示「该功能下已有能力 n 个(存活 m / 失效 k)」辅助勾选 CAP 范围;不改变 Continuity 判定优先级。
190
+ 6. KB 结果确认 Continuity:`iteration` / `similar-reference` / `new`;勾选本次涉及的 CAP。
191
+ 7. 写入 proposal frontmatter:`requirement-refs`(含 `feature-id`)/ `numbering-waiver` + `continuity`。**禁止**写本地 archive 文件夹名作为 `base-archive`。
192
+ 8. CAP 级「同 key + 同锚点、不同 entity_id」当场问 A/B/C;决议写入 `continuity-resolution.json` `capabilities[]`。
193
+ 9. KB 不可用 → `degraded` 继续,**禁止**扫本地 `archive/` 抄 UUID。
194
+ 10. **不得**在本阶段生成 STMT/AC/场景或裁决场景身份;**不得**铸/改 REQ/FEAT 号。
186
195
 
187
196
  ### 7. 【交互引导】文档拆分模式选择
188
197
 
@@ -132,10 +132,16 @@ node skywalk-sdd/context-client.cjs \
132
132
  - **禁止**从本地 `archive/` 抄 UUID 当跨迭代继承源;跨迭代只认 KB current。
133
133
  - 若返回 `available=false` / `degraded=true`,记录降级并继续,不得扫本地 archive 兜底。
134
134
  - `INHERIT`:unchanged 写继承引用;modified 复用 entity-id + predecessor。`REFERENCE`:只参考,新开身份。
135
- - reuseBundle / 实体上的 `externalRefs` 写入场景 `external-ref`(`requirement-mgmt:scenario:REQ-…:SCN-…`)。
136
- - 场景级「同 SCN key + 同锚点、不同 entity_id」在写完该 CAP identity 后、确认文档前**当场问** A/B/C;未决不得进入下一 CAP / design。决议追加到 `continuity-resolution.json` 的 `scenarios[]`。
135
+ - reuseBundle / 实体上的 `externalRefs` 写入场景 `external-ref`(完整键 `REQ-…:SCN-<slug>-<NNN>`)。
136
+ - **SCN 铸号(本仓唯一铸号点)**:
137
+ 1. 继承优先:`reuseBundle.externalRefs` 已有场景键的 unchanged/modified 场景一律沿用原 SCN 号,禁止另铸。
138
+ 2. 新场景:`SCN-<slug>-<NNN>`;slug=kebab-case 小写 ≤40;NNN=该 REQ 命名空间内 max+1(已用集合=KB 回传 ∪ 本 Change 已写键,含 removed 墓碑)。
139
+ 3. removed 号是墓碑:永不复用、永不重排;序号达 999 → 硬错误,回需求系统拆分需求,不扩位。
140
+ 4. 写完立即用 `cli.cjs external-key --validate … --type scenario` 校验;REQ 前缀必须 ∈ proposal `requirement-refs`。
141
+ - 场景级「同 SCN key + 同锚点、不同 entity_id」当场问 A/B/C;未决不得进入下一 CAP / design。决议追加到 `continuity-resolution.json` 的 `scenarios[]`;决议中的 `externalKey` 必须是归一化形态。
137
142
  - 优先消费 `reuseBundles[].statements`;`designElements` 只作理解上下文,不能写成 Spec 的 How。
138
143
  - 所有知识库内容均为 advisory;与用户确认 / proposal 冲突时以当前确认与 proposal 为准。
144
+ - **禁止**铸/改 REQ/FEAT;**禁止**自动重排/回收 SCN。
139
145
 
140
146
  **【可选】业务知识库检索**:
141
147
  术语含义不清且可能影响 spec 准确性时,可调用 **opsx-knowledge** skill。