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.
Files changed (37) hide show
  1. package/dist/adapters/visual/protocol-contract.d.ts +13 -0
  2. package/dist/adapters/visual/session-server.js +33 -6
  3. package/dist/adapters/visual/wire.d.ts +23 -0
  4. package/dist/adapters/visual/workspace-model.js +11 -0
  5. package/dist/adapters/visual-graph-entry.d.ts +1 -0
  6. package/dist/adapters/visual-graph-entry.js +6 -0
  7. package/dist/compiler.d.ts +21 -0
  8. package/dist/compiler.js +99 -48
  9. package/dist/fold-tree.d.ts +169 -0
  10. package/dist/fold-tree.js +295 -0
  11. package/dist/projection.d.ts +16 -0
  12. package/dist/projection.js +10 -0
  13. package/dist/schema-validators.generated.js +44 -31
  14. package/dist/visual-app/assets/index-DF3anVfS.css +1 -0
  15. package/dist/visual-app/assets/index-EegLnUfX.js +394 -0
  16. package/dist/visual-app/index.html +2 -2
  17. package/dist/visual-app-lib/editor.js +27427 -26907
  18. package/dist/visual-app-lib/styles.css +1 -1
  19. package/dist/visual-app-lib/types/adapters/visual/protocol-contract.d.ts +13 -0
  20. package/dist/visual-app-lib/types/adapters/visual/wire.d.ts +23 -0
  21. package/dist/visual-app-lib/types/compiler.d.ts +21 -0
  22. package/dist/visual-app-lib/types/fold-tree.d.ts +169 -0
  23. package/dist/visual-app-lib/types/projection.d.ts +16 -0
  24. package/dist/visual-app-lib/types/visual-app/badges.d.ts +1 -0
  25. package/dist/visual-app-lib/types/visual-app/context-menu-model.d.ts +42 -0
  26. package/dist/visual-app-lib/types/visual-app/graph-canvas.d.ts +37 -3
  27. package/dist/visual-app-lib/types/visual-app/slots-model.d.ts +58 -0
  28. package/dist/visual-app-lib/types/visual-app/view-tree-model.d.ts +18 -1
  29. package/dist/visual-app-lib/types/visual-app/view-tree.d.ts +4 -1
  30. package/dist/visual-app-lib/types/visual-app/workspace-state.d.ts +34 -0
  31. package/docs/CONSUMING-YARRAMATE.md +23 -1
  32. package/docs/NATIVE-DOCUMENT.md +1 -1
  33. package/package.json +1 -1
  34. package/schema/yarramate-projection.schema.json +4 -0
  35. package/schema/yarramate-visual-layout.schema.json +16 -0
  36. package/dist/visual-app/assets/index-Bd-3k5UB.css +0 -1
  37. 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 built = {};
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 built;
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
- built[sidecar.projectionId] = sidecar.positions;
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 built;
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';
@@ -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
- for (const [index, kind] of value.conceptKinds.entries()) {
432
- if (resolvedConceptKinds.has(kind.id)) {
433
- const position = positionFor(['conceptKinds', index, 'id']);
434
- profileDiagnostics.push({
435
- severity: 'error',
436
- code: 'YM409',
437
- message: `Concept kind "${kind.id}" conflicts with an inherited kind`,
438
- path: input.path,
439
- pointer: `/conceptKinds/${index}/id`,
440
- line: position.line,
441
- column: position.col,
442
- });
443
- continue;
444
- }
445
- const parent = conceptKindByIdentity.get(kind.parent);
446
- if (parent === undefined) {
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: 'YM413',
470
- 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`,
467
+ code: 'YM409',
468
+ message: `Concept kind "${kind.id}" conflicts with an inherited kind`,
471
469
  path: input.path,
472
- pointer: `/conceptKinds/${index}/rigidity`,
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
- const resolved = {
480
- identity: `${identity}#${kind.id}`,
481
- aspect: parent.aspect,
482
- layer: kind.layer ?? parent.layer,
483
- lineage: [...parent.lineage, `${identity}#${kind.id}`],
484
- ...(kind.rigidity === undefined ? {} : { rigidity: kind.rigidity }),
485
- };
486
- resolvedConceptKinds.set(kind.id, resolved);
487
- conceptKindByIdentity.set(resolved.identity, resolved);
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
+ };