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.
- package/README.md +13 -4
- package/catalogues/core-enrichment.yaml +79 -55
- package/dist/adapter-mapping.js +6 -1
- package/dist/adapters/likec4-cli.js +36 -7
- package/dist/adapters/visual/client.d.ts +2 -2
- package/dist/adapters/visual/client.js +79 -10
- package/dist/adapters/visual/protocol-contract.d.ts +124 -10
- package/dist/adapters/visual/protocol-contract.js +12 -1
- package/dist/adapters/visual/protocol.d.ts +63 -0
- package/dist/adapters/visual/protocol.js +96 -5
- package/dist/adapters/visual/session-server.d.ts +1 -6
- package/dist/adapters/visual/session-server.js +243 -143
- package/dist/adapters/visual/session-store.d.ts +9 -0
- package/dist/adapters/visual/session-store.js +14 -5
- package/dist/adapters/visual/view-identity.d.ts +135 -0
- package/dist/adapters/visual/view-identity.js +200 -0
- package/dist/adapters/visual/wire.d.ts +19 -4
- package/dist/adapters/visual/workspace-model.d.ts +116 -0
- package/dist/adapters/visual/workspace-model.js +212 -0
- package/dist/adapters/visual-cli.d.ts +1 -1
- package/dist/adapters/visual-cli.js +30 -21
- package/dist/apply-cli.d.ts +2 -0
- package/dist/apply-cli.js +83 -0
- package/dist/apply-command.d.ts +84 -3
- package/dist/apply-command.js +218 -109
- package/dist/archimate-relationships.generated.d.ts +22 -0
- package/dist/archimate-relationships.generated.js +156 -0
- package/dist/ask-command.js +18 -6
- package/dist/attestation-staleness.js +2 -2
- package/dist/changed.js +1 -1
- package/dist/check-command.js +13 -5
- package/dist/cli-support.d.ts +1 -1
- package/dist/cli-support.js +1 -1
- package/dist/cli.js +1 -1
- package/dist/compiler.d.ts +31 -3
- package/dist/compiler.js +321 -81
- package/dist/concept-drafting.d.ts +27 -0
- package/dist/concept-drafting.js +75 -0
- package/dist/core-contract.js +6 -1
- package/dist/deletion-drafting.d.ts +52 -0
- package/dist/deletion-drafting.js +100 -0
- package/dist/evidence.js +6 -1
- package/dist/graph-projection.d.ts +14 -0
- package/dist/graph-projection.js +4 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +8 -1
- package/dist/interrogate-command.js +63 -17
- package/dist/kind-label.d.ts +4 -3
- package/dist/kind-label.js +4 -3
- package/dist/nesting.d.ts +15 -0
- package/dist/nesting.js +2 -0
- package/dist/notation/archimate.d.ts +1 -1
- package/dist/notation/archimate.js +108 -9
- package/dist/profile.d.ts +9 -12
- package/dist/profile.js +17 -26
- package/dist/projection.d.ts +62 -3
- package/dist/projection.js +127 -43
- package/dist/relationship-drafting.d.ts +51 -0
- package/dist/relationship-drafting.js +92 -0
- package/dist/relationship-matrix.d.ts +14 -0
- package/dist/relationship-matrix.js +74 -0
- package/dist/shipped-profile.d.ts +1 -1
- package/dist/shipped-profile.js +19 -5
- package/dist/source-store.d.ts +95 -0
- package/dist/source-store.js +170 -0
- package/dist/subject-references.js +1 -1
- package/dist/visual-app/assets/index-BLcpI8WN.js +394 -0
- package/dist/visual-app/assets/index-DLutIWES.css +1 -0
- package/dist/visual-app/index.html +2 -2
- package/dist/visual-app-lib/editor.d.ts +1 -0
- package/dist/visual-app-lib/editor.js +90968 -0
- package/dist/visual-app-lib/styles.css +2 -0
- package/dist/visual-app-lib/types/adapter-mapping.d.ts +40 -0
- package/dist/visual-app-lib/types/adapters/visual/protocol-contract.d.ts +455 -0
- package/dist/visual-app-lib/types/adapters/visual/view-identity.d.ts +135 -0
- package/dist/visual-app-lib/types/adapters/visual/wire.d.ts +141 -0
- package/dist/visual-app-lib/types/adapters/visual/workspace-model.d.ts +116 -0
- package/dist/visual-app-lib/types/apply-command.d.ts +93 -0
- package/dist/visual-app-lib/types/archimate-relationships.generated.d.ts +22 -0
- package/dist/visual-app-lib/types/brief.d.ts +5 -0
- package/dist/visual-app-lib/types/compiler.d.ts +155 -0
- package/dist/visual-app-lib/types/concept-drafting.d.ts +27 -0
- package/dist/visual-app-lib/types/deletion-drafting.d.ts +52 -0
- package/dist/visual-app-lib/types/evidence.d.ts +62 -0
- package/dist/visual-app-lib/types/graph-claims.d.ts +18 -0
- package/dist/visual-app-lib/types/graph-projection.d.ts +77 -0
- package/dist/visual-app-lib/types/kind-label.d.ts +9 -0
- package/dist/visual-app-lib/types/nesting.d.ts +15 -0
- package/dist/visual-app-lib/types/notation/archimate.d.ts +71 -0
- package/dist/visual-app-lib/types/operations.d.ts +157 -0
- package/dist/visual-app-lib/types/profile.d.ts +47 -0
- package/dist/visual-app-lib/types/projection.d.ts +113 -0
- package/dist/visual-app-lib/types/relationship-drafting.d.ts +51 -0
- package/dist/visual-app-lib/types/relationship-matrix.d.ts +14 -0
- package/dist/visual-app-lib/types/shipped-profile.d.ts +2 -0
- package/dist/visual-app-lib/types/source-document.d.ts +36 -0
- package/dist/visual-app-lib/types/source-store.d.ts +95 -0
- package/dist/visual-app-lib/types/subject-references.d.ts +92 -0
- package/dist/visual-app-lib/types/visual-app/App.d.ts +15 -0
- package/dist/visual-app-lib/types/visual-app/badges.d.ts +7 -0
- package/dist/visual-app-lib/types/visual-app/changeset-tray.d.ts +106 -0
- package/dist/visual-app-lib/types/visual-app/confirm-dialog.d.ts +14 -0
- package/dist/visual-app-lib/types/visual-app/connection-panel.d.ts +23 -0
- package/dist/visual-app-lib/types/visual-app/context-menu-model.d.ts +187 -0
- package/dist/visual-app-lib/types/visual-app/context-menu.d.ts +24 -0
- package/dist/visual-app-lib/types/visual-app/describe-query.d.ts +9 -0
- package/dist/visual-app-lib/types/visual-app/editor-host.d.ts +70 -0
- package/dist/visual-app-lib/types/visual-app/faults.d.ts +26 -0
- package/dist/visual-app-lib/types/visual-app/graph-canvas.d.ts +73 -0
- package/dist/visual-app-lib/types/visual-app/kind-icons.d.ts +3 -0
- package/dist/visual-app-lib/types/visual-app/local-host.d.ts +38 -0
- package/dist/visual-app-lib/types/visual-app/mount.d.ts +56 -0
- package/dist/visual-app-lib/types/visual-app/prompt-dialog.d.ts +24 -0
- package/dist/visual-app-lib/types/visual-app/query-fields.d.ts +69 -0
- package/dist/visual-app-lib/types/visual-app/query-panel.d.ts +142 -0
- package/dist/visual-app-lib/types/visual-app/quick-filter.d.ts +11 -0
- package/dist/visual-app-lib/types/visual-app/relationship-kind-options.d.ts +50 -0
- package/dist/visual-app-lib/types/visual-app/save-view.d.ts +86 -0
- package/dist/visual-app-lib/types/visual-app/section-stack.d.ts +59 -0
- package/dist/visual-app-lib/types/visual-app/session-client.d.ts +38 -0
- package/dist/visual-app-lib/types/visual-app/state.d.ts +328 -0
- package/dist/visual-app-lib/types/visual-app/subject-draft-panel.d.ts +22 -0
- package/dist/visual-app-lib/types/visual-app/subject-form.d.ts +42 -0
- package/dist/visual-app-lib/types/visual-app/view-tree-model.d.ts +122 -0
- package/dist/visual-app-lib/types/visual-app/view-tree.d.ts +32 -0
- package/dist/visual-app-lib/types/visual-app/workspace-state.d.ts +268 -0
- package/dist/visual-app-lib/types/workspace.d.ts +29 -0
- package/dist/workspace.js +7 -1
- package/docs/CONSUMING-YARRAMATE.md +51 -2
- package/package.json +9 -2
- package/schema/yarramate-adapter-mapping.schema.json +1 -1
- package/schema/yarramate-ask-result.schema.json +24 -3
- package/schema/yarramate-check-result.schema.json +9 -0
- package/schema/yarramate-diagnostic-result.schema.json +9 -0
- package/schema/yarramate-document.schema.json +6 -0
- package/schema/yarramate-evidence-report.schema.json +1 -1
- package/schema/yarramate-evidence.schema.json +1 -1
- package/schema/yarramate-graph-v2.schema.json +34 -9
- package/schema/yarramate-likec4-generated-project-v2.schema.json +1 -1
- package/schema/yarramate-likec4-generated-project.schema.json +1 -1
- package/schema/yarramate-likec4-project.schema.json +1 -1
- package/schema/yarramate-projection-result.schema.json +3 -7
- package/schema/yarramate-projection.schema.json +16 -16
- package/schema/yarramate-reconciliation-report.schema.json +2 -2
- package/schema/yarramate-rtm.schema.json +1 -1
- package/schema/yarramate-state-comparison.schema.json +1 -1
- package/schema/yarramate-visual-diagnostic-result.schema.json +9 -0
- package/schema/yarramate-visual-event.schema.json +40 -41
- package/schema/yarramate-visual-graph.schema.json +8 -0
- package/schema/yarramate-visual-handoff.schema.json +3 -3
- package/schema/yarramate-visual-session-descriptor.schema.json +6 -6
- package/schema/yarramate-visual-session-started.schema.json +10 -9
- package/schema/yarramate-visual-status.schema.json +3 -3
- package/skills/yarramate-architecture/SKILL.md +3 -3
- package/skills/yarramate-architecture/references/modelling-patterns.md +23 -44
- package/skills/yarramate-architecture/references/native-authoring.md +32 -49
- package/skills/yarramate-architecture/references/visual-conversations.md +13 -10
- package/dist/visual-app/assets/index-CRd1Khma.js +0 -354
- package/dist/visual-app/assets/index-tZSHlU-a.css +0 -1
package/dist/compiler.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import
|
|
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
|
|
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
|
|
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-${
|
|
42
|
-
const distinctFromClaimId = (subject, other) => `${subject}~distinct-from-${
|
|
43
|
-
const supersedesClaimId = (subject, predecessor) => `${subject}~supersedes-${
|
|
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
|
|
46
|
-
// Candidate order is the
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
609
|
+
concept.id,
|
|
551
610
|
{
|
|
552
611
|
concept,
|
|
553
612
|
profile: value.profile,
|
|
554
613
|
document: value.id,
|
|
555
614
|
},
|
|
556
615
|
])));
|
|
557
|
-
|
|
558
|
-
|
|
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) =>
|
|
561
|
-
...value.concepts.map((concept) =>
|
|
562
|
-
...value.relationships.map((relationship) =>
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 ===
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
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
|
|
1068
|
-
const
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
const
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
const
|
|
1082
|
-
const
|
|
1083
|
-
const
|
|
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
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
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;
|