mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
Register the schema names the contract was discarding
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.
This commit is contained in:
1 parent
965d41f1ed
commit
032ea5b1a0
11 files changed
+1719
-1558
No files matched your search
+1612
-1465
File diff suppressed because it is too large.
Load diff
@@ -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<ZodOpenApiObject["paths"]> = {
|
||||
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<ZodOpenApiObject["paths"]> = {
|
||||
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<ZodOpenApiObject["paths"]> = {
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": { schema: createCustomMetricEntrySchema },
|
||||
"application/json": { schema: createCustomMetricEntryRequest },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
@@ -326,7 +329,7 @@ export const customMetricPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": { schema: updateCustomMetricEntrySchema },
|
||||
"application/json": { schema: updateCustomMetricEntryRequest },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
|
||||
@@ -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<ZodOpenApiObject["paths"]> = {
|
||||
"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<ZodOpenApiObject["paths"]> = {
|
||||
],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: documentUpdateSchema } },
|
||||
content: { "application/json": { schema: documentUpdateRequest } },
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
@@ -1078,7 +1081,7 @@ export const inboundDocumentPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: inboundFactEditSchema } },
|
||||
content: { "application/json": { schema: inboundFactEditRequest } },
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
@@ -1110,7 +1113,7 @@ export const inboundDocumentPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: inboundConfirmSchema } },
|
||||
content: { "application/json": { schema: inboundConfirmRequest } },
|
||||
},
|
||||
responses: {
|
||||
...idempotentWrite(),
|
||||
|
||||
@@ -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<ZodOpenApiObject["paths"]> = {
|
||||
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<ZodOpenApiObject["paths"]> = {
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": { schema: familyHistoryCreateSchema },
|
||||
"application/json": { schema: createFamilyHistoryRequest },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
@@ -158,7 +161,7 @@ export const familyHistoryPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": { schema: familyHistoryUpdateSchema },
|
||||
"application/json": { schema: updateFamilyHistoryRequest },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
|
||||
@@ -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<ZodOpenApiObject["paths"]> = {
|
||||
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<ZodOpenApiObject["paths"]> = {
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": { schema: illnessEpisodeCreateSchema },
|
||||
"application/json": { schema: createIllnessEpisodeRequest },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
@@ -375,7 +364,7 @@ export const illnessPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": { schema: illnessEpisodeUpdateSchema },
|
||||
"application/json": { schema: updateIllnessEpisodeRequest },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
@@ -456,7 +445,7 @@ export const illnessPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
requestBody: {
|
||||
required: false,
|
||||
content: {
|
||||
"application/json": { schema: illnessEpisodeResolveSchema },
|
||||
"application/json": { schema: resolveIllnessEpisodeRequest },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
@@ -514,7 +503,7 @@ export const illnessPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": { schema: illnessDayLogInputSchema },
|
||||
"application/json": { schema: upsertIllnessDayLogRequest },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
@@ -576,7 +565,7 @@ export const illnessPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
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.",
|
||||
|
||||
@@ -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<ZodOpenApiObject["paths"]> = {
|
||||
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<ZodOpenApiObject["paths"]> = {
|
||||
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<ZodOpenApiObject["paths"]> = {
|
||||
requestParams: { path: z.object({ id: z.string() }) },
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: updateLabResultSchema } },
|
||||
content: { "application/json": { schema: updateLabResultRequest } },
|
||||
},
|
||||
responses: {
|
||||
...recordRefusal(),
|
||||
|
||||
@@ -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<ZodOpenApiObject["paths"]> = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
medicationExtractionSchema,
|
||||
medicationExtractionResult,
|
||||
"MedicationExtractResponse",
|
||||
),
|
||||
},
|
||||
@@ -1462,7 +1465,7 @@ export const medicationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
medicationEfficacyResponseSchema,
|
||||
medicationEfficacyResponse,
|
||||
"GetMedicationEfficacyResponse",
|
||||
),
|
||||
},
|
||||
@@ -1488,7 +1491,7 @@ export const medicationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": { schema: efficacyTargetOverrideSchema },
|
||||
"application/json": { schema: setMedicationEfficacyTargetRequest },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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<ZodOpenApiObject["paths"]> = {
|
||||
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<ZodOpenApiObject["paths"]> = {
|
||||
parameters: [idempotencyKeyParameter],
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": { schema: createAssessmentSchema },
|
||||
"application/json": { schema: createMentalHealthAssessmentRequest },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
|
||||
@@ -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<ZodOpenApiObject["paths"]> = {
|
||||
"Mints a `custom:<uuid>` 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<ZodOpenApiObject["paths"]> = {
|
||||
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<ZodOpenApiObject["paths"]> = {
|
||||
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<ZodOpenApiObject["paths"]> = {
|
||||
"Mints a `customcat:<uuid>` 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<ZodOpenApiObject["paths"]> = {
|
||||
requestParams: { path: z.object({ key: z.string() }) },
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: updateCustomGroupSchema } },
|
||||
content: { "application/json": { schema: updateMoodTagGroupRequest } },
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
|
||||
@@ -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<ZodOpenApiObject["paths"]> = {
|
||||
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<ZodOpenApiObject["paths"]> = {
|
||||
"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<ZodOpenApiObject["paths"]> = {
|
||||
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<ZodOpenApiObject["paths"]> = {
|
||||
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<ZodOpenApiObject["paths"]> = {
|
||||
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<ZodOpenApiObject["paths"]> = {
|
||||
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<ZodOpenApiObject["paths"]> = {
|
||||
requestParams: idPath,
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: vaccinationLinkSchema } },
|
||||
content: { "application/json": { schema: vaccinationLinkRequest } },
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
|
||||
Reference in new issue
Block a user