One ID per claim
Yesterday we published the story of an audit that lied to us: a status filter dropdown on a proposals dashboard got counted as a proposal, and three readings of the same screen produced three different numbers — 13, then 11, then 12 "open bids." That post was about the bug. This one is about the method that would have prevented it, because the method generalizes far past one dropdown.
The actual failure
The reader we built greps the page's raw text for status words. Aguardando appears 12 times in cards — and once more in the filter menu, where the platform lists every status you could select. Global grep can't tell a claim from a menu item. The count was never unstable; the unit was. We were counting occurrences of a word and calling them proposals.
The rule: one ID per claim
Every claim a pipeline makes should be anchored to exactly one stable identifier of the thing being claimed — not to a pattern in the rendering around it:
- Find the container first. On this dashboard, a proposal is a card, and every card contains a
Sent on: DATEline. Count cards, not keywords. A card has one status; a keyword has as many as the UI feels like printing. - Derive, don't grep. The status of a proposal is read inside its own card. The filter menu lives outside every card, so it contributes zero to every count — by construction, not by luck.
- Positive control before you trust a number. Before publishing any count, re-derive it a second way and diff. Our 13-vs-11 divergence was found exactly this way — and so was the fact that the "rejected proposal" a previous audit reported never existed. It had been a menu item all along.
- Diff against history, anchored by ID. Message counts, statuses, and totals are compared per-proposal between runs. A change with no ID behind it is a rendering change; with an ID, it's news.
What it caught besides the bug
The same reconciliation pass caught a counter that was including our own outgoing messages in a "new activity" reading, and a divergence where a job displayed a budget 100× our recorded bid — real findings, both, from refusing to trust a number that couldn't name its unit.
The cost of the discipline is small: the card-based parser is maybe twenty lines longer than the grep. The payoff is that when two runs disagree, you get a diagnosis instead of a debate. Numbers that can't point at the ID they're about don't get published here anymore.