Brainversum · tuxametrics Graph Admin

adr vertraulich owner: matus

ADR-0012: Substanzstammdaten als eigener Service — Wirkstoff ist die Entität, Handelsname die Referenz

ADR-0012: Substanzstammdaten als eigener Service — Wirkstoff ist die Entität, Handelsname die Referenz

Status

Angenommen (2026-08-26)

Kontext

Leitplanke 4 in CLAUDE.md lautet seit dem ersten Tag dieses Repos:

Medikamenten-Interaktionen werden erfasst und angezeigt, aber NICHT regelbasiert geprüft. Medikationseintrag.interaktionspruefung ist ein Enum mit genau einem Wert (nicht-automatisiert-geprueft) … Grund: die Auflösung Handelsname → Wirkstoff hat im Material keine Quelle (OF-07).

gold-path-scope.md führt denselben Punkt unter „Ausdrücklich NICHT im Schnitt", und service-landscape.md nennt die Arzneimitteldatenbank unter „Externe Abhängigkeiten" mit dem Status „Existiert nicht (OF-07). Deshalb keine Medikamenten-Interaktionsprüfung."

Diese drei Stellen sagen zusammen zwei Dinge, die man leicht für eines hält:

  1. Es gibt keine Auflösung Handelsname → Wirkstoff. — eine Aussage über eine fehlende Fähigkeit.
  2. Es gibt keine Interaktionsprüfung. — eine Aussage über eine nicht gebaute Funktion.

OF-07 begründet (1), und (1) war bisher der Grund für (2). Dieses ADR ändert nur (1).

Der Product Owner hat am 2026-08-26 entschieden, einen Substanzstammdaten-Katalog anzulegen, und die Rückfrage nach dem Umfang ausdrücklich in Richtung Wirkstoff-Zuordnung beantwortet — nicht bloß eine Namensliste. Motivation ist die Erfassungspraxis aus dem Hi-Fi-Prototyp: Medikamente und Supplemente werden in jedem Beratungsgespräch erfasst, heute als reiner Freitext (Medikationseintrag.eingabetext, Supplementeintrag.produkt). Ein Katalog macht daraus eine nachschlagbare Referenz und ist zugleich die Anschlussstelle für eine spätere offizielle externe Datenbank.

Entscheidung

1. Eigener Service, nicht eine neunte Domäne im Laborauswertungs-Service

tuxametrics-substanzstammdaten-service (Modulkürzel sub), Port 8128, eigene Postgres-DB auf 5528, REST /api/substanz/v1, MCP nur lesend. Aufgenommen in Root-pom.xml, docker-compose.yml, nginx-locations.conf und vite.config.ts.

service-landscape.md begründet den bestehenden Zuschnitt „ein Service, acht Domänen" damit, dass der Gold Path in einem Durchlauf durch sechs Domänen führt und acht Services daraus sieben Netzwerkgrenzen gemacht hätten. Genau dieses Argument trägt hier nicht:

Das ist die „erste echte Trennung von Datenhoheit", die service-landscape.md als den richtigen Zeitpunkt für einen eigenen Service benennt.

2. Der Wirkstoff ist die Entität, der Handelsname hängt daran

Ausdrückliche Vorgabe des Product Owners: „Wirkstoff ist wichtiger als Medikament!" Umgesetzt als zwei Tabellen mit einer Richtung:

Entität Rolle
Wirkstoff (sub_wirkstoff) Die Stammdaten-Entität. Name, optionale Kategorie, Quelle. Steht für sich, auch ohne ein einziges Produkt
Substanzprodukt (sub_produkt) Handelsname/Produktname mit typ (MEDIKAMENT | SUPPLEMENT), Quelle — und genau einem wirkstoffKey

Ein Produkt ohne Wirkstoff kann nicht angelegt werden (422). Ein Wirkstoff ohne Produkt ist der Normalfall. Die Suche greift auf beiden Ebenen (?name= als Namensfragment), aber die Produkt-Antwort trägt den aufgelösten Wirkstoffnamen mit — die Auflösung ist das Produkt dieses Service, nicht ein Nebenprodukt.

Genau ein Wirkstoff, nicht n. Kombinationspräparate (zwei und mehr Wirkstoffe in einem Produkt) gibt es real und sie sind hier bewusst nicht abgebildet. Ein 1:n-Modell hätte sofort die Frage nach Mengenanteilen je Wirkstoff aufgeworfen, und dafür gibt es — wie für alles andere in diesem Bereich — keine belegte Quelle. Der Katalog führt deshalb ausschließlich Monopräparate; ein Kombinationspräparat wird nicht falsch, sondern gar nicht erfasst.

3. Nicht mandantengebunden — aber die API ist es trotzdem

Die Daten gehören keiner Praxis. Gespeichert werden sie deshalb unter dem Sentinel-Mandanten platform (util/SubstanzScope, wortgleiche Erwägung wie PlatformScope in der Authority): die Hard Rule „jede Entity trägt tenantKey" (GG-ARCH-BEJAVA-0001, maschinell durch TenantKeyOnEntitiesRule) gilt unverändert, und ein Sentinel ist die ehrlichere Antwort als eine Baseline-Ausnahme — er sagt „plattformweit", statt „vergessen" zu bedeuten.

Die API bleibt dennoch tenant-skopiert (/api/substanz/v1/tenants/{tenantKey}/wirkstoffe), und das ist eine Sicherheits-, keine Datenmodellentscheidung. zwirn.security.tenant-path-pattern greift nur auf Pfaden mit /tenants/{…}; ein Pfad ohne dieses Segment überspringt die Prüfung Pfad-Tenant gegen Token-Claim vollständig (UserContextResolver: pathTenantKey == null ⇒ kein tenant_mismatch möglich). Genau dieser Effekt war 2026-08-15 der Grund, Flowables rohe /process-api/**-Fassade zu entfernen. Ein neuer Service, der denselben Fehler wiederholt, wäre ein Rückschritt — auch wenn seine Daten harmlos sind.

Der Pfad-Tenant beantwortet damit „wer fragt", nicht „wessen Daten". Konkret:

Die Konsequenz wird hier benannt statt versteckt: eine Praxis mit Schreibrecht ändert den Katalog für alle. Das ist die Definition geteilter Referenzdaten und der Preis dafür, dass diese Daten nicht am Patienten hängen. Wer das nicht will, entzieht den Grant — es gibt keine zweite Stufe.

4. Vier Capabilities, keine davon [USER]-exklusiv

Key allowedActors
substanzstammdaten.wirkstoff.read USER, SERVICE, AGENT
substanzstammdaten.wirkstoff.create USER, SERVICE
substanzstammdaten.produkt.read USER, SERVICE, AGENT
substanzstammdaten.produkt.create USER, SERVICE

SERVICE ist bei create dabei, weil genau so eine spätere externe Datenbank angebunden würde — ein Importlauf ist ein Service, kein Mensch. AGENT ist bei read dabei und bei create nicht: der Katalog ist für einen Agenten eine Nachschlagequelle, keine Schreibfläche.

Keine dieser vier ist NUR_MENSCHEN. Bei txm.regelwerk.regel.aendern (ADR-0011) und txm.regelwerk.rpraxis.freigeben (ADR-0010) war die Begründung „Tragweite: ein Katalog, der Empfehlungen beeinflusst". Dieser Katalog beeinflusst keine Empfehlung — siehe die Abgrenzung unten. Sobald er es täte, wäre NUR_MENSCHEN fällig; das ist Teil der dann anstehenden Entscheidung, nicht dieser hier.

Eigener Keycloak-Client tuxametrics-substanzstammdaten-service (user_key svc-tuxametrics-substanz, tenant_key nur ["platform"]) statt des geteilten tuxametrics-services — dritter Fall desselben Musters nach ADR-0007 und dot-ADR-0071, mit einem zweiten Bootstrap-Grant auf platform.capability.create.

5. Startbestand mit Quellenangabe je Zeile

Der Katalog startet nicht leer und nicht erfunden: ein idempotenter Seeder legt eine überschaubare Zahl allgemein bekannter Wirkstoffe und Produkte an, jeder Datensatz mit quelleName und quelleUrl. Das ist dieselbe Belegdisziplin wie bei Regelkatalog.java („jede Zahl trägt ihre Fundstelle") — nur dass die Fundstelle hier eine öffentliche Arzneimittel-/Nährstoffquelle ist statt FACH-NNN, weil das Fachmaterial zu dieser Frage gerade nichts sagt (OF-07).

Die Quelle ist ein Pflichtfeld, auch für händisch angelegte Einträge. Ein Katalogeintrag ohne Herkunft wäre genau das, was dieses Repo an anderer Stelle „klingt plausibel" nennt.

Der Seeder ist standardmäßig an — anders als der Demo-Grantsatz der Authority und der Agent-Identity-Seeder der VID, die beide standardmäßig aus sind. Der Unterschied ist kein Versehen: jene beiden tragen einen handelnden Actor bzw. Rechte in ein laufendes System ein, dieser einen belegten Katalog ohne Actor, ohne Recht und ohne Patientenbezug. Er ist das Gegenstück zu Regelkatalog.java, das ebenfalls niemand einschaltet.

Was dieses ADR ausdrücklich NICHT entscheidet

Das Wichtigste zuerst, weil es sonst überlesen wird:

Die Wirkstoff-Auflösung existiert ab jetzt. Eine Interaktionsprüfung entsteht dadurch nicht.

Medikationseintrag.interaktionspruefung bleibt ein Enum mit genau einem Wert (NICHT_AUTOMATISIERT_GEPRUEFT), Interaktionspruefungs Javadoc bleibt wörtlich stehen, und Medikationseintrag.HINWEIS_NICHT_GEPRUEFT läuft unverändert durch jede REST-, MCP- und Berichtsantwort. Leitplanke 4 in CLAUDE.md ist in der Substanz unverändert gültig; sie bekommt nur einen Verweis hierher, weil ihr Begründungssatz („die Auflösung … hat im Material keine Quelle") seit heute unvollständig ist: das Material hat weiterhin keine, dieses Produkt hat jetzt einen eigenen, extern belegten Katalog.

Warum die Auflösung allein nicht reicht — und das ist der eigentliche Grund, nicht Vorsicht:

Ob und wie eine echte Interaktionsprüfung auf dieser Auflösung aufsetzt, ist eine eigene, spätere Entscheidung und braucht ein eigenes ADR. Sie müsste mindestens beantworten: welcher Interaktionskatalog, mit welcher Verbindlichkeit, welches Verhalten bei unbekanntem Präparat, und ob das Ergebnis blockiert oder hinweist (GM-7).

Weiter draußen bleiben:

Konsequenzen

Nachtrag 2026-08-26: Produktstärke (mengeWert/mengeEinheit) ist doch drin

Entscheidung des Product Owners am Tag der Annahme dieses ADR. Sie widerspricht einem Stichwort der Liste oben („Dosierungen") und wird deshalb hier begründet statt stillschweigend gebaut.

Was die ursprüngliche Begründung zu weit gefasst hat

Der Ausschlusssatz nannte zwei Gründe, und keiner der beiden trägt für die Produktstärke:

Genannter Grund Was er tatsächlich abdeckt Warum er hier nicht greift
OF-10 Die nationalen Höchstmengenempfehlungen — die Spalte ist im Material leer und laut GG-META-0005 ausdrücklich „nicht zu schätzen". Ein regulatorischer Grenzwert. „500 mg laut Gelbe-Liste-Produktseite" ist kein Grenzwert, sondern eine Eigenschaft eines konkreten Handelsprodukts aus der Quelle, die in derselben Zeile bereits zitiert wird. OF-10 bleibt unberührt: nationale Höchstmengen fehlen weiterhin vollständig.
„Alle Zahlen an einer Stelle" (Regelkatalog.java) Fachzahlen — Schwellen, Referenzbereiche, Sicherheitsobergrenzen, Modifikatoren. Also Zahlen, aus denen eine Regel eine Aussage über einen Patienten ableitet. Eine Produktstärke leitet nichts ab. Sie ist ein Attribut wie name oder typ. Nach dieser Lesart hätte auch der ATC-Code in Regelkatalog.java gemusst.

Dazu ein Umstand, der beim ersten Schnitt übersehen wurde: für 12 der 14 Startbestandszeilen stand die Zahl ohnehin schon in StammdatenSeeder.java — als Fließtext im name („ben-u-ron 500 mg Tabletten"). Der Ausschluss hat also keine Zahl aus dem Repo herausgehalten, sondern nur verhindert, dass ein Filter, eine Sortierung oder ein Vergleich sie erreicht. „Magnesium 250 mg", „500 mg" und „1000 mg" wären drei unverbundene Namensstrings gewesen.

Der Beleg, der die Entscheidung im Nachhinein bestätigt hat

Beim Befüllen zeigte sich, dass der Namensstring nicht nur unpraktisch, sondern in 2 von 14 Fällen schlicht die falsche Zahl ist, wenn man sie auf den katalogisierten Wirkstoff bezieht:

Produkt Zahl im Handelsnamen Stärke des katalogisierten Wirkstoffs (Quelle)
Unizink 50 Tabletten 50 10 mg Zink (50,39 mg Zinkbis(hydrogen-DL-aspartat) entsprechend 10 mg Zink)
Glucophage 500 mg Filmtabletten 500 389,91 mg Metformin (500 mg Metforminhydrochlorid entsprechend 389,91 mg Metformin)

Das ist das eigentliche Argument für das Feld: Wer die Stärke aus dem Namen liest, liest bei Salz-/Esterpräparaten die Salzmasse. Ein strukturiertes, quellenbelegtes Feld macht diesen Unterschied sichtbar — der Fließtext verdeckt ihn.

Was entschieden ist

  1. Substanzprodukt trägt mengeWert (BigDecimal, precision=12, scale=4) und mengeEinheit (Freitext, max 24). Dieselbe Form wie Regelkatalog.Obergrenze (BigDecimal + Einheit als String); ug statt µg ist die ASCII-Hausform dieses Repos.
  2. Beide optional, aber nur gemeinsam. Genau eines von beiden wird mit 422 abgewiesen — eine Zahl ohne Einheit ist keine Menge. Ein Präparat ohne sinnvoll extrahierbare Einzelstärke bleibt leer; in der Oberfläche, kein erfundener Text.
  3. Eine Befüllungsregel, nachprüfbar: die Stärke des katalogisierten Wirkstoffs je Bezugseinheit der Quelle (1 Tablette, 1 Kapsel, 1 ml), in der Einheit, in der die Quelle sie führt. Herleitung je Zeile als Kommentar in boot/StammdatenSeeder.
  4. Der name bleibt unangetastet. Die Stärke im Handelsnamen wird nicht gekürzt und nicht normalisiert — mengeWert/mengeEinheit sind eine zusätzliche Sicht auf dieselbe Quelle, kein Ersatz. Ein Test hält das fest (mengeAmProdukt_folgtDemWirkstoffUndNichtDerZahlImNamen).
  5. Keine Verdrahtung. Kein Auswertungspfad, keine Regel und keine Empfehlung liest dieses Feld. Regelkatalog.java bleibt die einzige Stelle für Schwellen und Sicherheitsobergrenzen.

Was ausdrücklich NICHT mitentschieden ist

Höchstmengen, Tagesdosen, Verzehrempfehlungen, Packungsgrößen, Applikations- und Darreichungsformen, PZN, ATC-Vollhierarchie — alles unverändert draußen. Und weiterhin: eine Stärke ist keine Dosierungsempfehlung. Sobald dieses Feld irgendwo eine Empfehlung beeinflusst, ist das eine neue Entscheidung — inklusive der Frage, ob die vier Capabilities dann NUR_MENSCHEN werden müssen (Abschnitt 4 oben).

Verweise