Measured, not asserted
Published
A randomisation list has to do two things at once, and they pull against each other. It has to be balanced, so the arms are comparable. And it has to be unpredictable, so nobody at the site can work out the next assignment and time an enrolment to land on the arm they want. Permuted blocks of mixed size buy the second at a small cost in the first. This test renamed the option carrying the block sizes. Every subject was still assigned exactly once, the arms came out exactly even, and half the list became derivable from the half before it.
A property that no reconciliation is defined over
Everything a monitor checks about a randomisation list is a property of the assignments. Is every subject assigned? Is each assigned once? Are the arms within their ratio? Is each stratum balanced? Those are all answerable from the list, and this test's fixture answers all of them correctly in every arm measured.
Unpredictability is not a property of the assignments. It is a property of the process that produced them, and specifically of how much of the sequence an observer can infer from the part they have already seen. A block of two is perfectly balanced and perfectly guessable: the second slot is always whatever the first was not. Blocks of four, six and eight drawn in an unknown order are less tidy and far harder to read.
So the two properties are not merely independent -- they trade against each other, and every metric on the reconciliation report is on the balance side of that trade. There is no line on the report that gets worse when concealment is lost. That is what makes this shape different from the seventeen this series has measured before it.
The fixture is a twenty-four-subject trial across three sites and two disease stages: six strata, one-to-one allocation, permuted blocks of four, six and eight, and a seeded list so the fixture reproduces itself. The correct run assigns thirteen subjects to drug and eleven to placebo, the worst stratum is two subjects out of balance, and four of the twenty-four allocations -- 16.7% -- are forced by the block already open.
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, under a guard that runs each original twice and refuses to compare anything against a fixture that will not reproduce itself.
So nothing below is caused by protecting the code. Every result in this article required member renaming aimed at a property name that both the protocol's own code and the installed randomisation engine read. That is a configuration question, not a verdict on protection.
The failing arm reads better than the correct one
Renaming the option carrying the block sizes reverts it to the vendor's default, which is the smallest block the library supports. The library cannot know your protocol; a small block is the safe choice for balance, and balance is the thing a randomisation library is most obviously judged on.
The result: forced-allocations=10/24 (41.7% of the list is derivable from the list so far), against 4/24 on the correct list. And the balance figures all improved. The worst stratum went from two subjects out of balance to one. The protocol's own allocation check, which is a good check, passed and printed worst stratum balanced within 1 where the correct run prints within 2.
Rename the stratification option alongside it and the trial collapses to a single stratum of blocks of two. Now forced-allocations=12/24 (50.0%) -- half the sequence readable off the other half -- and the balance is perfect: twelve and twelve exactly, worst running gap of one, worst stratum out of balance by zero, and the protocol's check printing worst stratum balanced within 0.
That is the finding, and it is worth stating without softening. On every number a data monitor would look at, the broken list is the better list. There is no reading of the reconciliation report under which the correct run wins.
The concealment claim survives, printed and false
The configuration screen in the block-size arm still reads concealment=concealed(the next assignment is not derivable from the ones issued). It still reads stratified-by=site+stage. Both lines are produced by the engine, both are what the protocol asked for, and one of them is now a straightforwardly false statement about the list sitting underneath it.
This is the pattern the series has hit repeatedly: asking the system whether a control is configured returns a true answer to the wrong question. Concealment is configured. The option is present, the flag is set, the engine reports it. What changed is the block structure that made concealment mean anything.
There is a sharper version one field away. The stratification keys are named in the options as text -- the strings 'site' and 'stage' -- and looked up on each subject record by that name. Rename the record fields instead of the option and the strata collapse from six to three while the configuration screen goes on reading stratified-by=site+stage. Renaming rewrites names in code; it does not rewrite the text a rule is written in.
A perfectly good list that is not the one you filed
Renaming the seed produced a result that is easy to skim past: the balance figures are unchanged, the predictability is unchanged, the protocol's check passes with the same wording. Only the list identifier moved, from 474b651 to ecebf571.
That is a valid randomisation list. It is simply not the list the statistician generated, filed with the trial master file, and printed the drug-kit labels from. Every kit at every site is labelled by the filed list. If the system issuing assignments is working from a different one, subjects receive the treatment the other list says, and nothing about the assignments themselves looks wrong.
The measurement lesson is that an arm which moves only an identifier is not automatically cosmetic. Ask what else in the world is keyed by that identifier.
What failed loudly
The plain quantities failed closed. Renaming the enrolment day produced non-finite-enrolment-day(refused before randomisation) -- nothing was assigned at all. Renaming the subject identifier left every consent-lag line reading undefined, which nobody ships past a first run.
Renaming the return shape of the protocol's own allocation rule -- the ok and how fields it hands back -- produced OK=undefined allocation-rejected(undefined). The rule ran, the rule approved, and the engine could not read the approval, so it refused. That is the safe direction, and it is the direction a permission-shaped flag fails in: an unestablished permission reads as no permission.
Set against those, the block-size arm is silent. Nothing throws, nothing is undefined, nothing is missing, and every printed figure is not merely plausible but better than the truth.
Why this one is hard to test for
Write the obvious assertion and it passes. Assert that twenty-four subjects were assigned: true. Assert that each was assigned once: true. Assert the arms are within one of each other: truer in the broken arm than the correct one. Assert that every stratum is represented: true.
The assertion that fails is one almost nobody writes, because it is a property of the generator rather than of the output: assert that no more than a stated fraction of the list is forced by the block already open, and assert the list identifier against the one on file. Both of those fail in the arms above, and the second costs one line.
If your randomisation runs in a browser -- an interactive-response front end, a site-facing enrolment screen -- then the script serving it is a control in the regulatory sense, and the software-of-record discipline that medical-device work already demands is the right frame: know which build is live, and test the behaviour rather than the configuration.
What to do about it
Do not let the balance report stand in for the whole answer. It is the part of a randomisation list that reconciles, and reconciling is exactly why it cannot tell you about the part that does not.
Pin the block structure in a test, not just in configuration. One run of a known seed against a known expected sequence catches every arm in this article, including the two that improve the balance figures.
And check the list identifier wherever the list crosses a boundary -- into kit labelling, into the interactive-response system, into the statistician's file. A list that is internally perfect and externally wrong is the failure mode with no local symptom at all.
Frequently asked questions
Did obfuscation change any randomisation on its own?
No. Five areas were run through five presets and every protected output matched the unprotected run -- twenty-five comparisons, no differences. Every failure in this article required member renaming aimed at a property name the installed randomisation engine also reads.
What actually changed when the block sizes were renamed?
The option reverted to the vendor's smallest block. Every subject was still assigned exactly once and the arms were still balanced -- better balanced, in fact -- but the share of the sequence derivable from the part already issued went from 16.7% to 41.7%, and to 50.0% once the stratification option was renamed too.
Why does the balance figure improve when something breaks?
Because a small block is what you would choose if balance were the only thing you cared about. Balance and unpredictability trade against each other, and the vendor's default sits at the balance end of that trade. Every metric on a reconciliation report is on the balance side.
Did the protocol's own allocation check catch it?
No. It passed, and it printed a better number than it prints on the correct list -- 'worst stratum balanced within 0' against 'within 2'. It is a good check. It is a check of the wrong property.
Does the system still say allocation is concealed?
Yes. The configuration line still reads 'concealed(the next assignment is not derivable from the ones issued)' while half the list is derivable. Asking whether concealment is configured returns a true answer to the wrong question.
What happened when the seed was renamed?
Nothing visible. Same balance, same predictability, same wording from the check -- and a different list identifier. It is a perfectly good randomisation list that is not the one filed with the trial master file or used to label the drug kits.
What is the cheapest test that would have caught this?
Run one known seed and assert the resulting sequence, plus the list identifier, against what the statistician filed. That fails in every arm here. A test that asserts the arms are balanced passes in all of them.
Related reading