pumllint 0.21.1__tar.gz
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.
- pumllint-0.21.1/LICENSE +21 -0
- pumllint-0.21.1/PKG-INFO +558 -0
- pumllint-0.21.1/README.md +527 -0
- pumllint-0.21.1/pumllint/__init__.py +68 -0
- pumllint-0.21.1/pumllint/__main__.py +3 -0
- pumllint-0.21.1/pumllint/baseline.py +130 -0
- pumllint-0.21.1/pumllint/cli.py +356 -0
- pumllint-0.21.1/pumllint/config.py +40 -0
- pumllint-0.21.1/pumllint/engine.py +225 -0
- pumllint-0.21.1/pumllint/fixer.py +203 -0
- pumllint-0.21.1/pumllint/model.py +512 -0
- pumllint-0.21.1/pumllint/parser/__init__.py +3 -0
- pumllint-0.21.1/pumllint/parser/activity.py +213 -0
- pumllint-0.21.1/pumllint/parser/class_.py +193 -0
- pumllint-0.21.1/pumllint/parser/sequence.py +475 -0
- pumllint-0.21.1/pumllint/parser/state.py +143 -0
- pumllint-0.21.1/pumllint/reporters/__init__.py +4 -0
- pumllint-0.21.1/pumllint/reporters/base.py +51 -0
- pumllint-0.21.1/pumllint/reporters/builtin.py +346 -0
- pumllint-0.21.1/pumllint/reporters/html.py +193 -0
- pumllint-0.21.1/pumllint/rules/__init__.py +125 -0
- pumllint-0.21.1/pumllint/rules/activity/__init__.py +0 -0
- pumllint-0.21.1/pumllint/rules/activity/structure.py +162 -0
- pumllint-0.21.1/pumllint/rules/catalog.toml +439 -0
- pumllint-0.21.1/pumllint/rules/class_/__init__.py +0 -0
- pumllint-0.21.1/pumllint/rules/class_/structure.py +165 -0
- pumllint-0.21.1/pumllint/rules/common/__init__.py +0 -0
- pumllint-0.21.1/pumllint/rules/common/consistency.py +199 -0
- pumllint-0.21.1/pumllint/rules/common/governance.py +315 -0
- pumllint-0.21.1/pumllint/rules/sequence/__init__.py +0 -0
- pumllint-0.21.1/pumllint/rules/sequence/codegen.py +367 -0
- pumllint-0.21.1/pumllint/rules/sequence/flows.py +230 -0
- pumllint-0.21.1/pumllint/rules/sequence/participants.py +81 -0
- pumllint-0.21.1/pumllint/rules/state/__init__.py +0 -0
- pumllint-0.21.1/pumllint/rules/state/structure.py +84 -0
- pumllint-0.21.1/pumllint/schema.py +147 -0
- pumllint-0.21.1/pumllint/schemas/lint.schema.json +23 -0
- pumllint-0.21.1/pumllint/schemas/score.schema.json +145 -0
- pumllint-0.21.1/pumllint/scoring.py +592 -0
- pumllint-0.21.1/pumllint/syntax.py +50 -0
- pumllint-0.21.1/pumllint.egg-info/PKG-INFO +558 -0
- pumllint-0.21.1/pumllint.egg-info/SOURCES.txt +65 -0
- pumllint-0.21.1/pumllint.egg-info/dependency_links.txt +1 -0
- pumllint-0.21.1/pumllint.egg-info/entry_points.txt +2 -0
- pumllint-0.21.1/pumllint.egg-info/requires.txt +7 -0
- pumllint-0.21.1/pumllint.egg-info/top_level.txt +1 -0
- pumllint-0.21.1/pyproject.toml +57 -0
- pumllint-0.21.1/setup.cfg +4 -0
- pumllint-0.21.1/tests/test_baseline.py +135 -0
- pumllint-0.21.1/tests/test_catalog.py +50 -0
- pumllint-0.21.1/tests/test_cli_score.py +377 -0
- pumllint-0.21.1/tests/test_codegen_rules.py +458 -0
- pumllint-0.21.1/tests/test_config.py +37 -0
- pumllint-0.21.1/tests/test_corpus.py +99 -0
- pumllint-0.21.1/tests/test_crossfile.py +245 -0
- pumllint-0.21.1/tests/test_engine.py +121 -0
- pumllint-0.21.1/tests/test_features_sync.py +40 -0
- pumllint-0.21.1/tests/test_fix.py +165 -0
- pumllint-0.21.1/tests/test_golden_scores.py +55 -0
- pumllint-0.21.1/tests/test_packaging.py +122 -0
- pumllint-0.21.1/tests/test_pilot_example.py +48 -0
- pumllint-0.21.1/tests/test_public_api.py +29 -0
- pumllint-0.21.1/tests/test_reporters_maturity.py +335 -0
- pumllint-0.21.1/tests/test_rules.py +818 -0
- pumllint-0.21.1/tests/test_schema.py +227 -0
- pumllint-0.21.1/tests/test_scoring.py +470 -0
- pumllint-0.21.1/tests/test_syntax.py +85 -0
pumllint-0.21.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 fdurieux
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pumllint-0.21.1/PKG-INFO
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pumllint
|
|
3
|
+
Version: 0.21.1
|
|
4
|
+
Summary: Semantic linter and maturity scorer for PlantUML diagrams, with SonarQube export
|
|
5
|
+
Author-email: fdurieux <fdurieux@i-kei.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/fdurieux/pumllint
|
|
8
|
+
Project-URL: Documentation, https://github.com/fdurieux/pumllint/tree/main/docs
|
|
9
|
+
Project-URL: Changelog, https://github.com/fdurieux/pumllint/releases
|
|
10
|
+
Project-URL: Issues, https://github.com/fdurieux/pumllint/issues
|
|
11
|
+
Project-URL: Live example report, https://fdurieux.github.io/pumllint/example-maturity-report.html
|
|
12
|
+
Keywords: plantuml,uml,linter,lint,static-analysis,diagrams,architecture,maturity-model,sonarqube,ci
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Documentation
|
|
21
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: yaml
|
|
26
|
+
Requires-Dist: PyYAML>=6; extra == "yaml"
|
|
27
|
+
Provides-Extra: test
|
|
28
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
29
|
+
Requires-Dist: pytest-bdd>=7; extra == "test"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# pumllint
|
|
33
|
+
|
|
34
|
+
[](https://pypi.org/project/pumllint/)
|
|
35
|
+
[](LICENSE)
|
|
36
|
+
|
|
37
|
+
A **semantic linter for PlantUML diagrams**. PlantUML validates syntax but is,
|
|
38
|
+
by its own admission, a drawing tool rather than a modeling tool: it happily
|
|
39
|
+
renders inconsistent diagrams. `pumllint` fills that gap with modeling-hygiene
|
|
40
|
+
and governance rules, and exports findings to **SonarQube** without needing a
|
|
41
|
+
SonarQube plugin.
|
|
42
|
+
|
|
43
|
+
Zero runtime dependencies (PyYAML only if you use a YAML config). Python ≥ 3.11.
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install pumllint # or: pipx / uv tool install pumllint
|
|
49
|
+
|
|
50
|
+
pumllint diagrams/ # lint a directory recursively
|
|
51
|
+
pumllint --list-rules # what can it check?
|
|
52
|
+
pumllint diagrams/ -f sonar -o pumllint-sonar.json
|
|
53
|
+
pumllint --profile codegen diagrams/ # + codegen-readiness rules
|
|
54
|
+
pumllint score diagrams/ --min-level 3 # maturity gate (see below)
|
|
55
|
+
pumllint fix diagrams/ # auto-fix mechanical findings
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
(`python -m pumllint` is equivalent wherever the console script is not on PATH.)
|
|
59
|
+
|
|
60
|
+
Exit codes: `0` clean, `1` findings at/above `--fail-on` (default `major`), `2` usage error — drop it straight into CI.
|
|
61
|
+
|
|
62
|
+
## Documentation by audience
|
|
63
|
+
|
|
64
|
+
This README is the reference. [docs/](docs/README.md) has role-specific guides:
|
|
65
|
+
[why adopt it](docs/case-for-pumllint.md) (management case, with the measured
|
|
66
|
+
evidence), [where the value lands in the SDLC](docs/value-in-the-sdlc.md)
|
|
67
|
+
(a value-stream assessment across the SAFe Continuous Delivery Pipeline),
|
|
68
|
+
[setup & CI integration](docs/setup-and-ci.md) (pipelines, ratchet,
|
|
69
|
+
Sonar, badge), [understanding findings & scores](docs/findings-and-scores.md)
|
|
70
|
+
(for report readers and diagram authors), and
|
|
71
|
+
[writing rules](docs/writing-rules.md) (a step-by-step programming guide with
|
|
72
|
+
an end-to-end example, including how the executable Gherkin spec works).
|
|
73
|
+
|
|
74
|
+
## Maturity scoring
|
|
75
|
+
|
|
76
|
+
`pumllint score` aggregates rule findings into a **360° maturity level** per
|
|
77
|
+
diagram — from 1 (*Sketchy*) to 5 (*Generation-ready*) — plus a prescriptive
|
|
78
|
+
gap report listing exactly which findings block the next level:
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
order.puml [Order]: Level 3 (Disciplined) — 68/100
|
|
82
|
+
To reach Level 4 (Precise):
|
|
83
|
+
• DIM-CMP is 61, needs >= 70 — fix:
|
|
84
|
+
SEQ102 major order.puml:18 participant declaration has no role type
|
|
85
|
+
|
|
86
|
+
Model set: Level 3 (Disciplined) — 68/100 weighted across 1 diagram(s)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Every report ends with a **model-set summary**: the worst per-diagram level
|
|
90
|
+
(the set is only as trustworthy as its weakest diagram) plus an
|
|
91
|
+
element-weighted composite across all scored diagrams. `--min-level` gates on
|
|
92
|
+
exactly that model-set level — it fails as soon as any diagram is below N.
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
python -m pumllint score diagrams/ --min-level 4 # CI gate: exit 1 below Level 4
|
|
96
|
+
python -m pumllint score diagrams/ --profile codegen # Level 5 requires this profile
|
|
97
|
+
python -m pumllint score diagrams/ --check-syntax # also run plantuml -checkonly
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Baseline / ratchet mode
|
|
101
|
+
|
|
102
|
+
On a brownfield model set, a fixed `--min-level` gate would demand a big-bang
|
|
103
|
+
cleanup. Ratchet instead: record today's per-diagram levels once, then fail CI
|
|
104
|
+
only when a diagram drops **below its own baseline**.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
python -m pumllint score diagrams/ --baseline maturity.json # 1st run records,
|
|
108
|
+
# later runs ratchet
|
|
109
|
+
python -m pumllint score diagrams/ --baseline maturity.json --update-baseline
|
|
110
|
+
# accept the status quo
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Commit `maturity.json`. Diagrams new since the baseline always pass the
|
|
114
|
+
ratchet (combine with `--min-level` to hold new work to a floor); regressions
|
|
115
|
+
are listed on stderr as `regression: <file>::<diagram>: Level 2 (baseline 3)`
|
|
116
|
+
and exit 1.
|
|
117
|
+
|
|
118
|
+
Ratchet-compare runs also annotate the report with **trends** — per diagram
|
|
119
|
+
and for the model set:
|
|
120
|
+
|
|
121
|
+
```text
|
|
122
|
+
order.puml [Order]: Level 4 (Precise) — 82/100 (Level 3 → 4 since last baseline)
|
|
123
|
+
checkout.puml: Level 3 (Disciplined) — 71/100 (new since baseline)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The json format carries the same machine-readably: each diagram (and
|
|
127
|
+
`modelSet`) gains `"baseline": {"level": 3, "delta": 1}` (`null` when not
|
|
128
|
+
ratcheting or new).
|
|
129
|
+
|
|
130
|
+
### Maturity badge
|
|
131
|
+
|
|
132
|
+
`-f badge` renders the model-set level as
|
|
133
|
+
[shields.io endpoint JSON](https://shields.io/badges/endpoint-badge):
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
python -m pumllint score diagrams/ -f badge -o badge.json
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Publish `badge.json` anywhere raw-fetchable (the repo itself, gh-pages, a CI
|
|
140
|
+
artifact) and embed:
|
|
141
|
+
|
|
142
|
+
```markdown
|
|
143
|
+

|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Colors follow the level: red (1) → orange (2) → yellow (3) → yellowgreen (4)
|
|
147
|
+
→ brightgreen (5).
|
|
148
|
+
|
|
149
|
+
### HTML report
|
|
150
|
+
|
|
151
|
+
`-f html` renders the score run as a single self-contained page for
|
|
152
|
+
architect-facing reviews — the model-set verdict first, then per-diagram
|
|
153
|
+
cards sorted worst-first, each with its level, per-dimension score bars, the
|
|
154
|
+
prescriptive gap report, and baseline trends when ratcheting:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
python -m pumllint score diagrams/ -f html -o maturity-report.html
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
No scripts, no external requests, no timestamps: the file renders offline
|
|
161
|
+
and is byte-identical across runs over the same model set — publish it as a
|
|
162
|
+
CI artifact, attach it to a review, or drop it in a wiki. In GitHub Actions:
|
|
163
|
+
|
|
164
|
+
```yaml
|
|
165
|
+
- name: Maturity report
|
|
166
|
+
uses: fdurieux/pumllint@v0.21.1
|
|
167
|
+
with:
|
|
168
|
+
command: score
|
|
169
|
+
paths: docs/diagrams
|
|
170
|
+
format: html
|
|
171
|
+
output: maturity-report.html
|
|
172
|
+
- uses: actions/upload-artifact@v4
|
|
173
|
+
with: { name: maturity-report, path: maturity-report.html }
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
A published example of this report — the bundled [examples/](examples/)
|
|
177
|
+
scored by the tool itself — lives at
|
|
178
|
+
<https://fdurieux.github.io/pumllint/example-maturity-report.html>
|
|
179
|
+
(`docs/example-maturity-report.html`, drift-guarded by
|
|
180
|
+
`tests/test_pilot_example.py`).
|
|
181
|
+
|
|
182
|
+
Why gate on it: in a measured experiment (75 generation runs, independent
|
|
183
|
+
LLM judge — see [EVIDENCE.md](EVIDENCE.md)), maturity scores correlated with
|
|
184
|
+
the fidelity of code generated from the diagrams (r ≈ 0.49), and diagrams
|
|
185
|
+
below Level 2 degraded generation sharply — fidelity dropped by roughly a
|
|
186
|
+
third and invented business logic doubled. The gate keeps those diagrams out.
|
|
187
|
+
Level 5 means *method-convention complete*: the diagram-side preconditions for
|
|
188
|
+
faithful generation, bound to the `codegen` profile so it cannot be claimed
|
|
189
|
+
without those rules running.
|
|
190
|
+
|
|
191
|
+
Scoring model, dimensions, thresholds, and calibration notes: [SCORING.md](SCORING.md).
|
|
192
|
+
All knobs are configurable under the `scoring` key (see `pumllint.toml`).
|
|
193
|
+
|
|
194
|
+
## Rules
|
|
195
|
+
|
|
196
|
+
| ID | Name | Default | What it catches |
|
|
197
|
+
|----|------|---------|-----------------|
|
|
198
|
+
| SEQ001 | undeclared-participant | critical | Participant used but never declared. **Typo detector**: PlantUML silently creates a phantom lifeline for `Custmer -> Bank`. |
|
|
199
|
+
| SEQ002 | unused-participant | minor | Declared participant that appears in no message. |
|
|
200
|
+
| SEQ003 | unbalanced-activation | major | `activate` never closed by `deactivate`/`return` (unterminated flow), or `deactivate` without prior `activate`. Understands `++`/`--` arrow shortcuts and `destroy`. |
|
|
201
|
+
| SEQ004 | unterminated-block | critical | `alt`/`opt`/`loop`/`par`/`group`/`box` without `end`. |
|
|
202
|
+
| SEQ005 | unlabelled-message | minor | Arrow with no label (dotted returns tolerated by default). |
|
|
203
|
+
| GEN001 | missing-title | minor | No `title`. |
|
|
204
|
+
| GEN002 | unnamed-diagram | info | `@startuml` without a name. |
|
|
205
|
+
| GEN003 | inline-skinparam | minor | Per-diagram styling instead of a central theme include. |
|
|
206
|
+
| GEN004 | participant-naming | minor | Names violating a configurable regex (per-kind overrides supported). |
|
|
207
|
+
| GEN005 | max-participants | minor | More lifelines than the configured max. |
|
|
208
|
+
| GEN006 | owner-tag | minor | No ownership tag in title/header/footer/caption/notes. Needs a `pattern`; dormant otherwise. |
|
|
209
|
+
| GEN007 | requirement-link | minor | No requirement/ADR reference in name/title/notes. Needs a `pattern`; dormant otherwise. |
|
|
210
|
+
| GEN008 | note-density | minor | Structure narrated in notes instead of modelled (≥ `min_notes`, > `max_ratio` notes/element). |
|
|
211
|
+
| GEN009 | max-elements | minor | More semantic elements than `max` (default 60), any diagram type. |
|
|
212
|
+
| UC001 | orphan-actor-or-usecase | major | Use-case diagrams: actor or use case linked to nothing. |
|
|
213
|
+
| UC002 | usecase-actor-naming | minor | Use case not phrased verb-first (verb–object). Needs a `verbs` whitelist; dormant otherwise. |
|
|
214
|
+
| UC003 | include-extend-direction | minor | `<<include>>`/`<<extend>>` arrow pointing the wrong way (judged via actor connectivity), or involving an actor. |
|
|
215
|
+
| SEQ006 | no-self-message | minor | Self-message; internal logic belongs in a note or `ref over`. Option `allowed` whitelists participants. |
|
|
216
|
+
| SEQ007 | unlabelled-block-condition | minor | `alt`/`opt`/`loop`/`break`/`critical` without a condition label. |
|
|
217
|
+
| SEQ008 | fragment-nesting-depth | minor | Combined fragments nested past `max_nesting_depth` (default 3) — extract a sub-diagram. |
|
|
218
|
+
| SEQ009 | unpaired-return | minor | Dashed return arrow (`-->`) that pairs with no preceding call. |
|
|
219
|
+
| SEQ010 | explicit-participant-order | info | Participant introduced by first use. Opt-in via `require_explicit_order`. |
|
|
220
|
+
| SEQ011 | max-messages | minor | More messages than `max` (default 30) — split per phase or `ref over`. |
|
|
221
|
+
| ACT001 | missing-start | major | Activity diagram with actions but no `start` node. |
|
|
222
|
+
| ACT002 | missing-stop | major | Activity flow never reaches `stop`/`end` (unterminated flow). |
|
|
223
|
+
| ACT003 | unlabelled-decision-branch | minor | `if (...) then` / `else` without a `(yes)`/`(no)` branch label. |
|
|
224
|
+
| ACT004 | unterminated-construct | critical | `if`/`while`/`repeat`/`fork`/`switch`/`partition` never closed. |
|
|
225
|
+
| ACT005 | swimlane-naming | minor | Swimlane (`|Lane|`) name violating a configurable `pattern`. |
|
|
226
|
+
| ACT006 | verb-first-activity | minor | Activity not phrased verb-first. Needs a `verbs` whitelist; dormant otherwise. |
|
|
227
|
+
| CLS001 | class-naming | minor | Class/member names violating configurable patterns (default PascalCase classes, camelCase members; enum members exempt). |
|
|
228
|
+
| CLS002 | association-multiplicity | major | Association/aggregation/composition without a quoted multiplicity on both ends. |
|
|
229
|
+
| CLS003 | unlabelled-association | minor | Plain association with no role/verb label (`: places`). |
|
|
230
|
+
| CLS004 | inheritance-cycle | major | Cycle in the generalization/realization hierarchy — invalid UML that PlantUML happily renders. |
|
|
231
|
+
| CLS005 | max-members-per-class | minor | God-class smell: more members than `max` (default 15). |
|
|
232
|
+
| STA001 | single-initial-state | blocker | State machine without exactly one top-level `[*] -->` (composite-body initials don't count). |
|
|
233
|
+
| STA002 | unreachable-state | major | State with no incoming transition (self-loops don't count) — dead model content. |
|
|
234
|
+
| STA003 | unlabelled-transition | minor | Transition without an `event [guard] / action` label; `[*]` transitions exempt. |
|
|
235
|
+
|
|
236
|
+
### Cross-diagram consistency pack (XD)
|
|
237
|
+
|
|
238
|
+
Active only when more than one diagram is linted, these build an entity
|
|
239
|
+
symbol table across the whole batch — the same entity must keep one identity
|
|
240
|
+
everywhere. XD001–003 compare sequence-diagram participants (kind,
|
|
241
|
+
stereotype, spelling); XD004–005 span diagram *types*: a class
|
|
242
|
+
`OrderService <<service>>` and a sequence lifeline `orderService <<gateway>>`
|
|
243
|
+
are one entity drifting apart, and the linter says so.
|
|
244
|
+
|
|
245
|
+
| ID | Name | Default | What it catches |
|
|
246
|
+
|----|------|---------|-----------------|
|
|
247
|
+
| XD001 | conflicting-participant-kind | major | Same participant declared `participant` here, `database` there — majority declaration wins, minority sites are flagged. |
|
|
248
|
+
| XD002 | conflicting-participant-stereotype | minor | Same participant with disagreeing stereotypes across sequence diagrams. |
|
|
249
|
+
| XD003 | participant-name-case-collision | minor | Participant spellings differing only by case across sequence diagrams. |
|
|
250
|
+
| XD004 | cross-type-name-collision | minor | Entity spellings differing only by case across diagram *types* (participants, classifiers, swimlanes). |
|
|
251
|
+
| XD005 | cross-type-stereotype-conflict | minor | Entity stereotyped differently in the class model than in the interaction models. |
|
|
252
|
+
|
|
253
|
+
### Codegen-readiness pack (profile: `codegen`)
|
|
254
|
+
|
|
255
|
+
Rules `SEQ101–SEQ109` validate whether a sequence diagram is precise and
|
|
256
|
+
complete enough for an AI coding agent (or any downstream generator) to
|
|
257
|
+
implement it **without inventing missing details**. They are disabled by
|
|
258
|
+
default and activate with `--profile codegen` or `profile: codegen` in the
|
|
259
|
+
config. Ids `SEQ100–SEQ199` are reserved for this range.
|
|
260
|
+
|
|
261
|
+
| ID | Name | Default | What it catches |
|
|
262
|
+
|----|------|---------|-----------------|
|
|
263
|
+
| SEQ101 | codegen-implicit-participant | blocker | Lifeline created implicitly on first use — the generator must guess what `OrderSvc` is. |
|
|
264
|
+
| SEQ102 | codegen-untyped-participant | major | Bare `participant X` with no typed keyword or `<<stereotype>>` — no mapping signal (actor → API boundary, `database` → repository, `<<external>>` → client stub). |
|
|
265
|
+
| SEQ103 | codegen-prose-message | blocker | Call labels that aren't operation signatures: `fetch the order details` instead of `findOrderById(orderId)` — including prose hiding inside the parentheses (`handle(the payment stuff)`); `name: Type` params and quoted literals stay legal. |
|
|
266
|
+
| SEQ104 | codegen-missing-return | major | Synchronous call (`->`) with no reply arrow or `return` — return type left undefined. Async `->>` is exempt. |
|
|
267
|
+
| SEQ105 | codegen-vague-guard | blocker | `alt`/`opt`/`loop` with an empty or vague guard (`sometimes`, `if needed`, …). `else` must carry a guard, or literal `[else]` in a two-branch alt. |
|
|
268
|
+
| SEQ106 | codegen-elision-marker | blocker | `...`, `TBD`, `TODO`, `etc`, `???`, `and so on` in labels, guards or notes — deliberately omitted behaviour the generator would fill with fiction. |
|
|
269
|
+
| SEQ107 | codegen-missing-failure-path | major | Call to an `<<external>>`/`database`/`queue` participant with no failure branch (alt error branch, `break`, or `group error`). |
|
|
270
|
+
| SEQ108 | codegen-activation-lifecycle | major | `activate`/`deactivate` not pairing as a well-formed per-lifeline stack — call nesting ambiguous. |
|
|
271
|
+
| SEQ109 | codegen-uninformative-reply | minor | Return drawn with a solid arrow, or a reply labelled `ok`/`done`/`result` instead of naming the returned value — breaks data-dependency inference. |
|
|
272
|
+
|
|
273
|
+
The vagueness / elision / non-informative lexicons are configurable per rule
|
|
274
|
+
(`vague_terms`, `tokens`, `failure_keywords`, `non_informative`).
|
|
275
|
+
|
|
276
|
+
## Auto-fix
|
|
277
|
+
|
|
278
|
+
`pumllint fix` applies the mechanical fixes — the ones that are
|
|
279
|
+
deterministic and semantics-preserving, where nothing has to be invented:
|
|
280
|
+
|
|
281
|
+
| Finding | Fix |
|
|
282
|
+
|---------|-----|
|
|
283
|
+
| GEN002 unnamed-diagram | `@startuml <name>` derived from the file stem (ordinal suffix for multiple diagrams per file) |
|
|
284
|
+
| GEN001 missing-title | `title <Humanized>` inserted after `@startuml` |
|
|
285
|
+
| SEQ001/SEQ101 undeclared-participant | `participant X` declarations in first-use order, anchored after the existing declarations |
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
python -m pumllint fix diagrams/ # apply fixes in place
|
|
289
|
+
python -m pumllint fix diagrams/ --dry-run # show the diff; exit 1 if fixes
|
|
290
|
+
# are pending (CI check mode)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Fixes are driven by the linter's actual findings, so suppressed findings and
|
|
294
|
+
disabled rules are never "fixed", and the run is idempotent. The fixer also
|
|
295
|
+
inherits the linter's judgment calls: SEQ001 deliberately stays quiet in
|
|
296
|
+
files that declare no participants at all (ad-hoc sketches aren't punished),
|
|
297
|
+
so such files get no declaration fixes either — set
|
|
298
|
+
`SEQ001: {only_if_any_declared: false}` if you want sketches fixed too.
|
|
299
|
+
Everything else (labels, guards, multiplicities) stays a human decision —
|
|
300
|
+
the linter tells you *what*, but will not guess *which*. In GitHub Actions,
|
|
301
|
+
use `command: fix` with `extra-args: --dry-run` as a "fixes pending?" CI
|
|
302
|
+
check.
|
|
303
|
+
|
|
304
|
+
## Report schemas
|
|
305
|
+
|
|
306
|
+
The machine-readable reports are a public contract, pinned by JSON Schemas
|
|
307
|
+
(draft 2020-12) shipped inside the package:
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
python -m pumllint schema lint # the shape of `pumllint -f json`
|
|
311
|
+
python -m pumllint schema score # the shape of `pumllint score -f json`
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Point any standard validator at them when building tooling on top of the
|
|
315
|
+
output. pumllint's own test suite validates every report shape it can emit
|
|
316
|
+
against these schemas — like the golden scores, the shape cannot drift
|
|
317
|
+
silently. The badge and sonar formats are deliberately not covered: those
|
|
318
|
+
shapes are shields.io's and SonarQube's contracts, not pumllint's.
|
|
319
|
+
|
|
320
|
+
## Configuration
|
|
321
|
+
|
|
322
|
+
`pumllint.yaml` (or `.toml` / `.json`) is auto-detected in the working
|
|
323
|
+
directory, or passed with `-c`. Rules are keyed by ID or kebab-case name;
|
|
324
|
+
`false` disables, a mapping supplies options and/or a `severity` override:
|
|
325
|
+
|
|
326
|
+
```yaml
|
|
327
|
+
rules:
|
|
328
|
+
unnamed-diagram: false
|
|
329
|
+
participant-naming:
|
|
330
|
+
severity: major
|
|
331
|
+
pattern: "^[A-Z][A-Za-z0-9]*$"
|
|
332
|
+
per_kind:
|
|
333
|
+
actor: "^[A-Z][a-z]+$"
|
|
334
|
+
max-participants:
|
|
335
|
+
max: 7
|
|
336
|
+
# Traceability rules are dormant until you supply your project's convention:
|
|
337
|
+
owner-tag:
|
|
338
|
+
pattern: "(?i)owner\\s*:"
|
|
339
|
+
requirement-link:
|
|
340
|
+
pattern: "REQ-\\d+|ADR-\\d+"
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Profiles
|
|
344
|
+
|
|
345
|
+
A profile switches on profile-gated rule packs and may escalate severities of
|
|
346
|
+
existing rules. Select it with `profile:` in the config or `--profile` on the
|
|
347
|
+
CLI (the CLI wins):
|
|
348
|
+
|
|
349
|
+
```yaml
|
|
350
|
+
profile: codegen
|
|
351
|
+
profiles:
|
|
352
|
+
codegen:
|
|
353
|
+
enable: # optional: activate gated rules explicitly by id/name
|
|
354
|
+
- SEQ101
|
|
355
|
+
- SEQ102
|
|
356
|
+
escalate: # optional: severity overrides while the profile is active
|
|
357
|
+
SEQ001: blocker # e.g. undeclared participant becomes blocking
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
`pumllint --profile codegen src/diagrams/` is enough on its own — rules
|
|
361
|
+
registered for a profile activate whenever that profile is selected; the
|
|
362
|
+
`enable:` list is only needed to pull in rules gated behind *other* profiles,
|
|
363
|
+
and `escalate:` to tighten the base catalog. Escalations win over rule-level
|
|
364
|
+
`severity:` settings — a profile is an opt-in quality gate.
|
|
365
|
+
|
|
366
|
+
## Inline suppressions
|
|
367
|
+
|
|
368
|
+
Findings can be silenced at the source, `eslint`-style, with PlantUML
|
|
369
|
+
comments — reviewable and diff-friendly, unlike config-file exclusions:
|
|
370
|
+
|
|
371
|
+
```plantuml
|
|
372
|
+
' pumllint: disable=SEQ006, unlabelled-message ← next line only
|
|
373
|
+
Batch -> Batch : self-trigger
|
|
374
|
+
|
|
375
|
+
' pumllint: disable-file=GEN003 ← whole file
|
|
376
|
+
' pumllint: disable ← all rules, next line
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Rules can be referenced by id or kebab-case name. CI can audit what is being
|
|
380
|
+
suppressed by running with `--no-suppressions` (or `suppressions: false` in
|
|
381
|
+
the config), which reports everything regardless of comments.
|
|
382
|
+
|
|
383
|
+
Suppressed findings never vanish silently from maturity scores: `pumllint
|
|
384
|
+
score` annotates every affected diagram — `100/100 (3 suppressed)` — and the
|
|
385
|
+
JSON report carries a `suppressedCount` per diagram and for the model set,
|
|
386
|
+
so a suppressed-clean diagram is always distinguishable from a clean one.
|
|
387
|
+
|
|
388
|
+
## Architecture
|
|
389
|
+
|
|
390
|
+
```
|
|
391
|
+
pumllint/
|
|
392
|
+
├── model.py # Diagram / Participant / Message / Violation dataclasses
|
|
393
|
+
│ # + call/reply pairing & activation-stack helpers
|
|
394
|
+
├── parser/ # line-oriented parser → semantic Diagram model
|
|
395
|
+
│ ├── sequence.py # sequence + use-case + suppression comments
|
|
396
|
+
│ ├── activity.py # new-style activity syntax (start/if/while/fork/…)
|
|
397
|
+
│ ├── class_.py # class diagrams (classifiers, members, relations)
|
|
398
|
+
│ └── state.py # state machines ([*], transitions, composites)
|
|
399
|
+
├── rules/ # rule packs; auto-discovered via @register decorator
|
|
400
|
+
│ ├── catalog.toml # declarative rule metadata (name/desc/severity/scope)
|
|
401
|
+
│ ├── sequence/ # SEQ* (participants.py, flows.py, codegen.py)
|
|
402
|
+
│ ├── activity/ # ACT* (structure.py)
|
|
403
|
+
│ ├── class_/ # CLS* (structure.py)
|
|
404
|
+
│ ├── state/ # STA* (structure.py)
|
|
405
|
+
│ └── common/ # GEN*, UC* (governance.py)
|
|
406
|
+
├── reporters/ # text / json / sonar / badge / html; auto-registered
|
|
407
|
+
│ # via @reporter
|
|
408
|
+
├── schemas/ # JSON Schemas — the `-f json` output contract
|
|
409
|
+
├── schema.py # loader + minimal validator (drift-guarded in tests)
|
|
410
|
+
├── engine.py # config merge → rule instantiation → run
|
|
411
|
+
├── config.py # yaml / toml / json loading
|
|
412
|
+
└── cli.py # argparse CLI, CI-friendly exit codes
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Design choices for extensibility:
|
|
416
|
+
|
|
417
|
+
- **Parser and rules are decoupled** through the `Diagram` model. Rules never
|
|
418
|
+
see raw text, so parser improvements benefit every rule.
|
|
419
|
+
- The parser recognizes a *governance-relevant subset* and ignores unknown
|
|
420
|
+
lines — deliberately tolerant, because the PlantUML grammar is defined by
|
|
421
|
+
its Java implementation and evolves constantly.
|
|
422
|
+
- **Adding a rule = one class + one catalog entry.** A rule class carries only
|
|
423
|
+
its `id` and its `check()` algorithm; the declarative metadata (name,
|
|
424
|
+
description, severity, scope, profiles) lives in `rules/catalog.toml` and is
|
|
425
|
+
stamped onto the class by `@register`. Drop a module anywhere under
|
|
426
|
+
`pumllint/rules/` — `discover()` walks the package, so there is nothing else
|
|
427
|
+
to wire up:
|
|
428
|
+
|
|
429
|
+
```toml
|
|
430
|
+
# pumllint/rules/catalog.toml
|
|
431
|
+
[SEQ006]
|
|
432
|
+
name = "no-self-message"
|
|
433
|
+
description = "Self-messages hide logic that belongs in a note or ref"
|
|
434
|
+
severity = "minor"
|
|
435
|
+
applies_to = ["sequence"]
|
|
436
|
+
profiles = []
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
```python
|
|
440
|
+
from pumllint.rules import Rule, register
|
|
441
|
+
|
|
442
|
+
@register
|
|
443
|
+
class NoSelfMessage(Rule):
|
|
444
|
+
id = "SEQ006"
|
|
445
|
+
|
|
446
|
+
def check(self, diagram):
|
|
447
|
+
for m in diagram.messages:
|
|
448
|
+
if m.source and m.source == m.target:
|
|
449
|
+
yield self.violation(diagram, m.line, f"Self-message on '{m.source}'")
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
- **Adding an output format = one class** decorated with `@reporter`.
|
|
453
|
+
- **Profile-gated rules** set `profiles = ["codegen"]` in their catalog entry;
|
|
454
|
+
the engine keeps them dormant until that profile is selected. Everything else
|
|
455
|
+
(config, suppressions, reporters) works identically for gated rules.
|
|
456
|
+
- New diagram types slot in as a parser extension plus a rule pack — exactly
|
|
457
|
+
how activity support (ACT001–004) was added in 0.2.0, class support
|
|
458
|
+
(CLS001–005) in 0.9.0 and state support (STA001–003) in 0.10.0; component
|
|
459
|
+
diagrams would follow the same pattern with `applies_to = ("component",)`.
|
|
460
|
+
|
|
461
|
+
## CI integration (GitHub Actions)
|
|
462
|
+
|
|
463
|
+
The repo ships a composite action — it installs pumllint from the exact ref
|
|
464
|
+
you pin and runs it:
|
|
465
|
+
|
|
466
|
+
```yaml
|
|
467
|
+
- uses: actions/checkout@v4
|
|
468
|
+
- name: Lint PlantUML diagrams
|
|
469
|
+
uses: fdurieux/pumllint@v0.21.1
|
|
470
|
+
with:
|
|
471
|
+
paths: docs/diagrams
|
|
472
|
+
- name: Maturity ratchet + floor
|
|
473
|
+
uses: fdurieux/pumllint@v0.21.1
|
|
474
|
+
with:
|
|
475
|
+
command: score
|
|
476
|
+
paths: docs/diagrams
|
|
477
|
+
baseline: maturity.json
|
|
478
|
+
min-level: "2"
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
Inputs mirror the CLI: `command` (`lint`|`score`), `paths`, `config`,
|
|
482
|
+
`profile`, `format`, `output`, `fail-on` (lint), `min-level` / `baseline` /
|
|
483
|
+
`update-baseline` (score), and `extra-args` for anything else.
|
|
484
|
+
|
|
485
|
+
Or call the CLI directly — e.g. to feed SonarQube:
|
|
486
|
+
|
|
487
|
+
```yaml
|
|
488
|
+
- name: Lint PlantUML diagrams
|
|
489
|
+
run: |
|
|
490
|
+
python -m pumllint docs/diagrams --fail-on major \
|
|
491
|
+
-f sonar -o pumllint-sonar.json
|
|
492
|
+
- name: SonarQube scan
|
|
493
|
+
uses: SonarSource/sonarqube-scan-action@v4
|
|
494
|
+
env:
|
|
495
|
+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
496
|
+
with:
|
|
497
|
+
args: >
|
|
498
|
+
-Dsonar.externalIssuesReportPaths=pumllint-sonar.json
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
The `sonar` reporter emits the **Generic Issue Import Format** (the 10.3+
|
|
502
|
+
schema with `rules` + `issues` and clean-code impacts). SonarQube ingests it
|
|
503
|
+
via `sonar.externalIssuesReportPaths` — findings land in dashboards, quality
|
|
504
|
+
gates and PR decoration with **no Java plugin to build or maintain**.
|
|
505
|
+
|
|
506
|
+
Recommended companion step: run PlantUML's own `-checkonly` first for pure
|
|
507
|
+
syntax, then `pumllint` for semantics.
|
|
508
|
+
|
|
509
|
+
## Pre-commit hooks
|
|
510
|
+
|
|
511
|
+
```yaml
|
|
512
|
+
repos:
|
|
513
|
+
- repo: https://github.com/fdurieux/pumllint
|
|
514
|
+
rev: v0.21.1
|
|
515
|
+
hooks:
|
|
516
|
+
- id: pumllint # lint staged diagrams
|
|
517
|
+
- id: pumllint-score
|
|
518
|
+
args: [--min-level, "3"] # maturity gate per commit
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
Both hooks receive the staged PlantUML files (`.puml`, `.plantuml`, `.iuml`,
|
|
522
|
+
`.wsd`). `pumllint-score` only gates when given `--min-level N` and/or
|
|
523
|
+
`--baseline FILE` via `args`; without them it just prints the report. Hook
|
|
524
|
+
environments are isolated — if your repo uses a `pumllint.yaml` config, add
|
|
525
|
+
`additional_dependencies: [PyYAML]` to the hook (toml/json configs need
|
|
526
|
+
nothing extra).
|
|
527
|
+
|
|
528
|
+
## Tests
|
|
529
|
+
|
|
530
|
+
Two complementary entry points:
|
|
531
|
+
|
|
532
|
+
```bash
|
|
533
|
+
# 1. Unit + integration tests. Zero-dependency runner — no pytest needed;
|
|
534
|
+
# ideal for offline/CI-minimal environments.
|
|
535
|
+
python tests/run_tests.py
|
|
536
|
+
|
|
537
|
+
# 2. Everything above PLUS the executable RULES.md spec (pytest-bdd).
|
|
538
|
+
# Needs the optional `test` extra.
|
|
539
|
+
pip install -e ".[test]"
|
|
540
|
+
python -m pytest
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
`run_tests.py` collects only the top-level `tests/test_*.py` (unit tests,
|
|
544
|
+
rule checks, the catalog parity guard, and the feature/RULES.md sync guard) and
|
|
545
|
+
never imports pytest-bdd. The BDD layer lives under `tests/bdd/` and runs only
|
|
546
|
+
under pytest.
|
|
547
|
+
|
|
548
|
+
**Executable spec (RULES.md ↔ tests).** The `` ```gherkin `` block under each
|
|
549
|
+
rule in [RULES.md](RULES.md) is the acceptance spec. `tools/extract_features.py`
|
|
550
|
+
generates `tests/bdd/features/<ID>.feature` from those blocks; pytest-bdd binds
|
|
551
|
+
them to a small canonical step vocabulary (`tests/bdd/test_features.py`) and runs
|
|
552
|
+
them against the real linter. Blocked/planned rules are `@skip`-tagged. A sync
|
|
553
|
+
test fails if the committed features drift from RULES.md — after editing a
|
|
554
|
+
Gherkin block, regenerate:
|
|
555
|
+
|
|
556
|
+
```bash
|
|
557
|
+
python tools/extract_features.py
|
|
558
|
+
```
|