yarramate 0.1.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/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/adapter-mapping.d.ts +40 -0
- package/dist/adapter-mapping.js +200 -0
- package/dist/adapters/graphify-cli.d.ts +3 -0
- package/dist/adapters/graphify-cli.js +127 -0
- package/dist/adapters/graphify-entry.d.ts +1 -0
- package/dist/adapters/graphify-entry.js +1 -0
- package/dist/adapters/graphify.d.ts +23 -0
- package/dist/adapters/graphify.js +47 -0
- package/dist/adapters/likec4-cli.d.ts +3 -0
- package/dist/adapters/likec4-cli.js +662 -0
- package/dist/adapters/likec4-export.d.ts +25 -0
- package/dist/adapters/likec4-export.js +204 -0
- package/dist/adapters/likec4-kind-mapping.d.ts +22 -0
- package/dist/adapters/likec4-kind-mapping.js +60 -0
- package/dist/adapters/likec4-prepare.d.ts +28 -0
- package/dist/adapters/likec4-prepare.js +154 -0
- package/dist/adapters/likec4-project.d.ts +58 -0
- package/dist/adapters/likec4-project.js +176 -0
- package/dist/adapters/likec4.d.ts +4 -0
- package/dist/adapters/likec4.js +4 -0
- package/dist/architecture-state.d.ts +22 -0
- package/dist/architecture-state.js +63 -0
- package/dist/check-command.d.ts +2 -0
- package/dist/check-command.js +232 -0
- package/dist/cli-support.d.ts +24 -0
- package/dist/cli-support.js +81 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +575 -0
- package/dist/compiler.d.ts +66 -0
- package/dist/compiler.js +940 -0
- package/dist/core-contract.d.ts +43 -0
- package/dist/core-contract.js +162 -0
- package/dist/evidence.d.ts +58 -0
- package/dist/evidence.js +161 -0
- package/dist/graph.d.ts +2 -0
- package/dist/graph.js +37 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +9 -0
- package/dist/profile.d.ts +30 -0
- package/dist/profile.js +162 -0
- package/dist/projection.d.ts +42 -0
- package/dist/projection.js +183 -0
- package/dist/reconciliation.d.ts +26 -0
- package/dist/reconciliation.js +47 -0
- package/dist/source-document.d.ts +24 -0
- package/dist/source-document.js +80 -0
- package/dist/workspace.d.ts +29 -0
- package/dist/workspace.js +114 -0
- package/docs/CONSUMING-YARRAMATE.md +145 -0
- package/package.json +110 -0
- package/schema/yarramate-adapter-mapping.schema.json +59 -0
- package/schema/yarramate-check-result.schema.json +92 -0
- package/schema/yarramate-core-contract.schema.json +132 -0
- package/schema/yarramate-diagnostic-result.schema.json +64 -0
- package/schema/yarramate-document.schema.json +168 -0
- package/schema/yarramate-evidence-report.schema.json +73 -0
- package/schema/yarramate-evidence.schema.json +92 -0
- package/schema/yarramate-graph-v2.schema.json +170 -0
- package/schema/yarramate-likec4-check-result.schema.json +50 -0
- package/schema/yarramate-likec4-diagnostic-result.schema.json +69 -0
- package/schema/yarramate-likec4-generated-project-v2.schema.json +100 -0
- package/schema/yarramate-likec4-generated-project.schema.json +76 -0
- package/schema/yarramate-likec4-kind-mapping.schema.json +65 -0
- package/schema/yarramate-likec4-project.schema.json +160 -0
- package/schema/yarramate-profile.schema.json +113 -0
- package/schema/yarramate-projection-result.schema.json +177 -0
- package/schema/yarramate-projection.schema.json +126 -0
- package/schema/yarramate-reconciliation-report.schema.json +90 -0
- package/schema/yarramate-state-comparison.schema.json +61 -0
- package/schema/yarramate-workspace.schema.json +55 -0
- package/skills/yarramate-architecture/SKILL.md +135 -0
- package/skills/yarramate-architecture/agents/openai.yaml +4 -0
- package/skills/yarramate-architecture/references/journey-checklists.md +57 -0
- package/skills/yarramate-architecture/references/native-authoring.md +167 -0
package/dist/compiler.js
ADDED
|
@@ -0,0 +1,940 @@
|
|
|
1
|
+
import Ajv2020Module from 'ajv/dist/2020.js';
|
|
2
|
+
import { LineCounter, parseDocument } from 'yaml';
|
|
3
|
+
import { conceptKinds, relationshipPolicies, } from './profile.js';
|
|
4
|
+
import documentSchema from '../schema/yarramate-document.schema.json' with {
|
|
5
|
+
type: 'json'
|
|
6
|
+
};
|
|
7
|
+
import profileSchema from '../schema/yarramate-profile.schema.json' with {
|
|
8
|
+
type: 'json'
|
|
9
|
+
};
|
|
10
|
+
const coreProfile = 'yarramate/core@0.1';
|
|
11
|
+
const Ajv2020 = Ajv2020Module.default;
|
|
12
|
+
const validateDocument = new Ajv2020({ allErrors: true }).compile(documentSchema);
|
|
13
|
+
const validateProfile = new Ajv2020({ allErrors: true }).compile(profileSchema);
|
|
14
|
+
const compareById = (left, right) => left.id.localeCompare(right.id);
|
|
15
|
+
const presenceClaimId = (subject, state) => `${subject}~present-in-${Buffer.from(state, 'utf8').toString('hex')}`;
|
|
16
|
+
const diagnosticFailure = (diagnostics) => ({
|
|
17
|
+
ok: false,
|
|
18
|
+
diagnostics: [...diagnostics].sort((left, right) => left.path.localeCompare(right.path) ||
|
|
19
|
+
left.line - right.line ||
|
|
20
|
+
left.column - right.column ||
|
|
21
|
+
left.code.localeCompare(right.code) ||
|
|
22
|
+
left.message.localeCompare(right.message)),
|
|
23
|
+
});
|
|
24
|
+
function compileWorkspaceResolved(sources) {
|
|
25
|
+
const profileInputs = [];
|
|
26
|
+
const documentInputs = [];
|
|
27
|
+
for (const source of sources) {
|
|
28
|
+
const probe = parseDocument(source.source);
|
|
29
|
+
if (probe.get('format') === 'yarramate/profile/v1') {
|
|
30
|
+
profileInputs.push(source);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
documentInputs.push(source);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const profiles = new Map();
|
|
37
|
+
const conceptKindByIdentity = new Map();
|
|
38
|
+
const relationshipKindByIdentity = new Map();
|
|
39
|
+
const coreConceptKinds = new Map();
|
|
40
|
+
for (const kind of conceptKinds) {
|
|
41
|
+
const resolved = {
|
|
42
|
+
identity: `${coreProfile}#${kind.id}`,
|
|
43
|
+
aspect: kind.aspect,
|
|
44
|
+
lineage: [`${coreProfile}#${kind.id}`],
|
|
45
|
+
};
|
|
46
|
+
coreConceptKinds.set(kind.id, resolved);
|
|
47
|
+
conceptKindByIdentity.set(resolved.identity, resolved);
|
|
48
|
+
}
|
|
49
|
+
const coreRelationshipKinds = new Map();
|
|
50
|
+
for (const policy of relationshipPolicies) {
|
|
51
|
+
const resolved = {
|
|
52
|
+
identity: `${coreProfile}#${policy.id}`,
|
|
53
|
+
lineage: [`${coreProfile}#${policy.id}`],
|
|
54
|
+
sourceAspects: policy.sourceAspects,
|
|
55
|
+
targetAspects: policy.targetAspects,
|
|
56
|
+
};
|
|
57
|
+
coreRelationshipKinds.set(policy.id, resolved);
|
|
58
|
+
relationshipKindByIdentity.set(resolved.identity, resolved);
|
|
59
|
+
}
|
|
60
|
+
profiles.set(coreProfile, {
|
|
61
|
+
identity: coreProfile,
|
|
62
|
+
lineage: [coreProfile],
|
|
63
|
+
conceptKinds: coreConceptKinds,
|
|
64
|
+
relationshipKinds: coreRelationshipKinds,
|
|
65
|
+
});
|
|
66
|
+
const profileDiagnostics = [];
|
|
67
|
+
const pendingProfiles = [];
|
|
68
|
+
for (const input of profileInputs) {
|
|
69
|
+
const lineCounter = new LineCounter();
|
|
70
|
+
const yaml = parseDocument(input.source, { lineCounter });
|
|
71
|
+
const value = yaml.toJS();
|
|
72
|
+
const positionFor = (yamlPath) => {
|
|
73
|
+
const node = yaml.getIn(yamlPath, true);
|
|
74
|
+
const offset = typeof node === 'object' &&
|
|
75
|
+
node !== null &&
|
|
76
|
+
'range' in node &&
|
|
77
|
+
Array.isArray(node.range)
|
|
78
|
+
? node.range[0]
|
|
79
|
+
: 0;
|
|
80
|
+
return lineCounter.linePos(offset);
|
|
81
|
+
};
|
|
82
|
+
if (yaml.errors.length > 0) {
|
|
83
|
+
for (const error of yaml.errors) {
|
|
84
|
+
const position = error.linePos?.[0] ?? { line: 1, col: 1 };
|
|
85
|
+
profileDiagnostics.push({
|
|
86
|
+
severity: 'error',
|
|
87
|
+
code: 'YM101',
|
|
88
|
+
message: error.message.split(' at line ')[0] ?? error.message,
|
|
89
|
+
path: input.path,
|
|
90
|
+
pointer: '/',
|
|
91
|
+
line: position.line,
|
|
92
|
+
column: position.col,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!validateProfile(value)) {
|
|
98
|
+
for (const error of validateProfile.errors ?? []) {
|
|
99
|
+
const property = error.keyword === 'additionalProperties'
|
|
100
|
+
? String(error.params.additionalProperty)
|
|
101
|
+
: undefined;
|
|
102
|
+
const pointer = property
|
|
103
|
+
? `${error.instancePath}/${property}`
|
|
104
|
+
: error.instancePath || '/';
|
|
105
|
+
const yamlPath = pointer
|
|
106
|
+
.split('/')
|
|
107
|
+
.slice(1)
|
|
108
|
+
.map((segment) => /^\d+$/.test(segment) ? Number(segment) : segment);
|
|
109
|
+
const position = positionFor(yamlPath);
|
|
110
|
+
profileDiagnostics.push({
|
|
111
|
+
severity: 'error',
|
|
112
|
+
code: 'YM201',
|
|
113
|
+
message: property
|
|
114
|
+
? `Property "${property}" is not allowed`
|
|
115
|
+
: `Profile schema violation: ${error.message ?? error.keyword}`,
|
|
116
|
+
path: input.path,
|
|
117
|
+
pointer,
|
|
118
|
+
line: position.line,
|
|
119
|
+
column: position.col,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const identity = `${value.id}@${value.version}`;
|
|
125
|
+
pendingProfiles.push({ input, value, identity, positionFor });
|
|
126
|
+
}
|
|
127
|
+
let unresolvedProfiles = pendingProfiles.sort((left, right) => left.identity.localeCompare(right.identity) ||
|
|
128
|
+
left.input.path.localeCompare(right.input.path));
|
|
129
|
+
while (unresolvedProfiles.length > 0) {
|
|
130
|
+
const next = [];
|
|
131
|
+
let resolvedAny = false;
|
|
132
|
+
for (const entry of unresolvedProfiles) {
|
|
133
|
+
const { input, value, identity, positionFor } = entry;
|
|
134
|
+
const parentProfile = profiles.get(value.extends);
|
|
135
|
+
if (parentProfile === undefined) {
|
|
136
|
+
next.push(entry);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (profiles.has(identity)) {
|
|
140
|
+
const position = positionFor(['id']);
|
|
141
|
+
profileDiagnostics.push({
|
|
142
|
+
severity: 'error',
|
|
143
|
+
code: 'YM411',
|
|
144
|
+
message: `Profile "${identity}" is declared more than once`,
|
|
145
|
+
path: input.path,
|
|
146
|
+
pointer: '/id',
|
|
147
|
+
line: position.line,
|
|
148
|
+
column: position.col,
|
|
149
|
+
});
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const resolvedConceptKinds = new Map(parentProfile.conceptKinds);
|
|
153
|
+
for (const [index, kind] of value.conceptKinds.entries()) {
|
|
154
|
+
if (resolvedConceptKinds.has(kind.id)) {
|
|
155
|
+
const position = positionFor(['conceptKinds', index, 'id']);
|
|
156
|
+
profileDiagnostics.push({
|
|
157
|
+
severity: 'error',
|
|
158
|
+
code: 'YM409',
|
|
159
|
+
message: `Concept kind "${kind.id}" conflicts with an inherited kind`,
|
|
160
|
+
path: input.path,
|
|
161
|
+
pointer: `/conceptKinds/${index}/id`,
|
|
162
|
+
line: position.line,
|
|
163
|
+
column: position.col,
|
|
164
|
+
});
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const parent = conceptKindByIdentity.get(kind.parent);
|
|
168
|
+
if (parent === undefined) {
|
|
169
|
+
const position = positionFor(['conceptKinds', index, 'parent']);
|
|
170
|
+
profileDiagnostics.push({
|
|
171
|
+
severity: 'error',
|
|
172
|
+
code: 'YM407',
|
|
173
|
+
message: `Concept parent "${kind.parent}" is not available`,
|
|
174
|
+
path: input.path,
|
|
175
|
+
pointer: `/conceptKinds/${index}/parent`,
|
|
176
|
+
line: position.line,
|
|
177
|
+
column: position.col,
|
|
178
|
+
});
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const resolved = {
|
|
182
|
+
identity: `${identity}#${kind.id}`,
|
|
183
|
+
aspect: parent.aspect,
|
|
184
|
+
lineage: [...parent.lineage, `${identity}#${kind.id}`],
|
|
185
|
+
};
|
|
186
|
+
resolvedConceptKinds.set(kind.id, resolved);
|
|
187
|
+
conceptKindByIdentity.set(resolved.identity, resolved);
|
|
188
|
+
}
|
|
189
|
+
const resolvedRelationshipKinds = new Map(parentProfile.relationshipKinds);
|
|
190
|
+
for (const [index, kind] of value.relationshipKinds.entries()) {
|
|
191
|
+
if (resolvedRelationshipKinds.has(kind.id)) {
|
|
192
|
+
const position = positionFor(['relationshipKinds', index, 'id']);
|
|
193
|
+
profileDiagnostics.push({
|
|
194
|
+
severity: 'error',
|
|
195
|
+
code: 'YM410',
|
|
196
|
+
message: `Relationship kind "${kind.id}" conflicts with an inherited kind`,
|
|
197
|
+
path: input.path,
|
|
198
|
+
pointer: `/relationshipKinds/${index}/id`,
|
|
199
|
+
line: position.line,
|
|
200
|
+
column: position.col,
|
|
201
|
+
});
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const parent = relationshipKindByIdentity.get(kind.parent);
|
|
205
|
+
if (parent === undefined) {
|
|
206
|
+
const position = positionFor(['relationshipKinds', index, 'parent']);
|
|
207
|
+
profileDiagnostics.push({
|
|
208
|
+
severity: 'error',
|
|
209
|
+
code: 'YM408',
|
|
210
|
+
message: `Relationship parent "${kind.parent}" is not available`,
|
|
211
|
+
path: input.path,
|
|
212
|
+
pointer: `/relationshipKinds/${index}/parent`,
|
|
213
|
+
line: position.line,
|
|
214
|
+
column: position.col,
|
|
215
|
+
});
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
let broadensParent = false;
|
|
219
|
+
for (const endpoint of ['source', 'target']) {
|
|
220
|
+
const field = `${endpoint}Aspects`;
|
|
221
|
+
const declared = kind[field];
|
|
222
|
+
const inherited = parent[field];
|
|
223
|
+
if (declared !== undefined &&
|
|
224
|
+
inherited !== undefined &&
|
|
225
|
+
declared.some((aspect) => !inherited.includes(aspect))) {
|
|
226
|
+
const position = positionFor([
|
|
227
|
+
'relationshipKinds',
|
|
228
|
+
index,
|
|
229
|
+
field,
|
|
230
|
+
]);
|
|
231
|
+
profileDiagnostics.push({
|
|
232
|
+
severity: 'error',
|
|
233
|
+
code: 'YM412',
|
|
234
|
+
message: `Relationship kind "${kind.id}" broadens its parent ${endpoint} aspects`,
|
|
235
|
+
path: input.path,
|
|
236
|
+
pointer: `/relationshipKinds/${index}/${field}`,
|
|
237
|
+
line: position.line,
|
|
238
|
+
column: position.col,
|
|
239
|
+
});
|
|
240
|
+
broadensParent = true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (broadensParent) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const resolved = {
|
|
247
|
+
identity: `${identity}#${kind.id}`,
|
|
248
|
+
lineage: [...parent.lineage, `${identity}#${kind.id}`],
|
|
249
|
+
sourceAspects: kind.sourceAspects ?? parent.sourceAspects,
|
|
250
|
+
targetAspects: kind.targetAspects ?? parent.targetAspects,
|
|
251
|
+
};
|
|
252
|
+
resolvedRelationshipKinds.set(kind.id, resolved);
|
|
253
|
+
relationshipKindByIdentity.set(resolved.identity, resolved);
|
|
254
|
+
}
|
|
255
|
+
profiles.set(identity, {
|
|
256
|
+
identity,
|
|
257
|
+
lineage: [...parentProfile.lineage, identity],
|
|
258
|
+
conceptKinds: resolvedConceptKinds,
|
|
259
|
+
relationshipKinds: resolvedRelationshipKinds,
|
|
260
|
+
});
|
|
261
|
+
resolvedAny = true;
|
|
262
|
+
}
|
|
263
|
+
if (!resolvedAny) {
|
|
264
|
+
for (const { input, value, positionFor } of next) {
|
|
265
|
+
const position = positionFor(['extends']);
|
|
266
|
+
profileDiagnostics.push({
|
|
267
|
+
severity: 'error',
|
|
268
|
+
code: 'YM406',
|
|
269
|
+
message: `Parent profile "${value.extends}" is not available`,
|
|
270
|
+
path: input.path,
|
|
271
|
+
pointer: '/extends',
|
|
272
|
+
line: position.line,
|
|
273
|
+
column: position.col,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
unresolvedProfiles = next;
|
|
279
|
+
}
|
|
280
|
+
if (profileDiagnostics.length > 0) {
|
|
281
|
+
return diagnosticFailure(profileDiagnostics);
|
|
282
|
+
}
|
|
283
|
+
const documents = documentInputs.map((input) => {
|
|
284
|
+
const lineCounter = new LineCounter();
|
|
285
|
+
const yaml = parseDocument(input.source, { lineCounter });
|
|
286
|
+
const value = yaml.toJS();
|
|
287
|
+
const location = (yamlPath, pointer) => {
|
|
288
|
+
const node = yaml.getIn(yamlPath, true);
|
|
289
|
+
const offset = typeof node === 'object' &&
|
|
290
|
+
node !== null &&
|
|
291
|
+
'range' in node &&
|
|
292
|
+
Array.isArray(node.range)
|
|
293
|
+
? node.range[0]
|
|
294
|
+
: 0;
|
|
295
|
+
const position = lineCounter.linePos(offset);
|
|
296
|
+
return {
|
|
297
|
+
document: value?.id ?? '<unknown>',
|
|
298
|
+
path: input.path,
|
|
299
|
+
pointer,
|
|
300
|
+
line: position.line,
|
|
301
|
+
column: position.col,
|
|
302
|
+
};
|
|
303
|
+
};
|
|
304
|
+
const parseDiagnostics = yaml.errors.map((error) => {
|
|
305
|
+
const position = error.linePos?.[0] ?? { line: 1, col: 1 };
|
|
306
|
+
return {
|
|
307
|
+
severity: 'error',
|
|
308
|
+
code: 'YM101',
|
|
309
|
+
message: error.message.split(' at line ')[0] ?? error.message,
|
|
310
|
+
path: input.path,
|
|
311
|
+
pointer: '/',
|
|
312
|
+
line: position.line,
|
|
313
|
+
column: position.col,
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
const valid = parseDiagnostics.length === 0 && validateDocument(value);
|
|
317
|
+
const schemaDiagnostics = parseDiagnostics.length > 0
|
|
318
|
+
? parseDiagnostics
|
|
319
|
+
: valid
|
|
320
|
+
? []
|
|
321
|
+
: (validateDocument.errors ?? []).map((error) => {
|
|
322
|
+
const property = error.keyword === 'additionalProperties'
|
|
323
|
+
? String(error.params.additionalProperty)
|
|
324
|
+
: undefined;
|
|
325
|
+
const pointer = property
|
|
326
|
+
? `${error.instancePath}/${property}`
|
|
327
|
+
: error.instancePath || '/';
|
|
328
|
+
const yamlPath = pointer
|
|
329
|
+
.split('/')
|
|
330
|
+
.slice(1)
|
|
331
|
+
.map((segment) => /^\d+$/.test(segment) ? Number(segment) : segment);
|
|
332
|
+
const source = location(yamlPath, pointer);
|
|
333
|
+
return {
|
|
334
|
+
severity: 'error',
|
|
335
|
+
code: 'YM201',
|
|
336
|
+
message: property
|
|
337
|
+
? `Property "${property}" is not allowed`
|
|
338
|
+
: `Document schema violation: ${error.message ?? error.keyword}`,
|
|
339
|
+
path: input.path,
|
|
340
|
+
pointer,
|
|
341
|
+
line: source.line,
|
|
342
|
+
column: source.column,
|
|
343
|
+
};
|
|
344
|
+
});
|
|
345
|
+
return { input, value, location, schemaDiagnostics };
|
|
346
|
+
});
|
|
347
|
+
const claims = [];
|
|
348
|
+
const subjects = [];
|
|
349
|
+
const diagnostics = documents.flatMap(({ schemaDiagnostics }) => schemaDiagnostics);
|
|
350
|
+
if (diagnostics.length > 0) {
|
|
351
|
+
return diagnosticFailure(diagnostics);
|
|
352
|
+
}
|
|
353
|
+
const seenDocumentIds = new Set();
|
|
354
|
+
for (const { input, value, location } of documents) {
|
|
355
|
+
if (seenDocumentIds.has(value.id)) {
|
|
356
|
+
const source = location(['id'], '/id');
|
|
357
|
+
diagnostics.push({
|
|
358
|
+
severity: 'error',
|
|
359
|
+
code: 'YM303',
|
|
360
|
+
message: `Duplicate document ID "${value.id}"`,
|
|
361
|
+
path: input.path,
|
|
362
|
+
pointer: '/id',
|
|
363
|
+
line: source.line,
|
|
364
|
+
column: source.column,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
seenDocumentIds.add(value.id);
|
|
368
|
+
}
|
|
369
|
+
const conceptByQualifiedId = new Map(documents.flatMap(({ value }) => value.concepts.map((concept) => [
|
|
370
|
+
`${value.id}#${concept.id}`,
|
|
371
|
+
{
|
|
372
|
+
concept,
|
|
373
|
+
profile: value.profile,
|
|
374
|
+
document: value.id,
|
|
375
|
+
},
|
|
376
|
+
])));
|
|
377
|
+
const qualifyReference = (documentId, reference) => reference.includes('#') ? reference : `${documentId}#${reference}`;
|
|
378
|
+
const architectureStateIds = new Set(documents.flatMap(({ value }) => (value.states ?? []).map((state) => `${value.id}#${state.id}`)));
|
|
379
|
+
const architectureStateAfter = new Map(documents.flatMap(({ value }) => (value.states ?? []).flatMap((state) => state.after === undefined
|
|
380
|
+
? []
|
|
381
|
+
: [
|
|
382
|
+
[
|
|
383
|
+
`${value.id}#${state.id}`,
|
|
384
|
+
qualifyReference(value.id, state.after),
|
|
385
|
+
],
|
|
386
|
+
])));
|
|
387
|
+
const participatesInStateCycle = (start) => {
|
|
388
|
+
const visited = new Set();
|
|
389
|
+
let current = architectureStateAfter.get(start);
|
|
390
|
+
while (current !== undefined) {
|
|
391
|
+
if (current === start)
|
|
392
|
+
return true;
|
|
393
|
+
if (visited.has(current))
|
|
394
|
+
return false;
|
|
395
|
+
visited.add(current);
|
|
396
|
+
current = architectureStateAfter.get(current);
|
|
397
|
+
}
|
|
398
|
+
return false;
|
|
399
|
+
};
|
|
400
|
+
for (const { input, value, location } of documents) {
|
|
401
|
+
const selectedProfile = profiles.get(value.profile);
|
|
402
|
+
if (selectedProfile === undefined) {
|
|
403
|
+
const source = location(['profile'], '/profile');
|
|
404
|
+
diagnostics.push({
|
|
405
|
+
severity: 'error',
|
|
406
|
+
code: 'YM403',
|
|
407
|
+
message: `Profile "${value.profile}" is not available`,
|
|
408
|
+
path: input.path,
|
|
409
|
+
pointer: '/profile',
|
|
410
|
+
line: source.line,
|
|
411
|
+
column: source.column,
|
|
412
|
+
});
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const seenIds = new Set();
|
|
416
|
+
const declarations = [
|
|
417
|
+
...(value.states ?? []).map((state, index) => ({
|
|
418
|
+
id: state.id,
|
|
419
|
+
yamlPath: ['states', index, 'id'],
|
|
420
|
+
pointer: `/states/${index}/id`,
|
|
421
|
+
})),
|
|
422
|
+
...value.concepts.map((concept, index) => ({
|
|
423
|
+
id: concept.id,
|
|
424
|
+
yamlPath: ['concepts', index, 'id'],
|
|
425
|
+
pointer: `/concepts/${index}/id`,
|
|
426
|
+
})),
|
|
427
|
+
...value.relationships.map((relationship, index) => ({
|
|
428
|
+
id: relationship.id,
|
|
429
|
+
yamlPath: ['relationships', index, 'id'],
|
|
430
|
+
pointer: `/relationships/${index}/id`,
|
|
431
|
+
})),
|
|
432
|
+
];
|
|
433
|
+
for (const declaration of declarations) {
|
|
434
|
+
if (seenIds.has(declaration.id)) {
|
|
435
|
+
const source = location(declaration.yamlPath, declaration.pointer);
|
|
436
|
+
diagnostics.push({
|
|
437
|
+
severity: 'error',
|
|
438
|
+
code: 'YM301',
|
|
439
|
+
message: `Duplicate local ID "${declaration.id}"`,
|
|
440
|
+
path: input.path,
|
|
441
|
+
pointer: declaration.pointer,
|
|
442
|
+
line: source.line,
|
|
443
|
+
column: source.column,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
seenIds.add(declaration.id);
|
|
447
|
+
}
|
|
448
|
+
for (const [index, state] of (value.states ?? []).entries()) {
|
|
449
|
+
const stateIdentity = `${value.id}#${state.id}`;
|
|
450
|
+
if (state.after !== undefined &&
|
|
451
|
+
!architectureStateIds.has(qualifyReference(value.id, state.after))) {
|
|
452
|
+
const pointer = `/states/${index}/after`;
|
|
453
|
+
const source = location(['states', index, 'after'], pointer);
|
|
454
|
+
diagnostics.push({
|
|
455
|
+
severity: 'error',
|
|
456
|
+
code: 'YM307',
|
|
457
|
+
message: `Unresolved architecture state reference "${state.after}"`,
|
|
458
|
+
path: input.path,
|
|
459
|
+
pointer,
|
|
460
|
+
line: source.line,
|
|
461
|
+
column: source.column,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
if (state.after !== undefined &&
|
|
465
|
+
participatesInStateCycle(stateIdentity)) {
|
|
466
|
+
const pointer = `/states/${index}/after`;
|
|
467
|
+
const source = location(['states', index, 'after'], pointer);
|
|
468
|
+
diagnostics.push({
|
|
469
|
+
severity: 'error',
|
|
470
|
+
code: 'YM502',
|
|
471
|
+
message: `Architecture state "${stateIdentity}" participates in an ordering cycle`,
|
|
472
|
+
path: input.path,
|
|
473
|
+
pointer,
|
|
474
|
+
line: source.line,
|
|
475
|
+
column: source.column,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
for (const [index, state] of (value.states ?? []).entries()) {
|
|
480
|
+
const subject = `${value.id}#${state.id}`;
|
|
481
|
+
subjects.push({ id: subject, type: 'concept' });
|
|
482
|
+
claims.push({
|
|
483
|
+
id: `${subject}~kind`,
|
|
484
|
+
subject,
|
|
485
|
+
predicate: 'yarramate/concept/kind',
|
|
486
|
+
object: { value: `${coreProfile}#plateau` },
|
|
487
|
+
origin: 'declared',
|
|
488
|
+
source: location(['states', index, 'kind'], `/states/${index}/kind`),
|
|
489
|
+
}, {
|
|
490
|
+
id: `${subject}~name`,
|
|
491
|
+
subject,
|
|
492
|
+
predicate: 'yarramate/concept/name',
|
|
493
|
+
object: { value: state.name },
|
|
494
|
+
origin: 'declared',
|
|
495
|
+
source: location(['states', index, 'name'], `/states/${index}/name`),
|
|
496
|
+
}, {
|
|
497
|
+
id: `${subject}~state-type`,
|
|
498
|
+
subject,
|
|
499
|
+
predicate: 'yarramate/state/type',
|
|
500
|
+
object: { value: state.kind },
|
|
501
|
+
origin: 'declared',
|
|
502
|
+
source: location(['states', index, 'kind'], `/states/${index}/kind`),
|
|
503
|
+
});
|
|
504
|
+
if (state.description !== undefined) {
|
|
505
|
+
claims.push({
|
|
506
|
+
id: `${subject}~description`,
|
|
507
|
+
subject,
|
|
508
|
+
predicate: 'yarramate/concept/description',
|
|
509
|
+
object: { value: state.description },
|
|
510
|
+
origin: 'declared',
|
|
511
|
+
source: location(['states', index, 'description'], `/states/${index}/description`),
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
if (state.after !== undefined) {
|
|
515
|
+
claims.push({
|
|
516
|
+
id: `${subject}~after`,
|
|
517
|
+
subject,
|
|
518
|
+
predicate: 'yarramate/state/after',
|
|
519
|
+
object: {
|
|
520
|
+
ref: qualifyReference(value.id, state.after),
|
|
521
|
+
},
|
|
522
|
+
origin: 'declared',
|
|
523
|
+
source: location(['states', index, 'after'], `/states/${index}/after`),
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
for (const [index, concept] of value.concepts.entries()) {
|
|
528
|
+
if (!selectedProfile.conceptKinds.has(concept.kind)) {
|
|
529
|
+
const pointer = `/concepts/${index}/kind`;
|
|
530
|
+
const source = location(['concepts', index, 'kind'], pointer);
|
|
531
|
+
diagnostics.push({
|
|
532
|
+
severity: 'error',
|
|
533
|
+
code: 'YM401',
|
|
534
|
+
message: `Unknown concept kind "${concept.kind}" in profile "${value.profile}"`,
|
|
535
|
+
path: input.path,
|
|
536
|
+
pointer,
|
|
537
|
+
line: source.line,
|
|
538
|
+
column: source.column,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
if (concept.owner !== undefined &&
|
|
542
|
+
!conceptByQualifiedId.has(qualifyReference(value.id, concept.owner))) {
|
|
543
|
+
const pointer = `/concepts/${index}/owner`;
|
|
544
|
+
const source = location(['concepts', index, 'owner'], pointer);
|
|
545
|
+
diagnostics.push({
|
|
546
|
+
severity: 'error',
|
|
547
|
+
code: 'YM304',
|
|
548
|
+
message: `Unresolved owner reference "${concept.owner}"`,
|
|
549
|
+
path: input.path,
|
|
550
|
+
pointer,
|
|
551
|
+
line: source.line,
|
|
552
|
+
column: source.column,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
const seenConstraintIds = new Set();
|
|
556
|
+
for (const [constraintIndex, constraint] of (concept.constraints ?? []).entries()) {
|
|
557
|
+
if (seenConstraintIds.has(constraint.id)) {
|
|
558
|
+
const pointer = `/concepts/${index}/constraints/${constraintIndex}/id`;
|
|
559
|
+
const source = location(['concepts', index, 'constraints', constraintIndex, 'id'], pointer);
|
|
560
|
+
diagnostics.push({
|
|
561
|
+
severity: 'error',
|
|
562
|
+
code: 'YM306',
|
|
563
|
+
message: `Duplicate constraint ID "${constraint.id}"`,
|
|
564
|
+
path: input.path,
|
|
565
|
+
pointer,
|
|
566
|
+
line: source.line,
|
|
567
|
+
column: source.column,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
seenConstraintIds.add(constraint.id);
|
|
571
|
+
if (!conceptByQualifiedId.has(qualifyReference(value.id, constraint.ref))) {
|
|
572
|
+
const pointer = `/concepts/${index}/constraints/${constraintIndex}/ref`;
|
|
573
|
+
const source = location(['concepts', index, 'constraints', constraintIndex, 'ref'], pointer);
|
|
574
|
+
diagnostics.push({
|
|
575
|
+
severity: 'error',
|
|
576
|
+
code: 'YM305',
|
|
577
|
+
message: `Unresolved constraint reference "${constraint.ref}"`,
|
|
578
|
+
path: input.path,
|
|
579
|
+
pointer,
|
|
580
|
+
line: source.line,
|
|
581
|
+
column: source.column,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
for (const [stateIndex, state] of (concept.presentIn ?? []).entries()) {
|
|
586
|
+
if (!architectureStateIds.has(qualifyReference(value.id, state))) {
|
|
587
|
+
const pointer = `/concepts/${index}/presentIn/${stateIndex}`;
|
|
588
|
+
const source = location(['concepts', index, 'presentIn', stateIndex], pointer);
|
|
589
|
+
diagnostics.push({
|
|
590
|
+
severity: 'error',
|
|
591
|
+
code: 'YM307',
|
|
592
|
+
message: `Unresolved architecture state reference "${state}"`,
|
|
593
|
+
path: input.path,
|
|
594
|
+
pointer,
|
|
595
|
+
line: source.line,
|
|
596
|
+
column: source.column,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
for (const [index, relationship] of value.relationships.entries()) {
|
|
602
|
+
for (const [stateIndex, state] of (relationship.presentIn ?? []).entries()) {
|
|
603
|
+
const stateIdentity = qualifyReference(value.id, state);
|
|
604
|
+
if (!architectureStateIds.has(stateIdentity)) {
|
|
605
|
+
const pointer = `/relationships/${index}/presentIn/${stateIndex}`;
|
|
606
|
+
const source = location(['relationships', index, 'presentIn', stateIndex], pointer);
|
|
607
|
+
diagnostics.push({
|
|
608
|
+
severity: 'error',
|
|
609
|
+
code: 'YM307',
|
|
610
|
+
message: `Unresolved architecture state reference "${state}"`,
|
|
611
|
+
path: input.path,
|
|
612
|
+
pointer,
|
|
613
|
+
line: source.line,
|
|
614
|
+
column: source.column,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
for (const endpoint of [
|
|
619
|
+
relationship.from,
|
|
620
|
+
relationship.to,
|
|
621
|
+
]) {
|
|
622
|
+
const endpointIdentity = qualifyReference(value.id, endpoint);
|
|
623
|
+
const resolved = conceptByQualifiedId.get(endpointIdentity);
|
|
624
|
+
const endpointStates = resolved?.concept.presentIn?.map((candidate) => qualifyReference(resolved.document, candidate)) ?? [];
|
|
625
|
+
if (resolved !== undefined &&
|
|
626
|
+
endpointStates.length > 0 &&
|
|
627
|
+
!endpointStates.includes(stateIdentity)) {
|
|
628
|
+
const pointer = `/relationships/${index}/presentIn/${stateIndex}`;
|
|
629
|
+
const source = location(['relationships', index, 'presentIn', stateIndex], pointer);
|
|
630
|
+
diagnostics.push({
|
|
631
|
+
severity: 'error',
|
|
632
|
+
code: 'YM503',
|
|
633
|
+
message: `Relationship "${relationship.id}" is present in "${stateIdentity}" but endpoint "${endpointIdentity}" is absent`,
|
|
634
|
+
path: input.path,
|
|
635
|
+
pointer,
|
|
636
|
+
line: source.line,
|
|
637
|
+
column: source.column,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
for (const controlled of [
|
|
644
|
+
{ field: 'mode', kind: 'access' },
|
|
645
|
+
{ field: 'content', kind: 'flow' },
|
|
646
|
+
]) {
|
|
647
|
+
if (relationship[controlled.field] !== undefined &&
|
|
648
|
+
relationship.kind !== controlled.kind) {
|
|
649
|
+
const pointer = `/relationships/${index}/${controlled.field}`;
|
|
650
|
+
const source = location(['relationships', index, controlled.field], pointer);
|
|
651
|
+
diagnostics.push({
|
|
652
|
+
severity: 'error',
|
|
653
|
+
code: 'YM405',
|
|
654
|
+
message: `Field "${controlled.field}" is only valid for relationship kind "${controlled.kind}"`,
|
|
655
|
+
path: input.path,
|
|
656
|
+
pointer,
|
|
657
|
+
line: source.line,
|
|
658
|
+
column: source.column,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (!selectedProfile.relationshipKinds.has(relationship.kind)) {
|
|
663
|
+
const pointer = `/relationships/${index}/kind`;
|
|
664
|
+
const source = location(['relationships', index, 'kind'], pointer);
|
|
665
|
+
diagnostics.push({
|
|
666
|
+
severity: 'error',
|
|
667
|
+
code: 'YM402',
|
|
668
|
+
message: `Unknown relationship kind "${relationship.kind}" in profile "${value.profile}"`,
|
|
669
|
+
path: input.path,
|
|
670
|
+
pointer,
|
|
671
|
+
line: source.line,
|
|
672
|
+
column: source.column,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
for (const endpoint of ['from', 'to']) {
|
|
676
|
+
const reference = relationship[endpoint];
|
|
677
|
+
if (!conceptByQualifiedId.has(qualifyReference(value.id, reference))) {
|
|
678
|
+
const pointer = `/relationships/${index}/${endpoint}`;
|
|
679
|
+
const source = location(['relationships', index, endpoint], pointer);
|
|
680
|
+
diagnostics.push({
|
|
681
|
+
severity: 'error',
|
|
682
|
+
code: 'YM302',
|
|
683
|
+
message: `Unresolved concept reference "${reference}"`,
|
|
684
|
+
path: input.path,
|
|
685
|
+
pointer,
|
|
686
|
+
line: source.line,
|
|
687
|
+
column: source.column,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const policy = selectedProfile.relationshipKinds.get(relationship.kind);
|
|
692
|
+
if (policy !== undefined) {
|
|
693
|
+
for (const endpoint of ['source', 'target']) {
|
|
694
|
+
const reference = endpoint === 'source' ? relationship.from : relationship.to;
|
|
695
|
+
const resolvedConcept = conceptByQualifiedId.get(qualifyReference(value.id, reference));
|
|
696
|
+
const kind = resolvedConcept === undefined
|
|
697
|
+
? undefined
|
|
698
|
+
: profiles
|
|
699
|
+
.get(resolvedConcept.profile)
|
|
700
|
+
?.conceptKinds.get(resolvedConcept.concept.kind);
|
|
701
|
+
const allowed = endpoint === 'source'
|
|
702
|
+
? policy.sourceAspects
|
|
703
|
+
: policy.targetAspects;
|
|
704
|
+
if (kind !== undefined &&
|
|
705
|
+
allowed !== undefined &&
|
|
706
|
+
!allowed.includes(kind.aspect)) {
|
|
707
|
+
const field = endpoint === 'source' ? 'from' : 'to';
|
|
708
|
+
const pointer = `/relationships/${index}/${field}`;
|
|
709
|
+
const source = location(['relationships', index, field], pointer);
|
|
710
|
+
diagnostics.push({
|
|
711
|
+
severity: 'error',
|
|
712
|
+
code: 'YM404',
|
|
713
|
+
message: `Relationship "${relationship.kind}" requires a ${endpoint} with aspect ${allowed.map((aspect) => `"${aspect}"`).join(' or ')}; "${reference}" has aspect "${kind.aspect}"`,
|
|
714
|
+
path: input.path,
|
|
715
|
+
pointer,
|
|
716
|
+
line: source.line,
|
|
717
|
+
column: source.column,
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
for (const [index, concept] of value.concepts.entries()) {
|
|
724
|
+
const subject = `${value.id}#${concept.id}`;
|
|
725
|
+
subjects.push({ id: subject, type: 'concept' });
|
|
726
|
+
claims.push({
|
|
727
|
+
id: `${subject}~kind`,
|
|
728
|
+
subject,
|
|
729
|
+
predicate: 'yarramate/concept/kind',
|
|
730
|
+
object: {
|
|
731
|
+
value: selectedProfile.conceptKinds.get(concept.kind)?.identity ??
|
|
732
|
+
concept.kind,
|
|
733
|
+
},
|
|
734
|
+
origin: 'declared',
|
|
735
|
+
source: location(['concepts', index, 'kind'], `/concepts/${index}/kind`),
|
|
736
|
+
}, {
|
|
737
|
+
id: `${subject}~name`,
|
|
738
|
+
subject,
|
|
739
|
+
predicate: 'yarramate/concept/name',
|
|
740
|
+
object: { value: concept.name },
|
|
741
|
+
origin: 'declared',
|
|
742
|
+
source: location(['concepts', index, 'name'], `/concepts/${index}/name`),
|
|
743
|
+
});
|
|
744
|
+
if (concept.description !== undefined) {
|
|
745
|
+
claims.push({
|
|
746
|
+
id: `${subject}~description`,
|
|
747
|
+
subject,
|
|
748
|
+
predicate: 'yarramate/concept/description',
|
|
749
|
+
object: { value: concept.description },
|
|
750
|
+
origin: 'declared',
|
|
751
|
+
source: location(['concepts', index, 'description'], `/concepts/${index}/description`),
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
if (concept.status !== undefined) {
|
|
755
|
+
claims.push({
|
|
756
|
+
id: `${subject}~status`,
|
|
757
|
+
subject,
|
|
758
|
+
predicate: 'yarramate/lifecycle/status',
|
|
759
|
+
object: { value: concept.status },
|
|
760
|
+
origin: 'declared',
|
|
761
|
+
source: location(['concepts', index, 'status'], `/concepts/${index}/status`),
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
if (concept.owner !== undefined) {
|
|
765
|
+
claims.push({
|
|
766
|
+
id: `${subject}~owner`,
|
|
767
|
+
subject,
|
|
768
|
+
predicate: 'yarramate/ownership/owner',
|
|
769
|
+
object: {
|
|
770
|
+
ref: qualifyReference(value.id, concept.owner),
|
|
771
|
+
},
|
|
772
|
+
origin: 'declared',
|
|
773
|
+
source: location(['concepts', index, 'owner'], `/concepts/${index}/owner`),
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
for (const [constraintIndex, constraint] of (concept.constraints ?? []).entries()) {
|
|
777
|
+
claims.push({
|
|
778
|
+
id: `${subject}~constraint-${constraint.id}`,
|
|
779
|
+
subject,
|
|
780
|
+
predicate: 'yarramate/constraint/requires',
|
|
781
|
+
object: {
|
|
782
|
+
ref: qualifyReference(value.id, constraint.ref),
|
|
783
|
+
},
|
|
784
|
+
origin: 'declared',
|
|
785
|
+
source: location(['concepts', index, 'constraints', constraintIndex, 'ref'], `/concepts/${index}/constraints/${constraintIndex}/ref`),
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
for (const [stateIndex, state] of (concept.presentIn ?? []).entries()) {
|
|
789
|
+
const stateIdentity = qualifyReference(value.id, state);
|
|
790
|
+
claims.push({
|
|
791
|
+
id: presenceClaimId(subject, stateIdentity),
|
|
792
|
+
subject,
|
|
793
|
+
predicate: 'yarramate/state/present-in',
|
|
794
|
+
object: { ref: stateIdentity },
|
|
795
|
+
origin: 'declared',
|
|
796
|
+
source: location(['concepts', index, 'presentIn', stateIndex], `/concepts/${index}/presentIn/${stateIndex}`),
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
for (const [index, relationship] of value.relationships.entries()) {
|
|
801
|
+
const id = `${value.id}#${relationship.id}`;
|
|
802
|
+
subjects.push({ id, type: 'relationship' });
|
|
803
|
+
claims.push({
|
|
804
|
+
id,
|
|
805
|
+
subject: qualifyReference(value.id, relationship.from),
|
|
806
|
+
predicate: selectedProfile.relationshipKinds.get(relationship.kind)?.identity ??
|
|
807
|
+
relationship.kind,
|
|
808
|
+
object: { ref: qualifyReference(value.id, relationship.to) },
|
|
809
|
+
origin: 'declared',
|
|
810
|
+
source: location(['relationships', index], `/relationships/${index}`),
|
|
811
|
+
});
|
|
812
|
+
if (relationship.name !== undefined) {
|
|
813
|
+
claims.push({
|
|
814
|
+
id: `${id}~name`,
|
|
815
|
+
subject: id,
|
|
816
|
+
predicate: 'yarramate/relationship/name',
|
|
817
|
+
object: { value: relationship.name },
|
|
818
|
+
origin: 'declared',
|
|
819
|
+
source: location(['relationships', index, 'name'], `/relationships/${index}/name`),
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
if (relationship.mode !== undefined) {
|
|
823
|
+
claims.push({
|
|
824
|
+
id: `${id}~mode`,
|
|
825
|
+
subject: id,
|
|
826
|
+
predicate: 'yarramate/access/mode',
|
|
827
|
+
object: { value: relationship.mode },
|
|
828
|
+
origin: 'declared',
|
|
829
|
+
source: location(['relationships', index, 'mode'], `/relationships/${index}/mode`),
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
if (relationship.content !== undefined) {
|
|
833
|
+
claims.push({
|
|
834
|
+
id: `${id}~content`,
|
|
835
|
+
subject: id,
|
|
836
|
+
predicate: 'yarramate/flow/content',
|
|
837
|
+
object: { value: relationship.content },
|
|
838
|
+
origin: 'declared',
|
|
839
|
+
source: location(['relationships', index, 'content'], `/relationships/${index}/content`),
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
if (relationship.status !== undefined) {
|
|
843
|
+
claims.push({
|
|
844
|
+
id: `${id}~status`,
|
|
845
|
+
subject: id,
|
|
846
|
+
predicate: 'yarramate/lifecycle/status',
|
|
847
|
+
object: { value: relationship.status },
|
|
848
|
+
origin: 'declared',
|
|
849
|
+
source: location(['relationships', index, 'status'], `/relationships/${index}/status`),
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
for (const [stateIndex, state] of (relationship.presentIn ?? []).entries()) {
|
|
853
|
+
const stateIdentity = qualifyReference(value.id, state);
|
|
854
|
+
claims.push({
|
|
855
|
+
id: presenceClaimId(id, stateIdentity),
|
|
856
|
+
subject: id,
|
|
857
|
+
predicate: 'yarramate/state/present-in',
|
|
858
|
+
object: { ref: stateIdentity },
|
|
859
|
+
origin: 'declared',
|
|
860
|
+
source: location(['relationships', index, 'presentIn', stateIndex], `/relationships/${index}/presentIn/${stateIndex}`),
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const wholePartByEndpoints = new Map();
|
|
866
|
+
const wholePartRelationships = documents
|
|
867
|
+
.flatMap(({ value, location }) => value.relationships.flatMap((relationship, index) => relationship.kind === 'composition' ||
|
|
868
|
+
relationship.kind === 'aggregation'
|
|
869
|
+
? [
|
|
870
|
+
{
|
|
871
|
+
id: `${value.id}#${relationship.id}`,
|
|
872
|
+
localId: relationship.id,
|
|
873
|
+
document: value.id,
|
|
874
|
+
kind: relationship.kind,
|
|
875
|
+
from: qualifyReference(value.id, relationship.from),
|
|
876
|
+
to: qualifyReference(value.id, relationship.to),
|
|
877
|
+
states: new Set((relationship.presentIn ?? []).map((state) => qualifyReference(value.id, state))),
|
|
878
|
+
source: location(['relationships', index, 'kind'], `/relationships/${index}/kind`),
|
|
879
|
+
},
|
|
880
|
+
]
|
|
881
|
+
: []))
|
|
882
|
+
.sort(compareById);
|
|
883
|
+
for (const relationship of wholePartRelationships) {
|
|
884
|
+
const endpointKey = `${relationship.from}\u0000${relationship.to}`;
|
|
885
|
+
const previousRelationships = wholePartByEndpoints.get(endpointKey) ?? [];
|
|
886
|
+
const previous = previousRelationships.find((candidate) => candidate.kind !== relationship.kind &&
|
|
887
|
+
(candidate.states.size === 0 ||
|
|
888
|
+
relationship.states.size === 0 ||
|
|
889
|
+
[...candidate.states].some((state) => relationship.states.has(state))));
|
|
890
|
+
if (previous !== undefined) {
|
|
891
|
+
const currentName = previous.document === relationship.document
|
|
892
|
+
? relationship.localId
|
|
893
|
+
: relationship.id;
|
|
894
|
+
const previousName = previous.document === relationship.document
|
|
895
|
+
? previous.localId
|
|
896
|
+
: previous.id;
|
|
897
|
+
diagnostics.push({
|
|
898
|
+
severity: 'error',
|
|
899
|
+
code: 'YM501',
|
|
900
|
+
message: `Relationship "${currentName}" contradicts "${previousName}": the same endpoints cannot be both aggregation and composition`,
|
|
901
|
+
path: relationship.source.path,
|
|
902
|
+
pointer: relationship.source.pointer,
|
|
903
|
+
line: relationship.source.line,
|
|
904
|
+
column: relationship.source.column,
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
previousRelationships.push(relationship);
|
|
908
|
+
wholePartByEndpoints.set(endpointKey, previousRelationships);
|
|
909
|
+
}
|
|
910
|
+
if (diagnostics.length > 0) {
|
|
911
|
+
return diagnosticFailure(diagnostics);
|
|
912
|
+
}
|
|
913
|
+
return {
|
|
914
|
+
ok: true,
|
|
915
|
+
profileContext: {
|
|
916
|
+
conceptKindLineages: new Map([...conceptKindByIdentity]
|
|
917
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
918
|
+
.map(([identity, kind]) => [identity, kind.lineage])),
|
|
919
|
+
relationshipKindLineages: new Map([...relationshipKindByIdentity]
|
|
920
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
921
|
+
.map(([identity, kind]) => [identity, kind.lineage])),
|
|
922
|
+
},
|
|
923
|
+
graph: {
|
|
924
|
+
format: 'yarramate/graph/v2',
|
|
925
|
+
profiles: [
|
|
926
|
+
...new Set(documents.flatMap(({ value }) => profiles.get(value.profile)?.lineage ?? [])),
|
|
927
|
+
].sort(),
|
|
928
|
+
documents: documents
|
|
929
|
+
.map(({ input, value }) => ({ id: value.id, source: input.path }))
|
|
930
|
+
.sort(compareById),
|
|
931
|
+
subjects: subjects.sort(compareById),
|
|
932
|
+
claims: claims.sort(compareById),
|
|
933
|
+
},
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
export function compileWorkspace(sources) {
|
|
937
|
+
const result = compileWorkspaceResolved(sources);
|
|
938
|
+
return result.ok ? { ok: true, graph: result.graph } : result;
|
|
939
|
+
}
|
|
940
|
+
export const compileWorkspaceWithProfileContext = (sources) => compileWorkspaceResolved(sources);
|