SGHI FHIR Profile Implementation Guide
0.1.0 - ci-build

SGHI FHIR Profile Implementation Guide, published by Kathurima Kimathi. This guide is not an authorized publication; it is the continuous build for version 0.1.0 built by the FHIR (HL7® FHIR® Standard) CI Build. This version is based on the current content of https://github.com/savannahghi/sil_fhir_profile_ig/ and changes regularly. See the Directory of published versions

StructureMap: Discharge Clearance: Clinical Readiness Extraction

Official URL: https://fhir.slade360.co.ke/fhir/StructureMap/ExtractDischargeClearanceClinicalReadiness Version: 0.1.0
Active as of 2026-09-14 Computable Name: ExtractDischargeClearanceClinicalReadiness

Turns the discharging clinician's readiness attestation into four survey Observations: oral intake, mobility, pain control and the condition the patient is leaving in. Each is its own Observation because a clinician answers four separate questions and one flat resource would let an answer to one stand in for an answer to another. The handover sentence rides as a note on the condition Observation rather than becoming a fifth: it is prose about the discharge, and an Observation whose value is a paragraph answers no query. Nothing here writes to the Encounter – closing the stay and recording the discharge disposition belong to the EMR's discharge workflow, and the condition a patient leaves in is a different axis from where they went. The signature the form collects is a typed name, which SGHIObservation cannot carry as a performer, so it stays in the response and out of the extracted resources.

/// url = 'https://fhir.slade360.co.ke/fhir/StructureMap/ExtractDischargeClearanceClinicalReadiness'
/// name = 'ExtractDischargeClearanceClinicalReadiness'
/// title = 'Discharge Clearance: Clinical Readiness Extraction'
/// status = 'active'
/// description = 'Turns the discharging clinician\'s readiness attestation into four survey Observations: oral intake, mobility, pain control and the condition the patient is leaving in. Each is its own Observation because a clinician answers four separate questions and one flat resource would let an answer to one stand in for an answer to another. The handover sentence rides as a note on the condition Observation rather than becoming a fifth: it is prose about the discharge, and an Observation whose value is a paragraph answers no query. Nothing here writes to the Encounter -- closing the stay and recording the discharge disposition belong to the EMR\'s discharge workflow, and the condition a patient leaves in is a different axis from where they went. The signature the form collects is a typed name, which SGHIObservation cannot carry as a performer, so it stays in the response and out of the extracted resources.'

uses "http://hl7.org/fhir/StructureDefinition/QuestionnaireResponse" alias QR as source
uses "http://hl7.org/fhir/StructureDefinition/Bundle" alias Bundle as target
uses "http://hl7.org/fhir/StructureDefinition/Observation" alias Observation as target

group ExtractDischargeClearanceClinicalReadiness(source qr : QR, target bundle : Bundle) {
  qr -> bundle.type = 'transaction' "setBundleType";
  // linkId 301989955559 — "Oral intake tolerated"
  qr.item as it where ((linkId = '301989955559') and answer.value.exists()) then {
    it.answer first as a then {
      a ->  bundle.entry as entry,  entry.resource = create('Observation') as obs then {
        qr ->  obs,  entry then BuildClearanceObs(qr, obs, entry) "oralIntakeBase";
        qr -> obs.code as oralIntakeCodeCc then {
          qr -> oralIntakeCodeCc.coding as oralIntakeCodeCoding then {
            qr -> oralIntakeCodeCoding.system = 'https://fhir.slade360.co.ke/fhir/CodeSystem/inpatient-clinical-concept-codesystem' "oralIntakeCodeSystem";
            qr -> oralIntakeCodeCoding.code = 'oral-intake-at-discharge' "oralIntakeCodeCode";
            qr -> oralIntakeCodeCoding.display = 'Oral intake tolerated at discharge' "oralIntakeCodeDisplay";
          } "oralIntakeCodeCodingRule";
        } "oralIntakeCode";
        a -> obs then SetCodedValue(a, obs) "oralIntakeValue";
      } "oralIntakeObs";
    } "oralIntakeAnswer";
  } "oralIntakeRule";
  // linkId 507198101338 — "Mobility"
  qr.item as it where ((linkId = '507198101338') and answer.value.exists()) then {
    it.answer first as a then {
      a ->  bundle.entry as entry,  entry.resource = create('Observation') as obs then {
        qr ->  obs,  entry then BuildClearanceObs(qr, obs, entry) "mobilityBase";
        qr -> obs.code as mobilityCodeCc then {
          qr -> mobilityCodeCc.coding as mobilityCodeCoding then {
            qr -> mobilityCodeCoding.system = 'https://fhir.slade360.co.ke/fhir/CodeSystem/inpatient-clinical-concept-codesystem' "mobilityCodeSystem";
            qr -> mobilityCodeCoding.code = 'mobility-at-discharge' "mobilityCodeCode";
            qr -> mobilityCodeCoding.display = 'Mobility at discharge' "mobilityCodeDisplay";
          } "mobilityCodeCodingRule";
        } "mobilityCode";
        a -> obs then SetCodedValue(a, obs) "mobilityValue";
      } "mobilityObs";
    } "mobilityAnswer";
  } "mobilityRule";
  // linkId 852812667233 — "Pain controlled"
  qr.item as it where ((linkId = '852812667233') and answer.value.exists()) then {
    it.answer first as a then {
      a ->  bundle.entry as entry,  entry.resource = create('Observation') as obs then {
        qr ->  obs,  entry then BuildClearanceObs(qr, obs, entry) "painControlBase";
        qr -> obs.code as painControlCodeCc then {
          qr -> painControlCodeCc.coding as painControlCodeCoding then {
            qr -> painControlCodeCoding.system = 'https://fhir.slade360.co.ke/fhir/CodeSystem/inpatient-clinical-concept-codesystem' "painControlCodeSystem";
            qr -> painControlCodeCoding.code = 'pain-control-at-discharge' "painControlCodeCode";
            qr -> painControlCodeCoding.display = 'Pain control at discharge' "painControlCodeDisplay";
          } "painControlCodeCodingRule";
        } "painControlCode";
        a -> obs then SetCodedValue(a, obs) "painControlValue";
      } "painControlObs";
    } "painControlAnswer";
  } "painControlRule";
  // linkId 108099570999 — "Condition at discharge". This one carries the
  // free-text handover as a note, so it is the Observation a reader lands on
  // when they want the clinician's own words.
  qr.item as it where ((linkId = '108099570999') and answer.value.exists()) then {
    it.answer first as a then {
      a ->  bundle.entry as entry,  entry.resource = create('Observation') as obs then {
        qr ->  obs,  entry then BuildClearanceObs(qr, obs, entry) "conditionBase";
        qr -> obs.code as conditionCodeCc then {
          qr -> conditionCodeCc.coding as conditionCodeCoding then {
            qr -> conditionCodeCoding.system = 'https://fhir.slade360.co.ke/fhir/CodeSystem/inpatient-clinical-concept-codesystem' "conditionCodeSystem";
            qr -> conditionCodeCoding.code = 'patient-condition-at-discharge' "conditionCodeCode";
            qr -> conditionCodeCoding.display = 'Condition at discharge' "conditionCodeDisplay";
          } "conditionCodeCodingRule";
        } "conditionCode";
        a -> obs then SetCodedValue(a, obs) "conditionValue";
        // linkId 474741611141 — "Anything the next clinician should know"
        qr.item as itNote where ((linkId = '474741611141') and answer.value.exists()) then {
          itNote.answer first as aNote then {
            aNote.value as nv -> obs.note as note then {
              nv -> note.text = nv "handoverNoteText";
              qr.authored as t -> note.time = t "handoverNoteTime";
            } "handoverNote";
          } "handoverNoteAnswer";
        } "handoverNoteRule";
      } "conditionObs";
    } "conditionAnswer";
  } "conditionRule";
}

// ─────────────────────────────────────────────────────────────────────────────
// Shared scaffolding. Repeated in each clearance map rather than imported: the
// transform engine resolves `imports` against whatever StructureMaps happen to
// be on the server, and a clearance that stops extracting because a shared map
// was not uploaded is a worse failure than a duplicated group.
// Source parameters are left untyped on purpose. Writing `: Element` makes the
// publisher's StructureMap validator compare QuestionnaireResponse.item against
// the literal 'Element' and report them as incompatible, which is noise.
// ─────────────────────────────────────────────────────────────────────────────
group BuildClearanceObs(source qr : QR, target obs : Observation, target entry) {
  qr -> obs.id = uuid() then SetObservationFullUrl(obs, entry) "idAndFullUrl";
  qr -> entry.request as request then {
    qr -> request.method = 'POST' "requestMethod";
    qr -> request.url = 'Observation' "requestUrl";
  } "entryRequest";
  qr -> obs.status = 'final' "status";
  qr -> obs.category as categoryCc then {
    qr -> categoryCc.coding as categoryCoding then {
      qr -> categoryCoding.system = 'http://terminology.hl7.org/CodeSystem/observation-category' "categorySystem";
      qr -> categoryCoding.code = 'survey' "categoryCode";
      qr -> categoryCoding.display = 'Survey' "categoryDisplay";
    } "categoryCodingRule";
  } "category";
  qr.subject as s -> obs.subject = s;
  qr.encounter as e -> obs.encounter = e;
  // `effective`, not `effectiveInstant`: the R5 transform engine writes choice
  // elements by their base name and rejects the typed form.
  // Advantage stamps `authored` when the clearances are submitted, not when each
  // was signed, so a clearance signed at two in the morning and submitted at the
  // handover carries the handover time. Sending the signature time on the
  // response would fix it at the source; there is nothing on the response for a
  // map to read it from.
  qr.authored as t -> obs.effective = t "effective";
  qr.authored as t -> obs.issued = cast(t, 'instant') "issued";
  // The facility, not the person who signed: SGHIObservation admits only an
  // Organization or a Patient as performer, and the form's signature is a typed
  // name that resolves to neither.
  qr.source as src -> obs.performer = create('Reference') as perf then {
    src.reference as ref -> perf.reference = ref "performerRef";
    src.display as disp -> perf.display = disp "performerDisplay";
  } "performer";
  // One identifier per resource. Copying the response's own would give every
  // Observation from a single form the same business identity.
  qr -> obs.identifier as ident then {
    qr -> ident.use = 'official' "identifierUse";
    qr -> ident.type as identifierTypeCc then {
      qr -> identifierTypeCc.coding as identifierTypeCoding then {
        qr -> identifierTypeCoding.system = 'https://fhir.slade360.co.ke/fhir/CodeSystem/default-identifier-codesystem' "identifierTypeSystem";
        qr -> identifierTypeCoding.code = 'default-id' "identifierTypeCode";
        qr -> identifierTypeCoding.display = 'Default Resource Identifier' "identifierTypeDisplay";
      } "identifierTypeCodingRule";
    } "identifierType";
    qr -> ident.system = 'https://fhir.slade360.co.ke/fhir/CodeSystem/default-identifier-codesystem' "identifierSystem";
    qr -> ident.value = uuid() "identifierValue";
    qr.source as src -> ident.assigner = src "identifierAssigner";
  } "identifier";
  // No append: under this engine `qr.id` already resolves to the type-qualified
  // relative reference, so prefixing it again produces a reference to nothing.
  qr -> obs.derivedFrom = create('Reference') as ref then {
    qr.id as qid -> ref.reference = qid "derivedFromRef";
  } "linkQuestionnaireResponse";
}

group SetCodedValue(source a, target obs : Observation) {
  a.value as cv -> obs.value = create('CodeableConcept') as concept then {
    cv -> concept.coding = cv "valueCoding";
    cv.display as d -> concept.text = d "valueText";
  } "codedValue";
}

group SetObservationFullUrl(source obs : Observation, target entry) {
  obs.id as id -> entry.fullUrl = append('https://fhir.slade360.co.ke/fhir/Observation/', id) "assignFullUrl";
}