Measured, not asserted
Published
An eligibility criterion is written as data: an identifier, the name of the field it reads, a comparison and a limit. Renaming rewrites names in code. It does not rewrite the text a rule is written in. So the criteria list stays complete, the engine walks all of it, and the count of criteria evaluated is correct in every arm below -- while a criterion pointed at a field that no longer answers to that name compares against nothing, does not fire, and is therefore a criterion the subject passes.
Eight criteria, all evaluated, one of them applied to nothing
The fixture is a cardiotoxicity study: three inclusion criteria (age, performance status, renal function) and five exclusion criteria (pregnancy, severe renal impairment, reduced ejection fraction, prior anthracycline, prolonged QTc), plus a twenty-eight-day washout. Six candidates are screened. Two qualify. Four are screen failures, each for a specific and correct reason.
The screening engine is copied in unprotected, the way an installed dependency would be. It reports what it did: 8/8 criteria evaluated for every candidate. That number is right in every arm measured in this article, including the ones that enrol people who should never have been enrolled. The engine really did walk all eight entries, find all eight, and apply all eight.
What it cannot report is that some of them were applied to a property that was not there. A criterion whose field cannot be found is not an error. The value is absent, every comparison against an absent value is false, and a criterion that does not fire is a criterion the subject passes.
Protection on its own moved nothing
Before any renaming, all five areas of this pass were run through five presets -- the ES5 default path, the modern target, the emit gate's own option set on both targets, and the string transforms. Twenty-five comparisons, every one byte-identical to the unprotected run.
So nothing below is caused by protecting the code. Every result required member renaming aimed at a property name that both the protocol's own records and the installed screening engine read.
The same transformation, opposite directions, decided by criterion polarity
Renaming the ejection-fraction field enrols S-102, whose ejection fraction is 38% against an exclusion threshold of 45%, into a study of a drug that is cardiotoxic. The output reads S-102=ENROL[8/8 criteria evaluated ...]. No criterion is reported as skipped, no field is reported as missing, and the protocol's own completeness rule passes with all 8 criteria evaluated for each.
Renaming the renal-function field instead produces the opposite: nobody is enrolled. Every candidate screen-fails, loudly, and somebody notices within an hour.
The only difference between those two fields is which polarity of criterion happens to name them. Ejection fraction is named by an exclusion criterion alone, so losing it removes a reason to refuse. Renal function is named by an inclusion criterion as well, so losing it also removes a reason to accept -- and an inclusion criterion that cannot be met is a screen failure.
That is not a property of the field's importance. Both are clinical safety criteria. It is a property of how the protocol happened to be written, and a protocol author choosing between 'eGFR at least 60' and 'exclude if eGFR under 60' is making a stylistic choice with no idea that it decides the failure direction.
Three exclusion-only fields, three enrolments
The pattern reproduces across the file. Renaming the QTc field enrols S-103, whose QTc is 502 ms against a 480 ms limit. Renaming the prior-anthracycline field enrols S-105. Rename all four exclusion-only fields together and five of the six candidates are enrolled, with only the washout failure surviving -- and 8/8 criteria evaluated printed against every one of them.
One arm did not reproduce it, and it is instructive. Renaming the pregnancy field leaves S-106 a screen failure anyway, because S-106 also fails the washout. A second, independent reason to refuse covered for the lost one. That is masking, and it means a spot check that happens to land on a doubly-disqualified candidate proves nothing about the singly-disqualified ones.
The option route announces itself; the record route does not
There is a second way to lose an exclusion criterion: rename the option carrying the list of criterion identifiers rather than the field a criterion names. It reaches a similar place -- five of six enrolled -- but it leaves a visible trace. The coverage line drops from 8/8 to 3/3, because the engine genuinely has fewer criteria to walk.
The record-field route keeps 8/8. Same enrolments, same subjects, same clinical consequence, and one of the two routes puts a number on the screen that a careful reviewer could catch.
This is worth carrying forward as a reading rule. When a configuration key is lost, the library usually knows something is missing and its own reporting reflects that. When a record field is lost, nothing is missing from the library's point of view -- the record is simply a record with different properties on it -- and the reporting is silent by construction.
What failed loudly
The criteria table's own shape failed closed. Renaming the field name, the comparison operator or the limit -- the three columns a criterion is written in -- made every criterion unusable and screen-failed all six candidates. Renaming the table wholesale did the same.
The washout dates failed closed too: losing either the prior-therapy day or the screening day makes the washout arithmetic unreadable and refuses everybody.
The contrast the series keeps measuring holds again. Losing a value fails closed and loud. Losing the meaning of a value -- which criterion applies to which field -- fails open and quiet.
Why the obvious guard does not help
The protocol's completeness rule in this fixture is a genuinely good rule: it refuses to enrol anybody unless every criterion in the table was evaluated for them. It is exactly the guard a thoughtful team writes after a near miss.
It passes in every silent arm here, and it is telling the truth when it does. All eight criteria were evaluated. The rule counts criteria; the failure is in what one criterion was pointed at, and there is no count of that.
A rule that would catch it looks different: require every criterion to have seen a value, and treat an absent field as a refusal rather than a pass. That is a one-line change to the comparison and it is worth making regardless of obfuscation -- a mistyped field name in a protocol amendment, a schema change in the EDC extract, or a dropped key over a serialisation hop all produce the same absent value. Teams building screening tools under the EU AI Act's expectations for automated decision systems will recognise the shape: the system must be able to say not only that it applied a rule, but that the rule had something to apply itself to.
What to do about it
Make an absent field an error, never a pass. This is the single change that closes every silent arm in this article. A criterion that cannot read its input has not been satisfied; it has not been evaluated.
Test with a candidate who fails exactly one exclusion criterion, per criterion. The masking arm shows why: a candidate who fails two criteria will screen-fail whichever one you break, and will pass your test while proving nothing.
And read the coverage count as evidence about the configuration, never about the data. It answered honestly in every arm here, including the ones that enrolled people who should have been refused.
Frequently asked questions
Did obfuscation change any eligibility determination on its own?
No. Five areas ran through five presets with every protected output byte-identical to the unprotected run. Every failure here required member renaming aimed at a name the installed screening engine also reads.
What happened when the ejection-fraction field was renamed?
A candidate with a 38% ejection fraction was enrolled into a cardiotoxic study, against an exclusion threshold of 45%. The output reported all eight criteria as evaluated and the protocol's completeness check passed.
Why did renaming the renal-function field fail loudly instead?
Because an inclusion criterion names that field as well as an exclusion criterion. Losing it removes a reason to accept as well as a reason to refuse, and an inclusion criterion that cannot be met is a screen failure. Everybody was refused.
So the failure direction depends on how the protocol was written?
Yes. 'eGFR at least 60' and 'exclude if eGFR under 60' express the same clinical intent and fail in opposite directions. The protocol author making that choice has no way to know it decides anything.
Did the criteria count give any warning?
Not on the record-field route. It read 8/8 in every silent arm. Renaming the option that carries the criteria list did drop the count to 3/3 -- so the configuration route announces itself and the data route does not.
Did any arm get masked?
Yes. Renaming the pregnancy field changed nothing, because that candidate also failed the washout. A second independent reason to refuse covered for the lost one, which is why per-criterion test candidates matter.
What is the cheapest fix?
Treat a criterion whose field is absent as a refusal rather than a pass. That closes every silent arm here, and it also protects you from a mistyped field name in a protocol amendment or a dropped key in an EDC extract.
Related reading