Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.
The Problem
Four methods of the sizing advice are above the Kotlin target of 15, measured by the nightly mutation build of 2026-10-05:
Method
Mutation-CRAP
Complexity
Mutation score
CapacitySizing.ram
26.87
15
0.63
CapacitySizing.column
26.83
6
0.17
CapacitySizing.disk
21.19
16
0.73
UiFormats.adviceLabel
15.55
5
0.25
ram and disk decide every verdict of their resource in one method each, as cpu did before PR#113. column and adviceLabel are short, but their tests never told the alternatives apart: every test tier carried the same values as averages and as maxima, and no test asked for a watch chip.
Non-Goals
No change in behaviour: every verdict, size and reason stays as it was.
The other groups above 15 follow in their own PRs.
The Scenarios
Feature: the sizing reads the right column and words every verdict
Scenario#123.01: RAM is sized from the quarter-hour maxima where the tier has them, else from the averages
So that a moment of overflow, which is what RAM is sized against, is never averaged away, and a tier without maxima still gets advice.
Given ten days of history whose RAM averages stay at 2.0 GiB
When the maxima hold one quarter hour at 4.4 GiB
Then the RAM peak is 4.4 GiB
When the tier has no maxima for RAM, or an empty column
Then the RAM peak is 2.0 GiB, from the averages
When the maxima hold only gaps
Then there is no peak at all, and a gap never counts as zero
Verified by
CapacitySizingUnitTestRAM is sized from the quarter-hour maxima where the tier has them, else from the averages (Scenario#123.01)
Scenario#123.02: the RAM and disk verdicts stay as they were
So that splitting the two methods changes no advice a host has been given.
Given the RAM and disk histories of the existing scenarios
When the advice is derived
Then each verdict, recommended size and reason is the same as before
Scenario#123.03: every verdict has its chip, threads whole and GiB with two decimals
So that the server-rendered chips read like the ones werkator.js renders after the first poll.
Given an advice with the verdict tight, downsize or upgrade
When its chip is worded
Then it reads watch, raise to …, downsize to … or upgrade to …
and threads are whole, a fraction cut off, and GiB carry two decimals
Verified by
UiViewsUnitTestevery verdict has its chip, threads whole and GiB with two decimals, as werkator.js words them (Scenario#123.03), and advice chips read like werkator.js adviceLabel (Scenario#30.03)
The Solution
The pattern of PR#113 for the CPU, applied to RAM and disk.
ram checks that there is a history, builds a RamUsage (peak, usable and booked size, step, stalled share, fill line) and asks two steps:
ramStalled raises when memory stalled in enough quarter hours, null otherwise;
ramUnstalled decides between a downsize and fits.
stalledShare counts the stalled quarter hours, null without a pressure history.
disk builds a DiskUsage (level, peak, total, step, whether to ask, held line) and asks four steps in order, each null when it does not apply:
diskFull: the level reaches the upgrade ratio;
diskGrowing: the projected level reaches it within the horizon;
diskTight: the level is in the watch band and needs more than the total;
diskSlack: a downsize when the window maximum leaves room, else fits.
column reads tier.values[key].orEmpty() instead of an elvis to an empty list.
adviceLabel kept three survivors after the new tests: the !! on the recommended size compiles to a checkNotNull call, and removing it fails the same way one line later.
The size is now worded by adviceSize, which reads a missing size as werkator.js renders it, 0 threads or n/a GiB.
No verdict but fits and collecting comes without a size, so this changes nothing a reader sees.
A local PIT run of CapacitySizing and UiFormats afterwards:
Method
Mutation-CRAP
Complexity
Mutation score
CapacitySizing.ram
6.56
6
0.75
CapacitySizing.ramStalled
5.40
4
0.56
CapacitySizing.ramUnstalled
7.33
6
0.67
CapacitySizing.stalledShare
3.33
3
0.67
CapacitySizing.column
6
6
1.00
CapacitySizing.disk
10.27
9
0.75
CapacitySizing.diskFull
2.06
2
0.75
CapacitySizing.diskGrowing
4.20
4
0.77
CapacitySizing.diskTight
3.33
3
0.67
CapacitySizing.diskSlack
5.20
5
0.80
UiFormats.adviceLabel
5
5
1.00
UiFormats.adviceSize
3
3
1.00
UiFormats.repoStatusLine (16.03) is the one method of these classes still above 15; it belongs to the build lists and follows in its own PR.
The project maximum stays NginxProxyManager.resolveSettings at 27.44, so the gate stays at 28.
> Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.
## The Problem
Four methods of the sizing advice are above the Kotlin target of 15, measured by the nightly mutation build of 2026-10-05:
| Method | Mutation-CRAP | Complexity | Mutation score |
|-------------------------|---------------|------------|----------------|
| `CapacitySizing.ram` | 26.87 | 15 | 0.63 |
| `CapacitySizing.column` | 26.83 | 6 | 0.17 |
| `CapacitySizing.disk` | 21.19 | 16 | 0.73 |
| `UiFormats.adviceLabel` | 15.55 | 5 | 0.25 |
`ram` and `disk` decide every verdict of their resource in one method each, as `cpu` did before PR#113.
`column` and `adviceLabel` are short, but their tests never told the alternatives apart: every test tier carried the same values as averages and as maxima, and no test asked for a `watch` chip.
## Non-Goals
- No change in behaviour: every verdict, size and reason stays as it was.
- The other groups above 15 follow in their own PRs.
## The Scenarios
### Feature: the sizing reads the right column and words every verdict
#### Scenario#123.01: RAM is sized from the quarter-hour maxima where the tier has them, else from the averages
So that a moment of overflow, which is what RAM is sized against, is never averaged away, and a tier without maxima still gets advice.
- **Given** ten days of history whose RAM averages stay at 2.0 GiB
- **When** the maxima hold one quarter hour at 4.4 GiB
- **Then** the RAM peak is 4.4 GiB
- **When** the tier has no maxima for RAM, or an empty column
- **Then** the RAM peak is 2.0 GiB, from the averages
- **When** the maxima hold only gaps
- **Then** there is no peak at all, and a gap never counts as zero
##### Verified by
- [CapacitySizingUnitTest](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingUnitTest.kt) `RAM is sized from the quarter-hour maxima where the tier has them, else from the averages (Scenario#123.01)`
#### Scenario#123.02: the RAM and disk verdicts stay as they were
So that splitting the two methods changes no advice a host has been given.
- **Given** the RAM and disk histories of the existing scenarios
- **When** the advice is derived
- **Then** each verdict, recommended size and reason is the same as before
##### Verified by
- [CapacitySizingUnitTest](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingUnitTest.kt): the tests of Scenario#30, #42, #43, #58, #78 and #79, unchanged
#### Scenario#123.03: every verdict has its chip, threads whole and GiB with two decimals
So that the server-rendered chips read like the ones `werkator.js` renders after the first poll.
- **Given** an advice with the verdict `tight`, `downsize` or `upgrade`
- **When** its chip is worded
- **Then** it reads `watch, raise to …`, `downsize to …` or `upgrade to …`
- **and** threads are whole, a fraction cut off, and GiB carry two decimals
##### Verified by
- [UiViewsUnitTest](../../src/test/kotlin/de/hoennig/werkator/server/UiViewsUnitTest.kt) `every verdict has its chip, threads whole and GiB with two decimals, as werkator.js words them (Scenario#123.03)`, and `advice chips read like werkator.js adviceLabel (Scenario#30.03)`
## The Solution
The pattern of PR#113 for the CPU, applied to RAM and disk.
`ram` checks that there is a history, builds a `RamUsage` (peak, usable and booked size, step, stalled share, fill line) and asks two steps:
- `ramStalled` raises when memory stalled in enough quarter hours, `null` otherwise;
- `ramUnstalled` decides between a downsize and fits.
`stalledShare` counts the stalled quarter hours, `null` without a pressure history.
`disk` builds a `DiskUsage` (level, peak, total, step, whether to ask, held line) and asks four steps in order, each `null` when it does not apply:
- `diskFull`: the level reaches the upgrade ratio;
- `diskGrowing`: the projected level reaches it within the horizon;
- `diskTight`: the level is in the watch band and needs more than the total;
- `diskSlack`: a downsize when the window maximum leaves room, else fits.
`column` reads `tier.values[key].orEmpty()` instead of an elvis to an empty list.
`adviceLabel` kept three survivors after the new tests: the `!!` on the recommended size compiles to a `checkNotNull` call, and removing it fails the same way one line later.
The size is now worded by `adviceSize`, which reads a missing size as `werkator.js` renders it, `0 threads` or `n/a GiB`.
No verdict but `fits` and `collecting` comes without a size, so this changes nothing a reader sees.
A local PIT run of `CapacitySizing` and `UiFormats` afterwards:
| Method | Mutation-CRAP | Complexity | Mutation score |
|-------------------------------|---------------|------------|----------------|
| `CapacitySizing.ram` | 6.56 | 6 | 0.75 |
| `CapacitySizing.ramStalled` | 5.40 | 4 | 0.56 |
| `CapacitySizing.ramUnstalled` | 7.33 | 6 | 0.67 |
| `CapacitySizing.stalledShare` | 3.33 | 3 | 0.67 |
| `CapacitySizing.column` | 6 | 6 | 1.00 |
| `CapacitySizing.disk` | 10.27 | 9 | 0.75 |
| `CapacitySizing.diskFull` | 2.06 | 2 | 0.75 |
| `CapacitySizing.diskGrowing` | 4.20 | 4 | 0.77 |
| `CapacitySizing.diskTight` | 3.33 | 3 | 0.67 |
| `CapacitySizing.diskSlack` | 5.20 | 5 | 0.80 |
| `UiFormats.adviceLabel` | 5 | 5 | 1.00 |
| `UiFormats.adviceSize` | 3 | 3 | 1.00 |
`UiFormats.repoStatusLine` (16.03) is the one method of these classes still above 15; it belongs to the build lists and follows in its own PR.
The project maximum stays `NginxProxyManager.resolveSettings` at 27.44, so the gate stays at 28.
## Additional Changes
- Version 1.37.4 with its entry in `releases.html`.
## Prerequisite PRs
- PR#113: the same split for the CPU advice.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
mi
changed title from WIP: RAM and disk sizing advice split into their steps to RAM and disk sizing advice split into their steps, every advice chip tested (v1.37.4)2026-10-05 18:46:31 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The Problem
Four methods of the sizing advice are above the Kotlin target of 15, measured by the nightly mutation build of 2026-10-05:
CapacitySizing.ramCapacitySizing.columnCapacitySizing.diskUiFormats.adviceLabelramanddiskdecide every verdict of their resource in one method each, ascpudid before PR#113.columnandadviceLabelare short, but their tests never told the alternatives apart: every test tier carried the same values as averages and as maxima, and no test asked for awatchchip.Non-Goals
The Scenarios
Feature: the sizing reads the right column and words every verdict
Scenario#123.01: RAM is sized from the quarter-hour maxima where the tier has them, else from the averages
So that a moment of overflow, which is what RAM is sized against, is never averaged away, and a tier without maxima still gets advice.
Verified by
RAM is sized from the quarter-hour maxima where the tier has them, else from the averages (Scenario#123.01)Scenario#123.02: the RAM and disk verdicts stay as they were
So that splitting the two methods changes no advice a host has been given.
Verified by
Scenario#123.03: every verdict has its chip, threads whole and GiB with two decimals
So that the server-rendered chips read like the ones
werkator.jsrenders after the first poll.tight,downsizeorupgradewatch, raise to …,downsize to …orupgrade to …Verified by
every verdict has its chip, threads whole and GiB with two decimals, as werkator.js words them (Scenario#123.03), andadvice chips read like werkator.js adviceLabel (Scenario#30.03)The Solution
The pattern of PR#113 for the CPU, applied to RAM and disk.
ramchecks that there is a history, builds aRamUsage(peak, usable and booked size, step, stalled share, fill line) and asks two steps:ramStalledraises when memory stalled in enough quarter hours,nullotherwise;ramUnstalleddecides between a downsize and fits.stalledSharecounts the stalled quarter hours,nullwithout a pressure history.diskbuilds aDiskUsage(level, peak, total, step, whether to ask, held line) and asks four steps in order, eachnullwhen it does not apply:diskFull: the level reaches the upgrade ratio;diskGrowing: the projected level reaches it within the horizon;diskTight: the level is in the watch band and needs more than the total;diskSlack: a downsize when the window maximum leaves room, else fits.columnreadstier.values[key].orEmpty()instead of an elvis to an empty list.adviceLabelkept three survivors after the new tests: the!!on the recommended size compiles to acheckNotNullcall, and removing it fails the same way one line later.The size is now worded by
adviceSize, which reads a missing size aswerkator.jsrenders it,0 threadsorn/a GiB.No verdict but
fitsandcollectingcomes without a size, so this changes nothing a reader sees.A local PIT run of
CapacitySizingandUiFormatsafterwards:CapacitySizing.ramCapacitySizing.ramStalledCapacitySizing.ramUnstalledCapacitySizing.stalledShareCapacitySizing.columnCapacitySizing.diskCapacitySizing.diskFullCapacitySizing.diskGrowingCapacitySizing.diskTightCapacitySizing.diskSlackUiFormats.adviceLabelUiFormats.adviceSizeUiFormats.repoStatusLine(16.03) is the one method of these classes still above 15; it belongs to the build lists and follows in its own PR.The project maximum stays
NginxProxyManager.resolveSettingsat 27.44, so the gate stays at 28.Additional Changes
releases.html.Prerequisite PRs
🤖 Generated with Claude Code
WIP: RAM and disk sizing advice split into their stepsto RAM and disk sizing advice split into their steps, every advice chip tested (v1.37.4)