@cosyte/transform 0.0.2

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,326 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+ Versions and publishing are managed with [Changesets](https://github.com/changesets/changesets);
8
+ this file is maintained by hand (Changesets handles the version bump and publish only).
9
+
10
+ ## [Unreleased]
11
+
12
+ The first pre-alpha release (`0.0.1`) will ship the initial public API surface. The package begins
13
+ its public history at `0.0.x`, per the cosyte version ladder (`0.0.x` until first alpha).
14
+
15
+ ### Added
16
+
17
+ - **Phase 6 — terminology value translation of coded fields** (roadmap §Phase 6). Coded fields that
18
+ earlier phases carried **structurally** (code preserved, system recognized) are now
19
+ **value-translated** through their IG segment-map `mappedVia` ConceptMaps. Adds a `$translate`-shaped,
20
+ additive, fail-safe engine — `toFhirCodeableConceptVia(cwe, map, ctx?)` — and the license-clean value
21
+ maps it applies, each transcribed and verified **firsthand against the raw published IG ConceptMap
22
+ JSON** (`hl7.fhir.uv.v2mappings`, STU1), down to the nested `TypeInfo → mappedVia` extension rows.
23
+ - **RXR-1 route → `route`/`dosageInstruction.route`** via `table-hl70162-to-v2-0162`
24
+ (`ROUTE_VALUE_MAP`): a 41-code identity group into `v2-0162` plus a 6-code remap into
25
+ `v3-RouteOfAdministration` (`ID→IDINJ`, `IM→IM`, `IV→IVINJ`, `PO→PO`, `SC→SQ`, `TD→TRNSDERM`).
26
+ - **RXR-2 site → `site`/`dosageInstruction.site`** via `table-hl70550-to-v2-0550` (`SITE_VALUE_MAP`):
27
+ the 443-code body-part identity map into `v2-0550`, transcribed verbatim (including the IG's
28
+ as-published `Â` encoding artifacts, preserved for source-fidelity and inert).
29
+ - **SCH-8 appointment type → `appointmentType`** via `table-hl70277-to-v2-0277`
30
+ (`APPOINTMENT_TYPE_VALUE_MAP`).
31
+ - **RXO-9 allow-substitution → `substitution.allowedCodeableConcept`** via `table-hl70161-to-v2-0161`
32
+ (`SUBSTITUTION_VALUE_MAP`, `N`/`G`/`T`), translate-or-withhold — a substitution permission is never
33
+ emitted from an unrecognized code.
34
+ - **OBR-5 priority → `ServiceRequest.priority`** via `table-hl70485-to-request-priority`
35
+ (`SERVICE_REQUEST_PRIORITY_MAP`: `S→stat`, `A→asap`, `R→routine`; every other v2-0485 code — the
36
+ whole `T{S,M,H,D,W,L}<integer>` timing-critical family and `PRN` included — is in the IG's
37
+ `(unmapped)` group → flagged, `priority` left absent).
38
+
39
+ **Grounding discipline — no invented targets.** A source code in the IG map's `(unmapped)` group is
40
+ flagged `TRANSFORM_CODE_UNMAPPED` and the raw coding preserved (or the value withheld), never coerced.
41
+ Translation is **additive** (the derived coding is added alongside the preserved raw coding, including
42
+ any CWE.4/5/6 alternate triplet and CWE.7 version) and **bound-table-guarded** — it fires only when the
43
+ field's primary coding is from the source table (CWE.3 absent or naming it); a field declaring a
44
+ _foreign_ coding system is carried structurally + flagged, never asserted as the standard concept. Two
45
+ fields the IG maps into **SNOMED CT** — **RXR-4 method** (`table-hl70165-to-sct`) and **SCH-7 reason**
46
+ (`table-hl70276-to-sct`) — stay structurally carried (BYO ConceptMap): SNOMED is license-encumbered
47
+ and **not bundled** (§5). Two fields the IG ships **no** value ConceptMap for — **TXA-2 document type**
48
+ and **RXA-5 vaccineCode** — are documented as such and left structural, never given an invented
49
+ translation (ADR 0018). Zero encumbered terminology content is bundled. New public surface (additions
50
+ only): `toFhirCodeableConceptVia`, `codeableConceptFromTarget`, `CodedTarget`, `CodedValueMap`,
51
+ `ROUTE_VALUE_MAP`, `SITE_VALUE_MAP`, `APPOINTMENT_TYPE_VALUE_MAP`, `SUBSTITUTION_VALUE_MAP`,
52
+ `SERVICE_REQUEST_PRIORITY_MAP`, and the target-system URI constants. No new issue codes.
53
+
54
+ - **Phase 5 — the thin IG singles: VXU_V04 → Immunization; SIU_S12 → Appointment; MDM_T02 →
55
+ DocumentReference** (roadmap §Phase 5). `toFhir(msg, opts?)` now assembles the three single-trigger IG
56
+ message families into a FHIR R4 message `Bundle`, alongside the Phase-2 `Patient`/`Encounter`. Every
57
+ segment/field/table/datatype map is grounded firsthand on the published HL7 v2-to-FHIR IG
58
+ (`hl7.fhir.uv.v2mappings`, STU1) ConceptMaps and cited in-source. **With Phase 5 the v2→FHIR direction
59
+ is feature-complete for the IG-covered message set.**
60
+ - **VXU_V04: RXA (+ RXR, ORC) → `Immunization`** (IG _Segment RXA/RXR/ORC to Immunization_; per the
61
+ VXU message map each `ORC` creates the Immunization and the `RXA`/`RXR` are incorporated): RXA-5 →
62
+ `vaccineCode`, RXA-3 → `occurrenceDateTime`, RXA-6/7 → `doseQuantity`, RXA-15 → `lotNumber`, RXA-16 →
63
+ `expirationDate`, RXA-18 → `statusReason`, RXA-19 → `reasonCode`, RXA-22/ORC-9 → `recorded`, RXR-1/2
64
+ → `route`/`site`, ORC-2/3 → `identifier` (PLAC/FILL), and `patient`/`encounter` wired to the bundle.
65
+ `status` follows the IG's three conditioned status rows: a delete action (RXA-21 = `D`) → the
66
+ IG-assigned `entered-in-error`, an unvalued RXA-20 → the IG-assigned `completed`, and a valued RXA-20
67
+ → the **`HL70322` → Event Status** ConceptMap (`IMMUNIZATION_STATUS_MAP`); a valued RXA-20 the map has
68
+ no target for is flagged and the Immunization withheld, never guessed. The order-group `OBX`s become
69
+ standalone patient `Observation`s.
70
+ - **SIU_S12: SCH (+ AIS, PID) → `Appointment`** (IG _Segment SCH/AIS/PID to Appointment_ + _Datatype TQ
71
+ to Appointment_): SCH-25 → `status` via the **`HL70278` → AppointmentStatus** ConceptMap
72
+ (`APPOINTMENT_STATUS_MAP`), SCH-1/2 → `identifier`, SCH-8 → `appointmentType`, SCH-7 → `reasonCode`,
73
+ SCH-9/10 → `minutesDuration`, SCH-11 TQ.4/TQ.5 → `start`/`end`, AIS-3 → `serviceType`, and the bundle
74
+ Patient wired as the required `participant`. An IG-unmatched filler status withholds the Appointment;
75
+ the participant's IG-unsourced required `status` is a `data-absent-reason` primitive, never fabricated.
76
+ - **MDM_T02: TXA (+ OBX) → `DocumentReference`** (IG _Segment TXA/OBX to DocumentReference_): TXA-2 →
77
+ `type`, TXA-6 → `date`, TXA-12 → `masterIdentifier`, TXA-16 → `identifier`, TXA-25 → `description`,
78
+ the `OBX` body → `content.attachment` (TX/FT base64-encoded verbatim, RP as a URL), and `subject`
79
+ wired to the Patient. `status` is grounded only for TXA-19 = `AV` → `current` (the IG ships no value
80
+ ConceptMap; `AV` has exactly one faithful `document-reference-status` target); every other
81
+ availability code withholds the resource and TXA-17 → `docStatus` is left absent (no IG value map).
82
+ - Fail-safe throughout: timezone-naked instants (Appointment `start`/`end`, DocumentReference `date`,
83
+ Immunization date fields) are dropped + flagged rather than assigned a fabricated UTC offset; a
84
+ non-mapped VXU/SIU/MDM trigger is segment-assembled + flagged, never invented; every emitted resource
85
+ passes the conservative-emit gate against `@cosyte/fhir` before it ships. New public exports:
86
+ `IMMUNIZATION_STATUS_MAP`, `APPOINTMENT_STATUS_MAP`, `IG_MAPPED_IMMUNIZATION_TRIGGERS`,
87
+ `IG_MAPPED_APPOINTMENT_TRIGGERS`, `IG_MAPPED_DOCUMENT_TRIGGERS`.
88
+
89
+ - **Phase 4 — ORM_O01 / OML_O21 → ServiceRequest; RXO → MedicationRequest, the order-entry graph**
90
+ (roadmap §Phase 4). `toFhir(msg, opts?)` now assembles order messages into a FHIR R4 message
91
+ `Bundle`: each ORC-anchored order becomes a **`ServiceRequest`** (an `OBR` order detail) or a
92
+ **`MedicationRequest`** (an `RXO` pharmacy detail), alongside the Phase-2 `Patient`/`Encounter`.
93
+ Every segment/field/table map is grounded firsthand on the published HL7 v2-to-FHIR IG
94
+ (`hl7.fhir.uv.v2mappings`, STU1) ConceptMaps and cited in-source.
95
+ - **ORC + OBR → `ServiceRequest`** (IG _Segment ORC/OBR to ServiceRequest_; per the ORM_O01/OML_O21
96
+ message maps the `ORC` creates the request and the `OBR` is incorporated into it): ORC-2/3 or
97
+ OBR-2/3 → `identifier` (PLAC/FILL, v2-0203), ORC-1 → `status` via the **`HL70119` → request-status**
98
+ ConceptMap (`REQUEST_STATUS_MAP`, only when ORC-5 is not valued), OBR-4 → `code`, OBR-6 →
99
+ `occurrenceDateTime`, ORC-9 → `authoredOn` (for a `NW` order), OBR-31 → `reasonCode`, and
100
+ `subject`/`encounter` wired to the bundle's Patient/Encounter. `intent` is fixed to the
101
+ `request-intent` code `order` from the order-message context (the IG maps `→ intent` with no value
102
+ table) — a resource-level constant, not a fabricated per-code row.
103
+ - **RXO (+ RXR) → `MedicationRequest`** (IG _Segment RXO/RXR to MedicationRequest_): RXO-1 →
104
+ `medicationCodeableConcept`, RXO-2/3 (+ RXO-4 units) → `dosageInstruction.doseAndRate.doseRange`,
105
+ RXR-1/2/4 → `dosageInstruction.route`/`.site`/`.method`, RXO-11/12 → `dispenseRequest.quantity`,
106
+ RXO-13 → `dispenseRequest.numberOfRepeatsAllowed`, ORC-9 → `authoredOn`. Doses/dispense amounts
107
+ carry the magnitude **precision-exact** and gate units against UCUM (a non-UCUM unit is preserved
108
+ verbatim, `code`/`system` absent + flagged) — never a rescaled magnitude or a fabricated UCUM code.
109
+ - **The fail-safes (never a confident wrong request).** A `ServiceRequest.status` that cannot be
110
+ grounded — an ORC-1 in the IG's HL70119 `(unmapped)` group, or a valued ORC-5 the IG routes through
111
+ an unspecified mapping — is left absent + flagged (`TRANSFORM_CODE_UNMAPPED`) and the required-`status`
112
+ emit gate **withholds** the request rather than guessing. The IG grounds **no** `MedicationRequest`
113
+ status, so it is set to the value-set's own `unknown` (an honest "not known", asserting nothing) +
114
+ flagged (`TRANSFORM_REQUIRED_ELEMENT_UNKNOWN`) — the `request-status` codes the HL70119 table yields
115
+ (`revoked`, …) are **not** valid `medicationrequest-status` codes and are never borrowed. **`RXE` has
116
+ no IG segment map (nor RDE message map) in STU1**, so any `RXE` is flagged (`TRANSFORM_ELEMENT_DROPPED`)
117
+ rather than assembled from a guessed layout, and non-`ORM^O01`/`OML^O21` order families (`OMP`, `OMG`,
118
+ `RDE`, …) are flagged `TRANSFORM_SEGMENT_ASSEMBLED`.
119
+ - New public export: **`REQUEST_STATUS_MAP`** (HL70119 → request-status) and
120
+ **`IG_MAPPED_ORDER_TRIGGERS`**.
121
+ - **Phase 3 — ORU^R01 → DiagnosticReport + Observation, the results graph** (roadmap §Phase 3).
122
+ `toFhir(msg, opts?)` now assembles a parsed HL7 v2 **ORU^R01** message into a FHIR R4 message
123
+ `Bundle` carrying a **`DiagnosticReport`** per OBR with its **`Observation`** results, alongside the
124
+ Phase-2 `Patient`/`Encounter`. Every segment/field/table map is grounded firsthand on the published
125
+ HL7 v2-to-FHIR IG (`hl7.fhir.uv.v2mappings`, STU1) ConceptMaps and cited in-source.
126
+ - **OBR → `DiagnosticReport`** (IG _Segment OBR to DiagnosticReport_): OBR-2/3 → `identifier`
127
+ (PLAC/FILL, v2-0203), OBR-4 → `code`, OBR-7/8 → `effectiveDateTime`/`effectivePeriod`, OBR-22 →
128
+ `issued` (a zoned `instant` only; a naked/date-only value is dropped + flagged), OBR-24 →
129
+ `category` (v2-0074), OBR-25 → `status` via the `HL70123` → diagnostic-report-status ConceptMap
130
+ (`DIAGNOSTIC_REPORT_STATUS_MAP`), and the OBX children → `result` references.
131
+ - **OBX → `Observation`** (IG _Segment OBX to Observation_): **OBX-2 discriminates OBX-5 →
132
+ `value[x]`** (NM → `valueQuantity`, CWE/CE/CF/CNE/IS → `valueCodeableConcept`, SN → structured
133
+ `valueQuantity`/`valueRange`/`valueRatio`, ST/TX/FT → `valueString`; a type with no first-class
134
+ target preserves the raw value as `valueString` + flags it — never a fabricated `Quantity`), OBX-3
135
+ → `code`, OBX-6 → `valueQuantity` units (UCUM-gated), OBX-7 → `referenceRange.text`, OBX-8 →
136
+ `interpretation` via the `HL70078` ConceptMap (`HL70078_INTERPRETATION_CODES`), OBX-11 → `status`
137
+ via the `HL70085` → observation-status ConceptMap (`OBSERVATION_STATUS_MAP`), OBX-14 →
138
+ `effectiveDateTime`.
139
+ - **The "never a confident wrong result" fail-safes.** A **corrected (`C`) / cancelled (`X`)** result
140
+ is modelled exactly and **never emitted as `final`**; an OBX-11/OBR-25 status the IG map has **no
141
+ target** for leaves `status` absent + flagged (`TRANSFORM_CODE_UNMAPPED`) and the required-`status`
142
+ emit gate **withholds** the resource (`TRANSFORM_RESOURCE_INVALID`) rather than guessing; an
143
+ **unrecognized abnormal flag** is surfaced and dropped, never coerced to normal; a numeric
144
+ magnitude is carried through **precision-exact** (read from the raw OBX-5, not a lossy JS `number`).
145
+ - New public surface: `IG_MAPPED_ORU_TRIGGERS`, `DIAGNOSTIC_REPORT_STATUS_MAP`,
146
+ `OBSERVATION_STATUS_MAP`, `HL70078_INTERPRETATION_CODES`. No new issue codes — the existing
147
+ `TRANSFORM_CODE_UNMAPPED` now also covers a table code with no IG-ConceptMap target (its message was
148
+ generalized accordingly), and `TRANSFORM_ELEMENT_DROPPED` covers a deferred richer `value[x]` type.
149
+ - **Phase 2 — ADT → Patient + Encounter, the first message-level assembly** (roadmap §Phase 2). The
150
+ top-level entry `toFhir(msg, opts?)` assembles a parsed HL7 v2 **ADT** message into a FHIR R4
151
+ **message `Bundle`** (a `MessageHeader` first, then the focal resources), establishing the
152
+ message-map → resource-graph pattern later phases reuse. Every segment→resource and field→element
153
+ map is grounded firsthand on the published HL7 v2-to-FHIR IG (`hl7.fhir.uv.v2mappings`, STU1)
154
+ ConceptMaps and cited in-source.
155
+ - **PID → `Patient`** (IG _Segment PID to Patient_): PID-3 → `identifier` (via `toFhirIdentifier`),
156
+ PID-5 → `name`, PID-7 → `birthDate` (reduced to `date` precision; a birth time is dropped +
157
+ flagged), PID-8 → `gender` via the `HL70001` → administrative-gender ConceptMap
158
+ (`ADMINISTRATIVE_GENDER_MAP`; an unmapped sex code leaves `gender` absent, never guessed),
159
+ PID-11 → `address`.
160
+ - **PV1 → `Encounter`** (IG _Segment PV1 to Encounter_ + both `HL70004` tables): PV1-2 → `class` via
161
+ `HL70004` → V3 ActCode (`ENCOUNTER_CLASS_V3_MAP`; the self-mapped classes stay in v2-0004), PV1-2/
162
+ PV1-45 → `status` via `HL70004` → Encounter Status (`ENCOUNTER_STATUS_MAP`; a valued discharge is
163
+ `finished`), PV1-19 → `identifier` (type `VN`), PV1-44/45 → `period`. `Encounter.subject` is wired
164
+ to the bundle Patient.
165
+ - **NK1 → `RelatedPerson`** (IG _Segment NK1 to RelatedPerson_): NK1-2 → `name`, NK1-3 →
166
+ `relationship`, NK1-4 → `address`, `patient` wired to the bundle Patient.
167
+ - **MSH → `MessageHeader`** + the `Bundle` envelope: MSH-9 → `eventCoding` (v2-0003), MSH-3 →
168
+ `source` (the required-but-underivable `source.endpoint` URL is emitted with a `data-absent-reason`
169
+ extension, never fabricated), MSH-7 → `Bundle.timestamp` (only a fully-zoned instant qualifies),
170
+ MSH-10 → `Bundle.identifier`; `MessageHeader.focus` and every intra-bundle reference wire to
171
+ `urn:uuid:` fullUrls that always resolve within the bundle.
172
+ - **Two message-level fail-safes.** A non-IG-mapped trigger is assembled from the reusable segment
173
+ maps and flagged `TRANSFORM_SEGMENT_ASSEMBLED` (never a fabricated message map); every produced
174
+ resource passes a **conservative-emit gate** against `@cosyte/fhir.validateResource` (`Patient`
175
+ strict against the built-in schema; the other types against minimal required-cardinality schemas
176
+ in lenient mode) and a structurally-invalid one — e.g. an Encounter with no `class`/`status` from
177
+ an unmapped patient class — is withheld + flagged `TRANSFORM_RESOURCE_INVALID`, never shipped invalid.
178
+ - New public surface: `toFhir`, `TransformResult`, `IG_MAPPED_ADT_TRIGGERS`, and the exported table
179
+ maps. New **additions-only** issue codes: `TRANSFORM_SEGMENT_ASSEMBLED`, `TRANSFORM_RESOURCE_INVALID`,
180
+ `TRANSFORM_REQUIRED_ELEMENT_UNKNOWN`. `TransformOptions` gains `namingSystem` and a `generateId`
181
+ allocator (for reproducible fullUrls). Property + fuzz coverage over the message boundary
182
+ (never-throw, value-free registered issues, references resolve, every Patient validates strict).
183
+ - **Phase 1 — the datatype foundation + the value-free diagnostic channel** (roadmap §Phase 1).
184
+ `@cosyte/transform` is the HL7 v2 → FHIR R4 **transformation** tier (a consumer of `@cosyte/hl7` +
185
+ `@cosyte/fhir`), not a parser. Every mapping is grounded firsthand on the published HL7 v2-to-FHIR
186
+ Implementation Guide (`hl7.fhir.uv.v2mappings`, STU Edition 1) datatype/table ConceptMaps.
187
+ - The six safety-critical datatype converters, each fail-safe and IG-grounded:
188
+ `toFhirDateTime` (DTM/TS → `dateTime`; a timezone-less time is reduced to date precision, never a
189
+ guessed UTC), `toFhirIdentifier` (CX → `Identifier`; the assigning authority resolves via a
190
+ NamingSystem registry, **never** synthesized from HD.1 alone), `toFhirCodeableConcept` (CWE/CE →
191
+ `CodeableConcept`; an unmapped code is preserved + flagged, never coerced),
192
+ `toFhirHumanName` (XPN → `HumanName`; HL70200 → name-use), `toFhirAddress` (XAD → `Address`; the
193
+ value-conditional XAD.7 split over HL70190 → address-use/type), `toFhirQuantity` (NM + units →
194
+ `Quantity`; magnitude carried precision-exact, non-UCUM unit preserved verbatim, never converted).
195
+ - The `OperationOutcome`-shaped, **value-free** diagnostic channel: `TransformIssue`, the stable
196
+ `ISSUE_CODES` + `FATAL_CODES` registries (`key === value`; renaming/removing one is breaking),
197
+ the `issue` factory, and `toOperationOutcome(issues)`.
198
+ - The minimal NamingSystem resolver: `createNamingSystem`, `DEFAULT_V2_CODE_SYSTEMS`,
199
+ `V2_0203_SYSTEM` — HD → `Identifier.system` (safe OID/UUID auto-derivation only) and v2 Table 0396
200
+ mnemonic → canonical URI (FHIR-core-fixed systems only; full THO crosswalk deferred to Phase 6).
201
+ - Property + fuzz coverage over the datatype boundary (never-throw, registered value-free issues,
202
+ and an emit gate against `@cosyte/fhir.validateResource`).
203
+ - **`@cosyte/hl7` + `@cosyte/fhir` as peer dependencies**, consumed as vendored `pnpm pack` tarballs
204
+ in `vendor/` for dev/test (ADR 0001 + umbrella ADR 0008), with `scripts/vendor-refresh.sh`. Pinned
205
+ sibling commits: `@cosyte/hl7` `46d50eb`, `@cosyte/fhir` `7a099b2`. **Third-party runtime deps: 0.**
206
+ - Two architecture ADRs (`documentation/decisions/`): `0001` — the transformation tier may depend on
207
+ the parser tier; third-party runtime deps stay zero. `0002` — terminology is a separate
208
+ `@cosyte/terminology` sibling; value translation is BYO-ConceptMap.
209
+
210
+ ### Changed
211
+
212
+ - **CI-REQUIRED-CHECKS: the build checks now BIND on `main`, and DEPENDABOT-PR-QUEUE: dependencies
213
+ are watched.** Until `PUBLIC-SURFACE-HYGIENE` (#11) this repo had **no ruleset at all**; that
214
+ change created `19914044` requiring exactly one context, `no-internal-refs`. The result is the
215
+ shape worth naming: **a repo can have a ruleset and still not bind its build.** `ci / verify` on
216
+ both matrix legs, `ci / actionlint` and `codeql / analyze` all stayed advisory, so any of them
217
+ could be red and the merge would still land on the branch that publishes. The four contexts are
218
+ now **folded into `19914044`** (renamed `ci-required-checks`) rather than added as a second
219
+ ruleset, deliberately: `ncpdp` is the cautionary case, where a correctly pinned base ruleset sat
220
+ beside two later rulesets that pinned nothing and the repo read as "pinned" because one of its
221
+ rulesets was. One ruleset per repo is one place to audit. Final state, read back live: five
222
+ contexts, each pinned to `integration_id: 15368`; `bypass_actors: []`; `~DEFAULT_BRANCH`; plus
223
+ `deletion` and `non_fast_forward`.
224
+ - **The context names were read off real check runs**, never off a workflow `name:` field.
225
+ Provenance stated exactly, because "two heads" is not true of all five: the four build contexts
226
+ were read off **two** independent `pull_request` heads (`66715e5b`, head of #11; `460bfcf8`,
227
+ head of #7); `no-internal-refs` could only be read off `66715e5b`, since `460bfcf8` predates the
228
+ workflow that emits it. All five were then confirmed together on this change's own PR (#12,
229
+ first head `57a62b2`), which read `BLOCKED` until they landed and `CLEAN` after, on that head
230
+ and on every later one. The
231
+ workflow named `Public-surface gate` emits the context `no-internal-refs`, and requiring a name
232
+ nothing emits leaves every PR **pending**, not failing, forever. `scorecard / analysis` and
233
+ `release / release` are excluded because neither has a `pull_request` trigger; the Advanced
234
+ Security `CodeQL` check (app `57789`) is excluded because it reports **alert state**, not
235
+ whether the analysis ran. No workflow here carries a `paths:` filter.
236
+ - **What the ruleset still does not protect, measured rather than asserted.** A required _job_
237
+ gates its _steps_, but the suites that job runs are chosen by the `include` glob in
238
+ `vitest.config.ts`, and the shared `@cosyte/vitest-config` sets no `test.include` of its own, so
239
+ that line decides today. It is not the only lever: the `test`/`test:coverage` script bodies in
240
+ `package.json` are plain `vitest run` invocations, and a path argument or `--exclude` added
241
+ there drops suites without touching the glob. Narrow either and
242
+ `test/messages/property.test.ts` stops running with the job green. Coverage is a thin,
243
+ incidental backstop: excluding that one file takes
244
+ `src/messages/**` branch coverage from **90.11% to 88.82%**, breaching the `>= 90` gate, so the
245
+ deletion is caught today, by 1.29 points, over the incidental fact that the property run is the
246
+ only thing reaching some branches, and never for the loss of the properties themselves. Banners
247
+ on `ci.yml` and `vitest.config.ts` say so.
248
+ - **The cost, expected and measured.** Requiring a context blocks any open PR that cannot emit it.
249
+ **PR #10 ("Version Packages", head `2996df7`) has zero check runs and reports `BLOCKED`**, and it is
250
+ the structural case, since Changesets opens it as `github-actions[bot]` on the default
251
+ `GITHUB_TOKEN` and GitHub starts no workflow runs for that token's events. The escape (one empty
252
+ commit onto `changeset-release/main`) is written on `release.yml`; a bypass actor is refused.
253
+ - **`.github/dependabot.yml` added** (weekly `npm` + `github-actions`, limit 5, dev-dependency
254
+ group). Zero open Dependabot PRs here meant nothing was looking. Two limits are stated in the
255
+ file rather than left to be discovered: `dependabot_security_updates` reads `disabled` on this
256
+ repo, so an advisory opens no fix PR; and Dependabot resolves neither the `file:vendor/*.tgz`
257
+ specifiers nor a peer dependency's registry move, so **both routes to `@cosyte/hl7` and
258
+ `@cosyte/fhir` are unwatched** and stay a `pnpm vendor:refresh` job by hand. Whether the pnpm
259
+ updater tolerates that `file:` shape at all is recorded as unobserved.
260
+ - **Nothing inside this repo can observe its own ruleset.** Delete it and the suite stays green,
261
+ `verify.sh` stays green, and the docs keep asserting protection. `CLAUDE.md` gained a "Branch
262
+ protection (and the limits of this claim)" section that says so and gives the `gh api` calls,
263
+ including `?includes_parents=true`, because checking one ruleset is how `ncpdp` was missed.
264
+ - **The changeset for this entry is deliberately not consumer-facing.** None of the above is
265
+ observable by someone installing the package, so its headline names `CodeQL`, `actionlint` and
266
+ `Dependabot`, which the shared `cosyte/.github` release-note renderer classifies as internal-only
267
+ and **drops from the published release body** rather than rewording into it. Verified by running
268
+ that renderer's `collectHeadlines` over this repo's eight pending changesets: seven kept, this
269
+ one dropped. Recorded because the earlier wording said the same thing in words the classifier
270
+ does not know, and would have published it.
271
+ - **Also noted, not fixed here:** three of the five required names
272
+ (`ci / verify (22|24, ubuntu-latest)`, `ci / actionlint`) are produced by the DEFAULT inputs of
273
+ `cosyte/.github/.github/workflows/ci.yml@main`, a different repo on a floating ref. Changing a
274
+ default there strands every PR in every repo pinning this set. Fails closed, ecosystem-wide,
275
+ and written into `CLAUDE.md`.
276
+ - **PUBLIC-SURFACE-HYGIENE: internal project bookkeeping removed from every surface a consumer
277
+ reads, and a gate added under it.** Founder directive, 2026-07-27: a README, a docs page, an npm
278
+ description, a JSDoc block a consumer's editor renders, and a message their log prints say what
279
+ the software does and what changed, never which internal item, phase or roadmap section produced
280
+ it. Measured on `e6c4531` with the rule set that ships in this change, because a count taken
281
+ against different rules is a different count: **32** violating lines across the public markdown
282
+ surface (`README.md`, `docs-content/intro.md`, `concepts-archetype.md`, `quickstart.md`,
283
+ `guides-overview.md`, `troubleshooting.md`, all of them "Phase N" framing; **zero** item
284
+ identifiers, and zero on the npm metadata) and **54** `src/` doc-comment lines plus **29** blocks
285
+ that matched only once reflowed across their line wraps. The built `dist/index.d.ts` went from
286
+ **45** violating lines to **0**, and `dist/index.d.cts` and the ESM/CJS bundles with it.
287
+ Separately, and **not** found by any rule: one runtime message string, where
288
+ `TRANSFORM_ELEMENT_DROPPED` told a reader an element's conversion was "deferred to a later phase".
289
+ It ends its clause at `phase`, which is the shape the rules deliberately do not cover, so it was a
290
+ reviewer catch. **51** `src/` doc-comment lines carried a roadmap-section citation
291
+ (`(roadmap §4.5)`, `(roadmap §Phase 5)`, or the bare `(§4.7)`) and were cleared by hand; 23 of
292
+ those overlap the 54 counted above, so the union of the two sweeps is 82 lines, not 105. Of the
293
+ 51 citations, 20 name the roadmap explicitly and a rule now catches them, 4 read `§Phase N` and
294
+ are caught by the ordinary phase rule, and the remaining **28** are bare section numbers that
295
+ nothing guards; that non-catch is deliberate and its reasoning is recorded in the script. Two `§`
296
+ citations remain in `//` comments, which this convention keeps out of scope.
297
+ - **`pnpm check:no-internal-refs` + its own CI workflow now gate that rule.** Four passes over the
298
+ README, `LICENSE`, `docs-content/`, the npm `description`/`keywords`, `src/` doc comments and
299
+ `src/` string literals, each scanned line by line and again paragraph-joined so a violation that
300
+ straddles a line wrap cannot hide. It is `hl7`'s gate ported shape-first, with `ncpdp`'s
301
+ string-literal fourth pass; the prefix list is `hl7`'s character for character, and the three rule
302
+ widenings on top of it (`phases?`, `/` in the ADR separator, `roadmap §N`) are each named in the
303
+ script and pinned by their own self-test. **The gate raises the floor; it does not seal the
304
+ category,** and the script writes down thirteen numbered residuals plus the boundaries stated at
305
+ each pass, rather than implying otherwise. The four worth knowing here: `phase` ending a clause
306
+ ("deferred to a later phase);") is not covered; a bare `(§4.7)` is a deliberate non-catch; a doc
307
+ comment that does not open its own line is invisible to the doc-comment extractor (residual (xi),
308
+ inherited from `hl7`) and a violation split across a template literal's line breaks is invisible
309
+ to the string-literal extractor (stated at that pass, which comes from `ncpdp` rather than `hl7`),
310
+ neither of them reachable on this tree today; and prose about our process stays a reviewer's
311
+ catch. `CHANGELOG.md` is excluded on purpose even though it ships inside the npm
312
+ tarball: this convention names it as one of the places identifiers belong. That contradiction is
313
+ ecosystem-wide and is recorded rather than settled here.
314
+ - **Replaced the parser-template scaffold** with the transformation shape: removed the placeholder
315
+ `parseTransform` / `WARNING_CODES` / `FATAL_CODES` parser stubs and the round-trip property test;
316
+ rewrote `docs-content/`, `README`, and this repo's `CLAUDE.md` for the transformation library.
317
+
318
+ ### Deprecated
319
+
320
+ ### Removed
321
+
322
+ ### Fixed
323
+
324
+ ### Security
325
+
326
+ [Unreleased]: https://github.com/cosyte/transform/commits/main
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cosyte
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.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # @cosyte/transform
2
+
3
+ > HL7 v2 → FHIR R4 transformation for Node.js and TypeScript — **IG-grounded, fail-safe, value-free
4
+ > diagnostics; never a confident wrong FHIR value**.
5
+
6
+ `@cosyte/transform` is the healthcare **transformation** layer of the cosyte suite. Unlike the
7
+ parsers, it is a **consumer**: it takes already-parsed [`@cosyte/hl7`](https://github.com/cosyte/hl7)
8
+ composites and produces validated [`@cosyte/fhir`](https://github.com/cosyte/fhir) model nodes,
9
+ grounded on the official **HL7 Version 2 to FHIR** Implementation Guide (`hl7.fhir.uv.v2mappings`).
10
+
11
+ > **Status:** pre-alpha (`0.0.x`), not yet published to npm. This release ships the
12
+ > six safety-critical datatype converters and the value-free diagnostic channel, the
13
+ > message-level assembly, HL7 v2 **ADT → FHIR Patient + Encounter**, the **ORU^R01 → FHIR
14
+ > DiagnosticReport + Observation** results graph, the order-entry graph — **ORM_O01 /
15
+ > OML_O21 → ServiceRequest** and **RXO → MedicationRequest** — the thin IG singles —
16
+ > **VXU_V04 → Immunization**, **SIU_S12 → Appointment**, **MDM_T02 → DocumentReference** — all
17
+ > via `toFhir(msg)`, and **terminology value translation** of coded fields: route/site,
18
+ > appointment type, order priority, and substitution are now value-translated through their IG
19
+ > `mappedVia` ConceptMaps via `toFhirCodeableConceptVia`, fail-safe on any code the IG leaves unmapped.
20
+ > The v2→FHIR direction is feature-complete for the IG-covered message set; deeper
21
+ > terminology, profiles, and the reverse FHIR → v2 direction are not implemented.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ npm install @cosyte/transform @cosyte/hl7 @cosyte/fhir
27
+ ```
28
+
29
+ `@cosyte/hl7` and `@cosyte/fhir` are **peer dependencies** — the transform maps between the models
30
+ they own. Its own third-party runtime dependencies are **zero**.
31
+
32
+ ## Convert a datatype
33
+
34
+ ```ts
35
+ import { toFhirHumanName } from "@cosyte/transform";
36
+
37
+ const { value, issues } = toFhirHumanName({
38
+ familyName: "Public",
39
+ givenName: "Jane",
40
+ nameTypeCode: "L", // HL7 Table 0200 "Legal name" → FHIR name-use "official"
41
+ });
42
+ // value: a FHIR HumanName node; issues: [] (clean, fully mapped)
43
+ ```
44
+
45
+ Each converter returns `{ value, issues }` — the FHIR datatype node it could faithfully produce, plus
46
+ the value-free diagnostics it raised.
47
+
48
+ ## The fail-safe rule
49
+
50
+ Every conversion is grounded on the IG and **refuses to guess**. On any ambiguity —
51
+
52
+ - a v2 timestamp with a time-of-day but **no timezone** (FHIR forbids time without a zone),
53
+ - an assigning authority that can't be **resolved to a system URI** (never synthesized from a bare
54
+ namespace — that would merge two patients),
55
+ - a code with an **unrecognized or absent coding system**,
56
+ - a unit that isn't valid **UCUM** (magnitudes are never converted),
57
+
58
+ — the converter produces what it _can_ (often reduced in precision) and raises a **typed, value-free
59
+ `TransformIssue`**: a stable code, the v2 location, and the FHIR path — never a value. Render a list
60
+ of issues as a FHIR `OperationOutcome` with `toOperationOutcome(issues)`.
61
+
62
+ ## The six datatype converters
63
+
64
+ | Converter | v2 → FHIR |
65
+ | ----------------------- | -------------------------- |
66
+ | `toFhirDateTime` | DTM/TS → `dateTime` |
67
+ | `toFhirIdentifier` | CX → `Identifier` |
68
+ | `toFhirCodeableConcept` | CWE/CE → `CodeableConcept` |
69
+ | `toFhirHumanName` | XPN → `HumanName` |
70
+ | `toFhirAddress` | XAD → `Address` |
71
+ | `toFhirQuantity` | NM + units → `Quantity` |
72
+
73
+ ## Assemble a message
74
+
75
+ `toFhir(msg)` takes a parsed `@cosyte/hl7` **ADT** message and returns a FHIR R4 **message `Bundle`** —
76
+ a `MessageHeader`, then the `Patient` and `Encounter` (and `RelatedPerson`) it describes — plus the
77
+ value-free issues, each map grounded firsthand on the IG's segment/table ConceptMaps.
78
+
79
+ ```ts
80
+ import { parseHL7 } from "@cosyte/hl7";
81
+ import { toFhir, createNamingSystem } from "@cosyte/transform";
82
+
83
+ const { bundle, issues } = toFhir(parseHL7(raw), {
84
+ namingSystem: createNamingSystem({ authorities: { HOSP: "urn:oid:1.2.840.114350" } }),
85
+ });
86
+ // bundle.type === "message"; every reference resolves to a urn:uuid: fullUrl inside the bundle.
87
+ ```
88
+
89
+ | Segment | FHIR resource | key maps |
90
+ | ------- | --------------- | ------------------------------------------------------------------------ |
91
+ | MSH | `MessageHeader` | MSH-9 → `eventCoding`; MSH-7/10 → `Bundle.timestamp`/`.identifier` |
92
+ | PID | `Patient` | PID-3/5/7/8/11 → `identifier`/`name`/`birthDate`/`gender`/`address` |
93
+ | PV1 | `Encounter` | PV1-2 → `class`/`status` (HL70004); PV1-19/44/45 → `identifier`/`period` |
94
+ | NK1 | `RelatedPerson` | NK1-2/3/4 → `name`/`relationship`/`address` |
95
+
96
+ The fail-safe rule holds at the message level: an unmapped patient class, a naked timestamp, or an
97
+ unresolvable authority becomes a typed issue — never a fabricated value. A trigger the IG has no
98
+ **message** map for is assembled from the segment maps and flagged, never invented; every emitted
99
+ resource is validated against `@cosyte/fhir` before it ships.
100
+
101
+ The same `toFhir(msg)` handles the other message families: **ORU^R01** → `DiagnosticReport` (OBR) +
102
+ `Observation` (OBX), and the order-entry graph — **ORM_O01 / OML_O21** ORC/OBR →
103
+ `ServiceRequest`, **RXO** (+ RXR route) → `MedicationRequest`, with `ServiceRequest.status`
104
+ grounded on the HL70119 → request-status ConceptMap and withheld when it cannot be grounded, and a
105
+ `MedicationRequest` whose IG-ungrounded status is the honest `unknown` rather than a guess. `RXE` has no
106
+ STU1 IG map and is flagged, never assembled.
107
+
108
+ The thin single-trigger families complete the IG-covered message set: **VXU_V04** RXA (+
109
+ RXR route, ORC) → `Immunization` (status via the IG's three conditioned rows — a delete action →
110
+ `entered-in-error`, an unvalued RXA-20 → `completed`, else the HL70322 → event-status ConceptMap, with a
111
+ valued-but-unmapped code withheld), **SIU_S12** SCH/AIS/PID → `Appointment` (status via the
112
+ HL70278 → appointmentstatus ConceptMap, the Patient wired as the required participant, its IG-unsourced
113
+ required status a `data-absent-reason` primitive), and **MDM_T02** TXA/OBX → `DocumentReference` (status
114
+ grounded only for TXA-19 `AV` → `current`, the document body base64-encoded verbatim, carried and never
115
+ interpreted). Timezone-naked instants are dropped and flagged, never assigned a fabricated UTC offset.
116
+
117
+ `toFhirCodeableConceptVia(cwe, map)` applies a license-clean IG value ConceptMap
118
+ (transcribed and verified firsthand against the raw published IG JSON) — **RXR** route/site
119
+ (HL70162/HL70550), **SCH-8** appointment type (HL70277), **RXO-9** substitution (HL70161), and **OBR-5**
120
+ priority (HL70485) — translating the source table code to its FHIR target coding, additively (the raw
121
+ coding is preserved alongside the derived one). It is fail-safe by refusal: a code the IG map leaves in
122
+ its `(unmapped)` group is flagged, never coerced to a neighbour. Fields whose IG target is **SNOMED CT**
123
+ (RXR-4 method, SCH-7 reason) stay structural — SNOMED is not bundled (BYO ConceptMap) — and fields the
124
+ IG ships no value map for (TXA-2 document type, RXA-5 vaccine code) are carried as-is, never invented.
125
+
126
+ ## License
127
+
128
+ MIT © Cosyte