Measured Behaviour

Does obfuscation break donor deferral periods?

A donor deferral is the only thing in a blood service that is not a fact about anybody. It is an absence of permission, and an absence has to be turned into something before software can hold it. Every donor system turns it into the same thing: a reason, a start date, and a period looked up by reason. That means two lookups stand between a deferred donor and the chair - and we measured what happens when each of them goes missing.

A prohibition has to be expressed as something

Ask a blood service what a deferral is and you get a clinical answer: this donor should not give blood yet, because of a tattoo, a trip to a malaria-endemic area, a low haemoglobin, a live vaccine, a dental extraction. Ask the software the same question and you get a different answer, because none of those is a thing a database row can be.

What the row actually holds is a REASON and a DATE. The number of days is somewhere else entirely, in a table keyed by reason, because the periods change: a rule review moves tattoos from six months to four, and a service that had written end dates onto twenty thousand donor records would have to rewrite all of them. So the end date is computed, every time, at the moment the donor walks up to the desk.

That is a completely reasonable design and it is the one nearly everybody uses. It also means the prohibition exists only as the result of an addition, and an addition has two operands. Lose either one and the deferral does not become an error. It becomes a different deferral.

What we built and what the correct run says

Fourteen donors presenting at one mobile session. Eight are eligible: some have never been deferred, and some are inside a deferral that has genuinely run its course - a dental extraction fifty days ago on a seven-day period, a live vaccine a hundred days ago on a twenty-eight-day period. Three are inside a temporary deferral that is still in force. One has a reason the period table does not carry, so the service's own ninety-day default applies to them and has not run. Two are permanently deferred. One is simply too soon since their last donation and one has a haemoglobin of 11.9 against a floor of 12.5.

The correct run bleeds eight and turns six away. Our price counter - donors bled while a deferral that exists to protect somebody was genuinely in force - reads zero, and so does the volume that entered the supply from one. Every donor is printed in the session list in every arm below. Nobody vanishes.

Two of the fourteen are permanent deferrals, and that ratio is not decoration. Permanent deferrals are always a small minority of a deferral register, which is exactly why a report that has gone wrong about them still reads plausibly: the rows a reviewer scans are overwhelmingly temporary ones, and those are fine.

The two operands fail in opposite directions

We removed the START DATE, the way a member-renaming pass removes a property name, and executed the result. The addition now has an unreadable left-hand side, so the end date is NaN, and every comparison against NaN is false. `today >= ends` is false, so the deferral never expires. BLED went from 8 to 4 and TURNED-AWAY from 6 to 10: four donors who were perfectly eligible were sent home.

That is a failure, and it is the good kind. Four people who gave up an evening are annoyed, they say so at the desk, the session supervisor rings somebody, and the problem is on a screen within a day. Nothing unsafe entered the supply - the price counter stayed at zero.

Then we removed the DEFAULT PERIOD instead - the single number the service applies to a reason its table does not carry. The library's own default for that key is zero, because a library asked to defer somebody for a reason it has never heard of cannot invent a length. A deferral of zero days expired the moment it began. BLED went from 8 to 9, and the price counter went from 0 to 1: 470 mL of blood entered the supply from a donor inside a deferral that had ninety days left to run. Nothing was refused, nobody complained, and no line of the session report says anything is wrong.

Same session, same donor register, same transformation. One key produced a loud, visible, harmless refusal. Another produced a silent acceptance. Which you get is not decided by how important the key sounds.

A permanent deferral is not a long one

The sharpest arm in this area is the one that shows why permanent deferrals are held separately. They are not a very large number of days, because forever is not a quantity. They are a LIST of reasons, checked before the period arithmetic runs at all.

A list the library cannot read is an empty list, and an empty list of permanent reasons says that no reason is permanent. Removing that one key sent both permanently deferred donors down the temporary path instead: their reasons are not in the period table either, so they picked up the ninety-day default, and both of their start dates are years old. BLED went from 8 to 10 and the price counter to 2 donors and 940 mL.

Removing the REASON FIELD on the donor record instead - one name on the row rather than a key on a configuration object - was worse than any option in the area. With no reason there is nothing to look up and nothing to match against the permanent list, so the code takes the branch it takes for a donor who has never been deferred at all. BLED went from 8 to 13 and the price counter to 5 donors and 2,350 mL. The library reports that thirteen people gave blood, which is true.

And the union of the two - both the permanent list and the reason field gone - is byte-identical to losing the reason field alone. That is worth sitting with. A pattern that reaches BOTH halves is not worse than the pattern that reaches the second half, because the first check needs the field the second one lost. Testing only the wide pattern would have shown you a number that the narrow one produces on its own.

What the loud arms tell you, and what they do not

Two arms in this area fail so loudly that nobody could miss them. Removing the last-donation date drove BLED to 0 - the interval test compares against NaN, the comparison is false, and every donor in the room is refused. Removing the haemoglobin field made the library refuse the entire session before any bleed, because it requires that value to be finite before comparing it.

That refusal is worth naming, because it is the standard mitigation for this whole class of problem and it works exactly as advertised here: require a number to be finite before you compare it, and treat a non-finite one as a refusal. It costs one line, it is worth having whether or not anything is ever obfuscated - a hand-edited configuration file and a dropped key over a serialisation hop produce the same value - and in this area it converted a silent wrong answer into a loud stop.

It also did not protect a single one of the arms above. The period, the permanent list and the reason are not numbers entering arithmetic. They are the things that decide WHICH arithmetic runs.

What this means if you protect a donor system

Nothing here is an argument against protecting the bundle. This is a JavaScript protection vendor writing it, and our position has not moved: name-level transformations are a real control against casual reading and cheap cloning, and they are worth using. The point is narrower and it is about how you configure them.

Renaming is driven by a pattern, and a pattern is a claim about which names in your bundle are yours to move, and member renaming is where you make that claim. `deferredFrom`, `deferralReason` and the keys on a deferral policy object are not yours in the sense that matters, because an installed library reads them and a library reads names literally. Put the options object and the record shapes outside the rename pattern, or supply an explicit member mapping rather than a broad regular expression.

Then test the halves. The most useful single finding in this area is that the wide pattern was quieter than one of its narrow halves - so a matrix that only exercises the realistic, broad, rename-the-whole-object case would never have shown you the arm where 470 mL entered the supply.

And test the direction of an outcome, not just whether the run completed. Six of the arms above finished cleanly, printed a full session report, and passed the service's own consistency check, which asks whether every donor was decided one way or the other. Every donor was.

Frequently asked questions

How does obfuscation affect a donor deferral calculation?

It affects it through names, not logic. A deferral end date is computed from a period looked up by reason plus the date the reason started, and if a renaming pattern reaches either of those names an installed library reads a name it does not recognise. In our measurements the period half and the start-date half moved the session in opposite directions.

What happens when the deferral start date cannot be read?

The addition produces NaN and every comparison against NaN is false, so the deferral never expires. In our fixture that took the session from eight donors bled to four, and turned away four people who were genuinely eligible. It is a loud failure that somebody notices the same evening.

What happens when the deferral period table cannot be read?

The library substitutes its own default for that key, which is zero, because it cannot invent a length for a reason it has never seen. A deferral of zero days expired the day it began. In our fixture that bled one donor who had ninety days left to run and put 470 mL into the supply, with nothing on the report to say so.

Why are permanent deferrals more fragile than temporary ones?

Because they are a different kind of record rather than a longer number. They live in a list of reasons that is consulted before the period arithmetic runs, and a list that cannot be read is an empty list. Removing that one key in our fixture sent both permanently deferred donors down the temporary path and bled both of them.

How much of a deferral register does this reach?

The proportion that is unusual. Permanent deferrals and reasons outside the standard table are always a small minority, so a reviewer scanning a session report is looking almost entirely at rows that are still correct. In our fourteen-donor session the two permanent deferrals were the two rows that mattered.

Does requiring numbers to be finite fix this?

It fixes part of it and it is worth doing regardless, because a hand-edited configuration file produces the same value. In our fixture it converted the missing-haemoglobin arm into a loud refusal of the whole session. It did nothing for the period, the permanent list or the reason field, because none of those is a number entering arithmetic.

What should a blood service do before protecting its bundle?

Keep the options object and the record shapes an installed library reads outside the rename pattern, or supply an explicit member mapping instead of a broad regular expression. Then run the eligibility engine against a session that contains a deferral in force, not one where everybody is eligible, and check the decisions rather than whether the run finished.

Related reading