FHIR adapter and conformance¶
This file documents the implemented FHIR boundary for Digital PEWS. The adapter maps between the chart's internal patient/observation model and FHIR R4 Bundle resources for interoperability; the first integration target is GESH on Oracle via SMART-on-FHIR. It records current code and tests only; draft or unresolved mapping ideas are quarantined in the appendix.
Adapter contract¶
Implemented in chart/fhir-adapter.js:
fromFhirBundleToChartModel(bundle)parses a FHIR R4Bundleinto{ patient, observations }for the chart.fromChartModelToFhirBundle(patient, observations, context?)builds a FHIR R4Bundlefrom chart data.- Both functions are pure data transforms. The adapter does not compute or recompute PEWS scores. It copies
observation.pewsTotalto/from the scoreObservation; score recomputation is a separate safety gate intest/fhir/score-conformance.test.jsusingchart/npews-scorer.js.
FHIR -> chart behaviour¶
fromFhirBundleToChartModel:
- Requires
bundle.resourceType === 'Bundle'andBundle.entryto be an array. - Requires at least one
Patientand oneObservation; otherwise it throwsNo Patient in bundleorNo Observation resources in bundle. - Requires each mapped
Observation.effectiveDateTimeto be a parseable string; otherwise it throwsUnsupported or malformed timestamp. - Uses the first
Patientand firstEncounteronly. - Reads patient fields as:
patient.name:Patient.name[0].text, falling back to joinedPatient.name[0].given.patient.dob:Patient.birthDate.patient.ageBand:Patient.extensionwithpews-age-bandURL.patient.nhsNumber:Patient.identifier[0].value.patient.admittedAt: firstEncounter.period.start.- Groups observations by exact
effectiveDateTime; each timestamp becomes one chart observation. - Uses only
Observation.code.coding[0].codefor dispatch. Unknown codes are ignored. - Sorts returned chart observations ascending by timestamp.
- Preserves stored
pewsTotalandescalationLevel; it does not validate them.
Chart -> FHIR behaviour¶
fromChartModelToFhirBundle:
- Emits a
Bundlewithtype: 'collection'. - Emits one
Patientwith fixed idpatient-1. - Emits one
Encounterwith idcontext.encounterId ?? 'encounter-1'.context.practitionerIdandcontext.organizationIdare accepted by the signature but are not used. - Emits per-chart-observation
Observationresources for numeric vitals, coded PEWS fields, blood pressure, optional oxygen delivery, and one score observation. - Sets all emitted observations to
status: 'final'and referencesPatient/patient-1plus the emitted encounter. - Emits a score
Observationcodedpews-totalwithvalueInteger: observation.pewsTotal; ifobservation.escalationLevelis present it is stored in thepews-escalation-levelextension. - Does not emit UK Core
meta.profile,QuestionnaireResponse,Condition,Flag,Provenance,Practitioner,Organization, or scorederivedFromlinks.
Code systems and mappings¶
Authoritative standards are used where the adapter has a standard code. RCPCH PEWS codes and extensions are repo-local conventions pending national/canonical decisions.
Resource and identifier systems¶
| Use | System / URL | Code or value | Status | Implemented behaviour |
|---|---|---|---|---|
| LOINC | http://loinc.org |
see vital-sign table below | Authoritative standard | Used for physiological observation codes and BP component codes. |
| UCUM | http://unitsofmeasure.org |
/min, %, s, Cel, mm[Hg]; oxygen delivery preserves the chart/FHIR unit literally |
Authoritative system; literal units are adapter conventions | Used in emitted valueQuantity.system; unit and code are written as listed. |
| Local PEWS code system | https://rcpch.github.io/fhir/CodeSystem/pews |
see local-code table below | Repo-local convention | Used for PEWS-specific observation codes and their coded values. |
| NHS number identifier | https://fhir.nhs.uk/Id/nhs-number |
patient.nhsNumber |
NHS/UK convention | Written on Patient.identifier; inbound currently reads Patient.identifier[0].value without checking the system. |
| Missing data | http://terminology.hl7.org/CodeSystem/data-absent-reason |
not-performed, unable-to-perform |
Authoritative HL7 terminology | Outbound maps local skip reason unable to unable-to-perform; any other skip reason to not-performed. |
| Encounter class | http://terminology.hl7.org/CodeSystem/v3-ActCode |
IMP |
Authoritative HL7 terminology | Outbound Encounter.class; status is in-progress. |
| Age band extension | https://rcpch.github.io/fhir/StructureDefinition/pews-age-band |
copied Patient.extension[].valueCode |
Repo-local convention | Written/read on Patient.extension; adapter does not derive age band. |
| Skip reason extension | https://rcpch.github.io/fhir/StructureDefinition/pews-skip-reason |
copied local valueCode |
Repo-local convention | Preserves field-level chart skip reasons when dataAbsentReason is present. |
| Escalation extension | https://rcpch.github.io/fhir/StructureDefinition/pews-escalation-level |
copied score valueCode |
Repo-local convention | Stored only on the score observation when present. |
LOINC vital-sign mappings¶
| Chart field | Observation code system | Observation code | Display used outbound | Value |
|---|---|---|---|---|
respiratoryRate |
http://loinc.org |
9279-1 |
Respiratory rate |
valueQuantity, UCUM /min |
oxygenSaturation |
http://loinc.org |
59408-5 |
Oxygen saturation |
valueQuantity, UCUM % |
heartRate |
http://loinc.org |
8867-4 |
Heart rate |
valueQuantity, UCUM /min |
bloodPressureSystolic / bloodPressureDiastolic |
http://loinc.org |
55284-4 |
Blood pressure systolic and diastolic |
single BP Observation with components |
temperature |
http://loinc.org |
8310-5 |
Body temperature |
valueQuantity, UCUM Cel |
capillaryRefill |
http://loinc.org |
44963-7 |
Capillary refill time |
valueQuantity, UCUM s |
BP components inside the 55284-4 observation:
| Component | Code system | Code | Display used outbound | Unit |
|---|---|---|---|---|
| Systolic BP | http://loinc.org |
8480-6 |
Systolic blood pressure |
UCUM mm[Hg] |
| Diastolic BP | http://loinc.org |
8462-4 |
Diastolic blood pressure |
UCUM mm[Hg] |
Local PEWS observation mappings¶
| Chart field | Observation code system | Observation code | FHIR value |
|---|---|---|---|
respiratoryDistress |
https://rcpch.github.io/fhir/CodeSystem/pews |
pews-resp-distress |
valueCodeableConcept.coding[0].code |
oxygenDevice |
https://rcpch.github.io/fhir/CodeSystem/pews |
pews-o2-device |
valueCodeableConcept.coding[0].code |
oxygenDelivery |
https://rcpch.github.io/fhir/CodeSystem/pews |
pews-o2-delivery |
valueQuantity.value, with unit/code copied from chart unit |
avpu |
https://rcpch.github.io/fhir/CodeSystem/pews |
pews-avpu |
valueCodeableConcept.coding[0].code |
pewsTotal |
https://rcpch.github.io/fhir/CodeSystem/pews |
pews-total |
valueInteger |
The adapter does not validate local value sets. Coded field values are passed through from/to the first coding code.
Special handling¶
Blood pressure¶
- Blood pressure is one
Observationcoded55284-4, not two separate observations. - Systolic and diastolic are stored as
component[]entries with LOINC8480-6and8462-4. - Inbound: if the BP observation has
dataAbsentReasonor a skip extension, both chart BP fields are set tonull; when the skip extension exists the same skip reason is copied to both BP fields. - Outbound: if both BP values are
null, the adapter emits a BP observation without components and addsdataAbsentReasononly when a BP skip reason is present. The current builder treats BP as skipped only when both values are null.
Oxygen delivery modality¶
- Oxygen delivery is represented by the local observation code
pews-o2-deliverywithvalueQuantity. - The adapter preserves the FHIR/chart unit literally. Current tests exercise
%andL/min. - Outbound sets
valueQuantity.unitandvalueQuantity.codeto the same chart unit, using UCUM as the system. - The adapter itself only preserves the unit. The rendered chart line-break behaviour is in
chart/chart.js: oxygen-delivery points are split into separate trend-line segments when a value is skipped/null or when adjacent non-nulloxygenDelivery.unitvalues differ.L/minvalues are scaled for display on the shared axis; the break marks that%andL/minare not a continuous modality.
Skipped observations and absent data¶
- Chart fields use
nullplus optionalfield_skipReason. - Inbound:
Observation.dataAbsentReasonmakes the mapped chart fieldnull; thepews-skip-reasonextension, when present, fillsfield_skipReason. - Outbound: a null field with a skip reason emits no
value[x], addsdataAbsentReason, and adds thepews-skip-reasonextension. - Skip reason
unablebecomesdataAbsentReasoncodeunable-to-perform; all other skip reasons becomenot-performedwhile the original local reason remains in the extension. - A bare null without a skip reason may be emitted without
dataAbsentReason; oxygen delivery is omitted entirely when null and not skipped.
Age band¶
- The adapter reads and writes the age band only through
Patient.extensionURLhttps://rcpch.github.io/fhir/StructureDefinition/pews-age-band. - It does not calculate age band from date of birth and timestamp. Age-band derivation belongs to scorer/chart logic, not this adapter.
Conformance testing¶
Current fixtures live in test/fhir/fixtures/:
| Fixture | Role currently implemented |
|---|---|
stable-normal-5-12y.json |
Positive normal observations, stored score 0. |
skip-reasons-0-11m.json |
Positive absent-data and skip-reason preservation, stored score 1. |
oxygen-modality-transition.json |
Positive two-round % then L/min oxygen delivery, stored scores 2 then 5. |
missing-patient.json |
Negative structural shape: no Patient. |
bad-score.json |
Negative score-conformance shape: stored score 5 while recomputed score is 0. |
Current gates:
| Gate | Implemented by | What it proves today |
|---|---|---|
| Structural | test/fhir/conformance-harness.test.js |
Positive bundles are Bundles with non-empty entries, one Patient, observations with effectiveDateTime, codes, and integer pews-total; negative fixtures expose expected bad shapes. |
| Profile/coding, lightweight only | test/fhir/conformance-harness.test.js, fixture JSON |
Checks local coding presence and selected clinical coding details: skip extension URL/value, dataAbsentReason codes, oxygen units, score observations. Strict UK Core/profile validation is not implemented. |
| Semantic mapping | test/fhir/roundtrip.test.js plus selected conformance-harness checks |
FHIR -> chart maps patient fields, vitals, coded fields, BP components, oxygen delivery { value, unit }, skip reasons, sorting, and hard errors for missing Patient/Observations. Chart -> FHIR emits the expected envelope, references, BP components, skip data, oxygen unit, and score observation. |
| Score conformance | test/fhir/score-conformance.test.js |
Recomputes scores from adapter output with npews-scorer.js; stored pewsTotal must match for positive fixtures and mismatch for bad-score. This is the safety gate that keeps scoring outside the adapter. |
| Round-trip purity | test/fhir/roundtrip.test.js |
Chart -> FHIR -> chart preserves chart-critical fields, including skip reason, oxygen delivery unit/value, stored score, and escalation level. FHIR -> chart -> FHIR assertions remain todo in the test file. |
Appendix: future work / unresolved mappings¶
These items are draft or aspirational. They are not implemented by chart/fhir-adapter.js unless stated above.
- Strict FHIR R4 and UK Core profile validation with pinned package versions, validator reports, and warning allow-lists.
- National/canonical code decisions for PEWS total, escalation labels, respiratory distress values, oxygen device values, AVPU including
Asleep, trigger codes, and skip-reason scoring impact. The current RCPCH PEWS CodeSystem and extensions are interim repo-local conventions. QuestionnaireResponserepresentation for carer concern and clinical intuition.ConditionorFlagrepresentation for sepsis/specific concern.Provenance,Practitioner, andOrganizationemission; the adapter currently ignores practitioner/organisation context. AProvenanceresource referencing the rendered Observations, with anagentidentifying the chart component and its version, would be the standard FHIR way to record which component version produced a given view (roadmap R61). It is not implemented because the SMART shell (smart/smart.js) is deliberately read-only (patient/*.read, seesmart/README.md): writing aProvenanceresource needs write scope, which most EHR deployments will not grant to a viewer-only app, and adding it would be a material change to the app's security posture requiring its own safety and governance review, not a drive-by addition. A custom request header (e.g.X-Npews-Chart-Version) was tried as a scope-free alternative and rejected: it needs no new grant in principle, but it turns every plain GET into a CORS preflight, and the local sandbox's HAPI server (representative of an unconfigured FHIR endpoint) does not allow the header, so the browser blocks the request outright - confirmed by running the SMART sandbox end-to-end. There is no channel demonstrated to work without either write scope or prior, EHR-specific CORS coordination, so R61 remains open pending that governance decision.- Score observation
derivedFrom, sub-score components, trigger metadata, and already-escalated flags. - Additional fixture categories from the draft conformance plan, such as deterioration-escalation and age-band-boundary bundles.