Write down that a source can be empty without being broken
The two preceding changes make sentences in docs/ false. INGESTION.md described `canParse` as a structural check without saying that "structural" excludes the presence of a row — which is exactly the drift that let this bug in — and described the Stage 1 gate as rejecting every empty parse. SOURCES.md § 11 left a reader to conclude that an empty Infinity Nikki lane means the source has died. AGENTS.md gets the rule in both places somebody would look: § Working on parsers, next to the bullet about a source resting on one row, and § Fandom where the fourth template is described. The general lesson is worth more than the incident. Any page that can list nothing can produce this, so the guidance is about what identifies a page versus what happens to be on it — not about this wiki. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4b2b3d0958
commit
4d45635b0e
@@ -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
|
||||
|
||||
+28
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user