@cosyte/terminology 0.0.10 → 0.0.12
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 +266 -27
- package/README.md +39 -37
- package/dist/index.cjs +51 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +359 -359
- package/dist/index.d.ts +359 -359
- package/dist/index.mjs +51 -51
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/vendor/ucum/NOTICE.md +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,13 +1,267 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## 0.0.12
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- db98da6: Repository PHI commit-gate only, with no runtime impact: the sweep now walks `scripts/`, so the scanner is under its own scan, and any run that had targets and read none of them refuses instead of printing its clean line.
|
|
8
|
+
|
|
9
|
+
Two false-green doors, both the same shape. The gate reported clean over something it never looked
|
|
10
|
+
at. Both were pre-existing, both were found by the refuters on the previous change to this scanner,
|
|
11
|
+
and every reading below was taken back to back on a clone of this repository at `7e68603`.
|
|
12
|
+
|
|
13
|
+
DOOR 1: THE SCANNER NEVER SCANNED ITSELF
|
|
14
|
+
|
|
15
|
+
`scripts/` was under no scan root. The recogniser's own patterns, the allow-list this scanner refuses
|
|
16
|
+
to run without, and the override log it points a developer at all live there, so the one directory
|
|
17
|
+
guaranteed to hold PHI-shaped text was the one nothing enumerated. Measured: `scripts/planted.ts`
|
|
18
|
+
carrying a dashed SSN and an off-domain address exited **0** `OK: no hits` in all-mode, while naming
|
|
19
|
+
the same file directly reported both at exit **1** over the same bytes.
|
|
20
|
+
|
|
21
|
+
The fix is the declaration and nothing else. Both completeness rules already read `SCAN_ROOTS`: the
|
|
22
|
+
per-root observation rule and the reconciliation against `git ls-files`. Adding `scripts` to that one
|
|
23
|
+
list widens the walk, the per-root floor and the reconciliation together, with no new machinery, and
|
|
24
|
+
each of the three is pinned separately rather than assumed to have followed.
|
|
25
|
+
|
|
26
|
+
The scope was re-derived and this root is not shaped like `test/`. `git ls-files scripts` returns 8
|
|
27
|
+
paths and they are not all TypeScript: two `.ts`, three `.mjs`, two `.sh`, one `.txt`. So it is the
|
|
28
|
+
first root partly outside the source-literal view, and the two halves of the previous widening land
|
|
29
|
+
unevenly: `.ts` and `.mjs` get the raw floor and the escape-decoded view, `.sh` and `.txt` get the
|
|
30
|
+
raw floor only. That is the right answer for a `.txt`, which is its own document, and a residual for
|
|
31
|
+
a `.sh`, which can spell a value through a shell escape. Recorded rather than guarded, and pinned by
|
|
32
|
+
an anti-fabrication case so the boundary is visible instead of inferred.
|
|
33
|
+
|
|
34
|
+
No exemption was needed and none was added. All 8 files were measured against both recognisers, raw
|
|
35
|
+
and decoded, before the root was declared: zero hits, so the widening lands green on its own bytes
|
|
36
|
+
rather than on a new carve-out. The scanner is now under its own scan, so a real-looking value
|
|
37
|
+
written into a comment in it reds the gate. That is the intended pressure.
|
|
38
|
+
|
|
39
|
+
DOOR 2: A BYPASS WITH NO PATH SCANNED ZERO FILES AND PASSED
|
|
40
|
+
|
|
41
|
+
`--allow-fixture <path>` seeds the positional path set, so the flag means "scan this, but allow it"
|
|
42
|
+
rather than a silent no-op. With no positional path the whole target list was that one path, the
|
|
43
|
+
bypass subtracted it, and the run printed `[phi-scan] OK: no hits` at exit **0** over zero files:
|
|
44
|
+
byte-identical stdout and exit code to the genuine clean sweep on the same tree. Measured with the
|
|
45
|
+
path logged in the override file exactly as this scanner's own rejection message instructs, so the
|
|
46
|
+
false green is reached by following the printed remedy.
|
|
47
|
+
|
|
48
|
+
It is fixed with the rule this repository already had, at the scope of the whole invocation, and not
|
|
49
|
+
with a new mechanism. That rule refuses a sweep that observed nothing under a root; the refusal now
|
|
50
|
+
also covers a run that observed nothing at all. A zero-target sweep is a refusal at exit 2, never a
|
|
51
|
+
pass. The per-root tier does not reach the path or staged modes, which enumerate no root, and the
|
|
52
|
+
whole-invocation tier says nothing about any individual root, so neither subsumes the other and both
|
|
53
|
+
are kept.
|
|
54
|
+
|
|
55
|
+
A denominator is still not the answer. Printing "0 files scanned" and exiting 0 is the same defect
|
|
56
|
+
with better telemetry, because a count counts the targets that did exist. The distinction that
|
|
57
|
+
matters is existence against observation, and the term this rule reads is never printed as a count
|
|
58
|
+
and is never compared to the number read.
|
|
59
|
+
|
|
60
|
+
One legitimate zero is named rather than inferred: a staged run whose commit touches nothing in
|
|
61
|
+
scope, which is the pre-commit hook's ordinary markdown-only case. A staged run whose in-scope files
|
|
62
|
+
were all withdrawn by a bypass had work and did none of it, and refuses exactly like a path run does.
|
|
63
|
+
|
|
64
|
+
Deliberately unchanged: the pre-commit route's scope predicate, which admits neither `test/**` nor
|
|
65
|
+
`scripts/**` and whose widening changes what a developer's commit is blocked on. No structured
|
|
66
|
+
detector was added, so a green sweep still means "no SSN or email shapes found", never "no PHI".
|
|
67
|
+
|
|
68
|
+
- 7e68603: Repository PHI commit-gate only, with no runtime impact: the all-mode sweep now walks `test/` as well as `src/`, reads each TypeScript source as the document its string literals spell rather than only as raw bytes, and reconciles what it read against `git ls-files` instead of vouching for itself.
|
|
69
|
+
|
|
70
|
+
`pnpm phi-scan` with no arguments is what CI runs. It walked `src` and nothing else, and the
|
|
71
|
+
pre-commit route's scope predicate admits `src/**.ts` plus `test/fixtures/**`, a directory that has
|
|
72
|
+
never existed in this repository. **So every tracked file under `test/` was enumerated by neither
|
|
73
|
+
route.** Measured on `d97a3de`: `git ls-files test/ | wc -l` is **50**, and all 50 are `.ts`. Back to
|
|
74
|
+
back on that sha, a dashed SSN written to `test/planted.ts` exited **0** `OK: no hits` in all-mode
|
|
75
|
+
while naming the same file directly exited **1** over the same bytes.
|
|
76
|
+
|
|
77
|
+
**The scope was re-derived here rather than ported, and it came out different from every sibling's.**
|
|
78
|
+
There is no `PID|` literal anywhere in this tree, so the residual lists that drove the sibling repos
|
|
79
|
+
describe those repos and not this one. Here the whole corpus is inline TypeScript, and exactly one
|
|
80
|
+
file carries violator shapes.
|
|
81
|
+
|
|
82
|
+
**Enumerating the files buys the SSN/email floor and nothing else, which is why the widening is
|
|
83
|
+
two-sided.** Both recognisers match raw file text, which assumes the file IS the document. That holds
|
|
84
|
+
for a fixture that is its own file and breaks for one that is a string literal, because a literal can
|
|
85
|
+
spell any character as an escape. Every fixture here is a `.ts` literal, so admitting 50 more files
|
|
86
|
+
without the other half would have carried the blind spot into all of them instead of closing it.
|
|
87
|
+
`decodeSourceLiterals` therefore scans the decoded document **in addition to** the raw text, never
|
|
88
|
+
instead, for source containers only. Proved red before and green after in `src/` as well as `test/`,
|
|
89
|
+
so it is not merely a consequence of the new root. Only values the raw pass did not itself report are
|
|
90
|
+
added, deduped against **that pass's own hits** rather than against the raw text: both recognisers
|
|
91
|
+
are word-boundary anchored, so a value can sit in the file as a substring the raw pass correctly
|
|
92
|
+
declines to report, and comparing against the text discarded a real escape-spelled SSN.
|
|
93
|
+
|
|
94
|
+
**An escaped backslash is consumed first.** A doubled backslash in source is one literal backslash,
|
|
95
|
+
so a doubled backslash followed by `u002D` decodes to a backslash and that text, not to a dash; a
|
|
96
|
+
naive global regex replace of the four-hex-digit escape gets that backwards. That ordering rule is
|
|
97
|
+
exact, but it is a statement about the decoder and **not** a guarantee about hits, and it is written
|
|
98
|
+
that way deliberately.
|
|
99
|
+
|
|
100
|
+
**The view is not a literal parser, and both directions of that are recorded rather than glossed.**
|
|
101
|
+
It reports on text that is not an escape at all, in a `String.raw` template and in any comment
|
|
102
|
+
quoting an escape sequence, so the value named can be one the document does not contain. It also
|
|
103
|
+
misses two ordinary spellings that do evaluate to a dashed SSN: string concatenation, and a line
|
|
104
|
+
continuation. Recording a false-positive class is only honest if the printed remedy works, and it did
|
|
105
|
+
not: the allow-list's `ids` set was declared and consulted nowhere, so the only way to clear an SSN
|
|
106
|
+
hit was to edit the source, which is the "gate whose own remedy leaves it refusing" shape this file
|
|
107
|
+
refuses in four other places. The floor's dashed-SSN check now consults it, as a whole-value match
|
|
108
|
+
against a reviewed committed declaration, never a pattern. All of it is pinned by tests.
|
|
109
|
+
|
|
110
|
+
**The sweep is now reconciled against an independent enumeration.** The per-root observation rule
|
|
111
|
+
from the previous change is a floor of ONE file, so declaring `test` a root would otherwise be
|
|
112
|
+
satisfied by any single file under it while the other 49 went unread: the same defect one level down.
|
|
113
|
+
A printed denominator is not the remedy and was refuted as one, because a count is derived from the
|
|
114
|
+
walk and therefore agrees with the walk by construction. Reconciling against `git ls-files` is the
|
|
115
|
+
only version that can disagree. **Three limits previously recorded as open are closed by it, for
|
|
116
|
+
tracked files:** a directory missing from inside a root, a root that is itself a symlink to a
|
|
117
|
+
directory (which the lexical path attribution used to satisfy), and the floor of one. All three
|
|
118
|
+
printed `OK: no hits` at exit 0 before and refuse at exit 2 now, naming the unread paths.
|
|
119
|
+
|
|
120
|
+
**Both rules are kept and neither subsumes the other:** a root with zero tracked files reconciles
|
|
121
|
+
vacuously and still has to starve. **What it deliberately does not claim** is recorded rather than
|
|
122
|
+
glossed: an untracked file under a root is walked, read and scanned, so it cannot hide PHI, but its
|
|
123
|
+
absence is invisible to both rules. The markdown skip and the gitignore filter are shared with the
|
|
124
|
+
walk, so the rule can never demand a file the walk would refuse to read. A tracked gitlink under a
|
|
125
|
+
root is demanded like any other tracked path and the printed remedy does not really fit it, which is
|
|
126
|
+
recorded as an edge rather than guessed at. And `git check-ignore` reports nothing for a path in the index
|
|
127
|
+
(measured), so a gitignore pattern does not excuse a tracked file, which is pinned because the
|
|
128
|
+
opposite is the intuitive guess and would open a hole a one-line `.gitignore` could drive through.
|
|
129
|
+
|
|
130
|
+
**One file is exempt from the shape passes, by path.** This scanner's own suite carries a dashed SSN
|
|
131
|
+
and two non-test-domain addresses on purpose, as the positive half of its tests, and sweeping it
|
|
132
|
+
would red the gate forever. The exemption is a path list and must stay one, because an extension rule
|
|
133
|
+
cannot tell a file that carries violator literals on purpose from one that carries them by accident.
|
|
134
|
+
Allow-listing the values instead is refused: the allow-list's email entry is global, so declaring
|
|
135
|
+
that domain would switch the detector off for the whole corpus while the suite's own positive case
|
|
136
|
+
asserts that address is reported. **The exemption is applied at the scan, not at the enumeration**, so
|
|
137
|
+
the file is still walked, still read, and still counts as observed and reconciled. **And it is scoped
|
|
138
|
+
to the sweep**: naming that file directly still reports its hits, because an unscoped exemption
|
|
139
|
+
deleted a detection the base had, which is "instead of" where this work may only be "in addition to".
|
|
140
|
+
The anti-vacuity control scans its exact bytes under another name and gets a hit.
|
|
141
|
+
|
|
142
|
+
**The retired-root refusal is now inert for its only entry and is kept anyway.** `test/fixtures` sits
|
|
143
|
+
under the new `test` root, so the refusal cannot fire from this repository's configuration. That is
|
|
144
|
+
the silencing its own remedy promises, and the outcome is strictly stronger: a corpus arriving there
|
|
145
|
+
is walked and scanned rather than refused as unaccountable, and a dangling link there is now refused
|
|
146
|
+
by name where the previous guard could not see it at all. The machinery is general, so it is
|
|
147
|
+
exercised against a copy of the scanner with a root retired outside every scan root, rather than left
|
|
148
|
+
as an untested claim.
|
|
149
|
+
|
|
150
|
+
**Two pre-existing minors are folded in.** Exit 1 is reserved for hits, and a directory the walk
|
|
151
|
+
cannot list (a non-directory root, or an unreadable directory at any depth) and an absent or
|
|
152
|
+
unreadable allow-list all used to reach node's default handler and exit **1**, printing a stack trace
|
|
153
|
+
under the code a caller reads as a verdict about PHI. All are exit **2** now, derived from this
|
|
154
|
+
scanner's own stated contract rather than ported, because siblings disagree with each other on this
|
|
155
|
+
exact case. Separately, the two direction-specific `invertGem` messages were pinned with a substring
|
|
156
|
+
match against this repository's own rule; both messages contain both direction tokens, so the pin was
|
|
157
|
+
weak twice over, and they are now pinned by whole-message equality. A third minor carried by siblings
|
|
158
|
+
was measured **not open here**: the staged route already runs `--diff-filter=AMTU`, so an unmerged
|
|
159
|
+
path is already enumerated and refused.
|
|
160
|
+
|
|
161
|
+
**Deliberately unchanged:** the `--staged` scope predicate. A staged file under `test/` outside
|
|
162
|
+
`test/fixtures/` is therefore scanned by CI and not by the pre-commit hook. Nothing regressed, since
|
|
163
|
+
that path was in neither route before, but the two sets now overlap differently and the pre-commit
|
|
164
|
+
one is narrower. Widening it changes what a developer's commit is blocked on, which is a decision
|
|
165
|
+
about the hook rather than the walk. **And no structured detector was added:** a green sweep still
|
|
166
|
+
means "no SSN/email shapes found", never "no PHI".
|
|
167
|
+
|
|
168
|
+
Tests run on throwaway repositories, so no violator is written into the committed corpus, except the
|
|
169
|
+
one exempt file that already carried them. Several are red against the superseded scanner, and the
|
|
170
|
+
anti-fabrication and anti-vacuity controls exist so that no case proves merely that a rule never
|
|
171
|
+
fires.
|
|
172
|
+
|
|
173
|
+
- d97a3de: Rewrote every em dash out of the package's prose and added a CI gate that keeps them out.
|
|
174
|
+
|
|
175
|
+
The em dash is banned across every Cosyte surface, and until now this package carried 1,327 of them: in the README, in the documentation pages, and in the `src/` JSDoc that compiles into the published type declarations and renders on hover in a consumer's editor. All of those are rewritten with a period, a colon, a comma or parentheses. No documented behaviour, code, warning code or type changes.
|
|
176
|
+
|
|
177
|
+
Three strings a consumer can observe do change wording, in punctuation only: the two `invertGem` refusal messages, the general `invertGem` fallback message, and the `reason` on a `parseUcum` failure for an unbalanced parenthesis. Each now uses a colon where it used a dash. Diagnostic codes are untouched, and no code was renamed.
|
|
178
|
+
|
|
179
|
+
A new check, `pnpm check:no-emdash`, scans every tracked file and every tracked filename, and on a pull request the title, body and commit messages as well. It is zero-dependency Node so that no shell tooling can silently narrow what it reads, it assembles every banned spelling from the character's own codepoint so that it holds its own source to the same rule, and it refuses to report a clean result whenever it cannot prove it read its subject. The dated history at the foot of `CHANGELOG.md` is out of scope on purpose: those entries are byte identical to the tarballs they shipped in.
|
|
180
|
+
|
|
181
|
+
## 0.0.11
|
|
182
|
+
|
|
183
|
+
### Patch Changes
|
|
184
|
+
|
|
185
|
+
- 9b5b390: The npm `description` no longer carries an em dash (`EMDASH-CONFORMANCE`).
|
|
186
|
+
|
|
187
|
+
The brand rule bans U+2014 on every cosyte surface, and this string is the most visible one the
|
|
188
|
+
package has: it is the subtitle on the npm package page and the one line shown in every npm search
|
|
189
|
+
result. It now reads with a colon, which is what the rule's own remedy list names first.
|
|
190
|
+
|
|
191
|
+
Scoped deliberately to the description alone. The full tree sweep for this repo is a separate unit,
|
|
192
|
+
still queued, and lands with the CI gate that keeps it swept: a gate without the sweep reds CI on
|
|
193
|
+
arrival, and a sweep without the gate grows the character back.
|
|
194
|
+
|
|
195
|
+
- 1374d15: `CHANGELOG.md` is now written by the release rather than by hand, so the copy inside every published tarball stops presenting already-shipped work as unreleased.
|
|
196
|
+
|
|
197
|
+
The file is listed in `package.json#files`, so it ships with the package. For the whole of this package's published history `.changeset/config.json` disabled Changesets' own changelog generation, which meant no release ever wrote a version heading into it and nothing ever rolled its single hand-maintained `[Unreleased]` heading over. Entries that had gone out sat under that heading beside entries that had not, with nothing to tell a reader which was which, while the package published its way to `0.0.10` on top of them. A later note in the same file described the changelog generator as off, which is a maintenance instruction pointing the wrong way inside a document a consumer reads.
|
|
198
|
+
|
|
199
|
+
The mechanism changed rather than the sentences: `changelog` now names the default Changesets generator, so each release writes its own version heading and the changeset summary becomes the entry a reader sees. Correcting the headings by hand would have left in place the mechanism that produced them, to produce them again on the next release.
|
|
200
|
+
|
|
201
|
+
The hand-written history is preserved verbatim, moved under a new `Released before this file was generated` heading with generated sections above it. No entry was reworded, re-ordered, re-wrapped or re-sorted, and this was measured against the registry rather than against the repository: every entry is byte identical to its copy inside the published `0.0.10` tarball, which is what an installed copy holds. What was dropped was scaffolding for the workflow that no longer runs, together with the prose it belonged to: the `[Unreleased]` heading, its link definition at the foot of the file, the preamble calling the file hand-maintained, and the note that described the generator as off. What release attribution the file had is kept rather than flattened: the heading that note sat under is retitled to name the versions its entries went out in, so a reader can still separate the entries that shipped in `0.0.10` from those that shipped across `0.0.2` through `0.0.9`, and from the dated `0.0.1` section below them. Two facts that note carried are kept because they remain true: no version between `0.0.2` and `0.0.10` has a section of its own, and `0.0.3` never reached npm.
|
|
202
|
+
|
|
203
|
+
The release's Prettier pass is left on, derived from this package having no `.prettierignore` and a format check that covers root markdown, so the archived history is already Prettier-canonical and comes through a release unchanged.
|
|
204
|
+
|
|
205
|
+
No runtime code, public API, type, diagnostic code or result changed.
|
|
206
|
+
|
|
207
|
+
- 7ae2c1b: The README lockup now links to cosyte.com (`ASSETS`).
|
|
208
|
+
|
|
209
|
+
The `<picture>` block above the H1 is wrapped in an anchor to https://cosyte.com, per the founder
|
|
210
|
+
requirement of 2026-08-06. Nothing inside the block moved: the `<source>`, the `<img>`, the alt text
|
|
211
|
+
and both tile URLs are byte-identical.
|
|
212
|
+
|
|
213
|
+
What the anchor does was measured on both surfaces by `fhir`, not assumed, because fourteen READMEs
|
|
214
|
+
carry this shape. On GitHub the anchor works and the colour-scheme switch keeps working, because the
|
|
215
|
+
`<img>` stays a direct child of `<picture>`, which is the condition the HTML spec puts on `<source>`
|
|
216
|
+
applying at all. On an npm package page the anchor is lost: npm wraps a README image in its own
|
|
217
|
+
anchor to the image file, a nested anchor is not representable, so the parser closes ours early and
|
|
218
|
+
the image ends up linked to the image file rather than to cosyte.com. Shipped anyway by founder
|
|
219
|
+
decision of 2026-08-07: on npm that is no worse than the unlinked lockup it replaces, and GitHub is
|
|
220
|
+
where these READMEs are read.
|
|
221
|
+
|
|
222
|
+
## Released before this file was generated
|
|
223
|
+
|
|
224
|
+
Every release section above this heading is written by
|
|
225
|
+
[Changesets](https://github.com/changesets/changesets) from the changesets in `.changeset/`, newest
|
|
226
|
+
release first. The release writes its own version heading, so nothing above this line is maintained
|
|
227
|
+
by hand: a change is recorded by adding a changeset, and that changeset's summary is the entry a
|
|
228
|
+
reader sees here.
|
|
229
|
+
|
|
230
|
+
Everything from here down to `## [0.0.1]` was maintained by hand, and all of it has shipped. It
|
|
231
|
+
accumulated under a single `[Unreleased]` heading that no release ever rolled over, so the file mixed
|
|
232
|
+
released work in with work that had not gone out and gave a reader no way to tell them apart, while
|
|
233
|
+
the package published its way to `0.0.10` on top of it. What release attribution the file does have
|
|
234
|
+
is carried by headings, and both of them are kept: the entries between here and the next heading went
|
|
235
|
+
out in `0.0.10`, the entries under `Released in 0.0.2 through 0.0.9` went out across those versions
|
|
236
|
+
without ever being split any finer, and `## [0.0.1]` below has a dated section and a release link of
|
|
237
|
+
its own.
|
|
238
|
+
|
|
239
|
+
The entries are left exactly as they were written rather than re-sorted into version sections:
|
|
240
|
+
re-sorting would rewrite text that published tarballs already carry, and no version from `0.0.2` to
|
|
241
|
+
`0.0.10` has a section of its own or a compare link. Measured against the registry rather than against
|
|
242
|
+
this repository, because the registry is what an installed copy holds: every entry below is byte
|
|
243
|
+
identical to its copy inside the published `0.0.10` tarball. Nothing below was reworded, re-ordered
|
|
244
|
+
or re-wrapped; neither what was removed nor what was added is an entry.
|
|
4
245
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
246
|
+
What was removed was scaffolding for the hand-written workflow that no longer runs, together with the
|
|
247
|
+
prose that scaffolding belonged to: the `[Unreleased]` heading itself, its link definition at the foot
|
|
248
|
+
of the file, the preamble that described this file as maintained by hand, and the note under the next
|
|
249
|
+
heading, which described the generator as off and pointed at the work this change performs. That
|
|
250
|
+
heading is kept and retitled to say which releases its entries went out in.
|
|
9
251
|
|
|
10
|
-
|
|
252
|
+
One consequence is left standing rather than edited, because editing it would rewrite published text:
|
|
253
|
+
an entry in the `0.0.2` through `0.0.9` block refers to "the `[Unreleased]` entry above", and no such
|
|
254
|
+
heading exists any more.
|
|
255
|
+
|
|
256
|
+
`0.0.3` never reaching npm is deliberate now, though it began as a consequence rather than a choice:
|
|
257
|
+
a leftover changeset meant that release run only opened the next version PR, and the content shipped
|
|
258
|
+
in `0.0.4`. It is not a hole to be filled, because publishing a `0.0.3` now would place it after
|
|
259
|
+
`0.0.4` in registry order.
|
|
260
|
+
|
|
261
|
+
The entries below follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the generated
|
|
262
|
+
sections above use the format Changesets writes, which is a version heading and a list of the
|
|
263
|
+
changes that release consumed. Versions follow the cosyte pre-alpha ladder, `0.0.x` until first
|
|
264
|
+
alpha, rather than [Semantic Versioning](https://semver.org/spec/v2.0.0.html) alone.
|
|
11
265
|
|
|
12
266
|
### Fixed
|
|
13
267
|
|
|
@@ -248,26 +502,12 @@ test/fixtures/<name>` staged as `:120000 120000 <sha> <sha> R100` with **two** p
|
|
|
248
502
|
file wins regardless of the argument allow-list and no argv-level pin can reach it. This change
|
|
249
503
|
narrows nothing there, and the gate's docblock keeps stating it as the known limit it is.
|
|
250
504
|
|
|
251
|
-
## Released
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
> of it as work still awaiting a release was wrong, which is why the heading changed and the entries
|
|
258
|
-
> did not.
|
|
259
|
-
>
|
|
260
|
-
> **What is deliberately NOT done here, and is tracked as `TERMINOLOGY-CHANGELOG-GAP`:** splitting
|
|
261
|
-
> this into one `## [x.y.z]` section per published version, and the founder-owned question of whether
|
|
262
|
-
> to turn the generator on across the repos that share this layout. No version from `0.0.2` to
|
|
263
|
-
> `0.0.9` has a section of its own below, and none has a compare link. The `[Unreleased]` link at the
|
|
264
|
-
> foot of this file now spans `v0.0.9...HEAD`, which is what that section actually holds; it spanned
|
|
265
|
-
> `v0.0.1...HEAD` while this block was mislabelled.
|
|
266
|
-
>
|
|
267
|
-
> **`0.0.3` never reached npm, and LEAVING it that way is the deliberate part.** The gap itself was
|
|
268
|
-
> a consequence, not a choice: a leftover changeset meant that release run only opened the next
|
|
269
|
-
> version PR, and the content shipped in `0.0.4`. It is not a hole to be filled, because publishing a
|
|
270
|
-
> `0.0.3` now would place it after `0.0.4` in registry order.
|
|
505
|
+
## Released in 0.0.2 through 0.0.9
|
|
506
|
+
|
|
507
|
+
These entries accumulated under `[Unreleased]` across those versions and were never split per
|
|
508
|
+
version; none of them has a section of its own or a compare link. One entry below carries a
|
|
509
|
+
`[Superseded]` annotation that was added during the `0.0.10` cycle, so that annotation shipped later
|
|
510
|
+
than the entry it annotates.
|
|
271
511
|
|
|
272
512
|
### Changed
|
|
273
513
|
|
|
@@ -1322,5 +1562,4 @@ ladder (`0.0.x` until first alpha).
|
|
|
1322
1562
|
`WARNING_CODES` entry, and the parse-oriented option/warning types). This package is an engine, not
|
|
1323
1563
|
a parser; that placeholder surface never shipped.
|
|
1324
1564
|
|
|
1325
|
-
[Unreleased]: https://github.com/cosyte/terminology/compare/v0.0.9...HEAD
|
|
1326
1565
|
[0.0.1]: https://github.com/cosyte/terminology/releases/tag/v0.0.1
|
package/README.md
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
|
-
<
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
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/terminology
|
|
7
9
|
|
|
8
|
-
> A **zero-dependency terminology engine** for US healthcare code systems
|
|
10
|
+
> A **zero-dependency terminology engine** for US healthcare code systems: FHIR-shaped, BYO-data,
|
|
9
11
|
> and it **never fabricates a code**.
|
|
10
12
|
|
|
11
13
|
`@cosyte/terminology` is **not a wire parser**. It mirrors the FHIR **Terminology Module**
|
|
12
14
|
(`$translate`, `$lookup`, `$validate-code`, `$expand`, …), operating over **consumer-supplied** FHIR
|
|
13
15
|
resources. It is the sibling engine `@cosyte/transform` and, later, the parsers' code-system
|
|
14
|
-
recognition consume. It ships the **engine, and no code-system release
|
|
16
|
+
recognition consume. It ships the **engine, and no code-system release**: SNOMED CT, CPT, LOINC,
|
|
15
17
|
UMLS/RxNorm, and VSAC value sets are strictly bring-your-own. It is not content-free, though: what it
|
|
16
18
|
_does_ bundle, including the UCUM unit table, the code-system _identities_ (OID ↔ canonical URI), the
|
|
17
19
|
SNOMED CT concepts the crosswalk resolver names and RxNorm's relationship and term-type names, is
|
|
18
20
|
listed with its copyright under [What is bundled](#what-is-bundled).
|
|
19
21
|
|
|
20
|
-
> **Status:** pre-alpha (`0.0.x`), **published on npm.** The **engine is complete
|
|
22
|
+
> **Status:** pre-alpha (`0.0.x`), **published on npm.** The **engine is complete**, every
|
|
21
23
|
> operation below ships today. The surface:
|
|
22
24
|
>
|
|
23
25
|
> - the code-system **identity resolver** (`resolveSystem`) and the ConceptMap **`$translate`** engine
|
|
@@ -27,17 +29,17 @@ listed with its copyright under [What is bundled](#what-is-bundled).
|
|
|
27
29
|
> - **ValueSet** binding (`compose` / **`$expand`** / **`$validate-code`**), subsumption read from the
|
|
28
30
|
> release's own hierarchy;
|
|
29
31
|
> - **UCUM** unit validation and canonicalization (`validateUcum` / `ucumEqual`, recognition only, **no
|
|
30
|
-
> magnitude conversion**)
|
|
31
|
-
> - the **crosswalk resolvers
|
|
32
|
+
> magnitude conversion**): the official UCUM functional-test suite is the conformance gate;
|
|
33
|
+
> - the **crosswalk resolvers**: the CMS **ICD-9↔ICD-10 GEMs** (`loadGems` / `applyGem`,
|
|
32
34
|
> public-domain) and the NLM **SNOMED CT → ICD-10-CM complex map** (`loadComplexMap` /
|
|
33
|
-
> `applyComplexMap`, BYO
|
|
35
|
+
> `applyComplexMap`, BYO: no SNOMED CT refset bundled), never inverted;
|
|
34
36
|
> - the **RxNorm drug graph** (`loadRxNormGraph` + `ingredientsOf` / `genericFor` / `brandsFor` /
|
|
35
37
|
> `doseFormsOf` / `consistsOf` / `resolveNdc` / `approximateMatch`) over a **BYO** RxNorm RRF release,
|
|
36
38
|
> edges read in RxNorm's documented direction and never inverted, an absent `RXCUI`/NDC typed, never
|
|
37
39
|
> fabricated.
|
|
38
40
|
>
|
|
39
41
|
> **Bring your own data.** The engine is whole; **no code-system release is bundled.** It runs over
|
|
40
|
-
> the FHIR resources and standard releases _you_ supply
|
|
42
|
+
> the FHIR resources and standard releases _you_ supply: SNOMED CT, CPT, LOINC, UMLS/RxNorm, the CMS
|
|
41
43
|
> GEM files, the NLM complex-map refset and VSAC value sets are all bring-your-own, under whatever
|
|
42
44
|
> terms their steward sets. The content packs that would change that (RxNorm Prescribable, ICD-10-CM)
|
|
43
45
|
> are not bundled either. For the things that **are** bundled, and under whose copyright, see
|
|
@@ -70,7 +72,7 @@ const map = loadConceptMap(myFhirConceptMapJson); // a standard FHIR R4 ConceptM
|
|
|
70
72
|
const result = translate({ system: "http://loinc.org", code: "2160-0" }, map);
|
|
71
73
|
|
|
72
74
|
if (result.unmapped) {
|
|
73
|
-
// Typed, surfaced outcome
|
|
75
|
+
// Typed, surfaced outcome: carries any group.unmapped fallback mode. Never a guessed target.
|
|
74
76
|
} else {
|
|
75
77
|
for (const m of result.matches) {
|
|
76
78
|
m.target; // a frozen Coding, drawn verbatim from the map
|
|
@@ -87,7 +89,7 @@ if (result.unmapped) {
|
|
|
87
89
|
import { loadCodeSystem, lookup, validateCode } from "@cosyte/terminology";
|
|
88
90
|
|
|
89
91
|
// Load a consumer-supplied release: RRF (RxNorm/UMLS), CSV (LOINC), fixed-width
|
|
90
|
-
// (ICD-10-CM order file
|
|
92
|
+
// (ICD-10-CM order file, see ICD10CM_ORDER_FILE_FIELDS), or a FHIR CodeSystem JSON.
|
|
91
93
|
const cs = loadCodeSystem({ format: "fhir", resource: myFhirCodeSystemJson });
|
|
92
94
|
|
|
93
95
|
const hit = lookup(cs, "2160-0"); // $lookup
|
|
@@ -95,48 +97,48 @@ if (hit.found) {
|
|
|
95
97
|
hit.display; // the preferred display, verbatim from the release
|
|
96
98
|
hit.status; // deprecated / header-not-billable / obsolete / … carried, never presented clean
|
|
97
99
|
} else {
|
|
98
|
-
hit.code; // "TERM_CODE_UNKNOWN"
|
|
100
|
+
hit.code; // "TERM_CODE_UNKNOWN": never a fabricated display
|
|
99
101
|
}
|
|
100
102
|
|
|
101
|
-
validateCode(cs, "2160-0").valid; // $validate-code
|
|
103
|
+
validateCode(cs, "2160-0").valid; // $validate-code: true iff present; never a guessed true
|
|
102
104
|
```
|
|
103
105
|
|
|
104
106
|
## Validate and compare UCUM units
|
|
105
107
|
|
|
106
|
-
Recognition and canonicalization only
|
|
108
|
+
Recognition and canonicalization only: **no magnitude conversion** (`5 mg/dL` → `mmol/L` needs the
|
|
107
109
|
analyte's molar mass, a clinical computation this engine refuses).
|
|
108
110
|
|
|
109
111
|
```ts
|
|
110
112
|
import { validateUcum, ucumEqual } from "@cosyte/terminology";
|
|
111
113
|
|
|
112
114
|
const v = validateUcum("mmol/L");
|
|
113
|
-
v.valid; // true
|
|
114
|
-
validateUcum("mg/dl/").valid; // false
|
|
115
|
+
v.valid; // true: v.canonical is a stable canonical descriptor
|
|
116
|
+
validateUcum("mg/dl/").valid; // false: code "TERM_UCUM_INVALID", never a guessed "nearest" unit
|
|
115
117
|
|
|
116
|
-
ucumEqual("N", "kg.m/s2"); // true
|
|
117
|
-
ucumEqual("mmol/L", "mmol.L-1"); // true
|
|
118
|
-
ucumEqual("mg", "g"); // false
|
|
119
|
-
ucumEqual("Cel", "K"); // false
|
|
118
|
+
ucumEqual("N", "kg.m/s2"); // true: same unit, different spelling
|
|
119
|
+
ucumEqual("mmol/L", "mmol.L-1"); // true: annotations inert, operators normalized
|
|
120
|
+
ucumEqual("mg", "g"); // false: different units (this is not conversion)
|
|
121
|
+
ucumEqual("Cel", "K"); // false: a special (non-linear) unit is never equated with a linear one
|
|
120
122
|
```
|
|
121
123
|
|
|
122
124
|
The UCUM table is the official `ucum-essence.xml` (version 2.2), and it **is bundled with this
|
|
123
|
-
package
|
|
125
|
+
package**: embedded verbatim in the published build and parsed at runtime. It is copyright
|
|
124
126
|
©1999–2024 Regenstrief Institute, Inc., reproduced under the UCUM Copyright Notice and License
|
|
125
127
|
(<https://ucum.org/license>); the engine ships no derivative of it and passes the official UCUM
|
|
126
|
-
functional-test suite. The full notice ships in the package at `vendor/ucum/NOTICE.md
|
|
128
|
+
functional-test suite. The full notice ships in the package at `vendor/ucum/NOTICE.md`: see
|
|
127
129
|
[What is bundled](#what-is-bundled).
|
|
128
130
|
|
|
129
|
-
> **Case-sensitive (c/s) mode only.** UCUM's normative interchange mode
|
|
131
|
+
> **Case-sensitive (c/s) mode only.** UCUM's normative interchange mode (the one FHIR/HL7 bind to)
|
|
130
132
|
> is case-sensitive (`m` = metre, `M` = mega, `Pa` = pascal, `pA` = picoampere). The
|
|
131
133
|
> case-**insensitive** (c/i) spelling variant is **not** supported; a c/i-only string returns a typed
|
|
132
|
-
> `TERM_UCUM_INVALID` (fail-safe
|
|
134
|
+
> `TERM_UCUM_INVALID` (fail-safe, never silently reinterpreted). Magnitude conversion is likewise a
|
|
133
135
|
> deliberate non-goal (recognition and canonicalization only).
|
|
134
136
|
|
|
135
137
|
## Navigate the RxNorm drug graph
|
|
136
138
|
|
|
137
139
|
Load a **bring-your-own** RxNorm RRF release and walk the ingredient / brand / clinical-drug /
|
|
138
140
|
dose-form graph. Relationships are read in RxNorm's documented direction (`RELA` is the relationship
|
|
139
|
-
the **second** `RXCUI` has to the **first**); navigation follows only authored edges
|
|
141
|
+
the **second** `RXCUI` has to the **first**); navigation follows only authored edges: the engine
|
|
140
142
|
never synthesizes an inverse, and never fabricates a concept, edge, or NDC.
|
|
141
143
|
|
|
142
144
|
Authored edges only means the graph you get is RxNorm's, including where its shape surprises you.
|
|
@@ -180,7 +182,7 @@ ingredientsOf(graph, "314076"); // an SCD → { found: true, targets: [] }, beca
|
|
|
180
182
|
consistsOf(graph, "314076"); // → the SCDC, the first of the two hops to the ingredient
|
|
181
183
|
genericFor(graph, "104377"); // an SBD → its SCD, via the authored tradename_of edge
|
|
182
184
|
resolveNdc(graph, "00000000001"); // { resolved: true, rxcui: "314076", status: "active", asOf: "RXNORM_2026AA" }
|
|
183
|
-
ingredientsOf(graph, "99999999"); // { found: false, code: "TERM_RXNORM_UNKNOWN_RXCUI" }
|
|
185
|
+
ingredientsOf(graph, "99999999"); // { found: false, code: "TERM_RXNORM_UNKNOWN_RXCUI" }, never a guess
|
|
184
186
|
```
|
|
185
187
|
|
|
186
188
|
The engine ships **no** RxNorm release; you supply it (the public-domain Current Prescribable
|
|
@@ -200,19 +202,19 @@ _names_, listed under [What is bundled](#what-is-bundled).
|
|
|
200
202
|
- **Liberal load, conservative assertion.** Malformed input degrades to a typed diagnostic; a 1:many
|
|
201
203
|
mapping returns the full candidate set, never collapsed to one.
|
|
202
204
|
- **Value-free diagnostic messages.** Nothing you configured and nothing your release or resource
|
|
203
|
-
contained reaches a `message`, `detail`, `reason` or `err.stack`
|
|
204
|
-
canonical URI, not a unit atom
|
|
205
|
+
contained reaches a `message`, `detail`, `reason` or `err.stack` (not a column name, not a
|
|
206
|
+
canonical URI, not a unit atom) at any length. What sits beside a message is a locus: a line
|
|
205
207
|
number, a column count, an index path into your own resource (`compose.include[2]`), or a fixed
|
|
206
208
|
token naming which file of a release it came from. The
|
|
207
209
|
**objects** are a different matter: a typed `unknown`/`unmapped` outcome names the code, unit or
|
|
208
|
-
NDC you asked about, so log the `code` and the locus rather than the whole result
|
|
210
|
+
NDC you asked about, so log the `code` and the locus rather than the whole result: a code _in
|
|
209
211
|
patient context_ can be PHI.
|
|
210
212
|
- **Zero runtime dependencies. Dual ESM + CJS.** Node stdlib only; built with `tsup`, validated with
|
|
211
213
|
`attw`. A release you load is frozen, its indexes included: a code system's concepts, a GEM's or a
|
|
212
214
|
complex map's entries and a drug graph's concepts, edges and NDCs are each a read-only **view** of a
|
|
213
215
|
map rather than the map itself, so nothing holding one can add, delete or clear an entry, whichever
|
|
214
216
|
way it tries. A view is not a `Map` instance, so a model carrying one cannot be `structuredClone`d
|
|
215
|
-
or posted to a worker
|
|
217
|
+
or posted to a worker: copy out what you need (`new Map(cs.concepts)`). The bundled unit table is frozen deeply, atom by atom and definition by
|
|
216
218
|
definition, because it is shared with the engine's own reducer; its two lookup maps refuse `set`,
|
|
217
219
|
`delete` and `clear` by name, and an entry forced past that through `Map.prototype` changes nothing
|
|
218
220
|
the engine reads, because `parseUcum` resolves an atom by scanning the frozen atom array rather than
|
|
@@ -225,14 +227,14 @@ _names_, listed under [What is bundled](#what-is-bundled).
|
|
|
225
227
|
The engine bundles **no code-system release**. What it _does_ bundle includes the following, named
|
|
226
228
|
with its copyright:
|
|
227
229
|
|
|
228
|
-
- **The UCUM unit table
|
|
230
|
+
- **The UCUM unit table**: `ucum-essence.xml`, version 2.2, revision-date 2024-06-17. The Unified
|
|
229
231
|
Code for Units of Measure (UCUM) is copyright ©1999–2024 Regenstrief Institute, Inc., all rights
|
|
230
232
|
reserved, and is reproduced **verbatim** under the UCUM Copyright Notice and License
|
|
231
233
|
(<https://ucum.org/license>). It is embedded byte-for-byte in the published build and parsed at
|
|
232
234
|
runtime; no modified or derivative copy is distributed. The UCUM Specification is provided "as is"
|
|
233
|
-
**without warranty of any kind
|
|
235
|
+
**without warranty of any kind**: see the License for the full disclaimer. The complete notice
|
|
234
236
|
ships with this package at `vendor/ucum/NOTICE.md`.
|
|
235
|
-
- **Code-system identity facts
|
|
237
|
+
- **Code-system identity facts**: the OID ↔ canonical-URI pairings (`SYSTEM_IDENTITIES`). These
|
|
236
238
|
identify the systems; they are not any system's code list.
|
|
237
239
|
- **The SNOMED CT concepts the crosswalk resolver names**, each with its description: the four
|
|
238
240
|
map-category concepts in `MAP_CATEGORIES` (`447637006`, `447638001`, `447639009`, `447640006`) that
|
|
@@ -241,7 +243,7 @@ with its copyright:
|
|
|
241
243
|
ICD-10-CM codes, appear in the API documentation examples, which are distributed in the compiled
|
|
242
244
|
declaration files and in the build's sourcemaps. SNOMED CT is copyright
|
|
243
245
|
© International Health Terminology Standards Development Organisation. **No SNOMED CT release or
|
|
244
|
-
refset is bundled
|
|
246
|
+
refset is bundled**: you supply the map rows under your own SNOMED CT licence.
|
|
245
247
|
- **The RxNorm relationship and term-type names** the drug-graph API is written against: the
|
|
246
248
|
relationship names in `RELA` / `RELA_INVERSE` (`has_ingredient`, `tradename_of`, …), and the
|
|
247
249
|
term-type codes in `TERM_TYPES`, each with its name as published by the U.S. National Library of
|
|
@@ -255,6 +257,6 @@ no claim about whether any particular use of these materials is permitted to you
|
|
|
255
257
|
|
|
256
258
|
## License
|
|
257
259
|
|
|
258
|
-
MIT © Cosyte
|
|
260
|
+
MIT © Cosyte: this package's own code. The bundled third-party materials above stay under their own
|
|
259
261
|
copyright and licences; the `LICENSE` file that ships with this package carries the MIT text and a
|
|
260
262
|
third-party notices section naming each of them.
|