Emit `AllergyIntolerance` and `FamilyMemberHistory` resources in the
health-record document bundle. Vendor-blind and non-diagnostic, matching the
illness `Condition` stance: the user's own label rides `code.text` (never a
machine-guessed clinical code); only the FHIR-fixed value sets — allergy
category, clinical/verification status, the v3 RoleCode family-member concept —
carry codings. Every resource is marked patient-reported (allergy
`verificationStatus: unconfirmed` plus an explicit "not a clinical diagnosis"
note) so a clinician's viewer never reads it as an adjudicated finding.
The records are loaded owner-scoped in the export route and folded into the
fhir + package bundle formats with their own Composition sections.
Add the clinical-signals wave's new measurement types and wire them through
the signal registry so FHIR, correlation eligibility, ingestion, and the
measurement-list surfaces derive from one definition each:
- MeasurementType: PHQ9_SCORE, GAD7_SCORE, GRIP_STRENGTH, PAIN_NRS,
WAIST_CIRCUMFERENCE, WAIST_TO_HEIGHT (+ AssessmentInstrument enum and the
MentalHealthAssessment model that backs the screener scores).
- Registry: the six measurement/score signals plus the longevity lab panel as
kind:"biomarker" entries; the FHIR golden fixture is regenerated in lockstep.
Grip-strength and waist-to-height carry a null LOINC (NEEDS-VERIFY) and emit a
local text concept until a stable code is confirmed.
- FHIR: add the survey observation category for the screener / pain signals.
- Labs: extend the biomarker catalogue with ApoB, Lp(a), fasting insulin, and
the omega-3 index; add LOINC codes for those plus GGT and hs-CRP; add a
computed HOMA-IR helper.
- Exhaustive maps updated to keep the enum coverage, category, list-meta, and
personal-record drift guards green.
Replace the hand-maintained MEASUREMENT_LOINC literal and the BUCKETED_TYPES
correlation list with signal-registry projections. Both reads are byte-identical
to the previous tables (guarded by the registry-invariant test), so there is no
behaviour change — only one source of truth for each.
Add first-class qualitative lab findings (positive / negative / borderline
serology) alongside the existing numeric path. A LabResult is now EITHER a
numeric reading (value + unit, optional reference bounds) OR a qualitative one
(valueText, e.g. "negativ"), never both, never neither.
- schema: make `value` nullable, add `value_text`; migration 0181 is additive
(DROP NOT NULL + ADD COLUMN), every existing numeric row is untouched.
- validation: numeric-XOR-qualitative refine on create + update; unit/bounds
apply to the numeric path only; valueText capped at 120 chars. The single
resource PUT rejects a cross-type edit.
- serialise: carry valueText through both DTOs; a qualitative row reports the
neutral `unknown` range verdict (no number to classify).
- FHIR: a qualitative result emits valueCodeableConcept — SNOMED qualifier
value for confidently-known terms (Negative 260385009, Positive 10828004,
Not detected 260415000, Detected 260373001), text-only for borderline /
grenzwertig and any unrecognised term. The raw text always rides .text.
- UI: a numeric/qualitative toggle in the lab form; list, history, detail,
chart, sparkline, and the doctor-report PDF all render qualitative readings
as their text and never plot or divide on a null value.
- i18n: new labs.form keys across all six locales.
- openapi: response carries a nullable value + valueText; regenerated.
Close the three FHIR R4 coding gaps in the health-record export. Both are
export-layer only — no schema column, no migration, no contract change.
Illness Conditions now carry a broad SNOMED CT category concept per
IllnessType (infectious disease, allergy, injury, mental disorder, autoimmune
disease, chronic disease, with the generic Disease root as the fallback for an
unknown type) instead of the single generic placeholder. The user's own label
stays on code.text, the class on category.text, and the unconfirmed +
patient-reported guard rails are unchanged — the export still asserts no
specific diagnosis.
Lab Observations resolve a curated analyte to a real LOINC coding alongside the
user's code.text for the common biomarkers (HbA1c, lipid panel, ferritin, TSH,
vitamin D, creatinine, eGFR, ALT, AST, CRP, fasting glucose, hemoglobin), with
analyte-key and EN/DE alias normalisation. The canonical UCUM code is stamped
on the value only when the recorded unit matches the mapped canonical symbol;
an unmapped analyte or a non-matching unit keeps the honest text-only code and
display-only unit, never a fabricated code. The resolver takes an optional
canonical-name argument so a future Biomarker-catalog link can take precedence
over the free-text analyte without a further change here.
One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
An active illness/condition episode annotates, it never penalises: the
measured number is untouched, only the narrative changes.
- rest-mode.ts: one server-authoritative resolver (module-gated, fail-soft)
for "is the account in Rest Mode + which episodes back it". iOS mirrors the
resolved context, never recomputes the suppression.
- health-score: /api/analytics attaches a value-free Rest Mode annotation;
computeHealthScore stays pure (score never adjusted).
- recovery-score: computeRecoveryScore carries the Rest Mode context as of the
scored day; the persisted blend is unchanged.
- coach-nudge: pause cadence-nudges during an active episode (new
skippedDuringIllness counter, gate sits before the trigger search).
- achievements: bridgeFrozenStreakGaps freezes (does not break) every
day-streak across illness days; never invents a streak, a non-illness gap
still breaks.
- coincident-deviation: reframe a fired flag as illness-explained; vital
numbers untouched. Surfaced as a neutral line on the insights card.
- coach snapshot: illness history + active illness as load-bearing context so
the coach knows about Rest Mode (no decrypted note reaches the prompt).
- FHIR: illness episodes map to Condition (generic disease root, label on
code.text, patient-reported/unconfirmed) + a bounding Encounter.
- doctor-report: module-gated illness section in the aggregator + PDF table
(retrospective, neutral palette); FHIR Condition section in the bundle.
i18n x6 for the report + reframe copy. Tests: score-annotation-not-
penalisation, streak-freeze, coach-nudge pause, FHIR Condition/Encounter
mapping, doctor-report inclusion, Rest Mode resolver. typecheck/lint/test
green, openapi in sync, knip clean.
Surface actual workout sessions and a per-analyte lab trend in the doctor
report PDF + FHIR bundle (gated on their modules), carry the user's stored
conditions/allergies through to the report and FHIR Condition/AllergyIntolerance,
and round medication adherence to the app's integer convention so the PDF, FHIR
and in-app numbers agree.
The server already computes the clinical glucose panel (TIR / GMI / estimated
A1C / mean / CV%) by the one literature-locked engine the insights panel and
coach consume, but nothing surfaced it in the export. Render it as a dedicated
PDF table after the vitals section and emit it as FHIR Observations
(LOINC-coded where a term exists; CV% under a survey text-only concept). Both
gate on the panel carrying readings, which the aggregator already zeroes when
the glucose module is off — so a disabled module emits no glucose panel. The
spot-reading caveat rides along when the readings are sparse. Adds clinical
glucose-panel keys across all locales and PDF + FHIR tests.
Add a Lab results section to the report aggregator (latest reading per
analyte over the window), a new labs section toggle (ON by default, the
same stance as BP and weight since the user recorded these to share with
a clinician), a PDF table with a quiet non-colour in/out-of-range marker,
and one laboratory Observation per result in the FHIR bundle carrying the
lab's reported referenceRange. Everything stays server-authoritative.
Promote the reproductive HealthKit category types out of the
Apple-Health-import privacy-hold list and route them into cycle day-logs
instead of measurements, and surface cycle data on the three export
surfaces.
Import promotion:
- A new HealthKit -> cycle mapping module is the single source of truth for
how MenstrualFlow, IntermenstrualBleeding, CervicalMucusQuality,
OvulationTestResult, SexualActivity, PregnancyTestResult,
ProgesteroneTestResult, Contraceptive and the symptom category types fold
into CycleDayLog fields / seeded symptom keys. The XML importer
accumulates same-day reproductive samples and writes one canonical
day-log per day with source APPLE_HEALTH and a synthetic per-day
externalId for idempotent re-import. The fold is gated on cycle tracking
being enabled, so a non-cycle export never provisions cycle rows. A
contraceptive method nudges the profile goal toward avoiding pregnancy
only when the goal is still on its default. Basal body temperature stays
a measurement and mirrors onto the day-log. Pregnancy/Lactation status
and the pregnancy-bleeding types stay deferred; the cycle awareness types
remain server-derived.
Export:
- FHIR emits LMP (LOINC 8665-2), average cycle length (64700-8), period
length (64698-4) and the current phase as opt-in reproductive-health
observations grouped into their own document section.
- The doctor PDF gains an opt-in cycle section (recent cycles, average
length and variability, current phase), privacy-default off like mood.
- The full-backup export, the weekly backup worker and the off-host DR
snapshot include the cycle tables, and the admin restore re-creates them;
the day-log note round-trips as ciphertext and is never decrypted onto a
report surface.
Mapping table documented in docs/api/cycle-healthkit-mapping.md. Adds unit
+ integration coverage for the mapping, the import routing, the FHIR
observations and the backup round-trip. OpenAPI regenerated. Six-locale
strings added.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a single canonical FHIR_REST_RESOURCE_TYPES catalogue (plus the
$everything operation and honoured search params) to fhir/rest.ts; the
metadata CapabilityStatement and the share-link resource-type enum now
derive from it so the advertised set can never drift from what is routed.
Extend GET /api/meta/capabilities with the FHIR REST descriptor
(restBaseUrl, readScope, resourceTypes, operations, searchParams) and a
clinician-share descriptor (supported, maxDays, resourceTypes, scopeable
sections) — every list sourced from its canonical registry, guarded by
the existing drift assertions. Grow the OpenAPI CapabilitiesResponse to
match.
Fill the clinician test-matrix gap: assert the clinician view never
requests a KVNR / identifier opt-in from the aggregator (default-OFF by
construction) and resolves a rolling window without reaching data older
than the frozen start.
GET /api/fhir/{metadata,Patient,Observation,MedicationStatement,
MedicationAdministration} and $everything, returning type:"searchset"
Bundles (total + self/next links, offset paging via _count clamped to 200)
as application/fhir+json, with an OperationOutcome on error. The per-resource
emitters are reused from src/lib/fhir/resources.ts so the document export and
the REST face share one coding source and can never drift. Auth is a narrow
fhir:read scope via requireAuth (cookie sessions also pass); userId is
narrowed from auth. Read-only — no write handlers. Rate-limited per user.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lift the Observation / MedicationStatement / MedicationAdministration /
Patient / Coverage builders — with their LOINC/ATC/SNOMED/UCUM codings and
the survey-category wellness split — out of build-bundle.ts into a shared
src/lib/fhir/resources.ts. buildFhirDocumentBundle now composes the emitters
so the upcoming FHIR REST search routes reuse the exact same coding logic
instead of a second copy of the mapping.
Pure refactor: the document-bundle output stays byte-identical (the existing
FHIR + export tests pass unchanged). The shared coding constants and option
types re-export from build-bundle so existing import paths keep working.
Expose the server's live id vocabularies (derived-metric ids,
vitals-baseline types, insights layout tile-ids, metric-status ids,
the HealthKit ingest mapping, computed-score types, write allowlist) and
the FHIR coding constants, plus the running contract version, behind
requireAuth (cookie or Bearer, not admin).
Every list is sourced from its canonical server constant / registry, so
the endpoint cannot drift from what the routes actually accept and emit —
retiring the doc-vs-server enum-drift class for the native client. Export
the FHIR ATC/SNOMED URIs and a shared german-ATC default-locale set so the
export route and the discovery surface share one source.
Register the route in the OpenAPI table; drift-guard tests pin
derivedMetricIds to the registry and writeAllowlist to the constant.
Surface the server-derived nightly scores (recovery / stress / strain) in the
health-record export without polluting the clinical layer. The aggregator now
emits a wellnessScores summary (latest / avg / min / max / count per score over
the window) built from the already-fetched COMPUTED rows. The PDF renders them
in a clearly-labelled "Wellness summary" section, separate from the clinical
vitals table, with a "descriptive, not a clinical assessment" disclaimer. The
FHIR bundle emits one Observation per score under the survey category with a
text-only concept and the same disclaimer as an Annotation note, so a viewer
never mistakes a band for a diagnosis.
Emit a SNOMED CT coding alongside the existing .text anchor on the
administration dosage. Route maps ORAL to 26643006 (Oral route) and
INJECTION to 34206005 (Subcutaneous route, the form HealthLog injections
take). Site keys on the eight InjectionSite enum values, collapsing to the
three gross body-region concepts (abdomen 818983003, thigh 68367000, upper
arm 40983000); laterality and the abdominal quadrant stay verbatim in the
.text anchor rather than inventing lateralised concepts.
Referencing concept ids in a FHIR instance is not redistributing the
licensed SNOMED distribution, so this is safe to ship. The .text anchor is
unchanged, so a consumer without a SNOMED binding is unaffected.
Reconcile the dose fixture to the real uppercase enum value the live
builder emits (the previous lowercased value never matched production).
Wire seven previously-deferred HealthKit quantity identifiers through
the full pipeline — enum, mapping, plausibility range, category, list-UI
metadata, FHIR coding, and the generic metric-status assessment:
CARDIO_RECOVERY heartRateRecoveryOneMinute bpm
WRIST_TEMPERATURE appleSleepingWristTemperature celsius
FALL_COUNT numberOfTimesFallen count
SIX_MINUTE_WALK_DISTANCE sixMinuteWalkTestDistance m
STAIR_ASCENT_SPEED stairAscentSpeed m/s
STAIR_DESCENT_SPEED stairDescentSpeed m/s
BREATHING_DISTURBANCES appleSleepingBreathingDisturbances count
Each value rides raw on the wire (identity conversion). Fall count is
cumulative (daily SUM); the stair speeds drain to a daily mean; the rest
store as latest per-workout / per-night spot readings. Wrist temperature
and breathing disturbances are flagged privacy-sensitive. The category
sleep-apnea and walking-steadiness EVENT identifiers stay deferred — they
are events, not quantities.
Each new metric gains an Insights sub-page on the shared scaffold with the
generic assessment card, localised across all six locales. Cardio fitness
(VO2 max) — ingested since v1.4.23 but assessment-less — gains its own
/insights/cardio-fitness page so it picks up the assessment instead of
riding as a bare chart-row on the pulse page.
Normal-range anchors are cited where defensible (six-minute walk 400-700 m,
ATS guideline + population reference); the rest defer to the user's own
baseline rather than fabricate a band.
Migration 0104 appends the enum values additively (IF NOT EXISTS, forward-only).
The administration set was windowed only to the report period (up to 730
days). A chronic medication dosed several times a day across that window,
multiplied across several medications, can still materialise thousands of
MedicationAdministration resources into a single Bundle — bounded by the
window but unbounded by dose frequency.
Cap the set at the most-recent 1000 acted intakes overall. The omitted
rows are the oldest in the window, so recent adherence is preserved
intact. When the cap trims rows the aggregator records the original and
included counts and the FHIR document narrative discloses the
truncation, so the export is honest about the omitted history rather
than silently dropping it. Cover the cap in the report aggregator and
the narrative note in the bundle builder.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit c6ce7df2bf6efb5e2c9d948d8f189d95d1289758)
Align the server's `Coverage` emission with the iOS exporter: a
Coverage is now emitted whenever ANY payor signal is present, including
a bare KVNR with no insurer name or IKNR. The KVNR-only case carries the
`subscriberId` with no contained payor Organization; the insurer-known
case is unchanged. A bundle with no payor signal at all still omits
Coverage entirely.
Also add a top-level `Composition.text` narrative (reusing the same
escaped summary the section narrative carries) so a strict
US-Core-style validator sees the document-level narrative it expects.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 22c5ed7fbe02993307c3f04cd0498d199c79cf43)
Emit one `MedicationAdministration` per intake the user actually
actioned — `completed` for a taken dose (`effectiveDateTime = takenAt`),
`not-done` for an explicit skip (`effectiveDateTime = scheduledFor`).
Pending / missed slots and soft-deleted tombstones are never exported,
so the record only asserts administrations that happened.
Each administration reuses the statement's ATC/RxNorm + text concept,
so it is self-describing without a cross-resource reference. A `dosage`
is emitted only when a structured dose Quantity is available (resolved
from the dose-change history in effect at the administration instant) —
a dosage carrying only `.text` would violate the R4 dose-or-rate
invariant. Route + injection site ride along as text-only concepts. The
administrations are referenced from the existing Medications section, so
the document's two-section graph is preserved.
`FhirMedicationAdministration` is new; `FhirDosage` gains `dose`,
`route`, and `site`. The source rows come from the same aggregator the
PDF consumes, keeping FHIR and PDF aligned by construction.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit dd22512f56b7b6dfa42386bbc88d1ced1d71255d)
The health-record export's `MedicationStatement.medicationCodeableConcept`
gains additive `coding[]` entries when the medication carries a stored
code: ATC primary (`http://www.whocc.no/atc`), RxNorm secondary
(`http://www.nlm.nih.gov/research/umls/rxnorm`). The free-text name
stays the `.text` anchor and the concept collapses to exactly the
prior text-only shape when no code is present — the export never
machine-guesses a code, only emits what the user stored.
The aggregator now carries `atcCode` / `rxNormCode` on `medications[]`
and assembles the acted-intake source rows
(`medicationAdministrations`) the administration builder consumes, so
the FHIR export keeps describing the same numbers as the PDF by
construction.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 740c86b3a830efa7e76d69b29ae376addeee6811)
The health-record FHIR bundle now carries a Coverage resource right
after the Patient. Coverage.status is active, beneficiary references
the Patient, and subscriberId carries the KVNR when present. The payor
is a contained Organization referenced by a local #-ref: its name is
the insurer name and its identifier carries the IKNR under the
http://fhir.de/sid/arge-ik/iknr system.
When the IKNR is absent the Organization keeps its name only; when
neither insurer name nor IKNR is known the Coverage is omitted rather
than emitting an empty payor. The KVNR continues to ride on
Patient.identifier unchanged — Coverage is purely additive.
Six HealthKit metrics without a published LOINC term were coded under
http://loinc.org, which fails FHIR R4 conformance (a non-LOINC code must
not sit in the LOINC namespace). Route any HKQuantityTypeIdentifier code
onto a shared custom CodeSystem (https://healthlog.dev/fhir/CodeSystem/healthkit),
keeping the raw HealthKit identifier as the code so the export stays
byte-aligned with the native client's exporter.