Clinical safety
Published
No, not on its own. We protected a weight-based dosing calculator on five presets and every dose came out identical. Then we renamed one option key, and a four-year-old's order went from 15 mg to 33 mg. Nothing threw, nothing was refused, and every safety check in the file reported normal.
The one result from this pass worth reading twice
Most of what goes wrong when a rename escapes its scope is a value that goes missing. A field becomes undefined, arithmetic on it produces NaN, and every comparison against NaN is false. That failure is well understood by now, and there is a one-line mitigation for it: require the number to be finite before you compare it or act on it.
We built that mitigation into the dosing library for this article, on purpose, so we could measure what it protects against. It works. It also does nothing whatsoever about the failure below.
The calculator takes a recorded weight and a milligrams-per-kilogram regimen. This hospital's chart records weight in pounds, which the library needs to be told, because its own default is kilograms. That instruction is one option key.
Rename that key and the number survives intact. It is finite. It is plausible. It is in the wrong unit.
The measured numbers
Here is the unprotected run and the renamed run side by side, for three patients on a one milligram per kilogram regimen.
A 154 lb adult is 69.85 kg, so the order is 70 mg. With the unit key renamed, the library read 154 as kilograms and computed 154 mg. The single-dose cap of 150 mg trimmed that to 150. The cap did not prevent the overdose; it rounded it down.
A 33 lb four-year-old is 14.97 kg, so the order is 15 mg. With the key renamed, the library read 33 kg and ordered 33 mg. No cap is anywhere near that number, no plausibility limit is troubled by it, and the finite check has nothing to say about it. The output line still reads DOSED=true.
A 200 lb patient with a creatinine clearance of 22 gets a halved dose of 45 mg. Renamed, that became 100 mg. The renal adjustment ran correctly the whole time and halved the wrong number.
Compare that with losing the number itself
One field away in the same object is the weight value. We renamed that too, as a contrast.
Every order was refused: non-finite-dose(refused before administration). The dosing screen stops working, the ward calls somebody within minutes, and nobody is harmed.
So on one axis, in one file, one field apart: losing the number fails closed and loudly, and losing its unit fails open and silently. The mitigation that catches the first is structurally blind to the second, because a number in the wrong unit is a perfectly good number.
That is the finding this pass was built to test, and it is not really about obfuscation. A configuration file edited by hand, a serialisation hop that drops an unrecognised key, a library upgrade that renames an option, and a hand-written test double all produce the same thing.
Why a unit behaves differently from a threshold
It is worth being precise about the mechanism, because it decides what you should test.
An option key handed to an installed library never becomes unreadable in the way a record field does. The library looks the key up, does not find it, and substitutes its own default. The value that reaches the arithmetic is present, finite and entirely reasonable. Nothing anywhere reports a missing value.
For a threshold, that substitution matters because of where the vendor's default points. For a unit, it matters because of what the vendor's default means. Every downstream guard keeps working perfectly, on a quantity that now denotes something else. The cap still caps. The plausibility limit still refuses implausible weights. They are all measuring the right thing about the wrong number.
The severity is the conversion ratio, and it is set by the vendor, not by you. Pounds to kilograms is 2.2. Milligrams to micrograms is a thousand.
The guards that did behave as expected
For completeness, the rest of the file behaved the way the previous work in this series predicts, which is the point of measuring it rather than assuming.
The single-dose cap is a ceiling whose true branch trims the dose. Renaming the field it reads skipped the trim, and the 385 lb patient's order went from 150 mg to 175 mg. Modest, and in the direction you would expect.
The renal adjustment is a callback that returns a number rather than a flag. Renaming that number produced NaN, which the finite check caught and refused. Loud again.
Renaming the enabling flag in front of the renal check, or the callback itself, both produced the same thing: the adjustment silently stopped happening and the impaired patient received the full dose. The union of the two was identical to the flag alone, because the flag is consulted first. A strict control behind a flag is only as loud as the flag.
What to do about it
Scope the rename so that it cannot reach option keys you hand to an installed library. That is the single control that removes this entire class, and member renaming is opt-in and pattern-driven precisely so you can do that.
Beyond obfuscation, there is a design change worth making regardless. Do not pass a bare number and its unit as two separate configuration items that can drift apart. Pass a quantity that carries its own unit, or normalise at the boundary where the value enters your system and let everything downstream speak one unit. A unit that lives in a different key from the number it describes is a unit that can be lost without the number noticing.
And keep the finite check. It is one line, it is worth having, and this article is a measurement of exactly where its edge is rather than an argument against it.
Frequently asked questions
Does obfuscating a dosing calculator change the dose it produces?
Not by itself. We protected the calculator on five presets covering the ES5 target, the modern target and the string transforms, and every dose matched the unprotected run exactly. Twenty-five comparisons, twenty-five identical results. The dose changed only when member renaming was allowed to reach an option key the installed library reads.
What exactly went wrong with the unit?
The chart records weight in pounds and the library defaults to kilograms, so the caller has to say so with an option key. Renaming that key meant the library did not recognise it, substituted its own default of kilograms, and interpreted every recorded weight as though it were already metric. A 33 lb child was dosed as a 33 kg child.
Would a maximum dose check have caught it?
Only partially, and only for large patients. The adult in our measurement was trimmed from 154 mg to the 150 mg cap, which is still more than double the intended 70 mg. The four-year-old's 33 mg was nowhere near any cap, so no ceiling in the file was ever consulted. Caps protect against extreme values, not against a systematic factor of 2.2.
Does requiring the number to be finite help?
It helps a great deal against a different failure, and not at all against this one. We built that check into the library deliberately. When we renamed the weight value itself, the check refused every order loudly. When we renamed the unit, the number stayed finite and plausible and the check had nothing to object to.
Why is losing a unit quieter than losing a value?
Because a missing value propagates. It becomes undefined, then NaN, and NaN is visible to any check that looks for it. A wrong unit produces an ordinary number that survives arithmetic, formatting, serialisation and every threshold comparison, and reaches a label or a pump looking exactly like a correct answer.
How should units be represented so this cannot happen?
Keep the unit with the number rather than in a separate configuration key, or normalise every quantity to one canonical unit at the boundary where it enters the system. The failure needs the unit and the value to be separable, so removing that separation removes the failure, whatever caused the key to go missing.
Is it safe to obfuscate clinical calculation code at all?
The measurement says the transform itself is behaviour-preserving here, which is the question most teams are actually asking. What needs care is the scope of member renaming, because that is a rewrite of names rather than a transformation of structure. Restrict the pattern to names your own code invented and keep any calculation you cannot afford to have altered on the server.
Related reading