diff --git a/AGENTS.md b/AGENTS.md index fad537e..f985680 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -254,6 +254,12 @@ These come from how gacha games actually schedule things, and they cause most bu refresh reported a shape change. Neither number was ever a count of the page. When a source's count is far below what the page shows, find out which shape is being skipped before the count reaches zero and the gate has to guess for you. +- **A count of zero has two causes, and only the page can tell them apart.** A source that broke and + a game that is between patches both read `0`, so nothing downstream can separate them — which is + why `canParse` must identify a page by its *structure* (headings, ids, column names) and never by + finding a row, and why a page that prints something like `There are no Events in this category` + gets to say so through `statesNoEvents` rather than being counted as a failure. Infinity Nikki hit + both halves of this at once; see § Fandom. ## Event IDs are localStorage keys @@ -618,6 +624,29 @@ by hand**, because an attribute never passes through `text()` and `Alison's otherwise become a slug, and a slug is a localStorage key. The sanitiser catches exactly that, and a parser needing repair on its own fixture is a parser with a bug. +**This page goes empty between versions, and that is a third thing a source can be.** On 2026-09-03, +with 2.7's events ended and 2.8 not yet listed, the wiki replaced both tables with `There are no +Events in this category` — the markup otherwise untouched, `Past Events` still carrying twenty +tables of the same shape. `isInfinityNikkiEventPage` identified the page by finding a *populated* +table, so it read that as a redesign, and the source spent four cycles reporting +`the source has likely been redesigned` at a page nobody had redesigned. Two rules came out of it, +and both generalise past this wiki: + +- **A `canParse` that reads data cannot tell a rewrite from a quiet week.** It now reads the section + headings, which an empty table does not take with it — *either* `Current Events` or + `Upcoming Events`, not both, because requiring the pair would fail the source over a renamed + heading it does not even read. `docs/INGESTION.md` already said to keep these checks structural; + this one had drifted into content and nothing caught it until a game went quiet. +- **A page that states its own emptiness is answering, not failing.** `statesNoEvents` (optional on + `SourceParser`) lets the runner store that as a real answer, so the snapshot advances and the + streak stays clean instead of reaching the `broken` tier over a correctly empty lane. It must rest + on the page's own words — a redesign yields zero rows too, and storing *that* is the silent + emptying the zero-events gate exists to prevent. Only this template implements it; the other three + say nothing either way when empty and keep the strict gate. + +An empty Nikki lane is therefore now the truth rather than a gap, exactly as GFL2's thin weeks are. +The lane refills on its own when 2.8 is listed, with no parser change. + **The second Fandom source's page is chosen, not obvious.** `fategrandorder.fandom.com` publishes two schedules: `Event_List` opens "This page lists all Events in Fate/Grand Order Japan", and `Event_List_(US)` is the English server. They run months apart, each diff --git a/docs/INGESTION.md b/docs/INGESTION.md index b43fdd8..0d4c6d2 100644 --- a/docs/INGESTION.md +++ b/docs/INGESTION.md @@ -202,6 +202,7 @@ export interface SourceParser { id: string; // "game8" label: string; // "Game8" canParse(html: string): boolean; // structural sanity check + statesNoEvents?(html: string): boolean; // the page says it lists none parse(html: string, ctx: ParseContext): GachaEvent[]; } ``` @@ -216,6 +217,24 @@ in attribute quote style (`class="a-table"` on Genshin, `class='a-table'` on NTE the kind of variation a naive check gets wrong. Every regex in `html.ts` is attribute-agnostic for the same reason. +**"Structural" means identity, never the presence of a row** — and that half was learned the +expensive way. `isInfinityNikkiEventPage` recognised its page by finding a *populated* +`Current`/`Upcoming` table, so when the wiki emptied both between versions (2026-09-03, § below) the +tripwire fired on a page whose markup had not changed by a byte, reported +`the source has likely been redesigned`, and reached the `broken` tier in a day and a half. A check +that reads data cannot tell a rewrite from a quiet week; a check that reads headings, ids or column +names can, because those survive an empty table. Ask what identifies the page, not what is on it. + +`statesNoEvents` is the other half of that lesson, and it is optional because most pages say nothing +either way. It answers a question `canParse` and a row count together cannot: **is this source +broken, or is this game simply between patches?** Both look identical from a count of zero, so the +signal has to be the page's own words — Infinity Nikki's wiki prints "There are no Events in this +category" under both headings when it is listing nothing. A parser that implements it lets the +refresh runner store an empty snapshot as a real answer (§ Stage 1); one that does not keeps the +strict gate, which is the right default. Never infer it from an empty table: a redesign that broke +every selector yields an empty table too, and storing *that* is the silently emptied calendar the +gate exists to prevent. + ### The adapter registry ```ts @@ -376,7 +395,15 @@ conduct for why that ordering is load-bearing. injection, so the whole runner is tested offline against a fake fetch. A fetched body is *rejected* — the previous snapshot survives — when it fails `canParse`, throws, or yields zero events; storing an empty parse would make the feed build prefer it over the fixture and silently empty a game's -calendar. One source down is a warning and exit 0; every source failing is exit 1, so CI never +calendar. + +The one exception is a page that states its own emptiness: when a parser's `statesNoEvents` says the +document itself reports listing nothing, the empty parse is stored as that source's real answer and +the cycle counts as confirmed rather than failed. Without it a game between versions fails every +cycle until its next patch ships — three of them reach the `broken` tier and fail the workflow, over +a lane that is correctly empty, while the snapshot it is holding goes on ageing. The distinction is +the page's statement and never a row count, so a redesign still rejects, and the run says which of +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. ## Stage 2 — parse diff --git a/docs/SOURCES.md b/docs/SOURCES.md index 0efaee5..62433c7 100644 --- a/docs/SOURCES.md +++ b/docs/SOURCES.md @@ -430,6 +430,22 @@ Two shapes the parser handles, both of which would otherwise lose or corrupt a r Asking the wiki's editors to state the zone on the Duration column would upgrade this source to exact instants, and remains the cheapest improvement available to it. +**Update, 2026-09-03: this page goes empty between versions, and the source was reading that as a +redesign.** With 2.7's events ended and 2.8 not yet listed, the wiki replaced both the `Current +Events` and `Upcoming Events` tables with `There are no Events in this category`, leaving everything +else — including twenty `Past Events` tables of identical shape — untouched. `canParse` identified +the page by finding a *populated* table, so it threw `the source has likely been redesigned` at a +page nobody had touched; four cycles of that reached the `broken` tier and the calendar went on +holding a snapshot whose every row had expired on 27 August. + +The fix was in the pipeline, not in this assessment: `canParse` now identifies the page by its +section headings, and `statesNoEvents` lets a parser report the page's own declaration of emptiness +so the runner stores it as an answer instead of a failure (`docs/INGESTION.md` § The parser interface +and § Stage 1). **So an empty Infinity Nikki lane is now the expected state between patches, not a +symptom** — the thing to check before suspecting this source is whether the wiki is listing anything +at all. It refills on its own when the next version is posted. The verdict on the source is +unchanged and it stays built. + ## 12. Thirteen Game8 hubs, swept on 2026-08-19 — two are worth building, eleven are not Not a P1 game between them: this section answers a direct request to check a list of Game8 game