A Release Can Be Complete and Not Ready
A release status records what a person decided. Readiness records what the evidence says. Hawzu keeps the two separate — and puts hard gates in front of the score, so an average can't launder an open blocker.
A release can be marked Complete with three critical defects open against it.
Nothing in Hawzu stops that, and nothing should. Someone decided to ship. Maybe the defects are in a feature that got flagged off, maybe there’s a hotfix scheduled, maybe it was a bad call. A status field records what a person decided. It was never in a position to check.
The problem starts when one status field is quietly doing both jobs — recording a decision and implying a verdict — and readers can’t tell which one they’re looking at.
Two things wearing the same word
There are two genuinely different questions here.
Where is this release in its lifecycle? Not started, in progress, paused, completed, archived. This is scheduling, and scheduling is a human matter. A release paused because a customer moved a date isn’t a data condition; no amount of test evidence implies it. So in Hawzu this is set by a person, with only two rules: you can’t send a release back to not-started, and nothing leaves archived.
Is the evidence good enough to ship? That one has an answer in the data, and it should never be typeable. If a person can type it, then sooner or later it says Ready because someone was optimistic on a Friday, and it means nothing afterwards.
A status records a decision. It can’t check one.
- Not started
- In progress
- Paused
- Completed
- Archived
Nothing feeds this. It records a decision.
No one can type this. It reports a finding.
Splitting them costs you the comfortable ambiguity of a single field and buys you a release page where the two disagree out loud. Completed, and Not ready. That’s not a contradiction to be designed away — it’s the most informative thing the screen can tell you.
Gates first, score second
Readiness is a fold over four dimensions — coverage, execution, requirements, defects — weighted, and renormalised when a dimension doesn’t apply to a release rather than scored as zero.
But a weighted score on its own launders blockers. A release with a 96% pass rate, good coverage and one open critical defect computes to a high number, and that number is a lie by arithmetic. Averages are built to let strong dimensions absorb weak ones. That’s the whole point of them, and it’s exactly wrong for a ship decision.
So gates run first and can veto the score outright: coverage below the bar, a failure rate above the threshold, any open critical, too many failures not linked to a defect, requirements verification below the bar. Each gate produces a verdict and names the cause. Only if nothing is gated does the score get to speak.
The number had to leave the browser
Readiness used to be computed in the front end. Three different screens each folded the same raw numbers into a verdict, in their own code.
Two of them disagreed.
Neither was wrong, exactly — they’d drifted through ordinary maintenance, the way three copies of anything drift. The bug wasn’t in either implementation. It was in there being three. So the whole calculation moved to one place on the server, as pure arithmetic with no database access at all: hand it counts, it hands back a verdict. Arithmetic hidden behind a database import is arithmetic nobody tests.
Porting it surfaced a trap worth knowing about if you ever move a calculation
between languages. JavaScript and Python disagree about exact half-values:
Math.round rounds a half upward, while Python’s round rounds a half to the
nearest even number — so 2.5 becomes 3 in one and 2 in the other. Same inputs,
same formula, different score, and it only shows up on exact halves, which is to
say rarely enough to ship and often enough to be found by a customer rather than
by us. The server now reproduces the browser’s rule deliberately.
The related discipline: every percentage recomputes from raw counts, never from another percentage that’s already been rounded. Round once, at the end, or you spend the rest of the calculation compounding a number you threw precision away on.
What the engine refuses to decide
Two things are deliberately outside the fold.
It doesn’t pick colours. A verdict comes back with a semantic tone — critical, caution, good, neutral — and the screen decides what that looks like. A calculation that names UI colours has opinions about a screen it can’t see, and the first time you render it somewhere else, the opinions come with it.
And it doesn’t own the bars. The thresholds are a parameter passed into the calculation, not constants baked into it. Two of them look interchangeable and aren’t: the share of your suite that’s been executed and the share of your requirements that have been verified are different claims that happened to be set to the same number. Keeping them as one constant would have been a bug waiting for the first team that cared about one and not the other.
To be straight about where this stands: every release today is scored against one shared set of defaults. Per-project acceptance criteria is a seam that exists and isn’t wired up yet — the calculation accepts them, nothing writes them.
What is shipped is that sign-off freezes the bars along with the numbers. When a release is signed off, the criteria it was judged against are frozen into the snapshot with it. Without that, editing your thresholds next quarter would retroactively re-judge every release you ever signed off — and a release that shipped as Ready would quietly start reading Not ready, with the sign-off certificate still saying otherwise.
Both fields, on purpose
The release page shows what a person decided and what the evidence found, side by side, and never lets one overwrite the other. The lifecycle is yours. The verdict isn’t for sale.
That’s the same commitment a frozen test run makes, one level up: the record of what happened doesn’t bend to what anyone would prefer it said.
If a person can type “Ready”, it stops meaning anything the day someone does.
The release readiness report