feat: add shared time, games and feed contracts

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]>
This commit is contained in:
Lucas Winther
2026-08-15 00:28:17 +02:00
co-authored by Claude Opus 5
parent 3b2feadf5d
commit 45ad5b39da
4 changed files with 325 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
import { z } from "zod";
import { GachaEvent, GameId } from "./schema.ts";
/**
* The wire contract between server and client.
*
* The client refuses a `schemaVersion` it does not know rather than guessing at
* unfamiliar fields. Additive fields do not bump it; removing or retyping one
* does. See docs/DATA-MODEL.md § Schema versioning.
*/
export const SCHEMA_VERSION = 1;
export const SourceHealth = z.object({
sourceId: z.string(),
game: GameId,
url: z.string().url(),
lastSuccessAt: z.string().datetime().nullable(),
eventCount: z.number().int().nonnegative(),
});
export const EventFeed = z.object({
schemaVersion: z.literal(SCHEMA_VERSION),
generatedAt: z.string().datetime(),
events: z.array(GachaEvent),
sources: z.array(SourceHealth),
});
export type SourceHealth = z.infer<typeof SourceHealth>;
export type EventFeed = z.infer<typeof EventFeed>;