The Right Rule, Wrongly Wired
A correct AND became a broken feature, got 'fixed' into an OR, and months later became the correct AND again — the difference was which side of the data got repaired.
Or: when a predicate and its data drift apart, don’t teach the reader to forgive — teach the writer to stop lying.
Here’s a three-act story about one boolean and one date, and how the same line of code was correct in May, a bug by June, “fixed” in July, and correct again in August — without the business rule ever changing.
Act one: the AND that starved
The feature is an authorization: a supervisor can permit a time-sensitive action, valid until a deadline. Two fields on the record: a flag (permission granted) and a date (granted until when). The eligibility check, as originally written:
authorized = flag AND (no deadline OR deadline >= today)
That’s the correct rule. The flag is the master switch; the date scopes it. Everyone who read it nodded.
It never worked. Not once, for anyone.
Because the UI that granted the authorization wrote only the date. Nothing in the entire system ever set the flag. The predicate demanded two fields; the write path supplied one. Every grant was born dead — supervisor sets the deadline, system stays blind, the very state the grant was supposed to unlock re-asserts itself on the next page load and undoes any manual repair. A perfect catch-22, made of one unwritten boolean.
💡 A predicate is only as correct as the write path that feeds it. An invariant nobody writes is indistinguishable from a bug.
Act two: the OR that accommodated
The July fix did what deadline-pressure fixes do: it went where the pain was visible. The read side. The predicate became:
authorized = (deadline present AND deadline >= today) OR flag
Date-first, flag demoted to “legacy fallback.” And it worked — grants granted, deadlines expired, the catch-22 dissolved. Tests went green. Everyone moved on.
But look at what actually happened: the reader was taught to accept what the broken writer produced. The drift between rule and data wasn’t repaired — it was ratified. And the OR carried a landmine: if anything ever did set that flag, the grant would become immortal. Past deadline? Doesn’t matter — the OR’s second arm says yes forever. The fix worked precisely because the write-path bug it accommodated was total. Partial repair of the writer would have detonated the reader.
I want to be honest: at the time, the OR looked like the principled choice. The flag was unset everywhere in the database — zero rows, measured — so date-primary matched observable reality. That’s the seduction of read-side fixes. They always match observable reality. That’s what accommodation means.
Act three: the AND, welded
Months later the business owner stated the intended semantics, unprompted, in one sentence: the flag means they’re permitted until the date specified; if the flag is off, the date is ignored. The original AND. The May rule. He was describing the predicate that had “failed” — because it never failed as a rule; it failed as a wiring.
So the August fix repaired what May actually got wrong:
- The predicate went back to AND — flag is the master switch, date scopes it.
- The write path got welded: the single choke point that persists the deadline now sets the flag in the same statement — writing a deadline arms the flag, clearing it disarms. The two fields can no longer disagree, because no code path can write one without the other.
- One data true-up: the forty-six records granted under the old regime got their flag armed, once, with a backup and a read-back count of zero stragglers.
And the landmine defused itself: under AND, flag-plus-expired-date is just an expired grant — a truthful “was permitted, no longer is.” The record that would have been immortal under OR became the most honest row in the table.
The general shape
When a rule and its data drift, you get to choose which side to fix, and the choice compounds:
- Fixing the reader (weakening the predicate to accept what’s there) works immediately, matches the observed data, and preserves the drift forever. Every future writer inherits the license to write half the truth. Every future reader inherits the obligation to forgive it.
- Fixing the writer (welding the fields so the invariant can’t be violated) costs a data migration for the historical rows — once — and then the predicate can be as strict as the rule actually is, forever.
The read-side fix is a loan. The write-side fix is the payment.
💡 Don’t teach the reader to forgive the writer. Teach the writer to stop producing things that need forgiveness — then true-up the past exactly once.
The part I keep chewing on: the May engineers wrote the right rule and shipped a broken feature. The July engineers shipped a working feature and encoded the wrong rule. Both were one honest conversation away from the August version — which took about an hour, most of it spent on the forty-six-row true-up. The expensive ingredient was never the code. It was someone finally saying, out loud, what the flag means.