Files
werkcrappit/docs/adr/0004-report-contract.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

3.0 KiB

ADR 0004: Report Contract

  • Status: accepted
  • Date: 2026-10-01
  • PR: #6

Context

The gate of step 6 evaluates werkcrappit's own JSON report, not PIT's XML (specification 6.4). That makes the report a contract: versioned, schema-checked, exact where a limit is compared, and never stale.

Decision

  • Files. mutation-crap.json (machine-readable) and mutation-crap.md (for people), both in PIT's report directory, next to PIT's own reports; plus a few lines on the console. All three are written from one ReportData, so they cannot disagree.
  • Schema. mutation-crap.schema.json (JSON Schema 2020-12) ships in the plugin JAR next to the report classes. schemaVersion is 1; any change of the shape raises it. Since PR#36 it is 2: the section classes carries cohesion and coupling per top-level class (RFC 0002). Since PR#41 it is 3: each class also carries its change risk, exact and rounded, and what it is made of (ADR 0008). The schema is strict (additionalProperties: false), so drift between code and contract fails a test.
  • Sections. run (status, diagnostics), definition (metric id and version, policy, formula, complexity provider and version), provenance (plugin and PIT version, analysis context, code roots, origin and SHA-256 of every analysed class file), configuration (werkcrappit's options and the PIT options that are known), scope, summary, methods.
  • Numbers. Undefined values are null, never 0, NaN or Infinity; the writer refuses non-finite numbers. mutationCrap is rounded to four decimals for reading; mutationCrapExact carries the exact fraction as decimal strings, and only it may be compared against a limit.
  • Summary. Counts per state and the ten highest scores. No sum or average: a project-wide CRAP would be a metric the specification does not define.
  • Determinism. No timestamp, no run id; maps are written in a fixed order. The same inputs and configuration give the same bytes.
  • Never stale. runStart() replaces both files with a RUNNING marker; only runEnd() writes the finished report. A run that dies in between leaves a report that says so, never an older finished one.
  • Configuration. werkcrappit's options come from PIT's pluginConfiguration and use the prefix werkcrappit.: analysisContext, excludeClasses, excludeMethods. An unknown key with that prefix stops the run; keys of other plugins are left alone.
  • PIT's configuration. Target and excluded classes, excluded methods, mutators and features are recorded; target tests, mutation filters and test framework settings are not. The report says so (pitConfigurationComplete: false) instead of implying a complete fingerprint. Environment variables and system properties are never written; they may hold credentials.

Consequences

  • A gate can tell a finished report from a running or broken one by run.status and the schema alone.
  • Writing the files is not atomic: a crash during the final write leaves a truncated file, which the schema rejects.