@cosyte/terminology 0.0.7 → 0.0.9

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
@@ -23,8 +23,244 @@ this file is maintained by hand (Changesets handles the version bump and publish
23
23
  without evaluating a part expansion still has to report. Reading `d.system` is now a type error.
24
24
  The rationale, and the other three sites, are under **Fixed** below.
25
25
 
26
+ - **The test suite no longer sets a global timeout, and its one repeated start-up tax is gone**
27
+ (`PARSER-TESTTIMEOUT-ASSERTS-AN-IDLE-BOX`). Development tooling only — `vitest.config.ts` and the
28
+ test suite. No source file, no build output and no part of the public surface is touched.
29
+
30
+ **The trim came first, and it was the larger win.** `test/scripts/phi-scan.test.ts` spawns the
31
+ scanner at twenty call sites, and the cost of each spawn was start-up, not scanning — the fixtures
32
+ are a few hundred bytes. Measured on a 12-CPU cgroup quota, warmed runs of each against the same
33
+ scanner and argv: a `tsx` start had a median around **470–500 ms** against **141–200 ms** for
34
+ `node` running the same TypeScript through its native type stripping — roughly 300 ms a spawn.
35
+ That roughly halved the file: a plain base run measured **13,925 ms** over 21 cases, and plain runs
36
+ after the change measured **5,657 ms and 6,853 ms** over 22. Both runners were checked to produce
37
+ byte-identical stdout and stderr and the same exit code, on a clean file and on a violator, before
38
+ the change; several cases assert on stderr exactly.
39
+
40
+ `pnpm phi-scan` still runs `tsx`, so one case still spawns it and asserts the two runners **agree**
41
+ on exit code, stdout and stderr — a parity assertion, so a divergence reds rather than letting the
42
+ sweep quietly test something the gate does not run. Node's type stripping is on by default from
43
+ **22.18** while `engines.node` is `>=22.0.0`; the CI matrix is 22 + 24 and both resolve above that,
44
+ but a developer on 22.0–22.17 would need a newer 22 to run the suite.
45
+
46
+ **`testTimeout: 10_000` and `hookTimeout: 10_000` are removed**; the shared `@cosyte/vitest-config`
47
+ sets no timeout at all. A global timeout asserts the **machine**, not the code: sized for the
48
+ slowest test in the repo and then inherited by every other test, so a genuinely hung fast test
49
+ looks merely slow instead of broken. `hookTimeout` was pure noise — Vitest's default hook timeout
50
+ is **exactly** 10,000 ms, so the literal restated it. Both defaults were established by measuring a
51
+ deliberately over-running test and hook, not by reading the documentation.
52
+
53
+ **Per-test, never global.** Every test whose cost is not fixed now declares its own budget beside
54
+ the work. The `attw-gate` cases that run a real `npm pack` have carried an explicit 60 s one all
55
+ along; three groups were added — the two 200,000-row bulk cases in
56
+ `test/rxnorm/term-types.test.ts`, the single `tsx`-parity case, and **all of `test/property/**`**,
57
+ whose runtime varies with the fast-check draw as well as with the box, since this package pins no
58
+ seed by design. With those declared, every remaining test peaked at **689 ms\*\*, a ~7x margin
59
+ against the 5 s default the config now inherits.
60
+
61
+ **The method is part of the claim.** Ten full runs on a contended box (12-CPU cgroup quota,
62
+ fourteen-worker fleet), of which **six were `vitest run --coverage`**. That is not optional: CI
63
+ gates on `pnpm test` _and_ `pnpm test:coverage`, and the instrumented run is materially slower,
64
+ roughly doubling the property-suite peaks. An earlier draft of this change measured only the plain
65
+ run, recorded 499 ms, and was refuted for it.
66
+
67
+ **Two things measurement corrected.** The argument-refusal cases in `attw-gate` look like they
68
+ belong with the slow ones and do not: the wrapper rejects the flag before invoking `attw`, so they
69
+ run no `npm pack` and finish in ~200 ms. And the case that actually approached the old ceiling —
70
+ **9,318 ms against 10,000 ms** under load, a 682 ms margin on correct code — was an `attw` case
71
+ that already had its own 60 s budget, so the global was never what stood between it and a false
72
+ red. The `attw` cost itself is genuine work (a real `npm pack` runs ~1,550 ms), so it is bounded
73
+ rather than trimmed. These figures describe one box on one day and are recorded so the next reader
74
+ re-measures rather than inherits them.
75
+
26
76
  ### Fixed
27
77
 
78
+ - **The publish gate's argument guard refused a fixed LIST of blinding spellings, and `-fjson` was
79
+ not on it — a value fused to a short flag hid the one sentence the gate reads, and it handed back a
80
+ pass over a tarball carrying no types** (`ATTW-GUARD-ALLOWLIST`). Development tooling only —
81
+ `scripts/attw.mjs` ships in no tarball, and the public surface, build output and runtime behaviour
82
+ are unchanged.
83
+
84
+ **The rules of the gate are stated once, in `scripts/attw.mjs`'s docblock. This entry points at it
85
+ rather than repeating them**, and `CLAUDE.md`'s paragraph now does the same. The guard was
86
+ described in several committed files at once and the copies drifted: `CLAUDE.md` claimed a refusal "by
87
+ option name, wholesale" that a fused short option walked past, and called `--config-path` refused
88
+ "by inference, not measurement" when the real-file half takes one command to measure.
89
+
90
+ **The remedy is an ALLOW-LIST, so no spelling is enumerated.** The gate forwards `--profile` and
91
+ `--no-definitely-typed` and refuses everything else, including options that blind nothing:
92
+ "harmless" is not a judgement a script can make from an option name. Measured on `7bdd5f5` against
93
+ this repo's pinned `@arethetypeswrong/cli@0.18.4`, on a fixture whose tarball carries no types —
94
+ `-fjson`, `-Pf json` and `-Pfjson` each exited **0** with the untyped sentence absent and a
95
+ non-empty transcript, so the empty-output net could not backstop them; `--help`, `-h`, `--version`
96
+ and `-V` did the same. `--config-path` naming a real file that sets `format` blinded identically,
97
+ while the nonexistent path the old test named blinds **nothing** — `readConfig()` swallows the
98
+ `ENOENT` and the sentence is still printed, so that test asserted a refusal over a case that could
99
+ not have hurt the gate. Both halves are pinned now.
100
+
101
+ **The preflight walks two promises it used to skip**, each measured to leave bare `attw` at exit 0:
102
+ `bin`, which `attw` does not look at at all, so a manifest could promise a command absent from the
103
+ tarball; and a path written without a leading `./`, which is legal and is the spelling npm's own
104
+ documentation uses, so `"main": "dist/index.js"` was dropped silently while the gate reported it
105
+ had checked. This package declares no `bin` — that half is pinned on a fixture that does, because
106
+ every sibling copies this file.
107
+
108
+ **A false explanation of a correct red is deleted rather than reworded.** The preflight used to
109
+ name the exit code `attw` "would have" produced. It reads the manifest and never the tarball, and
110
+ the tarball decides: `analysis.types` is `containsTypes()` in `@arethetypeswrong/core`, which is
111
+ `listFiles(directory).some(ts.hasTSFileExtension)` — any TypeScript-extension file in the packed
112
+ tarball, computed before an entrypoint is resolved. A package packing a whole `dist/` can lose
113
+ every declared declaration and still hand `attw` an undeclared chunk declaration, at which point it
114
+ exits 1 and the sentence is false. The failure now says what it checked and that it cannot speak
115
+ for `attw`.
116
+
117
+ **A limit found while grading this, filed rather than closed.** The `.attw.json` refusal names
118
+ `quiet` and `format`, but `readConfig()` sets an option value for **every** key except
119
+ `configPath`, `help` and `version` — so a committed config reaches options this gate does not name,
120
+ `definitelyTyped` pointed at a `.tgz` among them, which merges those types in and can make an
121
+ untyped tarball analyse as typed. It predates this change, and the allow-list closes only its argv
122
+ half — `--definitely-typed` on argv is now refused, where the deny-list forwarded it — while the
123
+ config route is untouched. Adding a key would rebuild, on the config side, the deny-list this
124
+ change retires on the argument side.
125
+
126
+ - **A symbolic link under a scan root read CLEAN on BOTH of the PHI scanner's enumerating routes, so
127
+ a link pointing at a file full of real identifiers passed the gate twice over**
128
+ (`PHI-SCAN-SYMLINK-BLIND-ON-BOTH-ROUTES`). Development tooling only — `scripts/phi-scan.ts` ships in
129
+ no tarball and the package's public surface is unchanged.
130
+
131
+ Reproduced on `80a8cc7` before any fix, with a synthetic name-bearing payload (a person name, a
132
+ DOB, a dashed SSN and an email at a non-test domain) placed outside the walk roots and a link to it
133
+ at `src/leak.ts`. `pnpm phi-scan` printed `OK — no hits` and exited **0**; `pnpm phi-scan --staged`,
134
+ after `git add`, printed `OK — no hits` and exited **0**. Naming the link's target explicitly on the
135
+ command line found both shapes and exited 1, so the payload was always detectable — the two routes
136
+ simply never looked at it.
137
+
138
+ The two blindnesses are separate mechanisms and needed separate fixes. The walk enumerates
139
+ `Dirent.isFile()`, which is an **lstat** answer, so a link is neither a file nor a directory and
140
+ fell out of the loop with no record that anything had been skipped — and `isDirectory()` answers
141
+ false for a linked directory too, so a whole subtree could disappear the same way. The `--staged`
142
+ route reads content with `git show :<path>`, and **git stores a symbolic link as its target path
143
+ under mode `120000`**, so it was handed the target's path text and scanned that. That second route
144
+ is this repo's `pre-commit` hook, which is exactly where the claim that `--staged` covers a link
145
+ would have been trusted.
146
+
147
+ **Neither route is made to follow the link.** Following would read bytes the enumeration does not
148
+ control — outside the repo, a loop, a device, a FIFO that blocks the gate forever — and git does not
149
+ carry those bytes anyway, so a hit on them would be a claim about something no commit contains.
150
+ Instead the enumeration is narrowed: an entry under a scan root that is not a regular file **refuses
151
+ the scan** (exit 2, the existing "could not complete" code), naming every offender rather than the
152
+ first. The walk classifies by `Dirent` (symbolic link, FIFO, socket, block device, character
153
+ device); `--staged` now reads `git diff --cached --raw -z` instead of `--name-only` so the
154
+ destination mode is visible, and refuses mode `120000` and `160000` (a gitlink) wherever that
155
+ route's own path scope reaches them. A `--raw` record that does not parse refuses as well, rather
156
+ than being skipped into a silently shortened list.
157
+
158
+ **`T` (typechange) is in the `--diff-filter`, and leaving it out made the mode check unreachable
159
+ whenever the file being replaced was already tracked.** Replacing a **tracked** regular file with a link is neither an add nor a
160
+ modify: git raises `:100644 120000 <sha> <sha> T`, so `--diff-filter=AM` deleted the record before
161
+ any mode could be read and the hook passed the link green. Measured on git 2.39.5: with `AM` the
162
+ raw output for that stage is empty. Typechange carries a single path, exactly like `A` and `M`, so
163
+ admitting it costs the record stride nothing — and it means the reverse typechange, a link replaced
164
+ by a real file, is now scanned as the ordinary file it became.
165
+
166
+ **A refusal names the entry's own repo-relative path and an engine-owned token for its kind, and
167
+ never the link target** — a target path is text off the working tree and can itself carry PHI. That
168
+ is asserted, not asserted-in-prose: the pinning payload and the target's own filename both carry a
169
+ synthetic person name, and every refusal message is checked to contain none of it.
170
+
171
+ **What this does not cover, stated narrowly, each measured.** Explicit-path mode already read
172
+ through a link and reported the target's hits; it is unchanged. The `--staged` path scope is
173
+ unchanged — still `test/fixtures/**` and `src/**.ts` — so a staged link outside it is still not
174
+ looked at; narrowing what a scope admits is not widening the scope. That scope also bounds the
175
+ gitlink half: a submodule staged at `test/fixtures/nested` is refused, while one at `src/nested` is
176
+ a directory name that fails the `.ts` suffix and is not looked at. `R` (rename) and `C` (copy) are
177
+ still **not** enumerated by `--staged` at all, so a staged rename that also appends PHI passes that
178
+ route — pre-existing, unchanged here, and admitting it needs the two-path record shape handled,
179
+ which is a scope decision rather than this one; CI's all-mode sweep does read the resulting
180
+ worktree file. A tracked file **absent from the worktree** is still caught at `git add` time only.
181
+ And the scanner still has no tolerance for a file that vanishes between enumeration and read, so an
182
+ untracked transient appearing under a walk root can still refuse a whole sweep; that is a different
183
+ defect, it fails closed, and it is not addressed here.
184
+
185
+ - **A loaded GEM map, complex map and RxNorm drug graph froze their wrapper and handed out plain
186
+ `Map`s inside it, so whoever held one could delete a diagnosis mapping, add a target the steward's
187
+ file never authored, or empty a medication's ingredient edges** (`TERMINOLOGY-INNER-MAPS-UNSEALED`).
188
+ `Object.freeze` cannot reach a `Map` — its entries live in an internal slot rather than in
189
+ properties — so freezing the model left `set` / `delete` / `clear` working on every index it points
190
+ at, and the `readonly` on those fields was a compile-time claim only.
191
+
192
+ Measured on `dd8465b`, before any fix: one `push` into `loadGems(…).entries.get("0010")` made
193
+ `applyGem` answer with a target that is in no GEM file; `entries.delete("0010")` turned the same
194
+ source into a typed `TERM_CROSSWALK_UNMAPPED`; `graph.edges.clear()` left `ingredientsOf` answering
195
+ `targets: []` for every drug while `edgeCount` still reported the loaded figure; and
196
+ `graph.concepts.set(…)` put a concept the caller's release never shipped where navigation would
197
+ return it. The candidate and rule lists were nominally `readonly` in the same way — writing over
198
+ element `0` landed too.
199
+
200
+ A code system's `concepts`, a GEM's and a complex map's `entries`, and a drug graph's `concepts` /
201
+ `edges` / `ndcs` are each now a **read-only view** of a map rather than the map itself — those six
202
+ indexes, not every field this package types `ReadonlyMap` — and every list, entry and warning
203
+ reachable from one of those models is frozen. Reads are exactly a `Map`'s: `get`, `has`, `size`,
204
+ `keys`, `values`, `entries`, `forEach`, iteration, `new Map(view)`.
205
+
206
+ **Two behaviour changes to know before you write against one.** A view is not a `Map` instance, so
207
+ `instanceof Map` is now `false` on `CodeSystem.concepts`, a `GemMap`'s and a `ComplexMap`'s
208
+ `entries`, and a graph's `concepts` / `edges` / `ndcs`; it prints as a plain object, `Object.keys`
209
+ lists its own methods, and `JSON.stringify` renders it as `{"size":2}` where a `Map` rendered `{}`.
210
+ And a model carrying one therefore **cannot be structured-cloned** — `structuredClone` and
211
+ `worker.postMessage` raise a `DataCloneError`, `v8.serialize` a plain `Error`, where before each
212
+ produced a working copy. Clone what the other side needs instead: `new Map(cs.concepts)`. A loaded
213
+ `ConceptMap`, `ValueSet` or unit table carries no view and clones as it always did. That refusal is
214
+ deliberate rather than incidental. Hiding the view's methods would make the clone _succeed_ and
215
+ hand back a model
216
+ whose indexes were empty while `count` / `conceptCount` / `edgeCount` still reported the loaded
217
+ figures — this defect's own shape, arriving silently. It is pinned as a test, not left as prose.
218
+
219
+ Not being a `Map` is also what closes the second route rather than only the first: replacing a
220
+ `Map`'s three mutators leaves `Map.prototype.set.call(theMap, …)` reaching the internal slot
221
+ directly, and a view has no slot to reach. `CodeSystem.concepts` was sealed that weaker way — a
222
+ fabricated concept could be forced into a loaded release and `lookup` would return it — and is a
223
+ view now too.
224
+
225
+ **The bundled UCUM table is deliberately not part of this and is unchanged.** `atomByCode` and
226
+ `prefixByCode` stay real `Map`s whose `set` / `delete` / `clear` are replaced by a refusal, so an
227
+ entry forced through `Map.prototype` still lands there. That residual is bounded and stays
228
+ documented: `parseUcum` resolves an atom by scanning the frozen `atoms` array, never through a
229
+ lookup map, so a forced entry changes no validation, reduction or `ucumEqual` answer — only the
230
+ caller's own `get` is lied to.
231
+
232
+ The guarantee is that the loaded model does not change, not that a write throws: a named mutator
233
+ raises a `TypeError` in either mode, while a frozen list or entry throws in strict mode and is
234
+ silently ignored in sloppy mode, which the CJS build lets a caller observe. Every route is
235
+ therefore asserted by the **reading** it would have changed as well as by the refusal, and a
236
+ reachability sweep walks **all seven** `load*` models for anything still writable, so a field added
237
+ later is covered without anyone maintaining a list of routes. The UCUM table is pinned in that
238
+ sweep as the named exception rather than skipped, so converting it later has to be deliberate. The
239
+ sweep carries its own negative controls: a planted mutable object and a raw `Map` behind a frozen
240
+ wrapper both have to be reported.
241
+
242
+ - **`parseEssence`'s example imported it from `@cosyte/terminology/ucum/essence`, a subpath the
243
+ package's `exports` does not declare.** It is package-internal and not re-exported from the entry
244
+ point; the example says so and imports it the way this package's own code does. Consumers reach the
245
+ table through `loadUcumEssence()`, which is unchanged.
246
+
247
+ - **Three `/* v8 ignore */` hints in `src/ucum/essence.ts` were inert, and are now plain comments.**
248
+ The branches they claimed to exclude were reported uncovered either way — measured with and
249
+ without a `--` reason suffix and under `--coverage.experimentalAstAwareRemapping` — so the hints
250
+ stated a coverage exclusion that no run performed. The package's posture is the one
251
+ `vitest.config.ts` already records for `rxnorm/`: an unreachable `noUncheckedIndexedAccess`
252
+ fallback stays visibly uncovered under a directory floor of 90, rather than being hidden by an
253
+ assertion about reachability that the next reader inherits with no way to check it. No coverage
254
+ figure moved, which is what proves they were inert.
255
+
256
+ - **A GEM source with more than one combination scenario had no deterministic test**, so the
257
+ comparator that orders them was reached only when a `fast-check` draw happened to generate a second
258
+ scenario — `Array.prototype.sort` never calls one for a single-element array. Scenarios are
259
+ alternative valid representations of the same source, so their order is part of the answer. Covered
260
+ now by a fixture whose file order is deliberately scenario 2 first. Found while measuring this
261
+ slice; `vitest run --coverage --exclude 'test/property/**'` reports `crosswalk/` at 100% functions
262
+ with it and 95.45% without.
263
+
28
264
  - **The UCUM unit table `loadUcumEssence()` hands out was `readonly` to TypeScript only, so a unit's
29
265
  definition could be rewritten in place — and doing so fabricated a unit equivalence that outlived
30
266
  putting the table back** (`TERMINOLOGY-MUTABLE-ESSENCE-TABLE`). One table is shared by every caller
package/README.md CHANGED
@@ -208,8 +208,15 @@ _names_, listed under [What is bundled](#what-is-bundled).
208
208
  NDC you asked about, so log the `code` and the locus rather than the whole result — a code _in
209
209
  patient context_ can be PHI.
210
210
  - **Zero runtime dependencies. Dual ESM + CJS.** Node stdlib only; built with `tsup`, validated with
211
- `attw`. The models the engine loads for you are frozen — the bundled unit table deeply so, atom by
212
- atom and definition by definition, because it is shared with the engine's own reducer. The AST and
211
+ `attw`. A release you load is frozen, its indexes included: a code system's concepts, a GEM's or a
212
+ complex map's entries and a drug graph's concepts, edges and NDCs are each a read-only **view** of a
213
+ map rather than the map itself, so nothing holding one can add, delete or clear an entry, whichever
214
+ 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
216
+ definition, because it is shared with the engine's own reducer; its two lookup maps refuse `set`,
217
+ `delete` and `clear` by name, and an entry forced past that through `Map.prototype` changes nothing
218
+ the engine reads, because `parseUcum` resolves an atom by scanning the frozen atom array rather than
219
+ through a lookup map. The AST and
213
220
  the reduction that `parseUcum` and `reduce` hand back are per-call values you own, and mutating one
214
221
  changes nothing for anyone else.
215
222
 
package/dist/index.cjs CHANGED
@@ -567,6 +567,31 @@ function translate(sourceCoding, map) {
567
567
  );
568
568
  }
569
569
 
570
+ // src/common/frozen-map.ts
571
+ function frozenMap(source, what) {
572
+ const refuse = () => {
573
+ throw new TypeError(`Cannot mutate ${what} (it is immutable)`);
574
+ };
575
+ const view = {
576
+ get size() {
577
+ return source.size;
578
+ },
579
+ get: (key) => source.get(key),
580
+ has: (key) => source.has(key),
581
+ keys: () => source.keys(),
582
+ values: () => source.values(),
583
+ entries: () => source.entries(),
584
+ forEach: (callback, thisArg) => {
585
+ for (const [key, value] of source) callback.call(thisArg, value, key, view);
586
+ },
587
+ [Symbol.iterator]: () => source[Symbol.iterator](),
588
+ set: refuse,
589
+ delete: refuse,
590
+ clear: refuse
591
+ };
592
+ return Object.freeze(view);
593
+ }
594
+
570
595
  // src/codesystem/status.ts
571
596
  var defaultStatusMapper = (raw) => {
572
597
  switch (raw.trim().toUpperCase()) {
@@ -973,15 +998,6 @@ function parseRrf(source) {
973
998
  }
974
999
 
975
1000
  // src/codesystem/load.ts
976
- function sealConceptMap(map) {
977
- const guard = () => {
978
- throw new TypeError("Cannot mutate a loaded CodeSystem's concepts map (it is immutable)");
979
- };
980
- for (const method of ["set", "delete", "clear"]) {
981
- Object.defineProperty(map, method, { value: guard, writable: false, configurable: false });
982
- }
983
- return map;
984
- }
985
1001
  function loadCodeSystem(source) {
986
1002
  let concepts;
987
1003
  let warnings;
@@ -1019,7 +1035,7 @@ function loadCodeSystem(source) {
1019
1035
  }
1020
1036
  }
1021
1037
  out.count = concepts.size;
1022
- out.concepts = sealConceptMap(concepts);
1038
+ out.concepts = frozenMap(concepts, "a loaded CodeSystem's concepts map");
1023
1039
  out.warnings = Object.freeze(warnings);
1024
1040
  return Object.freeze(out);
1025
1041
  }
@@ -4069,21 +4085,25 @@ function loadGems(source) {
4069
4085
  if (trimmed === "") continue;
4070
4086
  const fields = trimmed.split(/\s+/);
4071
4087
  if (fields.length !== 3) {
4072
- warnings.push({
4073
- code: "TERM_GEM_MALFORMED_ROW",
4074
- line: i + 1,
4075
- detail: `expected 3 whitespace-delimited fields, found ${String(fields.length)}`
4076
- });
4088
+ warnings.push(
4089
+ Object.freeze({
4090
+ code: "TERM_GEM_MALFORMED_ROW",
4091
+ line: i + 1,
4092
+ detail: `expected 3 whitespace-delimited fields, found ${String(fields.length)}`
4093
+ })
4094
+ );
4077
4095
  continue;
4078
4096
  }
4079
4097
  const [src, tgt, flagStr] = fields;
4080
4098
  const flags = decodeFlags(flagStr);
4081
4099
  if (flags === void 0) {
4082
- warnings.push({
4083
- code: "TERM_GEM_MALFORMED_ROW",
4084
- line: i + 1,
4085
- detail: "flag field is not a 5-digit code"
4086
- });
4100
+ warnings.push(
4101
+ Object.freeze({
4102
+ code: "TERM_GEM_MALFORMED_ROW",
4103
+ line: i + 1,
4104
+ detail: "flag field is not a 5-digit code"
4105
+ })
4106
+ );
4087
4107
  continue;
4088
4108
  }
4089
4109
  const entry = { source: src, flags };
@@ -4094,11 +4114,12 @@ function loadGems(source) {
4094
4114
  else entries.set(src, [frozen]);
4095
4115
  count++;
4096
4116
  }
4117
+ for (const list of entries.values()) Object.freeze(list);
4097
4118
  return Object.freeze({
4098
4119
  direction: source.direction,
4099
4120
  ...source.version !== void 0 ? { version: source.version } : {},
4100
4121
  count,
4101
- entries,
4122
+ entries: frozenMap(entries, "a loaded GEM map's entries"),
4102
4123
  warnings: Object.freeze(warnings)
4103
4124
  });
4104
4125
  }
@@ -4246,11 +4267,13 @@ function parseRf2(content) {
4246
4267
  const groupN = Number(at(f, iGroup));
4247
4268
  const priorityN = Number(at(f, iPriority));
4248
4269
  if (source === "" || !Number.isFinite(groupN) || !Number.isFinite(priorityN)) {
4249
- warnings.push({
4250
- code: "TERM_COMPLEX_MAP_MALFORMED_ROW",
4251
- line: i + 1,
4252
- detail: "row is missing referencedComponentId / mapGroup / mapPriority"
4253
- });
4270
+ warnings.push(
4271
+ Object.freeze({
4272
+ code: "TERM_COMPLEX_MAP_MALFORMED_ROW",
4273
+ line: i + 1,
4274
+ detail: "row is missing referencedComponentId / mapGroup / mapPriority"
4275
+ })
4276
+ );
4254
4277
  continue;
4255
4278
  }
4256
4279
  rows.push({
@@ -4293,7 +4316,7 @@ function loadComplexMap(input) {
4293
4316
  }
4294
4317
  const out = {
4295
4318
  count,
4296
- entries,
4319
+ entries: frozenMap(entries, "a loaded complex map's entries"),
4297
4320
  warnings
4298
4321
  };
4299
4322
  if (input.version !== void 0) out.version = input.version;
@@ -4587,9 +4610,9 @@ function loadRxNormGraph(source) {
4587
4610
  const ndcs = source.sat !== void 0 ? parseNdcs(source.sat, warnings) : /* @__PURE__ */ new Map();
4588
4611
  for (const list of edges.values()) Object.freeze(list);
4589
4612
  const out = {
4590
- concepts,
4591
- edges,
4592
- ndcs,
4613
+ concepts: frozenMap(concepts, "a loaded RxNorm graph's concepts"),
4614
+ edges: frozenMap(edges, "a loaded RxNorm graph's edges"),
4615
+ ndcs: frozenMap(ndcs, "a loaded RxNorm graph's NDC index"),
4593
4616
  conceptCount: concepts.size,
4594
4617
  edgeCount,
4595
4618
  warnings: Object.freeze(warnings)
@@ -4753,7 +4776,7 @@ function approximateMatch(graph, query, options = {}) {
4753
4776
  }
4754
4777
 
4755
4778
  // src/index.ts
4756
- var VERSION = "0.0.7";
4779
+ var VERSION = "0.0.9";
4757
4780
 
4758
4781
  exports.COMPLEX_MAP_SOURCE_SYSTEM = COMPLEX_MAP_SOURCE_SYSTEM;
4759
4782
  exports.DIAGNOSTIC_CODES = DIAGNOSTIC_CODES;