fix(ingest): keep an unannounced end out of the event blurb
The open-end vocabulary is written down twice and the two copies have to agree. `parseOpenRange` decides whether a row is datable; `RANGE_PREFIX` strips the range off the front of the cell so `proseAfterDates` can recover the blurb behind it. A word the first knows and the second does not still yields the event — with the leftover end tacked onto the summary a reader actually sees. That is what happened to all eight Endfield rows the previous commit recovered: `- TBA Sign-in to get extra pulls for Typhoeus!`. Adding `tba` beside `tbd` fixes it, and a comment now says why the lists travel together, since the next one of these will be found the same way — by reading generated fixture output. Pins the 1.5 page beside the 1.4 one rather than replacing it. Fixtures are permanent and these are different table shapes, not different data: 1.4 is a single dated row, 1.5 is nine rows of open ends. The group name gains the fixture path now that one adapter has two. Nine rows, eight events. `Ridgeline Flows of Autumn Sign-In` reads `Period: TBA` with no start at all, so it stays undatable and dropped. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
843e27b4f1
commit
0307d24b92
@@ -0,0 +1,170 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "endfield:fletched-irontip-sign-in:2026-09-02",
|
||||||
|
"game": "endfield",
|
||||||
|
"title": "Fletched Irontip Sign-In",
|
||||||
|
"type": "login",
|
||||||
|
"summary": "Sign-in to get extra pulls for Typhoeus!",
|
||||||
|
"startsAt": "2026-09-02T00:00:00.000Z",
|
||||||
|
"startPrecision": "day",
|
||||||
|
"endsAt": null,
|
||||||
|
"endPrecision": "unknown",
|
||||||
|
"regionScoped": false,
|
||||||
|
"regionEnds": null,
|
||||||
|
"sourceUrl": "https://game8.co/games/Arknights-Endfield/archives/535443",
|
||||||
|
"sourceId": "endfield-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": "endfield:snow-over-deep-woods:2026-09-02",
|
||||||
|
"game": "endfield",
|
||||||
|
"title": "Snow Over Deep Woods",
|
||||||
|
"type": "other",
|
||||||
|
"summary": "Explore Snowy Forest and complete missions to get various rewards!",
|
||||||
|
"startsAt": "2026-09-02T00:00:00.000Z",
|
||||||
|
"startPrecision": "day",
|
||||||
|
"endsAt": null,
|
||||||
|
"endPrecision": "unknown",
|
||||||
|
"regionScoped": false,
|
||||||
|
"regionEnds": null,
|
||||||
|
"sourceUrl": "https://game8.co/games/Arknights-Endfield/archives/535443",
|
||||||
|
"sourceId": "endfield-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": "endfield:trial-of-the-bow:2026-09-09",
|
||||||
|
"game": "endfield",
|
||||||
|
"title": "Trial of the Bow",
|
||||||
|
"type": "challenge",
|
||||||
|
"summary": "Equip Arrow Chips to complete trainings and exercises for rewards!",
|
||||||
|
"startsAt": "2026-09-09T00:00:00.000Z",
|
||||||
|
"startPrecision": "day",
|
||||||
|
"endsAt": null,
|
||||||
|
"endPrecision": "unknown",
|
||||||
|
"regionScoped": false,
|
||||||
|
"regionEnds": null,
|
||||||
|
"sourceUrl": "https://game8.co/games/Arknights-Endfield/archives/535443",
|
||||||
|
"sourceId": "endfield-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": "endfield:aic-support-chubby-lung-attacks:2026-09-16",
|
||||||
|
"game": "endfield",
|
||||||
|
"title": "AIC Support: Chubby Lung Attacks",
|
||||||
|
"type": "other",
|
||||||
|
"summary": "Participate in AIC Factory production and trade support goods for rewards!",
|
||||||
|
"startsAt": "2026-09-16T00:00:00.000Z",
|
||||||
|
"startPrecision": "day",
|
||||||
|
"endsAt": null,
|
||||||
|
"endPrecision": "unknown",
|
||||||
|
"regionScoped": false,
|
||||||
|
"regionEnds": null,
|
||||||
|
"sourceUrl": "https://game8.co/games/Arknights-Endfield/archives/535443",
|
||||||
|
"sourceId": "endfield-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": "endfield:echoing-bell-of-an-old-city:2026-09-24",
|
||||||
|
"game": "endfield",
|
||||||
|
"title": "Echoing Bell of an Old City",
|
||||||
|
"type": "other",
|
||||||
|
"summary": "Explore Suiming and complete missions to get various rewards!",
|
||||||
|
"startsAt": "2026-09-24T00:00:00.000Z",
|
||||||
|
"startPrecision": "day",
|
||||||
|
"endsAt": null,
|
||||||
|
"endPrecision": "unknown",
|
||||||
|
"regionScoped": false,
|
||||||
|
"regionEnds": null,
|
||||||
|
"sourceUrl": "https://game8.co/games/Arknights-Endfield/archives/535443",
|
||||||
|
"sourceId": "endfield-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": "endfield:purry-big-feline-rawr:2026-09-24",
|
||||||
|
"game": "endfield",
|
||||||
|
"title": "Purry Big Feline! RAWR!",
|
||||||
|
"type": "other",
|
||||||
|
"summary": null,
|
||||||
|
"startsAt": "2026-09-24T00:00:00.000Z",
|
||||||
|
"startPrecision": "day",
|
||||||
|
"endsAt": null,
|
||||||
|
"endPrecision": "unknown",
|
||||||
|
"regionScoped": false,
|
||||||
|
"regionEnds": null,
|
||||||
|
"sourceUrl": "https://game8.co/games/Arknights-Endfield/archives/535443",
|
||||||
|
"sourceId": "endfield-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": "endfield:orbipom-merge:2026-10-01",
|
||||||
|
"game": "endfield",
|
||||||
|
"title": "OrbiPom! MERGE!",
|
||||||
|
"type": "other",
|
||||||
|
"summary": "Merge identical OrbiPom to complete your collection and find the Golden Endmin!",
|
||||||
|
"startsAt": "2026-10-01T00:00:00.000Z",
|
||||||
|
"startPrecision": "day",
|
||||||
|
"endsAt": null,
|
||||||
|
"endPrecision": "unknown",
|
||||||
|
"regionScoped": false,
|
||||||
|
"regionEnds": null,
|
||||||
|
"sourceUrl": "https://game8.co/games/Arknights-Endfield/archives/535443",
|
||||||
|
"sourceId": "endfield-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": "endfield:runners-steeplechase:2026-10-01",
|
||||||
|
"game": "endfield",
|
||||||
|
"title": "Runners' Steeplechase",
|
||||||
|
"type": "other",
|
||||||
|
"summary": "Gather Chubby Lung Balloons along the course and reach the finish line to obtain rewards!",
|
||||||
|
"startsAt": "2026-10-01T00:00:00.000Z",
|
||||||
|
"startPrecision": "day",
|
||||||
|
"endsAt": null,
|
||||||
|
"endPrecision": "unknown",
|
||||||
|
"regionScoped": false,
|
||||||
|
"regionEnds": null,
|
||||||
|
"sourceUrl": "https://game8.co/games/Arknights-Endfield/archives/535443",
|
||||||
|
"sourceId": "endfield-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"
|
||||||
|
}
|
||||||
|
]
|
||||||
File diff suppressed because one or more lines are too long
@@ -362,15 +362,19 @@ const DATED_YEAR = String.raw`(?:\d{1,2}/\d{1,2}/\d{2,4}|[A-Za-z]+\.?\s+\d{1,2},
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A leading range, whose end may be a date or a stated non-date such as
|
* A leading range, whose end may be a date or a stated non-date such as
|
||||||
* "Permanent" or "End of 4.6". Those words are listed rather than matched
|
* "Permanent", "TBA" or "End of 4.6". Those words are listed rather than
|
||||||
* loosely, so a real description is never mistaken for a range end.
|
* matched loosely, so a real description is never mistaken for a range end.
|
||||||
|
*
|
||||||
|
* The list must track `parseOpenRange`'s: a shape that reader dates but this
|
||||||
|
* one does not strip still yields the event, with the leftover end tacked onto
|
||||||
|
* the front of its summary — "- TBA Sign-in to get extra pulls for Typhoeus!".
|
||||||
*/
|
*/
|
||||||
const RANGE_PREFIX = new RegExp(
|
const RANGE_PREFIX = new RegExp(
|
||||||
String.raw`^\s*` +
|
String.raw`^\s*` +
|
||||||
ONE_DATE +
|
ONE_DATE +
|
||||||
String.raw`(?:\s*[-\u2013\u2014]\s*(?:` +
|
String.raw`(?:\s*[-\u2013\u2014]\s*(?:` +
|
||||||
ONE_DATE +
|
ONE_DATE +
|
||||||
String.raw`|permanent|tbd|ongoing|end of [\d.]+)|\s+` +
|
String.raw`|permanent|tba|tbd|ongoing|end of [\d.]+)|\s+` +
|
||||||
DATED_YEAR +
|
DATED_YEAR +
|
||||||
String.raw`)?\s*`,
|
String.raw`)?\s*`,
|
||||||
"i",
|
"i",
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ const CASES: Array<{ adapter: Adapter; fixture: string }> = [
|
|||||||
{ adapter: adapter("wuwa-game8-events"), fixture: "fixtures/wuwa/game8-events-2026-08-14" },
|
{ adapter: adapter("wuwa-game8-events"), fixture: "fixtures/wuwa/game8-events-2026-08-14" },
|
||||||
{ adapter: adapter("zzz-game8-events"), fixture: "fixtures/zzz/game8-events-2026-08-14" },
|
{ adapter: adapter("zzz-game8-events"), fixture: "fixtures/zzz/game8-events-2026-08-14" },
|
||||||
{ adapter: adapter("endfield-game8-events"), fixture: "fixtures/endfield/game8-events-2026-08-14" },
|
{ adapter: adapter("endfield-game8-events"), fixture: "fixtures/endfield/game8-events-2026-08-14" },
|
||||||
|
// The same page a version later. 1.4's upcoming table held one dated row;
|
||||||
|
// 1.5's holds nine, every one of them ending "- TBA". Both are pinned because
|
||||||
|
// they are different shapes, not different data.
|
||||||
|
{ adapter: adapter("endfield-game8-events"), fixture: "fixtures/endfield/game8-events-2026-08-23" },
|
||||||
{ adapter: adapter("endfield-wikigg-events"), fixture: "fixtures/endfield/wikigg-events-2026-08-14" },
|
{ adapter: adapter("endfield-wikigg-events"), fixture: "fixtures/endfield/wikigg-events-2026-08-14" },
|
||||||
{ adapter: adapter("p5x-game8-events"), fixture: "fixtures/p5x/game8-events-2026-08-17" },
|
{ adapter: adapter("p5x-game8-events"), fixture: "fixtures/p5x/game8-events-2026-08-17" },
|
||||||
{ adapter: adapter("arknights-akwiki-events"), fixture: "fixtures/arknights/akwiki-events-2026-08-17" },
|
{ adapter: adapter("arknights-akwiki-events"), fixture: "fixtures/arknights/akwiki-events-2026-08-17" },
|
||||||
@@ -59,7 +63,7 @@ async function runAdapter(adapter: Adapter, fixture: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describe.each(CASES)("$adapter.id", ({ adapter, fixture }) => {
|
describe.each(CASES)("$adapter.id $fixture", ({ adapter, fixture }) => {
|
||||||
test("matches the checked-in expected output", async () => {
|
test("matches the checked-in expected output", async () => {
|
||||||
const events = await runAdapter(adapter, fixture);
|
const events = await runAdapter(adapter, fixture);
|
||||||
const expected = await Bun.file(`${fixture}.expected.json`).json();
|
const expected = await Bun.file(`${fixture}.expected.json`).json();
|
||||||
@@ -175,6 +179,35 @@ describe("nte fixture specifics", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("endfield fixture specifics", () => {
|
||||||
|
const fixture = "fixtures/endfield/game8-events-2026-08-23";
|
||||||
|
|
||||||
|
test("reads an upcoming version whose ends are unannounced", async () => {
|
||||||
|
// Game8 published the whole 1.5 schedule as "Period: 09/02/2026 - TBA".
|
||||||
|
// Nine rows; the ninth reads "Period: TBA" with no start at all, so it is
|
||||||
|
// undatable and dropped rather than guessed.
|
||||||
|
const events = await runAdapter(adapter("endfield-game8-events"), fixture);
|
||||||
|
expect(events).toHaveLength(8);
|
||||||
|
|
||||||
|
const signIn = events.find((e) => e.title === "Fletched Irontip Sign-In");
|
||||||
|
expect(signIn?.startsAt).toBe("2026-09-02T00:00:00.000Z");
|
||||||
|
expect(signIn?.endsAt).toBeNull();
|
||||||
|
expect(events.map((e) => e.title)).not.toContain(
|
||||||
|
"Ridgeline Flows of Autumn Sign-In",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps the unannounced end out of the summary", async () => {
|
||||||
|
// The range is structure, not prose. Left in, "- TBA" reaches the reader as
|
||||||
|
// the opening words of every blurb on the page.
|
||||||
|
const events = await runAdapter(adapter("endfield-game8-events"), fixture);
|
||||||
|
expect(
|
||||||
|
events.find((e) => e.title === "Fletched Irontip Sign-In")?.summary,
|
||||||
|
).toBe("Sign-in to get extra pulls for Typhoeus!");
|
||||||
|
for (const e of events) expect(e.summary ?? "").not.toMatch(/\bTBA\b/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("inferType", () => {
|
describe("inferType", () => {
|
||||||
const cases: Array<[string, EventType]> = [
|
const cases: Array<[string, EventType]> = [
|
||||||
["Overflowing Abundance Rerun", "rerun"],
|
["Overflowing Abundance Rerun", "rerun"],
|
||||||
|
|||||||
Reference in New Issue
Block a user