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.
- package/catalogues/core-enrichment.yaml +237 -2
- package/dist/adapters/visual/session-server.js +16 -1
- package/dist/adapters/visual/view-identity.d.ts +1 -1
- package/dist/adapters/visual/view-identity.js +61 -3
- package/dist/adapters/visual/wire.d.ts +30 -0
- package/dist/adapters/visual/workspace-model.d.ts +35 -3
- package/dist/adapters/visual/workspace-model.js +62 -2
- package/dist/ask-command.js +37 -14
- package/dist/cli-support.d.ts +1 -1
- package/dist/cli-support.js +1 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +26 -5
- package/dist/compiler.d.ts +13 -1
- package/dist/compiler.js +646 -0
- package/dist/concept-drafting.d.ts +13 -2
- package/dist/concept-drafting.js +15 -3
- package/dist/design-command.js +30 -4
- package/dist/evidence.js +25 -0
- package/dist/graph-projection.d.ts +10 -0
- package/dist/graph-projection.js +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/interrogate-command.d.ts +37 -1
- package/dist/interrogate-command.js +93 -42
- package/dist/interrogation-entry.d.ts +1 -1
- package/dist/layout-direction.d.ts +14 -0
- package/dist/layout-direction.js +5 -0
- package/dist/projection.d.ts +22 -6
- package/dist/projection.js +23 -2
- package/dist/relationship-drafting.d.ts +21 -2
- package/dist/relationship-drafting.js +49 -4
- package/dist/subject-references.js +5 -0
- package/dist/visual-app/assets/index-Rkq6smL2.css +1 -0
- package/dist/visual-app/assets/index-yCHtmQUH.js +394 -0
- package/dist/visual-app/index.html +2 -2
- package/dist/visual-app-lib/editor.js +29639 -27514
- package/dist/visual-app-lib/styles.css +1 -1
- package/dist/visual-app-lib/types/adapters/visual/view-identity.d.ts +1 -1
- package/dist/visual-app-lib/types/adapters/visual/wire.d.ts +30 -0
- package/dist/visual-app-lib/types/adapters/visual/workspace-model.d.ts +35 -3
- package/dist/visual-app-lib/types/compiler.d.ts +13 -1
- package/dist/visual-app-lib/types/concept-drafting.d.ts +13 -2
- package/dist/visual-app-lib/types/graph-projection.d.ts +10 -0
- package/dist/visual-app-lib/types/interrogate-command.d.ts +207 -0
- package/dist/visual-app-lib/types/layout-direction.d.ts +14 -0
- package/dist/visual-app-lib/types/projection.d.ts +22 -6
- package/dist/visual-app-lib/types/relationship-drafting.d.ts +21 -2
- package/dist/visual-app-lib/types/subject-identity.d.ts +34 -0
- package/dist/visual-app-lib/types/visual-app/App.d.ts +27 -2
- package/dist/visual-app-lib/types/visual-app/badges.d.ts +1 -0
- package/dist/visual-app-lib/types/visual-app/connection-panel.d.ts +9 -1
- package/dist/visual-app-lib/types/visual-app/context-menu-model.d.ts +15 -6
- package/dist/visual-app-lib/types/visual-app/graph-canvas.d.ts +77 -6
- package/dist/visual-app-lib/types/visual-app/kind-palette.d.ts +37 -0
- package/dist/visual-app-lib/types/visual-app/local-host.d.ts +42 -0
- package/dist/visual-app-lib/types/visual-app/mount.d.ts +73 -2
- package/dist/visual-app-lib/types/visual-app/open-questions.d.ts +15 -0
- package/dist/visual-app-lib/types/visual-app/query-fields.d.ts +3 -2
- package/dist/visual-app-lib/types/visual-app/query-panel.d.ts +8 -1
- package/dist/visual-app-lib/types/visual-app/save-view.d.ts +11 -5
- package/dist/visual-app-lib/types/visual-app/shipped-catalogue.d.ts +10 -0
- package/dist/visual-app-lib/types/visual-app/state.d.ts +15 -4
- package/dist/visual-app-lib/types/visual-app/subject-draft-panel.d.ts +13 -1
- package/dist/visual-app-lib/types/visual-app/subject-filter.d.ts +35 -0
- package/dist/visual-app-lib/types/visual-app/subject-form.d.ts +8 -0
- package/dist/visual-app-lib/types/visual-app/view-tree-model.d.ts +42 -9
- package/dist/visual-app-lib/types/visual-app/view-tree.d.ts +11 -2
- package/dist/visual-app-lib/types/visual-app/workspace-state.d.ts +81 -11
- package/dist/visual-app-lib/types/workspace.d.ts +2 -0
- package/dist/workspace.d.ts +2 -0
- package/dist/workspace.js +1 -0
- package/docs/CONSUMING-YARRAMATE.md +81 -0
- package/package.json +2 -1
- package/schema/yarramate-design-step.schema.json +26 -0
- package/schema/yarramate-document.schema.json +14 -0
- package/schema/yarramate-interrogation-report.schema.json +31 -0
- package/schema/yarramate-pattern.schema.json +133 -0
- package/schema/yarramate-projection.schema.json +9 -0
- package/schema/yarramate-question-catalogue.schema.json +34 -0
- package/schema/yarramate-visual-graph.schema.json +9 -0
- package/schema/yarramate-workspace.schema.json +4 -0
- package/skills/yarramate-architecture/SKILL.md +5 -0
- package/dist/visual-app/assets/index-CcmfL3oY.js +0 -394
- 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);
|