Tell a broken source apart from a stale one

CI failed on Infinity Nikki yielding no events, and was wrong to. Its
snapshot parses to six events; every one of them had ended by the morning
the build ran. The parser is fine — `--now 2026-08-14` gives six, today
gives none — and the page simply has nothing current left on it.

eventCount is counted after expired events are dropped, so "this parser has
stopped reading a redesigned page" and "this page's events have all
finished" arrived as the same zero. Only the first means our code is wrong,
and only the first should redden a build. So the feed now records what each
document yields parsed as of its own capture date, before expiry, and the
check fails on that instead.

Nikki cannot refresh itself out of this, either: docs/SOURCES.md records
that Fandom refuses the Actions runner. A lane with an empty calendar is a
real problem, but it is a refresh problem, so it is reported on the build
log and left visible rather than thrown.

parsedCount is nullable and defaulted, never required: the client validates
the whole feed with safeParse and the service worker serves the last feed it
downloaded, so a required field would have made every cached feed fail
validation and taken the offline promise with it. Null also covers a source
whose bytes were never confirmed live — there is no date to parse "as of",
and a build is not failed on missing information.

The rule moved to shared/feed.ts. A test did pin the old one, by grepping
the workflow for the string — which proved the check existed, never that it
was right.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-28 05:54:17 +02:00
co-authored by Claude Opus 5
parent 96d5230f29
commit ef109f224b
8 changed files with 177 additions and 15 deletions
+29 -1
View File
@@ -69,6 +69,25 @@ for (const adapter of ADAPTERS) {
game: adapter.game,
});
// Parsed a second time as of the document's own capture date, when nothing
// in it had expired yet. That figure is what separates "this parser has
// stopped reading the page" from "this page's events have all finished
// since it was captured" — the two are the same zero once expiry has been
// applied, and only the first means our code is wrong.
// Null when we do not know when these bytes were current: there is no date
// to parse "as of", and inventing one would manufacture a figure the check
// then trusts. Unknown is a real answer here, and `brokenSources` declines
// to fail a build on it.
const parsedCount =
at === null
? null
: adapter.parse(html, {
now: at,
sourceUrl: adapter.url,
sourceId: adapter.id,
game: adapter.game,
}).length;
const groups = byGame.get(adapter.game) ?? [];
groups.push(events);
byGame.set(adapter.game, groups);
@@ -81,9 +100,18 @@ for (const adapter of ADAPTERS) {
// this source has never been refreshed.
lastSuccessAt: at,
eventCount: events.length,
parsedCount,
});
console.log(` ${adapter.id.padEnd(24)} ${String(events.length).padStart(3)} events ← ${file}`);
// A source that parsed events and then lost them all to the calendar says
// so on the build log, because a bare "0 events" reads as a fault.
const note =
events.length === 0 && parsedCount !== null && parsedCount > 0
? ` (all ${parsedCount} have ended — stale page)`
: "";
console.log(
` ${adapter.id.padEnd(24)} ${String(events.length).padStart(3)} events ← ${file}${note}`,
);
}
const events: GachaEvent[] = [];