@cosyte/terminology 0.0.12 → 0.1.0

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,173 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - eead751: `@cosyte/terminology` reaches 0.1.0: the code-system resolver, `$translate`, `$lookup`, `$validate-code`, `$expand`, UCUM validation, the crosswalk resolvers and the RxNorm drug graph are settled enough to depend on.
8
+
9
+ What you can depend on from this release: `resolveSystem`; `loadConceptMap` and `translate`; `loadCodeSystem` with `lookup` and `validateCode` over RRF, CSV, fixed-width and FHIR JSON releases; `loadValueSet` with `expand` and `validateCodeInValueSet`; `validateUcum` and `ucumEqual`; `loadGems`, `applyGem`, `loadComplexMap` and `applyComplexMap`; `loadRxNormGraph` with its navigation functions; the shapes of the results these return; and the typed diagnostic codes you branch on. The engine never fabricates a code and never inverts a directional map, and an answer it could not compute comes back typed rather than guessed.
10
+
11
+ What the version number promises: while it is below 1.0.0, a breaking change to any of the above, renaming a diagnostic code included, ships in a new minor version, never a patch, and its entry in this changelog says what broke and what to do instead. A fix that changes nothing else a consumer relies on ships as a patch.
12
+
13
+ Not covered yet: any bundled code-system release (SNOMED CT, CPT, LOINC, UMLS and RxNorm releases and VSAC value sets stay bring-your-own), content packs such as RxNorm Prescribable Content or ICD-10-CM, UCUM magnitude conversion and case-insensitive UCUM spellings, and the terminology ecosystem test cases outside the suites and operations the README names.
14
+
15
+ ### Patch Changes
16
+
17
+ - 30da371: ValueSet binding: a pre-computed expansion marked `valueset-unclosed` is now read as incomplete, so a code it does not list is `undetermined` rather than a confident non-member.
18
+
19
+ FHIR R4 lets a server set `http://hl7.org/fhir/StructureDefinition/valueset-unclosed` on `ValueSet.expansion` when the value set is unbounded because it includes post-coordinated content (SNOMED CT, UCUM): the snapshot is a sample of its membership, not the whole of it. The loader did not read that extension, so such an expansion looked complete whenever its `total` matched its `contains` length, and `validateCodeInValueSet` answered a decided `result: false` for any code the server had not enumerated. That is a confident wrong answer about whether a code is allowed in a clinical slot, and it is exactly what the never-fabricate invariant exists to prevent.
20
+
21
+ `loadValueSet` now reads the extension into the existing `ValueSet.expansion.truncated` flag, alongside the `total`-exceeds-`contains` and `valueset-toocostly` derivations it already folds together there, and also surfaces it on its own new boolean `ValueSetExpansion.unclosed`. `validateCodeInValueSet` returns a typed `undetermined` with `code: "TERM_VALUESET_CANNOT_EXPAND"` for a code absent from such an expansion, and `expand` reports `complete: false` with a `TERM_VALUESET_EXPANSION_TRUNCATED` diagnostic located at `expansion`. The diagnostic's `detail` names `unclosed`, so a caller can tell an unbounded value set from a truncated page, and an expansion carrying both markers reports the unclosed one in a single diagnostic entry rather than only the too-costly one.
22
+
23
+ A code that IS listed in an unclosed expansion still validates `true`: enumerated membership is proven, and the marker bounds what absence means, never what presence means. The mark is read fail-safe, in the one direction that can only lower confidence: an entry naming the extension URL but carrying no readable `valueBoolean` is treated as set rather than ignored, and only an explicit `valueBoolean: false` reads as absent, because that is the sender saying the expansion is closed. An extension entry that never names the URL marks nothing.
24
+
25
+ The `valueset-toocostly` and `total`-exceeds-`contains` derivations are unchanged, including too-costly's stricter requirement of an explicit `valueBoolean: true` and its own diagnostic wording. The loader's conservative refusals are unchanged: a non-object `expansion`, or a `contains` entry that is not an object or lacks its `code`, is still a `TERM_VALUESET_MALFORMED` fatal with a value-free message. No exported member was removed or renamed; `ValueSetExpansion.unclosed` is added.
26
+
27
+ The engine only ever reads this marker off a supplied expansion. It never sets it on an expansion it computes: `expand` runs over consumer-supplied code systems and cannot assert post-coordination.
28
+
29
+ - 878bc96: Repository tooling: the PHI sweep now reads the bytes git carries, as a union with its working-tree walk.
30
+
31
+ The walk answers what is on disk under the scan roots. What a commit carries is the index, and where the two disagree the walk was the only voice. Three ordinary states were measured on a clone at the base of this change, with a detectable payload tracked at the path: the working tree short of a tracked file, the path occupied by a directory, and the two copies simply differing, which is what staging content and then scrubbing the file leaves behind.
32
+
33
+ Only the third printed the clean line and exited 0. The other two already refused, because this repository, unlike the carrier this machinery is shared with, already reconciled the sweep against `git ls-files` and could tell that a tracked file had gone unread. So the union closes one false green and upgrades the other two from "this sweep cannot account for a tracked file" to a report of what is actually in it. Both are worth having and they are not the same thing; the sizing is written down because a first draft of it called all three false greens, which is the sibling's number and not this one's.
34
+
35
+ The whole index is read with `git ls-files -s -z` and every in-scope tracked path whose stage-0 blob the walk did not already read verbatim is scanned through `git cat-file blob`. It is a union and never a replacement: the walk still runs first and still reads untracked files, which the index cannot name. Reading the object rather than re-reading the path is what makes the directory case readable at all, since the path does not resolve to a file there. Deduplication is by content under git's own blob framing, so a clean checkout adds zero reads and invokes `cat-file` not at all, measured here with a logging git on the path; where the two copies differ both are scanned, which is what makes it correct under end-of-line normalization rather than merely untested by it. A hit found only in the index is labelled as such, because a hit naming the bare path sends a reader to open a file that is clean or not there.
36
+
37
+ Five things a port must re-derive were re-derived here rather than inherited, and they are written down: the exit codes, the roots and their exclusions, the pre-commit route's own narrower scope, the modes the index records by reference, and end-of-line normalization. The union is keyed on the absence of stage 0, which cannot be ported from the pre-commit route: an unmerged path is reported at stages 1, 2 and 3 with ordinary blob modes, and a draft elsewhere that took the first record scanned the merge base and reported clean over a marker living only in the third stage. A fixture puts the payload in stage 3 alone, so any other choice reads clean and fails the case.
38
+
39
+ The sweep refuses when git cannot name the index or names it empty, because a sweep with no index is the walk's word alone. Those are two different routes to the same refusal and both are needed: outside a repository the command exits 128 rather than answering empty, so the surrounding catch is what keeps the run off the exit code this scanner reserves for findings.
40
+
41
+ It does not vouch for a scan root. Root starvation is still decided by the walk alone, so a root that is absent, dangling or empty still refuses even when every tracked blob under it was read: the two rules make different claims and neither substitutes for the other. The completeness rules that were already here are unchanged, and the tracked paths the union may read are folded into what the run enumerates before the first byte is read, so even a union producing no targets cannot let the sweep report clean over one.
42
+
43
+ Two residuals are unchanged and are recorded rather than implied: a tracked markdown file under a scan root is read by neither route, and the one source whose job is to carry violator literals is still exempted at the scan. Reading more bytes is not reading for more shapes, so a green sweep still means no SSN or email shapes were found, never that there is no PHI.
44
+
45
+ No change to the published package.
46
+
47
+ - 78aaeb2: ValueSet binding: an enumerated `compose` code the release you supplied does not define is no longer expanded into a silent, complete member.
48
+
49
+ A `compose.include` / `.exclude` component that lists codes explicitly was treated as always fully computable, so `expand` returned every enumerated code as a member and called the answer complete, even when the caller had supplied the very `CodeSystem` release that shows one of those codes does not exist. `validateCodeInValueSet` agreed with it and decided such a code a member. That is a decided, complete, diagnostic-free membership set carrying a code nothing defines: the engine asserting something it holds contrary evidence for, which is the failure the never-fabricate posture exists to prevent, and a wrong binding silently mistranslates clinical meaning downstream.
50
+
51
+ An enumerated entry is now admitted only where the evidence the caller supplied does not contradict it. Where the `ExpansionContext` holds a `CodeSystem` for the component's `system`, and the component either declares no `version` or the supplied release declares the same one, a code that release does not define is not a member: `expand` omits it from `contains` and `validateCodeInValueSet` returns a decided `result: false` for it. That is the same release-usability test the intensional branches already apply.
52
+
53
+ The answer stays **decided**. `expand` reports `complete: true` (the engine decided the component, on the evidence it was given, so `contains` is the answer and not a lower bound) and surfaces the drop as one new typed diagnostic per affected component, `TERM_VALUESET_ENUMERATED_CODE_UNDEFINED`, located by that component's own index path (`compose.include[0]`, `compose.exclude[1]`). A complete expansion can therefore carry diagnostics; read `diagnostics` whatever `complete` says. `validateCodeInValueSet` reports the same fact as a decided non-member rather than a diagnostic, because a decided membership carries none, and never as the typed `undetermined` refusal: the engine knows this code is not a member.
54
+
55
+ The rule is bounded to the case where contrary evidence is actually in hand, and the bound is the point: no release supplied for the component's `system`, a release for a different system, a declared `version` the supplied release disagrees with, a release that declares no version to agree with, or a component naming no `system` at all all leave the enumeration exactly as it was, with no diagnostic. Absence of evidence is not evidence a code is undefined, and widening to that case would turn most legitimate enumerated value sets incomplete. An `exclude` entry the release does not define removes nothing from the include union, and is reported on the `exclude` component's own path.
56
+
57
+ Everything else is unchanged: pre-computed `ValueSet.expansion` handling, the `filter`, whole-system, referenced-value-set and active-only branches, and the display fallback (a member the release carries with no display of its own still comes back with none, and is still a member). One diagnostic code was added and none was renamed or removed.
58
+
59
+ - 45ef8d3: ValueSet binding: four membership and display answers now agree with the HL7 terminology-ecosystem responses they used to contradict. Every one of them was answered with confidence before (a decided `result`, or an expansion marked `complete: true`), so a consumer had nothing to branch on.
60
+
61
+ `ValueSet.compose.inactive` is read and honoured. It was dropped at load, so a value set declaring `inactive: false` expanded to include a code the supplied release marks retired. `ValueSetCompose.inactive` now carries the declaration, and a declared `false` screens every `include` branch alike: enumerated, filtered and whole-system. A code the release marks not active is omitted; a concept the release carries no status for is KEPT, because absence of status is not evidence of inactivity; and a component whose activity no supplied release can decide contributes nothing, marks the result `complete: false` and surfaces a value-free `TERM_VALUESET_CANNOT_EXPAND` diagnostic at the component's index path, so `contains` stays a true lower bound rather than a set that may hold inactive codes. `validateCodeInValueSet` mirrors the same rule, so membership and expansion never disagree: a not-active code is a decided non-member, and an activity that cannot be checked is the typed `undetermined`, never a decided answer. An `exclude` is not screened, since screening it would leave an excluded code a member. An absent or `true` `inactive` omits no code on activity grounds, and a pre-computed `expansion` still passes through untouched. `loadValueSet` throws the typed `TERM_VALUESET_MALFORMED` for an `inactive` that is present and not a boolean, naming the value-free path, rather than dropping it silently. The value set's own declaration is what is read: `$expand`'s `activeOnly` request parameter is not modelled.
62
+
63
+ An enumerated member now takes its display from the supplied release when the value set supplies none. The value set's own `display` still wins and is still carried verbatim, even where the release names the code differently. Nothing is invented: with no release supplied for the component's system, no such code in it, or no display on that code, the member comes back with no display at all, and the expansion's completeness and diagnostics are unchanged either way, because a missing display is not a missing member. No display is taken from a release whose version disagrees with the component's declared pin. This closes an inconsistency between three branches of one operation: the filter and whole-system branches already built every member with the release's display.
64
+
65
+ A `property` filter now reads every value a concept carries for the property. `CodeSystem.concept.property` is `0..*`, and only the first match was read, so a concept carrying `dup = alpha` and `dup = beta` was not selected by `dup = beta` and a valid member read as a non-member with `complete: true`. `=` and `in` now match on ANY of the concept's values, `not-in` only where NONE of them is in the filter's set, and `exists` follows whether the concept carries any at all. A concept carrying no value for the property is unchanged (`=` and `in` miss, `not-in` matches), and a non-string property value still compares by its stringified form.
66
+
67
+ A `Coding` that carries no `system` is no longer decided a member of a system-scoped component. A component naming a `system` selects only codes drawn from it, so an unqualified coding names none of them: the component is a definite non-match, and a value set whose components all name a system decides `result: false` rather than asserting the code into it on the strength of its code string alone. This removes a fabricated `true`; it turns no refusal into an answer. A component the engine could not evaluate (no release supplied, an unsupported operator, a declared version the supplied release disagrees with) still returns the typed `undetermined` exactly as before, and a coding whose `system` matches the component's is answered exactly as before.
68
+
69
+ `ValueSetCompose.inactive` is added. No exported member was removed or renamed.
70
+
71
+ - 64d0143: Repository tooling: the build-time dependency pins and the install-hardening settings now match the shared engineering baseline the `@cosyte/*` packages are held to.
72
+
73
+ The `js-yaml` resolution override is re-pinned to the first patched version of each advisory it cites, on both branches that resolve here: `>=4.0.0 <4.3.1` to `4.3.1`, reached at build time, and `>=3.0.0 <3.15.1` to `3.15.1`, reached by the release tooling, which calls an API the 4.x line removed. The superseded single override is replaced rather than left beside them. The `esbuild` override is unchanged.
74
+
75
+ `typescript`, `eslint`, `tsup` and `@vitest/coverage-v8` are now declared devDependencies, exact-pinned at the versions this repository already resolved through the shared config packages, so an upgrade is a visible edit here rather than a silent change in a transitive. Nothing about how those tools are invoked changes: the five verb scripts still delegate to `@cosyte/process`.
76
+
77
+ A `pnpm-workspace.yaml` declares a 24 hour `minimumReleaseAge` floor and a `no-downgrade` `trustPolicy`. The pinned `pnpm@10.0.0` predates both settings and ignores them, so the file records the intended floor rather than enforcing one today; the file says so in place.
78
+
79
+ No change to the published package. The entry points, the exports map and the built bytes are identical to the ones this version already ships.
80
+
81
+ - e8836a7: A value set that pins a code system version now checks that pin against the release you supplied, instead of expanding against whichever release the context happened to hold.
82
+
83
+ `ValueSet.compose.include[].version` and `.exclude[].version` were carried through to the output codings and never compared with anything. So a value set that names one release, given another, returned membership computed from the release you passed in, stamped with the version the value set asked for: a wrong answer wearing the right label, and undetectable from the result.
84
+
85
+ Where an intensional part actually resolves a supplied `CodeSystem` (a `filter` selection, or a whole-system component with no `concept` list), the component's declared `version` is now compared against that release's own `version`, in `expand` and in `validateCodeInValueSet` alike, for `include` and `exclude` components alike:
86
+ - **They disagree**: the component contributes nothing, `expand` returns `complete: false` with a typed `TERM_VALUESET_CANNOT_EXPAND` located on that component's own `compose.include[n]` / `compose.exclude[n]` path, and `validateCodeInValueSet` returns `undetermined` carrying the same diagnostic. Exactly how an unresolvable code system already behaves: `contains` stays a lower bound and no member is fabricated from the other release.
87
+ - **The release declares no version of its own**: the pin can be neither confirmed nor refuted, so the same diagnostic says so, the outcome is incomplete or undetermined in the same way, and the members are still expanded but **without** the version stamp. An unconfirmed pin is never written onto a coding as though it had been checked.
88
+ - **The component declares no version, or the two agree**: unchanged, byte for byte, including the version stamp on the output codings.
89
+
90
+ Unaffected on purpose: an extensional `concept` list consults no release and keeps stamping its declared version; a component whose code system you did not supply at all keeps its existing "not supplied" diagnostic, which is a different situation from a release that carries no version.
91
+
92
+ Two limits, stated rather than implied. The check is that the two **declarations agree**, never that either is true, so a mislabelled release is still trusted. And the `ExpansionContext` holds one release per system URI, so the engine cannot hold several releases of a system and select the one a component asks for.
93
+
94
+ The diagnostic text is value-free like every other one here: it names the disagreement structurally and never interpolates either version string.
95
+
96
+ - 7bb96fd: ValueSet conformance is now a number a reader can check: `README.md` states how many of HL7's own terminology ecosystem test cases this engine answers identically, rather than resting on tests written here.
97
+
98
+ A pinned snapshot of HL7's FHIR terminology ecosystem test cases sits in the repository and is driven in process through the public API: each suite's setup resources are loaded through the load API, and each selected case is answered with `expand` or `validateCodeInValueSet` and compared to the response the suite records. The statement carries the claim in full: the suite it is made against, the snapshot version the counts were produced against (the upstream commit, since the suite publishes no version field), the suites and operations in scope, and the ran, passed and declined counts. It says in as many words that it is a measurement and not an approval, a certification or an endorsement by HL7 or anyone else.
99
+
100
+ The three outcomes are kept apart on purpose, because a number that blurs them means nothing. A case this library has no answer for is declined and named, never counted as a pass: it asks for a request parameter the public API takes no argument for, for HTTP or language negotiation a library call cannot perform, or it is met by the engine's own typed refusal. A case that answers differently from the recorded response is a failure and fails the run, and no code path can record one as a decline instead. A case held out of the measurement is named with its reason beside the counts it is absent from, so the selected set cannot shrink quietly.
101
+
102
+ The figures cannot go stale in silence. The committed report is re-derived from a fresh run and compared byte for byte, and the statement in `README.md` is held to that same run, so a count, a snapshot version or a scope that stops being true reds the test suite rather than standing as the answer.
103
+
104
+ No engine behaviour changed: this measures the engine and does not touch it. The vendored third-party cases are test material and are not part of the published package.
105
+
106
+ - 2ad8299: The published UCUM conformance claim now names the kinds of official case this package runs and the suite date it is made against, and the package's own tests fail rather than let it go stale.
107
+
108
+ `README.md` said, with no qualification and no date, that the official UCUM functional-test suite was the conformance gate. The vendored `UcumFunctionalTests.xml` carries four kinds of case and this package executes two of them: every `validation` case, and the `conversion` cases for their commensurability only (source and destination must reduce to the same dimension; the converted magnitude is not asserted, because magnitude conversion is a deliberate non-goal). The `displayNameGeneration` and `multiplication` cases are not executed. A reader could not tell which of those had been run, and UCUM's own conformance statement both permits qualifying a claim to particular kinds of case and asks that the most recent date from the suite's history entries be quoted.
109
+
110
+ The README now carries a `UCUM conformance` section stating the claim in full: the case kinds claimed (`validation`, `conversion`), the kinds explicitly not claimed (`displayNameGeneration`, `multiplication`), the suite date the claim is made against (3-Feb 2021, the most recent history entry in the vendored suite), and the unit-table release it is made against (`ucum-essence.xml` version 2.2, revision-date 2024-06-17). `vendor/ucum/NOTICE.md`, which ships in the tarball and already declared that date, now states the same kinds beside it.
111
+
112
+ The claim is not prose alone. A new test holds it to the vendored files on every run, and fails if the table's declared `version` or `revision-date` stops matching the release the claim names, if the suite's most recent history date stops matching the date the claim states, if the claim names a case kind no test executes or omits one that is executed, if a named kind's section carries no case, or if the README and the notice state different suite dates. The date is derived from the vendored suite at test time rather than pasted in as a constant, so re-vendoring the suite moves the date the claim must state instead of silently falsifying the published one. An absent, empty or structurally-wrong vendored artifact now fails by naming that artifact, rather than reporting a pass over content that was never read.
113
+
114
+ No behaviour changes and no exported member was added, removed or renamed. Every UCUM case that ran before still runs, and the claim can only narrow from here: naming a kind the tests do not execute is a failing test.
115
+
116
+ - e24a1b7: A ConceptMap's explicit "these concepts are not related" assertion no longer reads as a successful translation. This is a behavior change to the summary verdict `translate` reports.
117
+
118
+ FHIR R4 spells that assertion `equivalence: "disjoint"`, and this engine already normalized it to the R5 relationship `not-related-to` on the target itself. The summary verdict did not agree: a `disjoint` row came back as a match on a result carrying `unmapped: false`, so the per-target vocabulary said "not related" while the one flag a caller branches on said "translated". R4 `$translate` is explicit that its result "can only be true if at least one returned match has an equivalence which is not unmatched or disjoint".
119
+
120
+ What changed, precisely:
121
+ - **A source whose every declared target asserts non-relation (`disjoint` or `unmatched`) now reports as not translated**: `unmapped: true`, `code: "TERM_TRANSLATE_UNMAPPED"`.
122
+ - **Nothing the map author said is dropped.** The `disjoint` rows are carried on that result's new `notRelated` array, in declared order, each with the same target coding the row produced before (target code, the group's target system, the declared display and the group's target version), its verbatim R4 `equivalence`, and the author's `comment`. A new exported type, `TranslateNotRelated`, describes one. Never fabricated, and never a target to fall back to: it is the map's refusal, made readable.
123
+ - **Nothing else moves.** A source with at least one target asserting any other relationship is translated exactly as before, and a `disjoint` row still rides along in `matches` in its declared position. The `group.unmapped` fallback is untouched: a source that is present and asserted non-related reports mode `none` and does not open the author's fallback, which is what a source whose only target is `unmatched` already did; a source absent from the map still reports the declared fallback mode, `fixed` target or other-map URL included, reported and never applied.
124
+ - **The per-target normalization is unchanged**: `disjoint` and `unmatched` still fold to `not-related-to`. The defect was the summary flag, not the vocabulary.
125
+ - **`unmatched` rows are still not surfaced.** A pure `unmatched` assertion carries no target code, so there is nothing to carry; only `disjoint` rows appear on `notRelated`. A `disjoint` row that declares no target code at all reports the source as not translated and emits no target coding for it.
126
+
127
+ The change is one-directional and that direction is the safe one: a source that read as translated may now read as not translated, and no source gains a translation it did not have. Both halves are asserted as property-based invariants over generated maps, alongside the existing never-fabricate and never-invert ones.
128
+
129
+ If you branch on `result.unmapped`, a source whose map only ever said "not related" now takes the unmapped branch. The rows it asserted are on `result.notRelated`, so the reason is still readable from the result.
130
+
131
+ - 16ca313: Repository tooling: the build, test, lint, typecheck and format scripts now delegate to the shared `@cosyte/process` package instead of naming their tools here.
132
+
133
+ Each of the five script bodies is exactly `cosyte-process <verb>`, and the four variant scripts (`test:watch`, `test:coverage`, `lint:fix`, `format:check`) delegate through that package's modifiers. The tools those verbs run (tsup, vitest with its v8 coverage provider, eslint and typescript) are dependencies of `@cosyte/process` and resolve from it, so a shared toolchain upgrade reaches this repository as one version bump plus an install rather than as the same five edits made by hand in every repository.
134
+
135
+ The invocations are unchanged from what this repository ran before, token for token, so no override file is carried and `cosyte-process.config.json` is absent here. `prettier` stays a direct dependency, because the release `version` script invokes it directly, outside those five verbs. `vitest` stays one too, on the same major line as the one `@cosyte/process` runs, because this repository's own test files import its API; its v8 coverage provider is not declared here and resolves from `@cosyte/process` alone.
136
+
137
+ `cosyte-process check` grades that wiring, and it runs from the test suite rather than from a workflow of its own, so it sits inside the required CI job: a hand-edited script body or an invalid override file fails the build instead of drifting quietly.
138
+
139
+ No change to the published package. The entry points, the exports map and the built bytes are identical to the ones this version already ships.
140
+
141
+ - c5f524b: Repository tooling: the two-file contract between `CLAUDE.md` and the long-form agent notes is now checked, in the test suite, so it blocks a merge.
142
+
143
+ The 2026-08-04 split left the always-read guide holding a one-line imperative per trap and the on-demand record holding the reasoning behind it, with a pointer from each imperative to its section. Both files stated that promise in their own words and nothing checked it. A section reworded in the record silently strands every pointer at it, and neither file gets a compile error, so the worker who follows one gets the imperative and none of the reasoning: an inverted crosswalk, a concept typed from a synonym atom and a unit memo keyed on a string are all lessons that live only on that side.
144
+
145
+ The check enumerates its corpus with `git ls-files` and reconciles it as sets, so every tracked file is opened or the run refuses. There is no exclusion list, no binary skip and no NUL skip. It refuses (exit 2) rather than reporting green when it cannot make a claim: a corpus it could not fully open, a missing half of the contract, a heading whose anchor it would have to guess, and zero pointers found, which is indistinguishable from a clean run by any count.
146
+
147
+ The pointer forms were counted against this repository before the matcher was written, rather than inherited from a sibling. Every pointer here is written as the record's basename and an anchor, and the path-qualified spelling a sibling gate matches has no occurrences at all: ported verbatim, that matcher would have reported success while covering none of them. Both forms are scanned in every tracked file, and finding none in the live form is itself a refusal, so a matcher that stops matching cannot read as a clean tree.
148
+
149
+ Its limits are stated rather than implied. It does not assert that any other repository carries such a record, because several carry none. It does not assert that every trap has a pointer, which is a judgement about prose. It does not assert that a section is accurate or that the trap it describes is closed. A pointer in a Windows-1252 file is matched and one in UTF-16 is not, which is a disclosed miss pinned in both directions.
150
+
151
+ The suite pins the green run on this tree beside the mutations that must red, as fixtures, so a clean result is a measurement rather than the absence of one. Four equivalent mutations of the real tree were run at authoring, each restored afterwards; the suite does not mutate the real tree.
152
+
153
+ No change to the published package.
154
+
155
+ ## 0.0.13
156
+
157
+ ### Patch Changes
158
+
159
+ - 87d7263: Repository tooling: the PHI scanner now refuses a run that read only some of the targets it enumerated.
160
+
161
+ 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.
162
+
163
+ 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.
164
+
165
+ 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.
166
+
167
+ 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.
168
+
169
+ No change to the published package.
170
+
3
171
  ## 0.0.12
4
172
 
5
173
  ### Patch Changes
package/README.md CHANGED
@@ -19,17 +19,21 @@ _does_ bundle, including the UCUM unit table, the code-system _identities_ (OID
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
23
- > operation below ships today. The surface:
22
+ > **Status:** `0.1.0`, **published on npm.** The public API is settled: below `1.0.0` a breaking
23
+ > change ships in a new minor version, never a patch, and the changelog says what broke. The
24
+ > **engine is complete**, every operation below ships today. The surface:
24
25
  >
25
26
  > - the code-system **identity resolver** (`resolveSystem`) and the ConceptMap **`$translate`** engine
26
27
  > (`loadConceptMap` / `translate`);
27
28
  > - the **CodeSystem load layer** (RRF / CSV / fixed-width / FHIR JSON) with **`$lookup`** and
28
29
  > **`$validate-code`**;
29
30
  > - **ValueSet** binding (`compose` / **`$expand`** / **`$validate-code`**), subsumption read from the
30
- > release's own hierarchy;
31
+ > release's own hierarchy, measured against HL7's own terminology ecosystem test cases
32
+ > ([terminology ecosystem conformance](#terminology-ecosystem-conformance));
31
33
  > - **UCUM** unit validation and canonicalization (`validateUcum` / `ucumEqual`, recognition only, **no
32
- > magnitude conversion**): the official UCUM functional-test suite is the conformance gate;
34
+ > magnitude conversion**), gated on the official UCUM functional tests: the **validation** and
35
+ > **conversion** case kinds, against the suite dated 3-Feb 2021, and no other kind
36
+ > ([UCUM conformance](#ucum-conformance));
33
37
  > - the **crosswalk resolvers**: the CMS **ICD-9↔ICD-10 GEMs** (`loadGems` / `applyGem`,
34
38
  > public-domain) and the NLM **SNOMED CT → ICD-10-CM complex map** (`loadComplexMap` /
35
39
  > `applyComplexMap`, BYO: no SNOMED CT refset bundled), never inverted;
@@ -47,20 +51,23 @@ listed with its copyright under [What is bundled](#what-is-bundled).
47
51
 
48
52
  ## Install
49
53
 
50
- > **On npm.** `@cosyte/terminology` is **published**, on the pre-alpha `0.0.x` ladder. The command
51
- > below installs the real package; the registry, not this page, is the authority on the exact version.
54
+ > **On npm.** `@cosyte/terminology` is **published**, on the `0.1.x` line. The command below
55
+ > installs the real package; the registry, not this page, is the authority on the exact version.
52
56
 
53
57
  ```bash
54
58
  npm install @cosyte/terminology
55
59
  ```
56
60
 
61
+ Runnable programs for the main jobs are in
62
+ [`examples/`](https://github.com/cosyte/terminology/tree/main/examples).
63
+
57
64
  ## Resolve a code system to its canonical URI
58
65
 
59
66
  ```ts
60
67
  import { resolveSystem, isUnknownSystem } from "@cosyte/terminology";
61
68
 
62
69
  const r = resolveSystem("2.16.840.1.113883.6.1"); // OID, mnemonic, or URI
63
- isUnknownSystem(r) ? "unknown" : r.url; // "http://loinc.org"
70
+ console.log(isUnknownSystem(r) ? "unknown" : r.url); // http://loinc.org
64
71
  ```
65
72
 
66
73
  ## Translate a code through a ConceptMap
@@ -73,6 +80,7 @@ const result = translate({ system: "http://loinc.org", code: "2160-0" }, map);
73
80
 
74
81
  if (result.unmapped) {
75
82
  // Typed, surfaced outcome: carries any group.unmapped fallback mode. Never a guessed target.
83
+ result.notRelated; // the map's own "not related" assertions, when it made any (see below)
76
84
  } else {
77
85
  for (const m of result.matches) {
78
86
  m.target; // a frozen Coding, drawn verbatim from the map
@@ -83,6 +91,27 @@ if (result.unmapped) {
83
91
  }
84
92
  ```
85
93
 
94
+ A map can also say, explicitly, that a source and a target are **not** related (FHIR R4
95
+ `equivalence: "disjoint"`, R5 `not-related-to`). That is an assertion, not a translation: when
96
+ **every** declared target for a source asserts non-relation (`disjoint` or `unmatched`), the source
97
+ reports as **not translated** (`result.unmapped === true`), and the rows the map asserted are
98
+ carried on `result.notRelated` verbatim, each with its target coding, its R4 `equivalence` and the
99
+ author's `comment`. So a caller branching on the verdict never reads a declared "not related" as a
100
+ successful translation, and still gets to see what the steward said:
101
+
102
+ ```ts
103
+ if (result.unmapped) {
104
+ for (const n of result.notRelated ?? []) {
105
+ n.target; // the target the map declared, verbatim: NOT a mapping to use
106
+ n.equivalence; // "disjoint"
107
+ n.comment; // why the steward says these are unrelated, when they said
108
+ }
109
+ }
110
+ ```
111
+
112
+ When at least one target does assert a relationship, the source is translated exactly as before and
113
+ any `disjoint` row rides along in `result.matches`, in its declared position.
114
+
86
115
  ## Look up a code in a code system
87
116
 
88
117
  ```ts
@@ -124,9 +153,10 @@ ucumEqual("Cel", "K"); // false: a special (non-linear) unit is never equated wi
124
153
  The UCUM table is the official `ucum-essence.xml` (version 2.2), and it **is bundled with this
125
154
  package**: embedded verbatim in the published build and parsed at runtime. It is copyright
126
155
  ©1999–2024 Regenstrief Institute, Inc., reproduced under the UCUM Copyright Notice and License
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
129
- [What is bundled](#what-is-bundled).
156
+ (<https://ucum.org/license>); the engine ships no derivative of it. It passes the official UCUM
157
+ functional-test cases it claims, and claims no more than it runs: see
158
+ [UCUM conformance](#ucum-conformance). The full notice ships in the package at
159
+ `vendor/ucum/NOTICE.md`: see [What is bundled](#what-is-bundled).
130
160
 
131
161
  > **Case-sensitive (c/s) mode only.** UCUM's normative interchange mode (the one FHIR/HL7 bind to)
132
162
  > is case-sensitive (`m` = metre, `M` = mega, `Pa` = pascal, `pA` = picoampere). The
@@ -134,6 +164,68 @@ functional-test suite. The full notice ships in the package at `vendor/ucum/NOTI
134
164
  > `TERM_UCUM_INVALID` (fail-safe, never silently reinterpreted). Magnitude conversion is likewise a
135
165
  > deliberate non-goal (recognition and canonicalization only).
136
166
 
167
+ ### UCUM conformance
168
+
169
+ The claim in full, so a reader can tell what was run from what was not. UCUM permits a qualified
170
+ claim where an implementation does not cover every kind of case, and asks that the most recent date
171
+ in the suite's own history be quoted:
172
+
173
+ - **Case kinds claimed: validation, conversion.** Every `validation` case is executed and its
174
+ expected `valid` outcome asserted. The `conversion` cases are executed for **commensurability
175
+ only**: source and destination must reduce to the same dimension. The converted magnitude is not
176
+ asserted, because magnitude conversion is a deliberate non-goal of this engine.
177
+ - **Case kinds not claimed: displayNameGeneration, multiplication.** Those cases are not executed and
178
+ no conformance to them is claimed.
179
+ - **Suite date: 3-Feb 2021.** The most recent history entry in the vendored
180
+ `UcumFunctionalTests.xml`, which is the file the cases are read from.
181
+ - **Table: `ucum-essence.xml` version 2.2, revision-date 2024-06-17.** The unit-table release this
182
+ claim is made against, and the one embedded in the published build.
183
+
184
+ The package's own test suite holds the claim to those files, so it cannot go stale in silence. It
185
+ fails if the vendored table's declared version or revision-date stops matching the release named
186
+ here, if the suite's most recent history date stops matching the date named here, if a kind named
187
+ here is not one the tests execute, if a named kind's section carries no case, or if this page and
188
+ `vendor/ucum/NOTICE.md` state different suite dates.
189
+
190
+ ## Terminology ecosystem conformance
191
+
192
+ The ValueSet layer is measured against **HL7's own FHIR terminology ecosystem test cases**
193
+ (`HL7/fhir-tx-ecosystem-ig`) as well as against tests written here. The claim in full, so a reader
194
+ can tell what was run from what was not:
195
+
196
+ - **Snapshot: upstream commit `33d37fc0f8efaed5832032f3a73956f66d4d4f23`.** The suite publishes no
197
+ version field, so the commit is the version, and it is the version these counts were produced
198
+ against. A pinned copy of the cases sits in the repository with their origin and licence recorded
199
+ beside them; they are third-party material and are not part of the published package.
200
+ - **Suites in scope: `simple-cases`, `parameters`, `validation`. Operations in scope: `$expand` and
201
+ ValueSet `$validate-code`.** The suite's other suites and its other operations are not run, and no
202
+ conformance to them is claimed.
203
+ - **Cases ran: 101. Passed: 20. Declined: 81.** The three add up: every case that ran is exactly one
204
+ of them. Those three suites declare 112 cases in total; the rest declare a different operation,
205
+ are marked in the suite for one server's own behaviour, or are held out of the measurement, and
206
+ none of those is run or counted anywhere above.
207
+ - **A declined case is one this library has no answer for, named rather than counted as a pass.** It
208
+ asks for a request parameter the public API takes no argument for (`activeOnly`,
209
+ `displayLanguage`, `includeDesignations` and others), for HTTP or language negotiation that a
210
+ library call cannot perform, or it is met by this engine's own typed refusal. Every decline is
211
+ recorded with its case and its reason.
212
+ - **A case that answers differently fails the run, and is never recorded as a decline.** The counts
213
+ above come from a run in which no case answered differently.
214
+
215
+ The cases are written for a **server** answering over HTTP, and this package is a library, so they
216
+ are driven in process through the public API and the comparison is narrower than a server's would
217
+ be: an expansion is compared on its membership (system, code and display), a `$validate-code` case
218
+ on its recorded `result`. The recorded responses declare their own tolerances and those are
219
+ honoured as declared; every tolerance applied beyond them is written down beside the counts, on the
220
+ ground that a tolerance nobody can see is indistinguishable from a pass that was not earned.
221
+
222
+ The package's own test suite reproduces the run and holds these figures to it, so they cannot go
223
+ stale in silence.
224
+
225
+ **This is a measurement, not an endorsement.** HL7 publishes these cases and reviews outcomes under
226
+ its own process. Nothing here is an approval, a certification or an endorsement of this package by
227
+ HL7 or by anyone else.
228
+
137
229
  ## Navigate the RxNorm drug graph
138
230
 
139
231
  Load a **bring-your-own** RxNorm RRF release and walk the ingredient / brand / clinical-drug /