yarramate 1.19.1 → 1.21.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 (47) hide show
  1. package/dist/adapters/likec4-prepare.js +1 -1
  2. package/dist/adapters/visual/protocol-contract.d.ts +28 -0
  3. package/dist/adapters/visual/session-server.js +45 -12
  4. package/dist/adapters/visual/wire.d.ts +23 -0
  5. package/dist/adapters/visual/workspace-model.d.ts +4 -4
  6. package/dist/adapters/visual/workspace-model.js +28 -5
  7. package/dist/adapters/visual-graph-entry.d.ts +1 -0
  8. package/dist/adapters/visual-graph-entry.js +6 -0
  9. package/dist/ask-command.js +2 -2
  10. package/dist/check-command.js +12 -1
  11. package/dist/compiler.d.ts +21 -0
  12. package/dist/compiler.js +19 -0
  13. package/dist/export-command.js +4 -1
  14. package/dist/fold-tree.d.ts +169 -0
  15. package/dist/fold-tree.js +295 -0
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/projection.d.ts +68 -4
  19. package/dist/projection.js +185 -9
  20. package/dist/schema-validators.generated.js +355 -272
  21. package/dist/subject-references.js +8 -0
  22. package/dist/visual-app/assets/index-Ch27Sr5z.js +394 -0
  23. package/dist/visual-app/assets/index-DF3anVfS.css +1 -0
  24. package/dist/visual-app/index.html +2 -2
  25. package/dist/visual-app-lib/editor.js +28809 -28147
  26. package/dist/visual-app-lib/styles.css +1 -1
  27. package/dist/visual-app-lib/types/adapters/visual/protocol-contract.d.ts +28 -0
  28. package/dist/visual-app-lib/types/adapters/visual/wire.d.ts +23 -0
  29. package/dist/visual-app-lib/types/adapters/visual/workspace-model.d.ts +4 -4
  30. package/dist/visual-app-lib/types/compiler.d.ts +21 -0
  31. package/dist/visual-app-lib/types/fold-tree.d.ts +169 -0
  32. package/dist/visual-app-lib/types/projection.d.ts +68 -4
  33. package/dist/visual-app-lib/types/visual-app/badges.d.ts +1 -0
  34. package/dist/visual-app-lib/types/visual-app/context-menu-model.d.ts +65 -0
  35. package/dist/visual-app-lib/types/visual-app/graph-canvas.d.ts +37 -3
  36. package/dist/visual-app-lib/types/visual-app/session-client.d.ts +7 -1
  37. package/dist/visual-app-lib/types/visual-app/slots-model.d.ts +58 -0
  38. package/dist/visual-app-lib/types/visual-app/state.d.ts +2 -0
  39. package/dist/visual-app-lib/types/visual-app/view-tree-model.d.ts +18 -1
  40. package/dist/visual-app-lib/types/visual-app/view-tree.d.ts +4 -1
  41. package/dist/visual-app-lib/types/visual-app/workspace-state.d.ts +34 -0
  42. package/docs/CONSUMING-YARRAMATE.md +23 -1
  43. package/package.json +1 -1
  44. package/schema/yarramate-projection.schema.json +13 -0
  45. package/schema/yarramate-visual-layout.schema.json +16 -0
  46. package/dist/visual-app/assets/index-Bd-3k5UB.css +0 -1
  47. package/dist/visual-app/assets/index-CvTDYpur.js +0 -394
@@ -141,7 +141,7 @@ export function prepareLikeC4Export(input) {
141
141
  };
142
142
  }
143
143
  }
144
- const projectionResult = evaluateProjection(compilation.graph, projection.projection, compilation.profileContext);
144
+ const projectionResult = evaluateProjection(compilation.graph, projection.projection, compilation.profileContext, compilation.patternMemberships);
145
145
  const comparison = input.comparison === undefined
146
146
  ? undefined
147
147
  : compareArchitectureStates(compilation.graph, input.comparison.from, input.comparison.to);
@@ -9,6 +9,7 @@
9
9
  * `node:path`, or the schema documents into its bundle.
10
10
  */
11
11
  import type { CanvasGraph } from "../../graph-projection.js";
12
+ import type { NestingKind } from "../../nesting.js";
12
13
  import type { YarramateApplyResult, YarramateOperation } from "../../operations.js";
13
14
  import type { ProjectionDefinition, ProjectionExclusion, ProjectionQuery } from "../../projection.js";
14
15
  /**
@@ -163,6 +164,20 @@ export interface VisualKindOption {
163
164
  }
164
165
  export interface VisualFilterQueryPayload {
165
166
  readonly query: ProjectionQuery;
167
+ /**
168
+ * The nesting the canvas is drawing with, when the browser knows it (#473
169
+ * phase 2).
170
+ *
171
+ * Only `query.instances` reads it, and it must: the closure of an instance IS
172
+ * the containment tree, so evaluating it under a different nesting than the
173
+ * canvas answers a different question. On the ApertureX reference that is 15
174
+ * subjects against 2.
175
+ *
176
+ * Optional, so an older browser and every filter that names no instance keep
177
+ * working unchanged; absent, the evaluator falls back to the default nesting
178
+ * exactly as it did before.
179
+ */
180
+ readonly nesting?: readonly NestingKind[];
166
181
  }
167
182
  export interface VisualFilterResultPayload {
168
183
  readonly query: ProjectionQuery;
@@ -250,6 +265,19 @@ export interface VisualChangesetCommitPayload {
250
265
  export interface VisualLayoutSavePayload {
251
266
  readonly projectionId: string;
252
267
  readonly positions: VisualLayoutPositions;
268
+ /**
269
+ * What this view folds, saved beside the positions in ONE document (#473).
270
+ *
271
+ * Full state every time, never a patch. A half-applied fold state draws a
272
+ * box whose contents are somewhere else on the canvas, and the sidecar is
273
+ * written by a browser that may have been reloaded between any two saves.
274
+ *
275
+ * `unfolded` exists because the view's own `presentation.fold` is a default,
276
+ * not a rule: a reader who opened a box must not have it close again when the
277
+ * default is read back.
278
+ */
279
+ readonly folded?: readonly string[];
280
+ readonly unfolded?: readonly string[];
253
281
  }
254
282
  /**
255
283
  * 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
@@ -679,9 +690,15 @@ export const startVisualServer = async (options) => {
679
690
  if (!started.ok)
680
691
  standingDiagnostics = started.diagnostics;
681
692
  }
682
- const filterMatchedIds = (query) => compiledWorkspace === undefined
693
+ const filterMatchedIds = (query, nesting) => compiledWorkspace === undefined
683
694
  ? []
684
- : matchedIdsOf(compiledWorkspace.graph, query, compiledWorkspace.profileContext);
695
+ : matchedIdsOf(compiledWorkspace.graph, query, compiledWorkspace.profileContext,
696
+ // A view's query can name `instances`, and the facet resolves to the
697
+ // instance alone without these (ADR 0144).
698
+ compiledWorkspace.patternMemberships,
699
+ // And it resolves the WRONG closure without the nesting the canvas is
700
+ // drawing with, which is a wrong number rather than a missing one.
701
+ nesting);
685
702
  /**
686
703
  * Why a query dropped what it dropped, as the editor's "excluded, and why"
687
704
  * list reads it (#248).
@@ -693,9 +710,9 @@ export const startVisualServer = async (options) => {
693
710
  * the reason the editor shows and the set the canvas draws cannot come from
694
711
  * two readings of the same query.
695
712
  */
696
- const filterExclusions = (query) => compiledWorkspace === undefined
713
+ const filterExclusions = (query, nesting) => compiledWorkspace === undefined
697
714
  ? []
698
- : exclusionsOf(compiledWorkspace.graph, query, compiledWorkspace.profileContext);
715
+ : exclusionsOf(compiledWorkspace.graph, query, compiledWorkspace.profileContext, compiledWorkspace.patternMemberships, nesting);
699
716
  let listening = false;
700
717
  let bootstrapSpent = false;
701
718
  let agentAttached = false;
@@ -1232,8 +1249,8 @@ export const startVisualServer = async (options) => {
1232
1249
  kind: "filter-result",
1233
1250
  result: {
1234
1251
  query: event.payload.query,
1235
- matchedIds: filterMatchedIds(event.payload.query),
1236
- excluded: filterExclusions(event.payload.query),
1252
+ matchedIds: filterMatchedIds(event.payload.query, event.payload.nesting),
1253
+ excluded: filterExclusions(event.payload.query, event.payload.nesting),
1237
1254
  },
1238
1255
  });
1239
1256
  return;
@@ -1407,7 +1424,7 @@ export const startVisualServer = async (options) => {
1407
1424
  // never `git commit`ed. It never asks the agent anything, so it is
1408
1425
  // answered here directly rather than through the pending queue a
1409
1426
  // poll would drain.
1410
- const { projectionId, positions } = event.payload;
1427
+ const { projectionId, positions, folded, unfolded } = event.payload;
1411
1428
  if (!views.some((view) => view.id === projectionId)) {
1412
1429
  sendFrame(socket, {
1413
1430
  kind: "layout-save-result",
@@ -1424,10 +1441,26 @@ export const startVisualServer = async (options) => {
1424
1441
  format: "yarramate/visual-layout/v1",
1425
1442
  projectionId,
1426
1443
  positions,
1444
+ // With the positions, in one document, in full (#473). Same rule
1445
+ // the local host follows, and it has to be the same rule: a
1446
+ // sidecar written by one host is read by the other.
1447
+ ...(folded === undefined ? {} : { folded }),
1448
+ ...(unfolded === undefined ? {} : { unfolded }),
1427
1449
  }), "utf8");
1428
1450
  rendered = {
1429
1451
  ...rendered,
1430
1452
  layouts: { ...rendered.layouts, [projectionId]: positions },
1453
+ ...(folded === undefined && unfolded === undefined
1454
+ ? {}
1455
+ : {
1456
+ folds: {
1457
+ ...rendered.folds,
1458
+ [projectionId]: {
1459
+ folded: folded ?? [],
1460
+ unfolded: unfolded ?? [],
1461
+ },
1462
+ },
1463
+ }),
1431
1464
  };
1432
1465
  sendFrame(socket, {
1433
1466
  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
@@ -2,7 +2,7 @@ import type { Diagnostic } from "../../compiler.js";
2
2
  import type { PendingWrite, SourceStore } from "../../source-store.js";
3
3
  import type { ResolvedWorkspace } from "../../workspace.js";
4
4
  import type { VisualDiagnostic, VisualViewOperation } from "./protocol-contract.js";
5
- import type { ProjectionDefinition, ProjectionExclusion, ProjectionQuery } from "../../projection.js";
5
+ import type { NestingKind, ProjectionDefinition, ProjectionExclusion, ProjectionQuery } from "../../projection.js";
6
6
  import type { ResolvedProfileContext, SemanticGraph } from "../../compiler.js";
7
7
  import { type CataloguePatternMembership, type CataloguePatternVacancy } from "../../interrogate-command.js";
8
8
  import type { VisualKindOption, VisualViewSummary } from "./protocol-contract.js";
@@ -41,7 +41,7 @@ export declare const kindOptionsOf: (lineages: ReadonlyMap<string, readonly stri
41
41
  * subject count. A view over three components with two relationships between
42
42
  * them would read as five, and the reviewer counting boxes would find three.
43
43
  */
44
- export declare const conceptCountOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext) => number;
44
+ export declare const conceptCountOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext, memberships?: readonly CataloguePatternMembership[], nesting?: readonly NestingKind[]) => number;
45
45
  /**
46
46
  * Folds one interrogation report into what the canvas draws (#292).
47
47
  *
@@ -109,9 +109,9 @@ export declare const renderedWorkspaceOf: (compiled: {
109
109
  readonly views: readonly VisualViewSummary[];
110
110
  };
111
111
  /** Every subject a query draws, concepts and relationships alike. */
112
- export declare const matchedIdsOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext) => readonly string[];
112
+ export declare const matchedIdsOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext, memberships?: readonly CataloguePatternMembership[], nesting?: readonly NestingKind[]) => readonly string[];
113
113
  /** Every concept a query dropped, and the facet that dropped it (#248). */
114
- export declare const exclusionsOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext) => readonly ProjectionExclusion[];
114
+ export declare const exclusionsOf: (graph: SemanticGraph, query: ProjectionQuery, profileContext: ResolvedProfileContext, memberships?: readonly CataloguePatternMembership[], nesting?: readonly NestingKind[]) => readonly ProjectionExclusion[];
115
115
  /**
116
116
  * One saved view, as the rail reads it. `subjectCount` is the caller's,
117
117
  * because counting needs a compiled graph and a session builds its first list
@@ -45,7 +45,7 @@ export const kindOptionsOf = (lineages) => [...lineages.keys()].map((id) => ({
45
45
  * subject count. A view over three components with two relationships between
46
46
  * them would read as five, and the reviewer counting boxes would find three.
47
47
  */
48
- export const conceptCountOf = (graph, query, profileContext) => evaluateProjection(graph, adHoc(query), profileContext).subjects.filter(({ type }) => type === "concept").length;
48
+ export const conceptCountOf = (graph, query, profileContext, memberships, nesting) => evaluateProjection(graph, adHoc(query, nesting), profileContext, memberships).subjects.filter(({ type }) => type === "concept").length;
49
49
  export const interrogationOverlayOf = (compiled,
50
50
  /**
51
51
  * The catalogue, or the composed SET a workspace carries (#345, ADR 0129).
@@ -118,7 +118,14 @@ dismissed = []) => {
118
118
  export const renderedWorkspaceOf = (compiled, views, metadata, catalogue, dismissed) => {
119
119
  const refreshedViews = views.map((view) => ({
120
120
  ...view,
121
- subjectCount: conceptCountOf(compiled.graph, view.query, compiled.profileContext),
121
+ subjectCount: conceptCountOf(compiled.graph, view.query, compiled.profileContext,
122
+ // Without these an `instances` view counts 1 and the rail says so, which
123
+ // is a wrong number rather than a missing one (ADR 0144).
124
+ compiled.patternMemberships,
125
+ // Each view's OWN nesting, because each view's closure is its own. A rail
126
+ // sitting beside the canvas must not count a different tree than the
127
+ // canvas draws.
128
+ view.presentation?.nesting),
122
129
  }));
123
130
  const interrogation = catalogue === undefined
124
131
  ? undefined
@@ -132,23 +139,39 @@ export const renderedWorkspaceOf = (compiled, views, metadata, catalogue, dismis
132
139
  relationshipKinds: kindOptionsOf(compiled.profileContext.relationshipKindLineages),
133
140
  },
134
141
  ...(interrogation === undefined ? {} : { interrogation }),
142
+ // Containment context, forwarded rather than re-derived (#473). Both
143
+ // hosts build the model here, so local-host and session-server cannot
144
+ // disagree about what is inside a box - which is the shape of defect
145
+ // that made `patterns` ship non-functional in 1.4.0, ten source lists
146
+ // each dropping the same thing.
147
+ ...(compiled.patternMemberships === undefined
148
+ ? {}
149
+ : { memberships: compiled.patternMemberships }),
150
+ ...(compiled.patternVacancies === undefined
151
+ ? {}
152
+ : { vacancies: compiled.patternVacancies }),
135
153
  },
136
154
  views: refreshedViews,
137
155
  };
138
156
  };
139
157
  /** Every subject a query draws, concepts and relationships alike. */
140
- export const matchedIdsOf = (graph, query, profileContext) => evaluateProjection(graph, adHoc(query), profileContext).subjects.map(({ id }) => id);
158
+ export const matchedIdsOf = (graph, query, profileContext, memberships, nesting) => evaluateProjection(graph, adHoc(query, nesting), profileContext, memberships).subjects.map(({ id }) => id);
141
159
  /** Every concept a query dropped, and the facet that dropped it (#248). */
142
- export const exclusionsOf = (graph, query, profileContext) => explainProjection(graph, adHoc(query), profileContext);
160
+ export const exclusionsOf = (graph, query, profileContext, memberships, nesting) => explainProjection(graph, adHoc(query, nesting), profileContext, memberships);
143
161
  /**
144
162
  * A query on its own is not a projection, and every evaluator here wants one.
145
163
  * The id is a placeholder that never reaches a document.
146
164
  */
147
- const adHoc = (query) => ({
165
+ const adHoc = (query, nesting) => ({
148
166
  format: "yarramate/projection/v1",
149
167
  id: "ad-hoc",
150
168
  version: "0",
151
169
  query,
170
+ // `query.instances` resolves its closure through the view's nesting, so an
171
+ // ad-hoc projection that dropped the nesting would answer a DIFFERENT
172
+ // question than the canvas is drawing: 2 subjects against 15 on the
173
+ // ApertureX reference, with nothing to say it had (#473 phase 2).
174
+ ...(nesting === undefined ? {} : { presentation: { nesting } }),
152
175
  });
153
176
  /**
154
177
  * One saved view, as the rail reads it. `subjectCount` is the caller's,
@@ -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';
@@ -784,7 +784,7 @@ export function runAskCommand(options, cwd) {
784
784
  });
785
785
  if (!loaded.ok)
786
786
  continue;
787
- const membership = evaluateProjection(graph, loaded.projection, compilation.profileContext);
787
+ const membership = evaluateProjection(graph, loaded.projection, compilation.profileContext, compilation.patternMemberships);
788
788
  for (const subject of membership.subjects) {
789
789
  covered.add(subject.id);
790
790
  }
@@ -857,7 +857,7 @@ export function runAskCommand(options, cwd) {
857
857
  });
858
858
  if (!loaded.ok)
859
859
  return failed(loaded.diagnostics);
860
- const evaluated = evaluateProjection(graph, loaded.projection, compilation.profileContext);
860
+ const evaluated = evaluateProjection(graph, loaded.projection, compilation.profileContext, compilation.patternMemberships);
861
861
  const result = {
862
862
  format: 'yarramate/ask-result/v1',
863
863
  workspace: workspace.id,
@@ -214,7 +214,18 @@ export function runCheckCommand(options, cwd) {
214
214
  // does not build would bury the real failure under its consequences.
215
215
  const referenceDiagnostics = result.ok
216
216
  ? loadedProjections.flatMap(({ source, loaded }) => loaded.ok
217
- ? projectionReferenceDiagnostics(source, loaded.projection, result.graph, result.profileContext)
217
+ ? projectionReferenceDiagnostics(source, loaded.projection, result.graph, result.profileContext,
218
+ // Instance-hood from BOTH lists. A membership row exists only
219
+ // for a BOUND slot, so an instance whose slots are all empty
220
+ // has none - and judging it by bindings alone would call a real
221
+ // instance "not an instance" on the day it was authored, before
222
+ // anything was wired into it. The honest question is not "did
223
+ // it bind anything" but "does the model know it as an
224
+ // instance" (rule 2).
225
+ new Set([
226
+ ...(result.patternMemberships ?? []).map(({ instance }) => instance),
227
+ ...(result.patternVacancies ?? []).map(({ instance }) => instance),
228
+ ]))
218
229
  : [])
219
230
  : [];
220
231
  const optionalDiagnostics = sortDiagnostics([
@@ -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 = {
@@ -2471,6 +2489,7 @@ function compileWorkspaceResolved(parsed) {
2471
2489
  slot,
2472
2490
  instance,
2473
2491
  pattern: pattern.kindIdentity,
2492
+ wiring: slotWiringOf(pattern, slot),
2474
2493
  })))
2475
2494
  .sort((left, right) => left.member.localeCompare(right.member) ||
2476
2495
  left.pattern.localeCompare(right.pattern) ||
@@ -250,7 +250,10 @@ export function runExportCommand(options, cwd) {
250
250
  });
251
251
  if (!loadedProjection.ok)
252
252
  return failed(loadedProjection.diagnostics);
253
- result = evaluateProjection(compilation.graph, loadedProjection.projection, compilation.profileContext);
253
+ result = evaluateProjection(compilation.graph, loadedProjection.projection, compilation.profileContext,
254
+ // An AUTHORED projection can name `instances`, and the facet resolves
255
+ // to the instance alone without these (ADR 0144).
256
+ compilation.patternMemberships);
254
257
  }
255
258
  if (kind === 'xlsx') {
256
259
  // A workbook an architect can work in (#355). It takes a PROJECTION,
@@ -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
+ };