@cosyte/hl7 0.0.8 → 0.0.10

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 CHANGED
@@ -1,17 +1,246 @@
1
1
  # Changelog
2
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 complete v1 API surface below. An earlier
13
- `0.1.0` tag was prepared but never published, so the package begins its public history at `0.0.x`,
14
- per the cosyte version ladder (`0.0.x` until first alpha).
3
+ ## 0.0.10
4
+
5
+ ### Patch Changes
6
+
7
+ - 68898a3: The README lockup now links to cosyte.com (`ASSETS`).
8
+
9
+ The `<picture>` block above the H1 is wrapped in an anchor to https://cosyte.com, per the founder
10
+ requirement of 2026-08-06. Nothing inside the block moved: the `<source>`, the `<img>`, the alt text
11
+ and both tile URLs are byte-identical.
12
+
13
+ What the anchor does was measured on both surfaces by `fhir`, not assumed, because fourteen READMEs
14
+ carry this shape. On GitHub the anchor works and the colour-scheme switch keeps working, because the
15
+ `<img>` stays a direct child of `<picture>`, which is the condition the HTML spec puts on `<source>`
16
+ applying at all. On an npm package page the anchor is lost: npm wraps a README image in its own
17
+ anchor to the image file, a nested anchor is not representable, so the parser closes ours early and
18
+ the image ends up linked to the image file rather than to cosyte.com. Shipped anyway by founder
19
+ decision of 2026-08-07: on npm that is no worse than the unlinked lockup it replaces, and GitHub is
20
+ where these READMEs are read.
21
+
22
+ ## 0.0.9
23
+
24
+ ### Patch Changes
25
+
26
+ - 3ac0c28: `CHANGELOG.md`, which ships inside the tarball, is now written by the release instead of by hand, so it stops describing already-published code as unreleased.
27
+
28
+ `.changeset/config.json` set `"changelog": false`, so no release ever wrote a version heading
29
+ into `CHANGELOG.md` and nothing ever rolled `[Unreleased]` over. Every published version of this
30
+ package therefore carried a changelog with **no version headings at all**: one `[Unreleased]`
31
+ heading over the whole history, and a preamble stating that the first pre-alpha release "will
32
+ ship" the API surface listed below it, in a tarball that had shipped that surface several
33
+ versions earlier. `CHANGELOG.md` is listed in `package.json` `files`, so this was text on the
34
+ disk of everyone who installed the package, not internal bookkeeping.
35
+
36
+ **The flag is what changed, not the prose.** Correcting the sentence by hand would have left the
37
+ mechanism that wrote it, and the next release would have drifted the same way. `changelog` now
38
+ names the generator that ships with Changesets, so a release writes its own version heading and
39
+ its own entry from the changesets it consumed, and **the changeset summary is now the changelog
40
+ entry**. Nothing new is depended on: the generator is an entry point of `@changesets/cli`, which
41
+ was already a dev dependency.
42
+
43
+ **The file's shape changed with it, deliberately.** Changesets prepends a release by replacing
44
+ the first newline in the file, so exactly one line can sit above generated output. The
45
+ hand-written preamble sat on line 3, which means a release would have inserted itself between the
46
+ heading and the preamble and split the header in two. The hand-maintained history has therefore
47
+ moved under a `## Released before this file was generated` heading, with the false preamble
48
+ replaced by an accurate one. Three pieces of hand-workflow scaffolding were dropped and no entry
49
+ was reworded: the `[Unreleased]` heading, its link definition at the foot of the file, and the
50
+ five empty section stubs waiting for the next hand-written entry. The history itself is left as
51
+ it was written rather than re-sorted into version sections, because the file never recorded which
52
+ release each entry went out in and the text is already on disk in published copies.
53
+
54
+ Pinned by `test/scripts/changelog-generation.test.ts`, which runs the real `changeset version`
55
+ against the real `CHANGELOG.md` and the real config in a throwaway package rather than
56
+ reimplementing where the tool inserts text. Eight of its ten cases are red against the previous
57
+ state, measured on the tree this change was written against rather than recalled. **The rule it enforces is that nothing but the H1 sits above the first heading, and it is
58
+ asserted on the released document as well as the committed one**: a rule phrased as "the archive
59
+ heading comes second" holds only until the first release writes its own version heading there,
60
+ which would have redded the first Version PR this configuration ever opened. The archived history
61
+ is asserted byte identical across a release, so the run exercises this repo's own Prettier rather
62
+ than the one Changesets bundles as a fallback. Two further controls: the same inputs with
63
+ `"changelog": false` must write no version heading at all, so the flag is proved load-bearing
64
+ rather than incidental; and the old file shape must reproduce the split header, so the rule is
65
+ demonstrated rather than asserted.
66
+
67
+ One upstream behaviour is worth knowing before debugging a release, and is recorded in that
68
+ file: Changesets wraps the changelog write in a try/catch that only warns. A tree whose declared
69
+ Prettier config cannot be resolved bumps the version, consumes the changeset, and writes no
70
+ changelog at all. Reproduced here on this repo's inputs. A release that publishes with an
71
+ unchanged changelog is that failure, not a setting that quietly reverted.
72
+
73
+ `CONTRIBUTING.md` and `.changeset/README.md` said to add the entry to `CHANGELOG.md` by hand and
74
+ now say to write it in the changeset. No runtime code, no public API, no warning code and no
75
+ parse behaviour changed.
76
+
77
+ - d8d3be3: Repository PHI commit-gate only, with no runtime impact: the gate no longer reports a clean sweep over a scan root it never opened, where an absent or dangling `test/fixtures` used to print `OK: no hits` and exit 0 on the strength of `src/` alone.
78
+
79
+ `pnpm phi-scan` with no arguments is what CI runs. It walks two roots, `test/fixtures` and `src`,
80
+ and refused a sweep only when it had read **zero files in total**. That global rule is satisfied by
81
+ any one surviving file, so a single `src/` module vouched for the entire fixture corpus, and one
82
+ fixture vouched for the whole of `src`.
83
+
84
+ Reproduced before the fix, on this checkout, three ways, each printing `[phi-scan] OK: no hits` and
85
+ exiting **0**: with `test/fixtures` moved aside, with `test/fixtures` replaced by a **dangling
86
+ symlink**, and with `src` moved aside. The first two leave all 106 non-markdown fixtures unread;
87
+ the third leaves all 95 source files unread.
88
+
89
+ **The dangling case is the sharpest, and nothing else in the scanner can see it.** `existsSync`
90
+ **follows** the link and answers false, so `walk()` returns before `readdirSync` and the
91
+ not-a-regular-file refusal never fires. That rule only ever classifies entries found INSIDE a root,
92
+ and this is the root itself, so absent, dangling and empty are one state to the walk. This reporter
93
+ prints no file count either, so unlike the sibling this remedy was ported from there was not even a
94
+ suspicious denominator to notice.
95
+
96
+ **The rule is now per-root.** All-mode refuses (exit 2) unless every declared root yielded at least
97
+ one file that was actually READ, and the refusal names the starved roots. Observing nothing at all
98
+ is the all-starved case of that one rule rather than a second rule beside it, so the existing
99
+ wording carries. Hits found under the roots that DID yield are printed before the refusal, so an
100
+ incomplete sweep never swallows a real finding, and the exit code is still 2 regardless.
101
+
102
+ **Deliberately unchanged, and pinned:** `--staged` and named-path mode make no per-root promise,
103
+ because neither enumerates a root. `--staged` legitimately has nothing to scan when a commit
104
+ touches only markdown, and named-path mode is bounded by the caller's argv. Widening the rule to
105
+ either was run as a mutation rather than argued: widening it to `--staged` reds 7 cases, and
106
+ dropping the mode guard altogether reds 29.
107
+
108
+ **The granularity is the declared root and nothing finer**, which is a real bound and is recorded
109
+ rather than glossed. Three states still exit 0, each measured with the rule in place, each now in
110
+ the scanner's limits list with the command that produced it, and each pinned by a characterization
111
+ test so a change that closed one would have to say so: a directory missing from INSIDE a root
112
+ (`mv test/fixtures/canonical ..` still prints `OK: no hits` with 27 fixtures unread); a root that
113
+ is ITSELF a symlink, which is followed, so the rule is satisfied by whatever is on the other side
114
+ of it; and the rule is a floor of ONE file, whatever the root used to hold.
115
+
116
+ One shape often listed beside those is NOT open here, and it is written down so it is not ported in
117
+ from elsewhere as though it were: a root that is a regular file, or a link to one, refuses at exit
118
+ **2**, not the **1** this gate reserves for hits. `walk()` already wraps `readdirSync` and rethrows
119
+ the `ENOTDIR` as an invocation error on the scanner's own channel, and an existing test pins it.
120
+
121
+ Twelve tests added, on a throwaway repository so no starved root, no dangling link and no violator
122
+ is ever written into the committed corpus. Five are red against the superseded global rule; the
123
+ all-starved case is green on both, because it asserts the new rule SUBSUMES the old one rather than
124
+ sitting beside it; three assert the rule did not widen; three are characterizations of the limits
125
+ above. The scratch-repo helper now seeds both walk roots, which is load-bearing rather than
126
+ tidiness: with an empty `test/fixtures` every case built on it would refuse before reaching what it
127
+ meant to assert.
128
+
129
+ - be842bd: Repository PHI commit-gate only, with no runtime impact: the gate now reads tracked files directly under `test/`, reads an inline `PID|...` literal in hand-written source with the same field map a `.hl7` fixture gets, and no longer drops to a shape-only pass over a blob staged at exactly a scan root.
130
+
131
+ `pnpm phi-scan` with no arguments is what CI runs; `pnpm phi-scan --staged` is the pre-commit hook.
132
+ Four separate holes, each pre-existing and each measured on this checkout before the change.
133
+
134
+ **One: a tracked file directly under `test/` was enumerated by neither route.** The walk was rooted
135
+ at `test/fixtures` and `src`, and `--staged` matched `test/fixtures/**` plus `src/**.ts`. That
136
+ leaves **115 tracked files** in this repo scanned by nothing at all. A file at `test/<name>.ts`
137
+ carrying a live `PID-3`/`PID-5`/`PID-7`/`PID-11`/`PID-13` printed `OK: no hits` and exited **0** on
138
+ both routes, over the same bytes that give six segment-aware hits at a `.hl7` path.
139
+
140
+ **Two, and it is the half that a wider walk root does NOT buy: the recogniser assumes the file IS
141
+ the document, and an inline fixture is a TypeScript string literal.** Bringing `test/**` into scope
142
+ bought the conservative shape floor -- a dashed SSN and a non-test email domain -- over those 115
143
+ files and nothing else: no name, DOB, MRN, address or phone detection. **55 of the 115 carry an
144
+ inline `PID|` literal.** Widening the scope and widening the recogniser are two changes, and the
145
+ second is in addition to the first, never instead of it. So there is now an embedded-literal pass
146
+ that pulls `PID|...` runs out of source and hands them the same field map the whole-file parse uses.
147
+ Both halves are pinned with a case that is red before and green after, and the residual is pinned
148
+ too: the same identifying values written as PROSE in the same file are still not found, because a
149
+ name in narrative is not separable from clinical vocabulary without NLP.
150
+
151
+ The embedded pass is deliberately narrower than the whole-file parse, and each of its four limits
152
+ is stated rather than left to be discovered. Two of them are here because review refuted the first
153
+ draft of this paragraph, which is the reason to read them rather than trust the summary.
154
+
155
+ It uses the default delimiters only, because an embedded run has no reachable `MSH-2`: the encoding
156
+ characters are themselves source-escaped. It runs no unknown-segment backstop, which is right for a
157
+ known message and a noise cannon over program text, so an inline `Z...` segment carrying a name is
158
+ missed. It erases a `${...}` interpolation rather than reading the variable's identifier as a
159
+ person's name, and the bound there is any value reached through an interpolation, INCLUDING one
160
+ whose literal sits in the same file: measured, a surname in a same-file constant interpolated into a
161
+ PID line exits 1 on the MRN and the DOB and is silent on the name. This pass reads literals in field
162
+ position and evaluates nothing; the shape floor still reads the whole file, so a dashed SSN or a
163
+ non-test email in such a constant is caught either way. And a run is cut at the first source quote,
164
+ which cuts HL7 data as well as program text: the two-character literal `""` is HL7 v2's
165
+ explicit-null field value, so an inline fixture exercising null handling loses everything after it,
166
+ and an apostrophe inside a name truncates identically. No clause number is quoted for that, on
167
+ purpose: the behaviour is grounded in this package's own parser, whose null indicator is true iff
168
+ the field was exactly those two characters. That last one is a MISS, and it is written down rather than
169
+ patched, because the enclosing quote style is not knowable from a single line and a wrong guess
170
+ reads program text as patient data.
171
+
172
+ `src/` changed intent and not only scope, so the comment that said otherwise is corrected rather
173
+ than left standing: the embedded pass runs on EVERY non-fixture target, so a JSDoc `@example`
174
+ carrying a PID line with a non-allow-listed name now reds where it did not before. This package
175
+ requires an `@example` on every public export, so a new example must use allow-listed tokens or add
176
+ its own, exactly as a fixture does. The committed corpus is green as it stands.
177
+
178
+ **Three: a regular blob staged at exactly `test/fixtures` got the shape floor only, and `--staged`
179
+ is the pre-commit hook.** The scope and the mode check already passed it through; the DETECTOR gate
180
+ dropped it, because the fixture test wanted a `test/fixtures/` prefix or an `.hl7` suffix and the
181
+ root's own path has neither. Measured twice independently: the same payload gives six segment-aware
182
+ hits at `test/fixtures/<name>.hl7` and exit **0** at `test/fixtures`. A dashed SSN in that blob
183
+ exited **1** either way, so the floor held and only the segment-aware tier was missing -- which is
184
+ why the remedy is one clause and not a new rule.
185
+
186
+ **And a fourth, found by a sibling and reproduced here: `-B` is not inert, and this scanner's own
187
+ comment said it was.** That was the permissive half of a "do not add these flags" paragraph, so it
188
+ told the next reader that injecting `-B` was safe. The mechanism is sharper than "a `B` record the
189
+ filter drops": under `-B` a complete rewrite prints status letter **`M`** with a break score, one
190
+ path, which the raw-record parser handles happily -- but `--diff-filter` classifies a broken pair as
191
+ **`B`** whatever letter it prints, so `AMTU` deleted the record before anything saw it. Through the
192
+ scanner, over a staged dashed SSN in a wholly rewritten in-scope file: the shipped argv exits 1, the
193
+ same argv plus `-B` printed `OK: no hits` and exited 0. `B` is now in the filter. It costs the
194
+ enumeration nothing, because git cannot emit a broken pair without the flag. **The pin that let the
195
+ false claim survive was a pin on a rename**, the one shape `-B` really does leave alone: a case that
196
+ cannot fail proves nothing.
197
+
198
+ **Two behaviour changes that follow from `test` being a walk root, recorded rather than left to be
199
+ found.** The fixture root is now an ENTRY INSIDE another root, so a symlink there is classified by
200
+ `Dirent.isSymbolicLink()` and refused by name at exit 2, dangling or not: that closes, for
201
+ `test/fixtures` only, the residual the previous entry recorded as "a root that is itself a symlink
202
+ is followed". It does not generalise -- `src` and `test` have no walked parent, so both are still
203
+ followed, and the general rule is the parent, not the root. And the two roots nest, so coverage now
204
+ credits EVERY root a file sits under rather than the first: crediting one would leave the other
205
+ permanently starved, and which one depended on list order with nothing saying so. Walked entries are
206
+ de-duplicated for the same reason, or every fixture would be reported twice.
207
+
208
+ The scanner's own test file was the one place this could not be resolved with the allow-list, and it
209
+ is worth saying why: it holds deliberate violators, every one of them asserted on, so allow-listing
210
+ them would have made the positive tests vacuous and carving the file out of the scan would have been
211
+ the gate excusing itself from the rule it enforces. Its violator identities are assembled at runtime
212
+ instead -- the discipline the file already used for its SSN sentinel -- so the temp files it builds
213
+ still carry the full violator bytes and nothing in the tracked file is a PHI-shaped run. One
214
+ registrable `.org` email in it became a permanently unregistrable `.invalid` one on the way past.
215
+ Every other new allow-list token was surfaced by the gate rather than guessed at, and they are the
216
+ same species as the placeholder surnames already listed.
217
+
218
+ Twenty-three net new tests in the scanner's own suite (78 to 101), plus several existing ones
219
+ rewritten where the widening inverted their verdict, each change of verdict recorded rather than
220
+ deleted. All of them run on throwaway repositories, so no violator is ever written into the
221
+ committed corpus. Nothing here changes a single byte of the published package.
222
+
223
+ ## Released before this file was generated
224
+
225
+ Every release section above this heading is written by
226
+ [Changesets](https://github.com/changesets/changesets) from the changesets in `.changeset/`, newest
227
+ release first. The release writes its own version heading, so nothing above this line is maintained
228
+ by hand, and a change is recorded by adding a changeset rather than by editing this file.
229
+
230
+ Everything below this heading was maintained by hand. It sat under a single `[Unreleased]` heading
231
+ that no release ever rolled over, which is why it went on describing already-shipped code as
232
+ unreleased, inside the published tarball, for as long as it did. It is left as it was written
233
+ rather than re-sorted into version sections: the file never recorded which release each entry went
234
+ out in, and this is the text that installed copies already carry on disk. Only three things were
235
+ dropped, all of them scaffolding for the hand-written workflow that no longer runs: the
236
+ `[Unreleased]` heading itself, its link definition at the foot of the file, and the five empty
237
+ section stubs that existed to receive the next hand-written entry. No entry was reworded.
238
+
239
+ The entries below follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the generated
240
+ sections above use the format Changesets writes, which is a version heading and a list of the
241
+ changes that release consumed. Versions follow the cosyte pre-alpha ladder, `0.0.x` until first
242
+ alpha, rather than [Semantic Versioning](https://semver.org/spec/v2.0.0.html) alone. An earlier
243
+ `0.1.0` tag was prepared but never published, which is why the public history begins at `0.0.x`.
15
244
 
16
245
  ### Added
17
246
 
@@ -605,6 +834,128 @@ kind, value)`** sets one at a field or `field[rep]` dot-path. Component values a
605
834
 
606
835
  ### Fixed
607
836
 
837
+ - **The PHI scan reached neither `test/**` nor an inline fixture, and dropped to a shape-only pass
838
+ at a scan root's own path (`PHI-SCAN-WALK-ROOT-SCOPE`, plus `PHI-SCAN-RENAME-BLIND-AT-PRECOMMIT`▶(b)).** Four`PRE-EXISTING`holes, each measured on`d8d3be3` before the change.
839
+
840
+ **1. A tracked file directly under `test/` was enumerated by NEITHER route.** The walk was rooted
841
+ at `test/fixtures` + `src`; `--staged` matched `test/fixtures/**` + `src/**.ts`. That left **115
842
+ tracked files** scanned by nothing. A `test/<name>.ts` carrying a live
843
+ `PID-3`/`PID-5`/`PID-7`/`PID-11`/`PID-13` printed `OK: no hits` and exited **0** on both routes,
844
+ over bytes that give six segment-aware hits at a `.hl7` path. `SCAN_ROOTS` is now
845
+ `test/fixtures` + `test` + `src`, and `--staged` adds `test/**` (markdown excluded, copying
846
+ `walk()`'s rule) as a **union** clause, so nothing it enumerated before stops being enumerated.
847
+
848
+ **2. THE HALF A WIDER ROOT DOES NOT BUY: the recogniser assumes the file IS the document, and an
849
+ inline fixture is a `.ts` string literal.** Scope alone bought the dashed-SSN + email floor over
850
+ those 115 files and nothing else: no name, DOB, MRN, address or phone. **55 of the 115 carry an
851
+ inline `PID|` literal.** So there is now an embedded-literal pass (`scanEmbeddedHl7`) that pulls
852
+ `PID|…` runs out of source and hands them the **same field map** the whole-file parse uses; the
853
+ detectors are one shared function, not a second copy. Both halves are pinned RED-before /
854
+ GREEN-after, **and so is the residual**: the same values written as PROSE in the same file are
855
+ still not found. The pass is deliberately narrower than the whole-file parse and each limit is
856
+ stated, and two of them only after review refuted the first draft. Default delimiters only (an
857
+ embedded run has no reachable `MSH-2`); no unknown-segment backstop (a noise cannon over program
858
+ text, so an inline `Z…` name is missed); a `${…}` interpolation is **erased, not read**
859
+ (`familyName`, `content`, `marker` and `pidSurname` were each reported as a patient name), and the
860
+ bound there is **any value reached through an interpolation, including one whose literal is in the
861
+ same file** (measured: `const fam = "<surname>"` interpolated into a PID line exits 1 on the MRN
862
+ and DOB and is **silent on the name**); and **a run is cut at the first source quote, which cuts
863
+ HL7 data too** (the two-character literal `""` is HL7 v2's explicit-null field value, grounded in
864
+ this repo's own parser rather than a clause number nobody verified, so
865
+ `PID|1|""|MRN001||<surname>^<given>||<dob>|F` exits **1** at a `.hl7` path and **0** inline,
866
+ because the extracted run is `PID|1|`; `O'Brien` truncates identically). The last one is a MISS
867
+ and is written down rather than patched: the enclosing quote style is not knowable from one line,
868
+ and a wrong guess reads program text as patient data.
869
+
870
+ **`src/` changed intent, not just scope**, and the comment that said otherwise is corrected: the
871
+ embedded pass runs on **every** non-fixture target, so a JSDoc `@example` carrying a PID line with
872
+ a non-allow-listed name now reds (measured: exit 0 on `d8d3be3`, exit 1 here). This repo
873
+ hard-requires an `@example` on every public export, so a new example must use allow-listed tokens
874
+ or add its own, exactly like a fixture. The committed corpus is green as it stands.
875
+
876
+ **3. ▶(b): a regular blob staged at exactly `test/fixtures` got the shape floor only, and
877
+ `--staged` IS the pre-commit hook.** Scope and the mode check passed it through; the **detector
878
+ gate** dropped it, because `looksLikeHl7` wanted a `test/fixtures/` prefix or an `.hl7` suffix and
879
+ the root's own path has neither. Measured twice: six segment-aware hits at
880
+ `test/fixtures/<name>.hl7`, exit **0** at `test/fixtures`; a dashed SSN in the same blob exited
881
+ **1** either way, so the floor held and only the segment-aware tier was missing, so it is one clause, not
882
+ a new rule.
883
+
884
+ **4. `-B` IS NOT INERT, AND THIS SCANNER SAID IT WAS** (found by `terminology#45`, reproduced
885
+ here). The permissive half of a "do not add these flags" paragraph. The mechanism is sharper than
886
+ "a `B` record `AMTU` drops": a complete rewrite under `-B` prints status letter **`M`** with a
887
+ break score (the digits move with how much old content survives, so none are quoted here), one
888
+ path, which `RAW_RECORD` parses happily. But `--diff-filter`
889
+ classifies a broken pair as **`B`** whatever letter it prints. `--diff-filter=AMTU` returns empty;
890
+ `B` and `AMTUB` return the record. Through the scanner over a staged dashed SSN in a wholly
891
+ rewritten in-scope file: shipped argv exit **1**, same argv plus `-B` exit **0**. The filter is
892
+ now `AMTUB`, which costs the enumeration nothing (git cannot emit a broken pair without the flag).
893
+ **The pin that let the false claim live was a pin on a RENAME**, the one shape `-B` leaves alone.
894
+
895
+ **Two behaviour changes from `test` becoming a root.** `test/fixtures` is now an ENTRY INSIDE a
896
+ walked root, so a symlink there is refused by name at exit **2**, dangling or not: that closes,
897
+ **for `test/fixtures` only**, the "a root that is itself a symlink is followed" residual below.
898
+ It does not generalise: `src` and `test` have no walked parent, so both are still followed, and
899
+ the boundary is the PARENT, not the root. And the roots now NEST, so coverage credits **every**
900
+ root a file sits under rather than the first (crediting one would starve the other permanently,
901
+ and which one depended on list order with nothing saying so); walked entries are de-duplicated
902
+ for the same reason.
903
+
904
+ **The scanner's own test file could not be resolved with the allow-list**, and that is the
905
+ interesting part: it holds deliberate violators, every one asserted on, so allow-listing them
906
+ would make the positive tests vacuous and carving it out of the scan would be the gate excusing
907
+ itself from its own rule. Its violator identities are **assembled at runtime** instead (the
908
+ discipline the file already used for its SSN sentinel), so the temp files it builds still carry
909
+ the full violator bytes and the tracked file holds no PHI-shaped run. One registrable `.org`
910
+ email in it became a permanently unregistrable `.invalid` one on the way past. Every other new
911
+ allow-list token was **surfaced by the gate**, not guessed at. **23 net new tests** in
912
+ `test/scripts/phi-scan.test.ts` (78 to 101), plus several existing ones rewritten where the
913
+ widening inverted their verdict, each change of verdict recorded rather than deleted.
914
+
915
+ **Two `PRE-EXISTING` minors folded in with this item are ALREADY CLOSED here and were not
916
+ reopened**, measured rather than assumed: a missing or unreadable allow-list exits **2** (not the
917
+ **1** reserved for hits), `readdirSync` failing on a root exits **2**, and unmerged (`U`) entries
918
+ are already in the filter.
919
+
920
+ - **The PHI scan's observation rule was GLOBAL, so one root vouched for the other
921
+ (`PHI-SCAN-OBSERVED-NOTHING-IS-GLOBAL`).** `pnpm phi-scan` with no arguments (what CI runs) walks
922
+ two roots, `test/fixtures` and `src`, and refused a sweep only when it had read **zero files in
923
+ total**. Any one surviving file satisfied that, so a single `src/` module vouched for the whole
924
+ fixture corpus and vice versa. **Reproduced on `04e4bc6` before the fix, three ways**, each
925
+ printing `[phi-scan] OK: no hits` and exiting **0**: `test/fixtures` moved aside, `test/fixtures`
926
+ replaced by a **dangling symlink**, and `src` moved aside. The first two leave all 106
927
+ non-markdown fixtures unread; the third leaves all 95 source files unread.
928
+
929
+ **The dangling case is the sharpest, and nothing else in the scanner can see it.** `existsSync`
930
+ **follows** the link and answers false, so `walk()` returns before `readdirSync` and the
931
+ not-a-regular-file refusal never fires: that rule only ever classifies entries found **inside** a
932
+ root, and this is the root itself. Absent, dangling and empty are one state to the walk. This
933
+ reporter prints no file count either, so there was not even a suspicious denominator to notice.
934
+
935
+ **The rule is now per-root.** All-mode refuses (exit **2**) unless **every** declared root
936
+ yielded at least one file that was actually read, and the refusal names the starved roots.
937
+ Observing nothing at all is the all-starved case of that one rule, not a second rule beside it,
938
+ so the previous wording carries. Hits found under the roots that **did** yield are printed before
939
+ the refusal, so an incomplete sweep never swallows a real finding; the exit code is still 2.
940
+
941
+ **Deliberately unchanged:** `--staged` and named-path mode make no per-root promise, because
942
+ neither enumerates a root. `--staged` legitimately has nothing to scan when a commit touches only
943
+ markdown, and named-path mode is bounded by the caller's argv. Both boundaries are pinned by
944
+ tests, and widening the rule to either was run as a mutation rather than argued.
945
+
946
+ **The granularity is the declared root and nothing finer, which is a real bound, not a slogan.**
947
+ Three states still exit **0**, each measured with the rule in place and each now recorded in the
948
+ scanner's own limits list with the command that produced it and pinned by a characterization
949
+ test: a directory missing from **inside** a root (`mv test/fixtures/canonical ..` still prints
950
+ `OK: no hits`, 27 fixtures unread); a root that is **itself a symlink**, which is followed, so
951
+ the rule is satisfied by whatever is on the other side of it; and the floor is **one file**,
952
+ whatever the root used to hold. One shape often listed with those is **not** open here and is
953
+ written down so it is not ported in as if it were: a root that is a regular file, or a link to
954
+ one, refuses at exit **2** rather than the 1 this gate reserves for hits, because `walk()`
955
+ already wraps `readdirSync` and rethrows the `ENOTDIR` on the scanner's own channel.
956
+
957
+ Repository commit-gate only. No runtime or API change.
958
+
608
959
  - **A staged RENAME bypassed the PHI scan's pre-commit route entirely
609
960
  (`PHI-SCAN-RENAME-BLIND-AT-PRECOMMIT`).** `git diff --cached --raw --diff-filter=AMT` returns
610
961
  neither `R` (rename) nor `C` (copy), and rename detection is on by git's default, so
@@ -1640,15 +1991,3 @@ real JS object (literal,`Map`, `Proxy`, or otherwise) can hold two same-named en
1640
1991
  `src/helpers/`, `src/serialize/`, `src/builder/`. Lint, format, and
1641
1992
  TypeScript settings come from the shared `@cosyte/*` config packages
1642
1993
  (ESLint 9 + `typescript-eslint`). CI across Node 22 / 24.
1643
-
1644
- ### Changed
1645
-
1646
- ### Deprecated
1647
-
1648
- ### Removed
1649
-
1650
- ### Fixed
1651
-
1652
- ### Security
1653
-
1654
- [Unreleased]: https://github.com/cosyte/hl7/commits/main
package/README.md CHANGED
@@ -1,7 +1,9 @@
1
- <picture>
2
- <source media="(prefers-color-scheme: dark)" srcset="https://cosyte.com/tile/cosyte-lockup-tile-on-dark-1200x300.png">
3
- <img alt="Cosyte: a plus mark set in two overlapping rounded squares, one solid and one outlined, beside the Cosyte wordmark" src="https://cosyte.com/tile/cosyte-lockup-tile-on-light-1200x300.png">
4
- </picture>
1
+ <a href="https://cosyte.com">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://cosyte.com/tile/cosyte-lockup-tile-on-dark-1200x300.png">
4
+ <img alt="Cosyte: a plus mark set in two overlapping rounded squares, one solid and one outlined, beside the Cosyte wordmark" src="https://cosyte.com/tile/cosyte-lockup-tile-on-light-1200x300.png">
5
+ </picture>
6
+ </a>
5
7
 
6
8
  # @cosyte/hl7
7
9
 
package/dist/index.cjs CHANGED
@@ -6874,7 +6874,7 @@ async function* parseStream(source, optionsOrProfile) {
6874
6874
  }
6875
6875
 
6876
6876
  // src/index.ts
6877
- var VERSION = "0.0.8";
6877
+ var VERSION = "0.0.10";
6878
6878
 
6879
6879
  exports.ACK_CODES = ACK_CODES;
6880
6880
  exports.ACK_CONDITIONS = ACK_CONDITIONS;