From 032ea5b1a02d3c857f60ba8a28196cb4fbaae4f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 22 Aug 2026 15:24:05 +0200 Subject: [PATCH 1/3] Register the schema names the contract was discarding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zod 4's `.meta()` clones the schema and registers the clone, so a bare `someSchema.meta({ id })` statement registers nothing at all. Thirty-nine of those across eleven route modules published neither the name nor the description written for it: the schema inlined anonymously at every use site, and a client generated from the contract got a duplicated nameless type where one named model belongs. That last part is the cost that matters, because the native client is the reason the contract is published. Bind each annotated clone to a const and point the route table at that const. Twenty-nine ids now emit under `components.schemas`, referenced from their thirty use sites. Seven stay absent by design: they annotate `requestParams.query` objects, which the emitter expands into inline `parameters` rather than a component. `MoodTagLayout` stays absent too, for the reason `Medication` and `CoachPrefs` already carry a forced registration in the components block: its only consumer is an `.extend()`, and extending builds a new object rather than referencing the base. Two annotations in the illness module named ids that no route ever referenced, `GetIllnessDayLogQuery` and `ListIllnessDayLogsQuery` — the day-log path publishes the merged `GetIllnessDayLogsQuery` instead. They were dead twice over, so they go rather than get wired to nothing. `docs/api/openapi.yaml` grows the twenty-nine components and their refs. The only other movement in it is thirty descriptions appearing where the text had never reached the document. No request or response shape changes. --- docs/api/openapi.yaml | 3077 +++++++++-------- src/lib/openapi/routes/custom-metrics.ts | 19 +- src/lib/openapi/routes/documents.ts | 19 +- src/lib/openapi/routes/family-history.ts | 15 +- src/lib/openapi/routes/illness.ts | 41 +- src/lib/openapi/routes/labs.ts | 15 +- src/lib/openapi/routes/medications/paths.ts | 15 +- src/lib/openapi/routes/medications/schemas.ts | 5 +- src/lib/openapi/routes/mental-health.ts | 11 +- src/lib/openapi/routes/mood.ts | 31 +- src/lib/openapi/routes/vaccinations.ts | 29 +- 11 files changed, 1719 insertions(+), 1558 deletions(-) diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 3fb2e9127..e360c1932 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -6554,96 +6554,7 @@ paths: content: application/json: schema: - type: object - properties: - clear: - type: boolean - measurementType: - type: string - enum: - - WEIGHT - - BLOOD_PRESSURE_SYS - - BLOOD_PRESSURE_DIA - - PULSE - - BODY_FAT - - SLEEP_DURATION - - ACTIVITY_STEPS - - BLOOD_GLUCOSE - - TOTAL_BODY_WATER - - BONE_MASS - - OXYGEN_SATURATION - - HEART_RATE_VARIABILITY - - RESTING_HEART_RATE - - ACTIVE_ENERGY_BURNED - - FLIGHTS_CLIMBED - - WALKING_RUNNING_DISTANCE - - VO2_MAX - - BODY_TEMPERATURE - - FAT_FREE_MASS - - FAT_MASS - - MUSCLE_MASS - - SKIN_TEMPERATURE - - PULSE_WAVE_VELOCITY - - VASCULAR_AGE - - VISCERAL_FAT - - AUDIO_EXPOSURE_ENV - - AUDIO_EXPOSURE_HEADPHONE - - TIME_IN_DAYLIGHT - - WALKING_STEADINESS - - AUDIO_EXPOSURE_EVENT - - RESPIRATORY_RATE - - BODY_MASS_INDEX - - LEAN_BODY_MASS - - WALKING_HEART_RATE_AVERAGE - - WALKING_ASYMMETRY - - WALKING_DOUBLE_SUPPORT - - WALKING_STEP_LENGTH - - WALKING_SPEED - - CARDIO_RECOVERY - - WRIST_TEMPERATURE - - FALL_COUNT - - SIX_MINUTE_WALK_DISTANCE - - STAIR_ASCENT_SPEED - - STAIR_DESCENT_SPEED - - BREATHING_DISTURBANCES - - IRREGULAR_RHYTHM_NOTIFICATION - - HIGH_HEART_RATE_EVENT - - LOW_HEART_RATE_EVENT - - WALKING_STEADINESS_EVENT - - BREATHING_DISTURBANCE_EVENT - - RECOVERY_SCORE - - STRESS_SCORE - - STRAIN_SCORE - - HRV_RMSSD - - DAY_STRAIN - - WORKOUT_STRAIN - - SLEEP_PERFORMANCE - - SLEEP_EFFICIENCY - - SLEEP_CONSISTENCY - - SLEEP_NEED - - ENERGY_EXPENDITURE_KJ - - AVERAGE_HEART_RATE - - MAX_HEART_RATE - - SLEEP_DISTURBANCE_COUNT - - ANS_CHARGE - - CARDIO_LOAD - - SLEEP_SCORE - - BODY_TEMPERATURE_DEVIATION - - RESILIENCE - - PHQ9_SCORE - - GAD7_SCORE - - GRIP_STRENGTH - - PAIN_NRS - - WAIST_CIRCUMFERENCE - - WAIST_TO_HEIGHT - - WHO5_SCORE - - SCI_SCORE - biomarkerId: - type: string - minLength: 1 - maxLength: 64 - primary: - type: boolean + $ref: "#/components/schemas/SetMedicationEfficacyTargetRequest" responses: "200": description: Override set or cleared. @@ -12265,22 +12176,7 @@ paths: content: application/json: schema: - type: object - properties: - label: - type: string - minLength: 1 - maxLength: 40 - icon: - anyOf: - - type: string - - type: "null" - categoryKey: - type: string - minLength: 1 - maxLength: 80 - required: - - label + $ref: "#/components/schemas/CreateMoodTagRequest" responses: "201": description: Custom tag created. @@ -12309,22 +12205,7 @@ paths: content: application/json: schema: - type: object - properties: - label: - type: string - minLength: 1 - maxLength: 40 - icon: - anyOf: - - type: string - - type: "null" - isActive: - type: boolean - categoryKey: - type: string - minLength: 1 - maxLength: 80 + $ref: "#/components/schemas/UpdateMoodTagRequest" responses: "200": description: Custom tag updated. @@ -12389,12 +12270,7 @@ paths: content: application/json: schema: - type: object - properties: - hidden: - type: boolean - required: - - hidden + $ref: "#/components/schemas/HideMoodTagRequest" responses: "200": description: Visibility updated. @@ -12424,18 +12300,7 @@ paths: content: application/json: schema: - type: object - properties: - label: - type: string - minLength: 1 - maxLength: 40 - icon: - anyOf: - - type: string - - type: "null" - required: - - label + $ref: "#/components/schemas/CreateMoodTagGroupRequest" responses: "201": description: Custom group created. @@ -12464,18 +12329,7 @@ paths: content: application/json: schema: - type: object - properties: - label: - type: string - minLength: 1 - maxLength: 40 - icon: - anyOf: - - type: string - - type: "null" - isActive: - type: boolean + $ref: "#/components/schemas/UpdateMoodTagGroupRequest" responses: "200": description: Custom group updated. @@ -14144,68 +13998,7 @@ paths: content: application/json: schema: - type: object - properties: - biomarkerId: - type: string - minLength: 1 - maxLength: 64 - panel: - anyOf: - - type: string - minLength: 1 - maxLength: 120 - - type: string - const: "" - analyte: - type: string - minLength: 1 - maxLength: 120 - value: - type: number - valueText: - type: string - minLength: 1 - maxLength: 120 - unit: - type: string - minLength: 1 - maxLength: 40 - referenceLow: - type: number - referenceHigh: - type: number - sourceReferenceLow: - type: number - sourceReferenceHigh: - type: number - sourceReferenceText: - anyOf: - - type: string - maxLength: 120 - - type: string - const: "" - takenAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ - note: - anyOf: - - type: string - maxLength: 2000 - - type: string - const: "" - source: - type: string - enum: - - MANUAL - - OCR - encounterId: - type: string - minLength: 1 - maxLength: 40 - required: - - takenAt + $ref: "#/components/schemas/CreateLabResultRequest" responses: "201": description: Created lab result. @@ -14264,67 +14057,7 @@ paths: content: application/json: schema: - type: object - properties: - panel: - anyOf: - - anyOf: - - type: string - minLength: 1 - maxLength: 120 - - type: string - const: "" - - type: "null" - analyte: - type: string - minLength: 1 - maxLength: 120 - value: - type: number - valueText: - type: string - minLength: 1 - maxLength: 120 - unit: - type: string - minLength: 1 - maxLength: 40 - referenceLow: - anyOf: - - type: number - - type: "null" - referenceHigh: - anyOf: - - type: number - - type: "null" - sourceReferenceLow: - anyOf: - - type: number - - type: "null" - sourceReferenceHigh: - anyOf: - - type: number - - type: "null" - sourceReferenceText: - anyOf: - - anyOf: - - type: string - maxLength: 120 - - type: string - const: "" - - type: "null" - takenAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ - note: - anyOf: - - anyOf: - - type: string - maxLength: 2000 - - type: string - const: "" - - type: "null" + $ref: "#/components/schemas/UpdateLabResultRequest" responses: "200": description: Updated lab result. @@ -15463,81 +15196,7 @@ paths: content: application/json: schema: - type: object - properties: - occurredAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - antigenSlug: - anyOf: - - type: string - minLength: 1 - maxLength: 64 - - type: "null" - vaccineName: - anyOf: - - type: string - minLength: 1 - maxLength: 200 - - type: "null" - doseNumber: - anyOf: - - type: integer - minimum: 1 - maximum: 20 - - type: "null" - seriesDoses: - anyOf: - - type: integer - minimum: 1 - maximum: 10 - - type: "null" - lotNumber: - anyOf: - - type: string - minLength: 1 - maxLength: 64 - - type: "null" - site: - anyOf: - - type: string - enum: - - LEFT_ARM - - RIGHT_ARM - - LEFT_THIGH - - RIGHT_THIGH - - ORAL - - NASAL - - OTHER - - type: "null" - practitionerId: - anyOf: - - type: string - minLength: 1 - maxLength: 40 - - type: "null" - encounterId: - anyOf: - - type: string - minLength: 1 - maxLength: 40 - - type: "null" - note: - anyOf: - - type: string - maxLength: 2000 - - type: "null" - documentIds: - maxItems: 100 - type: array - items: - type: string - minLength: 1 - maxLength: 40 - required: - - occurredAt - additionalProperties: false + $ref: "#/components/schemas/CreateVaccinationRequest" responses: "201": description: Dose logged. @@ -15596,79 +15255,7 @@ paths: content: application/json: schema: - type: object - properties: - occurredAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - antigenSlug: - anyOf: - - type: string - minLength: 1 - maxLength: 64 - - type: "null" - vaccineName: - anyOf: - - type: string - minLength: 1 - maxLength: 200 - - type: "null" - doseNumber: - anyOf: - - type: integer - minimum: 1 - maximum: 20 - - type: "null" - seriesDoses: - anyOf: - - type: integer - minimum: 1 - maximum: 10 - - type: "null" - lotNumber: - anyOf: - - type: string - minLength: 1 - maxLength: 64 - - type: "null" - site: - anyOf: - - type: string - enum: - - LEFT_ARM - - RIGHT_ARM - - LEFT_THIGH - - RIGHT_THIGH - - ORAL - - NASAL - - OTHER - - type: "null" - practitionerId: - anyOf: - - type: string - minLength: 1 - maxLength: 40 - - type: "null" - encounterId: - anyOf: - - type: string - minLength: 1 - maxLength: 40 - - type: "null" - note: - anyOf: - - type: string - maxLength: 2000 - - type: "null" - documentIds: - maxItems: 100 - type: array - items: - type: string - minLength: 1 - maxLength: 40 - additionalProperties: false + $ref: "#/components/schemas/UpdateVaccinationRequest" responses: "200": description: Dose updated. @@ -15753,24 +15340,7 @@ paths: content: application/json: schema: - type: object - properties: - intervalMonths: - type: integer - minimum: 1 - maximum: 600 - label: - type: string - minLength: 1 - maxLength: 120 - notifyHour: - type: integer - minimum: 0 - maximum: 23 - required: - - intervalMonths - - label - additionalProperties: false + $ref: "#/components/schemas/VaccinationBoosterRequest" responses: "200": description: Existing booster reminder re-anchored. @@ -15835,24 +15405,7 @@ paths: content: application/json: schema: - type: object - properties: - targetKind: - type: string - enum: - - document - targetIds: - minItems: 1 - maxItems: 100 - type: array - items: - type: string - minLength: 1 - maxLength: 40 - required: - - targetKind - - targetIds - additionalProperties: false + $ref: "#/components/schemas/VaccinationLinkRequest" responses: "200": description: The dose's linked pages after the change. @@ -15881,24 +15434,7 @@ paths: content: application/json: schema: - type: object - properties: - targetKind: - type: string - enum: - - document - targetIds: - minItems: 1 - maxItems: 100 - type: array - items: - type: string - minLength: 1 - maxLength: 40 - required: - - targetKind - - targetIds - additionalProperties: false + $ref: "#/components/schemas/VaccinationLinkRequest" responses: "200": description: The dose's linked pages after the change. @@ -15957,54 +15493,7 @@ paths: content: application/json: schema: - type: object - properties: - label: - type: string - minLength: 1 - maxLength: 120 - type: - type: string - enum: - - INFECTION - - ALLERGY - - INJURY - - MENTAL_HEALTH - - AUTOIMMUNE - - CHRONIC - - OTHER - lifecycle: - default: ACUTE - type: string - enum: - - ACUTE - - CHRONIC_ONGOING - - RECURRING - - FLARE - onsetAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - resolvedAt: - anyOf: - - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - - type: "null" - parentConditionId: - anyOf: - - type: string - minLength: 1 - maxLength: 40 - - type: "null" - note: - anyOf: - - type: string - maxLength: 2000 - - type: "null" - required: - - label - - type + $ref: "#/components/schemas/CreateIllnessEpisodeRequest" responses: "201": description: Episode created. @@ -16063,51 +15552,7 @@ paths: content: application/json: schema: - type: object - properties: - label: - type: string - minLength: 1 - maxLength: 120 - type: - type: string - enum: - - INFECTION - - ALLERGY - - INJURY - - MENTAL_HEALTH - - AUTOIMMUNE - - CHRONIC - - OTHER - lifecycle: - type: string - enum: - - ACUTE - - CHRONIC_ONGOING - - RECURRING - - FLARE - onsetAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - resolvedAt: - anyOf: - - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - - type: "null" - parentConditionId: - anyOf: - - type: string - minLength: 1 - maxLength: 40 - - type: "null" - note: - anyOf: - - type: string - maxLength: 2000 - - type: "null" - additionalProperties: false + $ref: "#/components/schemas/UpdateIllnessEpisodeRequest" responses: "200": description: Episode updated. @@ -16192,12 +15637,7 @@ paths: content: application/json: schema: - type: object - properties: - resolvedAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + $ref: "#/components/schemas/ResolveIllnessEpisodeRequest" responses: "200": description: Episode resolved. @@ -16279,50 +15719,7 @@ paths: content: application/json: schema: - type: object - properties: - date: - type: string - pattern: ^\d{4}-\d{2}-\d{2}$ - functionalImpact: - anyOf: - - type: integer - minimum: 0 - maximum: 3 - - type: "null" - feverC: - anyOf: - - type: number - minimum: 30 - maximum: 45 - - type: "null" - symptoms: - maxItems: 40 - type: array - items: - type: object - properties: - key: - type: string - minLength: 1 - maxLength: 80 - severity: - type: integer - minimum: 1 - maximum: 3 - required: - - key - note: - anyOf: - - type: string - maxLength: 2000 - - type: "null" - loggedAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - required: - - date + $ref: "#/components/schemas/UpsertIllnessDayLogRequest" responses: "200": description: Existing day-log updated. @@ -16455,50 +15852,7 @@ paths: content: application/json: schema: - type: object - properties: - instrument: - type: string - enum: - - PHQ9 - - GAD7 - - WHO5 - - SCI - items: - minItems: 1 - maxItems: 12 - type: array - items: - type: integer - minimum: 0 - maximum: 5 - functionalDifficulty: - type: integer - minimum: 0 - maximum: 3 - takenAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ - tz: - type: string - maxLength: 64 - locale: - type: string - maxLength: 16 - source: - default: WEB - type: string - enum: - - WEB - - IOS - externalId: - type: string - minLength: 1 - maxLength: 120 - required: - - instrument - - items + $ref: "#/components/schemas/CreateMentalHealthAssessmentRequest" responses: "201": description: Assessment recorded. @@ -16817,44 +16171,7 @@ paths: content: application/json: schema: - type: object - properties: - relationship: - type: string - enum: - - MOTHER - - FATHER - - SISTER - - BROTHER - - DAUGHTER - - SON - - GRANDMOTHER_MATERNAL - - GRANDFATHER_MATERNAL - - GRANDMOTHER_PATERNAL - - GRANDFATHER_PATERNAL - - AUNT - - UNCLE - - COUSIN - - HALF_SIBLING - - OTHER - condition: - type: string - minLength: 1 - maxLength: 160 - ageAtOnset: - anyOf: - - type: integer - minimum: 0 - maximum: 120 - - type: "null" - note: - anyOf: - - type: string - maxLength: 2000 - - type: "null" - required: - - relationship - - condition + $ref: "#/components/schemas/CreateFamilyHistoryRequest" responses: "201": description: Entry created. @@ -16912,42 +16229,7 @@ paths: content: application/json: schema: - type: object - properties: - relationship: - type: string - enum: - - MOTHER - - FATHER - - SISTER - - BROTHER - - DAUGHTER - - SON - - GRANDMOTHER_MATERNAL - - GRANDFATHER_MATERNAL - - GRANDMOTHER_PATERNAL - - GRANDFATHER_PATERNAL - - AUNT - - UNCLE - - COUSIN - - HALF_SIBLING - - OTHER - condition: - type: string - minLength: 1 - maxLength: 160 - ageAtOnset: - anyOf: - - type: integer - minimum: 0 - maximum: 120 - - type: "null" - note: - anyOf: - - type: string - maxLength: 2000 - - type: "null" - additionalProperties: false + $ref: "#/components/schemas/UpdateFamilyHistoryRequest" responses: "200": description: Entry updated. @@ -17245,49 +16527,7 @@ paths: content: application/json: schema: - type: object - properties: - ids: - minItems: 1 - maxItems: 100 - type: array - items: - type: string - minLength: 1 - maxLength: 40 - action: - type: string - enum: - - setKind - - linkEpisode - - unlinkEpisode - - linkEncounter - - unlinkEncounter - - delete - - restore - kind: - type: string - enum: - - DOCTOR_REPORT - - DISCHARGE_LETTER - - LAB_RESULT - - IMAGING - - PRESCRIPTION - - REFERRAL - - INSURANCE - - VACCINATION - - OTHER - episodeId: - type: string - minLength: 1 - maxLength: 40 - encounterId: - type: string - minLength: 1 - maxLength: 40 - required: - - ids - - action + $ref: "#/components/schemas/DocumentBulkRequest" responses: "200": description: Per-id outcomes. @@ -17338,44 +16578,7 @@ paths: content: application/json: schema: - type: object - properties: - title: - anyOf: - - type: string - maxLength: 200 - - type: "null" - kind: - type: string - enum: - - DOCTOR_REPORT - - DISCHARGE_LETTER - - LAB_RESULT - - IMAGING - - PRESCRIPTION - - REFERRAL - - INSURANCE - - VACCINATION - - OTHER - documentDate: - anyOf: - - type: string - pattern: ^\d{4}-\d{2}-\d{2}$ - - type: "null" - episodeIds: - maxItems: 20 - type: array - items: - type: string - minLength: 1 - maxLength: 40 - encounterIds: - maxItems: 20 - type: array - items: - type: string - minLength: 1 - maxLength: 40 + $ref: "#/components/schemas/DocumentUpdateRequest" responses: "200": description: The updated document. @@ -17871,146 +17074,7 @@ paths: content: application/json: schema: - oneOf: - - type: object - properties: - factType: - type: string - const: CONDITION - label: - type: string - minLength: 1 - maxLength: 300 - code: - anyOf: - - type: string - maxLength: 64 - - type: "null" - codeSystem: - anyOf: - - type: string - enum: - - SNOMED - - ICD10 - - LOINC - - RXNORM - - ATC - - type: "null" - clinicalStatus: - anyOf: - - type: string - maxLength: 64 - - type: "null" - verificationStatus: - anyOf: - - type: string - maxLength: 64 - - type: "null" - onsetDate: - anyOf: - - type: string - pattern: ^\d{4}-\d{2}-\d{2}$ - - type: "null" - required: - - factType - - label - - type: object - properties: - factType: - type: string - const: OBSERVATION - label: - type: string - minLength: 1 - maxLength: 300 - code: - anyOf: - - type: string - maxLength: 64 - - type: "null" - codeSystem: - anyOf: - - type: string - enum: - - SNOMED - - ICD10 - - LOINC - - RXNORM - - ATC - - type: "null" - value: - anyOf: - - type: number - - type: "null" - valueText: - anyOf: - - type: string - maxLength: 300 - - type: "null" - unit: - anyOf: - - type: string - maxLength: 80 - - type: "null" - referenceLow: - anyOf: - - type: number - - type: "null" - referenceHigh: - anyOf: - - type: number - - type: "null" - referenceText: - anyOf: - - type: string - maxLength: 120 - - type: "null" - effectiveDate: - anyOf: - - type: string - pattern: ^\d{4}-\d{2}-\d{2}$ - - type: "null" - required: - - factType - - label - - type: object - properties: - factType: - type: string - const: MEDICATION_STATEMENT - name: - type: string - minLength: 1 - maxLength: 200 - dose: - anyOf: - - type: string - maxLength: 120 - - type: "null" - rxNormCode: - anyOf: - - type: string - maxLength: 20 - - type: "null" - atcCode: - anyOf: - - type: string - maxLength: 16 - - type: "null" - statusStated: - anyOf: - - type: string - maxLength: 64 - - type: "null" - effectiveDate: - anyOf: - - type: string - pattern: ^\d{4}-\d{2}-\d{2}$ - - type: "null" - required: - - factType - - name - type: object + $ref: "#/components/schemas/InboundFactEditRequest" responses: "200": description: The updated fact. @@ -18041,29 +17105,7 @@ paths: content: application/json: schema: - type: object - properties: - decisions: - minItems: 1 - maxItems: 120 - type: array - items: - type: object - properties: - factId: - type: string - minLength: 1 - maxLength: 40 - action: - type: string - enum: - - approve - - reject - required: - - factId - - action - required: - - decisions + $ref: "#/components/schemas/InboundConfirmRequest" responses: "200": description: Per-decision outcome. @@ -18106,33 +17148,7 @@ paths: content: application/json: schema: - type: object - properties: - name: - type: string - minLength: 1 - maxLength: 120 - unit: - type: string - minLength: 1 - maxLength: 40 - targetLow: - type: number - targetHigh: - type: number - decimals: - type: integer - minimum: 0 - maximum: 6 - description: - type: string - maxLength: 2000 - correlationEnabled: - default: false - type: boolean - required: - - name - - unit + $ref: "#/components/schemas/CreateCustomMetricRequest" responses: "201": description: Created custom metric. @@ -18195,37 +17211,7 @@ paths: content: application/json: schema: - type: object - properties: - name: - type: string - minLength: 1 - maxLength: 120 - unit: - type: string - minLength: 1 - maxLength: 40 - targetLow: - anyOf: - - type: number - - type: "null" - targetHigh: - anyOf: - - type: number - - type: "null" - decimals: - anyOf: - - type: integer - minimum: 0 - maximum: 6 - - type: "null" - description: - anyOf: - - type: string - maxLength: 2000 - - type: "null" - correlationEnabled: - type: boolean + $ref: "#/components/schemas/UpdateCustomMetricRequest" responses: "200": description: Updated custom metric. @@ -18322,23 +17308,7 @@ paths: content: application/json: schema: - type: object - properties: - value: - type: number - measuredAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ - note: - anyOf: - - type: string - maxLength: 2000 - - type: string - const: "" - required: - - value - - measuredAt + $ref: "#/components/schemas/CreateCustomMetricEntryRequest" responses: "201": description: Created value. @@ -18374,22 +17344,7 @@ paths: content: application/json: schema: - type: object - properties: - value: - type: number - measuredAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ - note: - anyOf: - - anyOf: - - type: string - maxLength: 2000 - - type: string - const: "" - - type: "null" + $ref: "#/components/schemas/UpdateCustomMetricEntryRequest" responses: "200": description: Updated value. @@ -23891,6 +22846,101 @@ components: description: Free-text medication description payload. The route runs the text through the Coach provider chain and returns a partial structured payload the wizard merges. Rate-limited 10 requests / 5 minutes / user; budget-gated against the daily Coach token ceiling. + SetMedicationEfficacyTargetRequest: + type: object + properties: + clear: + type: boolean + measurementType: + type: string + enum: + - WEIGHT + - BLOOD_PRESSURE_SYS + - BLOOD_PRESSURE_DIA + - PULSE + - BODY_FAT + - SLEEP_DURATION + - ACTIVITY_STEPS + - BLOOD_GLUCOSE + - TOTAL_BODY_WATER + - BONE_MASS + - OXYGEN_SATURATION + - HEART_RATE_VARIABILITY + - RESTING_HEART_RATE + - ACTIVE_ENERGY_BURNED + - FLIGHTS_CLIMBED + - WALKING_RUNNING_DISTANCE + - VO2_MAX + - BODY_TEMPERATURE + - FAT_FREE_MASS + - FAT_MASS + - MUSCLE_MASS + - SKIN_TEMPERATURE + - PULSE_WAVE_VELOCITY + - VASCULAR_AGE + - VISCERAL_FAT + - AUDIO_EXPOSURE_ENV + - AUDIO_EXPOSURE_HEADPHONE + - TIME_IN_DAYLIGHT + - WALKING_STEADINESS + - AUDIO_EXPOSURE_EVENT + - RESPIRATORY_RATE + - BODY_MASS_INDEX + - LEAN_BODY_MASS + - WALKING_HEART_RATE_AVERAGE + - WALKING_ASYMMETRY + - WALKING_DOUBLE_SUPPORT + - WALKING_STEP_LENGTH + - WALKING_SPEED + - CARDIO_RECOVERY + - WRIST_TEMPERATURE + - FALL_COUNT + - SIX_MINUTE_WALK_DISTANCE + - STAIR_ASCENT_SPEED + - STAIR_DESCENT_SPEED + - BREATHING_DISTURBANCES + - IRREGULAR_RHYTHM_NOTIFICATION + - HIGH_HEART_RATE_EVENT + - LOW_HEART_RATE_EVENT + - WALKING_STEADINESS_EVENT + - BREATHING_DISTURBANCE_EVENT + - RECOVERY_SCORE + - STRESS_SCORE + - STRAIN_SCORE + - HRV_RMSSD + - DAY_STRAIN + - WORKOUT_STRAIN + - SLEEP_PERFORMANCE + - SLEEP_EFFICIENCY + - SLEEP_CONSISTENCY + - SLEEP_NEED + - ENERGY_EXPENDITURE_KJ + - AVERAGE_HEART_RATE + - MAX_HEART_RATE + - SLEEP_DISTURBANCE_COUNT + - ANS_CHARGE + - CARDIO_LOAD + - SLEEP_SCORE + - BODY_TEMPERATURE_DEVIATION + - RESILIENCE + - PHQ9_SCORE + - GAD7_SCORE + - GRIP_STRENGTH + - PAIN_NRS + - WAIST_CIRCUMFERENCE + - WAIST_TO_HEIGHT + - WHO5_SCORE + - SCI_SCORE + biomarkerId: + type: string + minLength: 1 + maxLength: 64 + primary: + type: boolean + description: Set or clear the user's explicit efficacy-target override for a medication. `clear:true` removes the + override so the resolver reverts to the derived (ATC class prefix → name inference) target; otherwise pin + exactly ONE of `measurementType` (a metric series) / `biomarkerId` (a lab analyte). `userId` is never a field — + ownership is narrowed through the medication (and the biomarker for a lab target). CreateScheduleRevisionRequest: type: object properties: @@ -25467,6 +24517,85 @@ components: description: "The day around a mood entry: work, contacts, leisure and one notable event. Every field optional. Sleep, activity, vitals and body symptoms are deliberately ABSENT — they are owned by their own modules and are read from there, never captured a second time here." + CreateMoodTagRequest: + type: object + properties: + label: + type: string + minLength: 1 + maxLength: 40 + icon: + anyOf: + - type: string + - type: "null" + categoryKey: + type: string + minLength: 1 + maxLength: 80 + required: + - label + description: Create a per-user custom mood tag (BINARY only). `label` is encrypted at rest. `icon` must come from the + curated allowlist (unknown name → 422). `categoryKey` picks the home group — any seeded category key or one of + the caller's own `customcat:` group keys; omitted → the seeded `custom` category. Capped at 50 active custom + tags per user (422). + UpdateMoodTagRequest: + type: object + properties: + label: + type: string + minLength: 1 + maxLength: 40 + icon: + anyOf: + - type: string + - type: "null" + isActive: + type: boolean + categoryKey: + type: string + minLength: 1 + maxLength: 80 + description: Partial custom-tag edit; at least one field required. `isActive:false` archives (history intact), + `isActive:true` restores. `categoryKey` moves the tag to another group (a real categoryId move). + HideMoodTagRequest: + type: object + properties: + hidden: + type: boolean + required: + - hidden + description: Hide / show a CATALOGUE tag for the calling user. Custom tags are hidden via their own `isActive` flag on + the custom PATCH instead (this route 400s a `custom:` key). + CreateMoodTagGroupRequest: + type: object + properties: + label: + type: string + minLength: 1 + maxLength: 40 + icon: + anyOf: + - type: string + - type: "null" + required: + - label + description: Create a per-user custom mood-tag group. `label` is encrypted at rest; `icon` must come from the curated + allowlist. Capped at 12 active custom groups per user (422). + UpdateMoodTagGroupRequest: + type: object + properties: + label: + type: string + minLength: 1 + maxLength: 40 + icon: + anyOf: + - type: string + - type: "null" + isActive: + type: boolean + description: Partial custom-group edit; at least one field required. `isActive:false` retires the group without touching + its tags. MoodTagLayoutPutRequest: type: object properties: @@ -26467,6 +25596,143 @@ components: reminder's notifyHour in the profile timezone. Must be at least tomorrow and at most five years out. The regular cadence is untouched: lastSatisfiedAt and the anchor stay where they are, and the reminder resumes its normal interval after the snoozed cycle." + CreateLabResultRequest: + type: object + properties: + biomarkerId: + type: string + minLength: 1 + maxLength: 64 + panel: + anyOf: + - type: string + minLength: 1 + maxLength: 120 + - type: string + const: "" + analyte: + type: string + minLength: 1 + maxLength: 120 + value: + type: number + valueText: + type: string + minLength: 1 + maxLength: 120 + unit: + type: string + minLength: 1 + maxLength: 40 + referenceLow: + type: number + referenceHigh: + type: number + sourceReferenceLow: + type: number + sourceReferenceHigh: + type: number + sourceReferenceText: + anyOf: + - type: string + maxLength: 120 + - type: string + const: "" + takenAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + note: + anyOf: + - type: string + maxLength: 2000 + - type: string + const: "" + source: + type: string + enum: + - MANUAL + - OCR + encounterId: + type: string + minLength: 1 + maxLength: 40 + required: + - takenAt + description: 'Record a single biomarker reading (HbA1c, LDL, ferritin, TSH, …). A reading is EITHER numeric (`value` + + `unit`, optional reference bounds) OR qualitative (`valueText`, e.g. "negativ" / "positiv") — exactly one, never + both, never neither. `analyte` + `unit` are free-form (a lab prints its own naming); a qualitative reading needs + no unit / bounds. When both numeric bounds are present `referenceLow` must not exceed `referenceHigh`. `takenAt` + is a backdatable ISO instant (no future, ≤ 50 years past). The optional `note` is encrypted at rest. The + optional `source` records provenance: `"OCR"` for an on-device-OCR capture (the raw image never reaches the + server on this path), defaulting to `"MANUAL"` when omitted. The optional `encounterId` files the reading + against a visit the caller owns — always optional, and a link that could not be made never fails the reading.' + UpdateLabResultRequest: + type: object + properties: + panel: + anyOf: + - anyOf: + - type: string + minLength: 1 + maxLength: 120 + - type: string + const: "" + - type: "null" + analyte: + type: string + minLength: 1 + maxLength: 120 + value: + type: number + valueText: + type: string + minLength: 1 + maxLength: 120 + unit: + type: string + minLength: 1 + maxLength: 40 + referenceLow: + anyOf: + - type: number + - type: "null" + referenceHigh: + anyOf: + - type: number + - type: "null" + sourceReferenceLow: + anyOf: + - type: number + - type: "null" + sourceReferenceHigh: + anyOf: + - type: number + - type: "null" + sourceReferenceText: + anyOf: + - anyOf: + - type: string + maxLength: 120 + - type: string + const: "" + - type: "null" + takenAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + note: + anyOf: + - anyOf: + - type: string + maxLength: 2000 + - type: string + const: "" + - type: "null" + description: Partial edit of a lab result. An omitted key leaves the column untouched; an explicit `null` on `panel` / + `note` / a reference bound clears it. Edit the matching value field for the row's type — `value` for a numeric + row, `valueText` for a qualitative one; a request must not carry both, and a cross-type edit (the wrong field + for the row) is rejected. RestoreLabResultsRequest: type: object properties: @@ -26490,6 +25756,593 @@ components: required: - mode - text + CreateVaccinationRequest: + type: object + properties: + occurredAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + antigenSlug: + anyOf: + - type: string + minLength: 1 + maxLength: 64 + - type: "null" + vaccineName: + anyOf: + - type: string + minLength: 1 + maxLength: 200 + - type: "null" + doseNumber: + anyOf: + - type: integer + minimum: 1 + maximum: 20 + - type: "null" + seriesDoses: + anyOf: + - type: integer + minimum: 1 + maximum: 10 + - type: "null" + lotNumber: + anyOf: + - type: string + minLength: 1 + maxLength: 64 + - type: "null" + site: + anyOf: + - type: string + enum: + - LEFT_ARM + - RIGHT_ARM + - LEFT_THIGH + - RIGHT_THIGH + - ORAL + - NASAL + - OTHER + - type: "null" + practitionerId: + anyOf: + - type: string + minLength: 1 + maxLength: 40 + - type: "null" + encounterId: + anyOf: + - type: string + minLength: 1 + maxLength: 40 + - type: "null" + note: + anyOf: + - type: string + maxLength: 2000 + - type: "null" + documentIds: + maxItems: 100 + type: array + items: + type: string + minLength: 1 + maxLength: 40 + required: + - occurredAt + additionalProperties: false + description: "Log one administered dose. `occurredAt` is required and must not be in the future — a vaccination is + recorded after the fact, and a planned booster is a reminder rather than a record. Exactly one identity arm is + required: either `antigenSlug` (a slug the catalogue offers) or `vaccineName` (whatever the person's own record + says, verbatim, which may be a trade name). Everything else is optional, so a decades-old paper entry with + nothing but a date and a name is loggable. `note` is encrypted at rest. `documentIds` pre-links the scanned + pages; an id naming nothing the caller owns is dropped rather than refused, so a link never blocks a save. + Logging a dose satisfies any booster reminder keyed to one of its component antigens." + UpdateVaccinationRequest: + type: object + properties: + occurredAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + antigenSlug: + anyOf: + - type: string + minLength: 1 + maxLength: 64 + - type: "null" + vaccineName: + anyOf: + - type: string + minLength: 1 + maxLength: 200 + - type: "null" + doseNumber: + anyOf: + - type: integer + minimum: 1 + maximum: 20 + - type: "null" + seriesDoses: + anyOf: + - type: integer + minimum: 1 + maximum: 10 + - type: "null" + lotNumber: + anyOf: + - type: string + minLength: 1 + maxLength: 64 + - type: "null" + site: + anyOf: + - type: string + enum: + - LEFT_ARM + - RIGHT_ARM + - LEFT_THIGH + - RIGHT_THIGH + - ORAL + - NASAL + - OTHER + - type: "null" + practitionerId: + anyOf: + - type: string + minLength: 1 + maxLength: 40 + - type: "null" + encounterId: + anyOf: + - type: string + minLength: 1 + maxLength: 40 + - type: "null" + note: + anyOf: + - type: string + maxLength: 2000 + - type: "null" + documentIds: + maxItems: 100 + type: array + items: + type: string + minLength: 1 + maxLength: 40 + additionalProperties: false + description: Partial edit of a dose; an omitted key leaves the column untouched, and a body naming nothing is a 422. + `occurredAt` is editable because a transcription typo is the common case — and editing it deliberately does NOT + re-run the booster satisfaction, so correcting a date can never move a reminder's due date. An edit that would + leave the record with neither identity arm is refused. A present `documentIds` array replaces the links, empty + array included. + VaccinationBoosterRequest: + type: object + properties: + intervalMonths: + type: integer + minimum: 1 + maximum: 600 + label: + type: string + minLength: 1 + maxLength: 120 + notifyHour: + type: integer + minimum: 0 + maximum: 23 + required: + - intervalMonths + - label + additionalProperties: false + description: "The confirm body for a booster reminder — the person's own values, accepted or edited from the catalogue's + prefill. `intervalMonths` is bounded at 600 (a decade booster is 120); `label` is composed client-side so its + locale is the person's own. There is no antigen field: the server reads the antigen from the dose's catalogue + entry, never from the request, so a client cannot key a reminder onto an antigen the dose does not contain." + VaccinationLinkRequest: + type: object + properties: + targetKind: + type: string + enum: + - document + targetIds: + minItems: 1 + maxItems: 100 + type: array + items: + type: string + minLength: 1 + maxLength: 40 + required: + - targetKind + - targetIds + additionalProperties: false + description: Documents to file against, or unfile from, a dose. Idempotent in both directions and capped at 100 ids. Ids + naming nothing the caller owns come back in `unknown` rather than failing the request. + CreateIllnessEpisodeRequest: + type: object + properties: + label: + type: string + minLength: 1 + maxLength: 120 + type: + type: string + enum: + - INFECTION + - ALLERGY + - INJURY + - MENTAL_HEALTH + - AUTOIMMUNE + - CHRONIC + - OTHER + lifecycle: + default: ACUTE + type: string + enum: + - ACUTE + - CHRONIC_ONGOING + - RECURRING + - FLARE + onsetAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + resolvedAt: + anyOf: + - type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + - type: "null" + parentConditionId: + anyOf: + - type: string + minLength: 1 + maxLength: 40 + - type: "null" + note: + anyOf: + - type: string + maxLength: 2000 + - type: "null" + required: + - label + - type + description: Open an illness/condition episode. `label` is the user-facing name; `type` + `lifecycle` classify it. + `onsetAt` defaults to 'now' server-side when omitted. `parentConditionId` threads a FLARE/RECURRING bout under a + parent condition (must be an owned, live episode). The optional `note` is encrypted at rest. + UpdateIllnessEpisodeRequest: + type: object + properties: + label: + type: string + minLength: 1 + maxLength: 120 + type: + type: string + enum: + - INFECTION + - ALLERGY + - INJURY + - MENTAL_HEALTH + - AUTOIMMUNE + - CHRONIC + - OTHER + lifecycle: + type: string + enum: + - ACUTE + - CHRONIC_ONGOING + - RECURRING + - FLARE + onsetAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + resolvedAt: + anyOf: + - type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + - type: "null" + parentConditionId: + anyOf: + - type: string + minLength: 1 + maxLength: 40 + - type: "null" + note: + anyOf: + - type: string + maxLength: 2000 + - type: "null" + additionalProperties: false + description: Partial edit of an episode; an omitted key leaves the column untouched. A `null` `resolvedAt` re-opens a + resolved episode; a `null` `parentConditionId` detaches it from its parent. Rejects unknown keys. + ResolveIllnessEpisodeRequest: + type: object + properties: + resolvedAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + description: Mark an episode recovered. `resolvedAt` defaults to 'now' when omitted, so the one-tap 'mark recovered' + affordance can send an empty body. A CHRONIC_ONGOING episode has no recovery date by design and 422s. + UpsertIllnessDayLogRequest: + type: object + properties: + date: + type: string + pattern: ^\d{4}-\d{2}-\d{2}$ + functionalImpact: + anyOf: + - type: integer + minimum: 0 + maximum: 3 + - type: "null" + feverC: + anyOf: + - type: number + minimum: 30 + maximum: 45 + - type: "null" + symptoms: + maxItems: 40 + type: array + items: + type: object + properties: + key: + type: string + minLength: 1 + maxLength: 80 + severity: + type: integer + minimum: 1 + maximum: 3 + required: + - key + note: + anyOf: + - type: string + maxLength: 2000 + - type: "null" + loggedAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + required: + - date + description: Upsert one day's symptom / functional-impact / fever row for an episode (keyed on `(episodeId, date)`). + `date` is a `YYYY-MM-DD` tz-anchored day. `functionalImpact` (0–3) + `feverC` are queryable plaintext; + `symptoms` carry an optional 0–3 Jackson/WURSS severity per link; the optional `note` is encrypted at rest. + CreateMentalHealthAssessmentRequest: + type: object + properties: + instrument: + type: string + enum: + - PHQ9 + - GAD7 + - WHO5 + - SCI + items: + minItems: 1 + maxItems: 12 + type: array + items: + type: integer + minimum: 0 + maximum: 5 + functionalDifficulty: + type: integer + minimum: 0 + maximum: 3 + takenAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + tz: + type: string + maxLength: 64 + locale: + type: string + maxLength: 16 + source: + default: WEB + type: string + enum: + - WEB + - IOS + externalId: + type: string + minLength: 1 + maxLength: 120 + required: + - instrument + - items + description: "Record one completed screener administration: PHQ-9 (9 items, 0–3), GAD-7 (7 items, 0–3), WHO-5 (5 items, + 0–5) or SCI (8 items, 0–4). The array length and per-item ceiling must match the instrument. The item answers + are encrypted at rest and never returned. The server computes the total (WHO-5 reports raw-sum × 4 as 0–100) + + severity band + item-9 safety flag. Opt-in, beside mood tracking — this is a screening surface, not a + diagnosis." + CreateFamilyHistoryRequest: + type: object + properties: + relationship: + type: string + enum: + - MOTHER + - FATHER + - SISTER + - BROTHER + - DAUGHTER + - SON + - GRANDMOTHER_MATERNAL + - GRANDFATHER_MATERNAL + - GRANDMOTHER_PATERNAL + - GRANDFATHER_PATERNAL + - AUNT + - UNCLE + - COUSIN + - HALF_SIBLING + - OTHER + condition: + type: string + minLength: 1 + maxLength: 160 + ageAtOnset: + anyOf: + - type: integer + minimum: 0 + maximum: 120 + - type: "null" + note: + anyOf: + - type: string + maxLength: 2000 + - type: "null" + required: + - relationship + - condition + description: Record one condition for one relative (a FHIR FamilyMemberHistory with a single condition). `condition` is + the user-facing label (the `condition.code.text` anchor — never a machine-guessed code); `ageAtOnset` is the + relative's age (years) when it began. The free-text `note` is encrypted at rest. Patient-reported. + UpdateFamilyHistoryRequest: + type: object + properties: + relationship: + type: string + enum: + - MOTHER + - FATHER + - SISTER + - BROTHER + - DAUGHTER + - SON + - GRANDMOTHER_MATERNAL + - GRANDFATHER_MATERNAL + - GRANDMOTHER_PATERNAL + - GRANDFATHER_PATERNAL + - AUNT + - UNCLE + - COUSIN + - HALF_SIBLING + - OTHER + condition: + type: string + minLength: 1 + maxLength: 160 + ageAtOnset: + anyOf: + - type: integer + minimum: 0 + maximum: 120 + - type: "null" + note: + anyOf: + - type: string + maxLength: 2000 + - type: "null" + additionalProperties: false + description: Partial edit of a family-history entry; an omitted key leaves the column untouched. A `null` + `ageAtOnset`/`note` clears that field. Rejects unknown keys. + DocumentBulkRequest: + type: object + properties: + ids: + minItems: 1 + maxItems: 100 + type: array + items: + type: string + minLength: 1 + maxLength: 40 + action: + type: string + enum: + - setKind + - linkEpisode + - unlinkEpisode + - linkEncounter + - unlinkEncounter + - delete + - restore + kind: + type: string + enum: + - DOCTOR_REPORT + - DISCHARGE_LETTER + - LAB_RESULT + - IMAGING + - PRESCRIPTION + - REFERRAL + - INSURANCE + - VACCINATION + - OTHER + episodeId: + type: string + minLength: 1 + maxLength: 40 + encounterId: + type: string + minLength: 1 + maxLength: 40 + required: + - ids + - action + description: "One bulk action over up to 100 owner-scoped documents: `setKind` (requires `kind`), `linkEpisode` / + `unlinkEpisode` (require `episodeId`), `linkEncounter` / `unlinkEncounter` (require `encounterId`), `delete` + (tombstone, undo-able for 30 days), `restore` (clear tombstone). Partial failures never abort the batch — see + the per-id result array." + DocumentUpdateRequest: + type: object + properties: + title: + anyOf: + - type: string + maxLength: 200 + - type: "null" + kind: + type: string + enum: + - DOCTOR_REPORT + - DISCHARGE_LETTER + - LAB_RESULT + - IMAGING + - PRESCRIPTION + - REFERRAL + - INSURANCE + - VACCINATION + - OTHER + documentDate: + anyOf: + - type: string + pattern: ^\d{4}-\d{2}-\d{2}$ + - type: "null" + episodeIds: + maxItems: 20 + type: array + items: + type: string + minLength: 1 + maxLength: 40 + encounterIds: + maxItems: 20 + type: array + items: + type: string + minLength: 1 + maxLength: 40 + description: "Metadata edit for a stored document: `title` (user label; null clears it), `kind` (category), + `documentDate` (user filing date, YYYY-MM-DD; null clears it), `episodeIds` (REPLACE-SET of condition links — + the document's links become exactly this set; an empty array unlinks everything; every id must be a live episode + of the caller or the whole request answers 404), `encounterIds` (the same replace-set semantics over the + caller's visits). At least one field required. No `userId` field — narrowed from the session and fed to the + Prisma `where` with the row id." InboundExtractRequest: anyOf: - type: object @@ -26539,6 +26392,285 @@ components: - de required: - message + InboundFactEditRequest: + oneOf: + - type: object + properties: + factType: + type: string + const: CONDITION + label: + type: string + minLength: 1 + maxLength: 300 + code: + anyOf: + - type: string + maxLength: 64 + - type: "null" + codeSystem: + anyOf: + - type: string + enum: + - SNOMED + - ICD10 + - LOINC + - RXNORM + - ATC + - type: "null" + clinicalStatus: + anyOf: + - type: string + maxLength: 64 + - type: "null" + verificationStatus: + anyOf: + - type: string + maxLength: 64 + - type: "null" + onsetDate: + anyOf: + - type: string + pattern: ^\d{4}-\d{2}-\d{2}$ + - type: "null" + required: + - factType + - label + - type: object + properties: + factType: + type: string + const: OBSERVATION + label: + type: string + minLength: 1 + maxLength: 300 + code: + anyOf: + - type: string + maxLength: 64 + - type: "null" + codeSystem: + anyOf: + - type: string + enum: + - SNOMED + - ICD10 + - LOINC + - RXNORM + - ATC + - type: "null" + value: + anyOf: + - type: number + - type: "null" + valueText: + anyOf: + - type: string + maxLength: 300 + - type: "null" + unit: + anyOf: + - type: string + maxLength: 80 + - type: "null" + referenceLow: + anyOf: + - type: number + - type: "null" + referenceHigh: + anyOf: + - type: number + - type: "null" + referenceText: + anyOf: + - type: string + maxLength: 120 + - type: "null" + effectiveDate: + anyOf: + - type: string + pattern: ^\d{4}-\d{2}-\d{2}$ + - type: "null" + required: + - factType + - label + - type: object + properties: + factType: + type: string + const: MEDICATION_STATEMENT + name: + type: string + minLength: 1 + maxLength: 200 + dose: + anyOf: + - type: string + maxLength: 120 + - type: "null" + rxNormCode: + anyOf: + - type: string + maxLength: 20 + - type: "null" + atcCode: + anyOf: + - type: string + maxLength: 16 + - type: "null" + statusStated: + anyOf: + - type: string + maxLength: 64 + - type: "null" + effectiveDate: + anyOf: + - type: string + pattern: ^\d{4}-\d{2}-\d{2}$ + - type: "null" + required: + - factType + - name + description: A correction to a staged fact before approval (fixes OCR / units / dates / codes). Discriminated by + `factType` so the edit cannot change the fact's resource type. A successful edit clears `needsReview` — the + values become user-asserted. + type: object + InboundConfirmRequest: + type: object + properties: + decisions: + minItems: 1 + maxItems: 120 + type: array + items: + type: object + properties: + factId: + type: string + minLength: 1 + maxLength: 40 + action: + type: string + enum: + - approve + - reject + required: + - factId + - action + required: + - decisions + description: The approve/reject decisions a human made on the review screen. Each names a staged fact by id. Approved + facts are committed to the structured stores (labs / conditions / medications) through their normal create + paths; rejected facts are discarded. A fact still flagged `needsReview` (below the confidence floor, never + edited) cannot be approved — it is reported back as `needsReview`. No `userId` field — it is narrowed from the + session; every fact id is re-scoped to the document + caller. + CreateCustomMetricRequest: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 120 + unit: + type: string + minLength: 1 + maxLength: 40 + targetLow: + type: number + targetHigh: + type: number + decimals: + type: integer + minimum: 0 + maximum: 6 + description: + type: string + maxLength: 2000 + correlationEnabled: + default: false + type: boolean + required: + - name + - unit + description: "Define a user-scoped custom metric ONCE: free-text `name` + `unit`, an optional target window (`targetLow` + / `targetHigh`; when both present `targetLow` must not exceed `targetHigh`), optional display `decimals`, an + optional `description`, and explicit `correlationEnabled` opt-in. The name is unique per user. Logging a value + later snapshots its unit." + UpdateCustomMetricRequest: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 120 + unit: + type: string + minLength: 1 + maxLength: 40 + targetLow: + anyOf: + - type: number + - type: "null" + targetHigh: + anyOf: + - type: number + - type: "null" + decimals: + anyOf: + - type: integer + minimum: 0 + maximum: 6 + - type: "null" + description: + anyOf: + - type: string + maxLength: 2000 + - type: "null" + correlationEnabled: + type: boolean + description: Partial edit of a custom metric. An omitted key leaves the column untouched; an explicit `null` on a target + bound / `decimals` / `description` clears it. `correlationEnabled` explicitly controls use as a bounded + behaviour channel. A conflicting rename is rejected 409. + CreateCustomMetricEntryRequest: + type: object + properties: + value: + type: number + measuredAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + note: + anyOf: + - type: string + maxLength: 2000 + - type: string + const: "" + required: + - value + - measuredAt + description: "Log a value against a custom metric: numeric `value`, ISO 8601 `measuredAt`, optional free-text `note`. + The metric's current unit is snapshotted onto the entry server-side." + UpdateCustomMetricEntryRequest: + type: object + properties: + value: + type: number + measuredAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + note: + anyOf: + - anyOf: + - type: string + maxLength: 2000 + - type: string + const: "" + - type: "null" + description: Partial edit of a logged value. An omitted key leaves the column untouched; an explicit `null` on `note` + clears it. RestoreCustomMetricEntriesRequest: type: object properties: @@ -41296,89 +41428,7 @@ components: type: object properties: data: - type: object - properties: - name: - type: string - minLength: 1 - maxLength: 100 - dose: - type: string - minLength: 1 - maxLength: 50 - doseUnit: - type: string - enum: - - mg - - ml - - iu - - tablets - - drops - - puffs - - sprays - cadenceKind: - type: string - enum: - - daily - - weekdays - - everyNWeeks - - monthly - - everyNMonths - - yearly - - rolling - - oneShot - weekdays: - maxItems: 7 - type: array - items: - type: string - enum: - - MO - - TU - - WE - - TH - - FR - - SA - - SU - intervalWeeks: - type: integer - minimum: 1 - maximum: 52 - intervalMonths: - type: integer - minimum: 1 - maximum: 12 - dayOfMonth: - type: integer - minimum: 1 - maximum: 31 - rollingIntervalDays: - type: integer - minimum: 1 - maximum: 365 - timesOfDay: - maxItems: 12 - type: array - items: - type: string - pattern: ^([01]\d|2[0-3]):[0-5]\d$ - startsOn: - type: string - pattern: ^\d{4}-\d{2}-\d{2}$ - endsOn: - type: string - pattern: ^\d{4}-\d{2}-\d{2}$ - oneShot: - type: boolean - confidence: - type: object - propertyNames: - type: string - additionalProperties: - type: number - minimum: 0 - maximum: 1 - additionalProperties: false + $ref: "#/components/schemas/MedicationExtractionResult" error: type: "null" meta: @@ -41391,6 +41441,95 @@ components: - data - error additionalProperties: false + MedicationExtractionResult: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 100 + dose: + type: string + minLength: 1 + maxLength: 50 + doseUnit: + type: string + enum: + - mg + - ml + - iu + - tablets + - drops + - puffs + - sprays + cadenceKind: + type: string + enum: + - daily + - weekdays + - everyNWeeks + - monthly + - everyNMonths + - yearly + - rolling + - oneShot + weekdays: + maxItems: 7 + type: array + items: + type: string + enum: + - MO + - TU + - WE + - TH + - FR + - SA + - SU + intervalWeeks: + type: integer + minimum: 1 + maximum: 52 + intervalMonths: + type: integer + minimum: 1 + maximum: 12 + dayOfMonth: + type: integer + minimum: 1 + maximum: 31 + rollingIntervalDays: + type: integer + minimum: 1 + maximum: 365 + timesOfDay: + maxItems: 12 + type: array + items: + type: string + pattern: ^([01]\d|2[0-3]):[0-5]\d$ + startsOn: + type: string + pattern: ^\d{4}-\d{2}-\d{2}$ + endsOn: + type: string + pattern: ^\d{4}-\d{2}-\d{2}$ + oneShot: + type: boolean + confidence: + type: object + propertyNames: + type: string + additionalProperties: + type: number + minimum: 0 + maximum: 1 + additionalProperties: false + description: Citation-guarded partial extraction of medication scheduling fields. Every field is optional; the wizard + merges what is present onto the form state and leaves the rest blank. `name` and `dose` are post-validated + against the original free-text and dropped when not substring-matched, so the wizard cannot silently land a + hallucinated brand or dose. `cadenceKind` / `doseUnit` / `weekdays` are closed enums; numeric fields are clamped + to the wizard's wire bounds. GetMedicationCadenceResponse: type: object properties: @@ -41877,315 +42016,7 @@ components: type: object properties: data: - type: object - properties: - medicationId: - type: string - medicationName: - type: string - eligible: - type: boolean - reason: - type: string - enum: - - one_shot - - no_target - startsOn: - anyOf: - - type: string - - type: "null" - resolution: - type: object - properties: - tier: - type: string - enum: - - override - - atc - - name - - none - cls: - anyOf: - - type: string - - type: "null" - required: - - tier - - cls - additionalProperties: false - windowDays: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - minWeeksPerSide: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - markers: - type: object - properties: - start: - anyOf: - - type: string - - type: "null" - startSource: - anyOf: - - type: string - enum: - - startsOn - - firstReading - - type: "null" - doseChanges: - type: array - items: - type: object - properties: - at: - type: string - label: - type: string - required: - - at - - label - additionalProperties: false - pauses: - type: array - items: - type: object - properties: - from: - type: string - to: - anyOf: - - type: string - - type: "null" - required: - - from - - to - additionalProperties: false - required: - - start - - startSource - - doseChanges - - pauses - additionalProperties: false - targets: - type: array - items: - type: object - properties: - kind: - type: string - enum: - - metric - - lab - key: - type: string - label: - type: string - unit: - anyOf: - - type: string - - type: "null" - primary: - type: boolean - referenceBand: - anyOf: - - type: object - properties: - low: - type: number - high: - type: number - required: - - low - - high - additionalProperties: false - - type: "null" - series: - type: array - items: - type: object - properties: - t: - type: string - value: - type: number - status: - type: string - enum: - - in-range - - below - - above - - unknown - required: - - t - - value - additionalProperties: false - beforeAfter: - type: object - properties: - present: - type: boolean - reason: - type: string - enum: - - insufficient_before - - insufficient_after - - no_start - - no_data - before: - type: object - properties: - mean: - type: number - count: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - from: - type: string - to: - type: string - required: - - mean - - count - - from - - to - additionalProperties: false - after: - type: object - properties: - mean: - type: number - count: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - from: - type: string - to: - type: string - required: - - mean - - count - - from - - to - additionalProperties: false - delta: - anyOf: - - type: object - properties: - mean: - type: number - pct: - anyOf: - - type: number - - type: "null" - required: - - mean - - pct - additionalProperties: false - - type: "null" - required: - - present - additionalProperties: false - levelShift: - anyOf: - - type: object - properties: - present: - type: boolean - at: - type: string - nearStart: - type: boolean - required: - - present - additionalProperties: false - - type: "null" - required: - - kind - - key - - label - - unit - - primary - - referenceBand - - series - - beforeAfter - - levelShift - additionalProperties: false - adherence: - type: array - items: - type: object - properties: - date: - type: string - rate: - type: number - taken: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - missed: - type: integer - minimum: -9007199254740991 - maximum: 9007199254740991 - required: - - date - - rate - - taken - - missed - additionalProperties: false - overrideOptions: - type: object - properties: - metrics: - type: array - items: - type: object - properties: - key: - type: string - label: - type: string - required: - - key - - label - additionalProperties: false - biomarkers: - type: array - items: - type: object - properties: - id: - type: string - label: - type: string - unit: - type: string - required: - - id - - label - - unit - additionalProperties: false - required: - - metrics - - biomarkers - additionalProperties: false - required: - - medicationId - - medicationName - - eligible - - startsOn - - resolution - - windowDays - - minWeeksPerSide - - markers - - targets - - adherence - - overrideOptions - additionalProperties: false + $ref: "#/components/schemas/MedicationEfficacyResponse" error: type: "null" meta: @@ -42198,6 +42029,322 @@ components: - data - error additionalProperties: false + MedicationEfficacyResponse: + type: object + properties: + medicationId: + type: string + medicationName: + type: string + eligible: + type: boolean + reason: + type: string + enum: + - one_shot + - no_target + startsOn: + anyOf: + - type: string + - type: "null" + resolution: + type: object + properties: + tier: + type: string + enum: + - override + - atc + - name + - none + cls: + anyOf: + - type: string + - type: "null" + required: + - tier + - cls + additionalProperties: false + windowDays: + type: integer + minimum: -9007199254740991 + maximum: 9007199254740991 + minWeeksPerSide: + type: integer + minimum: -9007199254740991 + maximum: 9007199254740991 + markers: + type: object + properties: + start: + anyOf: + - type: string + - type: "null" + startSource: + anyOf: + - type: string + enum: + - startsOn + - firstReading + - type: "null" + doseChanges: + type: array + items: + type: object + properties: + at: + type: string + label: + type: string + required: + - at + - label + additionalProperties: false + pauses: + type: array + items: + type: object + properties: + from: + type: string + to: + anyOf: + - type: string + - type: "null" + required: + - from + - to + additionalProperties: false + required: + - start + - startSource + - doseChanges + - pauses + additionalProperties: false + targets: + type: array + items: + type: object + properties: + kind: + type: string + enum: + - metric + - lab + key: + type: string + label: + type: string + unit: + anyOf: + - type: string + - type: "null" + primary: + type: boolean + referenceBand: + anyOf: + - type: object + properties: + low: + type: number + high: + type: number + required: + - low + - high + additionalProperties: false + - type: "null" + series: + type: array + items: + type: object + properties: + t: + type: string + value: + type: number + status: + type: string + enum: + - in-range + - below + - above + - unknown + required: + - t + - value + additionalProperties: false + beforeAfter: + type: object + properties: + present: + type: boolean + reason: + type: string + enum: + - insufficient_before + - insufficient_after + - no_start + - no_data + before: + type: object + properties: + mean: + type: number + count: + type: integer + minimum: -9007199254740991 + maximum: 9007199254740991 + from: + type: string + to: + type: string + required: + - mean + - count + - from + - to + additionalProperties: false + after: + type: object + properties: + mean: + type: number + count: + type: integer + minimum: -9007199254740991 + maximum: 9007199254740991 + from: + type: string + to: + type: string + required: + - mean + - count + - from + - to + additionalProperties: false + delta: + anyOf: + - type: object + properties: + mean: + type: number + pct: + anyOf: + - type: number + - type: "null" + required: + - mean + - pct + additionalProperties: false + - type: "null" + required: + - present + additionalProperties: false + levelShift: + anyOf: + - type: object + properties: + present: + type: boolean + at: + type: string + nearStart: + type: boolean + required: + - present + additionalProperties: false + - type: "null" + required: + - kind + - key + - label + - unit + - primary + - referenceBand + - series + - beforeAfter + - levelShift + additionalProperties: false + adherence: + type: array + items: + type: object + properties: + date: + type: string + rate: + type: number + taken: + type: integer + minimum: -9007199254740991 + maximum: 9007199254740991 + missed: + type: integer + minimum: -9007199254740991 + maximum: 9007199254740991 + required: + - date + - rate + - taken + - missed + additionalProperties: false + overrideOptions: + type: object + properties: + metrics: + type: array + items: + type: object + properties: + key: + type: string + label: + type: string + required: + - key + - label + additionalProperties: false + biomarkers: + type: array + items: + type: object + properties: + id: + type: string + label: + type: string + unit: + type: string + required: + - id + - label + - unit + additionalProperties: false + required: + - metrics + - biomarkers + additionalProperties: false + required: + - medicationId + - medicationName + - eligible + - startsOn + - resolution + - windowDays + - minWeeksPerSide + - markers + - targets + - adherence + - overrideOptions + additionalProperties: false + description: Server-authoritative, strictly-descriptive efficacy view relating a medication to the outcome + metric(s)/lab(s) its class is prescribed to move, around its start. Carries the resolved target(s) with their + series, the start/dose-change/pause markers, a before/after-start comparison (honest `{present:false}` below the + per-side data floor), an adherence lane (cadence-aware per-day rate, never recomputed), an optional conservative + level-shift note, and the retarget options. There is NO verdict / score / assessment field by construction — the + client renders numbers and neutral connective phrasing only, never a causal or dose-advice claim. SetMedicationEfficacyTargetResponse: type: object properties: diff --git a/src/lib/openapi/routes/custom-metrics.ts b/src/lib/openapi/routes/custom-metrics.ts index d24a113dd..77e9ac8fc 100644 --- a/src/lib/openapi/routes/custom-metrics.ts +++ b/src/lib/openapi/routes/custom-metrics.ts @@ -30,25 +30,28 @@ import { stdResponses, } from "./shared"; -createCustomMetricSchema.meta({ +// Zod 4's `.meta()` returns a NEW instance carrying the id rather than +// annotating in place, so each annotated schema is bound to a const and the +// route table below references that const — a bare call would register nothing. +const createCustomMetricRequest = createCustomMetricSchema.meta({ id: "CreateCustomMetricRequest", description: "Define a user-scoped custom metric ONCE: free-text `name` + `unit`, an optional target window (`targetLow` / `targetHigh`; when both present `targetLow` must not exceed `targetHigh`), optional display `decimals`, an optional `description`, and explicit `correlationEnabled` opt-in. The name is unique per user. Logging a value later snapshots its unit.", }); -updateCustomMetricSchema.meta({ +const updateCustomMetricRequest = updateCustomMetricSchema.meta({ id: "UpdateCustomMetricRequest", description: "Partial edit of a custom metric. An omitted key leaves the column untouched; an explicit `null` on a target bound / `decimals` / `description` clears it. `correlationEnabled` explicitly controls use as a bounded behaviour channel. A conflicting rename is rejected 409.", }); -createCustomMetricEntrySchema.meta({ +const createCustomMetricEntryRequest = createCustomMetricEntrySchema.meta({ id: "CreateCustomMetricEntryRequest", description: "Log a value against a custom metric: numeric `value`, ISO 8601 `measuredAt`, optional free-text `note`. The metric's current unit is snapshotted onto the entry server-side.", }); -updateCustomMetricEntrySchema.meta({ +const updateCustomMetricEntryRequest = updateCustomMetricEntrySchema.meta({ id: "UpdateCustomMetricEntryRequest", description: "Partial edit of a logged value. An omitted key leaves the column untouched; an explicit `null` on `note` clears it.", @@ -164,7 +167,7 @@ export const customMetricPaths: NonNullable = { parameters: [idempotencyKeyParameter], requestBody: { required: true, - content: { "application/json": { schema: createCustomMetricSchema } }, + content: { "application/json": { schema: createCustomMetricRequest } }, }, responses: { ...idempotentWrite(), @@ -213,7 +216,7 @@ export const customMetricPaths: NonNullable = { requestParams: { path: z.object({ id: z.string() }) }, requestBody: { required: true, - content: { "application/json": { schema: updateCustomMetricSchema } }, + content: { "application/json": { schema: updateCustomMetricRequest } }, }, responses: { "200": { @@ -296,7 +299,7 @@ export const customMetricPaths: NonNullable = { requestBody: { required: true, content: { - "application/json": { schema: createCustomMetricEntrySchema }, + "application/json": { schema: createCustomMetricEntryRequest }, }, }, responses: { @@ -326,7 +329,7 @@ export const customMetricPaths: NonNullable = { requestBody: { required: true, content: { - "application/json": { schema: updateCustomMetricEntrySchema }, + "application/json": { schema: updateCustomMetricEntryRequest }, }, }, responses: { diff --git a/src/lib/openapi/routes/documents.ts b/src/lib/openapi/routes/documents.ts index 379f7a57f..81159f2b7 100644 --- a/src/lib/openapi/routes/documents.ts +++ b/src/lib/openapi/routes/documents.ts @@ -78,25 +78,28 @@ const documentChatListSchema = z }) .meta({ id: "DocumentChatList" }); -inboundConfirmSchema.meta({ +// Zod 4's `.meta()` returns a NEW instance carrying the id rather than +// annotating in place, so each annotated schema is bound to a const and the +// route table below references that const — a bare call would register nothing. +const inboundConfirmRequest = inboundConfirmSchema.meta({ id: "InboundConfirmRequest", description: "The approve/reject decisions a human made on the review screen. Each names a staged fact by id. Approved facts are committed to the structured stores (labs / conditions / medications) through their normal create paths; rejected facts are discarded. A fact still flagged `needsReview` (below the confidence floor, never edited) cannot be approved — it is reported back as `needsReview`. No `userId` field — it is narrowed from the session; every fact id is re-scoped to the document + caller.", }); -inboundFactEditSchema.meta({ +const inboundFactEditRequest = inboundFactEditSchema.meta({ id: "InboundFactEditRequest", description: "A correction to a staged fact before approval (fixes OCR / units / dates / codes). Discriminated by `factType` so the edit cannot change the fact's resource type. A successful edit clears `needsReview` — the values become user-asserted.", }); -documentUpdateSchema.meta({ +const documentUpdateRequest = documentUpdateSchema.meta({ id: "DocumentUpdateRequest", description: "Metadata edit for a stored document: `title` (user label; null clears it), `kind` (category), `documentDate` (user filing date, YYYY-MM-DD; null clears it), `episodeIds` (REPLACE-SET of condition links — the document's links become exactly this set; an empty array unlinks everything; every id must be a live episode of the caller or the whole request answers 404), `encounterIds` (the same replace-set semantics over the caller's visits). At least one field required. No `userId` field — narrowed from the session and fed to the Prisma `where` with the row id.", }); -documentBulkSchema.meta({ +const documentBulkRequest = documentBulkSchema.meta({ id: "DocumentBulkRequest", description: "One bulk action over up to 100 owner-scoped documents: `setKind` (requires `kind`), `linkEpisode` / `unlinkEpisode` (require `episodeId`), `linkEncounter` / `unlinkEncounter` (require `encounterId`), `delete` (tombstone, undo-able for 30 days), `restore` (clear tombstone). Partial failures never abort the batch — see the per-id result array.", @@ -514,7 +517,7 @@ export const inboundDocumentPaths: NonNullable = { "Applies one action to up to 100 owner-scoped documents and returns a per-id result array — a partial failure never aborts the batch. `error` is `notFound` (unknown / foreign / tombstoned where liveness is required) or `conflict` (restore blocked by a live duplicate of the same bytes).", requestBody: { required: true, - content: { "application/json": { schema: documentBulkSchema } }, + content: { "application/json": { schema: documentBulkRequest } }, }, responses: { "200": { @@ -587,7 +590,7 @@ export const inboundDocumentPaths: NonNullable = { ], requestBody: { required: true, - content: { "application/json": { schema: documentUpdateSchema } }, + content: { "application/json": { schema: documentUpdateRequest } }, }, responses: { "200": { @@ -1078,7 +1081,7 @@ export const inboundDocumentPaths: NonNullable = { ], requestBody: { required: true, - content: { "application/json": { schema: inboundFactEditSchema } }, + content: { "application/json": { schema: inboundFactEditRequest } }, }, responses: { "200": { @@ -1110,7 +1113,7 @@ export const inboundDocumentPaths: NonNullable = { ], requestBody: { required: true, - content: { "application/json": { schema: inboundConfirmSchema } }, + content: { "application/json": { schema: inboundConfirmRequest } }, }, responses: { ...idempotentWrite(), diff --git a/src/lib/openapi/routes/family-history.ts b/src/lib/openapi/routes/family-history.ts index 5f71484f7..83ab75a4d 100644 --- a/src/lib/openapi/routes/family-history.ts +++ b/src/lib/openapi/routes/family-history.ts @@ -30,19 +30,22 @@ import { stdResponses, } from "./shared"; -familyHistoryCreateSchema.meta({ +// Zod 4's `.meta()` returns a NEW instance carrying the id rather than +// annotating in place, so each annotated schema is bound to a const and the +// route table below references that const — a bare call would register nothing. +const createFamilyHistoryRequest = familyHistoryCreateSchema.meta({ id: "CreateFamilyHistoryRequest", description: "Record one condition for one relative (a FHIR FamilyMemberHistory with a single condition). `condition` is the user-facing label (the `condition.code.text` anchor — never a machine-guessed code); `ageAtOnset` is the relative's age (years) when it began. The free-text `note` is encrypted at rest. Patient-reported.", }); -familyHistoryUpdateSchema.meta({ +const updateFamilyHistoryRequest = familyHistoryUpdateSchema.meta({ id: "UpdateFamilyHistoryRequest", description: "Partial edit of a family-history entry; an omitted key leaves the column untouched. A `null` `ageAtOnset`/`note` clears that field. Rejects unknown keys.", }); -familyHistoryListQuerySchema.meta({ +const listFamilyHistoryQuery = familyHistoryListQuerySchema.meta({ id: "ListFamilyHistoryQuery", description: "Query params for the family-history list: optional `limit` (1–200, default 100). Newest-first.", @@ -78,7 +81,7 @@ export const familyHistoryPaths: NonNullable = { summary: "List family history (v1.25)", description: "Returns the caller's live (non-deleted) family-history entries, newest-first.", - requestParams: { query: familyHistoryListQuerySchema }, + requestParams: { query: listFamilyHistoryQuery }, responses: { ...recordRefusal(), "200": { @@ -104,7 +107,7 @@ export const familyHistoryPaths: NonNullable = { requestBody: { required: true, content: { - "application/json": { schema: familyHistoryCreateSchema }, + "application/json": { schema: createFamilyHistoryRequest }, }, }, responses: { @@ -158,7 +161,7 @@ export const familyHistoryPaths: NonNullable = { requestBody: { required: true, content: { - "application/json": { schema: familyHistoryUpdateSchema }, + "application/json": { schema: updateFamilyHistoryRequest }, }, }, responses: { diff --git a/src/lib/openapi/routes/illness.ts b/src/lib/openapi/routes/illness.ts index 9b51d6f9d..8cbb758ff 100644 --- a/src/lib/openapi/routes/illness.ts +++ b/src/lib/openapi/routes/illness.ts @@ -22,8 +22,6 @@ import { illnessEpisodeResolveSchema, illnessEpisodeListQuerySchema, illnessDayLogInputSchema, - illnessDayLogQuerySchema, - illnessDayLogListQuerySchema, illnessInsightsQuerySchema, illnessTypeEnum, illnessLifecycleEnum, @@ -38,48 +36,39 @@ import { stdResponses, } from "./shared"; -illnessEpisodeCreateSchema.meta({ +// Zod 4's `.meta()` returns a NEW instance carrying the id rather than +// annotating in place, so each annotated schema is bound to a const and the +// route table below references that const — a bare call would register nothing. +const createIllnessEpisodeRequest = illnessEpisodeCreateSchema.meta({ id: "CreateIllnessEpisodeRequest", description: "Open an illness/condition episode. `label` is the user-facing name; `type` + `lifecycle` classify it. `onsetAt` defaults to 'now' server-side when omitted. `parentConditionId` threads a FLARE/RECURRING bout under a parent condition (must be an owned, live episode). The optional `note` is encrypted at rest.", }); -illnessEpisodeUpdateSchema.meta({ +const updateIllnessEpisodeRequest = illnessEpisodeUpdateSchema.meta({ id: "UpdateIllnessEpisodeRequest", description: "Partial edit of an episode; an omitted key leaves the column untouched. A `null` `resolvedAt` re-opens a resolved episode; a `null` `parentConditionId` detaches it from its parent. Rejects unknown keys.", }); -illnessEpisodeResolveSchema.meta({ +const resolveIllnessEpisodeRequest = illnessEpisodeResolveSchema.meta({ id: "ResolveIllnessEpisodeRequest", description: "Mark an episode recovered. `resolvedAt` defaults to 'now' when omitted, so the one-tap 'mark recovered' affordance can send an empty body. A CHRONIC_ONGOING episode has no recovery date by design and 422s.", }); -illnessEpisodeListQuerySchema.meta({ +const listIllnessEpisodesQuery = illnessEpisodeListQuerySchema.meta({ id: "ListIllnessEpisodesQuery", description: "Query params for the episode history: optional `limit` (1–100, default 50) and `includeResolved` ('true' | 'false'); 'false' hides episodes that already carry a `resolvedAt`. Newest-first by `onsetAt`.", }); -illnessDayLogInputSchema.meta({ +const upsertIllnessDayLogRequest = illnessDayLogInputSchema.meta({ id: "UpsertIllnessDayLogRequest", description: "Upsert one day's symptom / functional-impact / fever row for an episode (keyed on `(episodeId, date)`). `date` is a `YYYY-MM-DD` tz-anchored day. `functionalImpact` (0–3) + `feverC` are queryable plaintext; `symptoms` carry an optional 0–3 Jackson/WURSS severity per link; the optional `note` is encrypted at rest.", }); -illnessDayLogQuerySchema.meta({ - id: "GetIllnessDayLogQuery", - description: - "Single-day read query: `date` is a `YYYY-MM-DD` day. Returns the matching day-log or `null` when nothing is logged for that day.", -}); - -illnessDayLogListQuerySchema.meta({ - id: "ListIllnessDayLogsQuery", - description: - "Date-less LIST query (v1.18.3): omit `date` to page the episode's whole day-log history. `limit` (1–200, default 60) + `offset` (default 0) + `sortDir` ('asc' | 'desc', default 'desc') with `meta.total`. Mutually exclusive with the single-day `date` read — presence of `date` selects the single-day mode.", -}); - /** * The documented query for `GET .../day-logs`: a `date` (single-day read) OR * the list pagination params (date-less list). Both are optional on the wire; @@ -97,7 +86,7 @@ const illnessDayLogGetQuery = z }) .meta({ id: "GetIllnessDayLogsQuery" }); -illnessInsightsQuerySchema.meta({ +const illnessInsightsQuery = illnessInsightsQuerySchema.meta({ id: "IllnessInsightsQuery", description: "Cross-episode retrospective window query: optional `windowDays` (30–1095, default 365) and optional `includeRecoveryGap` (default false). With `includeRecoveryGap` off the response carries the count breakdown and a null typical recovery gap on a single fast query; set it true to pay for the per-episode correlation that computes the recovery gap. Retrospective only — the engine summarises past episodes, never forecasts.", @@ -298,7 +287,7 @@ export const illnessPaths: NonNullable = { summary: "List illness episodes (v1.18.1)", description: "Returns the caller's live (non-deleted) illness/condition episodes, newest-first by onset. `includeResolved=false` hides episodes that already carry a `resolvedAt`. Born-gated.", - requestParams: { query: illnessEpisodeListQuerySchema }, + requestParams: { query: listIllnessEpisodesQuery }, responses: { ...recordRefusal(), "200": { @@ -324,7 +313,7 @@ export const illnessPaths: NonNullable = { requestBody: { required: true, content: { - "application/json": { schema: illnessEpisodeCreateSchema }, + "application/json": { schema: createIllnessEpisodeRequest }, }, }, responses: { @@ -375,7 +364,7 @@ export const illnessPaths: NonNullable = { requestBody: { required: true, content: { - "application/json": { schema: illnessEpisodeUpdateSchema }, + "application/json": { schema: updateIllnessEpisodeRequest }, }, }, responses: { @@ -456,7 +445,7 @@ export const illnessPaths: NonNullable = { requestBody: { required: false, content: { - "application/json": { schema: illnessEpisodeResolveSchema }, + "application/json": { schema: resolveIllnessEpisodeRequest }, }, }, responses: { @@ -514,7 +503,7 @@ export const illnessPaths: NonNullable = { requestBody: { required: true, content: { - "application/json": { schema: illnessDayLogInputSchema }, + "application/json": { schema: upsertIllnessDayLogRequest }, }, }, responses: { @@ -576,7 +565,7 @@ export const illnessPaths: NonNullable = { summary: "Cross-episode retrospective insights (v1.18.1)", description: "Returns the cross-episode retrospective summary over a trailing window: 'sick N times · typical recovery gap X days', a recurrence-by-month tally, and a per-type breakdown. The typical gap is withheld (null) below the min-sample floor (asserts nothing thin). Retrospective ONLY — the recurrence figure is a count of the past, never a forecast. Born-gated + owner-scoped.", - requestParams: { query: illnessInsightsQuerySchema }, + requestParams: { query: illnessInsightsQuery }, responses: { "200": { description: "The cross-episode retrospective summary.", diff --git a/src/lib/openapi/routes/labs.ts b/src/lib/openapi/routes/labs.ts index bf0aabe21..7cddeae2a 100644 --- a/src/lib/openapi/routes/labs.ts +++ b/src/lib/openapi/routes/labs.ts @@ -25,19 +25,22 @@ import { stdResponses, } from "./shared"; -createLabResultSchema.meta({ +// Zod 4's `.meta()` returns a NEW instance carrying the id rather than +// annotating in place, so each annotated schema is bound to a const and the +// route table below references that const — a bare call would register nothing. +const createLabResultRequest = createLabResultSchema.meta({ id: "CreateLabResultRequest", description: 'Record a single biomarker reading (HbA1c, LDL, ferritin, TSH, …). A reading is EITHER numeric (`value` + `unit`, optional reference bounds) OR qualitative (`valueText`, e.g. "negativ" / "positiv") — exactly one, never both, never neither. `analyte` + `unit` are free-form (a lab prints its own naming); a qualitative reading needs no unit / bounds. When both numeric bounds are present `referenceLow` must not exceed `referenceHigh`. `takenAt` is a backdatable ISO instant (no future, ≤ 50 years past). The optional `note` is encrypted at rest. The optional `source` records provenance: `"OCR"` for an on-device-OCR capture (the raw image never reaches the server on this path), defaulting to `"MANUAL"` when omitted. The optional `encounterId` files the reading against a visit the caller owns — always optional, and a link that could not be made never fails the reading.', }); -updateLabResultSchema.meta({ +const updateLabResultRequest = updateLabResultSchema.meta({ id: "UpdateLabResultRequest", description: "Partial edit of a lab result. An omitted key leaves the column untouched; an explicit `null` on `panel` / `note` / a reference bound clears it. Edit the matching value field for the row's type — `value` for a numeric row, `valueText` for a qualitative one; a request must not carry both, and a cross-type edit (the wrong field for the row) is rejected.", }); -listLabResultsSchema.meta({ +const listLabResultsQuery = listLabResultsSchema.meta({ id: "ListLabResultsQuery", description: "Query params for the lab-result list: optional `analyte` (exact) + `panel` filters, an inclusive `from`/`to` date range, and `limit` (≤ 500) / `offset` pagination. Defaults to `takenAt` DESC.", @@ -122,7 +125,7 @@ export const labsPaths: NonNullable = { summary: "List the caller's lab results", description: "Returns the caller's live (non-deleted) lab results, newest first, with optional analyte / panel / date-range filters. Each row carries a server-computed neutral `rangeStatus` and a `hasNote` flag (the encrypted note itself is not echoed).", - requestParams: { query: listLabResultsSchema }, + requestParams: { query: listLabResultsQuery }, responses: { ...recordRefusal(), "200": { @@ -144,7 +147,7 @@ export const labsPaths: NonNullable = { parameters: [idempotencyKeyParameter], requestBody: { required: true, - content: { "application/json": { schema: createLabResultSchema } }, + content: { "application/json": { schema: createLabResultRequest } }, }, responses: { ...idempotentWrite(), @@ -190,7 +193,7 @@ export const labsPaths: NonNullable = { requestParams: { path: z.object({ id: z.string() }) }, requestBody: { required: true, - content: { "application/json": { schema: updateLabResultSchema } }, + content: { "application/json": { schema: updateLabResultRequest } }, }, responses: { ...recordRefusal(), diff --git a/src/lib/openapi/routes/medications/paths.ts b/src/lib/openapi/routes/medications/paths.ts index 9d214a7fb..2ead8a4a4 100644 --- a/src/lib/openapi/routes/medications/paths.ts +++ b/src/lib/openapi/routes/medications/paths.ts @@ -19,7 +19,6 @@ import { bulkDeleteIntakeEventsSchema, updateIntakeEventSchema, } from "@/lib/validations/medication"; -import { medicationExtractionSchema } from "@/lib/ai/coach/medication-extract-prompt"; import { scheduleRevisionCreateSchema, scheduleRevisionUpdateSchema, @@ -39,13 +38,16 @@ import { stdResponses, } from "../shared"; -efficacyTargetOverrideSchema.meta({ +// Zod 4's `.meta()` returns a NEW instance carrying the id rather than +// annotating in place, so each annotated schema is bound to a const and the +// route table below references that const — a bare call would register nothing. +const setMedicationEfficacyTargetRequest = efficacyTargetOverrideSchema.meta({ id: "SetMedicationEfficacyTargetRequest", description: "Set or clear the user's explicit efficacy-target override for a medication. `clear:true` removes the override so the resolver reverts to the derived (ATC class prefix → name inference) target; otherwise pin exactly ONE of `measurementType` (a metric series) / `biomarkerId` (a lab analyte). `userId` is never a field — ownership is narrowed through the medication (and the biomarker for a lab target).", }); -medicationEfficacyResponseSchema.meta({ +const medicationEfficacyResponse = medicationEfficacyResponseSchema.meta({ id: "MedicationEfficacyResponse", description: "Server-authoritative, strictly-descriptive efficacy view relating a medication to the outcome metric(s)/lab(s) its class is prescribed to move, around its start. Carries the resolved target(s) with their series, the start/dose-change/pause markers, a before/after-start comparison (honest `{present:false}` below the per-side data floor), an adherence lane (cadence-aware per-day rate, never recomputed), an optional conservative level-shift note, and the retarget options. There is NO verdict / score / assessment field by construction — the client renders numbers and neutral connective phrasing only, never a causal or dose-advice claim.", @@ -62,6 +64,7 @@ import { scheduleRevisionResource, scheduleRevisionListResponse, medicationExtractRequest, + medicationExtractionResult, medicationListLayoutPutBody, medicationListLayoutResult, doseHistoryQuery, @@ -1336,7 +1339,7 @@ export const medicationPaths: NonNullable = { content: { "application/json": { schema: dataEnvelope( - medicationExtractionSchema, + medicationExtractionResult, "MedicationExtractResponse", ), }, @@ -1462,7 +1465,7 @@ export const medicationPaths: NonNullable = { content: { "application/json": { schema: dataEnvelope( - medicationEfficacyResponseSchema, + medicationEfficacyResponse, "GetMedicationEfficacyResponse", ), }, @@ -1488,7 +1491,7 @@ export const medicationPaths: NonNullable = { requestBody: { required: true, content: { - "application/json": { schema: efficacyTargetOverrideSchema }, + "application/json": { schema: setMedicationEfficacyTargetRequest }, }, }, responses: { diff --git a/src/lib/openapi/routes/medications/schemas.ts b/src/lib/openapi/routes/medications/schemas.ts index de32a1a80..2bad6dae8 100644 --- a/src/lib/openapi/routes/medications/schemas.ts +++ b/src/lib/openapi/routes/medications/schemas.ts @@ -967,7 +967,10 @@ export const doseHistoryResponse = z "Per-slot dose ledger over [from, to]: every expected slot with a status plus every off-schedule intake tagged ad-hoc. Built from the same band minter + `reconstructDoseHistory` the compliance % consumes, so the history view and the rate never contradict each other.", }); -medicationExtractionSchema.meta({ +// Zod 4's `.meta()` returns a NEW instance carrying the id rather than +// annotating in place, so the annotated clone is exported and the path table +// references it — a bare call would register nothing. +export const medicationExtractionResult = medicationExtractionSchema.meta({ id: "MedicationExtractionResult", description: "Citation-guarded partial extraction of medication scheduling fields. Every field is optional; the wizard merges what is present onto the form state and leaves the rest blank. `name` and `dose` are post-validated against the original free-text and dropped when not substring-matched, so the wizard cannot silently land a hallucinated brand or dose. `cadenceKind` / `doseUnit` / `weekdays` are closed enums; numeric fields are clamped to the wizard's wire bounds.", diff --git a/src/lib/openapi/routes/mental-health.ts b/src/lib/openapi/routes/mental-health.ts index d170d9294..6e9080da6 100644 --- a/src/lib/openapi/routes/mental-health.ts +++ b/src/lib/openapi/routes/mental-health.ts @@ -27,13 +27,16 @@ import { stdResponses, } from "./shared"; -createAssessmentSchema.meta({ +// Zod 4's `.meta()` returns a NEW instance carrying the id rather than +// annotating in place, so each annotated schema is bound to a const and the +// route table below references that const — a bare call would register nothing. +const createMentalHealthAssessmentRequest = createAssessmentSchema.meta({ id: "CreateMentalHealthAssessmentRequest", description: "Record one completed screener administration: PHQ-9 (9 items, 0–3), GAD-7 (7 items, 0–3), WHO-5 (5 items, 0–5) or SCI (8 items, 0–4). The array length and per-item ceiling must match the instrument. The item answers are encrypted at rest and never returned. The server computes the total (WHO-5 reports raw-sum × 4 as 0–100) + severity band + item-9 safety flag. Opt-in, beside mood tracking — this is a screening surface, not a diagnosis.", }); -listAssessmentsSchema.meta({ +const listMentalHealthAssessmentsQuery = listAssessmentsSchema.meta({ id: "ListMentalHealthAssessmentsQuery", description: "Filter the caller's screener history by instrument; paginate with limit/offset.", @@ -111,7 +114,7 @@ export const mentalHealthPaths: NonNullable = { summary: "List the caller's screener history", description: "Returns PHQ-9 / GAD-7 / WHO-5 / SCI administrations (newest first). Totals + bands + flags only; raw item answers are never returned.", - requestParams: { query: listAssessmentsSchema }, + requestParams: { query: listMentalHealthAssessmentsQuery }, responses: { ...recordRefusal(), "200": { @@ -136,7 +139,7 @@ export const mentalHealthPaths: NonNullable = { parameters: [idempotencyKeyParameter], requestBody: { content: { - "application/json": { schema: createAssessmentSchema }, + "application/json": { schema: createMentalHealthAssessmentRequest }, }, }, responses: { diff --git a/src/lib/openapi/routes/mood.ts b/src/lib/openapi/routes/mood.ts index 9d8b60c87..6b4f45cd0 100644 --- a/src/lib/openapi/routes/mood.ts +++ b/src/lib/openapi/routes/mood.ts @@ -61,40 +61,41 @@ import { } from "./shared"; // ── Request schemas — annotated for spec emission ──────────────────── +// +// Assigned rather than annotated in place: Zod 4's `.meta()` returns a NEW +// instance carrying the id, so the route table below must reference these +// consts for the names to reach `components.schemas`. -createCustomTagSchema.meta({ +const createMoodTagRequest = createCustomTagSchema.meta({ id: "CreateMoodTagRequest", description: "Create a per-user custom mood tag (BINARY only). `label` is encrypted at rest. `icon` must come from the curated allowlist (unknown name → 422). `categoryKey` picks the home group — any seeded category key or one of the caller's own `customcat:` group keys; omitted → the seeded `custom` category. Capped at 50 active custom tags per user (422).", }); -updateCustomTagSchema.meta({ +const updateMoodTagRequest = updateCustomTagSchema.meta({ id: "UpdateMoodTagRequest", description: "Partial custom-tag edit; at least one field required. `isActive:false` archives (history intact), `isActive:true` restores. `categoryKey` moves the tag to another group (a real categoryId move).", }); -createCustomGroupSchema.meta({ +const createMoodTagGroupRequest = createCustomGroupSchema.meta({ id: "CreateMoodTagGroupRequest", description: "Create a per-user custom mood-tag group. `label` is encrypted at rest; `icon` must come from the curated allowlist. Capped at 12 active custom groups per user (422).", }); -updateCustomGroupSchema.meta({ +const updateMoodTagGroupRequest = updateCustomGroupSchema.meta({ id: "UpdateMoodTagGroupRequest", description: "Partial custom-group edit; at least one field required. `isActive:false` retires the group without touching its tags.", }); -hideCatalogueTagSchema.meta({ +const hideMoodTagRequest = hideCatalogueTagSchema.meta({ id: "HideMoodTagRequest", description: "Hide / show a CATALOGUE tag for the calling user. Custom tags are hidden via their own `isActive` flag on the custom PATCH instead (this route 400s a `custom:` key).", }); -// Assigned rather than annotated in place: Zod 4's `.meta()` returns a NEW -// instance carrying the id, so the route table below must reference these -// consts for the names to reach `components.schemas`. const createMoodEntryRequest = createMoodEntrySchema.meta({ id: "CreateMoodEntryRequest", description: @@ -113,7 +114,7 @@ const listMoodEntriesQuery = listMoodEntriesSchema.meta({ "Filters + paging for the mood-entry list. `from` / `to` bound the local-day `date` label (YYYY-MM-DD, inclusive). `limit` capped at 500.", }); -moodTagLayoutSchema.meta({ +const moodTagLayout = moodTagLayoutSchema.meta({ id: "MoodTagLayout", description: "Per-user mood-tag presentation blob. `groupOrder`: category keys in display order (unknown dropped, missing appended in seeded order). `placements`: categoryKey → ordered tag keys; a placed tag renders in that group at that index, un-placed tags follow in their home category. Display-only — placements referencing hidden/archived/unknown keys are silently dropped at read time. Both fields optional: PUT merges preserve-when-absent.", @@ -220,7 +221,7 @@ const moodTagLayoutResolved = z // v1.32.22 (M2) — the PUT request extends the layout schema with the base // token (stripped pre-Zod at runtime). -const moodTagLayoutPutRequest = moodTagLayoutSchema +const moodTagLayoutPutRequest = moodTagLayout .extend({ baseUpdatedAt: baseUpdatedAtField }) .meta({ id: "MoodTagLayoutPutRequest", @@ -1349,7 +1350,7 @@ export const moodPaths: NonNullable = { "Mints a `custom:` key, encrypts the label at rest, stores a BINARY tag owned by the caller under the chosen group (default: the seeded `custom` category). 422 over the 50-tag cap or on an unknown / foreign `categoryKey`.", requestBody: { required: true, - content: { "application/json": { schema: createCustomTagSchema } }, + content: { "application/json": { schema: createMoodTagRequest } }, }, responses: { "201": { @@ -1376,7 +1377,7 @@ export const moodPaths: NonNullable = { requestParams: { path: z.object({ key: z.string() }) }, requestBody: { required: true, - content: { "application/json": { schema: updateCustomTagSchema } }, + content: { "application/json": { schema: updateMoodTagRequest } }, }, responses: { "200": { @@ -1429,7 +1430,7 @@ export const moodPaths: NonNullable = { requestParams: { path: z.object({ key: z.string() }) }, requestBody: { required: true, - content: { "application/json": { schema: hideCatalogueTagSchema } }, + content: { "application/json": { schema: hideMoodTagRequest } }, }, responses: { "200": { @@ -1460,7 +1461,7 @@ export const moodPaths: NonNullable = { "Mints a `customcat:` key, encrypts the label at rest, stores a group owned by the caller. 422 over the 12-group cap.", requestBody: { required: true, - content: { "application/json": { schema: createCustomGroupSchema } }, + content: { "application/json": { schema: createMoodTagGroupRequest } }, }, responses: { "201": { @@ -1487,7 +1488,7 @@ export const moodPaths: NonNullable = { requestParams: { path: z.object({ key: z.string() }) }, requestBody: { required: true, - content: { "application/json": { schema: updateCustomGroupSchema } }, + content: { "application/json": { schema: updateMoodTagGroupRequest } }, }, responses: { "200": { diff --git a/src/lib/openapi/routes/vaccinations.ts b/src/lib/openapi/routes/vaccinations.ts index 6e60f1643..193a7904a 100644 --- a/src/lib/openapi/routes/vaccinations.ts +++ b/src/lib/openapi/routes/vaccinations.ts @@ -41,37 +41,40 @@ import { stdResponses, } from "./shared"; -vaccinationCreateSchema.meta({ +// Zod 4's `.meta()` returns a NEW instance carrying the id rather than +// annotating in place, so each annotated schema is bound to a const and the +// route table below references that const — a bare call would register nothing. +const createVaccinationRequest = vaccinationCreateSchema.meta({ id: "CreateVaccinationRequest", description: "Log one administered dose. `occurredAt` is required and must not be in the future — a vaccination is recorded after the fact, and a planned booster is a reminder rather than a record. Exactly one identity arm is required: either `antigenSlug` (a slug the catalogue offers) or `vaccineName` (whatever the person's own record says, verbatim, which may be a trade name). Everything else is optional, so a decades-old paper entry with nothing but a date and a name is loggable. `note` is encrypted at rest. `documentIds` pre-links the scanned pages; an id naming nothing the caller owns is dropped rather than refused, so a link never blocks a save. Logging a dose satisfies any booster reminder keyed to one of its component antigens.", }); -vaccinationUpdateSchema.meta({ +const updateVaccinationRequest = vaccinationUpdateSchema.meta({ id: "UpdateVaccinationRequest", description: "Partial edit of a dose; an omitted key leaves the column untouched, and a body naming nothing is a 422. `occurredAt` is editable because a transcription typo is the common case — and editing it deliberately does NOT re-run the booster satisfaction, so correcting a date can never move a reminder's due date. An edit that would leave the record with neither identity arm is refused. A present `documentIds` array replaces the links, empty array included.", }); -vaccinationListQuerySchema.meta({ +const listVaccinationsQuery = vaccinationListQuerySchema.meta({ id: "ListVaccinationsQuery", description: "Filters for the immunization list: `antigenSlug`, `from` / `to` (ISO-8601 instants) and `limit` (1–500, default 300). The list is full and bounded rather than paged — a vaccination record is a lifetime document but a short one.", }); -vaccinationBoosterSchema.meta({ +const vaccinationBoosterRequest = vaccinationBoosterSchema.meta({ id: "VaccinationBoosterRequest", description: "The confirm body for a booster reminder — the person's own values, accepted or edited from the catalogue's prefill. `intervalMonths` is bounded at 600 (a decade booster is 120); `label` is composed client-side so its locale is the person's own. There is no antigen field: the server reads the antigen from the dose's catalogue entry, never from the request, so a client cannot key a reminder onto an antigen the dose does not contain.", }); -vaccinationSuggestQuerySchema.meta({ +const vaccinationSuggestQuery = vaccinationSuggestQuerySchema.meta({ id: "VaccinationSuggestQuery", description: "The single input to the upload suggestion: the document's anchor date (ISO-8601 with offset). No ids — the candidate doses come from the caller's own record, narrowed from the session.", }); -vaccinationLinkSchema.meta({ +const vaccinationLinkRequest = vaccinationLinkSchema.meta({ id: "VaccinationLinkRequest", description: "Documents to file against, or unfile from, a dose. Idempotent in both directions and capped at 100 ids. Ids naming nothing the caller owns come back in `unknown` rather than failing the request.", @@ -224,7 +227,7 @@ export const vaccinationPaths: NonNullable = { summary: "List vaccinations", description: "Returns the caller's live doses, newest first, each with its series resolved per component antigen. Soft-deleted doses are excluded.", - requestParams: { query: vaccinationListQuerySchema }, + requestParams: { query: listVaccinationsQuery }, responses: { ...recordRefusal(), "200": { @@ -246,7 +249,7 @@ export const vaccinationPaths: NonNullable = { "Records one administered dose and satisfies every live booster reminder keyed to one of its component antigens — so a single combined dose settles each of the antigens it contains. Satisfaction is forward-only: a dose older than a reminder's last satisfaction is a no-op, which is what makes back-entering an old paper record safe. Honours `Idempotency-Key`. Audits as `vaccination.record.create`.", requestBody: { required: true, - content: { "application/json": { schema: vaccinationCreateSchema } }, + content: { "application/json": { schema: createVaccinationRequest } }, }, responses: { ...idempotentWrite(), @@ -292,7 +295,7 @@ export const vaccinationPaths: NonNullable = { requestParams: idPath, requestBody: { required: true, - content: { "application/json": { schema: vaccinationUpdateSchema } }, + content: { "application/json": { schema: updateVaccinationRequest } }, }, responses: { ...recordRefusal(), @@ -363,7 +366,7 @@ export const vaccinationPaths: NonNullable = { requestParams: idPath, requestBody: { required: true, - content: { "application/json": { schema: vaccinationBoosterSchema } }, + content: { "application/json": { schema: vaccinationBoosterRequest } }, }, responses: { ...recordRefusal(), @@ -406,7 +409,7 @@ export const vaccinationPaths: NonNullable = { summary: "Suggest the dose a document belongs to", description: "Which dose does a document dated `anchor` most plausibly belong to? The document upload review asks this when a scan is classified `VACCINATION`. The window (±7 days, shared with the visit moment) and the one-pre-selects / many-offer-a-picker verdict are decided server-side so the browser never re-derives them. Owner-scoped; the anchor is the only input and it is a date, not an id.", - requestParams: { query: vaccinationSuggestQuerySchema }, + requestParams: { query: vaccinationSuggestQuery }, responses: { ...recordRefusal(), "200": { @@ -433,7 +436,7 @@ export const vaccinationPaths: NonNullable = { requestParams: idPath, requestBody: { required: true, - content: { "application/json": { schema: vaccinationLinkSchema } }, + content: { "application/json": { schema: vaccinationLinkRequest } }, }, responses: { "200": { @@ -462,7 +465,7 @@ export const vaccinationPaths: NonNullable = { requestParams: idPath, requestBody: { required: true, - content: { "application/json": { schema: vaccinationLinkSchema } }, + content: { "application/json": { schema: vaccinationLinkRequest } }, }, responses: { "200": { From 60e17a2617068acec6bf9631b9a02e81c7dfe544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 22 Aug 2026 15:34:27 +0200 Subject: [PATCH 2/3] Publish the mood-tag layout schema its own description relies on The annotation was already in the assigning form and still never reached the document, because both consumers extend the base rather than reference it and .extend() builds a new object. Same shape as Medication and CoachPrefs, same remedy: name it in the forced-registration block. --- docs/api/openapi.yaml | 28 ++++++++++++++++++++++++++++ src/lib/openapi/routes/index.ts | 8 +++++++- src/lib/openapi/routes/mood.ts | 10 +++++++++- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index e360c1932..a6cc5abb1 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -68573,6 +68573,34 @@ components: - showEvidenceByDefault - defaultWindow additionalProperties: false + MoodTagLayout: + type: object + properties: + groupOrder: + maxItems: 50 + type: array + items: + type: string + minLength: 1 + maxLength: 80 + placements: + type: object + propertyNames: + type: string + minLength: 1 + maxLength: 80 + additionalProperties: + type: array + items: + type: string + minLength: 1 + maxLength: 80 + additionalProperties: false + description: "Per-user mood-tag presentation blob. `groupOrder`: category keys in display order (unknown dropped, + missing appended in seeded order). `placements`: categoryKey → ordered tag keys; a placed tag renders in that + group at that index, un-placed tags follow in their home category. Display-only — placements referencing + hidden/archived/unknown keys are silently dropped at read time. Both fields optional: PUT merges + preserve-when-absent." parameters: AccountSelector: name: X-HealthLog-Account diff --git a/src/lib/openapi/routes/index.ts b/src/lib/openapi/routes/index.ts index a77ccf325..33a921c43 100644 --- a/src/lib/openapi/routes/index.ts +++ b/src/lib/openapi/routes/index.ts @@ -72,7 +72,7 @@ import { } from "./medications"; import { mcpPaths } from "./mcp"; import { metaPaths } from "./meta"; -import { moodPaths } from "./mood"; +import { moodPaths, moodTagLayout } from "./mood"; import { notificationTransportPaths } from "./notifications-transport"; import { notificationChannelPaths } from "./notifications"; import { nutrientPaths } from "./nutrients"; @@ -183,6 +183,11 @@ export const openApiComponents: NonNullable = { // this forced registration the schema the description points to would be // silently absent from the published spec. // + // `MoodTagLayout` joins them for exactly the same reason: its annotation is + // in the assigning form and correct, but both consumers `.extend()` it, so + // the base name is never referenced and the description the mood surface + // relies on would go unpublished. + // // `CoachPrefs` is the same shape: the PUT `/api/auth/me/coach-prefs` // description tells the reader "Body is validated against `CoachPrefs`", // but every actual consumer is a `.extend()` of it (`PutCoachPrefsRequest`, @@ -193,6 +198,7 @@ export const openApiComponents: NonNullable = { MedicationDoseHistoryImportFatalReason: medicationDoseHistoryImportFatalReasonEnum, CoachPrefs: coachPrefsSchema, + MoodTagLayout: moodTagLayout, }, // v1.36.0 — the per-request account selector, defined in the sharing route // module so that `src/__tests__/acting-account-boundary-guard.test.ts` keeps diff --git a/src/lib/openapi/routes/mood.ts b/src/lib/openapi/routes/mood.ts index 6b4f45cd0..92cb9afe4 100644 --- a/src/lib/openapi/routes/mood.ts +++ b/src/lib/openapi/routes/mood.ts @@ -114,7 +114,15 @@ const listMoodEntriesQuery = listMoodEntriesSchema.meta({ "Filters + paging for the mood-entry list. `from` / `to` bound the local-day `date` label (YYYY-MM-DD, inclusive). `limit` capped at 500.", }); -const moodTagLayout = moodTagLayoutSchema.meta({ +/** + * Exported for the forced registration in `./index.ts`. + * + * Both consumers are an `.extend()` of this base, and `.extend()` builds a new + * object rather than a reference, so the name never carries forward on its own + * even though the annotation is in the assigning form. Same shape as + * `CoachPrefs` and `Medication`, and the same remedy. + */ +export const moodTagLayout = moodTagLayoutSchema.meta({ id: "MoodTagLayout", description: "Per-user mood-tag presentation blob. `groupOrder`: category keys in display order (unknown dropped, missing appended in seeded order). `placements`: categoryKey → ordered tag keys; a placed tag renders in that group at that index, un-placed tags follow in their home category. Display-only — placements referencing hidden/archived/unknown keys are silently dropped at read time. Both fields optional: PUT merges preserve-when-absent.", From 3ae5bfe1c07e73a3eff975f066b074c26890150f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 22 Aug 2026 15:34:12 +0200 Subject: [PATCH 3/3] Capture the schemas that annotated themselves into nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `.meta()` clones the schema in Zod 4 and registers the clone, so a bare `schema.meta({ id, description })` statement registers nothing at all. Forty-one of them sat across nine route modules. The ids they named — `MeasurementType`, `MeasurementSource`, `LoginPasswordRequest`, `CreateMeasurementRequest`, `CreateBatchWorkoutRequest` and thirty more — reached `components.schemas` zero times, the descriptions written for them were published nowhere, and every use site inlined an anonymous copy. A client generated from the contract got duplicated nameless types where one named model belongs, which is the part that matters, because the iOS client is why the contract is published. Forty of them now bind their clone, and every use site points at it. Where the schema comes from `src/lib/validations/*` the import is aliased and the clone takes the name the use sites already spell, so nothing mutates the runtime validation schema. The seven that cross module boundaries are exported from `./shared` and the eight consumers import them from there; importing the raw schema from the validation module instead is exactly what inlined them. Thirty-four named components appear where inline copies stood. Fully dereferenced, the document differs by eighty-six added descriptions and nothing else: no operation's request or response shape moves. `CoachPrefs` was already force-registered in the index and finally carries its description. Five ids legitimately stay out of `components.schemas` — `ListMeasurementsQuery`, `ListAllergiesQuery`, `ListEncountersQuery`, `SuggestEncounterQuery` and `ListPractitionersQuery`. The emitter expands a `requestParams.query` object into inline `parameters`, so a query id has nowhere to land; they stay in the assigning form for consistency. Three findings the wiring turned up: `CreateShareLinkRequest` and `HealthRecordExportRequest` were annotated in the Coach module, which imported the two schemas for no other reason and never referenced them. Both operations that send these bodies live in the health-record module, so the annotations move there. Both descriptions described a `sections` field: the export body has no such field and rejects it with a 422, and the share-link body freezes scope through `selection` plus `documentIds` / `documentOnly`. Publishing either sentence verbatim would have published a lie, so both are corrected to the shape the routes actually take. `CoachPlansListQuery` — the forty-first — named a component that nothing referenced and that a query object could never reach. The operation publishes its two parameters by hand, because the schema carries a cross-field refinement the expansion cannot express, and every clause of the annotation is already published on the operation and on both parameters. It is deleted rather than left registering nothing. --- docs/api/openapi.yaml | 4605 ++++++++------------ src/lib/openapi/routes/allergies.ts | 16 +- src/lib/openapi/routes/auth.ts | 8 +- src/lib/openapi/routes/awards.ts | 6 +- src/lib/openapi/routes/biomarkers.ts | 12 +- src/lib/openapi/routes/coach.ts | 57 +- src/lib/openapi/routes/consent.ts | 13 +- src/lib/openapi/routes/cycle.ts | 58 +- src/lib/openapi/routes/encounters.ts | 38 +- src/lib/openapi/routes/health-record.ts | 30 +- src/lib/openapi/routes/import.ts | 12 +- src/lib/openapi/routes/index.ts | 2 +- src/lib/openapi/routes/insights/schemas.ts | 5 +- src/lib/openapi/routes/measurements.ts | 12 +- src/lib/openapi/routes/ocr.ts | 8 +- src/lib/openapi/routes/profile.ts | 2 +- src/lib/openapi/routes/shared.ts | 37 +- src/lib/openapi/routes/workouts.ts | 4 +- 18 files changed, 2045 insertions(+), 2880 deletions(-) diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 3fb2e9127..12b0b83dd 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -312,123 +312,7 @@ paths: content: application/json: schema: - type: object - properties: - date: - type: string - pattern: ^\d{4}-\d{2}-\d{2}$ - flow: - type: string - enum: - - NONE - - SPOTTING - - LIGHT - - MEDIUM - - HEAVY - intermenstrualBleeding: - type: boolean - basalBodyTempC: - type: number - minimum: 30 - maximum: 45 - temperatureExcluded: - type: boolean - ovulationTest: - type: string - enum: - - NEGATIVE - - POSITIVE_LH_SURGE - - ESTROGEN_SURGE - - INDETERMINATE - cervicalMucus: - type: string - enum: - - DRY - - STICKY - - CREAMY - - WATERY - - EGG_WHITE - cervixPosition: - type: string - enum: - - LOW - - HIGH - cervixFirmness: - type: string - enum: - - FIRM - - SOFT - cervixOpening: - type: string - enum: - - CLOSED - - OPEN - sexualActivity: - type: boolean - protectedSex: - anyOf: - - type: boolean - - type: "null" - pregnancyTest: - type: string - enum: - - NEGATIVE - - POSITIVE - - INDETERMINATE - progesteroneTest: - type: string - enum: - - NEGATIVE - - POSITIVE - - INDETERMINATE - contraceptive: - type: string - enum: - - NONE - - UNSPECIFIED - - IMPLANT - - INJECTION - - IUD - - INTRAVAGINAL_RING - - ORAL - - PATCH - - EMERGENCY - symptoms: - maxItems: 40 - type: array - items: - type: object - properties: - key: - type: string - minLength: 1 - maxLength: 80 - severity: - type: integer - minimum: 1 - maximum: 4 - required: - - key - note: - type: string - maxLength: 500 - loggedAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - source: - default: MANUAL - type: string - enum: - - MANUAL - - APPLE_HEALTH - externalId: - type: string - minLength: 1 - maxLength: 120 - required: - - date - - loggedAt + $ref: "#/components/schemas/CycleDayLogInput" responses: "200": description: Existing day-log updated. @@ -482,129 +366,7 @@ paths: content: application/json: schema: - type: object - properties: - flow: - anyOf: - - type: string - enum: - - NONE - - SPOTTING - - LIGHT - - MEDIUM - - HEAVY - - type: "null" - intermenstrualBleeding: - type: boolean - basalBodyTempC: - anyOf: - - type: number - minimum: 30 - maximum: 45 - - type: "null" - temperatureExcluded: - type: boolean - ovulationTest: - anyOf: - - type: string - enum: - - NEGATIVE - - POSITIVE_LH_SURGE - - ESTROGEN_SURGE - - INDETERMINATE - - type: "null" - cervicalMucus: - anyOf: - - type: string - enum: - - DRY - - STICKY - - CREAMY - - WATERY - - EGG_WHITE - - type: "null" - cervixPosition: - anyOf: - - type: string - enum: - - LOW - - HIGH - - type: "null" - cervixFirmness: - anyOf: - - type: string - enum: - - FIRM - - SOFT - - type: "null" - cervixOpening: - anyOf: - - type: string - enum: - - CLOSED - - OPEN - - type: "null" - sexualActivity: - type: boolean - protectedSex: - anyOf: - - type: boolean - - type: "null" - pregnancyTest: - anyOf: - - type: string - enum: - - NEGATIVE - - POSITIVE - - INDETERMINATE - - type: "null" - progesteroneTest: - anyOf: - - type: string - enum: - - NEGATIVE - - POSITIVE - - INDETERMINATE - - type: "null" - contraceptive: - anyOf: - - type: string - enum: - - NONE - - UNSPECIFIED - - IMPLANT - - INJECTION - - IUD - - INTRAVAGINAL_RING - - ORAL - - PATCH - - EMERGENCY - - type: "null" - symptoms: - maxItems: 40 - type: array - items: - type: object - properties: - key: - type: string - minLength: 1 - maxLength: 80 - severity: - type: integer - minimum: 1 - maximum: 4 - required: - - key - note: - anyOf: - - type: string - maxLength: 500 - - type: "null" - loggedAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + $ref: "#/components/schemas/CycleDayLogPatchRequest" responses: "200": description: Day-log updated. @@ -661,132 +423,7 @@ paths: content: application/json: schema: - type: object - properties: - entries: - minItems: 1 - maxItems: 500 - type: array - items: - type: object - properties: - date: - type: string - pattern: ^\d{4}-\d{2}-\d{2}$ - flow: - type: string - enum: - - NONE - - SPOTTING - - LIGHT - - MEDIUM - - HEAVY - intermenstrualBleeding: - type: boolean - basalBodyTempC: - type: number - minimum: 30 - maximum: 45 - temperatureExcluded: - type: boolean - ovulationTest: - type: string - enum: - - NEGATIVE - - POSITIVE_LH_SURGE - - ESTROGEN_SURGE - - INDETERMINATE - cervicalMucus: - type: string - enum: - - DRY - - STICKY - - CREAMY - - WATERY - - EGG_WHITE - cervixPosition: - type: string - enum: - - LOW - - HIGH - cervixFirmness: - type: string - enum: - - FIRM - - SOFT - cervixOpening: - type: string - enum: - - CLOSED - - OPEN - sexualActivity: - type: boolean - protectedSex: - anyOf: - - type: boolean - - type: "null" - pregnancyTest: - type: string - enum: - - NEGATIVE - - POSITIVE - - INDETERMINATE - progesteroneTest: - type: string - enum: - - NEGATIVE - - POSITIVE - - INDETERMINATE - contraceptive: - type: string - enum: - - NONE - - UNSPECIFIED - - IMPLANT - - INJECTION - - IUD - - INTRAVAGINAL_RING - - ORAL - - PATCH - - EMERGENCY - symptoms: - maxItems: 40 - type: array - items: - type: object - properties: - key: - type: string - minLength: 1 - maxLength: 80 - severity: - type: integer - minimum: 1 - maximum: 4 - required: - - key - note: - type: string - maxLength: 500 - loggedAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - source: - default: MANUAL - type: string - enum: - - MANUAL - - APPLE_HEALTH - externalId: - type: string - minLength: 1 - maxLength: 120 - required: - - date - - loggedAt - required: - - entries + $ref: "#/components/schemas/CycleDayLogBulkRequest" responses: "200": description: Batch processed (per-entry results). @@ -813,28 +450,7 @@ paths: content: application/json: schema: - type: object - properties: - action: - type: string - enum: - - start - - end - date: - type: string - pattern: ^\d{4}-\d{2}-\d{2}$ - externalId: - type: string - minLength: 1 - maxLength: 120 - loggedAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - required: - - action - - date - - loggedAt + $ref: "#/components/schemas/CyclePeriodRequest" responses: "200": description: Cycle + boundary day-log. @@ -1008,49 +624,7 @@ paths: content: application/json: schema: - type: object - properties: - enabled: - type: boolean - goal: - type: string - enum: - - GENERAL_HEALTH - - AVOID_PREGNANCY - - TRYING_TO_CONCEIVE - - PERIMENOPAUSE - - OFF - secondarySymptom: - type: string - enum: - - MUCUS - - CERVIX - rawChartMode: - type: boolean - predictionEnabled: - type: boolean - discreetNotifications: - type: boolean - sensitiveCategoryEncryption: - type: boolean - typicalCycleLength: - anyOf: - - type: integer - minimum: 15 - maximum: 60 - - type: "null" - typicalPeriodLength: - anyOf: - - type: integer - minimum: 1 - maximum: 15 - - type: "null" - lutealPhaseLength: - anyOf: - - type: integer - minimum: 10 - maximum: 16 - - type: "null" + $ref: "#/components/schemas/CyclePrefsRequest" responses: "200": description: Merged cycle preferences. @@ -1211,67 +785,7 @@ paths: content: application/json: schema: - type: object - properties: - format: - type: string - enum: - - pdf - - fhir - - package - range: - type: object - properties: - startDate: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - endDate: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - days: - type: integer - minimum: 1 - maximum: 365 - additionalProperties: false - selection: - type: object - properties: - v: - type: number - const: 2 - leaves: - maxItems: 94 - type: array - items: - type: string - minLength: 1 - maxLength: 64 - required: - - v - - leaves - additionalProperties: false - locale: - type: string - enum: - - de - - en - - fr - - es - - it - - pl - practiceName: - type: string - maxLength: 120 - includeCharts: - type: boolean - germanAtc: - type: boolean - required: - - format - - selection - additionalProperties: false + $ref: "#/components/schemas/HealthRecordExportRequest" responses: "200": description: "Export generated. Content-Type varies by `format`: application/pdf, application/fhir+json, or @@ -1324,57 +838,7 @@ paths: content: application/json: schema: - type: object - properties: - label: - type: string - minLength: 1 - maxLength: 120 - rangeStart: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - rangeEnd: - anyOf: - - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - - type: "null" - selection: - type: object - properties: - v: - type: number - const: 2 - leaves: - maxItems: 94 - type: array - items: - type: string - minLength: 1 - maxLength: 64 - required: - - v - - leaves - additionalProperties: false - documentIds: - maxItems: 50 - type: array - items: - type: string - minLength: 1 - maxLength: 64 - documentOnly: - type: boolean - expiresAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - required: - - label - - rangeStart - - expiresAt - additionalProperties: false + $ref: "#/components/schemas/CreateShareLinkRequest" responses: "201": description: Share link created. `token` carries the raw `hls_` value and is unrecoverable after this response. @@ -2222,17 +1686,7 @@ paths: content: application/json: schema: - type: object - properties: - email: - type: string - minLength: 1 - password: - type: string - minLength: 1 - required: - - email - - password + $ref: "#/components/schemas/LoginPasswordRequest" responses: "200": description: Login succeeded (token bundle / cookie) — or a second factor is required (`meta.mfaRequired`). @@ -4011,240 +3465,10 @@ paths: application/json: schema: anyOf: - - type: object - properties: - type: - type: string - enum: - - WEIGHT - - BLOOD_PRESSURE_SYS - - BLOOD_PRESSURE_DIA - - PULSE - - BODY_FAT - - SLEEP_DURATION - - ACTIVITY_STEPS - - BLOOD_GLUCOSE - - TOTAL_BODY_WATER - - BONE_MASS - - OXYGEN_SATURATION - - HEART_RATE_VARIABILITY - - RESTING_HEART_RATE - - ACTIVE_ENERGY_BURNED - - FLIGHTS_CLIMBED - - WALKING_RUNNING_DISTANCE - - VO2_MAX - - BODY_TEMPERATURE - - FAT_FREE_MASS - - FAT_MASS - - MUSCLE_MASS - - SKIN_TEMPERATURE - - PULSE_WAVE_VELOCITY - - VASCULAR_AGE - - VISCERAL_FAT - - AUDIO_EXPOSURE_ENV - - AUDIO_EXPOSURE_HEADPHONE - - TIME_IN_DAYLIGHT - - WALKING_STEADINESS - - AUDIO_EXPOSURE_EVENT - - RESPIRATORY_RATE - - BODY_MASS_INDEX - - LEAN_BODY_MASS - - WALKING_HEART_RATE_AVERAGE - - WALKING_ASYMMETRY - - WALKING_DOUBLE_SUPPORT - - WALKING_STEP_LENGTH - - WALKING_SPEED - - CARDIO_RECOVERY - - WRIST_TEMPERATURE - - FALL_COUNT - - SIX_MINUTE_WALK_DISTANCE - - STAIR_ASCENT_SPEED - - STAIR_DESCENT_SPEED - - BREATHING_DISTURBANCES - - IRREGULAR_RHYTHM_NOTIFICATION - - HIGH_HEART_RATE_EVENT - - LOW_HEART_RATE_EVENT - - WALKING_STEADINESS_EVENT - - BREATHING_DISTURBANCE_EVENT - - RECOVERY_SCORE - - STRESS_SCORE - - STRAIN_SCORE - - HRV_RMSSD - - DAY_STRAIN - - WORKOUT_STRAIN - - SLEEP_PERFORMANCE - - SLEEP_EFFICIENCY - - SLEEP_CONSISTENCY - - SLEEP_NEED - - ENERGY_EXPENDITURE_KJ - - AVERAGE_HEART_RATE - - MAX_HEART_RATE - - SLEEP_DISTURBANCE_COUNT - - ANS_CHARGE - - CARDIO_LOAD - - SLEEP_SCORE - - BODY_TEMPERATURE_DEVIATION - - RESILIENCE - - PHQ9_SCORE - - GAD7_SCORE - - GRIP_STRENGTH - - PAIN_NRS - - WAIST_CIRCUMFERENCE - - WAIST_TO_HEIGHT - - WHO5_SCORE - - SCI_SCORE - value: - type: number - measuredAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - notes: - type: string - maxLength: 200 - source: - default: MANUAL - type: string - enum: - - MANUAL - - APPLE_HEALTH - glucoseContext: - type: string - enum: - - FASTING - - POSTPRANDIAL - - RANDOM - - BEDTIME - deviceType: - anyOf: - - type: string - minLength: 1 - maxLength: 32 - - type: "null" - symptomsPresent: - type: boolean - required: - - type - - value - - measuredAt + - $ref: "#/components/schemas/CreateMeasurementRequest" - type: array items: - type: object - properties: - type: - type: string - enum: - - WEIGHT - - BLOOD_PRESSURE_SYS - - BLOOD_PRESSURE_DIA - - PULSE - - BODY_FAT - - SLEEP_DURATION - - ACTIVITY_STEPS - - BLOOD_GLUCOSE - - TOTAL_BODY_WATER - - BONE_MASS - - OXYGEN_SATURATION - - HEART_RATE_VARIABILITY - - RESTING_HEART_RATE - - ACTIVE_ENERGY_BURNED - - FLIGHTS_CLIMBED - - WALKING_RUNNING_DISTANCE - - VO2_MAX - - BODY_TEMPERATURE - - FAT_FREE_MASS - - FAT_MASS - - MUSCLE_MASS - - SKIN_TEMPERATURE - - PULSE_WAVE_VELOCITY - - VASCULAR_AGE - - VISCERAL_FAT - - AUDIO_EXPOSURE_ENV - - AUDIO_EXPOSURE_HEADPHONE - - TIME_IN_DAYLIGHT - - WALKING_STEADINESS - - AUDIO_EXPOSURE_EVENT - - RESPIRATORY_RATE - - BODY_MASS_INDEX - - LEAN_BODY_MASS - - WALKING_HEART_RATE_AVERAGE - - WALKING_ASYMMETRY - - WALKING_DOUBLE_SUPPORT - - WALKING_STEP_LENGTH - - WALKING_SPEED - - CARDIO_RECOVERY - - WRIST_TEMPERATURE - - FALL_COUNT - - SIX_MINUTE_WALK_DISTANCE - - STAIR_ASCENT_SPEED - - STAIR_DESCENT_SPEED - - BREATHING_DISTURBANCES - - IRREGULAR_RHYTHM_NOTIFICATION - - HIGH_HEART_RATE_EVENT - - LOW_HEART_RATE_EVENT - - WALKING_STEADINESS_EVENT - - BREATHING_DISTURBANCE_EVENT - - RECOVERY_SCORE - - STRESS_SCORE - - STRAIN_SCORE - - HRV_RMSSD - - DAY_STRAIN - - WORKOUT_STRAIN - - SLEEP_PERFORMANCE - - SLEEP_EFFICIENCY - - SLEEP_CONSISTENCY - - SLEEP_NEED - - ENERGY_EXPENDITURE_KJ - - AVERAGE_HEART_RATE - - MAX_HEART_RATE - - SLEEP_DISTURBANCE_COUNT - - ANS_CHARGE - - CARDIO_LOAD - - SLEEP_SCORE - - BODY_TEMPERATURE_DEVIATION - - RESILIENCE - - PHQ9_SCORE - - GAD7_SCORE - - GRIP_STRENGTH - - PAIN_NRS - - WAIST_CIRCUMFERENCE - - WAIST_TO_HEIGHT - - WHO5_SCORE - - SCI_SCORE - value: - type: number - measuredAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - notes: - type: string - maxLength: 200 - source: - default: MANUAL - type: string - enum: - - MANUAL - - APPLE_HEALTH - glucoseContext: - type: string - enum: - - FASTING - - POSTPRANDIAL - - RANDOM - - BEDTIME - deviceType: - anyOf: - - type: string - minLength: 1 - maxLength: 32 - - type: "null" - symptomsPresent: - type: boolean - required: - - type - - value - - measuredAt + $ref: "#/components/schemas/CreateMeasurementRequest" responses: "201": description: Created. A single resource for the object body; an array of resources, in request order, for the array body. @@ -4731,179 +3955,7 @@ paths: content: application/json: schema: - type: object - properties: - workouts: - minItems: 1 - maxItems: 100 - type: array - items: - type: object - properties: - sportType: - type: string - enum: - - walking - - running - - cycling - - hiking - - swimming - - rowing - - elliptical - - stairClimber - - yoga - - mindAndBody - - strength - - hiit - - dance - - golf - - badminton - - tennis - - basketball - - soccer - - crossTraining - - mixedCardio - - other - startedAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - endedAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - totalEnergyKcal: - type: number - minimum: 0 - maximum: 50000 - totalDistanceM: - type: number - minimum: 0 - maximum: 1000000 - avgHeartRate: - type: integer - minimum: 20 - maximum: 300 - maxHeartRate: - type: integer - minimum: 20 - maximum: 300 - minHeartRate: - type: integer - minimum: 20 - maximum: 300 - stepCount: - type: integer - minimum: 0 - maximum: 200000 - elevationM: - type: number - minimum: -500 - maximum: 10000 - pauseDurationSec: - type: integer - minimum: 0 - maximum: 86400 - source: - default: MANUAL - type: string - enum: - - MANUAL - - APPLE_HEALTH - externalId: - type: string - maxLength: 128 - externalSourceVersion: - type: string - maxLength: 64 - metadata: - type: object - propertyNames: - type: string - additionalProperties: {} - route: - type: object - properties: - geometry: - type: object - properties: - type: - type: string - const: LineString - coordinates: - minItems: 2 - maxItems: 20000 - type: array - items: - type: array - prefixItems: - - type: number - minimum: -180 - maximum: 180 - - type: number - minimum: -90 - maximum: 90 - items: - type: number - required: - - type - - coordinates - sampleTimestamps: - type: array - items: - type: object - properties: - t: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - speedMs: - type: number - minimum: 0 - maximum: 50 - hr: - type: integer - minimum: 20 - maximum: 300 - required: - - t - required: - - geometry - samples: - minItems: 1 - maxItems: 30000 - type: array - items: - type: object - properties: - t: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - hr: - type: integer - minimum: 20 - maximum: 300 - speedMs: - type: number - minimum: 0 - maximum: 50 - power: - type: number - minimum: 0 - maximum: 3000 - cadence: - type: number - minimum: 0 - maximum: 400 - required: - - t - required: - - sportType - - startedAt - - endedAt - required: - - workouts + $ref: "#/components/schemas/CreateBatchWorkoutRequest" responses: "200": description: Batch processed. @@ -9115,90 +8167,7 @@ paths: content: application/json: schema: - type: object - properties: - conversationId: - type: string - minLength: 1 - maxLength: 64 - message: - type: string - minLength: 1 - maxLength: 4000 - prefill: - type: string - maxLength: 2000 - locale: - type: string - enum: - - en - - de - scope: - type: object - properties: - sources: - maxItems: 40 - type: array - items: - type: string - enum: - - bp - - weight - - pulse - - mood - - compliance - - hrv - - sleep - - resting_hr - - steps - - active_energy - - flights - - distance - - vo2_max - - body_temp - - walking_hr - - respiratory_rate - - spo2 - - pulse_wave_velocity - - vascular_age - - body_fat - - fat_mass - - fat_free_mass - - muscle_mass - - lean_body_mass - - bone_mass - - total_body_water - - bmi - - visceral_fat - - glucose - - walking_steadiness - - walking_asymmetry - - walking_double_support - - walking_step_length - - walking_speed - - audio_env - - audio_headphone - - audio_event - - daylight - - skin_temp - - workouts - window: - type: string - enum: - - last7days - - last30days - - last90days - - lastYear - - allTime - guidedQuestion: - type: string - minLength: 1 - maxLength: 500 - workoutId: - type: string - maxLength: 64 - required: - - message + $ref: "#/components/schemas/CoachChatRequest" responses: "200": description: Server-Sent Events stream of `token` / `provenance` / `done` / `error` frames. @@ -9903,54 +8872,7 @@ paths: content: application/json: schema: - type: object - properties: - bloodType: - anyOf: - - type: string - enum: - - A_POS - - A_NEG - - B_POS - - B_NEG - - AB_POS - - AB_NEG - - O_POS - - O_NEG - - UNKNOWN - - type: "null" - organDonor: - anyOf: - - type: string - enum: - - YES - - NO - - UNKNOWN - - type: "null" - advanceDirective: - anyOf: - - type: string - enum: - - EXISTS - - NONE - - UNKNOWN - - type: "null" - contacts: - anyOf: - - type: string - maxLength: 2000 - - type: "null" - implants: - anyOf: - - type: string - maxLength: 1000 - - type: "null" - note: - anyOf: - - type: string - maxLength: 2000 - - type: "null" - additionalProperties: false + $ref: "#/components/schemas/UpdateEmergencyProfileRequest" responses: "200": description: The updated emergency profile. @@ -10157,24 +9079,7 @@ paths: content: application/json: schema: - type: object - properties: - status: - type: string - enum: - - proposed - - active - - met - - abandoned - - review_due - - reviewed - reviewDate: - anyOf: - - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - - type: "null" - additionalProperties: false + $ref: "#/components/schemas/CoachPlanPatchRequest" responses: "200": description: The plan after the lifecycle update. @@ -10243,21 +9148,7 @@ paths: content: application/json: schema: - type: object - properties: - cadenceId: - type: string - minLength: 1 - maxLength: 64 - action: - type: string - enum: - - accept - - dismiss - - stop - required: - - cadenceId - - action + $ref: "#/components/schemas/CoachReminderSuggestionAction" responses: "200": description: "A dismiss/stop, or an accept that matched an existing reminder (`duplicate: true`)." @@ -10986,86 +9877,8 @@ paths: - in: query name: metric schema: - type: string - enum: - - WEIGHT - - BLOOD_PRESSURE_SYS - - BLOOD_PRESSURE_DIA - - PULSE - - BODY_FAT - - SLEEP_DURATION - - ACTIVITY_STEPS - - BLOOD_GLUCOSE - - TOTAL_BODY_WATER - - BONE_MASS - - OXYGEN_SATURATION - - HEART_RATE_VARIABILITY - - RESTING_HEART_RATE - - ACTIVE_ENERGY_BURNED - - FLIGHTS_CLIMBED - - WALKING_RUNNING_DISTANCE - - VO2_MAX - - BODY_TEMPERATURE - - FAT_FREE_MASS - - FAT_MASS - - MUSCLE_MASS - - SKIN_TEMPERATURE - - PULSE_WAVE_VELOCITY - - VASCULAR_AGE - - VISCERAL_FAT - - AUDIO_EXPOSURE_ENV - - AUDIO_EXPOSURE_HEADPHONE - - TIME_IN_DAYLIGHT - - WALKING_STEADINESS - - AUDIO_EXPOSURE_EVENT - - RESPIRATORY_RATE - - BODY_MASS_INDEX - - LEAN_BODY_MASS - - WALKING_HEART_RATE_AVERAGE - - WALKING_ASYMMETRY - - WALKING_DOUBLE_SUPPORT - - WALKING_STEP_LENGTH - - WALKING_SPEED - - CARDIO_RECOVERY - - WRIST_TEMPERATURE - - FALL_COUNT - - SIX_MINUTE_WALK_DISTANCE - - STAIR_ASCENT_SPEED - - STAIR_DESCENT_SPEED - - BREATHING_DISTURBANCES - - IRREGULAR_RHYTHM_NOTIFICATION - - HIGH_HEART_RATE_EVENT - - LOW_HEART_RATE_EVENT - - WALKING_STEADINESS_EVENT - - BREATHING_DISTURBANCE_EVENT - - RECOVERY_SCORE - - STRESS_SCORE - - STRAIN_SCORE - - HRV_RMSSD - - DAY_STRAIN - - WORKOUT_STRAIN - - SLEEP_PERFORMANCE - - SLEEP_EFFICIENCY - - SLEEP_CONSISTENCY - - SLEEP_NEED - - ENERGY_EXPENDITURE_KJ - - AVERAGE_HEART_RATE - - MAX_HEART_RATE - - SLEEP_DISTURBANCE_COUNT - - ANS_CHARGE - - CARDIO_LOAD - - SLEEP_SCORE - - BODY_TEMPERATURE_DEVIATION - - RESILIENCE - - PHQ9_SCORE - - GAD7_SCORE - - GRIP_STRENGTH - - PAIN_NRS - - WAIST_CIRCUMFERENCE - - WAIST_TO_HEIGHT - - WHO5_SCORE - - SCI_SCORE description: "The metric the strip is about. Closed enum: an unknown value 422s." + $ref: "#/components/schemas/MeasurementType" required: true description: "The metric the strip is about. Closed enum: an unknown value 422s." responses: @@ -11307,86 +10120,8 @@ paths: - in: query name: type schema: - type: string - enum: - - WEIGHT - - BLOOD_PRESSURE_SYS - - BLOOD_PRESSURE_DIA - - PULSE - - BODY_FAT - - SLEEP_DURATION - - ACTIVITY_STEPS - - BLOOD_GLUCOSE - - TOTAL_BODY_WATER - - BONE_MASS - - OXYGEN_SATURATION - - HEART_RATE_VARIABILITY - - RESTING_HEART_RATE - - ACTIVE_ENERGY_BURNED - - FLIGHTS_CLIMBED - - WALKING_RUNNING_DISTANCE - - VO2_MAX - - BODY_TEMPERATURE - - FAT_FREE_MASS - - FAT_MASS - - MUSCLE_MASS - - SKIN_TEMPERATURE - - PULSE_WAVE_VELOCITY - - VASCULAR_AGE - - VISCERAL_FAT - - AUDIO_EXPOSURE_ENV - - AUDIO_EXPOSURE_HEADPHONE - - TIME_IN_DAYLIGHT - - WALKING_STEADINESS - - AUDIO_EXPOSURE_EVENT - - RESPIRATORY_RATE - - BODY_MASS_INDEX - - LEAN_BODY_MASS - - WALKING_HEART_RATE_AVERAGE - - WALKING_ASYMMETRY - - WALKING_DOUBLE_SUPPORT - - WALKING_STEP_LENGTH - - WALKING_SPEED - - CARDIO_RECOVERY - - WRIST_TEMPERATURE - - FALL_COUNT - - SIX_MINUTE_WALK_DISTANCE - - STAIR_ASCENT_SPEED - - STAIR_DESCENT_SPEED - - BREATHING_DISTURBANCES - - IRREGULAR_RHYTHM_NOTIFICATION - - HIGH_HEART_RATE_EVENT - - LOW_HEART_RATE_EVENT - - WALKING_STEADINESS_EVENT - - BREATHING_DISTURBANCE_EVENT - - RECOVERY_SCORE - - STRESS_SCORE - - STRAIN_SCORE - - HRV_RMSSD - - DAY_STRAIN - - WORKOUT_STRAIN - - SLEEP_PERFORMANCE - - SLEEP_EFFICIENCY - - SLEEP_CONSISTENCY - - SLEEP_NEED - - ENERGY_EXPENDITURE_KJ - - AVERAGE_HEART_RATE - - MAX_HEART_RATE - - SLEEP_DISTURBANCE_COUNT - - ANS_CHARGE - - CARDIO_LOAD - - SLEEP_SCORE - - BODY_TEMPERATURE_DEVIATION - - RESILIENCE - - PHQ9_SCORE - - GAD7_SCORE - - GRIP_STRENGTH - - PAIN_NRS - - WAIST_CIRCUMFERENCE - - WAIST_TO_HEIGHT - - WHO5_SCORE - - SCI_SCORE description: "The measurement type to read (single metric — no fan-out). Closed enum: an unknown type 422s." + $ref: "#/components/schemas/MeasurementType" required: true description: "The measurement type to read (single metric — no fan-out). Closed enum: an unknown type 422s." - in: query @@ -13401,25 +12136,7 @@ paths: content: application/json: schema: - type: object - properties: - kind: - type: string - enum: - - ai_full - - ai_insights_only - - ai_coach - artefact: - type: string - minLength: 1 - signedAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - required: - - kind - - artefact - - signedAt + $ref: "#/components/schemas/ConsentPostBody" responses: "200": description: The minted receipt. @@ -13451,14 +12168,7 @@ paths: content: application/json: schema: - type: object - properties: - intent: - default: heal - type: string - enum: - - heal - - affirmative + $ref: "#/components/schemas/WebConsentGrantBody" responses: "200": description: Grant outcome. @@ -14460,63 +13170,7 @@ paths: content: application/json: schema: - type: object - properties: - rows: - minItems: 1 - maxItems: 100 - type: array - items: - type: object - properties: - analyte: - type: string - minLength: 1 - maxLength: 120 - panel: - anyOf: - - type: string - minLength: 1 - maxLength: 120 - - type: string - const: "" - value: - type: number - valueText: - type: string - minLength: 1 - maxLength: 120 - unit: - type: string - minLength: 1 - maxLength: 40 - referenceLow: - type: number - referenceHigh: - type: number - referenceText: - anyOf: - - type: string - maxLength: 120 - - type: string - const: "" - takenAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ - required: - - analyte - - takenAt - documentId: - type: string - minLength: 1 - maxLength: 64 - encounterId: - type: string - minLength: 1 - maxLength: 40 - required: - - rows + $ref: "#/components/schemas/OcrCommitRequest" responses: "200": description: Inserted + skipped rows. @@ -14559,33 +13213,7 @@ paths: content: application/json: schema: - type: object - properties: - name: - type: string - minLength: 1 - maxLength: 120 - unit: - type: string - minLength: 1 - maxLength: 40 - lowerBound: - type: number - upperBound: - type: number - context: - type: string - maxLength: 2000 - panel: - anyOf: - - type: string - minLength: 1 - maxLength: 120 - - type: string - const: "" - required: - - name - - unit + $ref: "#/components/schemas/CreateBiomarkerRequest" responses: "201": description: Created biomarker. @@ -14649,40 +13277,7 @@ paths: content: application/json: schema: - type: object - properties: - name: - type: string - minLength: 1 - maxLength: 120 - unit: - type: string - minLength: 1 - maxLength: 40 - lowerBound: - anyOf: - - type: number - - type: "null" - upperBound: - anyOf: - - type: number - - type: "null" - context: - anyOf: - - type: string - maxLength: 2000 - - type: "null" - panel: - anyOf: - - anyOf: - - type: string - minLength: 1 - maxLength: 120 - - type: string - const: "" - - type: "null" - hidden: - type: boolean + $ref: "#/components/schemas/UpdateBiomarkerRequest" responses: "200": description: Updated biomarker. @@ -14791,78 +13386,7 @@ paths: content: application/json: schema: - type: object - properties: - occurredAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - status: - default: DONE - type: string - enum: - - PLANNED - - DONE - - CANCELLED - - NO_SHOW - kind: - default: OTHER - type: string - enum: - - ROUTINE - - ACUTE - - SPECIALIST - - PREVENTIVE - - EMERGENCY - - HOSPITAL - - THERAPY - - OTHER - practitionerId: - anyOf: - - type: string - minLength: 1 - maxLength: 40 - - type: "null" - reason: - anyOf: - - type: string - maxLength: 2000 - - type: "null" - outcome: - anyOf: - - type: string - maxLength: 4000 - - type: "null" - reminderId: - anyOf: - - type: string - minLength: 1 - maxLength: 40 - - type: "null" - documentIds: - maxItems: 100 - type: array - items: - type: string - minLength: 1 - maxLength: 40 - labResultIds: - maxItems: 100 - type: array - items: - type: string - minLength: 1 - maxLength: 40 - episodeIds: - maxItems: 100 - type: array - items: - type: string - minLength: 1 - maxLength: 40 - required: - - occurredAt - additionalProperties: false + $ref: "#/components/schemas/CreateEncounterRequest" responses: "201": description: Visit filed. @@ -14950,74 +13474,7 @@ paths: content: application/json: schema: - type: object - properties: - occurredAt: - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - status: - type: string - enum: - - PLANNED - - DONE - - CANCELLED - - NO_SHOW - kind: - type: string - enum: - - ROUTINE - - ACUTE - - SPECIALIST - - PREVENTIVE - - EMERGENCY - - HOSPITAL - - THERAPY - - OTHER - practitionerId: - anyOf: - - type: string - minLength: 1 - maxLength: 40 - - type: "null" - reason: - anyOf: - - type: string - maxLength: 2000 - - type: "null" - outcome: - anyOf: - - type: string - maxLength: 4000 - - type: "null" - reminderId: - anyOf: - - type: string - minLength: 1 - maxLength: 40 - - type: "null" - documentIds: - maxItems: 100 - type: array - items: - type: string - minLength: 1 - maxLength: 40 - labResultIds: - maxItems: 100 - type: array - items: - type: string - minLength: 1 - maxLength: 40 - episodeIds: - maxItems: 100 - type: array - items: - type: string - minLength: 1 - maxLength: 40 - additionalProperties: false + $ref: "#/components/schemas/UpdateEncounterRequest" responses: "200": description: Visit updated. @@ -15099,26 +13556,7 @@ paths: content: application/json: schema: - type: object - properties: - targetKind: - type: string - enum: - - document - - labResult - - conditionEpisode - targetIds: - minItems: 1 - maxItems: 100 - type: array - items: - type: string - minLength: 1 - maxLength: 40 - required: - - targetKind - - targetIds - additionalProperties: false + $ref: "#/components/schemas/EncounterLinkRequest" responses: "200": description: The visit's links after the change. @@ -15147,26 +13585,7 @@ paths: content: application/json: schema: - type: object - properties: - targetKind: - type: string - enum: - - document - - labResult - - conditionEpisode - targetIds: - minItems: 1 - maxItems: 100 - type: array - items: - type: string - minLength: 1 - maxLength: 40 - required: - - targetKind - - targetIds - additionalProperties: false + $ref: "#/components/schemas/EncounterLinkRequest" responses: "200": description: The visit's links after the change. @@ -15220,40 +13639,7 @@ paths: content: application/json: schema: - type: object - properties: - name: - type: string - minLength: 1 - maxLength: 160 - specialty: - anyOf: - - type: string - maxLength: 120 - - type: "null" - practice: - anyOf: - - type: string - maxLength: 160 - - type: "null" - location: - anyOf: - - type: string - maxLength: 300 - - type: "null" - phone: - anyOf: - - type: string - maxLength: 60 - - type: "null" - note: - anyOf: - - type: string - maxLength: 2000 - - type: "null" - required: - - name - additionalProperties: false + $ref: "#/components/schemas/CreatePractitionerRequest" responses: "201": description: Practitioner added. @@ -15311,38 +13697,7 @@ paths: content: application/json: schema: - type: object - properties: - name: - type: string - minLength: 1 - maxLength: 160 - specialty: - anyOf: - - type: string - maxLength: 120 - - type: "null" - practice: - anyOf: - - type: string - maxLength: 160 - - type: "null" - location: - anyOf: - - type: string - maxLength: 300 - - type: "null" - phone: - anyOf: - - type: string - maxLength: 60 - - type: "null" - note: - anyOf: - - type: string - maxLength: 2000 - - type: "null" - additionalProperties: false + $ref: "#/components/schemas/UpdatePractitionerRequest" responses: "200": description: Practitioner updated. @@ -16583,60 +14938,7 @@ paths: content: application/json: schema: - type: object - properties: - substance: - type: string - minLength: 1 - maxLength: 160 - category: - default: OTHER - type: string - enum: - - FOOD - - MEDICATION - - ENVIRONMENT - - BIOLOGIC - - OTHER - type: - default: ALLERGY - type: string - enum: - - ALLERGY - - INTOLERANCE - severity: - anyOf: - - type: string - enum: - - MILD - - MODERATE - - SEVERE - - type: "null" - status: - default: ACTIVE - type: string - enum: - - ACTIVE - - INACTIVE - - RESOLVED - onsetAt: - anyOf: - - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - - type: "null" - reaction: - anyOf: - - type: string - maxLength: 2000 - - type: "null" - note: - anyOf: - - type: string - maxLength: 2000 - - type: "null" - required: - - substance + $ref: "#/components/schemas/CreateAllergyRequest" responses: "201": description: Allergy created. @@ -16694,56 +14996,7 @@ paths: content: application/json: schema: - type: object - properties: - substance: - type: string - minLength: 1 - maxLength: 160 - category: - type: string - enum: - - FOOD - - MEDICATION - - ENVIRONMENT - - BIOLOGIC - - OTHER - type: - type: string - enum: - - ALLERGY - - INTOLERANCE - severity: - anyOf: - - type: string - enum: - - MILD - - MODERATE - - SEVERE - - type: "null" - status: - type: string - enum: - - ACTIVE - - INACTIVE - - RESOLVED - onsetAt: - anyOf: - - type: string - format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - - type: "null" - reaction: - anyOf: - - type: string - maxLength: 2000 - - type: "null" - note: - anyOf: - - type: string - maxLength: 2000 - - type: "null" - additionalProperties: false + $ref: "#/components/schemas/UpdateAllergyRequest" responses: "200": description: Allergy updated. @@ -22177,6 +20430,578 @@ components: description: "Partial custom-symptom edit; at least one of `label` / `icon` / `isActive` is required. `isActive: false` hides the symptom while every day log that references it stays intact; `isActive: true` brings it back, and the per-record cap is re-checked on the way in so hiding and re-enabling cannot be used to exceed it." + CycleDayLogInput: + type: object + properties: + date: + type: string + pattern: ^\d{4}-\d{2}-\d{2}$ + flow: + type: string + enum: + - NONE + - SPOTTING + - LIGHT + - MEDIUM + - HEAVY + intermenstrualBleeding: + type: boolean + basalBodyTempC: + type: number + minimum: 30 + maximum: 45 + temperatureExcluded: + type: boolean + ovulationTest: + type: string + enum: + - NEGATIVE + - POSITIVE_LH_SURGE + - ESTROGEN_SURGE + - INDETERMINATE + cervicalMucus: + type: string + enum: + - DRY + - STICKY + - CREAMY + - WATERY + - EGG_WHITE + cervixPosition: + type: string + enum: + - LOW + - HIGH + cervixFirmness: + type: string + enum: + - FIRM + - SOFT + cervixOpening: + type: string + enum: + - CLOSED + - OPEN + sexualActivity: + type: boolean + protectedSex: + anyOf: + - type: boolean + - type: "null" + pregnancyTest: + type: string + enum: + - NEGATIVE + - POSITIVE + - INDETERMINATE + progesteroneTest: + type: string + enum: + - NEGATIVE + - POSITIVE + - INDETERMINATE + contraceptive: + type: string + enum: + - NONE + - UNSPECIFIED + - IMPLANT + - INJECTION + - IUD + - INTRAVAGINAL_RING + - ORAL + - PATCH + - EMERGENCY + symptoms: + maxItems: 40 + type: array + items: + type: object + properties: + key: + type: string + minLength: 1 + maxLength: 80 + severity: + type: integer + minimum: 1 + maximum: 4 + required: + - key + note: + type: string + maxLength: 500 + loggedAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + source: + default: MANUAL + type: string + enum: + - MANUAL + - APPLE_HEALTH + externalId: + type: string + minLength: 1 + maxLength: 120 + required: + - date + - loggedAt + 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." + CycleDayLogPatchRequest: + type: object + properties: + flow: + anyOf: + - type: string + enum: + - NONE + - SPOTTING + - LIGHT + - MEDIUM + - HEAVY + - type: "null" + intermenstrualBleeding: + type: boolean + basalBodyTempC: + anyOf: + - type: number + minimum: 30 + maximum: 45 + - type: "null" + temperatureExcluded: + type: boolean + ovulationTest: + anyOf: + - type: string + enum: + - NEGATIVE + - POSITIVE_LH_SURGE + - ESTROGEN_SURGE + - INDETERMINATE + - type: "null" + cervicalMucus: + anyOf: + - type: string + enum: + - DRY + - STICKY + - CREAMY + - WATERY + - EGG_WHITE + - type: "null" + cervixPosition: + anyOf: + - type: string + enum: + - LOW + - HIGH + - type: "null" + cervixFirmness: + anyOf: + - type: string + enum: + - FIRM + - SOFT + - type: "null" + cervixOpening: + anyOf: + - type: string + enum: + - CLOSED + - OPEN + - type: "null" + sexualActivity: + type: boolean + protectedSex: + anyOf: + - type: boolean + - type: "null" + pregnancyTest: + anyOf: + - type: string + enum: + - NEGATIVE + - POSITIVE + - INDETERMINATE + - type: "null" + progesteroneTest: + anyOf: + - type: string + enum: + - NEGATIVE + - POSITIVE + - INDETERMINATE + - type: "null" + contraceptive: + anyOf: + - type: string + enum: + - NONE + - UNSPECIFIED + - IMPLANT + - INJECTION + - IUD + - INTRAVAGINAL_RING + - ORAL + - PATCH + - EMERGENCY + - type: "null" + symptoms: + maxItems: 40 + type: array + items: + type: object + properties: + key: + type: string + minLength: 1 + maxLength: 80 + severity: + type: integer + minimum: 1 + maximum: 4 + required: + - key + note: + anyOf: + - type: string + maxLength: 500 + - type: "null" + loggedAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + description: Partial day-log edit. Every field optional; `note` re-encrypts (explicit null clears it). `date` / `source` + / `externalId` are immutable on update. + CycleDayLogBulkRequest: + type: object + properties: + entries: + minItems: 1 + maxItems: 500 + type: array + items: + type: object + properties: + date: + type: string + pattern: ^\d{4}-\d{2}-\d{2}$ + flow: + type: string + enum: + - NONE + - SPOTTING + - LIGHT + - MEDIUM + - HEAVY + intermenstrualBleeding: + type: boolean + basalBodyTempC: + type: number + minimum: 30 + maximum: 45 + temperatureExcluded: + type: boolean + ovulationTest: + type: string + enum: + - NEGATIVE + - POSITIVE_LH_SURGE + - ESTROGEN_SURGE + - INDETERMINATE + cervicalMucus: + type: string + enum: + - DRY + - STICKY + - CREAMY + - WATERY + - EGG_WHITE + cervixPosition: + type: string + enum: + - LOW + - HIGH + cervixFirmness: + type: string + enum: + - FIRM + - SOFT + cervixOpening: + type: string + enum: + - CLOSED + - OPEN + sexualActivity: + type: boolean + protectedSex: + anyOf: + - type: boolean + - type: "null" + pregnancyTest: + type: string + enum: + - NEGATIVE + - POSITIVE + - INDETERMINATE + progesteroneTest: + type: string + enum: + - NEGATIVE + - POSITIVE + - INDETERMINATE + contraceptive: + type: string + enum: + - NONE + - UNSPECIFIED + - IMPLANT + - INJECTION + - IUD + - INTRAVAGINAL_RING + - ORAL + - PATCH + - EMERGENCY + symptoms: + maxItems: 40 + type: array + items: + type: object + properties: + key: + type: string + minLength: 1 + maxLength: 80 + severity: + type: integer + minimum: 1 + maximum: 4 + required: + - key + note: + type: string + maxLength: 500 + loggedAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + source: + default: MANUAL + type: string + enum: + - MANUAL + - APPLE_HEALTH + externalId: + type: string + minLength: 1 + maxLength: 120 + required: + - date + - loggedAt + required: + - entries + 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. + CyclePeriodRequest: + type: object + properties: + action: + type: string + enum: + - start + - end + date: + type: string + pattern: ^\d{4}-\d{2}-\d{2}$ + externalId: + type: string + minLength: 1 + maxLength: 120 + loggedAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + required: + - action + - date + - loggedAt + 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. + CyclePrefsRequest: + type: object + properties: + enabled: + type: boolean + goal: + type: string + enum: + - GENERAL_HEALTH + - AVOID_PREGNANCY + - TRYING_TO_CONCEIVE + - PERIMENOPAUSE + - OFF + secondarySymptom: + type: string + enum: + - MUCUS + - CERVIX + rawChartMode: + type: boolean + predictionEnabled: + type: boolean + discreetNotifications: + type: boolean + sensitiveCategoryEncryption: + type: boolean + typicalCycleLength: + anyOf: + - type: integer + minimum: 15 + maximum: 60 + - type: "null" + typicalPeriodLength: + anyOf: + - type: integer + minimum: 1 + maximum: 15 + - type: "null" + lutealPhaseLength: + anyOf: + - type: integer + minimum: 10 + maximum: 16 + - type: "null" + description: Partial cycle-preferences deep-merge. `enabled` flips the feature gate (`cycleTrackingEnabled`). Omitted + fields are left untouched. + HealthRecordExportRequest: + type: object + properties: + format: + type: string + enum: + - pdf + - fhir + - package + range: + type: object + properties: + startDate: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + endDate: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + days: + type: integer + minimum: 1 + maximum: 365 + additionalProperties: false + selection: + type: object + properties: + v: + type: number + const: 2 + leaves: + maxItems: 94 + type: array + items: + type: string + minLength: 1 + maxLength: 64 + required: + - v + - leaves + additionalProperties: false + locale: + type: string + enum: + - de + - en + - fr + - es + - it + - pl + practiceName: + type: string + maxLength: 120 + includeCharts: + type: boolean + germanAtc: + type: boolean + required: + - format + - selection + additionalProperties: false + 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." + CreateShareLinkRequest: + type: object + properties: + label: + type: string + minLength: 1 + maxLength: 120 + rangeStart: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + rangeEnd: + anyOf: + - type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + - type: "null" + selection: + type: object + properties: + v: + type: number + const: 2 + leaves: + maxItems: 94 + type: array + items: + type: string + minLength: 1 + maxLength: 64 + required: + - v + - leaves + additionalProperties: false + documentIds: + maxItems: 50 + type: array + items: + type: string + minLength: 1 + maxLength: 64 + documentOnly: + type: boolean + expiresAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + required: + - label + - rangeStart + - expiresAt + additionalProperties: false + 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." EncryptedExportRequest: type: object properties: @@ -22199,6 +21024,20 @@ components: required: - identifier description: "Discovery lookup for the iOS onboarding flow: given a typed email or username, resolve the next sign-in step." + LoginPasswordRequest: + type: object + properties: + email: + type: string + minLength: 1 + password: + type: string + minLength: 1 + required: + - email + - password + 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." MfaVerifyRequest: type: object properties: @@ -22735,6 +21574,127 @@ components: - credential description: Finish a passkey enrollment. The handler reads these two fields directly rather than through a Zod schema; a missing one is a 422. + CreateMeasurementRequest: + type: object + properties: + type: + type: string + enum: + - WEIGHT + - BLOOD_PRESSURE_SYS + - BLOOD_PRESSURE_DIA + - PULSE + - BODY_FAT + - SLEEP_DURATION + - ACTIVITY_STEPS + - BLOOD_GLUCOSE + - TOTAL_BODY_WATER + - BONE_MASS + - OXYGEN_SATURATION + - HEART_RATE_VARIABILITY + - RESTING_HEART_RATE + - ACTIVE_ENERGY_BURNED + - FLIGHTS_CLIMBED + - WALKING_RUNNING_DISTANCE + - VO2_MAX + - BODY_TEMPERATURE + - FAT_FREE_MASS + - FAT_MASS + - MUSCLE_MASS + - SKIN_TEMPERATURE + - PULSE_WAVE_VELOCITY + - VASCULAR_AGE + - VISCERAL_FAT + - AUDIO_EXPOSURE_ENV + - AUDIO_EXPOSURE_HEADPHONE + - TIME_IN_DAYLIGHT + - WALKING_STEADINESS + - AUDIO_EXPOSURE_EVENT + - RESPIRATORY_RATE + - BODY_MASS_INDEX + - LEAN_BODY_MASS + - WALKING_HEART_RATE_AVERAGE + - WALKING_ASYMMETRY + - WALKING_DOUBLE_SUPPORT + - WALKING_STEP_LENGTH + - WALKING_SPEED + - CARDIO_RECOVERY + - WRIST_TEMPERATURE + - FALL_COUNT + - SIX_MINUTE_WALK_DISTANCE + - STAIR_ASCENT_SPEED + - STAIR_DESCENT_SPEED + - BREATHING_DISTURBANCES + - IRREGULAR_RHYTHM_NOTIFICATION + - HIGH_HEART_RATE_EVENT + - LOW_HEART_RATE_EVENT + - WALKING_STEADINESS_EVENT + - BREATHING_DISTURBANCE_EVENT + - RECOVERY_SCORE + - STRESS_SCORE + - STRAIN_SCORE + - HRV_RMSSD + - DAY_STRAIN + - WORKOUT_STRAIN + - SLEEP_PERFORMANCE + - SLEEP_EFFICIENCY + - SLEEP_CONSISTENCY + - SLEEP_NEED + - ENERGY_EXPENDITURE_KJ + - AVERAGE_HEART_RATE + - MAX_HEART_RATE + - SLEEP_DISTURBANCE_COUNT + - ANS_CHARGE + - CARDIO_LOAD + - SLEEP_SCORE + - BODY_TEMPERATURE_DEVIATION + - RESILIENCE + - PHQ9_SCORE + - GAD7_SCORE + - GRIP_STRENGTH + - PAIN_NRS + - WAIST_CIRCUMFERENCE + - WAIST_TO_HEIGHT + - WHO5_SCORE + - SCI_SCORE + value: + type: number + measuredAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + notes: + type: string + maxLength: 200 + source: + default: MANUAL + type: string + enum: + - MANUAL + - APPLE_HEALTH + glucoseContext: + type: string + enum: + - FASTING + - POSTPRANDIAL + - RANDOM + - BEDTIME + deviceType: + anyOf: + - type: string + minLength: 1 + maxLength: 32 + - type: "null" + symptomsPresent: + type: boolean + required: + - type + - value + - measuredAt + 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." AppleHealthBatchRequest: type: object properties: @@ -22908,6 +21868,185 @@ components: - ids description: 1..200 measurement ids, scoped to the caller. Forged / foreign / not-deleted ids are silently skipped (no existence leak). + CreateBatchWorkoutRequest: + type: object + properties: + workouts: + minItems: 1 + maxItems: 100 + type: array + items: + type: object + properties: + sportType: + type: string + enum: + - walking + - running + - cycling + - hiking + - swimming + - rowing + - elliptical + - stairClimber + - yoga + - mindAndBody + - strength + - hiit + - dance + - golf + - badminton + - tennis + - basketball + - soccer + - crossTraining + - mixedCardio + - other + startedAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + endedAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + totalEnergyKcal: + type: number + minimum: 0 + maximum: 50000 + totalDistanceM: + type: number + minimum: 0 + maximum: 1000000 + avgHeartRate: + type: integer + minimum: 20 + maximum: 300 + maxHeartRate: + type: integer + minimum: 20 + maximum: 300 + minHeartRate: + type: integer + minimum: 20 + maximum: 300 + stepCount: + type: integer + minimum: 0 + maximum: 200000 + elevationM: + type: number + minimum: -500 + maximum: 10000 + pauseDurationSec: + type: integer + minimum: 0 + maximum: 86400 + source: + default: MANUAL + type: string + enum: + - MANUAL + - APPLE_HEALTH + externalId: + type: string + maxLength: 128 + externalSourceVersion: + type: string + maxLength: 64 + metadata: + type: object + propertyNames: + type: string + additionalProperties: {} + route: + type: object + properties: + geometry: + type: object + properties: + type: + type: string + const: LineString + coordinates: + minItems: 2 + maxItems: 20000 + type: array + items: + type: array + prefixItems: + - type: number + minimum: -180 + maximum: 180 + - type: number + minimum: -90 + maximum: 90 + items: + type: number + required: + - type + - coordinates + sampleTimestamps: + type: array + items: + type: object + properties: + t: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + speedMs: + type: number + minimum: 0 + maximum: 50 + hr: + type: integer + minimum: 20 + maximum: 300 + required: + - t + required: + - geometry + samples: + minItems: 1 + maxItems: 30000 + type: array + items: + type: object + properties: + t: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + hr: + type: integer + minimum: 20 + maximum: 300 + speedMs: + type: number + minimum: 0 + maximum: 50 + power: + type: number + minimum: 0 + maximum: 3000 + cadence: + type: number + minimum: 0 + maximum: 400 + required: + - t + required: + - sportType + - startedAt + - endedAt + required: + - workouts + 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)." DeviceRegisterRequest: type: object properties: @@ -24682,6 +23821,96 @@ components: required: - timezone description: The account's timezone. The handler reads and trims the field directly rather than through a Zod schema. + CoachChatRequest: + type: object + properties: + conversationId: + type: string + minLength: 1 + maxLength: 64 + message: + type: string + minLength: 1 + maxLength: 4000 + prefill: + type: string + maxLength: 2000 + locale: + type: string + enum: + - en + - de + scope: + type: object + properties: + sources: + maxItems: 40 + type: array + items: + type: string + enum: + - bp + - weight + - pulse + - mood + - compliance + - hrv + - sleep + - resting_hr + - steps + - active_energy + - flights + - distance + - vo2_max + - body_temp + - walking_hr + - respiratory_rate + - spo2 + - pulse_wave_velocity + - vascular_age + - body_fat + - fat_mass + - fat_free_mass + - muscle_mass + - lean_body_mass + - bone_mass + - total_body_water + - bmi + - visceral_fat + - glucose + - walking_steadiness + - walking_asymmetry + - walking_double_support + - walking_step_length + - walking_speed + - audio_env + - audio_headphone + - audio_event + - daylight + - skin_temp + - workouts + window: + type: string + enum: + - last7days + - last30days + - last90days + - lastYear + - allTime + guidedQuestion: + type: string + minLength: 1 + maxLength: 500 + workoutId: + type: string + maxLength: 64 + required: + - message + 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. RenameCoachConversationRequest: type: object properties: @@ -24867,6 +24096,58 @@ components: - version description: PUT body for the Insights layout — the layout fields plus the optional optimistic-concurrency base token (`baseUpdatedAt`). Omit the token for the legacy unconditional write. + UpdateEmergencyProfileRequest: + type: object + properties: + bloodType: + anyOf: + - type: string + enum: + - A_POS + - A_NEG + - B_POS + - B_NEG + - AB_POS + - AB_NEG + - O_POS + - O_NEG + - UNKNOWN + - type: "null" + organDonor: + anyOf: + - type: string + enum: + - YES + - NO + - UNKNOWN + - type: "null" + advanceDirective: + anyOf: + - type: string + enum: + - EXISTS + - NONE + - UNKNOWN + - type: "null" + contacts: + anyOf: + - type: string + maxLength: 2000 + - type: "null" + implants: + anyOf: + - type: string + maxLength: 1000 + - type: "null" + note: + anyOf: + - type: string + maxLength: 2000 + - type: "null" + additionalProperties: false + 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. CoachAboutMeAdoptRequest: type: object properties: @@ -24883,6 +24164,49 @@ components: description: 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. + CoachPlanPatchRequest: + type: object + properties: + status: + type: string + enum: + - proposed + - active + - met + - abandoned + - review_due + - reviewed + reviewDate: + anyOf: + - type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + - type: "null" + additionalProperties: false + 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." + CoachReminderSuggestionAction: + type: object + properties: + cadenceId: + type: string + minLength: 1 + maxLength: 64 + action: + type: string + enum: + - accept + - dismiss + - stop + required: + - cadenceId + - action + 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." CoachReminderCreate: type: object properties: @@ -25088,6 +24412,88 @@ components: genuinely different rating, which is the signal the aggregator wants. `providerType` and `promptVersion` are NOT accepted — the server fills both from the account's own attribution, so a client cannot tamper with the slice the quality dashboard reports on." + MeasurementType: + type: string + enum: + - WEIGHT + - BLOOD_PRESSURE_SYS + - BLOOD_PRESSURE_DIA + - PULSE + - BODY_FAT + - SLEEP_DURATION + - ACTIVITY_STEPS + - BLOOD_GLUCOSE + - TOTAL_BODY_WATER + - BONE_MASS + - OXYGEN_SATURATION + - HEART_RATE_VARIABILITY + - RESTING_HEART_RATE + - ACTIVE_ENERGY_BURNED + - FLIGHTS_CLIMBED + - WALKING_RUNNING_DISTANCE + - VO2_MAX + - BODY_TEMPERATURE + - FAT_FREE_MASS + - FAT_MASS + - MUSCLE_MASS + - SKIN_TEMPERATURE + - PULSE_WAVE_VELOCITY + - VASCULAR_AGE + - VISCERAL_FAT + - AUDIO_EXPOSURE_ENV + - AUDIO_EXPOSURE_HEADPHONE + - TIME_IN_DAYLIGHT + - WALKING_STEADINESS + - AUDIO_EXPOSURE_EVENT + - RESPIRATORY_RATE + - BODY_MASS_INDEX + - LEAN_BODY_MASS + - WALKING_HEART_RATE_AVERAGE + - WALKING_ASYMMETRY + - WALKING_DOUBLE_SUPPORT + - WALKING_STEP_LENGTH + - WALKING_SPEED + - CARDIO_RECOVERY + - WRIST_TEMPERATURE + - FALL_COUNT + - SIX_MINUTE_WALK_DISTANCE + - STAIR_ASCENT_SPEED + - STAIR_DESCENT_SPEED + - BREATHING_DISTURBANCES + - IRREGULAR_RHYTHM_NOTIFICATION + - HIGH_HEART_RATE_EVENT + - LOW_HEART_RATE_EVENT + - WALKING_STEADINESS_EVENT + - BREATHING_DISTURBANCE_EVENT + - RECOVERY_SCORE + - STRESS_SCORE + - STRAIN_SCORE + - HRV_RMSSD + - DAY_STRAIN + - WORKOUT_STRAIN + - SLEEP_PERFORMANCE + - SLEEP_EFFICIENCY + - SLEEP_CONSISTENCY + - SLEEP_NEED + - ENERGY_EXPENDITURE_KJ + - AVERAGE_HEART_RATE + - MAX_HEART_RATE + - SLEEP_DISTURBANCE_COUNT + - ANS_CHARGE + - CARDIO_LOAD + - SLEEP_SCORE + - BODY_TEMPERATURE_DEVIATION + - RESILIENCE + - PHQ9_SCORE + - GAD7_SCORE + - GRIP_STRENGTH + - PAIN_NRS + - WAIST_CIRCUMFERENCE + - WAIST_TO_HEIGHT + - WHO5_SCORE + - SCI_SCORE + 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). InsightsSettingsPutRequest: type: object properties: @@ -26160,6 +25566,41 @@ components: required: - confirm description: Typed confirmation for erasing the record. No undo. + ConsentPostBody: + type: object + properties: + kind: + type: string + enum: + - ai_full + - ai_insights_only + - ai_coach + artefact: + type: string + minLength: 1 + signedAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + required: + - kind + - artefact + - signedAt + 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: + type: object + properties: + intent: + default: heal + type: string + enum: + - heal + - affirmative + 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." JsonImportRequest: type: object properties: @@ -26179,85 +25620,7 @@ components: type: object properties: type: - type: string - enum: - - WEIGHT - - BLOOD_PRESSURE_SYS - - BLOOD_PRESSURE_DIA - - PULSE - - BODY_FAT - - SLEEP_DURATION - - ACTIVITY_STEPS - - BLOOD_GLUCOSE - - TOTAL_BODY_WATER - - BONE_MASS - - OXYGEN_SATURATION - - HEART_RATE_VARIABILITY - - RESTING_HEART_RATE - - ACTIVE_ENERGY_BURNED - - FLIGHTS_CLIMBED - - WALKING_RUNNING_DISTANCE - - VO2_MAX - - BODY_TEMPERATURE - - FAT_FREE_MASS - - FAT_MASS - - MUSCLE_MASS - - SKIN_TEMPERATURE - - PULSE_WAVE_VELOCITY - - VASCULAR_AGE - - VISCERAL_FAT - - AUDIO_EXPOSURE_ENV - - AUDIO_EXPOSURE_HEADPHONE - - TIME_IN_DAYLIGHT - - WALKING_STEADINESS - - AUDIO_EXPOSURE_EVENT - - RESPIRATORY_RATE - - BODY_MASS_INDEX - - LEAN_BODY_MASS - - WALKING_HEART_RATE_AVERAGE - - WALKING_ASYMMETRY - - WALKING_DOUBLE_SUPPORT - - WALKING_STEP_LENGTH - - WALKING_SPEED - - CARDIO_RECOVERY - - WRIST_TEMPERATURE - - FALL_COUNT - - SIX_MINUTE_WALK_DISTANCE - - STAIR_ASCENT_SPEED - - STAIR_DESCENT_SPEED - - BREATHING_DISTURBANCES - - IRREGULAR_RHYTHM_NOTIFICATION - - HIGH_HEART_RATE_EVENT - - LOW_HEART_RATE_EVENT - - WALKING_STEADINESS_EVENT - - BREATHING_DISTURBANCE_EVENT - - RECOVERY_SCORE - - STRESS_SCORE - - STRAIN_SCORE - - HRV_RMSSD - - DAY_STRAIN - - WORKOUT_STRAIN - - SLEEP_PERFORMANCE - - SLEEP_EFFICIENCY - - SLEEP_CONSISTENCY - - SLEEP_NEED - - ENERGY_EXPENDITURE_KJ - - AVERAGE_HEART_RATE - - MAX_HEART_RATE - - SLEEP_DISTURBANCE_COUNT - - ANS_CHARGE - - CARDIO_LOAD - - SLEEP_SCORE - - BODY_TEMPERATURE_DEVIATION - - RESILIENCE - - PHQ9_SCORE - - GAD7_SCORE - - GRIP_STRENGTH - - PAIN_NRS - - WAIST_CIRCUMFERENCE - - WAIST_TO_HEIGHT - - WHO5_SCORE - - SCI_SCORE + $ref: "#/components/schemas/MeasurementType" value: type: number description: Checked against the type's server-side plausibility range; an out-of-range value fails the whole request @@ -26490,6 +25853,498 @@ components: required: - mode - text + OcrCommitRequest: + type: object + properties: + rows: + minItems: 1 + maxItems: 100 + type: array + items: + type: object + properties: + analyte: + type: string + minLength: 1 + maxLength: 120 + panel: + anyOf: + - type: string + minLength: 1 + maxLength: 120 + - type: string + const: "" + value: + type: number + valueText: + type: string + minLength: 1 + maxLength: 120 + unit: + type: string + minLength: 1 + maxLength: 40 + referenceLow: + type: number + referenceHigh: + type: number + referenceText: + anyOf: + - type: string + maxLength: 120 + - type: string + const: "" + takenAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + required: + - analyte + - takenAt + documentId: + type: string + minLength: 1 + maxLength: 64 + encounterId: + type: string + minLength: 1 + maxLength: 40 + required: + - rows + 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. + CreateBiomarkerRequest: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 120 + unit: + type: string + minLength: 1 + maxLength: 40 + lowerBound: + type: number + upperBound: + type: number + context: + type: string + maxLength: 2000 + panel: + anyOf: + - type: string + minLength: 1 + maxLength: 120 + - type: string + const: "" + required: + - name + - unit + 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." + UpdateBiomarkerRequest: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 120 + unit: + type: string + minLength: 1 + maxLength: 40 + lowerBound: + anyOf: + - type: number + - type: "null" + upperBound: + anyOf: + - type: number + - type: "null" + context: + anyOf: + - type: string + maxLength: 2000 + - type: "null" + panel: + anyOf: + - anyOf: + - type: string + minLength: 1 + maxLength: 120 + - type: string + const: "" + - type: "null" + hidden: + type: boolean + 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. + CreateEncounterRequest: + type: object + properties: + occurredAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + status: + default: DONE + type: string + enum: + - PLANNED + - DONE + - CANCELLED + - NO_SHOW + kind: + default: OTHER + type: string + enum: + - ROUTINE + - ACUTE + - SPECIALIST + - PREVENTIVE + - EMERGENCY + - HOSPITAL + - THERAPY + - OTHER + practitionerId: + anyOf: + - type: string + minLength: 1 + maxLength: 40 + - type: "null" + reason: + anyOf: + - type: string + maxLength: 2000 + - type: "null" + outcome: + anyOf: + - type: string + maxLength: 4000 + - type: "null" + reminderId: + anyOf: + - type: string + minLength: 1 + maxLength: 40 + - type: "null" + documentIds: + maxItems: 100 + type: array + items: + type: string + minLength: 1 + maxLength: 40 + labResultIds: + maxItems: 100 + type: array + items: + type: string + minLength: 1 + maxLength: 40 + episodeIds: + maxItems: 100 + type: array + items: + type: string + minLength: 1 + maxLength: 40 + required: + - occurredAt + additionalProperties: false + 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." + UpdateEncounterRequest: + type: object + properties: + occurredAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + status: + type: string + enum: + - PLANNED + - DONE + - CANCELLED + - NO_SHOW + kind: + type: string + enum: + - ROUTINE + - ACUTE + - SPECIALIST + - PREVENTIVE + - EMERGENCY + - HOSPITAL + - THERAPY + - OTHER + practitionerId: + anyOf: + - type: string + minLength: 1 + maxLength: 40 + - type: "null" + reason: + anyOf: + - type: string + maxLength: 2000 + - type: "null" + outcome: + anyOf: + - type: string + maxLength: 4000 + - type: "null" + reminderId: + anyOf: + - type: string + minLength: 1 + maxLength: 40 + - type: "null" + documentIds: + maxItems: 100 + type: array + items: + type: string + minLength: 1 + maxLength: 40 + labResultIds: + maxItems: 100 + type: array + items: + type: string + minLength: 1 + maxLength: 40 + episodeIds: + maxItems: 100 + type: array + items: + type: string + minLength: 1 + maxLength: 40 + additionalProperties: false + 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. + EncounterLinkRequest: + type: object + properties: + targetKind: + type: string + enum: + - document + - labResult + - conditionEpisode + targetIds: + minItems: 1 + maxItems: 100 + type: array + items: + type: string + minLength: 1 + maxLength: 40 + required: + - targetKind + - targetIds + additionalProperties: false + 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. + CreatePractitionerRequest: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 160 + specialty: + anyOf: + - type: string + maxLength: 120 + - type: "null" + practice: + anyOf: + - type: string + maxLength: 160 + - type: "null" + location: + anyOf: + - type: string + maxLength: 300 + - type: "null" + phone: + anyOf: + - type: string + maxLength: 60 + - type: "null" + note: + anyOf: + - type: string + maxLength: 2000 + - type: "null" + required: + - name + additionalProperties: false + 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. + UpdatePractitionerRequest: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 160 + specialty: + anyOf: + - type: string + maxLength: 120 + - type: "null" + practice: + anyOf: + - type: string + maxLength: 160 + - type: "null" + location: + anyOf: + - type: string + maxLength: 300 + - type: "null" + phone: + anyOf: + - type: string + maxLength: 60 + - type: "null" + note: + anyOf: + - type: string + maxLength: 2000 + - type: "null" + additionalProperties: false + description: Partial edit; an omitted key leaves the column untouched, an explicit null clears it, and a body naming + nothing is a 422. + CreateAllergyRequest: + type: object + properties: + substance: + type: string + minLength: 1 + maxLength: 160 + category: + default: OTHER + type: string + enum: + - FOOD + - MEDICATION + - ENVIRONMENT + - BIOLOGIC + - OTHER + type: + default: ALLERGY + type: string + enum: + - ALLERGY + - INTOLERANCE + severity: + anyOf: + - type: string + enum: + - MILD + - MODERATE + - SEVERE + - type: "null" + status: + default: ACTIVE + type: string + enum: + - ACTIVE + - INACTIVE + - RESOLVED + onsetAt: + anyOf: + - type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + - type: "null" + reaction: + anyOf: + - type: string + maxLength: 2000 + - type: "null" + note: + anyOf: + - type: string + maxLength: 2000 + - type: "null" + required: + - substance + 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. + UpdateAllergyRequest: + type: object + properties: + substance: + type: string + minLength: 1 + maxLength: 160 + category: + type: string + enum: + - FOOD + - MEDICATION + - ENVIRONMENT + - BIOLOGIC + - OTHER + type: + type: string + enum: + - ALLERGY + - INTOLERANCE + severity: + anyOf: + - type: string + enum: + - MILD + - MODERATE + - SEVERE + - type: "null" + status: + type: string + enum: + - ACTIVE + - INACTIVE + - RESOLVED + onsetAt: + anyOf: + - type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + - type: "null" + reaction: + anyOf: + - type: string + maxLength: 2000 + - type: "null" + note: + anyOf: + - type: string + maxLength: 2000 + - type: "null" + additionalProperties: false + 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. InboundExtractRequest: anyOf: - type: object @@ -28488,13 +28343,7 @@ components: - type: "null" flow: anyOf: - - type: string - enum: - - NONE - - SPOTTING - - LIGHT - - MEDIUM - - HEAVY + - $ref: "#/components/schemas/FlowLevel" - type: "null" intermenstrualBleeding: type: boolean @@ -28506,22 +28355,11 @@ components: type: boolean ovulationTest: anyOf: - - type: string - enum: - - NEGATIVE - - POSITIVE_LH_SURGE - - ESTROGEN_SURGE - - INDETERMINATE + - $ref: "#/components/schemas/OvulationTest" - type: "null" cervicalMucus: anyOf: - - type: string - enum: - - DRY - - STICKY - - CREAMY - - WATERY - - EGG_WHITE + - $ref: "#/components/schemas/CervicalMucus" - type: "null" cervixPosition: anyOf: @@ -28552,19 +28390,11 @@ components: - type: "null" pregnancyTest: anyOf: - - type: string - enum: - - NEGATIVE - - POSITIVE - - INDETERMINATE + - $ref: "#/components/schemas/HomeTestResult" - type: "null" progesteroneTest: anyOf: - - type: string - enum: - - NEGATIVE - - POSITIVE - - INDETERMINATE + - $ref: "#/components/schemas/HomeTestResult" - type: "null" contraceptive: anyOf: @@ -28639,6 +28469,39 @@ components: additionalProperties: false description: The canonical day-log row iOS mirrors. `note` is decrypted on read. Soft-deleted rows ride `/api/sync/changes` as tombstones. + FlowLevel: + type: string + enum: + - NONE + - SPOTTING + - LIGHT + - MEDIUM + - HEAVY + description: Menstrual-flow intensity. + OvulationTest: + type: string + enum: + - NEGATIVE + - POSITIVE_LH_SURGE + - ESTROGEN_SURGE + - INDETERMINATE + description: Ovulation predictor-kit (OPK) reading. + CervicalMucus: + type: string + enum: + - DRY + - STICKY + - CREAMY + - WATERY + - EGG_WHITE + description: Cervical-mucus quality. + HomeTestResult: + type: string + enum: + - NEGATIVE + - POSITIVE + - INDETERMINATE + description: At-home test result (pregnancy / progesterone). CycleDayLogEnvelope: type: object properties: @@ -28852,13 +28715,7 @@ components: type: object properties: goal: - type: string - enum: - - GENERAL_HEALTH - - AVOID_PREGNANCY - - TRYING_TO_CONCEIVE - - PERIMENOPAUSE - - OFF + $ref: "#/components/schemas/CycleTrackingGoal" rawChartMode: type: boolean predictionEnabled: @@ -28905,13 +28762,7 @@ components: description: Whether a logged cycle opens on this day. Deleting this day's log removes that cycle start with it. flow: anyOf: - - type: string - enum: - - NONE - - SPOTTING - - LIGHT - - MEDIUM - - HEAVY + - $ref: "#/components/schemas/FlowLevel" - type: "null" hasSymptoms: type: boolean @@ -28925,22 +28776,11 @@ components: type: boolean ovulationTest: anyOf: - - type: string - enum: - - NEGATIVE - - POSITIVE_LH_SURGE - - ESTROGEN_SURGE - - INDETERMINATE + - $ref: "#/components/schemas/OvulationTest" - type: "null" cervicalMucus: anyOf: - - type: string - enum: - - DRY - - STICKY - - CREAMY - - WATERY - - EGG_WHITE + - $ref: "#/components/schemas/CervicalMucus" - type: "null" cervixPosition: anyOf: @@ -29012,6 +28852,15 @@ components: - data - error additionalProperties: false + CycleTrackingGoal: + type: string + enum: + - GENERAL_HEALTH + - AVOID_PREGNANCY + - TRYING_TO_CONCEIVE + - PERIMENOPAUSE + - OFF + description: Drives cycle copy + fertile-window gating. CyclePredictionDTO: type: object properties: @@ -29532,13 +29381,7 @@ components: type: object properties: goal: - type: string - enum: - - GENERAL_HEALTH - - AVOID_PREGNANCY - - TRYING_TO_CONCEIVE - - PERIMENOPAUSE - - OFF + $ref: "#/components/schemas/CycleTrackingGoal" cycleTrackingEnabled: type: boolean secondarySymptom: @@ -36581,85 +36424,7 @@ components: description: Stored row id, or a synthetic key (`day::`, `sleep-seg::`) on the collapsed list modes. type: - type: string - enum: - - WEIGHT - - BLOOD_PRESSURE_SYS - - BLOOD_PRESSURE_DIA - - PULSE - - BODY_FAT - - SLEEP_DURATION - - ACTIVITY_STEPS - - BLOOD_GLUCOSE - - TOTAL_BODY_WATER - - BONE_MASS - - OXYGEN_SATURATION - - HEART_RATE_VARIABILITY - - RESTING_HEART_RATE - - ACTIVE_ENERGY_BURNED - - FLIGHTS_CLIMBED - - WALKING_RUNNING_DISTANCE - - VO2_MAX - - BODY_TEMPERATURE - - FAT_FREE_MASS - - FAT_MASS - - MUSCLE_MASS - - SKIN_TEMPERATURE - - PULSE_WAVE_VELOCITY - - VASCULAR_AGE - - VISCERAL_FAT - - AUDIO_EXPOSURE_ENV - - AUDIO_EXPOSURE_HEADPHONE - - TIME_IN_DAYLIGHT - - WALKING_STEADINESS - - AUDIO_EXPOSURE_EVENT - - RESPIRATORY_RATE - - BODY_MASS_INDEX - - LEAN_BODY_MASS - - WALKING_HEART_RATE_AVERAGE - - WALKING_ASYMMETRY - - WALKING_DOUBLE_SUPPORT - - WALKING_STEP_LENGTH - - WALKING_SPEED - - CARDIO_RECOVERY - - WRIST_TEMPERATURE - - FALL_COUNT - - SIX_MINUTE_WALK_DISTANCE - - STAIR_ASCENT_SPEED - - STAIR_DESCENT_SPEED - - BREATHING_DISTURBANCES - - IRREGULAR_RHYTHM_NOTIFICATION - - HIGH_HEART_RATE_EVENT - - LOW_HEART_RATE_EVENT - - WALKING_STEADINESS_EVENT - - BREATHING_DISTURBANCE_EVENT - - RECOVERY_SCORE - - STRESS_SCORE - - STRAIN_SCORE - - HRV_RMSSD - - DAY_STRAIN - - WORKOUT_STRAIN - - SLEEP_PERFORMANCE - - SLEEP_EFFICIENCY - - SLEEP_CONSISTENCY - - SLEEP_NEED - - ENERGY_EXPENDITURE_KJ - - AVERAGE_HEART_RATE - - MAX_HEART_RATE - - SLEEP_DISTURBANCE_COUNT - - ANS_CHARGE - - CARDIO_LOAD - - SLEEP_SCORE - - BODY_TEMPERATURE_DEVIATION - - RESILIENCE - - PHQ9_SCORE - - GAD7_SCORE - - GRIP_STRENGTH - - PAIN_NRS - - WAIST_CIRCUMFERENCE - - WAIST_TO_HEIGHT - - WHO5_SCORE - - SCI_SCORE + $ref: "#/components/schemas/MeasurementType" value: type: number unit: @@ -36669,22 +36434,7 @@ components: format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ source: - type: string - enum: - - MANUAL - - WITHINGS - - IMPORT - - APPLE_HEALTH - - COMPUTED - - WHOOP - - FITBIT - - NIGHTSCOUT - - POLAR - - OURA - - TELEGRAM - - MCP - - GOOGLE_HEALTH - - STRAVA + $ref: "#/components/schemas/MeasurementSource" notes: anyOf: - type: string @@ -36771,6 +36521,24 @@ components: - syncVersion - updatedAt additionalProperties: false + MeasurementSource: + type: string + enum: + - MANUAL + - WITHINGS + - IMPORT + - APPLE_HEALTH + - COMPUTED + - WHOOP + - FITBIT + - NIGHTSCOUT + - POLAR + - OURA + - TELEGRAM + - MCP + - GOOGLE_HEALTH + - STRAVA + description: Origin of the measurement. v1.4.23 added APPLE_HEALTH for the iOS HealthKit batch ingest path. SyncMeasurementTombstone: type: object properties: @@ -37012,170 +36780,14 @@ components: series: type: object propertyNames: - type: string - enum: - - WEIGHT - - BLOOD_PRESSURE_SYS - - BLOOD_PRESSURE_DIA - - PULSE - - BODY_FAT - - SLEEP_DURATION - - ACTIVITY_STEPS - - BLOOD_GLUCOSE - - TOTAL_BODY_WATER - - BONE_MASS - - OXYGEN_SATURATION - - HEART_RATE_VARIABILITY - - RESTING_HEART_RATE - - ACTIVE_ENERGY_BURNED - - FLIGHTS_CLIMBED - - WALKING_RUNNING_DISTANCE - - VO2_MAX - - BODY_TEMPERATURE - - FAT_FREE_MASS - - FAT_MASS - - MUSCLE_MASS - - SKIN_TEMPERATURE - - PULSE_WAVE_VELOCITY - - VASCULAR_AGE - - VISCERAL_FAT - - AUDIO_EXPOSURE_ENV - - AUDIO_EXPOSURE_HEADPHONE - - TIME_IN_DAYLIGHT - - WALKING_STEADINESS - - AUDIO_EXPOSURE_EVENT - - RESPIRATORY_RATE - - BODY_MASS_INDEX - - LEAN_BODY_MASS - - WALKING_HEART_RATE_AVERAGE - - WALKING_ASYMMETRY - - WALKING_DOUBLE_SUPPORT - - WALKING_STEP_LENGTH - - WALKING_SPEED - - CARDIO_RECOVERY - - WRIST_TEMPERATURE - - FALL_COUNT - - SIX_MINUTE_WALK_DISTANCE - - STAIR_ASCENT_SPEED - - STAIR_DESCENT_SPEED - - BREATHING_DISTURBANCES - - IRREGULAR_RHYTHM_NOTIFICATION - - HIGH_HEART_RATE_EVENT - - LOW_HEART_RATE_EVENT - - WALKING_STEADINESS_EVENT - - BREATHING_DISTURBANCE_EVENT - - RECOVERY_SCORE - - STRESS_SCORE - - STRAIN_SCORE - - HRV_RMSSD - - DAY_STRAIN - - WORKOUT_STRAIN - - SLEEP_PERFORMANCE - - SLEEP_EFFICIENCY - - SLEEP_CONSISTENCY - - SLEEP_NEED - - ENERGY_EXPENDITURE_KJ - - AVERAGE_HEART_RATE - - MAX_HEART_RATE - - SLEEP_DISTURBANCE_COUNT - - ANS_CHARGE - - CARDIO_LOAD - - SLEEP_SCORE - - BODY_TEMPERATURE_DEVIATION - - RESILIENCE - - PHQ9_SCORE - - GAD7_SCORE - - GRIP_STRENGTH - - PAIN_NRS - - WAIST_CIRCUMFERENCE - - WAIST_TO_HEIGHT - - WHO5_SCORE - - SCI_SCORE + $ref: "#/components/schemas/MeasurementType" additionalProperties: type: array items: type: object properties: type: - type: string - enum: - - WEIGHT - - BLOOD_PRESSURE_SYS - - BLOOD_PRESSURE_DIA - - PULSE - - BODY_FAT - - SLEEP_DURATION - - ACTIVITY_STEPS - - BLOOD_GLUCOSE - - TOTAL_BODY_WATER - - BONE_MASS - - OXYGEN_SATURATION - - HEART_RATE_VARIABILITY - - RESTING_HEART_RATE - - ACTIVE_ENERGY_BURNED - - FLIGHTS_CLIMBED - - WALKING_RUNNING_DISTANCE - - VO2_MAX - - BODY_TEMPERATURE - - FAT_FREE_MASS - - FAT_MASS - - MUSCLE_MASS - - SKIN_TEMPERATURE - - PULSE_WAVE_VELOCITY - - VASCULAR_AGE - - VISCERAL_FAT - - AUDIO_EXPOSURE_ENV - - AUDIO_EXPOSURE_HEADPHONE - - TIME_IN_DAYLIGHT - - WALKING_STEADINESS - - AUDIO_EXPOSURE_EVENT - - RESPIRATORY_RATE - - BODY_MASS_INDEX - - LEAN_BODY_MASS - - WALKING_HEART_RATE_AVERAGE - - WALKING_ASYMMETRY - - WALKING_DOUBLE_SUPPORT - - WALKING_STEP_LENGTH - - WALKING_SPEED - - CARDIO_RECOVERY - - WRIST_TEMPERATURE - - FALL_COUNT - - SIX_MINUTE_WALK_DISTANCE - - STAIR_ASCENT_SPEED - - STAIR_DESCENT_SPEED - - BREATHING_DISTURBANCES - - IRREGULAR_RHYTHM_NOTIFICATION - - HIGH_HEART_RATE_EVENT - - LOW_HEART_RATE_EVENT - - WALKING_STEADINESS_EVENT - - BREATHING_DISTURBANCE_EVENT - - RECOVERY_SCORE - - STRESS_SCORE - - STRAIN_SCORE - - HRV_RMSSD - - DAY_STRAIN - - WORKOUT_STRAIN - - SLEEP_PERFORMANCE - - SLEEP_EFFICIENCY - - SLEEP_CONSISTENCY - - SLEEP_NEED - - ENERGY_EXPENDITURE_KJ - - AVERAGE_HEART_RATE - - MAX_HEART_RATE - - SLEEP_DISTURBANCE_COUNT - - ANS_CHARGE - - CARDIO_LOAD - - SLEEP_SCORE - - BODY_TEMPERATURE_DEVIATION - - RESILIENCE - - PHQ9_SCORE - - GAD7_SCORE - - GRIP_STRENGTH - - PAIN_NRS - - WAIST_CIRCUMFERENCE - - WAIST_TO_HEIGHT - - WHO5_SCORE - - SCI_SCORE + $ref: "#/components/schemas/MeasurementType" value: type: number description: Daily mean (spot metrics) or SUM (cumulative). @@ -37446,85 +37058,7 @@ components: description: Stored row id, or a synthetic key (`day::`, `sleep-seg::`) on the collapsed list modes. type: - type: string - enum: - - WEIGHT - - BLOOD_PRESSURE_SYS - - BLOOD_PRESSURE_DIA - - PULSE - - BODY_FAT - - SLEEP_DURATION - - ACTIVITY_STEPS - - BLOOD_GLUCOSE - - TOTAL_BODY_WATER - - BONE_MASS - - OXYGEN_SATURATION - - HEART_RATE_VARIABILITY - - RESTING_HEART_RATE - - ACTIVE_ENERGY_BURNED - - FLIGHTS_CLIMBED - - WALKING_RUNNING_DISTANCE - - VO2_MAX - - BODY_TEMPERATURE - - FAT_FREE_MASS - - FAT_MASS - - MUSCLE_MASS - - SKIN_TEMPERATURE - - PULSE_WAVE_VELOCITY - - VASCULAR_AGE - - VISCERAL_FAT - - AUDIO_EXPOSURE_ENV - - AUDIO_EXPOSURE_HEADPHONE - - TIME_IN_DAYLIGHT - - WALKING_STEADINESS - - AUDIO_EXPOSURE_EVENT - - RESPIRATORY_RATE - - BODY_MASS_INDEX - - LEAN_BODY_MASS - - WALKING_HEART_RATE_AVERAGE - - WALKING_ASYMMETRY - - WALKING_DOUBLE_SUPPORT - - WALKING_STEP_LENGTH - - WALKING_SPEED - - CARDIO_RECOVERY - - WRIST_TEMPERATURE - - FALL_COUNT - - SIX_MINUTE_WALK_DISTANCE - - STAIR_ASCENT_SPEED - - STAIR_DESCENT_SPEED - - BREATHING_DISTURBANCES - - IRREGULAR_RHYTHM_NOTIFICATION - - HIGH_HEART_RATE_EVENT - - LOW_HEART_RATE_EVENT - - WALKING_STEADINESS_EVENT - - BREATHING_DISTURBANCE_EVENT - - RECOVERY_SCORE - - STRESS_SCORE - - STRAIN_SCORE - - HRV_RMSSD - - DAY_STRAIN - - WORKOUT_STRAIN - - SLEEP_PERFORMANCE - - SLEEP_EFFICIENCY - - SLEEP_CONSISTENCY - - SLEEP_NEED - - ENERGY_EXPENDITURE_KJ - - AVERAGE_HEART_RATE - - MAX_HEART_RATE - - SLEEP_DISTURBANCE_COUNT - - ANS_CHARGE - - CARDIO_LOAD - - SLEEP_SCORE - - BODY_TEMPERATURE_DEVIATION - - RESILIENCE - - PHQ9_SCORE - - GAD7_SCORE - - GRIP_STRENGTH - - PAIN_NRS - - WAIST_CIRCUMFERENCE - - WAIST_TO_HEIGHT - - WHO5_SCORE - - SCI_SCORE + $ref: "#/components/schemas/MeasurementType" value: type: number unit: @@ -37534,22 +37068,7 @@ components: format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ source: - type: string - enum: - - MANUAL - - WITHINGS - - IMPORT - - APPLE_HEALTH - - COMPUTED - - WHOOP - - FITBIT - - NIGHTSCOUT - - POLAR - - OURA - - TELEGRAM - - MCP - - GOOGLE_HEALTH - - STRAVA + $ref: "#/components/schemas/MeasurementSource" notes: anyOf: - type: string @@ -37640,85 +37159,7 @@ components: type: object properties: type: - type: string - enum: - - WEIGHT - - BLOOD_PRESSURE_SYS - - BLOOD_PRESSURE_DIA - - PULSE - - BODY_FAT - - SLEEP_DURATION - - ACTIVITY_STEPS - - BLOOD_GLUCOSE - - TOTAL_BODY_WATER - - BONE_MASS - - OXYGEN_SATURATION - - HEART_RATE_VARIABILITY - - RESTING_HEART_RATE - - ACTIVE_ENERGY_BURNED - - FLIGHTS_CLIMBED - - WALKING_RUNNING_DISTANCE - - VO2_MAX - - BODY_TEMPERATURE - - FAT_FREE_MASS - - FAT_MASS - - MUSCLE_MASS - - SKIN_TEMPERATURE - - PULSE_WAVE_VELOCITY - - VASCULAR_AGE - - VISCERAL_FAT - - AUDIO_EXPOSURE_ENV - - AUDIO_EXPOSURE_HEADPHONE - - TIME_IN_DAYLIGHT - - WALKING_STEADINESS - - AUDIO_EXPOSURE_EVENT - - RESPIRATORY_RATE - - BODY_MASS_INDEX - - LEAN_BODY_MASS - - WALKING_HEART_RATE_AVERAGE - - WALKING_ASYMMETRY - - WALKING_DOUBLE_SUPPORT - - WALKING_STEP_LENGTH - - WALKING_SPEED - - CARDIO_RECOVERY - - WRIST_TEMPERATURE - - FALL_COUNT - - SIX_MINUTE_WALK_DISTANCE - - STAIR_ASCENT_SPEED - - STAIR_DESCENT_SPEED - - BREATHING_DISTURBANCES - - IRREGULAR_RHYTHM_NOTIFICATION - - HIGH_HEART_RATE_EVENT - - LOW_HEART_RATE_EVENT - - WALKING_STEADINESS_EVENT - - BREATHING_DISTURBANCE_EVENT - - RECOVERY_SCORE - - STRESS_SCORE - - STRAIN_SCORE - - HRV_RMSSD - - DAY_STRAIN - - WORKOUT_STRAIN - - SLEEP_PERFORMANCE - - SLEEP_EFFICIENCY - - SLEEP_CONSISTENCY - - SLEEP_NEED - - ENERGY_EXPENDITURE_KJ - - AVERAGE_HEART_RATE - - MAX_HEART_RATE - - SLEEP_DISTURBANCE_COUNT - - ANS_CHARGE - - CARDIO_LOAD - - SLEEP_SCORE - - BODY_TEMPERATURE_DEVIATION - - RESILIENCE - - PHQ9_SCORE - - GAD7_SCORE - - GRIP_STRENGTH - - PAIN_NRS - - WAIST_CIRCUMFERENCE - - WAIST_TO_HEIGHT - - WHO5_SCORE - - SCI_SCORE + $ref: "#/components/schemas/MeasurementType" value: type: number description: The bucket's aggregated value. @@ -38061,22 +37502,7 @@ components: description: Wake-day key (YYYY-MM-DD). source: anyOf: - - type: string - enum: - - MANUAL - - WITHINGS - - IMPORT - - APPLE_HEALTH - - COMPUTED - - WHOOP - - FITBIT - - NIGHTSCOUT - - POLAR - - OURA - - TELEGRAM - - MCP - - GOOGLE_HEALTH - - STRAVA + - $ref: "#/components/schemas/MeasurementSource" - type: "null" start: type: string @@ -38230,22 +37656,7 @@ components: description: Wake-day key (YYYY-MM-DD). source: anyOf: - - type: string - enum: - - MANUAL - - WITHINGS - - IMPORT - - APPLE_HEALTH - - COMPUTED - - WHOOP - - FITBIT - - NIGHTSCOUT - - POLAR - - OURA - - TELEGRAM - - MCP - - GOOGLE_HEALTH - - STRAVA + - $ref: "#/components/schemas/MeasurementSource" - type: "null" start: type: string @@ -38451,25 +37862,10 @@ components: maximum: 9007199254740991 description: Nights still needed before the debt is asserted (0 when ready). source: - type: string - enum: - - MANUAL - - WITHINGS - - IMPORT - - APPLE_HEALTH - - COMPUTED - - WHOOP - - FITBIT - - NIGHTSCOUT - - POLAR - - OURA - - TELEGRAM - - MCP - - GOOGLE_HEALTH - - STRAVA description: v1.25.0 — the active source the debt is resolved FROM, picked off the user's `sleepDebt` source ladder. `COMPUTED` is HealthLog's own rolling-balance estimate (the only producer today); a provider value would be a device-native debt. Clients explain the figure when it is `COMPUTED`. + $ref: "#/components/schemas/MeasurementSource" required: - state - debtMinutes @@ -38602,85 +37998,7 @@ components: type: string description: The RECORD's user id — under a delegated read this is the owner, not the caller. metricType: - type: string - enum: - - WEIGHT - - BLOOD_PRESSURE_SYS - - BLOOD_PRESSURE_DIA - - PULSE - - BODY_FAT - - SLEEP_DURATION - - ACTIVITY_STEPS - - BLOOD_GLUCOSE - - TOTAL_BODY_WATER - - BONE_MASS - - OXYGEN_SATURATION - - HEART_RATE_VARIABILITY - - RESTING_HEART_RATE - - ACTIVE_ENERGY_BURNED - - FLIGHTS_CLIMBED - - WALKING_RUNNING_DISTANCE - - VO2_MAX - - BODY_TEMPERATURE - - FAT_FREE_MASS - - FAT_MASS - - MUSCLE_MASS - - SKIN_TEMPERATURE - - PULSE_WAVE_VELOCITY - - VASCULAR_AGE - - VISCERAL_FAT - - AUDIO_EXPOSURE_ENV - - AUDIO_EXPOSURE_HEADPHONE - - TIME_IN_DAYLIGHT - - WALKING_STEADINESS - - AUDIO_EXPOSURE_EVENT - - RESPIRATORY_RATE - - BODY_MASS_INDEX - - LEAN_BODY_MASS - - WALKING_HEART_RATE_AVERAGE - - WALKING_ASYMMETRY - - WALKING_DOUBLE_SUPPORT - - WALKING_STEP_LENGTH - - WALKING_SPEED - - CARDIO_RECOVERY - - WRIST_TEMPERATURE - - FALL_COUNT - - SIX_MINUTE_WALK_DISTANCE - - STAIR_ASCENT_SPEED - - STAIR_DESCENT_SPEED - - BREATHING_DISTURBANCES - - IRREGULAR_RHYTHM_NOTIFICATION - - HIGH_HEART_RATE_EVENT - - LOW_HEART_RATE_EVENT - - WALKING_STEADINESS_EVENT - - BREATHING_DISTURBANCE_EVENT - - RECOVERY_SCORE - - STRESS_SCORE - - STRAIN_SCORE - - HRV_RMSSD - - DAY_STRAIN - - WORKOUT_STRAIN - - SLEEP_PERFORMANCE - - SLEEP_EFFICIENCY - - SLEEP_CONSISTENCY - - SLEEP_NEED - - ENERGY_EXPENDITURE_KJ - - AVERAGE_HEART_RATE - - MAX_HEART_RATE - - SLEEP_DISTURBANCE_COUNT - - ANS_CHARGE - - CARDIO_LOAD - - SLEEP_SCORE - - BODY_TEMPERATURE_DEVIATION - - RESILIENCE - - PHQ9_SCORE - - GAD7_SCORE - - GRIP_STRENGTH - - PAIN_NRS - - WAIST_CIRCUMFERENCE - - WAIST_TO_HEIGHT - - WHO5_SCORE - - SCI_SCORE + $ref: "#/components/schemas/MeasurementType" metricSlot: anyOf: - type: string @@ -38709,22 +38027,7 @@ components: description: The measurement that achieved the record. Nulled rather than cascaded when that measurement is later deleted, so the historical fact survives the reading behind it. source: - type: string - enum: - - MANUAL - - WITHINGS - - IMPORT - - APPLE_HEALTH - - COMPUTED - - WHOOP - - FITBIT - - NIGHTSCOUT - - POLAR - - OURA - - TELEGRAM - - MCP - - GOOGLE_HEALTH - - STRAVA + $ref: "#/components/schemas/MeasurementSource" externalId: anyOf: - type: string @@ -38850,22 +38153,7 @@ components: maximum: 9007199254740991 - type: "null" source: - type: string - enum: - - MANUAL - - WITHINGS - - IMPORT - - APPLE_HEALTH - - COMPUTED - - WHOOP - - FITBIT - - NIGHTSCOUT - - POLAR - - OURA - - TELEGRAM - - MCP - - GOOGLE_HEALTH - - STRAVA + $ref: "#/components/schemas/MeasurementSource" externalId: anyOf: - type: string @@ -38970,22 +38258,7 @@ components: maximum: 9007199254740991 - type: "null" source: - type: string - enum: - - MANUAL - - WITHINGS - - IMPORT - - APPLE_HEALTH - - COMPUTED - - WHOOP - - FITBIT - - NIGHTSCOUT - - POLAR - - OURA - - TELEGRAM - - MCP - - GOOGLE_HEALTH - - STRAVA + $ref: "#/components/schemas/MeasurementSource" externalId: anyOf: - type: string @@ -47579,67 +46852,7 @@ components: type: object properties: data: - type: object - properties: - bloodType: - anyOf: - - type: string - enum: - - A_POS - - A_NEG - - B_POS - - B_NEG - - AB_POS - - AB_NEG - - O_POS - - O_NEG - - UNKNOWN - - type: "null" - organDonor: - anyOf: - - type: string - enum: - - YES - - NO - - UNKNOWN - - type: "null" - advanceDirective: - anyOf: - - type: string - enum: - - EXISTS - - NONE - - UNKNOWN - - type: "null" - contacts: - anyOf: - - type: string - - type: "null" - contactsUnreadable: - type: boolean - implants: - anyOf: - - type: string - - type: "null" - implantsUnreadable: - type: boolean - note: - anyOf: - - type: string - - type: "null" - noteUnreadable: - type: boolean - required: - - bloodType - - organDonor - - advanceDirective - - contacts - - contactsUnreadable - - implants - - implantsUnreadable - - note - - noteUnreadable - additionalProperties: false + $ref: "#/components/schemas/EmergencyProfile" error: type: "null" meta: @@ -47652,71 +46865,76 @@ components: - data - error additionalProperties: false + EmergencyProfile: + type: object + properties: + bloodType: + anyOf: + - type: string + enum: + - A_POS + - A_NEG + - B_POS + - B_NEG + - AB_POS + - AB_NEG + - O_POS + - O_NEG + - UNKNOWN + - type: "null" + organDonor: + anyOf: + - type: string + enum: + - YES + - NO + - UNKNOWN + - type: "null" + advanceDirective: + anyOf: + - type: string + enum: + - EXISTS + - NONE + - UNKNOWN + - type: "null" + contacts: + anyOf: + - type: string + - type: "null" + contactsUnreadable: + type: boolean + implants: + anyOf: + - type: string + - type: "null" + implantsUnreadable: + type: boolean + note: + anyOf: + - type: string + - type: "null" + noteUnreadable: + type: boolean + required: + - bloodType + - organDonor + - advanceDirective + - contacts + - contactsUnreadable + - implants + - implantsUnreadable + - note + - noteUnreadable + additionalProperties: false + 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)." UpdateEmergencyProfileResponse: type: object properties: data: - type: object - properties: - bloodType: - anyOf: - - type: string - enum: - - A_POS - - A_NEG - - B_POS - - B_NEG - - AB_POS - - AB_NEG - - O_POS - - O_NEG - - UNKNOWN - - type: "null" - organDonor: - anyOf: - - type: string - enum: - - YES - - NO - - UNKNOWN - - type: "null" - advanceDirective: - anyOf: - - type: string - enum: - - EXISTS - - NONE - - UNKNOWN - - type: "null" - contacts: - anyOf: - - type: string - - type: "null" - contactsUnreadable: - type: boolean - implants: - anyOf: - - type: string - - type: "null" - implantsUnreadable: - type: boolean - note: - anyOf: - - type: string - - type: "null" - noteUnreadable: - type: boolean - required: - - bloodType - - organDonor - - advanceDirective - - contacts - - contactsUnreadable - - implants - - implantsUnreadable - - note - - noteUnreadable - additionalProperties: false + $ref: "#/components/schemas/EmergencyProfile" error: type: "null" meta: @@ -49227,85 +48445,7 @@ components: - type: object properties: metricType: - type: string - enum: - - WEIGHT - - BLOOD_PRESSURE_SYS - - BLOOD_PRESSURE_DIA - - PULSE - - BODY_FAT - - SLEEP_DURATION - - ACTIVITY_STEPS - - BLOOD_GLUCOSE - - TOTAL_BODY_WATER - - BONE_MASS - - OXYGEN_SATURATION - - HEART_RATE_VARIABILITY - - RESTING_HEART_RATE - - ACTIVE_ENERGY_BURNED - - FLIGHTS_CLIMBED - - WALKING_RUNNING_DISTANCE - - VO2_MAX - - BODY_TEMPERATURE - - FAT_FREE_MASS - - FAT_MASS - - MUSCLE_MASS - - SKIN_TEMPERATURE - - PULSE_WAVE_VELOCITY - - VASCULAR_AGE - - VISCERAL_FAT - - AUDIO_EXPOSURE_ENV - - AUDIO_EXPOSURE_HEADPHONE - - TIME_IN_DAYLIGHT - - WALKING_STEADINESS - - AUDIO_EXPOSURE_EVENT - - RESPIRATORY_RATE - - BODY_MASS_INDEX - - LEAN_BODY_MASS - - WALKING_HEART_RATE_AVERAGE - - WALKING_ASYMMETRY - - WALKING_DOUBLE_SUPPORT - - WALKING_STEP_LENGTH - - WALKING_SPEED - - CARDIO_RECOVERY - - WRIST_TEMPERATURE - - FALL_COUNT - - SIX_MINUTE_WALK_DISTANCE - - STAIR_ASCENT_SPEED - - STAIR_DESCENT_SPEED - - BREATHING_DISTURBANCES - - IRREGULAR_RHYTHM_NOTIFICATION - - HIGH_HEART_RATE_EVENT - - LOW_HEART_RATE_EVENT - - WALKING_STEADINESS_EVENT - - BREATHING_DISTURBANCE_EVENT - - RECOVERY_SCORE - - STRESS_SCORE - - STRAIN_SCORE - - HRV_RMSSD - - DAY_STRAIN - - WORKOUT_STRAIN - - SLEEP_PERFORMANCE - - SLEEP_EFFICIENCY - - SLEEP_CONSISTENCY - - SLEEP_NEED - - ENERGY_EXPENDITURE_KJ - - AVERAGE_HEART_RATE - - MAX_HEART_RATE - - SLEEP_DISTURBANCE_COUNT - - ANS_CHARGE - - CARDIO_LOAD - - SLEEP_SCORE - - BODY_TEMPERATURE_DEVIATION - - RESILIENCE - - PHQ9_SCORE - - GAD7_SCORE - - GRIP_STRENGTH - - PAIN_NRS - - WAIST_CIRCUMFERENCE - - WAIST_TO_HEIGHT - - WHO5_SCORE - - SCI_SCORE + $ref: "#/components/schemas/MeasurementType" daysInside: type: integer minimum: -9007199254740991 @@ -52905,22 +52045,7 @@ components: pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Device-reported event time (`measuredAt`). source: - type: string - enum: - - MANUAL - - WITHINGS - - IMPORT - - APPLE_HEALTH - - COMPUTED - - WHOOP - - FITBIT - - NIGHTSCOUT - - POLAR - - OURA - - TELEGRAM - - MCP - - GOOGLE_HEALTH - - STRAVA + $ref: "#/components/schemas/MeasurementSource" deviceType: anyOf: - type: string @@ -68426,6 +67551,8 @@ components: - showEvidenceByDefault - defaultWindow additionalProperties: false + description: Per-user Coach prompt-tuning preferences (v1.4.23 H4). All fields default to the legacy v1.4.22 behaviour + when omitted. parameters: AccountSelector: name: X-HealthLog-Account diff --git a/src/lib/openapi/routes/allergies.ts b/src/lib/openapi/routes/allergies.ts index 2be4d0a16..3f1629054 100644 --- a/src/lib/openapi/routes/allergies.ts +++ b/src/lib/openapi/routes/allergies.ts @@ -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 = { 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 = { 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 = { requestParams: { path: z.object({ id: z.string() }) }, requestBody: { required: true, - content: { "application/json": { schema: allergyUpdateSchema } }, + content: { "application/json": { schema: updateAllergyRequest } }, }, responses: { ...recordRefusal(), diff --git a/src/lib/openapi/routes/auth.ts b/src/lib/openapi/routes/auth.ts index ef464a1b1..3d2694ab7 100644 --- a/src/lib/openapi/routes/auth.ts +++ b/src/lib/openapi/routes/auth.ts @@ -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) ─────────────────── diff --git a/src/lib/openapi/routes/awards.ts b/src/lib/openapi/routes/awards.ts index a442b6076..d81221375 100644 --- a/src/lib/openapi/routes/awards.ts +++ b/src/lib/openapi/routes/awards.ts @@ -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"; diff --git a/src/lib/openapi/routes/biomarkers.ts b/src/lib/openapi/routes/biomarkers.ts index ff9a04202..230e0e47e 100644 --- a/src/lib/openapi/routes/biomarkers.ts +++ b/src/lib/openapi/routes/biomarkers.ts @@ -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 = { 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 = { requestParams: { path: z.object({ id: z.string() }) }, requestBody: { required: true, - content: { "application/json": { schema: updateBiomarkerSchema } }, + content: { "application/json": { schema: updateBiomarkerRequest } }, }, responses: { ...recordRefusal(), diff --git a/src/lib/openapi/routes/coach.ts b/src/lib/openapi/routes/coach.ts index 6ed940018..3a986fb7d 100644 --- a/src/lib/openapi/routes/coach.ts +++ b/src/lib/openapi/routes/coach.ts @@ -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 = { requestBody: { required: true, content: { - "application/json": { schema: coachChatRequestSchema }, + "application/json": { schema: coachChatRequest }, }, }, responses: { @@ -1468,7 +1451,7 @@ export const coachPaths: NonNullable = { content: { "application/json": { schema: dataEnvelope( - emergencyProfileDtoSchema, + emergencyProfile, "GetEmergencyProfileResponse", ), }, @@ -1485,7 +1468,7 @@ export const coachPaths: NonNullable = { requestBody: { required: true, content: { - "application/json": { schema: emergencyProfileUpdateSchema }, + "application/json": { schema: updateEmergencyProfileRequest }, }, }, responses: { @@ -1494,7 +1477,7 @@ export const coachPaths: NonNullable = { content: { "application/json": { schema: dataEnvelope( - emergencyProfileDtoSchema, + emergencyProfile, "UpdateEmergencyProfileResponse", ), }, @@ -1675,7 +1658,7 @@ export const coachPaths: NonNullable = { requestBody: { required: true, content: { - "application/json": { schema: coachPlanPatchSchema }, + "application/json": { schema: coachPlanPatchRequest }, }, }, responses: { diff --git a/src/lib/openapi/routes/consent.ts b/src/lib/openapi/routes/consent.ts index ccaf55697..e98167411 100644 --- a/src/lib/openapi/routes/consent.ts +++ b/src/lib/openapi/routes/consent.ts @@ -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.', diff --git a/src/lib/openapi/routes/cycle.ts b/src/lib/openapi/routes/cycle.ts index e4dacb5f5..903a8cd93 100644 --- a/src/lib/openapi/routes/cycle.ts +++ b/src/lib/openapi/routes/cycle.ts @@ -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 = { 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 = { 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 = { 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 = { 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 = { "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": { diff --git a/src/lib/openapi/routes/encounters.ts b/src/lib/openapi/routes/encounters.ts index 07df2104f..f1e3f4a64 100644 --- a/src/lib/openapi/routes/encounters.ts +++ b/src/lib/openapi/routes/encounters.ts @@ -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 = { 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 = { "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 = { 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 = { 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 = { 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 = { 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 = { 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 = { "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 = { requestParams: idPath, requestBody: { required: true, - content: { "application/json": { schema: practitionerUpdateSchema } }, + content: { "application/json": { schema: updatePractitionerRequest } }, }, responses: { ...recordRefusal(), diff --git a/src/lib/openapi/routes/health-record.ts b/src/lib/openapi/routes/health-record.ts index e37742ada..995101bb0 100644 --- a/src/lib/openapi/routes/health-record.ts +++ b/src/lib/openapi/routes/health-record.ts @@ -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 = { requestBody: { required: true, content: { - "application/json": { schema: exportSelectionSchema }, + "application/json": { schema: healthRecordExportRequest }, }, }, responses: { @@ -124,7 +150,7 @@ export const healthRecordPaths: NonNullable = { requestBody: { required: true, content: { - "application/json": { schema: createShareLinkSchema }, + "application/json": { schema: createShareLinkRequest }, }, }, responses: { diff --git a/src/lib/openapi/routes/import.ts b/src/lib/openapi/routes/import.ts index d64afc87e..04cc258eb 100644 --- a/src/lib/openapi/routes/import.ts +++ b/src/lib/openapi/routes/import.ts @@ -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({ diff --git a/src/lib/openapi/routes/index.ts b/src/lib/openapi/routes/index.ts index a77ccf325..55454a36f 100644 --- a/src/lib/openapi/routes/index.ts +++ b/src/lib/openapi/routes/index.ts @@ -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 = { ...cyclePaths, diff --git a/src/lib/openapi/routes/insights/schemas.ts b/src/lib/openapi/routes/insights/schemas.ts index 5d7dabf8e..c7d9dd371 100644 --- a/src/lib/openapi/routes/insights/schemas.ts +++ b/src/lib/openapi/routes/insights/schemas.ts @@ -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, diff --git a/src/lib/openapi/routes/measurements.ts b/src/lib/openapi/routes/measurements.ts index 36877fb65..6d9ca690c 100644 --- a/src/lib/openapi/routes/measurements.ts +++ b/src/lib/openapi/routes/measurements.ts @@ -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"; diff --git a/src/lib/openapi/routes/ocr.ts b/src/lib/openapi/routes/ocr.ts index 446dfd64f..3be1612d6 100644 --- a/src/lib/openapi/routes/ocr.ts +++ b/src/lib/openapi/routes/ocr.ts @@ -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 = { '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(), diff --git a/src/lib/openapi/routes/profile.ts b/src/lib/openapi/routes/profile.ts index f0c2739a4..f3e50f971 100644 --- a/src/lib/openapi/routes/profile.ts +++ b/src/lib/openapi/routes/profile.ts @@ -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, diff --git a/src/lib/openapi/routes/shared.ts b/src/lib/openapi/routes/shared.ts index f9aa9471c..4ce94c266 100644 --- a/src/lib/openapi/routes/shared.ts +++ b/src/lib/openapi/routes/shared.ts @@ -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(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).", diff --git a/src/lib/openapi/routes/workouts.ts b/src/lib/openapi/routes/workouts.ts index 99e80aea4..ceec2dcbb 100644 --- a/src/lib/openapi/routes/workouts.ts +++ b/src/lib/openapi/routes/workouts.ts @@ -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,