kld-sdd 2.5.1 → 2.5.2

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.
@@ -0,0 +1,610 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ DIAGNOSTIC_CODES,
5
+ normalizeProfile,
6
+ relationDefinition,
7
+ } = require('./schema.cjs');
8
+ const { isValidEntityId, isValidUuid } = require('./id.cjs');
9
+
10
+ function diagnostic(code, severity, message, context = {}) {
11
+ return {
12
+ code,
13
+ severity,
14
+ message,
15
+ file: context.file || '',
16
+ line: Number(context.line || 0),
17
+ entity_id: context.entity_id || undefined,
18
+ relation: context.relation || undefined,
19
+ suggestion: context.suggestion || undefined,
20
+ };
21
+ }
22
+
23
+ function sourceContext(item) {
24
+ return {
25
+ file: item && item.source && item.source.file,
26
+ line: item && item.source && item.source.line,
27
+ };
28
+ }
29
+
30
+ function findTaskCycles(relations) {
31
+ const graph = new Map();
32
+ for (const relation of relations.filter((item) => item.type === 'dependsOn')) {
33
+ if (!graph.has(relation.from)) graph.set(relation.from, []);
34
+ graph.get(relation.from).push(relation.to);
35
+ }
36
+ const visiting = new Set();
37
+ const visited = new Set();
38
+ const cycles = [];
39
+
40
+ function visit(node, path) {
41
+ if (visiting.has(node)) {
42
+ const start = path.indexOf(node);
43
+ cycles.push(path.slice(start).concat(node));
44
+ return;
45
+ }
46
+ if (visited.has(node)) return;
47
+ visiting.add(node);
48
+ for (const next of graph.get(node) || []) visit(next, path.concat(node));
49
+ visiting.delete(node);
50
+ visited.add(node);
51
+ }
52
+
53
+ for (const node of graph.keys()) visit(node, []);
54
+ return cycles;
55
+ }
56
+
57
+ function validateIdentitySemantics(facts, diagnostics, identityHistory) {
58
+ const entities = facts.entities || [];
59
+ const currentByEntityId = new Map();
60
+ const currentByVersionId = new Map();
61
+
62
+ for (const entity of entities) {
63
+ const context = { ...sourceContext(entity), entity_id: entity.id };
64
+ if (!entity.entity_id || !entity.version_id) {
65
+ diagnostics.push(diagnostic(
66
+ DIAGNOSTIC_CODES.UUID_MISSING,
67
+ 'error',
68
+ `实体缺少全局 entity-id 或 version-id: ${entity.id}`,
69
+ { ...context, suggestion: '使用 semantic-identity 生成 UUID;继承实体必须复用历史 UUID' },
70
+ ));
71
+ continue;
72
+ }
73
+ if (!isValidUuid(entity.entity_id) || !isValidUuid(entity.version_id)) {
74
+ diagnostics.push(diagnostic(
75
+ DIAGNOSTIC_CODES.UUID_INVALID,
76
+ 'error',
77
+ `实体 UUID 格式非法: ${entity.id}`,
78
+ { ...context, suggestion: '重新运行 semantic-identity,禁止手写伪 UUID' },
79
+ ));
80
+ continue;
81
+ }
82
+ if (!['added', 'modified', 'unchanged', 'removed'].includes(entity.delta_state)) {
83
+ diagnostics.push(diagnostic(
84
+ DIAGNOSTIC_CODES.DELTA_STATE_INVALID,
85
+ 'error',
86
+ `实体 delta-state 缺失或非法: ${entity.id}`,
87
+ { ...context, suggestion: '使用 added、modified、unchanged 或 removed' },
88
+ ));
89
+ }
90
+ if (['modified', 'removed'].includes(entity.delta_state)
91
+ && !isValidUuid(entity.predecessor_version_id)) {
92
+ diagnostics.push(diagnostic(
93
+ DIAGNOSTIC_CODES.VERSION_LINEAGE_MISSING,
94
+ 'error',
95
+ `${entity.delta_state} 实体缺少有效 predecessor-version: ${entity.id}`,
96
+ { ...context, suggestion: '指向同一 entity-id 的直接前序 version-id' },
97
+ ));
98
+ }
99
+ if (entity.delta_state === 'added' && entity.predecessor_version_id) {
100
+ diagnostics.push(diagnostic(
101
+ DIAGNOSTIC_CODES.VERSION_LINEAGE_MISSING,
102
+ 'error',
103
+ `added 实体不应声明 predecessor-version: ${entity.id}`,
104
+ context,
105
+ ));
106
+ }
107
+
108
+ const entityOwner = currentByEntityId.get(entity.entity_id);
109
+ if (entityOwner && (entityOwner.id !== entity.id || entityOwner.type !== entity.type)) {
110
+ diagnostics.push(diagnostic(
111
+ DIAGNOSTIC_CODES.ENTITY_IDENTITY_CONFLICT,
112
+ 'error',
113
+ `同一 entity-id 被不同实体复用: ${entityOwner.id} / ${entity.id}`,
114
+ { ...context, suggestion: '不同逻辑实体必须重新生成 entity-id' },
115
+ ));
116
+ } else if (!entityOwner) {
117
+ currentByEntityId.set(entity.entity_id, entity);
118
+ }
119
+
120
+ const versionOwner = currentByVersionId.get(entity.version_id);
121
+ if (versionOwner && (versionOwner.entity_id !== entity.entity_id || versionOwner.content_hash !== entity.content_hash)) {
122
+ diagnostics.push(diagnostic(
123
+ DIAGNOSTIC_CODES.VERSION_IDENTITY_CONFLICT,
124
+ 'error',
125
+ `同一 version-id 对应不同实体或内容: ${entity.version_id}`,
126
+ { ...context, suggestion: '每个新增、修改或删除版本必须生成新的 version-id' },
127
+ ));
128
+ } else if (!versionOwner) {
129
+ currentByVersionId.set(entity.version_id, entity);
130
+ }
131
+ }
132
+
133
+ if (!Array.isArray(identityHistory)) return;
134
+ const historyByEntityId = new Map();
135
+ const historyByVersionId = new Map();
136
+ const historyByAnchor = new Map();
137
+ for (const record of identityHistory) {
138
+ if (record.entity_id) {
139
+ if (!historyByEntityId.has(record.entity_id)) historyByEntityId.set(record.entity_id, []);
140
+ historyByEntityId.get(record.entity_id).push(record);
141
+ }
142
+ if (record.version_id && !historyByVersionId.has(record.version_id)) {
143
+ historyByVersionId.set(record.version_id, record);
144
+ }
145
+ if (record.anchor_id) {
146
+ if (!historyByAnchor.has(record.anchor_id)) historyByAnchor.set(record.anchor_id, []);
147
+ historyByAnchor.get(record.anchor_id).push(record);
148
+ }
149
+ }
150
+
151
+ for (const entity of entities) {
152
+ if (!isValidUuid(entity.entity_id) || !isValidUuid(entity.version_id)) continue;
153
+ const context = { ...sourceContext(entity), entity_id: entity.id };
154
+ const priorEntities = historyByEntityId.get(entity.entity_id) || [];
155
+ const priorAnchors = historyByAnchor.get(entity.anchor_id || entity.id) || [];
156
+ const priorVersion = historyByVersionId.get(entity.version_id);
157
+
158
+ if (priorEntities.some((record) => record.anchor_id !== entity.id || record.type !== entity.type)
159
+ || priorAnchors.some((record) => record.entity_id !== entity.entity_id)) {
160
+ diagnostics.push(diagnostic(
161
+ DIAGNOSTIC_CODES.ENTITY_IDENTITY_CONFLICT,
162
+ 'error',
163
+ `entity-id 与历史锚点或类型冲突: ${entity.id}`,
164
+ { ...context, suggestion: '确认是同一逻辑实体;复制产生的新实体必须生成新 UUID' },
165
+ ));
166
+ }
167
+
168
+ if (entity.delta_state === 'added') {
169
+ if (priorEntities.length > 0) {
170
+ diagnostics.push(diagnostic(
171
+ DIAGNOSTIC_CODES.ENTITY_IDENTITY_CONFLICT,
172
+ 'error',
173
+ `added 实体复用了历史 entity-id: ${entity.id}`,
174
+ { ...context, suggestion: 'added 必须生成新的 entity-id;同一实体变化应使用 modified' },
175
+ ));
176
+ }
177
+ if (priorVersion) {
178
+ diagnostics.push(diagnostic(
179
+ DIAGNOSTIC_CODES.VERSION_IDENTITY_CONFLICT,
180
+ 'error',
181
+ `added 实体复用了历史 version-id: ${entity.version_id}`,
182
+ context,
183
+ ));
184
+ }
185
+ }
186
+
187
+ if (['modified', 'removed'].includes(entity.delta_state)) {
188
+ const predecessor = historyByVersionId.get(entity.predecessor_version_id);
189
+ if (priorEntities.length === 0 || !predecessor || predecessor.entity_id !== entity.entity_id) {
190
+ diagnostics.push(diagnostic(
191
+ DIAGNOSTIC_CODES.VERSION_LINEAGE_MISSING,
192
+ 'error',
193
+ `${entity.delta_state} 无法定位同一实体的前序版本: ${entity.id}`,
194
+ { ...context, suggestion: '从 Archive 读取 entity-id 和直接前序 version-id' },
195
+ ));
196
+ }
197
+ if (priorVersion) {
198
+ diagnostics.push(diagnostic(
199
+ DIAGNOSTIC_CODES.VERSION_IDENTITY_CONFLICT,
200
+ 'error',
201
+ `${entity.delta_state} 必须生成新的 version-id: ${entity.id}`,
202
+ context,
203
+ ));
204
+ }
205
+ const knownSuccessor = priorEntities.find((record) => (
206
+ ['modified', 'removed'].includes(record.delta_state)
207
+ && record.predecessor_version_id === entity.predecessor_version_id
208
+ && record.version_id !== entity.version_id
209
+ ));
210
+ if (knownSuccessor) {
211
+ diagnostics.push(diagnostic(
212
+ DIAGNOSTIC_CODES.VERSION_LINEAGE_CONFLICT,
213
+ 'error',
214
+ `同一 predecessor 已存在后继版本,禁止并行或陈旧分支: ${entity.id}`,
215
+ { ...context, suggestion: `以 ${knownSuccessor.change} 的版本为直接前序;不要再次继承已消费的 predecessor` },
216
+ ));
217
+ }
218
+ }
219
+
220
+ if (entity.delta_state === 'unchanged') {
221
+ if (priorEntities.length === 0 || !priorVersion || priorVersion.entity_id !== entity.entity_id) {
222
+ diagnostics.push(diagnostic(
223
+ DIAGNOSTIC_CODES.VERSION_LINEAGE_MISSING,
224
+ 'error',
225
+ `unchanged 引用无法定位既有实体版本: ${entity.id}`,
226
+ { ...context, suggestion: '复用 Archive 中同一实体的 entity-id 和 version-id' },
227
+ ));
228
+ } else if (priorVersion.anchor_id !== entity.id
229
+ || priorVersion.type !== entity.type
230
+ || priorVersion.content_hash !== entity.content_hash) {
231
+ diagnostics.push(diagnostic(
232
+ DIAGNOSTIC_CODES.VERSION_IDENTITY_CONFLICT,
233
+ 'error',
234
+ `unchanged 实体与历史版本内容或锚点不一致: ${entity.id}`,
235
+ { ...context, suggestion: 'unchanged 必须完整复用历史 anchor、entity/version UUID 和内容;正文变化应使用 modified' },
236
+ ));
237
+ }
238
+ }
239
+ }
240
+ }
241
+
242
+ function validateArtifactPresence(facts, profile, diagnostics) {
243
+ const artifacts = facts.artifacts || [];
244
+ const hasType = (type) => artifacts.some((artifact) => artifact.type === type);
245
+ const addMissing = (message, file = '') => diagnostics.push(diagnostic(
246
+ DIAGNOSTIC_CODES.ARTIFACT_MISSING,
247
+ 'error',
248
+ message,
249
+ { file, line: 1 },
250
+ ));
251
+
252
+ if (!hasType('proposal')) addMissing('ontology v2 Change 缺少 proposal.md');
253
+ if (!hasType('spec')) addMissing(`${profile} Change 缺少 spec.md`);
254
+ if (profile !== 'full' && profile !== 'strict') return;
255
+
256
+ const activeCapabilities = (facts.entities || []).filter((entity) => (
257
+ entity.type === 'Capability' && entity.delta_state !== 'removed'
258
+ ));
259
+ for (const capability of activeCapabilities) {
260
+ for (const type of ['spec', 'design', 'tasks']) {
261
+ if (artifacts.some((artifact) => artifact.type === type && artifact.capability_id === capability.id)) continue;
262
+ addMissing(`Full Capability ${capability.id} 缺少 ${type} 产物`, capability.source && capability.source.file);
263
+ }
264
+ }
265
+ }
266
+
267
+ function validateInheritedReferences(facts, diagnostics, identityHistory) {
268
+ const declaredAnchors = new Set((facts.entities || []).map((entity) => entity.id));
269
+ const references = facts.inherited_references || [];
270
+ const historyByVersionId = new Map();
271
+ if (Array.isArray(identityHistory)) {
272
+ for (const record of identityHistory) {
273
+ if (record.version_id && !historyByVersionId.has(record.version_id)) {
274
+ historyByVersionId.set(record.version_id, record);
275
+ }
276
+ }
277
+ }
278
+
279
+ for (const reference of references) {
280
+ const context = {
281
+ ...sourceContext(reference),
282
+ entity_id: reference.anchor_id,
283
+ suggestion: 'unchanged 只复用 Archive 中的实体/版本 UUID、来源锚点和内容哈希,不复制正文',
284
+ };
285
+ if (!isValidUuid(reference.entity_id) || !isValidUuid(reference.version_id)) {
286
+ diagnostics.push(diagnostic(
287
+ DIAGNOSTIC_CODES.UUID_INVALID,
288
+ 'error',
289
+ `unchanged 引用缺少有效 UUID: ${reference.anchor_id}`,
290
+ context,
291
+ ));
292
+ continue;
293
+ }
294
+ if (reference.delta_state !== 'unchanged') {
295
+ diagnostics.push(diagnostic(
296
+ DIAGNOSTIC_CODES.DELTA_STATE_INVALID,
297
+ 'error',
298
+ `继承引用必须使用 unchanged: ${reference.anchor_id}`,
299
+ context,
300
+ ));
301
+ }
302
+ if (declaredAnchors.has(reference.anchor_id)) {
303
+ diagnostics.push(diagnostic(
304
+ DIAGNOSTIC_CODES.ENTITY_IDENTITY_CONFLICT,
305
+ 'error',
306
+ `同一锚点不能同时声明本次实体和 unchanged 引用: ${reference.anchor_id}`,
307
+ context,
308
+ ));
309
+ }
310
+ if (!reference.source_ref || !/^[a-f0-9]{64}$/i.test(reference.source_version_hash || '')) {
311
+ diagnostics.push(diagnostic(
312
+ DIAGNOSTIC_CODES.SOURCE_MISSING,
313
+ 'error',
314
+ `unchanged 引用缺少来源路径或版本内容哈希: ${reference.anchor_id}`,
315
+ context,
316
+ ));
317
+ }
318
+
319
+ if (!Array.isArray(identityHistory)) continue;
320
+ const priorVersion = historyByVersionId.get(reference.version_id);
321
+ if (!priorVersion
322
+ || priorVersion.entity_id !== reference.entity_id
323
+ || priorVersion.anchor_id !== reference.anchor_id) {
324
+ diagnostics.push(diagnostic(
325
+ DIAGNOSTIC_CODES.VERSION_LINEAGE_MISSING,
326
+ 'error',
327
+ `unchanged 引用无法定位历史实体版本: ${reference.anchor_id}`,
328
+ context,
329
+ ));
330
+ continue;
331
+ }
332
+ if (reference.source_version_hash && priorVersion.content_hash !== reference.source_version_hash) {
333
+ diagnostics.push(diagnostic(
334
+ DIAGNOSTIC_CODES.INHERITED_SOURCE_MISMATCH,
335
+ 'error',
336
+ `unchanged 来源内容哈希与 Archive 不一致: ${reference.anchor_id}`,
337
+ context,
338
+ ));
339
+ }
340
+ }
341
+ }
342
+
343
+ function validateTraceability(facts, options = {}) {
344
+ const profile = normalizeProfile(options.profile || 'auto', facts.profile || 'simple');
345
+ const graphEntities = options.effectiveGraph && options.effectiveGraph.effective_entities
346
+ ? options.effectiveGraph.effective_entities
347
+ : (facts.entities || []);
348
+ const graphRelations = options.effectiveGraph && options.effectiveGraph.effective_relations
349
+ ? options.effectiveGraph.effective_relations
350
+ : (facts.relations || []);
351
+ const diagnostics = (facts.diagnostics || []).map((item) => (
352
+ profile === 'strict' && item.code === DIAGNOSTIC_CODES.ID_MISSING
353
+ ? { ...item, severity: 'error' }
354
+ : item
355
+ ));
356
+ const entitiesById = new Map();
357
+
358
+ validateArtifactPresence(facts, profile, diagnostics);
359
+
360
+ if (profile === 'strict') {
361
+ for (const artifact of facts.artifacts || []) {
362
+ if (!['spec', 'design', 'tasks'].includes(artifact.type) || artifact.capability_id) continue;
363
+ diagnostics.push(diagnostic(
364
+ DIAGNOSTIC_CODES.ID_MISSING,
365
+ 'error',
366
+ `${artifact.type} 产物缺少 capability-id`,
367
+ {
368
+ file: artifact.path,
369
+ line: 1,
370
+ suggestion: '在 YAML frontmatter 中写入 proposal 已声明的 CAP ID',
371
+ },
372
+ ));
373
+ }
374
+ }
375
+
376
+ for (const entity of facts.entities || []) {
377
+ if (!isValidEntityId(entity.id, entity.type)) {
378
+ diagnostics.push(diagnostic(
379
+ DIAGNOSTIC_CODES.ID_INVALID,
380
+ 'error',
381
+ `实体 ID 与类型不匹配: ${entity.id} (${entity.type})`,
382
+ { ...sourceContext(entity), entity_id: entity.id, suggestion: '使用模板规定的实体前缀和稳定编号格式' },
383
+ ));
384
+ }
385
+ if (!entity.source || !entity.source.file || !entity.source.anchor_id) {
386
+ diagnostics.push(diagnostic(
387
+ DIAGNOSTIC_CODES.SOURCE_MISSING,
388
+ 'error',
389
+ `实体缺少可定位来源: ${entity.id}`,
390
+ { ...sourceContext(entity), entity_id: entity.id },
391
+ ));
392
+ }
393
+ if (!entitiesById.has(entity.id)) entitiesById.set(entity.id, []);
394
+ entitiesById.get(entity.id).push(entity);
395
+ }
396
+
397
+ validateIdentitySemantics(facts, diagnostics, options.identityHistory);
398
+ validateInheritedReferences(facts, diagnostics, options.identityHistory);
399
+
400
+ for (const [entityId, declarations] of entitiesById.entries()) {
401
+ if (declarations.length <= 1) continue;
402
+ const first = declarations[0];
403
+ diagnostics.push(diagnostic(
404
+ DIAGNOSTIC_CODES.ID_DUPLICATE,
405
+ 'error',
406
+ `实体 ID 重复声明: ${entityId}`,
407
+ { ...sourceContext(first), entity_id: entityId, suggestion: '保留原实体 ID;为真正新增实体分配下一个可用序号' },
408
+ ));
409
+ }
410
+
411
+ const graphEntitiesById = new Map();
412
+ for (const entity of graphEntities) {
413
+ if (!graphEntitiesById.has(entity.id)) graphEntitiesById.set(entity.id, entity);
414
+ }
415
+ const entityType = new Map();
416
+ const removedEntityIds = new Set();
417
+ for (const [entityId, entity] of graphEntitiesById.entries()) {
418
+ entityType.set(entityId, entity.type);
419
+ if (entity.delta_state === 'removed') removedEntityIds.add(entityId);
420
+ }
421
+ for (const reference of facts.inherited_references || []) {
422
+ if (!entityType.has(reference.anchor_id) && reference.type) {
423
+ entityType.set(reference.anchor_id, reference.type);
424
+ }
425
+ }
426
+
427
+ for (const relation of graphRelations) {
428
+ if (relation.assertion_type === 'inferred' && !String(relation.rule_id || '').trim()) {
429
+ diagnostics.push(diagnostic(
430
+ DIAGNOSTIC_CODES.RELATION_ASSERTION_INVALID,
431
+ 'error',
432
+ `inferred 关系缺少稳定 rule_id: ${relation.from} -${relation.type}-> ${relation.to}`,
433
+ { ...sourceContext(relation), relation: relation.id },
434
+ ));
435
+ }
436
+ const definition = relationDefinition(relation.type);
437
+ if (!definition) {
438
+ diagnostics.push(diagnostic(
439
+ DIAGNOSTIC_CODES.RELATION_UNKNOWN,
440
+ 'error',
441
+ `未知关系类型: ${relation.type}`,
442
+ { ...sourceContext(relation), relation: relation.id },
443
+ ));
444
+ continue;
445
+ }
446
+ if (!entityType.has(relation.from) || !entityType.has(relation.to)) {
447
+ diagnostics.push(diagnostic(
448
+ DIAGNOSTIC_CODES.RELATION_TARGET_MISSING,
449
+ 'error',
450
+ `关系引用不存在的实体: ${relation.from} -${relation.type}-> ${relation.to}`,
451
+ { ...sourceContext(relation), relation: relation.id, suggestion: '修正引用,或先在对应上游产物中声明实体' },
452
+ ));
453
+ continue;
454
+ }
455
+ if (removedEntityIds.has(relation.from) || removedEntityIds.has(relation.to)) {
456
+ diagnostics.push(diagnostic(
457
+ DIAGNOSTIC_CODES.RELATION_TARGET_REMOVED,
458
+ 'error',
459
+ `当前有效关系不能引用 removed 实体: ${relation.from} -${relation.type}-> ${relation.to}`,
460
+ { ...sourceContext(relation), relation: relation.id },
461
+ ));
462
+ continue;
463
+ }
464
+ const fromType = entityType.get(relation.from);
465
+ const toType = entityType.get(relation.to);
466
+ const matchesDomainRange = definition.domain.includes(fromType) && definition.range.includes(toType);
467
+ const matchesAllowedPair = !definition.pairs
468
+ || definition.pairs.some(([allowedFrom, allowedTo]) => allowedFrom === fromType && allowedTo === toType);
469
+ if (!matchesDomainRange || !matchesAllowedPair) {
470
+ diagnostics.push(diagnostic(
471
+ DIAGNOSTIC_CODES.RELATION_DOMAIN_RANGE,
472
+ 'error',
473
+ `关系定义域/值域非法: ${fromType} -${relation.type}-> ${toType}`,
474
+ { ...sourceContext(relation), relation: relation.id, suggestion: '按照本体 Schema 调整关系方向或目标实体类型' },
475
+ ));
476
+ }
477
+ }
478
+
479
+ const activeEntities = graphEntities.filter((entity) => entity.delta_state !== 'removed');
480
+ const statements = activeEntities.filter((entity) => entity.type === 'SpecificationStatement');
481
+ const acceptances = activeEntities.filter((entity) => entity.type === 'AcceptanceCriterion');
482
+ const designs = activeEntities.filter((entity) => entity.type === 'DesignElement');
483
+ const tasks = activeEntities.filter((entity) => entity.type === 'Task');
484
+ const relations = graphRelations.filter((relation) => (
485
+ relation.assertion_type === 'asserted'
486
+ || (relation.assertion_type === 'inferred' && String(relation.rule_id || '').trim())
487
+ ));
488
+
489
+ const capabilityByEntity = new Map();
490
+ for (const entity of activeEntities) {
491
+ const capabilityId = entity.type === 'Capability'
492
+ ? entity.id
493
+ : entity.attributes && entity.attributes.capability_id;
494
+ if (capabilityId) capabilityByEntity.set(entity.id, capabilityId);
495
+ }
496
+ for (const acceptance of acceptances) {
497
+ const statementId = acceptance.attributes && acceptance.attributes.statement_id;
498
+ if (statementId && capabilityByEntity.has(statementId)) {
499
+ capabilityByEntity.set(acceptance.id, capabilityByEntity.get(statementId));
500
+ }
501
+ }
502
+ for (const relation of relations) {
503
+ const fromCapability = capabilityByEntity.get(relation.from);
504
+ const toCapability = capabilityByEntity.get(relation.to);
505
+ if (fromCapability && toCapability && fromCapability !== toCapability) {
506
+ diagnostics.push(diagnostic(
507
+ DIAGNOSTIC_CODES.CAPABILITY_BOUNDARY,
508
+ 'error',
509
+ `关系跨越 Capability 边界: ${relation.from}(${fromCapability}) -${relation.type}-> ${relation.to}(${toCapability})`,
510
+ { ...sourceContext(relation), relation: relation.id },
511
+ ));
512
+ }
513
+ }
514
+
515
+ for (const statement of statements) {
516
+ if (!relations.some((relation) => relation.type === 'acceptedBy' && relation.from === statement.id)) {
517
+ diagnostics.push(diagnostic(
518
+ DIAGNOSTIC_CODES.STMT_WITHOUT_AC,
519
+ 'error',
520
+ `STMT 缺少验收条件: ${statement.id}`,
521
+ { ...sourceContext(statement), entity_id: statement.id, suggestion: '在该需求项下新增带 AC 编号的场景' },
522
+ ));
523
+ }
524
+ if ((profile === 'full' || profile === 'strict') && !relations.some((relation) => relation.type === 'realizes' && relation.to === statement.id)) {
525
+ diagnostics.push(diagnostic(
526
+ DIAGNOSTIC_CODES.STMT_WITHOUT_DESIGN,
527
+ 'error',
528
+ `Full 链路中 STMT 缺少 Design: ${statement.id}`,
529
+ { ...sourceContext(statement), entity_id: statement.id, suggestion: '新增 DES 实体并显式写入 realizes 引用' },
530
+ ));
531
+ }
532
+ }
533
+
534
+ for (const acceptance of acceptances) {
535
+ const owners = relations.filter((relation) => relation.type === 'acceptedBy' && relation.to === acceptance.id);
536
+ if (owners.length !== 1) {
537
+ diagnostics.push(diagnostic(
538
+ DIAGNOSTIC_CODES.AC_WITHOUT_STMT,
539
+ 'error',
540
+ `AC 必须且只能归属一个有效 STMT: ${acceptance.id}`,
541
+ { ...sourceContext(acceptance), entity_id: acceptance.id },
542
+ ));
543
+ }
544
+ }
545
+
546
+ for (const task of tasks) {
547
+ if (!relations.some((relation) => (
548
+ relation.from === task.id && (relation.type === 'implements' || relation.type === 'covers')
549
+ ))) {
550
+ diagnostics.push(diagnostic(
551
+ DIAGNOSTIC_CODES.TASK_WITHOUT_UPSTREAM,
552
+ 'error',
553
+ `Task 缺少 implements 或 covers 上游关系: ${task.id}`,
554
+ { ...sourceContext(task), entity_id: task.id },
555
+ ));
556
+ }
557
+ }
558
+
559
+ if (profile === 'full' || profile === 'strict') {
560
+ for (const design of designs) {
561
+ if (!relations.some((relation) => relation.type === 'implements' && relation.to === design.id)) {
562
+ diagnostics.push(diagnostic(
563
+ DIAGNOSTIC_CODES.DESIGN_WITHOUT_TASK,
564
+ 'error',
565
+ `Full 链路中 Design 缺少 Task: ${design.id}`,
566
+ { ...sourceContext(design), entity_id: design.id, suggestion: '新增 TASK 实体并显式写入 implements 引用' },
567
+ ));
568
+ }
569
+ }
570
+ }
571
+
572
+ for (const cycle of findTaskCycles(relations)) {
573
+ const taskId = cycle[0];
574
+ const task = graphEntitiesById.get(taskId);
575
+ diagnostics.push(diagnostic(
576
+ DIAGNOSTIC_CODES.TASK_DEPENDENCY_CYCLE,
577
+ 'error',
578
+ `Task 依赖成环: ${cycle.join(' -> ')}`,
579
+ { ...sourceContext(task), entity_id: taskId, suggestion: '移除至少一条 dependsOn 关系以恢复 DAG' },
580
+ ));
581
+ }
582
+
583
+ diagnostics.sort((left, right) => (
584
+ String(left.code).localeCompare(String(right.code))
585
+ || String(left.file || '').localeCompare(String(right.file || ''))
586
+ || Number(left.line || 0) - Number(right.line || 0)
587
+ || String(left.message || '').localeCompare(String(right.message || ''))
588
+ ));
589
+
590
+ return {
591
+ profile,
592
+ valid: !diagnostics.some((item) => item.severity === 'error'),
593
+ diagnostics,
594
+ counts: {
595
+ entities: graphEntities.length,
596
+ relations: graphRelations.length,
597
+ inherited_references: (facts.inherited_references || []).length,
598
+ errors: diagnostics.filter((item) => item.severity === 'error').length,
599
+ warnings: diagnostics.filter((item) => item.severity === 'warning').length,
600
+ },
601
+ };
602
+ }
603
+
604
+ module.exports = {
605
+ diagnostic,
606
+ findTaskCycles,
607
+ validateIdentitySemantics,
608
+ validateInheritedReferences,
609
+ validateTraceability,
610
+ };
@@ -1,3 +1,7 @@
1
+ ---
2
+ capability-id: "CAP-<CAPABILITY>" # 必须与对应 spec.md 一致
3
+ ---
4
+
1
5
  # 局部技术实现方案 - [Capability 名称]
2
6
 
3
7
  > **定位**:单一 Capability 的业务维度技术实现方案
@@ -8,6 +12,20 @@
8
12
 
9
13
  ---
10
14
 
15
+ ## 0. 本体语义锚点
16
+
17
+ <!-- 每个独立设计单元创建一个 DES;修改既有设计时复用原 ID -->
18
+
19
+ ### [DES-<CAPABILITY>-NNN] <!-- 设计单元名称 -->
20
+ - **entity-id**: <UUID>
21
+ - **version-id**: <UUID>
22
+ - **delta-state**: added
23
+ - **predecessor-version**: 无
24
+ - **realizes**: STMT-<CAPABILITY>-NNN
25
+ - **设计范围**: <!-- 本设计负责实现的局部范围 -->
26
+
27
+ ---
28
+
11
29
  ## 1. 字段完整性追溯表
12
30
 
13
31
  > **⛔ 核心红线**:用户在 Spec 中输入的所有字段必须在此表中体现,严禁无故丢弃!
@@ -1,5 +1,11 @@
1
1
  ---
2
2
  # 【用户选择配置 - 由 /opsx:propose 引导填写】
3
+ change-id: "CHG-<CHANGE-SLUG>" # 创建时生成,后续不得修改
4
+ entity-id: "<UUID>" # Change 的全局逻辑实体 UUID,added 时生成
5
+ version-id: "<UUID>" # 本次 Change 版本 UUID
6
+ delta-state: "added"
7
+ predecessor-version: "" # added 留空;modified/removed 指向直接前序版本
8
+ mode: "" # full=分 Capability 产物,simple=根目录精简产物
3
9
  test-strategy: "" # tdd=测试先行, impl-first=实现优先, none=无测试
4
10
  ---
5
11
 
@@ -7,7 +13,7 @@ test-strategy: "" # tdd=测试先行, impl-first=实现优先, none=无测试
7
13
 
8
14
  > **定位**:变更的业务意图(Why)与上下文总览
9
15
  >
10
- > **可选性**:【可跳过,直入spec】若跳过,必须将"影响范围"在 specs 中补齐
16
+ > **必需性**:ontology v2 新产物必须保留 proposal,用于承载 Change/Capability 身份和 profile;旧 proposal-less Simple 只能兼容扫描,不能直接归档为 confirmed
11
17
 
12
18
  ---
13
19
 
@@ -41,11 +47,19 @@ test-strategy: "" # tdd=测试先行, impl-first=实现优先, none=无测试
41
47
 
42
48
  ### 3.1 新增能力
43
49
  <!-- 每个能力会创建 specs/<name>/spec.md,使用 kebab-case 命名 -->
44
- - `<capability-name>`: <能力简要描述>
50
+ - [CAP-<CAPABILITY>] `<capability-name>`: <能力简要描述>
51
+ - **entity-id**: <UUID>
52
+ - **version-id**: <UUID>
53
+ - **delta-state**: added
54
+ - **predecessor-version**: 无
45
55
 
46
56
  ### 3.2 修改能力
47
- <!-- 仅当已有能力的需求级别变更时填写,检查 openspec/specs/ 现有规格 -->
48
- - `<existing-name>`: <修改什么需求>
57
+ <!-- 修改既有能力必须复用原 CAP ID,不得重新编号 -->
58
+ - [CAP-<EXISTING>] `<existing-name>`: <修改什么需求>
59
+ - **entity-id**: <复用历史 UUID>
60
+ - **version-id**: <新 UUID>
61
+ - **delta-state**: modified
62
+ - **predecessor-version**: <直接前序 version UUID>
49
63
 
50
64
  ---
51
65
 
@@ -103,5 +117,4 @@ test-strategy: "" # tdd=测试先行, impl-first=实现优先, none=无测试
103
117
  > - [ ] 逻辑链路已闭环
104
118
  > - [ ] 受影响模块已明确
105
119
  > - [ ] 依赖关系已梳理
106
- > - [ ] 若跳过本文档,影响范围已在 specs 中补齐
107
- > - [ ] 能力分解章节已明确列出所有能力
120
+ > - [ ] 能力分解章节已明确列出所有能力