Plate I · Title
One morning, one headline
Every morning at six, a program wakes up, reads about ninety news headlines, decides which ones are worth writing about, writes a briefing, scores the state of the world on a scale from war to calm, and clears a cache so that one reader can look at the result over coffee. Start to finish it takes about three minutes.
This is a study of one article's passage through that program. Not the system in the abstract — one specific item, followed from the moment it appeared on a wire to the moment it appeared on a phone, with every stage drawn and every number checked against the source.
The thing worth knowing in advance: almost nothing here is clever. The interesting parts are the decisions somebody didn't make.
- 15
- stages
- ~3 min
- of machine time
Plate II · The Wire
Eight feeds, ninety headlines
The pipeline doesn't subscribe to a wire service. It reads eight Google News RSS queries — one per section of the briefing: global, europe, ai, tech, science, markets, poland, wroclaw — each capped at ten or twelve items and each restricted to the last twenty-four hours.
Ninety headlines is the entire extent of what the system knows about the day. Every stage after this one is a transformation of those ninety lines of text. Nothing else gets in.
The outlet names you'd expect — Reuters, PAP, TVN24 — do turn up, but as metadata on individual items rather than as sources anybody chose. The system is reading a search engine's idea of the news, not a newsroom's, and that is a different thing in ways nobody downstream can detect.
- 8
- feeds
- 90
- headlines, at most
lambda_function.py — the headlines dict
Plate III · Emission
What an item actually is
A feed item, as the parser keeps it, is four fields: a title, a link, an outlet name, and a publication timestamp — plus a description truncated at a hundred and sixty characters. That is the whole record. Everything else in the XML is read and dropped.
The timestamp is the one field that matters more than it looks. It marks the only moment in this entire sequence that happens outside the system, and the last plate measures from it. Everything between is machinery.
- 4
- fields kept
- 160
- characters of dek
lambda_function.py — fetch_rss()
Plate IV · The Collector
Six o'clock, three times
An EventBridge rule fires at 06:00 UTC and invokes a Lambda. Two more rules fire at 06:10 and 06:30. That isn't redundancy by accident — it's a retry strategy with no coordination between the attempts. Each invocation checks whether today's edition already exists and returns immediately if it does.
So the six o'clock run does the work and the other two cost a few milliseconds each. It's a cruder mechanism than a proper retry with backoff, and it is exactly as reliable for a job that runs once a day.
The function is cold when it starts: no warm container, no cached state, 512MB of memory, and a fifteen-minute ceiling. The ceiling used to be ten, and the reason it is fifteen is that one morning the run hit it. That is plate VIII's story.
- 06:00
- UTC, daily
- 900 s
- timeout
infra/template.yaml — DailyAINewsTrigger; the backend README on the timeout-budget incident
Plate V · Retrieval
Eight requests, one after another
The eight feeds are fetched sequentially, not in parallel. They're built in a Python dict literal, which evaluates its values in order, so each fetch waits for the one before it to return. Each gets an eight-second timeout and none of them uses it.
Running them concurrently would save perhaps five seconds on a job that takes three minutes and runs once a day. It has never been worth the change — which is the correct engineering answer, and an unusually easy one to say out loud.
- 8 s
- timeout, each
- ~5 s
- total, in practice
lambda_function.py — the handler
Plate VI · Sorting
Eighty-nine get read and not written
There is no deduplication step here, no clustering, no relevance score. All ninety headlines go into a single prompt and the model decides what to write about. The ones it passes over aren't discarded as redundant — they're simply not chosen.
That distinction matters. A deduplication stage would be a claim that the discarded items said the same thing. What actually happens is an editorial judgement made by a language model against a system prompt, once, with no record of why.
Our specimen is one of the ones it kept. From this plate to the last it is the only thing on any sheet in vermilion, because from here on there is a specimen to follow.
- 1
- of ninety
- 0
- dedup stages
lambda_function.py — call_gemini_edition()
Plate VII · Anatomy
The article is never fetched
This is the fact that changes how you read every stage after it. The pipeline stores the article's URL and never follows it. No downstream step ever sees the article.
So the model writes from a headline and a hundred-and-sixty-character description. That is the entire input for a story that will run to a hundred words — and the system prompt is emphatic about the consequence: summarize only what the provided headlines state, never invent numbers, quotes, names, dates, causes or consequences.
It's a strict rule because with that little input the alternative isn't paraphrase, it's confabulation. Whether an instruction is sufficient protection against that is the open question the whole design rests on.
- 0
- articles fetched
- 1
- instruction standing in the way
lambda_function.py — SYSTEM_PROMPT
Plate VIII · Submersion
Ninety items, one prompt
All ninety headlines and their deks, yesterday's stability scores, and a set of Wikipedia 'on this day' events go to the model in a single call, with a JSON schema constraining the shape of what comes back.
Our specimen is a little over a thousand tokens of that prompt — a fraction of a percent of the context it sits in. It is not being read closely. It's being read alongside eighty-nine others.
The call has a two-hundred-second per-attempt timeout and a four-hundred-and-twenty-second budget across the whole fallback chain: three Gemini models, then Groq. Those numbers are scar tissue. An earlier version had a per-attempt timeout and no overall budget, spent all of it on one hung model, and hit the Lambda's own ceiling before it could send the alert telling anyone.
- 420 s
- total budget
- 4
- models in the chain
GenerateAINewsSummary/README.md — timeout budget design
Plate IX · Condensation
Where the article becomes a briefing
This is the transformation the whole pipeline exists to perform. A headline and a dek go in; a written, attributed, grounded story comes out, considerably shorter than the sum of what the model was shown.
Everything before this plate moves the article around. Everything after it moves the result around. It is the only stage a person couldn't do faster by hand, and the only stage whose output isn't a rearrangement of its input.
It is also the only stage that can be wrong in a way nothing downstream can detect. A failed fetch throws. A failed write throws. A plausible sentence that isn't supported by its source looks exactly like a good one.
- 1
- irreversible step
lambda_function.py — EDITION_SCHEMA
Plate X · The Assay
One number for the whole morning
Along with the stories, the model returns a single integer from −10 (war, chaos) to +10 (peace, calm), judged only from the headlines it was handed and anchored against yesterday's figure so the series doesn't lurch. It returns a short clause naming the headline that moved it, too.
It is a made-up number in the sense that no instrument measured anything. It is not a made-up number in the sense that it is bounded, reproducible, anchored to the previous day, and traceable to specific named inputs. Most published indices manage one of those and not the other.
- −10 … +10
- chaos to calm
lambda_function.py — stability_score
Plate XI · Cartography
Eight regions, scored every day
The same call scores eight regions from 0 (calm) to 10 (crisis), each with a clause naming what drove it. The scores are appended to a running series, so the briefing has a memory: today's figure is anchored to yesterday's, which was anchored to the day before.
The map is deliberately not a projection. The underlying data is one integer per region per day; drawing it on an accurate coastline would invite a reader to read a border off it, and the data cannot support that reading.
- 8
- regions
- 364
- days of history kept
lambda_function.py — REGION_KEYS, update_history()
Plate XII · Deposition
The whole edition, in one row
The briefing is written to DynamoDB as a single item under one partition key: id = LATEST, a date, and the entire edition serialised into a content field as JSON — eight sections, the cross-stream Signal, the back-page games, the featured paper.
There is no sort key. That is a design decision, not an omission: one read serves the whole page, there are no joins, and the only constraint that has ever bitten is DynamoDB's 400KB item ceiling. The archive of past editions is trimmed to stay under it, and currently holds about twenty-one.
The same run writes half a dozen more items — the archive, the score history, the local pulse, the games, the rolling chronicle. The edition is the one anybody reads.
- 1
- item, one key
- 400 KB
- ceiling
infra/template.yaml — AINewsSummaryTable
Plate XIII · The Signal
Telling the cache to let go
The Lambda POSTs to the frontend with a shared secret. Next.js drops its cached copies of /news, /reel, /play and /play/[game]. The round trip is a few hundred milliseconds.
Without it the edition still appears — but on a three-hundred-second revalidation timer, and because of stale-while-revalidate, only on the reload after that one. That is the difference between a request and a timeout, three orders of magnitude apart, and it is the entire reason this endpoint exists.
It was written to fix a specific complaint: the site showed yesterday's news until you hard-refreshed.
- ~300 ms
- round trip
- 300 s
- the alternative
app/api/revalidate/route.ts
Plate XIV · Distribution
Everywhere, in about a second
The invalidation propagates to the CDN's edge locations. From the origin in us-east-1 the wave reaches the furthest node in roughly a second.
This is the least interesting stage in the pipeline and by some distance the most reliable. It is also the only one nobody built. It is what you get for deploying to a platform, and it would be the hardest stage here to reproduce yourself.
- ~1 s
- to the far edge
Plate XV · The Reader
And then somebody glances at it
The headline arrives on a phone. Total elapsed time from the moment the item appeared on the wire to the moment the cache was clear is about an hour and fifty minutes — almost all of it spent waiting for six o'clock.
The pipeline's own share is roughly three minutes. Everything in this study — the eight feeds, the trap, the ninety slips, the column of sediment, the needle, the map, the strata, the pulse — resolves into something a person reads in four seconds and does not think about again.
That isn't a complaint. That is what infrastructure is: the measure of it working is that none of it shows.
- 4 s
- of reading