fix(ingest): read the Game8 block that holds the event blurbs

Every Wuthering Waves event published with no description, and two of them
did not publish at all. The page has always carried both: below the Ongoing
and Upcoming tables it repeats each live event as its own sub-section,
titled by an h4, over an `Event Duration` row and a paragraph of prose —
the shape Persona 5 uses, one heading level down. Two things kept the whole
block unreadable, and the second is why the first was never noticed.

`RANGE_LABEL` matched a bare `duration` and not `Event Duration`, so
`readLabelledDates` returned null for all ten of those tables and no
candidate was ever pushed — which is also why `summaryAfter`, whose only
job is that trailing paragraph, never ran. The label is now qualified the
way START_LABEL and END_LABEL already allow.

The other was the h4 rule. An unrecognised h4 could never name an event,
because Genshin uses h4 for sub-headings *inside* one ("Availability
Period") and letting a label claim the title renames the event to whatever
sits above its date table. But an h4 is all Wuthering Waves offers. Both
pages are served by the narrower rule: an h4 may fill an empty title, never
take one. Genshin's h3 has already claimed the slot before its labels
arrive; here the block sits under a section heading, so the slot is empty.
That only holds if the heading is recognised rather than fallen through,
since an unrecognised one is read as an event name and would spend the
title on the section — hence `events summary` in the vocabulary.

`dedupe` needed nothing. It already blends a missing summary onto the
better-dated copy for Persona 5 and never blends a date, which is what
keeps Gifts of Starpath's end on August 19 despite the block spelling it
"Auguts".

The fixture goes from ten events to twelve — `Recaptured: Action
Highlights` and `Mingshen Notices` are dated nowhere else on the page — and
nine of the ten blanks fill in. Ascendant Aces stays blank because
`Unlocked by default` is the only prose Game8 ever wrote for it, which is
what `isRequirementOnly` is for, and a test pins it there. Diffed across
every pinned fixture and all nine live Game8 snapshots: the only other
movement is Gifts of Drifting Mist gaining its blurb, and no date, no
count and no other source moved.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-20 22:38:41 +02:00
co-authored by Claude Opus 5
parent ae916a3f76
commit 67bbeaf28c
3 changed files with 119 additions and 25 deletions
@@ -4,7 +4,7 @@
"game": "wuwa",
"title": "A Glimpse of Xuanfang",
"type": "other",
"summary": null,
"summary": "A Glimpse of Xuanfang is an exploration based event that tasks players to complete quests, puzzles or other designated actions to fill up progress percentage.",
"startsAt": "2026-07-10T00:00:00.000Z",
"startPrecision": "day",
"endsAt": "2026-08-19T00:00:00.000Z",
@@ -25,7 +25,7 @@
"game": "wuwa",
"title": "Gifts of Aftertune",
"type": "other",
"summary": null,
"summary": "Gifts of Aftertune is a log-in based event where players can get 5 Lustrous Tides and 5 Radiant Tides after logging in for a total of 7 days .",
"startsAt": "2026-07-10T00:00:00.000Z",
"startPrecision": "day",
"endsAt": "2026-08-19T00:00:00.000Z",
@@ -46,7 +46,7 @@
"game": "wuwa",
"title": "Gifts of Starpath",
"type": "other",
"summary": null,
"summary": "Gifts of Starpath is a convene based reward event where characters can earn additional Radiant Tides by pulling on the Starpath Reverbs banner.",
"startsAt": "2026-07-10T00:00:00.000Z",
"startPrecision": "day",
"endsAt": "2026-08-19T00:00:00.000Z",
@@ -62,12 +62,33 @@
"firstSeenAt": "2026-08-14T00:00:00.000Z",
"updatedAt": "2026-08-14T00:00:00.000Z"
},
{
"id": "wuwa:mingshen-notices:2026-07-10",
"game": "wuwa",
"title": "Mingshen Notices",
"type": "other",
"summary": null,
"startsAt": "2026-07-10T00:00:00.000Z",
"startPrecision": "day",
"endsAt": null,
"endPrecision": "unknown",
"regionScoped": false,
"regionEnds": null,
"sourceUrl": "https://game8.co/games/Wuthering-Waves/archives/453473",
"sourceId": "wuwa-game8-events",
"status": "published",
"confidence": 0.75,
"extractionMethod": "parser",
"version": 1,
"firstSeenAt": "2026-08-14T00:00:00.000Z",
"updatedAt": "2026-08-14T00:00:00.000Z"
},
{
"id": "wuwa:shape-of-yesterday:2026-07-10",
"game": "wuwa",
"title": "Shape of Yesterday",
"type": "other",
"summary": null,
"summary": "Shape of Yesterday tasks players to take photos at specific locations around Xuanfang Hold. Some of these locations may require quest progression to unlock.",
"startsAt": "2026-07-10T00:00:00.000Z",
"startPrecision": "day",
"endsAt": null,
@@ -88,7 +109,7 @@
"game": "wuwa",
"title": "Lament Recon: Tacet Crisis",
"type": "other",
"summary": null,
"summary": "In Lament Recon: Tacet Crisis, defeat waves of enemies by upgrading character skills and projectile related enhancements as you navigate the battlefield with your character.",
"startsAt": "2026-07-11T00:00:00.000Z",
"startPrecision": "day",
"endsAt": "2026-08-19T00:00:00.000Z",
@@ -104,6 +125,27 @@
"firstSeenAt": "2026-08-14T00:00:00.000Z",
"updatedAt": "2026-08-14T00:00:00.000Z"
},
{
"id": "wuwa:recaptured-action-highlights:2026-07-16",
"game": "wuwa",
"title": "Recaptured: Action Highlights",
"type": "other",
"summary": "Recaptured: Action Highlights is a combat-photography event where players have to capture specific character actions through their idles or specific combos. Players can also add filters or effects for an additional layer of creativity.",
"startsAt": "2026-07-16T00:00:00.000Z",
"startPrecision": "day",
"endsAt": "2026-08-06T00:00:00.000Z",
"endPrecision": "day",
"regionScoped": false,
"regionEnds": null,
"sourceUrl": "https://game8.co/games/Wuthering-Waves/archives/453473",
"sourceId": "wuwa-game8-events",
"status": "published",
"confidence": 0.85,
"extractionMethod": "parser",
"version": 1,
"firstSeenAt": "2026-08-14T00:00:00.000Z",
"updatedAt": "2026-08-14T00:00:00.000Z"
},
{
"id": "wuwa:ascendant-aces:2026-07-30",
"game": "wuwa",
@@ -130,7 +172,7 @@
"game": "wuwa",
"title": "In Search of Lost Jade",
"type": "other",
"summary": null,
"summary": "In Search of Lost Jade is a web browser event where players accompany a Jadeplume Eagle out of a cave and back to its nest.",
"startsAt": "2026-07-30T00:00:00.000Z",
"startPrecision": "day",
"endsAt": "2026-08-13T00:00:00.000Z",
@@ -151,7 +193,7 @@
"game": "wuwa",
"title": "Virtual Crisis: Quadrant Trials",
"type": "other",
"summary": null,
"summary": "Virtual Crisis: Quadrant Trials is a boss focused combat event where players get to increase the difficulty between each stage by selecting boss upgrades or help their own characters by selecting self-buffs.",
"startsAt": "2026-07-30T00:00:00.000Z",
"startPrecision": "day",
"endsAt": "2026-08-19T00:00:00.000Z",
@@ -172,7 +214,7 @@
"game": "wuwa",
"title": "Lollo Campaign: New Journey",
"type": "other",
"summary": null,
"summary": "Lollo Campaign: New Journey is the newest iteration of the Lollo Campaign event where players get to earn stamps by logging in and spending Waveplates for a chance to spin a reward wheel.",
"startsAt": "2026-08-06T00:00:00.000Z",
"startPrecision": "day",
"endsAt": "2026-08-19T00:00:00.000Z",
+20 -7
View File
@@ -52,6 +52,14 @@ const INCLUDED_SECTIONS = [
// nothing.
/list of (all )?banners/i,
/current banners/i,
// Wuthering Waves follows its listing tables with a block that repeats every
// live event as its own sub-section — the shape Persona 5 uses, and the only
// place on the page the blurbs live. The heading names a version
// ("3.6 Events Summary"), so it is matched on the two words it always ends
// with. It has to be *recognised* rather than merely fallen through: an
// unrecognised heading is read as an event name, which would put the section
// title where the first event's should be.
/events summary/i,
];
/**
@@ -82,7 +90,8 @@ const EXCLUDED_SECTIONS = [
const START_LABEL = /^(event|test run|banner)?\s*start(\s+date)?$/i;
const END_LABEL = /^(event|test run|banner)?\s*end(\s+date)?$/i;
/** Label/value rows carrying a whole range in one cell. */
const RANGE_LABEL = /^(availability period|event period|duration|period|dates)$/i;
const RANGE_LABEL =
/^(availability period|event period|(event )?duration|period|dates)$/i;
/** Column-table header matchers. */
const COL_TITLE = /^(.*\b)?(events?|banners?)$/i;
@@ -133,12 +142,16 @@ export function parseGame8EventsPage(
} else if (INCLUDED_SECTIONS.some((re) => re.test(heading))) {
sectionIncluded = true;
currentTitle = null;
} else if (node.kind !== "h4") {
// An unrecognised h2/h3 names an event. An unrecognised h4 does not —
// Genshin uses them for sub-headings *within* one event ("Availability
// Period", "Characters & Rewards for this Test Run"), so letting one
// claim the title would rename "Character Test Runs" to the label above
// its own date table.
} else if (node.kind !== "h4" || currentTitle === null) {
// An unrecognised h2/h3 names an event. An unrecognised h4 may only
// *fill* an empty title, never take one: Genshin uses h4 for
// sub-headings *within* one event ("Availability Period", "Characters &
// Rewards for this Test Run"), and there the event's own h3 has already
// claimed the slot, so letting the label overwrite it would rename
// "Character Test Runs" to whatever sits above its date table. Where
// the slot is empty the h4 is all there is — Wuthering Waves names each
// sub-section of its events-summary block with one, and a reader that
// refuses it publishes the section heading once and drops the rest.
currentTitle = heading;
}
continue;
+49 -10
View File
@@ -217,16 +217,6 @@ describe("new source shapes", () => {
expect(open.length).toBeGreaterThan(0);
for (const e of open) expect(e.endPrecision).toBe("unknown");
});
test("wuwa parses ranges carrying a year on both sides", async () => {
const events = await runAdapter(
adapter("wuwa-game8-events"),
"fixtures/wuwa/game8-events-2026-08-14",
);
const jade = events.find((e) => e.title === "In Search of Lost Jade");
expect(jade?.startsAt).toBe("2026-07-30T00:00:00.000Z");
expect(jade?.endsAt).toBe("2026-08-13T00:00:00.000Z");
});
});
describe("endfield", () => {
@@ -341,6 +331,55 @@ describe("p5x", () => {
});
});
describe("wuwa", () => {
const fixture = "fixtures/wuwa/game8-events-2026-08-14";
test("recovers each event's blurb from its own h4 section", async () => {
// The listing table's second column holds unlock conditions, not prose. The
// description lives further down, in a per-event section whose title is an
// h4 and whose dates are labelled `Event Duration` — so both the section
// reader and the range vocabulary have to admit it before the blurb the
// page has always carried can reach a reader.
const events = await runAdapter(adapter("wuwa-game8-events"), fixture);
const shape = events.find((e) => e.title === "Shape of Yesterday");
expect(shape?.summary).toContain("photos at specific locations");
const jade = events.find((e) => e.title === "In Search of Lost Jade");
expect(jade?.summary).toContain("web browser event");
});
test("the enclosing summary heading never becomes an event", async () => {
// `3.5 Events Summary` heads the block; the h4s inside it name the events.
// A reader that lets the h3 keep the title publishes the section as an
// event and swallows every h4 below it.
const events = await runAdapter(adapter("wuwa-game8-events"), fixture);
expect(events.map((e) => e.title)).not.toContain("3.5 Events Summary");
});
test("publishes the events that only the summary block dates", async () => {
// Neither of these appears in the Ongoing or Upcoming tables, so before the
// summary block was readable they were silently absent from the calendar.
const events = await runAdapter(adapter("wuwa-game8-events"), fixture);
const titles = events.map((e) => e.title);
expect(titles).toContain("Recaptured: Action Highlights");
expect(titles).toContain("Mingshen Notices");
});
test("an unlock condition is still not a description", async () => {
// "Unlocked by default" sits where the blurb would go and the page offers
// no prose for this event anywhere, so the slot stays empty.
const events = await runAdapter(adapter("wuwa-game8-events"), fixture);
const aces = events.find((e) => e.title === "Ascendant Aces");
expect(aces?.summary).toBeNull();
});
test("parses ranges carrying a year on both sides", async () => {
const events = await runAdapter(adapter("wuwa-game8-events"), fixture);
const jade = events.find((e) => e.title === "In Search of Lost Jade");
expect(jade?.startsAt).toBe("2026-07-30T00:00:00.000Z");
expect(jade?.endsAt).toBe("2026-08-13T00:00:00.000Z");
});
});
describe("arknights wiki", () => {
const fixture = "fixtures/arknights/akwiki-events-2026-08-17";
const akwiki = adapter("arknights-akwiki-events");