/** * The day view and the timeline: the contract every part of v1.42 builds * against (#613). * * One file, so the server that answers, the web views that render and the * native client that decodes agree on one set of names. Constants and types * only, no Zod and no server import: the navigation chrome and the surface map * import from here, and neither may pull a validator into the client bundle. * The request and response schemas live beside it in `./wire-schemas.ts`, and * every response type below is inferred from them, so the two cannot drift. * * Five route families speak it: * * GET /api/day/{date} one local day across the record * GET /api/day/index which days hold anything, for a window * GET /api/timeline lanes, standing items and value series * GET /api/timeline/readiness what the timeline can already show * GET /api/life-events the record's life events * POST /api/life-events * PATCH /api/life-events/{id} * DELETE /api/life-events/{id} * * Rules the shapes carry rather than restate: * * - A day is the local calendar day in the record's time zone, never a UTC * slice. The day route reads the rows live; the rollups cut at UTC. * - The server decides and the clients render. Nothing here asks a client to * recompute a band, a running-day count or a verdict. `notable` and the * readiness details are keys with parameters; the wording lives in the * message bundles, never in a model. * - A section of a switched-off module is absent, not empty. A section the * caller's grant does not cover is named in `sections` with * `not_shared`, so the view can say so once. * - Life events are part of the `profile` sharing domain and never reach a * model: the model-facing projection of a day drops * {@link MODEL_EXCLUDED_DAY_SECTIONS}. */ import type { z } from "zod/v4"; import type { dayIndexResponseSchema, dayResponseSchema, lifeEventCreateSchema, lifeEventListResponseSchema, lifeEventSchema, lifeEventUpdateSchema, timelineQuerySchema, timelineReadinessResponseSchema, timelineResponseSchema, } from "./wire-schemas"; /* ─── Calendar dates ──────────────────────────────────────────────────────── */ /** * A local calendar date, `YYYY-MM-DD`. The `?day=` parameter, the day route's * path segment and every date-only field below use it. */ export type DateKey = string; /** The URL parameter that opens the day layer over any page. */ export const DAY_QUERY_PARAM = "day"; /** Widest window `GET /api/day/index` answers, in days, both ends included. */ export const DAY_INDEX_MAX_SPAN_DAYS = 366; /* ─── Day sections ────────────────────────────────────────────────────────── */ /** * The sections of a day, in the order the view shows them. Each one is a * surface in `src/lib/modules/surface.ts` (`day-section:`) when a module * owns it; a section without an owner is part of the core record. * * values readings in the local day window, canonical source per type * sleep the night that ended on this morning * mood mood entries for the date * assessments mental-health screeners taken that day * medications intakes, dose changes, pauses, course starts and ends * illness episodes running or starting, their day logs * symptoms occurrences of person-defined symptoms * allergies an allergy whose onset is this day * labs results drawn that day * visits visits and procedures * vaccinations doses given that day * checkups preventive check-ups completed that day * documents documents dated that day * workouts sessions started that day * cycle the cycle phase and the day log * environment the day's environment row and a travel period * lifestyle anamnesis facts valid on that day (shift work, …) * lifeEvents life events dated that day at DAY precision, or spanning it */ export const DAY_SECTION_KEYS = [ "values", "sleep", "mood", "assessments", "medications", "illness", "symptoms", "allergies", "labs", "visits", "vaccinations", "checkups", "documents", "workouts", "cycle", "environment", "lifestyle", "lifeEvents", ] as const; export type DaySectionKey = (typeof DAY_SECTION_KEYS)[number]; /** * Sections a model never sees, whatever it asks for. The Coach and the MCP * endpoint read a day through `get_day`, and that projection drops these * before anything leaves the instance (v1.42: life events go to no model). */ export const MODEL_EXCLUDED_DAY_SECTIONS = [ "lifeEvents", ] as const satisfies readonly DaySectionKey[]; /** Why a section is missing from a day the caller can otherwise read. */ export const DAY_SECTION_UNAVAILABLE_REASONS = [ /** The grant the caller reads under does not cover the section's domain. */ "not_shared", /** * The owning module is off. Only the model-facing projection (`get_day`) * reports it, as `{ present: false, reason: "module_disabled" }`; the web * and native views omit the section without a word. */ "module_disabled", ] as const; export type DaySectionUnavailableReason = (typeof DAY_SECTION_UNAVAILABLE_REASONS)[number]; /** Something that runs through the day rather than happening on it. */ export const DAY_RUNNING_KINDS = [ "medication", "medicationCourse", "medicationPause", "illness", "restMode", "cyclePhase", "travel", "lifestyle", "lifeEvent", ] as const; export type DayRunningKind = (typeof DAY_RUNNING_KINDS)[number]; /** Something that happened on the day, at a time or on the date. */ export const DAY_EVENT_KINDS = [ "intake", "doseChange", "medicationStart", "medicationEnd", "pauseStart", "pauseEnd", "courseStart", "courseEnd", "illnessOnset", "illnessResolved", "illnessDayLog", "symptom", "allergyOnset", "labResult", "visit", "procedure", "vaccination", "checkup", "document", "mood", "assessment", "workout", "cycleDayLog", "lifeEvent", ] as const; export type DayEventKind = (typeof DAY_EVENT_KINDS)[number]; /** * Deterministic, descriptive observations. No cause, no verdict: "highest * morning value since March 2025", never "because of". The client words them * from `day.notable.` with the parameters the server sends. * * extremeHigh / extremeLow highest or lowest daily value of a type for at * least three months, with at least 30 days of * history (`params.since`, `params.value`) * firstValue the first reading of a type ever (`params.type`) * gap a type that came at least three times a week * and then stopped for 14 days (`params.days`); * only in the visit preparation and readiness */ export const DAY_NOTABLE_KINDS = [ "extremeHigh", "extremeLow", "firstValue", "gap", ] as const; export type DayNotableKind = (typeof DAY_NOTABLE_KINDS)[number]; /* ─── Timeline ────────────────────────────────────────────────────────────── */ /** Zoom levels. `all` bundles hardest: no symptoms, no single intakes. */ export const TIMELINE_ZOOMS = ["all", "year", "quarter"] as const; export type TimelineZoom = (typeof TIMELINE_ZOOMS)[number]; /** * The lanes, top to bottom. An empty lane is not sent. Each lane a module * owns is a surface (`timeline-lane:`) and disappears with it. * * life life events, and travel periods from the environment module * illness episodes, chronic conditions as open lines * allergies allergies with an onset; the rest are `standing` * medications medications and courses, dose marks, pauses as gaps * vaccinations doses * visits visits and procedures * labs days with results * documents dated documents * cycle cycles, only with the module on */ export const TIMELINE_LANE_KEYS = [ "life", "illness", "allergies", "medications", "vaccinations", "visits", "labs", "documents", "cycle", ] as const; export type TimelineLaneKey = (typeof TIMELINE_LANE_KEYS)[number]; /** What one lane item is. */ export const TIMELINE_ITEM_KINDS = [ "lifeEvent", "travel", "episode", "chronic", "allergy", "medication", "course", "doseChange", "pause", "vaccination", "visit", "procedure", "labDay", "document", "cycle", ] as const; export type TimelineItemKind = (typeof TIMELINE_ITEM_KINDS)[number]; /** * The readiness inventory, one row per lane plus the value series. `values` * is always listed; `mood`, `environment` and `cycle` only with their module * on. */ export const TIMELINE_READINESS_KEYS = [ "values", ...TIMELINE_LANE_KEYS, "mood", "environment", ] as const; export type TimelineReadinessKey = (typeof TIMELINE_READINESS_KEYS)[number]; /** How well one lane is filled. Counts, never a score. */ export const TIMELINE_READINESS_STATUSES = [ "carries", "thin", "empty", ] as const; export type TimelineReadinessStatus = (typeof TIMELINE_READINESS_STATUSES)[number]; /** * The one-sentence verdict: `carries` from three carrying lanes including * the values, otherwise `thin`. */ export const TIMELINE_READINESS_VERDICTS = ["carries", "thin"] as const; export type TimelineReadinessVerdict = (typeof TIMELINE_READINESS_VERDICTS)[number]; /* ─── Life events ─────────────────────────────────────────────────────────── */ /** Mirrors the Prisma enum `LifeEventCategory`. */ export const LIFE_EVENT_CATEGORIES = [ "FAMILY", "HOME", "WORK", "LOSS", "OTHER", ] as const; export type LifeEventCategory = (typeof LIFE_EVENT_CATEGORIES)[number]; /** Mirrors the Prisma enum `LifeEventPrecision`. Applies to both ends. */ export const LIFE_EVENT_PRECISIONS = ["DAY", "MONTH", "YEAR"] as const; export type LifeEventPrecision = (typeof LIFE_EVENT_PRECISIONS)[number]; /** Longest title, in characters. */ export const LIFE_EVENT_TITLE_MAX = 120; /** Longest note, in characters. */ export const LIFE_EVENT_NOTE_MAX = 2000; /* ─── Coach and MCP ───────────────────────────────────────────────────────── */ /** * The read tool the Coach and the MCP endpoint get for one day. Reserved * here so both registries spell it the same; the loader is the day route's. * Its answer never carries a {@link MODEL_EXCLUDED_DAY_SECTIONS} section. */ export const DAY_TOOL_NAME = "get_day"; /** The tool's single argument. */ export interface DayToolInput { date: DateKey; } /* ─── Wire types, inferred from the schemas ───────────────────────────────── */ /** `GET /api/day/{date}` → `data`. */ export type DayResponse = z.infer; export type DayRunningItem = DayResponse["running"][number]; export type DayValue = DayResponse["values"][number]; export type DayEvent = DayResponse["events"][number]; export type DayNotable = DayResponse["notable"][number]; /** `GET /api/day/index` → `data`. */ export type DayIndexResponse = z.infer; /** `GET /api/timeline` query and `data`. */ export type TimelineQuery = z.infer; export type TimelineResponse = z.infer; export type TimelineLane = TimelineResponse["lanes"][number]; export type TimelineItem = TimelineLane["items"][number]; export type TimelineSeries = TimelineResponse["series"][number]; /** `GET /api/timeline/readiness` → `data`. */ export type TimelineReadinessResponse = z.infer< typeof timelineReadinessResponseSchema >; export type TimelineReadinessLane = TimelineReadinessResponse["lanes"][number]; /** One life event as the routes return it. */ export type LifeEventDTO = z.infer; export type LifeEventListResponse = z.infer; export type LifeEventCreateInput = z.infer; export type LifeEventUpdateInput = z.infer;