Status: DRAFT — awaiting Matthew's review of the two deviations recorded below (2026-10-01); the build itself is
live and verified. His instruction: "we need to focus on daily
updates to check for any changes to the register." His ruling on what that is for, given the same session:
"No one needs to be told when the register changes — that's over-engineering — its purpose is for looking up
licensed suppliers and nothing else." The check therefore exists for one reason: so the lookup is
current. No notifications, no change reports, no alerts — and nothing else is built on it.
The build is live and I verified it the same day. The full record (numbers, the rollback-safe probes, the rendered
health row, the suite) is on the handback in from-opencode and in the note in
from-hermes. In short: an unchanged register is a no-op with one release before
and after and no mail sent; an unreachable source records the reason and leaves the live release alone; a file that is
not a workbook is refused with nothing published; a different file is imported and goes live, the previous
release kept intact (proved with a deliberately edited copy, inside a rolled-back transaction); the health row reads
last checked / next due and the page never says "Release overdue"; the daily entry is 04:15 UTC.
Two things were built differently from what is written above — recorded here rather than left contradicting. The
first is now settled by Matthew (2026-10-01); the second is still his to decide:
health.notify() when an attempt fails or is unconfigured, and the register's health row raises an action alertTotal Records line, or a row count out of step). As built, the only refusals are a file that cannot be readTotal Records row andCASTLE MEAD MEDICAL CENTRE and a holder of UK WDA(H) 62089). A register export whose layout changed wouldRecorded so neither is re-derived: the no-op, both failure paths and the changed-file path are verified live, and
the routine touches no tenant database and no supplier row — the check's code contains no Supplier and no tenant
reference at all, which is the cheapest proof of §4's boundary.
The register is in the platform store as a release, and the supplier screen looks a supplier up against it
(both built, §2). What is missing is currency: the register changes when the MHRA adds, amends or withdraws an
authorisation, and today nothing notices until a file is imported by hand.
The daily check closes that gap and does nothing more: fetch the register's export once a day; if it is the same
file as the live release, do nothing; if it is a different file, import it so the lookup is answering from the
current register. Everything else the check might have done — telling somebody, diffing the register, flagging
suppliers — is out by his ruling (§5).
What it must never do (unchanged): never create, edit or delete a supplier; never rewrite the register's own
text; never invent a licence status; never touch a tenant database.
Recorded, so the decision is not reopened: an unattended daily fetch of the register is Matthew's
instruction, taken with the source's own terms in front of him — the clauses are quoted on
the register spec §7, and the use is for
the purpose the register serves, attributed. That is the record; this spec does not argue it again.
The platform already runs this shape of routine for the other national sources, so the register joins it instead of
growing a second mechanism:
| Piece | As it is today | What the register needs |
|---|---|---|
ReferenceSource rows |
dmd, drug_tariff, drug_tariff_news, price_concessions — each with a cadence (weekly/monthly/ad_hoc) and a routine (trud_pull, tariff_fetch, cpe_email, nhsbsa_notices, none) |
mhra_wda exists already; its routine is none and there is no daily cadence — both need adding |
| Release + publish | release_service imports a release and publishes it (is_live) |
the register's import command already does this: release wda-2026-09-30-f33878ba is live |
| Attempt log | ReferenceFetchAttempt (statuses incl. success, already_held, failed, held) |
one row per daily check — where "unchanged", "imported" and "the fetch failed" live |
| Scheduler | Celery Beat, the quiet window: dm+d 03:15, tariff 03:45, tariff news 04:00, pack links 04:30, derived prices 04:45, health 06:00 (UTC) | one more entry (W5) |
| HTTP client | trud_client for TRUD; a plain GET for the notices |
the register export is a plain GET, no auth, no key — the simplest of the set |
Build on the import's branch. The import is gp_booking_app feature/wda-register-import @ 967326f
(not merged to dev; deployed to the app engine by bundle; migration 0014 applied; release live). This should
land with it or straight after it merges — not on a third branch.
W1 — one GET, one attempt row. Fetch the export the register page links
(https://cms.mhra.gov.uk/mhra/wda?_format=xlsx) once a day under the datasets path. Record the attempt either
way: HTTP status, byte size, SHA-256, and the register's own Total Records: … line when it parses. One request,
no crawl, no search-page scraping, no retry storm — a single attempt per scheduled run.
W2 — the unchanged shortcut. If the file's SHA-256 equals the live release's, the check is done and silent:
the attempt row reads already held / unchanged, no release is created, nothing is re-parsed. This is expected to
be the normal case — a register only moves when an authorisation is added, amended or withdrawn, and the export is
not regenerated on the hour (the copy pulled on 2026-10-01 still carried the register's own stamp of
Wed Sep 30 21:35:41 BST 2026). One hash comparison is the whole day's work.
W3 — a different file is imported and goes live. Import it through the existing release path, which is
idempotent by SHA-256 and publishes on success, so the lookup answers from the new register immediately and the
previous release is kept (nothing is deleted). That is the whole of the change handling.
No change set is computed and no diff is stored. With nothing and nobody consuming a diff, it would be
machinery kept for its own sake — and the facts it would carry stay derivable from the retained releases if a need
ever arises. If OpenCode sees a concrete reason to store the diff in the release path itself, record it in its
handback rather than adding it quietly.
W4 — failure, and the file that is not the shape we know. Two failures, handled differently:
Total Records line unreadable, or a row count wildly out of step with that line: refuseW5 — when it runs. Daily at 04:15 UTC, with the other reference routines and inside the quiet window the
platform uses: after the tariff-news fetch (04:00), before the pack-link rebuild (04:30) and the health check
(06:00). Retiming it is a one-line change if ever wanted.
W6 — the health row, stated honestly. For a source with no publication schedule, "release overdue" is the
wrong question — the register publishes when it publishes. The register's row on the existing
/infrastructure/reference-data/ view reports last checked and next check due (tomorrow), and reads as a
fault only if the check did not run inside its window (grace one day) — never because a release is "late". A day
on which the register did not change is a healthy day.
That is the point of the ruling: the register exists to answer "is this wholesaler on the licensed register, and
what are its licensed sites?" The daily check keeps that answer current, and nothing downstream reads it.
One consequence, recorded because it is the reason people might expect more: if a licence leaves the register,
the lookup simply stops offering it, and a supplier whose details were taken from it keeps its stored licence
number, trading address and postcode on the record. Nothing flags the change to anyone, and no supplier record is
touched. The withdrawal is visible to whoever looks that supplier up — which, under this ruling, is the only place
it is meant to be visible. If that ever proves too quiet, the diff would be reconstructable from the retained
releases; it is not built now.
Supplier licence fields are built (at his directNone open. Q1 (who is told) is answered — nobody. Q2 (a supplier-side flag) is answered by the same ruling —
no flag; the lookup is the only consumer (§4). Q3 (the time) is set at 04:15 UTC for him to overrule if he
wants, and is a one-line change.
Off the real register, read-only, then on the live system: a check against the current file must record an
attempt and create no new release (proved by counting releases before and after); a second run the same day
likewise. Against a deliberately edited copy — a licence number removed, a holder renamed, a site address
changed — the check must import and publish a new live release, leave the earlier one intact and
re-importable, and change no supplier row; the lookup must then answer from the new release (the edited holder
findable, the removed licence no longer offered). A copy with the header block removed or the columns renamed must
be refused, with nothing published and the previous release still live. A simulated fetch failure must leave the
live release untouched and the attempt row carrying the reason. The health view must show the register's last
checked / next due and must not report it overdue for a day that simply passed with no change. And no mail
of any kind may be sent by any of these runs — the ruling in §5 is a testable property, so it gets a test.
The brief to OpenCode follows my live verification of the two OPEN notes below (the import handback and the
supplier-screen lookup) — the daily check builds directly on them, so they are verified first, then this goes to
agents/handoff/from-hermes as one brief.