Adam El Kadaoui
Selected work

Personal project · Open source · Interoperability

Onco
Bridge

Oncology interoperability with evidence you can trace.

A FHIR R4 workbench that preserves what arrived, normalizes selected oncology concepts into a FHIR-independent domain, evaluates deterministic quality checks, and traces every derived value back to its source.

Synthetic data only · Runs locally with Docker Compose

Role
Architecture · Full-stack engineering
Model
FHIR-independent canonical domain
Interop
FHIR R4 · mCODE STU4 subset
Stack
.NET · Angular · PostgreSQL
Delivery
Docker Compose · GitHub Actions
Testing
xUnit · Vitest · Playwright · real PostgreSQL

FHIR is the input.
It should not become the architecture.

Incoming data is shaped around FHIR resources and references. The application needs a model it owns, with its own invariants and its own cardinalities. So the two shapes are genuinely different, and the mapping between them is the product.

FHIR source graph7 resources

PatientSYN-0001

subject

Conditioncondition-001

focus

Stage group Observationstaging-group-001

hasMember · 3

T Observationstaging-t-001

N Observationstaging-n-001

M Observationstaging-m-001

A graph held together by references. Four resources carry one staging statement.

Canonical domain4 concepts

Patient SYN-0001

PrimaryCancerDiagnosis

CancerStaging

  • Stage IIA
  • T2
  • N1
  • M0

One aggregate with a stage group and an ordered T/N/M list. Names diverge where the concepts diverge.

OncoBridge.Domain contains no FHIR types.

Zero NuGet packages, zero project references. DomainBoundaryTests reads the real project files and the compiled assembly reference graph, so a leak fails the build rather than a review.

Four resources in.
One staging aggregate out.

Nothing becomes untraceable. One import screen carries the source, the derived value, the quality statement and the evidence behind each.

The OncoBridge inspector: a header carrying the import id, status, resource count and payload SHA-256; a canonical entity selector with Cancer staging Stage IIA active; and four panes — 01 SOURCE listing the four contributing Observation resources marked A to D, 02 NORMALIZED showing the single CancerStaging aggregate, 03 QUALITY listing findings OB-CONF-001, OB-CONF-002 and OB-REF-001 with citations, and 04 PROVENANCE listing four lineage records.
Fig. 01 — Evidence ledger inspector · Synthetic acceptance bundleFull size (opens in a new tab)
Detail — pane 02 normalizedStage IIAT2N1M0
Pane 02 NORMALIZED of the OncoBridge inspector: the Cancer staging aggregate listing Stage group Stage IIA from A, Method not stated with a link to finding OB-CONF-002, Effective 2019-04-02 at DAY precision from A, Category T equals T2 from B, Category N equals N1 from C and Category M equals M0 from D.
Every populated derived field names its source · Method absent, cross-linked to OB-CONF-002Fig. 01 — Pane 02 normalized, cropped for mobileOpen the full inspector (opens in a new tab)
  1. 01 · Source

    Four contributing Observations.

    Marked A–D, out of the seven resources in the bundle.

  2. 02 · Normalized

    One CancerStaging aggregate.

    Stage IIA · T2 from B · N1 from C · M0 from D.

  3. 03 · Quality

    Deterministic findings.

    Three raised, each with the statement it was derived from.

  4. 04 · Provenance

    Four lineage records.

    Whole-entity record first, then field-level records for T/N/M.

Bytes first,
meaning second.

Eight stages, one direction. The evidence path runs beneath the transformation path and stays reachable from the far end.

  1. 01FHIR R4 bundleposted bytes
  2. 02Immutable payloadbytea · SHA-256
  3. 03Source resourcesjsonb · entry order
  4. 04FHIR normalizationInterop.Fhir
  5. 05Canonical modelDomain · no FHIR
  6. 06Quality assessmentsix cited checks
  7. 07REST / OpenAPIsix routes
  8. 08Angular workbenchgenerated client

Provenance — every normalized field stays reachable from the API

Raw payloadByte-exact

ImportBatch.RawPayload · bytea
ImportBatch.ContentHash · SHA-256

The exact uploaded bytes, and a digest computed over exactly those bytes inside the factory that creates the batch — so a caller cannot supply one that does not match.

Resource JSONNot byte-identical

SourceResource.ResourceJson · jsonb

A parsed, queryable copy of one entry. It may reorder keys, drop insignificant whitespace and rewrite escapes, so it preserves meaning but cannot reproduce the digest. The inspector says so on screen.

Meaning and evidence are separate obligations.

Normalize the graph.
Keep the evidence.

Collapsing a graph is only safe if the collapse is reversible on paper. Every displayed derived field names its source; the lineage table records the whole-entity transformation plus field-level T/N/M provenance.

Source resources · import batchEvidence A–D

Each source resource is marked A to D. The same marker appears on the canonical field it produced and on its lineage record. Pointing at or focusing a source highlights its path; the same information is written out in the lineage table below.

  • AObservation · staging-group-001Whole entity · entry 2
  • BObservation · staging-t-001Category T · entry 3
  • CObservation · staging-n-001Category N · entry 4
  • DObservation · staging-m-001Category M · entry 5

CancerStaging1 canonical entity

Stage group
Stage IIAfrom A
Category T
T2from B
Category N
N1from C
Category M
M0from D
Effective
2019-04-02 Dayfrom A
Scope · field pathSource resourceTransformation
Whole entity · fieldPath: nullstaging-group-001 · entry 2FhirCancerStagingNormalization 1.0.0
Category T · fieldPath: PrimaryTumourstaging-t-001 · entry 3FhirCancerStagingNormalization 1.0.0
Category N · fieldPath: RegionalNodesstaging-n-001 · entry 4FhirCancerStagingNormalization 1.0.0
Category M · fieldPath: DistantMetastasesstaging-m-001 · entry 5FhirCancerStagingNormalization 1.0.0
4 source resources · 4 lineage records · 1 canonical entityWhole-entity record first, then field-level records for T/N/M

A defective record
remains data.

Six checks, each carrying a citation, an expectation and what was actually found. The import still succeeds — reporting on imperfect data is the point of the system.

Structural

OB-STR-001

A bundle entry could not be parsed as a known FHIR R4 resource.

Error

Conformance

OB-CONF-001OB-CONF-002

A primary cancer condition does not state a mandatory category, or a TNM stage group does not state the staging method mCODE STU4 states as 1..1.

Error · Error

Referential integrity

OB-REF-001OB-REF-002

A reference does not resolve within the same import batch, or a stage group member names a different subject. No external lookup is attempted.

Error · Error

Domain consistency

OB-DOM-001

A staging effective time is definitely before the onset it stages. Evaluated on the canonical model, with no FHIR involved.

Warning

Finding

A stated expectation was checked and not met.

Carries target, expected, actual and the specification statement it was derived from, so a reviewer can check the claim rather than trust the tool.

Coverage note

OncoBridge did not establish this.

Not a claim that the data is wrong. OB-DOM-001 fires only on Before; an Indeterminate comparison records a coverage note instead of guessing.

OncoBridge conformance checks — a subset of mCODE STU4. Not full mCODE profile validation.

2019-03 is not 2019-03-01.

FHIR dates are variable precision. Collapsing them into a DateTime would invent information the source never stated, so precision travels with the value.

Year
2019
Month
2019-03
Day
2019-03-14
Instant
2019-03-14T10:00:00+02:00

Comparison is a partial order · four outcomes

  • Before
  • After
  • Same
  • Indeterminate

2019 versus 2019-03 is Indeterminate: March falls inside 2019, and nothing in the record settles the order. That is an outcome, not an error — which is why partial dates cannot simply be sorted.

The OncoBridge timeline view: three projected groups — primary cancer diagnosis with onset 2019-03 at MONTH precision, cancer staging Stage IIA effective 2019-04-02 at DAY precision, and a cancer surgical procedure period from 2019-05 to 2019-06-12 sequenced on its start anchor — each event carrying the precision its source stated.
Fig. 02 — Server-projected chronology · Diagnosis 2019-03 · Staging 2019-04-02 · Procedure 2019-05 → 2019-06-12Full size (opens in a new tab)
Order is server-owned
The projection carries the policy sentence it was built under, and the view renders it verbatim so the screen cannot quietly disagree with the server.
No date arithmetic in the view
No client-side sorting, no widening, no inferred precision. Events whose anchors compare Same share a group with no order asserted inside it.
Periods anchor on their start
The end bound never anchors an event, and the response names which bound the anchor came from rather than leaving the client to guess.

FHIR stops at the interoperability boundary.

A modular monolith whose dependency direction points one way, and a stack a clean machine can run with one command.

Dependency direction · inward only
  1. Api Application · Infrastructure · Interop.Fhir
  2. Interop.Fhir Application · Domain
  3. Infrastructure Application · Domain
  4. Application Domain
  5. Domain nothing

Interop.Fhir is the only non-test project permitted to reference Hl7.Fhir.*. PublicSurfaceTests fails the run if the normalizer's entry point stops speaking domain types only.

Domain
Oncology model · temporal model · quality vocabulary
Application
Use cases · read projections
Interop.Fhir
FHIR parsing · normalization · lineage · source-facing checks
Infrastructure
EF Core · PostgreSQL · migrations
Api
HTTP contracts · minimal APIs · OpenAPI
Web
Angular · generated OpenAPI client, never hand-edited
Runtime path · Docker Compose

Browser

nginx · only published port · 8080

Angular

/api

ASP.NET Core

PostgreSQL 18.6

migrate · one-shot

Migrations run as their own Compose service that must complete successfully before the API starts — never as an API startup side effect.

Build
.NET 10 · Angular 22
Verify
xUnit · CsCheck · Vitest
Persist
Testcontainers · real PostgreSQL
Accept
1 real-stack Playwright journey
Contract
api:check · client vs snapshot
Package
Docker Compose · nginx
CI
GitHub Actions · 4 jobs

The Compose smoke test imports through the reverse proxy and asserts the returned contentHash equals the SHA-256 of the bytes it posted — so the proxy is proven not to have altered the body.

Source in.
Evidence intact.

What I optimized for, and the repository evidence behind each decision.

Traceability
Every derived value leads back to evidence. Four lineage records for the four source resources behind one staging aggregate.
Boundaries
FHIR does not leak into the canonical domain. Asserted by DomainBoundaryTests against the real project files, not by convention.
Honesty
Unknown ordering remains unknown. Indeterminate is a result the model carries, not an error it hides.
Reproducibility
A clean machine runs it with docker compose up --build. No local SDK, database or manual migration step.