Skip to main content
本页暂无完整中文版。以下先提供中文导读,随后是英文原文。
中文导读: 本页说明用药数据的存储结构、状态与查询工具。读者可先确认所需的是用药记录查询还是通用健康指标查询,再在下方英文原文中查相应字段。
Status: provisional. The vocabulary, the arithmetic and the reference storage are here and pinned by 74 golden tests. The reference application is the first consumer; a second production consumer will find things to move, and the shapes below may change in a minor release until one exists. Nothing derived is stored, so a change here is a change to code, not a migration.
A medication is not a reading, and every design decision on this page follows from that. A reading has a value, a unit and an instant. A plan has a schedule (a tuple of dosing instructions), a lifecycle (active → stopped → resumed as a NEW course) and a code system of its own. Storing “takes metformin 500 mg twice a day” as a reading forces a choice between losing the schedule and inventing a value, and makes every aggregation query step over rows that are not measurements.

The model

concept_key is what gets indexed, joined and logged

A coded concept and a text-only one never compare equal, even for the same drug. Auto-merging them is how a wrong code silently rewrites a person’s medication list; two spellings make two plans, the person sees both and deletes one, which is the safer failure. The key is safe to log. The NAME is not, and is kept out of every repr.

normalize_name is deterministic and convergent only

NFKC folding (so full-width 500mg and ㎎ become ASCII), case folding, whitespace dropped. No alias or brand→generic mapping: that is an open-ended asset behind the Terminology port, and this framework ships none.

Dose units and forms

A dose is an amount in a UCUM unit. Countable forms are UCUM annotations, and res/dose_forms.tsv is the whole table — sixteen forms, with the aliases each one accepts: Two notes on the table:
  • {pill} and {tablet} are different annotations. They are not merged, because a “pill” in a person’s words may be a capsule and the framework does not decide which. dose_unit_family puts both in count, so they compare as amounts without being claimed to be the same object.
  • {dose} and {application} carry no amount at all. They are accepted so a schedule can be recorded, and they never enter arithmetic — a “dose” is not a quantity a total can be taken of.

The instruction grammar

parse_dose_instruction(text) -> Schedule | None. Small and closed: one dose, one frequency, optional clock times. No partial parses. Half a regimen presented as a whole one is worse than no regimen: the caller keeps the original text either way, and a None is visible. FHIR’s Dosage.text goes through the same grammar when the structured fields are empty, which R4B expects and pharmacy systems commonly send.

The state tables

A plan’s stored status vs its effective status

Stored: active, stopped, entered_in_error. Everything else is derived by effective_status(plan, today) — with the subject’s local date, never date.today() on a server. entered_in_error is FHIR’s “recorded in error”: terminal and retroactive. A consumer whose “cancel” is reversible maps it to stopped with its own flag.

A dose’s state is derived from the clock

Stored: taken, skipped. That is the entire set. The deadline is the end of the slot’s local day in the slot’s own zone — the zone the person was in when the dose was due — or grace minutes after the instant. Why nothing derived is stored: a stored missed is a lie the moment the person opens the app and marks the dose taken.

Importing a FHIR MedicationStatement

The plan id is never the resource’s own id. A FHIR resource id is unique within the server that issued it; a plan id is unique across every person a consumer stores. Two people importing documents from the same clinic would collide and one medication list would overwrite another. plan_id_for(subject, record, concept_key) keeps the idempotency that made the raw id tempting and scopes it to the subject.

Dose identity is not an instant

DoseSlot.key is (plan_id, local_date, slot). utc_ms is DERIVED at projection time. A person flies from Shanghai to London. Their 08:00 dose is still their 08:00 dose; only the instant changed. Keying on the instant would make the same dose two different doses, and a re-projection after a zone change would churn every row. Daylight saving. A slot at 02:30 on a spring-forward day has no instant. gap="shift_forward" moves it to the first instant that exists; gap="skip" leaves utc_ms=None and slot_state answers unschedulable. Neither is a default — the caller says which, because a blood-pressure tablet and a contraceptive want different answers.

Adherence

over elapsed slots only, to one decimal.
  • None when nothing has elapsed — an as-needed plan, or a window that has not started. Not 0, which reads as “took nothing”.
  • extra counts taken doses with no slot; extra_skipped counts skips with no slot, which answer nothing and are reported separately.
  • unschedulable slots are excluded from every other count.
  • Roll-ups across plans add counts; they never average percentages.

The tool: query_medications

Medications have their own tool, with five parameters — view, keywords, start, end, member — because their grammar shares nothing with a reading’s resolution and aggregate (see answers.md).
The rows are pure functions in kernel.meds — plan_rows, log_rows, history_rows over the two store ports — so a consumer with its own storage renders the same table; agent/tools/medications_service.py is the thin binding. plan carries the note “a plan is what the person intends to take; it is not a record of doses taken”, because a model that is not told this reports a medication list as evidence of what was swallowed. And a dose missing from log is not evidence it was not taken.

Storage (the reference application)

schema/31_medications.sql: th_medication_plan, th_medication_course, th_dose_event, th_override. The schedule column carries structure only — times, counts, weekdays, dose amounts. The person’s own words live in the encrypted columns. collect/meds/store.py implements MedicationStore and DoseLogStore and asks mirobody.kernel.meds every question that has an answer there, rather than re-deriving one in SQL.

Golden tests

The local suite holds 77 cases across four groups: G the grammar, A adherence, S schedule projection and DST, K identity and FHIR round-trips, plus the Apple import path against a synthetic sample that ships with the package.