Files
werkcrappit/docs/rfcs/0003-structure-view.md
claude-code e98c594480
werkator build succeeded after 02:59
werkator/publish build succeeded after 00:18
werkator/mutation build succeeded after 00:56
Wrapped levels with room for their edges (0.26.12) (#67)
Co-authored-by: claude-code <claude-code@javagil.de>
2026-10-11 19:25:21 +02:00

422 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RFC 0003: A Structure View of Cycles, Complexity and Cohesion
- Status: accepted
- Date: 2026-10-09
- Author: claude-code, for Michael Hönnig
- Amended: 2026-10-09, PR#49: dependencies outside a package (section 5.4), levels as rows (section 5.5),
one package at a time, one meaning of red, the page in the style of Werkator,
an expandable matrix (section 5.6)
- Implementation: in four steps (see section 8)
## 1. Goal
A developer sees at one glance:
- which classes and packages are in a cycle,
- how complex they are,
- how cohesive they are (LCOM4).
werkcrappit already computes all of this, but no view shows it together.
CodeCharta's dependency view draws all edges of the city at once and is hard to read.
werkcrappit therefore writes a second, plain view beside the code city.
## 2. What Exists
- The export computes the edges between top-level classes, with `isCyclic` and `isPointingUpwards`,
and the level of every node in the dependency lens (RFC 0001, ADR 0007, `Levelization`).
Levels are local to a folder: a folder's edges are lifted to its direct children, and cycles among them are broken.
- The report has LCOM4, Ce and Ca per class (RFC 0002), and complexity, Mutation-CRAP and change risk (ADR 0008).
- No free tool shows cycles, complexity and cohesion together; research of 2026-10-09:
- Sonargraph-Explorer is free, but has no cycle or graph view; only the commercial Sonargraph-Architect has one.
- Structure101 is no longer sold; its successor in SonarQube 2026.4 is in the commercial editions only.
- IntelliJ's DSM is in Ultimate only and shows no metrics.
## 3. Constraints
- The view is one self-contained HTML file, `structure.html`, beside the code city's `index.html`.
It loads nothing from other servers: no CDN, no `src=`, no `@import`, as `CodeCityPage`.
- It works from disk (`file://`): it needs neither a viewer nor CORS.
- It shows only what werkcrappit already computes.
It computes no cycle, level or metric of its own; the hard invariants of `AGENTS.md` apply.
- The same input gives the same bytes.
- The code city page links it as a fifth button beside the four views.
## 4. The Sample
The variants were drawn from hsadmin-ng's `mutation-crap.cc.json` of the build `master@release` of 2026-10-09,
with a script outside the repository; hsadmin-ng is private, so the mock-ups are not committed.
| What | hsadmin-ng |
| -------------------------------------------- | ---------------------------- |
| Buildings (top-level classes) | 270 |
| Folders, of which hold classes | 46, 40 |
| Edges | 994 |
| Cyclic edges | 93 |
| Edges against the levels | 48, 35 of them cyclic |
| Class cycles (strongly connected components) | 7, of 22, 10, 7, 6, 5, 2, 2 |
| Package cycles without a class cycle | 13 edges, 5 pairs of folders |
| Most children of one folder | 23 |
| Most edges among the children of one folder | 42 |
| Highest level | 6 |
Every class cycle lies inside one package.
In each of them, one class knows the classes that depend on it: a registry its implementations, a base type its subtypes.
The largest, 22 classes, is `HostingAssetEntityValidatorRegistry` with its validators in `hs.hosting.asset.validators`,
the folder with the most children and edges.
The 13 edges against the levels that are not cyclic are package cycles: `hs.booking` uses `hs.office` and `hs.hosting.asset`,
`hs.validation` uses `hs.booking`, and `rbac.context` uses `rbac.role` and `rbac.subject`,
while other classes there depend the other way.
## 5. Variants
### 5.1 Dependency Matrix (DSM)
Rows and columns are the same units; a cell says how many things the row's unit uses of the column's unit.
The units are sorted by folder, depth first, and within a folder by level, highest first.
Then every edge along the levels lies right of the diagonal, and every edge against the levels is a red cell, mostly left of it.
The row heads are coloured by LCOM4 or change risk.
- **All 270 classes:** a matrix of 270 × 270 with 994 cells.
At 7 pixels per row it is about 2,000 pixels wide; the red cells are easy to find, but no name is readable.
It fits as an overview, not as a view to work with.
- **Per folder:** one matrix of a folder's children, at most 23 × 23 in hsadmin-ng.
Readable without scrolling.
A registry cycle is one red row and one pale red column: more compact than in any diagram.
- A matrix shows one metric only, as the colour of its row heads; complexity and LCOM4 together need a second colour.
- Few developers read a DSM without explanation.
### 5.2 Level Diagram
Per folder, its children are boxes in rows, one row per level, the highest on top.
Dependencies point down.
A level wider than about 1,300 pixels wraps into several lines.
Section 5.5 compares this with one column per level.
- Box height: complexity; box fill: LCOM4, green, yellow and red as the code city's cohesion view (`ColorBand.COHESION`).
- The frame's line tells the kind of box: solid for a class, dashed for a package; all frames have the same width.
- Red means "in a cycle", for every element: the frame of a class or a package, an edge, a class name.
A package is red when it is an end of a red edge in the diagram, or when a cycle lies inside it.
- An edge against the levels bends out to the right and points up; its direction says so, not a line style.
Edges are never dashed.
- A sub-folder box links to its own diagram.
- Each box has a tooltip (`<title>`) with complexity, LCOM4, Ce, Ca, Mutation-CRAP and change risk.
Findings with hsadmin-ng:
- Mock-up 3 used a thick red frame for a class in a cycle, a dashed red edge for a cyclic edge along the levels,
and no red for a package box in the middle.
Dashed then meant "package" for a frame and "cycle" for an edge; readers could not tell the frames apart.
Mock-up 4 has the rules above.
- Most folders read at one glance, also `net.hostsharing.hsadminng` with 13 children and 21 edges,
and `hs.hosting.asset` with 13 children and 40 edges.
The package cycles of `hs` are three red curves.
- `hs.hosting.asset.validators`, 23 children and 42 edges, is a fan of 19 red curves from the registry to its validators.
The fan is loud, but it shows exactly the pattern to fix.
- **All 270 classes in one diagram** is no option.
werkcrappit has no global level of a class, only levels within a folder;
a global layering would be a second computation.
- **Only the edges among the children of the folder show.**
An edge between classes of two packages shows only in the diagram of their common parent,
as one edge between two package boxes.
That is 626 of hsadmin-ng's 994 edges.
The diagram of a package does not show what the package uses outside, who uses it, or which classes close a package cycle.
Without this, the diagram is of little use; section 5.4 adds it.
### 5.3 Granularity
| Granularity | Units at most | Readable | Fits the levels |
| --------------------------- | ------------: | -------- | --------------- |
| Classes only, one picture | 270 | no | as a DSM only |
| Leaf packages only | 40 | yes | no |
| Per folder, with links down | 23 | yes | yes |
A matrix of leaf packages was tried first.
It fails: a folder's own classes and its sub-folders are siblings with levels of their own,
so a package row mixes units of different levels, and edges along the levels end up left of the diagonal.
Per folder, the units are exactly the nodes that `Levelization` gives a level, so the picture and the levels cannot disagree.
### 5.4 Dependencies Outside a Folder
Each diagram gets two more rows, outside its levels:
- **Used by,** on top: each package outside the folder with a class that uses a class inside,
with the number of such classes; its tooltip names them.
An edge leads from the package to each class inside that it uses.
- **Depends on,** at the bottom: each package outside the folder with a class that a class inside uses,
as a box that lists the classes used there.
An edge leads from each class inside to the package box, and its tooltip names the class edges behind it.
- A package outside is the folder that holds the class directly, named below the city's top folder, e.g. `hs.office.relation`.
Its box links to its own diagram.
- An edge is red when one of its class edges is cyclic or points against the levels at the folder that holds both ends.
A class in a box at the bottom is red when such an edge reaches it.
So the classes behind a package cycle show in red in the diagram of each package of the cycle,
not only as one red curve between two package boxes in the diagram of their parent.
Findings with hsadmin-ng, mock-up 3, with levels as columns:
- A first attempt drew every class outside as a box of its own, with an edge to it.
`hs.booking.item` uses 26 classes in 10 packages; the diagram was a web of long curves.
One edge per pair of class inside and package outside, with the classes listed in the box, reads well.
- `hs.booking.project` shows the package cycle with `hs.office` as red edges to `hs.office.debitor` and `hs.office.relation`,
with the used classes there in red.
- The most outside dependencies are 41 classes in 16 packages, in `hs.hosting.asset` with its sub-packages;
the most users are 24 packages, of `mapper`.
### 5.5 Levels as Columns or as Rows
Mock-ups 1 to 4 had one column per level, the highest on the left; mock-up 5 has one row per level, the highest on top.
Measured over hsadmin-ng's 45 diagrams, with outside dependencies:
| Diagrams | Columns | Rows |
| ------------------------ | -------: | -------: |
| widest | 2,080 px | 1,317 px |
| tallest | 1,095 px | 932 px |
| wider than 1,400 pixels | 16 | 0 |
| taller than 900 pixels | 2 | 1 |
- In columns, long class names stand one below the other, and each level adds a column as wide as its longest name.
`hs.booking.item` has 7 levels and is 2,080 pixels wide.
- In rows, the names stand side by side, and a wide level wraps.
No diagram needs horizontal scrolling.
- Users on top and dependencies at the bottom read as "above uses below".
- Where a level wraps, edges from its upper lines pass behind the boxes of its lower lines.
This shows in `hs.hosting.asset.validators`: 20 validators on one level wrap into 5 lines.
The names stay readable, as edges lie behind the boxes.
### 5.6 Expandable Matrix
Mock-up 7 is a DSM that starts small and opens package by package:
- It starts with the children of the top folder, 13 × 13 in hsadmin-ng.
- A click on a package's name or its ▸ replaces its row and column in place by its children.
Several packages can be open at once.
An open package stays as a group row with ▾ above its children; a click on it closes it and all that is open inside.
- "Expand all" shows all 270 classes, "Collapse all" goes back to the start.
- Rows and columns follow the tree depth first, within a folder by level, highest first, as in section 5.1.
So in every state the edges along the levels lie right of the diagonal.
- A cell counts the things the row's unit uses of the column's unit.
It is pale red when one of its class edges is cyclic, dark red when one points against the levels;
its tooltip names up to 12 class edges.
- Each row head shows the indented name, the complexity, summed for a package, and a colour field
by LCOM4 or change risk, highest of its classes for a package; two chips choose the metric.
- The column heads show the number and the name, written at 90 degrees; a package name there opens the package too.
The header is 210 pixels tall with the classes of `hs.booking.project` open, and 290 pixels with all classes.
Findings with hsadmin-ng:
- With `hs` and `booking` open, the package cycles show as two dark red cells left of the diagonal:
`item → hosting.asset` with 13 and `validation → item` with 5.
- The matrix is more compact than the diagram where many edges meet, as in the registry cycles,
and it shows the whole project at once when all is open; the diagram shows complexity and cohesion better.
- The page needs a script: CSS `:target` knows one state only, the open packages are a set.
The script is inline, about 60 lines; the data are inline JSON; the page loads nothing.
It is 117 KB.
## 6. Decision
- [x] Which variant stays readable at about 300 classes?
Both, as two views with a tab each: the level diagram per folder (`structure.html`)
and the expandable matrix (`matrix.html`, section 5.6).
The diagram shows complexity and cohesion in one picture, and a developer reads it without explanation.
The matrix is more compact where many edges meet and shows the whole project at once.
- [x] Granularity?
Per folder, one folder at a time.
A sub-folder box and a package box outside lead down or across to that folder's diagram;
a breadcrumb of the folder's path above the diagram leads back up to each parent.
The browser's back button works too: each step changes the URL's fragment, `#<node ID>`, and the page draws that folder.
- [x] Static or interactive?
Both pages: an inline script over inline JSON.
- The page holds the data once and draws the diagram or the matrix in the browser.
The matrix needs a script anyway, as its open packages are a set that CSS cannot hold.
- The diagram could do without one, with every diagram as finished SVG in the file and CSS `:target` showing one;
mock-up 6 did so, and with tooltips it was 1,045 KB; mock-up 8, drawn by a script, is 124 KB.
Each tooltip text there was written once per box, and a package outside was drawn once per diagram that uses it.
- The script loads nothing, so the pages work from disk;
the JSON is written in a fixed order, so the same input gives the same file.
The diagram measures its text widths in the browser, so its layout follows the font the browser has.
- A CI server's artifact view that forbids scripts shows the page with a sentence that says so.
- [x] Where do the data come from?
From the export's own model in memory: the tree, the buildings, the edges, `Levelization` and the class designs.
The page is written by `mutationCrapCodeCharta` together with the city; it does not parse `mutation-crap.cc.json`.
A script that reads the file would not work from disk, as a browser lets no page read a local file.
- [x] What when `codeCharta.levelization` is `false`?
`Levelization` is still computed for the page; the flag only leaves its fields out of `mutation-crap.cc.json`
for CodeCharta `vis-2.7.0`.
It stays one computation, used twice.
- [x] How does the page know which classes form one cycle?
`Levelization` already computes the strongly connected components (Tarjan); it keeps them, and the page reads them.
The page does not group cyclic edges itself.
- [x] Folder boxes?
Neutral fill; their tooltip names the number of classes, the summed complexity and the highest LCOM4,
and whether the folder is in a cycle in this diagram or holds one inside.
A red frame marks either; a fill by the worst class would make every large folder red.
- [x] Levels as columns or as rows?
As rows, the highest on top, as section 5.5 shows; a level wider than about 1,300 pixels wraps.
- [x] Order within a row?
By the mean position of the boxes in the rows above that depend on it, ties by name; a fixed number of passes.
This keeps crossings low and the result deterministic.
- [x] Dependencies outside a folder?
In two rows of the folder's diagram, "used by" on top and "depends on" at the bottom, as section 5.4 describes.
An edge leads to a package box, not to each class outside; the box lists the classes.
- [x] Which edges are red?
An edge with a cyclic class edge or a class edge against the levels, inside the folder or leaving it.
The pages read `isCyclic` and `isPointingUpwards` of the class edges, as `Levelization` sets them;
they add no rule of their own.
The matrix tells the two apart: pale red for cyclic, dark red for against the levels.
- [x] Width?
A level wraps instead; class names are never shortened, as that would hide the name a developer searches for.
- [x] Page style?
As Werkator's pages at `https://werkator.javagil.de`: its colour tokens for light and dark,
its fonts IBM Plex Sans and IBM Plex Mono, a sticky header, panels with a monospace title bar, tiles and a footer.
The fonts are named, not embedded; without them the browser takes `system-ui` and `ui-monospace`.
The diagram takes its colours from the same tokens, so it follows the light or dark theme.
LCOM4 1 is Werkator's soft accent, 2 its soft amber, from 3 its soft red; "in a cycle" is its red.
- [x] Tooltips?
Every element that carries a colour, a frame or a number explains it, in both views, as a native tooltip (`title`):
- a class: its values, each with what it means and which colour band it falls in,
e.g. "LCOM4 2, yellow: two groups of methods";
- a package: that a click opens it, its summed and highest values, and why it is red;
- an edge or a cell: the class edges behind it and why it is red, "in a cycle" or "against the levels";
- the legend's colour fields, the metric chips "LCOM4" and "Change risk", the column "C" and the tiles:
what the metric or the number means and where its bands start.
The texts come from one place in the code, so both views say the same.
- [x] Size?
For hsadmin-ng, mock-up 8, the diagram page, is 124 KB, 26 KB with gzip; mock-up 7, the matrix, 117 KB, 24 KB with gzip.
Both hold the same JSON; one page with both views would hold it once, but each view keeps its own page and tab.
- [x] How are the scripts tested?
By functional tests in a headless browser, Playwright for Java, against pages written from a fixture project:
which boxes, edges and cells are red, the navigation down and back up, opening and closing in the matrix,
and no error in the browser's console.
The JUnit tests of the Java side check the JSON and the frame of the page.
The layout stays in the script, as only the browser knows the width of a text.
Playwright downloads its browser once; if Werkator's build host cannot run it, this decision comes back here.
- [x] Can the pages be switched off?
No: `mutationCrapCodeCharta` always writes both, without a new option, as they take milliseconds and load nothing.
- [x] Does the matrix keep its state in the URL?
Yes: the open packages are in the URL's fragment, so the back button, a reload and a shared link keep them,
as in the diagram.
- [x] Large projects?
Measured is hsadmin-ng with 270 classes.
With more than 500 classes, "Expand all" is off, and its tooltip says why; packages still open one by one.
The diagram is not affected, as it shows one package at a time.
- [x] Which edges of a package cycle are marked, and how?
Every one, not only the edges against the levels (PR#55).
`Levelization` keeps the cycles among the children of a folder before it breaks them.
A cycle with two or more sub-folders runs across the hierarchy, e.g. `a.a1.a11` with `a.a1.a12` or `a.a2`: red.
A cycle of one sub-folder and classes of the folder itself runs along it, e.g. `a.a1.a11` with `a.a1` or `a`: amber,
as it stays in one branch; the human decided so on 2026-10-11.
In both, an edge of the cycle is pale, one against the levels dark; the overview counts both kinds apart.
- [ ] TODO.bundledViewer: the CodeCharta viewer beside the city, so that the code city needs no link to javagil.de.
The code city has two parts: the city, `mutation-crap.cc.json` beside the reports,
and the viewer, which the browser loads and which then loads the city.
- When viewer and city come from different servers, the report server must allow the viewer's server by CORS.
- A login on the report server does not reach a viewer from another server:
the browser does not send it with the viewer's request, so the city of a report behind a login stays empty.
A viewer on the report server itself sends the login along.
Where each place of the viewer works, as checked on 2026-10-11:
| Reports | at javagil.de, as today | in the artifact | with the Werkator instance |
| ------------------------------- | ----------------------------- | --------------- | -------------------------- |
| on Werkator, public | yes | no | yes |
| on Werkator, behind a login | no | no | yes |
| on another server, public | yes, with a CORS line there | yes | no |
| on another server, behind login | no | yes | no |
| effort | none | an option | a change of Werkator |
- In the artifact means an option of `mutationCrapCodeCharta`, a viewer build in the registry and some MB per build.
- The viewer at javagil.de is a static build with `<base href="./">`, so it runs from any path.
- Werkator serves artifacts with `Content-Security-Policy: sandbox allow-scripts`, so a viewer in the artifact
runs in an opaque origin: its `localStorage` and IndexedDB throw, and even the city beside it is another origin,
while Werkator allows only `https://javagil.de` by CORS.
- From disk the viewer cannot load the city either; that holds wherever the viewer comes from.
The viewer stays at javagil.de; all reports that use it today are public.
The decision comes with the move of hsadmin-ng to Woodpecker, once it is known how its reports will be reached.
- [x] Does the code city's `index.html` get the same style?
Yes, since PR#58: the same style, header with tabs, panels and footer; its texts, links and script stay.
- [x] Are the edges into a wrapped level easy to follow?
Not with 18 pixels between its lines: in Werkator's `server`, they ran behind the boxes of the lines above.
Since PR#67 the lines are 40 pixels apart, and every second line is shifted by half the step from box to box,
so the edges pass through the gaps; the human decided so on 2026-10-11, against routing them past the side.
## 7. The Pages
Both pages share a sticky header with "werkcrappit", the analysis context, and the tabs "Code City", "Structure" and "Matrix",
a legend panel, always visible, and a footer with the version of werkcrappit and a link back to the code city.
`structure.html`, title "Structure":
- One folder at a time: its breadcrumb, its numbers of children, edges and edges against the levels,
its numbers of packages that use it and of classes and packages it uses outside, and its diagram.
A chain of folders with one sub-folder each is one folder, as in the city (`@MC-CC-017`).
- Under the top folder's diagram, tiles with the numbers of classes, packages, edges, class cycles,
package cycles across the hierarchy and package cycles along it,
and a list of the cycles: per class cycle its folder and its classes;
per package cycle its kind, its folder, its members and the class edges that point against the levels,
each linked to its diagram.
It is empty, with a sentence that says so, when there is no cycle.
`matrix.html`, title "Matrix":
- The matrix of section 5.6 in a panel, with the chips for the metric and for "Expand all" and "Collapse all",
and the number of rows and of open packages.
- Under it the same tiles and list of cycles as on `structure.html`, linked to its diagrams.
`index.html` gets two more buttons, "Structure" and "Matrix", each with one sentence on what it shows.
Unlike the four views, they are relative links and stay visible when the page is opened from disk.
### 7.1 How to Open the Views
The two new pages are files beside `index.html` in the report directory; the CI server serves them as it serves `index.html`.
```
build/reports/codecharta/
├── index.html start page, as before
├── mutation-crap.cc.json for CodeCharta, as before
├── structure.html new: the level diagram
└── matrix.html new: the matrix
CI server: …/reports/codecharta/index.html
├─ "Mutation-CRAP" etc. ──▶ javagil.de/codecharta/?file=<URL of mutation-crap.cc.json> (unchanged)
│ └─ the viewer fetches the file from the CI server by CORS
├─ "Structure" ──▶ …/reports/codecharta/structure.html (same server)
└─ "Matrix" ──▶ …/reports/codecharta/matrix.html (same server)
```
- The links are relative: the reader stays on the CI server.
Each page holds its data and its script and loads nothing, neither from javagil.de nor from the CI server; no CORS is needed.
- From disk, `build/reports/codecharta/structure.html` opens after `./gradlew pitest mutationCrapCodeCharta`;
the CodeCharta views cannot do that.
- The tabs "Code City", "Structure" and "Matrix" lead between the pages; "Code City" leads to `index.html`
and from there to CodeCharta.
- CodeCharta stays loosely coupled: werkcrappit writes its file and links its viewer; the new pages are no part of it,
and the viewer does not know them.
- The CI server must serve the pages with their inline script.
`index.html` already builds its CodeCharta links with an inline script; the pilot with Werkator checks the larger scripts.
- For a private repository without a login, the CI server must publish the two pages too:
for Werkator, in `publicArtifacts` of `.werkator.yml`.
## 8. Steps
Each is one PR with green CI, a version of its own, Gherkin scenarios in a new feature file `10-structure-view.feature`
with IDs `@MC-SV-…`, and entries in `docs/acceptance-test-map.md`.
1. **The diagram:** `StructurePage` in `de.javagil.werkcrappit.pit.codecharta`,
written as `structure.html` by `mutationCrapCodeCharta`;
`Levelization` keeps its components and runs also without levelization in the file;
the button "Structure" on `index.html`.
2. **The matrix:** `MatrixPage`, written as `matrix.html` beside it; the button "Matrix" on `index.html`.
3. **The pilot, Werkator:** its nightly build `mutation` already runs `mutationCrapCodeCharta`;
it takes the new version, and its published code city gets both pages.
Werkator is written in Kotlin; the pages read bytecode as the city does, so they need nothing of their own for it.
Anything that shows up for Kotlin goes into the specification's Kotlin step.
4. **hsadmin-ng:** its nightly build publishes both pages with the city;
`structure.html` and `matrix.html` join its `publicArtifacts` in `.werkator.yml`, and the guide shows them.
## 9. Non-Goals
- A gate on cycles; a limit such as "no new cycle" may follow once the view proves useful.
- Edges to libraries and the JDK, as in the city.
- Kotlin specifics, as in RFC 0002.