fix: stop failing the build on a page that says it has no events
Infinity Nikki's wiki prints "There are no Events in this category" between versions, and the refresh runner has honoured that since 2026-09-03 — it stores the empty parse as the source's real answer rather than letting a quiet lane reach the broken tier. The feed build never asked, so the same page arrived at CI as parsedCount 0, indistinguishable from a parser that has stopped reading a redesigned page, and brokenSources failed every build while every refresh stayed green. The runner's verdict cannot travel on its own: only a parser has seen the page, and by the time brokenSources runs there is nothing left but the feed. So the fact rides on SourceHealth, defaulted so an older feed the service worker cached keeps validating and reads as the strict answer. Both ends now ask it the same way — of an empty parse only, from the page's own words only — because a redesign yields zero rows too, and excusing that is the silently emptied calendar the gate exists for. The rule sits in a module rather than in build-feed.ts, which writes public/ and so runs a build if a test imports it. That is how the two ends drifted apart with nothing to catch it. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e51585f2b2
commit
1156c358a7
+19
-4
@@ -608,7 +608,22 @@ partly applied.
|
||||
|
||||
## Schema versioning
|
||||
|
||||
`/api/events` responses carry `{ schemaVersion: 1, generatedAt, events: [...] }`. The client
|
||||
refuses to render a `schemaVersion` it does not know and shows a "refresh the page" prompt instead
|
||||
of guessing at unfamiliar fields. Additive fields do not bump the version; removing or retyping a
|
||||
field does.
|
||||
`/api/events` responses carry `{ schemaVersion: 1, generatedAt, events: [...], sources: [...] }`.
|
||||
The client refuses to render a `schemaVersion` it does not know and shows a "refresh the page"
|
||||
prompt instead of guessing at unfamiliar fields. Additive fields do not bump the version; removing
|
||||
or retyping a field does.
|
||||
|
||||
`sources` is a `SourceHealth` per registered source (`src/shared/feed.ts`), and it is what the
|
||||
footer's freshness line and CI's build gate both read. Three of its fields describe one source's
|
||||
last document and only make sense together: `eventCount` is what it contributed after expired events
|
||||
were dropped, `parsedCount` is what the same bytes yield when parsed as of their own capture date,
|
||||
and `statesNoEvents` says the page itself declared it currently lists none. That triple is what
|
||||
separates a parser that has stopped reading a redesigned page (a fault) from one whose events have
|
||||
all since ended, and from a game between versions (neither) — see `docs/INGESTION.md` § Stage 1.
|
||||
|
||||
**Every field added here is defaulted, and that is a rule rather than a habit.** The service worker
|
||||
serves the last feed it downloaded, so a required field fails validation on every cached feed and
|
||||
takes the offline promise down with it. A default must also be the *strict* reading of silence:
|
||||
`parsedCount` defaults to `null` — an older feed never recorded the figure, which is an absence of
|
||||
information and not evidence of health — and `statesNoEvents` to `false`, because a feed built
|
||||
before the field existed never asked the page and cannot be read as having got an answer.
|
||||
|
||||
@@ -406,6 +406,28 @@ the page's statement and never a row count, so a redesign still rejects, and the
|
||||
the two it saw: `0 events — the page states it currently lists none`. One source down is a warning and exit 0; every source failing is exit 1, so CI never
|
||||
commits a cycle that learned nothing.
|
||||
|
||||
**The fact has to reach the feed, because the runner is not the only thing that judges a zero.**
|
||||
`scripts/build-feed.ts` parses the same snapshot again and records a `SourceHealth` per source, and
|
||||
CI fails the build on `brokenSources` — `parsedCount === 0`. For four days from 2026-09-03 the
|
||||
runner excused Infinity Nikki's quiet page and the feed build did not, so every green refresh was
|
||||
followed by a red CI run on a lane that was correctly empty. The runner's verdict could not travel:
|
||||
only a parser has seen the page, and by the time `brokenSources` runs there is nothing left but the
|
||||
feed. So `SourceHealth.statesNoEvents` carries it, `src/ingest/health.ts` sets it, and the same rule
|
||||
applies at both ends — asked only of an empty parse, taken only from the page's own words.
|
||||
|
||||
Two properties of that field are load-bearing rather than incidental. It is **defaulted, never
|
||||
required**, for the reason `parsedCount` is: the service worker serves the last feed it downloaded,
|
||||
and a required field would fail every cached feed's validation and take the offline promise with it.
|
||||
And an absent value reads as `false` — the strict answer — because a feed built before the field
|
||||
existed never asked the question and must not be read as having answered it.
|
||||
|
||||
**The rule lives in a module rather than in the script, and that is the point.**
|
||||
`scripts/build-feed.ts` writes `public/`, so importing it from a test runs a build; the rule sat
|
||||
where no test could reach it, which is exactly how the two ends drifted apart. `src/ingest/health.ts`
|
||||
is a pure function over an adapter and a document, and `test/feed.test.ts` exercises it. Same lesson
|
||||
as `brokenSources` itself, which was inline in `ci.yml` and pinned by grepping that file for a
|
||||
string — proof the check existed, never that it was right.
|
||||
|
||||
## Stage 2 — parse
|
||||
|
||||
Hash the raw body (sha256) → `content_hash`. **If it matches `sources.content_hash`, end as
|
||||
|
||||
Reference in New Issue
Block a user