@dxos/diagram 0.11.1 → 0.13.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 (171) hide show
  1. package/dist/lib/chunk-parse.mjs +9396 -0
  2. package/dist/lib/chunk-parse.mjs.map +1 -0
  3. package/dist/lib/extension.mjs +204 -0
  4. package/dist/lib/extension.mjs.map +1 -0
  5. package/dist/lib/index.mjs +497 -2846
  6. package/dist/lib/index.mjs.map +1 -1
  7. package/dist/types/src/aesthetics.d.ts +12 -0
  8. package/dist/types/src/aesthetics.d.ts.map +1 -0
  9. package/dist/types/src/aesthetics.test.d.ts +2 -0
  10. package/dist/types/src/aesthetics.test.d.ts.map +1 -0
  11. package/dist/types/src/appeal.d.ts +55 -0
  12. package/dist/types/src/appeal.d.ts.map +1 -0
  13. package/dist/types/src/appeal.test.d.ts +2 -0
  14. package/dist/types/src/appeal.test.d.ts.map +1 -0
  15. package/dist/types/src/architecture.d.ts +86 -0
  16. package/dist/types/src/architecture.d.ts.map +1 -0
  17. package/dist/types/src/architecture.test.d.ts +2 -0
  18. package/dist/types/src/architecture.test.d.ts.map +1 -0
  19. package/dist/types/src/compact.d.ts +39 -0
  20. package/dist/types/src/compact.d.ts.map +1 -0
  21. package/dist/types/src/compact.test.d.ts +2 -0
  22. package/dist/types/src/compact.test.d.ts.map +1 -0
  23. package/dist/types/src/diagnostics.d.ts +70 -3
  24. package/dist/types/src/diagnostics.d.ts.map +1 -1
  25. package/dist/types/src/dsl/cli.d.ts +2 -0
  26. package/dist/types/src/dsl/cli.d.ts.map +1 -0
  27. package/dist/types/src/dsl/compile.d.ts +12 -0
  28. package/dist/types/src/dsl/compile.d.ts.map +1 -0
  29. package/dist/types/src/dsl/convert.d.ts +45 -0
  30. package/dist/types/src/dsl/convert.d.ts.map +1 -0
  31. package/dist/types/src/dsl/convert.test.d.ts +2 -0
  32. package/dist/types/src/dsl/convert.test.d.ts.map +1 -0
  33. package/dist/types/src/dsl/dsl.test.d.ts +2 -0
  34. package/dist/types/src/dsl/dsl.test.d.ts.map +1 -0
  35. package/dist/types/src/dsl/gen/diagram.d.ts +3 -0
  36. package/dist/types/src/dsl/gen/diagram.d.ts.map +1 -0
  37. package/dist/types/src/dsl/index.d.ts +7 -0
  38. package/dist/types/src/dsl/index.d.ts.map +1 -0
  39. package/dist/types/src/dsl/parse.d.ts +49 -0
  40. package/dist/types/src/dsl/parse.d.ts.map +1 -0
  41. package/dist/types/src/dsl/print.d.ts +19 -0
  42. package/dist/types/src/dsl/print.d.ts.map +1 -0
  43. package/dist/types/src/dsl/semantic.test.d.ts +2 -0
  44. package/dist/types/src/dsl/semantic.test.d.ts.map +1 -0
  45. package/dist/types/src/dsl/vocabulary.d.ts +61 -0
  46. package/dist/types/src/dsl/vocabulary.d.ts.map +1 -0
  47. package/dist/types/src/dx-svg.d.ts +12 -0
  48. package/dist/types/src/dx-svg.d.ts.map +1 -0
  49. package/dist/types/src/dx-svg.test.d.ts +2 -0
  50. package/dist/types/src/dx-svg.test.d.ts.map +1 -0
  51. package/dist/types/src/extension/complete.d.ts +3 -0
  52. package/dist/types/src/extension/complete.d.ts.map +1 -0
  53. package/dist/types/src/extension/extension.test.d.ts +2 -0
  54. package/dist/types/src/extension/extension.test.d.ts.map +1 -0
  55. package/dist/types/src/extension/highlight.d.ts +14 -0
  56. package/dist/types/src/extension/highlight.d.ts.map +1 -0
  57. package/dist/types/src/extension/index.d.ts +6 -0
  58. package/dist/types/src/extension/index.d.ts.map +1 -0
  59. package/dist/types/src/extension/language.d.ts +13 -0
  60. package/dist/types/src/extension/language.d.ts.map +1 -0
  61. package/dist/types/src/extension/lint.d.ts +21 -0
  62. package/dist/types/src/extension/lint.d.ts.map +1 -0
  63. package/dist/types/src/extension/syntax.d.ts +3 -0
  64. package/dist/types/src/extension/syntax.d.ts.map +1 -0
  65. package/dist/types/src/index.d.ts +14 -0
  66. package/dist/types/src/index.d.ts.map +1 -1
  67. package/dist/types/src/mermaid-engine.d.ts +40 -1
  68. package/dist/types/src/mermaid-engine.d.ts.map +1 -1
  69. package/dist/types/src/mermaid.d.ts +3 -3
  70. package/dist/types/src/mermaid.d.ts.map +1 -1
  71. package/dist/types/src/nudge.d.ts +30 -0
  72. package/dist/types/src/nudge.d.ts.map +1 -0
  73. package/dist/types/src/nudge.test.d.ts +2 -0
  74. package/dist/types/src/nudge.test.d.ts.map +1 -0
  75. package/dist/types/src/objective.d.ts +4 -0
  76. package/dist/types/src/objective.d.ts.map +1 -1
  77. package/dist/types/src/ortho-router.d.ts +13 -1
  78. package/dist/types/src/ortho-router.d.ts.map +1 -1
  79. package/dist/types/src/ports.d.ts +40 -0
  80. package/dist/types/src/ports.d.ts.map +1 -0
  81. package/dist/types/src/ports.test.d.ts +2 -0
  82. package/dist/types/src/ports.test.d.ts.map +1 -0
  83. package/dist/types/src/relations.test.d.ts +2 -0
  84. package/dist/types/src/relations.test.d.ts.map +1 -0
  85. package/dist/types/src/rules.d.ts +30 -0
  86. package/dist/types/src/rules.d.ts.map +1 -0
  87. package/dist/types/src/scene.d.ts +99 -65
  88. package/dist/types/src/scene.d.ts.map +1 -1
  89. package/dist/types/src/score.d.ts +54 -0
  90. package/dist/types/src/score.d.ts.map +1 -0
  91. package/dist/types/src/score.test.d.ts +2 -0
  92. package/dist/types/src/score.test.d.ts.map +1 -0
  93. package/dist/types/src/semantic-engine.d.ts +41 -0
  94. package/dist/types/src/semantic-engine.d.ts.map +1 -0
  95. package/dist/types/src/semantic-engine.test.d.ts +2 -0
  96. package/dist/types/src/semantic-engine.test.d.ts.map +1 -0
  97. package/dist/types/src/semantic-place.d.ts +81 -0
  98. package/dist/types/src/semantic-place.d.ts.map +1 -0
  99. package/dist/types/src/semantic-route.d.ts +133 -0
  100. package/dist/types/src/semantic-route.d.ts.map +1 -0
  101. package/dist/types/src/semantic.d.ts +164 -0
  102. package/dist/types/src/semantic.d.ts.map +1 -0
  103. package/dist/types/src/uml-grid.d.ts.map +1 -1
  104. package/dist/types/src/uml.d.ts +3 -5
  105. package/dist/types/src/uml.d.ts.map +1 -1
  106. package/dist/types/src/view.d.ts +53 -0
  107. package/dist/types/src/view.d.ts.map +1 -0
  108. package/dist/types/src/view.test.d.ts +2 -0
  109. package/dist/types/src/view.test.d.ts.map +1 -0
  110. package/dist/types/tsconfig.tsbuildinfo +1 -1
  111. package/package.json +23 -5
  112. package/rules/DIAGRAM.mdl +388 -0
  113. package/src/aesthetics.test.ts +89 -0
  114. package/src/aesthetics.ts +148 -0
  115. package/src/appeal.test.ts +144 -0
  116. package/src/appeal.ts +586 -0
  117. package/src/architecture.test.ts +94 -0
  118. package/src/architecture.ts +215 -0
  119. package/src/compact.test.ts +67 -0
  120. package/src/compact.ts +219 -0
  121. package/src/diagnostics.test.ts +207 -1
  122. package/src/diagnostics.ts +481 -37
  123. package/src/dsl/cli.ts +46 -0
  124. package/src/dsl/compile.ts +28 -0
  125. package/src/dsl/convert.test.ts +51 -0
  126. package/src/dsl/convert.ts +98 -0
  127. package/src/dsl/diagram.grammar +195 -0
  128. package/src/dsl/dsl.test.ts +286 -0
  129. package/src/dsl/gen/diagram.ts +22 -0
  130. package/src/dsl/index.ts +44 -0
  131. package/src/dsl/parse.ts +1054 -0
  132. package/src/dsl/print.ts +198 -0
  133. package/src/dsl/semantic.test.ts +200 -0
  134. package/src/dsl/vocabulary.ts +150 -0
  135. package/src/dx-svg.test.ts +44 -0
  136. package/src/dx-svg.ts +77 -0
  137. package/src/extension/complete.ts +105 -0
  138. package/src/extension/extension.test.ts +135 -0
  139. package/src/extension/highlight.ts +23 -0
  140. package/src/extension/index.ts +9 -0
  141. package/src/extension/language.ts +31 -0
  142. package/src/extension/lint.ts +60 -0
  143. package/src/extension/syntax.ts +62 -0
  144. package/src/index.ts +14 -0
  145. package/src/mermaid-engine.test.ts +6 -6
  146. package/src/mermaid-engine.ts +569 -119
  147. package/src/mermaid.test.ts +17 -1
  148. package/src/mermaid.ts +30 -18
  149. package/src/nudge.test.ts +155 -0
  150. package/src/nudge.ts +385 -0
  151. package/src/objective.test.ts +2 -0
  152. package/src/objective.ts +17 -1
  153. package/src/ortho-router.ts +240 -80
  154. package/src/ports.test.ts +157 -0
  155. package/src/ports.ts +186 -0
  156. package/src/relations.test.ts +98 -0
  157. package/src/rules.ts +117 -0
  158. package/src/scene.ts +64 -3
  159. package/src/score.test.ts +64 -0
  160. package/src/score.ts +130 -0
  161. package/src/semantic-engine.test.ts +635 -0
  162. package/src/semantic-engine.ts +888 -0
  163. package/src/semantic-place.ts +1508 -0
  164. package/src/semantic-route.ts +1136 -0
  165. package/src/semantic.ts +278 -0
  166. package/src/typings.d.ts +8 -0
  167. package/src/uml-grid.ts +6 -1
  168. package/src/uml.test.ts +5 -1
  169. package/src/uml.ts +24 -11
  170. package/src/view.test.ts +139 -0
  171. package/src/view.ts +376 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/diagram",
3
- "version": "0.11.1",
3
+ "version": "0.13.0",
4
4
  "description": "Renderer-neutral scene DSL, diagram dialects, and layout engines.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -18,6 +18,11 @@
18
18
  "types": "./dist/types/src/index.d.ts",
19
19
  "import": "./dist/lib/index.mjs"
20
20
  },
21
+ "./extension": {
22
+ "source": "./src/extension/index.ts",
23
+ "types": "./dist/types/src/extension/index.d.ts",
24
+ "import": "./dist/lib/extension.mjs"
25
+ },
21
26
  "./testing": {
22
27
  "source": "./src/testing.ts",
23
28
  "types": "./dist/types/src/testing.d.ts",
@@ -28,19 +33,32 @@
28
33
  "types": "dist/types/src/index.d.ts",
29
34
  "files": [
30
35
  "dist",
36
+ "rules",
31
37
  "src"
32
38
  ],
33
39
  "dependencies": {
40
+ "@codemirror/autocomplete": "^6.20.3",
41
+ "@codemirror/language": "^6.12.4",
42
+ "@codemirror/lint": "6.9.7",
43
+ "@codemirror/state": "^6.7.6",
44
+ "@codemirror/view": "^6.43.13",
34
45
  "@dagrejs/dagre": "^3.1.1",
46
+ "@lezer/common": "^1.5.2",
47
+ "@lezer/highlight": "^1.2.3",
48
+ "@lezer/lr": "^1.4.10",
35
49
  "elkjs": "^0.12.0",
36
- "@dxos/invariant": "0.11.1",
37
- "@dxos/util": "0.11.1"
50
+ "@dxos/effect": "0.13.0",
51
+ "@dxos/invariant": "0.13.0",
52
+ "@dxos/util": "0.13.0"
38
53
  },
39
54
  "devDependencies": {
40
- "effect": "4.0.0-rc.115"
55
+ "@lezer/generator": "^1.8.0",
56
+ "@uiw/codemirror-theme-vscode": "^4.25.2",
57
+ "effect": "4.0.0",
58
+ "tsx": "^4.21.0"
41
59
  },
42
60
  "peerDependencies": {
43
- "effect": "4.0.0-rc.115"
61
+ "effect": "4.0.0"
44
62
  },
45
63
  "publishConfig": {
46
64
  "access": "public"
@@ -0,0 +1,388 @@
1
+ ---
2
+ id: org.dxos.diagram.rules
3
+ name: Diagram rules
4
+ version: 0.1.0
5
+ ---
6
+
7
+ # Diagram rules
8
+
9
+ What makes an architecture diagram read well to a person, as rules a layout can be scored against.
10
+ `@dxos/diagram` loads this file (`Rules.RULES`); `Appeal` measures every `code` and `both` rule from the
11
+ scene's geometry, and the image judge asks every `vision` and `both` rule of a decision model shown
12
+ the rendered page. `weight` ranks the rules by the strength of the evidence behind them: crossings
13
+ first in every study that measured them, then orthogonality and flow, path continuity, bends, and
14
+ only then alignment, symmetry and compactness, which people prefer but trade against the rest.
15
+
16
+ Rules about what a diagram says (a title, a legend, labels naming technology, one level of
17
+ abstraction) are not here: they belong to `Architecture.RULES`, which judges content, and a layout
18
+ engine cannot change them.
19
+
20
+ Evidence keys used in `Source:` lines:
21
+
22
+ - Purchase 1997: "Which aesthetic has the greatest effect on human understanding?", GD'97, LNCS 1353.
23
+ - Purchase 2002: Purchase, Carrington & Allder, "Empirical evaluation of aesthetics-based graph layout",
24
+ Empirical Software Engineering 7(3), and "Graph layout aesthetics in UML diagrams", JGAA 6(3).
25
+ - Ware 2002: Ware, Purchase, Colpoys & McGill, "Cognitive measurements of graph aesthetics",
26
+ Information Visualization 1.
27
+ - HOLA: Kieffer, Dwyer, Marriott & Wybrow, "HOLA: Human-like orthogonal network layout", IEEE TVCG
28
+ 22(1), 2016 (InfoVis 2015).
29
+ - Ware 2020: Colin Ware, *Information Visualization: Perception for Design*, 4th ed., ch. 6 (Gestalt).
30
+ - Imhof 1975: "Positioning names on maps", The American Cartographer 2(2).
31
+ - C4: Simon Brown, the C4 model notation and diagram review checklist, https://c4model.com.
32
+ - Tufte: *The Visual Display of Quantitative Information* (data-ink) and *Envisioning Information*.
33
+ - Bertin: *Semiology of Graphics* (visual variables).
34
+
35
+ ## Extensions
36
+
37
+ | Term | URI |
38
+ | -------------- | ----------------------------- |
39
+ | `diagram-rule` | org.dxos.mdl.diagram-rule@1.0 |
40
+
41
+ ## Crossings and edges
42
+
43
+ ```mdl
44
+ diagram-rule no-edge-crossings: Arrows do not cross each other
45
+ category: crossings
46
+ evaluator: both
47
+ weight: 10
48
+ question: Can every arrow be followed without it crossing another arrow?
49
+ yes: No two arrows cross, or one crossing is unavoidable and clearly at a right angle.
50
+ no: Several arrows cross each other.
51
+ The most damaging aesthetic in every study that measured it: it slows tracing and causes misreads.
52
+ Code: the number of properly crossing connector pairs, scored 1 / (1 + crossings / 2).
53
+ Source: Purchase 1997; Purchase 2002; Ware 2002; HOLA.
54
+ ```
55
+
56
+ ```mdl
57
+ diagram-rule no-edge-through-node: Arrows never pass through a box they do not connect
58
+ category: edges
59
+ evaluator: code
60
+ weight: 8
61
+ An arrow running across a box reads as a connection to it ("edge tunnelling").
62
+ Code: 1 when no connector has an interior run through a box other than a frame, else 0.
63
+ Source: Dunne et al. 2015, IBM J. R&D 59; libavoid (Wybrow, Marriott & Stuckey 2010).
64
+ ```
65
+
66
+ ```mdl
67
+ diagram-rule no-shared-trunks: Distinct arrows never run along the same line
68
+ category: edges
69
+ evaluator: both
70
+ weight: 6
71
+ question: Does every arrow have its own line, with no two arrows merged into one shared trunk?
72
+ yes: Each arrow runs on its own line; parallel arrows keep a visible gap.
73
+ no: Two or more arrows share a segment, so their ends cannot be paired up.
74
+ Overlapping segments make it impossible to tell which arrow goes where; routers nudge parallel runs
75
+ apart for this reason. A deliberate merge of same-meaning arrows into one target (an inheritance
76
+ bus) does not count.
77
+ Code: connector pairs that share a collinear run, scored 1 / (1 + pairs).
78
+ Source: Wybrow et al. 2010 (nudging); Purchase 2002 ("no adjacent arrows" preferred).
79
+ ```
80
+
81
+ ```mdl
82
+ diagram-rule edges-head-toward-target: Each arrow takes a direct route to its target
83
+ category: edges
84
+ evaluator: both
85
+ weight: 5
86
+ question: Does each arrow run fairly directly from its source to its target, without long detours or doubling back?
87
+ yes: Arrows take short, direct routes.
88
+ no: Some arrows loop around the page, leave in the wrong direction, or double back.
89
+ Readers predict where an arrow ends from the way it leaves its source, and path length is the
90
+ strongest predictor of tracing time.
91
+ Code: per connector, route length over the gap between its two boxes (at least 64 units); the share
92
+ of connectors within twice that, softened by the mean excess. Measured against the boxes rather than
93
+ the route's own ends, so a loop round the page and a pair of related boxes placed far apart both count.
94
+ Source: Huang, Eades & Hong 2009 (geodesic-path tendency); Ware 2002.
95
+ ```
96
+
97
+ ```mdl
98
+ diagram-rule few-bends: Each arrow bends no more than its ends require
99
+ category: edges
100
+ evaluator: code
101
+ weight: 4
102
+ Bends rank below crossings, orthogonality and flow, but each one costs tracing time. Zero bends
103
+ when the boxes line up, one for an L, two for a Z.
104
+ Code: mean bends per connector beyond the minimum its end points allow, scored 1 / (1 + excess).
105
+ Source: Purchase 1997; Purchase 2002; Tamassia 1987.
106
+ ```
107
+
108
+ ```mdl
109
+ diagram-rule straight-when-aligned: Boxes that face each other are joined by a straight arrow
110
+ category: edges
111
+ evaluator: code
112
+ weight: 3
113
+ When two connected boxes overlap across the flow, the arrow between them should be one straight
114
+ segment: the cheapest bend saved, and collinearity aids recall.
115
+ Code: among connectors whose end boxes overlap horizontally or vertically, the share drawn with no bend.
116
+ Source: ELK Brandes-Köpf straightening; Marriott et al. 2012 (collinearity aids recall).
117
+ ```
118
+
119
+ ```mdl
120
+ diagram-rule uniform-edge-length: Arrows have similar lengths
121
+ category: edges
122
+ evaluator: code
123
+ weight: 2
124
+ Human-drawn layouts have uniform edge lengths, and their spread correlates with dislike.
125
+ Code: 1 − min(1, coefficient of variation of connector lengths).
126
+ Source: HOLA (R8); Dwyer et al. 2009; Purchase, Pilcher & Plimmer 2012.
127
+ ```
128
+
129
+ ## Flow
130
+
131
+ ```mdl
132
+ diagram-rule consistent-flow: Most arrows run the same way
133
+ category: flow
134
+ evaluator: both
135
+ weight: 5
136
+ question: Do nearly all arrows run in one main direction (down, or left to right), with only a few pointing back?
137
+ yes: The arrows share one direction; a reader follows dependencies one way.
138
+ no: Arrows run in mixed directions, or whole chains point back against the flow.
139
+ Information flow ranks third for software diagrams, after crossings and orthogonality. Arrows with
140
+ a hollow triangle head point to an abstraction above by convention and do not count against it.
141
+ Code: the share of connectors whose net displacement runs with the diagram's dominant direction.
142
+ Source: Purchase 2002; Sugiyama, Tagawa & Toda 1981.
143
+ ```
144
+
145
+ ```mdl
146
+ diagram-rule continuous-paths: A chain through a box continues on its far side
147
+ category: flow
148
+ evaluator: code
149
+ weight: 2
150
+ A box with one arrow in and one out reads as a step on a path when the arrow leaves on the side
151
+ opposite the one it entered by; turning back on the same side breaks the line of reading.
152
+ Code: for boxes with one incoming and one outgoing connector, 1 for opposite sides, 0.5 adjacent, 0 same.
153
+ Source: Ware 2002 (continuity); HOLA (R2).
154
+ ```
155
+
156
+ ## Alignment
157
+
158
+ ```mdl
159
+ diagram-rule grid-alignment: Boxes line up in rows and columns
160
+ category: alignment
161
+ evaluator: both
162
+ weight: 3
163
+ question: Do the boxes line up in clear rows and columns?
164
+ yes: Most boxes share a row or column with two or more others.
165
+ no: Boxes are staggered, with few clear rows or columns.
166
+ People prefer grid-like placement and choose it when they draw by hand.
167
+ Code: the share of boxes whose centre lines up with at least two others horizontally or vertically.
168
+ Source: HOLA (R4); Purchase, Pilcher & Plimmer 2012.
169
+ ```
170
+
171
+ ```mdl
172
+ diagram-rule no-near-misses: Boxes are either aligned or clearly apart
173
+ category: alignment
174
+ evaluator: code
175
+ weight: 2
176
+ Two boxes almost in line look like a mistake; it is the most visible sign of an automatic layout.
177
+ Code: box pairs whose centres or edges differ by more than 1 and less than 12 units along an axis,
178
+ scored 1 / (1 + pairs / 2).
179
+ Source: HOLA (the gap between human and algorithmic layouts).
180
+ ```
181
+
182
+ ```mdl
183
+ diagram-rule symmetry: Symmetric structure is drawn symmetrically
184
+ category: alignment
185
+ evaluator: vision
186
+ weight: 1
187
+ question: Where the structure is symmetric, such as a fan-out to similar parts, is it drawn mirror-symmetrically?
188
+ yes: Fan-outs and parallel paths are drawn evenly about an axis.
189
+ no: Parts that play the same role are drawn lopsided or scattered.
190
+ Symmetry aids understanding, recall and preference, though less than crossings.
191
+ Source: Purchase 1997; Marriott et al. 2012; HOLA (R5).
192
+ ```
193
+
194
+ ## Groups
195
+
196
+ ```mdl
197
+ diagram-rule frames-nested: Group frames are either nested or apart, never partly overlapping
198
+ category: groups
199
+ evaluator: code
200
+ weight: 8
201
+ Enclosure is the strongest grouping cue; a frame that partly overlaps another states no hierarchy.
202
+ Code: 1 when every pair of frames is disjoint or one contains the other, else 0.
203
+ Source: Palmer 1992 (common region); Ware 2020.
204
+ ```
205
+
206
+ ```mdl
207
+ diagram-rule frames-aligned: Groups line up with each other
208
+ category: groups
209
+ evaluator: both
210
+ weight: 3
211
+ question: Do the group frames line up with one another, sharing a top or left edge where they sit side by side or stacked?
212
+ yes: Groups share edges and read as one arrangement.
213
+ no: A group floats at an arbitrary height or offset beside another.
214
+ Aligned bands read as layers; ragged frames look accidental.
215
+ Code: the share of frames that share a top, bottom, left or right edge with another frame.
216
+ Source: HOLA (gridiness); Tufte, *Envisioning Information* (layering and separation).
217
+ ```
218
+
219
+ ```mdl
220
+ diagram-rule frames-hug-members: Group frames fit their contents
221
+ category: groups
222
+ evaluator: code
223
+ weight: 2
224
+ A frame much larger than what it holds suggests missing members and wastes space.
225
+ Code: per frame, the area of its members' bounding box over the frame's area, averaged and scaled
226
+ so that 0.5 or more scores 1.
227
+ Source: HOLA (R3 compactness); ELK padding.
228
+ ```
229
+
230
+ ```mdl
231
+ diagram-rule few-frame-crossings: Arrows cross group borders no more than they must
232
+ category: groups
233
+ evaluator: code
234
+ weight: 3
235
+ Every border crossing is noise, and an arrow that leaves and re-enters a group misstates membership.
236
+ An arrow should cross only the borders between its two ends' groups.
237
+ Code: border crossings beyond the minimum the group tree requires, scored 1 / (1 + extra / 2).
238
+ Source: Eades, Feng & Lin 1996 (clustered graphs); Ware 2020 (connectedness and enclosure).
239
+ ```
240
+
241
+ ```mdl
242
+ diagram-rule gestalt-cues-agree: Each box's nearest neighbour is in the same group
243
+ category: groups
244
+ evaluator: code
245
+ weight: 3
246
+ When a box sits closer to a box in another group than to any in its own, proximity contradicts the
247
+ frame and readers misread the grouping.
248
+ Code: the share of boxes whose nearest other box shares their innermost frame.
249
+ Source: Ware 2020 ch. 6; Palmer & Rock 1994.
250
+ ```
251
+
252
+ ## Space
253
+
254
+ ```mdl
255
+ diagram-rule compact-drawing: The drawing uses its area well
256
+ category: space
257
+ evaluator: both
258
+ weight: 3
259
+ question: Is the drawing compact, with no large empty regions between or inside groups?
260
+ yes: The page is evenly filled; connected boxes are near each other.
261
+ no: There are large empty regions, stretched groups, or connected boxes far apart.
262
+ Compactness correlates with preference, and spread lengthens arrows.
263
+ Code: box area over the drawing's area, scored against a target of 0.25 (1 at or above it).
264
+ Source: HOLA (R3); Ware 2002.
265
+ ```
266
+
267
+ ```mdl
268
+ diagram-rule balanced-whitespace: Ink is spread evenly across the page
269
+ category: space
270
+ evaluator: code
271
+ weight: 2
272
+ A crowded half next to an empty one looks unbalanced and suggests a grouping that is not there.
273
+ Code: split the drawing into a 3 × 3 grid; 1 − min(1, CV / 1.5), with CV the coefficient of variation of box area per cell.
274
+ Source: HOLA participant comments; Ngo, Teo & Byrne 2003 (balance).
275
+ ```
276
+
277
+ ```mdl
278
+ diagram-rule uniform-spacing: Gaps between neighbouring boxes are even
279
+ category: space
280
+ evaluator: code
281
+ weight: 2
282
+ Uneven gaps falsely imply grouping by proximity.
283
+ Code: each box's gap to its nearest neighbour; 1 − min(1, coefficient of variation of those gaps).
284
+ Source: ELK and Graphviz node spacing; Davidson & Harel 1996.
285
+ ```
286
+
287
+ ```mdl
288
+ diagram-rule landscape-aspect: The drawing is landscape and fits a screen
289
+ category: space
290
+ evaluator: code
291
+ weight: 2
292
+ A tall strip or a long ribbon has to be scaled down to fit a page, shrinking its text.
293
+ Code: aspect ratio width / height scored 1 / (1 + |ln(ratio / 1.6)|).
294
+ Source: ELK aspectRatio (1.6); Purchase 2002 (width of layout).
295
+ ```
296
+
297
+ ```mdl
298
+ diagram-rule readable-at-page-width: Text stays legible when the drawing fits a page
299
+ category: labels
300
+ evaluator: code
301
+ weight: 3
302
+ Diagrams are read as images in documents and pull requests; a drawing that must shrink below 11
303
+ pixel text to fit 1200 pixels has failed however good its geometry.
304
+ Code: the smallest text size after scaling the drawing to 1200 units wide, over 11, capped at 1.
305
+ Source: C4 (diagrams should stand alone); Tufte (resolution).
306
+ ```
307
+
308
+ ## Labels
309
+
310
+ ```mdl
311
+ diagram-rule labels-clear: Labels never overlap other labels, boxes or lines
312
+ category: labels
313
+ evaluator: both
314
+ weight: 6
315
+ question: Can every label be read on its own, without running into another label, a box or a line?
316
+ yes: Every label sits clear of other text and lines.
317
+ no: Some label overlaps another label, a box or a line, or is cut off.
318
+ Overlapping text is illegible; label placement treats non-overlap as a hard constraint.
319
+ Code: label overlaps counted by `Diagnostics` plus labels that overflow their boxes, scored 1 / (1 + count).
320
+ Source: Imhof 1975; Kakoulis & Tollis 1997.
321
+ ```
322
+
323
+ ```mdl
324
+ diagram-rule labels-attached: Each arrow label sits nearest its own arrow
325
+ category: labels
326
+ evaluator: both
327
+ weight: 3
328
+ question: Is it clear which arrow each arrow label belongs to?
329
+ yes: Every label sits on or right beside its own arrow.
330
+ no: Some label sits between arrows, or nearer another arrow than its own.
331
+ A label equally near two arrows is a misreading waiting to happen.
332
+ Code: the share of arrow labels whose nearest connector is their own.
333
+ Source: Imhof 1975; ELK edge-label placement.
334
+ ```
335
+
336
+ ## Overall
337
+
338
+ ```mdl
339
+ diagram-rule consistent-notation: The same kind of thing always looks the same
340
+ category: hierarchy
341
+ evaluator: vision
342
+ weight: 2
343
+ question: Does each kind of element (box, group, arrow style) look the same everywhere, with no unexplained differences in size, colour or line style?
344
+ yes: Every kind of element has one consistent look.
345
+ no: Similar elements differ in size, colour or line style for no evident reason.
346
+ Size, colour and line style are read as meaning; inconsistent use invites false distinctions.
347
+ Source: Bertin; C4 checklist; Ware 2020 ch. 5.
348
+ ```
349
+
350
+ ```mdl
351
+ diagram-rule minimal-non-data-ink: Nothing is drawn that carries no information
352
+ category: hierarchy
353
+ evaluator: vision
354
+ weight: 2
355
+ question: Is the drawing free of decoration that carries no information, such as background grids, shadows, heavy borders or gradients, with group frames lighter than the boxes they hold?
356
+ yes: Every mark carries meaning; frames recede behind the components.
357
+ no: Background grids, heavy frames or decoration compete with the structure.
358
+ Non-data ink competes with structure; frames are context, so they should recede.
359
+ Source: Tufte (data-ink ratio, layering and separation).
360
+ ```
361
+
362
+ ```mdl
363
+ diagram-rule tidy-overall: The diagram looks deliberately drawn
364
+ category: hierarchy
365
+ evaluator: vision
366
+ weight: 4
367
+ question: Does this look like a tidy diagram a careful designer drew by hand, rather than an automatic layout?
368
+ yes: It looks deliberate: aligned, balanced, uncluttered, easy to scan.
369
+ no: It looks machine-made: ragged, cluttered, with odd gaps or detours.
370
+ The gap between human-drawn and algorithmic layouts is the gap this library exists to close.
371
+ Source: HOLA; Purchase, Pilcher & Plimmer 2012.
372
+ ```
373
+
374
+ ## Appendix
375
+
376
+ ```mdl
377
+ ext diagram-rule
378
+ uri: org.dxos.mdl.diagram-rule@1.0
379
+ desc: A rule a diagram's drawing can be scored against.
380
+ inline-prose: true
381
+ fields:
382
+ category: crossings | flow | alignment | space | groups | labels | edges | hierarchy
383
+ evaluator: code | vision | both
384
+ weight: Number
385
+ question?: Prose
386
+ yes?: Prose
387
+ no?: Prose
388
+ ```
@@ -0,0 +1,89 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as DecisionModel from 'effect/ai/DecisionModel';
6
+ import * as Effect from 'effect/Effect';
7
+ import * as Layer from 'effect/Layer';
8
+ import { describe, test } from 'vitest';
9
+
10
+ import { IMAGE_RULES, RULES, judge } from './aesthetics.ts';
11
+ import { contentOf } from './architecture.ts';
12
+ import { parse } from './mermaid.ts';
13
+ import { evaluate } from './score.ts';
14
+
15
+ const GRAPH = parse('flowchart TB\n A --> B');
16
+
17
+ /** A model that answers every question with 0.7, recording the encoded input and questions it was asked. */
18
+ type Calls = { count: number; questions: string[]; state?: unknown; images?: readonly DecisionModel.Image[] };
19
+
20
+ const stubModel = (calls: Calls, supportsImages = false) =>
21
+ Layer.effect(
22
+ DecisionModel.DecisionModel,
23
+ DecisionModel.make({
24
+ supportsImages,
25
+ decide: ({ decisions, state, images }) =>
26
+ Effect.sync(() => {
27
+ calls.count += 1;
28
+ calls.questions = Object.keys(decisions);
29
+ calls.state = state;
30
+ calls.images = images;
31
+ return {
32
+ answers: Object.fromEntries(
33
+ Object.keys(decisions).map((key) => [key, { _tag: 'Probability' as const, probability: 0.7 }]),
34
+ ),
35
+ usage: { inputTokens: undefined, outputTokens: undefined },
36
+ };
37
+ }),
38
+ }),
39
+ );
40
+
41
+ describe('aesthetics', () => {
42
+ test('rules are judged from the layout in one batched call', ({ expect }) => {
43
+ const calls: Calls = { count: 0, questions: [] };
44
+ const scores = Effect.runSync(
45
+ evaluate([judge()], { content: contentOf(GRAPH, { layout: 'row 1 (top): "A"' }) }).pipe(
46
+ Effect.provide(stubModel(calls)),
47
+ ),
48
+ );
49
+ expect(calls.count).toBe(1);
50
+ expect(calls.state).toMatchObject({ layout: 'row 1 (top): "A"' });
51
+ expect(calls.questions).toEqual(RULES.map(({ key }) => key));
52
+ expect(scores.every(({ kind, score }) => kind === 'aesthetics' && score === 0.7)).toBe(true);
53
+ });
54
+
55
+ test('without a layout nothing is asked and every rule is an error', ({ expect }) => {
56
+ const calls: Calls = { count: 0, questions: [] };
57
+ const scores = Effect.runSync(
58
+ evaluate([judge()], { content: contentOf(GRAPH) }).pipe(Effect.provide(stubModel(calls))),
59
+ );
60
+ expect(calls.count).toBe(0);
61
+ expect(scores.every(({ error }) => error === 'No layout to judge.')).toBe(true);
62
+ });
63
+
64
+ test('an image of the page is judged in place of a layout, by a model that reads images', ({ expect }) => {
65
+ const calls: Calls = { count: 0, questions: [] };
66
+ const images = [{ mediaType: 'image/png', data: 'iVBORw==' }];
67
+ const scores = Effect.runSync(
68
+ evaluate([judge(IMAGE_RULES)], { content: contentOf(GRAPH), images }).pipe(
69
+ Effect.provide(stubModel(calls, true)),
70
+ ),
71
+ );
72
+ expect(calls.count).toBe(1);
73
+ expect(calls.images).toEqual(images);
74
+ expect(IMAGE_RULES.every(({ instructions }) => instructions.includes('the attached image is the page'))).toBe(true);
75
+ expect(scores.every(({ score, error }) => !error && score === 0.7)).toBe(true);
76
+ });
77
+
78
+ test('a model that reads no images scores every rule as an error rather than judging blind', ({ expect }) => {
79
+ const calls: Calls = { count: 0, questions: [] };
80
+ const scores = Effect.runSync(
81
+ evaluate([judge(IMAGE_RULES)], {
82
+ content: contentOf(GRAPH),
83
+ images: [{ mediaType: 'image/png', data: 'iVBORw==' }],
84
+ }).pipe(Effect.provide(stubModel(calls))),
85
+ );
86
+ expect(calls.count).toBe(0);
87
+ expect(scores.every(({ error }) => error !== undefined)).toBe(true);
88
+ });
89
+ });
@@ -0,0 +1,148 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ //
6
+ // Aesthetic rules: whether a diagram reads well as drawn, which the layout objective's counts
7
+ // (crossings, bends, length) do not capture — arrows against the flow, labels on top of boxes, edges
8
+ // merged into one trunk, an entry point in the far corner of its group. Judged from `Content.layout`,
9
+ // the text rendering of the page, by the same batched decision call as `Architecture`.
10
+ //
11
+
12
+ import type * as DecisionModel from 'effect/ai/DecisionModel';
13
+ import * as Effect from 'effect/Effect';
14
+
15
+ import * as Architecture from './architecture.ts';
16
+ import type * as Score from './score.ts';
17
+
18
+ const HOW_TO_READ =
19
+ 'The input is a diagram as drawn: `layout` renders the page as text, a character drawing followed by a ' +
20
+ 'row-by-row reading of the boxes and of which way each arrow runs. Judge how the drawing reads to a person ' +
21
+ 'looking at it, not whether the architecture it shows is good.';
22
+
23
+ const HOW_TO_SEE =
24
+ 'The input is a diagram as drawn: the attached image is the page. Judge how the drawing reads to a person ' +
25
+ 'looking at it, not whether the architecture it shows is good.';
26
+
27
+ const rule = (
28
+ id: string,
29
+ key: string,
30
+ description: string,
31
+ question: string,
32
+ criteria: Architecture.Rule['criteria'],
33
+ ): Architecture.Rule => ({ id, key, description, instructions: `${HOW_TO_READ} ${question}`, criteria });
34
+
35
+ export const RULES: readonly Architecture.Rule[] = [
36
+ rule(
37
+ 'arrows-follow-flow',
38
+ 'arrowsFollowFlow',
39
+ 'Arrows run with the main direction of the diagram, inside groups as well as between them.',
40
+ 'Do the arrows run with the main direction of the diagram (down in a top-to-bottom drawing, right in a ' +
41
+ 'left-to-right one), inside groups as well as between them, with at most a few pointing back against it? ' +
42
+ 'Arrows with a hollow triangle head (inheritance, implementation) point to the abstraction above by ' +
43
+ 'convention and do not count against the flow.',
44
+ {
45
+ true: 'Nearly every arrow runs with the flow; a reader follows dependencies in one direction.',
46
+ false: 'Many arrows, or whole chains inside a group, run up or back against the flow.',
47
+ },
48
+ ),
49
+ rule(
50
+ 'readable-labels',
51
+ 'readableLabels',
52
+ 'Every box and arrow label can be read on its own.',
53
+ 'Can every box label and arrow label be read on its own, without running into another label, a line or a box?',
54
+ {
55
+ true: 'Every label sits clear of other text and lines.',
56
+ false: 'Some label overlaps another label, a box or a line, or is cut off.',
57
+ },
58
+ ),
59
+ rule(
60
+ 'traceable-arrows',
61
+ 'traceableArrows',
62
+ 'Each arrow can be followed from its start to its end.',
63
+ 'Can a reader follow each arrow from its start to its end without ambiguity — no two arrows sharing a line ' +
64
+ 'segment or merged into one trunk, no arrow running through a group it does not connect to, no long detours ' +
65
+ 'around the page, and few crossings?',
66
+ {
67
+ true: 'Each arrow has its own short, direct route.',
68
+ false: 'Arrows share trunks, detour far, or cross so that their ends are hard to pair up.',
69
+ },
70
+ ),
71
+ rule(
72
+ 'entries-face-callers',
73
+ 'entriesFaceCallers',
74
+ 'Boxes that outside arrows reach sit on the side of their group facing the callers.',
75
+ 'Within each group, do the boxes that arrows from outside the group reach sit on the side of the group ' +
76
+ 'facing those callers, so that cross-group arrows are short and do not run through or around the group?',
77
+ {
78
+ true: 'Cross-group arrows land on the near side of each group.',
79
+ false: 'An arrow from outside travels through or around a group to reach a box on its far side.',
80
+ },
81
+ ),
82
+ rule(
83
+ 'compact',
84
+ 'compact',
85
+ 'The drawing is packed tightly: no large empty regions, groups sized to their content.',
86
+ 'Is the drawing compact — no large empty regions between or inside groups, groups no bigger than what they ' +
87
+ 'hold, and boxes that are connected placed next to each other?',
88
+ {
89
+ true: 'The page is tightly packed; connected boxes are neighbours.',
90
+ false: 'There are large empty regions, stretched groups, or connected boxes far apart.',
91
+ },
92
+ ),
93
+ rule(
94
+ 'groups-aligned',
95
+ 'groupsAligned',
96
+ 'Groups line up with each other rather than floating at arbitrary offsets.',
97
+ 'Do the groups line up with one another — sharing a top edge or a left edge where they sit side by side or ' +
98
+ 'stacked — rather than one group floating at an arbitrary height or offset beside another?',
99
+ {
100
+ true: 'Groups share edges and read as one arrangement.',
101
+ false: 'A group sits at an arbitrary offset from its neighbours.',
102
+ },
103
+ ),
104
+ rule(
105
+ 'names-only',
106
+ 'namesOnly',
107
+ 'Each box is labelled with a component name alone, without comments or qualifiers.',
108
+ 'Is every box labelled with just the name of what it is, with no comments, limits or qualifiers mixed into the ' +
109
+ 'name (such as "no cap", "≤10" or a colon followed by a description)?',
110
+ {
111
+ true: 'Box labels are plain names; any commentary sits on arrows or not at all.',
112
+ false: 'Some box label mixes a name with a comment, a limit or a description.',
113
+ },
114
+ ),
115
+ rule(
116
+ 'aligned-grid',
117
+ 'alignedGrid',
118
+ 'Boxes line up in rows and columns with consistent spacing.',
119
+ 'Are the boxes aligned into clear rows and columns with consistent spacing, so the structure reads as a grid?',
120
+ {
121
+ true: 'Boxes share rows and columns with even gaps.',
122
+ false: 'Boxes are staggered or unevenly spaced, with no clear rows or columns.',
123
+ },
124
+ ),
125
+ ];
126
+
127
+ /** {@link RULES} for a judge shown the page as an image (`Architecture.Subject.images`) instead of `layout`. */
128
+ export const IMAGE_RULES: readonly Architecture.Rule[] = RULES.map((rule) => ({
129
+ ...rule,
130
+ instructions: `${HOW_TO_SEE}${rule.instructions.slice(HOW_TO_READ.length)}`,
131
+ }));
132
+
133
+ /**
134
+ * Every aesthetic rule as a score from one `DecisionModel` call. Without `layout` or an image of the
135
+ * page there is nothing to judge, so each rule reports an error rather than a guess.
136
+ */
137
+ export const judge = (
138
+ rules: readonly Architecture.Rule[] = RULES,
139
+ ): Score.Batch<Architecture.Subject, DecisionModel.DecisionModel> => {
140
+ const inner = Architecture.judge(rules, 'aesthetics');
141
+ return {
142
+ entries: inner.entries,
143
+ evaluate: (subject) =>
144
+ subject.content.layout || subject.images?.length
145
+ ? inner.evaluate(subject)
146
+ : Effect.succeed(rules.map(() => ({ score: 0, error: 'No layout to judge.' }))),
147
+ };
148
+ };