dsh-model-everything 0.4.0 → 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 +148 -131
  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,137 @@
1
+ # Architecture by domain: what is decidable, and with what
2
+
3
+ The same engine is asked different questions in different domains. This file says, per domain, what the
4
+ architecture is made of, which claims decide something at design time, which of this plugin's families
5
+ decides them, and where the claim has to go instead.
6
+
7
+ Two rules apply everywhere:
8
+
9
+ - **The verdict is about the model.** A domain's real constraints — schedulability, timing closure,
10
+ trained-model quality, certification — are outside the model unless they were modelled.
11
+ - **Route the rest.** Every domain below ends with what this plugin cannot decide, and where to send it.
12
+
13
+ ## Layered systems, services and microservices
14
+
15
+ Made of: modules or services, their allowed dependencies, the direction of those dependencies, and the
16
+ contracts across the boundaries.
17
+
18
+ Decidable here:
19
+
20
+ - a dependency cycle, and the shortest one (UML022);
21
+ - an edge between two layers that the declared matrix forbids (UML023, UML024);
22
+ - a service nobody calls, or a required edge that is missing (UML020, UML025);
23
+ - an interface wider than its callers, a responsibility with no owner (`model_design_review`, DES-level).
24
+
25
+ Where it stops: a distributed transaction, a consistency guarantee, or a failure mode under partial
26
+ availability is not an edge. Those go to `model_compose_verify` when the protocol is modelled, and to a
27
+ saga or retry design review otherwise — the pattern language for that is Hohpe and Woolf's *Enterprise
28
+ Integration Patterns* and, for the failure side, Nygard's *Release It!*.
29
+
30
+ ## Event-driven and streaming
31
+
32
+ Made of: producers, topics, consumers, delivery guarantees, the order in which things are observed.
33
+
34
+ Decidable here: event completeness (every event handled), deadlock and livelock in a modelled consumer
35
+ protocol, an ordering hazard when two machines interleave (`model_compose_verify`), duplicate delivery
36
+ when the model declares idempotency.
37
+
38
+ Where it stops: the guarantee a broker actually provides. Idempotency as a property to model rather than
39
+ assume is Helland's argument (*Idempotence Is Not a Medical Condition*); the clock and ordering
40
+ foundations are Lamport's *Time, Clocks, and the Ordering of Events*.
41
+
42
+ ## Data and analytics
43
+
44
+ Made of: entities, keys, constraints, migrations, and the invariants the business relies on.
45
+
46
+ Decidable here: `model_datamodel_verify` — key and domain checks, referential integrity, migration
47
+ coverage, copy consistency, rollback symmetry, before/after preservation.
48
+
49
+ Where it stops: query performance, storage layout, and the modelling judgement itself. The relational
50
+ foundation is Codd (1970); dimensional design is Kimball and Ross; the distribution and consistency
51
+ vocabulary that a modern data architecture needs is Kleppmann's *Designing Data-Intensive Applications*;
52
+ organisational topology of data ownership is Dehghani's *Data Mesh*.
53
+
54
+ ## Distributed consensus and protocols
55
+
56
+ Made of: roles, messages, timeouts, elections, and the states each node may be in.
57
+
58
+ Decidable here: whether a protocol state machine can reach a forbidden state, whether two machines
59
+ deadlock on a handshake, whether an automaton accepts the language it claims, and whether two automata
60
+ accept the same one (`model_automaton_verify`). Export for a second opinion goes to TLA+, SPIN, UPPAAL or
61
+ PRISM.
62
+
63
+ Where it stops: liveness under fairness, and performance. The practical argument that this is worth doing
64
+ at all is Newcombe et al., *How Amazon Web Services Uses Formal Methods*; the specification language is
65
+ Lamport's *Specifying Systems*; consensus algorithms to read are Paxos (Lamport) and Raft (Ongaro and
66
+ Ousterhout).
67
+
68
+ ## Embedded and real-time
69
+
70
+ Made of: tasks, interrupts, shared state, modes, and the deadlines that decide whether the design works.
71
+
72
+ Decidable here: an interrupt handler and a task reaching the same state in an unsafe order
73
+ (`model_compose_verify` with the shared-location checks), a mode machine that can enter an impossible
74
+ combination (`model_verify`), a protocol FSM that drops a message the peer expects.
75
+
76
+ Where it stops, and this is the important part: **schedulability, worst-case execution time, jitter and
77
+ stack depth are not decided by a state machine.** They need rate-monotonic or response-time analysis
78
+ (Liu and Layland; Liu, *Real-Time Systems*), a timing-aware model (MARTE, timed automata), or measurement
79
+ on target. Architectural modelling standards for the domain are AUTOSAR and ARINC 653; certification
80
+ evidence is DO-178C for airborne software and ISO 26262 for automotive, and no report from this plugin is
81
+ evidence for either.
82
+
83
+ ## Hardware, SoC and platform architecture
84
+
85
+ Made of: IP blocks, buses, address maps, interrupts, and the hierarchy of subsystems.
86
+
87
+ Decidable here: an interface protocol described as a state machine, the dependency structure between
88
+ blocks (acyclic, layered), and the handshake between a controller and a peripheral model.
89
+
90
+ Where it stops: timing closure, power, area, and the register-level correctness of an RTL implementation.
91
+ The description standards worth knowing are IEEE 1685 (IP-XACT) for component descriptions and AMBA AXI
92
+ for the bus protocol; the architectural framing is platform-based design and the computer-architecture
93
+ background is Hennessy and Patterson.
94
+
95
+ ## Machine-learning systems
96
+
97
+ Made of: data sources, pipelines, features, training, serving, and the feedback loops between them.
98
+
99
+ Decidable here: the pipeline as a dependency graph (cycles, orphans, undeclared inputs), and the
100
+ data contract between stages as a data model.
101
+
102
+ Where it stops: whether the model is any good. That is data validation and evaluation, for which the
103
+ *ML Test Score* (Breck et al.) and *Hidden Technical Debt in Machine Learning Systems* (Sculley et al.)
104
+ are the vocabulary; Huyen's *Designing Machine Learning Systems* is the architecture-level treatment.
105
+
106
+ ## Safety, security and compliance architecture
107
+
108
+ Made of: hazards, controls, trust boundaries, and the evidence each claim needs.
109
+
110
+ Decidable here: almost nothing directly, and saying so is the point. This plugin can check that a
111
+ control's state machine cannot be bypassed, or that a trust boundary is not crossed by an undeclared
112
+ dependency. It cannot certify anything.
113
+
114
+ Where it stops: hazard analysis (STPA — Leveson and Thomas), threat modelling (Shostack, *Threat
115
+ Modeling: Designing for Security*), and every certification regime.
116
+
117
+ ## Provenance
118
+
119
+ | Part | Where it comes from |
120
+ |---|---|
121
+ | the per-domain split, and which family answers which question | this repository's own organisation of its tools |
122
+ | integration patterns and the failure-side vocabulary | Hohpe and Woolf, *Enterprise Integration Patterns* (2003); Nygard, *Release It!* (2007) |
123
+ | idempotency as a property to model | Helland, *Idempotence Is Not a Medical Condition*, ACM Queue (2012) |
124
+ | ordering in distributed systems | Lamport, *Time, Clocks, and the Ordering of Events in a Distributed System*, Communications of the ACM (1978) |
125
+ | the relational model | Codd, *A Relational Model of Data for Large Shared Data Banks*, Communications of the ACM (1970) |
126
+ | dimensional design and modern data architecture | Kimball and Ross, *The Data Warehouse Toolkit* (2013); Kleppmann, *Designing Data-Intensive Applications* (2017); Dehghani, *Data Mesh* (2022) |
127
+ | formal methods in industrial practice | Newcombe et al., *How Amazon Web Services Uses Formal Methods*, Communications of the ACM (2015); Lamport, *Specifying Systems* (2002) |
128
+ | consensus algorithms | Lamport, *The Part-Time Parliament* (1998); Ongaro and Ousterhout, *In Search of an Understandable Consensus Algorithm* (2014) |
129
+ | real-time analysis and the boundary of this plugin | Liu and Layland, *Scheduling Algorithms for Multiprogramming in a Hard-Real-Time Environment*, Journal of the ACM (1973); Liu, *Real-Time Systems* (2000); Kopetz, *Real-Time Systems* (2011) |
130
+ | embedded and safety standards named, not implemented | AUTOSAR; ARINC 653; DO-178C; ISO 26262; UML MARTE |
131
+ | hardware description and bus standards | IEEE 1685 (IP-XACT); AMBA AXI; Hennessy and Patterson, *Computer Architecture* |
132
+ | ML system architecture and its failure modes | Sculley et al., *Hidden Technical Debt in Machine Learning Systems* (2015); Breck et al., *The ML Test Score* (2017); Huyen, *Designing Machine Learning Systems* (2022) |
133
+ | hazard and threat analysis | Leveson and Thomas, *STPA Handbook* (2018); Shostack, *Threat Modeling: Designing for Security* (2014) |
134
+
135
+ Citations are given without volume and page numbers where those were not verified against the
136
+ publication. Every standard named here is named as a boundary — what the domain requires — not as
137
+ something this plugin implements.
@@ -0,0 +1,119 @@
1
+ # Architecture views, hierarchy, and conformance
2
+
3
+ The doctrine behind the design-time and as-is modes: what an architecture description is, why a level
4
+ tree is the unit a reviewer reads, and how a drawing is reconciled with code. Per-domain concerns are in
5
+ `architecture-domains.md`; the checks themselves live in the tools.
6
+
7
+ ## A view is what a reader acts on
8
+
9
+ An architecture is not one picture. It is a set of **views**, each answering one stakeholder's question,
10
+ and each written against a **viewpoint** that fixes what may appear in it. That framing is standardised
11
+ in ISO/IEC/IEEE 42010:2011 and predates it in practice: Kruchten's 4+1 model made the point that
12
+ logical, process, development and physical views are all "the architecture" and none of them is the
13
+ whole.
14
+
15
+ Two consequences for this plugin:
16
+
17
+ - **A report is a view.** A dependency graph, a state machine and a data model are three views of the
18
+ same system, and a verdict about one is not a verdict about the others.
19
+ - **The reader is fixed.** A view written for the architect who must decide is short and about
20
+ consequences; a view written for the team who must implement is longer and about interfaces. The
21
+ architecture-description literature keeps these apart deliberately — Clements et al. organise the
22
+ whole of *Documenting Software Architectures* around choosing the view for the audience, and Rozanski
23
+ and Woods write a viewpoint catalogue for exactly that reason.
24
+
25
+ Simon Brown's C4 model is the pragmatic version most working architects actually use: four zoom levels
26
+ (context, container, component, code), each one a different audience. It is not a formal method, and it
27
+ does not claim to decide anything — which is precisely the gap this plugin fills.
28
+
29
+ ## Why hierarchy is not an optional feature
30
+
31
+ Herbert Simon's *The Architecture of Complexity* (1962) is the argument: complex systems are
32
+ **nearly decomposable**, and their subsystems interact far more strongly internally than across
33
+ boundaries. That is why a level tree is how people describe and review them, and why a flat graph of a
34
+ large system is not a simplification but a different, less useful object.
35
+
36
+ The hierarchy literature supplies the semantics that makes a parent/child pair checkable rather than
37
+ decorative:
38
+
39
+ - **UML composite structure** describes a part inside a whole, with ports and connectors — the modelling
40
+ vocabulary for "this box expands".
41
+ - **Event-B (Abrial)** and the CSP/CCS refinement tradition (Hoare; Milner) give refinement as a relation
42
+ between levels: a child may decide more, and may not contradict the parent.
43
+ - Hierarchical graphs and algebraic graph transformation (Ehrig et al.) make containment itself the
44
+ morphism, which is what a parent/child diagram is.
45
+
46
+ This plugin takes three rules from that and nothing more:
47
+
48
+ 1. **Containment** — a child's scope is one node in the parent, and that node is expanded exactly once.
49
+ 2. **Refinement** — a child may not introduce a cross-boundary edge the parent does not have. It may
50
+ introduce everything internal to its scope.
51
+ 3. **Budget** — a level past the drawing threshold must be decomposed. The limit is not a rendering
52
+ apology; it is the point at which a view stops being reviewable.
53
+
54
+ ## Reconciling the drawing with the code
55
+
56
+ The as-is mode answers a question the design-time mode cannot: does the code still have the architecture
57
+ we say it has? The established name for the technique is **reflexion modelling** (Murphy, Notkin and
58
+ Sullivan): extract the implementation graph, declare the intended graph, and classify every edge as
59
+ agreed, absent, or convergent — the third category being the interesting one, because convergence is
60
+ usually a deliberate violation nobody wrote down.
61
+
62
+ Related traditions worth knowing, and what each adds:
63
+
64
+ - **Dependency Structure Matrix** (Steward; Browning) — cycles and layering read off a matrix instead of
65
+ a picture. A DSM is what tells you *which* edges to cut, where a graph tells you *that* there is a
66
+ cycle.
67
+ - **Software architecture reconstruction** (Kazman and Carriere) — recovering the conceptual
68
+ architecture from artefacts when no declaration exists. Our `repoPath` scan is the cheap end of this:
69
+ imports only, no clustering, no inference about intent.
70
+ - **Conformance tests in code** (ArchUnit and its descendants) — a rule checked by the build. That is the
71
+ right home for a rule once it is settled; this plugin is for deciding whether the rule is right.
72
+ - **Architecture erosion and drift** (Perry and Wolf for the vocabulary) — the observation that the
73
+ implemented architecture decays unless something keeps checking it.
74
+
75
+ The acyclic-dependencies and stable-dependencies principles (Martin) are the usual declared rules in this
76
+ space; they are what a matrix encodes, and the engine judges them as edges rather than as opinions.
77
+
78
+ ## Drawing a level
79
+
80
+ Layered graph drawing (Sugiyama, Tagawa and Toda) is the only layout this plugin needs to consider: place
81
+ nodes in ranks, order within a rank to reduce crossings, then route edges. The general reference is Di
82
+ Battista, Eades, Tamassia and Tollis, *Graph Drawing*.
83
+
84
+ Two aesthetic findings shape the choices here rather than taste: edge crossings and bends dominate how
85
+ quickly a reader parses a graph (Purchase's experiments on aesthetic criteria), and past a few dozen
86
+ nodes comprehension of any layout collapses. That is the evidence behind a fixed drawing budget: the
87
+ answer for a large level is a smaller level, not a cleverer layout.
88
+
89
+ ## Recording the decision
90
+
91
+ An architecture decision record is the artefact that survives the meeting. The practice is described by
92
+ Tyree and Akerman (*Architecture Decisions: Demystifying Architecture*, IEEE Software 2005) and
93
+ popularised by Nygard's *Documenting Architecture Decisions* (2011): context, decision, status,
94
+ consequences. arc42 is the document template most European teams use to hold them.
95
+
96
+ A record generated from a verified model has one property a hand-written one does not: every consequence
97
+ it states can name the check that produced it.
98
+
99
+ ## Provenance
100
+
101
+ | Part | Where it comes from |
102
+ |---|---|
103
+ | views and viewpoints as the unit of description | ISO/IEC/IEEE 42010:2011; Kruchten, *The 4+1 View Model of Architecture*, IEEE Software (1995) |
104
+ | choosing the view for its audience | Clements et al., *Documenting Software Architectures: Views and Beyond* (2010); Rozanski and Woods, *Software Systems Architecture* (2011) |
105
+ | zoom levels as a practical hierarchy | Brown, *The C4 Model for Visualising Software Architecture* (2018) |
106
+ | near-decomposability as the reason for levels | Simon, *The Architecture of Complexity* (1962) |
107
+ | containment and refinement semantics | UML 2.5 composite structure; Abrial, *Modeling in Event-B* (2010); Hoare, *Communicating Sequential Processes* (1985); Milner, *Communication and Concurrency* (1989) |
108
+ | the three hierarchy rules and the node budget | this repository's own decision, from the theory above |
109
+ | reflexion modelling as the as-is method | Murphy, Notkin and Sullivan, *Software Reflexion Models*, IEEE Transactions on Software Engineering (2001) |
110
+ | matrix view of cycles and layering | Steward, *The Design Structure System* (1981); Browning (2001) |
111
+ | reconstruction from artefacts | Kazman and Carriere, *Playing Detective: Reconstructing Software Architecture from Available Evidence* (1999) |
112
+ | erosion and drift | Perry and Wolf, *Foundations for the Study of Software Architecture* (1992) |
113
+ | acyclic and stable dependency principles | Martin, *Agile Software Development* (2002) |
114
+ | layered layout | Sugiyama, Tagawa and Toda, *Methods for Visual Understanding of Hierarchical System Structures* (1981); Di Battista et al., *Graph Drawing* (1999) |
115
+ | crossing and size limits | Purchase's studies of aesthetic criteria; the specific threshold is this repository's |
116
+ | decision records | Tyree and Akerman, *Architecture Decisions: Demystifying Architecture*, IEEE Software (2005); Nygard, *Documenting Architecture Decisions* (2011); arc42 |
117
+
118
+ Citations are given without volume and page numbers where those were not verified against the
119
+ publication. The argument does not depend on them; the reader who needs them can find the work by title.
@@ -0,0 +1,82 @@
1
+ ---
2
+ name: model-everything-diagram
3
+ description: "Use when drawing a diagram from its source text or checking its structure: Mermaid and PlantUML subsets, structure budgets, and containment diffs. Every line it cannot draw is refused by number. The drawing proves layout, not design. Doctrine: the model-everything skill.""
4
+ ---
5
+
6
+ # Model Everything — Drawing
7
+
8
+ A drawing is how a design is read, and a drawing that quietly drops a lifeline is worse than no drawing, because
9
+ it looks complete. This skill covers the drawing half: it renders the subset it can actually draw, refuses the rest
10
+ **by line**, measures the structure against budgets, and compares two drawings by what contains what.
11
+
12
+ The doctrine it inherits — say what a result does and does not prove, read `verdict` and never `ok` — lives in the
13
+ `model-everything` skill. Load that one when the question is whether a design is *correct*; load this one when the
14
+ question is what the picture shows.
15
+
16
+ ## When This Skill Applies
17
+
18
+ | The task involves… | What it does |
19
+ |---|---|
20
+ | "draw this mermaid", "render this PlantUML", "make a diagram of this flow" | `diagram_render`: source text to SVG, laid out and painted here, with `out` to write a file and `scheme` for the palette |
21
+ | "is this diagram too dense", "how deep do the packages nest" | `diagram_review`: group width, nesting depth and node count against budgets you set, plus **what it measured** |
22
+ | "what changed between these two diagrams", "did this package move" | `diagram_diff`: boundaries added, removed, renamed or re-parented, and nodes that changed owner |
23
+ | A source in a notation the plugin does not draw | Refused with the line number; `diagram_render` still draws everything else and reports what it skipped |
24
+ | A diagram already in a file, to link from a document | `diagram_render` with `out`, then link the SVG |
25
+
26
+ ## The two notations, and where each one stops
27
+
28
+ | Notation | Drawn | Refused by name |
29
+ |---|---|---|
30
+ | Mermaid | `graph`/`flowchart` (TD/TB/LR), node shapes `[..]`, `(..)`, `{..}`, `((..))`, `[(..)]`, `[[..]]`, `<<..>>`; edges `-->`, `---`, `-.->`, `==>`, labels `-->\|text\|` and `-- text -->`; `stateDiagram`/`-v2` with `[*]` start and end markers and `state "text" as id`; `subgraph … end` including nesting | `sequenceDiagram`, `classDiagram`, `erDiagram`, `gantt`, `pie`, `journey`, `mindmap`, `timeline`, `quadrantChart`, `gitGraph`, `C4Context`, and the `click`/`style`/`classDef` directives |
31
+ | PlantUML | `component`, `package`, `node`, `folder`, `cloud`, `database`, `state`, `artifact`, `rectangle` blocks and `[bracketed]` components; `-->`, `--->`, `..>`, either direction, `: label`; `[*] -->` start markers; `skinparam`/`!theme` ignored | `actor`, `participant`, `usecase`, `class`, `object`, `note`, `legend`, and the `alt`/`opt`/`loop`/`par` fragments |
32
+
33
+ A refusal is one line in `refused`, phrased `line N: …`. The drawing you get does not show the refused lines, and
34
+ the tool says so rather than leaving you to notice.
35
+
36
+ ## What the structure review reports
37
+
38
+ | Code | Severity | What it means |
39
+ |---|---|---|
40
+ | `B1_GROUP_TOO_WIDE` | error | A boundary holds more direct children than the budget. A package with twenty boxes has stopped reading as a package. |
41
+ | `B2_TOO_DEEP` | error | Nesting past the budget. Beyond three levels a reader needs the source, not the picture. |
42
+ | `B3_EMPTY_GROUP` | error | A boundary with nothing drawable inside it: it claims a containment the drawing cannot show. |
43
+ | `B4_SINGLE_MEMBER` | warning | A boundary around one thing. Often a layer that was never split, sometimes exactly right. |
44
+ | `B5_TOO_MANY_NODES` | error | The whole drawing past its node budget. |
45
+ | `D0_NO_SOURCE` | error | Nothing to review. |
46
+
47
+ `measured` is printed next to `findings` on purpose: an empty finding list from a source that never parsed would
48
+ otherwise read as a clean bill of health.
49
+
50
+ ## What the diff reports
51
+
52
+ | Code | Kind | What changed |
53
+ |---|---|---|
54
+ | `H1_GROUP_ADDED` | info | A boundary that did not exist, saying what contains it when it is nested |
55
+ | `H2_GROUP_REMOVED` | info | A boundary that is gone |
56
+ | `H3_GROUP_RENAMED` | info | Same id, different text |
57
+ | `H4_GROUP_MOVED` | warning | A boundary that changed parent, both sides named |
58
+ | `H5_NODE_ADDED` / `H6_NODE_REMOVED` | info | A node that appeared or disappeared |
59
+ | `H7_NODE_REGROUPED` | warning | A node that changed which group holds it |
60
+ | `D1_SIDE_UNREADABLE` | error | One side produced no drawing, so there was nothing to compare |
61
+
62
+ A rename is only a rename when the id survived. A vanished id beside a new one is an unrelated removal and
63
+ addition, because the ids are what a caller tracks across versions.
64
+
65
+ **It judges movement, not merit.** Whether a component belongs in its new package is the reader's call; the diff
66
+ reports that the move happened and nothing more.
67
+
68
+ ## The whiteboard
69
+
70
+ The right sidebar has a **Diagram** tab: paste a source, get the drawing. It scales to the pane, switches colour
71
+ scheme (`default`, `github`, `high-contrast`, `print`), follows the page's light or dark surface, and folds a group
72
+ into a single box when you click it. It reads no tool result, so it keeps working whatever changes on the tool side.
73
+
74
+ ## What this does not do
75
+
76
+ - **It does not verify behaviour.** A drawing of a machine says nothing about whether the machine is safe; that is
77
+ `model_verify` on the model the drawing came from.
78
+ - **It does not read a diagram back into a model.** `uml_parse` does that for the families it owns.
79
+ - **It does not audit dependencies.** `uml_structure_verify` reads a component diagram as a dependency graph and
80
+ judges the edges; `diagram_review` only measures the shape.
81
+ - **It does not draw everything.** A notation outside the subset above is refused by line; a picture that looks
82
+ complete and quietly lost half its arrows is the failure this whole subset exists to avoid.
package/src/client.ts ADDED
@@ -0,0 +1,307 @@
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
+
15
+ import { paintWith } from './render/paint.js'
16
+ import { schemeNames, type Palette, type PaletteMode, type PaletteScheme } from './render/palette.js'
17
+ import { render } from './render/index.js'
18
+ import { toSvg } from './render/svg.js'
19
+ import { viewModel } from './render/viewmodel.js'
20
+
21
+ export interface ReactLike {
22
+ createElement: (type: unknown, props?: Record<string, unknown> | null, ...children: unknown[]) => unknown
23
+ useState?: <T>(initial: T) => [T, (next: T) => void]
24
+ }
25
+
26
+ export interface SlotContext {
27
+ slots: {
28
+ inject: (slot: string, callback: () => unknown) => unknown
29
+ register: (options: Record<string, unknown>, component: unknown) => unknown
30
+ }
31
+ get?: (service: string) => unknown
32
+ }
33
+
34
+ export interface PluginShape {
35
+ inject: string[]
36
+ apply: (ctx: SlotContext) => void
37
+ /** The whiteboard mount, with its scheduler injectable. `apply` calls this with the default. */
38
+ mount: (ctx: SlotContext, options?: MountOptions) => unknown
39
+ }
40
+
41
+ /** A settled tool call, as the host hands it to a view. */
42
+ export interface ToolCallProps {
43
+ phase?: string
44
+ toolName?: string
45
+ block?: { args?: unknown; result?: unknown }
46
+ }
47
+
48
+ /** Tool names this package answers. A key that never dispatches simply never renders. */
49
+ export const TOOL_NAMES: string[] = ['model_verify']
50
+
51
+ /**
52
+ * Which surface the page is painting the drawing on.
53
+ *
54
+ * The host publishes its palette as private CSS variables, so naming them here would break on the next theme.
55
+ * The one fact that cannot drift is the rendered background: read it, and fall back to the media query, then
56
+ * to light. Never throws - a view that cannot read the document still draws.
57
+ */
58
+ export function themeMode(): PaletteMode {
59
+ try {
60
+ // Reached through a loose view of the globals: this module is also loaded in Node by the bundle suite,
61
+ // where `document` does not exist at all.
62
+ const scope = globalThis as unknown as {
63
+ document?: { body?: unknown }
64
+ getComputedStyle?: (element: unknown) => { backgroundColor?: string }
65
+ }
66
+ const body = scope.document?.body
67
+ const computed = typeof scope.getComputedStyle === 'function' && body !== undefined ? scope.getComputedStyle(body) : undefined
68
+ const colour = computed?.backgroundColor ?? ''
69
+ const rgb = /rgba?\((\d+),\s*(\d+),\s*(\d+)/.exec(colour)
70
+ if (rgb !== null) {
71
+ const [r, g, b] = [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])]
72
+ // Rec. 601 luma: good enough to tell a light surface from a dark one, and cheap.
73
+ return (0.299 * r + 0.587 * g + 0.114 * b) / 255 < 0.5 ? 'dark' : 'light'
74
+ }
75
+ } catch {
76
+ // A document that refuses to be read is not a reason to fail a render.
77
+ }
78
+ return 'light'
79
+ }
80
+
81
+ /** The background a drawing sits on, so a card does not paint dark lines on a dark page. */
82
+ function surfaceStyle(colours: Palette): Record<string, string> {
83
+ return { background: colours.background, color: colours.text, padding: '4px 0', overflowX: 'auto' }
84
+ }
85
+
86
+
87
+ /** The whiteboard's seat key, in both the tab registry and the body slot. */
88
+ export const WHITEBOARD_ID = 'model-everything-diagram'
89
+
90
+ /** What the whiteboard opens with: a diagram the reader can edit rather than an empty box. */
91
+ export const WHITEBOARD_SAMPLE = ['graph TD', ' a[Idea] --> b{Feasible?}', ' b -->|yes| c[Build]', ' b -->|no| d[Revise]', ' d --> a'].join('\n')
92
+
93
+ /**
94
+ * The view for one tool name.
95
+ *
96
+ * The drawing is emitted as markup, so React receives it through `dangerouslySetInnerHTML`. That is safe
97
+ * only because the writer escapes every value that came from a report or a model, which the render suite
98
+ * pins with an injection case.
99
+ */
100
+ export function viewFor(React: ReactLike, overrides: Partial<Palette> = {}): (props: ToolCallProps) => unknown {
101
+ return (props: ToolCallProps): unknown => {
102
+ // The whole block goes to the view model: the host shapes it differently per phase (a lazy arguments
103
+ // view while running, content blocks once settled), and only that module knows how to read it.
104
+ const diagram = viewModel({ phase: props.phase, toolName: props.toolName, block: props.block })
105
+ const { canvas, colours } = paintWith(diagram, overrides, { mode: themeMode() })
106
+ const written = toSvg(canvas, colours, diagram.kind === 'none' ? diagram.caption : diagram.title + ': ' + diagram.caption)
107
+ return React.createElement('div', {
108
+ style: surfaceStyle(colours),
109
+ dangerouslySetInnerHTML: { __html: written.markup },
110
+ })
111
+ }
112
+ }
113
+
114
+ /**
115
+ * The whiteboard: paste a diagram, get a drawing, zoom it.
116
+ *
117
+ * It holds no host data, which is the point: it is a place to think about a diagram while writing one, and
118
+ * it cannot be broken by a change to any tool's result shape. The source it draws is whatever is in its own
119
+ * box.
120
+ */
121
+ export function whiteboardFor(React: ReactLike, overrides: Partial<Palette> = {}): () => unknown {
122
+ const h = (type: unknown, props?: Record<string, unknown> | null, ...children: unknown[]): unknown => React.createElement(type, props, ...children)
123
+ return function Whiteboard(): unknown {
124
+ const useState = React.useState as (<T>(initial: T) => [T, (next: T) => void]) | undefined
125
+ if (useState === undefined) {
126
+ // A host without hooks: the same board, read-only. It still draws, which is the part that matters.
127
+ return h(
128
+ 'div',
129
+ { style: { padding: '8px' } },
130
+ h('textarea', {
131
+ value: WHITEBOARD_SAMPLE,
132
+ readOnly: true,
133
+ spellcheck: false,
134
+ rows: WHITEBOARD_SAMPLE.split('\n').length,
135
+ style: { width: '100%', font: '12px ui-monospace, SFMono-Regular, Menlo, monospace' },
136
+ }),
137
+ // The picker is present but inert: a host without hooks cannot hold the choice, and hiding the
138
+ // option would make the board look like it has none.
139
+ h('select', { disabled: true, style: { margin: '4px 0', fontSize: '12px' } },
140
+ ...schemeNames().map((name) => h('option', { value: name, selected: name === 'default' }, name))),
141
+ whiteboardBody(React, overrides, WHITEBOARD_SAMPLE, 1),
142
+ )
143
+ }
144
+ const [source, setSource] = useState(WHITEBOARD_SAMPLE)
145
+ const [zoom, setZoom] = useState(1)
146
+ const [scheme, setScheme] = useState<PaletteScheme>('default')
147
+ const [fit, setFit] = useState(true)
148
+ const [folded, setFolded] = useState<string[]>([])
149
+ const drawn = drawSample(React, overrides, source, fit ? 0 : zoom, scheme, folded)
150
+ const textarea = h('textarea', {
151
+ value: source,
152
+ spellcheck: false,
153
+ rows: Math.min(12, Math.max(4, String(source).split('\n').length)),
154
+ style: { width: '100%', font: '12px ui-monospace, SFMono-Regular, Menlo, monospace', padding: '6px', resize: 'vertical' },
155
+ onInput: (event: { target?: { value?: unknown } }) => setSource(String(event?.target?.value ?? '')),
156
+ })
157
+ const controls = h(
158
+ 'div',
159
+ { style: { display: 'flex', gap: '6px', alignItems: 'center', padding: '4px 0', flexWrap: 'wrap' } },
160
+ h('button', { type: 'button', onClick: () => { setFit(false); setZoom(Math.max(0.5, Math.round((Number(zoom) - 0.25) * 100) / 100)) } }, '−'),
161
+ h('span', { style: { fontSize: '12px', opacity: 0.7 } }, fit ? 'fit' : String(Math.round(Number(zoom) * 100)) + '%'),
162
+ h('button', { type: 'button', onClick: () => { setFit(false); setZoom(Math.min(3, Math.round((Number(zoom) + 0.25) * 100) / 100)) } }, '+'),
163
+ h('button', { type: 'button', onClick: () => { setFit(false); setZoom(1) } }, 'reset'),
164
+ h('button', { type: 'button', title: 'Scale to the pane width', onClick: () => setFit(!fit) }, fit ? 'fit: on' : 'fit: off'),
165
+ folded.length === 0
166
+ ? null
167
+ : h('button', { type: 'button', title: 'Unfold every group', onClick: () => setFolded([]) }, 'unfold ' + String(folded.length)),
168
+ h(
169
+ 'select',
170
+ {
171
+ value: scheme,
172
+ title: 'Colour scheme',
173
+ style: { marginLeft: 'auto', fontSize: '12px' },
174
+ onChange: (event: { target?: { value?: unknown } }) => setScheme(String(event?.target?.value ?? 'default') as PaletteScheme),
175
+ },
176
+ ...schemeNames().map((name) => h('option', { value: name, selected: name === scheme }, name)),
177
+ ),
178
+ )
179
+ return h(
180
+ 'div',
181
+ { style: { display: 'flex', flexDirection: 'column', gap: '4px', height: '100%', boxSizing: 'border-box', padding: '8px' } },
182
+ textarea,
183
+ controls,
184
+ // The drawing takes the rest of the pane and scrolls inside it, so an expanded tab uses its room.
185
+ h(
186
+ 'div',
187
+ {
188
+ style: { flex: '1 1 auto', minHeight: '0', overflow: 'auto' },
189
+ // A boundary carries its id in the markup, so one listener on the container is enough: no per-rect
190
+ // handler, and the handling survives a re-render that replaced the whole SVG.
191
+ onClick: (event: { target?: { closest?: (selector: string) => { getAttribute?: (name: string) => string | null } | null } }) => {
192
+ const hit = event?.target?.closest?.('[data-group-id]')
193
+ const id = hit?.getAttribute?.('data-group-id')
194
+ if (typeof id !== 'string' || id === '') return
195
+ setFolded(folded.includes(id) ? folded.filter((entry) => entry !== id) : [...folded, id])
196
+ },
197
+ },
198
+ drawn,
199
+ ),
200
+ )
201
+ }
202
+ }
203
+
204
+ function drawSample(React: ReactLike, overrides: Partial<Palette>, source: string, zoom: number, scheme: PaletteScheme, folded: string[] = []): unknown {
205
+ return whiteboardBody(React, overrides, source, zoom, scheme, folded)
206
+ }
207
+
208
+ /** Render one source at one zoom, or say why it could not be drawn. */
209
+ function whiteboardBody(React: ReactLike, overrides: Partial<Palette>, source: string, zoom: number, scheme: PaletteScheme = 'default', folded: string[] = []): unknown {
210
+ if (String(source).trim() === '') {
211
+ return React.createElement('p', { style: { fontSize: '12px', opacity: 0.7 } }, 'Paste Mermaid or PlantUML above.')
212
+ }
213
+ const drawn = render({ source: String(source), folded }, overrides, { mode: themeMode(), scheme })
214
+ // Zoom 0 is "fill the pane": the SVG scales to the width it is given, which is what an expanded tab wants.
215
+ const zoomed =
216
+ zoom === 0
217
+ ? drawn.markup.replace('<svg ', '<svg style="width: 100%; height: auto; display: block" ')
218
+ : drawn.markup.replace('<svg ', '<svg style="zoom: ' + String(zoom) + '; display: block" ')
219
+ const refused = drawn.refused.length === 0 ? null : React.createElement('pre', {
220
+ style: { fontSize: '11px', color: '#b7791f', whiteSpace: 'pre-wrap', margin: '6px 0' },
221
+ }, drawn.refused.join('\n'))
222
+ return React.createElement(
223
+ 'div',
224
+ { style: { background: drawn.colours.background, color: drawn.colours.text, borderRadius: '6px' } },
225
+ React.createElement('div', { dangerouslySetInnerHTML: { __html: zoomed } }),
226
+ refused,
227
+ )
228
+ }
229
+
230
+
231
+ /** How long to wait for the optional services, and how often. A page that never offers them registers nothing. */
232
+ const SIDEBAR_POLL_MS = 250
233
+ const SIDEBAR_POLL_ATTEMPTS = 40
234
+
235
+ interface SidebarTabsService {
236
+ register?: (options: Record<string, unknown>) => unknown
237
+ }
238
+
239
+ export interface MountOptions {
240
+ /** Injected so the suite can drive the poll without waiting. Defaults to `setTimeout`. */
241
+ schedule?: (run: () => void, delay: number) => unknown
242
+ }
243
+
244
+ /**
245
+ * Register the whiteboard tab once the sidebar's optional services are ready.
246
+ *
247
+ * The slot can be declared before `sidebarRightTabs` exists - the host composes services and seats in an
248
+ * order this plugin does not control - and a single `ctx.get` there is a race that loses the tab silently.
249
+ * So it polls, like the reference implementation does, and stops after a bounded number of attempts.
250
+ */
251
+ export function mountWhiteboard(React: ReactLike, ctx: SlotContext, options: MountOptions = {}): unknown {
252
+ const schedule =
253
+ options.schedule ??
254
+ ((run: () => void, delay: number) => {
255
+ // A host (or a test sandbox) without timers simply gets no retry, rather than an exception.
256
+ const timer = (globalThis as unknown as { setTimeout?: (f: () => void, ms: number) => unknown }).setTimeout
257
+ return typeof timer === 'function' ? timer(run, delay) : undefined
258
+ })
259
+ let attempts = 0
260
+
261
+ const attempt = (): unknown => {
262
+ attempts += 1
263
+ const tabs = typeof ctx.get === 'function' ? (ctx.get('sidebarRightTabs') as SidebarTabsService | undefined) : undefined
264
+ if (tabs === undefined || typeof tabs.register !== 'function') {
265
+ if (attempts < SIDEBAR_POLL_ATTEMPTS) schedule(attempt, SIDEBAR_POLL_MS)
266
+ return undefined
267
+ }
268
+ tabs.register({
269
+ id: WHITEBOARD_ID,
270
+ kind: 'panel',
271
+ keepMounted: true,
272
+ title: () => 'Diagram',
273
+ guide: [
274
+ {
275
+ id: 'diagram-whiteboard',
276
+ order: 120,
277
+ title: () => 'Diagram',
278
+ description: () => 'Paste Mermaid or PlantUML and read the drawing',
279
+ },
280
+ ],
281
+ })
282
+ return ctx.slots.register({ name: 'sidebar.right.pane.tab', key: WHITEBOARD_ID }, whiteboardFor(React))
283
+ }
284
+
285
+ return attempt()
286
+ }
287
+
288
+ export default function factory(require: (name: string) => unknown): PluginShape {
289
+ // Captured under a plain name: a static `require('react')` would be rewritten by the bundler and then
290
+ // resolve against a scope the factory body does not have. A call through this alias stays a call.
291
+ const hostRequire = require
292
+ const React = hostRequire('react') as ReactLike
293
+ return {
294
+ inject: ['slots'],
295
+ mount: (ctx: SlotContext, options: MountOptions = {}) => mountWhiteboard(React, ctx, options),
296
+ apply(ctx: SlotContext) {
297
+ for (const name of TOOL_NAMES) {
298
+ // Only the options a keyed slot declares: `key`. An extra `id` is not part of this slot's
299
+ // registration schema, and the working community reference (dsh-deepread) passes exactly this much.
300
+ ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({ name: 'tool.call.toolview', key: name }, viewFor(React)))
301
+ }
302
+ // The whiteboard seat only exists on a host with the official right sidebar. Presence of the slot is
303
+ // the capability check: if the callback never runs, this host has no sidebar and nothing is lost.
304
+ ctx.slots.inject('sidebar.right.pane.tab', () => mountWhiteboard(React, ctx))
305
+ },
306
+ }
307
+ }