kld-sdd 2.5.0 → 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.
- package/README.md +95 -8
- package/kld-sdd-guide.html +1109 -0
- package/lib/command-bridge.js +156 -0
- package/lib/deploy-codebuddy-hooks.js +99 -0
- package/lib/hook-gate-core.js +333 -0
- package/lib/init.js +137 -82
- package/lib/settings-merge.js +85 -0
- package/lib/skills-bundle.js +142 -5
- package/lib/tool-profiles.js +270 -0
- package/package.json +4 -2
- package/skywalk-sdd/index.cjs +329 -24
- package/skywalk-sdd/ontology/artifact-observer.cjs +91 -0
- package/skywalk-sdd/ontology/artifact-parser.cjs +621 -0
- package/skywalk-sdd/ontology/change-lock.cjs +126 -0
- package/skywalk-sdd/ontology/cli.cjs +146 -0
- package/skywalk-sdd/ontology/effective-graph.cjs +158 -0
- package/skywalk-sdd/ontology/id.cjs +126 -0
- package/skywalk-sdd/ontology/identity-index.cjs +262 -0
- package/skywalk-sdd/ontology/normalizer.cjs +107 -0
- package/skywalk-sdd/ontology/runtime.cjs +341 -0
- package/skywalk-sdd/ontology/schema.cjs +139 -0
- package/skywalk-sdd/ontology/structural-identity.cjs +77 -0
- package/skywalk-sdd/ontology/traceability-validator.cjs +610 -0
- package/templates/commands/kunlunzhima/skill-bridge.md +23 -0
- package/templates/hooks/codebuddy/hooks/sdd-apply-gate.cjs +16 -0
- package/templates/hooks/codebuddy/hooks/sdd-apply-test-gate.cjs +395 -0
- package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +123 -0
- package/templates/hooks/codebuddy/hooks/sdd-pre-tool.cjs +16 -0
- package/templates/hooks/codebuddy/hooks/sdd-prompt.cjs +48 -0
- package/templates/hooks/codebuddy/hooks/sdd-skill-apply-gate.cjs +16 -0
- package/templates/hooks/codebuddy/hooks/sdd-stop.cjs +70 -0
- package/templates/hooks/codebuddy/settings.json +72 -0
- package/templates/openspec/design.md +18 -0
- package/templates/openspec/proposal.md +19 -6
- package/templates/openspec/spec.md +62 -8
- package/templates/openspec/tasks.md +28 -6
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +5 -5
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +9 -0
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +17 -1
- package/templates/skills/kld-sdd/opsx-design/SKILL.md +10 -1
- package/templates/skills/kld-sdd/opsx-explore/SKILL.md +1 -1
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +11 -1
- package/templates/skills/kld-sdd/opsx-rules/SKILL.md +131 -0
- package/templates/skills/kld-sdd/opsx-rules/checklist.md +27 -0
- package/templates/skills/kld-sdd/opsx-rules/reference.md +124 -0
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +12 -1
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +10 -1
- package/templates/skills/kld-sdd/opsx-test/SKILL.md +1 -1
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "${BRIDGE_NAME}"
|
|
3
|
+
description: "${BRIDGE_DESCRIPTION}"
|
|
4
|
+
argument-hint: "${BRIDGE_ARGUMENT_HINT}"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# SDD KunlunZhima Skill Bridge
|
|
8
|
+
|
|
9
|
+
本命令是 KunlunZhima IDE 的 SDD Skill 桥接入口,不包含 Skill 的具体执行步骤。
|
|
10
|
+
|
|
11
|
+
## Skill 路径
|
|
12
|
+
|
|
13
|
+
请**完整读取并严格遵循**以下 Skill 文件:
|
|
14
|
+
|
|
15
|
+
`${SKILL_RELATIVE_PATH}`
|
|
16
|
+
|
|
17
|
+
## 用户输入
|
|
18
|
+
|
|
19
|
+
将本命令后的用户输入作为 Skill 的输入参数传递。
|
|
20
|
+
|
|
21
|
+
## 相对引用
|
|
22
|
+
|
|
23
|
+
Skill 正文中的相对路径引用均以 Skill 所在目录(`${SKILL_DIR}`)解析。
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const core = require('./hook-gate-core.cjs');
|
|
5
|
+
|
|
6
|
+
const parsed = core.parseHookInput(core.readStdin(), { strict: true });
|
|
7
|
+
if (!parsed.ok) {
|
|
8
|
+
core.blockWithReason(parsed.error);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const input = core.normalizeHookInput(parsed.input);
|
|
12
|
+
const result = core.evaluateApplyWriteGate(input, 'codebuddy');
|
|
13
|
+
if (result.action === 'block') {
|
|
14
|
+
core.blockWithReason(result.reason);
|
|
15
|
+
}
|
|
16
|
+
core.allowExit();
|