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.
- package/README.en-US.md +75 -19
- package/README.md +377 -345
- package/cordis.patch.yml +9 -0
- package/diagram/package.json +5 -0
- package/lib/client.js +3494 -0
- package/lib/diagram-diff-tool.js +90 -0
- package/lib/diagram-render-tool.js +102 -0
- package/lib/diagram-review-tool.js +99 -0
- package/lib/diagram.js +43 -0
- package/lib/gate.js +14 -12
- package/lib/presentation.js +10 -0
- package/lib/render/budget.js +99 -0
- package/lib/render/channels.js +243 -0
- package/lib/render/collapse.js +155 -0
- package/lib/render/components.js +99 -0
- package/lib/render/coords.js +288 -0
- package/lib/render/cycle-break.js +147 -0
- package/lib/render/diff.js +138 -0
- package/lib/render/dummy.js +43 -0
- package/lib/render/from-mermaid.js +235 -0
- package/lib/render/from-plantuml.js +430 -0
- package/lib/render/index.js +42 -0
- package/lib/render/layer.js +91 -0
- package/lib/render/layout.js +68 -0
- package/lib/render/model.js +17 -0
- package/lib/render/network-simplex.js +272 -0
- package/lib/render/order.js +122 -0
- package/lib/render/paint.js +891 -0
- package/lib/render/palette.js +127 -0
- package/lib/render/place.js +482 -0
- package/lib/render/route.js +225 -0
- package/lib/render/svg.js +229 -0
- package/lib/render/symbol.js +108 -0
- package/lib/render/text.js +74 -0
- package/lib/render/viewmodel.js +126 -0
- package/lib/render/wrap.js +87 -0
- package/lib/render/write.js +14 -0
- package/lib/scan.js +248 -8
- package/lib/structure-tool.js +2 -2
- package/lib/types/client.d.ts +83 -0
- package/lib/types/diagram-diff-tool.d.ts +8 -0
- package/lib/types/diagram-render-tool.d.ts +8 -0
- package/lib/types/diagram-review-tool.d.ts +8 -0
- package/lib/types/diagram.d.ts +34 -0
- package/lib/types/presentation.d.ts +7 -0
- package/lib/types/render/budget.d.ts +55 -0
- package/lib/types/render/channels.d.ts +63 -0
- package/lib/types/render/collapse.d.ts +23 -0
- package/lib/types/render/components.d.ts +57 -0
- package/lib/types/render/coords.d.ts +64 -0
- package/lib/types/render/cycle-break.d.ts +45 -0
- package/lib/types/render/diff.d.ts +44 -0
- package/lib/types/render/dummy.d.ts +32 -0
- package/lib/types/render/from-mermaid.d.ts +27 -0
- package/lib/types/render/from-plantuml.d.ts +33 -0
- package/lib/types/render/index.d.ts +38 -0
- package/lib/types/render/layer.d.ts +28 -0
- package/lib/types/render/layout.d.ts +54 -0
- package/lib/types/render/model.d.ts +73 -0
- package/lib/types/render/network-simplex.d.ts +54 -0
- package/lib/types/render/order.d.ts +27 -0
- package/lib/types/render/paint.d.ts +127 -0
- package/lib/types/render/palette.d.ts +41 -0
- package/lib/types/render/place.d.ts +55 -0
- package/lib/types/render/route.d.ts +77 -0
- package/lib/types/render/svg.d.ts +19 -0
- package/lib/types/render/symbol.d.ts +73 -0
- package/lib/types/render/text.d.ts +38 -0
- package/lib/types/render/viewmodel.d.ts +20 -0
- package/lib/types/render/wrap.d.ts +27 -0
- package/lib/types/render/write.d.ts +14 -0
- package/lib/types/scan.d.ts +10 -2
- package/lib/types/structure.d.ts +1 -0
- package/package.json +148 -132
- package/skills/model-everything/SKILL.md +454 -454
- package/skills/model-everything/references/architecture-domains.md +137 -0
- package/skills/model-everything/references/architecture-views.md +119 -0
- package/skills/model-everything-diagram/SKILL.md +84 -0
- package/src/client.ts +307 -0
- package/src/diagram-diff-tool.ts +94 -0
- package/src/diagram-render-tool.ts +108 -0
- package/src/diagram-review-tool.ts +109 -0
- package/src/diagram.ts +61 -0
- package/src/gate.ts +14 -12
- package/src/presentation.ts +12 -0
- package/src/render/budget.ts +135 -0
- package/src/render/channels.ts +309 -0
- package/src/render/collapse.ts +150 -0
- package/src/render/components.ts +126 -0
- package/src/render/coords.ts +328 -0
- package/src/render/cycle-break.ts +164 -0
- package/src/render/diff.ts +162 -0
- package/src/render/dummy.ts +65 -0
- package/src/render/from-mermaid.ts +261 -0
- package/src/render/from-plantuml.ts +469 -0
- package/src/render/index.ts +73 -0
- package/src/render/layer.ts +98 -0
- package/src/render/layout.ts +101 -0
- package/src/render/model.ts +86 -0
- package/src/render/network-simplex.ts +290 -0
- package/src/render/order.ts +139 -0
- package/src/render/paint.ts +1006 -0
- package/src/render/palette.ts +164 -0
- package/src/render/place.ts +527 -0
- package/src/render/route.ts +269 -0
- package/src/render/svg.ts +270 -0
- package/src/render/symbol.ts +137 -0
- package/src/render/text.ts +86 -0
- package/src/render/viewmodel.ts +136 -0
- package/src/render/wrap.ts +103 -0
- package/src/render/write.ts +17 -0
- package/src/scan.ts +807 -564
- package/src/structure-tool.ts +2 -2
- package/src/structure.ts +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The drawing tool: source text in, SVG out.
|
|
3
|
+
*
|
|
4
|
+
* One job per file: this module declares the tool and nothing else, so the documentation generator that walks
|
|
5
|
+
* `*-tool` modules finds it, and so a change to the drawing never touches the reviewing.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DIAGRAM_TOOL_NAME = "diagram_render";
|
|
8
|
+
export declare const diagramRenderTool: import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The structural review tool: budgets against one drawing.
|
|
3
|
+
*
|
|
4
|
+
* One job per file: this module declares the tool and nothing else, so the documentation generator that walks
|
|
5
|
+
* `*-tool` modules finds it, and so a change to the drawing never touches the reviewing.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DIAGRAM_REVIEW_TOOL_NAME = "diagram_review";
|
|
8
|
+
export declare const diagramReviewTool: import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The diagram component: one row, one switch, three tools.
|
|
3
|
+
*
|
|
4
|
+
* A separate component rather than a feature of the tools above it, because drawing has nothing to do with
|
|
5
|
+
* verifying. It can be on while the tools are off, off while they are on, and it carries no engine: a
|
|
6
|
+
* deployment that only wants pictures of its Mermaid diagrams installs the same package and leaves the rest of
|
|
7
|
+
* it alone.
|
|
8
|
+
*
|
|
9
|
+
* The switch is `config.enabled`, live, exactly like the routing gate: a deployment can turn the drawing off at
|
|
10
|
+
* boot, from the Plugins page, or by disabling this row, and none of those touches the tools or the skills.
|
|
11
|
+
*/
|
|
12
|
+
import z from '@deepseek-ai/schemastery';
|
|
13
|
+
export interface Config {
|
|
14
|
+
/** Whether the diagram tools are registered. Read through {@link enabledOf}. */
|
|
15
|
+
enabled: unknown;
|
|
16
|
+
/** The title a drawing carries when the caller did not name one. */
|
|
17
|
+
defaultTitle: string;
|
|
18
|
+
}
|
|
19
|
+
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
20
|
+
enabled: z<boolean, boolean, "defined">;
|
|
21
|
+
defaultTitle: z<string, string, "defined">;
|
|
22
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
23
|
+
enabled: z<boolean, boolean, "defined">;
|
|
24
|
+
defaultTitle: z<string, string, "defined">;
|
|
25
|
+
}>>, "plain">;
|
|
26
|
+
/** Read the switch as a boolean, whichever shape this host produced. */
|
|
27
|
+
export declare function enabledOf(config: Config): boolean;
|
|
28
|
+
export interface ComponentContext {
|
|
29
|
+
tools: {
|
|
30
|
+
register(definition: unknown): () => void;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export declare function apply(ctx: ComponentContext, config: Config): void;
|
|
34
|
+
export declare const inject: string[];
|
|
@@ -77,6 +77,13 @@ export interface ReportCard {
|
|
|
77
77
|
facts: number;
|
|
78
78
|
skipped: number;
|
|
79
79
|
};
|
|
80
|
+
/** `uml_structure_verify` with `repoPath`: what the scan itself resolved, and what it could not. */
|
|
81
|
+
scan?: {
|
|
82
|
+
files: number;
|
|
83
|
+
unresolved: number;
|
|
84
|
+
packages: number;
|
|
85
|
+
dynamic: number;
|
|
86
|
+
};
|
|
80
87
|
/** `model_interleaving_scan`: how many claims were found, and in which families. */
|
|
81
88
|
claims?: {
|
|
82
89
|
absolute: number;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural budgets for a diagram: the checks a drawing can fail without being wrong.
|
|
3
|
+
*
|
|
4
|
+
* A diagram is not verified the way a state machine is - there is no deadlock to find - but an architecture
|
|
5
|
+
* picture has properties a reader relies on, and they are checkable: a package that holds twenty things is not
|
|
6
|
+
* communicating anything, a boundary around one box is a layer the source did not mean, a group nothing points
|
|
7
|
+
* into and nothing leaves is a group that belongs on another page.
|
|
8
|
+
*
|
|
9
|
+
* Pure: a diagram and a set of limits in, findings out. The limits are arguments, not constants, because what
|
|
10
|
+
* counts as too wide depends on the medium - a slide holds less than a scrollable tab.
|
|
11
|
+
*/
|
|
12
|
+
import type { Diagram } from './model.js';
|
|
13
|
+
export type Severity = 'error' | 'warning';
|
|
14
|
+
export interface BudgetFinding {
|
|
15
|
+
code: string;
|
|
16
|
+
severity: Severity;
|
|
17
|
+
message: string;
|
|
18
|
+
/** The group the finding is about, when it is about one. */
|
|
19
|
+
group?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface BudgetLimits {
|
|
22
|
+
/** How many direct children a boundary may hold: its own nodes plus the groups directly inside it. */
|
|
23
|
+
maxMembers: number;
|
|
24
|
+
/** How deep boundaries may nest. */
|
|
25
|
+
maxDepth: number;
|
|
26
|
+
/** How many nodes the whole drawing may hold. */
|
|
27
|
+
maxNodes: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Defaults, chosen for a drawing read in a conversation pane: a package with more than a handful of boxes stops
|
|
31
|
+
* being a package and becomes a diagram of its own, and three levels is what fits on one screen before a reader
|
|
32
|
+
* needs the source.
|
|
33
|
+
*/
|
|
34
|
+
export declare const DEFAULT_LIMITS: BudgetLimits;
|
|
35
|
+
export interface BudgetReport {
|
|
36
|
+
verdict: 'pass' | 'pass_with_findings' | 'fail';
|
|
37
|
+
findings: BudgetFinding[];
|
|
38
|
+
/** What was measured, so a reader can tell "nothing to check" from "checked and clean". */
|
|
39
|
+
measured: {
|
|
40
|
+
nodes: number;
|
|
41
|
+
groups: number;
|
|
42
|
+
deepest: number;
|
|
43
|
+
widest: {
|
|
44
|
+
group: string;
|
|
45
|
+
size: number;
|
|
46
|
+
} | null;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Measure a drawing against its limits.
|
|
51
|
+
*
|
|
52
|
+
* Every finding names the group it is about, because a budget report that says "a group is too wide" without
|
|
53
|
+
* saying which one is a report nobody can act on.
|
|
54
|
+
*/
|
|
55
|
+
export declare function checkBudget(diagram: Diagram, limits?: Partial<BudgetLimits>): BudgetReport;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel assignment: where the vertical lanes and the horizontal runs between two rows go.
|
|
3
|
+
*
|
|
4
|
+
* The stages before this one decide where the boxes are, and the painter decides what a box looks like. What is left
|
|
5
|
+
* is the space between two rows, and an edge that decides that space alone will collide with the next one: every long
|
|
6
|
+
* edge has to cross a row somewhere, and every edge whose two ends are not vertically aligned needs one horizontal run
|
|
7
|
+
* inside the gap it crosses. Chosen independently, two runs land on one height and read as a junction, and a run
|
|
8
|
+
* sweeps across a vertical another edge is already using.
|
|
9
|
+
*
|
|
10
|
+
* So this stage assigns both, after the boxes have settled and before anything is drawn:
|
|
11
|
+
*
|
|
12
|
+
* - **A lane's x.** A long edge owns one vertical lane through the rows it crosses. The x is a position no box
|
|
13
|
+
* occupies in any of those rows, and lanes keep the order their dummies have in the ordering, so two lanes never
|
|
14
|
+
* share an x inside one gap. Alignment is worth a detour: a lane that lands inside a box's own port span turns a
|
|
15
|
+
* four-bend staircase into a straight drop, so alignment comes before distance from the proposed position.
|
|
16
|
+
* - **A run's y.** The horizontal runs inside one gap are stacked at distinct heights - Graphviz's virtual nodes - so
|
|
17
|
+
* two runs never lie on one line, and a nested run stays near the box it leaves.
|
|
18
|
+
*
|
|
19
|
+
* Pure: rows, chains and coordinates in; lane positions and run heights out.
|
|
20
|
+
*/
|
|
21
|
+
import type { Chain } from './dummy.js';
|
|
22
|
+
/** A box, as far as routing is concerned: only a box can move a lane. */
|
|
23
|
+
export interface ChannelBox {
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
}
|
|
29
|
+
export interface ChannelInput {
|
|
30
|
+
/** The ordered rows, dummies included: a lane keeps its dummy's order. */
|
|
31
|
+
layers: string[][];
|
|
32
|
+
/** Every edge's chain, from the layout source to the layout target. */
|
|
33
|
+
chains: Chain[];
|
|
34
|
+
/** A dummy instance, and the lane it belongs to. */
|
|
35
|
+
chainOf: ReadonlyMap<string, string>;
|
|
36
|
+
/** The boxes that block a lane. A dummy has none. */
|
|
37
|
+
boxes: ReadonlyMap<string, ChannelBox>;
|
|
38
|
+
/** The top of each row, and how tall it is. */
|
|
39
|
+
layerY: number[];
|
|
40
|
+
layerHeight: number[];
|
|
41
|
+
/** What coordinate assignment proposed for each lane. */
|
|
42
|
+
laneX: ReadonlyMap<string, number>;
|
|
43
|
+
}
|
|
44
|
+
export interface ChannelOptions {
|
|
45
|
+
/** How far a lane stays from a box in a row it crosses. */
|
|
46
|
+
clearance: number;
|
|
47
|
+
/** The narrowest two lanes sharing a gap may be. */
|
|
48
|
+
spacing: number;
|
|
49
|
+
}
|
|
50
|
+
/** One long edge's lane: the x of its vertical run, and the height of a run in each gap it crosses. */
|
|
51
|
+
export interface LaneChannel {
|
|
52
|
+
x: number;
|
|
53
|
+
y: number[];
|
|
54
|
+
}
|
|
55
|
+
export interface ChannelPlan {
|
|
56
|
+
/** By diagram edge index: the lane a long edge reserved, with one corridor height per gap it crosses. */
|
|
57
|
+
lanes: Map<number, LaneChannel>;
|
|
58
|
+
/** By diagram edge index: the height of the one run an edge between two adjacent rows needs. */
|
|
59
|
+
runs: Map<number, number>;
|
|
60
|
+
}
|
|
61
|
+
/** How far a port sits from a box's corner. The painter places ports from this same number. */
|
|
62
|
+
export declare const PORT_INSET = 10;
|
|
63
|
+
export declare function assignChannels(input: ChannelInput, options: ChannelOptions): ChannelPlan;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collapsing a group: the drawing with one boundary folded into a single box.
|
|
3
|
+
*
|
|
4
|
+
* A fold is a transform on the model, not a mode of the painter. That keeps the painter's rules unchanged -
|
|
5
|
+
* it still draws boxes and edges it is given - and it makes the behaviour testable without a browser: feed a
|
|
6
|
+
* diagram in, assert what came out.
|
|
7
|
+
*
|
|
8
|
+
* Folding is what makes a hierarchy usable: a reader who has understood a package wants it out of the way, and
|
|
9
|
+
* an architecture diagram that cannot be folded is read at one zoom level forever.
|
|
10
|
+
*/
|
|
11
|
+
import type { Diagram } from './model.js';
|
|
12
|
+
/**
|
|
13
|
+
* Fold the named groups.
|
|
14
|
+
*
|
|
15
|
+
* A folded group becomes a node carrying its name and how many nodes it holds; its members - and every group
|
|
16
|
+
* inside it - leave the drawing. An edge that pointed at something inside the fold is re-aimed at the fold's
|
|
17
|
+
* box, and an edge between two members of the same fold disappears: both ends are now the same box, and a
|
|
18
|
+
* self-loop would be a claim the source never made.
|
|
19
|
+
*
|
|
20
|
+
* Ids nobody declared are ignored, and folding a group whose contents are already gone changes nothing, so the
|
|
21
|
+
* function is total on any diagram and any set of names.
|
|
22
|
+
*/
|
|
23
|
+
export declare function collapse(diagram: Diagram, folded: Iterable<string>): Diagram;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two decompositions a drawing needs before it can be packed.
|
|
3
|
+
*
|
|
4
|
+
* **Weakly connected components.** Two subgraphs with no edge between them are two drawings that happen to share a
|
|
5
|
+
* page. Laying them out together makes them share one set of layers, so the wider one sets every column width and
|
|
6
|
+
* the narrower one is squeezed beside it; moving one moves the other. Split first, lay each out on its own, then pack
|
|
7
|
+
* the rectangles.
|
|
8
|
+
*
|
|
9
|
+
* **Packing.** A pile of rectangles has to become a page. The target is the aspect ratio a page and a screen share -
|
|
10
|
+
* 1.6 - and a shelf packer is enough to get there: sort by height, fill a shelf until the next rectangle would make
|
|
11
|
+
* the drawing wider than the page it is aiming at, then start the next shelf. No overlap, and a deterministic order,
|
|
12
|
+
* so the same rectangles always pack the same way.
|
|
13
|
+
*
|
|
14
|
+
* Pure, and asserted on the rectangles themselves.
|
|
15
|
+
*/
|
|
16
|
+
export interface SizedRectangle {
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
}
|
|
20
|
+
export interface PackedRectangle {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The nodes of each weakly connected component, in the order the nodes were written.
|
|
28
|
+
*
|
|
29
|
+
* An isolated node is a component of its own: it is not noise, it is a drawing with nothing joining it, and the
|
|
30
|
+
* packer is what decides where it goes.
|
|
31
|
+
*/
|
|
32
|
+
export declare function weakComponents(nodes: ReadonlyArray<{
|
|
33
|
+
id: string;
|
|
34
|
+
}>, edges: ReadonlyArray<{
|
|
35
|
+
from: string;
|
|
36
|
+
to: string;
|
|
37
|
+
}>): string[][];
|
|
38
|
+
export interface PackOptions {
|
|
39
|
+
/** The empty strip kept between two rectangles, on both axes. */
|
|
40
|
+
gap?: number;
|
|
41
|
+
/** The page the packer aims at; the drawing comes out as close to it as the rectangles allow. */
|
|
42
|
+
aspect?: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Pack rectangles into a page of roughly `aspect`, using shelves.
|
|
46
|
+
*
|
|
47
|
+
* The shelf width is derived from the total area and the target aspect: that is the width at which a perfect grid
|
|
48
|
+
* would have the wanted shape, so a shelf that fills it lands near the target. Rectangles are taken tallest first,
|
|
49
|
+
* which is what keeps a shelf from being mostly empty air above a short rectangle.
|
|
50
|
+
*/
|
|
51
|
+
export declare function packRectangles(rectangles: ReadonlyArray<SizedRectangle>, options?: PackOptions): {
|
|
52
|
+
placements: PackedRectangle[];
|
|
53
|
+
width: number;
|
|
54
|
+
height: number;
|
|
55
|
+
};
|
|
56
|
+
/** The aspect ratio of a box, as a number a reader can compare against the page's. */
|
|
57
|
+
export declare function aspectOf(width: number, height: number): number;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coordinate assignment: how far right each box sits. Brandes-Kopf.
|
|
3
|
+
*
|
|
4
|
+
* The layers are rows and the order inside them is settled; the only freedom left is the x of each box. Three
|
|
5
|
+
* properties make a drawing readable:
|
|
6
|
+
*
|
|
7
|
+
* - A box sits over what it is connected to, and a chain of boxes sits on one vertical line. Brandes and Kopf get
|
|
8
|
+
* this by *aligning*: every box is offered the median of its neighbours in the layer just above (or just below) and
|
|
9
|
+
* is joined to that neighbour's block when the join crosses no other edge. A block is then one node of a smaller
|
|
10
|
+
* graph, and a box that aligned downwards through five rows comes out as one straight segment.
|
|
11
|
+
* - Nothing overlaps, and the empty strip between two boxes in a row is the same everywhere. The blocks are compacted
|
|
12
|
+
* against each other by the least distance their rows allow, which is a longest path over the block graph: a block
|
|
13
|
+
* sits as far left as the block to its left permits, and no further.
|
|
14
|
+
* - The drawing is balanced rather than packed to one side. Aligning downwards alone would lean everything left, so
|
|
15
|
+
* the same two passes are run from the bottom up, and each pair is run once more mirrored left to right. The four
|
|
16
|
+
* layouts are then averaged. Averaging is a convex combination, and the separation constraints are linear, so the
|
|
17
|
+
* average of four layouts that keep the boxes apart also keeps them apart - the balance is free of overlaps by
|
|
18
|
+
* construction. A pass that starts at the top and one that starts at the bottom compact their blocks from opposite
|
|
19
|
+
* ends, so each side is first slid onto the origin of the first pass of that side, and only then averaged.
|
|
20
|
+
*
|
|
21
|
+
* Three things this module owes the rest of the pipeline rather than the paper:
|
|
22
|
+
*
|
|
23
|
+
* - A long edge's dummies share one x. They are one lane, not one point per row, so each lane is collapsed to the mean
|
|
24
|
+
* of the x its two ends want - the straight drop a reader follows. Collapsing can put a lane where a box already is,
|
|
25
|
+
* so the last pass spends the lane's space: boxes are pushed right past it and never the other way, and a row keeps
|
|
26
|
+
* the width its placeholders were given, which is the width a corridor needs when the lane has to go around a box.
|
|
27
|
+
* The lane is a proposal either way - the channel stage moves a lane into a free corridor when routing needs it, and
|
|
28
|
+
* a box never moves for a lane.
|
|
29
|
+
* - A lane is not a neighbour to be aligned with. A box that aligned itself to a lane passing through its row would be
|
|
30
|
+
* pulled off the boxes it does connect to, which is how a decision with one long edge comes out as a staircase. Lanes
|
|
31
|
+
* are placed by the compaction and then by the channel stage, which is where a lane belongs.
|
|
32
|
+
* - The empty strip between two rows is the caller's, not this module's: the y of a row is a running sum with one gap.
|
|
33
|
+
*
|
|
34
|
+
* Pure: layers and chains in, coordinates out.
|
|
35
|
+
*/
|
|
36
|
+
import type { Chain } from './dummy.js';
|
|
37
|
+
export interface Sized {
|
|
38
|
+
width: number;
|
|
39
|
+
height: number;
|
|
40
|
+
}
|
|
41
|
+
export interface CoordInput {
|
|
42
|
+
layers: string[][];
|
|
43
|
+
chains: Chain[];
|
|
44
|
+
sizeOf: (id: string) => Sized;
|
|
45
|
+
/** A dummy instance, and the shared lane it belongs to. */
|
|
46
|
+
chainOf: ReadonlyMap<string, string>;
|
|
47
|
+
}
|
|
48
|
+
export interface CoordOptions {
|
|
49
|
+
/** The empty space between two side-by-side boxes. */
|
|
50
|
+
gap: number;
|
|
51
|
+
/** The empty strip between two layers. */
|
|
52
|
+
layerGap: number;
|
|
53
|
+
}
|
|
54
|
+
export interface Coordinates {
|
|
55
|
+
/** Centre x, keyed by node id and by lane id. A long edge's dummies share one entry. */
|
|
56
|
+
x: Map<string, number>;
|
|
57
|
+
/** The top y of each layer. */
|
|
58
|
+
layerY: number[];
|
|
59
|
+
layerHeight: number[];
|
|
60
|
+
layerWidth: number[];
|
|
61
|
+
width: number;
|
|
62
|
+
height: number;
|
|
63
|
+
}
|
|
64
|
+
export declare function assignCoordinates(input: CoordInput, options: CoordOptions): Coordinates;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cycle breaking, in the order a layered layout does it.
|
|
3
|
+
*
|
|
4
|
+
* A layered drawing is a drawing of a DAG: an edge that runs back up the page has no layer to go to. The fix is to
|
|
5
|
+
* lay such an edge out backwards and draw its arrow the way the source wrote it - but *which* edges to turn around
|
|
6
|
+
* is a decision, and deciding it edge by edge is what makes two back edges of one cycle end up pointing opposite
|
|
7
|
+
* ways.
|
|
8
|
+
*
|
|
9
|
+
* So the graph is decomposed first. Tarjan's algorithm finds the strongly connected components; contracting each to
|
|
10
|
+
* a node gives the condensation, which is a DAG, so every edge between components already points one way and nothing
|
|
11
|
+
* between them needs reversing. Only inside a component - where by definition every edge lies on a cycle - is there
|
|
12
|
+
* an order to choose: the nodes are ranked once, greedily, and every edge inside the component is oriented along that
|
|
13
|
+
* single ranking. One ranking per component is what "consistently" means: after the pass, every surviving edge of a
|
|
14
|
+
* component points from a lower rank to a higher one, so the component cannot contain a cycle any more.
|
|
15
|
+
*
|
|
16
|
+
* The initial state is a special case: it is layer zero by definition, so an edge that runs into it is a back edge
|
|
17
|
+
* before the decomposition even starts. Turning those around leaves the state with no predecessor, so the ranking
|
|
18
|
+
* of its component puts it first and nothing is ever oriented back into it.
|
|
19
|
+
*
|
|
20
|
+
* Pure: the same graph in, the same edge directions out.
|
|
21
|
+
*/
|
|
22
|
+
import type { LayoutEdge, LayoutGraph } from './layout.js';
|
|
23
|
+
/** One strongly connected component: a set of nodes that can all reach each other. */
|
|
24
|
+
export interface Component {
|
|
25
|
+
/** The component's index in the condensation, numbered in topological order. */
|
|
26
|
+
id: number;
|
|
27
|
+
members: string[];
|
|
28
|
+
}
|
|
29
|
+
export interface BrokenGraph {
|
|
30
|
+
/** The same edges, in the same order, with the ones that close a cycle turned around. */
|
|
31
|
+
edges: LayoutEdge[];
|
|
32
|
+
/** The diagram edges whose direction the layout changed. */
|
|
33
|
+
reversed: Set<number>;
|
|
34
|
+
/** The strongly connected components, numbered in topological order. */
|
|
35
|
+
components: Component[];
|
|
36
|
+
componentOf: Map<string, number>;
|
|
37
|
+
/** Each node's rank inside its own component: every intra-component edge points from a lower rank to a higher. */
|
|
38
|
+
rank: Map<string, number>;
|
|
39
|
+
/** The condensation: an edge per pair of components that has at least one edge between them. Always acyclic. */
|
|
40
|
+
condensation: Array<{
|
|
41
|
+
from: number;
|
|
42
|
+
to: number;
|
|
43
|
+
}>;
|
|
44
|
+
}
|
|
45
|
+
export declare function breakCycles(graph: LayoutGraph): BrokenGraph;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural diff between two drawings.
|
|
3
|
+
*
|
|
4
|
+
* A text diff of two diagrams tells a reader that a line moved; it does not tell them that `Cache` left the
|
|
5
|
+
* `Platform` package, or that `Core` gained a nested boundary. Those are the changes a design review is about,
|
|
6
|
+
* and they are decided by comparing the containment tree, not the source.
|
|
7
|
+
*
|
|
8
|
+
* The diff judges movement, not merit: a group that moved, a member that changed owner, a boundary added or
|
|
9
|
+
* removed. Whether the new arrangement is better is the reader's call, so the verdict only says whether anything
|
|
10
|
+
* changed and whether the comparison could run at all.
|
|
11
|
+
*/
|
|
12
|
+
import type { Diagram } from './model.js';
|
|
13
|
+
export type ChangeKind = 'added' | 'removed' | 'renamed' | 'moved' | 'regrouped';
|
|
14
|
+
export interface StructuralChange {
|
|
15
|
+
code: string;
|
|
16
|
+
severity: 'error' | 'warning' | 'info';
|
|
17
|
+
kind: ChangeKind;
|
|
18
|
+
/** What changed, by id: a group id or a node id. */
|
|
19
|
+
subject: string;
|
|
20
|
+
message: string;
|
|
21
|
+
}
|
|
22
|
+
export interface DiffReport {
|
|
23
|
+
verdict: 'pass' | 'pass_with_findings' | 'fail';
|
|
24
|
+
changes: StructuralChange[];
|
|
25
|
+
/** Both sides, so an empty change list cannot be confused with a comparison that never ran. */
|
|
26
|
+
measured: {
|
|
27
|
+
before: {
|
|
28
|
+
nodes: number;
|
|
29
|
+
groups: number;
|
|
30
|
+
};
|
|
31
|
+
after: {
|
|
32
|
+
nodes: number;
|
|
33
|
+
groups: number;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Compare the containment structure of two drawings.
|
|
39
|
+
*
|
|
40
|
+
* Renames are reported by id: a group that kept its id but changed its text is a rename, while a group whose id
|
|
41
|
+
* is gone and another whose id is new are an unrelated removal and addition, because the ids are what a caller
|
|
42
|
+
* tracks across versions.
|
|
43
|
+
*/
|
|
44
|
+
export declare function diffStructure(before: Diagram, after: Diagram): DiffReport;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dummy nodes: the placeholders a long edge stands on.
|
|
3
|
+
*
|
|
4
|
+
* An edge from layer 0 to layer 3 has to cross layers 1 and 2. The router can go around whatever is in the way, but a
|
|
5
|
+
* drawing reads better if the edge owns a lane through those layers: then ordering can keep other edges off it, and
|
|
6
|
+
* coordinate assignment can give the whole lane one x. A dummy node is that lane: one per layer the edge crosses.
|
|
7
|
+
*
|
|
8
|
+
* A lane is one variable, not one variable per layer. The instances keep their own id (`#d<edge>_<layer>`) so the
|
|
9
|
+
* ordering stage can sort them inside their layer, and they all point at one lane id (`#e<edge>`) so coordinate
|
|
10
|
+
* assignment cannot give the same edge three different x values.
|
|
11
|
+
*
|
|
12
|
+
* Pure: the layers, the chains, and the map between the two.
|
|
13
|
+
*/
|
|
14
|
+
import type { LayoutEdge } from './layout.js';
|
|
15
|
+
import type { Layering } from './layer.js';
|
|
16
|
+
/** One edge, as the id it passes through in each layer it crosses. */
|
|
17
|
+
export interface Chain {
|
|
18
|
+
/** The diagram edge's index. */
|
|
19
|
+
index: number;
|
|
20
|
+
/** The ids from the layout source to the layout target: the endpoints plus one dummy per crossed layer. */
|
|
21
|
+
path: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface Augmented {
|
|
24
|
+
/** The layers with the dummies inserted; each dummy sits in the layer it crosses. */
|
|
25
|
+
layers: string[][];
|
|
26
|
+
chains: Chain[];
|
|
27
|
+
/** A dummy instance's lane id: the variable coordinate assignment shares between a long edge's dummies. */
|
|
28
|
+
chainOf: Map<string, string>;
|
|
29
|
+
/** The lane id of every edge that needed dummies. */
|
|
30
|
+
laneOf: Map<number, string>;
|
|
31
|
+
}
|
|
32
|
+
export declare function addDummies(layering: Layering, edges: LayoutEdge[]): Augmented;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mermaid text to a diagram, for the subset this renderer can actually draw.
|
|
3
|
+
*
|
|
4
|
+
* The subset is chosen by what a reader of an architecture document writes: a flowchart of boxes and
|
|
5
|
+
* arrows, a state diagram with a start marker, and the group syntax that turns a flat graph into a layered
|
|
6
|
+
* one. Everything outside it is refused **with its line number** rather than half-drawn: a picture that
|
|
7
|
+
* silently drops a lifeline or an interaction is worse than no picture, because it looks complete.
|
|
8
|
+
*
|
|
9
|
+
* Supported:
|
|
10
|
+
*
|
|
11
|
+
* - `graph TD|TB|LR` and `flowchart TD|TB|LR`, with direction carried into the layout as a hint;
|
|
12
|
+
* - node declarations `A[text]`, `A(text)`, `A{text}`, `A((text))`, and a bare `A`;
|
|
13
|
+
* - edges `-->`, `---`, `-.->`, `==>`, with a label as `-->|text|` or `-- text -->`;
|
|
14
|
+
* - `stateDiagram` / `stateDiagram-v2`: `[*] --> A`, `A --> B: event`, `state "text" as id`;
|
|
15
|
+
* - `subgraph name ... end`, which becomes a group;
|
|
16
|
+
* - `%%` comments and blank lines.
|
|
17
|
+
*
|
|
18
|
+
* Refused, by name: `sequenceDiagram`, `classDiagram`, `erDiagram`, `gantt`, `pie`, `journey`, `mindmap`,
|
|
19
|
+
* `timeline`, `quadrantChart`, `gitGraph`, `C4Context`, and `click`/`style`/`classDef` directives.
|
|
20
|
+
*/
|
|
21
|
+
import { type Diagram } from './model.js';
|
|
22
|
+
export interface Parsed {
|
|
23
|
+
diagram: Diagram;
|
|
24
|
+
/** Lines the parser refused to interpret, with the reason. Empty when the whole source was understood. */
|
|
25
|
+
refused: string[];
|
|
26
|
+
}
|
|
27
|
+
export declare function fromMermaid(source: string, title?: string): Parsed;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlantUML text to a diagram, for the subset this renderer can actually draw.
|
|
3
|
+
*
|
|
4
|
+
* PlantUML is a large language, and a renderer that accepted all of it would be a re-implementation of
|
|
5
|
+
* PlantUML. This one accepts what an architecture document actually contains - boxes, groups and arrows -
|
|
6
|
+
* and refuses the rest **by line**, because a component diagram that quietly loses a lifeline reads as
|
|
7
|
+
* complete and is not.
|
|
8
|
+
*
|
|
9
|
+
* Supported:
|
|
10
|
+
*
|
|
11
|
+
* - `@startuml` / `@enduml` (optional) and `title ...`;
|
|
12
|
+
* - elements: `component`, `node`, `rectangle`, `artifact` and `state` (a rectangle), `database` and
|
|
13
|
+
* `entity` (a cylinder), `usecase` (an ellipse), `[text]`, each with a `"text"` label and `as` alias;
|
|
14
|
+
* - boundaries: `package`, `folder`, `node`, `component`, `rectangle` and `artifact` blocks, closed by `}`;
|
|
15
|
+
* - arrows, either direction: association `-->`/`<--`, dependency `..>`/`<..`, generalisation `--|>`/`<|--`,
|
|
16
|
+
* realisation `..|>`/`<|..`, composition `*--`/`--*`, aggregation `o--`/`--o`, labelled after `:`;
|
|
17
|
+
* - an edge end that names a declared block attaches to that block's rectangle, the way an external
|
|
18
|
+
* dependency on a package is drawn; a name that is not a declared block is an implicit element;
|
|
19
|
+
* - `[*] --> X` for a start marker;
|
|
20
|
+
* - `'` comments and `skinparam`/`!theme` directives.
|
|
21
|
+
*
|
|
22
|
+
* Refused, by line and by name: the element keywords this renderer has no symbol for (`actor`, `class`,
|
|
23
|
+
* `cloud`, ...), the diagram kinds it does not draw (`sequence`), and an edge it cannot draw as written (a
|
|
24
|
+
* line with no arrow end, a mark at both ends, a dotted diamond, an edge between two boundaries, or an edge
|
|
25
|
+
* into a boundary that is nested or empty).
|
|
26
|
+
*/
|
|
27
|
+
import { type Diagram } from './model.js';
|
|
28
|
+
export interface Parsed {
|
|
29
|
+
diagram: Diagram;
|
|
30
|
+
/** Lines the parser refused to interpret, with the reason. Empty when the whole source was understood. */
|
|
31
|
+
refused: string[];
|
|
32
|
+
}
|
|
33
|
+
export declare function fromPlantUml(source: string, title?: string): Parsed;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The renderer's front door: whatever notation arrived, one drawing comes out.
|
|
3
|
+
*
|
|
4
|
+
* Three containers read this - the tool card, the full-screen view and the sidebar whiteboard - and each of
|
|
5
|
+
* them wants the same two things: shapes to draw and markup to embed. Neither knows which notation produced
|
|
6
|
+
* them, and neither needs to.
|
|
7
|
+
*
|
|
8
|
+
* Pure: no filesystem, because the same module is bundled into the page for the whiteboard.
|
|
9
|
+
*/
|
|
10
|
+
import type { Diagram } from './model.js';
|
|
11
|
+
import { type Canvas } from './paint.js';
|
|
12
|
+
import type { Palette, PaletteOptions } from './palette.js';
|
|
13
|
+
export type Notation = 'mermaid' | 'plantuml';
|
|
14
|
+
export interface RenderInput {
|
|
15
|
+
/** The notation, or `auto` to read it off the first non-empty line. */
|
|
16
|
+
notation?: Notation | 'auto';
|
|
17
|
+
source: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
/** Groups folded to a single box. Ids nobody declared are ignored. */
|
|
20
|
+
folded?: Iterable<string>;
|
|
21
|
+
}
|
|
22
|
+
export interface Rendered {
|
|
23
|
+
diagram: Diagram;
|
|
24
|
+
canvas: Canvas;
|
|
25
|
+
/** The colours the drawing was made with, so a container can paint the same surface behind it. */
|
|
26
|
+
colours: Palette;
|
|
27
|
+
/** Ready to embed: an `<svg>` with a viewBox and no fixed pixel size of its own. */
|
|
28
|
+
markup: string;
|
|
29
|
+
/** Which notation the source turned out to be. */
|
|
30
|
+
notation: Notation;
|
|
31
|
+
/** Lines the parser refused, with their reason. Empty when the whole source was understood. */
|
|
32
|
+
refused: string[];
|
|
33
|
+
/** Shapes the SVG writer had no case for. Must be zero; reported rather than hidden. */
|
|
34
|
+
dropped: number;
|
|
35
|
+
}
|
|
36
|
+
/** Read the notation off the source: PlantUML starts with `@startuml` or `@startmindmap`-style markers. */
|
|
37
|
+
export declare function detectNotation(source: string): Notation;
|
|
38
|
+
export declare function render(input: RenderInput, overrides?: Partial<Palette>, options?: PaletteOptions): Rendered;
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
import type { LayoutEdge, LayoutGraph } from './layout.js';
|
|
23
|
+
export interface Layering {
|
|
24
|
+
/** The ids of each layer, top to bottom. A layer holds at least one node. */
|
|
25
|
+
layers: string[][];
|
|
26
|
+
layerOf: Map<string, number>;
|
|
27
|
+
}
|
|
28
|
+
export declare function layer(graph: LayoutGraph, edges: LayoutEdge[]): Layering;
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
/** A box to place: an id, and the space its text needs. */
|
|
16
|
+
export interface LayoutNode {
|
|
17
|
+
id: string;
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
}
|
|
21
|
+
/** An edge to draw: which two boxes it joins, and which diagram edge it came from. */
|
|
22
|
+
export interface LayoutEdge {
|
|
23
|
+
/** The index in the diagram's own edge list, so the drawing can report a lane back for it. */
|
|
24
|
+
index: number;
|
|
25
|
+
from: string;
|
|
26
|
+
to: string;
|
|
27
|
+
}
|
|
28
|
+
/** One level of a nested drawing: what is placed together in a single pass of the pipeline. */
|
|
29
|
+
export interface LayoutGraph {
|
|
30
|
+
nodes: LayoutNode[];
|
|
31
|
+
edges: LayoutEdge[];
|
|
32
|
+
/** The box that must stay on layer zero, when the source named one. */
|
|
33
|
+
init?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface Ranked {
|
|
36
|
+
/** The state ids of each row, top to bottom, already ordered left to right. */
|
|
37
|
+
rows: string[][];
|
|
38
|
+
/** States no edge reaches from the initial one. */
|
|
39
|
+
unreachable: string[];
|
|
40
|
+
}
|
|
41
|
+
export declare function rankStates(nodes: ReadonlyArray<{
|
|
42
|
+
id: string;
|
|
43
|
+
}>, edges: ReadonlyArray<{
|
|
44
|
+
from: string;
|
|
45
|
+
to: string;
|
|
46
|
+
}>, init: string | undefined): Ranked;
|
|
47
|
+
/**
|
|
48
|
+
* The left-to-right seed the ordering stage starts from.
|
|
49
|
+
*
|
|
50
|
+
* Breadth-first discovery is what a reader expects a drawing to follow, so it is a better first
|
|
51
|
+
* guess than the order the parser collected. A state nothing reaches has no discovery order and
|
|
52
|
+
* keeps the order it was written in, after everything that was reached.
|
|
53
|
+
*/
|
|
54
|
+
export declare function seedOrder(graph: LayoutGraph): Map<string, number>;
|