A number your system can emit without declaring it first is a number nobody can audit later.
We run into this a lot in analytics and reporting work. A metric gets added in a hurry, the name is invented at the call site, the unit lives in someone's head, and six months later a dashboard shows a value nobody can explain. The code is correct. The output is unverifiable.
In our open-source text analysis project proseprobe, we made the registry the only door.
There is one table of declarations. Each entry is a frozen record holding the name, the unit, and a description of what the number means. Nothing else may declare a measurement.
When the extractor emits a reading, it goes through a single helper, and that helper looks the name up in the declaration index before it is allowed to construct anything. A name that is not declared does not produce an undocumented row. It fails. Every emitted value inherits its unit and description from the declaration, not from whoever wrote the line that computed it.
We care about that inversion more than anything else in the design. Metadata belongs to the schema, not to the caller.
Two decisions around it that we think generalise:
Every value carries evidence, a small tuple of collapsed excerpts from the source text, so a number points back at the prose it was read from. One ratio also carries a detail map with per-item rates per 1000 words. A metric you can click back into is a metric people trust.
Thresholds are named constants, not inline literals. The short-sentence and long-sentence boundaries have names. The expression that strips list and heading markers before punctuation is counted has a name. When someone argues about a result, the argument lands on a named decision instead of a magic number buried in a loop.
The last choice was the one we debated most: division is guarded, not refused. The per-100-words, per-word, per-sentence and per-line helpers return zero on an empty denominator. So empty input still yields the complete set of declared names, all at zero.
That is deliberate. A consumer should not have to handle a shrinking result shape. The schema is fixed by the declaration table, not by whether the input happened to contain data, so downstream code can assume the keys exist and stop writing defensive lookups.
Building this way costs almost nothing at the start, and retrofitting it hurts once dashboards, exports and client reports already depend on the loose version.
If you run a metrics pipeline, it is worth checking where your units actually live: in a schema, in a column name, or in a wiki page someone stopped updating.