The dashboard guessed whether the number was a percentage, and turned 1% into 100%
The headline figure on a compliance dashboard is a percentage. It comes out of a stored aggregate column, and between the column and the screen sits a five-line helper that decides what unit the stored number is in:
return n > 0 && n <= 1 ? round(n * 10000) / 100 : round(n * 1000) / 1000;
Read that as English. If the number is between zero and one, treat it as a fraction and multiply by a hundred. Otherwise treat it as a percentage and pass it through.
Which means a stored value of 1 — one per cent, an organisation where essentially nobody holds the
credentials they are required to hold — is rendered as 100%. A value of 0.5 renders as 50%. The
failure runs in exactly the wrong direction: the worse the real number, the more likely it is to be
read as a fraction, and the better it looks on screen.
Zero comes out right, by accident, because n > 0 is false.
The column could not say which it was
The heuristic exists because the schema did not settle the question. The column is a fixed-point numeric with three decimal places and six digits total, which permits any value up to 999.999. It does not constrain the value to nought-to-a-hundred, and a scale of three is equally plausible for a percentage with fractional precision and for a fraction expressed to a thousandth.
So both readings are consistent with the column definition, and different writers over the life of the project could plausibly have written either. When the reader could not tell, somebody wrote a guess and moved on.
A number’s unit is part of its type, and if the type cannot express the unit, the name must. A
column called compliance_percentage constrained with a check between 0 and 100 leaves nothing to
infer. A column called compliance_fraction with a check between 0 and 1 does the same. What does
not work is a bare numeric and a convention held in someone’s head, because conventions are not
enforced at three in the morning by whoever is writing the backfill script.
The check constraint is the important half of that. Naming the column is a hint. Constraining it is a guarantee, and it converts a silent misinterpretation into an insert that fails at the moment the wrong unit is written — next to the code that wrote it, by the person who can fix it.
Two branches that round differently
There is a second defect hiding inside the first one, and it is the sort of thing that makes people distrust a system without being able to say why.
The fraction branch rounds to two decimal places. The percentage branch rounds to three. So the same underlying value, entering by two different routes, comes out with different precision, and a figure shown on one screen will not match a figure shown on another that took the other branch.
Nobody chose that. It is what happens when two expressions are written on one line and the rounding
factor is adjusted in each to make the arithmetic come out — 10000 and 100 in one, 1000 and
1000 in the other. The two branches were written to be the same idea and they are not the same
function.
The validator had the same bug
The part that actually taught me something was what happened next.
There is a validation script in this codebase whose whole job is to check that a tenant’s dashboard figures are sane before anyone demonstrates them. It runs a series of named checks and exits with a failure code if any of them fail. That is a good instinct and it is the right shape for the problem.
It contains the same five-line helper, copied verbatim.
So the validator would look at an organisation whose true compliance rate is one per cent, apply the same guess, see a hundred per cent, and pass. The check that exists to catch the bad number reproduces the bad number, in identical code, and reports that everything is fine.
A validator that shares an implementation with the thing it validates is not a check. It is a second copy. The value of an independent check comes entirely from its independence — a different expression of the same requirement, so that a single mistaken assumption cannot satisfy both. If the validator must reuse code, it should reuse the query, never the interpretation, and it should assert against a bound derived from the requirement rather than from the implementation.
The cheap version here would have been one line: assert that the stored value is between zero and a hundred, and fail loudly if it is not. That check does not need to know which unit was intended. It only needs to know that a compliance rate above a hundred, or a fraction stored where a percentage was expected, is a defect either way.
Why the heuristic looked reasonable
I want to be fair to whoever wrote it, because the reasoning is easy to reconstruct and it is the same reasoning I have used.
There was data in the table from more than one source — a seeding script, a backfill script, and an intended aggregation job. They were written at different times. Somebody found a value that did not render sensibly, wrote a defensive conversion so the dashboard would not show something absurd, and the dashboard stopped showing something absurd.
That is the trap. A defensive conversion at the read layer makes the symptom disappear and leaves the disagreement in place. The screen looked right afterwards, so nobody went back to ask which writer was wrong. The tolerance became the specification, and the specification is now a guess applied to every organisation on every page load.
The correct response to a value you cannot interpret is to refuse to display it. An empty tile with “figure unavailable” is honest. A tile showing a converted number is a claim, and in a compliance product a claim of a hundred per cent is the most expensive claim you can make.
The rules
Put the unit in the type. A percentage column gets a check constraint between 0 and 100. A fraction gets one between 0 and 1. A duration gets a name ending in the unit. If a reader has to guess, a writer was allowed to be ambiguous.
Never write a conversion whose condition is a value range. A condition like “if it is between zero and one” is inferring intent from data, and data will eventually be in the range that means the other thing.
Independent checks must be independently implemented. If your validation imports the function it is validating, delete the import and write the assertion from the requirement.
The limit I will admit: nothing about this was detectable from the screen. A dashboard showing a hundred per cent compliance looks like good news, and good news does not generate tickets. It was found by reading the code, and I do not have a way to find the next one of these other than reading the code.