diff --git a/AGENTS.md b/AGENTS.md index c6d580e..c093fdc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -106,7 +106,7 @@ re-verify a sample against the live page afterward. ``` src/shared/ schema.ts (the contract), time.ts, daily.ts, effort.ts, games.ts, feed.ts custom.ts — reader-authored games and events, and their key spaces -src/ingest/ html.ts, dates.ts (fifteen formats), merge.ts, sanitize.ts, robots.ts, snapshots.ts +src/ingest/ html.ts, dates.ts (sixteen formats), merge.ts, sanitize.ts, robots.ts, snapshots.ts parsers/ game8.ts, wikigg.ts, akwiki.ts, fandom.ts, bawiki.ts, holodori.ts, iopwiki.ts, stellasora.ts — keyed by SITE, not game adapters/ index.ts — SOURCES registry binding url+game+parser, and the sanitize seam @@ -119,7 +119,7 @@ src/client/ React app, service worker, manifest theme.ts — dark or light, and what a game hue reads as on each scripts/ build-feed.ts, build-static.ts, parse-fixture.ts (offline), refresh-sources.ts (fetches) serve.ts static server + /api/health -test/ 700 tests +test/ 706 tests fixtures// raw HTML + .expected.json per source — pinned, kept forever snapshots/ current page per source, rewritten by refresh — see its README ``` @@ -164,7 +164,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 ten sources and `fandom` two; +- **Parsers are keyed by site, not game.** One `game8` parser serves nine sources and `fandom` four; `wikigg`, `akwiki`, `bawiki`, `holodoriwiki`, `iopwiki` and `stellasorawiki` serve one each — the first two share a host family and have entirely different templates, and the last two are both Miraheze wikis whose page templates have nothing in common. Adding a source for a known site is one `SOURCES` entry; a new @@ -256,9 +256,9 @@ Sources are community wikis. Treat them as a guest would: - Honor `robots.txt`; set a descriptive `User-Agent` with a contact URL. - One request per source per refresh cycle, minimum 6 hours apart. -- **Space requests to one host**, honouring its `Crawl-delay` and defaulting to 2s. Ten of the - eighteen sources are game8.co pages, so the per-source floor alone still permits one cycle to arrive - as ten back-to-back requests to a single site — which is the shape an edge network throttles, and +- **Space requests to one host**, honouring its `Crawl-delay` and defaulting to 2s. Nine of the + nineteen sources are game8.co pages, so the per-source floor alone still permits one cycle to arrive + as nine back-to-back requests to a single site — which is the shape an edge network throttles, and what a burst looks like from the far end regardless of our intent. - Send `If-None-Match` / `If-Modified-Since`; treat `304` as "skip, unchanged". - Cache raw snapshots so re-parsing never re-fetches. **Iterate against fixtures, not the network.** @@ -270,9 +270,9 @@ training, and no `User-agent: *` rule applies to our paths. Keep it that way: do rate, and do not add an LLM that consumes page content. **game8.co does not answer a GitHub Actions runner** (confirmed 2026-08-17). Its edge returns -`202 Accepted` with a bot-management body to every one of the ten game8 sources, from the first +`202 Accepted` with a bot-management body to every one of the nine game8 sources, from the first scheduled cycle onward — `last confirmed: never` — while the same URLs return `200` and parse -cleanly from a normal address. So `robots.txt` permits us and the network does not, and those ten +cleanly from a normal address. So `robots.txt` permits us and the network does not, and those nine games have only ever been built from checked-in fixtures in CI. The per-host spacing above does not fix this and was not meant to: a 202 on the very first request @@ -302,34 +302,30 @@ re-litigated each pass: | `game8.co/games/Chaos-Zero-Nightmare` | **Built** (2026-08-19). Zero parser work — the existing `game8` parser reads it. The ninth game8 source, so fixture-backed in CI from day one | | `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 | | `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 | | `guardian-tales.fandom.com` | **Declined.** Parses fine and contains no 2026 date at all — newest dated entry is 2025. The `bluearchive.fandom.com` failure again: parses cleanly to nothing live | | `blhx.fandom.com`, `azurlane-archive.fandom.com` | **Declined.** The two Fandom alternatives to the declined koumakan wiki are dead archives — `Event_Calendar` stops in **2021**, and the archive wiki's headings have nothing under them. Azur Lane still has no source | | Aether Gazer | **Do not build.** The developer confirmed no further content updates after 23 July 2026, with store listings removed 17 October 2026. The wiki dates nothing anyway — `Event_Guide_List` is an image gallery. A lane that will be empty by winter | +| game8.co hubs for Black Beacon, Brawl Stars, Destiny: Rising, Diablo Immortal, Epic Seven, Fire Emblem Shadows, Gundam UC Engage, Mongil: Star Dive, Pokémon Champions, Pokémon UNITE, Tower of Fantasy | **Declined** (2026-08-19). All thirteen hubs in that sweep exist and answer `200`; these eleven have no usable schedule. Six are abandoned wikis whose newest page is 2021–2025 — the Infinity Nikki failure mode, a source that parses perfectly and publishes history. Gundam's calendar prints ends with no starts, so no event ID; Pokémon UNITE is fresh but its template fails `canParse`, which is the check working. Per-game evidence in `docs/SOURCES.md` § 12 | +| `game8.co/games/MementoMori`, `game8.co/games/fire-emblem-heroes` | **Assessed, not yet built** (2026-08-19) — the two live finds of that sweep, and proposals rather than decisions. MementoMori parses today with no parser change; FEH has the freshest page of any source here and needs a ninth Game8 column shape plus a ruling on the 180-day rule, which one real seven-month banner breaks. See `docs/SOURCES.md` §§ 12a–12b | -**The Infinity Nikki lane is knowingly stale, and the fix is a decision rather than a search** -(checked 2026-08-19). `game8.co/games/Infinity-Nikki/archives/487445` fetches, parses and passes -every test — and its page says `Last updated on: August 31, 2025`. It mentions the year 2026 zero -times. Seven events parse out of it, of which **five carry `endsAt: null`** and so read as -live-with-unknown-end forever, on a calendar whose whole purpose is telling a reader what is still -on. That is worse than an empty lane, and it arrives through a source that looks perfectly healthy -to the runner, because a stale page is not a broken one. +**The Infinity Nikki lane was rebuilt on a live source, and its Game8 source was retired.** +`game8.co/games/Infinity-Nikki/archives/487445` stopped being updated on 31 August 2025 — it +mentions the year 2026 zero times — and had been publishing five year-old events with +`endsAt: null`, which the app renders as live-with-unknown-end indefinitely. That is worse than an +empty lane and it is the failure this product exists to prevent, arriving through a source that +looks perfectly healthy to the runner, because **a stale page is not a broken one**: no failure +streak, no annotation, no `broken` tier. Nothing in the pipeline catches this. When adding a source, +check when the page was last updated, not only whether it parses. -A live replacement exists: `infinity-nikki.fandom.com` (the canonical host; the unhyphenated name -301s to it) is maintained, permitted by the same standard Fandom `robots.txt` as Nikke, and its -`Event` page carries `Current Events` / `Upcoming Events` tables of -`Event | Duration | Description | Type` with full dates and clocks on both sides. **What it does not -carry is a timezone on that column** — only prose elsewhere dating version launches `(UTC-7)` and a -note that rewards reset at `04:00 (Server Time)`. The durations run `04:00 → 03:59`, which only -lands on a reset boundary if the column is server-local, so the case for UTC-7 is strong and -circumstantial. It matters because the offset moves the *day*, and the start's day is half an event -ID. `docs/SOURCES.md` § 11 lays out the four options and recommends taking the printed date at day -precision, which invents nothing and treats these cells exactly as every Game8 date is already -treated — and which conflicts with § Blue Archive as written, so that rule needs narrowing if it is -chosen. **Not to be decided by an agent**: retiring the game instead would drop a `GameId` that -prefixes every completion key its readers hold, with no server-side recovery. +The lane now comes from `infinity-nikki.fandom.com` at day precision (§ Fandom above), and the Game8 +entry is gone from `SOURCES` rather than kept as a second opinion — a source whose every row is +wrong is not corroboration. Its fixture stays in `fixtures/nikki/`, because it is the only page here +carrying Game8's labelled `Start: … End: Permanent` shape and `test/adapters` still drives it +through the parser directly as a regression test. `.github/ISSUE_TEMPLATE/feature_request.yml` points readers at that table by heading, so a source request can be checked against it before anyone writes it up — the loudest feedback on the first @@ -387,7 +383,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. -**Three Fandom templates now, and the third states its zone in a column header.** The Nikke wiki's +**Four 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 @@ -404,6 +400,28 @@ the left, and `canParse` asserts the lookup. Two more things about it: calendar day, and the start's day is half an event ID. That is the Fate/Grand Order rule below, applied to the opposite gap — there, a zone with no clock; here, a clock on only one side. +**The fourth is Infinity Nikki, and it is published at day precision on purpose.** +`infinity-nikki.fandom.com` (the unhyphenated name 301s to it) heads its `Current Events` and +`Upcoming Events` tables `Event | Duration | Description | Type`, and every duration reads +`July 20, 2026 04:00 – August 10, 2026 03:49` — a full date and a wall clock on both sides, and **no +zone anywhere on the page** for that column. The only zone statements are prose elsewhere dating +version launches `(UTC-7)` and a note that rewards reset at `04:00 (Server Time)`; the durations do +run `04:00 → 03:59`, which only lands on a reset boundary if the column is server-local. Strong, and +circumstantial. + +So `parseZonelessClockRange` reads the clock and throws it away, publishing the printed date at day +precision. That invents nothing and treats these cells exactly as every Game8 date is already +treated. Converting instead would mean picking an offset, and the offset moves the *day*: +`July 16, 2026 20:00` read as UTC-7 is `2026-07-17T03:00Z`, and the start's day is half of every +event ID this game will ever have. If the wiki's editors ever state the zone on that column, this +source can carry exact instants and should. + +Two shapes to know: `Permanent Events` and `Past Events` share the page and are fenced off by +heading, and titles come from a link's `title` attribute — which means they must be **entity-decoded +by hand**, because an attribute never passes through `text()` and `Alison's Travel Shop` would +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. + **Two Fandom sources now, and the second one'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 @@ -449,12 +467,21 @@ confidently wrong date: - **The page states no time of day and no timezone anywhere.** The schedule's dates are bare `YYYY-MM-DD`, which is honest day precision. Its five other tables (Mini-Event, Reward campaigns, Attendance bonuses, Guide missions, Joint Firing Drill) *do* carry a wall clock — `08/12/2026 11:00` - — but name no zone for it, and three of the five do not say which server they describe. Those are - deliberately unparsed: reading them as UTC invents the fact that matters, and rounding to a day does - not save it, because a 04:00 local boundary lands either side of UTC midnight depending on the - offset assumed and the start's day is part of the event ID. Attendance bonuses would be a real - dailies source if a zone is ever stated. For the same reason `ba` has no `resetOffsets`: Blue - Archive Global does run one worldwide server, but nothing in this source says on what clock. + — but name no zone for it, and **three of the five do not say which server they describe**. Those + are deliberately unparsed, and that second clause is the whole reason: a clock whose server is + unknown cannot even be labelled with a day, because you do not know whose day it is. Attendance + bonuses would be a real dailies source if a zone is ever stated. For the same reason `ba` has no + `resetOffsets`: Blue Archive Global does run one worldwide server, but nothing in this source says + on what clock. + + **This rule governs a clock with no known server, not a clock with no stated zone** — a + distinction worth drawing precisely, because it was drawn the wrong way once. As first written it + said rounding such a cell to a day "does not save it", which would also have condemned every Game8 + date in this repository: those state no zone either, and are published at day precision without + anyone minding. The Infinity Nikki wiki (§ Fandom) is the case that forced the correction — one + worldwide service, a clock, no zone — and it is read at day precision on the printed date, which + invents nothing. What is still forbidden is *converting* an unzoned clock by picking an offset, + because the offset moves the day and the start's day is half an event ID. **hololive Dreams: the same Miraheze call as Blue Archive, and the opposite data.** `holodori.wiki` is Miraheze too, so `/wiki/Events` is the surface `*` is allowed and `/w/` and `?action=` are closed @@ -696,6 +723,6 @@ to an open page). Four things hold it up: (PRD F7). `freshness()` in `src/shared/feed.ts` is the one definition: it takes the newest `lastSuccessAt` and **never `generatedAt`**, which is a build stamp that would call a fixture-backed calendar minutes old, and it treats a game as only as fresh as its *oldest* source, - so one live wiki cannot vouch for a stalled sibling. Given that ten sources cannot be fetched + so one live wiki cannot vouch for a stalled sibling. Given that thirteen sources cannot be fetched from CI at all (§ Scraping conduct), this disclosure is the only thing standing between a reader and a confidently stale calendar — do not let a future change source it from the build clock. diff --git a/docs/FEEDBACK.md b/docs/FEEDBACK.md index 2db276d..584f8f6 100644 --- a/docs/FEEDBACK.md +++ b/docs/FEEDBACK.md @@ -305,7 +305,7 @@ diagnosis in each item still holds — what changed is whether it has been acted | P1b cap the long list | **Done** (2026-08-18). Both sections cap at six with "show all N" — truncation of the view, not a re-sort | | P1b persist `view` | **Done** (2026-08-18). `prefs.view`, and the first run now asks which one to open on (PRD F8) | | P1b Calendar → Timeline | **Done.** The tab reads "Timeline" | -| P1c more games | **Done, ten of them.** Infinity Nikki, Persona 5: The Phantom X, Reverse: 1999, Blue Archive, then Fate/Grand Order and hololive Dreams, then Girls' Frontline 2, Stella Sora, Chaos Zero Nightmare, Umamusume and Nikke (2026-08-19). Of the games named in this thread, **Umamusume is now built** — via Game8, not the declined `uma.moe` — and Azur Lane and Aether Gazer stay declined. **Nikke is built too** (2026-08-19), once its `robots.txt` was read in a browser and turned out to be the standard Fandom file. See `docs/SOURCES.md` § 3 | +| P1c more games | **Done, ten of them, and one rebuilt.** Infinity Nikki, Persona 5: The Phantom X, Reverse: 1999, Blue Archive, then Fate/Grand Order and hololive Dreams, then Girls' Frontline 2, Stella Sora, Chaos Zero Nightmare, Umamusume and Nikke (2026-08-19). Of the games named in this thread, **Umamusume is now built** — via Game8, not the declined `uma.moe` — and Azur Lane and Aether Gazer stay declined. **Nikke is built too** (2026-08-19), once its `robots.txt` was read in a browser and turned out to be the standard Fandom file. See `docs/SOURCES.md` § 3. **Infinity Nikki was rebuilt** on a live Fandom source the same day, its Game8 page having been stale since August 2025 — § 11 | | P1c roadmap in the colophon | **Not done** | | P1 games still unserved | Azur Lane, Punishing: Gray Raven, Guardian Tales, Aether Gazer (shutting down), Silver Palace (unreleased). Separately, **Infinity Nikki's source is stale** and has no reachable replacement — `docs/SOURCES.md` § 11 | | P2 colour and screenshot | **Layout done** (2026-08-18), colour untouched. The palette was left alone deliberately — the two-axis system was not what made the screenshot read as unfinished, a dense single column on a wide screen was. Past `lg` the page is now a pinned deadline rail beside the lists, the timeline is a board with its axis and names pinned, and the footer and settings are columns. The screenshot still wants reshooting | diff --git a/docs/SOURCES.md b/docs/SOURCES.md index e7eddcd..85b1bbf 100644 --- a/docs/SOURCES.md +++ b/docs/SOURCES.md @@ -1,6 +1,7 @@ # SOURCES.md — event sources for the games still missing -A source assessment for the games named in `docs/FEEDBACK.md` § P1 that still have no adapter. +A source assessment for the games named in `docs/FEEDBACK.md` § P1 that still have no adapter, +plus § 12, a sweep of thirteen Game8 hubs asked for separately on 2026-08-19. Written 2026-08-18. **Acted on 2026-08-19**: four of the games below were built, one turned out to be blocked rather than merely awkward, and the rest were declined. Each section keeps its original reconnaissance and carries its outcome at the top, because the reasoning is what stops the next pass @@ -12,7 +13,10 @@ Umamusume (`game8`, after widening its section and column vocabulary). **Built o permission was read in a browser:** Nikke, whose `robots.txt` turned out to be the standard Fandom file. **Declined:** Punishing: Gray Raven, Guardian Tales, Azur Lane and Aether Gazer, all as proposed. One thing not on the original list also came out of the pass: the -**Infinity Nikki source is stale** and has no reachable replacement — see § 11. +**Infinity Nikki source is stale** and has no reachable replacement — see § 11. Later the same day a +second, unrelated request added § 12: thirteen Game8 hubs checked for a schedule page, of which +**MementoMori** needs no parser work at all and **Fire Emblem Heroes** has the best data in that +sweep behind a shape the parser does not read. The other eleven are declines. Verdicts here are proposals. Once one is acted on — built or declined — the *decision* belongs in `AGENTS.md` § Scraping conduct, whose table exists so a source is not re-litigated every pass and @@ -51,6 +55,7 @@ Two things this method cannot tell you, and both matter: | Nikke | **Built** (2026-08-19), once the `robots.txt` was read in a browser — see § 3 | | Azur Lane | Declined on conduct (`azurlane.koumakan.jp` `ai-input=no`), and both Fandom alternatives are dead archives | | Punishing: Gray Raven, Guardian Tales, Aether Gazer | Declined — see §§ 6, 7, 9 | +| Infinity Nikki | **Rebuilt** (2026-08-19) on `infinity-nikki.fandom.com` at day precision, replacing a Game8 page stale since August 2025 — see § 11 | | Silver Palace | Unreleased | ## Summary of findings @@ -368,21 +373,33 @@ projections point at late 2026 / early 2027. Nothing to scrape, as `docs/FEEDBAC --- -## 11. Infinity Nikki — a live source exists, and one fact is missing from it +## 11. Infinity Nikki — BUILT 2026-08-19, at day precision, replacing a stale source -This game already has an adapter, which is why it was not assessed on 2026-08-18. It should have -been. `game8.co/games/Infinity-Nikki/archives/487445` fetches, parses, and passes every test — and -the page itself says `Last updated on: August 31, 2025`, with **zero** occurrences of the string -`2026`. Seven events parse out of it, five with `endsAt: null`, which the app renders as -live-with-unknown-end indefinitely. A year-old event presented as current is precisely the failure -this product exists to prevent, and it arrives through a source that looks perfectly healthy to the -runner: a stale page is not a broken one. +> **Outcome: option (b), chosen by the repository owner.** The lane now reads +> `infinity-nikki.fandom.com` and the Game8 entry has been removed from `SOURCES` — a source whose +> every row is wrong is not a second opinion. Seven current 2026 events replace five year-old ones +> that had `endsAt: null` and rendered as live indefinitely. The Game8 fixture stays in +> `fixtures/nikki/` as the only regression test for that parser's labelled `Start: … End: Permanent` +> shape, driven through the parser directly now that no adapter routes it. +> +> The clock is read and discarded; the printed date is published at day precision. That invents +> nothing and treats these cells exactly as every Game8 date already is. **The Blue Archive rule in +> `AGENTS.md` has been narrowed to match**, because as written it forbade this: it now governs a +> clock whose *server* is unknown — which is what those Blue Archive tables actually were, three of +> five not saying which server they described — rather than any clock without a stated zone. What +> remains forbidden is converting an unzoned clock by picking an offset, since the offset moves the +> day and the start's day is half an event ID. -**A live replacement was found on 2026-08-19, and it is nearly right.** -`infinitynikki.fandom.com` 301s to `infinity-nikki.fandom.com`, whose `robots.txt` was read in a -browser and is the standard Fandom file — same permission as Nikke (§ 3). The wiki is maintained: -pages edited as recently as 2026-08-17, and its `Event` page mentions 2026 seven hundred and -thirty-one times. The page is shaped exactly as we would want: +This game already had an adapter, which is why it was not assessed on 2026-08-18. It should have +been. `game8.co/games/Infinity-Nikki/archives/487445` fetched, parsed, and passed every test — and +the page itself said `Last updated on: August 31, 2025`, with **zero** occurrences of the string +`2026`. That is the lesson worth keeping from this section: **a stale page is not a broken one**, so +nothing in the pipeline flags it. Check when a page was last updated, not only whether it parses. + +**The replacement.** `infinitynikki.fandom.com` 301s to `infinity-nikki.fandom.com`, whose +`robots.txt` was read in a browser and is the standard Fandom file — same permission as Nikke (§ 3). +The wiki is maintained: pages edited as recently as 2026-08-17, and its `Event` page mentions 2026 +seven hundred and thirty-one times. ``` Current Events / Upcoming Events / Permanent Events / Past Events (h2 headings) @@ -390,41 +407,24 @@ Current Events / Upcoming Events / Permanent Events / Past Events (h2 headings Deep Breakthrough July 20, 2026 04:00 – August 10, 2026 03:49 ... Double Rewards ``` -Full dates with the year on both sides, a wall clock on both sides, a description, and the source's -own event type. Better material than the Game8 page has ever carried. +**The missing fact was the timezone.** The Duration column names no zone. What the page *does* say, +elsewhere and in prose: version launches dated `(UTC-7)`; `Rewards reset daily at 04:00 (Server +Time)`; and durations running `04:00 → 03:59`, which only lands on a reset boundary if the column is +server-local. Strong circumstantial evidence for UTC-7, and still circumstantial — which is why the +clock is dropped rather than converted. -**The missing fact is the timezone, and it is missing in an interesting way.** The Duration column -names no zone. What the page *does* say, elsewhere and in prose: +Two shapes the parser handles, both of which would otherwise lose or corrupt a row: -- version launches are dated `(UTC-7)` — "Version 2.1 goes live on January 29 (UTC-7)"; -- `Rewards reset daily at 04:00 (Server Time)`; -- and the durations themselves run `04:00` → `03:59`, which only lands on a reset boundary if the - column is server-local. +- **Titles come from a link's `title` attribute**, so they need entity-decoding by hand — an + attribute never passes through `text()`, and `Alison's Travel Shop` would otherwise become the + slug `alison-39-s-travel-shop`, which is a localStorage key. The ingest sanitiser catches exactly + this, and a parser that needs repairing on its own fixture has a bug. +- **Recurring events live on dated subpages**, so a title arrives as `Deep Breakthrough/2026-07-20` + or, behind a red link, as the file name `Alison's Travel Shop 2026-08-06.png`. That date names the + run rather than the event and is stripped; the start date already keys the runs apart. -That is a strong circumstantial case for UTC-7 and it is still circumstantial. It matters more than -usual because the offset moves the *day*: `July 16, 2026 20:00` read as UTC-7 is -`2026-07-17T03:00Z`, and the start's day is half of every event ID this game will ever have. - -**Four options, and this is the repository owner's call:** - -| Option | What it costs | -|---|---| -| **(a) Leave the Game8 lane** | A knowingly stale lane, carried only by the footer's freshness disclosure. The status quo, and the worst of these | -| **(b) Build the Fandom source at day precision**, taking the printed date and dropping the clock | Loses a stated clock; invents nothing. Treats the cells exactly as every Game8 date is already treated. **Recommended** | -| **(c) Build it at exact precision, assuming UTC-7** | Almost certainly correct, and an assumption we would be unable to falsify from the page. If wrong, every date is hours out and some days are wrong | -| **(d) Retire the game** | Drops a `GameId` that prefixes every completion key its readers hold, with no server-side recovery | - -**(b) has a precedent tension worth stating rather than glossing.** `AGENTS.md` § Blue Archive -rules that unzoned wall clocks are "deliberately unparsed", and that "rounding to a day does not save -it". That ruling was made about tables which additionally *did not say which server they described*. -Here the wiki is the Global one for a game with a single global service, and the printed date is -being taken at face value in exactly the way `game8` dates already are — Game8 states no zone either, -and the whole day-precision mechanism in `clockFor` exists to resolve such a boundary on the reader's -own server. If (b) is chosen, that rule needs amending to say which case it governs, because as -written the two conflict. - -Asking the wiki's editors to state the zone on the Duration column would settle it outright, and is -cheaper than any of the above. +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. ## What every one of these costs, beyond the source @@ -443,11 +443,18 @@ Adding a game is never only a `SOURCES` entry: ## Recommended order Steps 1 through 5 were done on 2026-08-19, and step 6's declines are now written into `AGENTS.md` -§ Scraping conduct. What is left is not a queue of sources any more — it is two decisions: +§ Scraping conduct. What is left is one decision and two builds: -1. **Infinity Nikki** (§ 11) — choose between four options for a lane whose current source is a year - stale and whose live replacement states no timezone on its schedule column. Not a research task; - the research is done and the recommendation is option (b). +Nothing is queued. Every game assessed in this file is now built or declined with its reasoning +recorded, and the two decisions this pass raised — Nikke's permission and Infinity Nikki's timezone — +have both been made. +2. **MementoMori** (§ 12a) — the cheapest adapter available today: a `GameId`, a `GAMES` entry, one + `SOURCES` entry, a fixture and a test, with no parser change. Seven events parse out of a + seven-row table and six of them are live. +3. **Fire Emblem Heroes** (§ 12b) — better data and a real cost: a ninth Game8 column shape, a + single-date `MM/DD/YYYY` reader, and a decision about the 180-day rule, which one legitimate + seven-month banner breaks. Two commits, parser then source, with the vocabulary widening diffed + across every fixture and snapshot as Umamusume's was. Below that, in descending order of value: re-check `grayravens.com` if it ever puts its schedule in a table, and re-check Azur Lane if `koumakan` ever drops `ai-input=no`. Neither is worth a pass @@ -468,6 +475,18 @@ curl -sS -A "$UA" https://stellasora.miraheze.org/wiki/Main_Page > stellaso curl -sS -A "$UA" 'https://nikke-goddess-of-victory-international.fandom.com/api.php?action=parse&page=Event&prop=text&formatversion=2&format=json' > nikke.json curl -sS -A "$UA" https://game8.co/games/Chaos-Zero-Nightmare/archives/559899 > czn.html +# the § 12 sweep: hubs first, then the schedule page each one turned out to have. +# `--rate 30/m` keeps one request every 2s to a single host, per AGENTS.md § Scraping conduct. +for g in Black-Beacon Brawl-Stars Destiny-Rising Diablo-Immortal Epic-Seven \ + fire-emblem-heroes Fire-Emblem-Shadows gundam-uce MementoMori \ + Mongil-Star-Dive Pokemon-Champions Pokemon-UNITE Tower-of-Fantasy; do + args+=(-o "hub-$g.html" "https://game8.co/games/$g") +done +curl -sS --rate 30/m -A "$UA" --compressed -w '%{http_code} %{url_effective}\n' "${args[@]}" + +# how stale is it? — the first question, and the cheapest +grep -o 'Last updated on:[^<]*' mmori.html + # does an existing parser read it? (offline, once a source id exists) bun run parse czn-game8-events czn.html --json ``` @@ -481,3 +500,8 @@ Facts stated above that a future reader may want to re-check, with how they were | `fandom.ts`, `akwiki`, `wikigg`, `bawiki`, `holodori` cannot read the new pages | `canParse` returned `false` for every combination tried | | CI can fetch wiki.gg and Miraheze | `git log --name-only -- snapshots/` — `github-actions[bot]` commits carry `arknights-akwiki`, `endfield-wikigg`, `ba-bawiki` | | A Fandom `robots.txt` 403 means the source is skipped, not failed | `src/ingest/robots.ts` `RobotsCache.load` — `status >= 400` other than 404/410 → `usable: false` | +| All thirteen § 12 hubs exist | `GET https://game8.co/games/` → `200` for every one | +| The `game8` parser already reads the MementoMori events page | ran `game8Parser.parse` against the fetched bytes: 7 events from the 7 data rows of `Current Events \| Duration` | +| The FEH page is unreadable by shape, not by two header words | `columnLayout` finds no range column at all — `Availability` is a start and `End` is its own column, and `readStartEndTable` wants the rowspan `Start`/`End` pair | +| Game8's structural check rejects the Pokémon UNITE page | `game8Parser.canParse` → `false`; the page has 25 `a-table` matches and zero `a-header--3` | +| A hub's nav is not an index of its pages | the UNITE, Champions, Gundam and Black Beacon schedule pages exist and are linked from none of the hub navigation scanned; found by web search |