feat(holodori): track hololive Dreams from its own wiki
New holodoriwiki parser plus the holodori-holodoriwiki-events source, giving the game its two live and upcoming events at exact precision. holodori.wiki is Miraheze, so this is the same call bawiki makes and for the same reason: robots.txt disallows /w/ and /*?action=, which closes the API route and leaves the rendered /wiki/Events page as the surface * is allowed. It serves our own User-Agent a 200, sets no Content-Signal and no Crawl-delay for us, and the wiki is CC BY-SA 4.0. What makes this source unusually good is that it states its timezone on every cell — 08/17/2026 8:00PM (JST) — so it is the only wiki source here publishing exact precision on both boundaries without a per-region timer. parseSlashClockZone requires that zone rather than defaulting to UTC: a row that ever loses it drops out instead of silently landing nine hours off, and an abbreviation absent from the table is refused rather than guessed, which is why the table holds only JST and not the plausible-looking candidates that are not fixed offsets. The 12-hour clock gets its own handling and its own tests, because 12PM and 12AM are the two readings a naive parser gets wrong while still producing a valid-looking instant. Inclusion is fenced by the Current Events <h2> and bounded by the next one. That matters more than it looks: Past Events sits directly below with identical columns, so a reader taking every wikitable would put the back catalogue on the calendar with nothing to mark it. Rows are still checked against ctx.now on top of the heading, because Current is maintained by hand and goes stale before anyone moves a row down. Two rows are deliberately missing. Beginner Mission runs Game Launch → Unknown, and no start means no event ID — a permanent tutorial chore is not what a calendar of deadlines is for. An Unknown end is kept as endsAt null, and unlike bawiki this parser does not drop a started-but-undated row: the heading has already said the event is running, which is the fact bawiki lacks when reading an archive. Every event title is still a red link — the wiki has no article for any of them yet — so each points at ?action=edit&redlink=1, a create-page form on a path this robots.txt disallows. Refusing a href with a query keeps those out and falls back to the events page; the articles get linked when they exist, with no change here. holodori gets resetOffsets of UTC+9 across all three regions, evidenced rather than assumed the way Arknights' is: the game launched worldwide simultaneously on one service, every boundary is stated in JST, and Training Support Missions ends at 3:59AM JST, one minute before a 04:00 local reset. Only the offset is overridden; the hour is the default. Setting it now is free, because no reader has a day key for a game the app has never shipped — the same override a year from now would re-label ticks already logged. Both boundaries and the exclusions are asserted against an independent extraction off the fixture, computed through a timezone library rather than by the parser's own arithmetic. Several counts in AGENTS.md and PRD.md were already stale before this — the Fate/Grand Order source did not update them — and are corrected to what the tree now holds rather than to what it held yesterday. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f3d0df9fda
commit
7ab801f409
@@ -8,6 +8,7 @@ import {
|
||||
parseMonthDayYear,
|
||||
parseOpenRange,
|
||||
parseOrdinalDateTimeRange,
|
||||
parseSlashClockZone,
|
||||
parseSlashDateTimeRange,
|
||||
parseYearFirstSlashRange,
|
||||
} from "../src/ingest/dates.ts";
|
||||
@@ -367,3 +368,51 @@ describe("parseIsoDay", () => {
|
||||
expect(parseIsoDay("")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseSlashClockZone", () => {
|
||||
test("converts a stated JST wall clock to UTC", () => {
|
||||
expect(parseSlashClockZone("08/17/2026 8:00PM (JST)")).toEqual({
|
||||
iso: "2026-08-17T11:00:00.000Z",
|
||||
precision: "exact",
|
||||
});
|
||||
});
|
||||
|
||||
test("a small-hours JST boundary lands on the previous UTC day", () => {
|
||||
// The shift that makes storing the source's own wall clock unusable.
|
||||
expect(parseSlashClockZone("08/30/2026 3:59AM (JST)")?.iso).toBe(
|
||||
"2026-08-29T18:59:00.000Z",
|
||||
);
|
||||
});
|
||||
|
||||
test("12PM is noon and 12AM is midnight", () => {
|
||||
expect(parseSlashClockZone("08/20/2026 12:00PM (JST)")?.iso).toBe(
|
||||
"2026-08-20T03:00:00.000Z",
|
||||
);
|
||||
expect(parseSlashClockZone("08/21/2026 12:00AM (JST)")?.iso).toBe(
|
||||
"2026-08-20T15:00:00.000Z",
|
||||
);
|
||||
});
|
||||
|
||||
test("an unstated or unknown zone is null, never UTC", () => {
|
||||
expect(parseSlashClockZone("08/20/2026 12:00PM")).toBeNull();
|
||||
// Not in the table, and deliberately: `CST` names three different zones.
|
||||
expect(parseSlashClockZone("08/20/2026 12:00PM (CST)")).toBeNull();
|
||||
});
|
||||
|
||||
test("rejects a non-date and an impossible calendar day", () => {
|
||||
expect(parseSlashClockZone("Game Launch")).toBeNull();
|
||||
expect(parseSlashClockZone("Unknown")).toBeNull();
|
||||
// Validated on the stated local fields, before the offset shifts anything:
|
||||
// converting first would quietly turn Feb 30 into a real instant in March.
|
||||
expect(parseSlashClockZone("02/30/2026 12:00PM (JST)")).toBeNull();
|
||||
expect(parseSlashClockZone("08/20/2026 13:00PM (JST)")).toBeNull();
|
||||
});
|
||||
|
||||
test("is a whole-cell reader, not a scanner", () => {
|
||||
// Anchored at both ends: letting this match mid-prose is how a reader
|
||||
// starts finding dates inside sentences.
|
||||
expect(
|
||||
parseSlashClockZone("Starts 08/20/2026 12:00PM (JST) after maintenance"),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user