Metadata-Version: 2.4
Name: aimedicalcoding
Version: 0.3.10
Summary: Detect missing/broken clinical codes (LOINC / SNOMED CT / RxNorm / ICD-10-CM / CVX) in CCDA and FHIR documents, with recovery evidence
Author: Aakash Kag, Vinay Shankar Miryala, Siva Karthik, Mohammed Rayaan
License: MIT
Keywords: loinc snomed rxnorm icd-10-cm cvx ccda fhir clinical-terminology medical-coding interoperability
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Healthcare Industry
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: lxml>=4.9
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Dynamic: author
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: keywords
Dynamic: license
Dynamic: license-file
Dynamic: provides-extra
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# aimedicalcoding

**Find every missing or broken standard clinical code in a CCDA or FHIR document — with the evidence needed to fix it.**

`aimedicalcoding` parses a clinical document (CCDA XML or FHIR R4 Bundle JSON),
inspects every coded clinical item — labs, vitals, problems, medications,
immunizations, allergies, procedures, social history — and reports the ones
whose standard terminology code (**LOINC · SNOMED CT · RxNorm · ICD-10-CM ·
CVX**) is missing, local-only, crosswalkable, or malformed. Each finding (a
*gap*) is emitted as a self-contained JSON record carrying the classification,
the document facts, and a ready-made recovery plan (search terms, axis filters,
crosswalk candidates, check-digit corrections).

Items that are already correctly coded produce no output — the result is a
work list, not an inventory.

---

## Installation

```bash
pip install aimedicalcoding
```

Requires Python 3.10+. The only runtime dependency is `lxml`.

---

## Quick start (CLI)

The input format is **auto-detected** — the same command works for CCDA XML
and FHIR JSON, and the output schema is identical for both.

```bash
# human-readable gap report (card view)
aimedicalcoding patient.xml

# FHIR R4 Bundle — same flags, same output
aimedicalcoding bundle.json --table

# counts only
aimedicalcoding patient.xml --summary --table

# machine-readable JSON, written to a file as UTF-8
aimedicalcoding patient.xml --out gaps.json

# restrict to one terminology and/or certain contexts
aimedicalcoding patient.xml --system loinc --contexts result,vital
```

If the entry point is not on your PATH, `python -m aimedicalcoding <file> [flags]`
is equivalent.

### CLI flags

| Flag | Meaning |
|---|---|
| `--system` | `loinc` \| `snomed` \| `rxnorm` \| `icd10cm` \| `cvx` \| `all` (default `all`) |
| `--contexts` | comma-separated contexts to include, e.g. `result,vital,problem` |
| `--table` | compact one-line-per-gap table |
| `--json` | print the JSON report to stdout |
| `-o, --out FILE` | write the JSON report to FILE as UTF-8 (implies `--json`) |
| `--summary` | counts only |
| `--include-narrative` | also report narrative-table-only items (CCDA; off by default) |
| `--apply-dict` | expand lab abbreviations (BUN → urea nitrogen) in search terms |
| `--json-full` | dump the complete internal representation (debugging) |

---

## Quick start (Python)

```python
from aimedicalcoding import extract_gaps

result = extract_gaps("patient.xml")        # CCDA — auto-detected
result = extract_gaps("bundle.json")        # FHIR R4 Bundle — auto-detected
result = extract_gaps(raw_bytes,            # bytes / str / path / file-like
                      systems=("loinc", "snomed", "rxnorm", "icd10cm", "cvx"))

len(result)           # number of gaps
result.by_target      # {"LOINC": 17, "SNOMED-CT": 30, "RxNorm": 4, ...}
result.by_pattern     # {"LOCAL_CODE_ONLY": 15, "NULLFLAVOR": 32, ...}
result.gaps           # list[dict] — one record per (item, missing terminology)
result.to_json()      # the same JSON the CLI emits
```

Lower-level access — parse to the intermediate representation without gap
analysis:

```python
from aimedicalcoding.pipeline import parse_document

statements = parse_document(open("bundle.json", "rb").read())
for s in statements:
    print(s.context, s.raw_text, [(c.system, c.code) for c in s.codings])
```

---

## How it works

The library is built on two independent axes that meet at a format-agnostic
intermediate representation (IR):

```
 FORMAT axis (adapters)              TERMINOLOGY axis (extractors)
   CCDA XML  ──┐                       LOINC      SNOMED CT
   FHIR R4  ───┤──►  ClinicalStatement ──►  RxNorm   ICD-10-CM   CVX
               │         (the IR)
   (auto-detected)                    (identical for every input format)
```

1. **Detect** — XML with a `<ClinicalDocument>` root → CCDA adapter; JSON with
   `"resourceType": "Bundle"` → FHIR adapter.
2. **Parse to IR** — the adapter walks the document and emits one
   `ClinicalStatement` per clinical item: every coding found on the item (root
   code *plus* all translations / `coding[]` entries, each canonicalized by
   system), the human labels, the value (quantity or coded answer), specimen,
   panel membership, timestamps.
3. **Extract gaps** — each terminology extractor examines the statements
   relevant to it (labs/vitals for LOINC, problems/procedures/reactions for
   SNOMED, medications for RxNorm, diagnoses for ICD-10-CM, immunizations for
   CVX), validates any code present (check digits, format), and classifies
   everything else into a gap pattern.
4. **Serialize** — one JSON record per (item × missing terminology), identical
   schema for CCDA and FHIR input.

### What each adapter understands

**CCDA**: document/section/entry structure, `<code>` + `<translation>`
scan-and-identify, `nullFlavor`, `<originalText>` reference resolution into the
narrative, organizer → panel linkage, specimen, interpretation and reference
ranges, narrative-table fallback for sections without structured entries.

**FHIR R4 Bundle**: routing by resource type + `Observation.category` (with
section-tag fallback), `CodeableConcept.coding[]` as primary + translations,
`valueQuantity` / `valueCodeableConcept` / `valueString` / `dataAbsentReason`,
`hasMember` / `DiagnosticReport.result` panel linkage (children inherit context
and specimen), `medicationReference → Medication.code` and
`specimen → Specimen.type` resolution across the Bundle, per-reaction allergy
statements, and normalization of `"unknown"` null-marker strings.

---

## Gap patterns

Each record's `pattern` says *why* the code is missing — which determines how
to fix it:

| Pattern | What's wrong | Recovery route |
|---|---|---|
| `LOCAL_CODE_ONLY` | a local/in-house/CPT code, no standard code | crosswalk local → target, else text search |
| `NULLFLAVOR` | code explicitly declared unknown (`nullFlavor`, `"unknown"`, `dataAbsentReason`) | resolve the recovered label |
| `DISPLAY_ONLY` | only a display name, no code and no absence marker | resolve the label |
| `NARRATIVE_ONLY` | exists only as narrative table text (CCDA, opt-in) | text search, low priority |
| `BAD_CODE` | right shape, fails validation (LOINC Luhn · SNOMED Verhoeff · ICD format) | check-digit / format correction (suggested fix included) |
| `ICD_ONLY` | ICD present, SNOMED missing | official ICD ↔ SNOMED map |
| `SNOMED_ONLY` | SNOMED present, ICD-10-CM missing | official SNOMED → ICD-10-CM map |
| `NDC_ONLY` | NDC present, RxNorm/CVX missing | NDC ↔ RxNorm / CDC NDC ↔ CVX table |
| `RXNORM_ONLY` | RxNorm present, target missing (vaccine → CVX; drug allergen → SNOMED) | CVX ↔ RxNorm map / RxNorm ↔ SNOMED via UMLS |
| `MULTUM_ONLY` / `FDB_ONLY` / `MEDISPAN_ONLY` | proprietary drug vocabulary, RxNorm missing | vendor ↔ RxNorm via UMLS |

---

## Output

The JSON envelope:

```jsonc
{
  "source": "patient.xml",
  "source_format": "ccda",              // or "fhir" — auto-detected
  "targets_checked": ["LOINC", "SNOMED-CT", "RxNorm", "ICD-10-CM", "CVX"],
  "gap_count": 4,
  "by_pattern": { "LOCAL_CODE_ONLY": 1, "NULLFLAVOR": 3 },
  "by_target":  { "LOINC": 1, "SNOMED-CT": 3 },
  "gaps": [ ... ]
}
```

Each gap record separates four concerns — the classification, the facts, the
plan, and the (pending) answer:

```jsonc
{
  "id": 1,
  "target":  { "system": "LOINC", "slot": "code" },   // what's missing, and in which slot
  "pattern": "LOCAL_CODE_ONLY",                        // why
  "section": { "code": "30954-2", "name": "Results", "system": "LOINC" },

  "observation": {                                     // the facts, verbatim
    "code": {
      "display_text": "LIPID PANEL",
      "original_text": "LIPID PANEL",
      "codings": [
        { "code": null,    "system": "LOINC", "system_raw": "http://loinc.org" },
        { "code": "80061", "system": "CPT",   "system_raw": "http://www.ama-assn.org/go/cpt" }
      ]
    },
    "value": null,
    "qualifiers": { "specimen": { "value": "Bld", "confidence": "low" }, "...": "..." }
  },

  "recovery": {                                        // the plan
    "search_terms": ["LIPID PANEL", "lipid panel"],
    "match_key": "component:LIPID PANEL | specimen:Bld | property:? | scale:? | method:-",
    "crosswalk": { "from_system": "http://www.ama-assn.org/go/cpt", "from_code": "80061" },
    "suggested_action": "Try (source_system, local_code) crosswalk FIRST; ..."
  },

  "provenance": { "location": { "type": "fhirpath", "path": "..." } },
  "resolution": { "status": "pending", "code": null, "...": "..." }  // filled by your recovery step
}
```

Reading it: this lab was coded with CPT `80061` but its LOINC slot is empty
(`code: null` — the source wrote a null marker). The record hands you the CPT
crosswalk candidate, the text to search with, and the LOINC axis hints
(specimen/property/scale) to constrain candidates — everything a
crosswalk-database or LLM step needs to fill `resolution`.

---

## Scope and status

- Emits **gap detections + recovery evidence**. It does not itself query
  terminology databases or LLMs — `resolution` is intentionally left `pending`
  for your downstream step.
- Terminologies checked today: LOINC, SNOMED CT, RxNorm, ICD-10-CM, CVX.
  CPT / HCPCS / ICD-10-PCS are planned.
- Validation included: LOINC Luhn check digits, SNOMED Verhoeff check digits
  (incl. long-format extension SCTIDs), ICD-10-CM format rules, CVX shape.
- Code-system identification covers HL7 OIDs, FHIR URIs, `urn:oid:` forms and
  free-text names for 20+ vocabularies (Epic/local OIDs are recognized as
  local codes, HL7 structural vocabularies are excluded from gap detection).

## License

MIT — © 2026 Trove Health.
