JSPM

ebm-pathway-engine

3.3.0
    • ESM via JSPM
    • ES Module Entrypoint
    • Export Map
    • Keywords
    • License
    • Repository URL
    • TypeScript Types
    • README
    • Created
    • Published
    • Downloads 20
    • Score
      100M100P100Q79860F
    • License UNLICENSED

    Clinical pathway decision engine for EBM-based clinical decision support systems

    Package Exports

    • ebm-pathway-engine

    Readme

    EBM Clinical Pathway Assistant

    A high-fidelity Evidence-Based Medicine Clinical Decision Support System for MODHS (Metro Outer Diabetes and Heart Services) clinical pathways. Built with React, TypeScript, Tailwind CSS, and React Flow.

    Overview

    This application provides structured clinical pathway guidance for complex medical conditions, starting with Atrial Fibrillation (AF). It implements the MODHS AF Master Algorithm V2 with governance locks, decision gates, and mandatory data validation to ensure adherence to evidence-based protocols.

    Key Features

    Pathway Navigation

    • Step-by-step wizard interface with progress tracking
    • Automatic decision gate evaluation - transparent routing through validation logic
    • History tracking - Back navigation with preserved state
    • Visual flowchart - Real-time pathway visualization using React Flow

    Node Types

    • Information nodes - Display clinical context and guidance
    • Action nodes - Track mandatory clinical actions
    • Question checklists - Multi-select with configurable validation (allRequired flag)
    • Input nodes - Support for forms, selects, and numeric inputs
    • Decision gates - Auto-evaluated routing based on user answers
    • Terminal results - Pathway outcomes (success, blockers, emergencies)

    Clinical Pathways

    • Atrial Fibrillation (AF) - 382 nodes, 24 steps covering:
      • Patient eligibility & hemodynamic stability
      • AF diagnostic confirmation with ECG evidence
      • Phenotype classification (First-Detected, Paroxysmal, Persistent, Long-Standing Persistent, Permanent)
      • EHRA symptom burden assessment (Class 1-4)
      • Baseline clinical assessment & investigations
      • CHA₂DS₂-VASc stroke risk stratification
      • HAS-BLED bleeding risk assessment
      • Anticoagulation decision support
      • Rhythm control strategy evaluation
      • Rate control management
      • ICD-10-AM coding integration

    Installation

    npm install

    Development

    npm run dev

    Build & Distribution

    Build for Production

    npm run build

    This creates a single self-contained HTML file with all CSS and JavaScript embedded, perfect for sharing with testing teams.

    Quick Build Script

    ./build-package.sh

    Distribution Package

    The build process creates a dist/ folder containing:

    • index.html - Complete application (~460KB) with all assets inlined
    • atrial_fibrillation_v1.json - AF pathway guidelines
    • heart_failure_guidelines_v1.json - HF pathway guidelines
    • README.md - Usage instructions for testers

    To share with testers:

    1. Zip the entire dist/ folder
    2. Testers extract and open index.html in any modern browser
    3. No server or installation required - works completely offline

    Key features of the distribution:

    • ✅ Single HTML file with all assets embedded
    • ✅ Works offline - no internet required
    • ✅ No installation or server needed
    • ✅ JSON guidelines can be updated independently
    • ✅ Cross-platform (Windows, Mac, Linux)
    • ✅ Works with all modern browsers

    EMR Order Integration

    Overview

    Pathways can include order nodes that display clinical orders (Lab, Radiology, Medication) to the clinician. When the host application provides an OrderIntegrationConfig, these nodes expose a Generate Orders button that POSTs the selected orders to the host EMR.

    This feature is opt-in:

    • ✅ Disabled by default — the package works fully offline with no integration config
    • ✅ Host-injected — every URL, header, identifier, and code is supplied by the host at runtime; nothing EMR-specific is hardcoded
    • ✅ EMR-agnostic — default payload builders target the AINQA / KSA primary-care shape, but any EMR can plug in its own shape via payloadBuilders
    • ✅ Crash-safe — every network error is caught and surfaced to the UI; the active pathway session is never disrupted

    If the config is omitted or enabled: false, the Generate Orders button renders in a disabled state and no network calls are made.

    Quick Start

    import { PathwayWizard, OrderIntegrationConfig } from "ebm-pathway-engine";
    
    const orderIntegrationConfig: OrderIntegrationConfig = {
      enabled: true,
      context: {
        patientId: "Patient/14907",
        encounterId: "Encounter/18598",
        facilityId: "Organization/10138",
        tenantId: "1234",
      },
      endpoints: {
        labOrder: "https://emr.example.com/insertOrder",
        radiologyOrder: "https://emr.example.com/insertOrder",
        medicationOrder: "https://emr.example.com/insertMedicationOrder",
      },
      headers: {
        Authorization: `Bearer ${sessionToken}`,
      },
      defaultBuilderMetadata: {
        dbName: "primarycareng_ksa",
        metadataDbName: "ATP_Metadata_KSA",
        labMetadataId: "88b23ef1-7199-4503-b631-2de82ace6e03",
        medicationMetadataId: "your-med-metadata-id",
        labEntity: "CA_OrderLine",
        medicationEntity: "PH_OrderLine",
        orderNatureCode: "CodingMaster/11552",
        priorityCodes: {
          Routine: "CodingMaster/10689",
          Urgent:  "CodingMaster/10690",
          Stat:    "CodingMaster/10691",
        },
        orderCatalogPrefix: "OrderCatalog/",
      },
    };
    
    <PathwayWizard
      pathwayId="cardiology-heart-failure-v1"
      orderIntegrationConfig={orderIntegrationConfig}
    />

    That's the entire wiring. Order nodes in any pathway will now show a working Generate Orders button.

    OrderIntegrationConfig reference

    Top-level shape:

    interface OrderIntegrationConfig {
      enabled: boolean;                                  // Master switch — false disables the button
      context: OrderIntegrationContext;                  // Per-session patient/encounter identifiers
      endpoints: OrderIntegrationEndpoints;              // POST URLs, one per order category
      headers?: Record<string, string>;                  // Optional headers (auth tokens etc.)
      defaultBuilderMetadata?: DefaultBuilderMetadata;   // Required only when using the default builders
      payloadBuilders?: OrderIntegrationPayloadBuilders; // Optional per-order-type overrides
      fetchImpl?: typeof fetch;                          // Optional fetch override (testing)
    }

    context — injected into every order payload:

    Field Purpose Example
    patientId Patient reference (format is EMR-specific) "Patient/14907"
    encounterId Encounter / visit reference "Encounter/18598"
    facilityId Organization / facility reference "Organization/10138"
    tenantId Tenant identifier (multi-tenant EMRs) "1234"

    endpoints — one POST URL per category. Lab and radiology may share the same URL (the default builder differentiates by Oltype):

    Field Purpose
    labOrder POST URL for Laboratory orders
    radiologyOrder POST URL for Radiology orders
    medicationOrder POST URL for Medication orders

    headers — merged into every request. Content-Type: application/json is added automatically; do not duplicate it.

    defaultBuilderMetadata — consumed only by the built-in KSA payload builders. If you supply a custom payloadBuilders.{lab,radiology,medication} for a given category, this block can be omitted for that category.

    Field Purpose
    dbName Logical DB name (e.g. "primarycareng_ksa")
    metadataDbName Metadata DB name (e.g. "ATP_Metadata_KSA")
    labMetadataId Metadata document id used for Lab and Radiology orders
    medicationMetadataId Metadata document id used for Medication orders
    labEntity Entity name for Lab/Rad orders (e.g. "CA_OrderLine")
    medicationEntity Entity name for Medication orders (e.g. "PH_OrderLine")
    orderNatureCode CodingMaster reference for the order nature
    priorityCodes CodingMaster references keyed by "Routine" / "Urgent" / "Stat"
    orderCatalogPrefix Prefix prepended to ClinicalOrder.orderCode (e.g. "OrderCatalog/")

    payloadBuilders — per-category overrides for hosts whose EMR uses a different shape:

    interface OrderIntegrationPayloadBuilders {
      lab?:        (input: PayloadBuilderInput<LabRadiologyOrder>) => unknown;
      radiology?:  (input: PayloadBuilderInput<LabRadiologyOrder>) => unknown;
      medication?: (input: PayloadBuilderInput<MedicationOrder>)   => unknown;
    }
    
    interface PayloadBuilderInput<TOrder> {
      order: TOrder;                                   // The selected clinical order
      context: OrderIntegrationContext;                // Patient / encounter / facility
      metadata: DefaultBuilderMetadata | undefined;    // Pass-through, may be undefined
    }

    Each builder returns the exact JSON body that will be POSTed to the corresponding endpoint.

    Custom payload builder example

    import {
      PathwayWizard,
      OrderIntegrationConfig,
      PayloadBuilderInput,
    } from "ebm-pathway-engine";
    import type { LabRadiologyOrder } from "ebm-pathway-engine";
    
    // FHIR-style ServiceRequest shape for a different EMR
    const fhirLabBuilder = ({ order, context }: PayloadBuilderInput<LabRadiologyOrder>) => ({
      resourceType: "ServiceRequest",
      status: "active",
      intent: "order",
      priority: order.priority.toLowerCase(),
      subject:   { reference: context.patientId },
      encounter: { reference: context.encounterId },
      requester: { reference: context.facilityId },
      code: {
        coding: [{ system: "http://loinc.org", code: order.orderCode, display: order.orderName }],
      },
    });
    
    const orderIntegrationConfig: OrderIntegrationConfig = {
      enabled: true,
      context: { /* … */ },
      endpoints: {
        labOrder:        "https://fhir.example.com/ServiceRequest",
        radiologyOrder:  "https://fhir.example.com/ServiceRequest",
        medicationOrder: "https://fhir.example.com/MedicationRequest",
      },
      payloadBuilders: { lab: fhirLabBuilder, radiology: fhirLabBuilder },
      // No defaultBuilderMetadata needed for lab/rad since we override both;
      // supply it (or a custom medication builder) before sending medications.
    };

    The package will call your builder, JSON-stringify the result, and POST it to the matching endpoint with your headers.

    Testing locally

    A dev-only mock config lives at src/dev/mockOrderIntegrationConfig.ts. It is already wired into src/App.tsx and activates automatically with npm run dev — Generate Orders becomes interactive, each POST is logged to the browser console (collapsed group with URL, headers, payload), and the mock returns a simulated 200 response after a short delay so you can observe the per-row "Sending → Sent" state transition.

    Production safety

    The mock config is excluded from every production artifact:

    • The single-file HTML build (npm run build) gates the import behind import.meta.env.DEV, which Vite statically resolves to false and tree-shakes away
    • The npm library bundle (npm run build:lib, tsup) entries from src/index.ts only, which never imports src/dev/*

    No mock URLs, fake tokens, or dev placeholders ship in either output.

    Project Structure

    src/
    ├── components/
    │   ├── PathwayWizard.tsx       # Main wizard navigation & decision gate logic
    │   ├── PathwayFlow.tsx         # Node rendering engine (handles all node types)
    │   ├── GuidelineMap.tsx        # Flowchart visualization with React Flow
    │   ├── PathSummary.tsx         # Pathway summary & clinical notes
    │   ├── ClinicalSummary.tsx     # Export summary interface
    │   ├── ProgressTimeline.tsx    # Step progress tracker
    │   └── OrderTests.tsx          # Investigation ordering interface
    ├── data/
    │   └── pathways/
    │       └── af/
    │           └── atrial_fibrillation_v1.json  # AF pathway definition (382 nodes)
    ├── types/
    │   └── pathway.ts              # TypeScript interfaces for pathway nodes
    ├── docs/
    │   ├── atrial_fibrillation.md  # AF clinical documentation
    │   └── heart_failure.md        # Future: HF documentation
    ├── App.tsx                     # Main application & pathway loader
    ├── main.tsx                    # Entry point
    └── index.css                   # Global styles with Tailwind

    Branch Structure

    • clean-modhs-pathways-v2 - Current working branch
      • Clean codebase with refactored architecture
      • Working AF pathway with validation
      • Decision gate auto-skip with answer validation
      • Form support for all input types (select, number, form fields)
      • No legacy guideline data

    Technologies

    • React 18 - UI framework
    • TypeScript - Type safety
    • Tailwind CSS - Styling
    • React Flow - Flowchart visualization
    • Dagre - Auto-layout for flowcharts
    • Vite - Build tool & dev server

    Pathway JSON Schema

    Each pathway follows a standardized schema:

    {
      "id": "unique_node_id",
      "type": "information" | "action" | "question_checklist" | "input" | "decision_gate" | "terminal_result",
      "title": "Node Title",
      "content": "Main text content",
      "metadata": {
        "step": "1.1",
        "allRequired": true,  // For checklists
        "hardStop": "Validation message"
      },
      "nextId": "next_node_id" | null,
      "logic": {  // For decision_gate nodes
        "passRoute": "node_id_if_pass",
        "failRoute": "node_id_if_fail"
      }
    }

    Key Implementation Notes

    Decision Gate Logic

    • Decision gates auto-skip during navigation - no user interaction required
    • Validation logic evaluates user answers from previous steps
    • Checklist validation supports allRequired: false for "at least one" logic
    • Input validation checks for specific values or general presence

    Form Handling

    • Unified formData state at component level
    • Helper functions: handleFormFieldChange(), isFormValid()
    • Supports multiple field types: select, number, boolean
    • Real-time validation with submit button enable/disable

    Node Type Consolidation

    • Originally had separate input (checkbox) and question_checklist types
    • Consolidated to single question_checklist with allRequired flag
    • allRequired: true = All items must be selected
    • allRequired: false = At least one item must be selected

    Future Enhancements

    • Additional pathways: Heart Failure, Diabetes Management
    • Integration with EMR systems (HL7 FHIR)
    • Real-time clinical note generation
    • Multi-language support
    • Offline mode for mobile devices
    • Role-based access control (Doctor, Nurse, Pharmacist)

    Clinical Validation

    This system implements evidence-based clinical pathways developed by MODHS clinical governance teams. All decision logic, risk stratification tools (CHA₂DS₂-VASc, HAS-BLED), and phenotype classifications follow current Australian clinical guidelines and ICD-10-AM coding standards.

    Important: This is a clinical decision support tool. All clinical decisions must be made by qualified healthcare professionals considering individual patient circumstances.

    AI Assistant Guide

    For LLMs Working on This Codebase

    This section provides context for AI assistants (Copilot, Claude, ChatGPT, etc.) working on this project.

    Architecture Overview

    Core Pattern: This is a wizard-driven pathway navigator with:

    • PathwayWizard.tsx - Controls navigation, evaluates decision gates, manages state
    • PathwayFlow.tsx - Renders individual nodes based on type (switch-case pattern)
    • GuidelineMap.tsx - Visualizes the pathway as a flowchart
    • JSON pathway definitions in src/data/pathways/

    State Management:

    • Component-level state (no Redux/Context)
    • userAnswers - Map of nodeId → user response
    • pathHistory - Array of visited node IDs
    • formData - Object for form field values
    • checklistSelection - Array for checklist selections

    Key Files & Responsibilities

    File Purpose Key Functions
    PathwayWizard.tsx Navigation engine handleNext(), decision gate evaluation loop
    PathwayFlow.tsx Node renderer renderNodeContent(), handleFormFieldChange(), isFormValid()
    App.tsx Pathway loader Loads JSON, initializes wizard
    atrial_fibrillation_v1.json AF pathway data 382 nodes, 24 steps

    Common Tasks

    Adding a New Node Type:

    1. Add type to TypeScript interface in src/types/pathway.ts
    2. Add case in PathwayFlow.tsxrenderNodeContent() switch statement
    3. Add validation logic in PathwayWizard.tsxhandleNext() if needed
    4. Test with a sample node in JSON

    Modifying Decision Gate Logic:

    • Location: PathwayWizard.tsxhandleNext() while loop
    • Pattern: Evaluates currentNode.logic.passRoute vs failRoute
    • Validation: Checks userAnswers[conditionalNodeId] against expected values
    • Important: Must include fallback for general vs specific conditions

    Adding Form Field Types:

    • Location: PathwayFlow.tsxinput case
    • Pattern: Add conditional rendering for new field type (e.g., field.type === "date")
    • Required: Update isFormValid() to validate the new field type
    • Styling: Use Tailwind classes consistent with existing inputs

    Code Patterns & Conventions

    Node Rendering Pattern:

    case "node_type":
      return (
        <div>
          {/* Display title/content */}
          {current ? (
            // Interactive UI for current node
          ) : answer ? (
            // Display saved answer for completed nodes
          ) : null}
        </div>
      );

    Decision Gate Evaluation Pattern:

    while (currentNode.type === "decision_gate") {
      const logic = currentNode.logic;
      // Get conditional answer
      const answer = userAnswers[conditionalNodeId];
      // Evaluate pass/fail
      if (conditionMet) {
        routeToTake = logic.passRoute;
      } else {
        routeToTake = logic.failRoute;
      }
      // Move to next node
      currentNode = nodes.find(n => n.id === routeToTake);
    }

    Form Validation Pattern:

    const isFormValid = (node: PathwayNode) => {
      if (node.inputType !== "form" || !node.fields) return true;
      return node.fields.every((field: any) => {
        const value = formData[field.field || field.id];
        if (field.required && (value === undefined || value === "")) return false;
        return true;
      });
    };

    Critical Implementation Notes

    1. Decision Gates Auto-Skip: Never render decision_gate nodes - they evaluate automatically in navigation loop
    2. Checklist Validation: Use allRequired metadata flag to determine if all items or at least one required
    3. Form State: Always use component-level formData state, never declare state inside switch cases (React hooks violation)
    4. Node IDs: Must be unique across pathway. Reference in nextId, logic.passRoute, logic.failRoute
    5. Back Navigation: Clear answers for nodes after current position to prevent stale data

    Troubleshooting Common Issues

    Issue: "Rendered more hooks than previous render"

    • Cause: useState/useEffect inside conditional blocks or switch cases
    • Fix: Move all hooks to component top level, before any conditions

    Issue: "Cannot redeclare block-scoped variable"

    • Cause: Declaring functions/variables inside multiple switch cases
    • Fix: Hoist functions to component level or rename uniquely per case

    Issue: Decision gate not routing correctly

    • Check: userAnswers[conditionalNodeId] contains expected value
    • Check: Use .value for select options, direct value for checklists
    • Check: Distinguish specific value checks vs general "any answer" checks

    Issue: Form not submitting

    • Check: All required: true fields have values in formData
    • Check: Field identifiers match (field.field or field.id)
    • Check: isFormValid(node) receiving correct node parameter

    File Locations Quick Reference

    • Add new pathway: src/data/pathways/{condition}/{condition}_v{n}.json
    • Modify node rendering: src/components/PathwayFlow.tsx
    • Modify navigation logic: src/components/PathwayWizard.tsx
    • Add TypeScript types: src/types/pathway.ts
    • Modify flowchart: src/components/GuidelineMap.tsx
    • Clinical documentation: docs/{condition}.md

    Testing Checklist

    When modifying pathway logic, test:

    • Forward navigation through all step types
    • Back button preserves and clears answers correctly
    • Decision gates evaluate and route correctly
    • Form validation enables/disables submit button
    • Checklist validation respects allRequired flag
    • Terminal results display correctly
    • Flowchart updates as pathway progresses
    • No TypeScript compilation errors
    • No React hooks violations warnings

    Node Type Reference

    Type User Interaction Answer Format Validation
    information None (Continue button) null None
    action None (Continue button) null None
    question_checklist Multi-select checkboxes string[] Check against allRequired
    input (select) Button selection string Required
    input (form) Form fields {field: value} All required fields
    decision_gate None (auto-skip) N/A Evaluates logic
    terminal_result None (end state) null None

    Metadata Flags Reference

    • allRequired: true/false - Checklist validation mode
    • hardStop - Message shown when validation fails
    • step - Display step number (e.g., "3.1")
    • severity: "high" - Terminal result styling (red vs green)

    Common JSON Structure Patterns

    Checklist with "all required":

    {
      "type": "question_checklist",
      "requiredItems": ["Item A", "Item B", "Item C"],
      "metadata": { "allRequired": true }
    }

    Checklist with "at least one":

    {
      "type": "question_checklist",
      "requiredItems": ["Item A", "Item B", "Item C"],
      "metadata": { "allRequired": false }
    }

    Form with multiple fields:

    {
      "type": "input",
      "inputType": "form",
      "fields": [
        { "field": "field_name", "label": "Display Label", "type": "select|number", "required": true, "unit": "optional" }
      ]
    }

    Decision gate with validation:

    {
      "type": "decision_gate",
      "logic": {
        "conditionalNodeId": "node_to_check",
        "expectedValue": "specific_value_or_null_for_any",
        "passRoute": "node_if_pass",
        "failRoute": "node_if_fail"
      }
    }