@cosyte/terminology 0.0.5 → 0.0.7

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/dist/index.d.cts CHANGED
@@ -100,10 +100,63 @@ declare function codeableConcept(init: CodeableConcept): CodeableConcept;
100
100
  * them, so renaming or removing one is a **breaking change**, and the full set is snapshotted via
101
101
  * `sortedCodeSet` (`@cosyte/test-utils`) as a reviewable tripwire.
102
102
  *
103
- * **Value-free by construction.** A diagnostic carries a stable code plus, at most, a
104
- * code + system + version — **never** a surrounding patient identifier or clinical narrative. A
105
- * code *in patient context* can be PHI, so messages describe *structure* (a resource
106
- * path, a code system), never echo arbitrary input *values*.
103
+ * **Value-free by construction**, and the property is stated as what it actually is: every string a
104
+ * diagnostic or fatal message is assembled from is **owned by the engine**. Three kinds, and no
105
+ * fourth: a string literal; an entry in a frozen table keyed on the engine's own vocabulary; and a
106
+ * **locus this package built** — a 1-based `line`, a column count, or an index path over the
107
+ * caller's own resource made of integers and FHIR field names (`ConceptMap group[0].element[0].target[0]`,
108
+ * `compose.include[2]`). Nothing a caller configured and nothing their release or resource contained
109
+ * is ever one of them, so no such string reaches a `message`, a `stack`, or a `detail`, at any
110
+ * length.
111
+ *
112
+ * Read that as the rule, and note what it does **not** say. It does not say no factory takes a
113
+ * `string` parameter. Several do: `malformed(path, fault)` in `conceptmap/load.ts` and
114
+ * `valueset/load.ts`, and `cannotExpand(detail, path)`, `truncated(detail, path)` and
115
+ * `underPath(d, prefix)` in `valueset/` — where `expand.ts` and `validate.ts` each carry their own
116
+ * copy. It says every **argument** reaching them is engine-owned: each `fault` and `detail` is a
117
+ * literal at the call site, each `path` and `prefix` is built from indices by the loader that
118
+ * raises it. That distinction is the thing to preserve — an absolute "no factory takes a value
119
+ * parameter" reads as a stronger guarantee than the one the code makes, and those factories are the
120
+ * counter-example to it. Do not write the count down; derive it.
121
+ *
122
+ * Positional context is value-free the same way: a `line` integer, an `ExpansionDiagnostic.path`
123
+ * index path into the caller's own resource (`compose.include[2]`), or a closed-set token
124
+ * (`RXNCONSO` / `RXNREL` / `RXNSAT`) — never a URI, a column name, or a field the file supplied.
125
+ * This matters because a code *in patient context* can be PHI, and a thrown error is the one surface
126
+ * that reaches a log or an error reporter without the consumer choosing to put it there.
127
+ *
128
+ * **▶ THE CLAIM IS ABOUT THE MESSAGE FIELDS, NOT ABOUT THE OBJECTS. DO NOT SHORTEN IT TO "A
129
+ * DIAGNOSTIC IS SAFE TO LOG".** The safe strings are the *message* fields:
130
+ * `TerminologyError.message` / `.stack`, `LoadWarning.detail`, `GemLoadWarning.detail`,
131
+ * `ComplexMapLoadWarning.detail`, `RxNormLoadWarning.detail`, `ExpansionDiagnostic.detail`,
132
+ * `ParseFailure.reason` and `UcumValidation.reason`, plus the loci beside them.
133
+ *
134
+ * The **objects** these codes appear on are a different matter, and they carry verbatim text from
135
+ * two *different* provenances, which decide differently:
136
+ *
137
+ * - **Your query, echoed back** — `LookupUnknown.input`, `LookupResult.code`, `UnknownSystem.input`,
138
+ * `TranslateUnmapped.source`, `CrosswalkUnmapped.source`, `CrosswalkNoMap.source`,
139
+ * `RxNormUnknown.rxcui`, `RxNormRelated.predicates`, `NdcUnmapped.ndc`,
140
+ * `ValueSetMembership.coding`, and **`MapProvenance.sourceSystem`** — which is the `system` off
141
+ * the `Coding` you passed to `translate`, verbatim and unbounded, whenever that coding
142
+ * carries one; the map's own `group.source` is only the fallback for a coding that does not. The
143
+ * query is the one genuinely **patient-derived** input this engine takes (a code off a parsed
144
+ * message, a unit off an observation, an NDC off a prescription), so this is the class that
145
+ * decides a PHI question.
146
+ * - **Your loaded artifact, carried through** — `ComplexMapContextRequired.rules` (the map
147
+ * document's own `rule` / `advice` text) and `MapProvenance.targetSystem` / `.conceptMapUrl` /
148
+ * `.conceptMapVersion`, which are read off the loaded map (`group.target`, `map.url`,
149
+ * `map.version`). These are *reference data*, bounded by the map or release you loaded, not by
150
+ * anything a patient supplied. **`MapProvenance` straddles the two lists** — do not read the whole
151
+ * record as reference data on the strength of the fields that are.
152
+ *
153
+ * Read both lists as illustrative, not closed: **assume any result field may hold what you passed
154
+ * in.** So `JSON.stringify(lookupResult)` into a log is a PHI decision, and `String(err)` is not.
155
+ * Log the `code` and the locus; log a value only if you would log the code you passed in.
156
+ *
157
+ * Those echoes are deliberate and are not going away: they are how a never-fabricate outcome says
158
+ * *which* thing it refused to guess, they are the caller's own data returned to the caller, and
159
+ * bounding them would mean fabricating a miss.
107
160
  *
108
161
  * @packageDocumentation
109
162
  */
@@ -318,8 +371,10 @@ type FatalCode = (typeof FATAL_CODES)[keyof typeof FATAL_CODES];
318
371
  * The engine's typed error. Carries a stable {@link FatalCode} so callers branch on `err.code`
319
372
  * without string-matching the message. Thrown only for {@link FATAL_CODES} conditions.
320
373
  *
321
- * The `message` is **value-free**: it names the structural fault (a resource path, a missing
322
- * field) and never echoes input *values*.
374
+ * The `message` is **value-free**: it names the structural fault (a resource path, a missing field,
375
+ * the *role* of a configured column) and never echoes an input value — so it is safe to log, and
376
+ * safe in an `err.stack` that reaches an error reporter. The rule the strings are built by is in
377
+ * this module's docblock above.
323
378
  *
324
379
  * @example
325
380
  * ```ts
@@ -338,6 +393,9 @@ declare class TerminologyError extends Error {
338
393
  /**
339
394
  * @param code - The stable {@link FatalCode} describing the fault.
340
395
  * @param message - A **value-free** structural description (path + fault; never an input value).
396
+ * Every piece of it must be engine-owned — a literal, a frozen-table entry, or a locus this
397
+ * package built. This string reaches `err.stack`, so interpolating a caller-supplied or
398
+ * document-derived value into it is how a diagnostic becomes a leak.
341
399
  */
342
400
  constructor(code: FatalCode, message: string);
343
401
  }
@@ -611,7 +669,13 @@ interface MapProvenance {
611
669
  readonly conceptMapUrl?: string;
612
670
  /** The ConceptMap's business `version`, when declared (mappings are release-scoped). */
613
671
  readonly conceptMapVersion?: string;
614
- /** The source code system of the group that matched (or was consulted), when known. */
672
+ /**
673
+ * The source code system for this translation. **This is an echo of your query, not reference
674
+ * data off the map**: it is the `system` on the `Coding` you passed to `translate`, verbatim and
675
+ * unbounded, whenever that coding carries one; the matched (or consulted) group's own `source` is
676
+ * only the fallback for a coding that does not. Log it only if you would log the code you passed
677
+ * in.
678
+ */
615
679
  readonly sourceSystem?: string;
616
680
  /** The target code system of the group that matched, when known. */
617
681
  readonly targetSystem?: string;
@@ -1272,6 +1336,9 @@ declare function parseRrf(source: RrfSource): {
1272
1336
  * surfaced** as a `TERM_CSV_MALFORMED` warning; an unterminated final quote is surfaced too — never a
1273
1337
  * crash. A configured code/display header that is **absent from the header row** is a *fatal*
1274
1338
  * (`TERM_CODESYSTEM_MALFORMED`): the source config does not match the file, so nothing loads cleanly.
1339
+ * That fatal names the missing column's **role** (`code`, `display`, `status`, a property column)
1340
+ * from a frozen table, never the column name you configured — the message reaches `err.stack`, and a
1341
+ * column name is consumer-supplied text of unbounded length.
1275
1342
  *
1276
1343
  * @packageDocumentation
1277
1344
  */
@@ -1296,7 +1363,8 @@ declare function parseCsv(content: string): string[][];
1296
1363
  * @param source - The CSV source.
1297
1364
  * @returns The parsed concepts (by code) and the load warnings.
1298
1365
  * @throws {TerminologyError} `TERM_CODESYSTEM_MALFORMED` when the file has no header row, or a
1299
- * configured `code`/`display`/`status`/property column is absent from the header.
1366
+ * configured `code`/`display`/`status`/property column is absent from the header. The message
1367
+ * names the missing column's **role**, never the name you configured.
1300
1368
  * @example
1301
1369
  * ```ts
1302
1370
  * import { parseCsvSource } from "@cosyte/terminology";
@@ -1607,8 +1675,18 @@ interface ExpansionDiagnostic {
1607
1675
  readonly code: DiagnosticCode;
1608
1676
  /** A **value-free** structural description of the concern (never echoes a patient value). */
1609
1677
  readonly detail: string;
1610
- /** The code system / value set URI the concern is about (a published identity — not PHI). */
1611
- readonly system?: string;
1678
+ /**
1679
+ * A **value-free structural locus** into the caller's own resource — `"expansion"`, or a
1680
+ * `compose`-relative index path such as `"compose.include[2]"` or
1681
+ * `"compose.include[0].valueSet[1]"`. A diagnostic raised while expanding a *referenced* value set
1682
+ * is prefixed with the reference that reached it (`"compose.include[0].valueSet[1]/compose.include[3]"`).
1683
+ *
1684
+ * Built entirely from integers and the engine's own field names, so nothing the caller or their
1685
+ * resource supplied can reach it, at any length. It replaces the URI this field used to carry:
1686
+ * a canonical URI is unbounded consumer-supplied text on a diagnostic surface, and an index path
1687
+ * is strictly more precise anyway (two `include`s may name the same system).
1688
+ */
1689
+ readonly path?: string;
1612
1690
  }
1613
1691
  /**
1614
1692
  * The result of {@link expand}: the flattened membership plus an honest **completeness** signal.
@@ -2160,8 +2238,20 @@ declare function parseUcum(unit: string): ParseResult;
2160
2238
  * Reduce a parsed UCUM unit to its canonical {@link Reduction} — a linear scalar×dimension form, or
2161
2239
  * an opaque `special` form for expressions involving a non-linear special unit.
2162
2240
  *
2241
+ * The reduction of one atom is cached against **that atom object**, so a `UnitNode` you assembled
2242
+ * yourself is answered from its own atoms and cannot change how a unit off {@link parseUcum} reduces,
2243
+ * in this call or any later one.
2244
+ *
2163
2245
  * @param node - The AST from {@link parseUcum}.
2164
2246
  * @returns The reduced form.
2247
+ * @throws {Error} If reducing `node` reaches an atom with no linear definition, or one whose
2248
+ * definition does not parse. **No expression {@link parseUcum} accepts reaches either**; a node
2249
+ * you assemble yourself can, including by defining an atom in terms of a special unit such as
2250
+ * `Cel`, which lands on the table's own `Cel`. A third guard catches a definition that leads back
2251
+ * to itself. Naming a table atom does not reach it — a definition is resolved against the loaded
2252
+ * table, so your atom can name one but never be one — and neither does the table itself, which is
2253
+ * frozen; what reaches it is a `value` accessor that re-enters `reduce` for the same atom while
2254
+ * that atom is still being reduced. The message names the fault, never the atom.
2165
2255
  * @example
2166
2256
  * ```ts
2167
2257
  * import { parseUcum, reduce } from "@cosyte/terminology";
@@ -2192,7 +2282,13 @@ declare function reduce(node: UnitNode): Reduction;
2192
2282
  /**
2193
2283
  * Load the UCUM essence table, parsing the embedded verbatim XML on first call and caching it.
2194
2284
  *
2195
- * @returns The immutable in-memory {@link UcumEssence} model (7 base units, 24 prefixes, ~305 atoms).
2285
+ * One table is shared by every caller and by the engine itself, so it is handed out **frozen**: an
2286
+ * atom's definition cannot be rewritten in place, and neither can the `atoms` array the parser
2287
+ * scans. It used to be, and doing so before that atom's first reduction poisoned the
2288
+ * reduction memo for the life of the process — a fabricated unit equivalence that survived putting
2289
+ * the table back. A write is refused, which in strict mode means a `TypeError`.
2290
+ *
2291
+ * @returns The shared, frozen in-memory {@link UcumEssence} model.
2196
2292
  * @example
2197
2293
  * ```ts
2198
2294
  * import { loadUcumEssence } from "@cosyte/terminology";
package/dist/index.d.ts CHANGED
@@ -100,10 +100,63 @@ declare function codeableConcept(init: CodeableConcept): CodeableConcept;
100
100
  * them, so renaming or removing one is a **breaking change**, and the full set is snapshotted via
101
101
  * `sortedCodeSet` (`@cosyte/test-utils`) as a reviewable tripwire.
102
102
  *
103
- * **Value-free by construction.** A diagnostic carries a stable code plus, at most, a
104
- * code + system + version — **never** a surrounding patient identifier or clinical narrative. A
105
- * code *in patient context* can be PHI, so messages describe *structure* (a resource
106
- * path, a code system), never echo arbitrary input *values*.
103
+ * **Value-free by construction**, and the property is stated as what it actually is: every string a
104
+ * diagnostic or fatal message is assembled from is **owned by the engine**. Three kinds, and no
105
+ * fourth: a string literal; an entry in a frozen table keyed on the engine's own vocabulary; and a
106
+ * **locus this package built** — a 1-based `line`, a column count, or an index path over the
107
+ * caller's own resource made of integers and FHIR field names (`ConceptMap group[0].element[0].target[0]`,
108
+ * `compose.include[2]`). Nothing a caller configured and nothing their release or resource contained
109
+ * is ever one of them, so no such string reaches a `message`, a `stack`, or a `detail`, at any
110
+ * length.
111
+ *
112
+ * Read that as the rule, and note what it does **not** say. It does not say no factory takes a
113
+ * `string` parameter. Several do: `malformed(path, fault)` in `conceptmap/load.ts` and
114
+ * `valueset/load.ts`, and `cannotExpand(detail, path)`, `truncated(detail, path)` and
115
+ * `underPath(d, prefix)` in `valueset/` — where `expand.ts` and `validate.ts` each carry their own
116
+ * copy. It says every **argument** reaching them is engine-owned: each `fault` and `detail` is a
117
+ * literal at the call site, each `path` and `prefix` is built from indices by the loader that
118
+ * raises it. That distinction is the thing to preserve — an absolute "no factory takes a value
119
+ * parameter" reads as a stronger guarantee than the one the code makes, and those factories are the
120
+ * counter-example to it. Do not write the count down; derive it.
121
+ *
122
+ * Positional context is value-free the same way: a `line` integer, an `ExpansionDiagnostic.path`
123
+ * index path into the caller's own resource (`compose.include[2]`), or a closed-set token
124
+ * (`RXNCONSO` / `RXNREL` / `RXNSAT`) — never a URI, a column name, or a field the file supplied.
125
+ * This matters because a code *in patient context* can be PHI, and a thrown error is the one surface
126
+ * that reaches a log or an error reporter without the consumer choosing to put it there.
127
+ *
128
+ * **▶ THE CLAIM IS ABOUT THE MESSAGE FIELDS, NOT ABOUT THE OBJECTS. DO NOT SHORTEN IT TO "A
129
+ * DIAGNOSTIC IS SAFE TO LOG".** The safe strings are the *message* fields:
130
+ * `TerminologyError.message` / `.stack`, `LoadWarning.detail`, `GemLoadWarning.detail`,
131
+ * `ComplexMapLoadWarning.detail`, `RxNormLoadWarning.detail`, `ExpansionDiagnostic.detail`,
132
+ * `ParseFailure.reason` and `UcumValidation.reason`, plus the loci beside them.
133
+ *
134
+ * The **objects** these codes appear on are a different matter, and they carry verbatim text from
135
+ * two *different* provenances, which decide differently:
136
+ *
137
+ * - **Your query, echoed back** — `LookupUnknown.input`, `LookupResult.code`, `UnknownSystem.input`,
138
+ * `TranslateUnmapped.source`, `CrosswalkUnmapped.source`, `CrosswalkNoMap.source`,
139
+ * `RxNormUnknown.rxcui`, `RxNormRelated.predicates`, `NdcUnmapped.ndc`,
140
+ * `ValueSetMembership.coding`, and **`MapProvenance.sourceSystem`** — which is the `system` off
141
+ * the `Coding` you passed to `translate`, verbatim and unbounded, whenever that coding
142
+ * carries one; the map's own `group.source` is only the fallback for a coding that does not. The
143
+ * query is the one genuinely **patient-derived** input this engine takes (a code off a parsed
144
+ * message, a unit off an observation, an NDC off a prescription), so this is the class that
145
+ * decides a PHI question.
146
+ * - **Your loaded artifact, carried through** — `ComplexMapContextRequired.rules` (the map
147
+ * document's own `rule` / `advice` text) and `MapProvenance.targetSystem` / `.conceptMapUrl` /
148
+ * `.conceptMapVersion`, which are read off the loaded map (`group.target`, `map.url`,
149
+ * `map.version`). These are *reference data*, bounded by the map or release you loaded, not by
150
+ * anything a patient supplied. **`MapProvenance` straddles the two lists** — do not read the whole
151
+ * record as reference data on the strength of the fields that are.
152
+ *
153
+ * Read both lists as illustrative, not closed: **assume any result field may hold what you passed
154
+ * in.** So `JSON.stringify(lookupResult)` into a log is a PHI decision, and `String(err)` is not.
155
+ * Log the `code` and the locus; log a value only if you would log the code you passed in.
156
+ *
157
+ * Those echoes are deliberate and are not going away: they are how a never-fabricate outcome says
158
+ * *which* thing it refused to guess, they are the caller's own data returned to the caller, and
159
+ * bounding them would mean fabricating a miss.
107
160
  *
108
161
  * @packageDocumentation
109
162
  */
@@ -318,8 +371,10 @@ type FatalCode = (typeof FATAL_CODES)[keyof typeof FATAL_CODES];
318
371
  * The engine's typed error. Carries a stable {@link FatalCode} so callers branch on `err.code`
319
372
  * without string-matching the message. Thrown only for {@link FATAL_CODES} conditions.
320
373
  *
321
- * The `message` is **value-free**: it names the structural fault (a resource path, a missing
322
- * field) and never echoes input *values*.
374
+ * The `message` is **value-free**: it names the structural fault (a resource path, a missing field,
375
+ * the *role* of a configured column) and never echoes an input value — so it is safe to log, and
376
+ * safe in an `err.stack` that reaches an error reporter. The rule the strings are built by is in
377
+ * this module's docblock above.
323
378
  *
324
379
  * @example
325
380
  * ```ts
@@ -338,6 +393,9 @@ declare class TerminologyError extends Error {
338
393
  /**
339
394
  * @param code - The stable {@link FatalCode} describing the fault.
340
395
  * @param message - A **value-free** structural description (path + fault; never an input value).
396
+ * Every piece of it must be engine-owned — a literal, a frozen-table entry, or a locus this
397
+ * package built. This string reaches `err.stack`, so interpolating a caller-supplied or
398
+ * document-derived value into it is how a diagnostic becomes a leak.
341
399
  */
342
400
  constructor(code: FatalCode, message: string);
343
401
  }
@@ -611,7 +669,13 @@ interface MapProvenance {
611
669
  readonly conceptMapUrl?: string;
612
670
  /** The ConceptMap's business `version`, when declared (mappings are release-scoped). */
613
671
  readonly conceptMapVersion?: string;
614
- /** The source code system of the group that matched (or was consulted), when known. */
672
+ /**
673
+ * The source code system for this translation. **This is an echo of your query, not reference
674
+ * data off the map**: it is the `system` on the `Coding` you passed to `translate`, verbatim and
675
+ * unbounded, whenever that coding carries one; the matched (or consulted) group's own `source` is
676
+ * only the fallback for a coding that does not. Log it only if you would log the code you passed
677
+ * in.
678
+ */
615
679
  readonly sourceSystem?: string;
616
680
  /** The target code system of the group that matched, when known. */
617
681
  readonly targetSystem?: string;
@@ -1272,6 +1336,9 @@ declare function parseRrf(source: RrfSource): {
1272
1336
  * surfaced** as a `TERM_CSV_MALFORMED` warning; an unterminated final quote is surfaced too — never a
1273
1337
  * crash. A configured code/display header that is **absent from the header row** is a *fatal*
1274
1338
  * (`TERM_CODESYSTEM_MALFORMED`): the source config does not match the file, so nothing loads cleanly.
1339
+ * That fatal names the missing column's **role** (`code`, `display`, `status`, a property column)
1340
+ * from a frozen table, never the column name you configured — the message reaches `err.stack`, and a
1341
+ * column name is consumer-supplied text of unbounded length.
1275
1342
  *
1276
1343
  * @packageDocumentation
1277
1344
  */
@@ -1296,7 +1363,8 @@ declare function parseCsv(content: string): string[][];
1296
1363
  * @param source - The CSV source.
1297
1364
  * @returns The parsed concepts (by code) and the load warnings.
1298
1365
  * @throws {TerminologyError} `TERM_CODESYSTEM_MALFORMED` when the file has no header row, or a
1299
- * configured `code`/`display`/`status`/property column is absent from the header.
1366
+ * configured `code`/`display`/`status`/property column is absent from the header. The message
1367
+ * names the missing column's **role**, never the name you configured.
1300
1368
  * @example
1301
1369
  * ```ts
1302
1370
  * import { parseCsvSource } from "@cosyte/terminology";
@@ -1607,8 +1675,18 @@ interface ExpansionDiagnostic {
1607
1675
  readonly code: DiagnosticCode;
1608
1676
  /** A **value-free** structural description of the concern (never echoes a patient value). */
1609
1677
  readonly detail: string;
1610
- /** The code system / value set URI the concern is about (a published identity — not PHI). */
1611
- readonly system?: string;
1678
+ /**
1679
+ * A **value-free structural locus** into the caller's own resource — `"expansion"`, or a
1680
+ * `compose`-relative index path such as `"compose.include[2]"` or
1681
+ * `"compose.include[0].valueSet[1]"`. A diagnostic raised while expanding a *referenced* value set
1682
+ * is prefixed with the reference that reached it (`"compose.include[0].valueSet[1]/compose.include[3]"`).
1683
+ *
1684
+ * Built entirely from integers and the engine's own field names, so nothing the caller or their
1685
+ * resource supplied can reach it, at any length. It replaces the URI this field used to carry:
1686
+ * a canonical URI is unbounded consumer-supplied text on a diagnostic surface, and an index path
1687
+ * is strictly more precise anyway (two `include`s may name the same system).
1688
+ */
1689
+ readonly path?: string;
1612
1690
  }
1613
1691
  /**
1614
1692
  * The result of {@link expand}: the flattened membership plus an honest **completeness** signal.
@@ -2160,8 +2238,20 @@ declare function parseUcum(unit: string): ParseResult;
2160
2238
  * Reduce a parsed UCUM unit to its canonical {@link Reduction} — a linear scalar×dimension form, or
2161
2239
  * an opaque `special` form for expressions involving a non-linear special unit.
2162
2240
  *
2241
+ * The reduction of one atom is cached against **that atom object**, so a `UnitNode` you assembled
2242
+ * yourself is answered from its own atoms and cannot change how a unit off {@link parseUcum} reduces,
2243
+ * in this call or any later one.
2244
+ *
2163
2245
  * @param node - The AST from {@link parseUcum}.
2164
2246
  * @returns The reduced form.
2247
+ * @throws {Error} If reducing `node` reaches an atom with no linear definition, or one whose
2248
+ * definition does not parse. **No expression {@link parseUcum} accepts reaches either**; a node
2249
+ * you assemble yourself can, including by defining an atom in terms of a special unit such as
2250
+ * `Cel`, which lands on the table's own `Cel`. A third guard catches a definition that leads back
2251
+ * to itself. Naming a table atom does not reach it — a definition is resolved against the loaded
2252
+ * table, so your atom can name one but never be one — and neither does the table itself, which is
2253
+ * frozen; what reaches it is a `value` accessor that re-enters `reduce` for the same atom while
2254
+ * that atom is still being reduced. The message names the fault, never the atom.
2165
2255
  * @example
2166
2256
  * ```ts
2167
2257
  * import { parseUcum, reduce } from "@cosyte/terminology";
@@ -2192,7 +2282,13 @@ declare function reduce(node: UnitNode): Reduction;
2192
2282
  /**
2193
2283
  * Load the UCUM essence table, parsing the embedded verbatim XML on first call and caching it.
2194
2284
  *
2195
- * @returns The immutable in-memory {@link UcumEssence} model (7 base units, 24 prefixes, ~305 atoms).
2285
+ * One table is shared by every caller and by the engine itself, so it is handed out **frozen**: an
2286
+ * atom's definition cannot be rewritten in place, and neither can the `atoms` array the parser
2287
+ * scans. It used to be, and doing so before that atom's first reduction poisoned the
2288
+ * reduction memo for the life of the process — a fabricated unit equivalence that survived putting
2289
+ * the table back. A write is refused, which in strict mode means a `TypeError`.
2290
+ *
2291
+ * @returns The shared, frozen in-memory {@link UcumEssence} model.
2196
2292
  * @example
2197
2293
  * ```ts
2198
2294
  * import { loadUcumEssence } from "@cosyte/terminology";