Building the AWS Monthly Intelligence report

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.

1. What prompted it

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.

The measured gap. August 2026 produced 245 announcements on the AWS What's New feed. The report covered about 40 of them — roughly 17%. More damagingly, its cover claimed “Coverage: August 1 through August 31” while 31 August was almost entirely absent: nineteen announcements that day, including AWS Agent Registry reaching GA, EC2 R9g on Graviton5, and AWS Interconnect with Azure in preview. Whole domains were missing too — fifteen SageMaker announcements, six Redshift, five WorkSpaces, none of them mentioned.

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.

2. The constraint that shapes everything

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.

This is the same constraint that made 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.

3. Where the month actually lives

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:

SourceCoversWhy 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.

4. Completeness is checked, not claimed

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.

5. Why the analysis is not generated

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.

6. What the first run produced

$ 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.

7. What is deliberately not built

8. Running it next month

# 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.
The one thing to preserve. If this is ever changed, keep the coverage assertion. Everything else here is convenience; that check is the only reason the document's central claim can be believed.