feat(merge): pair a title with a qualifier in front of it
titleExtends caught a source appending words ("Bedazzling Dawnstar Sign-In") but not prepending them, which is the shape Game8's "Battle Pass - …" label took. The same qualifier at the other end now pairs too. The two-word floor holds at both ends, because a one-word base would fuse half of a patch day.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2924605bd4
commit
1f6b8fd1d6
3 files changed
+58
-7
No files matched your search
+7
-2
@@ -484,8 +484,13 @@ Only meaningful when a game has more than one source; a single-source game passe
|
||||
|
||||
1. **Same ID** → same event; keep the higher-confidence copy. Applies within a source as well as
|
||||
across sources — an identical id is the same row seen twice, whatever it is called.
|
||||
2. **Near match** — **different sources**, same game, title similarity ≥ 0.80, starts within 24h —
|
||||
→ same event under different titles; keep the higher-confidence copy.
|
||||
2. **Near match** — **different sources**, same game, starts within 24h, and either title similarity
|
||||
≥ 0.80 or one title is the other with words added at one end (`titleExtends`) — → same event
|
||||
under different titles; keep the higher-confidence copy. The second test exists because a source
|
||||
qualifier scores badly on overlap: `Bedazzling Dawnstar` / `… Sign-In` is 0.67. It reads both
|
||||
ends since 2026-10-14 — a qualifier can be a category label in front, as Game8's
|
||||
`Battle Pass - …` was — and both ends keep the two-word floor, because a one-word base would
|
||||
fuse half of a patch day.
|
||||
|
||||
**The survivor lists what it absorbed** in `mergedIds`, and the client moves readers' marks off
|
||||
those ids when the feed arrives — so a rule that starts fusing two published ids costs nobody a
|
||||
|
||||
+12
-5
@@ -169,19 +169,26 @@ function isSameEvent(
|
||||
}
|
||||
|
||||
/**
|
||||
* True when one title is the other with words appended — the shape a source
|
||||
* qualifier actually takes ("Bedazzling Dawnstar" → "… Sign-In").
|
||||
* True when one title is the other with words added at either end — the shape
|
||||
* a source qualifier actually takes. Appended: "Bedazzling Dawnstar" → "…
|
||||
* Sign-In". Prepended: Game8's "Battle Pass - Moontrace" beside Fandom's
|
||||
* "Moontrace", where the other source files the category in a column instead.
|
||||
*
|
||||
* Deliberately a prefix test, not a subset test. Subset matching would also
|
||||
* Deliberately an edge test, not a subset test. Subset matching would also
|
||||
* fuse "Gold Clash" with "Gold Rush Clash Royale", which are different events.
|
||||
* Two words is the floor: a one-word title would swallow half the calendar.
|
||||
* Two words is the floor at both ends: a one-word title would swallow half the
|
||||
* calendar.
|
||||
*/
|
||||
export function titleExtends(a: string, b: string): boolean {
|
||||
const ta = tokenList(a);
|
||||
const tb = tokenList(b);
|
||||
const [small, large] = ta.length <= tb.length ? [ta, tb] : [tb, ta];
|
||||
if (small.length < 2 || small.length === large.length) return false;
|
||||
return small.every((word, i) => large[i] === word);
|
||||
const offset = large.length - small.length;
|
||||
return (
|
||||
small.every((word, i) => large[i] === word) ||
|
||||
small.every((word, i) => large[offset + i] === word)
|
||||
);
|
||||
}
|
||||
|
||||
function findConflict(
|
||||
|
||||
@@ -160,6 +160,22 @@ describe("titleExtends", () => {
|
||||
test("refuses two titles of equal length", () => {
|
||||
expect(titleExtends("Gold Clash", "Gold Rush")).toBe(false);
|
||||
});
|
||||
|
||||
test("matches a title with a category label in front", () => {
|
||||
// Game8 heads Genshin's pass `Battle Pass - Moontrace` where Fandom says
|
||||
// `Moontrace`: the same qualifier, written at the other end.
|
||||
expect(titleExtends("Frozen Wilds", "Battle Pass - Frozen Wilds")).toBe(true);
|
||||
});
|
||||
|
||||
test("refuses a one-word title under a label, as at the end", () => {
|
||||
// The floor holds in both directions. `Moontrace` itself is made one ID by
|
||||
// the Game8 parser dropping the label, not by loosening this.
|
||||
expect(titleExtends("Moontrace", "Battle Pass - Moontrace")).toBe(false);
|
||||
});
|
||||
|
||||
test("refuses shared words in the middle", () => {
|
||||
expect(titleExtends("Gold Clash", "Royal Gold Clash Rush")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("merging two sources for one game", () => {
|
||||
@@ -191,6 +207,29 @@ describe("merging two sources for one game", () => {
|
||||
expect(conflicts[0]?.field).toBe("endsAt");
|
||||
});
|
||||
|
||||
test("collapses a leading-label duplicate and keeps the first copy's id", () => {
|
||||
const g8 = event({
|
||||
id: "genshin:battle-pass-frozen-wilds:2026-09-23",
|
||||
title: "Battle Pass - Frozen Wilds",
|
||||
startsAt: "2026-09-23T00:00:00.000Z",
|
||||
endsAt: "2026-11-02T00:00:00.000Z",
|
||||
sourceId: "genshin-game8-events",
|
||||
confidence: 0.85,
|
||||
});
|
||||
const fandom = event({
|
||||
id: "genshin:frozen-wilds:2026-09-23",
|
||||
title: "Frozen Wilds",
|
||||
startsAt: "2026-09-23T00:00:00.000Z",
|
||||
endsAt: "2026-11-02T00:00:00.000Z",
|
||||
sourceId: "genshin-fandom-events",
|
||||
confidence: 0.85,
|
||||
});
|
||||
const { events, conflicts } = mergeEvents([[g8], [fandom]]);
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0]?.id).toBe(g8.id);
|
||||
expect(conflicts).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("the survivor lists every id it absorbed", () => {
|
||||
const a = event({ id: "genshin:stygian-onslaught:2026-08-12", title: "Stygian Onslaught", sourceId: "s-a" });
|
||||
const b = event({ id: "genshin:stygian-onslaught-event:2026-08-12", title: "Stygian Onslaught Event", sourceId: "s-b" });
|
||||
|
||||
Reference in new issue
Block a user