Layman manual
Current as at 2026-10-22, after steps 1–7. Rule 25.
This replaces a stub which said "There is no user-facing surface yet" and
"No diagnosis can be entered, read, or reported." Both were true at step 1.a
and false from step 5 onward.
Three readers: the clinician who records a diagnosis, the coder who
resolves an ICD-10-SE mapping, and the operator who runs the service.
Synthetic data only in this phase. No real patient data, not even
de-identified. The interaction is what is being measured, not the patient.
The service answers one question the current record cannot: what did we think
was wrong with this patient, and when did we think it.
/ ("Registrera diagnos")Four steps on one page.
1 · Hitta koden. Type the clinical text you would write anyway, in Swedish.
The service asks WHO, which answers with a stem code — and often with an
unspecified one. The match text and score are shown for that reason: a residual
answer is correct behaviour, and you have to see that it happened.
2 · Kontrollera koden. Paste or type a code instead if you prefer. Nothing
is stored on an unvalidated code.
3 · Precisera (postkoordinering). This is where ICD-11 earns its keep. A
stem code says what; the axes say how — severity, side, cause,
manifestation. Only the axes WHO permits for your code appear, and only the
values each one accepts:
| you see | it means |
|---|---|
| a dropdown, grouped | the axis has a listable set of values — 1 to 571 of them |
| a search box over named chapters | the axis accepts anything in those chapters, so there is nothing to list. Orsakande tillstånd is 14 whole chapters |
| obligatorisk | ICD-11 marks this axis mandatory for this code ("code also"). Leave it empty and step 2 refuses |
| ett värde / flera värden | whether a second pick is added or replaces the first |
| restkategori on a value | "annan specificerad" / "ospecificerad" — allowed, and counted in the coding-depth follow-up |
Every pick rewrites the code in step 2 and re-checks it against WHO. Remove one
with the × on its chip.
Why there is a picker here at all. An earlier version of this manual sent
you to WHO's own Coding Tool for anything beyond a stem code, on the argument
that building a second one would mean reimplementing rules this project had
already got wrong twice. The picker avoids that by not holding any of those
rules: which axes exist, which are mandatory, how many values each takes and
whether a value belongs are all read from WHO's own postcoordinationScale and
judged by WHO's own validation. What the page contributes is the two things WHO
does not do — it refuses a value WHO would silently discard, and it assembles
the parts in the one order WHO accepts. /ct is still there if you would rather
work in WHO's tool and paste the result.
4 · Registrera. Then:
| field | note |
|---|---|
| Patient-GUID | typed today. ips is not yet wired (#858) |
| Patientens organisation (ips) | the patient's organisation, not yours. Every access filter reads this, and it cannot be corrected later — ips keeps no assignment history |
| Debut | mandatory. A diagnosis without an onset cannot answer a question about time |
| Klinisk status / Verifiering | active / confirmed by default |
| Vårdkontakt + Roll + Rank | optional. Rank 1 with role CC is the primary diagnosis, and the database refuses a second one for the same encounter |
Nothing is stored unless WHO validates the code. If WHO cannot be reached
you get a 503 and nothing is written — "could not validate" is never recorded
as "valid".
Nothing is edited and nothing is deleted. A wrong diagnosis is corrected by
recording a new assertion — typically entered-in-error — and the original
stays visible. This is the point of the service rather than a limitation: the
record has to keep showing what was believed at the time, or it cannot answer a
question about the past.
/patient?patient=<guid>The problem list, plus the control that carries the whole argument: as-of.
Set a date and you see the list as it stood then, rebuilt from the assertion
log — not the current list with old rows hidden.
That is what makes "the same question gives the same answer" something you
click rather than something you are told.
ICD-10-SE codes are accepted and recorded as ICD-10-SE, with the ICD-11
binding declared separately. A code is never silently converted:
| state | meaning |
|---|---|
auto |
the map gave exactly one ICD-11 answer |
pending |
several answers — a coder chooses |
resolved |
a coder chose |
unmappable |
the map answered and ICD-11 has no equivalent |
no_map |
we could not ask. A statement about us, not about the code |
The distinction between unmappable and no_map is deliberate: one is a fact
about ICD-11, the other is a missing data source, and they are fixed by
different people.
GET /api/v1/mapping-decisionsEvery ICD-10-SE code waiting on a decision.
GET /api/v1/reports/release-drift — which stored codes are behind a
release, gone from the current one, or unbound (no ICD-11 expression at
all). Re-expressing one is a new assertion, never an edit.
GET /api/v1/reports/mapping-rebind — which no_map decisions the current
map would now answer. It changes nothing; it tells you where a new assertion
would now succeed.
auto_safe, not actionableThis is the one thing in the manual that could cost you something.
A unique map answer is not a faithful one. Measured on the pilot data:
E11.2 Diabetes mellitus typ 2 - MED njurkomplikationer -> 5A11
E11.9 Diabetes mellitus typ 2 - UTAN komplikationer -> 5A11
Both resolve uniquely to the same ICD-11 code. Apply both and two clinically
different statements merge, and the kidney complication is gone. WHO marks both
as modifiedcategory: in ICD-11 that modifier lives outside the stem and needs
postcoordination, so a one-to-one binding cannot carry it.
So the report gives you two numbers. actionable is how many a new
assertion could bind. auto_safe is how many no other source code also
claims. Rows that collide name each other in collides_with. Those are yours
to decide, not the map's. See #860.
cd ~/T7_sidewinder/icd11.pdhc
cp .env.example .env # then fill it in
chmod 600 .env # miserver has three local accounts
./start.sh
.env must be filled first. DB_PASSWORD has no default: compose is
written ${DB_PASSWORD:?DB_PASSWORD must be set} and refuses to start without
it. Deliberate — a ${KEY:-value} default is a working credential whenever the
key is missing, and termbank ran on the literal changeme for seven weeks that
way.
AUTH_MODE=off is local dev only. The factory refuses to start with
anything else under FLASK_ENV=production.
There is no flask create-su and none is needed: this service has no user
table, and admin comes from the SSO blob's is_su_admin. An SSO SU admin has
admin here as soon as the _ICD11 pair is registered in sso.pdhc.
curl -s http://127.0.0.1:9150/healthz
"status":"ok", "database":"connected", HTTP 200. Two degraded states, and
they mean different things:
"database":"unavailable" |
the app is up, the database is not |
"icd11_source": {"configured": true, "reachable": false} |
WHO is down. No diagnosis can be recorded, because validation is delegated |
"icd11_source": {"configured": false} |
WHO was never wired up. Terminal — retrying will not help |
The last two are separate fields on purpose: "nobody configured this" and "it
blipped" need different responses, and reporting the first as the second tells a
caller to retry forever.
flask projection-status --target <cdr> # what is queued for cdr
flask project-to-cdr --target <cdr> # drain it
flask register-used-concepts # step 2.f, before a termbank import
The projection queue is drained on demand. Recording never waits for cdr —
a diagnosis is stored whether or not the projection succeeds, and the queue
retries.
curl -s http://127.0.0.1:9150/api/v1/release-currency
Separate from /healthz on purpose: being behind a release is an operational
decision, not a health problem, and /healthz must answer without leaving the
machine. "checked": false means the cloud credentials are not set — the
service runs perfectly without them.
Don't, casually. The image is digest-pinned and every response is checked
against ICD11_RELEASE; a mismatch is a loud error, not a silent upgrade. A
release must never move under stored diagnoses (Rule 28). Changing it is a
deliberate operation with a drift report afterwards.
docker-compose stop
Never colima stop, colima delete or colima restart — every other PDHC
service's containers live in the same VM.
patient_guid and the patient's organisation are typedNA00/8A84.1&XS5B, severity on the second stem — and thatno_map, which is correct, not an error.