@cosyte/astm 0.0.22 → 0.1.1
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 +346 -7
- package/README.md +559 -224
- package/dist/index.cjs +309 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1061 -184
- package/dist/index.d.ts +1061 -184
- package/dist/index.mjs +301 -65
- package/dist/index.mjs.map +1 -1
- package/package.json +25 -8
package/CHANGELOG.md
CHANGED
|
@@ -9,8 +9,347 @@ this file is maintained by hand (Changesets handles the version bump and publish
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
**What 0.1 means for you.** This release is the first whose public API we treat as settled: the
|
|
13
|
+
exported names, options, return shapes and warning codes are the surface we keep stable. What it
|
|
14
|
+
covers is reading ASTM E1394 (CLSI LIS02-A2) records with the delimiters each header declares,
|
|
15
|
+
checksum-verified E1381 (CLSI LIS01-A2) framing with a pure receiver state machine for the
|
|
16
|
+
`ENQ`/`ACK`/`NAK`/`EOT` exchange, spec-clean emit of records and frames that writes only the values
|
|
17
|
+
you supply, LOINC mapping through a LIVD catalog you supply, and date conversions that never assume
|
|
18
|
+
a timezone. While the package is below 1.0, a breaking change bumps the minor version and is called
|
|
19
|
+
out here with its migration; a fix that changes no public value ships as a patch. Upgrading from
|
|
20
|
+
0.0.x is itself breaking in the LIVD mapping and in what `primaryCode()` returns, as the entries
|
|
21
|
+
below set out. Not covered yet: no named per-vendor profile ships (the built-in set is `default` and
|
|
22
|
+
`referenceCorpus`), no LOINC, SNOMED or LIVD data is bundled and no LOINC is validated, the transfer
|
|
23
|
+
protocol's timers are yours to drive, and three behaviors (forward scoping of redeclared delimiters,
|
|
24
|
+
the Latin-1 wire encoding and the reserved-byte set) are reasoned from this package's reader rather
|
|
25
|
+
than cited to the standard's purchase-gated text.
|
|
26
|
+
|
|
27
|
+
**The pending changeset set is classified `minor`, not `patch`, so the next release is a minor
|
|
28
|
+
release.** Eight changesets are pending and five of them arrived carrying `patch`. Three of those
|
|
29
|
+
five were reclassified against their own text: one removes public values and changes what an
|
|
30
|
+
exported function returns, and two add public values. None of the three is a fix that adds nothing
|
|
31
|
+
and removes nothing, which is the only thing a `patch` may claim, and only those three bump lines
|
|
32
|
+
moved. The other two keep `patch`: one changes repository tooling and install configuration, the
|
|
33
|
+
other changes what a release carries beside the package, and neither touches a published value or
|
|
34
|
+
emits a byte differently, so `patch` is what each one's own text supports. Two more arrived
|
|
35
|
+
carrying `minor` already, each written against this same rule, so both are applied as written rather
|
|
36
|
+
than reclassified. The eighth is the release statement summarized above, which carries `minor` as the
|
|
37
|
+
class of the release it describes and changes no public value. A set carrying a `patch` beside a
|
|
38
|
+
`minor` still resolves to the minor channel.
|
|
39
|
+
|
|
40
|
+
The breaking change below therefore ships in the minor channel of the pre-1.0 ladder, which is where
|
|
41
|
+
a break belongs before `1.0.0`, rather than as a patch. Every break a consumer of the last published
|
|
42
|
+
version would see is enumerated with its migration in `documentation/release-readiness.md`, together
|
|
43
|
+
with the public export surface this release certifies as settled. No version number is written in
|
|
44
|
+
this section's entries, because Changesets owns the bump and `scripts/sync-version.mjs` mirrors it.
|
|
45
|
+
|
|
46
|
+
The entries the pending set carries are the date conversion surface entry, the LIVD units entry, the
|
|
47
|
+
LIVD publication metadata entry and the vocabulary-attribution entry under Added, and the LIVD
|
|
48
|
+
catalog entry, the PHI scanner entry and the dependency inventory entry under Changed. This section
|
|
49
|
+
also holds entries written
|
|
50
|
+
before them, which is where they have always been kept: no released entry was moved, reworded or
|
|
51
|
+
deleted by the reclassification, because rewriting a shipped changelog destroys the traceability it
|
|
52
|
+
exists for.
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- **Published through npm trusted publishing, with provenance.** The code, API and behavior are
|
|
57
|
+
unchanged from 0.1.0. Only the README and some doc comments differ: a few doc comments now write
|
|
58
|
+
number ranges with "to".
|
|
59
|
+
- **Every published release now carries a machine-readable dependency inventory as a downloadable
|
|
60
|
+
asset.** No published value, type, warning code or emitted byte moves: this entry is release
|
|
61
|
+
infrastructure only, and nothing under `src/` is touched. The inventory is a CycloneDX document
|
|
62
|
+
naming this package and the version being built alongside every dependency that build resolved,
|
|
63
|
+
runtime and build-time alike, transitively, each with the exact version it resolved to. It is read
|
|
64
|
+
from the lockfile rather than from the manifest's two lists, because the manifest states ranges
|
|
65
|
+
while a build resolves versions, so the artifact answers the question a reader of a dependency
|
|
66
|
+
claim actually has. A team evaluating this package for a regulated laboratory can download it from
|
|
67
|
+
the release page and feed it to their own tooling without cloning anything or reproducing an
|
|
68
|
+
install. `scripts/check-dependency-inventory.ts` both produces and grades it, from the checkout,
|
|
69
|
+
with no registry credential and no network call, and it is generated into the gitignored
|
|
70
|
+
`dist-artifacts/` rather than committed, so it can never describe a version other than the one it
|
|
71
|
+
ships beside. The grading runs in `prepublishOnly`, which is BEFORE the registry upload: an
|
|
72
|
+
inventory that cannot be produced at all, or that is empty, unparseable, missing this package's
|
|
73
|
+
identity or version, or short of a dependency the build resolved, is refused by name and aborts
|
|
74
|
+
the publish, so a bad inventory costs a failed run rather than a published version nothing
|
|
75
|
+
describes. A refusal also removes any older artifact at that path rather than leaving one to be
|
|
76
|
+
read as this build's. Attaching it to the release is a second job in the release workflow, because
|
|
77
|
+
the shared pipeline attaches a fixed two-name asset list; that job runs after the publish, since
|
|
78
|
+
the release does not exist before one, and it fails its run rather than skipping quietly if the
|
|
79
|
+
asset does not arrive. What the inventory does not claim: it states what was resolved and nothing
|
|
80
|
+
about whether that is safe. It is not a vulnerability scan, a licence assessment, an attestation
|
|
81
|
+
or a signature, it carries component names and versions only, and npm provenance remains the
|
|
82
|
+
separate thing it already was.
|
|
83
|
+
- **The PHI scanner refuses a run that enumerated a target and never read it, and the install
|
|
84
|
+
settings that hold a resolution back are now in force rather than merely written down.** No
|
|
85
|
+
published value, type, warning code or emitted byte moves: this entry is repository tooling and
|
|
86
|
+
supply chain configuration only. `scripts/phi-scan.ts` reconciles the targets a run ENUMERATED
|
|
87
|
+
against the ones it actually READ, on every route rather than on the whole-tree sweep alone. The
|
|
88
|
+
whole-file bypass is subtracted from each route's finished target list, so before this a run
|
|
89
|
+
naming two paths and withdrawing one opened one file and answered for both, at the same exit code
|
|
90
|
+
the same arguments produce over a corpus whose only violator is the withdrawn one, and a caller
|
|
91
|
+
could not tell those two runs apart. The run now exits with the scanner's invocation-error code,
|
|
92
|
+
names every path nobody opened, and still prints any hit it had already found, because a refusal
|
|
93
|
+
must not swallow a real finding. Detection is untouched: the loci it reads, the format-agnostic
|
|
94
|
+
floor and the allow list are exactly as they were, and a run passing no bypass is byte-identical
|
|
95
|
+
on all three routes, asserted against a copy of the scanner without the reconciliation rather
|
|
96
|
+
than assumed. Alongside it, `pnpm-workspace.yaml` declares a 1440 minute minimum release age and
|
|
97
|
+
a no-downgrade trust policy and the pinned package manager moves to a release that enforces both
|
|
98
|
+
(an older one ignores the keys entirely, which is a settings file that decorates rather than
|
|
99
|
+
defends), and the `js-yaml` resolution override is superseded rather than joined, because the
|
|
100
|
+
stale selector pinned a lower version over an overlapping range. The 3.x resolution reached
|
|
101
|
+
through the release tooling is a known residual and is deliberately left where it is.
|
|
102
|
+
- **The unrecognized-flag and unrecognized-status warning messages name the same attribution the
|
|
103
|
+
interpreted value reports.** `ASTM_RECORD_UNDEFINED_ABNORMAL_FLAG` now names the code system
|
|
104
|
+
identifier and version the flag was graded against, and `ASTM_RECORD_UNDEFINED_RESULT_STATUS` now
|
|
105
|
+
states that no citable published source binds the status letter set. Both read the same constants
|
|
106
|
+
the interpreted values read, so message and value cannot drift apart. Both warning **codes** are
|
|
107
|
+
unchanged, and both messages remain value-free: position and fixed prose only, never the field
|
|
108
|
+
text, the result value, or any other field content.
|
|
109
|
+
- **BREAKING: a populated first component no longer bypasses the consumer's LIVD catalog, and no
|
|
110
|
+
public value reports one as a LOINC** (defect 9). The Universal Test ID's first component is a
|
|
111
|
+
LOINC slot, and the governing mapping guide puts transmitting LOINC directly from IVD instruments
|
|
112
|
+
explicitly out of scope: the analyte arrives as a vendor-defined code and a LOINC is what a
|
|
113
|
+
consumer maps to afterwards. This package answered the analyte-identity question from that slot
|
|
114
|
+
anyway. `recognizeUniversalTestId` tagged **any** non-empty first component
|
|
115
|
+
`inline-loinc-candidate` with no evidence whatsoever, `primaryCode()` preferred it over the vendor
|
|
116
|
+
local code, and `mapTestId` returned `{ status: "inline-loinc" }` on it and **never reached
|
|
117
|
+
`catalog.lookup`**, so the catalog a consumer supplied was bypassed for every record with a
|
|
118
|
+
populated first component and `R|1|Glucose|28.6|U/L||N||F` reported a LOINC candidate of
|
|
119
|
+
`Glucose`. A right value under the wrong analyte is a wrong clinical result, so the direction of
|
|
120
|
+
that default was the defect.
|
|
121
|
+
|
|
122
|
+
**No LOINC shape test was added, and none is coming.** This package performs no LOINC validation
|
|
123
|
+
of any kind and never decides what a first-component value "looks like": `2345-7` and `Glucose`
|
|
124
|
+
there are treated identically and every route below is positional, decided by which components are
|
|
125
|
+
populated. Believing component 1 only when it is "LOINC-shaped" would need a definition of that
|
|
126
|
+
shape, which is exactly the unverified claim this change is written to avoid.
|
|
127
|
+
|
|
128
|
+
Public values **removed or renamed**, each with its replacement:
|
|
129
|
+
- `UniversalTestId.loincCandidate` is **removed**; `UniversalTestId.unvalidatedWireValue` replaces
|
|
130
|
+
it, carrying the same verbatim component 1 value under a name that vouches for nothing.
|
|
131
|
+
- The `inline-loinc-candidate` token is **removed** from `UniversalTestIdProvenance`. A populated
|
|
132
|
+
component 1 **with** a vendor local code is now `local-code`, since the local code is the
|
|
133
|
+
identifier; **without** one it is the new token `unvalidated-wire-value-only`, which reports
|
|
134
|
+
that the value was seen without claiming it is a code, so such a record stays distinguishable
|
|
135
|
+
from one whose Universal Test ID was empty.
|
|
136
|
+
- The `LivdMapping` variant `{ status: "inline-loinc", loinc, source: "wire" }` is **removed**:
|
|
137
|
+
no disposition reports a wire value as a LOINC. Its positional replacement is the new
|
|
138
|
+
`{ status: "no-vendor-code" }`, a populated component 1 with no vendor local code to look up.
|
|
139
|
+
|
|
140
|
+
**The four retained dispositions keep their documented meanings exactly.** `mapped` still means
|
|
141
|
+
the consumer's catalog vouched for this LOINC and nothing else, never that the library adjudicated
|
|
142
|
+
the wire against it; `ambiguous` still means more than one distinct candidate with none chosen;
|
|
143
|
+
`unmapped` is still a miss; `no-code` is still nothing to map. Only which inputs reach them
|
|
144
|
+
changed. No pre-existing assertion covering those four outcomes for a record with an **empty**
|
|
145
|
+
first component was edited.
|
|
146
|
+
|
|
147
|
+
**`primaryCode()` keeps its name and changes its behavior**, which is a break with no compile
|
|
148
|
+
error behind it: audit every call site. It now returns the vendor local code and nothing else, so
|
|
149
|
+
where it used to answer with a first-component value it returns `undefined`, the same absence it
|
|
150
|
+
already returned for a record carrying nothing usable and which its callers already had to handle.
|
|
151
|
+
It reports no key rather than a key this library cannot vouch for.
|
|
152
|
+
|
|
153
|
+
New on `LivdAnnotation`, alongside the disposition rather than inside it, so both are inspectable
|
|
154
|
+
without reading prose:
|
|
155
|
+
- `unvalidatedWireValue`, the verbatim first-component value, carried on **every** disposition
|
|
156
|
+
(a miss, an ambiguity and a disagreement all keep it) and absent only when component 1 is empty.
|
|
157
|
+
- `wireValueDisagreesWithCatalog`, `true` if and only if the catalog vouched for exactly one LOINC,
|
|
158
|
+
component 1 is populated, and the two are not byte-identical. **`false` in every other case and
|
|
159
|
+
never absent**, including on the ordinary `R|1|^^^687|...` record, so no standing false
|
|
160
|
+
disagreement ships. It is deliberately not computed against an ambiguous candidate list, which
|
|
161
|
+
would corroborate exactly one candidate. It reports the difference and nothing else: both values
|
|
162
|
+
stay surfaced, neither is marked correct, neither is rewritten, and no field claims the
|
|
163
|
+
difference was settled.
|
|
164
|
+
- `reportedCode` is now always the code the catalog was consulted **with**, verbatim, and is
|
|
165
|
+
absent when nothing was looked up.
|
|
166
|
+
|
|
167
|
+
Two fail-safe corners are now stated and tested rather than left to be found. A consumer-supplied
|
|
168
|
+
catalog whose `lookup` throws propagates to the caller unchanged: a consumer's own crash must not
|
|
169
|
+
be indistinguishable from "your code is not in the catalog", so it is never reported as a miss and
|
|
170
|
+
no partially annotated result is returned. And a hit whose LOINC is a zero-length string is
|
|
171
|
+
reported as a miss, exactly as if the catalog held no entry, never as a vouched-for empty LOINC.
|
|
172
|
+
|
|
173
|
+
A record with no vendor local code emits **no** warning of its own, and emits neither
|
|
174
|
+
`ASTM_LIVD_UNMAPPED_CODE` nor `ASTM_LIVD_AMBIGUOUS_MAPPING`: no lookup happened, and a warning
|
|
175
|
+
asserting one did would claim more than happened. Neither warning's code, message or payload
|
|
176
|
+
changed.
|
|
177
|
+
|
|
12
178
|
### Added
|
|
13
179
|
|
|
180
|
+
- **A LIVD catalog says which publication it came from and which LOINC version the mapping was made
|
|
181
|
+
against, and that version rides on every annotation.** A consumer could record the vendor rows and
|
|
182
|
+
nothing about the publication behind them, so a mapping this library annotated was not
|
|
183
|
+
reproducible and the attribution statement the LOINC license requires had nowhere to live beside
|
|
184
|
+
the mappings it applies to.
|
|
185
|
+
- `LivdPublication`, an **optional second argument** to `defineLivdCatalog` and readable back as
|
|
186
|
+
`LivdCatalog.publication`: `publisher`, `publicationVersion`, `loincVersion` and
|
|
187
|
+
`loincCopyright`, all optional. Each declared value is preserved **verbatim**: nothing trimmed,
|
|
188
|
+
case folded, reordered, reformatted or defaulted. Absent, empty and whitespace only are one
|
|
189
|
+
case, no value declared, stored as absent rather than as a blank.
|
|
190
|
+
- `LivdAnnotation.catalogLoincVersion` carries that LOINC version on **every** annotation the
|
|
191
|
+
catalog produced, whatever the lookup answered (`mapped`, `unmapped`, `ambiguous`,
|
|
192
|
+
`no-vendor-code`, `no-code`), so a mapping records what it was made against and can be
|
|
193
|
+
reproduced. It is provenance about the CATALOG: no field says a LOINC was checked against it,
|
|
194
|
+
because none was.
|
|
195
|
+
- `ASTM_LIVD_CATALOG_NO_LOINC_VERSION` joins `LIVD_WARNING_CODES`, with `AstmLivdCatalogWarning`,
|
|
196
|
+
`LivdCatalogIdentity`, `LIVD_CATALOG_IDENTITY_UNDECLARED` and the builder
|
|
197
|
+
`livdCatalogMissingLoincVersion`. A catalog defined with no LOINC version surfaces one on
|
|
198
|
+
`LivdCatalog.warnings` and is **still built**: it is indexed and answers every lookup exactly as
|
|
199
|
+
it would have. The warning is raised once, where the catalog is defined, never per record, so
|
|
200
|
+
the per-record stream `applyLivd` returns keeps the same codes, in the same order, in the same
|
|
201
|
+
number.
|
|
202
|
+
- The warning's message is a **constant** and carries no consumer-supplied text: the metadata is
|
|
203
|
+
text this library cannot vouch for, and interpolating it would put whatever a consumer stored
|
|
204
|
+
into every log that prints the message. `AstmLivdCatalogWarning.catalog` carries the publisher
|
|
205
|
+
and publication version the catalog declared instead, and a catalog that declared neither is
|
|
206
|
+
reported as having declared no identity, positively, with no name, ordinal or borrowed row value
|
|
207
|
+
invented to stand in for one.
|
|
208
|
+
- **None of it is validated and none of it is a claim by this package.** No version format check,
|
|
209
|
+
no copyright-text check, no publisher check: this package performs **no LOINC validation of any
|
|
210
|
+
kind**, so each value is surfaced exactly as supplied and `catalogLoincVersion` never means a
|
|
211
|
+
LOINC was checked, found or conformed against that version. **Carrying an attribution statement
|
|
212
|
+
is not discharging it**: the license obligation stays the consumer's.
|
|
213
|
+
- Additive and source compatible. The second argument is optional, so the existing
|
|
214
|
+
single-argument call is unchanged; `publication` and `warnings` are optional members, so a
|
|
215
|
+
catalog a consumer implemented by hand still satisfies the interface and still annotates without
|
|
216
|
+
raising; and `catalogLoincVersion` is conditional, so an annotation from a catalog declaring no
|
|
217
|
+
LOINC version keeps the exact key set it had. The one visible widening is `LivdWarningCode`,
|
|
218
|
+
which gains the new registry member, so an exhaustive `switch` over it needs one more arm.
|
|
219
|
+
- **`toObject`, `toISO` and `toDate`: the shared `@cosyte` date conversion surface, with the
|
|
220
|
+
timezone honesty ASTM forces.** A parsed `AstmDate` could be rendered (`astmDateToLocalISO`) but
|
|
221
|
+
not read as components and not converted to an instant, and the name it was rendered under was
|
|
222
|
+
this package's alone, so a consumer holding dates from two `@cosyte` parsers wrote two different
|
|
223
|
+
conversions. The three names, their return shapes and their timezone rule are now identical across
|
|
224
|
+
the sibling parsers.
|
|
225
|
+
- `toObject(value)` returns a **frozen** object carrying only the components the value stated
|
|
226
|
+
(`{ year, month, day }` for `"20240315"`), with a spec-native 1-to-12 month and singular
|
|
227
|
+
`hour`/`minute`/`second` keys. Nothing is zero-filled, no key holds `undefined`, and the parse
|
|
228
|
+
bookkeeping (`raw`, `precision`, `truncated`) stays on `AstmDate` rather than crossing over: a
|
|
229
|
+
digit run that cut a component in half reports only its complete components, and the dangling
|
|
230
|
+
digit reaches no field. `Object.keys()` is therefore the value's precision. Deleting
|
|
231
|
+
`offsetMinutes`, which an ASTM value never carries, leaves an object
|
|
232
|
+
`Temporal.PlainDateTime.from` and luxon's `DateTime.fromObject` accept unchanged; neither
|
|
233
|
+
library is a dependency, the shape is the interoperability.
|
|
234
|
+
- `toISO(value)` renders ISO-8601 truncated to the stated precision and appends no `Z` and no
|
|
235
|
+
offset, because ASTM states none. It returns the same string `astmDateToLocalISO` returns for
|
|
236
|
+
every value `parseAstmDate` produces whose components are a real calendar date; that function is
|
|
237
|
+
unchanged, still exported, and still the repo-native name. On a value whose components are not,
|
|
238
|
+
it still renders the digits it was handed (`"1988-13-01"`) and `toISO` answers `undefined`
|
|
239
|
+
instead, which is the only place the two differ.
|
|
240
|
+
- `toDate(value, options?)` returns a `Date` **only** when the caller supplies
|
|
241
|
+
`options.assumeOffsetMinutes`, signed minutes east of UTC, with an explicit `0` meaning "read
|
|
242
|
+
this value as UTC". An ASTM timestamp is local to the instrument that wrote it, so without that
|
|
243
|
+
assumption there is no instant to give and the answer is `undefined`: the host machine's zone is
|
|
244
|
+
never read and UTC is never assumed, both of which would silently shift a date by a day in every
|
|
245
|
+
negative-offset zone. Components below the stated precision fill to their lowest legal value for
|
|
246
|
+
the instant only, leaving the value's own precision untouched, and a four-digit year below 100
|
|
247
|
+
stays that year (`"00500101"` is the year 50, never 1950). An offset that names no usable zone
|
|
248
|
+
is refused rather than applied, with `undefined` and never an `Invalid Date`: one that is not a
|
|
249
|
+
finite number, and one large enough that applying it lands outside the range a JS `Date`
|
|
250
|
+
represents. An `Invalid Date` satisfies the declared return type and defeats it, since it is
|
|
251
|
+
indistinguishable from a real instant without testing `getTime()` for `NaN`.
|
|
252
|
+
- The `DateParts` and `ToDateOptions` types, and a README section covering all three together with
|
|
253
|
+
the aliasing pattern the shared names force on a consumer importing two `@cosyte` parsers in one
|
|
254
|
+
file.
|
|
255
|
+
- A component that is not on the calendar is refused by all three rather than normalised.
|
|
256
|
+
`parseAstmDate` is lenient by design and reads `"19881301"` into month 13 rather than dropping
|
|
257
|
+
the field, and that is unchanged, so the conversions are where it is answered: a month outside 1
|
|
258
|
+
to 12, a day past the end of its own month (29 February in a common year included), an hour past
|
|
259
|
+
23 and a minute or second past 59 each yield `undefined`. A JS `Date` would instead roll month 13
|
|
260
|
+
into January of the following year and return a date of birth a year out with nothing to say so.
|
|
261
|
+
The refusal is of the whole value, never of the offending component alone.
|
|
262
|
+
- None of the three ever throws: `undefined`, `null`, a value this parser refused, a value stating
|
|
263
|
+
no component, and a value whose components are not a real calendar date all answer `undefined`.
|
|
264
|
+
Nothing else moved. No existing export was removed, renamed or changed in behaviour, no
|
|
265
|
+
dependency of any kind was added, and the engine floor is unchanged.
|
|
266
|
+
|
|
267
|
+
- **A LIVD catalog can now say what tells two candidate LOINCs apart, and the reported units choose
|
|
268
|
+
between them.** One vendor analyte code mapping to several LOINCs is the ordinary case, not an
|
|
269
|
+
edge: the governing mapping guide gives the commonest chemistry analytes as its own worked
|
|
270
|
+
examples (a serum glucose reported as a mass concentration versus a substance concentration; a
|
|
271
|
+
urine analyte reported as a spot concentration versus a 24 hour excretion rate) and its remedy is
|
|
272
|
+
a mapping per unit. Until now `LivdEntry` had nowhere to record any of that, so a catalog covering
|
|
273
|
+
more than one reporting unit returned `ambiguous` for a large share of ordinary analytes.
|
|
274
|
+
- `LivdEntry` gains three **optional** attributes, each carried **verbatim** with no trimming,
|
|
275
|
+
case folding or normalization: `vendorSpecimenDescription`, `vendorResultDescription` and
|
|
276
|
+
`representativeUnit`.
|
|
277
|
+
- `LivdCatalog.lookup` gains an **optional** second parameter, the units the record reported.
|
|
278
|
+
`applyLivd` and `lookupLivdForRecord` pass field 5 of an `R` record, verbatim; an `O` record has
|
|
279
|
+
no units field, so nothing is passed for one. A hand-written catalog whose `lookup` declares
|
|
280
|
+
only the vendor code still satisfies the interface and answers exactly as before.
|
|
281
|
+
- Where a vendor code carries several distinct candidate LOINCs, the candidate whose
|
|
282
|
+
`representativeUnit` is **exactly equal** to the reported units is answered `mapped`. **The
|
|
283
|
+
comparison is verbatim and case sensitive and it is not UCUM**: nothing is normalized, case
|
|
284
|
+
folded, scaled or converted on either side, so `MG/DL` does not match `mg/dL` and `mg/L` never
|
|
285
|
+
matches `ug/dL`. UCUM requires a program declaring full conformance to compare unit expressions
|
|
286
|
+
by their semantics, so every unit-selected answer carries the new `unitComparison`
|
|
287
|
+
(`comparison: "verbatim-case-sensitive"`, `ucumSemantic: false`, both units verbatim, and a
|
|
288
|
+
note) rather than letting a matched unit read as a conformance claim this package does not make.
|
|
289
|
+
- Refusing still beats guessing, and nothing about `ambiguous` was narrowed. The units matching no
|
|
290
|
+
candidate, matching more than one, or not being readable at all (absent, empty, whitespace only,
|
|
291
|
+
or lost to a truncated or malformed record) are all `ambiguous` with every candidate surfaced
|
|
292
|
+
and no LOINC chosen. An unreadable units field reads as **no units reported**: a typed
|
|
293
|
+
annotation is still produced, nothing raises and no record is omitted. An ambiguous answer now
|
|
294
|
+
also carries `candidateDetails` (each candidate row's LOINC and LIVD attributes, one per catalog
|
|
295
|
+
row, including rows that are not unit qualified) and a `reason` naming which refusal it was.
|
|
296
|
+
- A candidate whose `representativeUnit` is absent, empty or whitespace only is **not unit
|
|
297
|
+
qualified**: never selected by a unit comparison, and still surfaced among the candidates. A
|
|
298
|
+
vendor code carrying exactly **one** candidate LOINC is answered whether or not the units agree,
|
|
299
|
+
with no `unitComparison`, because no unit chose anything there. An answer that read `mapped`
|
|
300
|
+
before this change can never become `ambiguous`.
|
|
301
|
+
- A `mapped` answer's `representativeUnit` is provenance about the **catalog row**, not a
|
|
302
|
+
restatement of what the record reported. Where a vendor code carries a single candidate LOINC
|
|
303
|
+
across several rows spelling the unit differently, the answer takes the first row's attributes
|
|
304
|
+
(exactly as it has always taken the first row's `loincLongName`), so that field can name a unit
|
|
305
|
+
the record did not report. Only `unitComparison` asserts the two were equal, and it is present
|
|
306
|
+
only where a unit actually chose between candidates: branch on that field, not on this one.
|
|
307
|
+
- `vendorSpecimenDescription` and `vendorResultDescription` are **never matched on**. Both are
|
|
308
|
+
free text, and the guide states directly that this information is not intended to be parsed by
|
|
309
|
+
software that automates the mapping, so they are stored and surfaced for a human and nothing
|
|
310
|
+
else. Only the representative unit ever selects.
|
|
311
|
+
- **Additive throughout.** All three attributes are optional and every new answer field is
|
|
312
|
+
conditional, so a catalog carrying none of them answers byte-identically to how it answered
|
|
313
|
+
before, for `mapped`, `unmapped` and `ambiguous` alike. No LOINC is validated or shape-tested,
|
|
314
|
+
no unit is converted, no data is migrated and no consumer catalog is rewritten. New exported
|
|
315
|
+
types: `LivdCandidate`, `LivdUnitComparison`, `LivdAmbiguityReason`.
|
|
316
|
+
|
|
317
|
+
- **Every interpreted abnormal flag and result status reports the vocabulary it was graded against,
|
|
318
|
+
and `HU`/`LU` are recognized.** The `R` record's flag (field 7) and status (field 9) letters come
|
|
319
|
+
from vocabularies maintained on other bodies' schedules, while the record grammar around them is
|
|
320
|
+
archived and frozen, so this package's exposure to a moving vocabulary was real and invisible from
|
|
321
|
+
the outside: a consumer holding `{ recognized: false, meaning: "undefined" }` could not tell a code
|
|
322
|
+
**no published vocabulary defines** from one **this library had not caught up to**. That
|
|
323
|
+
distinction is now carried on the value itself.
|
|
324
|
+
- `AbnormalFlag.vocabulary` names the code system by `system` and `version`, present whether or not
|
|
325
|
+
the letter was recognized. It is the HL7 v3 ObservationInterpretation code system, which carries
|
|
326
|
+
the concepts kept aligned with the HL7 v2 Table 0078 interpretation codes previously named in
|
|
327
|
+
prose only. **The version records what this library compared against**, not what the sender
|
|
328
|
+
meant, and it is not a conformance claim about any instrument.
|
|
329
|
+
- `ResultStatus.vocabulary` is always the explicit **no citable published source**
|
|
330
|
+
(`attributed: false`, with fixed prose in `reason`). The normative text that would bind the
|
|
331
|
+
status letter set is purchase-gated and was not read; a neighbouring HL7 table whose letters
|
|
332
|
+
partly agree is deliberately **not** adopted in its place, because asserting that binding is not
|
|
333
|
+
something the public record supports. Saying the set is unattributed is the honest answer and is
|
|
334
|
+
distinguishable from an attribution that is simply absent; it never carries an identifier.
|
|
335
|
+
- `HU` (`"significantly-high"`) and `LU` (`"significantly-low"`) are recognized. Both are active
|
|
336
|
+
concepts in the graded vocabulary and were absent from the sixteen letters this package
|
|
337
|
+
recognized, so a feed sending either got a warning and `"undefined"`. They are distinct from
|
|
338
|
+
`H`/`L`, from `HH`/`LL` and from the directional `U`/`D`, all six of which keep their readings
|
|
339
|
+
unchanged. The recognized set is eighteen: `L`, `H`, `LL`, `HH`, `<`, `>`, `N`, `A`, `AA`, `U`,
|
|
340
|
+
`D`, `B`, `W`, `S`, `R`, `I`, `HU`, `LU`.
|
|
341
|
+
- New public values: `ABNORMAL_FLAG_CODES` (the recognized set as a list),
|
|
342
|
+
`ABNORMAL_FLAG_VOCABULARY`, `RESULT_STATUS_VOCABULARY`, `describeVocabulary`, and the
|
|
343
|
+
`NamedVocabulary` / `UnattributedVocabulary` / `VocabularyAttribution` types.
|
|
344
|
+
- **No tolerance was widened.** Recognition stays exact match after the existing trim: no case
|
|
345
|
+
folding, so `hu` and `f` are reported unrecognized with their raw text intact. No other concept
|
|
346
|
+
from that code system is adopted, including the deprecated `H>` and `L<` whose replacements
|
|
347
|
+
`HU`/`LU` are; those stay unrecognized, which is the fail-safe direction. An unrecognized flag is
|
|
348
|
+
still never `normal` and an absent status is still never `final`.
|
|
349
|
+
- **Nothing reaches the wire.** The attribution is interpretation output; a parsed and
|
|
350
|
+
re-serialized record emits byte-identical output, pinned by literal expected strings in
|
|
351
|
+
`test/records/golden-round-trip.test.ts`. No new dependency.
|
|
352
|
+
|
|
14
353
|
- **The PHI sweep now reads the bytes git carries, as a union with the working-tree walk.** All mode
|
|
15
354
|
reconciled the paths it walked against the paths git tracks, and a comparison of path _sets_ is
|
|
16
355
|
satisfied by a working tree that carries different content at those paths. Four states were
|
|
@@ -51,7 +390,7 @@ this file is maintained by hand (Changesets handles the version bump and publish
|
|
|
51
390
|
adding a workflow that would have to be made a required context separately. It asserts that every
|
|
52
391
|
pointer resolves, that no section a pointer reaches is empty, that the record stays reachable from
|
|
53
392
|
the always-read file, and that the always-read file still links the record by path. It asserts
|
|
54
|
-
**no** ecosystem-wide contract: several
|
|
393
|
+
**no** ecosystem-wide contract: several Cosyte repos carry no such record at all, and a gate
|
|
55
394
|
phrased as though every repo did would be an overclaim.
|
|
56
395
|
- **The matcher was re-derived against this tree rather than ported, and that was the whole job.**
|
|
57
396
|
Both sibling spellings score **zero** here: the path-qualified form used by `ccda` and `mllp`,
|
|
@@ -1094,7 +1433,7 @@ this file is maintained by hand (Changesets handles the version bump and publish
|
|
|
1094
1433
|
**The refusal removes no capability.** `composeAstmFrames` already accepted `Uint8Array`, so a
|
|
1095
1434
|
consumer who knows their instrument's code page encodes with their own encoder and passes bytes,
|
|
1096
1435
|
which are written through untouched. A stream whose every character is Latin-1 is byte-identical
|
|
1097
|
-
to before, the full `0x00
|
|
1436
|
+
to before, the full `0x00` to `0xFF` range included. The **record** layer is deliberately unchanged:
|
|
1098
1437
|
`serializeAstmRecords` returns a `string`, which is not yet bytes, so the caller keeps the choice.
|
|
1099
1438
|
|
|
1100
1439
|
**Stated no wider than it was measured.** This closes the string-to-bytes step only. It is not a
|
|
@@ -1571,7 +1910,7 @@ phase 8` passes while `Phase 8` reds). An arm keyed on a following digit was wri
|
|
|
1571
1910
|
acceptance under a tolerating profile) is _measured_, on the tree before and after the change.
|
|
1572
1911
|
|
|
1573
1912
|
- **The em dash (U+2014) is gone from every tracked file, and a CI gate keeps it out**
|
|
1574
|
-
(`EMDASH-CONFORMANCE`). The brand rule bans the character outright across every
|
|
1913
|
+
(`EMDASH-CONFORMANCE`). The brand rule bans the character outright across every Cosyte surface
|
|
1575
1914
|
and names commit messages explicitly; this package was the last one it had not reached, and the
|
|
1576
1915
|
only one where it did not arrive on a clean tree. **1,129 occurrences across 108 of the 142
|
|
1577
1916
|
tracked files** were rewritten with a comma, a colon, or a period. The sweep and the gate are one
|
|
@@ -2035,7 +2374,7 @@ phase 8` passes while `Phase 8` reds). An arm keyed on a following digit was wri
|
|
|
2035
2374
|
|
|
2036
2375
|
## [0.0.1] - 2026-07-22
|
|
2037
2376
|
|
|
2038
|
-
The first pre-alpha release. The package begins its public history at `0.0.x`, per the
|
|
2377
|
+
The first pre-alpha release. The package begins its public history at `0.0.x`, per the Cosyte version
|
|
2039
2378
|
ladder (`0.0.x` until first alpha).
|
|
2040
2379
|
|
|
2041
2380
|
### Added
|
|
@@ -2163,7 +2502,7 @@ warnings: readonly AstmFrameWarning[] }`. A frame is `<STX> FN text <ETB|ETX> CS
|
|
|
2163
2502
|
- **Modulo-256 checksum** over the bytes after `STX` up to and **including** the `ETB`/`ETX`
|
|
2164
2503
|
terminator, two hex chars: **verified on decode, emitted uppercase, accepted lowercase** (a real
|
|
2165
2504
|
vendor quirk). `computeChecksum` / `toChecksumHex` / `parseChecksumHex` exported.
|
|
2166
|
-
- **Frame-number `0
|
|
2505
|
+
- **Frame-number `0` to `7` sequencing** (rolls over `7 → 0 → 1`, starts at `1`) and **multi-frame record
|
|
2167
2506
|
reassembly**: text is capped at **240 bytes** (the seven control bytes are **not** counted), `ETB`
|
|
2168
2507
|
is intermediate / `ETX` final. `parseFramedAstm(bytes, opts?)` composes the framing and record layers
|
|
2169
2508
|
at the edge (decode → reassemble trusted records → `parseAstmRecords`).
|
|
@@ -2199,7 +2538,7 @@ defaulted: boolean; warnings }`. A leading `STX`/`ENQ` ⇒ **framed** (serial, a
|
|
|
2199
2538
|
by `ltpInitialState()`. Events are the four LTP control signals (`enq`/`ack`/`nak`/`eot`) plus a
|
|
2200
2539
|
codec-decoded `frame`; actions are `sendAck` / `sendNak` / `sendEot` / `deliverRecord`. It models
|
|
2201
2540
|
the LIS01-A2 establishment → transfer → termination phases as `neutral ⇄ transfer`, reassembling
|
|
2202
|
-
`ETB…ETX` runs into delivered records and tracking the `0
|
|
2541
|
+
`ETB…ETX` runs into delivered records and tracking the `0` to `7` frame sequence.
|
|
2203
2542
|
- **ACK-failsafe (safety-critical, borrowed from `mllp`).** A frame the codec did not vouch for (a
|
|
2204
2543
|
**bad checksum**, an **unterminated** frame, or one **out of sequence**) is answered with `NAK`,
|
|
2205
2544
|
**never** a fabricated positive `ACK`, and its bytes are **never** appended to a record or
|
|
@@ -2236,7 +2575,7 @@ EOT` session **reassembles exactly the source records**; a **raw-TCP stream equa
|
|
|
2236
2575
|
counters, the `L` terminator) is **computed, not guessed** (a sequence number may be overridden).
|
|
2237
2576
|
- **Frame encoder.** `composeAstmFrames(records, opts?)` is the exact inverse of `decodeAstmFrames`:
|
|
2238
2577
|
it wraps reassembled record bytes into `<STX> FN text <ETB|ETX> CS <CR><LF>` frames with the
|
|
2239
|
-
modulo-256 **checksum** and the `0
|
|
2578
|
+
modulo-256 **checksum** and the `0` to `7` **frame number** **computed** (never accepted-as-given or
|
|
2240
2579
|
faked; emitted uppercase), numbered continuously across the stream (start `1`, roll over `7 → 0`),
|
|
2241
2580
|
and every record over **240** text bytes **split** `ETB…ETX` (the seven control bytes never counted).
|
|
2242
2581
|
`serializeFramedAstm(msg | records)` composes both emit layers at the edge: the mirror of
|