dsh-model-everything 0.4.1 → 0.5.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 (78) hide show
  1. package/README.en-US.md +63 -19
  2. package/README.md +373 -345
  3. package/cordis.patch.yml +9 -0
  4. package/diagram/package.json +5 -0
  5. package/lib/client.js +1273 -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/collapse.js +163 -0
  14. package/lib/render/diff.js +138 -0
  15. package/lib/render/from-mermaid.js +211 -0
  16. package/lib/render/from-plantuml.js +158 -0
  17. package/lib/render/index.js +42 -0
  18. package/lib/render/layout.js +52 -0
  19. package/lib/render/model.js +17 -0
  20. package/lib/render/paint.js +194 -0
  21. package/lib/render/palette.js +127 -0
  22. package/lib/render/place.js +210 -0
  23. package/lib/render/svg.js +107 -0
  24. package/lib/render/viewmodel.js +126 -0
  25. package/lib/render/write.js +14 -0
  26. package/lib/scan.js +248 -8
  27. package/lib/structure-tool.js +2 -2
  28. package/lib/types/client.d.ts +83 -0
  29. package/lib/types/diagram-diff-tool.d.ts +8 -0
  30. package/lib/types/diagram-render-tool.d.ts +8 -0
  31. package/lib/types/diagram-review-tool.d.ts +8 -0
  32. package/lib/types/diagram.d.ts +34 -0
  33. package/lib/types/presentation.d.ts +7 -0
  34. package/lib/types/render/budget.d.ts +55 -0
  35. package/lib/types/render/collapse.d.ts +23 -0
  36. package/lib/types/render/diff.d.ts +44 -0
  37. package/lib/types/render/from-mermaid.d.ts +27 -0
  38. package/lib/types/render/from-plantuml.d.ts +27 -0
  39. package/lib/types/render/index.d.ts +38 -0
  40. package/lib/types/render/layout.d.ts +22 -0
  41. package/lib/types/render/model.d.ts +56 -0
  42. package/lib/types/render/paint.d.ts +94 -0
  43. package/lib/types/render/palette.d.ts +41 -0
  44. package/lib/types/render/place.d.ts +39 -0
  45. package/lib/types/render/svg.d.ts +19 -0
  46. package/lib/types/render/viewmodel.d.ts +20 -0
  47. package/lib/types/render/write.d.ts +14 -0
  48. package/lib/types/scan.d.ts +10 -2
  49. package/lib/types/structure.d.ts +1 -0
  50. package/package.json +35 -19
  51. package/skills/model-everything/SKILL.md +454 -454
  52. package/skills/model-everything/references/architecture-domains.md +137 -0
  53. package/skills/model-everything/references/architecture-views.md +119 -0
  54. package/skills/model-everything-diagram/SKILL.md +82 -0
  55. package/src/client.ts +307 -0
  56. package/src/diagram-diff-tool.ts +94 -0
  57. package/src/diagram-render-tool.ts +108 -0
  58. package/src/diagram-review-tool.ts +109 -0
  59. package/src/diagram.ts +61 -0
  60. package/src/gate.ts +14 -12
  61. package/src/presentation.ts +12 -0
  62. package/src/render/budget.ts +135 -0
  63. package/src/render/collapse.ts +155 -0
  64. package/src/render/diff.ts +162 -0
  65. package/src/render/from-mermaid.ts +236 -0
  66. package/src/render/from-plantuml.ts +178 -0
  67. package/src/render/index.ts +73 -0
  68. package/src/render/layout.ts +62 -0
  69. package/src/render/model.ts +67 -0
  70. package/src/render/paint.ts +290 -0
  71. package/src/render/palette.ts +164 -0
  72. package/src/render/place.ts +234 -0
  73. package/src/render/svg.ts +127 -0
  74. package/src/render/viewmodel.ts +136 -0
  75. package/src/render/write.ts +17 -0
  76. package/src/scan.ts +807 -564
  77. package/src/structure-tool.ts +2 -2
  78. package/src/structure.ts +1 -1
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The Client half: a lazy factory the page binds to this package's name.
3
+ *
4
+ * Everything that decides anything lives in `render/`; this file only turns shapes into React elements and
5
+ * registers them where the page offers a seat. Two seats are used, and they fail independently:
6
+ *
7
+ * - `tool.call.toolview` replaces the card of a tool whose result this package can draw;
8
+ * - `sidebar.right.pane.tab` is the whiteboard: a tab that renders whatever the reader pastes, with no tool
9
+ * and no host data at all, so it keeps working when the card seat does not.
10
+ *
11
+ * This module is not a page script: it exports the factory, and `scripts/build-client.mjs` wraps the
12
+ * compiled output in `window.__ModuleLoader__.load(...)`, which is the loader contract itself.
13
+ */
14
+ import { type Palette, type PaletteMode } from './render/palette.js';
15
+ export interface ReactLike {
16
+ createElement: (type: unknown, props?: Record<string, unknown> | null, ...children: unknown[]) => unknown;
17
+ useState?: <T>(initial: T) => [T, (next: T) => void];
18
+ }
19
+ export interface SlotContext {
20
+ slots: {
21
+ inject: (slot: string, callback: () => unknown) => unknown;
22
+ register: (options: Record<string, unknown>, component: unknown) => unknown;
23
+ };
24
+ get?: (service: string) => unknown;
25
+ }
26
+ export interface PluginShape {
27
+ inject: string[];
28
+ apply: (ctx: SlotContext) => void;
29
+ /** The whiteboard mount, with its scheduler injectable. `apply` calls this with the default. */
30
+ mount: (ctx: SlotContext, options?: MountOptions) => unknown;
31
+ }
32
+ /** A settled tool call, as the host hands it to a view. */
33
+ export interface ToolCallProps {
34
+ phase?: string;
35
+ toolName?: string;
36
+ block?: {
37
+ args?: unknown;
38
+ result?: unknown;
39
+ };
40
+ }
41
+ /** Tool names this package answers. A key that never dispatches simply never renders. */
42
+ export declare const TOOL_NAMES: string[];
43
+ /**
44
+ * Which surface the page is painting the drawing on.
45
+ *
46
+ * The host publishes its palette as private CSS variables, so naming them here would break on the next theme.
47
+ * The one fact that cannot drift is the rendered background: read it, and fall back to the media query, then
48
+ * to light. Never throws - a view that cannot read the document still draws.
49
+ */
50
+ export declare function themeMode(): PaletteMode;
51
+ /** The whiteboard's seat key, in both the tab registry and the body slot. */
52
+ export declare const WHITEBOARD_ID = "model-everything-diagram";
53
+ /** What the whiteboard opens with: a diagram the reader can edit rather than an empty box. */
54
+ export declare const WHITEBOARD_SAMPLE: string;
55
+ /**
56
+ * The view for one tool name.
57
+ *
58
+ * The drawing is emitted as markup, so React receives it through `dangerouslySetInnerHTML`. That is safe
59
+ * only because the writer escapes every value that came from a report or a model, which the render suite
60
+ * pins with an injection case.
61
+ */
62
+ export declare function viewFor(React: ReactLike, overrides?: Partial<Palette>): (props: ToolCallProps) => unknown;
63
+ /**
64
+ * The whiteboard: paste a diagram, get a drawing, zoom it.
65
+ *
66
+ * It holds no host data, which is the point: it is a place to think about a diagram while writing one, and
67
+ * it cannot be broken by a change to any tool's result shape. The source it draws is whatever is in its own
68
+ * box.
69
+ */
70
+ export declare function whiteboardFor(React: ReactLike, overrides?: Partial<Palette>): () => unknown;
71
+ export interface MountOptions {
72
+ /** Injected so the suite can drive the poll without waiting. Defaults to `setTimeout`. */
73
+ schedule?: (run: () => void, delay: number) => unknown;
74
+ }
75
+ /**
76
+ * Register the whiteboard tab once the sidebar's optional services are ready.
77
+ *
78
+ * The slot can be declared before `sidebarRightTabs` exists - the host composes services and seats in an
79
+ * order this plugin does not control - and a single `ctx.get` there is a race that loses the tab silently.
80
+ * So it polls, like the reference implementation does, and stops after a bounded number of attempts.
81
+ */
82
+ export declare function mountWhiteboard(React: ReactLike, ctx: SlotContext, options?: MountOptions): unknown;
83
+ export default function factory(require: (name: string) => unknown): PluginShape;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The structural diff tool: two drawings, compared by containment.
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_DIFF_TOOL_NAME = "diagram_diff";
8
+ export declare const diagramDiffTool: import("@deepseek-ai/dsh-tools").ToolDefinition;
@@ -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,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,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,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,27 @@
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
+ * - `component X`, `component "text" as X`, `[text] as X`, `[text]`;
13
+ * - `package "text" { ... }`, `node`, `folder`, `cloud`, `database` - a group, closed by `}`;
14
+ * - `X --> Y`, `X ---> Y`, `X ..> Y`, `X --> Y : label`, arrow either direction;
15
+ * - `state X`, `state "text" as X`, and `[*] --> X` for a start marker;
16
+ * - `'` comments and `skinparam`/`!theme` directives.
17
+ *
18
+ * Refused, by name: `sequence`, `actor`, `participant`, `usecase`, `class`, `object`, `note`, `legend`,
19
+ * `alt`/`opt`/`loop`/`par` and any other fragment keyword.
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 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,22 @@
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
+ import type { DiagramEdge, DiagramNode } from './model.js';
16
+ export interface Ranked {
17
+ /** The state ids of each row, top to bottom, already ordered left to right. */
18
+ rows: string[][];
19
+ /** States no edge reaches from the initial one. */
20
+ unreachable: string[];
21
+ }
22
+ export declare function rankStates(nodes: DiagramNode[], edges: DiagramEdge[], init: string | undefined): Ranked;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The diagram a renderer draws, in the notation it came from being nobody's business.
3
+ *
4
+ * Every source - a verification report, a Mermaid file, a PlantUML component diagram, a dependency scan -
5
+ * is reduced to this one shape before anything is laid out. A renderer that understood notations would have
6
+ * to be changed for each of them; a renderer that understands this does not.
7
+ *
8
+ * Pure data, no functions: a model can be serialised, compared and asserted.
9
+ */
10
+ /** What a node is, as far as colour is concerned. */
11
+ export type NodeRole = 'normal' | 'error' | 'warning' | 'muted';
12
+ export interface DiagramNode {
13
+ id: string;
14
+ /** The text drawn inside the box. Defaults to the id when a notation labelled it. */
15
+ label?: string;
16
+ role: NodeRole;
17
+ }
18
+ export interface DiagramEdge {
19
+ from: string;
20
+ to: string;
21
+ label: string;
22
+ /** A dashed edge, for notations that distinguish one. */
23
+ dashed?: boolean;
24
+ }
25
+ export interface DiagramGroup {
26
+ id: string;
27
+ label: string;
28
+ /**
29
+ * The nodes declared directly inside it: a group is a box around boxes, and its own members are the ones a
30
+ * source named there. A node inside a nested group is that group's member, not this one's.
31
+ */
32
+ members: string[];
33
+ /**
34
+ * The group that contains this one, when the source said so.
35
+ *
36
+ * Only a parser can know this - nesting is a property of where the text put the boundary - so the layout does
37
+ * not guess it. Left absent, the painter falls back to inferring containment from member sets, which is the
38
+ * best a hand-written model can offer.
39
+ */
40
+ parent?: string;
41
+ }
42
+ export interface Diagram {
43
+ kind: 'machine' | 'graph' | 'none';
44
+ /** The state the machine starts in, when the source declared one. */
45
+ init?: string;
46
+ title: string;
47
+ nodes: DiagramNode[];
48
+ edges: DiagramEdge[];
49
+ groups?: DiagramGroup[];
50
+ /** One line under the drawing: what was decided, in words the drawing cannot carry. */
51
+ caption: string;
52
+ }
53
+ /** An empty drawing, for a source that produced no nodes. */
54
+ export declare function emptyDiagram(title: string, caption: string): Diagram;
55
+ /** The label to draw for a node: its own label when the notation gave one, else its id. */
56
+ export declare function labelOf(node: DiagramNode): string;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Geometry: what a diagram turns into, in coordinates.
3
+ *
4
+ * A drawing has to survive three containers - inline in a conversation, full-screen, and a persistent
5
+ * sidebar tab - so nothing here knows about pixel size. It emits a content-sized canvas and lets the
6
+ * container scale it: the same numbers serve a 360px card and a 1200px whiteboard.
7
+ *
8
+ * Two placements, chosen by the source:
9
+ *
10
+ * - **layered** (no groups): breadth-first rows, forward edges dropping straight down, back edges on a side
11
+ * channel clear of every box, self loops as small loops. The shape a state machine wants.
12
+ * - **tree** (groups present): a group is a block, its members stack inside it, a nested group is a block
13
+ * inside its parent, and the top level stacks. The shape an architecture diagram wants, because a package
14
+ * whose members float somewhere else on the page is not showing containment at all.
15
+ *
16
+ * Edges are placed from where the boxes ended up, so neither placement needs to tell the edge code which one
17
+ * ran: "below" is a straight line, "beside" is horizontal, "above" routes around.
18
+ *
19
+ * Pure: a diagram in, shapes out, every decision assertable.
20
+ */
21
+ import { type ColourRole, type Palette, type PaletteOptions } from './palette.js';
22
+ import { type Diagram, type DiagramNode } from './model.js';
23
+ export interface Point {
24
+ x: number;
25
+ y: number;
26
+ }
27
+ export interface BoxShape {
28
+ kind: 'box';
29
+ x: number;
30
+ y: number;
31
+ width: number;
32
+ height: number;
33
+ label: string;
34
+ colour: ColourRole;
35
+ initial: boolean;
36
+ }
37
+ export interface LinkShape {
38
+ kind: 'link';
39
+ x1: number;
40
+ y1: number;
41
+ x2: number;
42
+ y2: number;
43
+ label: string;
44
+ labelX: number;
45
+ labelY: number;
46
+ colour: ColourRole;
47
+ }
48
+ export interface PathShape {
49
+ kind: 'path';
50
+ points: Point[];
51
+ label: string;
52
+ labelX: number;
53
+ labelY: number;
54
+ colour: ColourRole;
55
+ }
56
+ /** A boundary around several states: a package, a layer, a subgraph. */
57
+ export interface GroupShape {
58
+ kind: 'group';
59
+ /** The group's own id, so a container can fold it without guessing from the label. */
60
+ id: string;
61
+ x: number;
62
+ y: number;
63
+ width: number;
64
+ height: number;
65
+ label: string;
66
+ colour: ColourRole;
67
+ }
68
+ export interface CaptionShape {
69
+ kind: 'caption';
70
+ x: number;
71
+ y: number;
72
+ lines: string[];
73
+ colour: ColourRole;
74
+ }
75
+ export interface TitleShape {
76
+ kind: 'title';
77
+ x: number;
78
+ y: number;
79
+ text: string;
80
+ colour: ColourRole;
81
+ }
82
+ export type Shape = GroupShape | BoxShape | LinkShape | PathShape | CaptionShape | TitleShape;
83
+ export interface Canvas {
84
+ width: number;
85
+ height: number;
86
+ shapes: Shape[];
87
+ }
88
+ /** Which colour a box earns from its role. One mapping, used by every container. */
89
+ export declare function roleOfNode(node: DiagramNode): ColourRole;
90
+ export declare function paintWith(diagram: Diagram, overrides?: Partial<Palette>, options?: PaletteOptions): {
91
+ canvas: Canvas;
92
+ colours: Palette;
93
+ };
94
+ export declare function paint(diagram: Diagram, overrides?: Partial<Palette>): Canvas;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Verdict to colour, in the two surfaces the page can be in, and in the scheme a reader prefers.
3
+ *
4
+ * A drawing is read before it is parsed, so the first thing it says is the answer. The roles are named rather
5
+ * than coloured: a scheme maps a role to a value, and every scheme is chosen so the drawing survives both a
6
+ * light and a dark page - the reader never has to fix a palette to read a result.
7
+ *
8
+ * The mode is read at render time from the page's own background (see `client.ts`). The host publishes its
9
+ * palette as CSS variables whose names are private to it, and a drawing that hard-coded them would break on
10
+ * the next theme; the luminance of the surface is the one fact that cannot drift.
11
+ *
12
+ * Pure, and tested: a wrong role is a wrong verdict shown as a shape.
13
+ */
14
+ export type ColourRole = 'pass' | 'findings' | 'fail' | 'undecided' | 'muted' | 'text' | 'edge' | 'surface' | 'background';
15
+ export type Palette = Record<ColourRole, string>;
16
+ /** Which surface the drawing will sit on. */
17
+ export type PaletteMode = 'light' | 'dark';
18
+ /** A named set of colours. A scheme with both variants follows the page; one with only light does not. */
19
+ export type PaletteScheme = 'default' | 'github' | 'high-contrast' | 'print';
20
+ export interface PaletteOptions {
21
+ mode?: PaletteMode;
22
+ scheme?: PaletteScheme;
23
+ }
24
+ /**
25
+ * Resolve a palette.
26
+ *
27
+ * `overrides` win over everything, so a caller can retheme one role without inventing a scheme. A scheme with
28
+ * a dark variant follows the surface; one without stays as declared - high-contrast and print are what they
29
+ * say they are, on purpose. An unknown name falls back to the default palette rather than throwing, because
30
+ * the value can arrive from a DOM control.
31
+ */
32
+ export declare function palette(overrides?: Partial<Palette>, options?: PaletteOptions): Palette;
33
+ /**
34
+ * The role a verdict earns.
35
+ *
36
+ * `pass_with_findings` is deliberately not `pass`: it reads as the middle band, because a card that paints a
37
+ * report with warnings the same colour as a clean one has stopped carrying the answer.
38
+ */
39
+ export declare function roleForVerdict(verdict: string | undefined): ColourRole;
40
+ /** Every scheme by name, so a picker does not hard-code the list. */
41
+ export declare function schemeNames(): PaletteScheme[];
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Where the boxes go.
3
+ *
4
+ * Two placements, chosen by the source:
5
+ *
6
+ * - **layered** (no groups): breadth-first rows, centred, no containment - the shape a state machine wants.
7
+ * - **tree** (groups present): a group is a block, its members stack inside it, a nested group is a block inside
8
+ * its parent, and the top level stacks - the shape an architecture diagram wants, because a package whose
9
+ * members float somewhere else on the page is not showing containment at all.
10
+ *
11
+ * Split out of the painter so the two questions stay separate: this module decides positions, `paint.ts` decides
12
+ * what to draw between them. A change to edge routing cannot move a box, and a change to nesting cannot move a
13
+ * line.
14
+ */
15
+ import type { Diagram } from './model.js';
16
+ /** Geometry of one placed box, before anything is drawn. */
17
+ export type Box = {
18
+ x: number;
19
+ y: number;
20
+ width: number;
21
+ height: number;
22
+ };
23
+ /** What a placement returns: every box, every drawn boundary, and how wide the content is. */
24
+ export interface Placement {
25
+ boxes: Map<string, Box>;
26
+ groupRects: Map<string, Box>;
27
+ width: number;
28
+ }
29
+ /** Geometry constants shared by both placements. */
30
+ export declare const MARGIN = 20;
31
+ /** A box wide enough for its label, clamped so a long id cannot dominate the page. */
32
+ export declare const nodeWidth: (label: string) => number;
33
+ /**
34
+ * Place every box of a diagram.
35
+ *
36
+ * The choice of placement is a property of the source, not of the caller: a drawing that declares boundaries is
37
+ * read as containers and gets the tree; one that does not is read as a machine and gets the ranks.
38
+ */
39
+ export declare function place(diagram: Diagram, top: number): Placement;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shapes to SVG markup.
3
+ *
4
+ * The one module that knows tags, so it is also the one place where a shape can silently disappear if a
5
+ * writer forgets a case: the writer table has an entry per shape kind, and the caller counts the drops
6
+ * instead of hiding them.
7
+ *
8
+ * Three containers read this output - an inline card, a full-screen view, and a sidebar tab - so the markup
9
+ * carries no size of its own beyond its `viewBox`: the container decides how large the drawing is, and the
10
+ * same string serves a thumbnail and a whiteboard.
11
+ *
12
+ * Pure, and the output is asserted as text: escaping, geometry, arrowheads, wrapped captions.
13
+ */
14
+ import type { Canvas } from './paint.js';
15
+ import type { Palette } from './palette.js';
16
+ export declare function toSvg(canvas: Canvas, colours: Palette, label?: string): {
17
+ markup: string;
18
+ dropped: number;
19
+ };