Files
werkator/docs/prs/2026-09-17-PR#78-a-moment-is-not-a-level.md
e703639e8e
werkator build succeeded after 01:56
werkator/deploy build succeeded after 00:07
fix(sizing): a disk is sized by the level it holds, not by one quarter hour (#78)
The SSD's one moment at 90 % kept a 16 GiB quota on `upgrade` for the
thirty days it stayed in the window; the HDD's three imports on three
days read as "growing 0.41 GiB/day". A disk is now judged by the 95th
percentile of the last week's quarter-hour maxima, the growth is the
median of the daily deltas, and the projection starts from the level.
The window maximum is still named and still vetoes a downsize.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: mhoennig <michael@hoennig.de>
Reviewed-on: #78
2026-09-17 16:36:47 +02:00

111 lines
6.6 KiB
Markdown

> **WARNING:** This document describes only the change applied in this PR.
> It may already be outdated once the next PR is merged.
> Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.
## The Problem
The System page of the production host advised an upgrade for both disks on 2026-09-17, and both verdicts were wrong.
The SSD read `upgrade | peak 14.46 of 16.00 GiB (90 %) in 9 d`.
That peak was one quarter hour out of 885, the moment the old per-repository Gradle caches and the new shared one lay side by side during the migration of PR#76.
The disk held 8.4 GiB before and 11 GiB after.
A disk was sized by the highest quarter hour of the 30-day window, so that one moment would have kept the quota on `upgrade` for a month.
A one-off — an accident, a deliberate action of one afternoon — must not ask for a raise, least of all for thirty days.
The HDD read `upgrade | growing 0.41 GiB/day, 90 % of the 20.00 GiB group quota in 22 days` at 45 % of its quota.
The growth was three steps: a rootfs imported on 09.09, Testcontainers images on 11.09, another rootfs on 16.09, and six days of nothing in between.
The trend estimate of PR#42 took the smaller regression slope of the two window halves, which catches one step and not three.
The projection also started from the window peak, 8.97 GiB, while the disk held 7.05 GiB.
## Non-Goals
- No change to RAM and CPU sizing.
- No change to the health verdict; the live gauges keep showing the moment.
- No change to the thresholds (`UPGRADE_RATIO`, `WATCH_RATIO`, `DOWNSIZE_RATIO`) or to the bookable steps.
## The Scenarios
### Feature: a disk is sized by the level it holds and the growth it shows on most days
#### Background
- The *level* of a disk is the 95th percentile of the quarter-hour maxima of the last seven days — the percentile the CPU watch band has used since PR#42.
A quarter hour is a moment; a level is what the disk holds in more than a twentieth of the week.
- The *growth* of a disk is the median of its daily deltas over the last seven days.
A step is a delta on one day; a trend is a delta on most days.
- The *peak* stays the highest quarter hour of the whole window.
It is named in the reason, and it can veto a downsize, but it cannot ask for a raise.
#### Scenario#78.01: A moment is not a level
- **Given** a 16 GiB quota holding 8.4 GiB for ten days, and one quarter hour at 14.46 GiB
- **When** the disk is sized
- **Then** the verdict is `fits`
- **and** the reason names both: `holds 8.40 of 16.00 GiB (53 %), peak 14.46 (90 %) in 10 d`
- **But** a disk at 85 % throughout the last two days is `tight`, because a level held is a level
##### Verified by
- [CapacitySizingTest — one quarter hour at 90 % is a moment, not a level: the production SSD of 2026-09-17](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt)
- [CapacitySizingTest — a level held for two days is a level](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt)
#### Scenario#78.02: The window maximum is named and vetoes a downsize, nothing more
- **Given** a disk that held 93 % for two days a week ago and 50 % since
- **When** the disk is sized
- **Then** the verdict is `fits`, the reason names the peak
- **Given** a disk at 25 % with one quarter hour at 60 %
- **Then** the verdict is `fits`, not `downsize`: a moment cannot prove the slack a shrink needs
##### Verified by
- [CapacitySizingTest — a level the disk no longer holds is named, not judged](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt)
- [CapacitySizingTest — a moment vetoes a downsize the level alone would allow](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt)
#### Scenario#78.03: Steps are not growth
- **Given** a 20 GiB quota with steps of 1.9, 1.6 and 1.8 GiB on three of ten days and no change on the others
- **When** the disk is sized
- **Then** the verdict is `fits` and no growth is reported
##### Verified by
- [CapacitySizingTest — three steps in nine days are not growth: the production HDD of 2026-09-17](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt)
#### Scenario#78.04: Growth is projected from the level
- **Given** a disk growing 0.1 GiB on every day and one quarter hour at 88 % on the first day
- **When** the disk is sized
- **Then** the projection starts from the level, not from that moment, and the verdict is `fits`
- **Given** fewer than four daily deltas
- **Then** no growth is reported at all
##### Verified by
- [CapacitySizingTest — growth is what most days show, and it is projected from the level, not from a moment](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt)
- [CapacitySizingTest — a trend needs a majority of the week's days](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt)
- [CapacitySizingTest — disk growing towards 90 % within thirty days is an upgrade even below the peak threshold](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt)
## The Solution
`CapacitySizing.disk` no longer goes through `byPeak`.
It reads the level with `sustainedLevel` — `SUSTAINED_PERCENTILE` (0.95) of the maxima column of the last `DISK_LEVEL_DAYS` (7) — and judges `upgrade` and `tight` from it; the recommendation is the level with a third free, rounded to the bookable step.
The window maximum is still computed, named in the reason (`holds … , peak …`), and used for the downsize test as before: slack is proven against the worst moment, a raise against the level.
`growthPerDay` takes the level at the end of each of the last seven days from the quarter-hour averages, forms the daily deltas, and returns their median; fewer than `MIN_TREND_DAYS` (4) deltas or a negative median count as no growth.
The projection is the level plus thirty days of that growth, and the days left are counted from the level.
The two-halves regression and `MIN_TREND_POINTS` are gone.
CPU's two percentile calls now use the shared `SUSTAINED_PERCENTILE` instead of a literal.
The level is read from the last week rather than the whole window on purpose: what a disk held three weeks ago and has since been cleaned up is not what it holds, and the verdict should follow a cleanup within the week, not within the month.
## Open Questions
- A disk that grows on most days and is pruned once a month shows its growth phase as a trend; whether retention pruning needs to be accounted for is left until it shows on a host.
## Additional Changes
- Release notes and version bump to v1.21.1.
- `AGENTS.md` invariant and the architecture skill describe the disk rule.