dsh-model-everything 0.5.0 → 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 (62) hide show
  1. package/README.en-US.md +12 -0
  2. package/README.md +4 -0
  3. package/lib/client.js +2437 -216
  4. package/lib/render/channels.js +243 -0
  5. package/lib/render/collapse.js +12 -20
  6. package/lib/render/components.js +99 -0
  7. package/lib/render/coords.js +288 -0
  8. package/lib/render/cycle-break.js +147 -0
  9. package/lib/render/dummy.js +43 -0
  10. package/lib/render/from-mermaid.js +34 -10
  11. package/lib/render/from-plantuml.js +318 -46
  12. package/lib/render/layer.js +91 -0
  13. package/lib/render/layout.js +27 -11
  14. package/lib/render/network-simplex.js +272 -0
  15. package/lib/render/order.js +122 -0
  16. package/lib/render/paint.js +764 -67
  17. package/lib/render/place.js +393 -121
  18. package/lib/render/route.js +225 -0
  19. package/lib/render/svg.js +143 -21
  20. package/lib/render/symbol.js +108 -0
  21. package/lib/render/text.js +74 -0
  22. package/lib/render/wrap.js +87 -0
  23. package/lib/types/render/channels.d.ts +63 -0
  24. package/lib/types/render/components.d.ts +57 -0
  25. package/lib/types/render/coords.d.ts +64 -0
  26. package/lib/types/render/cycle-break.d.ts +45 -0
  27. package/lib/types/render/dummy.d.ts +32 -0
  28. package/lib/types/render/from-plantuml.d.ts +12 -6
  29. package/lib/types/render/layer.d.ts +28 -0
  30. package/lib/types/render/layout.d.ts +43 -11
  31. package/lib/types/render/model.d.ts +17 -0
  32. package/lib/types/render/network-simplex.d.ts +54 -0
  33. package/lib/types/render/order.d.ts +27 -0
  34. package/lib/types/render/paint.d.ts +42 -9
  35. package/lib/types/render/place.d.ts +35 -19
  36. package/lib/types/render/route.d.ts +77 -0
  37. package/lib/types/render/svg.d.ts +1 -1
  38. package/lib/types/render/symbol.d.ts +73 -0
  39. package/lib/types/render/text.d.ts +38 -0
  40. package/lib/types/render/wrap.d.ts +27 -0
  41. package/package.json +148 -148
  42. package/skills/model-everything-diagram/SKILL.md +84 -82
  43. package/src/render/channels.ts +309 -0
  44. package/src/render/collapse.ts +150 -155
  45. package/src/render/components.ts +126 -0
  46. package/src/render/coords.ts +328 -0
  47. package/src/render/cycle-break.ts +164 -0
  48. package/src/render/dummy.ts +65 -0
  49. package/src/render/from-mermaid.ts +261 -236
  50. package/src/render/from-plantuml.ts +469 -178
  51. package/src/render/layer.ts +98 -0
  52. package/src/render/layout.ts +101 -62
  53. package/src/render/model.ts +86 -67
  54. package/src/render/network-simplex.ts +290 -0
  55. package/src/render/order.ts +139 -0
  56. package/src/render/paint.ts +1006 -290
  57. package/src/render/place.ts +527 -234
  58. package/src/render/route.ts +269 -0
  59. package/src/render/svg.ts +270 -127
  60. package/src/render/symbol.ts +137 -0
  61. package/src/render/text.ts +86 -0
  62. package/src/render/wrap.ts +103 -0
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Layering: which row each box is in.
3
+ *
4
+ * The graph is acyclic by the time it gets here. A layered drawing needs every edge to span at least one layer, so it
5
+ * always travels downwards, and it wants the total of the layers every edge spans beyond one to be as small as
6
+ * possible: those are the edges that have to be drawn through a row of placeholders instead of going straight down a
7
+ * gap.
8
+ *
9
+ * Longest paths are the first guess: every source on the top row, every other node one layer below its latest
10
+ * predecessor. It is feasible and it is not optimal, because it puts every node as high as it can go - a node whose
11
+ * edges leave it with slack above the boxes it points at is a node that could have moved down and shortened them.
12
+ * `network-simplex.ts` solves the program the mature layering implementations solve: the ranking that minimises the
13
+ * total length, found by exchanging edges in a tight tree until no exchange is left that shortens it.
14
+ *
15
+ * The initial state is layer zero. It is a node with no predecessor - cycle breaking has turned around every edge
16
+ * that ran into it - so a source, and every source is a candidate for the top row. The pin is passed to the solver as
17
+ * a constraint, so the search never trades it away: an initial state that moved down the page would be a machine
18
+ * diagram read from the wrong end.
19
+ *
20
+ * Pure: the graph in, one layer per node out.
21
+ */
22
+
23
+ import type { LayoutEdge, LayoutGraph } from './layout.js'
24
+ import { minimumLength, type RankEdge } from './network-simplex.js'
25
+
26
+ export interface Layering {
27
+ /** The ids of each layer, top to bottom. A layer holds at least one node. */
28
+ layers: string[][]
29
+ layerOf: Map<string, number>
30
+ }
31
+
32
+ /**
33
+ * The longest-path ranking: the highest layer each node can possibly take.
34
+ *
35
+ * Every source starts on layer zero and every other node one below its latest predecessor, which is the shortest
36
+ * ranking that is feasible at all - and, because no node can then move up, the starting point the exchange search
37
+ * needs. A node the walk never reaches is part of a cycle and is put on layer zero: the stage is defined on a DAG,
38
+ * and a graph that arrived cyclic is laid out as far as it can be rather than losing the boxes of the cycle.
39
+ */
40
+ function longestPath(ids: string[], edges: LayoutEdge[], index: Map<string, number>): number[] {
41
+ const rank = new Array<number>(ids.length).fill(0)
42
+ const pending = new Array<number>(ids.length).fill(0)
43
+ const outgoing: number[][] = ids.map(() => [])
44
+ for (const edge of edges) {
45
+ outgoing[index.get(edge.from) as number].push(index.get(edge.to) as number)
46
+ pending[index.get(edge.to) as number] += 1
47
+ }
48
+ const queue: number[] = []
49
+ for (let node = 0; node < ids.length; node += 1) if (pending[node] === 0) queue.push(node)
50
+ for (let head = 0; head < queue.length; head += 1) {
51
+ const node = queue[head]
52
+ for (const target of outgoing[node]) {
53
+ rank[target] = Math.max(rank[target], rank[node] + 1)
54
+ pending[target] -= 1
55
+ if (pending[target] === 0) queue.push(target)
56
+ }
57
+ }
58
+ return rank
59
+ }
60
+
61
+ export function layer(graph: LayoutGraph, edges: LayoutEdge[]): Layering {
62
+ // A model is data: the same box may be written twice, and an edge may name a box this level does not have.
63
+ const ids: string[] = []
64
+ const seen = new Set<string>()
65
+ for (const node of graph.nodes) {
66
+ if (seen.has(node.id)) continue
67
+ seen.add(node.id)
68
+ ids.push(node.id)
69
+ }
70
+ const index = new Map(ids.map((id, offset) => [id, offset] as const))
71
+ const forward = edges.filter((edge) => index.has(edge.from) && index.has(edge.to) && edge.from !== edge.to)
72
+ const rankEdges: RankEdge[] = forward.map((edge) => ({
73
+ from: index.get(edge.from) as number,
74
+ to: index.get(edge.to) as number,
75
+ minLength: 1,
76
+ weight: 1,
77
+ }))
78
+
79
+ // The pin only means something for a node that could be on the top row. A node with a predecessor cannot be: it is
80
+ // forced below it, and no ranking puts it first while keeping the edge that points at it.
81
+ let pinned: number | undefined
82
+ if (graph.init !== undefined && index.has(graph.init)) {
83
+ const start = index.get(graph.init) as number
84
+ const forced = forward.some((edge) => edge.to === graph.init)
85
+ if (!forced) pinned = start
86
+ }
87
+
88
+ const rank = minimumLength({ count: ids.length, edges: rankEdges, start: longestPath(ids, forward, index), ...(pinned === undefined ? {} : { pinned }) })
89
+
90
+ const layers: string[][] = []
91
+ for (const [offset, id] of ids.entries()) {
92
+ const depth = rank[offset]
93
+ while (layers.length <= depth) layers.push([])
94
+ layers[depth].push(id)
95
+ }
96
+ const layerOf = new Map(ids.map((id, offset) => [id, rank[offset]] as const))
97
+ return { layers, layerOf }
98
+ }
@@ -1,62 +1,101 @@
1
- /**
2
- * Ranks for a machine, and the order of states within each rank.
3
- *
4
- * Breadth-first from the initial state decides the row: the layout a reader expects, and the one a layered
5
- * drawing asks for - edges mostly flow down, and a state's row is how far it is from the start. A state
6
- * nothing reaches goes in a block of its own rather than being dropped.
7
- *
8
- * Within a row, states keep the order breadth-first found them in: a row is filled by walking the row above
9
- * left to right, so a state already sits under the parent that discovered it. A barycentre pass was written
10
- * and removed - with breadth-first insertion it could never move a state, so it was complexity without an
11
- * observable effect.
12
- *
13
- * Pure, and asserted on the rows themselves.
14
- */
15
-
16
- import type { DiagramEdge, DiagramNode } from './model.js'
17
-
18
- export interface Ranked {
19
- /** The state ids of each row, top to bottom, already ordered left to right. */
20
- rows: string[][]
21
- /** States no edge reaches from the initial one. */
22
- unreachable: string[]
23
- }
24
-
25
- export function rankStates(nodes: DiagramNode[], edges: DiagramEdge[], init: string | undefined): Ranked {
26
- const ids = nodes.map((node) => node.id)
27
- const known = new Set(ids)
28
- const start = init !== undefined && known.has(init) ? init : ids[0]
29
-
30
- const outgoing = new Map<string, string[]>()
31
- const incoming = new Map<string, string[]>()
32
- for (const edge of edges) {
33
- if (!known.has(edge.from) || !known.has(edge.to)) continue
34
- outgoing.set(edge.from, [...(outgoing.get(edge.from) ?? []), edge.to])
35
- incoming.set(edge.to, [...(incoming.get(edge.to) ?? []), edge.from])
36
- }
37
-
38
- const depth = new Map<string, number>()
39
- if (start !== undefined) {
40
- depth.set(start, 0)
41
- const queue: string[] = [start]
42
- while (queue.length > 0) {
43
- const id = queue.shift() as string
44
- const level = depth.get(id) as number
45
- for (const target of outgoing.get(id) ?? []) {
46
- if (depth.has(target)) continue
47
- depth.set(target, level + 1)
48
- queue.push(target)
49
- }
50
- }
51
- }
52
-
53
- const rows: string[][] = []
54
- for (const id of ids) {
55
- const level = depth.get(id)
56
- if (level === undefined) continue
57
- while (rows.length <= level) rows.push([])
58
- rows[level].push(id)
59
- }
60
-
61
- return { rows, unreachable: ids.filter((id) => !depth.has(id)) }
62
- }
1
+ /**
2
+ * The graph the layout stages agree on, and the order they start from.
3
+ *
4
+ * A layout stage has no business knowing what a diagram is: it sees boxes with a size and edges
5
+ * between them. That is what `LayoutGraph` is, and it is the whole vocabulary the pipeline shares -
6
+ * cycle breaking reads it, layering writes layers, ordering and coordinate assignment read those.
7
+ *
8
+ * `rankStates` stays: it is the breadth-first order a source's own reading suggests, and it seeds
9
+ * the ordering stage with the discovery order instead of whatever order the parser happened to
10
+ * collect. It no longer decides which row a state is in - `layer.ts` does that now - so the rows it
11
+ * returns are only a first guess at the left-to-right order.
12
+ *
13
+ * Pure, and asserted on the answer itself.
14
+ */
15
+
16
+ /** A box to place: an id, and the space its text needs. */
17
+ export interface LayoutNode {
18
+ id: string
19
+ width: number
20
+ height: number
21
+ }
22
+
23
+ /** An edge to draw: which two boxes it joins, and which diagram edge it came from. */
24
+ export interface LayoutEdge {
25
+ /** The index in the diagram's own edge list, so the drawing can report a lane back for it. */
26
+ index: number
27
+ from: string
28
+ to: string
29
+ }
30
+
31
+ /** One level of a nested drawing: what is placed together in a single pass of the pipeline. */
32
+ export interface LayoutGraph {
33
+ nodes: LayoutNode[]
34
+ edges: LayoutEdge[]
35
+ /** The box that must stay on layer zero, when the source named one. */
36
+ init?: string
37
+ }
38
+
39
+ export interface Ranked {
40
+ /** The state ids of each row, top to bottom, already ordered left to right. */
41
+ rows: string[][]
42
+ /** States no edge reaches from the initial one. */
43
+ unreachable: string[]
44
+ }
45
+
46
+ export function rankStates(
47
+ nodes: ReadonlyArray<{ id: string }>,
48
+ edges: ReadonlyArray<{ from: string; to: string }>,
49
+ init: string | undefined,
50
+ ): Ranked {
51
+ const ids = nodes.map((node) => node.id)
52
+ const known = new Set(ids)
53
+ const start = init !== undefined && known.has(init) ? init : ids[0]
54
+
55
+ const outgoing = new Map<string, string[]>()
56
+ for (const edge of edges) {
57
+ if (!known.has(edge.from) || !known.has(edge.to)) continue
58
+ outgoing.set(edge.from, [...(outgoing.get(edge.from) ?? []), edge.to])
59
+ }
60
+
61
+ const depth = new Map<string, number>()
62
+ if (start !== undefined) {
63
+ depth.set(start, 0)
64
+ const queue: string[] = [start]
65
+ while (queue.length > 0) {
66
+ const id = queue.shift() as string
67
+ const level = depth.get(id) as number
68
+ for (const target of outgoing.get(id) ?? []) {
69
+ if (depth.has(target)) continue
70
+ depth.set(target, level + 1)
71
+ queue.push(target)
72
+ }
73
+ }
74
+ }
75
+
76
+ const rows: string[][] = []
77
+ for (const id of ids) {
78
+ const level = depth.get(id)
79
+ if (level === undefined) continue
80
+ while (rows.length <= level) rows.push([])
81
+ rows[level].push(id)
82
+ }
83
+
84
+ return { rows, unreachable: ids.filter((id) => !depth.has(id)) }
85
+ }
86
+
87
+ /**
88
+ * The left-to-right seed the ordering stage starts from.
89
+ *
90
+ * Breadth-first discovery is what a reader expects a drawing to follow, so it is a better first
91
+ * guess than the order the parser collected. A state nothing reaches has no discovery order and
92
+ * keeps the order it was written in, after everything that was reached.
93
+ */
94
+ export function seedOrder(graph: LayoutGraph): Map<string, number> {
95
+ const ranked = rankStates(graph.nodes, graph.edges, graph.init)
96
+ const order = new Map<string, number>()
97
+ let index = 0
98
+ for (const row of ranked.rows) for (const id of row) order.set(id, index++)
99
+ for (const id of ranked.unreachable) order.set(id, index++)
100
+ return order
101
+ }
@@ -1,67 +1,86 @@
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
-
11
- /** What a node is, as far as colour is concerned. */
12
- export type NodeRole = 'normal' | 'error' | 'warning' | 'muted'
13
-
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
- }
20
-
21
- export interface DiagramEdge {
22
- from: string
23
- to: string
24
- label: string
25
- /** A dashed edge, for notations that distinguish one. */
26
- dashed?: boolean
27
- }
28
-
29
- export interface DiagramGroup {
30
- id: string
31
- label: string
32
- /**
33
- * The nodes declared directly inside it: a group is a box around boxes, and its own members are the ones a
34
- * source named there. A node inside a nested group is that group's member, not this one's.
35
- */
36
- members: string[]
37
- /**
38
- * The group that contains this one, when the source said so.
39
- *
40
- * Only a parser can know this - nesting is a property of where the text put the boundary - so the layout does
41
- * not guess it. Left absent, the painter falls back to inferring containment from member sets, which is the
42
- * best a hand-written model can offer.
43
- */
44
- parent?: string
45
- }
46
-
47
- export interface Diagram {
48
- kind: 'machine' | 'graph' | 'none'
49
- /** The state the machine starts in, when the source declared one. */
50
- init?: string
51
- title: string
52
- nodes: DiagramNode[]
53
- edges: DiagramEdge[]
54
- groups?: DiagramGroup[]
55
- /** One line under the drawing: what was decided, in words the drawing cannot carry. */
56
- caption: string
57
- }
58
-
59
- /** An empty drawing, for a source that produced no nodes. */
60
- export function emptyDiagram(title: string, caption: string): Diagram {
61
- return { kind: 'none', title, nodes: [], edges: [], caption }
62
- }
63
-
64
- /** The label to draw for a node: its own label when the notation gave one, else its id. */
65
- export function labelOf(node: DiagramNode): string {
66
- return node.label !== undefined && node.label !== '' ? node.label : node.id
67
- }
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
+
11
+ /** What a node is, as far as colour is concerned. */
12
+ export type NodeRole = 'normal' | 'error' | 'warning' | 'muted'
13
+
14
+ /** The symbol a node is drawn as. UML and Mermaid both distinguish these; the notation decides which one applies. */
15
+ export type NodeShape = 'rect' | 'diamond' | 'cylinder' | 'stadium' | 'circle' | 'subroutine' | 'hexagon'
16
+
17
+ export interface DiagramNode {
18
+ id: string
19
+ /** The text drawn inside the box. Defaults to the id when a notation labelled it. */
20
+ label?: string
21
+ role: NodeRole
22
+ /** Defaults to a rectangle: a node the source gave no shape has no shape to draw. */
23
+ shape?: NodeShape
24
+ }
25
+
26
+ /**
27
+ * What an edge's head means, when the notation distinguished it.
28
+ *
29
+ * The painter draws the meaning: a plain arrow for an association, a hollow triangle for a generalisation or a
30
+ * realisation, a filled diamond at the source for a composition, a hollow one for an aggregation, and a dashed
31
+ * line for the two dependency-flavoured kinds.
32
+ */
33
+ export type EdgeArrow = 'association' | 'dependency' | 'generalization' | 'realization' | 'composition' | 'aggregation'
34
+
35
+ export interface DiagramEdge {
36
+ from: string
37
+ to: string
38
+ label: string
39
+ /** A dashed edge, for notations that distinguish one. */
40
+ dashed?: boolean
41
+ /**
42
+ * The arrow the notation stated. Absent means the plain arrow every edge has been drawn with: a Mermaid edge
43
+ * states no arrow type, so it carries no field and is drawn exactly as before.
44
+ */
45
+ arrow?: EdgeArrow
46
+ }
47
+
48
+ export interface DiagramGroup {
49
+ id: string
50
+ label: string
51
+ /**
52
+ * The nodes declared directly inside it: a group is a box around boxes, and its own members are the ones a
53
+ * source named there. A node inside a nested group is that group's member, not this one's.
54
+ */
55
+ members: string[]
56
+ /**
57
+ * The group that contains this one, when the source said so.
58
+ *
59
+ * Only a parser can know this - nesting is a property of where the text put the boundary - so the layout does
60
+ * not guess it. Left absent, the painter falls back to inferring containment from member sets, which is the
61
+ * best a hand-written model can offer.
62
+ */
63
+ parent?: string
64
+ }
65
+
66
+ export interface Diagram {
67
+ kind: 'machine' | 'graph' | 'none'
68
+ /** The state the machine starts in, when the source declared one. */
69
+ init?: string
70
+ title: string
71
+ nodes: DiagramNode[]
72
+ edges: DiagramEdge[]
73
+ groups?: DiagramGroup[]
74
+ /** One line under the drawing: what was decided, in words the drawing cannot carry. */
75
+ caption: string
76
+ }
77
+
78
+ /** An empty drawing, for a source that produced no nodes. */
79
+ export function emptyDiagram(title: string, caption: string): Diagram {
80
+ return { kind: 'none', title, nodes: [], edges: [], caption }
81
+ }
82
+
83
+ /** The label to draw for a node: its own label when the notation gave one, else its id. */
84
+ export function labelOf(node: DiagramNode): string {
85
+ return node.label !== undefined && node.label !== '' ? node.label : node.id
86
+ }