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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -0
  3. package/dist/adapter-mapping.d.ts +40 -0
  4. package/dist/adapter-mapping.js +200 -0
  5. package/dist/adapters/graphify-cli.d.ts +3 -0
  6. package/dist/adapters/graphify-cli.js +127 -0
  7. package/dist/adapters/graphify-entry.d.ts +1 -0
  8. package/dist/adapters/graphify-entry.js +1 -0
  9. package/dist/adapters/graphify.d.ts +23 -0
  10. package/dist/adapters/graphify.js +47 -0
  11. package/dist/adapters/likec4-cli.d.ts +3 -0
  12. package/dist/adapters/likec4-cli.js +662 -0
  13. package/dist/adapters/likec4-export.d.ts +25 -0
  14. package/dist/adapters/likec4-export.js +204 -0
  15. package/dist/adapters/likec4-kind-mapping.d.ts +22 -0
  16. package/dist/adapters/likec4-kind-mapping.js +60 -0
  17. package/dist/adapters/likec4-prepare.d.ts +28 -0
  18. package/dist/adapters/likec4-prepare.js +154 -0
  19. package/dist/adapters/likec4-project.d.ts +58 -0
  20. package/dist/adapters/likec4-project.js +176 -0
  21. package/dist/adapters/likec4.d.ts +4 -0
  22. package/dist/adapters/likec4.js +4 -0
  23. package/dist/architecture-state.d.ts +22 -0
  24. package/dist/architecture-state.js +63 -0
  25. package/dist/check-command.d.ts +2 -0
  26. package/dist/check-command.js +232 -0
  27. package/dist/cli-support.d.ts +24 -0
  28. package/dist/cli-support.js +81 -0
  29. package/dist/cli.d.ts +4 -0
  30. package/dist/cli.js +575 -0
  31. package/dist/compiler.d.ts +66 -0
  32. package/dist/compiler.js +940 -0
  33. package/dist/core-contract.d.ts +43 -0
  34. package/dist/core-contract.js +162 -0
  35. package/dist/evidence.d.ts +58 -0
  36. package/dist/evidence.js +161 -0
  37. package/dist/graph.d.ts +2 -0
  38. package/dist/graph.js +37 -0
  39. package/dist/index.d.ts +11 -0
  40. package/dist/index.js +9 -0
  41. package/dist/profile.d.ts +30 -0
  42. package/dist/profile.js +162 -0
  43. package/dist/projection.d.ts +42 -0
  44. package/dist/projection.js +183 -0
  45. package/dist/reconciliation.d.ts +26 -0
  46. package/dist/reconciliation.js +47 -0
  47. package/dist/source-document.d.ts +24 -0
  48. package/dist/source-document.js +80 -0
  49. package/dist/workspace.d.ts +29 -0
  50. package/dist/workspace.js +114 -0
  51. package/docs/CONSUMING-YARRAMATE.md +145 -0
  52. package/package.json +110 -0
  53. package/schema/yarramate-adapter-mapping.schema.json +59 -0
  54. package/schema/yarramate-check-result.schema.json +92 -0
  55. package/schema/yarramate-core-contract.schema.json +132 -0
  56. package/schema/yarramate-diagnostic-result.schema.json +64 -0
  57. package/schema/yarramate-document.schema.json +168 -0
  58. package/schema/yarramate-evidence-report.schema.json +73 -0
  59. package/schema/yarramate-evidence.schema.json +92 -0
  60. package/schema/yarramate-graph-v2.schema.json +170 -0
  61. package/schema/yarramate-likec4-check-result.schema.json +50 -0
  62. package/schema/yarramate-likec4-diagnostic-result.schema.json +69 -0
  63. package/schema/yarramate-likec4-generated-project-v2.schema.json +100 -0
  64. package/schema/yarramate-likec4-generated-project.schema.json +76 -0
  65. package/schema/yarramate-likec4-kind-mapping.schema.json +65 -0
  66. package/schema/yarramate-likec4-project.schema.json +160 -0
  67. package/schema/yarramate-profile.schema.json +113 -0
  68. package/schema/yarramate-projection-result.schema.json +177 -0
  69. package/schema/yarramate-projection.schema.json +126 -0
  70. package/schema/yarramate-reconciliation-report.schema.json +90 -0
  71. package/schema/yarramate-state-comparison.schema.json +61 -0
  72. package/schema/yarramate-workspace.schema.json +55 -0
  73. package/skills/yarramate-architecture/SKILL.md +135 -0
  74. package/skills/yarramate-architecture/agents/openai.yaml +4 -0
  75. package/skills/yarramate-architecture/references/journey-checklists.md +57 -0
  76. package/skills/yarramate-architecture/references/native-authoring.md +167 -0
@@ -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);