Completions and prefs live in localStorage and never reach the server —
there is no account and nothing to log into. Reads never throw, so a corrupt
value costs a preference rather than the whole screen.
Import merges and never removes: a completion present on either side stays
completed, because nothing else holds a copy to restore from.
The feed client refuses a schemaVersion it does not know rather than
guessing at unfamiliar fields.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Tailwind v4 @theme carries the two colour axes as tokens: a deep blue-black
ground, per-game hues for identity, and a heat ramp for urgency that is
monotonic in salience (dim, blue, amber, red).
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Emits exactly the shape GET /api/events.json will serve, so the client can
be developed against real parsed data before the server and database land.
Reads fixtures only, never the network.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Time is this product's subject, so the vocabulary lives in one module:
remaining, progress through a window, and urgency. Urgency is derived from
absolute time left, deliberately independent of proportion — a 90-day event
with three hours left is as urgent as a 3-day one.
An unannounced end is never urgent and has no progress: "we don't know" and
"loads of time" are different facts, and conflating them is what this
product exists to avoid.
Games carry a hue for identity only. Urgency is a separate axis, so one
glance answers both "whose event is this?" and "how long have I got?".
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Removes the extraction-evaluator agent and reframes adapter-author around
reusing an existing site parser before writing a new one.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Event data is parsed deterministically; there is no model call, API key or
per-run cost anywhere in the pipeline. A source that cannot be parsed
deterministically gets no adapter rather than an inference fallback.
Documents the parser/adapter/merge split, records that Game8 uses three
page templates, and adds Neverness to Everness.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Collapses the same event seen by two sources, matching on ID or on title
similarity plus start-date proximity. Proximity is the real guard against
false positives, since a rerun reuses its name months later.
Agreement from an independent source raises confidence; the same row seen
twice in one document does not. Disagreement on an end date is recorded as
a conflict for the review gate rather than averaged — splitting the
difference would publish a date neither source asserts.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Parsers are keyed by site template, not game: one Game8 parser serves
every Game8 page, and adapters bind a URL and game to a parser. Adding a
source for a known site is one SOURCES entry.
Game8 uses at least three templates. This handles label/value detail
tables (Genshin, 9 events) and column tables (NTE, 13 events). Its
image-grid schedule has no year and no end date, so Arknights: Endfield
is not supportable from that source and gets no adapter.
canParse is a structural tripwire: a site redesign fails the run loudly
instead of publishing an empty calendar that reads as "no events".
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Every date function returns null rather than inferring a missing year,
month or end date. A source that does not state an end yields
endsAt: null, never a plausible guess — a confidently wrong end date is
the failure this product exists to prevent.
Handles the three formats seen on Game8: "August 12, 2026",
"August 12 - September 21, 2026" (year applies to both ends, rolling the
start back when the range crosses New Year), and a slash datetime range.
Abbreviated months parse too, so a source switching to "Apr. 29" does not
silently drop events.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Schema-level invariants enforce that endsAt is null exactly when
endPrecision is "unknown", and that regionEnds is populated exactly when
regionScoped is set, so an event object cannot be constructed in a state
the UI would misrender.
Event IDs double as localStorage keys on the client, so slugify() and
eventId() carry a warning: changing them orphans every user's completion
marks with no server-side recovery.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Bun is the runtime, bundler, test runner and SQLite driver, so zod is the
only dependency. Strict TS with noUncheckedIndexedAccess.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>