yarramate 1.2.0 → 1.4.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 (84) hide show
  1. package/catalogues/core-enrichment.yaml +237 -2
  2. package/dist/adapters/visual/session-server.js +16 -1
  3. package/dist/adapters/visual/view-identity.d.ts +1 -1
  4. package/dist/adapters/visual/view-identity.js +61 -3
  5. package/dist/adapters/visual/wire.d.ts +30 -0
  6. package/dist/adapters/visual/workspace-model.d.ts +35 -3
  7. package/dist/adapters/visual/workspace-model.js +62 -2
  8. package/dist/ask-command.js +37 -14
  9. package/dist/cli-support.d.ts +1 -1
  10. package/dist/cli-support.js +1 -1
  11. package/dist/cli.d.ts +1 -0
  12. package/dist/cli.js +26 -5
  13. package/dist/compiler.d.ts +13 -1
  14. package/dist/compiler.js +646 -0
  15. package/dist/concept-drafting.d.ts +13 -2
  16. package/dist/concept-drafting.js +15 -3
  17. package/dist/design-command.js +30 -4
  18. package/dist/evidence.js +25 -0
  19. package/dist/graph-projection.d.ts +10 -0
  20. package/dist/graph-projection.js +1 -0
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js +1 -1
  23. package/dist/interrogate-command.d.ts +37 -1
  24. package/dist/interrogate-command.js +93 -42
  25. package/dist/interrogation-entry.d.ts +1 -1
  26. package/dist/layout-direction.d.ts +14 -0
  27. package/dist/layout-direction.js +5 -0
  28. package/dist/projection.d.ts +22 -6
  29. package/dist/projection.js +23 -2
  30. package/dist/relationship-drafting.d.ts +21 -2
  31. package/dist/relationship-drafting.js +49 -4
  32. package/dist/subject-references.js +5 -0
  33. package/dist/visual-app/assets/index-Rkq6smL2.css +1 -0
  34. package/dist/visual-app/assets/index-yCHtmQUH.js +394 -0
  35. package/dist/visual-app/index.html +2 -2
  36. package/dist/visual-app-lib/editor.js +29639 -27514
  37. package/dist/visual-app-lib/styles.css +1 -1
  38. package/dist/visual-app-lib/types/adapters/visual/view-identity.d.ts +1 -1
  39. package/dist/visual-app-lib/types/adapters/visual/wire.d.ts +30 -0
  40. package/dist/visual-app-lib/types/adapters/visual/workspace-model.d.ts +35 -3
  41. package/dist/visual-app-lib/types/compiler.d.ts +13 -1
  42. package/dist/visual-app-lib/types/concept-drafting.d.ts +13 -2
  43. package/dist/visual-app-lib/types/graph-projection.d.ts +10 -0
  44. package/dist/visual-app-lib/types/interrogate-command.d.ts +207 -0
  45. package/dist/visual-app-lib/types/layout-direction.d.ts +14 -0
  46. package/dist/visual-app-lib/types/projection.d.ts +22 -6
  47. package/dist/visual-app-lib/types/relationship-drafting.d.ts +21 -2
  48. package/dist/visual-app-lib/types/subject-identity.d.ts +34 -0
  49. package/dist/visual-app-lib/types/visual-app/App.d.ts +27 -2
  50. package/dist/visual-app-lib/types/visual-app/badges.d.ts +1 -0
  51. package/dist/visual-app-lib/types/visual-app/connection-panel.d.ts +9 -1
  52. package/dist/visual-app-lib/types/visual-app/context-menu-model.d.ts +15 -6
  53. package/dist/visual-app-lib/types/visual-app/graph-canvas.d.ts +77 -6
  54. package/dist/visual-app-lib/types/visual-app/kind-palette.d.ts +37 -0
  55. package/dist/visual-app-lib/types/visual-app/local-host.d.ts +42 -0
  56. package/dist/visual-app-lib/types/visual-app/mount.d.ts +73 -2
  57. package/dist/visual-app-lib/types/visual-app/open-questions.d.ts +15 -0
  58. package/dist/visual-app-lib/types/visual-app/query-fields.d.ts +3 -2
  59. package/dist/visual-app-lib/types/visual-app/query-panel.d.ts +8 -1
  60. package/dist/visual-app-lib/types/visual-app/save-view.d.ts +11 -5
  61. package/dist/visual-app-lib/types/visual-app/shipped-catalogue.d.ts +10 -0
  62. package/dist/visual-app-lib/types/visual-app/state.d.ts +15 -4
  63. package/dist/visual-app-lib/types/visual-app/subject-draft-panel.d.ts +13 -1
  64. package/dist/visual-app-lib/types/visual-app/subject-filter.d.ts +35 -0
  65. package/dist/visual-app-lib/types/visual-app/subject-form.d.ts +8 -0
  66. package/dist/visual-app-lib/types/visual-app/view-tree-model.d.ts +42 -9
  67. package/dist/visual-app-lib/types/visual-app/view-tree.d.ts +11 -2
  68. package/dist/visual-app-lib/types/visual-app/workspace-state.d.ts +81 -11
  69. package/dist/visual-app-lib/types/workspace.d.ts +2 -0
  70. package/dist/workspace.d.ts +2 -0
  71. package/dist/workspace.js +1 -0
  72. package/docs/CONSUMING-YARRAMATE.md +81 -0
  73. package/package.json +2 -1
  74. package/schema/yarramate-design-step.schema.json +26 -0
  75. package/schema/yarramate-document.schema.json +14 -0
  76. package/schema/yarramate-interrogation-report.schema.json +31 -0
  77. package/schema/yarramate-pattern.schema.json +133 -0
  78. package/schema/yarramate-projection.schema.json +9 -0
  79. package/schema/yarramate-question-catalogue.schema.json +34 -0
  80. package/schema/yarramate-visual-graph.schema.json +9 -0
  81. package/schema/yarramate-workspace.schema.json +4 -0
  82. package/skills/yarramate-architecture/SKILL.md +5 -0
  83. package/dist/visual-app/assets/index-CcmfL3oY.js +0 -394
  84. package/dist/visual-app/assets/index-DLutIWES.css +0 -1
package/dist/compiler.js CHANGED
@@ -7,6 +7,8 @@ import documentSchema from '../schema/yarramate-document.schema.json' with { typ
7
7
  };
8
8
  import profileSchema from '../schema/yarramate-profile.schema.json' with { type: 'json'
9
9
  };
10
+ import patternSchema from '../schema/yarramate-pattern.schema.json' with { type: 'json'
11
+ };
10
12
  import { ATTESTATION_PREDICATE_PREFIX, attestationClaimValue } from './graph-claims.js';
11
13
  import { shippedPolicyIdentity, shippedPolicySource, } from './shipped-profile.js';
12
14
  const coreProfile = 'yarramate/core@0.1';
@@ -14,6 +16,7 @@ const ajv2020Module = Ajv2020Import;
14
16
  const Ajv2020 = ajv2020Module.default ?? ajv2020Module;
15
17
  const validateDocument = new Ajv2020({ allErrors: true }).compile(documentSchema);
16
18
  const validateProfile = new Ajv2020({ allErrors: true }).compile(profileSchema);
19
+ const validatePattern = new Ajv2020({ allErrors: true }).compile(patternSchema);
17
20
  const immutableMap = (entries) => {
18
21
  const backing = new Map(entries);
19
22
  const facade = {
@@ -133,6 +136,18 @@ const parseWorkspaceSource = (input) => {
133
136
  // Classification reads the composed mapping through the YAML document, which
134
137
  // types the lookup as `unknown` - the same key the old probe pass read.
135
138
  const fresh = { yaml, lineCounter };
139
+ if (yaml.get('format') === 'yarramate/pattern/v1') {
140
+ return {
141
+ entry: {
142
+ source: input.source,
143
+ kind: 'pattern',
144
+ value,
145
+ schemaDiagnostics: parseDiagnostics,
146
+ positions: new Map(),
147
+ },
148
+ fresh,
149
+ };
150
+ }
136
151
  if (yaml.get('format') === 'yarramate/profile/v1') {
137
152
  return {
138
153
  entry: {
@@ -260,11 +275,15 @@ export const withDiagnosticSubjects = (diagnostics, sources) => {
260
275
  };
261
276
  function compileWorkspaceResolved(parsed) {
262
277
  const profileInputs = [];
278
+ const patternInputs = [];
263
279
  const documentInputs = [];
264
280
  for (const source of parsed) {
265
281
  if (source.entry.kind === 'profile') {
266
282
  profileInputs.push(source);
267
283
  }
284
+ else if (source.entry.kind === 'pattern') {
285
+ patternInputs.push(source);
286
+ }
268
287
  else {
269
288
  documentInputs.push(source);
270
289
  }
@@ -563,6 +582,268 @@ function compileWorkspaceResolved(parsed) {
563
582
  if (profileDiagnostics.length > 0) {
564
583
  return diagnosticFailure(profileDiagnostics);
565
584
  }
585
+ /**
586
+ * Which core relationship kinds the vendored 3.2 table permits between two
587
+ * resolved concept kinds, or `undefined` where either side is an extension
588
+ * whose core ancestor is not a core kind. The same rule `profileContext`
589
+ * publishes below; needed here because a pattern's legality is decided
590
+ * before any document is read.
591
+ */
592
+ const permittedBetween = (fromKindIdentity, toKindIdentity) => {
593
+ const from = conceptKindByIdentity.get(fromKindIdentity);
594
+ const to = conceptKindByIdentity.get(toKindIdentity);
595
+ if (from === undefined || to === undefined)
596
+ return undefined;
597
+ const fromCore = localKindId(from.lineage[0] ?? from.identity);
598
+ const toCore = localKindId(to.lineage[0] ?? to.identity);
599
+ return isCoreConceptKindId(fromCore) && isCoreConceptKindId(toCore)
600
+ ? new Set(tablePermittedKinds(fromCore, toCore))
601
+ : undefined;
602
+ };
603
+ // ---- structural patterns (#268, ADR 0123) --------------------------------
604
+ //
605
+ // Resolved here, after the profiles they name and before the documents that
606
+ // instantiate them. A pattern declares the shape a kind promises: the slots
607
+ // an instance binds and the wiring the compiler mints between them. Faults
608
+ // in the pattern itself are reported ONCE, against the pattern, rather than
609
+ // once per instance - including whether the wiring it describes is legal
610
+ // ArchiMate at all, which is a property of the slot kinds and so knowable
611
+ // without any instance.
612
+ const patternDiagnostics = [];
613
+ const patternsByKind = new Map();
614
+ for (const { input, entry, fresh } of patternInputs) {
615
+ const value = entry.value;
616
+ const positionFor = positionReader(input.source, entry.positions, fresh);
617
+ const at = (yamlPath, pointer) => {
618
+ const position = positionFor(yamlPath);
619
+ return {
620
+ path: input.path,
621
+ pointer,
622
+ line: position.line,
623
+ column: position.col,
624
+ };
625
+ };
626
+ if (entry.schemaDiagnostics.length > 0) {
627
+ patternDiagnostics.push(...entry.schemaDiagnostics);
628
+ continue;
629
+ }
630
+ if (!validatePattern(value)) {
631
+ for (const error of validatePattern.errors ?? []) {
632
+ const property = error.keyword === 'additionalProperties'
633
+ ? String(error.params.additionalProperty)
634
+ : undefined;
635
+ const pointer = property
636
+ ? `${error.instancePath}/${property}`
637
+ : error.instancePath || '/';
638
+ const yamlPath = pointer
639
+ .split('/')
640
+ .slice(1)
641
+ .map((segment) => (/^\d+$/.test(segment) ? Number(segment) : segment));
642
+ patternDiagnostics.push({
643
+ severity: 'error',
644
+ code: 'YM201',
645
+ message: describeSchemaViolation(error),
646
+ ...at(yamlPath, pointer),
647
+ });
648
+ }
649
+ continue;
650
+ }
651
+ for (const [index, pattern] of value.patterns.entries()) {
652
+ const anchor = conceptKindByIdentity.get(pattern.kind);
653
+ if (anchor === undefined) {
654
+ patternDiagnostics.push({
655
+ severity: 'error',
656
+ code: 'YM401',
657
+ message: `Concept kind "${pattern.kind}" is not available to this pattern`,
658
+ ...at(['patterns', index, 'kind'], `/patterns/${index}/kind`),
659
+ });
660
+ continue;
661
+ }
662
+ const existing = patternsByKind.get(pattern.kind);
663
+ if (existing !== undefined) {
664
+ patternDiagnostics.push({
665
+ severity: 'error',
666
+ code: 'YM411',
667
+ message: `Concept kind "${pattern.kind}" already has a pattern, declared by "${existing.declaredBy}"; a kind has at most one shape`,
668
+ ...at(['patterns', index, 'kind'], `/patterns/${index}/kind`),
669
+ });
670
+ continue;
671
+ }
672
+ const slots = new Map();
673
+ let slotsOk = true;
674
+ for (const [slot, part] of Object.entries(pattern.parts)) {
675
+ const kind = conceptKindByIdentity.get(part.kind);
676
+ if (kind === undefined) {
677
+ patternDiagnostics.push({
678
+ severity: 'error',
679
+ code: 'YM401',
680
+ message: `Concept kind "${part.kind}" is not available to the "${slot}" part`,
681
+ ...at(['patterns', index, 'parts', slot, 'kind'], `/patterns/${index}/parts/${slot}/kind`),
682
+ });
683
+ slotsOk = false;
684
+ continue;
685
+ }
686
+ slots.set(slot, {
687
+ name: slot,
688
+ kindIdentity: kind.identity,
689
+ required: part.required === true,
690
+ });
691
+ }
692
+ if (!slotsOk)
693
+ continue;
694
+ // `self` is the instance, so it is spelled apart from the slots and can
695
+ // never be one: a slot named `self` would make the wiring ambiguous
696
+ // about which end it meant.
697
+ if (slots.has('self')) {
698
+ patternDiagnostics.push({
699
+ severity: 'error',
700
+ code: 'YM201',
701
+ message: `A part cannot be named "self"; "self" names the instance itself in wiring`,
702
+ ...at(['patterns', index, 'parts'], `/patterns/${index}/parts`),
703
+ });
704
+ continue;
705
+ }
706
+ const kindOfEndpoint = (endpoint) => endpoint === 'self' ? anchor.identity : slots.get(endpoint)?.kindIdentity;
707
+ const wiring = [];
708
+ let wiringOk = true;
709
+ const seenWires = new Set();
710
+ for (const [wireIndex, wire] of pattern.wiring.entries()) {
711
+ const wireAt = at(['patterns', index, 'wiring', wireIndex], `/patterns/${index}/wiring/${wireIndex}`);
712
+ const fromKind = kindOfEndpoint(wire.from);
713
+ const toKind = kindOfEndpoint(wire.to);
714
+ for (const [end, endpoint, kind] of [
715
+ ['from', wire.from, fromKind],
716
+ ['to', wire.to, toKind],
717
+ ]) {
718
+ if (kind !== undefined)
719
+ continue;
720
+ patternDiagnostics.push({
721
+ severity: 'error',
722
+ code: 'YM302',
723
+ message: `Wiring names "${endpoint}" as its ${end}, which is neither "self" nor a declared part`,
724
+ ...wireAt,
725
+ });
726
+ wiringOk = false;
727
+ }
728
+ if (wire.from === wire.to) {
729
+ patternDiagnostics.push({
730
+ severity: 'error',
731
+ code: 'YM201',
732
+ message: `Wiring joins "${wire.from}" to itself`,
733
+ ...wireAt,
734
+ });
735
+ wiringOk = false;
736
+ }
737
+ const policy = relationshipKindByIdentity.get(wire.kind);
738
+ if (policy === undefined) {
739
+ patternDiagnostics.push({
740
+ severity: 'error',
741
+ code: 'YM402',
742
+ message: `Relationship kind "${wire.kind}" is not available to this pattern`,
743
+ ...wireAt,
744
+ });
745
+ wiringOk = false;
746
+ }
747
+ if (fromKind === undefined || toKind === undefined || policy === undefined) {
748
+ continue;
749
+ }
750
+ const duplicate = `${wire.from}\u0000${wire.kind}\u0000${wire.to}`;
751
+ if (seenWires.has(duplicate)) {
752
+ patternDiagnostics.push({
753
+ severity: 'error',
754
+ code: 'YM201',
755
+ message: `Wiring repeats "${wire.from}" ${policy.coreKind} "${wire.to}"`,
756
+ ...wireAt,
757
+ });
758
+ wiringOk = false;
759
+ continue;
760
+ }
761
+ seenWires.add(duplicate);
762
+ // Legality is a property of the PATTERN, because the slot kinds fix
763
+ // both endpoint kinds: a pattern whose wiring the relationship table
764
+ // forbids can never expand legally, and saying so once here beats
765
+ // saying it against every instance that was authored correctly.
766
+ const permitted = permittedBetween(fromKind, toKind);
767
+ if (permitted !== undefined && !permitted.has(policy.coreKind)) {
768
+ patternDiagnostics.push({
769
+ severity: 'error',
770
+ code: 'YM404',
771
+ message: `Wiring "${wire.from}" ${policy.coreKind} "${wire.to}" is not permitted between ` +
772
+ `"${fromKind}" and "${toKind}"`,
773
+ ...wireAt,
774
+ });
775
+ wiringOk = false;
776
+ continue;
777
+ }
778
+ wiring.push({
779
+ from: wire.from,
780
+ to: wire.to,
781
+ kindIdentity: policy.identity,
782
+ coreKind: policy.coreKind,
783
+ });
784
+ }
785
+ if (!wiringOk)
786
+ continue;
787
+ const ports = new Map();
788
+ let portsOk = true;
789
+ for (const [portIndex, port] of (pattern.ports ?? []).entries()) {
790
+ const portAt = at(['patterns', index, 'ports', portIndex], `/patterns/${index}/ports/${portIndex}`);
791
+ const policy = relationshipKindByIdentity.get(port.kind);
792
+ if (policy === undefined) {
793
+ patternDiagnostics.push({
794
+ severity: 'error',
795
+ code: 'YM402',
796
+ message: `Relationship kind "${port.kind}" is not available to this pattern`,
797
+ ...portAt,
798
+ });
799
+ portsOk = false;
800
+ continue;
801
+ }
802
+ for (const [end, slot] of [
803
+ ['out', port.out],
804
+ ['in', port.in],
805
+ ]) {
806
+ if (slots.has(slot))
807
+ continue;
808
+ patternDiagnostics.push({
809
+ severity: 'error',
810
+ code: 'YM302',
811
+ message: `Port "${policy.coreKind}" names "${slot}" as its ${end}, which is not a declared part`,
812
+ ...portAt,
813
+ });
814
+ portsOk = false;
815
+ }
816
+ if (ports.has(port.kind)) {
817
+ patternDiagnostics.push({
818
+ severity: 'error',
819
+ code: 'YM201',
820
+ message: `Port "${policy.coreKind}" is declared twice; a kind lands one way`,
821
+ ...portAt,
822
+ });
823
+ portsOk = false;
824
+ continue;
825
+ }
826
+ ports.set(port.kind, {
827
+ out: port.out,
828
+ in: port.in,
829
+ coreKind: policy.coreKind,
830
+ });
831
+ }
832
+ if (!portsOk)
833
+ continue;
834
+ patternsByKind.set(pattern.kind, {
835
+ kindIdentity: pattern.kind,
836
+ declaredBy: input.path,
837
+ slots,
838
+ wiring,
839
+ ports,
840
+ });
841
+ }
842
+ }
843
+ if (patternDiagnostics.length > 0) {
844
+ return diagnosticFailure(patternDiagnostics);
845
+ }
846
+ const patternInstances = [];
566
847
  const documents = documentInputs.map(({ input, entry, fresh }) => {
567
848
  // Schema-checked by `parseWorkspaceSource`; the faults it found are the
568
849
  // `schemaDiagnostics` returned below, and they gate every later phase.
@@ -1276,6 +1557,55 @@ function compileWorkspaceResolved(parsed) {
1276
1557
  for (const [index, concept] of value.concepts.entries()) {
1277
1558
  const subject = concept.id;
1278
1559
  subjects.push({ id: subject, type: 'concept' });
1560
+ // A pattern instance is only COLLECTED here. What its slots bind may be
1561
+ // declared in any document, so the bindings cannot be checked until
1562
+ // every document has been read (#268, ADR 0123).
1563
+ if (concept.parts !== undefined) {
1564
+ const kindIdentity = selectedProfile.conceptKinds.get(concept.kind)?.identity ??
1565
+ concept.kind;
1566
+ const pattern = patternsByKind.get(kindIdentity);
1567
+ if (pattern === undefined) {
1568
+ const where = location(['concepts', index, 'parts'], `/concepts/${index}/parts`);
1569
+ diagnostics.push({
1570
+ severity: 'error',
1571
+ code: 'YM419',
1572
+ message: `Concept "${subject}" declares parts, but kind "${kindIdentity}" has no pattern to bind them to`,
1573
+ path: where.path,
1574
+ pointer: where.pointer,
1575
+ line: where.line,
1576
+ column: where.column,
1577
+ });
1578
+ }
1579
+ else {
1580
+ const bindings = new Map();
1581
+ const sourceOf = new Map();
1582
+ for (const [slot, target] of Object.entries(concept.parts)) {
1583
+ const where = location(['concepts', index, 'parts', slot], `/concepts/${index}/parts/${slot}`);
1584
+ if (!pattern.slots.has(slot)) {
1585
+ diagnostics.push({
1586
+ severity: 'error',
1587
+ code: 'YM419',
1588
+ message: `Concept "${subject}" binds "${slot}", which the pattern for ` +
1589
+ `"${kindIdentity}" does not declare; its parts are ` +
1590
+ `${[...pattern.slots.keys()].map((name) => `"${name}"`).join(', ')}`,
1591
+ path: where.path,
1592
+ pointer: where.pointer,
1593
+ line: where.line,
1594
+ column: where.column,
1595
+ });
1596
+ continue;
1597
+ }
1598
+ bindings.set(slot, qualifyReference(value.id, target));
1599
+ sourceOf.set(slot, where);
1600
+ }
1601
+ patternInstances.push({
1602
+ instance: subject,
1603
+ pattern,
1604
+ bindings,
1605
+ sourceOf,
1606
+ });
1607
+ }
1608
+ }
1279
1609
  claims.push({
1280
1610
  id: `${subject}~kind`,
1281
1611
  subject,
@@ -1695,6 +2025,310 @@ function compileWorkspaceResolved(parsed) {
1695
2025
  });
1696
2026
  }
1697
2027
  }
2028
+ // ---- pattern expansion (#268, ADR 0123) ----------------------------------
2029
+ //
2030
+ // Every document has been read, so this can see the whole workspace: what a
2031
+ // slot binds, what kind that subject is, and whether an authored
2032
+ // relationship already says what a wiring edge says. Minted claims are
2033
+ // `declared` and sourced to the binding line that produced them, because
2034
+ // that is where the author said it - the pattern only says which KIND of
2035
+ // edge a bound pair gets.
2036
+ if (patternInstances.length > 0) {
2037
+ const kindOfSubject = new Map();
2038
+ for (const claim of claims) {
2039
+ if (claim.predicate === 'yarramate/concept/kind' &&
2040
+ 'value' in claim.object) {
2041
+ kindOfSubject.set(claim.subject, claim.object.value);
2042
+ }
2043
+ }
2044
+ const declaredIds = new Set(subjects.map(({ id }) => id));
2045
+ // Authored relationships indexed by the UNORDERED pair they join, which is
2046
+ // the granularity the ownership rule is stated at: the pattern speaks for
2047
+ // a pair, so a reversed edge is a contradiction rather than a new fact.
2048
+ const pairKey = (left, right) => left < right ? `${left}\u0000${right}` : `${right}\u0000${left}`;
2049
+ const authoredByPair = new Map();
2050
+ const relationshipIds = new Set(subjects.filter(({ type }) => type === 'relationship').map(({ id }) => id));
2051
+ for (const claim of claims) {
2052
+ if (!relationshipIds.has(claim.id) || !('ref' in claim.object))
2053
+ continue;
2054
+ const entry = {
2055
+ id: claim.id,
2056
+ from: claim.subject,
2057
+ to: claim.object.ref,
2058
+ predicate: claim.predicate,
2059
+ source: claim.source,
2060
+ };
2061
+ const key = pairKey(entry.from, entry.to);
2062
+ const group = authoredByPair.get(key);
2063
+ if (group === undefined)
2064
+ authoredByPair.set(key, [entry]);
2065
+ else
2066
+ group.push(entry);
2067
+ }
2068
+ for (const { instance, pattern, bindings, sourceOf } of patternInstances) {
2069
+ const boundTo = new Map();
2070
+ for (const [slot, target] of bindings) {
2071
+ const where = sourceOf.get(slot);
2072
+ if (where === undefined)
2073
+ continue;
2074
+ const slotShape = pattern.slots.get(slot);
2075
+ if (slotShape === undefined)
2076
+ continue;
2077
+ if (!declaredIds.has(target)) {
2078
+ diagnostics.push({
2079
+ severity: 'error',
2080
+ code: 'YM315',
2081
+ message: `Part "${slot}" of "${instance}" names "${target}", which is not a declared subject`,
2082
+ path: where.path,
2083
+ pointer: where.pointer,
2084
+ line: where.line,
2085
+ column: where.column,
2086
+ });
2087
+ continue;
2088
+ }
2089
+ const already = boundTo.get(target);
2090
+ if (already !== undefined) {
2091
+ diagnostics.push({
2092
+ severity: 'error',
2093
+ code: 'YM315',
2094
+ message: `"${instance}" binds "${target}" as both "${already}" and "${slot}"; one subject fills one slot`,
2095
+ path: where.path,
2096
+ pointer: where.pointer,
2097
+ line: where.line,
2098
+ column: where.column,
2099
+ });
2100
+ continue;
2101
+ }
2102
+ boundTo.set(target, slot);
2103
+ const actual = kindOfSubject.get(target);
2104
+ if (actual !== slotShape.kindIdentity) {
2105
+ diagnostics.push({
2106
+ severity: 'error',
2107
+ code: 'YM417',
2108
+ message: `Part "${slot}" of "${instance}" binds "${target}", which is ` +
2109
+ `"${actual ?? 'not a concept'}"; the pattern declares this part ` +
2110
+ `"${slotShape.kindIdentity}"`,
2111
+ path: where.path,
2112
+ pointer: where.pointer,
2113
+ line: where.line,
2114
+ column: where.column,
2115
+ });
2116
+ }
2117
+ }
2118
+ const instanceSource = [...sourceOf.values()][0];
2119
+ for (const slot of pattern.slots.values()) {
2120
+ if (!slot.required || bindings.has(slot.name))
2121
+ continue;
2122
+ if (instanceSource === undefined)
2123
+ continue;
2124
+ diagnostics.push({
2125
+ severity: 'error',
2126
+ code: 'YM416',
2127
+ message: `"${instance}" leaves the required part "${slot.name}" unbound`,
2128
+ path: instanceSource.path,
2129
+ pointer: instanceSource.pointer,
2130
+ line: instanceSource.line,
2131
+ column: instanceSource.column,
2132
+ });
2133
+ }
2134
+ const subjectOf = (endpoint) => endpoint === 'self' ? instance : bindings.get(endpoint);
2135
+ for (const wire of pattern.wiring) {
2136
+ const from = subjectOf(wire.from);
2137
+ const to = subjectOf(wire.to);
2138
+ // An optional part nobody bound wires nothing. The required ones have
2139
+ // already been reported above, so this is silence by design.
2140
+ if (from === undefined || to === undefined)
2141
+ continue;
2142
+ if (!declaredIds.has(from) || !declaredIds.has(to))
2143
+ continue;
2144
+ const where = sourceOf.get(wire.to === 'self' ? wire.from : wire.to) ??
2145
+ instanceSource;
2146
+ if (where === undefined)
2147
+ continue;
2148
+ const authored = authoredByPair.get(pairKey(from, to)) ?? [];
2149
+ let satisfied = false;
2150
+ for (const edge of authored) {
2151
+ if (edge.from === from &&
2152
+ edge.to === to &&
2153
+ edge.predicate === wire.kindIdentity) {
2154
+ // Exactly what the wiring says. The authored relationship IS the
2155
+ // wiring, so nothing is minted and nothing is reported: adoption
2156
+ // costs no edit, and the redundant lines can go later.
2157
+ satisfied = true;
2158
+ continue;
2159
+ }
2160
+ diagnostics.push({
2161
+ severity: 'error',
2162
+ code: 'YM418',
2163
+ message: `Relationship "${edge.id}" joins "${edge.from}" to "${edge.to}", a pair ` +
2164
+ `the pattern for "${pattern.kindIdentity}" wires as "${from}" ` +
2165
+ `${wire.coreKind} "${to}"`,
2166
+ path: edge.source.path,
2167
+ pointer: edge.source.pointer,
2168
+ line: edge.source.line,
2169
+ column: edge.source.column,
2170
+ });
2171
+ }
2172
+ if (satisfied)
2173
+ continue;
2174
+ const wireId = [
2175
+ instance,
2176
+ ...(wire.from === 'self' ? [] : [wire.from]),
2177
+ wire.coreKind,
2178
+ wire.to,
2179
+ ].join('-');
2180
+ if (declaredIds.has(wireId)) {
2181
+ diagnostics.push({
2182
+ severity: 'error',
2183
+ code: 'YM420',
2184
+ message: `The pattern for "${pattern.kindIdentity}" derives the wiring id ` +
2185
+ `"${wireId}" for "${instance}", which is already a declared subject`,
2186
+ path: where.path,
2187
+ pointer: where.pointer,
2188
+ line: where.line,
2189
+ column: where.column,
2190
+ });
2191
+ continue;
2192
+ }
2193
+ declaredIds.add(wireId);
2194
+ subjects.push({ id: wireId, type: 'relationship' });
2195
+ claims.push({
2196
+ id: wireId,
2197
+ subject: from,
2198
+ predicate: wire.kindIdentity,
2199
+ object: { ref: to },
2200
+ origin: 'declared',
2201
+ source: where,
2202
+ });
2203
+ }
2204
+ }
2205
+ }
2206
+ // ---- macro edges through ports (#268 phase 2, ADR 0124) ------------------
2207
+ //
2208
+ // A relationship authored BETWEEN two pattern instances is a macro-grain
2209
+ // fact: "System API serves Process API", one line. Where both patterns
2210
+ // declare a port for its kind, it is expanded to the canonical pair the
2211
+ // ports name - the source's `out` slot to the target's `in` slot - and the
2212
+ // macro edge itself SURVIVES, because it is what a collapsed view has to
2213
+ // draw. That is the whole inversion: the simple picture is authored and the
2214
+ // detailed one derived, rather than the other way about.
2215
+ if (patternInstances.length > 0) {
2216
+ const instanceByIdForPorts = new Map(patternInstances.map((entry) => [entry.instance, entry]));
2217
+ const kindOfSubjectForPorts = new Map();
2218
+ for (const claim of claims) {
2219
+ if (claim.predicate === 'yarramate/concept/kind' &&
2220
+ 'value' in claim.object) {
2221
+ kindOfSubjectForPorts.set(claim.subject, claim.object.value);
2222
+ }
2223
+ }
2224
+ const declaredIdsForPorts = new Set(subjects.map(({ id }) => id));
2225
+ const relationshipIdsForPorts = new Set(subjects.filter(({ type }) => type === 'relationship').map(({ id }) => id));
2226
+ const authoredPairs = new Set();
2227
+ const macroEdges = [];
2228
+ for (const claim of claims) {
2229
+ if (!relationshipIdsForPorts.has(claim.id) || !('ref' in claim.object)) {
2230
+ continue;
2231
+ }
2232
+ authoredPairs.add(`${claim.subject}\u0000${claim.predicate}\u0000${claim.object.ref}`);
2233
+ if (instanceByIdForPorts.has(claim.subject) &&
2234
+ instanceByIdForPorts.has(claim.object.ref)) {
2235
+ macroEdges.push(claim);
2236
+ }
2237
+ }
2238
+ for (const edge of macroEdges) {
2239
+ if (!('ref' in edge.object))
2240
+ continue;
2241
+ const source = instanceByIdForPorts.get(edge.subject);
2242
+ const target = instanceByIdForPorts.get(edge.object.ref);
2243
+ if (source === undefined || target === undefined)
2244
+ continue;
2245
+ const outPort = source.pattern.ports.get(edge.predicate);
2246
+ const inPort = target.pattern.ports.get(edge.predicate);
2247
+ // Only a kind BOTH patterns give a port is a macro edge. Anything else
2248
+ // between two instances is an ordinary relationship and is left alone:
2249
+ // groupings may legally relate, and a pattern that says nothing about a
2250
+ // kind has not claimed it.
2251
+ if (outPort === undefined || inPort === undefined)
2252
+ continue;
2253
+ const from = source.bindings.get(outPort.out);
2254
+ const to = target.bindings.get(inPort.in);
2255
+ if (from === undefined || to === undefined) {
2256
+ // The macro edge is a promise the expansion keeps, so an unbound
2257
+ // landing slot is a promise the model cannot cash. Reported against
2258
+ // the macro edge, which is the line someone would have to change.
2259
+ diagnostics.push({
2260
+ severity: 'error',
2261
+ code: 'YM421',
2262
+ message: `"${edge.id}" is a ${outPort.coreKind} between pattern instances, but ` +
2263
+ `${from === undefined ? `"${edge.subject}" binds no "${outPort.out}"` : `"${'ref' in edge.object ? edge.object.ref : ''}" binds no "${inPort.in}"`}` +
2264
+ `, so it has nowhere to land`,
2265
+ path: edge.source.path,
2266
+ pointer: edge.source.pointer,
2267
+ line: edge.source.line,
2268
+ column: edge.source.column,
2269
+ });
2270
+ continue;
2271
+ }
2272
+ // Legality cannot be settled when the pattern resolves, the way wiring's
2273
+ // is: the two ends belong to DIFFERENT patterns, and neither knows the
2274
+ // other's slot kinds. So the pair the ports actually name is judged here,
2275
+ // against the macro edge that asked for it. Without this the compiler
2276
+ // would emit a relationship the table forbids - the one thing `check`
2277
+ // exists to make impossible.
2278
+ const expandedKind = relationshipKindByIdentity.get(edge.predicate);
2279
+ const fromKind = kindOfSubjectForPorts.get(from);
2280
+ const toKind = kindOfSubjectForPorts.get(to);
2281
+ if (expandedKind !== undefined &&
2282
+ fromKind !== undefined &&
2283
+ toKind !== undefined) {
2284
+ const permitted = permittedBetween(fromKind, toKind);
2285
+ if (permitted !== undefined && !permitted.has(expandedKind.coreKind)) {
2286
+ diagnostics.push({
2287
+ severity: 'error',
2288
+ code: 'YM404',
2289
+ message: `Expanding "${edge.id}" through the ports lands ` +
2290
+ `"${expandedKind.coreKind}" from "${from}" to "${to}", which is ` +
2291
+ `not permitted between "${fromKind}" and "${toKind}"`,
2292
+ path: edge.source.path,
2293
+ pointer: edge.source.pointer,
2294
+ line: edge.source.line,
2295
+ column: edge.source.column,
2296
+ });
2297
+ continue;
2298
+ }
2299
+ }
2300
+ if (authoredPairs.has(`${from}\u0000${edge.predicate}\u0000${to}`)) {
2301
+ // Already said at the member grain. The macro edge and the authored
2302
+ // pair agree, which is exactly the correspondence a description used
2303
+ // to assert; nothing is minted and nothing is reported.
2304
+ continue;
2305
+ }
2306
+ const expandedId = `${edge.id}-expansion`;
2307
+ if (declaredIdsForPorts.has(expandedId)) {
2308
+ diagnostics.push({
2309
+ severity: 'error',
2310
+ code: 'YM420',
2311
+ message: `Expanding "${edge.id}" derives the id "${expandedId}", which is ` +
2312
+ `already a declared subject`,
2313
+ path: edge.source.path,
2314
+ pointer: edge.source.pointer,
2315
+ line: edge.source.line,
2316
+ column: edge.source.column,
2317
+ });
2318
+ continue;
2319
+ }
2320
+ declaredIdsForPorts.add(expandedId);
2321
+ subjects.push({ id: expandedId, type: 'relationship' });
2322
+ claims.push({
2323
+ id: expandedId,
2324
+ subject: from,
2325
+ predicate: edge.predicate,
2326
+ object: { ref: to },
2327
+ origin: 'declared',
2328
+ source: edge.source,
2329
+ });
2330
+ }
2331
+ }
1698
2332
  if (diagnostics.length > 0) {
1699
2333
  return diagnosticFailure(diagnostics);
1700
2334
  }
@@ -1745,6 +2379,18 @@ function compileWorkspaceResolved(parsed) {
1745
2379
  relationshipKindCoreAncestors: immutableMap([...relationshipKindByIdentity]
1746
2380
  .sort(([left], [right]) => left.localeCompare(right))
1747
2381
  .map(([identity, kind]) => [identity, kind.coreKind])),
2382
+ patternPortKinds: immutableMap([...patternsByKind]
2383
+ .sort(([left], [right]) => left.localeCompare(right))
2384
+ .flatMap(([identity, pattern]) => pattern.ports.size === 0
2385
+ ? []
2386
+ : [
2387
+ [
2388
+ identity,
2389
+ Object.freeze([...pattern.ports.values()]
2390
+ .map(({ coreKind }) => coreKind)
2391
+ .sort()),
2392
+ ],
2393
+ ])),
1748
2394
  permittedRelationshipKinds: (fromKindIdentity, toKindIdentity) => {
1749
2395
  const from = conceptKindByIdentity.get(fromKindIdentity);
1750
2396
  const to = conceptKindByIdentity.get(toKindIdentity);