@cosyte/hl7 0.0.5 → 0.0.7
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 +248 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -436,6 +436,116 @@ kind, value)`** sets one at a field or `field[rep]` dot-path. Component values a
|
|
|
436
436
|
|
|
437
437
|
### Changed
|
|
438
438
|
|
|
439
|
+
- **The PHI gate's own enumeration no longer lets a vanished build transient refuse an entire sweep
|
|
440
|
+
(`PHI-SCAN-ENUMERATE-THEN-READ-CLASS`).** `scripts/phi-scan.ts` all-mode lists its roots and reads
|
|
441
|
+
each file afterwards; a file deleted inside that window threw `ENOENT` and the scanner refused with
|
|
442
|
+
exit 2. `@cosyte/ccda` is where that landed for real (`ccda#80`, `747ac20`): its walk starts at the
|
|
443
|
+
**repo root**, `tsup` writes and then removes `tsup.config.bundled_<hash>.mjs` there, and a
|
|
444
|
+
publish-time sweep refused, failing `prepublishOnly` and blocking a real publish.
|
|
445
|
+
|
|
446
|
+
**No CI or publish run in this repo was reachable through that window, so this is hardening, not
|
|
447
|
+
the repair of a live defect here.** Say so plainly, because the temptation is to claim the bigger
|
|
448
|
+
thing. The walk is rooted at `test/fixtures` and `src`, so the repo-root transient is never
|
|
449
|
+
enumerated. **Verified in both directions rather than assumed, and re-derived three times in three
|
|
450
|
+
separate runs:** a real `tsup` build was polled while it ran and the only transient observed each
|
|
451
|
+
time was a `tsup.config.bundled_<hash>.mjs` **at the repo root**, outside both walk roots, with
|
|
452
|
+
zero files appearing under either root; and the pre-fix scanner, driven against a throwaway repo
|
|
453
|
+
with an untracked decoy placed **inside** `src/`, refused the whole sweep with exit 2 on the first
|
|
454
|
+
read. The hash differs on every build, which is why no specific filename is quoted here. The
|
|
455
|
+
immunity is scope, not design. Nothing gitignores that name (`git check-ignore` exits 1 on it),
|
|
456
|
+
and `test/docs-content.test.ts` really does run a build from inside the suite while this scanner
|
|
457
|
+
sweeps in all mode from another worker, so the two halves already race. **Widening a walk root,
|
|
458
|
+
for any reason, reintroduces it verbatim.**
|
|
459
|
+
|
|
460
|
+
**A narrower case was already live on a developer box rather than in CI, and the review gate found
|
|
461
|
+
it. It is a class, not one filename.** `.gitignore` covers `*.swp` and `*.swo` and **nothing else
|
|
462
|
+
of the kind**, so editor and atomic-save transients create and remove un-ignored files **inside a
|
|
463
|
+
walk root**. Verified un-ignored: `src/index.ts~`, `src/.index.ts.swn`, `src/#index.ts#`,
|
|
464
|
+
`src/index.ts___jb_tmp___`, `src/.goutputstream-abc123`, `src/4913`, and `.tmp` / `.orig` / `.bak`
|
|
465
|
+
beside a source file. Before this change each is a spurious exit 2 on a developer's machine; none
|
|
466
|
+
reaches CI or a publish, which is why the claim above is scoped to those.
|
|
467
|
+
|
|
468
|
+
**The refusal was correct; the enumeration was unsound.** Refusing a scan it could not complete is
|
|
469
|
+
what makes this gate worth having and it is untouched. Exactly one case is now tolerated: a file
|
|
470
|
+
the walk enumerated **itself**, that git does **not track**, failing with **`ENOENT`**. It is
|
|
471
|
+
written to stderr as skipped, never dropped silently, and the clean line on **stdout** is
|
|
472
|
+
qualified to match (`OK: no hits (N untracked file(s) skipped, see stderr)`), so a reader watching
|
|
473
|
+
stdout alone cannot take an unqualified OK from a sweep that did not observe everything it
|
|
474
|
+
enumerated. A clean sweep still prints the bare line, and a test pins it exactly.
|
|
475
|
+
|
|
476
|
+
**Five of the six bounds are pinned by a test, and which five is measured, not asserted.** Each
|
|
477
|
+
was confirmed by a mutant that only its own test kills:
|
|
478
|
+
- a **tracked** file that cannot be read (mutant: tolerate tracked files too);
|
|
479
|
+
- any **non-`ENOENT`** failure, since `EACCES` / `EISDIR` is a scan that failed, not a file that
|
|
480
|
+
went away (mutant: tolerate any errno);
|
|
481
|
+
- a `git` that cannot report the tracked set, which switches the tolerance off rather than
|
|
482
|
+
guessing (mutant: return an empty set instead of `null`);
|
|
483
|
+
- a tracked set that comes back **empty**, the one state in which the tracked-file bound stops
|
|
484
|
+
existing (same mutant). A **removed** `.git/index` makes `git ls-files` exit 0 with no output,
|
|
485
|
+
which is what the `size > 0` guard catches; a **corrupt** one exits non-zero and was already
|
|
486
|
+
caught by the `catch`;
|
|
487
|
+
- an all-mode sweep that **observed no files at all** (mutant: drop the guard), so the tolerance
|
|
488
|
+
can never decay into a clean report of a tree nothing was read from.
|
|
489
|
+
|
|
490
|
+
**The sixth is UNPINNED and named as the gap it is:** a tolerated file that is **back on disk**
|
|
491
|
+
when the sweep ends still refuses, but no test drives it. The scanner calls `git` only before the
|
|
492
|
+
first read, so there is no deterministic post-read hook, and `walk()` enumerates regular files
|
|
493
|
+
only, so a FIFO cannot be used as a blocking one either (both measured). Reaching it needs a timed
|
|
494
|
+
re-create against a deliberately slowed sweep, and a load-sensitive sleep in a suite guarding a
|
|
495
|
+
load-dependent race is the failure that race teaches. Losing that branch would cost the re-check,
|
|
496
|
+
never the tolerance's bounds. **Unpinned is not unverified:** the review gate drove the branch by
|
|
497
|
+
hand against a deliberately slowed sweep and it refused correctly with exit 2, and deleting the
|
|
498
|
+
re-check outright still leaves all 29 tests green, which is what makes "unpinned" the accurate
|
|
499
|
+
word rather than an understatement.
|
|
500
|
+
|
|
501
|
+
**The test technique is the reusable part: no sleep and no real build.** The scanner runs
|
|
502
|
+
`git check-ignore` and `git ls-files` between the walk and the first read, so a `git` shim placed
|
|
503
|
+
first on `PATH` is a deterministic hook into exactly that gap. Every case runs against a throwaway
|
|
504
|
+
git repo, so no decoy is ever written into this one. As a negative control the same tolerance case
|
|
505
|
+
was replayed against all twelve sibling scanners, and every sibling that has not yet taken this
|
|
506
|
+
change refuses it with exit 2, so the new assertion is not one a sibling's code satisfies by
|
|
507
|
+
accident. **No count is quoted, deliberately:** the same command returned a different tally on
|
|
508
|
+
three consecutive runs while sibling repos landed the same change underneath it, so any number
|
|
509
|
+
written here would be wrong by the time it is read.
|
|
510
|
+
|
|
511
|
+
**Residual, stated rather than closed:** the post-sweep re-check is keyed on the enumerated path,
|
|
512
|
+
not on content, so the class is **content that survives at a path the walk did not enumerate**. An
|
|
513
|
+
untracked file **renamed** inside the window is the obvious instance and goes unscanned under a
|
|
514
|
+
clean report; a hard link followed by an unlink is the other. Both legs of the boundedness
|
|
515
|
+
argument were driven by the review gate rather than argued: after `git add` the all-mode sweep
|
|
516
|
+
catches the renamed file, and `--staged` reports the hit from the index **even with the file
|
|
517
|
+
deleted from disk**. Closing it needs a content-addressed sweep, which is a different design
|
|
518
|
+
rather than a wider bound. The `--staged` pre-commit path reads blobs from the git index
|
|
519
|
+
(`git show :path`) and never depended on any of this.
|
|
520
|
+
|
|
521
|
+
**Two PRE-EXISTING scope limits of the walk are now written down** so they are not confused with
|
|
522
|
+
the window above. Both were measured, and an earlier draft of this entry claimed `--staged` covers
|
|
523
|
+
the commit path in both cases; **the review gate refuted that and it is false, so read `--staged`
|
|
524
|
+
as a compensating control for neither.**
|
|
525
|
+
- **Regular files only**, the sharper of the two. A symlink under a walk root pointing at a
|
|
526
|
+
PHI-bearing file is not enumerated, so all-mode never reads it, **and** git stores a symlink as
|
|
527
|
+
its target string (mode `120000`), so `git show :path` hands `--staged` the path text rather
|
|
528
|
+
than the pointed-to bytes. **Both routes report clean, driven and reproduced independently.**
|
|
529
|
+
What lands in the commit is the link, not the capture, but an identifying _filename_ still
|
|
530
|
+
reaches the tree and gets only the shape pass, which has no name detection. A FIFO is skipped
|
|
531
|
+
rather than blocking the sweep.
|
|
532
|
+
- **Working tree only.** A tracked file absent from the worktree is never enumerated in all-mode.
|
|
533
|
+
`--staged` catches such a file when it is **added** (measured, exit 1), so that coverage is at
|
|
534
|
+
add time and past tense: once it is committed and removed from the worktree, both routes report
|
|
535
|
+
clean.
|
|
536
|
+
|
|
537
|
+
Neither limit is changed here, and widening the walk to close either is a separate slice.
|
|
538
|
+
|
|
539
|
+
**CLOSED for the first bullet by the "not a regular file" entry below; the second bullet still
|
|
540
|
+
stands.** The paragraphs above are left exactly as they shipped rather than rewritten, because
|
|
541
|
+
they described a real state the package passed through and they shipped inside a published
|
|
542
|
+
tarball, so they are text a consumer already has on disk. What is no longer true of the current
|
|
543
|
+
code is the whole of the first bullet, in three places rather than one: a symbolic link under a
|
|
544
|
+
walk root, or staged into `test/fixtures/**` or `src/**.ts`, now **refuses the scan** on both
|
|
545
|
+
routes instead of reading clean on both; **a FIFO refuses too** rather than being skipped; and the
|
|
546
|
+
shape pass no longer runs on a link's target _path_, because the entry is refused before anything
|
|
547
|
+
is read. The second bullet, working-tree-only, still stands exactly as written.
|
|
548
|
+
|
|
439
549
|
- **`README.md` now opens with the shared Cosyte lockup in a `<picture>` block, replacing the
|
|
440
550
|
per-package banner (`1aee04b`).** The dark-ground org tile sits behind a
|
|
441
551
|
`prefers-color-scheme: dark` media query with the light-ground tile as the inner `<img>`, so on a
|
|
@@ -495,6 +605,144 @@ kind, value)`** sets one at a field or `field[rep]` dot-path. Component values a
|
|
|
495
605
|
|
|
496
606
|
### Fixed
|
|
497
607
|
|
|
608
|
+
- **The PHI scan read a symbolic link as clean on BOTH of its enumerating routes
|
|
609
|
+
(`PHI-SCAN-SYMLINK-BLIND-ON-BOTH-ROUTES`).** A link under a scan root pointing at a PHI-bearing
|
|
610
|
+
file passed the gate twice over. **Reproduced on `5debd18` before the fix**, in a throwaway repo,
|
|
611
|
+
with a synthetic name-bearing HL7 message placed outside the walk roots and a link to it at
|
|
612
|
+
`test/fixtures/leak.hl7`: all-mode printed `OK: no hits` and exited **0**, `--staged` after
|
|
613
|
+
`git add` did the same, and naming the target file explicitly exited **1** with seven hits
|
|
614
|
+
(`PID-5` twice, `PID-7`, `PID-11`, `PID-3`, and the shape pass's `(ssn)` and `(email)`). The
|
|
615
|
+
payload was always detectable; the two routes never looked at it.
|
|
616
|
+
|
|
617
|
+
**Two mechanisms, two fixes, and neither route is made to follow the link.** `walk()` enumerates
|
|
618
|
+
`Dirent.isFile()`, which is an lstat answer, so a link is neither a file nor a directory and fell
|
|
619
|
+
out of the loop silently; `isDirectory()` answers false for a **linked directory** too, so a whole
|
|
620
|
+
subtree left the same way. `--staged` reads content with `git show :<path>`, and git stores a link
|
|
621
|
+
as its **target path** under mode `120000`, so that route was handed the path text and never the
|
|
622
|
+
target's bytes. That route is this package's pre-commit hook. Following a link would read bytes
|
|
623
|
+
the enumeration does not control (outside the repo, a loop, a device, a FIFO that blocks the gate
|
|
624
|
+
forever), and git does not carry those bytes anyway, so a hit on them would be a claim about
|
|
625
|
+
something no commit contains. **The enumeration is narrowed instead:** an in-scope entry that is
|
|
626
|
+
not a regular file **refuses the scan** (exit 2), naming every offender rather than the first.
|
|
627
|
+
`--staged` reads `git diff --cached --raw -z` so the destination mode is visible, and refuses
|
|
628
|
+
`120000` and `160000`; an unparseable `--raw` record refuses too, rather than scanning a list that
|
|
629
|
+
may be short.
|
|
630
|
+
|
|
631
|
+
**`--diff-filter=AMT`, and the `T` is load-bearing.** Replacing a **tracked** regular file with a
|
|
632
|
+
link is neither an add nor a modify: git raises it as a typechange
|
|
633
|
+
(`:100644 120000 <sha> <sha> T`), so `--diff-filter=AM` deleted the record before any mode could
|
|
634
|
+
be read and the mode check was unreachable for exactly the files the repo already had. Measured
|
|
635
|
+
here: with `AM` the raw output for that stage is empty. Admitting `T` also scans the **reverse**
|
|
636
|
+
typechange, a link replaced by a real file carrying PHI, as the file it became.
|
|
637
|
+
|
|
638
|
+
**A refusal names the entry's own repo-relative path and an engine-owned kind token, never the
|
|
639
|
+
link target**, which is working-tree text that can itself carry PHI. The scanner's docblock states
|
|
640
|
+
the risky target shape in the abstract rather than showing one, because a diagnostic about a PHI
|
|
641
|
+
leak is itself a PHI surface and that applies to the prose explaining it.
|
|
642
|
+
|
|
643
|
+
**Two behaviour changes that fall out of the same narrowing, both measured, both intended.** A
|
|
644
|
+
FIFO under a walk root used to be skipped (base exit 0) and now refuses (exit 2, named as
|
|
645
|
+
`a FIFO`). And an SSN or email shape sitting in a link's target _path_ used to fire on the shape
|
|
646
|
+
pass under `test/fixtures/` and now never runs, because the entry is refused before anything is
|
|
647
|
+
read. A refusal that names the entry is a better answer than a hit that only describes a filename,
|
|
648
|
+
so both are the intended trade rather than a loss.
|
|
649
|
+
|
|
650
|
+
**What this does NOT close, stated so it is not read as wider than it is.** `R` (rename) and `C`
|
|
651
|
+
(copy) are still not enumerated by `--staged` at all, so a staged rename that appends PHI passes
|
|
652
|
+
that route; admitting them needs the two-path `--raw` record shape handled, which is a scope
|
|
653
|
+
decision rather than this one. If such a record ever reached the parser the field stride would
|
|
654
|
+
desync and it refuses, which is the safe outcome. `--staged`'s path scope is unchanged
|
|
655
|
+
(`test/fixtures/**` and `src/**.ts`), and the walk's gitignore exemption is unchanged: an ignored
|
|
656
|
+
link is out of scope by the same rule that already excludes an ignored file, though an entry in
|
|
657
|
+
the index cannot be excused that way. That prefix is literal, so a link staged at exactly
|
|
658
|
+
`test/fixtures` (the root itself, not an entry under it) is outside the `--staged` filter and is
|
|
659
|
+
unchanged here; it is not both-routes-blind, because `readdirSync` resolves a symlinked walk root
|
|
660
|
+
and all-mode reads straight through it (measured, exit 1 on the PHI). A tracked file absent from
|
|
661
|
+
the worktree is still caught at `git add` time only. The enumerate-then-read tolerance and the
|
|
662
|
+
refuse-a-sweep-that-observed-nothing rule are untouched.
|
|
663
|
+
|
|
664
|
+
Pinned by 19 cases in `test/scripts/phi-scan.test.ts`, **8 of them red on `5debd18`**, all against
|
|
665
|
+
throwaway git repositories so no link and no violator is ever written into the committed corpus.
|
|
666
|
+
The payload is name-bearing and the link target's own filename carries a name, so the
|
|
667
|
+
"never echo the target" assertions are not vacuous.
|
|
668
|
+
|
|
669
|
+
- **The `attw` publish gate reported a pass on a tarball that carried no types (`ATTW-FALSE-GREEN-PORT`).**
|
|
670
|
+
`package.json` ran `attw --pack .`, and that command prints "This package does not contain types."
|
|
671
|
+
and **exits 0**. Every caller that reads the status, including `prepublishOnly` and the shared CI
|
|
672
|
+
pipeline, read the 0. A false red costs an hour; a false green merges.
|
|
673
|
+
|
|
674
|
+
**The defect is `attw`'s exit code, not the invocation and not concurrency.** In this repo's own
|
|
675
|
+
`@arethetypeswrong/cli@0.18.4`, `dist/getExitCode.js` opens with `if (!analysis.types) return 0`,
|
|
676
|
+
so the problem list is never consulted. No `--profile`, `--ignore-rules` or config setting reaches
|
|
677
|
+
that early return. For a package that ships types, "does not contain types" does not mean "fine,
|
|
678
|
+
untyped"; it means the declarations were not in the tarball, which is a broken publish.
|
|
679
|
+
|
|
680
|
+
**Reproduced here with no concurrency at all** (2026-08-03, Node 22): `rm -f dist/index.d.ts
|
|
681
|
+
dist/index.d.cts && pnpm attw` and `rm -rf dist && pnpm attw` both printed the sentence and exited 0. A race only supplies the condition. `tsup` emits the JS bundles in one pass and the declaration
|
|
682
|
+
files in a later one, so there is a window in every build of this package where `dist/` holds
|
|
683
|
+
`.mjs`/`.cjs` and no `.d.ts`: timed on two clean builds here, `dist/index.mjs` appeared at 3.57s
|
|
684
|
+
and 3.58s and `dist/index.d.ts` at 5.81s and 5.65s, a window of 2.24s and 2.07s. A concurrent
|
|
685
|
+
build or `pnpm clean` in the same working tree lands `attw` in it. **That is deliberately not
|
|
686
|
+
answered with a lock, a lease or a build queue**: the gate has to be able to say its own inputs
|
|
687
|
+
were missing, whatever removed them.
|
|
688
|
+
|
|
689
|
+
**The fix is `scripts/attw.mjs`, and it carries two nets that catch different things.** A
|
|
690
|
+
**preflight** checks that every relative artifact path `package.json` promises (`main`, `module`,
|
|
691
|
+
`types`, `typings`, and every string leaf of `exports`) exists and is non-empty before `attw` runs;
|
|
692
|
+
it is structural, does no string matching, and names the missing file. A path written **without** a
|
|
693
|
+
leading `./` counts: `"types": "dist/index.d.ts"` is legal for those four fields and is normalized
|
|
694
|
+
rather than skipped, since skipping it would leave the preflight quietly narrower than every
|
|
695
|
+
sentence describing it. Wildcard `exports` patterns (they name a set, not a file), absolute paths
|
|
696
|
+
and `package.json` itself are skipped. A manifest that declares **nothing** is refused rather than
|
|
697
|
+
passed, because a preflight with no inputs is not a preflight that succeeded. A **post-check** fails
|
|
698
|
+
when
|
|
699
|
+
`attw` still reports an untyped package, which is the case the preflight structurally cannot see:
|
|
700
|
+
declarations present on disk but excluded from the tarball by `files` or `.npmignore`. **No
|
|
701
|
+
instance of that second case is on record in this repo.** The post-check reads a printed sentence,
|
|
702
|
+
so the routes that would hide it are refused rather than tolerated: `--quiet`, `-q`,
|
|
703
|
+
`--format json`, `-f json`, `--format=json`, `-fjson`, `-qP`, `-Pf json`, a `.attw.json` setting
|
|
704
|
+
`quiet` or `format`, and `--config-path`. Each of the first nine was measured against this
|
|
705
|
+
package's own untyped tree to return exit 0 with the sentence absent; `--config-path` is refused by
|
|
706
|
+
inference, not measurement. The refusal is by option name, wholesale, not by value. Other arguments
|
|
707
|
+
are still forwarded, so `--profile node16`, `--no-summary`, `--no-emoji` and `--no-color` work.
|
|
708
|
+
|
|
709
|
+
**Short options are matched by letter anywhere in the cluster, not by whole token, and that is not
|
|
710
|
+
cosmetic.** `commander` bundles short options and lets a value ride on the end, so `-fjson`, `-qP`
|
|
711
|
+
and `-Pf json` are `--format json` and `--quiet` in another spelling. Measured against a package
|
|
712
|
+
whose declarations sit on disk but are excluded from `files`, which is the one shape only the
|
|
713
|
+
post-check can see, a token-matching guard returned **exit 0** on `-fjson` and on `-Pf json`.
|
|
714
|
+
`-qP` was caught, but only by the empty-transcript net rather than by the refusal.
|
|
715
|
+
|
|
716
|
+
**A second class of argument is refused for a different reason, and calling it blinding would be
|
|
717
|
+
wrong.** `--help`, `-h`, `--version` and `-V` print and exit 0 without analysing anything: measured
|
|
718
|
+
on the untyped fixture at 2766 and 48 bytes of output, exit 0, no untyped sentence. The transcript
|
|
719
|
+
is non-empty, so the empty-transcript net does not fire either, and the gate would have read a pass
|
|
720
|
+
off a run that checked nothing. `--definitely-typed` is refused alongside them **by inference, not
|
|
721
|
+
by measurement**, and the distinction is kept because the measurement was attempted and did not
|
|
722
|
+
reproduce: with a version range the untyped sentence still printed. It is refused on a reading of
|
|
723
|
+
the CLI, where a value that looks like a path merges external declarations into the analysis, which
|
|
724
|
+
would let a package that ships none come back typed. `--no-definitely-typed` is a distinct option
|
|
725
|
+
name and is deliberately not refused.
|
|
726
|
+
|
|
727
|
+
**Both of this repo's manifests are fixed, because the census that matters is manifests rather
|
|
728
|
+
than repo roots.** `package.json` and `examples/profile-starter-kit/package.json` each ran the
|
|
729
|
+
bare CLI, and the kit is a template a consumer copies out and publishes from, so leaving it would
|
|
730
|
+
have re-minted the false green in every generated package. The kit gets its own copy, whose code is
|
|
731
|
+
**byte-identical** to the root one below the docblock and is pinned that way by a test; only the
|
|
732
|
+
comment differs, because one is written for a maintainer here and the other for a consumer who
|
|
733
|
+
copied the kit out. Two copies of a gate drift, and the kit's copy is executed by nothing in this
|
|
734
|
+
repo's CI, so the identity pin is what makes the behavioural tests true of both.
|
|
735
|
+
|
|
736
|
+
`test/scripts/attw-gate.test.ts` pins the defect and the remedy against the real binary: that bare
|
|
737
|
+
`attw` exits 0 on both shapes (declarations unpacked, and declarations never built) and on
|
|
738
|
+
`-fjson`, that the wrapper reds each of them, that the preflight names a missing, an empty and a
|
|
739
|
+
`./`-less artifact and refuses a manifest that declares none, that a real `attw` failure still
|
|
740
|
+
propagates `attw`'s own status, that the wrapper is transparent on a well-formed package and does
|
|
741
|
+
not refuse the options that blind nothing, and that each refusal holds. If a future `attw` fixes
|
|
742
|
+
the exit code or rewords the sentence, the suite reds and sends the reader to the wrapper rather
|
|
743
|
+
than letting the net go quietly slack. No runtime or API change; this is a build and publish gate
|
|
744
|
+
only.
|
|
745
|
+
|
|
498
746
|
- **A warning or error message could carry clinical text into a log line (`HL7-WARN-MSG-PHI`).**
|
|
499
747
|
No options were required to reach it: plain `parseHL7(raw)` on lenient defaults.
|
|
500
748
|
|
package/dist/index.cjs
CHANGED