Merge the second half of the meta sweep

The generated document is the whole visible effect of both halves, so it
conflicted line for line. Regenerated from the merged registry rather
than resolved by hand.
This commit is contained in:
Marc-André Bombeck committed 2026-08-22 15:38:10 +02:00
commit 475db3073c
18 files changed
+2045 -2880

No files matched your search

+1866 -2739
View File
File diff suppressed because it is too large. Load diff
+10 -6
View File
@@ -33,19 +33,23 @@ import {
stdResponses,
} from "./shared";
allergyCreateSchema.meta({
// `.meta()` CLONES in Zod 4 rather than annotating in place, so the returned
// schema has to be captured and referenced. A bare `schema.meta({...})`
// statement registers nothing and the component id it names never reaches the
// emitted document.
const createAllergyRequest = allergyCreateSchema.meta({
id: "CreateAllergyRequest",
description:
"Record an allergy/intolerance. `substance` is the user-facing allergen name (the FHIR AllergyIntolerance `code.text` anchor — never a machine-guessed code). The free-text `reaction` + `note` are encrypted at rest. `onsetAt` is optional (unknown when omitted). Patient-reported.",
});
allergyUpdateSchema.meta({
const updateAllergyRequest = allergyUpdateSchema.meta({
id: "UpdateAllergyRequest",
description:
"Partial edit of an allergy; an omitted key leaves the column untouched. A `null` `severity`/`onsetAt`/`reaction`/`note` clears that field. Rejects unknown keys.",
});
allergyListQuerySchema.meta({
const listAllergiesQuery = allergyListQuerySchema.meta({
id: "ListAllergiesQuery",
description:
"Query params for the allergy list: optional `limit` (1–200, default 100) and `includeInactive` ('true' | 'false'); 'false' returns only ACTIVE records. Newest-first.",
@@ -85,7 +89,7 @@ export const allergyPaths: NonNullable<ZodOpenApiObject["paths"]> = {
summary: "List allergies (v1.25)",
description:
"Returns the caller's live (non-deleted) allergy/intolerance records, newest-first. `includeInactive=false` returns only ACTIVE records.",
requestParams: { query: allergyListQuerySchema },
requestParams: { query: listAllergiesQuery },
responses: {
...recordRefusal(),
"200": {
@@ -107,7 +111,7 @@ export const allergyPaths: NonNullable<ZodOpenApiObject["paths"]> = {
parameters: [idempotencyKeyParameter],
requestBody: {
required: true,
content: { "application/json": { schema: allergyCreateSchema } },
content: { "application/json": { schema: createAllergyRequest } },
},
responses: {
...idempotentWrite(),
@@ -153,7 +157,7 @@ export const allergyPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestParams: { path: z.object({ id: z.string() }) },
requestBody: {
required: true,
content: { "application/json": { schema: allergyUpdateSchema } },
content: { "application/json": { schema: updateAllergyRequest } },
},
responses: {
...recordRefusal(),
+6 -2
View File
@@ -14,7 +14,6 @@ import { z } from "zod/v4";
import type { ZodOpenApiObject } from "zod-openapi";
import {
changePasswordSchema,
loginPasswordSchema,
passkeyRenameSchema,
registerSchema,
} from "@/lib/validations/auth";
@@ -36,7 +35,12 @@ import {
stepUpMintSchema,
stepUpOptionsSchema,
} from "@/lib/validations/step-up";
import { dataEnvelope, stdResponses, errorEnvelope } from "./shared";
import {
dataEnvelope,
stdResponses,
errorEnvelope,
loginPasswordSchema,
} from "./shared";
// ── Sub-schemas owned here (route-specific shapes) ───────────────────
+2 -4
View File
@@ -12,14 +12,12 @@
import { z } from "zod/v4";
import type { ZodOpenApiObject } from "zod-openapi";
import {
measurementSourceEnum,
measurementTypeEnum,
} from "@/lib/validations/measurement";
import {
MODULE_DISABLED_DESCRIPTION,
dataEnvelope,
errorEnvelope,
measurementSourceEnum,
measurementTypeEnum,
recordRefusal,
stdResponses,
} from "./shared";
+8 -4
View File
@@ -28,13 +28,17 @@ import {
stdResponses,
} from "./shared";
createBiomarkerSchema.meta({
// `.meta()` CLONES in Zod 4 rather than annotating in place, so the returned
// schema has to be captured and referenced. A bare `schema.meta({...})`
// statement registers nothing and the component id it names never reaches the
// emitted document.
const createBiomarkerRequest = createBiomarkerSchema.meta({
id: "CreateBiomarkerRequest",
description:
"Define a user-scoped biomarker ONCE: canonical `name`, `unit`, optional reference bounds (`lowerBound` / `upperBound`; when both present `lowerBound` must not exceed `upperBound`), an optional encrypted `context` note, and an optional `panel` grouping. The name is unique per user (no second 'LDL'). Recording a value later just picks this marker — its unit + range are never re-entered.",
});
updateBiomarkerSchema.meta({
const updateBiomarkerRequest = updateBiomarkerSchema.meta({
id: "UpdateBiomarkerRequest",
description:
"Partial edit of a biomarker. An omitted key leaves the column untouched; an explicit `null` on `context` / `panel` / a bound clears it. Set `hidden` to drop / restore the marker in the active list + lab-entry pickers. A rename that collides with another of the caller's markers is rejected 409.",
@@ -106,7 +110,7 @@ export const biomarkerPaths: NonNullable<ZodOpenApiObject["paths"]> = {
parameters: [idempotencyKeyParameter],
requestBody: {
required: true,
content: { "application/json": { schema: createBiomarkerSchema } },
content: { "application/json": { schema: createBiomarkerRequest } },
},
responses: {
...idempotentWrite(),
@@ -153,7 +157,7 @@ export const biomarkerPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestParams: { path: z.object({ id: z.string() }) },
requestBody: {
required: true,
content: { "application/json": { schema: updateBiomarkerSchema } },
content: { "application/json": { schema: updateBiomarkerRequest } },
},
responses: {
...recordRefusal(),
+20 -37
View File
@@ -33,15 +33,12 @@ import { COACH_PLAN_STATUSES } from "@/lib/ai/coach/plans";
import {
COACH_PLAN_SCOPES,
coachPlanPatchSchema,
coachPlansListQuerySchema,
} from "@/lib/validations/coach-plan";
import { coachChatRequestSchema } from "@/lib/ai/coach/types";
import {
coachAttachmentCreateSchema,
fencedChatRequestSchema,
} from "@/lib/validations/inbound-documents";
import { exportSelectionSchema } from "@/lib/validations/health-record-export";
import { createShareLinkSchema } from "@/lib/validations/clinician-share-link";
import { coachReminderSuggestionActionSchema } from "@/lib/validations/coach-reminder-suggestion";
import { COACH_REMINDER_STATUSES } from "@/lib/ai/coach/reminders";
import {
@@ -71,13 +68,13 @@ const aboutMeAdoptRequest = aboutMeAdoptSchema.meta({
"An answer to fold into the stored self-context. `question` is the clarifying question it belongs to and is what the server matches the target field from; omit it for the remember-a-message path and the field is matched from `answer` instead. `answer` is capped at 500 characters — the same cap as a structured self-context field.",
});
emergencyProfileUpdateSchema.meta({
const updateEmergencyProfileRequest = emergencyProfileUpdateSchema.meta({
id: "UpdateEmergencyProfileRequest",
description:
"Partial edit of the emergency (Notfalldaten) profile. An omitted key leaves the column untouched; a `null` enum or an emptied free-text field clears it. `bloodType`, `organDonor` and `advanceDirective` are closed enums; `contacts`, `implants` and `note` are encrypted at rest. Rejects unknown keys.",
});
emergencyProfileDtoSchema.meta({
const emergencyProfile = emergencyProfileDtoSchema.meta({
id: "EmergencyProfile",
description:
"The caller's emergency profile: three closed-enum facts plus three decrypted free-text fields. A free-text field is null when unset; its `*Unreadable` flag is true when ciphertext was present but could not be decrypted (a key-rotation gap, fail-soft rather than 500).",
@@ -88,7 +85,7 @@ emergencyProfileDtoSchema.meta({
// client sends ONLY the cadence id + the action; the server resolves the
// metric + schedule + course window from the closed catalog and (for
// `accept`) mints a `MeasurementReminder` with `origin: COACH`.
coachReminderSuggestionActionSchema.meta({
const coachReminderSuggestionAction = coachReminderSuggestionActionSchema.meta({
id: "CoachReminderSuggestionAction",
description:
"v1.18.1 — act on a Coach cadence suggestion. `cadenceId` names a closed-catalog preset (e.g. `weight_daily`, `bp_7_2_2`); the client never sends a schedule. `action`: `accept` creates a `MeasurementReminder` (origin: COACH) through the same engine the Vorsorge surface uses; `dismiss` records dismissal memory; `stop` suppresses all future cadence suggestions. Strict: unknown keys 422.",
@@ -130,7 +127,7 @@ export const coachReminderSuggestionPaths: NonNullable<
requestBody: {
required: true,
content: {
"application/json": { schema: coachReminderSuggestionActionSchema },
"application/json": { schema: coachReminderSuggestionAction },
},
},
responses: {
@@ -264,24 +261,6 @@ const insightsLayoutResult = insightsLayoutSchema
"Resolved Insights layout plus the optimistic-concurrency `updatedAt` token.",
});
// v1.7.0 — health-record export selection. Strict shape: unknown keys
// (including any attempt to smuggle a userId) 422 via returnAllZodIssues.
// v1.11.0 — clinician share-link create payload. Strict; no `userId` field
// (the owner is always narrowed from the session/Bearer). `expiresAt` is
// required and capped at SHARE_LINK_MAX_DAYS; the scope columns are frozen
// write-once at creation.
createShareLinkSchema.meta({
id: "CreateShareLinkRequest",
description:
"v1.11.0 — owner request to mint a clinician share link to their own health record. `expiresAt` is required (absolute ISO instant) and capped at 90 days. `rangeStart`/`rangeEnd` freeze the reporting window (rangeEnd null = rolling). `sections` freezes which record domains the link may serve. A share link serves the rendered page and the documents frozen onto it — there is no FHIR or other machine-readable face behind a share token. Strict: unknown keys 422.",
});
exportSelectionSchema.meta({
id: "HealthRecordExportRequest",
description:
"v1.7.0 — health-record / doctor-handover export selection. `format` picks PDF, FHIR R4 document Bundle, or a combined zip package. Grouped `sections` toggles drive which domains are read (mood is opt-in, off by default). No `userId` field — the user is always narrowed from the session/Bearer. The route is strict: unknown keys 422.",
});
// ── Coach facts (v1.11.1) ────────────────────────────────────────────
// Read + delete surface for the durable facts the Coach extracts. Facts
// are server-extracted, not user-authored, so there is no create/update
@@ -396,17 +375,21 @@ const coachPlanDeletedResponse = z.object({
),
});
coachPlanPatchSchema.meta({
const coachPlanPatchRequest = coachPlanPatchSchema.meta({
id: "CoachPlanPatchRequest",
description:
"v1.21.3 — confirm or update a Coach plan's lifecycle. `status` moves a `proposed` plan to `active` (confirm), or to `met` / `abandoned`. `reviewDate` pins (ISO instant) or clears (null) a check-in checkpoint. At least one field is required. Strict: unknown keys 422 — the body can never carry the metric, the encrypted text, or a userId.",
});
coachPlansListQuerySchema.meta({
id: "CoachPlansListQuery",
description:
"Optional `?status=` filter for the plans list (a single lifecycle status), or `?scope=` for a named group (open = proposed + active + review_due, past = met + abandoned + reviewed, all = every non-deleted plan). Mutually exclusive. Both omitted returns the non-terminal set (proposed + active).",
});
// `GET /api/coach/plans` publishes its two query parameters by hand below,
// not through `requestParams.query`: the runtime schema carries a cross-field
// `.refine()` (status and scope are mutually exclusive) that the expansion
// cannot express, and the hand-written pair carries per-parameter prose it
// would drop. A `CoachPlansListQuery` annotation sat here claiming a component
// id that nothing referenced and that a query object could never reach anyway
// — the emitter inlines query objects into `parameters`. Every clause of it is
// already published on the operation and on both parameters, so it is gone
// rather than left registering nothing.
// ── Coach episodic reminders (v1.22 B2/B6) ──────────────────────────────
const coachReminderItem = z
@@ -727,7 +710,7 @@ const coachAttachmentsResponseSchema = z
})
.meta({ id: "CoachAttachments" });
coachChatRequestSchema.meta({
const coachChatRequest = coachChatRequestSchema.meta({
id: "CoachChatRequest",
description:
"Inbound Coach turn. `message` is the user's turn (1–4 000 chars). `conversationId` is omitted to start a new conversation (the server mints a title from the first message) and supplied to continue one. `scope` narrows which metrics the snapshot ships and which window the timeline covers; omitted fields fall back to server defaults. `locale` picks the reply language. `guidedQuestion` carries the clarifying question a message answers (client-side bubble, never persisted). No `userId` field — the owner is narrowed from the session / Bearer.",
@@ -804,7 +787,7 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestBody: {
required: true,
content: {
"application/json": { schema: coachChatRequestSchema },
"application/json": { schema: coachChatRequest },
},
},
responses: {
@@ -1468,7 +1451,7 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
content: {
"application/json": {
schema: dataEnvelope(
emergencyProfileDtoSchema,
emergencyProfile,
"GetEmergencyProfileResponse",
),
},
@@ -1485,7 +1468,7 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestBody: {
required: true,
content: {
"application/json": { schema: emergencyProfileUpdateSchema },
"application/json": { schema: updateEmergencyProfileRequest },
},
},
responses: {
@@ -1494,7 +1477,7 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
content: {
"application/json": {
schema: dataEnvelope(
emergencyProfileDtoSchema,
emergencyProfile,
"UpdateEmergencyProfileResponse",
),
},
@@ -1675,7 +1658,7 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestBody: {
required: true,
content: {
"application/json": { schema: coachPlanPatchSchema },
"application/json": { schema: coachPlanPatchRequest },
},
},
responses: {
+9 -4
View File
@@ -26,18 +26,23 @@ import { z } from "zod/v4";
import {
consentKindEnum,
consentPostBody,
webConsentGrantBody,
consentPostBody as consentPostBodyBase,
webConsentGrantBody as webConsentGrantBodyBase,
} from "@/lib/validations/consent";
import { dataEnvelope, errorEnvelope, stdResponses } from "./shared";
consentPostBody.meta({
// `.meta()` CLONES in Zod 4 rather than annotating in place, so the returned
// schema has to be captured and referenced. A bare `schema.meta({...})`
// statement registers nothing and the component id it names never reaches the
// emitted document. The imports are aliased so the annotated clone can keep the
// name every use site below already spells.
const consentPostBody = consentPostBodyBase.meta({
id: "ConsentPostBody",
description:
"Explicit AI-consent grant. `artefact` is an opaque signed receipt (base64 PDF or JWT, ≤ 64 KB UTF-8 bytes); `signedAt` is an ISO-8601 instant. Always appends a fresh row — re-granting after a revoke mints a new receipt.",
});
webConsentGrantBody.meta({
const webConsentGrantBody = webConsentGrantBodyBase.meta({
id: "WebConsentGrantBody",
description:
'Optional. Omitting the body (or sending `intent: "heal"`) is the AI-settings mount heal: it mints only for an account with no `ai_full` consent history at all, so a standing revocation is never resurrected. `intent: "affirmative"` is the user\'s own grant action and may supersede an earlier revocation, exactly as a first grant would.',
+29 -29
View File
@@ -52,52 +52,56 @@ const cyclePhaseEnumOpenapi = z
.enum(["MENSTRUAL", "FOLLICULAR", "OVULATORY", "LUTEAL"])
.meta({ id: "CyclePhase" });
flowLevelEnum.meta({
// `.meta()` CLONES in Zod 4 rather than annotating in place, so the returned
// schema has to be captured and referenced. A bare `schema.meta({...})`
// statement registers nothing and the component id it names never reaches the
// emitted document.
const flowLevelEnumOpenapi = flowLevelEnum.meta({
id: "FlowLevel",
description: "Menstrual-flow intensity.",
});
ovulationTestEnum.meta({
const ovulationTestEnumOpenapi = ovulationTestEnum.meta({
id: "OvulationTest",
description: "Ovulation predictor-kit (OPK) reading.",
});
cervicalMucusEnum.meta({
const cervicalMucusEnumOpenapi = cervicalMucusEnum.meta({
id: "CervicalMucus",
description: "Cervical-mucus quality.",
});
homeTestResultEnum.meta({
const homeTestResultEnumOpenapi = homeTestResultEnum.meta({
id: "HomeTestResult",
description: "At-home test result (pregnancy / progesterone).",
});
cycleTrackingGoalEnum.meta({
const cycleTrackingGoalEnumOpenapi = cycleTrackingGoalEnum.meta({
id: "CycleTrackingGoal",
description: "Drives cycle copy + fertile-window gating.",
});
cycleDayLogInputSchema.meta({
const cycleDayLogInput = cycleDayLogInputSchema.meta({
id: "CycleDayLogInput",
description:
"One day's cycle capture. `note` is encrypted at rest; every other field is queryable plaintext. UPSERT key: `(userId, source, externalId)` when externalId present, else `(userId, date)`. Shared by the single POST, the bulk drain, and the period shortcut.",
});
cycleBulkSchema.meta({
const cycleDayLogBulkRequest = cycleBulkSchema.meta({
id: "CycleDayLogBulkRequest",
description:
"Outbox / HealthKit drain. Up to 500 entries per call; wrapped in `withIdempotency`; rate-limited 60/min. Each entry upserts per the day-log key.",
});
cyclePeriodSchema.meta({
const cyclePeriodRequest = cyclePeriodSchema.meta({
id: "CyclePeriodRequest",
description:
"One-tap period boundary. `start` opens a new cycle (closing the prior), `end` stamps the current cycle's periodEndDate; both write a boundary day-log.",
});
cyclePrefsSchema.meta({
const cyclePrefsRequest = cyclePrefsSchema.meta({
id: "CyclePrefsRequest",
description:
"Partial cycle-preferences deep-merge. `enabled` flips the feature gate (`cycleTrackingEnabled`). Omitted fields are left untouched.",
});
cycleDayLogPatchSchema.meta({
const cycleDayLogPatchRequest = cycleDayLogPatchSchema.meta({
id: "CycleDayLogPatchRequest",
description:
"Partial day-log edit. Every field optional; `note` re-encrypts (explicit null clears it). `date` / `source` / `externalId` are immutable on update.",
@@ -113,19 +117,19 @@ const cycleDayLogDto = z
id: z.string(),
date: z.string(),
cycleId: z.string().nullable(),
flow: flowLevelEnum.nullable(),
flow: flowLevelEnumOpenapi.nullable(),
intermenstrualBleeding: z.boolean(),
basalBodyTempC: z.number().nullable(),
temperatureExcluded: z.boolean(),
ovulationTest: ovulationTestEnum.nullable(),
cervicalMucus: cervicalMucusEnum.nullable(),
ovulationTest: ovulationTestEnumOpenapi.nullable(),
cervicalMucus: cervicalMucusEnumOpenapi.nullable(),
cervixPosition: cervixPositionEnum.nullable(),
cervixFirmness: cervixFirmnessEnum.nullable(),
cervixOpening: cervixOpeningEnum.nullable(),
sexualActivity: z.boolean(),
protectedSex: z.boolean().nullable(),
pregnancyTest: homeTestResultEnum.nullable(),
progesteroneTest: homeTestResultEnum.nullable(),
pregnancyTest: homeTestResultEnumOpenapi.nullable(),
progesteroneTest: homeTestResultEnumOpenapi.nullable(),
contraceptive: z.string().nullable(),
symptoms: z.array(cycleSymptomDto),
note: z.string().nullable(),
@@ -191,7 +195,7 @@ const cycleCalendarDayDto = z.object({
description:
"Whether a logged cycle opens on this day. Deleting this day's log removes that cycle start with it.",
}),
flow: flowLevelEnum.nullable(),
flow: flowLevelEnumOpenapi.nullable(),
hasSymptoms: z.boolean(),
confidence: z.number(),
// v1.15.0 — logged basal-body-temperature + fertility-sign markers, surfaced
@@ -199,8 +203,8 @@ const cycleCalendarDayDto = z.object({
// loaded server-side for the symptothermal layer; no extra query).
basalBodyTempC: z.number().nullable(),
temperatureExcluded: z.boolean(),
ovulationTest: ovulationTestEnum.nullable(),
cervicalMucus: cervicalMucusEnum.nullable(),
ovulationTest: ovulationTestEnumOpenapi.nullable(),
cervicalMucus: cervicalMucusEnumOpenapi.nullable(),
cervixPosition: cervixPositionEnum.nullable(),
cervixFirmness: cervixFirmnessEnum.nullable(),
cervixOpening: cervixOpeningEnum.nullable(),
@@ -247,7 +251,7 @@ const cycleVerdictDto = z
const cycleProfileDto = z
.object({
goal: cycleTrackingGoalEnum,
goal: cycleTrackingGoalEnumOpenapi,
cycleTrackingEnabled: z.boolean(),
secondarySymptom: secondarySymptomEnum,
rawChartMode: z.boolean(),
@@ -266,7 +270,7 @@ const cycleProfileDto = z
const cycleCalendarResponse = z.object({
profile: z.object({
goal: cycleTrackingGoalEnum,
goal: cycleTrackingGoalEnumOpenapi,
rawChartMode: z.boolean(),
predictionEnabled: z.boolean(),
cyclesObserved: z.number().int(),
@@ -444,10 +448,6 @@ const cycleDisabledOnADelegableRoute = recordRefusal(
// rest and never reaches a wide event or an audit excerpt — only the icon and
// the minted key do.
// `.meta()` RETURNS a new schema in Zod 4 rather than annotating in place, so
// the result has to be captured and referenced. A bare `schema.meta({...})`
// statement registers nothing, and the component id it names never reaches
// the document.
const createCycleCustomSymptomRequest = createCustomSymptomSchema.meta({
id: "CreateCycleCustomSymptomRequest",
description:
@@ -682,7 +682,7 @@ export const cyclePaths: NonNullable<ZodOpenApiObject["paths"]> = {
parameters: [idempotencyKeyParameter],
requestBody: {
required: true,
content: { "application/json": { schema: cycleDayLogInputSchema } },
content: { "application/json": { schema: cycleDayLogInput } },
},
responses: {
...idempotentWrite(),
@@ -719,7 +719,7 @@ export const cyclePaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestParams: { path: z.object({ id: z.string() }) },
requestBody: {
required: true,
content: { "application/json": { schema: cycleDayLogPatchSchema } },
content: { "application/json": { schema: cycleDayLogPatchRequest } },
},
responses: {
"200": {
@@ -764,7 +764,7 @@ export const cyclePaths: NonNullable<ZodOpenApiObject["paths"]> = {
parameters: [idempotencyKeyParameter],
requestBody: {
required: true,
content: { "application/json": { schema: cycleBulkSchema } },
content: { "application/json": { schema: cycleDayLogBulkRequest } },
},
responses: {
...idempotentWrite(),
@@ -790,7 +790,7 @@ export const cyclePaths: NonNullable<ZodOpenApiObject["paths"]> = {
parameters: [idempotencyKeyParameter],
requestBody: {
required: true,
content: { "application/json": { schema: cyclePeriodSchema } },
content: { "application/json": { schema: cyclePeriodRequest } },
},
responses: {
...idempotentWrite(),
@@ -979,7 +979,7 @@ export const cyclePaths: NonNullable<ZodOpenApiObject["paths"]> = {
"Deep-merges the supplied fields. `enabled` flips `cycleTrackingEnabled`. Returns the merged CycleProfileDTO. NOT gated.",
requestBody: {
required: true,
content: { "application/json": { schema: cyclePrefsSchema } },
content: { "application/json": { schema: cyclePrefsRequest } },
},
responses: {
"200": {
+21 -17
View File
@@ -41,49 +41,53 @@ import {
stdResponses,
} from "./shared";
encounterCreateSchema.meta({
// `.meta()` CLONES in Zod 4 rather than annotating in place, so the returned
// schema has to be captured and referenced. A bare `schema.meta({...})`
// statement registers nothing and the component id it names never reaches the
// emitted document.
const createEncounterRequest = encounterCreateSchema.meta({
id: "CreateEncounterRequest",
description:
"File a visit, or book one. `occurredAt` is the only required field — a visit saves with a date and nothing else. A future instant with `status: PLANNED` books an appointment and mints exactly one reminder for it. `reason` and `outcome` are encrypted at rest. The three id arrays pre-link documents, lab results and condition episodes; an id naming nothing the caller owns is dropped rather than refused, so a link never blocks a save.",
});
encounterUpdateSchema.meta({
const updateEncounterRequest = encounterUpdateSchema.meta({
id: "UpdateEncounterRequest",
description:
"Partial edit of a visit; an omitted key leaves the column untouched, and a body naming nothing is a 422. Moving `occurredAt` re-anchors the appointment reminder the visit already owns and never mints a second. Moving `status` to DONE closes the checkup named by `reminderId`, on the transition only. CANCELLED and NO_SHOW stop the reminder without deleting the row. A present id array replaces that link family, empty array included.",
});
encounterListQuerySchema.meta({
const listEncountersQuery = encounterListQuerySchema.meta({
id: "ListEncountersQuery",
description:
"Window and filters for the visit list: `from` / `to` (ISO-8601 instants), `status`, `practitionerId`, `episodeId` (only visits filed against that condition episode — the condition side of the link, and the only direction it is offered in), and `limit` (1–200, default 100).",
});
encounterSuggestQuerySchema.meta({
const suggestEncounterQuery = encounterSuggestQuerySchema.meta({
id: "SuggestEncounterQuery",
description:
"The date to resolve around, as an ISO-8601 instant with an offset. A document uses `reportDate ?? documentDate`; a lab panel uses `takenAt`.",
});
encounterLinkSchema.meta({
const encounterLinkRequest = encounterLinkSchema.meta({
id: "EncounterLinkRequest",
description:
"Targets to file against, or unfile from, a visit. Idempotent in both directions and capped at 100 ids. Ids naming nothing the caller owns come back in `unknown` rather than failing the request.",
});
practitionerCreateSchema.meta({
const createPractitionerRequest = practitionerCreateSchema.meta({
id: "CreatePractitionerRequest",
description:
"Add a doctor or practice to the caller's own address book. `name` is the only required field and is stored as queryable plaintext so the picker can search and sort it; `note` is encrypted at rest. `specialty` is free text rather than an enum. Nothing is unique across accounts.",
});
practitionerUpdateSchema.meta({
const updatePractitionerRequest = practitionerUpdateSchema.meta({
id: "UpdatePractitionerRequest",
description:
"Partial edit; an omitted key leaves the column untouched, an explicit null clears it, and a body naming nothing is a 422.",
});
practitionerListQuerySchema.meta({
const listPractitionersQuery = practitionerListQuerySchema.meta({
id: "ListPractitionersQuery",
description:
"Query params for the address book: `q` (case-insensitive substring of the name OR the practice — a person looks one up by whichever of the two they remember) and `limit` (1–200, default 100). Name-ordered.",
@@ -233,7 +237,7 @@ export const encounterPaths: NonNullable<ZodOpenApiObject["paths"]> = {
summary: "List visits",
description:
"Returns the caller's live visits, split into `upcoming` and `past`. Bounded and windowed; soft-deleted visits are excluded.",
requestParams: { query: encounterListQuerySchema },
requestParams: { query: listEncountersQuery },
responses: {
...recordRefusal(),
"200": {
@@ -255,7 +259,7 @@ export const encounterPaths: NonNullable<ZodOpenApiObject["paths"]> = {
"Creates one visit. A future instant with `status: PLANNED` books an appointment and mints its reminder; a past instant with `status: DONE` and a `reminderId` closes that checkup. Claiming both is a 422 — a visit that has not happened cannot have closed anything. Honours `Idempotency-Key`. Audits as `encounter.visit.create`.",
requestBody: {
required: true,
content: { "application/json": { schema: encounterCreateSchema } },
content: { "application/json": { schema: createEncounterRequest } },
},
responses: {
...idempotentWrite(),
@@ -278,7 +282,7 @@ export const encounterPaths: NonNullable<ZodOpenApiObject["paths"]> = {
summary: "Which visit does a record dated around this day belong to?",
description:
"Resolves the caller's live DONE or PLANNED visits within ±7 days of `anchor` into a verdict, so the three capture moments that ask the question — a document arriving, a lab panel committed from an extraction, a lab panel typed by hand — all read the same answer. `one` means pre-select it visibly with one undo; `many` means offer a picker with NOTHING pre-selected, because pre-selecting the first of two teaches a person to stop reading suggestions; `none` means offer nothing at all. Cancelled visits and no-shows are never candidates: neither happened, so neither produced the record being filed. No AI provider is involved and none is required.",
requestParams: { query: encounterSuggestQuerySchema },
requestParams: { query: suggestEncounterQuery },
responses: {
...recordRefusal(),
"200": {
@@ -325,7 +329,7 @@ export const encounterPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestParams: idPath,
requestBody: {
required: true,
content: { "application/json": { schema: encounterUpdateSchema } },
content: { "application/json": { schema: updateEncounterRequest } },
},
responses: {
...recordRefusal(),
@@ -396,7 +400,7 @@ export const encounterPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestParams: idPath,
requestBody: {
required: true,
content: { "application/json": { schema: encounterLinkSchema } },
content: { "application/json": { schema: encounterLinkRequest } },
},
responses: {
"200": {
@@ -425,7 +429,7 @@ export const encounterPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestParams: idPath,
requestBody: {
required: true,
content: { "application/json": { schema: encounterLinkSchema } },
content: { "application/json": { schema: encounterLinkRequest } },
},
responses: {
"200": {
@@ -453,7 +457,7 @@ export const encounterPaths: NonNullable<ZodOpenApiObject["paths"]> = {
summary: "List practitioners",
description:
"Returns the caller's address book, name-ordered, with an optional case-insensitive name search.",
requestParams: { query: practitionerListQuerySchema },
requestParams: { query: listPractitionersQuery },
responses: {
...recordRefusal(),
"200": {
@@ -478,7 +482,7 @@ export const encounterPaths: NonNullable<ZodOpenApiObject["paths"]> = {
"Creates one address-book entry. Honours `Idempotency-Key`. Audits as `practitioner.contact.create`.",
requestBody: {
required: true,
content: { "application/json": { schema: practitionerCreateSchema } },
content: { "application/json": { schema: createPractitionerRequest } },
},
responses: {
...idempotentWrite(),
@@ -524,7 +528,7 @@ export const encounterPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestParams: idPath,
requestBody: {
required: true,
content: { "application/json": { schema: practitionerUpdateSchema } },
content: { "application/json": { schema: updatePractitionerRequest } },
},
responses: {
...recordRefusal(),
+28 -2
View File
@@ -19,6 +19,32 @@ import {
stdResponses,
} from "./shared";
// The two request bodies this module publishes. `.meta()` CLONES in Zod 4
// rather than annotating in place, so the returned schema has to be captured
// and referenced: a bare `schema.meta({...})` statement registers nothing and
// the component id it names never reaches the emitted document. Both
// annotations used to sit in `./coach`, which imported the schemas for no
// other reason and never referenced them; the two operations that DO send
// these bodies are both here.
//
// v1.7.0 — health-record export selection. Strict shape: unknown keys
// (including any attempt to smuggle a userId) 422 via returnAllZodIssues.
// v1.11.0 — clinician share-link create payload. Strict; no `userId` field
// (the owner is always narrowed from the session/Bearer). `expiresAt` is
// required and capped at SHARE_LINK_MAX_DAYS; the scope columns are frozen
// write-once at creation.
const createShareLinkRequest = createShareLinkSchema.meta({
id: "CreateShareLinkRequest",
description:
"v1.11.0 — owner request to mint a clinician share link to their own health record. `expiresAt` is required (absolute ISO instant) and capped at 90 days. `rangeStart`/`rangeEnd` freeze the reporting window (rangeEnd null = rolling). `selection` freezes which record leaves the link may serve, and omitting it means an empty scope rather than a default one. `documentIds` freezes the documents the link carries; `documentOnly` mints a share that serves those documents and no record scope at all. A share link serves the rendered page and the documents frozen onto it — there is no FHIR or other machine-readable face behind a share token. Strict: unknown keys 422.",
});
const healthRecordExportRequest = exportSelectionSchema.meta({
id: "HealthRecordExportRequest",
description:
"v1.7.0 — health-record / doctor-handover export selection. `format` picks PDF, FHIR R4 document Bundle, or a combined zip package. `selection` is REQUIRED and carries the leaf inclusion list — membership is inclusion, absence is exclusion, and there is no server-side default; the retired grouped `sections` shape is a 422. No `userId` field — the user is always narrowed from the session/Bearer. The route is strict: unknown keys 422.",
});
// v1.11.0 — clinician share-link owner-facing summary (never the raw token).
const shareLinkSummary = z.object({
id: z.string(),
@@ -91,7 +117,7 @@ export const healthRecordPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestBody: {
required: true,
content: {
"application/json": { schema: exportSelectionSchema },
"application/json": { schema: healthRecordExportRequest },
},
},
responses: {
@@ -124,7 +150,7 @@ export const healthRecordPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestBody: {
required: true,
content: {
"application/json": { schema: createShareLinkSchema },
"application/json": { schema: createShareLinkRequest },
},
},
responses: {
+7 -5
View File
@@ -14,12 +14,14 @@
import type { ZodOpenApiObject } from "zod-openapi";
import { z } from "zod/v4";
import {
glucoseContextEnum,
measurementTypeEnum,
} from "@/lib/validations/measurement";
import { glucoseContextEnum } from "@/lib/validations/measurement";
import { moodLevelEnum } from "@/lib/validations/mood";
import { dataEnvelope, errorEnvelope, stdResponses } from "./shared";
import {
dataEnvelope,
errorEnvelope,
measurementTypeEnum,
stdResponses,
} from "./shared";
const csvImportRowResult = z
.object({
+1 -1
View File
@@ -82,7 +82,7 @@ import { recordSettingsPaths } from "./record-settings";
import { settingsPaths } from "./settings";
import { syncPaths } from "./sync";
import { workoutPaths } from "./workouts";
import { coachPrefsSchema } from "@/lib/validations/coach-prefs";
import { coachPrefsSchema } from "./shared";
export const openApiPaths: NonNullable<ZodOpenApiObject["paths"]> = {
...cyclePaths,
+1 -4
View File
@@ -6,10 +6,7 @@
* runtime request parsing, so the wire contract stays single-source.
*/
import { z } from "zod/v4";
import {
measurementTypeEnum,
measurementSourceEnum,
} from "@/lib/validations/measurement";
import { measurementSourceEnum, measurementTypeEnum } from "../shared";
import { METRIC_STATUS_IDS } from "@/lib/insights/metric-status-registry";
import {
DERIVED_METRIC_IDS,
+5 -7
View File
@@ -7,21 +7,19 @@
*/
import { z } from "zod/v4";
import type { ZodOpenApiObject } from "zod-openapi";
import {
createMeasurementSchema,
listMeasurementsSchema,
measurementTypeEnum,
updateMeasurementSchema,
measurementSourceEnum,
} from "@/lib/validations/measurement";
import { updateMeasurementSchema } from "@/lib/validations/measurement";
import { seriesBatchQuerySchema } from "@/lib/validations/series-batch";
import { deviceTypeEnum } from "@/lib/validations/source-priority";
import {
MODULE_DISABLED_DESCRIPTION,
createMeasurementSchema,
dataEnvelope,
errorEnvelope,
idempotencyKeyParameter,
idempotentWrite,
listMeasurementsSchema,
measurementSourceEnum,
measurementTypeEnum,
recordRefusal,
stdResponses,
} from "./shared";
+6 -2
View File
@@ -24,7 +24,11 @@ import {
stdResponses,
} from "./shared";
ocrCommitSchema.meta({
// `.meta()` CLONES in Zod 4 rather than annotating in place, so the returned
// schema has to be captured and referenced. A bare `schema.meta({...})`
// statement registers nothing and the component id it names never reaches the
// emitted document.
const ocrCommitRequest = ocrCommitSchema.meta({
id: "OcrCommitRequest",
description:
"The rows a human confirmed on the Lab-OCR review screen. Each row is EITHER numeric (`value` + `unit`, optional reference bounds) OR qualitative (`valueText`) — exactly one. `analyte` drives a resolve-or-mint of the user-scoped biomarker; `takenAt` is a backdatable ISO instant. No `userId` field — it is narrowed from the session. 1..100 rows. The route skips a row that duplicates a live reading (same analyte + day + value). The optional `encounterId` files the panel against a visit the caller owns — ONE link per written result row, so a marker re-run on a different day belongs to its own visit; always optional, and a link that could not be made never fails the commit.",
@@ -209,7 +213,7 @@ export const ocrPaths: NonNullable<ZodOpenApiObject["paths"]> = {
'Writes ONLY the rows the human confirmed on the review screen. Each row resolves-or-mints a user-scoped biomarker and creates a `LabResult` with `source: "OCR"`; a row that duplicates a live reading is skipped. Idempotent (Idempotency-Key). Audits as `labs.ocr.commit`. `userId` is narrowed from the session, never a body field.',
requestBody: {
required: true,
content: { "application/json": { schema: ocrCommitSchema } },
content: { "application/json": { schema: ocrCommitRequest } },
},
responses: {
...idempotentWrite(),
+1 -1
View File
@@ -7,7 +7,6 @@
*/
import { z } from "zod/v4";
import type { ZodOpenApiObject } from "zod-openapi";
import { coachPrefsSchema } from "@/lib/validations/coach-prefs";
import { notificationPrefsSchema } from "@/lib/validations/notification-prefs";
import { sourcePrioritySchema } from "@/lib/validations/source-priority";
import { modulePrefsPatchSchema } from "@/lib/validations/modules";
@@ -38,6 +37,7 @@ import {
healthProfileFactKindSchema,
} from "@/lib/validations/health-profile-facts";
import {
coachPrefsSchema,
dataEnvelope,
errorEnvelope,
stdResponses,
+23 -14
View File
@@ -7,14 +7,14 @@
*/
import { z } from "zod/v4";
import {
createMeasurementSchema,
listMeasurementsSchema,
measurementTypeEnum,
measurementSourceEnum,
createMeasurementSchema as createMeasurementSchemaBase,
listMeasurementsSchema as listMeasurementsSchemaBase,
measurementTypeEnum as measurementTypeEnumBase,
measurementSourceEnum as measurementSourceEnumBase,
} from "@/lib/validations/measurement";
import { loginPasswordSchema } from "@/lib/validations/auth";
import { coachPrefsSchema } from "@/lib/validations/coach-prefs";
import { createBatchWorkoutSchema } from "@/lib/validations/workout";
import { loginPasswordSchema as loginPasswordSchemaBase } from "@/lib/validations/auth";
import { coachPrefsSchema as coachPrefsSchemaBase } from "@/lib/validations/coach-prefs";
import { createBatchWorkoutSchema as createBatchWorkoutSchemaBase } from "@/lib/validations/workout";
/**
* Common envelopes — every HealthLog API response wraps payload in
@@ -48,44 +48,53 @@ export function dataEnvelope<T extends z.ZodType>(payload: T, id: string) {
}
// ── Schemas — annotated for spec emission ────────────────────────────
//
// `.meta()` CLONES in Zod 4 rather than annotating in place, so the returned
// schema has to be captured and referenced: a bare `schema.meta({...})`
// statement registers nothing and the component id it names never reaches the
// emitted document. These seven are shared across route modules, so the
// annotated clone is exported from here under the name the use sites already
// spell and the base import is aliased. A module that wants the published
// form imports it from `./shared`; importing the raw schema from
// `@/lib/validations/*` instead is what inlined these anonymously.
measurementTypeEnum.meta({
export const measurementTypeEnum = measurementTypeEnumBase.meta({
id: "MeasurementType",
description:
"DB-stored measurement category. v1.4.23 added 7 Apple Health values (HRV, resting HR, active energy, flights, walking/running distance, VO2 max, body temperature).",
});
measurementSourceEnum.meta({
export const measurementSourceEnum = measurementSourceEnumBase.meta({
id: "MeasurementSource",
description:
"Origin of the measurement. v1.4.23 added APPLE_HEALTH for the iOS HealthKit batch ingest path.",
});
loginPasswordSchema.meta({
export const loginPasswordSchema = loginPasswordSchemaBase.meta({
id: "LoginPasswordRequest",
description:
"Email-or-username login. The native-client flow returns a paired access + refresh token when X-Client-Type: native or the iOS UA prefix is present.",
});
createMeasurementSchema.meta({
export const createMeasurementSchema = createMeasurementSchemaBase.meta({
id: "CreateMeasurementRequest",
description:
"Single-measurement ingest body. Plausibility-range guard runs server-side; out-of-range values fail 422. `glucoseContext` stays REQUIRED on a `BLOOD_GLUCOSE` row here: this is the hand-entry surface, where the person taking the reading knows whether it was fasting or after a meal. The bulk ingest paths (CSV import, JSON import, device sync) accept a contextless reading, because a sensor export classifies nothing per sample.",
});
listMeasurementsSchema.meta({
export const listMeasurementsSchema = listMeasurementsSchemaBase.meta({
id: "ListMeasurementsQuery",
description:
"Query params for the measurements list endpoint. `limit` capped at 500.",
});
coachPrefsSchema.meta({
export const coachPrefsSchema = coachPrefsSchemaBase.meta({
id: "CoachPrefs",
description:
"Per-user Coach prompt-tuning preferences (v1.4.23 H4). All fields default to the legacy v1.4.22 behaviour when omitted.",
});
createBatchWorkoutSchema.meta({
export const createBatchWorkoutSchema = createBatchWorkoutSchemaBase.meta({
id: "CreateBatchWorkoutRequest",
description:
"Typed workout batch ingest. Each entry is an HKWorkout-aligned record with an optional nested GeoJSON LineString route AND an optional route-independent per-workout heart-rate series (`samples`: `[{ t, hr?, speedMs?, power?, cadence? }]`, up to 30 000 points). The `samples` series is the strain-engine input for indoor workouts that have no GPS route. Up to 100 workouts per call; nested route geometry capped at 20 000 points. Withings server-to-server callers pass source: WITHINGS and ship no route (Withings reports aggregates only).",
+2 -2
View File
@@ -7,14 +7,14 @@
*/
import { z } from "zod/v4";
import type { ZodOpenApiObject } from "zod-openapi";
import { measurementSourceEnum } from "@/lib/validations/measurement";
import { createBatchWorkoutSchema } from "@/lib/validations/workout";
import {
MODULE_DISABLED_DESCRIPTION,
createBatchWorkoutSchema,
dataEnvelope,
errorEnvelope,
idempotencyKeyParameter,
idempotentWrite,
measurementSourceEnum,
moduleDisabledResponse,
recordRefusal,
stdResponses,