Design note · 1 September 2026
This is the reasoning behind scripts/build_monthly_report.py and
why it works the way it does rather than the obvious way. It is written down
because the obvious way is wrong in a manner that produces no error, and the
next person to look at this — including me — will be tempted by it
again.
The starting point was a generated monthly report: a nine-page PDF covering August 2026, produced by asking a model to summarise the month. The question was whether it could be printed and handed to a manager.
Checking it was worthwhile, and the result was more nuanced than expected. Nothing in it was fabricated. Fifteen of its specific claims were probed against the raw AWS feed and fourteen were there with matching dates. Two figures checked at source came back exactly right: the Glue 6.0 “30% price reduction” and all three ACM email-validation deadlines.
The failure was not accuracy. It was coverage.
That is the failure mode to design against, and it is worth naming precisely: a model reading feeds does not produce wrong facts, it produces silent omissions. Spot-checking cannot detect it, because every item you check is present. This has been measured on this repo before — summarising the feed once missed 24 of 66 announcements in a single week, a third of it, with no error raised.
The natural design is the one the weekly builder already uses: fetch the AWS What's New feed, filter to the date range, render. That works for a week and cannot work for a month.
The feed is capped at 100 items. Measured on 1 September 2026, those 100 items reached back only to 19 August. Run a month-long fetch on the 1st and the first two-thirds of the month is simply not there — and nothing in the response says so. The output would look like a complete month and be a third of one.
DAILY-BACKLOG.md necessary
in the first place: an item ranked but not written ages out of the feed and is
lost. The monthly report inherits the problem and has to inherit the solution.
The insight that makes this tractable: the monthly report is not a data-gathering job. The work has already been done, four times, by the weekly roundups. Each was built from the raw feed on the Saturday it covered, with every link validated, and published. They are the durable record precisely because the feed is not.
So the builder assembles from three sources:
| Source | Covers | Why it is trustworthy |
|---|---|---|
posts/weekly-*.html |
Every complete Mon–Fri window in the month | Built from the raw feed on the day, links already validated |
| Live What's New feed | Days after the last weekly window | Still within the feed's reach when run on the 1st |
DAILY-BACKLOG.md |
Importance ranking per item | Assigned on the day it landed, not reconstructed later |
The third source is what turns 245 rows into a prioritised document rather than a list. Every announcement was already ranked when it was fresh; the report reuses that judgement instead of re-deriving it a month later, when the context that produced it is gone.
This is the part that answers the original failure. The script builds the set of weekdays in the month and asserts each one is covered by a weekly inventory or by the live fetch. Any that is not is named in the output and on stderr, and the script exits non-zero.
COVERAGE HOLE -- these weekdays are in neither a published roundup nor the live feed: 2026-08-05 The feed reaches back only to 2026-08-19, so those days cannot be recovered now.
A short month therefore fails loudly instead of looking complete. That single property is the difference between this and the draft it replaces.
The same arithmetic is printed into the document, as a source table whose rows sum to the stated total. Your manager can check the coverage claim rather than take it on trust — which is the thing a generated summary structurally cannot offer, because it has no way to prove what it did not miss.
The script fills every data-derived section and stops. The four prose sections — executive summary, what to act on, what the month had in common, and the lifecycle calendar — are read from a sidecar file:
reports/aws-monthly-<YYYY-MM>-analysis.html
Two reasons. The first is that finding a month's patterns is the part of the document worth reading, and it is not a summarisation task — “AWS is moving operational lifecycles into the platform” is visible across four weeks of ECS, EKS, DRS and Batch announcements and invisible in any one of them.
The second is discipline. Keeping the prose in a file a human writes makes the rule enforceable: no figure appears in the analysis unless it was read from AWS's own page. For August that meant checking the two lifecycle claims still open before they went in — the IoT Device Defender Detect closure and the two Elastic Beanstalk platform-branch retirements — and it is why the report cites AWS's own one-line summary per announcement rather than a paraphrase. Paraphrase is where the draft's unsupported “4,096 dimensions” claim came from.
If the sidecar is absent the report still builds, with the prose sections marked unwritten, so the data can be reviewed before the analysis is done.
$ python scripts/build_monthly_report.py 2026-08 Month 2026-08: 245 announcements across 22 day(s) with news Weekly roundup, 3-7 August 2026-08-03..2026-08-07 66 Weekly roundup, 10-14 August 2026-08-10..2026-08-14 41 Weekly roundup, 17-21 August 2026-08-17..2026-08-21 63 Weekly roundup, 24-28 August 2026-08-24..2026-08-28 54 live feed tail 2026-08-29..2026-08-31 21 backlog: 190 item(s) ranked, 64 High/Med-High, 21 became posts wrote reports/out/aws-monthly-2026-08.html
The document contains, in order: a completeness box and source table; the four written sections; how the month was ranked at the time, from the backlog; and the complete inventory of all 245 announcements by day, each with AWS's own summary. Five announcements whose AWS links return 404 are rendered as unlinked text with a note, rather than dropped — dropping them would break the completeness promise the whole thing rests on.
posts/. A published monthly
would be a different rendering — trends and lifecycle calendar, no
inventory, linking out to the four weeklies — so that it never
republishes an announcement the weekly series already covered.
max-width: 640px block.
--no-inventory flag. The inventory is most
of the 113KB and roughly 25 pages of the print. A short version would be
about four pages, which is the readable-on-a-phone form.
scripts/morning.py, which already computes what is due from
cadence rules and writes MORNING.md into every worktree.
# 1. Build the data. Fails loudly if the month cannot be covered. python scripts/build_monthly_report.py 2026-09 # 2. Write the four prose blocks, checking every figure at source. # reports/aws-monthly-2026-09-analysis.html # 3. Rebuild with the analysis in place. python scripts/build_monthly_report.py 2026-09 # 4. Open reports/out/aws-monthly-2026-09.html and print to PDF.