PART 3 — THE MEASUREMENT, AND THE DECISION IT PRODUCED
Measured on demo-hp, controller v0.220.2 as shipped, BEFORE any code change.

THE READING (prompt §3): "a held app has one container still up — its database — so it does not
aggregate to StateStopped; it should aggregate to a state the down-predicate treats as a fault", and
therefore a held app gets a dead-app banner and a customer email.

THE MEASUREMENT: IT DID NOT REPRODUCE.

  hold created            21:11:19Z (deliberate trigger, see 01)
  waited                  2m20s, then longer; deadapp-check runs every 30s (main.go:726) and the
                          scheduler log shows 9 executions in the window — the scans DID run over it
  docmost's run state     `unhealthy`      <- NOT StateStopped, and NOT a down state
  IsDownState             {stopped, exited, degraded}  (stacks/manager.go:54) — `unhealthy` absent
  dead-app count          `0 currently down`, 8 apps evaluated, at 21:13:48 and 21:23:48
  customer event          NONE. The only event was this session's own operator-tier
                          `backup_run_failures`, which is the hold's intended notification.
  database container      docmost-postgres STILL UP (Part 2.3's subject — that half was right)

WHY THE READING WAS WRONG. The first half was right: a held app is not StateStopped. The second half
assumed the remaining state would be one the predicate treats as a fault. It is not.
`aggregateState` (stacks/manager.go:~810) checks `if unhealthy > 0 → StateUnhealthy` FIRST, before the
mixed-case degraded branch. A held app's surviving database plus its failing app healthcheck put the
stack in `unhealthy`, and `unhealthy` is deliberately excluded from IsDownState.

DECISION: PART 2 IS DROPPED IN FULL — 2.1 (suppression), 2.2 (replacement banner) and 2.3 (stopping
the database). 2.2 and 2.3 existed only to make 2.1 safe and complete; with nothing to suppress there
is nothing to replace, and stopping the database would be a behaviour change with no defect behind it.
No register row is opened for the held-app alarm, per the runbook. The negative is recorded in the
capability map instead.

POSITIVE CONTROL — and the two live attempts that FAILED, recorded because they are the finding.
  attempt 1: `docker stop privatebin` (single-container stack) -> stack read `stopped`, which is
             WHITELISTED as a deliberate user stop. No alarm, correctly by design.
  attempt 2: `docker stop bookstack-db` (multi-container) -> `degraded` for a moment, then `unhealthy`
             once the app's own healthcheck failed. No alarm.
  So NEITHER live attempt put a stack in a lasting down state, and an absent alarm from a detector
  never shown working proves nothing.
  THE CONTROL THAT WORKS is at the layer the detector lives in — `classifyRunStates` is a pure
  function: TestClassifyRunStates_PositiveControl_ADownStackDoesAlarm feeds it `degraded` and
  `exited` and both raise the banner and a Down run state; the companion test feeds it the states
  MEASURED LIVE (`unhealthy`, `stopped`) and both are silent. The detector works; these states do not
  reach it.

ADJACENT FINDING, measured rather than reasoned, and OUT OF THIS TASK'S SCOPE:
  `bookstack` sat with its DATABASE CONTAINER EXITED and its app `unhealthy` for several minutes and
  the dead-app watcher reported `0 currently down` throughout. An app whose database has died raises
  no dead-app banner and no customer email, because `unhealthy` masks the mixed state. It is not
  wholly invisible — the health report counts it (`cr.Unhealthy++`, report/builder.go:251) and that
  reaches the hub — but it does not alarm. Filed as a register row; NOT acted on here.
