kld-sdd 2.5.1 → 2.6.0

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 (35) hide show
  1. package/README.md +118 -8
  2. package/kld-sdd-guide.html +1 -1
  3. package/lib/init.js +24 -5
  4. package/lib/tool-profiles.js +1 -1
  5. package/package.json +4 -2
  6. package/skywalk-sdd/context-client.cjs +160 -0
  7. package/skywalk-sdd/index.cjs +445 -36
  8. package/skywalk-sdd/ontology/archive-package.cjs +489 -0
  9. package/skywalk-sdd/ontology/artifact-observer.cjs +91 -0
  10. package/skywalk-sdd/ontology/artifact-parser.cjs +621 -0
  11. package/skywalk-sdd/ontology/change-lock.cjs +126 -0
  12. package/skywalk-sdd/ontology/cli.cjs +146 -0
  13. package/skywalk-sdd/ontology/effective-graph.cjs +158 -0
  14. package/skywalk-sdd/ontology/id.cjs +126 -0
  15. package/skywalk-sdd/ontology/identity-index.cjs +287 -0
  16. package/skywalk-sdd/ontology/normalizer.cjs +107 -0
  17. package/skywalk-sdd/ontology/runtime.cjs +466 -0
  18. package/skywalk-sdd/ontology/schema.cjs +139 -0
  19. package/skywalk-sdd/ontology/structural-identity.cjs +77 -0
  20. package/skywalk-sdd/ontology/traceability-validator.cjs +610 -0
  21. package/skywalk-sdd/ontology/working-artifacts.cjs +243 -0
  22. package/templates/openspec/design.md +18 -0
  23. package/templates/openspec/proposal.md +19 -6
  24. package/templates/openspec/spec.md +62 -8
  25. package/templates/openspec/tasks.md +28 -6
  26. package/templates/skills/kld-sdd/opsx-archive/SKILL.md +19 -1
  27. package/templates/skills/kld-sdd/opsx-archive/checklist.md +5 -1
  28. package/templates/skills/kld-sdd/opsx-check/SKILL.md +18 -0
  29. package/templates/skills/kld-sdd/opsx-check/checklist.md +2 -0
  30. package/templates/skills/kld-sdd/opsx-design/SKILL.md +11 -0
  31. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +12 -0
  32. package/templates/skills/kld-sdd/opsx-propose/checklist.md +2 -0
  33. package/templates/skills/kld-sdd/opsx-spec/SKILL.md +34 -0
  34. package/templates/skills/kld-sdd/opsx-spec/checklist.md +5 -0
  35. package/templates/skills/kld-sdd/opsx-task/SKILL.md +11 -0
@@ -0,0 +1,466 @@
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
+ WORKING_ARTIFACT_SCHEMA_VERSION,
15
+ ARTIFACT_INDEX_SCHEMA_VERSION,
16
+ buildWorkingArtifactFacts,
17
+ } = require('./working-artifacts.cjs');
18
+ const {
19
+ acquireChangeLock,
20
+ assertChangeLock,
21
+ releaseChangeLock,
22
+ } = require('./change-lock.cjs');
23
+
24
+ function sha256(value) {
25
+ return crypto.createHash('sha256').update(value).digest('hex');
26
+ }
27
+
28
+ function resolveChangeDir(projectRoot, changeName) {
29
+ const root = path.resolve(projectRoot || process.cwd());
30
+ return path.join(root, 'openspec', 'changes', safeChangeName(changeName));
31
+ }
32
+
33
+ function changeOntologyPaths(projectRoot, changeName) {
34
+ const changeDir = resolveChangeDir(projectRoot, changeName);
35
+ return {
36
+ changeDir,
37
+ working: path.join(changeDir, 'working-ontology.json'),
38
+ diagnostics: path.join(changeDir, 'diagnostics.json'),
39
+ fileIndex: path.join(changeDir, 'file-index.json'),
40
+ artifactIndex: path.join(changeDir, 'artifact-index.json'),
41
+ artifactDir: path.join(changeDir, 'artifacts'),
42
+ };
43
+ }
44
+
45
+ function statePaths(projectRoot, changeName) {
46
+ const root = path.resolve(projectRoot || process.cwd());
47
+ const safeName = safeChangeName(changeName);
48
+ const dir = path.join(root, 'skywalk-sdd', 'state', 'ontology', safeName);
49
+ return {
50
+ dir,
51
+ current: path.join(dir, 'current.json'),
52
+ };
53
+ }
54
+
55
+ function serialized(value) {
56
+ return JSON.stringify(value, null, 2) + '\n';
57
+ }
58
+
59
+ function atomicWriteIfChanged(filePath, value) {
60
+ const content = typeof value === 'string' ? value : serialized(value);
61
+ if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === content) {
62
+ return false;
63
+ }
64
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
65
+ const tempPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
66
+ fs.writeFileSync(tempPath, content, 'utf8');
67
+ fs.renameSync(tempPath, filePath);
68
+ return true;
69
+ }
70
+
71
+ function stateError(code, message) {
72
+ const error = new Error(`${code}: ${message}`);
73
+ error.code = code;
74
+ return error;
75
+ }
76
+
77
+ function readJson(filePath, code) {
78
+ try {
79
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
80
+ } catch (error) {
81
+ throw stateError(code, `无法读取状态文件 ${filePath}: ${error.message}`);
82
+ }
83
+ }
84
+
85
+ function readRevisionBundle(paths, expectedRevision) {
86
+ for (const filePath of [paths.working, paths.diagnostics, paths.fileIndex, paths.artifactIndex]) {
87
+ if (!fs.existsSync(filePath)) {
88
+ throw stateError('SEM_STATE_INCOMPLETE', `revision ${expectedRevision} 缺少 ${path.basename(filePath)}`);
89
+ }
90
+ }
91
+ const working = readJson(paths.working, 'SEM_STATE_CORRUPT');
92
+ const diagnostics = readJson(paths.diagnostics, 'SEM_STATE_CORRUPT');
93
+ const fileIndex = readJson(paths.fileIndex, 'SEM_STATE_CORRUPT');
94
+ const artifactIndex = readJson(paths.artifactIndex, 'SEM_STATE_CORRUPT');
95
+ const revisions = [working.revision, diagnostics.revision, fileIndex.revision, artifactIndex.revision];
96
+ if (revisions.some((revision) => revision !== expectedRevision)) {
97
+ throw stateError(
98
+ 'SEM_STATE_REVISION_MISMATCH',
99
+ `current=${expectedRevision},working/diagnostics/file-index=${revisions.join('/')}`,
100
+ );
101
+ }
102
+ for (const artifact of artifactIndex.artifacts || []) {
103
+ const artifactPath = path.resolve(paths.changeDir, artifact.json_path || '');
104
+ const artifactRelative = path.relative(paths.changeDir, artifactPath);
105
+ if (
106
+ !artifact.json_path
107
+ || artifactRelative.startsWith('..')
108
+ || path.isAbsolute(artifactRelative)
109
+ || !fs.existsSync(artifactPath)
110
+ ) {
111
+ throw stateError(
112
+ 'SEM_STATE_INCOMPLETE',
113
+ `revision ${expectedRevision} 缺少 Artifact JSON: ${artifact.json_path || '(empty)'}`,
114
+ );
115
+ }
116
+ const artifactFacts = readJson(artifactPath, 'SEM_STATE_CORRUPT');
117
+ if (artifactFacts.revision !== expectedRevision) {
118
+ throw stateError(
119
+ 'SEM_STATE_REVISION_MISMATCH',
120
+ `current=${expectedRevision},artifact=${artifactFacts.revision}`,
121
+ );
122
+ }
123
+ }
124
+ return { working, diagnostics, fileIndex, artifactIndex };
125
+ }
126
+
127
+ function pruneStaleArtifacts(changeDir, artifactIndex) {
128
+ const artifactRoot = path.join(changeDir, 'artifacts');
129
+ if (!fs.existsSync(artifactRoot)) return;
130
+ const allowed = new Set((artifactIndex.artifacts || []).map((artifact) => artifact.json_path));
131
+ const stale = [];
132
+ (function walk(currentDir, relativePrefix) {
133
+ for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
134
+ const relativePath = relativePrefix ? `${relativePrefix}/${entry.name}` : entry.name;
135
+ const absolutePath = path.join(currentDir, entry.name);
136
+ if (entry.isDirectory()) {
137
+ walk(absolutePath, relativePath);
138
+ continue;
139
+ }
140
+ if (!entry.name.endsWith('.ontology.json')) continue;
141
+ const jsonPath = `artifacts/${relativePath.replace(/\\/g, '/')}`;
142
+ if (!allowed.has(jsonPath)) stale.push(absolutePath);
143
+ }
144
+ }(artifactRoot, ''));
145
+ for (const filePath of stale) {
146
+ fs.rmSync(filePath, { force: true });
147
+ }
148
+ (function removeEmptyDirs(currentDir) {
149
+ if (!fs.existsSync(currentDir) || currentDir === artifactRoot) return;
150
+ const entries = fs.readdirSync(currentDir);
151
+ for (const entry of entries) {
152
+ const child = path.join(currentDir, entry);
153
+ if (fs.statSync(child).isDirectory()) removeEmptyDirs(child);
154
+ }
155
+ if (fs.readdirSync(currentDir).length === 0) fs.rmdirSync(currentDir);
156
+ }(artifactRoot));
157
+ }
158
+
159
+ function commitRevision(projectRoot, changeName, result, lock) {
160
+ assertChangeLock(lock);
161
+ const basePaths = statePaths(projectRoot, changeName);
162
+ const paths = changeOntologyPaths(projectRoot, changeName);
163
+ fs.mkdirSync(basePaths.dir, { recursive: true });
164
+ fs.mkdirSync(paths.changeDir, { recursive: true });
165
+
166
+ if (fs.existsSync(paths.working)) {
167
+ const existing = readJson(paths.working, 'SEM_STATE_CORRUPT');
168
+ if (existing.revision === result.revision) {
169
+ readRevisionBundle(paths, result.revision);
170
+ const artifactIndex = readJson(paths.artifactIndex, 'SEM_STATE_CORRUPT');
171
+ return {
172
+ changed: false,
173
+ paths: {
174
+ ...paths,
175
+ current: basePaths.current,
176
+ artifactFacts: (artifactIndex.artifacts || []).map((artifact) => (
177
+ path.join(paths.changeDir, artifact.json_path)
178
+ )),
179
+ },
180
+ };
181
+ }
182
+ }
183
+
184
+ const stagingDir = path.join(basePaths.dir, `.${result.revision}.${lock.token}.staging`);
185
+ fs.rmSync(stagingDir, { recursive: true, force: true });
186
+ fs.mkdirSync(stagingDir, { recursive: true });
187
+ const artifactBundle = buildWorkingArtifactFacts(result);
188
+ const stagedFiles = {
189
+ working: path.join(stagingDir, 'working-ontology.json'),
190
+ diagnostics: path.join(stagingDir, 'diagnostics.json'),
191
+ fileIndex: path.join(stagingDir, 'file-index.json'),
192
+ artifactIndex: path.join(stagingDir, 'artifact-index.json'),
193
+ };
194
+ fs.writeFileSync(stagedFiles.working, serialized(result.state), 'utf8');
195
+ fs.writeFileSync(stagedFiles.diagnostics, serialized({
196
+ schema_version: SCHEMA_VERSION,
197
+ change: changeName,
198
+ profile: result.profile,
199
+ valid: result.valid,
200
+ revision: result.revision,
201
+ diagnostics: result.diagnostics,
202
+ }), 'utf8');
203
+ fs.writeFileSync(stagedFiles.fileIndex, serialized(result.fileIndex), 'utf8');
204
+ fs.writeFileSync(stagedFiles.artifactIndex, serialized(artifactBundle.index), 'utf8');
205
+ for (const artifactFile of artifactBundle.files) {
206
+ const targetPath = path.join(stagingDir, artifactFile.path);
207
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
208
+ fs.writeFileSync(targetPath, serialized(artifactFile.value), 'utf8');
209
+ }
210
+ assertChangeLock(lock);
211
+ readRevisionBundle({
212
+ changeDir: stagingDir,
213
+ working: stagedFiles.working,
214
+ diagnostics: stagedFiles.diagnostics,
215
+ fileIndex: stagedFiles.fileIndex,
216
+ artifactIndex: stagedFiles.artifactIndex,
217
+ }, result.revision);
218
+
219
+ atomicWriteIfChanged(paths.working, fs.readFileSync(stagedFiles.working, 'utf8'));
220
+ atomicWriteIfChanged(paths.diagnostics, fs.readFileSync(stagedFiles.diagnostics, 'utf8'));
221
+ atomicWriteIfChanged(paths.fileIndex, fs.readFileSync(stagedFiles.fileIndex, 'utf8'));
222
+ atomicWriteIfChanged(paths.artifactIndex, fs.readFileSync(stagedFiles.artifactIndex, 'utf8'));
223
+ for (const artifactFile of artifactBundle.files) {
224
+ const stagedPath = path.join(stagingDir, artifactFile.path);
225
+ atomicWriteIfChanged(path.join(paths.changeDir, artifactFile.path), fs.readFileSync(stagedPath, 'utf8'));
226
+ }
227
+ pruneStaleArtifacts(paths.changeDir, artifactBundle.index);
228
+ fs.rmSync(stagingDir, { recursive: true, force: true });
229
+
230
+ const pointer = {
231
+ schema_version: 'kld-sdd-state-pointer/v1',
232
+ change: safeChangeName(changeName),
233
+ revision: result.revision,
234
+ change_dir: path.relative(path.resolve(projectRoot), paths.changeDir).replace(/\\/g, '/'),
235
+ artifact_index: 'artifact-index.json',
236
+ };
237
+ assertChangeLock(lock);
238
+ const changed = atomicWriteIfChanged(basePaths.current, pointer);
239
+ return {
240
+ changed,
241
+ paths: {
242
+ ...paths,
243
+ current: basePaths.current,
244
+ artifactFacts: artifactBundle.index.artifacts.map((artifact) => (
245
+ path.join(paths.changeDir, artifact.json_path)
246
+ )),
247
+ },
248
+ };
249
+ }
250
+
251
+ function scanChange(projectRoot, changeName, options = {}) {
252
+ const parsed = parseChangeArtifacts(projectRoot, changeName, options);
253
+ const facts = normalizeFacts(parsed);
254
+ const profile = normalizeProfile(options.profile || 'auto', facts.profile);
255
+ const identityCatalog = buildIdentityCatalog(projectRoot, { excludeChangeDir: parsed.changeDir });
256
+ const identityHistory = identityCatalog.records;
257
+ const identityHistoryHash = sha256(JSON.stringify({
258
+ records: identityCatalog.records,
259
+ diagnostics: identityCatalog.diagnostics,
260
+ }));
261
+ const effectiveGraph = buildEffectiveGraph(facts, identityCatalog);
262
+ const validationFacts = {
263
+ ...facts,
264
+ diagnostics: [
265
+ ...(facts.diagnostics || []),
266
+ ...(identityCatalog.diagnostics || []),
267
+ ...(effectiveGraph.resolution_diagnostics || []),
268
+ ],
269
+ };
270
+ const validation = validateTraceability(validationFacts, {
271
+ profile,
272
+ identityHistory,
273
+ effectiveGraph,
274
+ });
275
+ const reviewStatus = options.markPending && validation.valid ? 'pending' : 'draft';
276
+ const revisionInput = {
277
+ schema_version: SCHEMA_VERSION,
278
+ artifact_schema_version: WORKING_ARTIFACT_SCHEMA_VERSION,
279
+ artifact_index_schema_version: ARTIFACT_INDEX_SCHEMA_VERSION,
280
+ change: changeName,
281
+ profile,
282
+ files: facts.files,
283
+ entities: effectiveGraph.effective_entities,
284
+ relations: effectiveGraph.effective_relations,
285
+ inherited_references: facts.inherited_references,
286
+ inherited_relations: facts.inherited_relations,
287
+ inherited_resolutions: effectiveGraph.inherited_resolutions,
288
+ diagnostics: validation.diagnostics,
289
+ identity_history_hash: identityHistoryHash,
290
+ review_status: reviewStatus,
291
+ };
292
+ const revision = sha256(JSON.stringify(revisionInput));
293
+ const state = JSON.parse(JSON.stringify({
294
+ schema_version: SCHEMA_VERSION,
295
+ artifact_schema_version: WORKING_ARTIFACT_SCHEMA_VERSION,
296
+ artifact_index_schema_version: ARTIFACT_INDEX_SCHEMA_VERSION,
297
+ change: changeName,
298
+ change_id: facts.change_id,
299
+ profile,
300
+ review_status: reviewStatus,
301
+ valid: validation.valid,
302
+ revision,
303
+ identity_history_hash: identityHistoryHash,
304
+ artifacts: facts.artifacts,
305
+ files: facts.files,
306
+ facts_hash: facts.facts_hash,
307
+ entities: effectiveGraph.effective_entities,
308
+ relations: effectiveGraph.effective_relations,
309
+ current_entities: facts.entities,
310
+ current_relations: facts.relations,
311
+ inherited_references: facts.inherited_references,
312
+ inherited_relations: facts.inherited_relations,
313
+ inherited_resolutions: effectiveGraph.inherited_resolutions,
314
+ }));
315
+ return {
316
+ profile,
317
+ valid: validation.valid,
318
+ revision,
319
+ state,
320
+ diagnostics: validation.diagnostics,
321
+ counts: validation.counts,
322
+ identityHistoryCount: identityHistory.length,
323
+ fileIndex: {
324
+ schema_version: SCHEMA_VERSION,
325
+ change: changeName,
326
+ files: facts.files,
327
+ revision,
328
+ },
329
+ };
330
+ }
331
+
332
+ function reconcileChange(projectRoot, changeName, options = {}) {
333
+ const execute = (lock) => {
334
+ assertChangeLock(lock);
335
+ const result = scanChange(projectRoot, changeName, options);
336
+ const committed = commitRevision(projectRoot, changeName, result, lock);
337
+ return {
338
+ ...result,
339
+ changed: committed.changed,
340
+ paths: committed.paths,
341
+ };
342
+ };
343
+
344
+ if (options.lock) return execute(options.lock);
345
+ const lock = acquireChangeLock(projectRoot, changeName, options.lockOptions);
346
+ try {
347
+ return execute(lock);
348
+ } finally {
349
+ releaseChangeLock(lock);
350
+ }
351
+ }
352
+
353
+ function readWorkingState(projectRoot, changeName) {
354
+ const basePaths = statePaths(projectRoot, changeName);
355
+ const paths = changeOntologyPaths(projectRoot, changeName);
356
+ if (fs.existsSync(basePaths.current)) {
357
+ const pointer = readJson(basePaths.current, 'SEM_STATE_POINTER_CORRUPT');
358
+ if (!pointer.revision || pointer.change !== safeChangeName(changeName)) {
359
+ throw stateError('SEM_STATE_POINTER_CORRUPT', `current.json 不属于 Change ${changeName}`);
360
+ }
361
+ return readRevisionBundle(paths, pointer.revision).working;
362
+ }
363
+ if (!fs.existsSync(paths.working)) return null;
364
+ const working = readJson(paths.working, 'SEM_STATE_CORRUPT');
365
+ readRevisionBundle(paths, working.revision);
366
+ return working;
367
+ }
368
+
369
+ function createArchiveSnapshot(projectRoot, changeName, archiveDir, inputState) {
370
+ const targetDir = path.resolve(archiveDir);
371
+ if (!fs.existsSync(targetDir)) {
372
+ throw new Error(`归档源目录不存在: ${targetDir}`);
373
+ }
374
+ const actualFacts = normalizeFacts(parseChangeArtifacts(projectRoot, changeName, {
375
+ changeDir: targetDir,
376
+ persistStructuralIdentities: false,
377
+ }));
378
+ const scanned = inputState ? null : scanChange(projectRoot, changeName, {
379
+ changeDir: targetDir,
380
+ profile: 'auto',
381
+ markPending: true,
382
+ persistStructuralIdentities: false,
383
+ });
384
+ const state = inputState || (scanned && scanned.state) || readWorkingState(projectRoot, changeName);
385
+ if (!state) {
386
+ throw new Error(`不存在工作态本体实例: ${changeName}`);
387
+ }
388
+ if (!state.valid) {
389
+ throw new Error(`工作态本体仍有阻断错误,不能归档: ${changeName}`);
390
+ }
391
+ if (!state.facts_hash || state.facts_hash !== actualFacts.facts_hash) {
392
+ throw new Error(`归档正文与待确认本体事实不一致: ${changeName}`);
393
+ }
394
+ const artifactIndexPath = path.join(targetDir, 'artifact-index.json');
395
+ if (!fs.existsSync(artifactIndexPath)) {
396
+ throw new Error(`归档目录缺少 artifact-index.json: ${targetDir}`);
397
+ }
398
+ const artifactIndex = readJson(artifactIndexPath, 'SEM_STATE_CORRUPT');
399
+ if (artifactIndex.revision !== state.revision) {
400
+ throw new Error(`归档 artifact-index 与待确认 revision 不一致: ${changeName}`);
401
+ }
402
+ const snapshotId = `SNAP-${sha256(`${changeName}:${state.revision}`).slice(0, 16).toUpperCase()}`;
403
+ const targetPath = path.join(targetDir, 'archive-ontology.json');
404
+ const previous = fs.existsSync(targetPath)
405
+ ? JSON.parse(fs.readFileSync(targetPath, 'utf8'))
406
+ : null;
407
+ const previousEntity = previous && previous.source_revision === state.revision
408
+ ? (previous.entities || []).find((entity) => entity.type === 'OntologySnapshot')
409
+ : null;
410
+ const snapshotEntity = {
411
+ id: snapshotId,
412
+ anchor_id: snapshotId,
413
+ type: 'OntologySnapshot',
414
+ entity_id: previousEntity && previousEntity.entity_id
415
+ ? previousEntity.entity_id
416
+ : generateUuidV7(),
417
+ version_id: previousEntity && previousEntity.version_id
418
+ ? previousEntity.version_id
419
+ : generateUuidV7(),
420
+ predecessor_version_id: null,
421
+ delta_state: 'added',
422
+ generation_role: 'produced',
423
+ content_hash: sha256(JSON.stringify({
424
+ change: changeName,
425
+ source_revision: state.revision,
426
+ facts_hash: state.facts_hash,
427
+ })),
428
+ attributes: {
429
+ source_revision: state.revision,
430
+ facts_hash: state.facts_hash,
431
+ },
432
+ source: {
433
+ artifact_type: 'snapshot',
434
+ file: 'archive-ontology.json',
435
+ line: 1,
436
+ anchor_id: snapshotId,
437
+ },
438
+ };
439
+ const snapshot = {
440
+ ...state,
441
+ review_status: 'confirmed',
442
+ snapshot_id: snapshotId,
443
+ snapshot_entity_id: snapshotEntity.entity_id,
444
+ source_revision: state.revision,
445
+ confirmed_at: new Date().toISOString(),
446
+ entities: [
447
+ ...(state.entities || []).filter((entity) => entity.type !== 'OntologySnapshot'),
448
+ snapshotEntity,
449
+ ],
450
+ };
451
+ atomicWriteIfChanged(targetPath, snapshot);
452
+ return snapshot;
453
+ }
454
+
455
+ module.exports = {
456
+ resolveChangeDir,
457
+ changeOntologyPaths,
458
+ statePaths,
459
+ atomicWriteIfChanged,
460
+ readRevisionBundle,
461
+ commitRevision,
462
+ scanChange,
463
+ reconcileChange,
464
+ readWorkingState,
465
+ createArchiveSnapshot,
466
+ };
@@ -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
+ };