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,341 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { SCHEMA_VERSION, normalizeProfile } = require('./schema.cjs');
|
|
7
|
+
const { safeChangeName, parseChangeArtifacts } = require('./artifact-parser.cjs');
|
|
8
|
+
const { normalizeFacts } = require('./normalizer.cjs');
|
|
9
|
+
const { validateTraceability } = require('./traceability-validator.cjs');
|
|
10
|
+
const { buildIdentityCatalog } = require('./identity-index.cjs');
|
|
11
|
+
const { buildEffectiveGraph } = require('./effective-graph.cjs');
|
|
12
|
+
const { generateUuidV7 } = require('./id.cjs');
|
|
13
|
+
const {
|
|
14
|
+
acquireChangeLock,
|
|
15
|
+
assertChangeLock,
|
|
16
|
+
releaseChangeLock,
|
|
17
|
+
} = require('./change-lock.cjs');
|
|
18
|
+
|
|
19
|
+
function sha256(value) {
|
|
20
|
+
return crypto.createHash('sha256').update(value).digest('hex');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function statePaths(projectRoot, changeName, revision = '') {
|
|
24
|
+
const root = path.resolve(projectRoot || process.cwd());
|
|
25
|
+
const safeName = safeChangeName(changeName);
|
|
26
|
+
const dir = path.join(root, 'skywalk-sdd', 'state', 'ontology', safeName);
|
|
27
|
+
const revisions = path.join(dir, 'revisions');
|
|
28
|
+
const revisionDir = revision ? path.join(revisions, revision) : '';
|
|
29
|
+
return {
|
|
30
|
+
dir,
|
|
31
|
+
revisions,
|
|
32
|
+
current: path.join(dir, 'current.json'),
|
|
33
|
+
revisionDir,
|
|
34
|
+
working: revisionDir
|
|
35
|
+
? path.join(revisionDir, 'working-ontology.json')
|
|
36
|
+
: path.join(dir, 'working-ontology.json'),
|
|
37
|
+
diagnostics: revisionDir
|
|
38
|
+
? path.join(revisionDir, 'diagnostics.json')
|
|
39
|
+
: path.join(dir, 'diagnostics.json'),
|
|
40
|
+
fileIndex: revisionDir
|
|
41
|
+
? path.join(revisionDir, 'file-index.json')
|
|
42
|
+
: path.join(dir, 'file-index.json'),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function serialized(value) {
|
|
47
|
+
return JSON.stringify(value, null, 2) + '\n';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function atomicWriteIfChanged(filePath, value) {
|
|
51
|
+
const content = serialized(value);
|
|
52
|
+
if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === content) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
56
|
+
const tempPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
57
|
+
fs.writeFileSync(tempPath, content, 'utf8');
|
|
58
|
+
fs.renameSync(tempPath, filePath);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function stateError(code, message) {
|
|
63
|
+
const error = new Error(`${code}: ${message}`);
|
|
64
|
+
error.code = code;
|
|
65
|
+
return error;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readJson(filePath, code) {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw stateError(code, `无法读取状态文件 ${filePath}: ${error.message}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readRevisionBundle(paths, expectedRevision) {
|
|
77
|
+
for (const filePath of [paths.working, paths.diagnostics, paths.fileIndex]) {
|
|
78
|
+
if (!fs.existsSync(filePath)) {
|
|
79
|
+
throw stateError('SEM_STATE_INCOMPLETE', `revision ${expectedRevision} 缺少 ${path.basename(filePath)}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const working = readJson(paths.working, 'SEM_STATE_CORRUPT');
|
|
83
|
+
const diagnostics = readJson(paths.diagnostics, 'SEM_STATE_CORRUPT');
|
|
84
|
+
const fileIndex = readJson(paths.fileIndex, 'SEM_STATE_CORRUPT');
|
|
85
|
+
const revisions = [working.revision, diagnostics.revision, fileIndex.revision];
|
|
86
|
+
if (revisions.some((revision) => revision !== expectedRevision)) {
|
|
87
|
+
throw stateError(
|
|
88
|
+
'SEM_STATE_REVISION_MISMATCH',
|
|
89
|
+
`current=${expectedRevision},working/diagnostics/file-index=${revisions.join('/')}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return { working, diagnostics, fileIndex };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function commitRevision(projectRoot, changeName, result, lock) {
|
|
96
|
+
assertChangeLock(lock);
|
|
97
|
+
const basePaths = statePaths(projectRoot, changeName);
|
|
98
|
+
const paths = statePaths(projectRoot, changeName, result.revision);
|
|
99
|
+
fs.mkdirSync(basePaths.revisions, { recursive: true });
|
|
100
|
+
|
|
101
|
+
if (fs.existsSync(paths.revisionDir)) {
|
|
102
|
+
readRevisionBundle(paths, result.revision);
|
|
103
|
+
} else {
|
|
104
|
+
const stagingDir = path.join(
|
|
105
|
+
basePaths.revisions,
|
|
106
|
+
`.${result.revision}.${lock.token}.staging`,
|
|
107
|
+
);
|
|
108
|
+
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
109
|
+
fs.mkdirSync(stagingDir, { recursive: true });
|
|
110
|
+
const stagingPaths = {
|
|
111
|
+
working: path.join(stagingDir, 'working-ontology.json'),
|
|
112
|
+
diagnostics: path.join(stagingDir, 'diagnostics.json'),
|
|
113
|
+
fileIndex: path.join(stagingDir, 'file-index.json'),
|
|
114
|
+
};
|
|
115
|
+
fs.writeFileSync(stagingPaths.working, serialized(result.state), 'utf8');
|
|
116
|
+
fs.writeFileSync(stagingPaths.diagnostics, serialized({
|
|
117
|
+
schema_version: SCHEMA_VERSION,
|
|
118
|
+
change: changeName,
|
|
119
|
+
profile: result.profile,
|
|
120
|
+
valid: result.valid,
|
|
121
|
+
revision: result.revision,
|
|
122
|
+
diagnostics: result.diagnostics,
|
|
123
|
+
}), 'utf8');
|
|
124
|
+
fs.writeFileSync(stagingPaths.fileIndex, serialized(result.fileIndex), 'utf8');
|
|
125
|
+
assertChangeLock(lock);
|
|
126
|
+
fs.renameSync(stagingDir, paths.revisionDir);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const pointer = {
|
|
130
|
+
schema_version: 'kld-sdd-state-pointer/v1',
|
|
131
|
+
change: safeChangeName(changeName),
|
|
132
|
+
revision: result.revision,
|
|
133
|
+
path: `revisions/${result.revision}`,
|
|
134
|
+
};
|
|
135
|
+
assertChangeLock(lock);
|
|
136
|
+
const changed = atomicWriteIfChanged(basePaths.current, pointer);
|
|
137
|
+
return { changed, paths: { ...paths, current: basePaths.current, revisions: basePaths.revisions } };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function scanChange(projectRoot, changeName, options = {}) {
|
|
141
|
+
const parsed = parseChangeArtifacts(projectRoot, changeName, options);
|
|
142
|
+
const facts = normalizeFacts(parsed);
|
|
143
|
+
const profile = normalizeProfile(options.profile || 'auto', facts.profile);
|
|
144
|
+
const identityCatalog = buildIdentityCatalog(projectRoot, { excludeChangeDir: parsed.changeDir });
|
|
145
|
+
const identityHistory = identityCatalog.records;
|
|
146
|
+
const identityHistoryHash = sha256(JSON.stringify({
|
|
147
|
+
records: identityCatalog.records,
|
|
148
|
+
diagnostics: identityCatalog.diagnostics,
|
|
149
|
+
}));
|
|
150
|
+
const effectiveGraph = buildEffectiveGraph(facts, identityCatalog);
|
|
151
|
+
const validationFacts = {
|
|
152
|
+
...facts,
|
|
153
|
+
diagnostics: [
|
|
154
|
+
...(facts.diagnostics || []),
|
|
155
|
+
...(identityCatalog.diagnostics || []),
|
|
156
|
+
...(effectiveGraph.resolution_diagnostics || []),
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
const validation = validateTraceability(validationFacts, {
|
|
160
|
+
profile,
|
|
161
|
+
identityHistory,
|
|
162
|
+
effectiveGraph,
|
|
163
|
+
});
|
|
164
|
+
const reviewStatus = options.markPending && validation.valid ? 'pending' : 'draft';
|
|
165
|
+
const revisionInput = {
|
|
166
|
+
schema_version: SCHEMA_VERSION,
|
|
167
|
+
change: changeName,
|
|
168
|
+
profile,
|
|
169
|
+
files: facts.files,
|
|
170
|
+
entities: effectiveGraph.effective_entities,
|
|
171
|
+
relations: effectiveGraph.effective_relations,
|
|
172
|
+
inherited_references: facts.inherited_references,
|
|
173
|
+
inherited_relations: facts.inherited_relations,
|
|
174
|
+
inherited_resolutions: effectiveGraph.inherited_resolutions,
|
|
175
|
+
diagnostics: validation.diagnostics,
|
|
176
|
+
identity_history_hash: identityHistoryHash,
|
|
177
|
+
review_status: reviewStatus,
|
|
178
|
+
};
|
|
179
|
+
const revision = sha256(JSON.stringify(revisionInput));
|
|
180
|
+
const state = JSON.parse(JSON.stringify({
|
|
181
|
+
schema_version: SCHEMA_VERSION,
|
|
182
|
+
change: changeName,
|
|
183
|
+
change_id: facts.change_id,
|
|
184
|
+
profile,
|
|
185
|
+
review_status: reviewStatus,
|
|
186
|
+
valid: validation.valid,
|
|
187
|
+
revision,
|
|
188
|
+
identity_history_hash: identityHistoryHash,
|
|
189
|
+
artifacts: facts.artifacts,
|
|
190
|
+
files: facts.files,
|
|
191
|
+
facts_hash: facts.facts_hash,
|
|
192
|
+
entities: effectiveGraph.effective_entities,
|
|
193
|
+
relations: effectiveGraph.effective_relations,
|
|
194
|
+
current_entities: facts.entities,
|
|
195
|
+
current_relations: facts.relations,
|
|
196
|
+
inherited_references: facts.inherited_references,
|
|
197
|
+
inherited_relations: facts.inherited_relations,
|
|
198
|
+
inherited_resolutions: effectiveGraph.inherited_resolutions,
|
|
199
|
+
}));
|
|
200
|
+
return {
|
|
201
|
+
profile,
|
|
202
|
+
valid: validation.valid,
|
|
203
|
+
revision,
|
|
204
|
+
state,
|
|
205
|
+
diagnostics: validation.diagnostics,
|
|
206
|
+
counts: validation.counts,
|
|
207
|
+
identityHistoryCount: identityHistory.length,
|
|
208
|
+
fileIndex: {
|
|
209
|
+
schema_version: SCHEMA_VERSION,
|
|
210
|
+
change: changeName,
|
|
211
|
+
files: facts.files,
|
|
212
|
+
revision,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function reconcileChange(projectRoot, changeName, options = {}) {
|
|
218
|
+
const execute = (lock) => {
|
|
219
|
+
assertChangeLock(lock);
|
|
220
|
+
const result = scanChange(projectRoot, changeName, options);
|
|
221
|
+
const committed = commitRevision(projectRoot, changeName, result, lock);
|
|
222
|
+
return {
|
|
223
|
+
...result,
|
|
224
|
+
changed: committed.changed,
|
|
225
|
+
paths: committed.paths,
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
if (options.lock) return execute(options.lock);
|
|
230
|
+
const lock = acquireChangeLock(projectRoot, changeName, options.lockOptions);
|
|
231
|
+
try {
|
|
232
|
+
return execute(lock);
|
|
233
|
+
} finally {
|
|
234
|
+
releaseChangeLock(lock);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function readWorkingState(projectRoot, changeName) {
|
|
239
|
+
const basePaths = statePaths(projectRoot, changeName);
|
|
240
|
+
if (fs.existsSync(basePaths.current)) {
|
|
241
|
+
const pointer = readJson(basePaths.current, 'SEM_STATE_POINTER_CORRUPT');
|
|
242
|
+
if (!pointer.revision || pointer.change !== safeChangeName(changeName)) {
|
|
243
|
+
throw stateError('SEM_STATE_POINTER_CORRUPT', `current.json 不属于 Change ${changeName}`);
|
|
244
|
+
}
|
|
245
|
+
return readRevisionBundle(
|
|
246
|
+
statePaths(projectRoot, changeName, pointer.revision),
|
|
247
|
+
pointer.revision,
|
|
248
|
+
).working;
|
|
249
|
+
}
|
|
250
|
+
if (!fs.existsSync(basePaths.working)) return null;
|
|
251
|
+
return readJson(basePaths.working, 'SEM_STATE_CORRUPT');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function createArchiveSnapshot(projectRoot, changeName, archiveDir, inputState) {
|
|
255
|
+
const targetDir = path.resolve(archiveDir);
|
|
256
|
+
if (!fs.existsSync(targetDir)) {
|
|
257
|
+
throw new Error(`归档源目录不存在: ${targetDir}`);
|
|
258
|
+
}
|
|
259
|
+
const actualFacts = normalizeFacts(parseChangeArtifacts(projectRoot, changeName, {
|
|
260
|
+
changeDir: targetDir,
|
|
261
|
+
persistStructuralIdentities: false,
|
|
262
|
+
}));
|
|
263
|
+
const scanned = inputState ? null : scanChange(projectRoot, changeName, {
|
|
264
|
+
changeDir: targetDir,
|
|
265
|
+
profile: 'auto',
|
|
266
|
+
markPending: true,
|
|
267
|
+
persistStructuralIdentities: false,
|
|
268
|
+
});
|
|
269
|
+
const state = inputState || (scanned && scanned.state) || readWorkingState(projectRoot, changeName);
|
|
270
|
+
if (!state) {
|
|
271
|
+
throw new Error(`不存在工作态本体实例: ${changeName}`);
|
|
272
|
+
}
|
|
273
|
+
if (!state.valid) {
|
|
274
|
+
throw new Error(`工作态本体仍有阻断错误,不能归档: ${changeName}`);
|
|
275
|
+
}
|
|
276
|
+
if (!state.facts_hash || state.facts_hash !== actualFacts.facts_hash) {
|
|
277
|
+
throw new Error(`归档正文与待确认本体事实不一致: ${changeName}`);
|
|
278
|
+
}
|
|
279
|
+
const snapshotId = `SNAP-${sha256(`${changeName}:${state.revision}`).slice(0, 16).toUpperCase()}`;
|
|
280
|
+
const targetPath = path.join(targetDir, 'archive-ontology.json');
|
|
281
|
+
const previous = fs.existsSync(targetPath)
|
|
282
|
+
? JSON.parse(fs.readFileSync(targetPath, 'utf8'))
|
|
283
|
+
: null;
|
|
284
|
+
const previousEntity = previous && previous.source_revision === state.revision
|
|
285
|
+
? (previous.entities || []).find((entity) => entity.type === 'OntologySnapshot')
|
|
286
|
+
: null;
|
|
287
|
+
const snapshotEntity = {
|
|
288
|
+
id: snapshotId,
|
|
289
|
+
anchor_id: snapshotId,
|
|
290
|
+
type: 'OntologySnapshot',
|
|
291
|
+
entity_id: previousEntity && previousEntity.entity_id
|
|
292
|
+
? previousEntity.entity_id
|
|
293
|
+
: generateUuidV7(),
|
|
294
|
+
version_id: previousEntity && previousEntity.version_id
|
|
295
|
+
? previousEntity.version_id
|
|
296
|
+
: generateUuidV7(),
|
|
297
|
+
predecessor_version_id: null,
|
|
298
|
+
delta_state: 'added',
|
|
299
|
+
generation_role: 'produced',
|
|
300
|
+
content_hash: sha256(JSON.stringify({
|
|
301
|
+
change: changeName,
|
|
302
|
+
source_revision: state.revision,
|
|
303
|
+
facts_hash: state.facts_hash,
|
|
304
|
+
})),
|
|
305
|
+
attributes: {
|
|
306
|
+
source_revision: state.revision,
|
|
307
|
+
facts_hash: state.facts_hash,
|
|
308
|
+
},
|
|
309
|
+
source: {
|
|
310
|
+
artifact_type: 'snapshot',
|
|
311
|
+
file: 'archive-ontology.json',
|
|
312
|
+
line: 1,
|
|
313
|
+
anchor_id: snapshotId,
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
const snapshot = {
|
|
317
|
+
...state,
|
|
318
|
+
review_status: 'confirmed',
|
|
319
|
+
snapshot_id: snapshotId,
|
|
320
|
+
snapshot_entity_id: snapshotEntity.entity_id,
|
|
321
|
+
source_revision: state.revision,
|
|
322
|
+
confirmed_at: new Date().toISOString(),
|
|
323
|
+
entities: [
|
|
324
|
+
...(state.entities || []).filter((entity) => entity.type !== 'OntologySnapshot'),
|
|
325
|
+
snapshotEntity,
|
|
326
|
+
],
|
|
327
|
+
};
|
|
328
|
+
atomicWriteIfChanged(targetPath, snapshot);
|
|
329
|
+
return snapshot;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
module.exports = {
|
|
333
|
+
statePaths,
|
|
334
|
+
atomicWriteIfChanged,
|
|
335
|
+
readRevisionBundle,
|
|
336
|
+
commitRevision,
|
|
337
|
+
scanChange,
|
|
338
|
+
reconcileChange,
|
|
339
|
+
readWorkingState,
|
|
340
|
+
createArchiveSnapshot,
|
|
341
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const SCHEMA_VERSION = 'kld-sdd-ontology/v2';
|
|
4
|
+
|
|
5
|
+
const ENTITY_TYPES = Object.freeze([
|
|
6
|
+
'Change',
|
|
7
|
+
'Capability',
|
|
8
|
+
'SpecificationStatement',
|
|
9
|
+
'AcceptanceCriterion',
|
|
10
|
+
'Constraint',
|
|
11
|
+
'DesignElement',
|
|
12
|
+
'Task',
|
|
13
|
+
'Artifact',
|
|
14
|
+
'DocumentSection',
|
|
15
|
+
'OntologySnapshot',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const ID_PREFIX_TYPES = Object.freeze({
|
|
19
|
+
CHG: 'Change',
|
|
20
|
+
CAP: 'Capability',
|
|
21
|
+
STMT: 'SpecificationStatement',
|
|
22
|
+
AC: 'AcceptanceCriterion',
|
|
23
|
+
CON: 'Constraint',
|
|
24
|
+
DES: 'DesignElement',
|
|
25
|
+
TASK: 'Task',
|
|
26
|
+
ART: 'Artifact',
|
|
27
|
+
SEC: 'DocumentSection',
|
|
28
|
+
SNAP: 'OntologySnapshot',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const RELATION_TYPES = Object.freeze({
|
|
32
|
+
contains: Object.freeze({
|
|
33
|
+
domain: Object.freeze(['Change', 'Capability']),
|
|
34
|
+
range: Object.freeze(['Capability', 'SpecificationStatement']),
|
|
35
|
+
pairs: Object.freeze([
|
|
36
|
+
Object.freeze(['Change', 'Capability']),
|
|
37
|
+
Object.freeze(['Capability', 'SpecificationStatement']),
|
|
38
|
+
]),
|
|
39
|
+
}),
|
|
40
|
+
acceptedBy: Object.freeze({
|
|
41
|
+
domain: Object.freeze(['SpecificationStatement']),
|
|
42
|
+
range: Object.freeze(['AcceptanceCriterion']),
|
|
43
|
+
}),
|
|
44
|
+
constrainedBy: Object.freeze({
|
|
45
|
+
domain: Object.freeze(['SpecificationStatement']),
|
|
46
|
+
range: Object.freeze(['Constraint']),
|
|
47
|
+
}),
|
|
48
|
+
realizes: Object.freeze({
|
|
49
|
+
domain: Object.freeze(['DesignElement']),
|
|
50
|
+
range: Object.freeze(['SpecificationStatement']),
|
|
51
|
+
}),
|
|
52
|
+
implements: Object.freeze({
|
|
53
|
+
domain: Object.freeze(['Task']),
|
|
54
|
+
range: Object.freeze(['DesignElement']),
|
|
55
|
+
}),
|
|
56
|
+
covers: Object.freeze({
|
|
57
|
+
domain: Object.freeze(['Task']),
|
|
58
|
+
range: Object.freeze(['SpecificationStatement']),
|
|
59
|
+
}),
|
|
60
|
+
dependsOn: Object.freeze({
|
|
61
|
+
domain: Object.freeze(['Task']),
|
|
62
|
+
range: Object.freeze(['Task']),
|
|
63
|
+
}),
|
|
64
|
+
declares: Object.freeze({
|
|
65
|
+
domain: Object.freeze(['Artifact']),
|
|
66
|
+
range: Object.freeze(ENTITY_TYPES.filter((type) => !['Artifact', 'DocumentSection', 'OntologySnapshot'].includes(type))),
|
|
67
|
+
}),
|
|
68
|
+
sourcedFrom: Object.freeze({
|
|
69
|
+
domain: Object.freeze(ENTITY_TYPES.filter((type) => !['DocumentSection', 'OntologySnapshot'].includes(type))),
|
|
70
|
+
range: Object.freeze(['DocumentSection']),
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const VALID_PROFILES = Object.freeze(['simple', 'full', 'strict']);
|
|
75
|
+
|
|
76
|
+
const DIAGNOSTIC_CODES = Object.freeze({
|
|
77
|
+
ID_MISSING: 'SEM_ID_MISSING',
|
|
78
|
+
ID_INVALID: 'SEM_ID_INVALID',
|
|
79
|
+
ID_DUPLICATE: 'SEM_ID_DUPLICATE',
|
|
80
|
+
ARTIFACT_MISSING: 'SEM_ARTIFACT_MISSING',
|
|
81
|
+
PROFILE_INVALID: 'SEM_PROFILE_INVALID',
|
|
82
|
+
PROFILE_INFERRED: 'SEM_PROFILE_INFERRED',
|
|
83
|
+
RELATION_UNKNOWN: 'SEM_RELATION_UNKNOWN',
|
|
84
|
+
RELATION_TARGET_MISSING: 'SEM_RELATION_TARGET_MISSING',
|
|
85
|
+
RELATION_DOMAIN_RANGE: 'SEM_RELATION_DOMAIN_RANGE',
|
|
86
|
+
STMT_WITHOUT_AC: 'SEM_TRACE_STMT_WITHOUT_AC',
|
|
87
|
+
STMT_WITHOUT_DESIGN: 'SEM_TRACE_STMT_WITHOUT_DESIGN',
|
|
88
|
+
DESIGN_WITHOUT_TASK: 'SEM_TRACE_DESIGN_WITHOUT_TASK',
|
|
89
|
+
TASK_DEPENDENCY_CYCLE: 'SEM_TASK_DEPENDENCY_CYCLE',
|
|
90
|
+
SOURCE_MISSING: 'SEM_SOURCE_MISSING',
|
|
91
|
+
UUID_MISSING: 'SEM_UUID_MISSING',
|
|
92
|
+
UUID_INVALID: 'SEM_UUID_INVALID',
|
|
93
|
+
DELTA_STATE_INVALID: 'SEM_DELTA_STATE_INVALID',
|
|
94
|
+
ENTITY_IDENTITY_CONFLICT: 'SEM_ENTITY_IDENTITY_CONFLICT',
|
|
95
|
+
VERSION_IDENTITY_CONFLICT: 'SEM_VERSION_IDENTITY_CONFLICT',
|
|
96
|
+
VERSION_LINEAGE_MISSING: 'SEM_VERSION_LINEAGE_MISSING',
|
|
97
|
+
VERSION_LINEAGE_CONFLICT: 'SEM_VERSION_LINEAGE_CONFLICT',
|
|
98
|
+
INHERITED_SOURCE_MISMATCH: 'SEM_INHERITED_SOURCE_MISMATCH',
|
|
99
|
+
RELATION_ASSERTION_INVALID: 'SEM_RELATION_ASSERTION_INVALID',
|
|
100
|
+
RELATION_TARGET_REMOVED: 'SEM_RELATION_TARGET_REMOVED',
|
|
101
|
+
AC_WITHOUT_STMT: 'SEM_TRACE_AC_WITHOUT_STMT',
|
|
102
|
+
TASK_WITHOUT_UPSTREAM: 'SEM_TRACE_TASK_WITHOUT_UPSTREAM',
|
|
103
|
+
CAPABILITY_BOUNDARY: 'SEM_CAPABILITY_BOUNDARY',
|
|
104
|
+
ARCHIVE_NOT_CONFIRMED: 'SEM_ARCHIVE_NOT_CONFIRMED',
|
|
105
|
+
ARCHIVE_REVISION_MISMATCH: 'SEM_ARCHIVE_REVISION_MISMATCH',
|
|
106
|
+
INHERITED_RELATION_MISSING: 'SEM_INHERITED_RELATION_MISSING',
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
function normalizeProfile(profile, inferredProfile = 'simple') {
|
|
110
|
+
const normalized = String(profile || 'auto').trim().toLowerCase();
|
|
111
|
+
if (normalized === 'auto') {
|
|
112
|
+
return VALID_PROFILES.includes(inferredProfile) ? inferredProfile : 'simple';
|
|
113
|
+
}
|
|
114
|
+
if (!VALID_PROFILES.includes(normalized)) {
|
|
115
|
+
throw new Error(`不支持的语义校验 profile: ${profile}`);
|
|
116
|
+
}
|
|
117
|
+
return normalized;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function entityTypeForId(entityId) {
|
|
121
|
+
const prefix = String(entityId || '').trim().toUpperCase().split('-')[0];
|
|
122
|
+
return ID_PREFIX_TYPES[prefix] || null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function relationDefinition(type) {
|
|
126
|
+
return RELATION_TYPES[type] || null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
SCHEMA_VERSION,
|
|
131
|
+
ENTITY_TYPES,
|
|
132
|
+
ID_PREFIX_TYPES,
|
|
133
|
+
RELATION_TYPES,
|
|
134
|
+
VALID_PROFILES,
|
|
135
|
+
DIAGNOSTIC_CODES,
|
|
136
|
+
normalizeProfile,
|
|
137
|
+
entityTypeForId,
|
|
138
|
+
relationDefinition,
|
|
139
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { SCHEMA_VERSION } = require('./schema.cjs');
|
|
6
|
+
const { generateUuidV7, isValidUuidV7 } = require('./id.cjs');
|
|
7
|
+
|
|
8
|
+
const REGISTRY_FILE = 'ontology-identities.json';
|
|
9
|
+
|
|
10
|
+
function registryPath(changeDir) {
|
|
11
|
+
return path.join(path.resolve(changeDir), REGISTRY_FILE);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readRegistry(changeDir) {
|
|
15
|
+
const filePath = registryPath(changeDir);
|
|
16
|
+
if (!fs.existsSync(filePath)) {
|
|
17
|
+
return { schema_version: SCHEMA_VERSION, identities: {} };
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
21
|
+
return {
|
|
22
|
+
schema_version: SCHEMA_VERSION,
|
|
23
|
+
identities: parsed && typeof parsed.identities === 'object' ? parsed.identities : {},
|
|
24
|
+
};
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw new Error(`结构身份 sidecar 非法: ${filePath}: ${error.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function writeRegistry(changeDir, registry) {
|
|
31
|
+
const filePath = registryPath(changeDir);
|
|
32
|
+
const content = `${JSON.stringify(registry, null, 2)}\n`;
|
|
33
|
+
if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === content) return false;
|
|
34
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
35
|
+
const tempPath = `${filePath}.${process.pid}.tmp`;
|
|
36
|
+
fs.writeFileSync(tempPath, content, 'utf8');
|
|
37
|
+
fs.renameSync(tempPath, filePath);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ensureStructuralIdentities(changeDir, descriptors, options = {}) {
|
|
42
|
+
const registry = readRegistry(changeDir);
|
|
43
|
+
let changed = false;
|
|
44
|
+
for (const descriptor of descriptors) {
|
|
45
|
+
const existing = registry.identities[descriptor.key];
|
|
46
|
+
if (existing) {
|
|
47
|
+
if (existing.anchor_id !== descriptor.anchor_id || existing.type !== descriptor.type
|
|
48
|
+
|| !isValidUuidV7(existing.entity_id) || !isValidUuidV7(existing.version_id)) {
|
|
49
|
+
throw new Error(`结构身份 sidecar 与当前结构冲突: ${descriptor.key}`);
|
|
50
|
+
}
|
|
51
|
+
if (existing.content_hash !== descriptor.content_hash) {
|
|
52
|
+
existing.content_hash = descriptor.content_hash;
|
|
53
|
+
changed = true;
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (options.persist === false) continue;
|
|
58
|
+
registry.identities[descriptor.key] = {
|
|
59
|
+
anchor_id: descriptor.anchor_id,
|
|
60
|
+
type: descriptor.type,
|
|
61
|
+
entity_id: generateUuidV7(),
|
|
62
|
+
version_id: generateUuidV7(),
|
|
63
|
+
content_hash: descriptor.content_hash,
|
|
64
|
+
};
|
|
65
|
+
changed = true;
|
|
66
|
+
}
|
|
67
|
+
if (changed && options.persist !== false) writeRegistry(changeDir, registry);
|
|
68
|
+
return registry;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
REGISTRY_FILE,
|
|
73
|
+
registryPath,
|
|
74
|
+
readRegistry,
|
|
75
|
+
writeRegistry,
|
|
76
|
+
ensureStructuralIdentities,
|
|
77
|
+
};
|