add Genshin Impact Fandom event source

Add genshin-impact.fandom.com as a second source for Genshin Impact,
which has been fixture-backed in CI because Game8 CloudFront-blocks the
Actions runner with 202s.

- Add fifth Fandom template parser in src/ingest/parsers/fandom.ts
  extracting titles from caption display text, stripping dated subpage
  suffixes, and checking currency via latestBoundaryMs.
- Register genshin-fandom-events in src/ingest/adapters/index.ts and
  set genshin-game8-events priority: 10 so existing event IDs survive
  near-match ties, preventing localStorage completion marks from
  orphaning.
- Explicitly sort adapter groups by priority descending in
  scripts/build-feed.ts to align with parseGame.
- Pin fixtures and add test coverage in test/adapters/game8.test.ts.
- Update AGENTS.md, README.md, docs/INGESTION.md, and docs/SOURCES.md.
This commit is contained in:
Lucas Winther
2026-09-12 05:20:56 +02:00
parent 0f224b62f3
commit 81ac9fb3dd
10 changed files with 771 additions and 22 deletions
+37 -4
View File
@@ -37,7 +37,7 @@ A web app that aggregates live and upcoming events across popular gacha games, p
calendar, sorts them by end date or by what the reader is partway through, tracks day-by-day
progress on events that repeat daily, and lets a user mark events completed.
**Status: working app, refreshing itself on a schedule.** Schema, nine parsers, twenty sources across
**Status: working app, refreshing itself on a schedule.** Schema, nine parsers, twenty-one sources across
nineteen games, the full interface, offline support, a static server, a Docker image and CI all exist and
are tested. The refresh runner (`bun run refresh`) fetches, caches raw snapshots and rebuilds the
feed; `.github/workflows/refresh.yml` runs it twice a day and commits only when a page actually
@@ -184,7 +184,7 @@ These come from how gacha games actually schedule things, and they cause most bu
"ongoing" heading decides currency against `ctx.now` itself, and comparing the 00:00Z placeholder
to `now` retires a row hours before `clockFor` calls it over for anybody — the reader watches a
deadline they were counting down to vanish on its last day. So `latestBoundaryMs` answers the same
question for the *last* region, and `bawiki.ts` and two branches of `fandom.ts` ask it. Nothing
question for the *last* region, and `bawiki.ts` and three branches of `fandom.ts` ask it. Nothing
stored changes: it is one comparison, not a resolved boundary written to the feed.
- **Patch cycles are ~6 weeks, and a span over a year is a parse error rather than a long event.**
`test/adapters/game8.test.ts` rejects one across every fixture. Two corrections to what this rule
@@ -209,7 +209,7 @@ These come from how gacha games actually schedule things, and they cause most bu
- **Skip, never guess.** Every function in `dates.ts` returns `null` rather than inferring a missing
year, month, or end. `readColumnTable` drops a row it cannot date. An omitted event is a
recoverable disappointment; a confidently wrong date is the failure this product exists to prevent.
- **Parsers are keyed by site, not game.** One `game8` parser serves nine sources and `fandom` four;
- **Parsers are keyed by site, not game.** One `game8` parser serves nine sources and `fandom` five;
`wikigg`, `akwiki`, `bawiki`, `holodoriwiki`, `iopwiki`, `stellasorawiki` and `arustats` serve one
each — `wikigg` and `akwiki` share a host family and have entirely different templates, and
`bawiki`, `holodoriwiki` and `stellasorawiki` are all Miraheze wikis whose page templates have
@@ -382,6 +382,7 @@ re-litigated each pass:
| `game8.co/games/Umamusume-Pretty-Derby` | **Built** (2026-08-19), off the stable `List of All Banners` page, not the monthly release-schedule pages whose URL changes every month. Cost a widening of `game8.ts`'s section and column vocabulary — see § Working on parsers |
| `nikke-…-international.fandom.com` | **Built** (2026-08-19), via `api.php` like Reverse: 1999 and FGO. Its `robots.txt` was read in a browser and is the standard Fandom file — see § Fandom below. Richest schedule of anything added in this pass: story events *and* dated pickup banners, with the reset clock evidenced on the page |
| `infinity-nikki.fandom.com` | **Built** (2026-08-19), replacing the Game8 page for Infinity Nikki, which had been stale since August 2025. Same standard Fandom `robots.txt`. Published at **day precision**: the page states a wall clock and no zone for it — see § Fandom |
| `genshin-impact.fandom.com` | **Built** (2026-09-12), via `api.php` like the other Fandom sources — see § Fandom below. Fifth Fandom template. Adds 11 events Game8 never listed; priority kept lower than Game8 to protect existing localStorage event IDs |
| `infinitynikki.miraheze.org` | **Declined.** Exists and serves `robots.txt`, but the wiki is abandoned — front page last edited 11 February 2025 and `/wiki/Events` returns a permission error. Checked as a replacement for the stale Infinity Nikki Game8 page |
| `prydwen.gg/infinity-nikki` | **Declined.** 404 — prydwen does not cover Infinity Nikki |
| `grayravens.com` (Punishing: Gray Raven) | **Declined.** Conduct is fine; the data is not. The whole 626 KB `/wiki/Events` page contains exactly one date range, written as prose, one event per six-week patch |
@@ -599,7 +600,7 @@ broken build — `skipped_robots` does not touch the failure streak, and the run
falls back to the checked-in fixture. Refreshing it means running `bun run refresh` from an address
Fandom serves, which is how its first snapshot was taken.
**Four Fandom templates now, and the third states its zone in a column header.** The Nikke wiki's
**Five Fandom templates now, and the third states its zone in a column header.** The Nikke wiki's
`Event` page is `Event | Start(UTC+9) | End(UTC+9) | Archived(?)` for story events and
`Nikke | Start(UTC+9) | End(UTC+9)` for pickup banners. That header is the safety property, not a
convenience: no date in any cell carries an offset, so a table whose Start/End columns stop naming a
@@ -677,6 +678,38 @@ reads as exactly the fault the gate just declined to call it, and it is the only
prompt somebody to ask whether a month-long quiet lane is a quiet game or a wording change under a
`statesNoEvents` that still matches.
**The fifth is Genshin Impact, adding a second source for a lane CI could not fetch.**
`genshin-impact.fandom.com` publishes `Current Events` and `Upcoming Events` wikitables under `h3`
headings with columns `Event | Duration | Type(s)`. Genshin's Game8 page has never been fetchable from
CI (CloudFront returns a `202` on all runners), so until this source existed the game's lane was built
from a checked-in fixture and could only age. Fandom answers via `api.php` and gives Genshin a live
source CI can refresh. Four things to know about it:
- **The title is the caption link's display text, and neither attribute.** The banner cell contains an
image link and a text caption below it. The link `title` names the parent article
(`Miliastra Pass/2026-08-12` for `Phantasmagoric Chronicle`), and the `img alt` carries whatever file
name was uploaded (`Stygian Onslaught 2025-10-29` on an event starting 2026-08-19). Reading attributes
here would corrupt event titles — the opposite of the Nikke rule, where cell text was missing and
attributes were required. A row with no caption is skipped rather than guessing from misleading
attributes.
- **Run-date suffixes are stripped.** Recurring events often carry their run date in the subpage title
(`Overflowing Abundance 2026-09-14`). The date names the run, the start date is already half the
event ID, and keeping it would duplicate the date on the calendar.
- **`latestBoundaryMs` gates currency.** Like FGO and Infinity Nikki, this source prints day-precision
dates (`Aug. 14, 2026 Aug. 24, 2026`), so `parseFullRange` stores UTC midnight as a placeholder.
Currency is checked against `latestBoundaryMs` so an event is not retired prematurely on its final day
before the last region has rolled.
- **Game8 priority protects localStorage keys.** The incumbent `genshin-game8-events` has
`priority: 10` while Fandom takes default priority. Two live events are titled slightly differently
(`To Temper Thyself and Journey Far` vs `… Cycle 5`, and `Stygian Onslaught` vs
`…: Battle of the Starburst`). In `merge.ts`, higher priority breaks ties when confidence is equal
(0.85). If Fandom won those near-matches, the event IDs would change and readers' localStorage
completion marks would silently orphan. With Game8 prioritized: 0 IDs lost, 11 net-new events added,
0 conflicts, and 5 corroborated events earning confidence bonuses.
- **The 336-day event raised the duration ceiling to 365 days.** Genshin's anniversary 5-star selection
runs 336 days (`2025-10-22``2026-09-23`), correctly dated on this wiki. This motivated raising the
sanity ceiling in `test/adapters/game8.test.ts` and `docs/INGESTION.md` from 180 to 365 days.
**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