yarramate 0.23.0 → 1.0.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 (159) hide show
  1. package/README.md +13 -4
  2. package/catalogues/core-enrichment.yaml +79 -55
  3. package/dist/adapter-mapping.js +6 -1
  4. package/dist/adapters/likec4-cli.js +36 -7
  5. package/dist/adapters/visual/client.d.ts +2 -2
  6. package/dist/adapters/visual/client.js +79 -10
  7. package/dist/adapters/visual/protocol-contract.d.ts +124 -10
  8. package/dist/adapters/visual/protocol-contract.js +12 -1
  9. package/dist/adapters/visual/protocol.d.ts +63 -0
  10. package/dist/adapters/visual/protocol.js +96 -5
  11. package/dist/adapters/visual/session-server.d.ts +1 -6
  12. package/dist/adapters/visual/session-server.js +243 -143
  13. package/dist/adapters/visual/session-store.d.ts +9 -0
  14. package/dist/adapters/visual/session-store.js +14 -5
  15. package/dist/adapters/visual/view-identity.d.ts +135 -0
  16. package/dist/adapters/visual/view-identity.js +200 -0
  17. package/dist/adapters/visual/wire.d.ts +19 -4
  18. package/dist/adapters/visual/workspace-model.d.ts +116 -0
  19. package/dist/adapters/visual/workspace-model.js +212 -0
  20. package/dist/adapters/visual-cli.d.ts +1 -1
  21. package/dist/adapters/visual-cli.js +30 -21
  22. package/dist/apply-cli.d.ts +2 -0
  23. package/dist/apply-cli.js +83 -0
  24. package/dist/apply-command.d.ts +84 -3
  25. package/dist/apply-command.js +218 -109
  26. package/dist/archimate-relationships.generated.d.ts +22 -0
  27. package/dist/archimate-relationships.generated.js +156 -0
  28. package/dist/ask-command.js +18 -6
  29. package/dist/attestation-staleness.js +2 -2
  30. package/dist/changed.js +1 -1
  31. package/dist/check-command.js +13 -5
  32. package/dist/cli-support.d.ts +1 -1
  33. package/dist/cli-support.js +1 -1
  34. package/dist/cli.js +1 -1
  35. package/dist/compiler.d.ts +31 -3
  36. package/dist/compiler.js +321 -81
  37. package/dist/concept-drafting.d.ts +27 -0
  38. package/dist/concept-drafting.js +75 -0
  39. package/dist/core-contract.js +6 -1
  40. package/dist/deletion-drafting.d.ts +52 -0
  41. package/dist/deletion-drafting.js +100 -0
  42. package/dist/evidence.js +6 -1
  43. package/dist/graph-projection.d.ts +14 -0
  44. package/dist/graph-projection.js +4 -0
  45. package/dist/index.d.ts +8 -1
  46. package/dist/index.js +8 -1
  47. package/dist/interrogate-command.js +63 -17
  48. package/dist/kind-label.d.ts +4 -3
  49. package/dist/kind-label.js +4 -3
  50. package/dist/nesting.d.ts +15 -0
  51. package/dist/nesting.js +2 -0
  52. package/dist/notation/archimate.d.ts +1 -1
  53. package/dist/notation/archimate.js +108 -9
  54. package/dist/profile.d.ts +9 -12
  55. package/dist/profile.js +17 -26
  56. package/dist/projection.d.ts +62 -3
  57. package/dist/projection.js +127 -43
  58. package/dist/relationship-drafting.d.ts +51 -0
  59. package/dist/relationship-drafting.js +92 -0
  60. package/dist/relationship-matrix.d.ts +14 -0
  61. package/dist/relationship-matrix.js +74 -0
  62. package/dist/shipped-profile.d.ts +1 -1
  63. package/dist/shipped-profile.js +19 -5
  64. package/dist/source-store.d.ts +95 -0
  65. package/dist/source-store.js +170 -0
  66. package/dist/subject-references.js +1 -1
  67. package/dist/visual-app/assets/index-BLcpI8WN.js +394 -0
  68. package/dist/visual-app/assets/index-DLutIWES.css +1 -0
  69. package/dist/visual-app/index.html +2 -2
  70. package/dist/visual-app-lib/editor.d.ts +1 -0
  71. package/dist/visual-app-lib/editor.js +90968 -0
  72. package/dist/visual-app-lib/styles.css +2 -0
  73. package/dist/visual-app-lib/types/adapter-mapping.d.ts +40 -0
  74. package/dist/visual-app-lib/types/adapters/visual/protocol-contract.d.ts +455 -0
  75. package/dist/visual-app-lib/types/adapters/visual/view-identity.d.ts +135 -0
  76. package/dist/visual-app-lib/types/adapters/visual/wire.d.ts +141 -0
  77. package/dist/visual-app-lib/types/adapters/visual/workspace-model.d.ts +116 -0
  78. package/dist/visual-app-lib/types/apply-command.d.ts +93 -0
  79. package/dist/visual-app-lib/types/archimate-relationships.generated.d.ts +22 -0
  80. package/dist/visual-app-lib/types/brief.d.ts +5 -0
  81. package/dist/visual-app-lib/types/compiler.d.ts +155 -0
  82. package/dist/visual-app-lib/types/concept-drafting.d.ts +27 -0
  83. package/dist/visual-app-lib/types/deletion-drafting.d.ts +52 -0
  84. package/dist/visual-app-lib/types/evidence.d.ts +62 -0
  85. package/dist/visual-app-lib/types/graph-claims.d.ts +18 -0
  86. package/dist/visual-app-lib/types/graph-projection.d.ts +77 -0
  87. package/dist/visual-app-lib/types/kind-label.d.ts +9 -0
  88. package/dist/visual-app-lib/types/nesting.d.ts +15 -0
  89. package/dist/visual-app-lib/types/notation/archimate.d.ts +71 -0
  90. package/dist/visual-app-lib/types/operations.d.ts +157 -0
  91. package/dist/visual-app-lib/types/profile.d.ts +47 -0
  92. package/dist/visual-app-lib/types/projection.d.ts +113 -0
  93. package/dist/visual-app-lib/types/relationship-drafting.d.ts +51 -0
  94. package/dist/visual-app-lib/types/relationship-matrix.d.ts +14 -0
  95. package/dist/visual-app-lib/types/shipped-profile.d.ts +2 -0
  96. package/dist/visual-app-lib/types/source-document.d.ts +36 -0
  97. package/dist/visual-app-lib/types/source-store.d.ts +95 -0
  98. package/dist/visual-app-lib/types/subject-references.d.ts +92 -0
  99. package/dist/visual-app-lib/types/visual-app/App.d.ts +15 -0
  100. package/dist/visual-app-lib/types/visual-app/badges.d.ts +7 -0
  101. package/dist/visual-app-lib/types/visual-app/changeset-tray.d.ts +106 -0
  102. package/dist/visual-app-lib/types/visual-app/confirm-dialog.d.ts +14 -0
  103. package/dist/visual-app-lib/types/visual-app/connection-panel.d.ts +23 -0
  104. package/dist/visual-app-lib/types/visual-app/context-menu-model.d.ts +187 -0
  105. package/dist/visual-app-lib/types/visual-app/context-menu.d.ts +24 -0
  106. package/dist/visual-app-lib/types/visual-app/describe-query.d.ts +9 -0
  107. package/dist/visual-app-lib/types/visual-app/editor-host.d.ts +70 -0
  108. package/dist/visual-app-lib/types/visual-app/faults.d.ts +26 -0
  109. package/dist/visual-app-lib/types/visual-app/graph-canvas.d.ts +73 -0
  110. package/dist/visual-app-lib/types/visual-app/kind-icons.d.ts +3 -0
  111. package/dist/visual-app-lib/types/visual-app/local-host.d.ts +38 -0
  112. package/dist/visual-app-lib/types/visual-app/mount.d.ts +56 -0
  113. package/dist/visual-app-lib/types/visual-app/prompt-dialog.d.ts +24 -0
  114. package/dist/visual-app-lib/types/visual-app/query-fields.d.ts +69 -0
  115. package/dist/visual-app-lib/types/visual-app/query-panel.d.ts +142 -0
  116. package/dist/visual-app-lib/types/visual-app/quick-filter.d.ts +11 -0
  117. package/dist/visual-app-lib/types/visual-app/relationship-kind-options.d.ts +50 -0
  118. package/dist/visual-app-lib/types/visual-app/save-view.d.ts +86 -0
  119. package/dist/visual-app-lib/types/visual-app/section-stack.d.ts +59 -0
  120. package/dist/visual-app-lib/types/visual-app/session-client.d.ts +38 -0
  121. package/dist/visual-app-lib/types/visual-app/state.d.ts +328 -0
  122. package/dist/visual-app-lib/types/visual-app/subject-draft-panel.d.ts +22 -0
  123. package/dist/visual-app-lib/types/visual-app/subject-form.d.ts +42 -0
  124. package/dist/visual-app-lib/types/visual-app/view-tree-model.d.ts +122 -0
  125. package/dist/visual-app-lib/types/visual-app/view-tree.d.ts +32 -0
  126. package/dist/visual-app-lib/types/visual-app/workspace-state.d.ts +268 -0
  127. package/dist/visual-app-lib/types/workspace.d.ts +29 -0
  128. package/dist/workspace.js +7 -1
  129. package/docs/CONSUMING-YARRAMATE.md +51 -2
  130. package/package.json +9 -2
  131. package/schema/yarramate-adapter-mapping.schema.json +1 -1
  132. package/schema/yarramate-ask-result.schema.json +24 -3
  133. package/schema/yarramate-check-result.schema.json +9 -0
  134. package/schema/yarramate-diagnostic-result.schema.json +9 -0
  135. package/schema/yarramate-document.schema.json +6 -0
  136. package/schema/yarramate-evidence-report.schema.json +1 -1
  137. package/schema/yarramate-evidence.schema.json +1 -1
  138. package/schema/yarramate-graph-v2.schema.json +34 -9
  139. package/schema/yarramate-likec4-generated-project-v2.schema.json +1 -1
  140. package/schema/yarramate-likec4-generated-project.schema.json +1 -1
  141. package/schema/yarramate-likec4-project.schema.json +1 -1
  142. package/schema/yarramate-projection-result.schema.json +3 -7
  143. package/schema/yarramate-projection.schema.json +16 -16
  144. package/schema/yarramate-reconciliation-report.schema.json +2 -2
  145. package/schema/yarramate-rtm.schema.json +1 -1
  146. package/schema/yarramate-state-comparison.schema.json +1 -1
  147. package/schema/yarramate-visual-diagnostic-result.schema.json +9 -0
  148. package/schema/yarramate-visual-event.schema.json +40 -41
  149. package/schema/yarramate-visual-graph.schema.json +8 -0
  150. package/schema/yarramate-visual-handoff.schema.json +3 -3
  151. package/schema/yarramate-visual-session-descriptor.schema.json +6 -6
  152. package/schema/yarramate-visual-session-started.schema.json +10 -9
  153. package/schema/yarramate-visual-status.schema.json +3 -3
  154. package/skills/yarramate-architecture/SKILL.md +3 -3
  155. package/skills/yarramate-architecture/references/modelling-patterns.md +23 -44
  156. package/skills/yarramate-architecture/references/native-authoring.md +32 -49
  157. package/skills/yarramate-architecture/references/visual-conversations.md +13 -10
  158. package/dist/visual-app/assets/index-CRd1Khma.js +0 -354
  159. package/dist/visual-app/assets/index-tZSHlU-a.css +0 -1
package/dist/compiler.js CHANGED
@@ -1,6 +1,7 @@
1
- import { createRequire } from 'node:module';
2
- import { LineCounter, parseDocument } from 'yaml';
1
+ import Ajv2020Import from 'ajv/dist/2020.js';
2
+ import { LineCounter, parse, parseDocument } from 'yaml';
3
3
  import { conceptKinds, relationshipPolicies, } from './profile.js';
4
+ import { isCoreConceptKindId, matrixEndpointAspects, permittedRelationshipKinds as tablePermittedKinds, } from './relationship-matrix.js';
4
5
  import { closestCandidate, describeSchemaViolation, } from './source-document.js';
5
6
  import documentSchema from '../schema/yarramate-document.schema.json' with { type: 'json'
6
7
  };
@@ -9,8 +10,7 @@ import profileSchema from '../schema/yarramate-profile.schema.json' with { type:
9
10
  import { ATTESTATION_PREDICATE_PREFIX, attestationClaimValue } from './graph-claims.js';
10
11
  import { shippedPolicyIdentity, shippedPolicySource, } from './shipped-profile.js';
11
12
  const coreProfile = 'yarramate/core@0.1';
12
- const require = createRequire(import.meta.url);
13
- const ajv2020Module = require('ajv/dist/2020.js');
13
+ const ajv2020Module = Ajv2020Import;
14
14
  const Ajv2020 = ajv2020Module.default ?? ajv2020Module;
15
15
  const validateDocument = new Ajv2020({ allErrors: true }).compile(documentSchema);
16
16
  const validateProfile = new Ajv2020({ allErrors: true }).compile(profileSchema);
@@ -33,32 +33,35 @@ const immutableMap = (entries) => {
33
33
  return Object.freeze(facade);
34
34
  };
35
35
  const compareById = (left, right) => left.id.localeCompare(right.id);
36
- const presenceClaimId = (subject, state) => `${subject}~present-in-${Buffer.from(state, 'utf8').toString('hex')}`;
36
+ const utf8Encoder = new TextEncoder();
37
+ const utf8Hex = (value) => {
38
+ let hex = '';
39
+ for (const byte of utf8Encoder.encode(value)) {
40
+ hex += byte.toString(16).padStart(2, '0');
41
+ }
42
+ return hex;
43
+ };
44
+ const presenceClaimId = (subject, state) => `${subject}~present-in-${utf8Hex(state)}`;
37
45
  // Alternative labels and distinctness records are unordered sets, not
38
46
  // authored lists with ids. Hex-encoding the value the way presence
39
47
  // encodes its state keeps the claim id derived from content rather than
40
48
  // position, so reordering the YAML leaves the graph byte-identical.
41
- const aliasClaimId = (subject, alias) => `${subject}~alias-${Buffer.from(alias, 'utf8').toString('hex')}`;
42
- const distinctFromClaimId = (subject, other) => `${subject}~distinct-from-${Buffer.from(other, 'utf8').toString('hex')}`;
43
- const supersedesClaimId = (subject, predecessor) => `${subject}~supersedes-${Buffer.from(predecessor, 'utf8').toString('hex')}`;
49
+ const aliasClaimId = (subject, alias) => `${subject}~alias-${utf8Hex(alias)}`;
50
+ const distinctFromClaimId = (subject, other) => `${subject}~distinct-from-${utf8Hex(other)}`;
51
+ const supersedesClaimId = (subject, predecessor) => `${subject}~supersedes-${utf8Hex(predecessor)}`;
44
52
  export { ATTESTATION_PREDICATE_PREFIX, attestationClaimValue, parseAttestationClaimValue, parseConstraintExpectsValue, } from './graph-claims.js';
45
- const describeAspect = (aspect) => aspect.replace('-', ' ');
46
- // Candidate order is the policy-matrix declaration order: the resolved kind
47
- // map inserts core policies first, then extension kinds as declared.
48
- const candidateKindHint = (kinds, rejected, sourceAspect, targetAspect) => {
49
- const candidates = [...kinds]
50
- .filter(([id, kind]) => id !== rejected &&
51
- (kind.sourceAspects?.includes(sourceAspect) ?? true) &&
52
- (kind.targetAspects?.includes(targetAspect) ?? true))
53
- .map(([id]) => id);
54
- if (candidates.length === 0) {
55
- return '';
56
- }
57
- const observed = sourceAspect === targetAspect
58
- ? `both endpoints are ${describeAspect(sourceAspect)}`
59
- : `source is ${describeAspect(sourceAspect)} and target is ${describeAspect(targetAspect)}`;
60
- return `; ${observed}; valid candidates: ${candidates.join(', ')}`;
61
- };
53
+ const localKindId = (identity) => identity.slice(identity.indexOf('#') + 1);
54
+ // Candidate order is the resolved kind map's insertion order: core kinds
55
+ // first, then extension kinds as declared. Scoped to the selected profile
56
+ // (ADR 0079), so a profile nobody selected never surfaces here. A candidate
57
+ // has to clear both the table and any narrowing the kind declares, or the
58
+ // suggestion would be the next rejection.
59
+ const permittedCandidates = (kinds, rejected, permitted, sourceAspect, targetAspect) => [...kinds]
60
+ .filter(([id, kind]) => id !== rejected &&
61
+ permitted.has(kind.coreKind) &&
62
+ (kind.sourceAspects?.includes(sourceAspect) ?? true) &&
63
+ (kind.targetAspects?.includes(targetAspect) ?? true))
64
+ .map(([id]) => id);
62
65
  const diagnosticFailure = (diagnostics) => ({
63
66
  ok: false,
64
67
  diagnostics: [...diagnostics].sort((left, right) => left.path.localeCompare(right.path) ||
@@ -199,6 +202,60 @@ const parseSources = (sources, previous) => {
199
202
  });
200
203
  return { parsed, cache: { sources: entries }, reused };
201
204
  };
205
+ // Resolves each diagnostic's subject from the pointer it already carries, so
206
+ // the rules themselves stay unchanged and no construction site has to remember
207
+ // to name what it refused.
208
+ //
209
+ // A pointer into a document reads `/concepts/<n>/...` or
210
+ // `/relationships/<n>/...`, and `<n>` indexes the authored array, so the id is
211
+ // one lookup away in the same parsed value the diagnostic was located against.
212
+ // Pointers are positional - inserting a concept shifts every index below it -
213
+ // which is exactly why this is done here, against the parse that produced the
214
+ // diagnostic, rather than by a consumer against whatever it holds later.
215
+ //
216
+ // Anything else keeps no subjects: a parse failure, a whole-document schema
217
+ // violation, a manifest, a projection's own definition. That absence is the
218
+ // signal a consumer needs, so it is left empty rather than guessed at.
219
+ const subjectsForPointer = (pointer, documentValue) => {
220
+ const match = /^\/(concepts|relationships)\/(\d+)(?:\/|$)/.exec(pointer);
221
+ if (match === null)
222
+ return [];
223
+ const value = documentValue;
224
+ const documentId = typeof value?.id === 'string' ? value.id : undefined;
225
+ if (documentId === undefined)
226
+ return [];
227
+ const items = value?.[match[1]];
228
+ if (!Array.isArray(items))
229
+ return [];
230
+ const item = items[Number(match[2])];
231
+ return typeof item?.id === 'string' ? [item.id] : [];
232
+ };
233
+ export const withDiagnosticSubjects = (diagnostics, sources) => {
234
+ if (diagnostics.length === 0)
235
+ return diagnostics;
236
+ // Only the documents something was actually said about are parsed, and each
237
+ // at most once: a clean workspace pays nothing, and a workspace refused on
238
+ // one document does not pay for the rest.
239
+ const wanted = new Set(diagnostics.map((diagnostic) => diagnostic.path));
240
+ const valueByPath = new Map();
241
+ for (const source of sources) {
242
+ if (!wanted.has(source.path))
243
+ continue;
244
+ try {
245
+ valueByPath.set(source.path, parse(source.source));
246
+ }
247
+ catch {
248
+ // A source that will not parse is exactly one whose diagnostics belong
249
+ // to no subject, so failing to read it here is the right answer.
250
+ }
251
+ }
252
+ return diagnostics.map((diagnostic) => {
253
+ if (diagnostic.subjects !== undefined)
254
+ return diagnostic;
255
+ const subjects = subjectsForPointer(diagnostic.pointer, valueByPath.get(diagnostic.path));
256
+ return subjects.length === 0 ? diagnostic : { ...diagnostic, subjects };
257
+ });
258
+ };
202
259
  function compileWorkspaceResolved(parsed) {
203
260
  const profileInputs = [];
204
261
  const documentInputs = [];
@@ -230,9 +287,7 @@ function compileWorkspaceResolved(parsed) {
230
287
  const resolved = {
231
288
  identity: `${coreProfile}#${policy.id}`,
232
289
  lineage: [`${coreProfile}#${policy.id}`],
233
- sourceAspects: policy.sourceAspects,
234
- targetAspects: policy.targetAspects,
235
- repair: policy.repair,
290
+ coreKind: policy.id,
236
291
  };
237
292
  coreRelationshipKinds.set(policy.id, resolved);
238
293
  relationshipKindByIdentity.set(resolved.identity, resolved);
@@ -441,9 +496,12 @@ function compileWorkspaceResolved(parsed) {
441
496
  for (const endpoint of ['source', 'target']) {
442
497
  const field = `${endpoint}Aspects`;
443
498
  const declared = kind[field];
444
- const inherited = parent[field];
499
+ // A parent that declares no narrowing is bounded by the table:
500
+ // an extension may not admit an aspect its core ancestor can never
501
+ // carry at that end.
502
+ const inherited = parent[field] ??
503
+ [...matrixEndpointAspects(parent.coreKind, endpoint)];
445
504
  if (declared !== undefined &&
446
- inherited !== undefined &&
447
505
  declared.some((aspect) => !inherited.includes(aspect))) {
448
506
  const position = positionFor([
449
507
  'relationshipKinds',
@@ -468,6 +526,7 @@ function compileWorkspaceResolved(parsed) {
468
526
  const resolved = {
469
527
  identity: `${identity}#${kind.id}`,
470
528
  lineage: [...parent.lineage, `${identity}#${kind.id}`],
529
+ coreKind: parent.coreKind,
471
530
  sourceAspects: kind.sourceAspects ?? parent.sourceAspects,
472
531
  targetAspects: kind.targetAspects ?? parent.targetAspects,
473
532
  };
@@ -547,25 +606,29 @@ function compileWorkspaceResolved(parsed) {
547
606
  seenDocumentIds.add(value.id);
548
607
  }
549
608
  const conceptByQualifiedId = new Map(documents.flatMap(({ value }) => value.concepts.map((concept) => [
550
- `${value.id}#${concept.id}`,
609
+ concept.id,
551
610
  {
552
611
  concept,
553
612
  profile: value.profile,
554
613
  document: value.id,
555
614
  },
556
615
  ])));
557
- const qualifyReference = (documentId, reference) => reference.includes('#') ? reference : `${documentId}#${reference}`;
558
- const architectureStateIds = new Set(documents.flatMap(({ value }) => (value.states ?? []).map((state) => `${value.id}#${state.id}`)));
616
+ // Subject identity is the authored id, unique across the workspace, so a
617
+ // reference resolves as written. Kept as a named step rather than inlined
618
+ // because every reference in the model passes through here, and that is
619
+ // where a future scheme would hook.
620
+ const qualifyReference = (_documentId, reference) => reference;
621
+ const architectureStateIds = new Set(documents.flatMap(({ value }) => (value.states ?? []).map((state) => state.id)));
559
622
  const subjectIds = new Set(documents.flatMap(({ value }) => [
560
- ...(value.states ?? []).map((state) => `${value.id}#${state.id}`),
561
- ...value.concepts.map((concept) => `${value.id}#${concept.id}`),
562
- ...value.relationships.map((relationship) => `${value.id}#${relationship.id}`),
623
+ ...(value.states ?? []).map((state) => state.id),
624
+ ...value.concepts.map((concept) => concept.id),
625
+ ...value.relationships.map((relationship) => relationship.id),
563
626
  ]));
564
627
  const architectureStateAfter = new Map(documents.flatMap(({ value }) => (value.states ?? []).flatMap((state) => state.after === undefined
565
628
  ? []
566
629
  : [
567
630
  [
568
- `${value.id}#${state.id}`,
631
+ state.id,
569
632
  qualifyReference(value.id, state.after),
570
633
  ],
571
634
  ])));
@@ -589,7 +652,7 @@ function compileWorkspaceResolved(parsed) {
589
652
  ? []
590
653
  : [
591
654
  [
592
- `${value.id}#${concept.id}`,
655
+ concept.id,
593
656
  concept.supersedes.map((predecessor) => qualifyReference(value.id, predecessor)),
594
657
  ],
595
658
  ])));
@@ -612,6 +675,14 @@ function compileWorkspaceResolved(parsed) {
612
675
  }
613
676
  return false;
614
677
  };
678
+ // Subject identity is the authored id and carries no document prefix, so an
679
+ // id declared twice anywhere in the workspace names one subject twice. The
680
+ // walk below therefore remembers across documents, not just within one.
681
+ const declaringDocument = new Map();
682
+ // A document whose own id is a duplicate (YM303) would collide on every
683
+ // subject it declares, burying the one fault worth acting on under a
684
+ // diagnostic per concept. It is walked for nothing else here.
685
+ const documentIdsSeen = new Set();
615
686
  for (const { input, value, location } of documents) {
616
687
  const selectedProfile = profiles.get(value.profile);
617
688
  if (selectedProfile === undefined) {
@@ -627,7 +698,6 @@ function compileWorkspaceResolved(parsed) {
627
698
  });
628
699
  continue;
629
700
  }
630
- const seenIds = new Set();
631
701
  const declarations = [
632
702
  ...(value.states ?? []).map((state, index) => ({
633
703
  id: state.id,
@@ -645,23 +715,36 @@ function compileWorkspaceResolved(parsed) {
645
715
  pointer: `/relationships/${index}/id`,
646
716
  })),
647
717
  ];
718
+ const duplicateDocumentId = documentIdsSeen.has(value.id);
719
+ documentIdsSeen.add(value.id);
648
720
  for (const declaration of declarations) {
649
- if (seenIds.has(declaration.id)) {
721
+ if (duplicateDocumentId)
722
+ break;
723
+ const firstDeclarer = declaringDocument.get(declaration.id);
724
+ if (firstDeclarer !== undefined) {
650
725
  const source = location(declaration.yamlPath, declaration.pointer);
726
+ // Two documents claiming one id is a different fault from one document
727
+ // repeating itself, and it reads differently to whoever has to fix it:
728
+ // the second names a file they may not have open.
729
+ const sameDocument = firstDeclarer === input.path;
651
730
  diagnostics.push({
652
731
  severity: 'error',
653
- code: 'YM301',
654
- message: `Duplicate local ID "${declaration.id}"`,
732
+ code: sameDocument ? 'YM301' : 'YM314',
733
+ message: sameDocument
734
+ ? `Duplicate ID "${declaration.id}"`
735
+ : `ID "${declaration.id}" is already declared by "${firstDeclarer}"; a subject id is unique across the workspace`,
655
736
  path: input.path,
656
737
  pointer: declaration.pointer,
657
738
  line: source.line,
658
739
  column: source.column,
659
740
  });
660
741
  }
661
- seenIds.add(declaration.id);
742
+ else {
743
+ declaringDocument.set(declaration.id, input.path);
744
+ }
662
745
  }
663
746
  for (const [index, state] of (value.states ?? []).entries()) {
664
- const stateIdentity = `${value.id}#${state.id}`;
747
+ const stateIdentity = state.id;
665
748
  if (state.after !== undefined &&
666
749
  !architectureStateIds.has(qualifyReference(value.id, state.after))) {
667
750
  const pointer = `/states/${index}/after`;
@@ -692,7 +775,7 @@ function compileWorkspaceResolved(parsed) {
692
775
  }
693
776
  }
694
777
  for (const [index, state] of (value.states ?? []).entries()) {
695
- const subject = `${value.id}#${state.id}`;
778
+ const subject = state.id;
696
779
  subjects.push({ id: subject, type: 'concept' });
697
780
  claims.push({
698
781
  id: `${subject}~kind`,
@@ -787,7 +870,7 @@ function compileWorkspaceResolved(parsed) {
787
870
  column: source.column,
788
871
  });
789
872
  }
790
- else if (otherIdentity === `${value.id}#${concept.id}`) {
873
+ else if (otherIdentity === concept.id) {
791
874
  const source = location(['concepts', index, 'distinctFrom', distinctIndex], pointer);
792
875
  diagnostics.push({
793
876
  severity: 'error',
@@ -803,7 +886,7 @@ function compileWorkspaceResolved(parsed) {
803
886
  for (const [supersedesIndex, predecessor] of (concept.supersedes ?? []).entries()) {
804
887
  const pointer = `/concepts/${index}/supersedes/${supersedesIndex}`;
805
888
  const predecessorIdentity = qualifyReference(value.id, predecessor);
806
- const subjectIdentity = `${value.id}#${concept.id}`;
889
+ const subjectIdentity = concept.id;
807
890
  if (!conceptByQualifiedId.has(predecessorIdentity)) {
808
891
  const source = location(['concepts', index, 'supersedes', supersedesIndex], pointer);
809
892
  diagnostics.push({
@@ -1046,7 +1129,24 @@ function compileWorkspaceResolved(parsed) {
1046
1129
  diagnostics.push({
1047
1130
  severity: 'error',
1048
1131
  code: 'YM302',
1049
- message: `Unresolved concept reference "${reference}"`,
1132
+ message: `Unresolved concept reference "${reference}"${(() => {
1133
+ // A reference written the way ids read before 1.0 flattened them
1134
+ // (ADR 0099) is not a typo, and the edit distance from
1135
+ // `<document>#<local>` to `<local>` is usually past the
1136
+ // suggestion threshold, so it would otherwise get no hint at all
1137
+ // on the one change most likely to produce it.
1138
+ const hash = reference.lastIndexOf('#');
1139
+ if (hash !== -1) {
1140
+ const local = reference.slice(hash + 1);
1141
+ if (conceptByQualifiedId.has(qualifyReference(value.id, local))) {
1142
+ return `; a subject id carries no document prefix since 1.0, and "${local}" exists`;
1143
+ }
1144
+ }
1145
+ const suggestion = closestCandidate(reference, [...conceptByQualifiedId.keys()]);
1146
+ return suggestion === undefined
1147
+ ? ''
1148
+ : `; did you mean "${suggestion}"?`;
1149
+ })()}`,
1050
1150
  path: input.path,
1051
1151
  pointer,
1052
1152
  line: source.line,
@@ -1056,46 +1156,82 @@ function compileWorkspaceResolved(parsed) {
1056
1156
  }
1057
1157
  const policy = selectedProfile.relationshipKinds.get(relationship.kind);
1058
1158
  if (policy !== undefined) {
1059
- const aspectOf = (reference) => {
1159
+ // Each endpoint resolves to the core kind its declared kind descends
1160
+ // from: the ArchiMate relationship table is defined over core kinds,
1161
+ // and an extension inherits its parent's row and column (ADR 0097).
1162
+ const endpointKind = (reference) => {
1060
1163
  const resolvedConcept = conceptByQualifiedId.get(qualifyReference(value.id, reference));
1061
- return resolvedConcept === undefined
1062
- ? undefined
1063
- : profiles
1064
- .get(resolvedConcept.profile)
1065
- ?.conceptKinds.get(resolvedConcept.concept.kind)?.aspect;
1164
+ if (resolvedConcept === undefined)
1165
+ return undefined;
1166
+ const resolvedKind = profiles
1167
+ .get(resolvedConcept.profile)
1168
+ ?.conceptKinds.get(resolvedConcept.concept.kind);
1169
+ if (resolvedKind === undefined)
1170
+ return undefined;
1171
+ const core = localKindId(resolvedKind.lineage[0] ?? resolvedKind.identity);
1172
+ return isCoreConceptKindId(core)
1173
+ ? { declared: resolvedConcept.concept.kind, core, aspect: resolvedKind.aspect }
1174
+ : undefined;
1066
1175
  };
1067
- const sourceAspect = aspectOf(relationship.from);
1068
- const targetAspect = aspectOf(relationship.to);
1069
- const candidates = sourceAspect === undefined || targetAspect === undefined
1070
- ? ''
1071
- : candidateKindHint(selectedProfile.relationshipKinds, relationship.kind, sourceAspect, targetAspect);
1072
- for (const endpoint of ['source', 'target']) {
1073
- const reference = endpoint === 'source' ? relationship.from : relationship.to;
1074
- const aspect = endpoint === 'source' ? sourceAspect : targetAspect;
1075
- const allowed = endpoint === 'source'
1076
- ? policy.sourceAspects
1077
- : policy.targetAspects;
1078
- if (aspect !== undefined &&
1079
- allowed !== undefined &&
1080
- !allowed.includes(aspect)) {
1081
- const field = endpoint === 'source' ? 'from' : 'to';
1082
- const pointer = `/relationships/${index}/${field}`;
1083
- const source = location(['relationships', index, field], pointer);
1176
+ const from = endpointKind(relationship.from);
1177
+ const to = endpointKind(relationship.to);
1178
+ // An unresolved endpoint was already reported as YM302; nothing more
1179
+ // can be said about a pair that does not exist.
1180
+ if (from !== undefined && to !== undefined) {
1181
+ const permitted = tablePermittedKinds(from.core, to.core);
1182
+ const describe = (endpoint) => endpoint.declared === endpoint.core
1183
+ ? endpoint.core
1184
+ : `${endpoint.declared}, a ${endpoint.core}`;
1185
+ const candidates = permittedCandidates(selectedProfile.relationshipKinds, relationship.kind, permitted, from.aspect, to.aspect);
1186
+ const tail = candidates.length === 0
1187
+ ? ''
1188
+ : `; ArchiMate 3.2 permits: ${candidates.join(', ')}`;
1189
+ if (!permitted.has(policy.coreKind)) {
1190
+ const pointer = `/relationships/${index}/kind`;
1191
+ const source = location(['relationships', index, 'kind'], pointer);
1192
+ const kindLabel = relationship.kind === policy.coreKind
1193
+ ? `"${relationship.kind}"`
1194
+ : `"${relationship.kind}" (${policy.coreKind})`;
1084
1195
  diagnostics.push({
1085
1196
  severity: 'error',
1086
1197
  code: 'YM404',
1087
- message: `Relationship "${relationship.kind}" requires a ${endpoint} with aspect ${allowed.map((entry) => `"${entry}"`).join(' or ')}; "${reference}" has aspect "${aspect}"${policy.repair === undefined ? '' : `; ${policy.repair}`}${candidates}`,
1198
+ message: `Relationship ${kindLabel} is not permitted from "${relationship.from}" (${describe(from)}) to "${relationship.to}" (${describe(to)})${tail}`,
1088
1199
  path: input.path,
1089
1200
  pointer,
1090
1201
  line: source.line,
1091
1202
  column: source.column,
1092
1203
  });
1093
1204
  }
1205
+ else {
1206
+ // The table permits the pair; an extension kind may still
1207
+ // narrow it by aspect, and that narrowing is reported per end.
1208
+ for (const endpoint of ['source', 'target']) {
1209
+ const reference = endpoint === 'source' ? relationship.from : relationship.to;
1210
+ const aspect = endpoint === 'source' ? from.aspect : to.aspect;
1211
+ const allowed = endpoint === 'source'
1212
+ ? policy.sourceAspects
1213
+ : policy.targetAspects;
1214
+ if (allowed !== undefined && !allowed.includes(aspect)) {
1215
+ const field = endpoint === 'source' ? 'from' : 'to';
1216
+ const pointer = `/relationships/${index}/${field}`;
1217
+ const source = location(['relationships', index, field], pointer);
1218
+ diagnostics.push({
1219
+ severity: 'error',
1220
+ code: 'YM404',
1221
+ message: `Relationship "${relationship.kind}" requires a ${endpoint} with aspect ${allowed.map((entry) => `"${entry}"`).join(' or ')}; "${reference}" has aspect "${aspect}"${tail}`,
1222
+ path: input.path,
1223
+ pointer,
1224
+ line: source.line,
1225
+ column: source.column,
1226
+ });
1227
+ }
1228
+ }
1229
+ }
1094
1230
  }
1095
1231
  }
1096
1232
  }
1097
1233
  for (const [index, concept] of value.concepts.entries()) {
1098
- const subject = `${value.id}#${concept.id}`;
1234
+ const subject = concept.id;
1099
1235
  subjects.push({ id: subject, type: 'concept' });
1100
1236
  claims.push({
1101
1237
  id: `${subject}~kind`,
@@ -1190,6 +1326,20 @@ function compileWorkspaceResolved(parsed) {
1190
1326
  source: location(['concepts', index, 'owner'], `/concepts/${index}/owner`),
1191
1327
  });
1192
1328
  }
1329
+ if (concept.folder !== undefined) {
1330
+ claims.push({
1331
+ id: `${subject}~folder`,
1332
+ subject,
1333
+ predicate: 'yarramate/organisation/folder',
1334
+ // A VALUE, not a ref. A folder is a label the author writes, not a
1335
+ // subject the workspace declares: nothing can be said about it, it
1336
+ // resolves to nothing, and two documents writing the same label mean
1337
+ // the same folder without either naming the other (ADR 0104).
1338
+ object: { value: concept.folder },
1339
+ origin: 'declared',
1340
+ source: location(['concepts', index, 'folder'], `/concepts/${index}/folder`),
1341
+ });
1342
+ }
1193
1343
  for (const [constraintIndex, constraint] of (concept.constraints ?? []).entries()) {
1194
1344
  claims.push({
1195
1345
  id: `${subject}~constraint-${constraint.id}`,
@@ -1289,7 +1439,7 @@ function compileWorkspaceResolved(parsed) {
1289
1439
  }
1290
1440
  }
1291
1441
  for (const [index, relationship] of value.relationships.entries()) {
1292
- const id = `${value.id}#${relationship.id}`;
1442
+ const id = relationship.id;
1293
1443
  subjects.push({ id, type: 'relationship' });
1294
1444
  claims.push({
1295
1445
  id,
@@ -1381,7 +1531,7 @@ function compileWorkspaceResolved(parsed) {
1381
1531
  relationship.kind === 'aggregation'
1382
1532
  ? [
1383
1533
  {
1384
- id: `${value.id}#${relationship.id}`,
1534
+ id: relationship.id,
1385
1535
  localId: relationship.id,
1386
1536
  document: value.id,
1387
1537
  kind: relationship.kind,
@@ -1420,6 +1570,72 @@ function compileWorkspaceResolved(parsed) {
1420
1570
  previousRelationships.push(relationship);
1421
1571
  wholePartByEndpoints.set(endpointKey, previousRelationships);
1422
1572
  }
1573
+ // A junction takes the kind of the relationships that pass through it, so
1574
+ // every relationship on one junction must be the same kind (ArchiMate 3.2,
1575
+ // junctions). The table cannot say this: it rules on one pair at a time,
1576
+ // and this is a property of the set. Sorted by id, so the first-listed
1577
+ // relationship's kind is the one the others are measured against and the
1578
+ // diagnostic lands on the later ones.
1579
+ const junctionKinds = new Set([
1580
+ 'andJunction',
1581
+ 'orJunction',
1582
+ ]);
1583
+ const junctionRelationships = documents
1584
+ .flatMap(({ value, location }) => value.relationships.flatMap((relationship, index) => {
1585
+ const policy = profiles
1586
+ .get(value.profile)
1587
+ ?.relationshipKinds.get(relationship.kind);
1588
+ if (policy === undefined)
1589
+ return [];
1590
+ return ['from', 'to'].flatMap((endpoint) => {
1591
+ const junction = qualifyReference(value.id, relationship[endpoint]);
1592
+ const resolvedConcept = conceptByQualifiedId.get(junction);
1593
+ const resolvedKind = resolvedConcept === undefined
1594
+ ? undefined
1595
+ : profiles
1596
+ .get(resolvedConcept.profile)
1597
+ ?.conceptKinds.get(resolvedConcept.concept.kind);
1598
+ if (resolvedKind === undefined ||
1599
+ !junctionKinds.has(localKindId(resolvedKind.lineage[0] ?? resolvedKind.identity))) {
1600
+ return [];
1601
+ }
1602
+ return [
1603
+ {
1604
+ id: relationship.id,
1605
+ localId: relationship.id,
1606
+ junction,
1607
+ kind: relationship.kind,
1608
+ coreKind: policy.coreKind,
1609
+ source: location(['relationships', index, 'kind'], `/relationships/${index}/kind`),
1610
+ },
1611
+ ];
1612
+ });
1613
+ }))
1614
+ .sort(compareById);
1615
+ const relationshipsByJunction = new Map();
1616
+ for (const entry of junctionRelationships) {
1617
+ const group = relationshipsByJunction.get(entry.junction) ?? [];
1618
+ group.push(entry);
1619
+ relationshipsByJunction.set(entry.junction, group);
1620
+ }
1621
+ for (const [junction, group] of relationshipsByJunction) {
1622
+ const expected = group[0]?.coreKind;
1623
+ if (expected === undefined)
1624
+ continue;
1625
+ for (const entry of group) {
1626
+ if (entry.coreKind === expected)
1627
+ continue;
1628
+ diagnostics.push({
1629
+ severity: 'error',
1630
+ code: 'YM414',
1631
+ message: `Relationship "${entry.localId}" (${entry.kind}) joins junction "${junction}" whose relationships are "${expected}"; every relationship on one junction must be the same kind`,
1632
+ path: entry.source.path,
1633
+ pointer: entry.source.pointer,
1634
+ line: entry.source.line,
1635
+ column: entry.source.column,
1636
+ });
1637
+ }
1638
+ }
1423
1639
  if (diagnostics.length > 0) {
1424
1640
  return diagnosticFailure(diagnostics);
1425
1641
  }
@@ -1449,14 +1665,38 @@ function compileWorkspaceResolved(parsed) {
1449
1665
  .map(([identity, kind]) => [
1450
1666
  identity,
1451
1667
  Object.freeze({
1452
- ...(kind.sourceAspects === undefined
1453
- ? {}
1454
- : { source: Object.freeze([...kind.sourceAspects]) }),
1455
- ...(kind.targetAspects === undefined
1456
- ? {}
1457
- : { target: Object.freeze([...kind.targetAspects]) }),
1668
+ source: Object.freeze([
1669
+ ...(kind.sourceAspects ??
1670
+ matrixEndpointAspects(kind.coreKind, 'source')),
1671
+ ]),
1672
+ target: Object.freeze([
1673
+ ...(kind.targetAspects ??
1674
+ matrixEndpointAspects(kind.coreKind, 'target')),
1675
+ ]),
1458
1676
  }),
1459
1677
  ])),
1678
+ conceptKindCoreAncestors: immutableMap([...conceptKindByIdentity]
1679
+ .sort(([left], [right]) => left.localeCompare(right))
1680
+ .flatMap(([identity, kind]) => {
1681
+ const core = localKindId(kind.lineage[0] ?? identity);
1682
+ return isCoreConceptKindId(core)
1683
+ ? [[identity, core]]
1684
+ : [];
1685
+ })),
1686
+ relationshipKindCoreAncestors: immutableMap([...relationshipKindByIdentity]
1687
+ .sort(([left], [right]) => left.localeCompare(right))
1688
+ .map(([identity, kind]) => [identity, kind.coreKind])),
1689
+ permittedRelationshipKinds: (fromKindIdentity, toKindIdentity) => {
1690
+ const from = conceptKindByIdentity.get(fromKindIdentity);
1691
+ const to = conceptKindByIdentity.get(toKindIdentity);
1692
+ if (from === undefined || to === undefined)
1693
+ return undefined;
1694
+ const fromCore = localKindId(from.lineage[0] ?? from.identity);
1695
+ const toCore = localKindId(to.lineage[0] ?? to.identity);
1696
+ return isCoreConceptKindId(fromCore) && isCoreConceptKindId(toCore)
1697
+ ? tablePermittedKinds(fromCore, toCore)
1698
+ : undefined;
1699
+ },
1460
1700
  },
1461
1701
  graph: {
1462
1702
  format: 'yarramate/graph/v2',
@@ -0,0 +1,27 @@
1
+ import type { CanvasGraph } from './graph-projection.js';
2
+ import type { YarramateOperation } from './operations.js';
3
+ /**
4
+ * A kebab-case id derived from a name, unique across the graph, or `null` when
5
+ * the name yields nothing an id may be made of.
6
+ *
7
+ * Deriving rather than asking is the point: an id is a stable address a human
8
+ * reads in a diff, and asking a reviewer to invent one while they are thinking
9
+ * about a name produces worse ids than a transliteration of the name does.
10
+ * Returning null rather than a placeholder keeps a subject called `"???"` from
11
+ * landing as `subject-1`, which nothing could later be traced back from.
12
+ */
13
+ export declare const proposeConceptId: (graph: CanvasGraph, name: string) => string | null;
14
+ /**
15
+ * The operation that lands a new subject, or `null` when the draft is one no
16
+ * document could accept.
17
+ *
18
+ * `kinds` is the vocabulary the workspace's profile actually offers. Refusing
19
+ * a kind outside it here, rather than trusting the caller's palette, is the
20
+ * same posture `draftRelationship` takes: the guarantee has to hold for any
21
+ * caller, not only one that filtered first.
22
+ */
23
+ export declare const draftConcept: (graph: CanvasGraph, input: {
24
+ readonly name: string;
25
+ readonly kind: string;
26
+ readonly document: string;
27
+ }, kinds: readonly string[]) => YarramateOperation | null;