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.
- package/README.md +118 -8
- package/kld-sdd-guide.html +1 -1
- package/lib/init.js +24 -5
- package/lib/tool-profiles.js +1 -1
- package/package.json +4 -2
- package/skywalk-sdd/context-client.cjs +160 -0
- package/skywalk-sdd/index.cjs +445 -36
- package/skywalk-sdd/ontology/archive-package.cjs +489 -0
- 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 +287 -0
- package/skywalk-sdd/ontology/normalizer.cjs +107 -0
- package/skywalk-sdd/ontology/runtime.cjs +466 -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/skywalk-sdd/ontology/working-artifacts.cjs +243 -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-archive/SKILL.md +19 -1
- package/templates/skills/kld-sdd/opsx-archive/checklist.md +5 -1
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +18 -0
- package/templates/skills/kld-sdd/opsx-check/checklist.md +2 -0
- package/templates/skills/kld-sdd/opsx-design/SKILL.md +11 -0
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +12 -0
- package/templates/skills/kld-sdd/opsx-propose/checklist.md +2 -0
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +34 -0
- package/templates/skills/kld-sdd/opsx-spec/checklist.md +5 -0
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +11 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { parseChangeArtifacts } = require('./artifact-parser.cjs');
|
|
6
|
+
const { normalizeFacts } = require('./normalizer.cjs');
|
|
7
|
+
const { DIAGNOSTIC_CODES } = require('./schema.cjs');
|
|
8
|
+
|
|
9
|
+
function catalogDiagnostic(code, message, context = {}) {
|
|
10
|
+
return {
|
|
11
|
+
code,
|
|
12
|
+
severity: context.severity || 'error',
|
|
13
|
+
message,
|
|
14
|
+
file: context.file || '',
|
|
15
|
+
line: Number(context.line || 0),
|
|
16
|
+
entity_id: context.entity_id || undefined,
|
|
17
|
+
suggestion: context.suggestion || undefined,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readJson(filePath) {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function identityRoots(projectRoot) {
|
|
30
|
+
const root = path.resolve(projectRoot || process.cwd());
|
|
31
|
+
const changesRoot = path.join(root, 'openspec', 'changes');
|
|
32
|
+
const candidates = [];
|
|
33
|
+
if (!fs.existsSync(changesRoot)) return candidates;
|
|
34
|
+
|
|
35
|
+
for (const entry of fs.readdirSync(changesRoot, { withFileTypes: true })) {
|
|
36
|
+
if (!entry.isDirectory() || entry.name === 'archive' || entry.name.startsWith('.')) continue;
|
|
37
|
+
candidates.push({
|
|
38
|
+
change: entry.name,
|
|
39
|
+
scope: 'active',
|
|
40
|
+
changeDir: path.join(changesRoot, entry.name),
|
|
41
|
+
rootKey: `active:${entry.name}`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const archiveRoot = path.join(changesRoot, 'archive');
|
|
46
|
+
if (fs.existsSync(archiveRoot)) {
|
|
47
|
+
for (const entry of fs.readdirSync(archiveRoot, { withFileTypes: true })) {
|
|
48
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name.includes('.staging-')) continue;
|
|
49
|
+
candidates.push({
|
|
50
|
+
change: entry.name,
|
|
51
|
+
scope: 'archive',
|
|
52
|
+
changeDir: path.join(archiveRoot, entry.name),
|
|
53
|
+
rootKey: `archive:${entry.name}`,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return candidates.sort((left, right) => left.changeDir.localeCompare(right.changeDir));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function validateArchiveCandidate(candidate, facts) {
|
|
62
|
+
const snapshotPath = path.join(candidate.changeDir, 'archive-ontology.json');
|
|
63
|
+
const manifestPath = path.join(candidate.changeDir, 'archive-manifest.json');
|
|
64
|
+
const snapshot = fs.existsSync(snapshotPath) ? readJson(snapshotPath) : null;
|
|
65
|
+
const manifest = fs.existsSync(manifestPath) ? readJson(manifestPath) : null;
|
|
66
|
+
const diagnostics = [];
|
|
67
|
+
|
|
68
|
+
if (!snapshot || snapshot.review_status !== 'confirmed' || !manifest) {
|
|
69
|
+
diagnostics.push(catalogDiagnostic(
|
|
70
|
+
DIAGNOSTIC_CODES.ARCHIVE_NOT_CONFIRMED,
|
|
71
|
+
`Archive 不能作为继承来源,缺少 confirmed snapshot 或 manifest: ${candidate.change}`,
|
|
72
|
+
{ file: snapshotPath, suggestion: '通过 KLD-SDD Archive 两阶段事务重新归档,禁止手工伪造 confirmed' },
|
|
73
|
+
));
|
|
74
|
+
return { eligible: false, snapshot, manifest, diagnostics };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const projectRoot = path.resolve(candidate.changeDir, '..', '..', '..', '..');
|
|
78
|
+
const declaredArchiveDir = manifest.archive_path
|
|
79
|
+
? path.resolve(projectRoot, manifest.archive_path)
|
|
80
|
+
: '';
|
|
81
|
+
const expectedArchiveDir = path.resolve(candidate.expectedArchiveDir || candidate.changeDir);
|
|
82
|
+
if (!declaredArchiveDir || declaredArchiveDir !== expectedArchiveDir) {
|
|
83
|
+
diagnostics.push(catalogDiagnostic(
|
|
84
|
+
DIAGNOSTIC_CODES.ARCHIVE_REVISION_MISMATCH,
|
|
85
|
+
`Archive manifest 路径与实际目录不一致: ${candidate.change}`,
|
|
86
|
+
{ file: manifestPath, suggestion: '通过 KLD-SDD Archive 事务重新生成 manifest,禁止复制旧 manifest 冒充新 Archive' },
|
|
87
|
+
));
|
|
88
|
+
return { eligible: false, snapshot, manifest, diagnostics };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!snapshot.facts_hash || snapshot.facts_hash !== facts.facts_hash) {
|
|
92
|
+
diagnostics.push(catalogDiagnostic(
|
|
93
|
+
DIAGNOSTIC_CODES.ARCHIVE_REVISION_MISMATCH,
|
|
94
|
+
`Archive snapshot 与实际归档正文不一致: ${candidate.change}`,
|
|
95
|
+
{ file: snapshotPath, suggestion: '重新解析归档正文并生成一致的 confirmed snapshot' },
|
|
96
|
+
));
|
|
97
|
+
return { eligible: false, snapshot, manifest, diagnostics };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const artifactIndexPath = path.join(candidate.changeDir, 'artifact-index.json');
|
|
101
|
+
if (!fs.existsSync(artifactIndexPath)) {
|
|
102
|
+
diagnostics.push(catalogDiagnostic(
|
|
103
|
+
DIAGNOSTIC_CODES.ARCHIVE_NOT_CONFIRMED,
|
|
104
|
+
`Archive 缺少 change 目录下的 artifact-index.json: ${candidate.change}`,
|
|
105
|
+
{ file: artifactIndexPath, suggestion: '通过 KLD-SDD reconcile/archive 在 change 目录生成工作态 JSON 后再归档' },
|
|
106
|
+
));
|
|
107
|
+
return { eligible: false, snapshot, manifest, diagnostics };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (manifest.schema_version === 'kld-sdd-archive-manifest/v2') {
|
|
111
|
+
const canonicalPath = path.join(
|
|
112
|
+
candidate.changeDir,
|
|
113
|
+
manifest.canonical_facts_path || 'canonical-facts.json',
|
|
114
|
+
);
|
|
115
|
+
if (!fs.existsSync(canonicalPath)) {
|
|
116
|
+
diagnostics.push(catalogDiagnostic(
|
|
117
|
+
DIAGNOSTIC_CODES.ARCHIVE_NOT_CONFIRMED,
|
|
118
|
+
`Archive v2 manifest 缺少 canonical facts: ${candidate.change}`,
|
|
119
|
+
{ file: canonicalPath, suggestion: '运行 archive-docs 或 materializeArchivePackage 生成 canonical-facts.json' },
|
|
120
|
+
));
|
|
121
|
+
return { eligible: false, snapshot, manifest, diagnostics };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { eligible: true, snapshot, manifest, diagnostics };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function recordsFor(candidate, facts, sourceRevision) {
|
|
129
|
+
return facts.entities
|
|
130
|
+
.filter((entity) => entity.entity_id || entity.version_id)
|
|
131
|
+
.map((entity) => ({
|
|
132
|
+
change: candidate.change,
|
|
133
|
+
scope: candidate.scope,
|
|
134
|
+
root_key: candidate.rootKey,
|
|
135
|
+
change_dir: candidate.changeDir,
|
|
136
|
+
source_revision: sourceRevision || undefined,
|
|
137
|
+
anchor_id: entity.anchor_id || entity.id,
|
|
138
|
+
type: entity.type,
|
|
139
|
+
entity_id: entity.entity_id,
|
|
140
|
+
version_id: entity.version_id,
|
|
141
|
+
predecessor_version_id: entity.predecessor_version_id,
|
|
142
|
+
delta_state: entity.delta_state,
|
|
143
|
+
content_hash: entity.content_hash,
|
|
144
|
+
source: entity.source,
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function groupBy(records, field) {
|
|
149
|
+
const grouped = new Map();
|
|
150
|
+
for (const record of records) {
|
|
151
|
+
const value = record[field];
|
|
152
|
+
if (!value) continue;
|
|
153
|
+
if (!grouped.has(value)) grouped.set(value, []);
|
|
154
|
+
grouped.get(value).push(record);
|
|
155
|
+
}
|
|
156
|
+
return grouped;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function buildCatalogDiagnostics(records) {
|
|
160
|
+
const diagnostics = [];
|
|
161
|
+
const byEntityId = groupBy(records, 'entity_id');
|
|
162
|
+
const byVersionId = groupBy(records, 'version_id');
|
|
163
|
+
const byAnchor = groupBy(records, 'anchor_id');
|
|
164
|
+
const byPredecessor = groupBy(records, 'predecessor_version_id');
|
|
165
|
+
|
|
166
|
+
for (const [entityId, items] of byEntityId.entries()) {
|
|
167
|
+
const signatures = new Set(items.map((item) => `${item.anchor_id}|${item.type}`));
|
|
168
|
+
if (signatures.size > 1) {
|
|
169
|
+
diagnostics.push(catalogDiagnostic(
|
|
170
|
+
DIAGNOSTIC_CODES.ENTITY_IDENTITY_CONFLICT,
|
|
171
|
+
`历史集合中同一 entity-id 对应多个锚点或类型: ${entityId}`,
|
|
172
|
+
{ entity_id: items[0].anchor_id, file: items[0].source && items[0].source.file },
|
|
173
|
+
));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const [versionId, items] of byVersionId.entries()) {
|
|
178
|
+
const signatures = new Set(items.map((item) => `${item.entity_id}|${item.content_hash}`));
|
|
179
|
+
if (signatures.size > 1) {
|
|
180
|
+
diagnostics.push(catalogDiagnostic(
|
|
181
|
+
DIAGNOSTIC_CODES.VERSION_IDENTITY_CONFLICT,
|
|
182
|
+
`历史集合中同一 version-id 对应不同实体或内容: ${versionId}`,
|
|
183
|
+
{ entity_id: items[0].anchor_id, file: items[0].source && items[0].source.file },
|
|
184
|
+
));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const [anchorId, items] of byAnchor.entries()) {
|
|
189
|
+
const entityIds = new Set(items.map((item) => item.entity_id).filter(Boolean));
|
|
190
|
+
if (entityIds.size > 1) {
|
|
191
|
+
diagnostics.push(catalogDiagnostic(
|
|
192
|
+
DIAGNOSTIC_CODES.ENTITY_IDENTITY_CONFLICT,
|
|
193
|
+
`历史锚点被不同 entity-id 复用: ${anchorId}`,
|
|
194
|
+
{ entity_id: anchorId, file: items[0].source && items[0].source.file },
|
|
195
|
+
));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (const [predecessorId, items] of byPredecessor.entries()) {
|
|
200
|
+
const successors = new Set(items.map((item) => item.version_id).filter(Boolean));
|
|
201
|
+
if (successors.size > 1) {
|
|
202
|
+
diagnostics.push(catalogDiagnostic(
|
|
203
|
+
DIAGNOSTIC_CODES.VERSION_LINEAGE_CONFLICT,
|
|
204
|
+
`历史集合中同一 predecessor 存在多个后继: ${predecessorId}`,
|
|
205
|
+
{ entity_id: items[0].anchor_id, file: items[0].source && items[0].source.file },
|
|
206
|
+
));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const record of records) {
|
|
211
|
+
if (!record.predecessor_version_id) continue;
|
|
212
|
+
const predecessors = byVersionId.get(record.predecessor_version_id) || [];
|
|
213
|
+
if (!predecessors.some((item) => item.entity_id === record.entity_id)) {
|
|
214
|
+
diagnostics.push(catalogDiagnostic(
|
|
215
|
+
DIAGNOSTIC_CODES.VERSION_LINEAGE_MISSING,
|
|
216
|
+
`历史版本 predecessor 不属于同一 entity 或不存在: ${record.anchor_id}`,
|
|
217
|
+
{ entity_id: record.anchor_id, file: record.source && record.source.file },
|
|
218
|
+
));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return diagnostics;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function buildIdentityCatalog(projectRoot, options = {}) {
|
|
226
|
+
const root = path.resolve(projectRoot || process.cwd());
|
|
227
|
+
const excluded = options.excludeChangeDir ? path.resolve(options.excludeChangeDir) : '';
|
|
228
|
+
const records = [];
|
|
229
|
+
const factsByRoot = new Map();
|
|
230
|
+
const diagnostics = [];
|
|
231
|
+
|
|
232
|
+
for (const candidate of identityRoots(root)) {
|
|
233
|
+
if (excluded && path.resolve(candidate.changeDir) === excluded) continue;
|
|
234
|
+
const facts = normalizeFacts(parseChangeArtifacts(root, candidate.change, {
|
|
235
|
+
changeDir: candidate.changeDir,
|
|
236
|
+
persistStructuralIdentities: false,
|
|
237
|
+
}));
|
|
238
|
+
let sourceRevision = '';
|
|
239
|
+
if (candidate.scope === 'archive') {
|
|
240
|
+
const archive = validateArchiveCandidate(candidate, facts);
|
|
241
|
+
diagnostics.push(...archive.diagnostics);
|
|
242
|
+
if (!archive.eligible) continue;
|
|
243
|
+
sourceRevision = archive.snapshot.source_revision || '';
|
|
244
|
+
}
|
|
245
|
+
factsByRoot.set(candidate.rootKey, {
|
|
246
|
+
...candidate,
|
|
247
|
+
source_revision: sourceRevision || undefined,
|
|
248
|
+
facts,
|
|
249
|
+
});
|
|
250
|
+
records.push(...recordsFor(candidate, facts, sourceRevision));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
records.sort((left, right) => (
|
|
254
|
+
String(left.entity_id || '').localeCompare(String(right.entity_id || ''))
|
|
255
|
+
|| String(left.version_id || '').localeCompare(String(right.version_id || ''))
|
|
256
|
+
|| String(left.change || '').localeCompare(String(right.change || ''))
|
|
257
|
+
|| String(left.anchor_id || '').localeCompare(String(right.anchor_id || ''))
|
|
258
|
+
));
|
|
259
|
+
diagnostics.push(...buildCatalogDiagnostics(records));
|
|
260
|
+
diagnostics.sort((left, right) => (
|
|
261
|
+
String(left.code || '').localeCompare(String(right.code || ''))
|
|
262
|
+
|| String(left.file || '').localeCompare(String(right.file || ''))
|
|
263
|
+
|| String(left.message || '').localeCompare(String(right.message || ''))
|
|
264
|
+
));
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
records,
|
|
268
|
+
factsByRoot,
|
|
269
|
+
byEntityId: groupBy(records, 'entity_id'),
|
|
270
|
+
byVersionId: groupBy(records, 'version_id'),
|
|
271
|
+
byAnchor: groupBy(records, 'anchor_id'),
|
|
272
|
+
byPredecessor: groupBy(records, 'predecessor_version_id'),
|
|
273
|
+
diagnostics,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function buildIdentityHistory(projectRoot, options = {}) {
|
|
278
|
+
return buildIdentityCatalog(projectRoot, options).records;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
module.exports = {
|
|
282
|
+
identityRoots,
|
|
283
|
+
validateArchiveCandidate,
|
|
284
|
+
buildCatalogDiagnostics,
|
|
285
|
+
buildIdentityCatalog,
|
|
286
|
+
buildIdentityHistory,
|
|
287
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { SCHEMA_VERSION } = require('./schema.cjs');
|
|
5
|
+
|
|
6
|
+
function compareText(left, right) {
|
|
7
|
+
return String(left || '').localeCompare(String(right || ''));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function stableRelationId(relation) {
|
|
11
|
+
return `${relation.type}:${relation.from_entity_id || relation.from}:${relation.to_entity_id || relation.to}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeFacts(parsed) {
|
|
15
|
+
const entities = (parsed.entities || []).map((entity) => ({
|
|
16
|
+
...entity,
|
|
17
|
+
id: String(entity.id || '').trim().toUpperCase(),
|
|
18
|
+
anchor_id: String(entity.anchor_id || entity.id || '').trim().toUpperCase(),
|
|
19
|
+
entity_id: String(entity.entity_id || '').trim().toLowerCase() || undefined,
|
|
20
|
+
version_id: String(entity.version_id || '').trim().toLowerCase() || undefined,
|
|
21
|
+
predecessor_version_id: String(entity.predecessor_version_id || '').trim().toLowerCase() || undefined,
|
|
22
|
+
delta_state: String(entity.delta_state || '').trim().toLowerCase() || undefined,
|
|
23
|
+
})).sort((left, right) => (
|
|
24
|
+
compareText(left.id, right.id)
|
|
25
|
+
|| compareText(left.type, right.type)
|
|
26
|
+
|| compareText(left.source && left.source.file, right.source && right.source.file)
|
|
27
|
+
|| Number((left.source && left.source.line) || 0) - Number((right.source && right.source.line) || 0)
|
|
28
|
+
));
|
|
29
|
+
|
|
30
|
+
const seenRelations = new Set();
|
|
31
|
+
const relations = [];
|
|
32
|
+
const identityByAnchor = new Map();
|
|
33
|
+
for (const entity of entities) {
|
|
34
|
+
if (!identityByAnchor.has(entity.id) && entity.entity_id) {
|
|
35
|
+
identityByAnchor.set(entity.id, entity.entity_id);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const reference of parsed.inheritedReferences || []) {
|
|
39
|
+
const anchorId = String(reference.anchor_id || '').toUpperCase();
|
|
40
|
+
const entityId = String(reference.entity_id || '').toLowerCase();
|
|
41
|
+
if (anchorId && entityId && !identityByAnchor.has(anchorId)) {
|
|
42
|
+
identityByAnchor.set(anchorId, entityId);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (const input of parsed.relations || []) {
|
|
46
|
+
const relation = {
|
|
47
|
+
...input,
|
|
48
|
+
from: String(input.from || '').trim().toUpperCase(),
|
|
49
|
+
to: String(input.to || '').trim().toUpperCase(),
|
|
50
|
+
};
|
|
51
|
+
relation.from_entity_id = identityByAnchor.get(relation.from);
|
|
52
|
+
relation.to_entity_id = identityByAnchor.get(relation.to);
|
|
53
|
+
relation.id = stableRelationId(relation);
|
|
54
|
+
const sourceKey = `${relation.id}:${relation.source && relation.source.file}:${relation.source && relation.source.line}`;
|
|
55
|
+
if (seenRelations.has(sourceKey)) continue;
|
|
56
|
+
seenRelations.add(sourceKey);
|
|
57
|
+
relations.push(relation);
|
|
58
|
+
}
|
|
59
|
+
relations.sort((left, right) => (
|
|
60
|
+
compareText(left.id, right.id)
|
|
61
|
+
|| compareText(left.source && left.source.file, right.source && right.source.file)
|
|
62
|
+
|| Number((left.source && left.source.line) || 0) - Number((right.source && right.source.line) || 0)
|
|
63
|
+
));
|
|
64
|
+
|
|
65
|
+
const diagnostics = (parsed.diagnostics || []).slice().sort((left, right) => (
|
|
66
|
+
compareText(left.code, right.code)
|
|
67
|
+
|| compareText(left.file, right.file)
|
|
68
|
+
|| Number(left.line || 0) - Number(right.line || 0)
|
|
69
|
+
));
|
|
70
|
+
|
|
71
|
+
const facts = {
|
|
72
|
+
schema_version: SCHEMA_VERSION,
|
|
73
|
+
change: parsed.changeName,
|
|
74
|
+
change_id: parsed.changeId || undefined,
|
|
75
|
+
profile: parsed.profile,
|
|
76
|
+
proposal_mode: parsed.proposalMode || undefined,
|
|
77
|
+
artifacts: (parsed.artifacts || []).slice().sort((left, right) => compareText(left.path, right.path)),
|
|
78
|
+
files: (parsed.files || []).slice().sort((left, right) => compareText(left.path, right.path)),
|
|
79
|
+
entities,
|
|
80
|
+
relations,
|
|
81
|
+
inherited_references: (parsed.inheritedReferences || []).map((reference) => ({
|
|
82
|
+
...reference,
|
|
83
|
+
anchor_id: String(reference.anchor_id || '').toUpperCase(),
|
|
84
|
+
entity_id: String(reference.entity_id || '').toLowerCase() || undefined,
|
|
85
|
+
version_id: String(reference.version_id || '').toLowerCase() || undefined,
|
|
86
|
+
delta_state: String(reference.delta_state || '').toLowerCase() || undefined,
|
|
87
|
+
})).sort((left, right) => compareText(left.anchor_id, right.anchor_id)),
|
|
88
|
+
inherited_relations: (parsed.inheritedRelations || []).map((relation) => ({
|
|
89
|
+
...relation,
|
|
90
|
+
type: String(relation.type || '').trim(),
|
|
91
|
+
from: String(relation.from || '').trim().toUpperCase(),
|
|
92
|
+
to: String(relation.to || '').trim().toUpperCase(),
|
|
93
|
+
})).sort((left, right) => (
|
|
94
|
+
compareText(left.type, right.type)
|
|
95
|
+
|| compareText(left.from, right.from)
|
|
96
|
+
|| compareText(left.to, right.to)
|
|
97
|
+
)),
|
|
98
|
+
diagnostics,
|
|
99
|
+
};
|
|
100
|
+
facts.facts_hash = crypto.createHash('sha256').update(JSON.stringify(facts)).digest('hex');
|
|
101
|
+
return facts;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
stableRelationId,
|
|
106
|
+
normalizeFacts,
|
|
107
|
+
};
|