diff --git a/AGENTS.md b/AGENTS.md index 5ddc676..ec341aa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -243,6 +243,14 @@ These come from how gacha games actually schedule things, and they cause most bu `docs/INGESTION.md`, which is specified and **not built**. So the rule is enforced per adapter, by a test, against a pinned fixture: a source added without a fixture in that table is a source the rule does not cover. +- **A redeem code is dead when the owner says so, even while its source says active** (owner, + 2026-10-20). Game8 kept `Y1INABQB6DEX`, `2BJ64QRZ7RT8` and `PLDXIB886Y1S` in Genshin's active + table after the game refused them. The owner's in-game check is recorded in `CODE_CHECKS` + (`src/shared/codeChecks.ts`): game, code, date. `build-codes` stamps the matching code + `confirmedDead`, and the reader sees it expired with "Confirmed expired in game, YYYY-MM-DD." Only + "dead" exists; never add a check from a guess, a comment thread or a code's age, and never delete + the code instead, because the source still owns the code, its reward and its link. A check whose + code the source has dropped is logged at build and pruned when convenient. ## Working on parsers diff --git a/docs/DESIGN.md b/docs/DESIGN.md index e520837..c58dd35 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -644,7 +644,8 @@ legend on the same baseline as "Next: …". date-only expiry counts as passed only once that date has ended everywhere (12:00 UTC the next day). Settings → Your games marks a followed game with "N new codes" until its Codes tab is opened (`codeSeen`); codes already expired or redeemed are not counted. Games the reader does not follow - get no marker. + get no marker. A code the owner confirmed dead in game (2026-10-20) is expired whatever the source + says, and reads "Confirmed expired in game, YYYY-MM-DD." in place of its expiry. - **The tour (2026-10-18, owner).** Every reader takes it once before using the app: new readers straight after setup, returning readers on their first visit, and readers who saw the earlier tour once more. The order is the owner's (2026-10-18): a welcome on its own over the dimmed Checklist, diff --git a/docs/INGESTION.md b/docs/INGESTION.md index aef3fa8..ce148f5 100644 --- a/docs/INGESTION.md +++ b/docs/INGESTION.md @@ -694,3 +694,13 @@ so it shows the in-game route only. **Refreshing.** These are game8 pages, so the CI caveat in AGENTS.md § Scraping conduct applies: if the runner is refused, they refresh by hand like the event pages. + +## Codes confirmed dead in game (2026-10-20) + +A source can keep a dead code in its active table: on 2026-10-20 Game8 still listed three Genshin +codes the game refused. `CODE_CHECKS` (`src/shared/codeChecks.ts`) records each code the owner +tried and found dead, with the date. `scripts/build-codes.ts` runs `applyCodeChecks` after parsing, +which sets `confirmedDead` on the matching code (by its `["game","CODE"]` id) and returns the checks +that matched nothing; the build logs those so they can be pruned. Nothing else about the code +changes, and the check is never used to add a code. The field is optional, so `schemaVersion` +stays 1. diff --git a/scripts/build-codes.ts b/scripts/build-codes.ts index b8134b0..349d4b2 100644 --- a/scripts/build-codes.ts +++ b/scripts/build-codes.ts @@ -1,6 +1,7 @@ import { mkdir, rename } from "node:fs/promises"; import { CODE_SOURCES } from "../src/ingest/codes/registry.ts"; -import { buildCodesFeed } from "../src/ingest/codes/build.ts"; +import { applyCodeChecks, buildCodesFeed } from "../src/ingest/codes/build.ts"; +import { CODE_CHECKS } from "../src/shared/codeChecks.ts"; import { CodesFeedSchema, type CodesFeed } from "../src/shared/codes.ts"; import { SnapshotStore, freshnessAt } from "../src/ingest/snapshots.ts"; import type { SourceHealth } from "../src/shared/feed.ts"; @@ -55,7 +56,16 @@ export async function buildCodes() { : false, }); } - const feed = CodesFeedSchema.parse({ schemaVersion: 1, codes, sources }); + const checked = applyCodeChecks(codes, CODE_CHECKS); + for (const c of checked.unmatched) + console.log( + `build-codes: check ${c.game} ${c.code} (${c.checked}) matches no code; prune it`, + ); + const feed = CodesFeedSchema.parse({ + schemaVersion: 1, + codes: checked.codes, + sources, + }); await mkdir(`${out}/data`, { recursive: true }); await Bun.write(`${out}/data/codes.v1.json`, JSON.stringify(feed)); await mkdir(root, { recursive: true }); diff --git a/src/client/components/redesign/CodesPage.tsx b/src/client/components/redesign/CodesPage.tsx index b832423..e7b8c43 100644 --- a/src/client/components/redesign/CodesPage.tsx +++ b/src/client/components/redesign/CodesPage.tsx @@ -69,13 +69,15 @@ export function CodesPage({ {c.reward &&
{c.reward}
} - {codeExpired(c, now) - ? "Expired" - : c.expiresAt - ? `Expires ${formatInZone(c.expiresAt, true, typeof state.prefs.timezone === "string" ? state.prefs.timezone : null)}` - : c.expiresOn - ? `Expires on ${c.expiresOn} (time not stated)` - : "Expiry not announced"}{" "} + {c.confirmedDead + ? `Confirmed expired in game, ${c.confirmedDead}.` + : codeExpired(c, now) + ? "Expired" + : c.expiresAt + ? `Expires ${formatInZone(c.expiresAt, true, typeof state.prefs.timezone === "string" ? state.prefs.timezone : null)}` + : c.expiresOn + ? `Expires on ${c.expiresOn} (time not stated)` + : "Expiry not announced"}{" "} ·{" "} Source diff --git a/src/client/state/reader/codes.ts b/src/client/state/reader/codes.ts index 54a96e8..a6ebc92 100644 --- a/src/client/state/reader/codes.ts +++ b/src/client/state/reader/codes.ts @@ -11,9 +11,11 @@ export function newCodes( * Whether a code's stated expiry has passed. A date with no time ("Expires on * 2026-10-20") counts as passed only once that date has ended everywhere, at * 12:00 UTC the next day, so a code is never shown expired while it may - * still redeem somewhere. + * still redeem somewhere. A code the owner confirmed dead in game is expired + * whatever it states. */ export function codeExpired(code: RedeemCode, now: number): boolean { + if (code.confirmedDead) return true; if (code.expiresAt) return Date.parse(code.expiresAt) <= now; if (code.expiresOn) return Date.parse(`${code.expiresOn}T00:00:00Z`) + 36 * 3600_000 <= now; diff --git a/src/ingest/codes/build.ts b/src/ingest/codes/build.ts index af9a832..43ffa47 100644 --- a/src/ingest/codes/build.ts +++ b/src/ingest/codes/build.ts @@ -1,4 +1,5 @@ import type { CodesFeed, RedeemCode } from "../../shared/codes.ts"; +import type { CodeCheck } from "../../shared/codeChecks.ts"; export function buildCodesFeed( previous: CodesFeed | null, parsed: readonly Omit