@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 CHANGED
@@ -1,13 +1,267 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to this project will be documented in this file.
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
- 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).
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
- ## [Unreleased]
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, pending a per-version split
252
-
253
- > **Everything between this heading and `[0.0.1]` has already SHIPPED.** It accumulated under
254
- > `[Unreleased]` across the releases from `0.0.2` through `0.0.9`, because this file is maintained by
255
- > hand and the Changesets changelog generator is off (`"changelog": false` in
256
- > `.changeset/config.json`), so no release run ever moved an entry out of that section. Reading any
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
- <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/terminology
7
9
 
8
- > A **zero-dependency terminology engine** for US healthcare code systems — FHIR-shaped, BYO-data,
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** — SNOMED CT, CPT, LOINC,
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** — every
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**) — the official UCUM functional-test suite is the conformance gate;
31
- > - the **crosswalk resolvers** — the CMS **ICD-9↔ICD-10 GEMs** (`loadGems` / `applyGem`,
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 — no SNOMED CT refset bundled), never inverted;
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 — SNOMED CT, CPT, LOINC, UMLS/RxNorm, the CMS
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 — carries any group.unmapped fallback mode. Never a guessed target.
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 — see ICD10CM_ORDER_FILE_FIELDS), or a FHIR CodeSystem JSON.
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" — never a fabricated display
100
+ hit.code; // "TERM_CODE_UNKNOWN": never a fabricated display
99
101
  }
100
102
 
101
- validateCode(cs, "2160-0").valid; // $validate-code — true iff present; never a guessed true
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 — **no magnitude conversion** (`5 mg/dL` → `mmol/L` needs the
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 — v.canonical is a stable canonical descriptor
114
- validateUcum("mg/dl/").valid; // false — code "TERM_UCUM_INVALID", never a guessed "nearest" unit
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 — same unit, different spelling
117
- ucumEqual("mmol/L", "mmol.L-1"); // true — annotations inert, operators normalized
118
- ucumEqual("mg", "g"); // false — different units (this is not conversion)
119
- ucumEqual("Cel", "K"); // false — a special (non-linear) unit is never equated with a linear one
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** — embedded verbatim in the published build and parsed at runtime. It is copyright
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` — see
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 — the one FHIR/HL7 bind to —
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 — never silently reinterpreted). Magnitude conversion is likewise a
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 — the engine
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" } — never a guess
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` — not a column name, not a
204
- canonical URI, not a unit atom — at any length. What sits beside a message is a locus: a line
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 — a code _in
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 — copy out what you need (`new Map(cs.concepts)`). The bundled unit table is frozen deeply, atom by atom and definition by
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** — `ucum-essence.xml`, version 2.2, revision-date 2024-06-17. The Unified
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** — see the License for the full disclaimer. The complete notice
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** — the OID ↔ canonical-URI pairings (`SYSTEM_IDENTITIES`). These
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** — you supply the map rows under your own SNOMED CT licence.
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 — this package's own code. The bundled third-party materials above stay under their own
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.