dsh-model-everything 0.4.1 → 0.5.1

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 (114) hide show
  1. package/README.en-US.md +75 -19
  2. package/README.md +377 -345
  3. package/cordis.patch.yml +9 -0
  4. package/diagram/package.json +5 -0
  5. package/lib/client.js +3494 -0
  6. package/lib/diagram-diff-tool.js +90 -0
  7. package/lib/diagram-render-tool.js +102 -0
  8. package/lib/diagram-review-tool.js +99 -0
  9. package/lib/diagram.js +43 -0
  10. package/lib/gate.js +14 -12
  11. package/lib/presentation.js +10 -0
  12. package/lib/render/budget.js +99 -0
  13. package/lib/render/channels.js +243 -0
  14. package/lib/render/collapse.js +155 -0
  15. package/lib/render/components.js +99 -0
  16. package/lib/render/coords.js +288 -0
  17. package/lib/render/cycle-break.js +147 -0
  18. package/lib/render/diff.js +138 -0
  19. package/lib/render/dummy.js +43 -0
  20. package/lib/render/from-mermaid.js +235 -0
  21. package/lib/render/from-plantuml.js +430 -0
  22. package/lib/render/index.js +42 -0
  23. package/lib/render/layer.js +91 -0
  24. package/lib/render/layout.js +68 -0
  25. package/lib/render/model.js +17 -0
  26. package/lib/render/network-simplex.js +272 -0
  27. package/lib/render/order.js +122 -0
  28. package/lib/render/paint.js +891 -0
  29. package/lib/render/palette.js +127 -0
  30. package/lib/render/place.js +482 -0
  31. package/lib/render/route.js +225 -0
  32. package/lib/render/svg.js +229 -0
  33. package/lib/render/symbol.js +108 -0
  34. package/lib/render/text.js +74 -0
  35. package/lib/render/viewmodel.js +126 -0
  36. package/lib/render/wrap.js +87 -0
  37. package/lib/render/write.js +14 -0
  38. package/lib/scan.js +248 -8
  39. package/lib/structure-tool.js +2 -2
  40. package/lib/types/client.d.ts +83 -0
  41. package/lib/types/diagram-diff-tool.d.ts +8 -0
  42. package/lib/types/diagram-render-tool.d.ts +8 -0
  43. package/lib/types/diagram-review-tool.d.ts +8 -0
  44. package/lib/types/diagram.d.ts +34 -0
  45. package/lib/types/presentation.d.ts +7 -0
  46. package/lib/types/render/budget.d.ts +55 -0
  47. package/lib/types/render/channels.d.ts +63 -0
  48. package/lib/types/render/collapse.d.ts +23 -0
  49. package/lib/types/render/components.d.ts +57 -0
  50. package/lib/types/render/coords.d.ts +64 -0
  51. package/lib/types/render/cycle-break.d.ts +45 -0
  52. package/lib/types/render/diff.d.ts +44 -0
  53. package/lib/types/render/dummy.d.ts +32 -0
  54. package/lib/types/render/from-mermaid.d.ts +27 -0
  55. package/lib/types/render/from-plantuml.d.ts +33 -0
  56. package/lib/types/render/index.d.ts +38 -0
  57. package/lib/types/render/layer.d.ts +28 -0
  58. package/lib/types/render/layout.d.ts +54 -0
  59. package/lib/types/render/model.d.ts +73 -0
  60. package/lib/types/render/network-simplex.d.ts +54 -0
  61. package/lib/types/render/order.d.ts +27 -0
  62. package/lib/types/render/paint.d.ts +127 -0
  63. package/lib/types/render/palette.d.ts +41 -0
  64. package/lib/types/render/place.d.ts +55 -0
  65. package/lib/types/render/route.d.ts +77 -0
  66. package/lib/types/render/svg.d.ts +19 -0
  67. package/lib/types/render/symbol.d.ts +73 -0
  68. package/lib/types/render/text.d.ts +38 -0
  69. package/lib/types/render/viewmodel.d.ts +20 -0
  70. package/lib/types/render/wrap.d.ts +27 -0
  71. package/lib/types/render/write.d.ts +14 -0
  72. package/lib/types/scan.d.ts +10 -2
  73. package/lib/types/structure.d.ts +1 -0
  74. package/package.json +148 -132
  75. package/skills/model-everything/SKILL.md +454 -454
  76. package/skills/model-everything/references/architecture-domains.md +137 -0
  77. package/skills/model-everything/references/architecture-views.md +119 -0
  78. package/skills/model-everything-diagram/SKILL.md +84 -0
  79. package/src/client.ts +307 -0
  80. package/src/diagram-diff-tool.ts +94 -0
  81. package/src/diagram-render-tool.ts +108 -0
  82. package/src/diagram-review-tool.ts +109 -0
  83. package/src/diagram.ts +61 -0
  84. package/src/gate.ts +14 -12
  85. package/src/presentation.ts +12 -0
  86. package/src/render/budget.ts +135 -0
  87. package/src/render/channels.ts +309 -0
  88. package/src/render/collapse.ts +150 -0
  89. package/src/render/components.ts +126 -0
  90. package/src/render/coords.ts +328 -0
  91. package/src/render/cycle-break.ts +164 -0
  92. package/src/render/diff.ts +162 -0
  93. package/src/render/dummy.ts +65 -0
  94. package/src/render/from-mermaid.ts +261 -0
  95. package/src/render/from-plantuml.ts +469 -0
  96. package/src/render/index.ts +73 -0
  97. package/src/render/layer.ts +98 -0
  98. package/src/render/layout.ts +101 -0
  99. package/src/render/model.ts +86 -0
  100. package/src/render/network-simplex.ts +290 -0
  101. package/src/render/order.ts +139 -0
  102. package/src/render/paint.ts +1006 -0
  103. package/src/render/palette.ts +164 -0
  104. package/src/render/place.ts +527 -0
  105. package/src/render/route.ts +269 -0
  106. package/src/render/svg.ts +270 -0
  107. package/src/render/symbol.ts +137 -0
  108. package/src/render/text.ts +86 -0
  109. package/src/render/viewmodel.ts +136 -0
  110. package/src/render/wrap.ts +103 -0
  111. package/src/render/write.ts +17 -0
  112. package/src/scan.ts +807 -564
  113. package/src/structure-tool.ts +2 -2
  114. package/src/structure.ts +1 -1
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The diagram a renderer draws, in the notation it came from being nobody's business.
3
+ *
4
+ * Every source - a verification report, a Mermaid file, a PlantUML component diagram, a dependency scan -
5
+ * is reduced to this one shape before anything is laid out. A renderer that understood notations would have
6
+ * to be changed for each of them; a renderer that understands this does not.
7
+ *
8
+ * Pure data, no functions: a model can be serialised, compared and asserted.
9
+ */
10
+ /** What a node is, as far as colour is concerned. */
11
+ export type NodeRole = 'normal' | 'error' | 'warning' | 'muted';
12
+ /** The symbol a node is drawn as. UML and Mermaid both distinguish these; the notation decides which one applies. */
13
+ export type NodeShape = 'rect' | 'diamond' | 'cylinder' | 'stadium' | 'circle' | 'subroutine' | 'hexagon';
14
+ export interface DiagramNode {
15
+ id: string;
16
+ /** The text drawn inside the box. Defaults to the id when a notation labelled it. */
17
+ label?: string;
18
+ role: NodeRole;
19
+ /** Defaults to a rectangle: a node the source gave no shape has no shape to draw. */
20
+ shape?: NodeShape;
21
+ }
22
+ /**
23
+ * What an edge's head means, when the notation distinguished it.
24
+ *
25
+ * The painter draws the meaning: a plain arrow for an association, a hollow triangle for a generalisation or a
26
+ * realisation, a filled diamond at the source for a composition, a hollow one for an aggregation, and a dashed
27
+ * line for the two dependency-flavoured kinds.
28
+ */
29
+ export type EdgeArrow = 'association' | 'dependency' | 'generalization' | 'realization' | 'composition' | 'aggregation';
30
+ export interface DiagramEdge {
31
+ from: string;
32
+ to: string;
33
+ label: string;
34
+ /** A dashed edge, for notations that distinguish one. */
35
+ dashed?: boolean;
36
+ /**
37
+ * The arrow the notation stated. Absent means the plain arrow every edge has been drawn with: a Mermaid edge
38
+ * states no arrow type, so it carries no field and is drawn exactly as before.
39
+ */
40
+ arrow?: EdgeArrow;
41
+ }
42
+ export interface DiagramGroup {
43
+ id: string;
44
+ label: string;
45
+ /**
46
+ * The nodes declared directly inside it: a group is a box around boxes, and its own members are the ones a
47
+ * source named there. A node inside a nested group is that group's member, not this one's.
48
+ */
49
+ members: string[];
50
+ /**
51
+ * The group that contains this one, when the source said so.
52
+ *
53
+ * Only a parser can know this - nesting is a property of where the text put the boundary - so the layout does
54
+ * not guess it. Left absent, the painter falls back to inferring containment from member sets, which is the
55
+ * best a hand-written model can offer.
56
+ */
57
+ parent?: string;
58
+ }
59
+ export interface Diagram {
60
+ kind: 'machine' | 'graph' | 'none';
61
+ /** The state the machine starts in, when the source declared one. */
62
+ init?: string;
63
+ title: string;
64
+ nodes: DiagramNode[];
65
+ edges: DiagramEdge[];
66
+ groups?: DiagramGroup[];
67
+ /** One line under the drawing: what was decided, in words the drawing cannot carry. */
68
+ caption: string;
69
+ }
70
+ /** An empty drawing, for a source that produced no nodes. */
71
+ export declare function emptyDiagram(title: string, caption: string): Diagram;
72
+ /** The label to draw for a node: its own label when the notation gave one, else its id. */
73
+ export declare function labelOf(node: DiagramNode): string;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Minimum-length ranking: the network simplex of Gansner, Koutsofios, North and Vo (1993).
3
+ *
4
+ * The ranking problem: give every node a layer such that each edge spans at least its minimum length, and make the
5
+ * total of all the slack - the layers every edge spans beyond its minimum - as small as it can be. Slack is what a
6
+ * reader pays for: an edge that spans three layers has to be drawn through two of them.
7
+ *
8
+ * Longest paths are one feasible answer and they are not the best one. They put every node as high as it can
9
+ * possibly go, which leaves a node whose edges leave it with nothing to do above the boxes it points at. The best
10
+ * answer is the solution of a linear program, and network simplex is how the mature layering implementations solve
11
+ * it: hold a *feasible tree* - a spanning tree every edge of which is tight - and a ranking that tightens it, attach
12
+ * a *cut value* to each tree edge (the weight flowing one way across that cut minus the weight flowing the other),
13
+ * and while some tree edge has a negative cut value, take it out, let the cheapest edge across its cut in, and move
14
+ * the ranks so the new edge is tight. The cut value is the rate at which the total length falls as the tree edge is
15
+ * left out, so every exchange shortens the drawing, and a tree with no negative cut value is the optimum: no single
16
+ * exchange is left that would shorten it, and for this program that is the definition of optimal.
17
+ *
18
+ * Three things are done differently from the paper, each because this graph is not the paper's graph:
19
+ *
20
+ * - **Components are solved one at a time.** The ranking is translation invariant, so two components that share no
21
+ * edge can be laid out apart from each other; each is solved on its own and its top row is then numbered zero.
22
+ * - **A node may be pinned to the top.** A state machine has an initial state and a reader expects it first, so the
23
+ * stage can name one node that must be on the topmost layer: a zero-length, zero-weight constraint edge runs from
24
+ * it to every other node of its component (`rank(v) >= rank(pinned)`), and the ranking is minimised subject to that.
25
+ * The constraint costs length when the free optimum would have put the pinned node below another source, and that
26
+ * price is what "the initial state is first" means.
27
+ * - **The iteration is capped.** A degenerate exchange (one that moves nothing) is legal and in theory can go round
28
+ * in circles. The cap turns that into a ranking that is feasible and no worse than where it started.
29
+ *
30
+ * Pure, and generic in the ranking program: no ids, no sizes, no drawing.
31
+ */
32
+ /** One edge of the ranking program: two layers, how far apart they may be, and what one layer of slack costs. */
33
+ export interface RankEdge {
34
+ from: number;
35
+ to: number;
36
+ /** The least number of layers this edge may span. One for an edge that is drawn, zero for a constraint. */
37
+ minLength: number;
38
+ /** What one extra layer on this edge costs. Zero for a constraint, which must hold but is not worth shortening. */
39
+ weight: number;
40
+ }
41
+ export interface RankProblem {
42
+ count: number;
43
+ edges: RankEdge[];
44
+ /** A feasible ranking to start from: it already gives every edge at least its minimum length. */
45
+ start: number[];
46
+ /**
47
+ * The node that has to sit on the topmost layer, when the source named one.
48
+ *
49
+ * It has to be a node nothing points at, or no ranking can hold it at the top - a predecessor forces it down. The
50
+ * constraint is dropped when the start ranking cannot satisfy it.
51
+ */
52
+ pinned?: number;
53
+ }
54
+ export declare function minimumLength(problem: RankProblem): number[];
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Ordering: the left-to-right order of each layer.
3
+ *
4
+ * The layer a box is in is fixed by now; the only freedom left is the order inside each layer, and that order decides
5
+ * how many edges cross. A crossing is counted on a pair of adjacent layers: two edges between the same two layers
6
+ * cross when their end points are in opposite order on the top and the bottom.
7
+ *
8
+ * The method is the one every layered layout uses. A median sweep takes each layer in turn and sorts it by the median
9
+ * position of its neighbours in the layer just visited - down the drawing, then up. A down sweep alone settles each
10
+ * layer against the one above it and can be undone by the next; alternating is what makes the passes converge.
11
+ *
12
+ * The median is a good guess, not a search. Greedy switch is the search: try swapping two neighbours in a layer, keep
13
+ * the swap if the total crossing count falls, and stop when a whole pass finds nothing. That is where a crossing the
14
+ * median cannot see gets removed.
15
+ *
16
+ * Pure: layers of ids in, layers of ids out, plus the crossing count that justified them.
17
+ */
18
+ import type { Chain } from './dummy.js';
19
+ export interface Ordered {
20
+ /** The same layers, each in its final order. */
21
+ layers: string[][];
22
+ /** The number of crossings between adjacent layers left by the ordering. */
23
+ crossings: number;
24
+ }
25
+ /** How many pairs of edges between adjacent layers cross. */
26
+ export declare function countCrossings(layers: string[][], chains: Chain[]): number;
27
+ export declare function order(seedLayers: string[][], chains: Chain[], seed?: Map<string, number>): Ordered;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Geometry: what a diagram turns into, in coordinates.
3
+ *
4
+ * A drawing has to survive three containers - inline in a conversation, full-screen, and a persistent
5
+ * sidebar tab - so nothing here knows about pixel size. It emits a content-sized canvas and lets the
6
+ * container scale it: the same numbers serve a 360px card and a 1200px whiteboard.
7
+ *
8
+ * The drawing is placed by the layered pipeline (`place.ts`), which also says which lane a long edge reserved.
9
+ * Edges are drawn from where the boxes ended up, in two ways only: a long edge follows its lane as one polyline,
10
+ * and everything else goes through the orthogonal router. Which side of a box an edge leaves by is read off the
11
+ * route, not picked by comparing the boxes' centres: a route between two rows arrives vertically, so the endpoint
12
+ * is snapped onto the top or bottom perimeter in the direction the route approaches. Where along that side it sits
13
+ * follows the symbol: a rectangle spreads its ports along the side, a cylinder's top port sits on its cap curve, and a
14
+ * diamond or a circle has four legal points and no side to spread along at all.
15
+ *
16
+ * A symbol with discrete points gives each of its edges a point of its own: the point whose outward normal faces the
17
+ * box at the other end is the one that edge takes, and an edge only shares a point when all four are spoken for. A
18
+ * node with a run on its side keeps doing what it always did, because there the ports are places on the run and not
19
+ * points at all. Sharing is the fallback, not the rule: two labelled branches of a decision leave by two vertices.
20
+ *
21
+ * An edge's name is drawn on the edge, not beside it: it is anchored at the midpoint of the edge's longest straight
22
+ * run, over a rectangle in the page colour that masks the line behind the text, and it slides along that run when
23
+ * something is in the way. Labels are placed after every edge is drawn, because what a label has to avoid is the
24
+ * whole picture.
25
+ *
26
+ * Pure: a diagram in, shapes out, every decision assertable.
27
+ */
28
+ import { type ColourRole, type Palette, type PaletteOptions } from './palette.js';
29
+ import { type Diagram, type DiagramNode, type EdgeArrow, type NodeShape } from './model.js';
30
+ export interface Point {
31
+ x: number;
32
+ y: number;
33
+ }
34
+ export interface BoxShape {
35
+ kind: 'box';
36
+ x: number;
37
+ y: number;
38
+ width: number;
39
+ height: number;
40
+ /** The lines to draw, already wrapped; `label` keeps the whole text for anything that reads the shape. */
41
+ label: string;
42
+ lines: string[];
43
+ /** The symbol to draw. Undefined means a rectangle. */
44
+ shape?: NodeShape;
45
+ colour: ColourRole;
46
+ initial: boolean;
47
+ }
48
+ export interface LinkShape {
49
+ kind: 'link';
50
+ x1: number;
51
+ y1: number;
52
+ x2: number;
53
+ y2: number;
54
+ label: string;
55
+ /** The centre the label is drawn on, and therefore a point on the edge itself. */
56
+ labelX: number;
57
+ labelY: number;
58
+ colour: ColourRole;
59
+ /** A dashed line, when the notation distinguished one. */
60
+ dashed?: boolean;
61
+ /** The arrow the source edge stated. Undefined means the plain arrow every edge has been drawn with. */
62
+ arrow?: EdgeArrow;
63
+ }
64
+ export interface PathShape {
65
+ kind: 'path';
66
+ points: Point[];
67
+ label: string;
68
+ /** The centre the label is drawn on, and therefore a point on the edge itself. */
69
+ labelX: number;
70
+ labelY: number;
71
+ colour: ColourRole;
72
+ dashed?: boolean;
73
+ arrow?: EdgeArrow;
74
+ }
75
+ /** A boundary around several states: a package, a layer, a subgraph. */
76
+ export interface GroupShape {
77
+ kind: 'group';
78
+ /** The group's own id, so a container can fold it without guessing from the label. */
79
+ id: string;
80
+ x: number;
81
+ y: number;
82
+ width: number;
83
+ height: number;
84
+ label: string;
85
+ colour: ColourRole;
86
+ }
87
+ export interface CaptionShape {
88
+ kind: 'caption';
89
+ x: number;
90
+ y: number;
91
+ lines: string[];
92
+ colour: ColourRole;
93
+ }
94
+ export interface TitleShape {
95
+ kind: 'title';
96
+ x: number;
97
+ y: number;
98
+ text: string;
99
+ colour: ColourRole;
100
+ }
101
+ export type Shape = GroupShape | BoxShape | LinkShape | PathShape | CaptionShape | TitleShape;
102
+ export interface Canvas {
103
+ width: number;
104
+ height: number;
105
+ shapes: Shape[];
106
+ }
107
+ /**
108
+ * How far apart two edges that join the same pair are drawn.
109
+ *
110
+ * Wider than the least two ports may sit: two statements about one pair have to read as two lines, and a gap the
111
+ * port packer would allow is a gap a reader sees as one thick line. Exported so a test can name the pattern.
112
+ */
113
+ export declare const MULTI_EDGE_SPACING = 20;
114
+ /** Which colour a box earns from its role. One mapping, used by every container. */
115
+ export declare function roleOfNode(node: DiagramNode): ColourRole;
116
+ /**
117
+ * How tall the mask an edge label is drawn over is.
118
+ *
119
+ * Exported because two modules must agree on it: the painter reserves the rectangle when it looks for a clear
120
+ * place, and the writer draws the rectangle that was reserved. A label with two sizes is a label half masked.
121
+ */
122
+ export declare const LABEL_HEIGHT = 12;
123
+ export declare function paintWith(diagram: Diagram, overrides?: Partial<Palette>, options?: PaletteOptions): {
124
+ canvas: Canvas;
125
+ colours: Palette;
126
+ };
127
+ export declare function paint(diagram: Diagram, overrides?: Partial<Palette>): Canvas;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Verdict to colour, in the two surfaces the page can be in, and in the scheme a reader prefers.
3
+ *
4
+ * A drawing is read before it is parsed, so the first thing it says is the answer. The roles are named rather
5
+ * than coloured: a scheme maps a role to a value, and every scheme is chosen so the drawing survives both a
6
+ * light and a dark page - the reader never has to fix a palette to read a result.
7
+ *
8
+ * The mode is read at render time from the page's own background (see `client.ts`). The host publishes its
9
+ * palette as CSS variables whose names are private to it, and a drawing that hard-coded them would break on
10
+ * the next theme; the luminance of the surface is the one fact that cannot drift.
11
+ *
12
+ * Pure, and tested: a wrong role is a wrong verdict shown as a shape.
13
+ */
14
+ export type ColourRole = 'pass' | 'findings' | 'fail' | 'undecided' | 'muted' | 'text' | 'edge' | 'surface' | 'background';
15
+ export type Palette = Record<ColourRole, string>;
16
+ /** Which surface the drawing will sit on. */
17
+ export type PaletteMode = 'light' | 'dark';
18
+ /** A named set of colours. A scheme with both variants follows the page; one with only light does not. */
19
+ export type PaletteScheme = 'default' | 'github' | 'high-contrast' | 'print';
20
+ export interface PaletteOptions {
21
+ mode?: PaletteMode;
22
+ scheme?: PaletteScheme;
23
+ }
24
+ /**
25
+ * Resolve a palette.
26
+ *
27
+ * `overrides` win over everything, so a caller can retheme one role without inventing a scheme. A scheme with
28
+ * a dark variant follows the surface; one without stays as declared - high-contrast and print are what they
29
+ * say they are, on purpose. An unknown name falls back to the default palette rather than throwing, because
30
+ * the value can arrive from a DOM control.
31
+ */
32
+ export declare function palette(overrides?: Partial<Palette>, options?: PaletteOptions): Palette;
33
+ /**
34
+ * The role a verdict earns.
35
+ *
36
+ * `pass_with_findings` is deliberately not `pass`: it reads as the middle band, because a card that paints a
37
+ * report with warnings the same colour as a clean one has stopped carrying the answer.
38
+ */
39
+ export declare function roleForVerdict(verdict: string | undefined): ColourRole;
40
+ /** Every scheme by name, so a picker does not hard-code the list. */
41
+ export declare function schemeNames(): PaletteScheme[];
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Where the boxes go.
3
+ *
4
+ * The drawing is placed by the layered pipeline, in the order the mature implementations run it: cycle breaking turns
5
+ * the graph into a DAG, layering gives every box a row, dummy nodes give every long edge a lane, ordering reduces the
6
+ * crossings, and coordinate assignment decides the x of each lane and box. This module composes those stages; it does
7
+ * not decide any of them itself. Each stage is a pure module next door, so it can be asserted without a canvas.
8
+ *
9
+ * Two decompositions sit around the pipeline. A level - the top of the drawing, or the inside of a boundary - is
10
+ * split into weakly connected components, each laid out on its own and then packed onto the page, so a wide
11
+ * subgraph cannot squeeze an unrelated one. A boundary is a node in its parent's layer whose size is whatever its
12
+ * contents need, and its inside is laid out by the same pipeline; that is how nesting is drawn.
13
+ *
14
+ * What comes out is geometry only: a box per node, a rectangle per boundary, and - for a long edge - the lane its
15
+ * dummies reserved, so the painter can draw one polyline through it instead of searching around the boxes.
16
+ */
17
+ import { type Measured as TextBox } from './text.js';
18
+ import type { Diagram, DiagramNode } from './model.js';
19
+ /** Geometry of one placed box, before anything is drawn. */
20
+ export type Box = {
21
+ x: number;
22
+ y: number;
23
+ width: number;
24
+ height: number;
25
+ };
26
+ /** A point in the drawing's own coordinates. */
27
+ export interface Point {
28
+ x: number;
29
+ y: number;
30
+ }
31
+ /** The lane a long edge's dummies reserved, in layout order: from the layout source down to the layout target. */
32
+ export interface Lane {
33
+ /** One point per gap the edge crosses, at the lane's own x: it keeps its own order, and the painter walks it. */
34
+ points: Point[];
35
+ /** True when the layout runs opposite to the source's arrow, so the painter walks the lane backwards. */
36
+ reversed: boolean;
37
+ }
38
+ /** What a placement returns: every box, every drawn boundary, every reserved lane, and how wide the content is. */
39
+ export interface Placement {
40
+ boxes: Map<string, Box>;
41
+ groupRects: Map<string, Box>;
42
+ width: number;
43
+ lanes: Map<number, Lane>;
44
+ /** The height of the one horizontal run an edge between two adjacent rows needs. */
45
+ runs: Map<number, number>;
46
+ /** Edges whose two ends ended up in different wrapped columns: they are read across, not down. */
47
+ across: Set<number>;
48
+ }
49
+ /** Geometry constants in one place: a container with different proportions changes these, not the rules. */
50
+ export declare const MARGIN = 20;
51
+ /** The text a node will draw, measured from its label - the id is what the source called it, not what it says. */
52
+ export declare function textOf(node: DiagramNode): TextBox;
53
+ export declare function place(diagram: Diagram, top: number): Placement;
54
+ /** The aspect ratio of a placement, for the tests that check a drawing is a page and not a strip. */
55
+ export declare function placementAspect(placement: Placement): number;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Orthogonal edge routing around boxes.
3
+ *
4
+ * A straight line between two boxes is right until a third box sits between them; then it is a line drawn through a
5
+ * component, which reads as a connection that is not there. Graphviz solves this by routing along a visibility
6
+ * graph and then fitting splines; ELK, yFiles and mxGraph build a *sparse* grid from the boxes' own edges and run
7
+ * A* over it with a bend penalty. Sparse is the whole trick: a pixel grid is O(pixels²), while the candidate lines
8
+ * are the coordinate of every box edge plus a margin - O(boxes) per axis.
9
+ *
10
+ * So this module does exactly that, in the two steps those routers use:
11
+ *
12
+ * 1. **Candidate lines.** Every box contributes four: one lane just outside each side. Together with the anchor
13
+ * coordinates they form the grid a route may travel on.
14
+ * 2. **A* with bends.** Moving along a candidate line costs its length; changing direction costs extra, because a
15
+ * route that zig-zags to save three points is harder to follow than one that goes around.
16
+ *
17
+ * A route is only computed for an edge that needs it: the cheap straight segment is tried first, and routing is the
18
+ * fallback. That keeps the common case - a box directly above another - at two points and no search.
19
+ *
20
+ * The layered painter mostly does not ask: an edge between two rows and an edge with a lane are both built from the
21
+ * channel stage's answer, because a route searched on its own cannot know which lanes the other edges already hold.
22
+ * What is left for this router is the drawing the layers do not describe - an edge across two wrapped columns, an
23
+ * edge one of whose ends is a boundary - and the fallback when a channel route would meet a box.
24
+ */
25
+ import type { Point } from './paint.js';
26
+ /** A rectangle an edge may not cross. */
27
+ export interface Obstacle {
28
+ x: number;
29
+ y: number;
30
+ width: number;
31
+ height: number;
32
+ }
33
+ /** The four sides a route may leave a box from. */
34
+ export type Side = 'top' | 'bottom' | 'left' | 'right';
35
+ /** The length of the straight run an edge takes before it is allowed to turn. */
36
+ export declare const ROUTE_STUB = 16;
37
+ /** The outward normal of a side: the direction an edge leaves along. */
38
+ export declare function normalOf(side: Side): Point;
39
+ /** A segment crossing a box at all, with a little slack so a route does not shave a corner. */
40
+ export declare function crossesBox(x1: number, y1: number, x2: number, y2: number, box: Obstacle): boolean;
41
+ /** The point on a side of a box, in the middle of that side. */
42
+ export declare function anchor(box: Obstacle, side: Side): Point;
43
+ /**
44
+ * Which side of `from` an edge leaves by, and which side of `to` it arrives at.
45
+ *
46
+ * The side is a property of the drawing, not of the distance between two centres. A layered drawing puts its boxes
47
+ * in horizontal rows, and an edge between two rows approaches vertically: out of the bottom, into the top, and the
48
+ * other way round for an edge drawn against the flow. Only two boxes in the same row are joined sideways, and then
49
+ * the direction of travel decides which side.
50
+ *
51
+ * This replaces the old rule, which compared the two centres' dx and dy and picked the larger axis - so an edge
52
+ * between two rows left through a box's side as soon as the target was far enough sideways, which is the hook a
53
+ * top-down drawing is read as a defect.
54
+ */
55
+ export declare function approachSides(from: Obstacle, to: Obstacle): {
56
+ exit: Side;
57
+ entry: Side;
58
+ };
59
+ /**
60
+ * Route from one box to another, going around any box in the way.
61
+ *
62
+ * Returns the polyline to draw. When nothing blocks the straight segment, or when every candidate is blocked (a
63
+ * single box that fills the drawing), the straight line is returned and the caller keeps a two-point shape - an
64
+ * unroutable edge is still an edge, and drawing it straight is more honest than dropping it.
65
+ */
66
+ export interface Ports {
67
+ exit: Point;
68
+ entry: Point;
69
+ exitSide: Side;
70
+ entrySide: Side;
71
+ }
72
+ export declare function routeEdge(from: Obstacle, to: Obstacle, obstacles: Obstacle[], anchors?: Ports): {
73
+ points: Point[];
74
+ routed: boolean;
75
+ };
76
+ /** Drop the points in the middle of a straight run: three collinear points are one segment. */
77
+ export declare function simplify(points: Point[]): Point[];
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shapes to SVG markup.
3
+ *
4
+ * The one module that knows tags, so it is also the one place where a shape can silently disappear if a
5
+ * writer forgets a case: the writer table has an entry per shape kind, and the caller counts the drops
6
+ * instead of hiding them.
7
+ *
8
+ * Three containers read this output - an inline card, a full-screen view, and a sidebar tab - so the markup
9
+ * carries no size of its own beyond its `viewBox`: the container decides how large the drawing is, and the
10
+ * same string serves a thumbnail and a whiteboard.
11
+ *
12
+ * Pure, and the output is asserted as text: escaping, geometry, arrowheads, wrapped captions.
13
+ */
14
+ import { type Canvas } from './paint.js';
15
+ import type { Palette } from './palette.js';
16
+ export declare function toSvg(canvas: Canvas, colours: Palette, label?: string): {
17
+ markup: string;
18
+ dropped: number;
19
+ };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The geometry of a node's symbol, shared by the module that draws it and the module that attaches edges to it.
3
+ *
4
+ * A box is a rectangle, but a symbol is not: the diamond's side midpoints are its vertices, a circle has no side at
5
+ * all, and a cylinder's top is an arc. Any rule that decides where an edge may meet a node therefore needs the
6
+ * symbol's own points - and the module that draws the symbol needs the same numbers, or the anchor and the outline
7
+ * disagree and an edge ends in the air.
8
+ *
9
+ * So the arithmetic lives here once: the painter reads a port from it, the writer draws the shape from it, and a
10
+ * change to a symbol's outline moves both. Pure data and pure functions, no SVG markup.
11
+ */
12
+ import type { Box, Point } from './place.js';
13
+ import type { NodeShape } from './model.js';
14
+ /** A cubic Bezier, as the writer draws a curve and the painter reads a point from it. */
15
+ export interface Cubic {
16
+ from: Point;
17
+ c1: Point;
18
+ c2: Point;
19
+ to: Point;
20
+ }
21
+ /** Where the two caps of a cylinder start and end, and how deep each one is. */
22
+ export interface CylinderCaps {
23
+ /** How far the cap reaches into the box: the writer's `min(10, h / 3)`. */
24
+ cap: number;
25
+ /** The top cap, drawn from its left shoulder to its right shoulder. */
26
+ top: Cubic;
27
+ /** The bottom cap, drawn from its right shoulder to its left shoulder. */
28
+ bottom: Cubic;
29
+ }
30
+ /** A point on a cubic Bezier at parameter `t`, which is what makes "the top of the cap" a shared fact. */
31
+ export declare function cubicPoint(curve: Cubic, t: number): Point;
32
+ /**
33
+ * A diamond's four points, in drawing order: top, right, bottom, left.
34
+ *
35
+ * These are the only four places an edge may meet a diamond: each side of the box meets the symbol at exactly that
36
+ * point, and every other point on the side is in the air inside or outside the outline.
37
+ */
38
+ export declare function diamondPoints(box: Box): [Point, Point, Point, Point];
39
+ /**
40
+ * Does this symbol meet each of its four sides at exactly one point?
41
+ *
42
+ * A diamond's side midpoints are its four vertices, and an ellipse's are its four cardinal points: both offer one legal
43
+ * port per side and no run to spread along. Every other symbol offers a run on at least one side - a rectangle along
44
+ * its whole side, a hexagon along the flat between its cut corners, a stadium between its end caps, a cylinder along
45
+ * its straight flank - so a port there has a place to sit rather than a point to take.
46
+ *
47
+ * This is the distinction the painter needs: a node with one legal point per side can give its edges a point each,
48
+ * and a node with a run can only give them places along it. The two points the discrete shapes offer are the same
49
+ * four points of the box, which is why one answer serves both.
50
+ */
51
+ export declare function hasDiscretePorts(shape: NodeShape | undefined): boolean;
52
+ /** How much of each end of a hexagon the corner cuts away. */
53
+ export declare function hexagonCut(width: number): number;
54
+ /**
55
+ * A hexagon's six points, in drawing order: top-left, top-right, right, bottom-right, bottom-left, left.
56
+ *
57
+ * The top and bottom are flat runs between the first two and the last two points; the left and right sides are the
58
+ * single points at index 5 and 2, because the corner cut removed everything else.
59
+ */
60
+ export declare function hexagonPoints(box: Box): [Point, Point, Point, Point, Point, Point];
61
+ /** The radius of a stadium's end caps: half its height, unless the box is narrower than that. */
62
+ export declare function stadiumRadius(box: Box): number;
63
+ /**
64
+ * A cylinder's two caps.
65
+ *
66
+ * The writer draws the top cap from the left shoulder to the right shoulder with the box's own top edge as the
67
+ * control line, and the bottom cap mirrored. The cap curve's apex - the point the painter attaches a top or bottom
68
+ * port to - is therefore a property of this curve, not a constant: it sits a quarter of the cap's depth inside the
69
+ * box, not `cap` inside it.
70
+ */
71
+ export declare function cylinderCaps(box: Box): CylinderCaps;
72
+ /** The apex of a cylinder's top or bottom cap: the only point on that side an edge meets the outline at. */
73
+ export declare function cylinderApex(box: Box, side: 'top' | 'bottom'): Point;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Text as the layout sees it: measured, broken into lines, and turned into a box.
3
+ *
4
+ * Mermaid and Graphviz both measure a label in the platform's own text engine and then lay out around that
5
+ * measurement - a node's size is its text plus padding, and a label too long for its maximum width is wrapped
6
+ * before anything is placed. There is no DOM here, so the measurement is an estimate over a per-character width
7
+ * table; what matters is that the *order* is the same: text first, geometry second. A renderer that fixes the box
8
+ * and pours text into it cannot help overflowing.
9
+ *
10
+ * Explicit breaks (`<br>`, `<br/>`, `<br />`, a real newline) are honoured as written, because a source that put
11
+ * one there wanted that line to end.
12
+ */
13
+ /** A line longer than this wraps on a space, or breaks in the middle when it has none. */
14
+ export declare const MAX_LINE_CHARS = 30;
15
+ /** Height of one line of box text, and the breathing room around the block. */
16
+ export declare const LINE_HEIGHT = 15;
17
+ export declare const TEXT_PADDING = 12;
18
+ /** The smallest and largest box a label may produce. */
19
+ export declare const MIN_NODE_WIDTH = 88;
20
+ export declare const MAX_NODE_WIDTH = 260;
21
+ /** The lines a label asks for, before wrapping: an explicit break is a line end, not a space. */
22
+ export declare function explicitLines(label: string): string[];
23
+ /**
24
+ * Wrap one line to the maximum width.
25
+ *
26
+ * A word longer than the limit is broken rather than allowed to overflow: a single token with no spaces is the
27
+ * common case in identifiers, and letting it run off the box is the defect this function exists to prevent.
28
+ */
29
+ export declare function wrapLine(line: string, limit?: number): string[];
30
+ export interface Measured {
31
+ lines: string[];
32
+ width: number;
33
+ height: number;
34
+ }
35
+ /** The text block a label produces: its lines, and the box it needs. */
36
+ export declare function measure(label: string): Measured;
37
+ /** Rough width of a single-line piece of text, for placing an edge label without a box. */
38
+ export declare function textWidth(text: string): number;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * What a tool card shows, reduced to a drawing.
3
+ *
4
+ * The input is what the host hands a settled tool view: a phase and a block whose shape differs per phase.
5
+ * A settled call carries the paired call's arguments as a **lazy view** (`PartialArguments`) and the result
6
+ * as content blocks, so nothing here reads `block.args.model`: the view is asked, and the report text is
7
+ * parsed for the two facts a drawing needs. Every reader is total - an absent field removes a shape rather
8
+ * than throwing, because the same view renders during streaming.
9
+ *
10
+ * Pure. Everything decided here is a fact a test can assert: which nodes exist, which edges, why they are
11
+ * coloured the way they are.
12
+ */
13
+ import type { Diagram } from './model.js';
14
+ export interface ViewInput {
15
+ phase?: unknown;
16
+ toolName?: unknown;
17
+ /** The whole block the host hands a view; its shape depends on the phase. */
18
+ block?: unknown;
19
+ }
20
+ export declare function viewModel(input: ViewInput): Diagram;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Wrapping: turning one endless column into a page.
3
+ *
4
+ * A chain of twelve boxes has twelve layers, and layering alone draws it as a single column taller than anything a
5
+ * reader can take in - a strip, not a drawing. Graphviz and ELK both answer this with a wrapping pass: cut the layer
6
+ * sequence into bands and stand the bands side by side, so reading order becomes "down the page, then the next
7
+ * column", the way text wraps.
8
+ *
9
+ * The cut is chosen, not guessed: each candidate number of bands is scored by how close the packed page gets to the
10
+ * aspect ratio a page and a landscape screen share, and the best one wins. A drawing that is already close to that
11
+ * shape is left alone - wrapping exists for the strip, not for every tall drawing.
12
+ *
13
+ * Pure: layer sizes in, band ranges and offsets out. The caller applies them.
14
+ */
15
+ export interface BandRange {
16
+ /** The first and last layer the band holds, inclusive. */
17
+ first: number;
18
+ last: number;
19
+ /** How far right the band stands, relative to the first one. */
20
+ offsetX: number;
21
+ }
22
+ export interface BandPlan {
23
+ bands: BandRange[];
24
+ width: number;
25
+ height: number;
26
+ }
27
+ export declare function planBands(layerWidth: number[], layerHeight: number[], gap: number, aspect?: number): BandPlan;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Writing a drawing to disk: the CLI's job, kept out of the renderer.
3
+ *
4
+ * `render/index.ts` is bundled into the page as well as run in Node, so it may not import a filesystem;
5
+ * everything that touches one lives here, where only the host reaches it.
6
+ */
7
+ import { type RenderInput, type Rendered } from './index.js';
8
+ import type { Palette, PaletteOptions } from './palette.js';
9
+ /** Write the drawing where a document can link to it. Returns the path, the byte count and the render. */
10
+ export declare function writeSvg(input: RenderInput, path: string, overrides?: Partial<Palette>, options?: PaletteOptions): {
11
+ path: string;
12
+ bytes: number;
13
+ rendered: Rendered;
14
+ };