Files
werkcrappit/docs/adr/0008-change-risk.md
miandmhoennig 18248cb975
werkator build succeeded after 01:44
werkator/publish build succeeded after 00:11
Change risk per class from Mutation-CRAP, cohesion and coupling (0.22.0) (#41)
## 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
2026-10-04 16:51:32 +02:00

5.3 KiB
Raw Permalink Blame History

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.