@cosyte/terminology 0.0.11 → 0.0.13

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,5 +1,199 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.13
4
+
5
+ ### Patch Changes
6
+
7
+ - 87d7263: Repository tooling: the PHI scanner now refuses a run that read only some of the targets it enumerated.
8
+
9
+ The observation rule already existed at two scopes, and each was a floor of one at its own scope: a scan root was satisfied by yielding one file, and a whole run was satisfied by reading one target. Nothing spoke for the other targets on the list. So naming an ordinary file beside a bypassed one printed `OK: no hits` and exited 0 while the bypassed file was never opened, byte-identical to a genuine clean run, on both the `paths` route and the `--staged` pre-commit route. The false green was reached by following the printed remedy of the tier above it.
10
+
11
+ A third tier refuses (exit 2) when a target the run enumerated was never read, and names those paths. It compares sets rather than counts, so it says which target went unread rather than how many did. It runs after the whole-invocation tier, whose refusal set is a strict subset of its own.
12
+
13
+ Three supporting fixes. `--allow-fixture` now seeds the target list unconditionally: it used to do so only when no positional path was given, so with one present the flag was validated, audit-checked and then silently ignored. A bypass naming a path the run does not enumerate now refuses as well, which is what makes the pre-commit route obey this: that route builds its target list from its own predicate, so a bypass outside it landed on nothing and passed. And the hit footer no longer offers a whole-file bypass as a remedy, because that remedy now leads to a refusal.
14
+
15
+ A whole-file bypass therefore cannot reach exit 0 in any mode, by those two refusals together and neither alone. The token-level allow-list, which leaves the file read, is the mechanism that remains. No invocation became more permissive: every changed exit code moved toward a refusal.
16
+
17
+ No change to the published package.
18
+
19
+ ## 0.0.12
20
+
21
+ ### Patch Changes
22
+
23
+ - 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.
24
+
25
+ Two false-green doors, both the same shape. The gate reported clean over something it never looked
26
+ at. Both were pre-existing, both were found by the refuters on the previous change to this scanner,
27
+ and every reading below was taken back to back on a clone of this repository at `7e68603`.
28
+
29
+ DOOR 1: THE SCANNER NEVER SCANNED ITSELF
30
+
31
+ `scripts/` was under no scan root. The recogniser's own patterns, the allow-list this scanner refuses
32
+ to run without, and the override log it points a developer at all live there, so the one directory
33
+ guaranteed to hold PHI-shaped text was the one nothing enumerated. Measured: `scripts/planted.ts`
34
+ carrying a dashed SSN and an off-domain address exited **0** `OK: no hits` in all-mode, while naming
35
+ the same file directly reported both at exit **1** over the same bytes.
36
+
37
+ The fix is the declaration and nothing else. Both completeness rules already read `SCAN_ROOTS`: the
38
+ per-root observation rule and the reconciliation against `git ls-files`. Adding `scripts` to that one
39
+ list widens the walk, the per-root floor and the reconciliation together, with no new machinery, and
40
+ each of the three is pinned separately rather than assumed to have followed.
41
+
42
+ The scope was re-derived and this root is not shaped like `test/`. `git ls-files scripts` returns 8
43
+ paths and they are not all TypeScript: two `.ts`, three `.mjs`, two `.sh`, one `.txt`. So it is the
44
+ first root partly outside the source-literal view, and the two halves of the previous widening land
45
+ unevenly: `.ts` and `.mjs` get the raw floor and the escape-decoded view, `.sh` and `.txt` get the
46
+ raw floor only. That is the right answer for a `.txt`, which is its own document, and a residual for
47
+ a `.sh`, which can spell a value through a shell escape. Recorded rather than guarded, and pinned by
48
+ an anti-fabrication case so the boundary is visible instead of inferred.
49
+
50
+ No exemption was needed and none was added. All 8 files were measured against both recognisers, raw
51
+ and decoded, before the root was declared: zero hits, so the widening lands green on its own bytes
52
+ rather than on a new carve-out. The scanner is now under its own scan, so a real-looking value
53
+ written into a comment in it reds the gate. That is the intended pressure.
54
+
55
+ DOOR 2: A BYPASS WITH NO PATH SCANNED ZERO FILES AND PASSED
56
+
57
+ `--allow-fixture <path>` seeds the positional path set, so the flag means "scan this, but allow it"
58
+ rather than a silent no-op. With no positional path the whole target list was that one path, the
59
+ bypass subtracted it, and the run printed `[phi-scan] OK: no hits` at exit **0** over zero files:
60
+ byte-identical stdout and exit code to the genuine clean sweep on the same tree. Measured with the
61
+ path logged in the override file exactly as this scanner's own rejection message instructs, so the
62
+ false green is reached by following the printed remedy.
63
+
64
+ It is fixed with the rule this repository already had, at the scope of the whole invocation, and not
65
+ with a new mechanism. That rule refuses a sweep that observed nothing under a root; the refusal now
66
+ also covers a run that observed nothing at all. A zero-target sweep is a refusal at exit 2, never a
67
+ pass. The per-root tier does not reach the path or staged modes, which enumerate no root, and the
68
+ whole-invocation tier says nothing about any individual root, so neither subsumes the other and both
69
+ are kept.
70
+
71
+ A denominator is still not the answer. Printing "0 files scanned" and exiting 0 is the same defect
72
+ with better telemetry, because a count counts the targets that did exist. The distinction that
73
+ matters is existence against observation, and the term this rule reads is never printed as a count
74
+ and is never compared to the number read.
75
+
76
+ One legitimate zero is named rather than inferred: a staged run whose commit touches nothing in
77
+ scope, which is the pre-commit hook's ordinary markdown-only case. A staged run whose in-scope files
78
+ were all withdrawn by a bypass had work and did none of it, and refuses exactly like a path run does.
79
+
80
+ Deliberately unchanged: the pre-commit route's scope predicate, which admits neither `test/**` nor
81
+ `scripts/**` and whose widening changes what a developer's commit is blocked on. No structured
82
+ detector was added, so a green sweep still means "no SSN or email shapes found", never "no PHI".
83
+
84
+ - 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.
85
+
86
+ `pnpm phi-scan` with no arguments is what CI runs. It walked `src` and nothing else, and the
87
+ pre-commit route's scope predicate admits `src/**.ts` plus `test/fixtures/**`, a directory that has
88
+ never existed in this repository. **So every tracked file under `test/` was enumerated by neither
89
+ route.** Measured on `d97a3de`: `git ls-files test/ | wc -l` is **50**, and all 50 are `.ts`. Back to
90
+ back on that sha, a dashed SSN written to `test/planted.ts` exited **0** `OK: no hits` in all-mode
91
+ while naming the same file directly exited **1** over the same bytes.
92
+
93
+ **The scope was re-derived here rather than ported, and it came out different from every sibling's.**
94
+ There is no `PID|` literal anywhere in this tree, so the residual lists that drove the sibling repos
95
+ describe those repos and not this one. Here the whole corpus is inline TypeScript, and exactly one
96
+ file carries violator shapes.
97
+
98
+ **Enumerating the files buys the SSN/email floor and nothing else, which is why the widening is
99
+ two-sided.** Both recognisers match raw file text, which assumes the file IS the document. That holds
100
+ for a fixture that is its own file and breaks for one that is a string literal, because a literal can
101
+ spell any character as an escape. Every fixture here is a `.ts` literal, so admitting 50 more files
102
+ without the other half would have carried the blind spot into all of them instead of closing it.
103
+ `decodeSourceLiterals` therefore scans the decoded document **in addition to** the raw text, never
104
+ instead, for source containers only. Proved red before and green after in `src/` as well as `test/`,
105
+ so it is not merely a consequence of the new root. Only values the raw pass did not itself report are
106
+ added, deduped against **that pass's own hits** rather than against the raw text: both recognisers
107
+ are word-boundary anchored, so a value can sit in the file as a substring the raw pass correctly
108
+ declines to report, and comparing against the text discarded a real escape-spelled SSN.
109
+
110
+ **An escaped backslash is consumed first.** A doubled backslash in source is one literal backslash,
111
+ so a doubled backslash followed by `u002D` decodes to a backslash and that text, not to a dash; a
112
+ naive global regex replace of the four-hex-digit escape gets that backwards. That ordering rule is
113
+ exact, but it is a statement about the decoder and **not** a guarantee about hits, and it is written
114
+ that way deliberately.
115
+
116
+ **The view is not a literal parser, and both directions of that are recorded rather than glossed.**
117
+ It reports on text that is not an escape at all, in a `String.raw` template and in any comment
118
+ quoting an escape sequence, so the value named can be one the document does not contain. It also
119
+ misses two ordinary spellings that do evaluate to a dashed SSN: string concatenation, and a line
120
+ continuation. Recording a false-positive class is only honest if the printed remedy works, and it did
121
+ not: the allow-list's `ids` set was declared and consulted nowhere, so the only way to clear an SSN
122
+ hit was to edit the source, which is the "gate whose own remedy leaves it refusing" shape this file
123
+ refuses in four other places. The floor's dashed-SSN check now consults it, as a whole-value match
124
+ against a reviewed committed declaration, never a pattern. All of it is pinned by tests.
125
+
126
+ **The sweep is now reconciled against an independent enumeration.** The per-root observation rule
127
+ from the previous change is a floor of ONE file, so declaring `test` a root would otherwise be
128
+ satisfied by any single file under it while the other 49 went unread: the same defect one level down.
129
+ A printed denominator is not the remedy and was refuted as one, because a count is derived from the
130
+ walk and therefore agrees with the walk by construction. Reconciling against `git ls-files` is the
131
+ only version that can disagree. **Three limits previously recorded as open are closed by it, for
132
+ tracked files:** a directory missing from inside a root, a root that is itself a symlink to a
133
+ directory (which the lexical path attribution used to satisfy), and the floor of one. All three
134
+ printed `OK: no hits` at exit 0 before and refuse at exit 2 now, naming the unread paths.
135
+
136
+ **Both rules are kept and neither subsumes the other:** a root with zero tracked files reconciles
137
+ vacuously and still has to starve. **What it deliberately does not claim** is recorded rather than
138
+ glossed: an untracked file under a root is walked, read and scanned, so it cannot hide PHI, but its
139
+ absence is invisible to both rules. The markdown skip and the gitignore filter are shared with the
140
+ walk, so the rule can never demand a file the walk would refuse to read. A tracked gitlink under a
141
+ root is demanded like any other tracked path and the printed remedy does not really fit it, which is
142
+ recorded as an edge rather than guessed at. And `git check-ignore` reports nothing for a path in the index
143
+ (measured), so a gitignore pattern does not excuse a tracked file, which is pinned because the
144
+ opposite is the intuitive guess and would open a hole a one-line `.gitignore` could drive through.
145
+
146
+ **One file is exempt from the shape passes, by path.** This scanner's own suite carries a dashed SSN
147
+ and two non-test-domain addresses on purpose, as the positive half of its tests, and sweeping it
148
+ would red the gate forever. The exemption is a path list and must stay one, because an extension rule
149
+ cannot tell a file that carries violator literals on purpose from one that carries them by accident.
150
+ Allow-listing the values instead is refused: the allow-list's email entry is global, so declaring
151
+ that domain would switch the detector off for the whole corpus while the suite's own positive case
152
+ asserts that address is reported. **The exemption is applied at the scan, not at the enumeration**, so
153
+ the file is still walked, still read, and still counts as observed and reconciled. **And it is scoped
154
+ to the sweep**: naming that file directly still reports its hits, because an unscoped exemption
155
+ deleted a detection the base had, which is "instead of" where this work may only be "in addition to".
156
+ The anti-vacuity control scans its exact bytes under another name and gets a hit.
157
+
158
+ **The retired-root refusal is now inert for its only entry and is kept anyway.** `test/fixtures` sits
159
+ under the new `test` root, so the refusal cannot fire from this repository's configuration. That is
160
+ the silencing its own remedy promises, and the outcome is strictly stronger: a corpus arriving there
161
+ is walked and scanned rather than refused as unaccountable, and a dangling link there is now refused
162
+ by name where the previous guard could not see it at all. The machinery is general, so it is
163
+ exercised against a copy of the scanner with a root retired outside every scan root, rather than left
164
+ as an untested claim.
165
+
166
+ **Two pre-existing minors are folded in.** Exit 1 is reserved for hits, and a directory the walk
167
+ cannot list (a non-directory root, or an unreadable directory at any depth) and an absent or
168
+ unreadable allow-list all used to reach node's default handler and exit **1**, printing a stack trace
169
+ under the code a caller reads as a verdict about PHI. All are exit **2** now, derived from this
170
+ scanner's own stated contract rather than ported, because siblings disagree with each other on this
171
+ exact case. Separately, the two direction-specific `invertGem` messages were pinned with a substring
172
+ match against this repository's own rule; both messages contain both direction tokens, so the pin was
173
+ weak twice over, and they are now pinned by whole-message equality. A third minor carried by siblings
174
+ was measured **not open here**: the staged route already runs `--diff-filter=AMTU`, so an unmerged
175
+ path is already enumerated and refused.
176
+
177
+ **Deliberately unchanged:** the `--staged` scope predicate. A staged file under `test/` outside
178
+ `test/fixtures/` is therefore scanned by CI and not by the pre-commit hook. Nothing regressed, since
179
+ that path was in neither route before, but the two sets now overlap differently and the pre-commit
180
+ one is narrower. Widening it changes what a developer's commit is blocked on, which is a decision
181
+ about the hook rather than the walk. **And no structured detector was added:** a green sweep still
182
+ means "no SSN/email shapes found", never "no PHI".
183
+
184
+ Tests run on throwaway repositories, so no violator is written into the committed corpus, except the
185
+ one exempt file that already carried them. Several are red against the superseded scanner, and the
186
+ anti-fabrication and anti-vacuity controls exist so that no case proves merely that a rule never
187
+ fires.
188
+
189
+ - d97a3de: Rewrote every em dash out of the package's prose and added a CI gate that keeps them out.
190
+
191
+ 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.
192
+
193
+ 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.
194
+
195
+ 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.
196
+
3
197
  ## 0.0.11
4
198
 
5
199
  ### Patch Changes
package/README.md CHANGED
@@ -7,19 +7,19 @@
7
7
 
8
8
  # @cosyte/terminology
9
9
 
10
- > 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,
11
11
  > and it **never fabricates a code**.
12
12
 
13
13
  `@cosyte/terminology` is **not a wire parser**. It mirrors the FHIR **Terminology Module**
14
14
  (`$translate`, `$lookup`, `$validate-code`, `$expand`, …), operating over **consumer-supplied** FHIR
15
15
  resources. It is the sibling engine `@cosyte/transform` and, later, the parsers' code-system
16
- 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,
17
17
  UMLS/RxNorm, and VSAC value sets are strictly bring-your-own. It is not content-free, though: what it
18
18
  _does_ bundle, including the UCUM unit table, the code-system _identities_ (OID ↔ canonical URI), the
19
19
  SNOMED CT concepts the crosswalk resolver names and RxNorm's relationship and term-type names, is
20
20
  listed with its copyright under [What is bundled](#what-is-bundled).
21
21
 
22
- > **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
23
23
  > operation below ships today. The surface:
24
24
  >
25
25
  > - the code-system **identity resolver** (`resolveSystem`) and the ConceptMap **`$translate`** engine
@@ -29,17 +29,17 @@ listed with its copyright under [What is bundled](#what-is-bundled).
29
29
  > - **ValueSet** binding (`compose` / **`$expand`** / **`$validate-code`**), subsumption read from the
30
30
  > release's own hierarchy;
31
31
  > - **UCUM** unit validation and canonicalization (`validateUcum` / `ucumEqual`, recognition only, **no
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
+ > 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`,
34
34
  > public-domain) and the NLM **SNOMED CT → ICD-10-CM complex map** (`loadComplexMap` /
35
- > `applyComplexMap`, BYO — no SNOMED CT refset bundled), never inverted;
35
+ > `applyComplexMap`, BYO: no SNOMED CT refset bundled), never inverted;
36
36
  > - the **RxNorm drug graph** (`loadRxNormGraph` + `ingredientsOf` / `genericFor` / `brandsFor` /
37
37
  > `doseFormsOf` / `consistsOf` / `resolveNdc` / `approximateMatch`) over a **BYO** RxNorm RRF release,
38
38
  > edges read in RxNorm's documented direction and never inverted, an absent `RXCUI`/NDC typed, never
39
39
  > fabricated.
40
40
  >
41
41
  > **Bring your own data.** The engine is whole; **no code-system release is bundled.** It runs over
42
- > 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
43
43
  > GEM files, the NLM complex-map refset and VSAC value sets are all bring-your-own, under whatever
44
44
  > terms their steward sets. The content packs that would change that (RxNorm Prescribable, ICD-10-CM)
45
45
  > are not bundled either. For the things that **are** bundled, and under whose copyright, see
@@ -72,7 +72,7 @@ const map = loadConceptMap(myFhirConceptMapJson); // a standard FHIR R4 ConceptM
72
72
  const result = translate({ system: "http://loinc.org", code: "2160-0" }, map);
73
73
 
74
74
  if (result.unmapped) {
75
- // 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.
76
76
  } else {
77
77
  for (const m of result.matches) {
78
78
  m.target; // a frozen Coding, drawn verbatim from the map
@@ -89,7 +89,7 @@ if (result.unmapped) {
89
89
  import { loadCodeSystem, lookup, validateCode } from "@cosyte/terminology";
90
90
 
91
91
  // Load a consumer-supplied release: RRF (RxNorm/UMLS), CSV (LOINC), fixed-width
92
- // (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.
93
93
  const cs = loadCodeSystem({ format: "fhir", resource: myFhirCodeSystemJson });
94
94
 
95
95
  const hit = lookup(cs, "2160-0"); // $lookup
@@ -97,48 +97,48 @@ if (hit.found) {
97
97
  hit.display; // the preferred display, verbatim from the release
98
98
  hit.status; // deprecated / header-not-billable / obsolete / … carried, never presented clean
99
99
  } else {
100
- hit.code; // "TERM_CODE_UNKNOWN" — never a fabricated display
100
+ hit.code; // "TERM_CODE_UNKNOWN": never a fabricated display
101
101
  }
102
102
 
103
- 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
104
104
  ```
105
105
 
106
106
  ## Validate and compare UCUM units
107
107
 
108
- 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
109
109
  analyte's molar mass, a clinical computation this engine refuses).
110
110
 
111
111
  ```ts
112
112
  import { validateUcum, ucumEqual } from "@cosyte/terminology";
113
113
 
114
114
  const v = validateUcum("mmol/L");
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
+ 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
117
117
 
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
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
122
122
  ```
123
123
 
124
124
  The UCUM table is the official `ucum-essence.xml` (version 2.2), and it **is bundled with this
125
- 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
126
126
  ©1999–2024 Regenstrief Institute, Inc., reproduced under the UCUM Copyright Notice and License
127
127
  (<https://ucum.org/license>); the engine ships no derivative of it and passes the official UCUM
128
- 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
129
129
  [What is bundled](#what-is-bundled).
130
130
 
131
- > **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)
132
132
  > is case-sensitive (`m` = metre, `M` = mega, `Pa` = pascal, `pA` = picoampere). The
133
133
  > case-**insensitive** (c/i) spelling variant is **not** supported; a c/i-only string returns a typed
134
- > `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
135
135
  > deliberate non-goal (recognition and canonicalization only).
136
136
 
137
137
  ## Navigate the RxNorm drug graph
138
138
 
139
139
  Load a **bring-your-own** RxNorm RRF release and walk the ingredient / brand / clinical-drug /
140
140
  dose-form graph. Relationships are read in RxNorm's documented direction (`RELA` is the relationship
141
- 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
142
142
  never synthesizes an inverse, and never fabricates a concept, edge, or NDC.
143
143
 
144
144
  Authored edges only means the graph you get is RxNorm's, including where its shape surprises you.
@@ -182,7 +182,7 @@ ingredientsOf(graph, "314076"); // an SCD → { found: true, targets: [] }, beca
182
182
  consistsOf(graph, "314076"); // → the SCDC, the first of the two hops to the ingredient
183
183
  genericFor(graph, "104377"); // an SBD → its SCD, via the authored tradename_of edge
184
184
  resolveNdc(graph, "00000000001"); // { resolved: true, rxcui: "314076", status: "active", asOf: "RXNORM_2026AA" }
185
- 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
186
186
  ```
187
187
 
188
188
  The engine ships **no** RxNorm release; you supply it (the public-domain Current Prescribable
@@ -202,19 +202,19 @@ _names_, listed under [What is bundled](#what-is-bundled).
202
202
  - **Liberal load, conservative assertion.** Malformed input degrades to a typed diagnostic; a 1:many
203
203
  mapping returns the full candidate set, never collapsed to one.
204
204
  - **Value-free diagnostic messages.** Nothing you configured and nothing your release or resource
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
+ 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
207
207
  number, a column count, an index path into your own resource (`compose.include[2]`), or a fixed
208
208
  token naming which file of a release it came from. The
209
209
  **objects** are a different matter: a typed `unknown`/`unmapped` outcome names the code, unit or
210
- 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
211
211
  patient context_ can be PHI.
212
212
  - **Zero runtime dependencies. Dual ESM + CJS.** Node stdlib only; built with `tsup`, validated with
213
213
  `attw`. A release you load is frozen, its indexes included: a code system's concepts, a GEM's or a
214
214
  complex map's entries and a drug graph's concepts, edges and NDCs are each a read-only **view** of a
215
215
  map rather than the map itself, so nothing holding one can add, delete or clear an entry, whichever
216
216
  way it tries. A view is not a `Map` instance, so a model carrying one cannot be `structuredClone`d
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
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
218
218
  definition, because it is shared with the engine's own reducer; its two lookup maps refuse `set`,
219
219
  `delete` and `clear` by name, and an entry forced past that through `Map.prototype` changes nothing
220
220
  the engine reads, because `parseUcum` resolves an atom by scanning the frozen atom array rather than
@@ -227,14 +227,14 @@ _names_, listed under [What is bundled](#what-is-bundled).
227
227
  The engine bundles **no code-system release**. What it _does_ bundle includes the following, named
228
228
  with its copyright:
229
229
 
230
- - **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
231
231
  Code for Units of Measure (UCUM) is copyright ©1999–2024 Regenstrief Institute, Inc., all rights
232
232
  reserved, and is reproduced **verbatim** under the UCUM Copyright Notice and License
233
233
  (<https://ucum.org/license>). It is embedded byte-for-byte in the published build and parsed at
234
234
  runtime; no modified or derivative copy is distributed. The UCUM Specification is provided "as is"
235
- **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
236
236
  ships with this package at `vendor/ucum/NOTICE.md`.
237
- - **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
238
238
  identify the systems; they are not any system's code list.
239
239
  - **The SNOMED CT concepts the crosswalk resolver names**, each with its description: the four
240
240
  map-category concepts in `MAP_CATEGORIES` (`447637006`, `447638001`, `447639009`, `447640006`) that
@@ -243,7 +243,7 @@ with its copyright:
243
243
  ICD-10-CM codes, appear in the API documentation examples, which are distributed in the compiled
244
244
  declaration files and in the build's sourcemaps. SNOMED CT is copyright
245
245
  © International Health Terminology Standards Development Organisation. **No SNOMED CT release or
246
- 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.
247
247
  - **The RxNorm relationship and term-type names** the drug-graph API is written against: the
248
248
  relationship names in `RELA` / `RELA_INVERSE` (`has_ingredient`, `tradename_of`, …), and the
249
249
  term-type codes in `TERM_TYPES`, each with its name as published by the U.S. National Library of
@@ -257,6 +257,6 @@ no claim about whether any particular use of these materials is permitted to you
257
257
 
258
258
  ## License
259
259
 
260
- 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
261
261
  copyright and licences; the `LICENSE` file that ships with this package carries the MIT text and a
262
262
  third-party notices section naming each of them.