Skip to content

Version Packages - #45

Open
NSchatz wants to merge 1 commit into
mainfrom
changeset-release/main
Open

Version Packages#45
NSchatz wants to merge 1 commit into
mainfrom
changeset-release/main

Conversation

@NSchatz

@NSchatz NSchatz commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or setup this action to publish automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

Releases

@cosyte/transform@0.0.10

Patch Changes

  • 76984bb: Add a narrow reverse path, FHIR to HL7 v2: toV2Patient(patient, trigger) emits a complete ADT message carrying a PID, and toV2Observation(observation, trigger) a complete ORU message carrying an OBX (roadmap §Phase 7, shipped for two of the three scoped shapes).

    Each takes the FHIR resource plus the v2 trigger the message should carry, and returns the same { value, issues } envelope the forward direction uses, where value is a complete @cosyte/hl7 message. The trigger is required and is never inferred: no FHIR resource carries an HL7 v2 message trigger, so a missing, empty or non-string one returns no message and a TRANSFORM_MISSING_TRIGGER diagnostic without calling the builder at all, and a trigger that is not a bare token (whitespace, or a delimiter that would split MSH-9) returns TRANSFORM_VALUE_NOT_REPRESENTABLE rather than being trimmed into something else.

    This direction is lossy by design and is not a round-trip. The IG maps v2 to FHIR and publishes no map the other way, so every row here is the inverse of a published row, and an inverse is only usable where the forward row is one-to-one. invertCodeMap enforces exactly that, and the many-to-one rows are refused with TRANSFORM_CODE_NOT_INVERTIBLE rather than resolved to their likeliest source code: gender other, name use official and temp, address use work, every Address.type, and Observation.status entered-in-error. An element with no v2 field in this map is flagged TRANSFORM_NO_V2_TARGET, a value v2 cannot carry unchanged is left out with TRANSFORM_VALUE_NOT_REPRESENTABLE, and a coding system with no v2 mnemonic is flagged TRANSFORM_CODE_SYSTEM_NOT_V2 rather than written under a borrowed table. Nothing asserts that a message transformed to FHIR and back equals the original; the property suite verifies only that every emitted message parses back under parseHL7 without a fatal error and carries the caller's trigger verbatim in MSH-9.

    Seven issue codes are added (TRANSFORM_MISSING_TRIGGER, TRANSFORM_UNSUPPORTED_RESOURCE, TRANSFORM_RESOURCE_MALFORMED, TRANSFORM_NO_V2_TARGET, TRANSFORM_VALUE_NOT_REPRESENTABLE, TRANSFORM_CODE_NOT_INVERTIBLE, TRANSFORM_CODE_SYSTEM_NOT_V2), additions only: no existing ISSUE_CODES or FATAL_CODES key is renamed or removed. They are ISSUE_CODES entries because they are returned rather than thrown, which is the structural line between the two registries in this package.

    The third scoped shape, a Patient + Encounter visit-carrying ADT, is deferred with a dated rationale in documentation/decisions/0003: the vendored parser exports no ADT assembly entry point, and hand-assembling that message structure here would invert the tier split ADR 0001 draws.

  • 09138da: Publish what this library's output conformance actually is, instead of a README sentence that reads
    stronger than the check behind it. The guide's seven published v2 test messages are now carried in
    the repository, transformed, and every resource of every resulting Bundle is validated against pinned
    FHIR R4 4.0.1 definitions and the pinned hl7.fhir.us.core 9.0.0 profiles, with the outcome committed
    as documentation/conformance/report.md and documentation/conformance/result.json.

    Nothing about the library's output moved. No mapping, no emitted value, no issue code and no emit
    gate changed, and the package entry point exports exactly what it exported before. This is an
    apparatus, so the result it publishes on day one is a measurement of the library as it stands.

    The result, stated plainly: none of the seven messages produces a Bundle with zero error-severity
    results
    against R4 plus those profiles. Against base R4 4.0.1 alone, six of the seven are clean; the
    exception is the SIU_S12, whose Appointment is booked with no start, which R4's own app-3
    invariant forbids. The profile findings are mostly one shape: US Core requires elements the guide's
    segment maps publish no row for, among them Encounter.type, Coverage.relationship,
    Observation.category and DocumentReference.category, and this library leaves an ungrounded element
    absent rather than guessing at it. Every finding is in the report with its element path, the profile
    canonical it came from and that profile's version, so a result is reproducible against a pin rather
    than against whatever was current. Each per-message table prints one row per finding per resource
    instance, naming which Bundle entry it came from, so the rows count to the number stated above them
    rather than collapsing sibling resources that fail the same way into one.

    The README's old claim, that every emitted resource "is validated against @cosyte/fhir before it
    ships", is gone. What it described is a small internal required-element schema, and the replacement
    says so and points at the measurement. The opening paragraph, where a reader meets the word
    "validated" first, now carries that qualification where they meet it rather than leaving it in a
    section 130 lines down the page.

    Three things keep the number honest. The published result is checked against a hand-written claims
    register on every test run, in BOTH directions: a pair declared conformant that starts failing breaks
    the build, and a pair declared non-conformant that stops failing breaks it too, so the register cannot
    rot into a list of stale excuses. The report states which classes of check were performed and which
    were not, external terminology resolution and mapping correctness among the latter, so it cannot be
    read as a full implementation-guide validator's output. And the two definition packages are carried in
    the repository and verified by digest at run time, with no network fetch and no fallback: an absent,
    unreadable or mismatched package fails the run explicitly rather than reporting a pass, as do an empty
    corpus, a zero-document run, and a corpus message that cannot be parsed or transformed.

    The corpus fixtures carry their own provenance, including the one line the guide's own page mangles
    (an OBX whose Base64 component the page emits as markup), which is recorded as published rather than
    repaired with invented v2 content.

  • 69fc3a2: Carry the diagnoses, procedures and insurance a v2 admit states into the bundle it produces. A DG1 now becomes a Condition, a PR1 a Procedure and an IN1 a Coverage, each wired to the bundle Patient by the guide's own ADT_A01 message-map rows, and each Condition is referenced back from Encounter.diagnosis when the same message also produced an Encounter. Bundles therefore carry three resource types they did not before, and the completeness diagnostic stops reporting those three names as library gaps wherever their occurrences reach an emitted resource. A message carrying none of the three produces exactly the bundle and the issue list it produced before.

    The elements are the ones the published segment maps ground, and no others. Condition takes DG1-3 and DG1-4 as code and code.text, DG1-5 as onsetDateTime, DG1-19 as recordedDate and DG1-20 as identifier. Procedure takes PR1-3 and PR1-4 as code and code.text (the description only where the coded field carries no original text, per the row's own condition), PR1-6 as category, PR1-15 as reasonCode, PR1-19 as identifier, and PR1-5 with PR1-7 as either performedDateTime or a performedPeriod. Coverage takes IN1-2 as identifier, IN1-12 and IN1-13 as period, IN1-15 as type, and IN1-10 (where its identifier type is SN) and IN1-49 as the subscriber-id extension.

    Four refusals matter to a consumer. Coverage.status is never asserted: no published row grounds a coverage status, and R4's binding has no neutral member, so the element ships value-absent with a data-absent-reason of unknown and a TRANSFORM_REQUIRED_ELEMENT_UNKNOWN issue. Read it as unknown, not as active coverage. Coverage.payor names the insurer without resolving it: IN1-4.1 becomes a reference display with no literal reference, because no Organization resource is built, and an IN1 that names no insurance company is withheld entirely with a diagnostic naming the occurrence, since payor is required and nothing else grounds it. Procedure.status is the unknown the map's own row directs where the message context determines none; completed is never selected from a message that did not say so. A DG1-21 of D is the only action code that sets verificationStatus (entered-in-error, the value the row assigns); every other published Table 0206 code leaves the element absent with a TRANSFORM_CODE_UNMAPPED.

    Nothing is dropped without saying so. A diagnosis that grounds neither a code nor a description still becomes a Condition, with its empty code flagged. With no Patient in the bundle every Condition, Procedure and Coverage is withheld and declared per occurrence, naming the reference it could not anchor, so no reference ever points outside the bundle. Every row of the three maps whose target needs a Practitioner, Location or Organization this tier does not build, or resolves by identifier rather than by bundle position, raises a TRANSFORM_ELEMENT_DROPPED naming the v2 field and the FHIR path it did not build. One declaration comes from outside those three maps and is scoped to them: a visit that states a financial class (PV1-20), which the guide routes into the same Coverage an IN1 creates, is declared on a message that carries an IN1, and never on one that does not. No issue code is added, renamed or removed, and no existing conversion changes.

  • 088e29f: Give an ORU consumer the typed value the guide already publishes, instead of a string to re-parse. Five OBX-2 value types that used to degrade to valueString plus a dropped-element flag now reach their mapped FHIR target, and an ORU's specimens and per-result comments reach the bundle.

    DR becomes Observation.valuePeriod, NR becomes valueRange, TM becomes valueTime, and NA becomes valueSampledData. An ED whose OBX-5.4 says Base64 becomes the guide's named valueAttachment extension, and its payload is copied from OBX-5.5 byte-for-byte: not decoded, not re-encoded, not normalized, not validated, not truncated. A bundle that previously carried a raw string in these five cases now carries a period, a range, a time, a waveform or an attachment, so a consumer reading value[x] sees a different shape for the same message.

    The fail-safe floor is unchanged and still reached wherever the map cannot carry the value faithfully: a DR neither of whose bounds is a dateTime this library will emit, an NR neither of whose bounds is a decimal FHIR carries unaltered, a TM carrying a UTC offset (an R4 time admits none, and discarding one would move the clinical instant), an NA carrying a magnitude that would have to be rewritten, and an ED under any encoding other than Base64, all fall back to the raw OBX-5 text as valueString with TRANSFORM_ELEMENT_DROPPED. Where the fallback fires for one of these five, it now carries the whole OBX-5 rather than its first component, so a two-ended DR no longer loses its second bound on the way out. RP is deliberately untouched: its extension target carries the guide's own comment that it is unresolved.

    A SampledData never asserts an origin or a period. R4 makes both required and the guide sources neither, so each ships value-absent with a data-absent-reason and a TRANSFORM_REQUIRED_ELEMENT_UNKNOWN, and the observation still emits. .dimensions and .data follow the guide's own worked examples, with E for a position a repetition did not carry.

    An SPM in an ORU now becomes a Specimen that the DiagnosticReport scoping it references, so it is no longer reported as a segment that reached nothing. A report the emit gate withholds takes its specimens with it: the bundle never carries a DiagnosticReport.specimen pointing at an absent resource, nor a Specimen entry orphaned by a report that was never emitted. Specimen.status is never asserted, because the only row that reaches it does so through a Table 0136 value map this library does not carry, and the parent, collection-quantity and status rows are declared rather than guessed.

    An NTE inside an ORU's OBSERVATION group now becomes Observation.note, one annotation per comment repetition, with NTE-6 as its time. The PATIENT-level and ORDER_OBSERVATION-level NTE rows publish no FHIR target, so neither reaches an observation however close to one it sits, and both are still reported as unread.

    No issue code is added, renamed or removed, and no diagnostic carries a value: not an attachment payload, not a specimen identifier, not a note's text, not an observation magnitude.

  • f753c72: Refuse a PHI scan that withdrew a target it had enumerated. scripts/phi-scan.ts still requires a whole-file --allow-fixture <path> bypass to carry a logged ### <path> entry in phi-scan-overrides.md, and an unlogged one is still rejected before any target is read. What changes is what a logged one buys: an audit trail, and nothing else. The run reads and reports every target it did not withdraw, and then exits 2, the could-not-complete code, naming what it withdrew. The flag can no longer reach exit 0 in any mode.

    The reason is a false clean that the exit code could not express. A withdrawn target is a file the run enumerated and then never opened, and a scan that did not open a file has no verdict about it. While the bypass was honoured the withdrawal left no trace in the exit code at all, so the same invocation over a corpus whose only violator was withdrawn reported no hits and exited 0. Nothing about what the scanner detects is narrowed, and no route that passes no bypass moves: the pre-commit hook and the whole-corpus sweep still exit 0 clean and 1 on hits, and the suite pins both as controls alongside the new refusal.

    The dependency override for js-yaml moves to the range the advisory now covers, >=4.0.0 <4.3.0 resolved at 4.3.0, and the superseded entry beside it is removed rather than left as a second answer to the same question. Install hardening is declared at the repository root, a 24 hour cooldown on newly published versions and a trust policy that fails an install when a package's trust evidence is weaker than an earlier version of the same package. Those two settings need a package manager that knows them, so the pinned pnpm is raised to the version that does; without that raise the file would have decorated rather than defended, and the pinned one refused to install with the file present at all.

    The agent instruction file is brought back under its declared size by pointing at the long form it already carries in documentation/agent-notes.md, which gains the bypass contract in full. No guidance is dropped, every required heading stays, and the checked contract between the two files stays green.

  • 990be26: Corroborate every HL7 v2 field number the PHI scanner reads against a published HL7 v2.5.1, and read seven PHI-bearing fields it previously disclosed as unread (PHI-SCAN-RESIDUALS).

    The segment field list is the detector in this package: the cross-cutting SSN/email floor finds nothing in a corpus whose messages are inline v2 string literals, so a wrong field number is either a missed leak or a false positive on a clinical field. Fifteen numbers had never been checked against any published source (the whole GT1 row, plus PID-6, PID-9, PID-19, PID-20, NK1-30, NK1-33, IN1-18 and IN1-19). All fifteen are now checked against the v2.5.1 segment attribute tables in Chapter 3 (PID 3.4.2, NK1 3.4.5) and Chapter 6 (GT1 6.5.5, IN1 6.5.6), cross-checked against a second version-pinned publication, and none of them was wrong. The GT1 clause citation was: it said 6.5.4.

    Every row in the scanner's coverage table and in the suite's coverage case now carries the v2.5.1 item number the field number was corroborated by, which is the standard's own stable identifier for an element.

    Seven fields that a review had measured as reported-clean are read now, because the same published tables ground them: NK1-26, NK1-31, NK1-32, NK1-37, GT1-2, GT1-4 and IN1-49. This is a union with the previous list rather than a replacement, and the superset is pinned cell by cell: every field that reported before still reports, every deliberate non-field (IN1-17, IN1-7, PID-10, PID-18, NK1-3, GT1-11, PV1-19) still reports nothing, and the tracked corpus stays clean. PHI_SEGMENTS is derived from the union of all five field tables instead of one of them, so a segment added to a single table can no longer go silently unlocated.

  • 8a2f73f: Carry the allergies an HL7 v2 message states into the bundle it produces. A message that contains an AL1 segment now yields one AllergyIntolerance per occurrence, wired to the Patient in the same bundle, so a returned bundle contains a resource type it did not contain before. If you branch on the resource types you receive, or count entries, this is the change to read: an allergy list that used to disappear between the v2 feed and the FHIR record now arrives, and the completeness diagnostic stops reporting an AL1 it emits.

    Every value is grounded on a published HL7 Version 2 to FHIR ConceptMap, transcribed firsthand from the guide at 1.0.0 (published 2025-10-07, retrieved 2026-08-28) and exported beside IG_ALLERGY_VALUE_MAPS_VERSION so a later release of the guide can be told apart from a defect here. clinicalStatus is the active the guide assigns, for the reason the guide gives: constraint ait-1 requires one and no AL1 component can state the retraction that would excuse its absence. AL1-3 becomes code, AL1-5 becomes reaction.manifestation.text, one manifestation per repetition.

    AL1-2 is resolved against two separate maps over Table 0127, one to category and one to type, independently, because that is what the guide publishes and their unmapped sets differ: MA yields the type allergy and no category at all, MC yields neither, and each absence raises its own TRANSFORM_CODE_UNMAPPED rather than borrowing the other map's answer or a neighbouring code. AL1-4 becomes criticality through the Table 0128 map, whose MO and U have no target: those leave criticality absent and flagged. AllergyIntolerance.reaction.severity is never populated from that component, because the guide offers it only as a local variation conditioned on a severity that was not used equivalently to criticality, which no v2 message states, and emitting both from one code would assert a clinical grading the message does not carry.

    Whatever those maps do or do not translate, the code the sender wrote survives: the guide's alternate-codes extension carries the original AL1-2 code in v2-0127 on category, and the original AL1-4 code in v2-0128 on criticality, whether or not the translating map had a target for it. An unmapped category is therefore a category element carrying that extension and no code value, not an absent element.

    Two conditions withhold the allergy rather than emit an unsafe one, each with a TRANSFORM_ELEMENT_DROPPED naming what could not be grounded: a message with no Patient to anchor patient, where the alternative would be a reference resolving to nothing; and an AL1-3 that carries neither an allergen code nor allergen text, where the alternative would be an allergy that never says what it is to. A withheld occurrence keeps its completeness issue, so it is still reported rather than silently absent. AL1-6 is read as onsetDateTime only for a message whose version identifier is readable and earlier than 2.7, the version at which the guide records the field as withdrawn; on 2.7 or later, or when the version cannot be read, it is dropped with a diagnostic rather than read as an onset the sender may not have meant. IAM, the guide's other allergy segment, is still not built and still reports TRANSFORM_SEGMENT_NOT_EMITTED.

    No new issue code, no new dependency, and no change to any other message family: an AL1-free message produces exactly the bundle and the issues list it produced before.

  • 673890c: Tell a consumer which segments a message carried that did not reach the returned bundle, instead of leaving the omission to be found by diffing the two by hand. Two new issue codes, TRANSFORM_SEGMENT_NOT_EMITTED and TRANSFORM_SEGMENT_NO_IG_MAP, are raised once per segment occurrence that contributed nothing to a resource the bundle contains, and they split a gap in this library from a gap in the standard so the two read differently.

    Until now an issues list said nothing about an AL1, a DG1 or an IN1, whether the bundle carried it or nobody had looked. The library already refuses to emit a confident wrong FHIR value at the datatype and resource levels; this is the same rule at the segment level, where silence had been reading as completeness. Nothing is transformed that was not transformed before: a flagged segment tells you what is missing, not what it said.

    Reaching means contributing. An occurrence counts as reached only if the assembly took a value or the identity of it into a resource that is present in the returned bundle, so an RXE this library counts rather than assembles is reported, an orphan OBX is reported, and so is a resource the emit gate withheld. A segment whose values were all dropped downstream is not reported here: the resource built from it did reach the bundle, and those drops already have their own codes.

    The location is the only message-derived thing either code carries, in exactly two shapes: NAME[k], 1-based among the occurrences of that name, when the name passes the HL7 v2 segment-identifier shape; and [#n], the segment's position in the message, when it does not, with no part of the name reproduced. The shape is applied by this library to the value the parser published, as a positive test, rather than by excluding a marker constant of the parser package: the peer range admits any parser version, and an exclusion test would promote an unrecognized value into a rendering the first time that constant moved. A position carrying neither a name nor any field content is the parser's ordinal placeholder for a blank line, and raises nothing wherever it sits, while still counting toward later ordinals so they match the line the sender wrote.

    The split between the two codes comes from a hand transcription of the HL7 Version 2 to FHIR Implementation Guide's Segment Maps index at 1.0.0, published 2025-10-07 and retrieved 2026-08-22, exported as IG_MAPPED_SEGMENT_NAMES with its version, publication date, retrieval date and source URL so a later release of the guide can be told apart from a defect here. A name that could not be classified at all takes the no-map code as well, because a damaged identifier is never re-derived from the line; the core concepts documentation states that, and states that these locations are 1-based while the per-occurrence locations the library already emitted are 0-based and have not moved.

    Nothing else changed, and it was measured rather than asserted. Both codes are informational and neither is fatal. Every resource, every value and every issue the library raised before is unchanged, and the new issues are appended after all of them: the Bundle and the full pre-existing issues list were captured for twenty-three inputs from the tree before the diagnostic existed, and both are asserted against those baselines rather than against the new code's own output.

  • 76984bb: Declare, rather than merely leave, what the reverse (FHIR to v2) direction cannot supply. Two issue codes are added, additions only: no existing ISSUE_CODES or FATAL_CODES key is renamed or removed, and no emitted segment content changes.

    TRANSFORM_V2_REQUIRED_FIELD_ABSENT is raised once per v2-required field that ends up absent from an emitted segment because the FHIR resource carried no source this map could ground it from. Until now that absence was silent: a Patient with neither identifier nor name emitted an ADT whose PID carried neither PID-3 (Patient Identifier List) nor PID-5 (Patient Name), and an Observation with no status emitted an ORU whose OBX carried no OBX-11 (Observation Result Status), both with an empty issues array. The field is still left absent, exactly as before, because a placeholder written to satisfy v2 structure would be a fabricated clinical value; what changes is that the receiver is no longer the first to find out.

    TRANSFORM_NO_V2_MESSAGE_EMITTED is raised when a conversion produces no message at all, because nothing in the resource grounded a single field of the target segment. That case previously returned { value: undefined, issues: [] }, which a caller could not tell apart from a successful empty conversion. It is distinct from the refusals that name their own cause (an absent trigger, an unsupported resource type, a structurally malformed resource), each of which still returns its own code.

    Both codes carry an error severity, a v2 location and a FHIR path, and no value, in keeping with the value-free diagnostic contract. Callers that branch on severity will now see these two, which is the intent: an emitted message missing a v2-required field is not conformant, and the diagnostic channel is where that is said.

  • 64930ef: An order's timing segment now reaches the request it times: a TQ1 accompanying an RXO becomes MedicationRequest.dosageInstruction.timing, and a TQ1 accompanying a service order becomes ServiceRequest.occurrenceTiming. Until now a pharmacy order transformed to a drug, a dose range, a route and a dispense request with no schedule at all, so "give 5 mg every 4 hours for 3 days" and "give 5 mg once" arrived as the same resource.

    What a TQ1 grounds. TQ1-3 (RPT) becomes the Timing over four components and only four: RPT.1 Repeat Pattern Code to Timing.code, carried verbatim under the v2-0335 CodeSystem when the published HL70335 map has a row for it; RPT.5 Period Quantity to repeat.period, precision-exact; RPT.6 Period Units to repeat.periodUnit when it already is one of FHIR's seven UnitsOfTime codes; and RPT.8 Event to repeat.when when the published HL70528 map gives it a v3-TimingEvent target. TQ1-7 and TQ1-8 become repeat.boundsPeriod.start and .end, and value either one on its own and you get exactly that endpoint. TQ1-10 is carried verbatim into dosageInstruction.additionalInstruction.text and TQ1-11 into the resource's own text narrative, which are two different targets the guide names separately; neither is written to dosageInstruction.text, which the guide targets from nothing. Both rows are TX, a v2 primitive with no component structure, so both are read whole: a raw ^, & or ~ inside a free-text instruction is content, not structure, and a taper written 2 tabs^then 1 tab arrives with its second half intact instead of truncated at the first delimiter. That includes a delimiter the instruction ends on, which a field's canonical wire form drops as an empty position: on a free-text primitive it is the last character of a clinical instruction, so it is put back. It also includes a row that is nothing but delimiters (^, &~, ^&~), which carries the same characters as ^leading with the letters removed and arrives as the text it is: "valued" on a primitive asks whether the wire put anything in the field, not whether some component position is non-empty, and the composite question would answer no for every one of them. The v2 formatting escapes a narrative carries are resolved on the way in (a delimiter escape to its literal character, a line-break command to a line break); a sequence with no defined rendering is preserved exactly as it stands rather than guessed at.

    Only a row that carries a value is written. The HL7 explicit null, the two-character literal "", is the wire saying a field carries no value; reading a field whole makes that marker look like text, because the marker is the field's characters. It is read here exactly as an absent field: no additionalInstruction, no narrative, no diagnostic about a field that dropped nothing, and a TQ1 whose only content was a null is still reported as not reaching the bundle. A row that did carry content is different even when nothing can be placed from it: content whose whole projection resolves away (a highlight-boundary pair with nothing between it is display markup, not text), and a TQ1-11 of nothing but whitespace, which R4's txt-2 refuses in a narrative ("the narrative SHALL have some non-whitespace content") and which no structural validator here models, each write no element and raise a value-free diagnostic naming the row. TQ1-10 is not whitespace-tested, because its target is a plain string that R4 constrains no further and inventing a rule there would drop text the sender sent.

    A schedule is fully grounded or absent and flagged, and that is the whole safety claim. A half-built timing reads to the receiving system as a complete dosing instruction, and a wrong frequency is a dosing error that no re-run undoes. So the whole Timing is withheld, with a value-free diagnostic naming what caused it, when: any RPT component outside those four is valued (RPT.2 calendar alignment, RPT.7 institution-specified time and RPT.11 general timing specification have no target row at all; RPT.3/RPT.4 need a day translation with no published table behind it; RPT.9/RPT.10 need a rescale to minutes, which this library never performs; and the test is on whatever position the wire carried, so a component past the eleven the datatype defines is refused too rather than read as absent); a repeat-pattern, event, period or period-unit value cannot be carried without inventing a translation; any of the three bound-table components (RPT.1, RPT.6, RPT.8) declares a coding system that is not the table it is read against, because a site's local AC need not mean the published "before meal" concept that shares its spelling and repeat.when's binding is required; a schedule-narrowing field is valued (TQ1-4, TQ1-5, TQ1-6, TQ1-12, TQ1-13, TQ1-14, each of which makes the sent instruction narrower than the one that could be built without it); a bound is valued and yields no FHIR date-time, or the end precedes the start; a period quantity arrives without its period units, or period units arrive without a quantity, or the quantity is written with a minus sign (the test is on the sender's own lexical form, which is what the emitted value preserves, so a negative magnitude too small for a double to tell from zero is refused like any other), each of which produces a Timing.repeat that breaks a published R4 invariant (tim-2 requires period units wherever a period exists, tim-5 requires the period to be non-negative) and which no structural validator models, so it would otherwise reach a consumer as a grounded repeat with nothing to flag it; or one order carries two TQ1 segments, or one TQ1-3 carries two repeat patterns. A repetition of TQ1-3 that carries no value is not one of them: TQ1-3 is 0..-1, so Q4H~, ~Q4H and a second repetition the sender explicitly nulled all arrive in real traffic and each sends exactly one schedule, which is read from whichever repetition carries it. In every one of those cases the request is still emitted, with no timing element and no lone bounds period.

    TQ1-2 quantity and TQ1-9 priority behave differently and deliberately: the guide maps them to the dose and the priority, which the RXO/OBR path already grounds, so they are flagged as dropped, neither field is touched, and the schedule the rest of the segment grounds is still emitted.

    occurrence[x] is a choice, so exactly one member is ever emitted. A service order that would yield both an occurrenceDateTime from OBR-6 and an occurrenceTiming from its TQ1 emits the timing and flags the dropped OBR-6; a refused timing hands the choice back to OBR-6 unchanged.

    A TQ1 that contributed a timing, an additional instruction or a narrative is no longer reported as not reaching the bundle; one that was read and refused still is, because it did not. No issue code is added: every diagnostic here is an existing member of ISSUE_CODES, and an order carrying no TQ1 produces a byte-identical bundle and an identical issue list to before, measured against a captured baseline rather than asserted.

  • 13750f1: Consume @cosyte/hl7 from the registry at 0.0.10 instead of a vendored tarball at 0.0.1, and prove by measurement that nothing this package emits moved with it. No emitted FHIR value changes, no diagnostic changes, no issue code is added, renamed or removed, and no mapping is touched: src/ is byte-for-byte unchanged by this release.

    The parser was nine published versions behind, pinned to a pnpm pack archive committed into the repository, which meant the version the tests exercised was watched by no dependency route and could only move by hand. It is now a plain devDependency resolved through the lockfile, the archive is deleted, and no copy of the library source remains in the tree. @cosyte/fhir is unaffected and stays vendored, for the one reason it always was: the registry does not have it.

    Because this package sits in a PHI dataflow and its promise is never a confident wrong FHIR value, a green suite was not accepted as evidence on its own. Every HL7 v2 fixture the repository carries was collected mechanically into a frozen corpus, four synthetic inputs were authored for the malformed, truncated, empty and wrong-version classes, and all of it was transformed once on each side of the bump. The compared surface, meaning the FHIR output plus every diagnostic in emission order, is byte-identical across all 128 members. Both captures are committed beside the record so the comparison can be rerun rather than believed.

    Two suites are added and none is removed or relaxed. One asserts the fail-safe rule class by class for malformed input, and sweeps every diagnostic the whole corpus raises to confirm each carries a registry-static message at a positional locator and no field content. The other records, as a call that compiles and runs, two upstream capabilities whose earlier absence shaped this package: an ADT message builder and a typed composite encoder both exist now. Neither is adopted here. The visit-carrying Patient plus Encounter output stays deferred, and the reverse path keeps building its fields by hand, because a refresh whose whole claim is that nothing moved cannot also change what is emitted.

@NSchatz
NSchatz force-pushed the changeset-release/main branch 4 times, most recently from e6cafdf to 611b83f Compare September 8, 2026 09:20
@NSchatz
NSchatz force-pushed the changeset-release/main branch from 611b83f to f51c3da Compare September 8, 2026 13:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant