kld-sdd 2.6.3 → 2.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kld-sdd",
3
- "version": "2.6.3",
3
+ "version": "2.6.4",
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-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/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",
@@ -24,12 +24,17 @@ function queryFromArgs(args) {
24
24
  if (typeof args['query-file'] === 'string' && args['query-file'].trim()) {
25
25
  return fs.readFileSync(args['query-file'], 'utf8').trim();
26
26
  }
27
- throw new Error('missing --query or --query-file');
27
+ return '';
28
28
  }
29
29
 
30
- function buildEndpoint(apiBase, spaceId) {
30
+ function buildEndpoint(apiBase, spaceId, kbId, mode) {
31
31
  const base = String(apiBase || 'http://localhost:8090/api').replace(/\/$/, '');
32
- return `${base}/v1/spaces/${encodeURIComponent(spaceId)}/context/match-requirement`;
32
+ const space = encodeURIComponent(spaceId);
33
+ const kb = encodeURIComponent(kbId);
34
+ if (mode === 'resolve') {
35
+ return `${base}/v1/spaces/${space}/knowledge-bases/${kb}/entities/resolve`;
36
+ }
37
+ return `${base}/v1/spaces/${space}/knowledge-bases/${kb}/context/match-requirement`;
33
38
  }
34
39
 
35
40
  function requestJson(url, payload, token, timeoutMs) {
@@ -81,52 +86,85 @@ function requestJson(url, payload, token, timeoutMs) {
81
86
  });
82
87
  }
83
88
 
84
- function compactContext(data, includeCandidates) {
89
+ function compactContext(data, includeCandidates, mode = 'match') {
85
90
  if (includeCandidates) return data;
86
- return {
91
+ const compacted = {
87
92
  matchType: data.matchType,
93
+ resolution: data.resolution,
88
94
  inheritanceAllowed: data.inheritanceAllowed,
89
95
  reviewRequired: data.reviewRequired,
96
+ removedBindingCount: data.removedBindingCount,
90
97
  advisory: data.advisory,
91
98
  degraded: data.degraded,
92
99
  degradationReasons: data.degradationReasons,
93
100
  targetStage: data.targetStage,
101
+ reuseMode: data.reuseMode,
94
102
  reuseBundles: data.reuseBundles,
95
103
  answeredQuestions: data.answeredQuestions,
96
104
  warnings: data.warnings,
97
105
  clarificationQuestions: data.clarificationQuestions,
98
106
  specGenerationContext: data.specGenerationContext,
107
+ supportEvidence: data.supportEvidence,
108
+ oppositionEvidence: data.oppositionEvidence,
99
109
  };
110
+ // Continuity resolve 必须透出 candidates;match 默认隐藏内部候选除非 --include-candidates
111
+ if (mode === 'resolve') {
112
+ compacted.candidates = data.candidates;
113
+ }
114
+ return compacted;
100
115
  }
101
116
 
102
- async function retrieveContext(args = parseArgs(process.argv), env = process.env) {
117
+ function buildPayload(args, mode) {
118
+ const payload = {};
103
119
  const query = queryFromArgs(args);
120
+ if (mode !== 'resolve') {
121
+ if (!query) {
122
+ throw new Error('missing --query or --query-file');
123
+ }
124
+ payload.query = query;
125
+ payload.targetStage = args['target-stage'] || 'spec';
126
+ } else if (query) {
127
+ payload.query = query;
128
+ }
129
+
130
+ if (args['entity-id']) payload.entityId = args['entity-id'];
131
+ if (args['previous-version-id']) payload.previousVersionId = args['previous-version-id'];
132
+ if (args['entity-type']) payload.entityType = args['entity-type'];
133
+ if (args['external-system']) payload.externalSystem = args['external-system'];
134
+ if (args['external-object-type']) payload.externalObjectType = args['external-object-type'];
135
+ if (args['external-id']) payload.externalId = args['external-id'];
136
+ if (args['canonical-key']) payload.canonicalKey = args['canonical-key'];
137
+ return payload;
138
+ }
139
+
140
+ async function retrieveContext(args = parseArgs(process.argv), env = process.env) {
104
141
  const spaceId = args['space-id'] || env.ENGINEERING_KB_SPACE_ID || '';
105
- if (!spaceId) {
142
+ const kbId = args['kb-id'] || env.ENGINEERING_KB_KB_ID || '';
143
+ if (!spaceId || !kbId) {
106
144
  return {
107
145
  available: false,
108
146
  advisory: true,
109
147
  degraded: true,
110
148
  reason: 'engineering_kb_not_configured',
111
- hint: 'set ENGINEERING_KB_SPACE_ID to enable reusable Spec context',
149
+ hint: 'set ENGINEERING_KB_SPACE_ID and ENGINEERING_KB_KB_ID; do not fall back to local archive/',
112
150
  };
113
151
  }
114
152
 
153
+ const mode = String(args.mode || 'match').trim().toLowerCase() === 'resolve' ? 'resolve' : 'match';
115
154
  const apiBase = args['api-base'] || env.ENGINEERING_KB_API || 'http://localhost:8090/api';
116
155
  const token = args.token || env.ENGINEERING_KB_TOKEN || '';
117
156
  const timeoutMs = Number(args.timeout || env.ENGINEERING_KB_TIMEOUT_MS || 30000);
157
+ const payload = buildPayload(args, mode);
118
158
  const data = await requestJson(
119
- buildEndpoint(apiBase, spaceId),
120
- {
121
- query,
122
- targetStage: args['target-stage'] || 'spec',
123
- },
159
+ buildEndpoint(apiBase, spaceId, kbId, mode),
160
+ payload,
124
161
  token,
125
162
  timeoutMs,
126
163
  );
127
164
  return {
128
165
  available: true,
129
- ...compactContext(data, Boolean(args['include-candidates'])),
166
+ mode,
167
+ ...compactContext(data, Boolean(args['include-candidates']), mode),
130
168
  };
131
169
  }
132
170
 
@@ -141,6 +179,7 @@ async function main() {
141
179
  degraded: true,
142
180
  reason: 'engineering_kb_unavailable',
143
181
  message: error.message,
182
+ hint: 'KB unavailable: continue degraded; never scan local archive/ for inheritance UUIDs',
144
183
  }, null, 2));
145
184
  if (args.strict) process.exitCode = 1;
146
185
  }
@@ -154,6 +193,7 @@ module.exports = {
154
193
  parseArgs,
155
194
  queryFromArgs,
156
195
  buildEndpoint,
196
+ buildPayload,
157
197
  requestJson,
158
198
  compactContext,
159
199
  retrieveContext,
@@ -8,7 +8,7 @@ const { generateUuidV7 } = require('./id.cjs');
8
8
  const PRODUCER_VERSION = require('../../package.json').version;
9
9
  const PRODUCER = `kld-sdd@${PRODUCER_VERSION}`;
10
10
  const MANIFEST_SCHEMA_VERSION = 'kld-sdd-archive-manifest/v2';
11
- const CANONICAL_FACTS_SCHEMA_VERSION = 'kld-sdd-canonical-facts/v1';
11
+ const CANONICAL_FACTS_SCHEMA_VERSION = 'kld-sdd-canonical-facts/v2';
12
12
  const CONVERSION_REPORT_SCHEMA_VERSION = 'kld-sdd-conversion-report/v1';
13
13
  const PROJECT_IDENTITY_SCHEMA_VERSION = 'kld-sdd-project-identity/v1';
14
14
  const MANIFEST_PATH = 'archive-manifest.json';
@@ -100,16 +100,42 @@ function requiredText(value, field, owner) {
100
100
  return normalized;
101
101
  }
102
102
 
103
- function canonicalEntity(archiveDir, entity) {
103
+ function normalizeExternalRefs(refs) {
104
+ if (!Array.isArray(refs)) return [];
105
+ const seen = new Set();
106
+ const result = [];
107
+ for (const ref of refs) {
108
+ const system = String(ref.system || ref.system_name || '').trim();
109
+ 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;
112
+ const key = `${system}\0${objectType}\0${externalId}`;
113
+ if (seen.has(key)) continue;
114
+ seen.add(key);
115
+ result.push({
116
+ system,
117
+ object_type: objectType,
118
+ external_id: externalId,
119
+ });
120
+ }
121
+ return result;
122
+ }
123
+
124
+ function canonicalEntity(archiveDir, entity, inheritedRequirementRefs = []) {
104
125
  const anchorId = requiredText(entity.anchor_id || entity.id, 'anchor_id', '本体实体').toUpperCase();
105
126
  const versionId = requiredText(
106
127
  entity.entity_version_id || entity.version_id,
107
128
  'entity_version_id/version_id',
108
129
  anchorId,
109
130
  ).toLowerCase();
131
+ const entityType = requiredText(entity.type, 'type', anchorId);
132
+ let externalRefs = normalizeExternalRefs(entity.external_refs || entity.externalRefs || []);
133
+ if (entityType === 'Capability' && inheritedRequirementRefs.length > 0) {
134
+ externalRefs = normalizeExternalRefs([...externalRefs, ...inheritedRequirementRefs]);
135
+ }
110
136
  return {
111
137
  anchor_id: anchorId,
112
- type: requiredText(entity.type, 'type', anchorId),
138
+ type: entityType,
113
139
  name: String(entity.name || anchorId).trim(),
114
140
  entity_id: requiredText(entity.entity_id, 'entity_id', anchorId).toLowerCase(),
115
141
  entity_version_id: versionId,
@@ -123,6 +149,7 @@ function canonicalEntity(archiveDir, entity) {
123
149
  review_status: 'confirmed',
124
150
  generation_role: entity.generation_role || 'current',
125
151
  ...(entity.inherited_from ? { inherited_from: entity.inherited_from } : {}),
152
+ ...(externalRefs.length > 0 ? { external_refs: externalRefs } : {}),
126
153
  attributes: entity.attributes && typeof entity.attributes === 'object' ? entity.attributes : {},
127
154
  source: sourceWithVerifiedHash(archiveDir, entity.source, anchorId, true),
128
155
  };
@@ -198,8 +225,11 @@ function buildCanonicalFacts(archiveDir, snapshot, projectId, archiveId) {
198
225
  throw new Error('archive-ontology.json 必须是 confirmed 快照');
199
226
  }
200
227
  const changeId = requiredText(snapshot.change_id, 'change_id', 'archive-ontology.json');
228
+ const requirementRefs = normalizeExternalRefs(
229
+ snapshot.requirement_refs || snapshot.requirementRefs || [],
230
+ ).filter((ref) => ref.object_type === 'requirement');
201
231
  const entities = (snapshot.entities || [])
202
- .map((entity) => canonicalEntity(archiveDir, entity))
232
+ .map((entity) => canonicalEntity(archiveDir, entity, requirementRefs))
203
233
  .sort((left, right) => (
204
234
  left.anchor_id.localeCompare(right.anchor_id)
205
235
  || left.entity_version_id.localeCompare(right.entity_version_id)
@@ -241,6 +271,7 @@ function buildCanonicalFacts(archiveDir, snapshot, projectId, archiveId) {
241
271
  facts_hash: requiredText(snapshot.facts_hash, 'facts_hash', 'archive-ontology.json'),
242
272
  source_revision: snapshot.source_revision,
243
273
  snapshot_id: snapshot.snapshot_id,
274
+ ...(requirementRefs.length > 0 ? { requirement_refs: requirementRefs } : {}),
244
275
  entities,
245
276
  relations,
246
277
  },
@@ -7,7 +7,7 @@ const { DIAGNOSTIC_CODES, entityTypeForId } = require('./schema.cjs');
7
7
  const { ensureStructuralIdentities } = require('./structural-identity.cjs');
8
8
 
9
9
  const ENTITY_ID_PATTERN = /\b(?:CHG|CAP|STMT|AC|CON|DES|TASK|ART|SEC|SNAP)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi;
10
- const IDENTITY_META_PATTERN = /^\s*-\s+\*\*(entity-id|version-id|predecessor-version|delta-state)\*\*\s*[::]\s*(.*?)\s*$/i;
10
+ const IDENTITY_META_PATTERN = /^\s*-\s+\*\*(entity-id|version-id|predecessor-version|delta-state|external-ref)\*\*\s*[::]\s*(.*?)\s*$/i;
11
11
 
12
12
  function toPosix(value) {
13
13
  return String(value || '').replace(/\\/g, '/');
@@ -126,6 +126,9 @@ function sourceFor(file, line, anchorId) {
126
126
 
127
127
  function addEntity(target, type, id, name, file, line, attributes = {}, identity = {}, semanticContent = '') {
128
128
  const anchorId = String(id || '').toUpperCase();
129
+ const externalRefs = Array.isArray(identity.external_refs)
130
+ ? identity.external_refs.filter(Boolean)
131
+ : [];
129
132
  const entity = {
130
133
  id: anchorId,
131
134
  anchor_id: anchorId,
@@ -135,6 +138,7 @@ function addEntity(target, type, id, name, file, line, attributes = {}, identity
135
138
  version_id: String(identity.version_id || '').trim().toLowerCase() || undefined,
136
139
  predecessor_version_id: String(identity.predecessor_version_id || '').trim().toLowerCase() || undefined,
137
140
  delta_state: String(identity.delta_state || '').trim().toLowerCase() || undefined,
141
+ ...(externalRefs.length > 0 ? { external_refs: externalRefs } : {}),
138
142
  content_hash: sha256(stripIdentityMetadata(semanticContent || `${type}\n${anchorId}\n${name}`)),
139
143
  attributes,
140
144
  source: sourceFor(file, line, anchorId),
@@ -162,25 +166,123 @@ function stripIdentityMetadata(value) {
162
166
  return String(value || '')
163
167
  .split('\n')
164
168
  .filter((line) => !IDENTITY_META_PATTERN.test(line))
165
- .filter((line) => !/^\s*(?:entity-id|version-id|predecessor-version|delta-state)\s*:/i.test(line))
169
+ .filter((line) => !/^\s*(?:entity-id|version-id|predecessor-version|delta-state|external-ref)\s*:/i.test(line))
166
170
  .filter((line) => !/^\s*-\s+\*\*(?:realizes|implements|covers|dependsOn|constrains|约束对象|实现需求|覆盖需求|依赖)\*\*\s*[::]/i.test(line))
167
171
  .join('\n')
168
172
  .trim();
169
173
  }
170
174
 
175
+ function parseExternalRefValue(value) {
176
+ const raw = String(value || '').trim();
177
+ if (!raw || /^(?:无|none|null|-)$/i.test(raw)) return null;
178
+ const parts = raw.split(':').map((part) => part.trim()).filter(Boolean);
179
+ if (parts.length < 3) return null;
180
+ return {
181
+ system: parts[0],
182
+ object_type: parts[1].toLowerCase(),
183
+ external_id: parts.slice(2).join(':'),
184
+ };
185
+ }
186
+
171
187
  function identityFromValues(values = {}) {
172
188
  const optionalValue = (value) => {
173
189
  const normalized = String(value || '').trim();
174
190
  return /^(?:无|none|null|-)$/i.test(normalized) ? '' : normalized;
175
191
  };
192
+ const externalRef = parseExternalRefValue(values['external-ref']);
176
193
  return {
177
194
  entity_id: optionalValue(values['entity-id']),
178
195
  version_id: optionalValue(values['version-id']),
179
196
  predecessor_version_id: optionalValue(values['predecessor-version']),
180
197
  delta_state: String(values['delta-state'] || '').trim(),
198
+ external_refs: externalRef ? [externalRef] : [],
181
199
  };
182
200
  }
183
201
 
202
+ function flushRequirementRef(requirementRefs, currentRef) {
203
+ if (!currentRef) return null;
204
+ const system = currentRef.system;
205
+ const objectType = currentRef['object-type'] || currentRef.object_type;
206
+ const externalId = currentRef['external-id'] || currentRef.external_id;
207
+ if (system && objectType && externalId) {
208
+ requirementRefs.push({
209
+ system,
210
+ object_type: String(objectType).toLowerCase(),
211
+ external_id: externalId,
212
+ });
213
+ }
214
+ return null;
215
+ }
216
+
217
+ function parseStructuredFrontmatter(lines) {
218
+ const flat = {};
219
+ const requirementRefs = [];
220
+ const continuity = {};
221
+ if (!lines.length || lines[0].trim() !== '---') {
222
+ return { flat, requirementRefs, continuity };
223
+ }
224
+ let section = null;
225
+ let currentRef = null;
226
+ let currentCap = null;
227
+ for (let index = 1; index < lines.length; index += 1) {
228
+ const line = lines[index].replace(/\r$/, '');
229
+ if (line.trim() === '---') break;
230
+ const topKey = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/);
231
+ const listItem = line.match(/^\s+-\s+([A-Za-z0-9_-]+):\s*(.*?)\s*$/);
232
+ const nested = line.match(/^\s{2,}([A-Za-z0-9_-]+):\s*(.*?)\s*$/);
233
+
234
+ if (topKey && !/^\s/.test(line)) {
235
+ currentRef = flushRequirementRef(requirementRefs, currentRef);
236
+ section = topKey[1].toLowerCase();
237
+ const value = unwrapScalar(stripInlineYamlComment(topKey[2]));
238
+ flat[section] = value;
239
+ currentCap = null;
240
+ if (section === 'continuity' && value) continuity.kind = value;
241
+ continue;
242
+ }
243
+
244
+ if (section === 'requirement-refs' && listItem) {
245
+ currentRef = flushRequirementRef(requirementRefs, currentRef) || {};
246
+ currentRef = { [listItem[1].toLowerCase()]: unwrapScalar(stripInlineYamlComment(listItem[2])) };
247
+ continue;
248
+ }
249
+
250
+ if (section === 'requirement-refs' && nested) {
251
+ currentRef = currentRef || {};
252
+ currentRef[nested[1].toLowerCase()] = unwrapScalar(stripInlineYamlComment(nested[2]));
253
+ continue;
254
+ }
255
+
256
+ if (section === 'continuity' && nested) {
257
+ const key = nested[1].toLowerCase().replace(/-/g, '_');
258
+ const value = unwrapScalar(stripInlineYamlComment(nested[2]));
259
+ if (key === 'base_capabilities') {
260
+ continuity.baseCapabilities = continuity.baseCapabilities || [];
261
+ currentCap = {};
262
+ continuity.baseCapabilities.push(currentCap);
263
+ } else if (currentCap && ['anchor', 'entity_id', 'current_version_id'].includes(key)) {
264
+ currentCap[key] = value;
265
+ } else {
266
+ continuity[key] = value;
267
+ }
268
+ continue;
269
+ }
270
+
271
+ if (section === 'continuity' && listItem) {
272
+ continuity.baseCapabilities = continuity.baseCapabilities || [];
273
+ if (!currentCap || Object.keys(currentCap).length > 0) {
274
+ currentCap = {};
275
+ continuity.baseCapabilities.push(currentCap);
276
+ }
277
+ currentCap[listItem[1].toLowerCase().replace(/-/g, '_')] = unwrapScalar(
278
+ stripInlineYamlComment(listItem[2]),
279
+ );
280
+ }
281
+ }
282
+ flushRequirementRef(requirementRefs, currentRef);
283
+ return { flat, requirementRefs, continuity };
284
+ }
285
+
184
286
  function parseIdentityBlock(lines, startIndex, endIndex = lines.length) {
185
287
  const values = {};
186
288
  let started = false;
@@ -217,8 +319,12 @@ function statementContentEnd(lines, startIndex) {
217
319
  }
218
320
 
219
321
  function parseProposal(target, file, lines, frontmatter) {
220
- target.proposalMode = String(frontmatter.mode || '').trim().toLowerCase();
221
- const changeId = String(frontmatter['change-id'] || '').trim().toUpperCase();
322
+ const structured = parseStructuredFrontmatter(lines);
323
+ const mergedFrontmatter = { ...structured.flat, ...frontmatter };
324
+ target.proposalMode = String(mergedFrontmatter.mode || '').trim().toLowerCase();
325
+ target.requirementRefs = structured.requirementRefs;
326
+ target.continuity = structured.continuity;
327
+ const changeId = String(mergedFrontmatter['change-id'] || '').trim().toUpperCase();
222
328
  if (changeId) {
223
329
  addEntity(
224
330
  target,
@@ -228,7 +334,7 @@ function parseProposal(target, file, lines, frontmatter) {
228
334
  file,
229
335
  1,
230
336
  { change_name: target.changeName },
231
- identityFromValues(frontmatter),
337
+ identityFromValues(mergedFrontmatter),
232
338
  file.content,
233
339
  );
234
340
  target.changeId = changeId;
@@ -248,6 +354,12 @@ function parseProposal(target, file, lines, frontmatter) {
248
354
  if (!match) continue;
249
355
  const capabilityId = match[1].toUpperCase();
250
356
  const identity = parseIdentityBlock(lines, index + 1);
357
+ if (structured.requirementRefs.length > 0) {
358
+ identity.external_refs = [
359
+ ...(identity.external_refs || []),
360
+ ...structured.requirementRefs,
361
+ ];
362
+ }
251
363
  const slug = unwrapScalar(match[2]);
252
364
  addEntity(target, 'Capability', capabilityId, slug, file, index + 1, {
253
365
  slug,
@@ -570,6 +682,8 @@ function parseChangeArtifacts(projectRoot, changeName, options = {}) {
570
682
  changeId: '',
571
683
  profile: 'simple',
572
684
  proposalMode: '',
685
+ requirementRefs: [],
686
+ continuity: {},
573
687
  artifacts: [],
574
688
  files: files.map((file) => ({ path: file.relativePath, content_hash: file.contentHash })),
575
689
  entities: [],
@@ -142,6 +142,7 @@ function recordsFor(candidate, facts, sourceRevision) {
142
142
  delta_state: entity.delta_state,
143
143
  content_hash: entity.content_hash,
144
144
  source: entity.source,
145
+ external_refs: Array.isArray(entity.external_refs) ? entity.external_refs : [],
145
146
  }));
146
147
  }
147
148
 
@@ -263,6 +264,15 @@ function buildIdentityCatalog(projectRoot, options = {}) {
263
264
  || String(left.message || '').localeCompare(String(right.message || ''))
264
265
  ));
265
266
 
267
+ const byExternalRef = new Map();
268
+ for (const record of records) {
269
+ for (const ref of record.external_refs || []) {
270
+ const key = `${ref.system}|${ref.object_type}|${ref.external_id}`;
271
+ if (!byExternalRef.has(key)) byExternalRef.set(key, []);
272
+ byExternalRef.get(key).push(record);
273
+ }
274
+ }
275
+
266
276
  return {
267
277
  records,
268
278
  factsByRoot,
@@ -270,6 +280,7 @@ function buildIdentityCatalog(projectRoot, options = {}) {
270
280
  byVersionId: groupBy(records, 'version_id'),
271
281
  byAnchor: groupBy(records, 'anchor_id'),
272
282
  byPredecessor: groupBy(records, 'predecessor_version_id'),
283
+ byExternalRef,
273
284
  diagnostics,
274
285
  };
275
286
  }
@@ -20,6 +20,13 @@ function normalizeFacts(parsed) {
20
20
  version_id: String(entity.version_id || '').trim().toLowerCase() || undefined,
21
21
  predecessor_version_id: String(entity.predecessor_version_id || '').trim().toLowerCase() || undefined,
22
22
  delta_state: String(entity.delta_state || '').trim().toLowerCase() || undefined,
23
+ external_refs: Array.isArray(entity.external_refs)
24
+ ? entity.external_refs.map((ref) => ({
25
+ system: String(ref.system || '').trim(),
26
+ object_type: String(ref.object_type || ref.objectType || '').trim().toLowerCase(),
27
+ external_id: String(ref.external_id || ref.externalId || '').trim(),
28
+ })).filter((ref) => ref.system && ref.object_type && ref.external_id)
29
+ : undefined,
23
30
  })).sort((left, right) => (
24
31
  compareText(left.id, right.id)
25
32
  || compareText(left.type, right.type)
@@ -74,6 +81,10 @@ function normalizeFacts(parsed) {
74
81
  change_id: parsed.changeId || undefined,
75
82
  profile: parsed.profile,
76
83
  proposal_mode: parsed.proposalMode || undefined,
84
+ requirement_refs: Array.isArray(parsed.requirementRefs) ? parsed.requirementRefs : undefined,
85
+ continuity: parsed.continuity && Object.keys(parsed.continuity).length > 0
86
+ ? parsed.continuity
87
+ : undefined,
77
88
  artifacts: (parsed.artifacts || []).slice().sort((left, right) => compareText(left.path, right.path)),
78
89
  files: (parsed.files || []).slice().sort((left, right) => compareText(left.path, right.path)),
79
90
  entities,
@@ -271,6 +271,11 @@ function scanChange(projectRoot, changeName, options = {}) {
271
271
  profile,
272
272
  identityHistory,
273
273
  effectiveGraph,
274
+ projectRoot,
275
+ continuityResolutionPath: path.join(
276
+ parsed.changeDir,
277
+ 'continuity-resolution.json',
278
+ ),
274
279
  });
275
280
  const reviewStatus = options.markPending && validation.valid ? 'pending' : 'draft';
276
281
  const revisionInput = {
@@ -104,6 +104,11 @@ const DIAGNOSTIC_CODES = Object.freeze({
104
104
  ARCHIVE_NOT_CONFIRMED: 'SEM_ARCHIVE_NOT_CONFIRMED',
105
105
  ARCHIVE_REVISION_MISMATCH: 'SEM_ARCHIVE_REVISION_MISMATCH',
106
106
  INHERITED_RELATION_MISSING: 'SEM_INHERITED_RELATION_MISSING',
107
+ EXTERNAL_REF_INVALID: 'EXTERNAL_REF_INVALID',
108
+ EXTERNAL_REF_TYPE_MISMATCH: 'EXTERNAL_REF_TYPE_MISMATCH',
109
+ EXTERNAL_REF_CONFLICT: 'EXTERNAL_REF_CONFLICT',
110
+ CONTINUITY_DECISION_REQUIRED: 'CONTINUITY_DECISION_REQUIRED',
111
+ CONTINUITY_IDENTITY_MISMATCH: 'CONTINUITY_IDENTITY_MISMATCH',
107
112
  });
108
113
 
109
114
  function normalizeProfile(profile, inferredProfile = 'simple') {
@@ -340,6 +340,161 @@ function validateInheritedReferences(facts, diagnostics, identityHistory) {
340
340
  }
341
341
  }
342
342
 
343
+ function validateContinuityAndExternalRefs(facts, diagnostics, options = {}) {
344
+ const allowed = {
345
+ requirement: new Set(['Capability']),
346
+ scenario: new Set(['SpecificationStatement', 'AcceptanceCriterion']),
347
+ feature: new Set(['Capability']),
348
+ };
349
+ const continuity = facts.continuity || {};
350
+ const kind = String(continuity.kind || '').toLowerCase();
351
+ const resolutionPath = options.continuityResolutionPath
352
+ || (facts.change
353
+ ? require('path').join(
354
+ options.projectRoot || process.cwd(),
355
+ 'openspec',
356
+ 'changes',
357
+ facts.change,
358
+ 'continuity-resolution.json',
359
+ )
360
+ : '');
361
+ let resolution = null;
362
+ if (resolutionPath) {
363
+ try {
364
+ const fs = require('fs');
365
+ if (fs.existsSync(resolutionPath)) {
366
+ resolution = JSON.parse(fs.readFileSync(resolutionPath, 'utf8'));
367
+ }
368
+ } catch {
369
+ resolution = null;
370
+ }
371
+ }
372
+
373
+ if (kind === 'iteration') {
374
+ const caps = (facts.entities || []).filter((entity) => entity.type === 'Capability');
375
+ for (const capability of caps) {
376
+ if (!capability.entity_id || !capability.version_id) {
377
+ diagnostics.push(diagnostic(
378
+ DIAGNOSTIC_CODES.CONTINUITY_IDENTITY_MISMATCH,
379
+ 'error',
380
+ `Continuity=iteration 时 Capability 缺少 KB 回传身份: ${capability.id}`,
381
+ {
382
+ ...sourceContext(capability),
383
+ entity_id: capability.id,
384
+ suggestion: '在 propose 用 entities/resolve 回填 entity-id / version-id',
385
+ },
386
+ ));
387
+ }
388
+ }
389
+ if (!resolution || resolution.status === 'pending') {
390
+ diagnostics.push(diagnostic(
391
+ DIAGNOSTIC_CODES.CONTINUITY_DECISION_REQUIRED,
392
+ 'error',
393
+ 'Continuity=iteration 但 continuity-resolution.json 缺失或仍为 pending',
394
+ {
395
+ file: resolutionPath || 'continuity-resolution.json',
396
+ suggestion: '在 propose/spec 完成 A/B/C 决议后再 check',
397
+ },
398
+ ));
399
+ }
400
+ }
401
+
402
+ if (resolution && resolution.status && resolution.status !== 'pending') {
403
+ const decisions = [
404
+ ...(Array.isArray(resolution.capabilities) ? resolution.capabilities : []),
405
+ ...(Array.isArray(resolution.scenarios) ? resolution.scenarios : []),
406
+ ];
407
+ const entitiesByAnchor = new Map(
408
+ (facts.entities || []).map((entity) => [String(entity.anchor_id || entity.id || '').toUpperCase(), entity]),
409
+ );
410
+ for (const decision of decisions) {
411
+ const anchor = String(decision.anchor || '').toUpperCase();
412
+ if (!anchor) continue;
413
+ const entity = entitiesByAnchor.get(anchor);
414
+ if (!entity) continue;
415
+ const decisionCode = String(decision.decision || '').toLowerCase();
416
+ const decidedEntityId = String(decision.entityId || decision.entity_id || '').toLowerCase();
417
+ const actualEntityId = String(entity.entity_id || '').toLowerCase();
418
+ if (
419
+ (decisionCode === 'reuse-existing-identity' || decisionCode === 'reuse_existing' || decisionCode === 'a')
420
+ && decidedEntityId
421
+ && actualEntityId
422
+ && decidedEntityId !== actualEntityId
423
+ ) {
424
+ diagnostics.push(diagnostic(
425
+ DIAGNOSTIC_CODES.CONTINUITY_IDENTITY_MISMATCH,
426
+ 'error',
427
+ `决议复用历史身份但产物仍使用新 entity_id: ${anchor}`,
428
+ {
429
+ ...sourceContext(entity),
430
+ entity_id: entity.id,
431
+ suggestion: '按 continuity-resolution 回填历史 entity-id / version-id',
432
+ },
433
+ ));
434
+ }
435
+ if (
436
+ (decisionCode === 'assign-new-anchor' || decisionCode === 'assign_new' || decisionCode === 'b')
437
+ && decidedEntityId
438
+ && actualEntityId
439
+ && decidedEntityId === actualEntityId
440
+ && String(decision.notes || '').toLowerCase().includes('new-anchor-required')
441
+ ) {
442
+ diagnostics.push(diagnostic(
443
+ DIAGNOSTIC_CODES.CONTINUITY_IDENTITY_MISMATCH,
444
+ 'error',
445
+ `决议为新对象但仍共用旧锚点/身份: ${anchor}`,
446
+ {
447
+ ...sourceContext(entity),
448
+ entity_id: entity.id,
449
+ suggestion: '更换新锚点并分配新 entity_id 后重跑 check',
450
+ },
451
+ ));
452
+ }
453
+ }
454
+ }
455
+
456
+ const bindingKeys = new Map();
457
+ for (const entity of facts.entities || []) {
458
+ for (const ref of entity.external_refs || []) {
459
+ const objectType = String(ref.object_type || '').toLowerCase();
460
+ const allowedTypes = allowed[objectType];
461
+ if (!allowedTypes) {
462
+ diagnostics.push(diagnostic(
463
+ DIAGNOSTIC_CODES.EXTERNAL_REF_INVALID,
464
+ 'error',
465
+ `未知 external object_type: ${objectType} (${entity.id})`,
466
+ { ...sourceContext(entity), entity_id: entity.id },
467
+ ));
468
+ continue;
469
+ }
470
+ if (!allowedTypes.has(entity.type)) {
471
+ diagnostics.push(diagnostic(
472
+ DIAGNOSTIC_CODES.EXTERNAL_REF_TYPE_MISMATCH,
473
+ 'error',
474
+ `external object_type=${objectType} 与实体类型 ${entity.type} 不匹配: ${entity.id}`,
475
+ { ...sourceContext(entity), entity_id: entity.id },
476
+ ));
477
+ }
478
+ const key = `${ref.system}|${objectType}|${ref.external_id}|${entity.anchor_id || entity.id}`;
479
+ const prior = bindingKeys.get(key);
480
+ if (prior && prior !== entity.entity_id) {
481
+ diagnostics.push(diagnostic(
482
+ DIAGNOSTIC_CODES.EXTERNAL_REF_CONFLICT,
483
+ 'error',
484
+ `同外部键+同锚点绑定了不同 entity_id: ${entity.id}`,
485
+ {
486
+ ...sourceContext(entity),
487
+ entity_id: entity.id,
488
+ suggestion: '复用历史身份或更换新锚点后重开 entity_id',
489
+ },
490
+ ));
491
+ } else if (entity.entity_id) {
492
+ bindingKeys.set(key, entity.entity_id);
493
+ }
494
+ }
495
+ }
496
+ }
497
+
343
498
  function validateTraceability(facts, options = {}) {
344
499
  const profile = normalizeProfile(options.profile || 'auto', facts.profile || 'simple');
345
500
  const graphEntities = options.effectiveGraph && options.effectiveGraph.effective_entities
@@ -356,6 +511,7 @@ function validateTraceability(facts, options = {}) {
356
511
  const entitiesById = new Map();
357
512
 
358
513
  validateArtifactPresence(facts, profile, diagnostics);
514
+ validateContinuityAndExternalRefs(facts, diagnostics, options);
359
515
 
360
516
  if (profile === 'strict') {
361
517
  for (const artifact of facts.artifacts || []) {
@@ -0,0 +1,32 @@
1
+ {
2
+ "status": "resolved",
3
+ "updatedAt": "2026-07-23T00:00:00.000Z",
4
+ "capabilities": [
5
+ {
6
+ "anchor": "CAP-ORDER-CANCEL",
7
+ "externalKey": {
8
+ "system": "requirement-mgmt",
9
+ "objectType": "requirement",
10
+ "externalId": "REQ-FI-2024-001"
11
+ },
12
+ "decision": "reuse-existing-identity",
13
+ "entityId": "00000000-0000-4000-8000-000000000001",
14
+ "currentVersionId": "00000000-0000-4000-8000-000000000011",
15
+ "notes": "propose 阶段 A/B/C:复用历史能力"
16
+ }
17
+ ],
18
+ "scenarios": [
19
+ {
20
+ "anchor": "AC-ORDER-CANCEL-001",
21
+ "externalKey": {
22
+ "system": "requirement-mgmt",
23
+ "objectType": "scenario",
24
+ "externalId": "REQ-FI-2024-001:SCN-req-date-empty-002"
25
+ },
26
+ "decision": "reuse-existing-identity",
27
+ "entityId": "00000000-0000-4000-8000-000000000021",
28
+ "currentVersionId": "00000000-0000-4000-8000-000000000031",
29
+ "notes": "spec 阶段场景冲突决议"
30
+ }
31
+ ]
32
+ }
@@ -7,6 +7,24 @@ 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 种桥)
11
+ requirement-refs:
12
+ - system: requirement-mgmt
13
+ object-type: requirement
14
+ external-id: REQ-<DOMAIN>-<NNN>
15
+ # Continuity:字段一律来自知识库 resolve,禁止本地 archive 文件夹名
16
+ continuity:
17
+ kind: new # iteration | similar-reference | new
18
+ kb-space-id: "" # KB space UUID(iteration 时必填)
19
+ kb-id: "" # KB UUID(iteration 时必填)
20
+ base-change-id: "" # 可选,来自 KB source/change_id
21
+ base-archive-id: "" # 可选溯源(KB archive_id),不是本地目录名
22
+ base-capabilities: []
23
+ # iteration 示例:
24
+ # base-capabilities:
25
+ # - anchor: CAP-<CAPABILITY>
26
+ # entity-id: <uuid>
27
+ # current-version-id: <uuid>
10
28
  ---
11
29
 
12
30
  # proposal.md - 业务意图与上下文总览
@@ -34,6 +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
38
  - **当** <!-- 触发条件 -->
38
39
  - **预期** <!-- 预期结果 -->
39
40
 
@@ -42,6 +43,7 @@ capability-id: "CAP-<CAPABILITY>" # 必须与 proposal.md 中的 Capability ID
42
43
  - **version-id**: <UUID>
43
44
  - **delta-state**: added
44
45
  - **predecessor-version**: 无
46
+ - **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<NNN>:SCN-<slug>
45
47
  - **当** <!-- 触发条件 -->
46
48
  - **预期** <!-- 预期结果 -->
47
49
 
@@ -61,6 +63,7 @@ capability-id: "CAP-<CAPABILITY>" # 必须与 proposal.md 中的 Capability ID
61
63
  - **version-id**: <新 UUID>
62
64
  - **delta-state**: <modified|added>
63
65
  - **predecessor-version**: <modified 时填写;added 为无>
66
+ - **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<NNN>:SCN-<slug>
64
67
  - **当** <!-- 触发条件 -->
65
68
  - **预期** <!-- 预期结果 -->
66
69
 
@@ -108,13 +108,17 @@ node skywalk-sdd/log.cjs archive-docs --project=. --change=<变更名称> --reas
108
108
  该命令成功后必须已经完成:
109
109
  - 活动目录 `openspec/changes/<name>/` 被移入 `openspec/changes/archive/<日期>-<name>/`。
110
110
  - 归档目录写入 `archive-ontology.json`、`canonical-facts.json`、`conversion-report.json` 和新版 `archive-manifest.json`。
111
- - 生成知识库可直接消费的 `openspec/changes/archive/<日期>-<name>.zip`,包内文件必须由 manifest 完整列举并通过 SHA-256 校验。
111
+ - 生成知识库可直接消费的 `openspec/changes/archive/<日期>-<name>.zip`(canonical-facts **v2**,含 `external_refs`),包内文件必须由 manifest 完整列举并通过 SHA-256 校验。
112
112
  - Full Spec 的 `specs/<capability>/spec.md` 同步到 `openspec/specs/<capability>/spec.md`。
113
113
  - archive 阶段写入 `stage_end`。
114
114
  - 未勾选 tasks 被写入 `archive_result.task_completion`。
115
115
  - 最终中文报告生成到 `openspec/changes/archive/<日期>-<name>/reports/<name>-report.md` 及同名 `<name>-report.html`(默认同时生成 .md 与 .html 双产物,默认归档后 archive 目录,可用 --report-output 自定义)。
116
116
  - 执行日志 `openspec/changes/archive/<日期>-<name>/logs/execution-log.md` 随归档整目录迁移(人读审计层)。
117
117
 
118
+ ### 5.5 收尾入库(opsx-kb-ingest)
119
+
120
+ 归档 zip 生成后,提示用户用知识库技能 **`opsx-kb-ingest`** 上传;成功则写 `ingest-receipt.json`。若返回 `EXTERNAL_REF_CONFLICT`,引导回 spec/check 修正后重入,**禁止**在 KB 内现场改绑。
121
+
118
122
  > 注意:`archive-docs` 成功执行后已经在内部写入 `stage_end`,因此**不要在成功的归档后再单独运行 `node skywalk-sdd/log.cjs end --command=archive ...`**。仅在第 5 步归档命令失败时,才需要运行下方的失败分支 `end`。
119
123
 
120
124
  如果该命令失败,以失败状态结束 telemetry:
@@ -216,6 +216,16 @@ node skywalk-sdd/log.cjs record --type=conformance_review --command=check --proj
216
216
 
217
217
  ---
218
218
 
219
+ ## Continuity / external_ref 确定性门禁
220
+
221
+ `opsx-check` **不联网提问**。Agent 应在 propose/spec 已问完;本阶段只验证并入既有 apply 前门禁:
222
+
223
+ - Continuity=`iteration` 时每个 CAP 具备 KB 回传 `entity-id` / `version-id`
224
+ - 已写 spec 的 Capability:场景 `external-ref` 与 `continuity-resolution.json` 决议一致;同 key+同锚点未偷偷换 entity_id
225
+ - 用户选「原对象」却仍用新 id、或选「新对象」却仍共用旧锚点 → 失败(`CONTINUITY_IDENTITY_MISMATCH` / `EXTERNAL_REF_CONFLICT`)
226
+ - 决议缺失 / pending / 与产物不一致 → `CONTINUITY_DECISION_REQUIRED`
227
+ - CI/非交互:失败即非零退出并打印修复说明,不挂起等待输入
228
+
219
229
  ## 本体语义关系门禁
220
230
 
221
231
  在其他质量检查前必须运行:
@@ -159,6 +159,27 @@ openspec instructions proposal --change "<name>" --json
159
159
 
160
160
  > 完整性检查(问题描述/目标/模块/约束 4 项)与缺失补充机制见 `./checklist.md`「§6 需求完整性检查」。发现缺失时主动询问用户补充。
161
161
 
162
+ ### 6.5 【Continuity】需求 / Capability 身份(只到 CAP,不做场景)
163
+
164
+ 在创建变更目录之后、写 proposal 能力列表之前(或紧接 CAP 编号分配前):
165
+
166
+ 1. 提取 / 询问外部需求号 `REQ-*`(没有则问一次)。
167
+ 2. 调用知识库 **`opsx-ontology-query`**(或薄封装):
168
+ ```bash
169
+ node skywalk-sdd/context-client.cjs --mode=resolve \
170
+ --external-system=requirement-mgmt \
171
+ --external-object-type=requirement \
172
+ --external-id="<REQ-...>" \
173
+ --entity-type=Capability \
174
+ --space-id="$ENGINEERING_KB_SPACE_ID" \
175
+ --kb-id="$ENGINEERING_KB_KB_ID"
176
+ ```
177
+ 3. 按 KB 结果确认 Continuity:`iteration` / `similar-reference` / `new`;勾选本次涉及的 CAP。
178
+ 4. 写入 proposal frontmatter:`requirement-refs` + `continuity`(字段来自 KB:`kb-space-id` / `kb-id` / `base-capabilities[].entity-id` / `current-version-id`)。**禁止**写本地 archive 文件夹名作为 `base-archive`。
179
+ 5. CAP 级「同 key + 同锚点、不同 entity_id」当场问 A/B/C;决议写入 `openspec/changes/<name>/continuity-resolution.json` 的 `capabilities[]`。
180
+ 6. KB 不可用 → `degraded` 继续,**禁止**扫本地 `archive/` 抄 UUID。预期:恢复后同锚点入库可能触发 `EXTERNAL_REF_CONFLICT`。
181
+ 7. **不得**在本阶段生成 STMT/AC/场景或裁决场景身份。
182
+
162
183
  ### 7. 【交互引导】文档拆分模式选择
163
184
 
164
185
  **❗ 必须主动询问用户,不得默认选择**。Full / Simple / Auto 三种模式的目录结构、适用场景与 AskUserQuestion 文案见 `./reference.md`「§7 文档拆分模式选择」。根据用户选择设置 `mode: full | simple`(Auto 按能力域数量判断),记录到 proposal.md 的 YAML frontmatter。
@@ -107,21 +107,31 @@ openspec list
107
107
 
108
108
  > 上下文类型(需求文档 / 代码文件 / API 文档)与用途见 `./reference.md`「§3 上下文类型与用途」。
109
109
 
110
- **【默认尝试】工程 Spec 知识库上下文**:
110
+ **【默认尝试】工程 Spec 知识库上下文(场景身份主战场)**:
111
111
 
112
- 从用户输入、proposal.md 中当前 Capability 的名称、业务目标、边界和约束组成一段自然语言查询。不要要求用户提供 `entity_id`。执行:
112
+ 读取 proposal Continuity。对**当前 Capability** 各调一次(「全部」= 循环 N 次,不是一次大查询):
113
113
 
114
114
  ```bash
115
- node skywalk-sdd/context-client.cjs --query="<当前 Capability 的自然语言需求>" --target-stage=spec
115
+ node skywalk-sdd/context-client.cjs \
116
+ --query="<当前 Capability 的自然语言需求>" \
117
+ --target-stage=spec \
118
+ --entity-id="<该 CAP 的 entity_id>" \
119
+ --external-system=requirement-mgmt \
120
+ --external-object-type=requirement \
121
+ --external-id="<REQ-...>" \
122
+ --space-id="$ENGINEERING_KB_SPACE_ID" \
123
+ --kb-id="$ENGINEERING_KB_KB_ID"
116
124
  ```
117
125
 
118
- - 配置项:`ENGINEERING_KB_API`、`ENGINEERING_KB_SPACE_ID`、可选 `ENGINEERING_KB_TOKEN`。
119
- - 若返回 `available=false` `degraded=true`,记录降级原因并继续 Spec 流程,不得阻塞。
120
- - 优先消费 `reuseBundles[].statements` 中的历史 STMT、AC、Constraint;`designElements` 只能作为理解上下文,不能写成 Spec 的 How
121
- - `answeredQuestions` 表示历史事实已经覆盖的内容,不要对用户重复提问。
122
- - 只向用户询问 `clarificationQuestions` 中仍与当前 Capability 有关的问题。
123
- - `reuseMode=REFERENCE` 只能参考复用,不得复制历史 UUID;只有 `reuseMode=INHERIT` 才允许按本体语义生成契约复用历史身份。
124
- - 所有知识库内容均为 advisory;若与用户输入、proposal.md 或用户确认冲突,以当前用户确认和 proposal.md 为准。
126
+ - 配置项:`ENGINEERING_KB_API`、`ENGINEERING_KB_SPACE_ID`、`ENGINEERING_KB_KB_ID`、可选 `ENGINEERING_KB_TOKEN`。查询权威技能是知识库 **`opsx-ontology-query`**;本脚本只是薄封装。
127
+ - Continuity=`iteration` 时**必须**带 `--entity-id` + external。
128
+ - **禁止**从本地 `archive/` UUID 当跨迭代继承源;跨迭代只认 KB current
129
+ - 若返回 `available=false` / `degraded=true`,记录降级并继续,不得扫本地 archive 兜底。
130
+ - `INHERIT`:unchanged 写继承引用;modified 复用 entity-id + predecessor。`REFERENCE`:只参考,新开身份。
131
+ - reuseBundle / 实体上的 `externalRefs` 写入场景 `external-ref`(`requirement-mgmt:scenario:REQ-…:SCN-…`)。
132
+ - 场景级「同 SCN key + 同锚点、不同 entity_id」在写完该 CAP identity 后、确认文档前**当场问** A/B/C;未决不得进入下一 CAP / design。决议追加到 `continuity-resolution.json` 的 `scenarios[]`。
133
+ - 优先消费 `reuseBundles[].statements`;`designElements` 只作理解上下文,不能写成 Spec 的 How。
134
+ - 所有知识库内容均为 advisory;与用户确认 / proposal 冲突时以当前确认与 proposal 为准。
125
135
 
126
136
  **【可选】业务知识库检索**:
127
137
  术语含义不清且可能影响 spec 准确性时,可调用 **opsx-knowledge** skill。
@@ -203,7 +213,7 @@ node skywalk-sdd/context-client.cjs --query="<当前 Capability 的自然语言
203
213
  - 新需求、场景和约束分别使用 `STMT-*`、`AC-*`、`CON-*`;分配规则是同前缀同 Capability 当前最大序号 + 1。
204
214
  - 修改已有实体必须复用原 ID;删除实体只写 removal 语义,不得把编号分配给新实体。
205
215
  - `added` 必须调用 `semantic-identity --delta-state=added` 生成新的实体 UUID 和版本 UUID;禁止通过复制另一需求的 UUID 创建新实体。
206
- - `modified/removed` 必须从 Archive 读取历史 `entity-id` 和直接前序 `version-id`,调用 `semantic-identity --delta-state=<modified|removed> --entity-id=<UUID> --predecessor-version=<UUID>`;实体 UUID 复用,版本 UUID 新建。
216
+ - `modified/removed` 必须从 **KB**(match-requirement / resolve)取得历史 `entity-id` 和直接前序 `version-id`,调用 `semantic-identity --delta-state=<modified|removed> --entity-id=<UUID> --predecessor-version=<UUID>`;实体 UUID 复用,版本 UUID 新建。禁止把本地 archive 目录当跨迭代继承权威。
207
217
  - AC 必须嵌套在所属 STMT 下;CON 必须通过 `**constrains**` 显式引用 STMT。
208
218
  - 对 `reuseMode=REFERENCE` 的历史候选必须创建新的实体身份;禁止因为内容相似而复用历史 `entity-id`。
209
219
  - 对 `reuseMode=INHERIT` 的历史事实,必须使用返回的实体与版本来源完成 unchanged/modified 身份参数校验。