Skip to content

Part 3 of 7 · Tax rate updater series ~5 min read

How a source gets read

The failure mode of every watcher ever built is that it quietly stops watching. The page moves, the selector breaks, the fetch starts returning a cookie banner, and the system reports no changes for eighteen months because it is reading nothing. This post is mostly about that.

Key takeaways

  • Anchor on the label, not the markup. “Standard rate” survives a redesign; a CSS path does not.
  • Every fetch stores a snapshot, so a change can be evidenced eighteen months later.
  • A source that becomes unreadable raises an alarm. Silence is the dangerous outcome.
  • PDFs are read the same way as pages, through Textract, and change less often.
  • The model reads the page; the comparison is plain equality on a number.

How a page gets read

How a tax rate source is fetched, snapshotted and readA vertical chain of five steps entered by a box labelled Scheduled check, running daily per source. Step one fetches the source with a real user agent; a fetch error exits to Fetch failed, which alarms after three consecutive days. Step two stores the snapshot in S3, content-addressed. Step three asks whether the bytes are unchanged by comparing the digest with last time; unchanged exits to Nothing to do, the usual outcome. Step four finds the labelled value with a single Bedrock call, grounded by the label named in the register row; if the label is not found it exits to Label not found, which alarms rather than assuming anything. Step five compares the found value with the register using plain equality. A note says the byte digest short-circuits almost every check, so the model is rarely called.AWS ACCOUNTScheduled checkdaily, per sourceFetch the sourcewith a real user agentFetch failedalarm after 3 dayserrorStore the snapshotS3, content-addressedUnchanged bytes?digest vs last timeNothing to dothe usual outcomesameFind the labelled valueone Bedrock callRegister rowthe label to findLabel not foundalarm, do not assumemissingCompare with the registerplain equalityThe byte digest short-circuits almost every check, so the model is rarely called.
Fig 1. How one source is read on a schedule. The digest check means the model is only involved on the rare days a page actually changes, and a missing label is an alarm rather than an assumption.
  • Storage
  • App integration
  • Machine learning
  • Management
  • Analytics
  • Outside AWS

Anchoring on the label

The naive approach is a CSS selector or an XPath, and it breaks on the first redesign, which for a government site is roughly annual. The robust approach is to describe what you are looking for in words — “the standard rate percentage” — and ask the model to find it in the page text.

That survives a redesign, a table becoming a list, and a value moving between paragraphs. What it does not survive is the label itself changing, which is exactly right: if a page stops describing something as the standard rate, that is a real event and a human should look at it. So a missing label is an alarm, not a silent zero.

Why the snapshot is kept

Two reasons, and the second is the one people do not anticipate. The first is audit: when somebody asks in eighteen months why a rate was applied from a particular date, the answer should be a stored copy of the page as it read on that date, not a memory and a link to a page that has since changed.

The second is debugging the watcher itself. When a source starts producing a value that looks wrong, having the last thirty snapshots means you can see exactly when the page changed shape. Content-addressed storage means thirty daily snapshots of an unchanged page cost one object, not thirty.

The failure mode that matters

Five ways a rate watcher can silently stop workingA horizontal row of five boxes. Fetch fails: alarms after three consecutive days. Label missing: alarms immediately. Value unparseable: alarms immediately. Source moved: the redirect chain is logged and followed once. No check at all: the worst one. A note says the fifth needs its own heartbeat, because a watcher that is not running reports nothing.FIVE WAYS A WATCHER SILENTLY STOPSFetch fails3 days -> alarmLabel missingimmediate alarmValue unparseableimmediate alarmSource movedredirect chain loggedNo check at allthe worst oneThe fifth needs its own heartbeat, because a watcher that is not running reports nothing.
Fig 2. The five ways a watcher stops working, and why the last one needs a separate mechanism. Nothing inside a system that is not running can report that it is not running.
  • Networking
  • Security & identity
  • Management
  • Analytics

The last one deserves the emphasis. Every other failure produces a message from inside the system; a system that is not running produces nothing, which is indistinguishable from a quiet month. So the register carries a last_checked timestamp per row, and a separate scheduled job — a different rule, in a different function — alarms if any row has not been checked in three days.

That is a small amount of duplication and it is the difference between a watcher and the appearance of one.

PDFs

A good proportion of authoritative rate schedules are PDFs, often several hundred pages of them. They are handled the same way: fetch, digest, and only if the bytes changed do they go through Textract and the same label-anchored read. PDFs change less often than pages, which makes the digest short-circuit even more effective, and when they do change it is usually a whole new document with a new URL — which the redirect and link-following logic has to handle.

Next: what the differ does with a change, and why the effective date matters more than the value.

All posts