Measured, not asserted

Does obfuscation break visit windows and deviations?

A visit window is a tolerance, and a tolerance a library cannot read is not a tolerance of zero. It is no tolerance at all -- the permissive reading is the only default a vendor can ship, because refusing every visit would make the library unusable on its first run. So an unreadable window does not produce a flood of deviations that somebody investigates. It produces a clean deviation log on a trial that has three. What makes this file worth reading is that neither of the two options carrying the windows does anything on its own.

Fifteen visits, three deviations, one major

Three subjects, five scheduled visits each, anchored on the randomisation day. The schedule is day 0, 28, 56, 84 and 112, with windows of nought, three, three, five and seven days. Three visits are genuinely out of window: one nineteen days late, which crosses the fourteen-day threshold and is a major deviation; one eight days late against a five-day window; and one nine days early against a seven-day window.

The correct output is in-window=12 minor-deviations=2 MAJOR-DEVIATIONS=1 reportable=3, with the three attributed one to each site. Everything below is scored against that.

Protection on its own moved nothing

All five areas ran through five presets before any renaming -- ES5, modern, the emit gate's option set on both targets, and the string transforms. Twenty-five comparisons, every one byte-identical to the unprotected run.

Nothing below is caused by protecting the code. Every result required member renaming aimed at a name that both the protocol's records and the installed monitoring engine read.

Both halves are inert, and the union is not

The protocol expresses its tolerance twice: a per-visit table of windows, and a default window for any visit the table does not list. Renaming the per-visit table produced output byte-identical to correct. Renaming the default window produced output byte-identical to correct.

A reviewer probing this file one option at a time would tick both off as safe, and both measurements would be true.

Renaming the two together produced in-window=15 minor-deviations=0 MAJOR-DEVIATIONS=0 reportable=0 by-site=[<none>]. Every visit in window. Nothing reportable. The nineteen-day major deviation gone.

The mechanism is simple once seen. Lose the table alone and every visit falls back to the protocol's own three-day default, which happens to be close enough to the listed windows that the same three visits are still out. Lose the default alone and nothing changes, because every visit is listed in the table. Lose both and the fallback is the library's default, which is unbounded -- and a tolerance of infinity contains everything.

This series has measured pairs where one half was inert before. It has not previously measured a pair where both halves are inert and the union erases the answer. It means a clean result on each half individually is not evidence about the pair, and the dangerous cell is not at the wide end of a rename pattern or at the narrow end. It is at a specific combination.

A downgrade that keeps the count

One arm deserves separating out. Renaming the threshold at which a deviation becomes major reverts it to the library's default of never. The output reads minor-deviations=3 MAJOR-DEVIATIONS=0 reportable=3.

The count of reportable deviations is unchanged. All three are still found, still listed, still attributed to the right sites and the right visits. What moved is the severity of one of them: a visit that arrived nineteen days late, which most protocols would escalate, is now filed alongside a two-day drift.

Escalation paths key off that classification. A major deviation typically triggers a sponsor notification, sometimes an IRB report, sometimes a subject's exclusion from the per-protocol set. None of that fires, and the deviation log still shows the right number of entries.

What failed loudly

The dates failed closed. Renaming the actual visit day or the randomisation day the schedule is anchored to made visits unclassifiable, and the sponsor's rule refused the whole review by name: deviation-check(V-0401 could not be classified: actualDay unreadable).

Renaming the subject identifier attached every visit to one subject and invented nine major deviations. Loud, wrong, and investigated immediately.

Renaming the site identifier left all three deviations correct and reported them as by-site=[undefined=3]. The finding survives; the attribution that decides which site gets a monitoring visit does not.

The arm that measured clean, and the experiment that broke it

One arm in this file measured identical to correct on both presets: renaming the three fields the library uses to hand back a classification -- the kind, the drift and the note. Those names are written by the installed engine and read by the sponsor's own rule, so renaming moves the read and leaves the write alone. It should have broken something.

It measured clean because in the base fixture every visit is classifiable, so the branch that reads the classification never has anything to catch. An arm can measure inert for a reason that has nothing to do with whether it is safe, which is why this series treats an inert result as a question rather than an answer.

So a second fixture was built to run the operation those names govern: the same review over an extract containing one visit the site marked as performed but never keyed a date for -- the commonest data-management query on any trial. Two arms, identical in every respect but one. The control asks the library a yes-or-no question. Production reads the classification off the object the library returns, because it also wants the reason to put in the query.

Unprotected, both refuse the review and name the visit. Under renaming, the control still refuses -- and production passes, reporting a clean monitoring review. On the same output line, the library's own summary still reads unassessable=1. The alarm is computed, correct, printed, and read by nobody.

Both presets agreed exactly. And note where the danger sat: not in the arm the matrix flagged, but in the one it cleared.

Why this is hard to catch

The deviation log is a report of exceptions, and the healthy state of an exception report is empty. Nobody investigates a quiet week. A monitoring system that finds nothing looks like a well-run trial right up until an inspector re-derives the windows from the protocol and finds three.

The tests teams write here check that the window table is loaded and that a deliberately out-of-window visit is flagged. The second of those is the right idea and it catches the union arm -- but only if the fixture visit is out of window by more than the fallback, which in this file means more than the protocol's own default rather than more than the listed window.

For teams working to a validated-software standard, the useful framing is the one cryptographic module validation applies to algorithm boundaries: what matters is not that the control is present in the configuration but that the operation, run end to end, produces the documented result on a known input.

What to do about it

Test the unions, not just the halves. Two options that are each individually harmless can erase the answer together, and this file is the cleanest example the series has measured.

Assert a known out-of-window visit end to end, and assert its severity as well as its presence. The major-threshold arm keeps the count and moves the classification, so a test that counts deviations passes it.

Make an unreadable tolerance an error rather than an unbounded one. A vendor has to default permissively; you do not. And where a library hands you a decision as an object, ask it for the decision as a question -- the control arm of the experiment above never named a field, and never lost one.

Frequently asked questions

Did obfuscation change any deviation classification 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 monitoring engine also reads.

What happened when both window options were renamed?

All fifteen visits read as in window and nothing was reportable, on a trial with three genuine deviations including one nineteen days late. Renaming either option alone was byte-identical to correct.

Why is a lost tolerance unbounded rather than zero?

Because a vendor cannot know your protocol. A library that refused every visit when no window was configured would be unusable on its first run, so the permissive reading is the only reasonable default -- and it is the one that reports a clean trial.

Does the count of deviations give any warning?

Not in the union arm, where it is zero. And not in the major-threshold arm either, where all three deviations are still found and counted -- only the severity of the nineteen-day one is downgraded to minor, which is what escalation keys off.

What was the arm that measured clean?

Renaming the fields the library uses to return a classification. It measured clean because no visit in the base fixture was unclassifiable, so the branch reading them never fired. A purpose-built fixture with one undated visit broke it.

What did that experiment show?

The control, which asks the library a yes-or-no question, still refused the review. Production, which reads the classification off the returned object, passed and reported a clean review -- while the library's own summary on the same line still read 'unassessable=1'.

What is the cheapest test that would have caught this?

One known visit, out of window by more than any plausible fallback, asserted to be reportable and asserted to be major. That fails in the union arm and in the severity arm. A test that checks the window table is loaded passes both.

Related reading