FifeRouter

4 September 2026 · product-as-code process

Diagnostic codes are an API — retire, never renumber

A code in a build log is referenced by scripts, waivers, commit messages and someone's memory. Reusing one is worse than leaving a gap.

The product validator emits codes:

PAC-902  acceptance criterion's test ref does not resolve to a real test
PAC-904  shipped feature has no monitor probe
PAC-060  the generated bundle is stale

0xx06x belong to the library. 9xx are this repository's. And the rule is that a code is never reused for a different meaning.

Why a number is an interface

A code shows up in more places than its definition:

None of those are updated when you renumber. They all keep referring to the number, and the number now means something else — so a waiver written to accept one finding silently accepts a different one, and a script watching for a stale bundle starts watching for something unrelated.

That is the same failure as reusing an error code in an HTTP API, or a migration id, and it has the same shape: the identifier travelled further than the system that issued it.

Retiring is cheap

If a check goes away, its number goes with it. PAC-907 being absent is a gap in a list, which costs nothing and tells the truth — there used to be a check here and there is not any more.

Gaps look untidy. Untidiness is the correct appearance of a system with history. A dense sequence with no gaps means either nothing has ever been removed or somebody has been reusing numbers, and only one of those is good news.

The renumbering that did happen

There is a decision record about it: PDR-0003, when the product-as-code machinery moved from a copy in this repository to the FastPDLC library.

The library owns 0xx06x. This repo's codes had to move out of that range into 9xx, which renumbered every one of them.

The record exists to say why that was acceptable once: adopting a shared engine means adopting its namespace, and the alternative was a permanent collision between two sets of codes with the same numbers and different meanings. It also says why it does not happen again — the ranges are now disjoint, so there is no future event that forces it.

Writing that down converted a one-time exception into something that stays an exception. Without the record, the precedent is "we renumbered before", and the next person with a tidy-looking reason does it again.

Ranges as ownership

The split does something else useful: it says at a glance who owns a failure.

A 0xx is the library's rule — shared with KibiPay, which uses the same engine, and changing it means changing something two products depend on. A 9xx is ours, defined in product_hooks.py, and can change with a pull request here.

That is visible in the code itself. Reading PAC-903 you know immediately that "a shipped feature must name its deployment manifest" is a rule this repository chose, not one inherited — and therefore one you can argue with locally.

The general rule

Any identifier that appears in a log, a config file, or a commit message is an API, whether or not it was designed as one. Codes, migration names, feature flags, metric names, queue names.

The test is simple: could somebody have written this string down somewhere you cannot edit? If yes, changing what it means is a breaking change, and the fact that it is only a number does not make it less of one.


← All posts