kld-sdd 2.6.2 → 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.
Files changed (34) hide show
  1. package/package.json +2 -2
  2. package/skywalk-sdd/context-client.cjs +54 -14
  3. package/skywalk-sdd/ontology/archive-package.cjs +35 -4
  4. package/skywalk-sdd/ontology/artifact-parser.cjs +119 -5
  5. package/skywalk-sdd/ontology/identity-index.cjs +11 -0
  6. package/skywalk-sdd/ontology/normalizer.cjs +11 -0
  7. package/skywalk-sdd/ontology/runtime.cjs +5 -0
  8. package/skywalk-sdd/ontology/schema.cjs +5 -0
  9. package/skywalk-sdd/ontology/traceability-validator.cjs +156 -0
  10. package/templates/openspec/continuity-resolution.example.json +32 -0
  11. package/templates/openspec/proposal.md +18 -0
  12. package/templates/openspec/spec.md +3 -0
  13. package/templates/skills/kld-sdd/opsx-apply/SKILL.md +2 -1
  14. package/templates/skills/kld-sdd/opsx-apply/checklist.md +2 -2
  15. package/templates/skills/kld-sdd/opsx-archive/SKILL.md +5 -1
  16. package/templates/skills/kld-sdd/opsx-check/SKILL.md +15 -1
  17. package/templates/skills/kld-sdd/opsx-check/checklist.md +5 -1
  18. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +21 -0
  19. package/templates/skills/kld-sdd/opsx-spec/SKILL.md +21 -11
  20. package/templates/skills/kld-sdd/opsx-spec/checklist.md +1 -0
  21. package/templates/skills/kld-sdd/opsx-task/SKILL.md +2 -1
  22. package/templates/skills/kld-sdd/opsx-task/checklist.md +1 -1
  23. package/templates/skills/kld-sdd/opsx-task/reference.md +7 -5
  24. package/templates/skills/kld-sdd/opsx-tdd-anti-patterns/SKILL.md +5 -0
  25. package/templates/skills/kld-sdd/opsx-tdd-core/SKILL.md +64 -4
  26. package/templates/skills/kld-sdd/opsx-tdd-core/checklist.md +3 -1
  27. package/templates/skills/kld-sdd/opsx-tdd-core/reference.md +13 -9
  28. package/templates/skills/kld-sdd/opsx-tdd-metrics/SKILL.md +1 -0
  29. package/templates/skills/kld-sdd/opsx-tdd-review/SKILL.md +39 -0
  30. package/templates/skills/kld-sdd/opsx-tdd-rules/rules/controller-strategy.md +33 -11
  31. package/templates/skills/kld-sdd/opsx-tdd-rules/rules/dag-generation-rules.md +13 -0
  32. package/templates/skills/kld-sdd/opsx-tdd-rules/rules/non-tdd-modules.md +4 -3
  33. package/templates/skills/kld-sdd/opsx-tdd-rules/rules/task-type-definitions.md +7 -0
  34. package/templates/skills/kld-sdd/opsx-test/SKILL.md +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kld-sdd",
3
- "version": "2.6.2",
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') {