feat(routines): a built-in can roll on one server's reset for everyone

Routine gains clock?: Region, code only. Its day key, its countdown and
its catch-up days are cut on that server's reset rather than the
reader's: for a cycle that ends at one instant on every server. Make it
mine leaves it behind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-16 05:31:28 +02:00
1 parent be3c1408d2
commit 8eef068332
7 files changed
+83 -10

No files matched your search

+8 -1
View File
@@ -1,6 +1,6 @@
import { z } from "zod";
import { PeriodSchema } from "./period.ts";
import { GameId } from "./schema.ts";
import { GameId, Region } from "./schema.ts";
import { shownFor, type PerGame } from "./perGame.ts";
import { MeasureSchema, type TaskShape } from "./task.ts";
@@ -47,6 +47,13 @@ export const RoutineSchema = z.object({
* (`prefs.standardPulls`, owner, 2026-10-16). Absent on everything else.
*/
pulls: z.enum(["premium", "standard"]).optional(),
/**
* The server whose reset this rolls on for every reader — for a cycle that
* ends at one instant everywhere, such as NTE's Beyond the Rails on Asia's
* (spec 2026-10-16, decision 3). Code only, never stored; *Make it mine*
* leaves it behind, since a reader's task has no such field.
*/
clock: Region.optional(),
});
export type Routine = z.infer<typeof RoutineSchema> & TaskShape;
+3 -2
View File
@@ -68,14 +68,15 @@ export type TaskShape = Pick<Task, "id" | "game" | "title" | "period" | "measure
/** Where the reader is with a task this period. Pure; `now` explicit. */
export function taskStatus(input: {
task: Pick<Task, "game" | "period" | "measure">;
/** `clock`: a built-in's server, when it rolls on one server's reset for everyone. */
task: Pick<Task, "game" | "period" | "measure"> & { clock?: Region | undefined };
region: Region;
now: number;
logged: readonly string[];
amounts?: AmountLog | undefined;
}): GoalStatus {
const { task, region, now, logged, amounts } = input;
const today = dayKey(now, region, task.game as LaneId);
const today = dayKey(now, task.clock ?? region, task.game as LaneId);
const { start, days } = periodOf(task.period, today);
const scope = task.period.unit;
const left = days.filter((k) => k >= today);