aimedicalcoding 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. aimedicalcoding-0.3.1/LICENSE +21 -0
  2. aimedicalcoding-0.3.1/MANIFEST.in +14 -0
  3. aimedicalcoding-0.3.1/PKG-INFO +276 -0
  4. aimedicalcoding-0.3.1/README.md +262 -0
  5. aimedicalcoding-0.3.1/README_PYPI.md +239 -0
  6. aimedicalcoding-0.3.1/aimedicalcoding/__init__.py +29 -0
  7. aimedicalcoding-0.3.1/aimedicalcoding/__main__.py +313 -0
  8. aimedicalcoding-0.3.1/aimedicalcoding/adapters/__init__.py +3 -0
  9. aimedicalcoding-0.3.1/aimedicalcoding/adapters/base.py +25 -0
  10. aimedicalcoding-0.3.1/aimedicalcoding/adapters/ccda/__init__.py +3 -0
  11. aimedicalcoding-0.3.1/aimedicalcoding/adapters/ccda/adapter.py +294 -0
  12. aimedicalcoding-0.3.1/aimedicalcoding/adapters/ccda/codes.py +198 -0
  13. aimedicalcoding-0.3.1/aimedicalcoding/adapters/ccda/narrative.py +121 -0
  14. aimedicalcoding-0.3.1/aimedicalcoding/adapters/ccda/sections.py +44 -0
  15. aimedicalcoding-0.3.1/aimedicalcoding/adapters/custom_json/__init__.py +3 -0
  16. aimedicalcoding-0.3.1/aimedicalcoding/adapters/custom_json/adapter.py +30 -0
  17. aimedicalcoding-0.3.1/aimedicalcoding/adapters/fhir/__init__.py +3 -0
  18. aimedicalcoding-0.3.1/aimedicalcoding/adapters/fhir/adapter.py +326 -0
  19. aimedicalcoding-0.3.1/aimedicalcoding/adapters/fhir/codes.py +211 -0
  20. aimedicalcoding-0.3.1/aimedicalcoding/adapters/fhir/mapping.py +68 -0
  21. aimedicalcoding-0.3.1/aimedicalcoding/api.py +163 -0
  22. aimedicalcoding-0.3.1/aimedicalcoding/core/__init__.py +34 -0
  23. aimedicalcoding-0.3.1/aimedicalcoding/core/checkdigits.py +103 -0
  24. aimedicalcoding-0.3.1/aimedicalcoding/core/models.py +149 -0
  25. aimedicalcoding-0.3.1/aimedicalcoding/core/system_registry.py +185 -0
  26. aimedicalcoding-0.3.1/aimedicalcoding/pipeline/__init__.py +3 -0
  27. aimedicalcoding-0.3.1/aimedicalcoding/pipeline/orchestrator.py +33 -0
  28. aimedicalcoding-0.3.1/aimedicalcoding/recovery/__init__.py +3 -0
  29. aimedicalcoding-0.3.1/aimedicalcoding/recovery/engine.py +16 -0
  30. aimedicalcoding-0.3.1/aimedicalcoding/serialize.py +257 -0
  31. aimedicalcoding-0.3.1/aimedicalcoding/systems/__init__.py +3 -0
  32. aimedicalcoding-0.3.1/aimedicalcoding/systems/base.py +43 -0
  33. aimedicalcoding-0.3.1/aimedicalcoding/systems/cvx/__init__.py +15 -0
  34. aimedicalcoding-0.3.1/aimedicalcoding/systems/cvx/handler.py +55 -0
  35. aimedicalcoding-0.3.1/aimedicalcoding/systems/cvx/normalize.py +75 -0
  36. aimedicalcoding-0.3.1/aimedicalcoding/systems/cvx/patterns.py +134 -0
  37. aimedicalcoding-0.3.1/aimedicalcoding/systems/icd10cm/__init__.py +16 -0
  38. aimedicalcoding-0.3.1/aimedicalcoding/systems/icd10cm/handler.py +55 -0
  39. aimedicalcoding-0.3.1/aimedicalcoding/systems/icd10cm/icdformat.py +43 -0
  40. aimedicalcoding-0.3.1/aimedicalcoding/systems/icd10cm/patterns.py +133 -0
  41. aimedicalcoding-0.3.1/aimedicalcoding/systems/loinc/__init__.py +14 -0
  42. aimedicalcoding-0.3.1/aimedicalcoding/systems/loinc/axes.py +199 -0
  43. aimedicalcoding-0.3.1/aimedicalcoding/systems/loinc/handler.py +80 -0
  44. aimedicalcoding-0.3.1/aimedicalcoding/systems/loinc/normalize.py +81 -0
  45. aimedicalcoding-0.3.1/aimedicalcoding/systems/loinc/patterns.py +166 -0
  46. aimedicalcoding-0.3.1/aimedicalcoding/systems/rxnorm/__init__.py +13 -0
  47. aimedicalcoding-0.3.1/aimedicalcoding/systems/rxnorm/handler.py +55 -0
  48. aimedicalcoding-0.3.1/aimedicalcoding/systems/rxnorm/normalize.py +88 -0
  49. aimedicalcoding-0.3.1/aimedicalcoding/systems/rxnorm/patterns.py +143 -0
  50. aimedicalcoding-0.3.1/aimedicalcoding/systems/snomed/__init__.py +15 -0
  51. aimedicalcoding-0.3.1/aimedicalcoding/systems/snomed/handler.py +60 -0
  52. aimedicalcoding-0.3.1/aimedicalcoding/systems/snomed/hierarchy.py +96 -0
  53. aimedicalcoding-0.3.1/aimedicalcoding/systems/snomed/patterns.py +166 -0
  54. aimedicalcoding-0.3.1/aimedicalcoding/systems/snomed/qualifiers.py +90 -0
  55. aimedicalcoding-0.3.1/aimedicalcoding/terminology/__init__.py +3 -0
  56. aimedicalcoding-0.3.1/aimedicalcoding/terminology/service.py +31 -0
  57. aimedicalcoding-0.3.1/aimedicalcoding.egg-info/PKG-INFO +276 -0
  58. aimedicalcoding-0.3.1/aimedicalcoding.egg-info/SOURCES.txt +71 -0
  59. aimedicalcoding-0.3.1/aimedicalcoding.egg-info/dependency_links.txt +1 -0
  60. aimedicalcoding-0.3.1/aimedicalcoding.egg-info/entry_points.txt +2 -0
  61. aimedicalcoding-0.3.1/aimedicalcoding.egg-info/requires.txt +6 -0
  62. aimedicalcoding-0.3.1/aimedicalcoding.egg-info/top_level.txt +1 -0
  63. aimedicalcoding-0.3.1/docs/ARCHITECTURE.md +242 -0
  64. aimedicalcoding-0.3.1/docs/GAP_SCHEMA.md +202 -0
  65. aimedicalcoding-0.3.1/docs/OUTPUT_GUIDE.md +116 -0
  66. aimedicalcoding-0.3.1/docs/patterns/CVX_pattern_analysis.md +97 -0
  67. aimedicalcoding-0.3.1/docs/patterns/ICD10CM_pattern_analysis.md +185 -0
  68. aimedicalcoding-0.3.1/docs/patterns/LOINC_pattern_analysis.md +501 -0
  69. aimedicalcoding-0.3.1/docs/patterns/RxNorm_pattern_analysis.md +312 -0
  70. aimedicalcoding-0.3.1/docs/patterns/SNOMED_pattern_analysis.md +456 -0
  71. aimedicalcoding-0.3.1/pyproject.toml +9 -0
  72. aimedicalcoding-0.3.1/setup.cfg +4 -0
  73. aimedicalcoding-0.3.1/setup.py +43 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Trove Health
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,14 @@
1
+ # sdist contents. IMPORTANT: setuptools does NOT honor .gitignore — the PHI
2
+ # directories must be pruned explicitly here.
3
+ include LICENSE
4
+ include README.md
5
+ include README_PYPI.md
6
+ recursive-include docs *.md
7
+ prune docs/input
8
+ prune docs/Input_FHIRs
9
+ prune docs/samples
10
+ recursive-exclude aimedicalcoding/tests *
11
+ exclude gaps*.json
12
+ exclude *.gaps.json
13
+ exclude run.sh
14
+ exclude test.py
@@ -0,0 +1,276 @@
1
+ Metadata-Version: 2.4
2
+ Name: aimedicalcoding
3
+ Version: 0.3.1
4
+ Summary: Detect missing/broken clinical codes (LOINC / SNOMED CT / RxNorm / ICD-10-CM / CVX) in CCDA and FHIR documents, with recovery evidence
5
+ Author: Aakash Kag, Vinay Shankar Miryala, Siva Karthik, Mohammed Rayaan
6
+ License: MIT
7
+ Keywords: loinc snomed rxnorm icd-10-cm cvx ccda fhir clinical-terminology medical-coding interoperability
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Healthcare Industry
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: lxml>=4.9
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7; extra == "dev"
24
+ Requires-Dist: build; extra == "dev"
25
+ Requires-Dist: twine; extra == "dev"
26
+ Dynamic: author
27
+ Dynamic: classifier
28
+ Dynamic: description
29
+ Dynamic: description-content-type
30
+ Dynamic: keywords
31
+ Dynamic: license
32
+ Dynamic: license-file
33
+ Dynamic: provides-extra
34
+ Dynamic: requires-dist
35
+ Dynamic: requires-python
36
+ Dynamic: summary
37
+
38
+ # aimedicalcoding
39
+
40
+ **Find every missing or broken standard clinical code in a CCDA or FHIR document — with the evidence needed to fix it.**
41
+
42
+ `aimedicalcoding` parses a clinical document (CCDA XML or FHIR R4 Bundle JSON),
43
+ inspects every coded clinical item — labs, vitals, problems, medications,
44
+ immunizations, allergies, procedures, social history — and reports the ones
45
+ whose standard terminology code (**LOINC · SNOMED CT · RxNorm · ICD-10-CM ·
46
+ CVX**) is missing, local-only, crosswalkable, or malformed. Each finding (a
47
+ *gap*) is emitted as a self-contained JSON record carrying the classification,
48
+ the document facts, and a ready-made recovery plan (search terms, axis filters,
49
+ crosswalk candidates, check-digit corrections).
50
+
51
+ Items that are already correctly coded produce no output — the result is a
52
+ work list, not an inventory.
53
+
54
+ ---
55
+
56
+ ## Installation
57
+
58
+ ```bash
59
+ pip install aimedicalcoding
60
+ ```
61
+
62
+ Requires Python 3.10+. The only runtime dependency is `lxml`.
63
+
64
+ ---
65
+
66
+ ## Quick start (CLI)
67
+
68
+ The input format is **auto-detected** — the same command works for CCDA XML
69
+ and FHIR JSON, and the output schema is identical for both.
70
+
71
+ ```bash
72
+ # human-readable gap report (card view)
73
+ aimedicalcoding patient.xml
74
+
75
+ # FHIR R4 Bundle — same flags, same output
76
+ aimedicalcoding bundle.json --table
77
+
78
+ # counts only
79
+ aimedicalcoding patient.xml --summary --table
80
+
81
+ # machine-readable JSON, written to a file as UTF-8
82
+ aimedicalcoding patient.xml --out gaps.json
83
+
84
+ # restrict to one terminology and/or certain contexts
85
+ aimedicalcoding patient.xml --system loinc --contexts result,vital
86
+ ```
87
+
88
+ If the entry point is not on your PATH, `python -m aimedicalcoding <file> [flags]`
89
+ is equivalent.
90
+
91
+ ### CLI flags
92
+
93
+ | Flag | Meaning |
94
+ |---|---|
95
+ | `--system` | `loinc` \| `snomed` \| `rxnorm` \| `icd10cm` \| `cvx` \| `all` (default `all`) |
96
+ | `--contexts` | comma-separated contexts to include, e.g. `result,vital,problem` |
97
+ | `--table` | compact one-line-per-gap table |
98
+ | `--json` | print the JSON report to stdout |
99
+ | `-o, --out FILE` | write the JSON report to FILE as UTF-8 (implies `--json`) |
100
+ | `--summary` | counts only |
101
+ | `--include-narrative` | also report narrative-table-only items (CCDA; off by default) |
102
+ | `--apply-dict` | expand lab abbreviations (BUN → urea nitrogen) in search terms |
103
+ | `--json-full` | dump the complete internal representation (debugging) |
104
+
105
+ ---
106
+
107
+ ## Quick start (Python)
108
+
109
+ ```python
110
+ from aimedicalcoding import extract_gaps
111
+
112
+ result = extract_gaps("patient.xml") # CCDA — auto-detected
113
+ result = extract_gaps("bundle.json") # FHIR R4 Bundle — auto-detected
114
+ result = extract_gaps(raw_bytes, # bytes / str / path / file-like
115
+ systems=("loinc", "snomed", "rxnorm", "icd10cm", "cvx"))
116
+
117
+ len(result) # number of gaps
118
+ result.by_target # {"LOINC": 17, "SNOMED-CT": 30, "RxNorm": 4, ...}
119
+ result.by_pattern # {"LOCAL_CODE_ONLY": 15, "NULLFLAVOR": 32, ...}
120
+ result.gaps # list[dict] — one record per (item, missing terminology)
121
+ result.to_json() # the same JSON the CLI emits
122
+ ```
123
+
124
+ Lower-level access — parse to the intermediate representation without gap
125
+ analysis:
126
+
127
+ ```python
128
+ from aimedicalcoding.pipeline import parse_document
129
+
130
+ statements = parse_document(open("bundle.json", "rb").read())
131
+ for s in statements:
132
+ print(s.context, s.raw_text, [(c.system, c.code) for c in s.codings])
133
+ ```
134
+
135
+ ---
136
+
137
+ ## How it works
138
+
139
+ The library is built on two independent axes that meet at a format-agnostic
140
+ intermediate representation (IR):
141
+
142
+ ```
143
+ FORMAT axis (adapters) TERMINOLOGY axis (extractors)
144
+ CCDA XML ──┐ LOINC SNOMED CT
145
+ FHIR R4 ───┤──► ClinicalStatement ──► RxNorm ICD-10-CM CVX
146
+ │ (the IR)
147
+ (auto-detected) (identical for every input format)
148
+ ```
149
+
150
+ 1. **Detect** — XML with a `<ClinicalDocument>` root → CCDA adapter; JSON with
151
+ `"resourceType": "Bundle"` → FHIR adapter.
152
+ 2. **Parse to IR** — the adapter walks the document and emits one
153
+ `ClinicalStatement` per clinical item: every coding found on the item (root
154
+ code *plus* all translations / `coding[]` entries, each canonicalized by
155
+ system), the human labels, the value (quantity or coded answer), specimen,
156
+ panel membership, timestamps.
157
+ 3. **Extract gaps** — each terminology extractor examines the statements
158
+ relevant to it (labs/vitals for LOINC, problems/procedures/reactions for
159
+ SNOMED, medications for RxNorm, diagnoses for ICD-10-CM, immunizations for
160
+ CVX), validates any code present (check digits, format), and classifies
161
+ everything else into a gap pattern.
162
+ 4. **Serialize** — one JSON record per (item × missing terminology), identical
163
+ schema for CCDA and FHIR input.
164
+
165
+ ### What each adapter understands
166
+
167
+ **CCDA**: document/section/entry structure, `<code>` + `<translation>`
168
+ scan-and-identify, `nullFlavor`, `<originalText>` reference resolution into the
169
+ narrative, organizer → panel linkage, specimen, interpretation and reference
170
+ ranges, narrative-table fallback for sections without structured entries.
171
+
172
+ **FHIR R4 Bundle**: routing by resource type + `Observation.category` (with
173
+ section-tag fallback), `CodeableConcept.coding[]` as primary + translations,
174
+ `valueQuantity` / `valueCodeableConcept` / `valueString` / `dataAbsentReason`,
175
+ `hasMember` / `DiagnosticReport.result` panel linkage (children inherit context
176
+ and specimen), `medicationReference → Medication.code` and
177
+ `specimen → Specimen.type` resolution across the Bundle, per-reaction allergy
178
+ statements, and normalization of `"unknown"` null-marker strings.
179
+
180
+ ---
181
+
182
+ ## Gap patterns
183
+
184
+ Each record's `pattern` says *why* the code is missing — which determines how
185
+ to fix it:
186
+
187
+ | Pattern | What's wrong | Recovery route |
188
+ |---|---|---|
189
+ | `LOCAL_CODE_ONLY` | a local/in-house/CPT code, no standard code | crosswalk local → target, else text search |
190
+ | `NULLFLAVOR` | code explicitly declared unknown (`nullFlavor`, `"unknown"`, `dataAbsentReason`) | resolve the recovered label |
191
+ | `DISPLAY_ONLY` | only a display name, no code and no absence marker | resolve the label |
192
+ | `NARRATIVE_ONLY` | exists only as narrative table text (CCDA, opt-in) | text search, low priority |
193
+ | `BAD_CODE` | right shape, fails validation (LOINC Luhn · SNOMED Verhoeff · ICD format) | check-digit / format correction (suggested fix included) |
194
+ | `ICD_ONLY` | ICD present, SNOMED missing | official ICD ↔ SNOMED map |
195
+ | `SNOMED_ONLY` | SNOMED present, ICD-10-CM missing | official SNOMED → ICD-10-CM map |
196
+ | `NDC_ONLY` | NDC present, RxNorm/CVX missing | NDC ↔ RxNorm / CDC NDC ↔ CVX table |
197
+ | `RXNORM_ONLY` | RxNorm vaccine product, CVX missing | CVX ↔ RxNorm map |
198
+ | `MULTUM_ONLY` / `FDB_ONLY` / `MEDISPAN_ONLY` | proprietary drug vocabulary, RxNorm missing | vendor ↔ RxNorm via UMLS |
199
+
200
+ ---
201
+
202
+ ## Output
203
+
204
+ The JSON envelope:
205
+
206
+ ```jsonc
207
+ {
208
+ "source": "patient.xml",
209
+ "source_format": "ccda", // or "fhir" — auto-detected
210
+ "targets_checked": ["LOINC", "SNOMED-CT", "RxNorm", "ICD-10-CM", "CVX"],
211
+ "gap_count": 4,
212
+ "by_pattern": { "LOCAL_CODE_ONLY": 1, "NULLFLAVOR": 3 },
213
+ "by_target": { "LOINC": 1, "SNOMED-CT": 3 },
214
+ "gaps": [ ... ]
215
+ }
216
+ ```
217
+
218
+ Each gap record separates four concerns — the classification, the facts, the
219
+ plan, and the (pending) answer:
220
+
221
+ ```jsonc
222
+ {
223
+ "id": 1,
224
+ "target": { "system": "LOINC", "slot": "code" }, // what's missing, and in which slot
225
+ "pattern": "LOCAL_CODE_ONLY", // why
226
+ "section": { "code": "30954-2", "name": "Results", "system": "LOINC" },
227
+
228
+ "observation": { // the facts, verbatim
229
+ "code": {
230
+ "display_text": "LIPID PANEL",
231
+ "original_text": "LIPID PANEL",
232
+ "codings": [
233
+ { "code": null, "system": "LOINC", "system_raw": "http://loinc.org" },
234
+ { "code": "80061", "system": "CPT", "system_raw": "http://www.ama-assn.org/go/cpt" }
235
+ ]
236
+ },
237
+ "value": null,
238
+ "qualifiers": { "specimen": { "value": "Bld", "confidence": "low" }, "...": "..." }
239
+ },
240
+
241
+ "recovery": { // the plan
242
+ "search_terms": ["LIPID PANEL", "lipid panel"],
243
+ "match_key": "component:LIPID PANEL | specimen:Bld | property:? | scale:? | method:-",
244
+ "crosswalk": { "from_system": "http://www.ama-assn.org/go/cpt", "from_code": "80061" },
245
+ "suggested_action": "Try (source_system, local_code) crosswalk FIRST; ..."
246
+ },
247
+
248
+ "provenance": { "location": { "type": "fhirpath", "path": "..." } },
249
+ "resolution": { "status": "pending", "code": null, "...": "..." } // filled by your recovery step
250
+ }
251
+ ```
252
+
253
+ Reading it: this lab was coded with CPT `80061` but its LOINC slot is empty
254
+ (`code: null` — the source wrote a null marker). The record hands you the CPT
255
+ crosswalk candidate, the text to search with, and the LOINC axis hints
256
+ (specimen/property/scale) to constrain candidates — everything a
257
+ crosswalk-database or LLM step needs to fill `resolution`.
258
+
259
+ ---
260
+
261
+ ## Scope and status
262
+
263
+ - Emits **gap detections + recovery evidence**. It does not itself query
264
+ terminology databases or LLMs — `resolution` is intentionally left `pending`
265
+ for your downstream step.
266
+ - Terminologies checked today: LOINC, SNOMED CT, RxNorm, ICD-10-CM, CVX.
267
+ CPT / HCPCS / ICD-10-PCS are planned.
268
+ - Validation included: LOINC Luhn check digits, SNOMED Verhoeff check digits
269
+ (incl. long-format extension SCTIDs), ICD-10-CM format rules, CVX shape.
270
+ - Code-system identification covers HL7 OIDs, FHIR URIs, `urn:oid:` forms and
271
+ free-text names for 20+ vocabularies (Epic/local OIDs are recognized as
272
+ local codes, HL7 structural vocabularies are excluded from gap detection).
273
+
274
+ ## License
275
+
276
+ MIT — © 2026 Trove Health.
@@ -0,0 +1,262 @@
1
+ # aimedicalcoding
2
+
3
+ Recover missing clinical codes (**LOINC · SNOMED CT · RxNorm · ICD-10-CM**) from
4
+ CCDA / FHIR / custom documents. One pass detects every observation whose code is
5
+ missing, local, ICD/NDC/SNOMED-only, malformed, or blank, and emits a
6
+ **target-tagged gap record** for the (DB + LLM) recovery layer. DB and LLM layers
7
+ are placeholders for now.
8
+
9
+ ## Install
10
+
11
+ Editable (dev) from the repo:
12
+ ```bash
13
+ pip install -e .
14
+ ```
15
+ Build and install a wheel:
16
+ ```bash
17
+ python -m build && pip install dist/aimedicalcoding-*.whl
18
+ ```
19
+ Straight from CodeCommit (no registry):
20
+ ```bash
21
+ pip install "git+https://git-codecommit.us-east-1.amazonaws.com/v1/repos/Trident.MedCodeParser@v0.2.0"
22
+ ```
23
+ Version is set manually in `setup.py` (`version="0.3.0"`) — bump it there for
24
+ each release, then build.
25
+
26
+ ## Use as a library
27
+
28
+ ```python
29
+ from aimedicalcoding import extract_gaps
30
+
31
+ result = extract_gaps("patient.xml") # CCDA — checks LOINC + SNOMED + RxNorm
32
+ result = extract_gaps("bundle.json") # FHIR R4 Bundle — same call, auto-detected
33
+ result = extract_gaps(data_bytes, systems=("rxnorm",)) # path | bytes | file-like; pick systems
34
+ result.by_target # {"LOINC": 5, "SNOMED-CT": 6, "RxNorm": 4}
35
+ result.by_pattern # {"LOCAL_CODE_ONLY": 6, ...}
36
+ result.gaps # list[dict] — target-tagged records (see docs/GAP_SCHEMA.md)
37
+ result.to_json() # str
38
+ result.to_dict() # full envelope
39
+ ```
40
+
41
+ ## Use as a CLI
42
+
43
+ ```bash
44
+ aimedicalcoding patient.xml --json # installed entry point
45
+ # or, without installing:
46
+ ./run.sh patient.xml --json
47
+ ```
48
+
49
+ ## Build / publish (dev workflow)
50
+
51
+ Install dev tools (pytest, build, twine) once:
52
+ ```bash
53
+ pip install -e ".[dev]"
54
+ ```
55
+ Run tests:
56
+ ```bash
57
+ pytest
58
+ ```
59
+ Build distributables (version comes from the git tag):
60
+ ```bash
61
+ python -m build
62
+ ```
63
+ Publish privately to AWS CodeArtifact (the repo is on CodeCommit):
64
+ ```bash
65
+ aws codeartifact login --tool twine --domain <domain> --repository <repo>
66
+ twine upload --repository codeartifact dist/*
67
+ ```
68
+
69
+ ## Status
70
+ - **CCDA adapter** — implemented (`adapters/ccda/`): document/section/entry codes,
71
+ scan-and-identify over `<code>` + `<translation>`, `<value>`/units/interpretation/
72
+ reference range, originalText `#reference` resolution, narrative-table fallback,
73
+ organizer→panel linkage.
74
+ - **FHIR adapter** — implemented (`adapters/fhir/`): parses an R4 Bundle
75
+ (Trove flavor) into the same IR. Routing by resource type +
76
+ `Observation.category`, falling back to the CCDA section LOINC stamped in
77
+ `meta.tag` (reuses `SECTION_ROUTING`). Handles `coding[]` as root +
78
+ translations, the Trove `"unknown"` null marker (code / display / system /
79
+ text → normalized to null; all-unknown concepts become a NULLFLAVOR signal),
80
+ `dataAbsentReason`, `hasMember`/`DiagnosticReport.result` → panel linkage
81
+ (children inherit context + specimen), `medicationReference` → `Medication.code`
82
+ and `specimen` → `Specimen.type` resolution across the Bundle, per-reaction
83
+ `allergy_reaction` statements, and value-slot placement for `Condition` /
84
+ `FamilyMemberHistory`. `detect()` requires `resourceType == "Bundle"`.
85
+ - **Custom JSON adapter** — placeholder (config-driven mapping TODO).
86
+ - **systems/loinc/** — LOINC gaps-only extractor (implemented): classifies each
87
+ LOINC-expected observation into a pattern and emits records ONLY for codes that
88
+ need correcting/filling, each with a recovery evidence packet + composite match
89
+ key. Patterns: `LOCAL_CODE_ONLY`, `NULLFLAVOR`, `DISPLAY_ONLY`, `NARRATIVE_ONLY`,
90
+ `BAD_CODE`. Valid LOINCs (incl. via `<translation>`) are skipped.
91
+ - **systems/snomed/** — SNOMED CT gaps-only extractor (implemented): reads the
92
+ concept from the value/answer slot (problems, family hx, social hx, reactions)
93
+ or the code slot (procedures, allergen substance), classifies by section →
94
+ target hierarchy (ECL), and emits gaps. Patterns add `ICD_ONLY` (ICD↔SNOMED
95
+ crosswalk). Valid SNOMED + structural `ASSERTION` codes are skipped.
96
+ - **systems/rxnorm/** — RxNorm gaps-only extractor (implemented): medications
97
+ (CCDA `manufacturedMaterial/code`). Disambiguates by ingredient + strength +
98
+ dose form + route → SCD/SBD. Patterns add `NDC_ONLY` (NDC↔RxNorm crosswalk).
99
+ RxCUI has no check digit (validity = DB existence + TTY).
100
+ - **systems/icd10cm/** — ICD-10-CM gaps-only extractor (implemented): billing
101
+ diagnoses, the mirror of SNOMED (same value slot). Reads value + `<translation>`,
102
+ disambiguates by laterality/encounter/complication. Patterns add `SNOMED_ONLY`
103
+ (SNOMED→ICD-10-CM map). Format-validated (regex), no check digit.
104
+ - **systems/cvx/** — CVX gaps-only extractor (implemented): immunizations
105
+ (CCDA `manufacturedMaterial/code` under `11369-6`). Code slot, no check digit
106
+ (short numeric). Patterns add `RXNORM_ONLY` (CVX↔RxNorm map) and `NDC_ONLY`
107
+ (CDC vaccine NDC↔CVX table); recovery key is the normalized vaccine concept.
108
+ - **terminology/ · recovery/ · CPT/ICD-10-PCS systems** — placeholder contracts only.
109
+
110
+ ## LOINC gap extraction
111
+ ```python
112
+ from aimedicalcoding.systems.loinc import LoincGapExtractor
113
+ recs = LoincGapExtractor().extract_from_document(open("doc.xml", "rb").read())
114
+ for r in recs: # only codes needing work
115
+ print(r.gap.type, r.gap.match_key, r.gap.evidence)
116
+ ```
117
+ Note: the adapters (CCDA and FHIR) still extract *everything* (panel/sibling
118
+ context is needed to recover a gap); the LOINC layer is what filters to gaps-only.
119
+
120
+ ## Layout
121
+ ```
122
+ core/ IR models, system registry (OID/URI/name → System), check digits
123
+ adapters/ FORMAT axis — ccda (done), fhir (done), custom_json (stub)
124
+ systems/ TERMINOLOGY axis — loinc/snomed/rxnorm/icd10cm/cvx extractors
125
+ terminology/ DB service (stub)
126
+ recovery/ LLM engine (stub)
127
+ pipeline/ orchestrator: detect format → run adapter → IR
128
+ tests/ fixtures/sample_ccda.xml + sample_fhir_bundle.json + per-axis tests
129
+ ```
130
+
131
+ ## Run on a document (CLI)
132
+ Works identically for CCDA XML and FHIR JSON — the format is auto-detected and
133
+ the output schema is the same. From the repo root; each block is copy-paste ready.
134
+
135
+ Try the bundled sample (all targets, card view):
136
+ ```bash
137
+ ./run.sh docs/samples/sample_all_systems.xml
138
+ ```
139
+ A FHIR R4 Bundle (same flags, same output):
140
+ ```bash
141
+ ./run.sh /path/to/bundle.json --table
142
+ ```
143
+ Compact table:
144
+ ```bash
145
+ ./run.sh /path/to/patient.xml --table
146
+ ```
147
+ Counts only (per pattern + per target):
148
+ ```bash
149
+ ./run.sh /path/to/patient.xml --summary
150
+ ```
151
+ Target-tagged JSON for downstream consumers (written as UTF-8; prefer `--out`
152
+ over shell `>` — PowerShell redirection re-encodes output as UTF-16):
153
+ ```bash
154
+ ./run.sh /path/to/patient.xml --out gaps.json
155
+ ```
156
+ Filter to one terminology and/or certain sections:
157
+ ```bash
158
+ ./run.sh /path/to/patient.xml --system loinc --contexts result,vital
159
+ ```
160
+ Opt-in extras (abbreviations, narrative-only rows, raw IR):
161
+ ```bash
162
+ ./run.sh /path/to/patient.xml --apply-dict --include-narrative
163
+ ./run.sh /path/to/patient.xml --json-full
164
+ ```
165
+
166
+ Output is **gaps only** — observations whose code needs correcting/filling.
167
+ **One pass checks both LOINC and SNOMED**; each gap is tagged with its `target`
168
+ (`{system, slot}`). `--system` just filters which targets to show.
169
+
170
+ Flags:
171
+ - `--system` — `loinc` | `snomed` | `rxnorm` | `icd10cm` | `all` (default `all`); CPT/ICD-10-PCS planned.
172
+ - `--contexts result,vital` — restrict to certain section contexts (default: all).
173
+ - `--include-narrative` — also report `NARRATIVE_ONLY` (human-readable table text); **off by default**.
174
+ - `--apply-dict` — expand lab abbreviations (BUN→urea nitrogen) in the match key; **off by default**.
175
+ - `--table` / `--json` / `--json-full` / `--summary` — output modes.
176
+ - `-o/--out FILE` — write the JSON report to FILE as UTF-8 (implies `--json`).
177
+
178
+ `run.sh` cds to the repo root, ensures `lxml` is installed, then runs the CLI.
179
+ Equivalent without the wrapper: `python3 -m aimedicalcoding <document> [flags]`.
180
+
181
+ ## Coding systems the parser generates gaps for
182
+ One pass over a document classifies every observation and emits a target-tagged
183
+ gap for each terminology whose code is missing, non-standard, or malformed. The
184
+ five terminologies currently generated, with where each lives in a CCDA and in
185
+ a FHIR Bundle, and which gap patterns each can produce:
186
+
187
+ | Target | CCDA source (slot) | FHIR source (slot) | Gap patterns it can emit |
188
+ |---|---|---|---|
189
+ | **LOINC** | results / vitals / surveys — observation `code` (question) | `Observation.code` (category `laboratory`/`vital-signs`/`social-history`/`survey`) | `LOCAL_CODE_ONLY` · `NULLFLAVOR` · `DISPLAY_ONLY` · `NARRATIVE_ONLY` · `BAD_CODE` |
190
+ | **SNOMED CT** | problems / family hx / social hx / reactions — `value`; procedures / allergen substance — `code` | `Condition.code` / `FamilyMemberHistory.condition[].code` / `Observation.valueCodeableConcept` / `reaction[].manifestation[]` — value slot; `Procedure.code` / `AllergyIntolerance.code` — code slot | `LOCAL_CODE_ONLY` · **`ICD_ONLY`** · `NULLFLAVOR` · `DISPLAY_ONLY` · `NARRATIVE_ONLY` · `BAD_CODE` |
191
+ | **RxNorm** | medications — `manufacturedMaterial/code` | `Medication.code` via `medicationReference` (or inline `medicationCodeableConcept`) | **`NDC_ONLY`** · **`MULTUM_ONLY`** · **`FDB_ONLY`** · **`MEDISPAN_ONLY`** · `LOCAL_CODE_ONLY` · `NULLFLAVOR` · `DISPLAY_ONLY` · `NARRATIVE_ONLY` |
192
+ | **ICD-10-CM** | problems / encounters — diagnosis `value` | `Condition.code` (value slot) | **`SNOMED_ONLY`** · `LOCAL_CODE_ONLY` · `NULLFLAVOR` · `DISPLAY_ONLY` · `NARRATIVE_ONLY` · `BAD_CODE` |
193
+ | **CVX** | immunizations — `manufacturedMaterial/code` | `Immunization.vaccineCode` | **`RXNORM_ONLY`** · **`NDC_ONLY`** · `LOCAL_CODE_ONLY` · `NULLFLAVOR` · `DISPLAY_ONLY` · `NARRATIVE_ONLY` · `BAD_CODE` |
194
+
195
+ FHIR note: Trove bundles use the literal string `"unknown"` as the null marker
196
+ (the CCDA `nullFlavor` equivalent). The FHIR adapter normalizes it everywhere
197
+ (code / display / system / text); an entirely-"unknown" concept is reported as
198
+ a `NULLFLAVOR` gap, and a real translation next to an "unknown" primary (e.g.
199
+ CPT beside an "unknown" LOINC) is reported as `LOCAL_CODE_ONLY` with a
200
+ crosswalk candidate.
201
+
202
+ Planned (contracts stubbed): CPT · HCPCS · ICD-10-PCS.
203
+
204
+ ## Gap patterns (glossary)
205
+ Two families. **Absence/quality patterns** apply to every system; **crosswalk
206
+ patterns** (bold above) mean a code from *another* system is present that maps
207
+ deterministically to the target. Full schema + per-pattern JSON in
208
+ `docs/GAP_SCHEMA.md`.
209
+
210
+ | Pattern | What's wrong | Recovery hint |
211
+ |---|---|---|
212
+ | `LOCAL_CODE_ONLY` | a local/in-house/proprietary code, no standard code | crosswalk local→target, else text+axis search |
213
+ | `NULLFLAVOR` | code deliberately blank (`nullFlavor`); label from originalText/narrative | resolve recovered label |
214
+ | `DISPLAY_ONLY` | a display name (or empty code) only, no usable code | resolve label |
215
+ | `NARRATIVE_ONLY` | exists only as narrative-table text (opt-in) | value + reference range constrain |
216
+ | `BAD_CODE` | right shape but fails validation (LOINC Mod-10 · SNOMED Verhoeff · ICD format · CVX numeric) | check-digit / format correction |
217
+ | `ICD_ONLY` | ICD-9/10 present, no SNOMED | ICD↔SNOMED map (NLM) |
218
+ | `SNOMED_ONLY` | SNOMED present, no ICD-10-CM | SNOMED→ICD-10-CM map (NLM) |
219
+ | `NDC_ONLY` | NDC present, no RxNorm/CVX | NDC↔RxNorm / CDC NDC↔CVX table |
220
+ | `RXNORM_ONLY` | RxNorm vaccine product, no CVX | CVX↔RxNorm map |
221
+ | `MULTUM_ONLY` / `FDB_ONLY` / `MEDISPAN_ONLY` | proprietary drug vocab, no RxNorm | vendor↔RxNorm via UMLS CUI |
222
+
223
+ Skipped (never emitted): a valid code for the target (incl. via `<translation>`),
224
+ and observations whose only code is an HL7 structural marker like `ASSERTION` /
225
+ `ActClass` (the real concept lives in the value slot).
226
+
227
+ ## Output schema (for the recovery / LLM team)
228
+ The `--json` output is **format-agnostic** (identical for CCDA and FHIR) and
229
+ flat. Per gap: `id, pattern, concept, section{}, codings[], absent_reason,
230
+ value{type}, reference_range?, match_key, specimen{}, panel?, checkdigit_correction?,
231
+ suggested_action, location{}`. The full contract, field reference, and a worked
232
+ JSON example per pattern are in **`docs/GAP_SCHEMA.md`** — share that with the LLM team.
233
+
234
+ ## Tests
235
+ ```bash
236
+ pip3 install lxml pytest --break-system-packages
237
+ python3 -m pytest aimedicalcoding/tests/ -q
238
+ python3 -m aimedicalcoding.tests.test_loinc_gaps # prints a gap summary
239
+ python3 -m aimedicalcoding.tests.test_fhir_adapter # prints parsed FHIR statements
240
+ ```
241
+ Fixtures are synthetic (no PHI): `fixtures/sample_ccda.xml`,
242
+ `fixtures/sample_fhir_bundle.json`. Real documents live in `docs/input/` and
243
+ `docs/Input_FHIRs/` (both gitignored — never commit).
244
+
245
+ ## Quick use
246
+ ```python3
247
+ from aimedicalcoding.pipeline import parse_document
248
+ stmts = parse_document(open("doc.xml", "rb").read())
249
+ for s in stmts:
250
+ print(s.context, s.raw_text, [(c.system, c.code) for c in s.codings])
251
+ ```
252
+
253
+ ## Docs
254
+ - **`docs/GAP_SCHEMA.md`** — the JSON output contract for consumers: envelope,
255
+ field reference, the discriminated `value`, and a JSON example per pattern with
256
+ the recommended recovery action. **Share this with the LLM/recovery team.**
257
+ - **`docs/OUTPUT_GUIDE.md`** — how to read the human card/table report.
258
+ - **`docs/ARCHITECTURE.md`** — system design.
259
+ - **`docs/patterns/`** — per-terminology pattern analyses (LOINC, SNOMED, RxNorm):
260
+ structure, identifiers, where each appears in CCDA/FHIR, disambiguation, gap taxonomy.
261
+ - **`docs/samples/`** — example CCDAs (`sample_all_systems.xml` exercises every
262
+ pattern across all three systems).