## Related Links - [ADR 0008](../adr/0008-change-risk.md), the formula and its alternatives - [RFC 0002](../rfcs/0002-cohesion-and-coupling.md), which brought LCOM4 and Ce ## The Problem The report has three values per class or method, but none says where a change most likely breaks something unnoticed. Mutation-CRAP per method misses a large class whose many simple methods are weakly tested. In werkcrappit, `ClassReferences` has no method above 6.8, but 43 % of its mutants survive. ## Non-Goals - A limit for the change risk: the gate keeps judging the single values (`@MC-CR-005`). - Any change of Mutation-CRAP, LCOM4 or Ce. ## The Scenarios The scenarios were agreed one by one on 2026-10-04, in the new feature file `09-change-risk.feature` and in `07-codecharta.feature`. Name, formula and constants were decided before: "Change risk", Mutation-CRAP over the whole class as the base (option C of three), fixed `k_L = 2` and `k_C = 20`, a band from 30 to 100. - `@MC-CR-001` change risk is Mutation-CRAP over the whole class: 20 methods of complexity 1 with half their mutants killed give 70, though no method exceeds 2. - `@MC-CR-002` LCOM4 and Ce raise it, `× (1 + (LCOM4 − 1)/2) × (1 + Ce/20)`, as an exact fraction. - `@MC-CR-003` only scored methods count, nested classes belong to their top-level class; no scored method, no change risk; no LCOM4 counts as 1. - `@MC-CR-004` an `INCONCLUSIVE` method or one without complexity voids the change risk of its class, with the reason `method-not-scored:…`. - `@MC-CR-005` the gate does not judge the change risk, and the methods' Mutation-CRAP stays as it was. - `@MC-CR-006` `mutation-crap.md` lists the ten classes with the highest change risk, with what makes them risky. - `@MC-CC-035` the city has the metric `change_risk`, a fourth scenario `change-risk.ccscenario` and a link coloured by it. ##### Verified by - [ChangeRiskTest](../../werkcrappit-pit/src/test/java/de/javagil/werkcrappit/pit/core/ChangeRiskTest.java) - [ChangeRisksTest](../../werkcrappit-pit/src/test/java/de/javagil/werkcrappit/pit/design/ChangeRisksTest.java) - [JsonReportTest](../../werkcrappit-pit/src/test/java/de/javagil/werkcrappit/pit/report/JsonReportTest.java) `theClassEntryCarriesTheChangeRisk` - [MarkdownReportTest](../../werkcrappit-pit/src/test/java/de/javagil/werkcrappit/pit/report/MarkdownReportTest.java) `theTenClassesWithTheHighestChangeRisk` - [GateTest](../../werkcrappit-pit/src/test/java/de/javagil/werkcrappit/pit/gate/GateTest.java) `changeRiskIsNotJudged` - [ReportInputTest](../../werkcrappit-pit/src/test/java/de/javagil/werkcrappit/pit/codecharta/ReportInputTest.java) `theChangeRiskOfTheReportIsReadBackExactly` - [CodeCityTest](../../werkcrappit-pit/src/test/java/de/javagil/werkcrappit/pit/codecharta/CodeCityTest.java) `theCityShowsTheChangeRisk`, [CodeCityScenariosTest](../../werkcrappit-pit/src/test/java/de/javagil/werkcrappit/pit/codecharta/CodeCityScenariosTest.java) `changeRiskIsTheFourthScenario`, [CodeCityPageTest](../../werkcrappit-pit/src/test/java/de/javagil/werkcrappit/pit/codecharta/CodeCityPageTest.java) `thePageOpensTheCityByChangeRisk` - [CodeChartaFunctionalTest](../../werkcrappit-gradle-plugin/src/functionalTest/java/de/javagil/werkcrappit/gradle/CodeChartaFunctionalTest.java) `aRealBuildWritesAValidCity` ## The Solution - `ChangeRisk` in the metric core computes the value as a reduced fraction from the summed complexity and mutants, LCOM4 and Ce; its base is `MutationCrap.of`. - `ChangeRisks` joins the methods to their top-level classes; `ClassDesign` carries the result as `ClassRisk`. `ReportData` does the join, so JSON, Markdown and console read the same values, and the listener's coupling stays at its ratchet of 21. - `mutation-crap.json` has schema version 3; the gate and the export read version 3. - `mutation-crap.md` has a section "Change Risk"; the city a metric `change_risk`, a scenario and a link, yellow from 30 and red from 100. - Version 0.22.0. ## Additional Changes - `FeatureFilesTest` now also checks the IDs `MC-CK-…` and `MC-CR-…` against the acceptance map; it knew only `MC-…` and `MC-CC-…`. - The acceptance map is column-aligned, and its note on test locations moved below the table, where it no longer splits it. ## Follow-up PRs - werkcrappit-pit uses the Gradle plugin 0.22.0: until then the nightly mutation build fails, as its gate reads schema version 2. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: mhoennig <michael@hoennig.de> Reviewed-on: #41
74 lines
5.3 KiB
Markdown
74 lines
5.3 KiB
Markdown
# ADR 0008: Change Risk per Class
|
||
|
||
- Status: accepted
|
||
- Date: 2026-10-04
|
||
- PR: #41
|
||
|
||
## Context
|
||
|
||
Since RFC 0002, the report has three values that each tell part of the risk of a change.
|
||
Mutation-CRAP rates a method by its complexity and the mutants its tests kill.
|
||
LCOM4 tells whether a class does more than one thing, Ce how many other classes of the analysed code it depends on.
|
||
A reader wants one value per class that says where a change most likely breaks something unnoticed, with a reason.
|
||
|
||
Mutation-CRAP per method has a blind spot at class level.
|
||
A class with 20 straight methods of complexity 1, half of whose mutants survive, has no method above 2.
|
||
Yet the class is large and half untested.
|
||
In werkcrappit, `ClassReferences` is such a class: its highest method has 6.8, but 43 % of its mutants survive.
|
||
|
||
## Decision
|
||
|
||
- **Name.** Change risk, `changeRisk` in the report and `change_risk` in the code city.
|
||
- **Formula.** Per top-level class, nested classes and lambdas included:
|
||
|
||
`changeRisk = MC(ΣC, ΣK, ΣE) × (1 + (LCOM4 − 1) / 2) × (1 + Ce / 20)`
|
||
|
||
`MC` is the Mutation-CRAP formula, applied to the summed complexity `ΣC` and the summed killed and eligible mutants `ΣK` and `ΣE` of the class's scored methods.
|
||
The sum of the complexities is WMC of Chidamber and Kemerer; the score is weighted by mutants, not averaged over methods.
|
||
- **Factors.** Poor cohesion and outgoing coupling raise the risk the tests leave, they do not add a risk of their own.
|
||
One method group more adds half of the base, 20 dependencies double it.
|
||
The constants are fixed, not configurable, so that values of different projects compare.
|
||
- **Which methods.** Only scored methods add complexity and mutants; a method without mutants or excluded adds nothing.
|
||
A method in state `INCONCLUSIVE` or `COMPLEXITY_UNAVAILABLE` voids the change risk of its class, as a problem state voids a method's score.
|
||
The reason `method-not-scored:<method>` names it; without any scored method the reason is `no-scored-methods`.
|
||
A class without LCOM4 counts as cohesive, LCOM4 1.
|
||
- **Exactness.** The value is a reduced fraction like Mutation-CRAP; the report carries `changeRisk` rounded and `changeRiskExact`.
|
||
- **No gate.** The gate judges the three single values; each violation names one value and its limit.
|
||
- **Colour band.** Without a limit, the band is fixed: yellow from 30, the classic CRAP threshold, red from 100.
|
||
- **Report.** Schema version 3: each entry of `classes` adds `complexity`, `killed`, `denominator`, `mutationScore`, `changeRisk`, `changeRiskExact` and `changeRiskReasons`; `definition` adds `changeRiskFormula`.
|
||
`ReportData` joins the methods to their classes, so JSON, Markdown and console cannot disagree.
|
||
|
||
## Alternatives
|
||
|
||
| Alternative | Why not |
|
||
| ------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
||
| Highest method Mutation-CRAP as the base | Blind to a large class with many simple, weakly tested methods, such as an orchestrator |
|
||
| Weighted sum of value ÷ limit per dimension | The weights are arbitrary, the critique of the Maintainability Index; wiring classes rank high |
|
||
| Euclidean norm of value ÷ limit | Hard to explain; normalised by a project's own ratchets, values of projects do not compare |
|
||
| Highest of value ÷ limit | The same information as the gate |
|
||
| Afferent coupling as a third factor | Punishes stable utility classes, which should be depended on |
|
||
| A limit `maxChangeRisk` | A violation could not name what to fix; the single limits can |
|
||
|
||
## Calibration
|
||
|
||
werkcrappit's nightly report of 2026-10-04, 47 classes with a change risk: median 14.7, 90 % quantile 58.7, highest 235.5.
|
||
At 30 and 100, 13 classes are yellow and 2 red.
|
||
|
||
| Change risk | Base | ΣC | Score | LCOM4 | Ce | Highest method | Class |
|
||
| ----------: | ----: | --: | ----: | ----: | --: | -------------: | ---------------------- |
|
||
| 235.5 | 235.5 | 48 | 0.57 | 1 | 0 | 6.80 | `ClassReferences` |
|
||
| 130.8 | 70.7 | 52 | 0.81 | 1 | 17 | 7.10 | `CodeChartaExport` |
|
||
| 87.7 | 53.2 | 52 | 0.92 | 2 | 2 | 6.40 | `Levelization` |
|
||
| 60.5 | 29.5 | 27 | 0.85 | 1 | 21 | 4.00 | `MutationCrapListener` |
|
||
| 58.7 | 25.5 | 20 | 0.76 | 3 | 3 | 8.00 | `JacocoComplexity` |
|
||
|
||
In hsadmin-ng, the generated OpenAPI classes have neither Mutation-CRAP nor LCOM4; they only raise Ce of the controllers that implement them, by up to 17.
|
||
That coupling is real: a change of the API specification changes these controllers.
|
||
|
||
## Consequences
|
||
|
||
- The change risk is a class value, not a method value; it says where to look, the single values say what to fix.
|
||
- Size counts: a well tested class keeps its summed complexity as its change risk.
|
||
- A reader of schema version 2 refuses the report; werkcrappit's own nightly needs the Gradle plugin 0.22.0, a follow-up bump.
|
||
- Changing a constant or the formula changes every value; it needs a new decision.
|