@cosyte/dicom 0.0.1 → 0.0.3

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
@@ -4,9 +4,139 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ### Added
8
+
9
+ - **Em-dash brand gate in CI (`EMDASH-CONFORMANCE`).** The founder directive of 2026-07-24
10
+ (`knowledgebase/06-brand/voice-and-tone.md`, "No em dashes. Ever.") bans `U+2014` outright across
11
+ every cosyte surface and names commit messages explicitly, and the meta-repo's
12
+ `documentation/conventions.md` has stated for weeks that the rule is CI-gated. It now actually is,
13
+ here: `scripts/check-no-emdash.sh` (`pnpm check:no-emdash`) plus a dedicated
14
+ `.github/workflows/no-emdash.yml` job that scans **both** halves the rule covers, the tracked files
15
+ **and** the PR title, body, and commit messages. The workflow carries the non-default `edited`
16
+ pull-request trigger, which is load-bearing: this repo squash-merges under
17
+ `squash_merge_commit_title: COMMIT_OR_PR_TITLE` and `squash_merge_commit_message: COMMIT_MESSAGES`
18
+ (read off the repo, not assumed), so the subject that lands on `main` is the PR title, or the lone
19
+ commit's subject when the branch has exactly one, and the body is the branch commit messages.
20
+ Without `edited` a title changed after the last push would never be re-checked. The PR body does
21
+ **not** land; the gate scans it anyway, as deliberate over-strictness on a surface that costs
22
+ nothing to cover. It is a separate workflow rather than a job in `ci.yml` because that trigger
23
+ would otherwise re-run the whole Node 22 + 24 matrix on every typo fix, and because the shared
24
+ `cosyte/.github` pipeline runs no arbitrary repo script.
25
+ **This one did change content, unlike the earlier ports.** An ecosystem survey had measured
26
+ markdown only (0 of 25 `.md` files) and read dicom as already clean. Measuring all 178 tracked
27
+ files found six live em dashes in four non-markdown files, all removed here (see Changed, below).
28
+ Measure every tracked file, because that is what the scan covers.
29
+ The script is the **text-only** variant, taken from `ncpdp` (PR #34, `39212bb`) rather than from
30
+ the older `knowledgebase` copy, so it carries `ncpdp`'s two fixes to the shared shape instead of
31
+ inheriting the holes: a tracked file named `-` was read by `grep` as standard input (which `xargs`
32
+ points at `/dev/null`) and so was never opened, and `-d skip` silently passed a tracked symlink to
33
+ a directory. Both are re-proved here rather than taken on faith: with the `./` path prefix removed
34
+ the gate prints OK and exits 0 over a live em dash in a file named `-`, and with `-d skip` restored
35
+ it goes green over an unread symlink.
36
+ It deliberately omits `grep -I`, and in this repo that is the most load-bearing flag choice in the
37
+ file. `src/dataset/vr/charset.ts` holds a functional NUL byte inside `/[\x00 ]+$/u`, the regex that
38
+ strips DICOM's own padding, so grep classifies it binary. It carries no em dash today and scans
39
+ green, which is the whole answer to the belief that this port was blocked: the red would come from
40
+ a match, never from the NUL. **If it ever gains one, the gate reds.** Measured with GNU grep 3.8:
41
+ grep exits 0, writes nothing to stdout, and writes `binary file matches` to stderr, so the hit
42
+ never reaches the hit list and the stderr capture fails the run instead. With `-I` added, the same
43
+ edit goes green, which is why `-I` stays out. The failure message names that case explicitly rather
44
+ than blaming an I/O error that did not happen.
45
+ A gate that prints OK when it did not read its input is worse than no gate, so nine routes by which
46
+ a dead or blind scan could still report green are each checked red, not assumed: a corrupt git
47
+ index, an unreadable tracked file, a tracked file named `-q`, a C-quoted non-ASCII path, a
48
+ mis-encoded text file, an empty tracked-file list, a tracked file named `-`, a tracked symlink to a
49
+ directory, and the NUL-bearing source file gaining an em dash.
50
+ Known limits are documented in the script header and inherited knowingly from the shared shape
51
+ rather than patched in this copy alone: encoded-form matching is literal (so `&#x2014` without the
52
+ semicolon, and lowercase `%e2%80%94`, pass), stderr capture binds to the scanning `grep` rather
53
+ than to the filters ahead of it, an em dash encoded in a non-UTF-8 charset is not matched (a live
54
+ possibility here, given `(0008,0005)` Specific Character Set fixtures), and the scan reads file
55
+ **contents** only, so a tracked path that itself carries an em dash passes. Tooling only: no
56
+ runtime, public-API, or parse-behavior change.
57
+ - **`docs-content/` now covers the full canonical Diátaxis spine** (`DOCS-CONTENT-P6`). Beyond the
58
+ existing Overview (`intro`), the sidebar gains **Installation** and **Quickstart** (tutorials),
59
+ five **Core Concepts** notes (the object model, the tolerance/warning model, the typed value
60
+ layer, the safety-critical views, and the source-profile system), a **Guides** cookbook (four
61
+ recipes: re-serialize, de-identify, read raw pixel data, triage warnings), and a
62
+ **Troubleshooting & known limitations** reference. Every documented capability is grounded in the
63
+ package's actually-shipped surface; the metadata-first boundary is stated explicitly, with the
64
+ permanent non-goals named in Troubleshooting (**no pixel decode, no DIMSE networking, no
65
+ DICOMweb**, no pixel-level de-identification).
66
+ - **Doc/code-agreement gate (`test/docs-content.test.ts`).** Every ` ```ts runnable ` snippet in
67
+ `docs-content/` is extracted, compiled, and executed against the **built** package via
68
+ `docSnippetSuite()` from `@cosyte/vitest-config/snippets`, with its inline `// =>` assertions
69
+ checked, so a documented example can never silently drift from the code. All examples use
70
+ synthetic, base64-encoded Part 10 objects (invented patient, fake UIDs); no real PHI, no `.dcm`
71
+ file on disk.
72
+
73
+ ### Changed
74
+
75
+ - **Removed the six em dashes the new gate found (`EMDASH-CONFORMANCE`).** Four tracked files, none
76
+ of them markdown: `.github/CODEOWNERS` (2), `.github/workflows/release.yml` (2),
77
+ `vendor/nema/SHA.txt` (1), and the npm `description` in `package.json` (1). The last is the only
78
+ one visible outside the repo: the package description on npm and on the GitHub sidebar now reads
79
+ "Developer-focused DICOM Part 10 parser + utility library for Node.js and TypeScript:
80
+ metadata-first, vendor-quirky-tolerant, dual ESM/CJS." Each rewrite replaces the character with a
81
+ period, colon, or comma, per the rule's own instruction; none re-encodes it, and no wording beyond
82
+ the punctuation changed.
83
+ - **Bumped the `@cosyte/vitest-config` devDependency to `^0.0.2`** to pick up the `./snippets`
84
+ export that ships the doc/code-agreement runner.
85
+ - **Corrected the element-access examples in `intro.md`.** `Dataset.get` / `has` take the
86
+ 8-character `(group,element)` **tag** form only. The prior snippets showed `get("PatientName")`,
87
+ `get("(0010,0010)")`, and `get("StudyDate")`, all of which return `undefined`. They now use the
88
+ tag form (and show resolving a keyword to its tag via `Dictionary.byKeyword`), matching the code.
89
+
90
+ - **All tests now live in a top-level `test/` mirroring `src/`.** `@cosyte/dicom` was the lone parser
91
+ that co-located `*.test.ts` files beside their source; the 32 co-located suites were moved to
92
+ `test/<path>/` preserving the `src/` sub-structure, bringing the repo in line with the
93
+ `hl7`/`mllp`/`x12`/`ccda`/`ncpdp` archetype (`DICOM-TEST-RELOCATE`). Relative imports in the moved
94
+ files were retargeted (`./foo.js` → `../../src/<path>/foo.js`). `vitest.config.ts` drops the
95
+ `src/**/*.test.ts` include glob, so the config now matches the archetype and a stray co-located
96
+ test is no longer silently collected alongside the `test/` suite. Test-only, no public-surface
97
+ change: same 585 passing tests (+1 todo), coverage gates (still pointed at the `src/` source
98
+ directories) unchanged and green.
99
+
7
100
  ### Fixed
8
101
 
9
- - **`private: true` removed — `@cosyte/dicom` can publish.** The flag dated to the very first
102
+ - **`Dictionary Regen` is green again (`DICOM-DICT-REGEN`).** The byte-identical regen gate had been
103
+ red since 2026-06-24: seven consecutive failing runs, four of them pushes to `main`, every one of
104
+ them failing in its first real step. The last green run was 2026-05-04.
105
+ `.github/workflows/dictionary-regen.yml` passed `version: 10` to `pnpm/action-setup@v6` while
106
+ `package.json` already declares `packageManager: pnpm@10.0.0`, and v6 treats two sources of truth as
107
+ a hard error: "Multiple versions of pnpm specified". The job died in about 13 seconds having never
108
+ installed anything, never run `pnpm gen:all`, and never compared a single generated byte. Dropping
109
+ the redundant `version:` input lets `packageManager` win, matching the fix `astm` already applied to
110
+ its fuzz workflow (`44c3d88`).
111
+ Two smaller repairs to the same file, both aimed at the gate actually asserting something rather
112
+ than merely starting. The workflow now lists **itself** in its `paths` filters, so a change to the
113
+ gate re-runs the gate; without that, the PR fixing a broken gate could not demonstrate the gate
114
+ fixed. And the verify step now also fails on **untracked** files under `src/dictionary/generated/`:
115
+ `git diff` reports modified tracked files only, so a generator that began emitting a brand-new
116
+ artifact would have left it untracked and invisible to the check.
117
+ The committed dictionary itself is **not** drifted. Regenerating from the pinned inputs
118
+ (`vendor/innolitics/90571bc`) reproduces all four artifacts byte for byte: 5,129 tags, 5,035
119
+ keywords, 268 UIDs, 617 Annex E entries, zero diff. Verified as a positive control rather than
120
+ assumed: perturbing a VR in the vendor input made the gate red, and restoring it made the gate
121
+ green, so the check reads its input. Nothing in the tag tables needed correcting, and nothing
122
+ regenerated was committed.
123
+
124
+ - **Corrected the `private: true` claim in `.github/workflows/release.yml`.** A comment at the top of
125
+ the release caller asserted that `@cosyte/dicom` is `private: true` and that `changeset publish` is
126
+ therefore "a no-op until that flag is removed". `package.json` carries no `private` field, the repo
127
+ is public, and the package ships on npm at `0.0.1`, so the comment described the release pipeline as
128
+ inert when it is live. Documentation only, no behavior change, but a misleading note on a workflow
129
+ that really does publish is worth more than a stale one elsewhere.
130
+
131
+ - **Corrected stale publish-status language in `docs-content/` (`README-ORG-SWEEP`).**
132
+ `installation.md` claimed the package was "not yet published to npm" and that the install command
133
+ was only "the shape it will take at first publish"; `troubleshooting.md` listed a "Not yet
134
+ published … not on npm; the first provenance publish is gated on the coordinated public launch"
135
+ non-goal. `@cosyte/dicom` is published on npm at `0.0.1` and public. Both now state the truth
136
+ (published, public, still pre-alpha on the `0.0.x`-until-first-alpha ladder), and the troubleshooting
137
+ bullet becomes an honest pin-your-version pre-alpha caveat rather than a stale non-goal.
138
+
139
+ - **`private: true` removed: `@cosyte/dicom` can publish.** The flag dated to the very first
10
140
  scaffold commit and was never explained; `changeset publish` silently skips a private package, so
11
141
  this repo was the one parser that could not reach npm even once its pipeline worked. It also
12
142
  contradicted the `publishConfig: { access: "public" }` in the same file. Removed as part of the
@@ -18,25 +148,25 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
18
148
  reference, retargeted at `src/version.ts`) and the `version` script that runs it after
19
149
  `changeset version`, so the bump and the `VERSION` constant land in the same commit.
20
150
  - **`VERSION` is no longer typed as a string literal.** It was declared `export const VERSION =
21
- "0.0.0"`, giving it the literal type `"0.0.0"` — so the exported type would change on every
151
+ "0.0.0"`, giving it the literal type `"0.0.0"`, so the exported type would change on every
22
152
  release, making each version bump a breaking type change. Now annotated `: string`, matching the
23
153
  `hl7` reference. Type-only; the runtime value is unchanged. Done now because the package is
24
- unpublished — after the first publish this would itself be a breaking change.
154
+ unpublished. After the first publish this would itself be a breaking change.
25
155
 
26
156
  - **The Release workflow can actually start.** `.github/workflows/release.yml` calls the shared
27
157
  `cosyte/.github` pipeline, which requests `contents`/`id-token`/`pull-requests: write`, but declared
28
- no `permissions:` of its own — so it inherited the repo default of `contents: read`. A called
158
+ no `permissions:` of its own, so it inherited the repo default of `contents: read`. A called
29
159
  workflow may only downgrade the caller's `GITHUB_TOKEN`, never escalate it, so GitHub rejected the
30
160
  workflow at startup (~1s, no jobs, no logs). Every Release run from June 2026 until now failed this
31
161
  way, unnoticed, because a `startup_failure` produces no logs to read. The caller job now declares
32
- the three scopes explicitly. CI-only — no runtime or API change.
162
+ the three scopes explicitly. CI-only: no runtime or API change.
33
163
 
34
164
  ### Security
35
165
 
36
- - **Dev-dependency advisory remediation (no runtime impact — the published
166
+ - **Dev-dependency advisory remediation (no runtime impact: the published
37
167
  artifact is unchanged).** Added scoped `pnpm.overrides` pinning two
38
168
  transitive **dev/build-time** packages to their patched releases: `esbuild`
39
- (`>=0.27.3 <0.28.1` → `0.28.1`; GHSA dev-server path-traversal — not
169
+ (`>=0.27.3 <0.28.1` → `0.28.1`; GHSA dev-server path-traversal, not
40
170
  reachable here: the library builds via `tsup`/`vitest` and never runs
41
171
  `esbuild serve`) and the `@changesets/parse` copy of `js-yaml`
42
172
  (`>=4.0.0 <4.2.0` → `4.2.0`; GHSA-h67p-54hq-rp68 merge-key DoS). The
@@ -49,19 +179,25 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
49
179
 
50
180
  ### Added
51
181
 
182
+ - **Trademark notice (`TRADEMARKS.md`).** This package names third-party systems to describe what it
183
+ interoperates with; the notice records that cosyte is not affiliated with, endorsed by, or
184
+ sponsored by any of them, that every reference is descriptive, and that the built-in profiles are
185
+ authored from public sources only. Added to `files` so it ships inside the published tarball, not
186
+ just on GitHub. Documentation only: no runtime or API change.
187
+
52
188
  - **Lossless File Meta round-trip.** The parser now retains non-modeled
53
- `(0002,xxxx)` File Meta elements — e.g. `(0002,0017)`/`(0002,0018)`
189
+ `(0002,xxxx)` File Meta elements (e.g. `(0002,0017)`/`(0002,0018)`
54
190
  Sending/Receiving AE Title, `(0002,0100)` Private Information Creator UID,
55
- `(0002,0102)` Private Information — as raw on-wire bytes on the new
191
+ `(0002,0102)` Private Information) as raw on-wire bytes on the new
56
192
  `FileMeta.extraElements` view (each a `FileMetaRawElement` carrying the tag,
57
193
  its Explicit-VR-LE VR, and a defensively copied even-length value). The
58
194
  serializer merges these with the typed fields and emits the whole group in
59
195
  ascending tag order (PS3.5 §7.4) with a recomputed `(0002,0000)` group length
60
- (PS3.10 §7.1), so an exotic File Meta group now round-trips byte-for-byte —
196
+ (PS3.10 §7.1), so an exotic File Meta group now round-trips byte-for-byte,
61
197
  not just the typed fields. New exported type: `FileMetaRawElement`. This
62
198
  resolves the Phase 5 serializer known-limitation ("only the typed `FileMeta`
63
199
  fields round-trip").
64
- - **Documentation completeness (Phase 8).** Rewrote `README.md` into a full developer guide — quickstart,
200
+ - **Documentation completeness (Phase 8).** Rewrote `README.md` into a full developer guide: quickstart,
65
201
  feature tour, a "DICOM in 90 seconds" primer, the two access patterns, an 80/20 **cookbook** (index a
66
202
  folder, build routing keys, read pixel-interpretation metadata safely, de-identify, bridge to FHIR
67
203
  `ImagingStudy` / HL7 v2, round-trip serialize), the four-tier tolerance model, the warning/fatal code
@@ -69,13 +205,13 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
69
205
  export now carries a JSDoc `@example`. Extended the dual ESM/CJS smoke harnesses to exercise the full
70
206
  Phase 1–7 published surface so the documented entrypoints are guaranteed importable from both module
71
207
  systems. Also corrected an `intro.md` snippet that referenced a nonexistent `ds.pixelData` getter (the
72
- real accessor is `ds.get("PixelData")?.value`). Docs-only — no runtime API change.
208
+ real accessor is `ds.get("PixelData")?.value`). Docs-only: no runtime API change.
73
209
  - **Metadata-level de-identification (Phase 7).** New `deidentify(ds, options?)` applies the PS3.15
74
210
  Annex E **Basic Application Level Confidentiality Profile** plus the nine metadata-affecting Annex E
75
211
  Options (`RetainUIDs`, `RetainLongitudinalTemporal`, `RetainPatientCharacteristics`,
76
212
  `RetainDeviceIdentity`, `RetainInstitutionIdentity`, `RetainSafePrivate`, `CleanDescriptors`,
77
213
  `CleanStructuredContent`, `CleanGraphics`), driven by the generated Table E.1-1 action map. It is a
78
- **pure** function — the input `Dataset` is never mutated; it returns a fresh de-identified `Dataset`
214
+ **pure** function: the input `Dataset` is never mutated; it returns a fresh de-identified `Dataset`
79
215
  and a value-free `DeidentifyReport` (tags, keywords, resolved action codes, the UID map, warnings).
80
216
  Each attribute's action (`D` dummy, `Z` zero-length, `X` remove, `K` keep, `C` clean, `U` consistent
81
217
  UID) is resolved from the Basic Profile, overridden by any active Option; conditional codes (`Z/D`,
@@ -101,31 +237,31 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
101
237
  `escalate` (Tier-2 warning codes promoted to a thrown `DicomParseError`), `suppress` (codes silenced
102
238
  as a documented benign quirk of the source), and `privateTags` (a private-creator-keyed overlay that
103
239
  resolves the Implicit VR of vendor private data elements). Private resolution is keyed on the file's
104
- **live** private-creator string and the canonical `"GGGGxxLL"` key (PS3.5 §7.8.1) — never a
105
- hard-coded block number — so the same vendor schema resolves regardless of which block it landed in.
240
+ **live** private-creator string and the canonical `"GGGGxxLL"` key (PS3.5 §7.8.1), never a
241
+ hard-coded block number, so the same vendor schema resolves regardless of which block it landed in.
106
242
  Profiles compose via `extends` (de-duplicated lineage, union of escalations/suppressions, child-wins
107
243
  dictionary merge) and expose a deterministic `describe()` summary. Five built-ins ship under the
108
244
  frozen `profiles` namespace: three vendor overlays (`ge`, `siemens`, `philips`, grounded in the
109
245
  public GDCM / dcm4che / dcm2niix private dictionaries) and two posture presets (`strict` escalates
110
246
  integrity-relevant warnings; `lenient` suppresses cosmetic, high-volume ones). A creator the active
111
247
  profile does not recognize degrades to generic `UN` plus the new `DICOM_PRIVATE_CREATOR_UNKNOWN`
112
- warning — never a wrong decode. Selecting a profile never changes a correct decode. New public
248
+ warning, never a wrong decode. Selecting a profile never changes a correct decode. New public
113
249
  exports: `defineProfile`, `profiles`, `ProfileDefinitionError`, and the types `Profile`,
114
250
  `PrivateTagDefinition`, `DefineProfileOptions`, `ProfilePrivateTags`; `ParseOptions` gains an
115
251
  optional `profile` field. The reserved `DICOM_PRIVATE_CREATOR_UNKNOWN` code is now actively emitted
116
252
  (no change to the `WARNING_CODES` registry surface).
117
253
  - **Spec-clean Part 10 serializer (Phase 5).** New `serializeDicom(ds)` writes a `Dataset` back to a
118
- DICOM Part 10 `Buffer` — the conservative half of Postel's Law. Emits the 128-byte zero preamble +
254
+ DICOM Part 10 `Buffer`, the conservative half of Postel's Law. Emits the 128-byte zero preamble +
119
255
  `DICM`, a File Meta group (always Explicit VR LE) with a computed `(0002,0000)` group length and
120
256
  conservative Type-1 defaults (File Meta Version `0x0001`, cosyte Implementation Class UID under the
121
- `2.25` UUID arc), then the dataset body in the dataset's own transfer syntax — **no transcode** —
257
+ `2.25` UUID arc), then the dataset body in the dataset's own transfer syntax (**no transcode**)
122
258
  across all four v1 syntaxes (Implicit VR LE, Explicit VR LE/BE, Deflated Explicit VR LE). Scalar
123
259
  values are padded to even length per PS3.5 §6.2 (`0x00` for `UI`/byte-stream VRs, `0x20` for text),
124
260
  short vs long-form headers are chosen by VR per §7.1.2 (`SV`/`UV` long-form), retired `(gggg,0000)`
125
261
  group-length elements are omitted per §7.2, and sequence + encapsulated-pixel-data spans pass
126
- through byte-for-byte per §7.5 / §A.4. Pure function — the input `Dataset` is never mutated.
262
+ through byte-for-byte per §7.5 / §A.4. Pure function: the input `Dataset` is never mutated.
127
263
  - **Serializer error taxonomy.** New `DicomSerializeError` with codes `MISSING_TRANSFER_SYNTAX`
128
- (no File Meta Transfer Syntax UID) and `UNSUPPORTED_TRANSFER_SYNTAX` (a UID outside the v1 set) —
264
+ (no File Meta Transfer Syntax UID) and `UNSUPPORTED_TRANSFER_SYNTAX` (a UID outside the v1 set),
129
265
  separate from the parser's fatal codes and the value layer's `DicomValueError`. The message is
130
266
  built only from the code + the offending Transfer Syntax UID (structural facts), never a decoded
131
267
  value, so it is always safe to log. New public exports: `serializeDicom`, `DicomSerializeError`,
@@ -137,7 +273,7 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
137
273
  fields round-trip; any other `(0002,xxxx)` element a source file carried (e.g. `(0002,0100)` Private
138
274
  Information Creator UID) is dropped at _parse_ time (the Phase 2 `FileMeta` view does not model it)
139
275
  and so cannot be re-emitted. The preamble is normalized to zeros and odd-length values are padded
140
- even — the output stays spec-clean but is not a byte-identical copy of a non-conformant input.
276
+ even. The output stays spec-clean but is not a byte-identical copy of a non-conformant input.
141
277
 
142
278
  ### Tests
143
279
 
@@ -158,7 +294,7 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
158
294
  tuple plus Other Patient IDs (so a caller never matches on a bare, non-unique `(0010,0020)`) and
159
295
  keeps `PN` structured; `study`/`series` surface the cross-system UIDs, accession number, modality
160
296
  and Frame of Reference UID. `image` surfaces the pixel-interpretation + geometry metadata a
161
- renderer needs — with the safety-critical omissions intact: `rescaleSlope` is **absent** (not `1`)
297
+ renderer needs, with the safety-critical omissions intact: `rescaleSlope` is **absent** (not `1`)
162
298
  when the tag is absent, `signed` is absent (never guessed) unless `(0028,0103)` was present,
163
299
  `photometricInterpretation` is never defaulted to `MONOCHROME2`, and the three pixel-spacing tags
164
300
  (`(0028,0030)` / `(0018,1164)` / `(0018,2010)`) are distinct, never aliased.
@@ -166,7 +302,7 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
166
302
  Per-Frame-else-Shared (PS3.3 §C.7.6.16): Pixel Measures, Plane Position, Plane Orientation, Pixel
167
303
  Value Transformation, Frame VOI LUT. `image.isEnhancedMultiFrame` flags such objects.
168
304
  - **Value-layer error taxonomy.** New `DicomValueError` (codes `FRAME_INDEX_OUT_OF_RANGE`,
169
- `MISSING_REQUIRED_FUNCTIONAL_GROUP`) — separate from the parser's four fatal codes. The helpers are
305
+ `MISSING_REQUIRED_FUNCTIONAL_GROUP`), separate from the parser's four fatal codes. The helpers are
170
306
  otherwise fail-safe (typed-absent for missing data) and throw only for a structural contract
171
307
  violation; the error message carries only structural facts (indices, tag/macro names), never a
172
308
  decoded PHI value.
@@ -179,7 +315,7 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
179
315
  `CodedConcept`, `RealWorldValueMap`, `FrameFunctionalGroups`, `ValueErrorCode`, plus
180
316
  `VALUE_ERROR_CODES` and the `readCode` / `codingSchemeOid` / `CODING_SCHEME_OIDS` helpers.
181
317
  - **VR value decode + dataset navigation (Phase 3).** `Element.value` now lazily decodes (and
182
- memoizes) an element's raw bytes into a typed, discriminated `DicomValue` covering all 34 VRs —
318
+ memoizes) an element's raw bytes into a typed, discriminated `DicomValue` covering all 34 VRs:
183
319
  numbers (`US/UL/SS/SL/FL/FD`), 64-bit `bigint`s (`SV/UV`), attribute tags (`AT`), person names
184
320
  (`PN` → 3-group / 5-component), strings, free text, numeric strings (`DS/IS` → `number | null`,
185
321
  never `NaN`→0), temporal values (`DA/TM/DT`), sequences (`SQ` → threaded items), and raw `binary`
@@ -203,7 +339,7 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
203
339
  (`_arbitraries.ts`) plus invariant suites for round-trip fidelity, lenient-mode robustness,
204
340
  parsed-model immutability, warning/fatal-code stability (snapshot), and a byte-parser fuzz sweep
205
341
  that feeds arbitrary buffers + random truncations and asserts the parser only ever throws a
206
- sanctioned Tier-3 `DicomParseError` — never an unexpected error, hang, or OOM. No public API
342
+ sanctioned Tier-3 `DicomParseError`, never an unexpected error, hang, or OOM. No public API
207
343
  change. (devDeps: `@cosyte/test-utils@^0.0.1`, `fast-check@3.23.2`.)
208
344
 
209
345
  ### Changed
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # @cosyte/dicom
2
2
 
3
- > Read a real-world, vendor-quirky DICOM Part 10 file and pull the metadata you need in one line — without having read the DICOM standard.
3
+ > Read a real-world, vendor-quirky DICOM Part 10 file and pull the metadata you need in one line, without having read the DICOM standard.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@cosyte/dicom.svg)](https://www.npmjs.com/package/@cosyte/dicom)
6
6
  [![CI](https://img.shields.io/github/actions/workflow/status/cosyte/dicom/ci.yml?branch=main&label=CI)](https://github.com/cosyte/dicom/actions/workflows/ci.yml)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
8
8
  [![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg)](https://nodejs.org)
9
9
 
10
- A developer-focused DICOM Part 10 parser and utility library for Node.js and TypeScript. **Metadata-first**: it reads the headers — patient, study, series, image, codes, UIDs — leniently and fast, exposes pixel data as raw bytes, and **never decodes pixels**. Sibling to [`@cosyte/hl7`](https://github.com/cosyte/hl7); same engineering bar.
10
+ A developer-focused DICOM Part 10 parser and utility library for Node.js and TypeScript. **Metadata-first**: it reads the headers (patient, study, series, image, codes, UIDs) leniently and fast, exposes pixel data as raw bytes, and **never decodes pixels**. Sibling to [`@cosyte/hl7`](https://github.com/cosyte/hl7); same engineering bar.
11
11
 
12
12
  ---
13
13
 
@@ -16,7 +16,7 @@ A developer-focused DICOM Part 10 parser and utility library for Node.js and Typ
16
16
  Useful output after install + parse. No DICOM spec knowledge required.
17
17
 
18
18
  ```bash
19
- # pnpm (recommended) — also works with: npm install @cosyte/dicom | yarn add @cosyte/dicom
19
+ # pnpm (recommended). Also works with: npm install @cosyte/dicom | yarn add @cosyte/dicom
20
20
  pnpm add @cosyte/dicom
21
21
  ```
22
22
 
@@ -26,25 +26,25 @@ import { parseDicom } from "@cosyte/dicom";
26
26
 
27
27
  const ds = parseDicom(await readFile("study.dcm"));
28
28
 
29
- ds.get("PatientName")?.value; // PN value — structured, never flattened
30
- ds.study.instanceUid; // "1.2.840.…" — the global study anchor
31
- ds.image.rescaleSlope; // number | undefined — undefined means "absent", never 1
29
+ ds.get("PatientName")?.value; // PN value: structured, never flattened
30
+ ds.study.instanceUid; // "1.2.840.…": the global study anchor
31
+ ds.image.rescaleSlope; // number | undefined: undefined means "absent", never 1
32
32
  ```
33
33
 
34
- That's the pitch: no config, no schema upload, no spec lookup. The parser accepts vendor-quirky input by default — it tolerates a missing preamble, a wrong File Meta group length, odd-length values, and the dozen-or-so deviations real scanners emit — and records each as a stable warning code rather than failing. You reach for typed value decode, safety-critical views, profiles, or the serializer when you want them.
34
+ That's the pitch: no config, no schema upload, no spec lookup. The parser accepts vendor-quirky input by default (it tolerates a missing preamble, a wrong File Meta group length, odd-length values, and the dozen-or-so deviations real scanners emit) and records each as a stable warning code rather than failing. You reach for typed value decode, safety-critical views, profiles, or the serializer when you want them.
35
35
 
36
36
  ---
37
37
 
38
38
  ## Features
39
39
 
40
- - **One-line metadata extraction** — `ds.patient`, `ds.study`, `ds.series`, `ds.image`: typed, fail-safe views over the safety-critical attributes. No `(group,element)` tags to memorise.
41
- - **Two access patterns** — named views, or structural `ds.get("PatientName")` / `ds.get("(0010,0010)")` by keyword or tag, plus `ds.elements()` to walk everything.
42
- - **Lazy typed value decode** — `element.value` decodes raw bytes into a discriminated `DicomValue` across all 34 VRs (numbers, `bigint`s, person names, dates/times, sequences, raw `binary`), honoring `(0008,0005)` Specific Character Set through nested items.
43
- - **Real-world tolerance, Postel's Law** — a lenient reader emits 24 stable warning codes for what it tolerated; only 4 truly-structural conditions are fatal. The serializer always writes spec-clean Part 10.
44
- - **Source/vendor profile system** — `defineProfile()` + 5 built-ins (`ge`, `siemens`, `philips`, `strict`, `lenient`) that only ever _tighten or annotate_ a parse, resolving vendor private tags by the file's live Private Creator string — never a wrong decode.
45
- - **Metadata-level de-identification** — `deidentify()` applies the PS3.15 Annex E Basic Profile + the nine metadata Options, returning a fresh dataset and a value-free audit report.
46
- - **Spec-clean serializer** — `serializeDicom(ds)` round-trips a dataset back to Part 10 bytes in its source transfer syntax (no transcode), with correct File Meta group length, even-length padding, byte-exact sequence passthrough, and lossless File Meta — non-modeled `(0002,xxxx)` elements are preserved and re-emitted in tag order.
47
- - **Strict TypeScript, dual ESM + CJS, Node ≥ 22** — `noUncheckedIndexedAccess`, no `any`, JSDoc + `@example` on every public export feeding your editor's IntelliSense. Zero runtime dependencies today.
40
+ - **One-line metadata extraction**: `ds.patient`, `ds.study`, `ds.series`, `ds.image`: typed, fail-safe views over the safety-critical attributes. No `(group,element)` tags to memorise.
41
+ - **Two access patterns**: named views, or structural `ds.get("PatientName")` / `ds.get("(0010,0010)")` by keyword or tag, plus `ds.elements()` to walk everything.
42
+ - **Lazy typed value decode**: `element.value` decodes raw bytes into a discriminated `DicomValue` across all 34 VRs (numbers, `bigint`s, person names, dates/times, sequences, raw `binary`), honoring `(0008,0005)` Specific Character Set through nested items.
43
+ - **Real-world tolerance, Postel's Law**: a lenient reader emits 24 stable warning codes for what it tolerated; only 4 truly-structural conditions are fatal. The serializer always writes spec-clean Part 10.
44
+ - **Source/vendor profile system**: `defineProfile()` + 5 built-ins (`ge`, `siemens`, `philips`, `strict`, `lenient`) that only ever _tighten or annotate_ a parse, resolving vendor private tags by the file's live Private Creator string, never a wrong decode.
45
+ - **Metadata-level de-identification**: `deidentify()` applies the PS3.15 Annex E Basic Profile + the nine metadata Options, returning a fresh dataset and a value-free audit report.
46
+ - **Spec-clean serializer**: `serializeDicom(ds)` round-trips a dataset back to Part 10 bytes in its source transfer syntax (no transcode), with correct File Meta group length, even-length padding, byte-exact sequence passthrough, and lossless File Meta: non-modeled `(0002,xxxx)` elements are preserved and re-emitted in tag order.
47
+ - **Strict TypeScript, dual ESM + CJS, Node ≥ 22**: `noUncheckedIndexedAccess`, no `any`, JSDoc + `@example` on every public export feeding your editor's IntelliSense. Zero runtime dependencies today.
48
48
 
49
49
  ---
50
50
 
@@ -52,18 +52,18 @@ That's the pitch: no config, no schema upload, no spec lookup. The parser accept
52
52
 
53
53
  A DICOM Part 10 file is a 128-byte preamble + the `DICM` magic, then a **File Meta** group (always Explicit VR Little Endian) naming the **transfer syntax**, then the **dataset**: a flat, tag-ordered stream of **data elements**.
54
54
 
55
- Each element is identified by a `(group,element)` **tag** (e.g. `(0010,0010)` = Patient's Name) and carries a two-letter **VR** (Value Representation — `PN`, `DA`, `US`, `SQ`, …) that says how to decode its bytes. Some elements are **sequences** (`SQ`): ordered lists of **items**, each a nested dataset. The transfer syntax decides endianness, whether VRs are written explicitly, and whether the stream is deflated.
55
+ Each element is identified by a `(group,element)` **tag** (e.g. `(0010,0010)` = Patient's Name) and carries a two-letter **VR** (Value Representation: `PN`, `DA`, `US`, `SQ`, …) that says how to decode its bytes. Some elements are **sequences** (`SQ`): ordered lists of **items**, each a nested dataset. The transfer syntax decides endianness, whether VRs are written explicitly, and whether the stream is deflated.
56
56
 
57
57
  ```
58
58
  DICOM file
59
59
  ├── preamble (128 bytes) + "DICM"
60
- ├── File Meta group (0002,xxxx) — transfer syntax UID, SOP Class/Instance UID
60
+ ├── File Meta group (0002,xxxx) : transfer syntax UID, SOP Class/Instance UID
61
61
  └── dataset
62
62
  ├── (0008,0060) Modality "CT"
63
63
  ├── (0010,0010) PatientName PN "Doe^Jane"
64
64
  ├── (0020,000D) StudyInstanceUID UI "1.2.840.…"
65
65
  ├── (0028,0100) BitsAllocated US 16
66
- └── (7FE0,0010) PixelData OW «raw bytes — not decoded»
66
+ └── (7FE0,0010) PixelData OW «raw bytes, not decoded»
67
67
  ```
68
68
 
69
69
  `@cosyte/dicom` reads all of that leniently and hands you typed accessors over it. The one thing it deliberately does **not** do is decode the pixels.
@@ -74,11 +74,11 @@ DICOM file
74
74
 
75
75
  ### Safety-critical views
76
76
 
77
- The four views — `patient`, `study`, `series`, `image` — pull the right field out of the right tag for the jobs that matter most, and they are **fail-safe**: a missing value is typed-absent (`undefined`), never a substituted default.
77
+ The four views (`patient`, `study`, `series`, `image`) pull the right field out of the right tag for the jobs that matter most, and they are **fail-safe**: a missing value is typed-absent (`undefined`), never a substituted default.
78
78
 
79
79
  ```ts
80
80
  const p = ds.patient;
81
- p.id; // "MRN-42" — NOT globally unique on its own…
81
+ p.id; // "MRN-42": NOT globally unique on its own…
82
82
  p.issuerOfId; // …pair with the issuer for cross-system matching
83
83
  p.name?.alphabetic.familyName; // structured PN, never flattened
84
84
 
@@ -95,7 +95,7 @@ s.accessionNumber; // ties the study to the HIS order (0008,0050)
95
95
  ds.get("Modality"); // by keyword
96
96
  ds.get("(0008,0060)"); // same element, by tag
97
97
  ds.has("PixelData"); // boolean
98
- ds.elements(); // readonly Element[] — walk everything
98
+ ds.elements(); // readonly Element[]: walk everything
99
99
  ```
100
100
 
101
101
  ### Typed values
@@ -120,7 +120,7 @@ Recipes for the jobs a metadata parser is actually asked to do. Every attribute
120
120
 
121
121
  ### Index a folder of studies
122
122
 
123
- Pull a few fields out of each file to build a searchable index — the bread-and-butter PACS/archive job.
123
+ Pull a few fields out of each file to build a searchable index: the bread-and-butter PACS/archive job.
124
124
 
125
125
  ```ts
126
126
  import { readFile } from "node:fs/promises";
@@ -145,18 +145,18 @@ Nothing here throws on a quirky file; absent fields come back `undefined`. Check
145
145
 
146
146
  ### Build routing keys
147
147
 
148
- Routing and reconciliation hang off a small set of identifiers. Surface them correctly — a Patient ID without its issuer is ambiguous across systems (PS3.3 C.7.1.1).
148
+ Routing and reconciliation hang off a small set of identifiers. Surface them correctly: a Patient ID without its issuer is ambiguous across systems (PS3.3 C.7.1.1).
149
149
 
150
150
  ```ts
151
151
  // Hierarchy keys for filing into Study → Series → Instance:
152
- const studyKey = ds.study.instanceUid; // (0020,000D) — global anchor
152
+ const studyKey = ds.study.instanceUid; // (0020,000D): global anchor
153
153
  const seriesKey = ds.series.instanceUid; // (0020,000E)
154
154
  const instanceKey = ds.image.sopInstanceUid; // (0008,0018)
155
155
 
156
- // Cross-system patient key — id ALONE is not unique; pair it with the issuer:
156
+ // Cross-system patient key: id ALONE is not unique; pair it with the issuer:
157
157
  const p = ds.patient;
158
158
  const patientKey = `${p.issuerOfId ?? "?"}|${p.id ?? "?"}`;
159
- p.otherIds; // (0010,1002) Other Patient IDs Sequence — additional {id, issuer} pairs
159
+ p.otherIds; // (0010,1002) Other Patient IDs Sequence: additional {id, issuer} pairs
160
160
  ```
161
161
 
162
162
  ### Read pixel-interpretation metadata safely
@@ -170,12 +170,12 @@ img.rescaleIntercept; // (0028,1052) apply as: stored*slope + intercept
170
170
  img.signed; // true/false only if (0028,0103) Pixel Representation was present; else undefined
171
171
  img.bitsStored; // (0028,0101)
172
172
  img.photometricInterpretation; // (0028,0004) never defaulted to MONOCHROME2
173
- img.pixelSpacing; // (0028,0030) patient-plane mm — distinct from imagerPixelSpacing
173
+ img.pixelSpacing; // (0028,0030) patient-plane mm, distinct from imagerPixelSpacing
174
174
  ```
175
175
 
176
- > **Vendor note.** Philips writes private rescale tags `(2005,1409/140A/140B)` that shadow the standard `(0028,1052/1053)`; using the standard tags alone can yield non-quantitative values. This parser **preserves** the private tags so you can prefer them — reach them with `ds.get("(2005,1409)")` (optionally under `profiles.philips`).
176
+ > **Vendor note.** Philips writes private rescale tags `(2005,1409/140A/140B)` that shadow the standard `(0028,1052/1053)`; using the standard tags alone can yield non-quantitative values. This parser **preserves** the private tags so you can prefer them. Reach them with `ds.get("(2005,1409)")` (optionally under `profiles.philips`).
177
177
 
178
- For Enhanced multi-frame objects, `image.frame(i)` resolves each frame's functional-group macros Per-Frame-else-Shared (PS3.3 C.7.6.16). It throws a `DicomValueError` — carrying only structural facts, never PHI — for an out-of-range frame or a required geometry macro missing from both groups.
178
+ For Enhanced multi-frame objects, `image.frame(i)` resolves each frame's functional-group macros Per-Frame-else-Shared (PS3.3 C.7.6.16). It throws a `DicomValueError` (carrying only structural facts, never PHI) for an out-of-range frame or a required geometry macro missing from both groups.
179
179
 
180
180
  ```ts
181
181
  if (img.isEnhancedMultiFrame) {
@@ -187,15 +187,15 @@ if (img.isEnhancedMultiFrame) {
187
187
 
188
188
  ### De-identify before sharing
189
189
 
190
- `deidentify()` applies the PS3.15 Annex E Basic Application Level Confidentiality Profile — replacing, emptying, or removing every attribute the standard lists as identifying — and returns a fresh dataset plus a value-free report.
190
+ `deidentify()` applies the PS3.15 Annex E Basic Application Level Confidentiality Profile (replacing, emptying, or removing every attribute the standard lists as identifying) and returns a fresh dataset plus a value-free report.
191
191
 
192
192
  ```ts
193
193
  import { parseDicom, deidentify, serializeDicom } from "@cosyte/dicom";
194
194
 
195
195
  const { dataset, report } = deidentify(parseDicom(buf));
196
- const safe = serializeDicom(dataset); // safe to share — input dataset never mutated
196
+ const safe = serializeDicom(dataset); // safe to share: input dataset never mutated
197
197
 
198
- report.attributes.length; // count of attributes acted on (each carries tag/keyword/action — no values)
198
+ report.attributes.length; // count of attributes acted on (each carries tag/keyword/action, no values)
199
199
  report.warnings; // e.g. DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED
200
200
  ```
201
201
 
@@ -230,7 +230,7 @@ const imagingStudy = {
230
230
  const obr18 = ds.study.accessionNumber;
231
231
  ```
232
232
 
233
- > Series and SOP Instance UIDs are **not** represented in HL7 v2 — image-level identity lives only in DICOM.
233
+ > Series and SOP Instance UIDs are **not** represented in HL7 v2. Image-level identity lives only in DICOM.
234
234
 
235
235
  ### Round-trip: read, edit, re-serialize
236
236
 
@@ -238,10 +238,10 @@ const obr18 = ds.study.accessionNumber;
238
238
  import { parseDicom, serializeDicom } from "@cosyte/dicom";
239
239
 
240
240
  const ds = parseDicom(buf);
241
- const out = serializeDicom(ds); // spec-clean Part 10, same transfer syntax — no transcode
241
+ const out = serializeDicom(ds); // spec-clean Part 10, same transfer syntax, no transcode
242
242
  ```
243
243
 
244
- The serializer is the conservative half of Postel's Law: it rebuilds the File Meta group with a correct `(0002,0000)` length, pads values to even length, and re-emits sequences and encapsulated pixel data byte-for-byte. The File Meta group round-trips losslessly — non-modeled `(0002,xxxx)` elements (Sending/Receiving AE Title, Private Information, etc.) are preserved on parse and re-emitted in ascending tag order.
244
+ The serializer is the conservative half of Postel's Law: it rebuilds the File Meta group with a correct `(0002,0000)` length, pads values to even length, and re-emits sequences and encapsulated pixel data byte-for-byte. The File Meta group round-trips losslessly: non-modeled `(0002,xxxx)` elements (Sending/Receiving AE Title, Private Information, etc.) are preserved on parse and re-emitted in ascending tag order.
245
245
 
246
246
  ---
247
247
 
@@ -256,13 +256,13 @@ import { parseDicom, profiles } from "@cosyte/dicom";
256
256
  const ds = parseDicom(buf, { profile: profiles.siemens });
257
257
  ```
258
258
 
259
- A profile bundles three things that only ever **tighten or annotate** a parse — never loosen it past the lenient default:
259
+ A profile bundles three things that only ever **tighten or annotate** a parse, never loosen it past the lenient default:
260
260
 
261
- - **Private-dictionary overlay** — resolves the Implicit VR of vendor private data elements by the file's _live_ Private Creator string (e.g. `"SIEMENS CSA HEADER"`), keyed canonically as `"GGGGxxLL"` (PS3.5 §7.8.1), never a hard-coded block number. (This is why Agfa IMPAX re-assigning blocks still resolves.) An unknown creator degrades to `UN` plus a `DICOM_PRIVATE_CREATOR_UNKNOWN` warning.
262
- - **Escalations** — Tier-2 warning codes promoted to a thrown `DicomParseError` (a stricter posture for known-unsafe deviations).
263
- - **Suppressions** — benign, high-volume warning codes silenced for a known-quirky source.
261
+ - **Private-dictionary overlay**: resolves the Implicit VR of vendor private data elements by the file's _live_ Private Creator string (e.g. `"SIEMENS CSA HEADER"`), keyed canonically as `"GGGGxxLL"` (PS3.5 §7.8.1), never a hard-coded block number. (This is why Agfa IMPAX re-assigning blocks still resolves.) An unknown creator degrades to `UN` plus a `DICOM_PRIVATE_CREATOR_UNKNOWN` warning.
262
+ - **Escalations**: Tier-2 warning codes promoted to a thrown `DicomParseError` (a stricter posture for known-unsafe deviations).
263
+ - **Suppressions**: benign, high-volume warning codes silenced for a known-quirky source.
264
264
 
265
- Five built-ins ship under the `profiles` namespace: `ge`, `siemens`, `philips` (vendor overlays, grounded in the public GDCM / dcm4che / dcm2niix dictionaries) and `strict` / `lenient` (posture presets). Build your own with `defineProfile()` — it validates input, composes via `extends`, and returns a frozen profile:
265
+ Five built-ins ship under the `profiles` namespace: `ge`, `siemens`, `philips` (vendor overlays, grounded in the public GDCM / dcm4che / dcm2niix dictionaries) and `strict` / `lenient` (posture presets). Build your own with `defineProfile()`. It validates input, composes via `extends`, and returns a frozen profile:
266
266
 
267
267
  ```ts
268
268
  import { defineProfile, profiles } from "@cosyte/dicom";
@@ -284,7 +284,7 @@ At an RSNA-era interoperability test, ~80% of real-world patient CDs failed stri
284
284
 
285
285
  | Tier | Behavior | When | Example codes |
286
286
  | ---- | -------------- | ------------------------------ | ------------------------ |
287
- | 0 | Silent | Spec-compliant input | — |
287
+ | 0 | Silent | Spec-compliant input | none |
288
288
  | 1 | Auto-handled | Trivial deviation, no warning | trailing-space tidy |
289
289
  | 2 | Warning | Recoverable deviation | `DICOM_MISSING_PREAMBLE` |
290
290
  | 3 | Fatal (always) | Unrecoverable structural error | `NOT_DICOM_PART_10` |
@@ -304,7 +304,7 @@ for (const w of ds.warnings) {
304
304
 
305
305
  The 24 Tier-2 codes (`DICOM_MISSING_PREAMBLE`, `DICOM_FILE_META_GROUP_LENGTH_MISMATCH`, `DICOM_UN_PARSED_AS_SQ`, `DICOM_ODD_LENGTH_VALUE_PADDED`, `DICOM_PRIVATE_CREATOR_UNKNOWN`, `DICOM_VR_MISMATCH`, `DICOM_DA_LEGACY_FORMAT`, … ) live in [`src/parser/warnings.ts`](./src/parser/warnings.ts). Narrow on `w.code === WARNING_CODES.…` for typo-free comparisons, or pass `{ onWarning }` to `parseDicom` to stream them.
306
306
 
307
- The 4 Tier-3 fatal codes — `NOT_DICOM_PART_10`, `INVALID_FILE_META`, `UNSUPPORTED_TRANSFER_SYNTAX`, `EMPTY_INPUT` — always throw a `DicomParseError`; they represent input the parser cannot meaningfully recover.
307
+ The 4 Tier-3 fatal codes (`NOT_DICOM_PART_10`, `INVALID_FILE_META`, `UNSUPPORTED_TRANSFER_SYNTAX`, `EMPTY_INPUT`) always throw a `DicomParseError`; they represent input the parser cannot meaningfully recover.
308
308
 
309
309
  ---
310
310
 
@@ -330,7 +330,7 @@ try {
330
330
 
331
331
  ### `DicomValueError`
332
332
 
333
- Thrown only by `image.frame(i)` — `FRAME_INDEX_OUT_OF_RANGE` for an index outside `[0, numberOfFrames)`, or `MISSING_REQUIRED_FUNCTIONAL_GROUP` when an enhanced object lacks a required geometry macro in both the Per-Frame and Shared groups. Value decode (`element.value`) never throws — it warns and returns `null`/typed-absent instead.
333
+ Thrown only by `image.frame(i)`: `FRAME_INDEX_OUT_OF_RANGE` for an index outside `[0, numberOfFrames)`, or `MISSING_REQUIRED_FUNCTIONAL_GROUP` when an enhanced object lacks a required geometry macro in both the Per-Frame and Shared groups. Value decode (`element.value`) never throws. It warns and returns `null`/typed-absent instead.
334
334
 
335
335
  ### `DicomSerializeError`
336
336
 
@@ -338,7 +338,7 @@ Thrown by `serializeDicom` for `MISSING_TRANSFER_SYNTAX` (the dataset names no t
338
338
 
339
339
  ### `ProfileDefinitionError` · `DeidentifyError`
340
340
 
341
- `defineProfile()` throws `ProfileDefinitionError` for a structurally invalid profile; `deidentify()` throws `DeidentifyError` (`INVALID_OPTIONS`) for an unknown Retain option or malformed UID root. Both messages carry only structural facts — option names, the UID root — never a decoded value.
341
+ `defineProfile()` throws `ProfileDefinitionError` for a structurally invalid profile; `deidentify()` throws `DeidentifyError` (`INVALID_OPTIONS`) for an unknown Retain option or malformed UID root. Both messages carry only structural facts (option names, the UID root), never a decoded value.
342
342
 
343
343
  ---
344
344
 
@@ -360,18 +360,23 @@ Supported transfer syntaxes (structure for all; **pixels never decoded**): Impli
360
360
 
361
361
  v1 is metadata-feature-complete. Future companion packages (separate repos, demand-sequenced):
362
362
 
363
- - **`@cosyte/dicom-pixel`** — pixel decode/decompression, frame extraction, burned-in-annotation cleaning.
364
- - **`@cosyte/dicom-net`** — DIMSE network services.
365
- - **`@cosyte/dicomweb`** — QIDO / WADO / STOW REST clients.
363
+ - **`@cosyte/dicom-pixel`**: pixel decode/decompression, frame extraction, burned-in-annotation cleaning.
364
+ - **`@cosyte/dicom-net`**: DIMSE network services.
365
+ - **`@cosyte/dicomweb`**: QIDO / WADO / STOW REST clients.
366
366
 
367
367
  ---
368
368
 
369
369
  ## Contributing
370
370
 
371
- Vendor-quirk fixtures (synthetic or properly de-identified), profile improvements, and dictionary corrections are all welcome — the more real-world edge cases the test suite covers, the more robust the parser gets. See [CONTRIBUTING.md](./CONTRIBUTING.md) if present, or open an issue.
371
+ Vendor-quirk fixtures (synthetic or properly de-identified), profile improvements, and dictionary corrections are all welcome. The more real-world edge cases the test suite covers, the more robust the parser gets. See [CONTRIBUTING.md](./CONTRIBUTING.md) if present, or open an issue.
372
372
 
373
373
  ---
374
374
 
375
+ ## Trademarks
376
+
377
+ GE, Siemens, and Philips are trademarks of their respective owners. cosyte is not affiliated with, endorsed by, or
378
+ sponsored by any of them. The names identify the vendors whose private dictionaries the built-in profiles resolve. See [TRADEMARKS.md](./TRADEMARKS.md).
379
+
375
380
  ## License
376
381
 
377
382
  [MIT](./LICENSE) © Cosyte
package/TRADEMARKS.md ADDED
@@ -0,0 +1,32 @@
1
+ # Trademarks
2
+
3
+ `@cosyte/dicom` is an independent open-source project. cosyte is **not affiliated with, endorsed by,
4
+ or sponsored by** any company named in this repository or its documentation.
5
+
6
+ ## Why these names appear
7
+
8
+ DICOM private data elements carry no on-wire VR under Implicit VR Little Endian, so resolving them
9
+ requires knowing whose private blocks they belong to. Naming the vendor is the only way to
10
+ describe (or correctly decode) its private dictionary.
11
+
12
+ Every reference is **descriptive**: it identifies whose private dictionary a profile resolves, and nothing more. Naming a system is the only way to say
13
+ whether a library works with it.
14
+
15
+ ## Where the profiles come from
16
+
17
+ The built-in source profiles are authored through this package's own public `defineProfile()` API,
18
+ from public open-source dictionaries (the GDCM private dictionary and dcm2niix tag notes) and
19
+ from vendor-published schema identifiers. Each profile documents its sources inline. They embed
20
+ no privileged, confidential, or reverse-engineered material, and no patient data.
21
+
22
+ ## Names referenced
23
+
24
+ | Name | Where it appears |
25
+ | ------- | ----------------------------------------------------------------------------- |
26
+ | GE | `profiles.ge`: a built-in profile name, resolving the `GEMS_*` private blocks |
27
+ | Siemens | `profiles.siemens`: a built-in profile name |
28
+ | Philips | `profiles.philips`: a built-in profile name |
29
+
30
+ All product names, logos, and brands are the property of their respective owners. Use of a name here
31
+ does not imply any affiliation with, or endorsement by, its owner. If you own one of these marks and
32
+ would like a reference changed, please open an issue.