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.interaktionspruefungist 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:
- Es gibt keine Auflösung Handelsname → Wirkstoff. — eine Aussage über eine fehlende Fähigkeit.
- 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:
- Substanzstammdaten sind nicht patientenbezogen und nicht Teil des Durchlaufs. Sie werden nachgeschlagen, nicht durchlaufen.
- Sie sind nicht mandantengebunden (siehe 3.) — der einzige Datenbestand dieses Produkts, auf den das zutrifft, außer dem Capability-Katalog der Authority.
- Sie sind der einzige Datenbestand, der perspektivisch durch eine externe Quelle ersetzt wird. Genau deshalb darf er nicht in derselben DB liegen wie Befunde, Findings und Freigaben: eine Ablösung wäre sonst eine Operation an der Patientendatenbank.
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:
contextUser.tenantKey()ist der Pfad-Tenant, und derCapabilityGuardprüft den Schreib-Grant gegen genau ihn. Das Recht, den Katalog zu ändern, wird also je Praxis vergeben.- Die Repository-Queries laufen gegen den Sentinel, nie gegen den Pfad-Tenant. Sie heißen deshalb
findByTenantKeyAndWirkstoffKey(…)und bekommenSubstanzScope.GLOBAL_TENANT_KEYübergeben — die Tenant-Skopierung ist echt, sie zeigt nur auf den Sentinel.
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:
- Eine Interaktionsprüfung braucht neben der Auflösung einen Interaktionskatalog (Wirkstoff × Mikronährstoff → Aussage). Der ist eine völlig andere Quelle als „welcher Wirkstoff steckt in Aspirin", und für ihn gibt es hier nichts — weder im Fachmaterial noch in diesem Service.
FACH-021sagt: „Nur der aufgelöste Wirkstoff ist Grundlage der Interaktionsprüfung." Das ist eine notwendige, keine hinreichende Bedingung. Wer sie für hinreichend hält, liest den Satz rückwärts.- Eine Prüfung, die auf einem 12-Zeilen-Startbestand fußt, wäre schlimmer als keine: sie meldete „keine Interaktion" für jedes Präparat, das im Katalog fehlt. Ein stiller Falsch-Negativ-Befund in einem klinischen Kontext ist genau die Sorte Kulisse, die dieses Repo an vier weiteren Stellen bewusst vermeidet.
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:
- Jede Verdrahtung in die Auswertungslogik.
TherapieplanService,AuswertungslaufServiceundRegelkatalog.javasind nicht angefasst und kennen diesen Service nicht. Der Katalog ist Nachschlagewerk, kein Regelwerk — dieselbe Trennung wie zwischen ADR-0010 und ADR-0011, nur dass hier kein Rückkanal folgt. - Kombinationspräparate (siehe 2.).
- Dosierungen, Höchstmengen, Applikationsformen, Darreichungsformen, PZN, ATC-Vollhierarchie.
Alles davon wäre eine zweite Quelle für Zahlen, die dieses Produkt an einer Stelle führt
(
Regelkatalog.java) oder gar nicht führt (OF-10). → Für die Produktstärke am selben Tag revidiert, siehe Nachtrag unten. Der Rest der Zeile gilt unverändert. DELETE. Wie in ADR-0010: ein Katalogeintrag, auf den irgendwo verwiesen wurde, verschwindet nicht. Ein Deaktivierungsstatus wäre die richtige Form dafür und ist bewusst noch nicht gebaut — es gibt bislang nichts, was verweist.- Automatischer Import aus einer offiziellen Datenbank. Der Service ist so geschnitten, dass er
später einer sein kann (Quellenfelder,
SERVICE-Schreibrecht, kein Patientenbezug). Ein Importlauf ist er nicht. - Pflicht zur Katalogauswahl bei der Erfassung.
Medikationseintrag.eingabetextundSupplementeintrag.produktbleiben Freitext und bleiben führend. Ein späterer Lookup in den Erfassungsformularen setzt optional einen Schlüssel obendrauf; die bestehende Validierung ändert sich nicht.
Konsequenzen
- + Die Fähigkeit, die
OF-07seit dem ersten Tag als fehlend markiert, existiert jetzt — als eigener, belegter Datenbestand statt als geschätzte Zuordnung im Fachcode. - + Der einzige nicht patientenbezogene Fachdatenbestand des Produkts liegt in einer eigenen DB. Eine spätere Ablösung durch eine offizielle Quelle ist ein Service-Austausch, keine Migration an Patientendaten.
- + Die Patientenakte kann Medikation und Supplemente über alle Auswertungen hinweg zeigen (gleicher Auftrag, eigener Commit im Laborauswertungs-Service) — die Stammdaten liefern dafür die Referenzebene, ohne dass ein Patientenschlüssel je diesen Service erreicht.
- − Ein fünftes Maven-Modul, ein achter Container, eine zehnte Datenbank. Für einen Katalog mit zwei Tabellen ist das viel — gerechtfertigt allein durch die Datenhoheitsgrenze aus 1., nicht durch den Umfang.
- − Leitplanke 4 ist ab heute erklärungsbedürftig. Bis gestern war „keine Wirkstoffauflösung" ein einzeiliger Grund; ab heute muss man zwei Sätze lesen, um zu verstehen, warum es die Auflösung gibt und die Prüfung trotzdem nicht. Genau dafür existiert dieses ADR — und genau deshalb steht der Verweis darauf in der Leitplanke und nicht nur in dieser Datei.
- − Geteilte Referenzdaten mit praxisweise vergebenem Schreibrecht sind eine Asymmetrie, die auffallen wird (siehe 3.). Sie ist benannt, nicht gelöst.
- − Der Startbestand ist klein und deutschsprachig-marktbezogen. Er ist ein Startbestand, keine Datenbank; jede Aussage der Form „das Präparat ist nicht im System" bedeutet hier nichts.
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
SubstanzproduktträgtmengeWert(BigDecimal,precision=12, scale=4) undmengeEinheit(Freitext,max 24). Dieselbe Form wieRegelkatalog.Obergrenze(BigDecimal+ Einheit als String);ugstattµgist die ASCII-Hausform dieses Repos.- 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. - 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. - Der
namebleibt unangetastet. Die Stärke im Handelsnamen wird nicht gekürzt und nicht normalisiert —mengeWert/mengeEinheitsind eine zusätzliche Sicht auf dieselbe Quelle, kein Ersatz. Ein Test hält das fest (mengeAmProdukt_folgtDemWirkstoffUndNichtDerZahlImNamen). - Keine Verdrahtung. Kein Auswertungspfad, keine Regel und keine Empfehlung liest dieses Feld.
Regelkatalog.javableibt 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
CLAUDE.md— Leitplanke 4 (Medikamenten-Interaktionen), mit Verweis auf dieses ADRguidelines/project/gold-path-scope.md— „Medikament-InteraktionsprüfungFACH-021/FACH-043als Prüfung" bleibt außerhalb des Schnittsguidelines/project/service-landscape.md— „Externe Abhängigkeiten": der Status der Arzneimitteldatenbank ist mit diesem ADR präzisiert, nicht erledigt- ADR-0005 —
PlatformScope, das Vorbild für den Sentinel - ADR-0007 — eigener Keycloak-Client je Service statt des geteilten Accounts (Präzedenzfall dot-ADR-0071)
- ADR-0010 / ADR-0011 — die Trennung „Verwaltungsfläche ohne Wirkung" gegen „Rückkanal mit Wirkung", hier auf einen Katalog ohne Rückkanal angewandt
Interaktionspruefung.java— das Enum mit einem Wert, unverändert