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,86 @@
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
+
14
+ /** Average advance per character, in the drawing's units, at the 11px font the writer emits. */
15
+ const CHAR_WIDTH = 6.6
16
+
17
+ /** A line longer than this wraps on a space, or breaks in the middle when it has none. */
18
+ export const MAX_LINE_CHARS = 30
19
+
20
+ /** Height of one line of box text, and the breathing room around the block. */
21
+ export const LINE_HEIGHT = 15
22
+ export const TEXT_PADDING = 12
23
+
24
+ /** The smallest and largest box a label may produce. */
25
+ export const MIN_NODE_WIDTH = 88
26
+ export const MAX_NODE_WIDTH = 260
27
+
28
+ /** `<br>`, `<br/>`, `<br />` and a real newline all end a line. */
29
+ const BREAK = /<br\s*\/?>|\r?\n/gi
30
+
31
+ /** The lines a label asks for, before wrapping: an explicit break is a line end, not a space. */
32
+ export function explicitLines(label: string): string[] {
33
+ return label.split(BREAK).map((line) => line.trim())
34
+ }
35
+
36
+ /**
37
+ * Wrap one line to the maximum width.
38
+ *
39
+ * A word longer than the limit is broken rather than allowed to overflow: a single token with no spaces is the
40
+ * common case in identifiers, and letting it run off the box is the defect this function exists to prevent.
41
+ */
42
+ export function wrapLine(line: string, limit: number = MAX_LINE_CHARS): string[] {
43
+ if (line === '') return ['']
44
+ const out: string[] = []
45
+ let current = ''
46
+ for (const word of line.split(/\s+/).filter((part) => part !== '')) {
47
+ const candidate = current === '' ? word : current + ' ' + word
48
+ if (candidate.length <= limit) {
49
+ current = candidate
50
+ continue
51
+ }
52
+ if (current !== '') out.push(current)
53
+ if (word.length <= limit) {
54
+ current = word
55
+ continue
56
+ }
57
+ let rest = word
58
+ while (rest.length > limit) {
59
+ out.push(rest.slice(0, limit))
60
+ rest = rest.slice(limit)
61
+ }
62
+ current = rest
63
+ }
64
+ if (current !== '') out.push(current)
65
+ return out.length === 0 ? [''] : out
66
+ }
67
+
68
+ export interface Measured {
69
+ lines: string[]
70
+ width: number
71
+ height: number
72
+ }
73
+
74
+ /** The text block a label produces: its lines, and the box it needs. */
75
+ export function measure(label: string): Measured {
76
+ const lines = explicitLines(label).flatMap((line) => wrapLine(line))
77
+ const longest = lines.reduce((max, line) => Math.max(max, line.length), 0)
78
+ const width = Math.min(MAX_NODE_WIDTH, Math.max(MIN_NODE_WIDTH, longest * CHAR_WIDTH + TEXT_PADDING * 2))
79
+ const height = lines.length * LINE_HEIGHT + TEXT_PADDING
80
+ return { lines, width, height }
81
+ }
82
+
83
+ /** Rough width of a single-line piece of text, for placing an edge label without a box. */
84
+ export function textWidth(text: string): number {
85
+ return text.length * CHAR_WIDTH
86
+ }
@@ -0,0 +1,136 @@
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
+
14
+ const record = (value: unknown): Record<string, unknown> | undefined =>
15
+ typeof value === 'object' && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined
16
+
17
+ const array = (value: unknown): unknown[] => (Array.isArray(value) ? value : [])
18
+
19
+ const text = (value: unknown): string | undefined => (typeof value === 'string' && value !== '' ? value : undefined)
20
+
21
+ /** The report text a settled call carries, joined: a card reads words, not blocks. */
22
+ function reportText(content: unknown): string {
23
+ const parts: string[] = []
24
+ for (const entry of array(content)) {
25
+ const block = record(entry)
26
+ const body = text(block?.text)
27
+ if (body !== undefined) parts.push(body)
28
+ }
29
+ return parts.join(String.fromCharCode(10))
30
+ }
31
+
32
+ /** Two facts out of this plugin's own report format: the verdict, and the first error it named. */
33
+ function readReport(content: unknown): { verdict?: string; code?: string; message?: string } {
34
+ const body = reportText(content)
35
+ if (body === '') return {}
36
+ const verdict = /verdict=(\w+)/.exec(body)?.[1]
37
+ const named = /\[error\/([A-Z0-9_]+)\]\s*(.*)/.exec(body)
38
+ if (named !== null) return { verdict, code: named[1], message: named[2] }
39
+ const first = /first error:\s*([A-Z0-9_]+)/.exec(body)
40
+ return first === null ? { verdict } : { verdict, code: first[1] }
41
+ }
42
+
43
+ /**
44
+ * The `model` argument out of the host's lazy arguments view.
45
+ *
46
+ * A settled call exposes `value(key)`; a plain object (a test, or an older shape) is read directly. The
47
+ * view answers `undefined` until the field closes, which is exactly when a drawing should be attempted.
48
+ */
49
+ function readModel(args: unknown): Record<string, unknown> | undefined {
50
+ const view = record(args)
51
+ if (view === undefined) return undefined
52
+ const read = view.value
53
+ if (typeof read === 'function') return record((read as (key: string) => unknown)('model'))
54
+ return record(view.model)
55
+ }
56
+
57
+ /** The states of a model: a machine, or nothing when the call carried none. */
58
+ function machineOf(model: Record<string, unknown> | undefined): { nodes: DiagramNode[]; edges: DiagramEdge[]; init?: string } | undefined {
59
+ if (model === undefined) return undefined
60
+ const states = array(model.states).map(record).filter((entry): entry is Record<string, unknown> => entry !== undefined && typeof entry.id === 'string')
61
+ if (states.length === 0) return undefined
62
+ const edges = array(model.transitions)
63
+ .map(record)
64
+ .filter((entry): entry is Record<string, unknown> => entry !== undefined && typeof entry.from === 'string' && typeof entry.to === 'string')
65
+ .map((entry) => ({ from: String(entry.from), to: String(entry.to), label: text(entry.event) ?? '' }))
66
+ return {
67
+ nodes: states.map((state) => ({ id: String(state.id), role: 'normal' as const })),
68
+ edges,
69
+ ...(typeof model.init === 'string' ? { init: model.init } : {}),
70
+ }
71
+ }
72
+
73
+ /** The caption: the verdict, then the first error's code and message when the report named one. */
74
+ function captionOf(report: { verdict?: string; code?: string; message?: string }): string {
75
+ const verdict = report.verdict ?? 'no report yet'
76
+ if (report.code === undefined) return verdict
77
+ const message = report.message ?? ''
78
+ return verdict + ' — ' + report.code + (message === '' ? '' : ': ' + message)
79
+ }
80
+
81
+ /**
82
+ * Which nodes the report blames.
83
+ *
84
+ * A finding names a state inside its message as `"x"`; matching that is a heuristic, so it only ever *adds*
85
+ * emphasis and never decides the verdict.
86
+ */
87
+ function blame(nodes: DiagramNode[], report: { message?: string }): DiagramNode[] {
88
+ if (report.message === undefined) return nodes
89
+ const message = report.message
90
+ // Whole words only: a report does not quote the state, and a state called `do` must not match `done`.
91
+ const named = nodes
92
+ .filter((node) => {
93
+ const escaped = node.id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
94
+ return new RegExp('(?<![A-Za-z0-9_])' + escaped + '(?![A-Za-z0-9_])').test(message)
95
+ })
96
+ .map((node) => node.id)
97
+ if (named.length === 0) return nodes
98
+ return nodes.map((node) => (named.includes(node.id) ? { ...node, role: 'error' as const } : node))
99
+ }
100
+
101
+ import type { Diagram, DiagramEdge, DiagramNode } from './model.js'
102
+
103
+ export interface ViewInput {
104
+ phase?: unknown
105
+ toolName?: unknown
106
+ /** The whole block the host hands a view; its shape depends on the phase. */
107
+ block?: unknown
108
+ }
109
+
110
+ export function viewModel(input: ViewInput): Diagram {
111
+ const block = record(input.block)
112
+ const title = text(block?.name) ?? text(input.toolName) ?? 'tool'
113
+ const report = readReport(block?.content)
114
+ // A shape that still carries a parsed report object is read from there; it is what older callers pass.
115
+ const parsed = record(block?.result)
116
+ const findings = array(parsed?.findings).map(record)
117
+ const firstError = findings.find((finding) => finding?.severity === 'error')
118
+ const merged = {
119
+ verdict: text(parsed?.verdict) ?? report.verdict,
120
+ ...(report.code === undefined && typeof firstError?.code === 'string' ? { code: firstError.code } : { code: report.code }),
121
+ ...(report.message === undefined && typeof firstError?.message === 'string' ? { message: firstError.message } : { message: report.message }),
122
+ }
123
+ const caption = captionOf(merged)
124
+ const machine = machineOf(readModel(block?.args))
125
+ if (machine === undefined) {
126
+ return { kind: 'none', title, nodes: [], edges: [], caption }
127
+ }
128
+ return {
129
+ kind: 'machine',
130
+ title,
131
+ ...(machine.init === undefined ? {} : { init: machine.init }),
132
+ nodes: blame(machine.nodes, merged),
133
+ edges: machine.edges,
134
+ caption,
135
+ }
136
+ }
@@ -0,0 +1,103 @@
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
+
16
+ /** The aspect ratio a page and a landscape screen share. */
17
+ const PAGE_ASPECT = 1.6
18
+
19
+ /** A drawing shorter than this many layers is read better stacked, whatever its proportions. */
20
+ const MIN_LAYERS_TO_WRAP = 4
21
+
22
+ /** A drawing that is not this tall relative to its width is not a strip. */
23
+ const STRIP_RATIO = 2
24
+
25
+ export interface BandRange {
26
+ /** The first and last layer the band holds, inclusive. */
27
+ first: number
28
+ last: number
29
+ /** How far right the band stands, relative to the first one. */
30
+ offsetX: number
31
+ }
32
+
33
+ export interface BandPlan {
34
+ bands: BandRange[]
35
+ width: number
36
+ height: number
37
+ }
38
+
39
+ /** The height of a run of layers with the gap between them. */
40
+ function runHeight(layerHeight: number[], first: number, last: number, gap: number): number {
41
+ let total = 0
42
+ for (let index = first; index <= last; index += 1) {
43
+ total += layerHeight[index]
44
+ if (index < last) total += gap
45
+ }
46
+ return total
47
+ }
48
+
49
+ /** The width of a run of layers: the widest layer sets it. */
50
+ function runWidth(layerWidth: number[], first: number, last: number): number {
51
+ let widest = 0
52
+ for (let index = first; index <= last; index += 1) widest = Math.max(widest, layerWidth[index])
53
+ return widest
54
+ }
55
+
56
+ /** How far the packed page is from the target shape, in log space so half and double count the same. */
57
+ function shapeError(width: number, height: number, aspect: number): number {
58
+ if (width <= 0 || height <= 0) return Infinity
59
+ const ratio = Math.max(width, height) / Math.min(width, height)
60
+ return Math.abs(Math.log(ratio / aspect))
61
+ }
62
+
63
+ export function planBands(layerWidth: number[], layerHeight: number[], gap: number, aspect: number = PAGE_ASPECT): BandPlan {
64
+ const count = layerWidth.length
65
+ if (count === 0) return { bands: [], width: 0, height: 0 }
66
+ const single: BandPlan = {
67
+ bands: [{ first: 0, last: count - 1, offsetX: 0 }],
68
+ width: runWidth(layerWidth, 0, count - 1),
69
+ height: runHeight(layerHeight, 0, count - 1, gap),
70
+ }
71
+ if (count < MIN_LAYERS_TO_WRAP || single.height <= single.width * STRIP_RATIO) return single
72
+
73
+ const columnGap = gap * 2
74
+ let best = single
75
+ let bestError = shapeError(single.width, single.height, aspect)
76
+ for (let bands = 2; bands < count; bands += 1) {
77
+ const base = Math.floor(count / bands)
78
+ const extra = count % bands
79
+ const ranges: Array<{ first: number; last: number }> = []
80
+ let cursor = 0
81
+ for (let band = 0; band < bands; band += 1) {
82
+ const size = base + (band < extra ? 1 : 0)
83
+ ranges.push({ first: cursor, last: cursor + size - 1 })
84
+ cursor += size
85
+ }
86
+ let width = columnGap * (bands - 1)
87
+ let height = 0
88
+ for (const range of ranges) {
89
+ width += runWidth(layerWidth, range.first, range.last)
90
+ height = Math.max(height, runHeight(layerHeight, range.first, range.last, gap))
91
+ }
92
+ if (shapeError(width, height, aspect) >= bestError) continue
93
+ const planned: BandRange[] = []
94
+ let offsetX = 0
95
+ for (const range of ranges) {
96
+ planned.push({ first: range.first, last: range.last, offsetX })
97
+ offsetX += runWidth(layerWidth, range.first, range.last) + columnGap
98
+ }
99
+ best = { bands: planned, width, height }
100
+ bestError = shapeError(width, height, aspect)
101
+ }
102
+ return best
103
+ }
@@ -0,0 +1,17 @@
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
+
8
+ import { writeFileSync } from 'node:fs'
9
+ import { render, type RenderInput, type Rendered } from './index.js'
10
+ import type { Palette, PaletteOptions } from './palette.js'
11
+
12
+ /** Write the drawing where a document can link to it. Returns the path, the byte count and the render. */
13
+ export function writeSvg(input: RenderInput, path: string, overrides: Partial<Palette> = {}, options: PaletteOptions = {}): { path: string; bytes: number; rendered: Rendered } {
14
+ const rendered = render(input, overrides, options)
15
+ writeFileSync(path, rendered.markup, 'utf8')
16
+ return { path, bytes: rendered.markup.length, rendered }
17
+ }