The app finds the risks and then forgets that anyone looked. Of the ten review-workflow capabilities the platform survey scored, this portfolio has none, and the survey called that its strongest single finding. This page designs the two smallest pieces that would change how a reviewer actually works — a durable record that someone looked, and a comparison against what they saw rather than against last week — and it starts with a measurement that moves the hard problem somewhere unexpected.
The requirement frames the hard problem as mutable review state living beside immutable snapshots. Measuring it found the opposite difficulty: the snapshots are not immutable. The published snapshot named ps-001 was published once and then rewritten four more times, and the data underneath that single name went from 1,898 rows to 1,918, to 1,920, to 3,835. The rebuild script deletes every snapshot directory and re-issues the names starting again from one.
So a review recorded today against ps-001 would, after the next rebuild, assert that a named person approved data they never saw. That is not a storage problem. It is the reason the first recommendation on this page is about what a review points at, not about where it is kept.
The study app surfaces flagged sites and flagged participants at every data cut, but nothing anywhere records that a human looked at one, what they concluded, or what they did — so the same rows demand attention again at the next cut and the reasoning behind every decision lives in a meeting, an email, or nowhere. The survey of thirteen safety and monitoring platforms scored ten review-workflow capabilities and found this portfolio has none of them, while the single capability with the broadest support across those platforms is the one that tells a returning reviewer what has changed since they last looked. This page proposes the two smallest pieces of that layer, and asks six questions whose answers determine whether the record is trustworthy or merely present.
Only nine of the thirteen surveyed platforms touch review workflow at all. The four open-source clinical packages closest to this project — the safetyGraphics family, Roche's teal, Boehringer's DaVinci modules and Biogen's tidyCDISC — carry none of it. What they have instead is state persistence, which is a different thing: teal has named snapshots and report cards, DaVinci has a bookmark manager. Those remember a view. None of them remembers a judgment.
Among the nine that do, the depth varies enormously, and the survey is careful that a platform was scored as having a capability because its documentation describes one. Nothing was tested, and the report says plainly that vendor documentation flatters.
| Platform | Of the ten | What it actually describes doing |
|---|---|---|
| elluminate | 9 | A digital review plan where each objective links to the displays that satisfy it, with completion tracking; role-based review; issue management; and identification of data changes since the last review. |
| JReview | 6 | A per-patient "I-Reviewed" checkbox; patients with new data since the last review highlighted; colour-coding and comments on patients. |
| CluePoints | 5 | Issue tracking integrated across detection modules, findings and corrective actions in one place, automatic breach documentation with justification and evidence. |
| Oracle Empirica | 5 | Tracked versus informational alerts monitored across successive data updates, so a reviewer sees how a signal moves; plus an alerts-reviewed completeness measure per product. |
| Veeva CDB | 5 | Change detection feeding an automated query lifecycle. The change is consumed by automation rather than shown to a reviewer as new. |
| JMP Clinical | 3 | One subject selection broadcast to every report in a review, patient profiles, auto-generated narratives. |
| clinDataReview | 1 | Comparison across two data batches — the interim-refresh diff, and the only one of these in an open-source package. |
| Spotfire, Medidata | 1 each | Bookmarked filter selections that persist between sessions; alert criteria. Medidata publishes almost nothing here. |
Five platforms carry this capability and the survey describes three genuinely different mechanisms plus one bare assertion. They are worth separating, because the differences are the whole design.
The design proposed below — compare the current data against the snapshot a particular reviewer last signed, per finding — is not an observed platform behaviour. No surveyed platform is documented as doing that. It is the survey's own proposal and it is this page's proposal; it should not be attributed to JReview or to anyone else, and the requirement's description of a Veeva product reverting a changed row to incomplete describes a different Veeva product from the one the survey scored.
The requirement names the central tension: review state is mutable and per-person, while the whole data architecture rests on immutable published snapshots that can be re-run and reproduced. Both halves of that sentence turn out to need correcting, and correcting them is what makes the design small.
A review is not a state. It is an event: at a moment, a named person asserted something about a named finding as that finding stood in a particular set of data. An event is a fact, and facts do not change. What people want to see is a status — reviewed, escalated, outstanding — and a status is not a stored thing either. It is a fold over the events, joined against whatever the data says now, computed when the page renders.
That leaves two immutable inputs and one derived view, which is precisely the shape the pipeline already has. Nothing in the design ever writes a field twice, and nothing ever needs a lock, a merge or a last-writer-wins rule.
The published site holds three snapshots under the names ps-001, ps-002 and ps-003, and the app addresses history by those names. Reading the publish history of the first one shows it was published once and rewritten four times afterwards. Hashing the results file at each of those five commits shows four genuinely different datasets under the one name.
| Commit to the published branch | Rows in the results file | Same data as before? |
|---|---|---|
| Publish ps-001 (cut-1) | 1,898 | — |
| Pin one package to a tagged version | 1,898 | yes |
| Republish from the unified raw layer | 1,918 | no |
| Republish with real tolerance limits and a real denominator | 1,920 | no |
| Republish with the safety overview and participant metrics | 3,835 | no |
The rebuild script makes this structural rather than accidental: it deletes every snapshot directory on the published branch, deletes the index that lists them, and re-issues the names from one. A review recorded against a snapshot name therefore has no defence at all. After a rebuild it points at whatever now holds that name, and the page would show a person's name and a date beside data they have never seen.
The published site says of itself that every number on it comes from the snapshot tree, and the design record describes a snapshot as an immutable static tree that is the only data interface. Nothing currently checks either sentence, and nothing can: no part of the pipeline, the published tree, the charts or the site hashes anything. The only cryptographic identifiers anywhere in the system pin the package versions, which identify the code and say nothing about the data.
A review layer is the first feature that makes those sentences load-bearing, because it is the first feature where a person's name is attached to a claim about specific data. It does not create the weakness. It is what makes the weakness visible.
A finding needs two identifiers and they do different jobs.
Note what this excludes. The requirement proposes putting the flag into the key. It should not be there: across the three published snapshots, 310 findings carry two different flag values and 13 carry three, so keying on the flag detaches a reviewer's conclusion at precisely the moment the flag flips, which is the one moment they care about. The flag is something the reviewer saw, not something that names the thing they looked at.
The same correction collapses the requirement's second key. It proposes a separate identity for participant-level safety findings, but those already publish through the identical seven columns, with the participant identifier sitting in the group field. There is one key, not two, and it already covers 1,930 published participant findings alongside the site and country ones.
The obvious implementation hashes the whole result row and reopens the finding when the hash moves. On this data that reverts essentially every finding to unreviewed at essentially every snapshot, because a site that is still enrolling has a different denominator every time the pipeline runs. The feature would then mark everything as changed, a reviewer would learn within two cuts to ignore it, and the capability would be worse than not having it.
The mechanism the requirement admires — a changed row reverting its listing to incomplete — works in the product it comes from because that product watches source data records, where a change means somebody corrected a value. A risk-indicator row is a derived aggregate that moves by construction. Copying the mechanism across that difference without noticing it is the failure mode here.
So materiality has to be declared rather than computed, and it belongs next to the metric's threshold, because deciding what counts as a meaningful move is the same kind of judgment as deciding where the threshold sits. Three tiers are enough.
| Tier | What moved | What should happen |
|---|---|---|
| The verdict | The flag, in either direction | Always reopens. Not configurable — this is the finding changing its mind. |
| The magnitude | The score or the rate, by more than a tolerance the metric declares | Reopens by default, and the metric may set the tolerance or opt out. |
| The substrate | The numerator, the denominator, the participants underneath | Noted on the row, never reopens by default. This is the one that moves every single cut. |
The site already computes a snapshot-to-snapshot comparison and renders it as a "since the last snapshot" section. It compares flag levels and nothing else, and it skips any finding whose flag level did not change.
That is fine as far as it goes, but the statistical bounds are recomputed at every snapshot from that snapshot's own results — the flagging is relative to peers. On the bundled study the boundary curve for one indicator moved by more than half its range across three cuts. A site can therefore go from amber to red with no change whatever in its own numbers, purely because the other sites improved. The comparison reports that the flag moved; it cannot say whether this site got worse or the field got better, and those two findings call for opposite actions. Whatever is built here should name which, and today nothing can.
The function that compares two snapshots takes the two sets of rows as its first two arguments. Today the page hands it the previous snapshot. Handing it instead the results of the snapshot a particular reviewer last signed turns an impersonal weekly diff into a personal one, and the function itself needs no change at all.
The watermark this needs is not a session, a cookie or a page view. It is the digest recorded on that reviewer's most recent entry. That makes it durable, auditable, portable between machines, and computable with no login and no server — and it means a reviewer who was away for a month sees one month of change rather than one week of it.
This matters beyond convenience: the roadmap item that records what you personally have looked at is explicitly local-only and says the public static site cannot do that even in principle, and should not, because per-visitor tracking has no place on a public page. A watermark taken from a signed record rather than from a visit is how the same capability arrives on the static side without any tracking at all.
One append-only entry per finding reviewed, carrying the four-part key, the digest of what was on screen, the state, a reason from a short controlled list, free text, and the action taken or recommended. Those fields are not invented: the regulator's own question-and-answer on risk-based monitoring asks for the date, the individuals involved, a summary of the data reviewed, a description of the deficiency and a description of the actions taken, to be taken, or recommended, and the entry maps onto that field for field.
The controlled reason list is the part that earns its keep. Free text alone cannot be counted; a short vocabulary lets the study say that most dismissals of one indicator were the same known site practice, which is itself a finding about the study. Requiring a reason before a dismissal is the mechanism that makes a dismissal worth reading, and it is proven — it is what the code-scanning workflow this project already lives inside requires.
Four states are enough to start: outstanding, which is the default and is recorded nowhere; acknowledged, meaning seen and no action needed; escalated, meaning seen and action required; and resolved, meaning the action happened or the finding cleared.
One surveyed platform offers a flag that hides a finding from future refreshes. This design should decline it. Acknowledged already means do not show me this again unless it changes, because the digest is what brings it back. Adding a second way to hide a row creates a second thing to audit and a second way for a finding to disappear quietly.
The site is static by contract, stated in the requirements, in the publishing notes and in the design record, and it is served from a branch verbatim with no build step. That rules out a form that posts somewhere. It does not rule out writing, because the hosting platform will accept a fully composed file through a link.
This was executed rather than assumed. Opening a prefilled new-file link against the demo study repository opens the platform's own editor with the path and the entry already filled in, under whatever account the reviewer is already signed into. Nothing was committed. No credential is ever handled by the page, and there is no authentication code to write.
Then the failure direction, by binary search on the same link:
| Entry content carried in the link | What the platform did |
|---|---|
| 2,000 characters | Accepted, content intact |
| 8,000 characters | Accepted, content intact |
| 8,250 characters | Refused — request too long |
| 16,000 and 32,000 characters | Refused |
| 64,000 characters | Connection dropped before any reply |
A realistic entry carrying the full regulatory field set is 400 characters, which becomes 580 once encoded into a link, so one hand-off carries about thirteen findings. That is a real bound and the design should state what happens above it rather than discover it in front of a user: the page splits a longer session into a second hand-off. It is also the reason the record is one file per review session rather than one file per finding, which has a second benefit — two reviewers working at once never touch the same file, so a conflict between them is not merely unlikely but structurally impossible.
The study repository has a source branch and a published branch, and the published one is arranged by a script. That script replaces exactly three named directories and copies four named files; it never enumerates the published root to remove anything unknown. A review folder kept at that root would therefore survive every publish — while the rebuild deletes every snapshot directory beside it.
That makes the published branch a survivable address but a bad home, for four reasons that all point the same way: everything on it is world-readable, the pipeline pushes to it without rebasing so a reviewer's commit landing at the wrong moment breaks the next publish, that branch is the one branch policy reserves for your approval so no reviewer could write to it unattended anyway, and it holds generated output rather than authored material.
The review record is authored. It belongs on the source branch with the other authored things, where changes arrive as pull requests on the ordinary lane, and the publish step should carry a derived projection of it onto the site the same way it carries everything else. That resolves the visibility question, the race, and the policy collision in one move, and it costs one thing worth naming: a review recorded on Tuesday does not appear on the site until the pipeline next runs, which today is Monday.
Of the ten capabilities, this design proposes two and deliberately defers the rest. A review plan with objectives linked to displays is the rarest thing in the survey and the closest analogue to this project's own traceability, but it needs the record to exist first. Role-based assignment needs roles, which needs accounts. An audit trail with electronic signatures is a gate on adoption rather than a display and is declined above. Participant-level alerting needs a notion of a refresh that this design supplies and should be designed after it. Raising a query back into the data-capture system is out of scope and should stay out. Annotation should never be built at all as a separate feature — an entry with a note is the annotation, and building both would be two records of the same thing. Issue tracking with a documented resolution comes almost free: an escalation opens an issue in the same study repository, which is the mechanism the study-level repository work already proposes for change requests and sign-offs.
This is the fork everything else follows from. The record can be authored material in the study repository, or generated material on the published branch, or it can leave the repository entirely for a database, which would end the static-hosting contract.
A review has to name the data it was written against, or it cannot be checked later. The published names are currently reused.
Too sensitive and everything is permanently unreviewed; too blunt and a signature quietly outlives the thing it was about.
The demo study is a public repository served verbatim, so anything the site carries is published rather than stored. A clinical judgment about a named participant is not obviously something to publish, and blinding is already treated elsewhere as a property enforced where the data is written out.
The write path borrows the hosting platform's identity, so attribution is genuine and free, but it only exists for people with an account there.
One surveyed platform makes a reviewed-completeness percentage its headline tile. A different tradition scopes the count to the period under review so that it shrinks to zero when the work is done.
| If you answer | What starts | What it costs |
|---|---|---|
| RL2 alone — key to a digest | Publishing a results digest with each snapshot, which is independently worth having and needs no review layer to justify it | A few lines in the study's publisher, plus a decision about whether the digest is asserted or checked |
| RL1, RL2, RL5 | The record: an entry schema, the compose-and-hand-off write path, and the publish check that makes attribution true | The bulk of the build, and a per-study decision about repository access |
| RL3 and RL6 | The personal watermark and the outstanding count, reusing the comparison that already ships | A tolerance per metric, decided once beside each threshold |
| RL4 | The projection onto the published site, and with it the first honest answer to what a public study site may say about a participant | A per-study switch, and a revision to the provenance chip so two sources are distinguishable |
| Defer the whole thing | Nothing here. The gap analysis's strongest finding stays open, and the requirement stays in the backlog | Nothing immediately — except that the snapshot-name weakness measured on this page remains unaddressed, and it is a defect in the provenance story whether or not a review layer is ever built |
This page is also the answer to the fifth question on the clinical-priorities artifact, which asked whether the review layer becomes first-release scope or is deferred in writing. The recommendation here is the narrow middle: the digest and the record are worth building, the remaining eight capabilities are deferred in writing above, and nothing in this design gates a clinical release.
Requirement: the action log, on the roadmap — #139. Answers: the fifth question on the clinical-priorities artifact, D0026. Sources: the platform gap analysis, survey of thirteen platforms; the dashboard design that filed this requirement, app dashboard design. Adjacent: study-level repositories #131, blinding as a published property #144, what you personally last looked at #205, and the repeated-snapshot problem in the demo study demo-301#8.
Drafted by 👯🤖 Claude Code (Claude Opus 5, worker W0139). Not reviewed by @jwildfire before publication.