Merge branch 'v143/life' into release/v1.43.0

# Conflicts:
#	docs/api/openapi.yaml
#	src/__tests__/contract-stub-inventory.test.ts
#	tests/integration/sharing-manage-handler-matrix.test.ts
This commit is contained in:
Marc-André Bombeck committed 2026-10-16 03:39:40 +02:00
commit 4daad8f2fb
88 files changed
+8313 -1386

No files matched your search

+1 -1
View File
@@ -60,6 +60,7 @@ export const ERROR_CODE_CATALOGUE: Readonly<Record<string, readonly string[]>> =
analytics: ["analytics.invalid_query"],
anamnesis: [
"anamnesis.auditC.invalid",
"anamnesis.auditC.notFound",
"anamnesis.auditC.rateLimited",
"anamnesis.fact.conflict",
"anamnesis.fact.currentExists",
@@ -471,7 +472,6 @@ export const ERROR_CODE_CATALOGUE: Readonly<Record<string, readonly string[]>> =
"modules_conflict",
"mood_tag_layout_conflict",
"not_configured",
"not_implemented",
"notification_prefs_conflict",
"oidc_only",
"private_origin_not_approved",
+14 -11
View File
@@ -13,12 +13,15 @@ import {
} from "@/lib/validations/about-me";
import {
healthProfileAiSectionSchema,
healthProfileFactCorrectionSchema,
healthProfileFactDtoSchema,
healthProfileFactsResponseSchema,
healthProfileFactWriteSchema,
removedHealthProfileFactSchema,
} from "@/lib/validations/health-profile-facts";
import {
factCorrectionSchema,
factWriteSchema,
lifeFactRevisionSchema,
removedFactSchema,
} from "@/lib/validations/life-circumstances";
import {
emergencyProfileDtoSchema,
emergencyProfileUpdateSchema,
@@ -1526,7 +1529,7 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
tags: ["Insights"],
summary: "Read effective-dated anamnesis facts",
description:
"Returns the current value and immutable revision history for the caller's smoking status, alcohol pattern and shift schedule. Values are decrypted server-side; unreadable ciphertext returns `value: null` with `unreadable: true`.",
"Returns the current value and immutable revision history for the caller's smoking status, alcohol pattern and shift schedule, and only those three kinds: the life-circumstance kinds are read through `GET /api/anamnesis/life-circumstances`. Values are decrypted server-side; unreadable ciphertext returns `value: null` with `unreadable: true`.",
responses: {
...recordRefusal(),
"200": {
@@ -1547,12 +1550,12 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
tags: ["Insights"],
summary: "Create an anamnesis fact",
description:
"Creates the first current revision for one closed fact kind. The answer is encrypted before persistence. A second current value for the same kind returns 409; corrections use the revision endpoint.",
"Creates the first current revision for one fact kind. The answer is encrypted before persistence. The three closed kinds take their string value and answer the closed revision shape; the six life-circumstance kinds (v1.43, send only when `features.lifeCircumstances` is true) take an object with `schema: 1` and answer a `LifeFactRevision`. Optional `validFrom` (a calendar day) dates the answer back. A second current value for the same kind returns 409; corrections use the revision endpoint.",
parameters: [idempotencyKeyParameter],
requestBody: {
required: true,
content: {
"application/json": { schema: healthProfileFactWriteSchema },
"application/json": { schema: factWriteSchema },
},
},
responses: {
@@ -1562,7 +1565,7 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
content: {
"application/json": {
schema: dataEnvelope(
healthProfileFactDtoSchema,
z.union([healthProfileFactDtoSchema, lifeFactRevisionSchema]),
"CreateAnamnesisFactResponse",
),
},
@@ -1578,7 +1581,7 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
tags: ["Insights"],
summary: "Correct a current anamnesis fact",
description:
"Closes the current revision at the correction instant and creates a successor. Prior ciphertext and its half-open validity interval remain in history.",
"Closes the current revision at the correction instant (or at the optional `validFrom` day, never earlier than the revision it replaces) and creates a successor. The value takes the stored kind's form: a string for the closed kinds, an object with `schema: 1` for the life-circumstance kinds; any other is 422 `anamnesis.fact.invalidValue`. Prior ciphertext and its half-open validity interval remain in history.",
parameters: [
{
name: "id",
@@ -1590,7 +1593,7 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
requestBody: {
required: true,
content: {
"application/json": { schema: healthProfileFactCorrectionSchema },
"application/json": { schema: factCorrectionSchema },
},
},
responses: {
@@ -1599,7 +1602,7 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
content: {
"application/json": {
schema: dataEnvelope(
healthProfileFactDtoSchema,
z.union([healthProfileFactDtoSchema, lifeFactRevisionSchema]),
"CorrectAnamnesisFactResponse",
),
},
@@ -1629,7 +1632,7 @@ export const coachPaths: NonNullable<ZodOpenApiObject["paths"]> = {
content: {
"application/json": {
schema: dataEnvelope(
removedHealthProfileFactSchema,
removedFactSchema,
"RemoveAnamnesisFactResponse",
),
},
+7 -28
View File
@@ -14,8 +14,8 @@ import { z } from "zod/v4";
import {
AUDIT_C_CUTOFF_SOURCES,
LIFE_AI_SECTIONS,
LIFE_FACT_KINDS,
LIFE_SECTION_KEYS,
lifeFactRevisionSchema,
auditCRequestSchema,
lifeAiSectionsPatchSchema,
lifeConfirmSchema,
@@ -26,32 +26,14 @@ import {
errorEnvelope,
idempotencyKeyParameter,
idempotentWrite,
notImplementedResponse,
recordRefusal,
stdResponses,
} from "./shared";
const lifeFactRevision = z
.object({
id: z.string(),
kind: z.enum(["SMOKING_STATUS", "SHIFT_SCHEDULE", ...LIFE_FACT_KINDS]),
value: z
.unknown()
.describe(
"`SMOKING_STATUS` and `SHIFT_SCHEDULE` carry their string value; every other kind an object with `schema: 1` whose shape is fixed per kind (see the life-circumstances contract). `null` when `unreadable`.",
),
unreadable: z
.boolean()
.describe("The sealed value could not be opened on this host."),
validFrom: z.iso.datetime({ offset: true }),
validUntil: z.null(),
provenance: z.enum(["USER_REPORTED", "USER_CORRECTION"]),
createdAt: z.iso.datetime({ offset: true }),
})
.meta({
id: "LifeFactRevision",
description: "The current revision of one life-circumstance fact.",
});
const lifeFactRevision = lifeFactRevisionSchema.meta({
id: "LifeFactRevision",
description: "The current revision of one life-circumstance fact.",
});
const auditCCutoff = z.object({
value: z.number().int(),
@@ -210,7 +192,6 @@ export const lifeCircumstancesPaths: NonNullable<ZodOpenApiObject["paths"]> = {
},
},
...stdResponses,
...notImplementedResponse,
},
},
},
@@ -249,7 +230,6 @@ export const lifeCircumstancesPaths: NonNullable<ZodOpenApiObject["paths"]> = {
content: { "application/json": { schema: errorEnvelope } },
},
...stdResponses,
...notImplementedResponse,
},
},
},
@@ -279,7 +259,6 @@ export const lifeCircumstancesPaths: NonNullable<ZodOpenApiObject["paths"]> = {
},
},
...stdResponses,
...notImplementedResponse,
},
},
},
@@ -295,6 +274,7 @@ export const lifeCircumstancesPaths: NonNullable<ZodOpenApiObject["paths"]> = {
content: { "application/json": { schema: auditCRequest } },
},
responses: {
...recordRefusal(),
...idempotentWrite(),
"200": {
description: "A replay of an entry already stored.",
@@ -319,7 +299,6 @@ export const lifeCircumstancesPaths: NonNullable<ZodOpenApiObject["paths"]> = {
},
},
...stdResponses,
...notImplementedResponse,
},
},
},
@@ -330,6 +309,7 @@ export const lifeCircumstancesPaths: NonNullable<ZodOpenApiObject["paths"]> = {
description: "Soft delete. 404 `anamnesis.auditC.notFound`.",
requestParams: { path: z.object({ id: z.string() }) },
responses: {
...recordRefusal(),
"200": {
description: "The removed entry.",
content: {
@@ -349,7 +329,6 @@ export const lifeCircumstancesPaths: NonNullable<ZodOpenApiObject["paths"]> = {
content: { "application/json": { schema: errorEnvelope } },
},
...stdResponses,
...notImplementedResponse,
},
},
},
-13
View File
@@ -557,19 +557,6 @@ export function recordRefusal(...alsoRefusesFor: string[]): RefusalResponse {
export const MODULE_DISABLED_DESCRIPTION =
'Module disabled for this account: the user (or operator) has the module turned off. `meta.errorCode` = `module.disabled` and `meta.module` carries the disabled module key (e.g. "sleep"). Returned even for a valid Bearer token. Clients hide the whole module surface end-to-end rather than retry.';
/**
* The 501 a published operation answers while its behaviour has not landed on
* this server (`src/lib/http/not-implemented.ts`). Spread into an operation's
* responses with the stub, and removed with it.
*/
export const notImplementedResponse = {
"501": {
description:
"Not implemented on this server yet: the contract is published, the effect is not. `meta.errorCode` = `not_implemented`, `meta.operation` names the operation. Authentication, module gates and request validation already answer as documented. A client reaches this only by ignoring the server feature flag that guards the operation.",
content: { "application/json": { schema: errorEnvelope } },
},
};
export const moduleDisabledResponse = {
"403": {
description: MODULE_DISABLED_DESCRIPTION,