From 1b116218e392b9b9152abbd686e201a1de25528d Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Fri, 16 Oct 2026 06:35:02 +0200 Subject: [PATCH] fix(sources): Mira Journey ends at Asia's reset after the version's last day The Version table prints the version's last day as its End Date. Read as an ordinary day-precision end, the event left the calendar 9 to 24 hours before the pass did. The owner's timer on Europe put the end at about 20:00 UTC on 29 Oct: Asia's 04:00 reset opening the 30th, for every server. The source's entry now says so, with that evidence, and the event ends at exactly 2026-10-29T20:00Z. Found in the whole-branch review. Co-Authored-By: Claude Opus 5.5 --- docs/SOURCES.md | 2 +- ...ndom-mira-journey-2026-10-16.expected.json | 4 +-- src/ingest/parsers/versionTable.ts | 31 +++++++++++++++---- test/adapters/versionTable.test.ts | 24 +++++++------- 4 files changed, 41 insertions(+), 20 deletions(-) diff --git a/docs/SOURCES.md b/docs/SOURCES.md index 057b8da..e0fd048 100644 --- a/docs/SOURCES.md +++ b/docs/SOURCES.md @@ -883,7 +883,7 @@ passed. A `fetched` with more than zero events means the wiki is back. **Source:** `https://infinity-nikki.fandom.com/api.php?action=parse&page=Version&prop=text&formatversion=2&format=json` (page `https://infinity-nikki.fandom.com/wiki/Version`), source id `nikki-fandom-mira-journey`, parser `fandomversion` (`src/ingest/parsers/versionTable.ts`). -**What it is:** Mira Journey has no dated page. The `Version` page dates *versions*: three tables headed `Version Name | Version Number | Release Date | End Date | Duration` (2.x, 1.x, prerelease), 25 rows in all (counted independently of the parser), e.g. `The Feather of Justice | 2.10 | September 23, 2026 | October 29, 2026 | 36 Days`. A version stands in for the pass only because the owner confirmed in game, 2026-10-16, that the pass ends with it (it read 13d 19h at 00:47 UTC, ending 2026-10-29 with version 2.10). That claim and its evidence are one entry in `VERSION_PASSES`, keyed by source id; the event's summary repeats it (`Follows version 2.10 (The Feather of Justice). That the pass ends with the version was confirmed in game, 2026-10-16.`). Rows already ended are skipped, a row without a readable end is skipped; at capture exactly one row survives: 2026-09-23 to 2026-10-29, day precision, titled `Mira Journey`. The page states no zone, so nothing is region-scoped. +**What it is:** Mira Journey has no dated page. The `Version` page dates *versions*: three tables headed `Version Name | Version Number | Release Date | End Date | Duration` (2.x, 1.x, prerelease), 25 rows in all (counted independently of the parser), e.g. `The Feather of Justice | 2.10 | September 23, 2026 | October 29, 2026 | 36 Days`. A version stands in for the pass only because the owner confirmed in game, 2026-10-16, that the pass ends with it (it read 13d 19h at 00:47 UTC, ending 2026-10-29 with version 2.10). That claim and its evidence are one entry in `VERSION_PASSES`, keyed by source id; the event's summary repeats it (`Follows version 2.10 (The Feather of Justice). That the pass ends with the version was confirmed in game, 2026-10-16.`). Rows already ended are skipped, a row without a readable end is skipped; at capture exactly one row survives, titled `Mira Journey`, from 2026-09-23 (day precision). **Its End Date is the version's last day**, not the day it ends: the owner's timer on Europe put the end between 19:47 and 20:47 UTC on 2026-10-29, which is Asia's 04:00 reset opening the 30th. So the entry carries `endsAfterLastDay: { clock: "asia" }` and the event ends at exactly 2026-10-29T20:00Z for every server. Read as an ordinary day-precision end it would have left the calendar 9 to 24 hours early, before the pass did. Nothing is region-scoped. **Not the history-table reader:** `fandomtable` refuses any table with a `Duration` heading and this reader requires one, so neither claims the other's page (tested across every fixture). diff --git a/fixtures/nikki/fandom-mira-journey-2026-10-16.expected.json b/fixtures/nikki/fandom-mira-journey-2026-10-16.expected.json index 4cc3625..206cf46 100644 --- a/fixtures/nikki/fandom-mira-journey-2026-10-16.expected.json +++ b/fixtures/nikki/fandom-mira-journey-2026-10-16.expected.json @@ -7,8 +7,8 @@ "summary": "Follows version 2.10 (The Feather of Justice). That the pass ends with the version was confirmed in game, 2026-10-16.", "startsAt": "2026-09-23T00:00:00.000Z", "startPrecision": "day", - "endsAt": "2026-10-29T00:00:00.000Z", - "endPrecision": "day", + "endsAt": "2026-10-29T20:00:00.000Z", + "endPrecision": "exact", "regionScoped": false, "regionEnds": null, "sourceUrl": "https://infinity-nikki.fandom.com/wiki/Version", diff --git a/src/ingest/parsers/versionTable.ts b/src/ingest/parsers/versionTable.ts index 05a3b68..267e5b6 100644 --- a/src/ingest/parsers/versionTable.ts +++ b/src/ingest/parsers/versionTable.ts @@ -1,5 +1,5 @@ -import { eventId, type GachaEvent } from "../../shared/schema.ts"; -import { latestBoundaryMs } from "../../shared/time.ts"; +import { eventId, type GachaEvent, type GameId, type Region } from "../../shared/schema.ts"; +import { DAY, dayStartMs, latestBoundaryMs } from "../../shared/time.ts"; import { parseMonthDayYear, type ParsedInstant } from "../dates.ts"; import { text } from "../html.ts"; import type { ParseContext } from "../adapters/types.ts"; @@ -53,6 +53,13 @@ export interface VersionPass { /** The sentence the summary closes with: the stated link, with its source. */ readonly link: string; readonly evidence: VersionLinkEvidence; + /** + * When the table's End Date is the version's *last* day rather than the day + * it ends on: the pass then ends at `clock`'s reset opening the next day, for + * every server. Only where the evidence shows it; absent, the End Date is + * read as other day-precision ends are, the reset opening that day. + */ + readonly endsAfterLastDay?: { readonly clock: Region }; } /** Keyed by source id, because the claim is a fact about the source. */ @@ -64,8 +71,9 @@ export const VERSION_PASSES: Readonly> = { by: "owner", on: "2026-10-16", reading: - "in game, the pass read 13d 19h at 00:47 UTC, ending 2026-10-29 with version 2.10", + "in game on Europe, the pass read 13d 19h at 00:47 UTC: an end between 19:47 and 20:47 UTC on 2026-10-29, the table's End Date for version 2.10 — Asia's 04:00 reset opening 2026-10-30, not Europe's", }, + endsAfterLastDay: { clock: "asia" }, }, }; @@ -146,6 +154,16 @@ function pageUrl(sourceUrl: string): string { : `${url.origin}/wiki/${encodeURIComponent(page).replace(/%2F/g, "/")}`; } +/** A row's end, moved to the stated reset when the table prints a last day. */ +function endOf(end: ParsedInstant, pass: VersionPass, game: GameId): ParsedInstant { + if (pass.endsAfterLastDay === undefined || end.precision !== "day") return end; + const next = new Date(Date.parse(end.iso) + DAY).toISOString().slice(0, 10); + return { + iso: new Date(dayStartMs(next, pass.endsAfterLastDay.clock, game)).toISOString(), + precision: "exact", + }; +} + export function parseFandomVersionTable( body: string, ctx: ParseContext, @@ -161,9 +179,10 @@ export function parseFandomVersionTable( for (const row of versionRows(rendered).rows) { if (row.end.iso <= row.start.iso) continue; + const end = endOf(row.end, pass, ctx.game); // Day-precision end: judged by the last moment any reset could put it at, // not the UTC-midnight placeholder (AGENTS.md § Domain rules). - if (latestBoundaryMs(row.end.iso, row.end.precision, ctx.game) < nowMs) { + if (latestBoundaryMs(end.iso, end.precision, ctx.game) < nowMs) { continue; } @@ -175,8 +194,8 @@ export function parseFandomVersionTable( summary: `Follows version ${row.number} (${row.name}). ${pass.link}`, startsAt: row.start.iso, startPrecision: row.start.precision, - endsAt: row.end.iso, - endPrecision: row.end.precision, + endsAt: end.iso, + endPrecision: end.precision, regionScoped: false, regionEnds: null, sourceUrl: pageUrl(ctx.sourceUrl), diff --git a/test/adapters/versionTable.test.ts b/test/adapters/versionTable.test.ts index 1bb6669..04c2782 100644 --- a/test/adapters/versionTable.test.ts +++ b/test/adapters/versionTable.test.ts @@ -42,7 +42,7 @@ describe("fandomversion parser", () => { // Only 2.10 (to October 29) has not ended on 2026-10-16. const now = fandomVersionParser.parse(page, ctx("2026-10-16T00:00:00.000Z")); expect(now.map((e) => [e.title, e.startsAt, e.endsAt])).toEqual([ - ["Mira Journey", "2026-09-23T00:00:00.000Z", "2026-10-29T00:00:00.000Z"], + ["Mira Journey", "2026-09-23T00:00:00.000Z", "2026-10-29T20:00:00.000Z"], ]); }); @@ -52,18 +52,19 @@ describe("fandomversion parser", () => { "Follows version 2.10 (The Feather of Justice). That the pass ends with the version was confirmed in game, 2026-10-16.", ); expect(e?.startPrecision).toBe("day"); - expect(e?.endPrecision).toBe("day"); + // The table's End Date is the version's last day. The owner's timer on + // Europe put the pass's end at ~20:00 UTC on it: Asia's 04:00 reset that + // opens the next day, for every server. + expect(e?.endsAt).toBe("2026-10-29T20:00:00.000Z"); + expect(e?.endPrecision).toBe("exact"); expect(e?.type).toBe("other"); }); - test("after 2026-10-29 the 2.10 row is gone", async () => { - expect(fandomVersionParser.parse(await html, ctx("2026-10-30T00:00:00.000Z"))).toEqual([]); - }); - - test("the row stays on its last day", async () => { - expect( - fandomVersionParser.parse(await html, ctx("2026-10-29T05:00:00.000Z")), - ).toHaveLength(1); + test("the 2.10 row stays through its last day and is gone at Asia's next reset", async () => { + const at = async (now: string) => fandomVersionParser.parse(await html, ctx(now)).length; + expect(await at("2026-10-29T15:00:00.000Z")).toBe(1); + expect(await at("2026-10-29T19:59:00.000Z")).toBe(1); + expect(await at("2026-10-29T20:01:00.000Z")).toBe(0); }); test("a row without a readable end is skipped, not given one", () => { @@ -75,7 +76,8 @@ describe("fandomversion parser", () => { ]), ctx("2026-09-10T00:00:00.000Z"), ); - expect(events.map((e) => e.endsAt)).toEqual(["2026-10-12T00:00:00.000Z"]); + // End Date October 12 is the last day: Asia's reset opening the 13th. + expect(events.map((e) => e.endsAt)).toEqual(["2026-10-12T20:00:00.000Z"]); }); test("a source id with no entry emits nothing", () => {