yarramate 1.19.0 → 1.20.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/dist/adapters/visual/protocol-contract.d.ts +13 -0
- package/dist/adapters/visual/session-server.js +33 -6
- package/dist/adapters/visual/wire.d.ts +23 -0
- package/dist/adapters/visual/workspace-model.js +11 -0
- package/dist/adapters/visual-graph-entry.d.ts +1 -0
- package/dist/adapters/visual-graph-entry.js +6 -0
- package/dist/compiler.d.ts +21 -0
- package/dist/compiler.js +99 -48
- package/dist/fold-tree.d.ts +169 -0
- package/dist/fold-tree.js +295 -0
- package/dist/projection.d.ts +16 -0
- package/dist/projection.js +10 -0
- package/dist/schema-validators.generated.js +44 -31
- package/dist/visual-app/assets/index-DF3anVfS.css +1 -0
- package/dist/visual-app/assets/index-EegLnUfX.js +394 -0
- package/dist/visual-app/index.html +2 -2
- package/dist/visual-app-lib/editor.js +27427 -26907
- package/dist/visual-app-lib/styles.css +1 -1
- package/dist/visual-app-lib/types/adapters/visual/protocol-contract.d.ts +13 -0
- package/dist/visual-app-lib/types/adapters/visual/wire.d.ts +23 -0
- package/dist/visual-app-lib/types/compiler.d.ts +21 -0
- package/dist/visual-app-lib/types/fold-tree.d.ts +169 -0
- package/dist/visual-app-lib/types/projection.d.ts +16 -0
- package/dist/visual-app-lib/types/visual-app/badges.d.ts +1 -0
- package/dist/visual-app-lib/types/visual-app/context-menu-model.d.ts +42 -0
- package/dist/visual-app-lib/types/visual-app/graph-canvas.d.ts +37 -3
- package/dist/visual-app-lib/types/visual-app/slots-model.d.ts +58 -0
- package/dist/visual-app-lib/types/visual-app/view-tree-model.d.ts +18 -1
- package/dist/visual-app-lib/types/visual-app/view-tree.d.ts +4 -1
- package/dist/visual-app-lib/types/visual-app/workspace-state.d.ts +34 -0
- package/docs/CONSUMING-YARRAMATE.md +23 -1
- package/docs/NATIVE-DOCUMENT.md +1 -1
- package/package.json +1 -1
- package/schema/yarramate-projection.schema.json +4 -0
- package/schema/yarramate-visual-layout.schema.json +16 -0
- package/dist/visual-app/assets/index-Bd-3k5UB.css +0 -1
- package/dist/visual-app/assets/index-CvTDYpur.js +0 -394
|
@@ -250,6 +250,19 @@ export interface VisualChangesetCommitPayload {
|
|
|
250
250
|
export interface VisualLayoutSavePayload {
|
|
251
251
|
readonly projectionId: string;
|
|
252
252
|
readonly positions: VisualLayoutPositions;
|
|
253
|
+
/**
|
|
254
|
+
* What this view folds, saved beside the positions in ONE document (#473).
|
|
255
|
+
*
|
|
256
|
+
* Full state every time, never a patch. A half-applied fold state draws a
|
|
257
|
+
* box whose contents are somewhere else on the canvas, and the sidecar is
|
|
258
|
+
* written by a browser that may have been reloaded between any two saves.
|
|
259
|
+
*
|
|
260
|
+
* `unfolded` exists because the view's own `presentation.fold` is a default,
|
|
261
|
+
* not a rule: a reader who opened a box must not have it close again when the
|
|
262
|
+
* default is read back.
|
|
263
|
+
*/
|
|
264
|
+
readonly folded?: readonly string[];
|
|
265
|
+
readonly unfolded?: readonly string[];
|
|
253
266
|
}
|
|
254
267
|
/**
|
|
255
268
|
* Terminal event payload. Every reason is the runtime's to choose: only it
|
|
@@ -456,14 +456,15 @@ export const startVisualServer = async (options) => {
|
|
|
456
456
|
// like a broken saved view above — presentation state must never fail a
|
|
457
457
|
// session.
|
|
458
458
|
const layoutDir = resolve(options.cwd, ".yarramate/visual-layout");
|
|
459
|
-
const layouts = (() => {
|
|
460
|
-
const
|
|
459
|
+
const { layouts, folds } = (() => {
|
|
460
|
+
const layouts = {};
|
|
461
|
+
const folds = {};
|
|
461
462
|
let entries;
|
|
462
463
|
try {
|
|
463
464
|
entries = readdirSync(layoutDir);
|
|
464
465
|
}
|
|
465
466
|
catch {
|
|
466
|
-
return
|
|
467
|
+
return { layouts, folds };
|
|
467
468
|
}
|
|
468
469
|
for (const entry of entries) {
|
|
469
470
|
if (extname(entry) !== ".yaml" && extname(entry) !== ".yml")
|
|
@@ -474,13 +475,22 @@ export const startVisualServer = async (options) => {
|
|
|
474
475
|
if (!validateVisualLayout(parsed))
|
|
475
476
|
continue;
|
|
476
477
|
const sidecar = parsed;
|
|
477
|
-
|
|
478
|
+
layouts[sidecar.projectionId] = sidecar.positions;
|
|
479
|
+
// A sidecar written before #473 has neither list, and says nothing
|
|
480
|
+
// about folding rather than saying "fold nothing" - the view's own
|
|
481
|
+
// default decides for it. Only a sidecar that STATES a fold overrides.
|
|
482
|
+
if (sidecar.folded !== undefined || sidecar.unfolded !== undefined) {
|
|
483
|
+
folds[sidecar.projectionId] = {
|
|
484
|
+
folded: [...(sidecar.folded ?? [])],
|
|
485
|
+
unfolded: [...(sidecar.unfolded ?? [])],
|
|
486
|
+
};
|
|
487
|
+
}
|
|
478
488
|
}
|
|
479
489
|
catch {
|
|
480
490
|
// Skipped sidecar: presentation state must never fail a session.
|
|
481
491
|
}
|
|
482
492
|
}
|
|
483
|
-
return
|
|
493
|
+
return { layouts, folds };
|
|
484
494
|
})();
|
|
485
495
|
// `request.initialModel.graph` is the caller's compile (`buildVisualModelGraph`,
|
|
486
496
|
// before invoking `yarramate-visual start`) and is only the fallback below:
|
|
@@ -494,6 +504,7 @@ export const startVisualServer = async (options) => {
|
|
|
494
504
|
documents: [],
|
|
495
505
|
vocabulary: { conceptKinds: [], relationshipKinds: [] },
|
|
496
506
|
layouts,
|
|
507
|
+
...(Object.keys(folds).length === 0 ? {} : { folds }),
|
|
497
508
|
sourceDigests: request.initialModel.sourceDigests,
|
|
498
509
|
// The request's model has no projections in it - `visual-model/v1` carries
|
|
499
510
|
// a graph, not a workspace - so the fallback states nothing rather than
|
|
@@ -1407,7 +1418,7 @@ export const startVisualServer = async (options) => {
|
|
|
1407
1418
|
// never `git commit`ed. It never asks the agent anything, so it is
|
|
1408
1419
|
// answered here directly rather than through the pending queue a
|
|
1409
1420
|
// poll would drain.
|
|
1410
|
-
const { projectionId, positions } = event.payload;
|
|
1421
|
+
const { projectionId, positions, folded, unfolded } = event.payload;
|
|
1411
1422
|
if (!views.some((view) => view.id === projectionId)) {
|
|
1412
1423
|
sendFrame(socket, {
|
|
1413
1424
|
kind: "layout-save-result",
|
|
@@ -1424,10 +1435,26 @@ export const startVisualServer = async (options) => {
|
|
|
1424
1435
|
format: "yarramate/visual-layout/v1",
|
|
1425
1436
|
projectionId,
|
|
1426
1437
|
positions,
|
|
1438
|
+
// With the positions, in one document, in full (#473). Same rule
|
|
1439
|
+
// the local host follows, and it has to be the same rule: a
|
|
1440
|
+
// sidecar written by one host is read by the other.
|
|
1441
|
+
...(folded === undefined ? {} : { folded }),
|
|
1442
|
+
...(unfolded === undefined ? {} : { unfolded }),
|
|
1427
1443
|
}), "utf8");
|
|
1428
1444
|
rendered = {
|
|
1429
1445
|
...rendered,
|
|
1430
1446
|
layouts: { ...rendered.layouts, [projectionId]: positions },
|
|
1447
|
+
...(folded === undefined && unfolded === undefined
|
|
1448
|
+
? {}
|
|
1449
|
+
: {
|
|
1450
|
+
folds: {
|
|
1451
|
+
...rendered.folds,
|
|
1452
|
+
[projectionId]: {
|
|
1453
|
+
folded: folded ?? [],
|
|
1454
|
+
unfolded: unfolded ?? [],
|
|
1455
|
+
},
|
|
1456
|
+
},
|
|
1457
|
+
}),
|
|
1431
1458
|
};
|
|
1432
1459
|
sendFrame(socket, {
|
|
1433
1460
|
kind: "layout-save-result",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CanvasGraph } from '../../graph-projection.js';
|
|
2
|
+
import type { PatternMembership, PatternVacancy } from '../../compiler.js';
|
|
2
3
|
import type { VISUAL_PROTOCOL_VERSION, VisualApplyResultPayload, VisualAuthority, VisualBrowserInput, VisualCapabilities, VisualChoicePresentPayload, VisualDiagnostic, VisualFilterResultPayload, VisualFreezeReason, VisualKindOption, VisualLayoutPositions, VisualLayoutSaveResultPayload, VisualResponse, VisualTerminationReason, VisualViewSummary } from './protocol-contract.js';
|
|
3
4
|
/**
|
|
4
5
|
* Transport shapes the session server and the browser application both speak.
|
|
@@ -55,6 +56,28 @@ export interface VisualRenderedModel {
|
|
|
55
56
|
readonly layouts: {
|
|
56
57
|
readonly [projectionId: string]: VisualLayoutPositions;
|
|
57
58
|
};
|
|
59
|
+
/**
|
|
60
|
+
* What each view folds, keyed by projection id (#473). A SIBLING of
|
|
61
|
+
* `layouts` rather than a field inside its entries: a layout entry is
|
|
62
|
+
* positions, one shape a host may already be reading, and widening it would
|
|
63
|
+
* make every reader of `layouts[id]` handle a case that did not exist.
|
|
64
|
+
*/
|
|
65
|
+
readonly folds?: {
|
|
66
|
+
readonly [projectionId: string]: {
|
|
67
|
+
readonly folded: readonly string[];
|
|
68
|
+
readonly unfolded: readonly string[];
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Which subject fills which slot of which instance (ADR 0131), and which
|
|
73
|
+
* slots nothing fills (#447), forwarded so the browser can draw containment
|
|
74
|
+
* and answer "what is inside this box" without a second round trip.
|
|
75
|
+
*
|
|
76
|
+
* Optional: a host that never folds and never shows slots need not supply
|
|
77
|
+
* them, and a frame from before #473 has neither.
|
|
78
|
+
*/
|
|
79
|
+
readonly memberships?: readonly PatternMembership[];
|
|
80
|
+
readonly vacancies?: readonly PatternVacancy[];
|
|
58
81
|
/**
|
|
59
82
|
* The sha256 of every workspace source this graph was compiled from, keyed by
|
|
60
83
|
* manifest-relative path — the same map `visual-model/v1` already requires of
|
|
@@ -132,6 +132,17 @@ export const renderedWorkspaceOf = (compiled, views, metadata, catalogue, dismis
|
|
|
132
132
|
relationshipKinds: kindOptionsOf(compiled.profileContext.relationshipKindLineages),
|
|
133
133
|
},
|
|
134
134
|
...(interrogation === undefined ? {} : { interrogation }),
|
|
135
|
+
// Containment context, forwarded rather than re-derived (#473). Both
|
|
136
|
+
// hosts build the model here, so local-host and session-server cannot
|
|
137
|
+
// disagree about what is inside a box - which is the shape of defect
|
|
138
|
+
// that made `patterns` ship non-functional in 1.4.0, ten source lists
|
|
139
|
+
// each dropping the same thing.
|
|
140
|
+
...(compiled.patternMemberships === undefined
|
|
141
|
+
? {}
|
|
142
|
+
: { memberships: compiled.patternMemberships }),
|
|
143
|
+
...(compiled.patternVacancies === undefined
|
|
144
|
+
? {}
|
|
145
|
+
: { vacancies: compiled.patternVacancies }),
|
|
135
146
|
},
|
|
136
147
|
views: refreshedViews,
|
|
137
148
|
};
|
|
@@ -1 +1,2 @@
|
|
|
1
1
|
export { projectGraphForCanvas, type CanvasGraph, type CanvasNode, type CanvasEdge, } from '../graph-projection.js';
|
|
2
|
+
export { foldTree, foldGraph, nestingTree, liftedEdgeId, NESTING_KIND_IDS, type FoldInput, type FoldNode, type FoldEdge, type FoldMembership, type FoldTree, type LiftedEdge, type NestingConflict, type SlotWiring, } from '../fold-tree.js';
|
|
@@ -1 +1,7 @@
|
|
|
1
1
|
export { projectGraphForCanvas, } from '../graph-projection.js';
|
|
2
|
+
// What contains what, and what a fold draws instead (#473). Published here
|
|
3
|
+
// rather than only on the canvas because a host that never renders still has to
|
|
4
|
+
// answer both: an interview counts open questions per box, a report says what
|
|
5
|
+
// an application is made of. `fold-tree.ts` imports nothing, so this subpath
|
|
6
|
+
// stays runtime-neutral.
|
|
7
|
+
export { foldTree, foldGraph, nestingTree, liftedEdgeId, NESTING_KIND_IDS, } from '../fold-tree.js';
|
package/dist/compiler.d.ts
CHANGED
|
@@ -108,6 +108,27 @@ export interface PatternMembership {
|
|
|
108
108
|
readonly slot: string;
|
|
109
109
|
readonly instance: string;
|
|
110
110
|
readonly pattern: string;
|
|
111
|
+
/**
|
|
112
|
+
* How the pattern's WIRING relates this slot to the instance (#473).
|
|
113
|
+
*
|
|
114
|
+
* - `owned` — a wire runs `self -> slot`. The instance holds the member out:
|
|
115
|
+
* it is a part, and a view that folds instances may draw it inside.
|
|
116
|
+
* - `context` — a wire runs `slot -> self`. The member acts on the instance
|
|
117
|
+
* rather than belonging to it: the upstream API it calls, the plane it runs
|
|
118
|
+
* on. Folding these would swallow half the landscape into whichever box
|
|
119
|
+
* happened to name it.
|
|
120
|
+
* - `unwired` — the pattern declares the slot and wires nothing through it.
|
|
121
|
+
* Still a part; nothing about containment changes.
|
|
122
|
+
*
|
|
123
|
+
* A slot with wires in BOTH directions is `owned`: the instance holding
|
|
124
|
+
* something out is the stronger statement, and it is what a reader means by
|
|
125
|
+
* the box.
|
|
126
|
+
*
|
|
127
|
+
* Optional, so no existing reader breaks. `yarramate/graph/v2` is unchanged:
|
|
128
|
+
* this is compile CONTEXT like the rest of membership (ADR 0131), never a
|
|
129
|
+
* claim an author could have written.
|
|
130
|
+
*/
|
|
131
|
+
readonly wiring?: 'owned' | 'context' | 'unwired';
|
|
111
132
|
}
|
|
112
133
|
/**
|
|
113
134
|
* One slot of one pattern instance that nothing is bound into (#447): the
|
package/dist/compiler.js
CHANGED
|
@@ -6,6 +6,24 @@ import { ATTESTATION_PREDICATE_PREFIX, attestationClaimValue } from './graph-cla
|
|
|
6
6
|
import { shippedPolicyIdentity, shippedPolicySource, } from './shipped-profile.js';
|
|
7
7
|
import { validateDocument, validateProfile, validatePattern } from './schema-validation.js';
|
|
8
8
|
const coreProfile = 'yarramate/core@0.1';
|
|
9
|
+
/**
|
|
10
|
+
* Which way the pattern's wiring runs between an instance and one of its slots
|
|
11
|
+
* (#473). Read from the pattern rather than from the graph, because it is a
|
|
12
|
+
* fact about the SHAPE and holds whether or not the slot is bound.
|
|
13
|
+
*/
|
|
14
|
+
const slotWiringOf = (pattern, slot) => {
|
|
15
|
+
let held = false;
|
|
16
|
+
let acts = false;
|
|
17
|
+
for (const wire of pattern.wiring) {
|
|
18
|
+
if (wire.from === 'self' && wire.to === slot)
|
|
19
|
+
held = true;
|
|
20
|
+
if (wire.from === slot && wire.to === 'self')
|
|
21
|
+
acts = true;
|
|
22
|
+
}
|
|
23
|
+
// Both directions is `owned`: holding something out is the stronger claim,
|
|
24
|
+
// and it is what a reader means by drawing the box around it.
|
|
25
|
+
return held ? 'owned' : acts ? 'context' : 'unwired';
|
|
26
|
+
};
|
|
9
27
|
const immutableMap = (entries) => {
|
|
10
28
|
const backing = new Map(entries);
|
|
11
29
|
const facade = {
|
|
@@ -428,63 +446,95 @@ function compileWorkspaceResolved(parsed) {
|
|
|
428
446
|
continue;
|
|
429
447
|
}
|
|
430
448
|
const resolvedConceptKinds = new Map(parentProfile.conceptKinds);
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const position = positionFor(['conceptKinds', index, 'parent']);
|
|
448
|
-
profileDiagnostics.push({
|
|
449
|
-
severity: 'error',
|
|
450
|
-
code: 'YM407',
|
|
451
|
-
message: `Concept parent "${kind.parent}" is not available`,
|
|
452
|
-
path: input.path,
|
|
453
|
-
pointer: `/conceptKinds/${index}/parent`,
|
|
454
|
-
line: position.line,
|
|
455
|
-
column: position.col,
|
|
456
|
-
});
|
|
457
|
-
continue;
|
|
458
|
-
}
|
|
459
|
-
// OntoClean: an anti-rigid kind cannot subsume a rigid one. The
|
|
460
|
-
// parent's lineage is the full ancestor chain, and subsumption is
|
|
461
|
-
// transitive, so an unannotated kind sitting in between does not
|
|
462
|
-
// launder the violation (ADR 0078).
|
|
463
|
-
if (kind.rigidity === 'rigid') {
|
|
464
|
-
const antiRigidAncestor = parent.lineage.find((ancestor) => conceptKindByIdentity.get(ancestor)?.rigidity === 'anti-rigid');
|
|
465
|
-
if (antiRigidAncestor !== undefined) {
|
|
466
|
-
const position = positionFor(['conceptKinds', index, 'rigidity']);
|
|
449
|
+
// A profile is a SET of kind declarations, so a kind whose parent is
|
|
450
|
+
// declared below it resolves like any other (#470). This used to be one
|
|
451
|
+
// pass in declaration order, which made a forward reference
|
|
452
|
+
// indistinguishable from a parent that does not exist - and `YM407` says
|
|
453
|
+
// "not available", so an author went looking for a missing kind while the
|
|
454
|
+
// file in front of them already declared it three lines down.
|
|
455
|
+
//
|
|
456
|
+
// Rounds until one adds nothing. Everything that compiled before compiles
|
|
457
|
+
// now and resolves on the first round, because parent-first was the only
|
|
458
|
+
// order that worked.
|
|
459
|
+
let pendingKinds = [...value.conceptKinds.entries()];
|
|
460
|
+
for (;;) {
|
|
461
|
+
const deferred = [];
|
|
462
|
+
for (const [index, kind] of pendingKinds) {
|
|
463
|
+
if (resolvedConceptKinds.has(kind.id)) {
|
|
464
|
+
const position = positionFor(['conceptKinds', index, 'id']);
|
|
467
465
|
profileDiagnostics.push({
|
|
468
466
|
severity: 'error',
|
|
469
|
-
code: '
|
|
470
|
-
message: `
|
|
467
|
+
code: 'YM409',
|
|
468
|
+
message: `Concept kind "${kind.id}" conflicts with an inherited kind`,
|
|
471
469
|
path: input.path,
|
|
472
|
-
pointer: `/conceptKinds/${index}/
|
|
470
|
+
pointer: `/conceptKinds/${index}/id`,
|
|
473
471
|
line: position.line,
|
|
474
472
|
column: position.col,
|
|
475
473
|
});
|
|
476
474
|
continue;
|
|
477
475
|
}
|
|
476
|
+
const parent = conceptKindByIdentity.get(kind.parent);
|
|
477
|
+
if (parent === undefined) {
|
|
478
|
+
// Might resolve in a later round. Whether it never resolves at all
|
|
479
|
+
// is decided once the rounds stop, not here.
|
|
480
|
+
deferred.push([index, kind]);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
// OntoClean: an anti-rigid kind cannot subsume a rigid one. The
|
|
484
|
+
// parent's lineage is the full ancestor chain, and subsumption is
|
|
485
|
+
// transitive, so an unannotated kind sitting in between does not
|
|
486
|
+
// launder the violation (ADR 0078).
|
|
487
|
+
if (kind.rigidity === 'rigid') {
|
|
488
|
+
const antiRigidAncestor = parent.lineage.find((ancestor) => conceptKindByIdentity.get(ancestor)?.rigidity === 'anti-rigid');
|
|
489
|
+
if (antiRigidAncestor !== undefined) {
|
|
490
|
+
const position = positionFor(['conceptKinds', index, 'rigidity']);
|
|
491
|
+
profileDiagnostics.push({
|
|
492
|
+
severity: 'error',
|
|
493
|
+
code: 'YM413',
|
|
494
|
+
message: `Rigid concept kind "${kind.id}" specializes anti-rigid kind "${antiRigidAncestor}"; nothing is essentially of an anti-rigid kind, so parent "${kind.id}" under an entity kind, or drop its "rigid" annotation`,
|
|
495
|
+
path: input.path,
|
|
496
|
+
pointer: `/conceptKinds/${index}/rigidity`,
|
|
497
|
+
line: position.line,
|
|
498
|
+
column: position.col,
|
|
499
|
+
});
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const resolved = {
|
|
504
|
+
identity: `${identity}#${kind.id}`,
|
|
505
|
+
aspect: parent.aspect,
|
|
506
|
+
layer: kind.layer ?? parent.layer,
|
|
507
|
+
lineage: [...parent.lineage, `${identity}#${kind.id}`],
|
|
508
|
+
...(kind.rigidity === undefined ? {} : { rigidity: kind.rigidity }),
|
|
509
|
+
};
|
|
510
|
+
resolvedConceptKinds.set(kind.id, resolved);
|
|
511
|
+
conceptKindByIdentity.set(resolved.identity, resolved);
|
|
478
512
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
513
|
+
// No round can help what the last one could not.
|
|
514
|
+
if (deferred.length === 0 || deferred.length === pendingKinds.length) {
|
|
515
|
+
pendingKinds = deferred;
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
pendingKinds = deferred;
|
|
519
|
+
}
|
|
520
|
+
// What survived every round names a parent that never arrives. Two
|
|
521
|
+
// causes, and they are different problems for the author: the parent is
|
|
522
|
+
// declared nowhere, or it is in a cycle with this kind and neither can
|
|
523
|
+
// ever have a lineage.
|
|
524
|
+
const unresolvableIds = new Set(pendingKinds.map(([, kind]) => `${identity}#${kind.id}`));
|
|
525
|
+
for (const [index, kind] of pendingKinds) {
|
|
526
|
+
const position = positionFor(['conceptKinds', index, 'parent']);
|
|
527
|
+
profileDiagnostics.push({
|
|
528
|
+
severity: 'error',
|
|
529
|
+
code: 'YM407',
|
|
530
|
+
message: unresolvableIds.has(kind.parent)
|
|
531
|
+
? `Concept parent "${kind.parent}" cannot resolve: it is in a parent cycle with "${kind.id}", so neither kind has a lineage`
|
|
532
|
+
: `Concept parent "${kind.parent}" is not available`,
|
|
533
|
+
path: input.path,
|
|
534
|
+
pointer: `/conceptKinds/${index}/parent`,
|
|
535
|
+
line: position.line,
|
|
536
|
+
column: position.col,
|
|
537
|
+
});
|
|
488
538
|
}
|
|
489
539
|
const resolvedRelationshipKinds = new Map(parentProfile.relationshipKinds);
|
|
490
540
|
for (const [index, kind] of value.relationshipKinds.entries()) {
|
|
@@ -2439,6 +2489,7 @@ function compileWorkspaceResolved(parsed) {
|
|
|
2439
2489
|
slot,
|
|
2440
2490
|
instance,
|
|
2441
2491
|
pattern: pattern.kindIdentity,
|
|
2492
|
+
wiring: slotWiringOf(pattern, slot),
|
|
2442
2493
|
})))
|
|
2443
2494
|
.sort((left, right) => left.member.localeCompare(right.member) ||
|
|
2444
2495
|
left.pattern.localeCompare(right.pattern) ||
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What contains what on a canvas, and what a folded container draws instead of
|
|
3
|
+
* its contents (#473).
|
|
4
|
+
*
|
|
5
|
+
* Two questions, one module, because they are the same question asked twice. A
|
|
6
|
+
* VIEW says which relationships nest (ADR 0101); a PATTERN says which subjects
|
|
7
|
+
* are parts of an instance (ADR 0123). Both produce a parent-of map over the
|
|
8
|
+
* same node ids, and folding reads that one map. Answering them apart would
|
|
9
|
+
* mean two trees that can disagree about who owns a node.
|
|
10
|
+
*
|
|
11
|
+
* Imports nothing but the `NestingKind` type, and that from `./nesting.js`,
|
|
12
|
+
* which itself imports nothing. The same weight argument that split
|
|
13
|
+
* `nesting.ts` out of `projection.ts` applies here and harder: this module is
|
|
14
|
+
* reached from `yarramate/adapter/visual-graph`, the runtime-neutral subpath a
|
|
15
|
+
* Durable Object imports, where `node:module` and Ajv are not available at any
|
|
16
|
+
* price. `test/visual-app-browser-safety.test.ts` is what holds that line.
|
|
17
|
+
*
|
|
18
|
+
* Everything here is a pure function over plain data. The canvas adapts its own
|
|
19
|
+
* shapes to {@link FoldInput}; nothing in this file knows what cytoscape is.
|
|
20
|
+
*/
|
|
21
|
+
import type { NestingKind } from './nesting.js';
|
|
22
|
+
/**
|
|
23
|
+
* A view names which relationships nest, in precedence order (ADR 0101). The
|
|
24
|
+
* short names a projection is authored in resolve to the kind identities the
|
|
25
|
+
* graph carries, in one place, so the schema's vocabulary and the canvas's
|
|
26
|
+
* cannot drift.
|
|
27
|
+
*/
|
|
28
|
+
export declare const NESTING_KIND_IDS: Readonly<Record<NestingKind, string>>;
|
|
29
|
+
/**
|
|
30
|
+
* Whether a view draws pattern instances folded by default (#473).
|
|
31
|
+
*
|
|
32
|
+
* Lives here rather than in `projection.ts` for the reason `nesting.ts` exists:
|
|
33
|
+
* the browser needs the VALUE, and `projection.ts` drags Ajv and the projection
|
|
34
|
+
* schema in behind it. `projection.ts` re-exports both.
|
|
35
|
+
*/
|
|
36
|
+
export type FoldMode = 'instances' | 'none';
|
|
37
|
+
/**
|
|
38
|
+
* What a view folds when it does not say: nothing. Folding hides detail, and a
|
|
39
|
+
* view that hid detail without being asked would be a surprise its author never
|
|
40
|
+
* wrote down.
|
|
41
|
+
*/
|
|
42
|
+
export declare const DEFAULT_FOLD: FoldMode;
|
|
43
|
+
/** One node, reduced to what containment needs to know about it. */
|
|
44
|
+
export interface FoldNode {
|
|
45
|
+
readonly id: string;
|
|
46
|
+
/** The kind as authored, profile-qualified or not. Unused by the rules here. */
|
|
47
|
+
readonly kind: string;
|
|
48
|
+
/**
|
|
49
|
+
* The core-vocabulary kind this resolves to. Every rule below reads THIS and
|
|
50
|
+
* never `kind`: a profile's `mule-api-operation` is an `applicationService`
|
|
51
|
+
* and must be treated as one, and the label it happens to carry is not a
|
|
52
|
+
* fact about what it is.
|
|
53
|
+
*/
|
|
54
|
+
readonly coreKind: string;
|
|
55
|
+
}
|
|
56
|
+
/** One relationship, reduced to what containment needs to know about it. */
|
|
57
|
+
export interface FoldEdge {
|
|
58
|
+
readonly id: string;
|
|
59
|
+
readonly kind: string;
|
|
60
|
+
readonly from: string;
|
|
61
|
+
readonly to: string;
|
|
62
|
+
}
|
|
63
|
+
/** How a pattern's wiring relates a slot to the instance that declares it. */
|
|
64
|
+
export type SlotWiring = 'owned' | 'context' | 'unwired';
|
|
65
|
+
/** One bound slot, as {@link foldTree} needs it. */
|
|
66
|
+
export interface FoldMembership {
|
|
67
|
+
readonly member: string;
|
|
68
|
+
readonly slot: string;
|
|
69
|
+
readonly instance: string;
|
|
70
|
+
readonly wiring?: SlotWiring;
|
|
71
|
+
}
|
|
72
|
+
export interface FoldInput {
|
|
73
|
+
readonly nodes: readonly FoldNode[];
|
|
74
|
+
readonly edges: readonly FoldEdge[];
|
|
75
|
+
readonly memberships: readonly FoldMembership[];
|
|
76
|
+
readonly nesting: readonly NestingKind[];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Two parents claiming one child at the same precedence. Returned rather than
|
|
80
|
+
* resolved: picking a winner would hide a real modelling anomaly behind a
|
|
81
|
+
* layout that looks deliberate. The caller renders the child unnested and says
|
|
82
|
+
* so, which is what composition alone already did.
|
|
83
|
+
*/
|
|
84
|
+
export interface NestingConflict {
|
|
85
|
+
readonly child: string;
|
|
86
|
+
readonly claims: readonly {
|
|
87
|
+
readonly edgeId: string;
|
|
88
|
+
readonly kind: string;
|
|
89
|
+
readonly from: string;
|
|
90
|
+
}[];
|
|
91
|
+
}
|
|
92
|
+
export interface FoldTree {
|
|
93
|
+
readonly parentOf: ReadonlyMap<string, string>;
|
|
94
|
+
readonly consumedEdgeIds: ReadonlySet<string>;
|
|
95
|
+
readonly conflicts: readonly NestingConflict[];
|
|
96
|
+
/** Ids left unnested because their parent chain loops. */
|
|
97
|
+
readonly cycleMembers: readonly string[];
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The parent-of map a view's nesting kinds imply.
|
|
101
|
+
*
|
|
102
|
+
* The compiler's `YM501` rule rejects one pair declaring both composition and
|
|
103
|
+
* aggregation; it does not reject two different compositions naming one child,
|
|
104
|
+
* which a single-parent field cannot represent either, nor a composition chain
|
|
105
|
+
* that loops. Both are real modelling anomalies, surfaced here rather than
|
|
106
|
+
* silently resolved: affected subjects come back unnested and every edge naming
|
|
107
|
+
* them stays an ordinary line, so the conflicting claims remain visible.
|
|
108
|
+
*/
|
|
109
|
+
export declare function nestingTree(edges: readonly FoldEdge[], nesting: readonly NestingKind[], coreKindOf: (id: string) => string): FoldTree;
|
|
110
|
+
/**
|
|
111
|
+
* The containment tree: what a view nests, plus what a pattern owns.
|
|
112
|
+
*
|
|
113
|
+
* A slot member joins the tree only when all three hold, and each condition is
|
|
114
|
+
* a different way of getting the answer wrong:
|
|
115
|
+
*
|
|
116
|
+
* - **Exclusive.** A subject bound into two instances has two owners, and a
|
|
117
|
+
* single-parent tree would silently pick one. Shared subjects stay outside.
|
|
118
|
+
* - **`owned` or `unwired`, never `context`.** A context slot names something
|
|
119
|
+
* the instance USES and does not contain — the upstream API it calls, the
|
|
120
|
+
* plane it runs on. Folding those would swallow half the landscape into
|
|
121
|
+
* whichever box happened to reference it.
|
|
122
|
+
* - **Not a ruling.** See {@link RULING_CORE_KINDS}.
|
|
123
|
+
*
|
|
124
|
+
* A view's own nesting wins where both apply: the view is the more specific
|
|
125
|
+
* statement, and a reader who wrote `nesting: [composition]` meant it.
|
|
126
|
+
*/
|
|
127
|
+
export declare function foldTree(input: FoldInput): FoldTree;
|
|
128
|
+
/** An edge that stands for one or more relationships hidden inside a fold. */
|
|
129
|
+
export interface LiftedEdge {
|
|
130
|
+
readonly id: string;
|
|
131
|
+
readonly kind: string;
|
|
132
|
+
readonly from: string;
|
|
133
|
+
readonly to: string;
|
|
134
|
+
readonly count: number;
|
|
135
|
+
readonly relationshipIds: readonly string[];
|
|
136
|
+
}
|
|
137
|
+
/** The id a lifted edge takes. Deterministic, so a re-render is stable. */
|
|
138
|
+
export declare const liftedEdgeId: (from: string, to: string, kind: string) => string;
|
|
139
|
+
/**
|
|
140
|
+
* What the canvas draws once some instances are folded.
|
|
141
|
+
*
|
|
142
|
+
* A folded instance KEEPS its own node — it is still a subject, still
|
|
143
|
+
* selectable, still the thing a question is about — and gains what it is
|
|
144
|
+
* standing in for. Its descendants leave the output, and every edge with an end
|
|
145
|
+
* inside it is lifted to the box.
|
|
146
|
+
*
|
|
147
|
+
* Lifted edges of one kind between one ordered pair merge into a single edge
|
|
148
|
+
* carrying `count` and the ids it stands for, so seven `serving` relationships
|
|
149
|
+
* between two applications draw as one line labelled ×7 rather than as seven
|
|
150
|
+
* lines the reader has to count. An edge whose ends fold into the SAME box
|
|
151
|
+
* vanishes: it is internal, and the box is the statement now.
|
|
152
|
+
*/
|
|
153
|
+
export declare function foldGraph<N extends {
|
|
154
|
+
readonly id: string;
|
|
155
|
+
}, E extends {
|
|
156
|
+
readonly id: string;
|
|
157
|
+
readonly kind: string;
|
|
158
|
+
readonly from: string;
|
|
159
|
+
readonly to: string;
|
|
160
|
+
}>(graph: {
|
|
161
|
+
readonly nodes: readonly N[];
|
|
162
|
+
readonly edges: readonly E[];
|
|
163
|
+
}, tree: Pick<FoldTree, 'parentOf'>, folded: ReadonlySet<string>): {
|
|
164
|
+
readonly nodes: (N & {
|
|
165
|
+
readonly folded: boolean;
|
|
166
|
+
readonly insideIds: readonly string[];
|
|
167
|
+
})[];
|
|
168
|
+
readonly edges: (E | LiftedEdge)[];
|
|
169
|
+
};
|