@cosyte/terminology 0.0.6 → 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/CHANGELOG.md CHANGED
@@ -25,6 +25,67 @@ this file is maintained by hand (Changesets handles the version bump and publish
25
25
 
26
26
  ### Fixed
27
27
 
28
+ - **The UCUM unit table `loadUcumEssence()` hands out was `readonly` to TypeScript only, so a unit's
29
+ definition could be rewritten in place — and doing so fabricated a unit equivalence that outlived
30
+ putting the table back** (`TERMINOLOGY-MUTABLE-ESSENCE-TABLE`). One table is shared by every caller
31
+ and by the engine's own reducer, `essence.ts` froze nothing, and `reduce` memoizes each atom's
32
+ reduction against the atom object. So corrupting a loaded atom **before that atom's first
33
+ reduction** wrote the memo, and the reading survived restoring the table exactly as shipped.
34
+
35
+ Measured on `bf153cb`, and live on published `0.0.5` and `0.0.6` alike — `0.0.6` is that commit plus
36
+ a version bump. Defining the loaded litre as `1` with no prior
37
+ touch of `L`: `ucumEqual("L", "1")` and `ucumEqual("mg/L", "mg")` both answered `true` where the
38
+ same calls without it answered `false`, and `mmol/L` fell from `6.0221407599999985e+23 × m-3` to a
39
+ dimensionless `6.02214076e+20` — **a concentration comparing equal to a mass**. Two further routes
40
+ to the same readings on that build: writing the nested definition object in place
41
+ (`atom.value.unit = "1"`), and replacing an element of the `atoms` array, which is the one
42
+ `parseUcum` scans, so it hands the parser a forged atom without touching the memo at all.
43
+
44
+ This is the half that keying the memo on the atom **object** did not reach. That closed the route
45
+ where the atom was one you had _built_; this is the route where the atom is one you were _handed_,
46
+ and the consequence class is identical. Neither substitutes for the other.
47
+
48
+ The table is frozen when it is parsed, deeply: every atom, every atom's definition object, every
49
+ prefix, and both arrays. Shallow would have been worth nothing — freezing the atom alone leaves
50
+ `atom.value.unit`, which reproduces all three readings. `atomByCode` and `prefixByCode` refuse
51
+ `set`, `delete` and `clear`, which `Object.freeze` cannot do for a `Map`. `parseEssence` freezes
52
+ what it returns on the same rule.
53
+
54
+ **The guarantee is that the table does not change, not that a write throws.** `Object.freeze`
55
+ refuses a write by throwing in strict mode and ignoring it in sloppy mode, and this package ships a
56
+ CJS build a sloppy-mode caller can require; the table is the one that was parsed either way. The
57
+ tests assert the readings, and the `TypeError` only as how a strict-mode caller observes it.
58
+
59
+ **Two things this does not claim, both measured rather than reasoned.** `Map.prototype.set.call()`
60
+ can still force an entry into a lookup map — no `Map` can be made immutable in place — and what
61
+ that reaches is bounded and pinned in both directions: `parseUcum` resolves an atom by scanning
62
+ `atoms`, never through `atomByCode`, so a forced entry changes **no** reduction and no `ucumEqual`
63
+ answer, only what the caller's own `get` returns. And the values `parseUcum` and `reduce` hand back
64
+ are deliberately still per-call and mutable.
65
+
66
+ **No reduction changes.** 3,080 expressions built from the table — every atom bare, squared,
67
+ inverted and annotated; every prefix over rotating metric atoms; rotating ratios, products and
68
+ grouped powers; and the shapes the README and the tests name — plus 312 `ucumEqual` pairs, reduce
69
+ byte-identically on `bf153cb` and on this tree (3,393 output lines, one sha256).
70
+
71
+ One knock-on, stated because getting it wrong is easy: freezing the table removes **a** route to
72
+ `reduce`'s cyclic-definition guard, not the last one. Two routes that look like they should reach
73
+ it do not, and both are now pinned as tests — naming a table atom in a definition (it is resolved
74
+ against the loaded table, so your atom can _name_ one but never _be_ one; a self-referential `Pa`
75
+ taken off a table copy resolves to the shipped pascal and answers `1000 × g.m-1.s-2`), and
76
+ corrupting a loaded atom, which is what is now refused. The route that does reach it needs no table
77
+ at all: `UcumAtom.value` is `readonly` to TypeScript, which an **accessor** satisfies, and the
78
+ definition is read after the atom is marked in progress, so a getter can re-enter `reduce` for the
79
+ same atom. That is a supported public call, and the guard is pinned by it under a 100,000-byte atom
80
+ code — so all three of `reduce`'s messages are again asserted whole, against both `message` and
81
+ `err.stack`, with the caller's own code as the unbounded value in each.
82
+
83
+ Also here: the `/* v8 ignore next */` on `reduce`'s `atom.base` line is removed. It justified
84
+ itself with a property of the bundled table — every base atom there carries a `dim` — for a line
85
+ reachable through the exported surface with a caller-built `{ base: true }` atom that carries none.
86
+ That arm is answered on the atom's own code, is now covered by a test rather than excluded, and
87
+ changes nothing for the table's own base units.
88
+
28
89
  - **A `UnitNode` you assembled yourself could change how `reduce` and `ucumEqual` answer for a unit
29
90
  parsed out of the bundled UCUM table, for the rest of the process**
30
91
  (`TERMINOLOGY-ATOMMEMO-POISONING`). `reduce` is exported and takes a caller-built node, and
package/README.md CHANGED
@@ -208,7 +208,10 @@ _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`. Immutable by construction (every returned value is deep-frozen).
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
213
+ the reduction that `parseUcum` and `reduce` hand back are per-call values you own, and mutating one
214
+ changes nothing for anyone else.
212
215
 
213
216
  ## What is bundled
214
217
 
package/dist/index.cjs CHANGED
@@ -3733,7 +3733,29 @@ function parseEssence(xml) {
3733
3733
  atoms.sort((a, b) => b.code.length - a.code.length);
3734
3734
  const atomByCode = new Map(atoms.map((a) => [a.code, a]));
3735
3735
  const prefixByCode = new Map(prefixes.map((p) => [p.code, p]));
3736
- return { prefixes, atoms, atomByCode, prefixByCode };
3736
+ return freezeEssence({ prefixes, atoms, atomByCode, prefixByCode });
3737
+ }
3738
+ function refuseTableWrite() {
3739
+ throw new TypeError("the UCUM unit table is immutable");
3740
+ }
3741
+ function sealLookup(map) {
3742
+ return Object.defineProperties(map, {
3743
+ set: { value: refuseTableWrite },
3744
+ delete: { value: refuseTableWrite },
3745
+ clear: { value: refuseTableWrite }
3746
+ });
3747
+ }
3748
+ function freezeEssence(essence) {
3749
+ for (const atom of essence.atoms) {
3750
+ if (atom.value) Object.freeze(atom.value);
3751
+ Object.freeze(atom);
3752
+ }
3753
+ for (const prefix of essence.prefixes) Object.freeze(prefix);
3754
+ Object.freeze(essence.atoms);
3755
+ Object.freeze(essence.prefixes);
3756
+ sealLookup(essence.atomByCode);
3757
+ sealLookup(essence.prefixByCode);
3758
+ return Object.freeze(essence);
3737
3759
  }
3738
3760
  var cached;
3739
3761
  function loadUcumEssence() {
@@ -4731,7 +4753,7 @@ function approximateMatch(graph, query, options = {}) {
4731
4753
  }
4732
4754
 
4733
4755
  // src/index.ts
4734
- var VERSION = "0.0.6";
4756
+ var VERSION = "0.0.7";
4735
4757
 
4736
4758
  exports.COMPLEX_MAP_SOURCE_SYSTEM = COMPLEX_MAP_SOURCE_SYSTEM;
4737
4759
  exports.DIAGNOSTIC_CODES = DIAGNOSTIC_CODES;