@cosyte/dicom 0.0.1

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.
@@ -0,0 +1,2577 @@
1
+ import { Buffer } from 'node:buffer';
2
+
3
+ /**
4
+ * Package version string for `@cosyte/dicom`. Synchronized with `package.json#version` by
5
+ * `scripts/sync-version.mjs`, which the `version` script runs after `changeset version`
6
+ * so the bump and this constant land in the same "Version Packages" commit.
7
+ *
8
+ * Stays on the uniform `0.0.x`-until-first-alpha ladder (locked across the `@cosyte/*` suite):
9
+ * patch bumps via Changesets through pre-alpha, with no `0.1.0` milestone bump.
10
+ *
11
+ * @example
12
+ * import { VERSION } from "@cosyte/dicom";
13
+ * console.log(`@cosyte/dicom v${VERSION}`);
14
+ */
15
+ declare const VERSION: string;
16
+
17
+ /**
18
+ * Type definitions for the Phase 1 public Dictionary namespace.
19
+ *
20
+ * Frozen surface. Phase 2 (parser), Phase 3 (dataset/VR), and Phase 7
21
+ * (validate) all depend on this
22
+ * shape — schema-breaking changes here cascade through the codebase.
23
+ *
24
+ * @module
25
+ */
26
+ /**
27
+ * 8-character uppercase hex DICOM tag, e.g., `"00100010"` (Patient's Name).
28
+ *
29
+ * For repeating-group attribute families (`(50xx,xxxx)` curves, `(60xx,xxxx)`
30
+ * overlays, etc.), the tag string preserves lowercase `x` placeholders verbatim
31
+ * — those entries cannot be looked up by concrete tag and are flagged via
32
+ * {@link DictionaryEntry.repeatingGroup}.
33
+ *
34
+ * @example
35
+ * const tag: Tag = "00100010"; // Patient's Name
36
+ */
37
+ type Tag = string;
38
+ /**
39
+ * The 33 standard DICOM Value Representations from PS3.5 §6.2 plus the 64-bit
40
+ * additions (`OV`, `SV`, `UV`) introduced in DICOM 2018.
41
+ *
42
+ * Note that some attributes in the data dictionary list MULTIPLE possible VRs
43
+ * — see {@link DictionaryEntry.vr} (which is always an array, possibly empty
44
+ * for retired entries with no VR or special "See Note" entries).
45
+ *
46
+ * @example
47
+ * const vr: VR = "PN"; // Person Name
48
+ */
49
+ type VR = "AE" | "AS" | "AT" | "CS" | "DA" | "DS" | "DT" | "FL" | "FD" | "IS" | "LO" | "LT" | "OB" | "OD" | "OF" | "OL" | "OV" | "OW" | "PN" | "SH" | "SL" | "SQ" | "SS" | "ST" | "SV" | "TM" | "UC" | "UI" | "UL" | "UN" | "UR" | "US" | "UT" | "UV";
50
+ /**
51
+ * One DICOM attribute as published in PS3.6 (Data Dictionary).
52
+ *
53
+ * @example
54
+ * import { Dictionary } from "@cosyte/dicom";
55
+ * const entry = Dictionary.lookup("00100010");
56
+ * if (entry) {
57
+ * // entry.keyword === "PatientName"
58
+ * // entry.vr === ["PN"]
59
+ * }
60
+ */
61
+ interface DictionaryEntry {
62
+ readonly tag: Tag;
63
+ readonly keyword: string;
64
+ readonly name: string;
65
+ readonly vr: readonly VR[];
66
+ readonly vm: string;
67
+ readonly retired: boolean;
68
+ /**
69
+ * `true` for repeating-group families (`(50xx,xxxx)`, `(60xx,xxxx)` —
70
+ * curves/overlays); the {@link DictionaryEntry.tag} field for these contains
71
+ * lowercase `x` placeholders, NOT a concrete 8-hex-char tag. Repeating-group
72
+ * entries are not surfaced by `Dictionary.lookup(tag)` for concrete tags.
73
+ */
74
+ readonly repeatingGroup?: boolean;
75
+ }
76
+ /**
77
+ * One DICOM UID as published in PS3.6 / PS3.4 (UID registry).
78
+ *
79
+ * @example
80
+ * import { Dictionary } from "@cosyte/dicom";
81
+ * const ts = Dictionary.uid("1.2.840.10008.1.2.1");
82
+ * if (ts) {
83
+ * // ts.name === "Explicit VR Little Endian"
84
+ * // ts.type === "TransferSyntax"
85
+ * }
86
+ */
87
+ interface UidEntry {
88
+ readonly uid: string;
89
+ readonly name: string;
90
+ readonly type: "TransferSyntax" | "SOPClass" | "MetaSOPClass" | "WellKnownFrameOfReference" | "WellKnownSOPInstance" | "CodingScheme" | "ApplicationContext" | "ServiceClass" | "Other";
91
+ readonly retired: boolean;
92
+ }
93
+
94
+ /**
95
+ * Public Dictionary namespace for `@cosyte/dicom` — Phase 1 deliverable.
96
+ *
97
+ * D-10:
98
+ *
99
+ * - {@link lookup} — accepts either an 8-char hex tag (`"00100010"`) or a
100
+ * keyword (`"PatientName"`) and returns the typed {@link DictionaryEntry}
101
+ * or `undefined`.
102
+ * - {@link byKeyword} — keyword-only lookup; for cases where the caller has
103
+ * already validated the input shape and wants a narrower call.
104
+ * - {@link uid} — UID lookup (DICT-06).
105
+ *
106
+ * No function throws on miss (D-10: "Returns the typed entry or `undefined`.
107
+ * No throws on miss."). Returned entries are deeply frozen — mutation attempts
108
+ * throw `TypeError` in strict mode (which all of `@cosyte/dicom`'s emitted code
109
+ * runs under, given `"use strict"` is implicit for ES modules).
110
+ *
111
+ * @module
112
+ */
113
+
114
+ /**
115
+ * Look up a DICOM attribute by tag (`"00100010"`) or keyword (`"PatientName"`).
116
+ *
117
+ * Hex tag input is normalized to uppercase; keyword input is case-sensitive.
118
+ * Returns `undefined` for unknown tags, unknown keywords, malformed input, or
119
+ * tags from repeating-group families (those resolve via the family entry's
120
+ * `repeatingGroup` flag — see {@link DictionaryEntry}).
121
+ *
122
+ * @example
123
+ * import { Dictionary } from "@cosyte/dicom";
124
+ * const a = Dictionary.lookup("00100010"); // by tag
125
+ * const b = Dictionary.lookup("PatientName"); // by keyword (DICT-04)
126
+ * const c = Dictionary.lookup("not-real"); // undefined (D-10 no-throw)
127
+ */
128
+ declare function lookup(tagOrKeyword: string): DictionaryEntry | undefined;
129
+ /**
130
+ * Look up a DICOM attribute strictly by keyword (`"PatientName"`).
131
+ *
132
+ * Returns `undefined` for unknown keywords or malformed input. Use
133
+ * {@link lookup} if the input could be either a tag or a keyword.
134
+ *
135
+ * @example
136
+ * import { Dictionary } from "@cosyte/dicom";
137
+ * const e = Dictionary.byKeyword("StudyInstanceUID");
138
+ * // e?.tag === "0020000D"
139
+ */
140
+ declare function byKeyword(keyword: string): DictionaryEntry | undefined;
141
+ /**
142
+ * Look up a DICOM UID by its dotted-decimal value.
143
+ *
144
+ * Returns `undefined` for unknown UIDs or malformed input. Used by
145
+ * `parseDicom()` (Phase 2) to render human-readable Transfer Syntax names
146
+ * (DICT-06).
147
+ *
148
+ * @example
149
+ * import { Dictionary } from "@cosyte/dicom";
150
+ * const ts = Dictionary.uid("1.2.840.10008.1.2.1");
151
+ * // ts?.name === "Explicit VR Little Endian"
152
+ * // ts?.type === "TransferSyntax"
153
+ */
154
+ declare function uid(uidValue: string): UidEntry | undefined;
155
+
156
+ type index_DictionaryEntry = DictionaryEntry;
157
+ type index_Tag = Tag;
158
+ type index_UidEntry = UidEntry;
159
+ type index_VR = VR;
160
+ declare const index_byKeyword: typeof byKeyword;
161
+ declare const index_lookup: typeof lookup;
162
+ declare const index_uid: typeof uid;
163
+ declare namespace index {
164
+ export { type index_DictionaryEntry as DictionaryEntry, type index_Tag as Tag, type index_UidEntry as UidEntry, type index_VR as VR, index_byKeyword as byKeyword, index_lookup as lookup, index_uid as uid };
165
+ }
166
+
167
+ /**
168
+ * Shared parser-pipeline types for `@cosyte/dicom`.
169
+ *
170
+ * Phase 2 core-parser context:
171
+ * - D-02 — `ParseOptions` shape (Phase 2 only; no `profile` field).
172
+ * - D-03 — `OnWarningCallback` ordering contract (invoked AFTER push to `ctx.warnings`).
173
+ * - D-07 — `DicomPosition` shape (`byteOffset`, optional `fileMeta` / `deflated` / `contextPath`).
174
+ * - D-45 — `ParseContext.profile?: unknown` is reserved for Phase 6; Phase 2 never sets it.
175
+ *
176
+ * Public types (exported via `src/index.ts`): `DicomPosition`, `ParseOptions`, `OnWarningCallback`.
177
+ * Internal type (NOT exported from `src/index.ts`): `ParseContext`.
178
+ *
179
+ * @module
180
+ */
181
+
182
+ /**
183
+ * One private-data attribute definition supplied by a {@link Profile}'s
184
+ * private-dictionary overlay. The `vr` resolves the Implicit-VR of a private
185
+ * data element whose on-wire encoding carries no VR; `keyword` / `name` carry
186
+ * the vendor-documented identity for tooling and docs.
187
+ *
188
+ * @example
189
+ * ```ts
190
+ * import type { PrivateTagDefinition } from "@cosyte/dicom";
191
+ * const def: PrivateTagDefinition = {
192
+ * vr: "OB",
193
+ * keyword: "CSAImageHeaderInfo",
194
+ * name: "CSA Image Header Info",
195
+ * };
196
+ * ```
197
+ */
198
+ interface PrivateTagDefinition {
199
+ readonly vr: VR;
200
+ readonly keyword: string;
201
+ readonly name: string;
202
+ }
203
+ /**
204
+ * A source/vendor tolerance preset (Phase 6). A `Profile` bundles three
205
+ * things that only ever **tighten or annotate** a parse — never loosen it
206
+ * past the Postel's-Law default:
207
+ *
208
+ * - `escalations` — Tier-2 warning codes promoted to a thrown
209
+ * `DicomParseError` (a stricter posture for known-unsafe deviations).
210
+ * - `suppressions` — Tier-2 warning codes silenced because they are a
211
+ * documented, benign quirk of the named source (annotation, not loss).
212
+ * - `privateDictionary` — a private-creator-keyed overlay resolving the
213
+ * Implicit-VR of vendor private data elements via the file's **live**
214
+ * private-creator string (never a hard-coded block number).
215
+ *
216
+ * Build one with `defineProfile()`; never hand-author the frozen shape.
217
+ * Profiles are immutable and composable via `extends`.
218
+ *
219
+ * @example
220
+ * ```ts
221
+ * import { parseDicom, profiles } from "@cosyte/dicom";
222
+ * const ds = parseDicom(buf, { profile: profiles.siemens });
223
+ * console.log(ds.fileMeta?.transferSyntaxUID);
224
+ * ```
225
+ */
226
+ interface Profile {
227
+ readonly name: string;
228
+ readonly lineage: readonly string[];
229
+ readonly description?: string;
230
+ readonly escalations: ReadonlySet<WarningCode>;
231
+ readonly suppressions: ReadonlySet<WarningCode>;
232
+ /**
233
+ * Creator string → canonical private-tag key (`"GGGGxxEE"`, e.g.
234
+ * `"0029xx10"`) → definition. The `xx` placeholder stands for the
235
+ * file-assigned private block byte, mirroring the published DICOM
236
+ * private-dictionary notation; resolution is therefore by creator string,
237
+ * never by a fixed block number.
238
+ */
239
+ readonly privateDictionary: ReadonlyMap<string, ReadonlyMap<string, PrivateTagDefinition>>;
240
+ /** Render a human-readable, deterministic one-line summary of the profile. */
241
+ readonly describe?: () => string;
242
+ }
243
+ /**
244
+ * Positional context for a `DicomParseWarning` or `DicomParseError`.
245
+ *
246
+ * Byte offsets are relative to the source buffer for non-deflated transfer
247
+ * syntaxes; for the Deflated Explicit VR LE transfer syntax (D-27),
248
+ * `deflated: true` indicates the offset is into the inflated dataset buffer
249
+ * rather than the on-disk source.
250
+ *
251
+ * With `exactOptionalPropertyTypes: true`, callers should omit unset keys
252
+ * rather than passing `undefined` (mirrors `@cosyte/hl7` sibling discipline).
253
+ *
254
+ * @example
255
+ * ```ts
256
+ * import type { DicomPosition } from "@cosyte/dicom";
257
+ * const p: DicomPosition = { byteOffset: 132, fileMeta: true };
258
+ * ```
259
+ */
260
+ interface DicomPosition {
261
+ readonly byteOffset: number;
262
+ /** True when offset is inside the File Meta group. Omit (do not pass `undefined`) when not applicable. */
263
+ readonly fileMeta?: boolean;
264
+ /** True when offset is into the inflated dataset buffer (Deflated TS only). Omit when not applicable. */
265
+ readonly deflated?: boolean;
266
+ /** Tag chain for nested SQ items, e.g. `["0040A730", "0", "00080100"]`. Omit when at root. */
267
+ readonly contextPath?: readonly string[];
268
+ }
269
+ /**
270
+ * Synchronous callback invoked once per Tier-2 warning emitted during parse.
271
+ *
272
+ * Per `02-CONTEXT.md` D-03, the callback fires AFTER the warning has been
273
+ * pushed to `ctx.warnings`; if the callback throws, the parser silently
274
+ * swallows the exception and continues (mirrors `@cosyte/hl7` sibling).
275
+ *
276
+ * @example
277
+ * ```ts
278
+ * import type { OnWarningCallback } from "@cosyte/dicom";
279
+ * const onWarning: OnWarningCallback = (w) => {
280
+ * if (w.code === "DICOM_MISSING_PREAMBLE") {
281
+ * // ...
282
+ * }
283
+ * };
284
+ * ```
285
+ */
286
+ type OnWarningCallback = (warning: DicomParseWarning) => void;
287
+ /**
288
+ * Options accepted by `parseDicom`.
289
+ *
290
+ * Per `02-CONTEXT.md` D-02 — Phase 2 form only. No `profile` field; Phase 6
291
+ * adds it. With `exactOptionalPropertyTypes: true`, callers omit unset keys
292
+ * rather than passing `undefined` for any field below.
293
+ *
294
+ * @example
295
+ * ```ts
296
+ * import { parseDicom } from "@cosyte/dicom";
297
+ * const ds = parseDicom(buf, {
298
+ * strict: false,
299
+ * stripPreamble: "tolerate",
300
+ * copyValues: false,
301
+ * onWarning: (w) => console.warn(w.code),
302
+ * });
303
+ * ```
304
+ */
305
+ interface ParseOptions {
306
+ /**
307
+ * When `true`, every Tier-2 warning is escalated to a thrown
308
+ * `DicomParseError` carrying the warning code. Default `false`.
309
+ *
310
+ * Omit (do not pass `undefined`) to use the default.
311
+ */
312
+ readonly strict?: boolean;
313
+ /**
314
+ * Preamble policy:
315
+ * - `"tolerate"` (default): attempt to start at offset 0 if `DICM` magic
316
+ * is missing at offset 128; emit `DICOM_MISSING_PREAMBLE`.
317
+ * - `"require"`: throw `DicomParseError(NOT_DICOM_PART_10)` when no
318
+ * `DICM` magic is present.
319
+ *
320
+ * Omit to use the default.
321
+ */
322
+ readonly stripPreamble?: "tolerate" | "require";
323
+ /**
324
+ * Synchronous callback invoked once per Tier-2 warning, after the warning
325
+ * has been pushed to `Dataset.warnings`. Throwing handlers are silently
326
+ * swallowed (parser-state safety per D-03).
327
+ *
328
+ * Omit to skip the callback entirely.
329
+ */
330
+ readonly onWarning?: OnWarningCallback;
331
+ /**
332
+ * When `true`, every `Element.rawBytes` is `Buffer.from(slice)` — copying
333
+ * each value out so the source buffer can be released. When `false` (the
334
+ * default), `Element.rawBytes` is `Buffer.subarray(slice)` — a zero-copy
335
+ * view that pins the source ArrayBuffer until every Element is GC'd.
336
+ *
337
+ * Per D-16 / MODEL-03. Omit to use the default.
338
+ */
339
+ readonly copyValues?: boolean;
340
+ /**
341
+ * Source/vendor tolerance preset (Phase 6, D-45). Applies the profile's
342
+ * `escalations` / `suppressions` to warning emission and its
343
+ * `privateDictionary` to Implicit-VR resolution of private data elements.
344
+ * A profile only tightens or annotates — it never makes the default
345
+ * lenient parse throw outside the four Tier-3 fatals, and a private
346
+ * creator the profile does not recognize degrades to generic UN handling
347
+ * plus a `DICOM_PRIVATE_CREATOR_UNKNOWN` warning, never a wrong decode.
348
+ *
349
+ * Omit (do not pass `undefined`) for the unprofiled default behaviour.
350
+ */
351
+ readonly profile?: Profile;
352
+ }
353
+
354
+ /**
355
+ * Tier-2 warning registry and factories for the `@cosyte/dicom` parser
356
+ * pipeline.
357
+ *
358
+ * Phase 2 core-parser context:
359
+ * - D-08 — `WARNING_CODES` is a frozen `as const` registry with every code
360
+ * in the TOL-03 catalog (≥25 entries). Phase 2 actively emits 13;
361
+ * 7 VR-decode-time codes are reserved for Phase 3; 2 charset codes are
362
+ * reserved for Phase 4; 2 codes are reserved for Phase 6 / Phase 7.
363
+ * - D-12 — Exactly one named factory per actively-emitted code; each
364
+ * factory carries its own JSDoc + `@example` and returns a typed
365
+ * `DicomParseWarning`.
366
+ *
367
+ * Consumers compare `warning.code === WARNING_CODES.<CODE>` to narrow and
368
+ * react; the parser uses the factories here so message templates, payload
369
+ * shape, and positional context stay consistent across stages.
370
+ *
371
+ * @module
372
+ */
373
+
374
+ /**
375
+ * Stable string codes for every Tier-2 warning the parser may emit.
376
+ *
377
+ * The registry is frozen via `as const` so TypeScript infers the exact
378
+ * string-literal union for `WarningCode` — there is zero runtime cost and
379
+ * no magic-string comparisons for consumers. Reserved-but-not-emitted
380
+ * codes carry inline comments documenting which phase activates them
381
+ * (Phase 2 declares the union so the schema is stable for downstream
382
+ * phases per D-08, D-42, D-43).
383
+ *
384
+ * @example
385
+ * ```ts
386
+ * import { parseDicom, WARNING_CODES } from "@cosyte/dicom";
387
+ * const ds = parseDicom(buf);
388
+ * if (ds.warnings.some((w) => w.code === WARNING_CODES.DICOM_MISSING_PREAMBLE)) {
389
+ * // handle bare File Meta input
390
+ * }
391
+ * ```
392
+ */
393
+ declare const WARNING_CODES: {
394
+ readonly DICOM_EMPTY_ITEM_IN_SEQUENCE: "DICOM_EMPTY_ITEM_IN_SEQUENCE";
395
+ readonly DICOM_FILE_META_GROUP_LENGTH_MISMATCH: "DICOM_FILE_META_GROUP_LENGTH_MISMATCH";
396
+ readonly DICOM_FILE_META_GROUP_LENGTH_MISSING: "DICOM_FILE_META_GROUP_LENGTH_MISSING";
397
+ readonly DICOM_GROUP_LENGTH_IN_DATASET: "DICOM_GROUP_LENGTH_IN_DATASET";
398
+ readonly DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR: "DICOM_IMPLICIT_VR_FOR_PRIVATE_TAG_WITHOUT_VR";
399
+ readonly DICOM_MISSING_PREAMBLE: "DICOM_MISSING_PREAMBLE";
400
+ readonly DICOM_NONZERO_RESERVED_BYTES: "DICOM_NONZERO_RESERVED_BYTES";
401
+ readonly DICOM_ODD_LENGTH_VALUE_PADDED: "DICOM_ODD_LENGTH_VALUE_PADDED";
402
+ readonly DICOM_PIXEL_DATA_LENGTH_MISMATCH: "DICOM_PIXEL_DATA_LENGTH_MISMATCH";
403
+ readonly DICOM_PRIVATE_TAG_NO_CREATOR: "DICOM_PRIVATE_TAG_NO_CREATOR";
404
+ readonly DICOM_UN_PARSED_AS_SQ: "DICOM_UN_PARSED_AS_SQ";
405
+ readonly DICOM_UNDEFINED_LENGTH_IN_EXPLICIT_VR: "DICOM_UNDEFINED_LENGTH_IN_EXPLICIT_VR";
406
+ readonly DICOM_VR_MISMATCH: "DICOM_VR_MISMATCH";
407
+ readonly DICOM_BOM_IN_TEXT_VR: "DICOM_BOM_IN_TEXT_VR";
408
+ readonly DICOM_DA_LEGACY_FORMAT: "DICOM_DA_LEGACY_FORMAT";
409
+ readonly DICOM_DT_NONSTANDARD_OFFSET: "DICOM_DT_NONSTANDARD_OFFSET";
410
+ readonly DICOM_IS_NONINTEGER_VALUE: "DICOM_IS_NONINTEGER_VALUE";
411
+ readonly DICOM_NON_ASCII_IN_ASCII_VR: "DICOM_NON_ASCII_IN_ASCII_VR";
412
+ readonly DICOM_TRAILING_NULL_IN_TEXT_VR: "DICOM_TRAILING_NULL_IN_TEXT_VR";
413
+ readonly DICOM_UI_TRAILING_SPACE: "DICOM_UI_TRAILING_SPACE";
414
+ readonly DICOM_CHARSET_AMBIGUOUS_SEPARATOR: "DICOM_CHARSET_AMBIGUOUS_SEPARATOR";
415
+ readonly DICOM_UNSUPPORTED_CHARSET: "DICOM_UNSUPPORTED_CHARSET";
416
+ readonly DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED: "DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED";
417
+ readonly DICOM_PRIVATE_CREATOR_UNKNOWN: "DICOM_PRIVATE_CREATOR_UNKNOWN";
418
+ };
419
+ /**
420
+ * Discriminant type for `DicomParseWarning.code`. Narrowing a warning by
421
+ * this code lets consumers write exhaustive `switch` blocks (enabled by
422
+ * the `switch-exhaustiveness-check` lint rule) and guarantees a typo-free
423
+ * comparison against the `WARNING_CODES` registry.
424
+ *
425
+ * @example
426
+ * ```ts
427
+ * import type { DicomParseWarning, WarningCode } from "@cosyte/dicom";
428
+ * function describe(w: DicomParseWarning): string {
429
+ * const code: WarningCode = w.code;
430
+ * if (code === "DICOM_MISSING_PREAMBLE") return "preamble missing";
431
+ * return `warning: ${code}`;
432
+ * }
433
+ * ```
434
+ */
435
+ type WarningCode = (typeof WARNING_CODES)[keyof typeof WARNING_CODES];
436
+ /**
437
+ * Data shape for every Tier-2 warning emitted by the parser. Warnings are
438
+ * plain data (distinct from `DicomParseError`, which is a thrown `Error`
439
+ * subclass) so they can be safely accumulated into `Dataset.warnings` and
440
+ * passed to `onWarning` callbacks.
441
+ *
442
+ * Per D-07 there is intentionally NO `snippet` field on warnings —
443
+ * real-world files routinely produce 50+ warnings and a per-warning
444
+ * snippet would balloon retained memory. Snippets appear only on
445
+ * `DicomParseError` (the strict-mode escalation path).
446
+ *
447
+ * @example
448
+ * ```ts
449
+ * import type { DicomParseWarning } from "@cosyte/dicom";
450
+ * const w: DicomParseWarning = {
451
+ * code: "DICOM_MISSING_PREAMBLE",
452
+ * message: "No DICM magic at offset 128.",
453
+ * position: { byteOffset: 0 },
454
+ * };
455
+ * ```
456
+ */
457
+ interface DicomParseWarning {
458
+ readonly code: WarningCode;
459
+ readonly message: string;
460
+ readonly position: DicomPosition;
461
+ }
462
+
463
+ /**
464
+ * Phase 2 structural `Item` — a single `(FFFE,E000)`-delimited Item
465
+ * inside a Sequence, carrying a nested `Dataset`.
466
+ *
467
+ * Per `02-CONTEXT.md` D-04: structural surface only; navigation methods
468
+ * land in Phase 3 (via `Dataset` extension per D-42).
469
+ *
470
+ * @module
471
+ */
472
+
473
+ /**
474
+ * Initialiser shape for an `Item` — extends `DatasetInit` with `index`,
475
+ * the 0-based position of the item inside its parent `Sequence`.
476
+ *
477
+ * @internal
478
+ */
479
+ interface ItemInit extends DatasetInit {
480
+ readonly index: number;
481
+ }
482
+ /**
483
+ * One sequence item. Inherits `fileMeta` (always `undefined` for nested
484
+ * items), `warnings`, and the protected element map from `Dataset`.
485
+ *
486
+ * Phase 3 surfaces `Item.get(...)` / `Item.has(...)` / etc. via the
487
+ * `Dataset` superclass extension per D-42.
488
+ *
489
+ * @example
490
+ * ```ts
491
+ * import { Item } from "@cosyte/dicom";
492
+ * // Producers (parser plan 02-04) construct items as follows:
493
+ * // const item = new Item({ index: 0, warnings: [], elements: new Map() });
494
+ * ```
495
+ */
496
+ declare class Item extends Dataset {
497
+ readonly index: number;
498
+ /**
499
+ * Construct a new structural `Item`. Producers are the SQ / FFFE
500
+ * marker parsers in plan 02-04.
501
+ *
502
+ * @internal
503
+ */
504
+ constructor(init: ItemInit);
505
+ }
506
+
507
+ /**
508
+ * Decoded-value model for Phase 3 — the typed result of `Element.value`.
509
+ *
510
+ * `DicomValue` is a discriminated union keyed on `kind`; each variant maps
511
+ * to a family of VRs that share a decode shape (per PS3.5 §6.2 Table 6.2-1):
512
+ *
513
+ * - `empty` — zero-length value (any VR).
514
+ * - `text` — single-value text VRs `LT ST UT UR` (no backslash
515
+ * multiplicity; charset-decoded except `UR`).
516
+ * - `strings` — backslash-multi-valued string VRs
517
+ * `AE AS CS LO SH UC UI` (charset-decoded for
518
+ * `LO SH UC`; ASCII for the rest).
519
+ * - `personName` — `PN`, decoded to its 3-group / 5-component structure.
520
+ * - `numbers` — binary numeric VRs `US UL SS SL FL FD` (signedness
521
+ * from the VR, never guessed).
522
+ * - `bigints` — 64-bit binary VRs `SV UV` (kept as `bigint` so no
523
+ * precision is lost above 2^53).
524
+ * - `attributeTags` — `AT`, decoded to 8-hex tag strings.
525
+ * - `decimalString` — `DS`, parsed to `number | null` (non-numeric → null).
526
+ * - `integerString` — `IS`, parsed to `number | null` (non-integer → null).
527
+ * - `dates` — `DA`, tolerant temporal decode.
528
+ * - `times` — `TM`, tolerant temporal decode.
529
+ * - `dateTimes` — `DT`, tolerant temporal decode.
530
+ * - `binary` — bulk byte VRs `OB OD OF OL OV OW UN` (raw preserved;
531
+ * not interpreted in v1 — avoids the giant-typed-array
532
+ * footgun for pixel / LUT payloads).
533
+ * - `sequence` — `SQ`, the parsed `Item[]`.
534
+ *
535
+ * Every variant whose decode may flag a tolerated deviation carries an
536
+ * optional `warnings` array (decode is lazy and post-parse, so these cannot
537
+ * be folded into the frozen `Dataset.warnings`; they ride on the value).
538
+ *
539
+ * @module
540
+ */
541
+
542
+ /**
543
+ * One of the three component groups of a `PN` value (PS3.5 §6.2.1.1):
544
+ * alphabetic, ideographic, or phonetic. Each holds the five `^`-delimited
545
+ * components; missing components are the empty string (never `undefined`).
546
+ *
547
+ * @example
548
+ * ```ts
549
+ * import type { PersonNameGroup } from "@cosyte/dicom";
550
+ * const g: PersonNameGroup = {
551
+ * familyName: "Doe",
552
+ * givenName: "Jane",
553
+ * middleName: "",
554
+ * namePrefix: "",
555
+ * nameSuffix: "",
556
+ * };
557
+ * ```
558
+ */
559
+ interface PersonNameGroup {
560
+ readonly familyName: string;
561
+ readonly givenName: string;
562
+ readonly middleName: string;
563
+ readonly namePrefix: string;
564
+ readonly nameSuffix: string;
565
+ }
566
+ /**
567
+ * A decoded `PN` value — up to three component groups separated on-wire by
568
+ * `=` (PS3.5 §6.2.1.1). `alphabetic` is always present; `ideographic` and
569
+ * `phonetic` are present only when the value supplied them.
570
+ *
571
+ * @example
572
+ * ```ts
573
+ * import type { PersonName } from "@cosyte/dicom";
574
+ * // "Yamada^Tarou=山田^太郎=やまだ^たろう"
575
+ * declare const pn: PersonName;
576
+ * pn.alphabetic.familyName; // "Yamada"
577
+ * pn.ideographic?.familyName; // "山田"
578
+ * ```
579
+ */
580
+ interface PersonName {
581
+ readonly alphabetic: PersonNameGroup;
582
+ readonly ideographic?: PersonNameGroup;
583
+ readonly phonetic?: PersonNameGroup;
584
+ }
585
+ /**
586
+ * A tolerantly-decoded `DA` (Date) value. `raw` always carries the on-wire
587
+ * string (PHI-safe — a date is not an identifier on its own, but is
588
+ * preserved verbatim regardless). `valid` is `true` only when the string
589
+ * parsed cleanly to a `YYYYMMDD` calendar date; otherwise the numeric
590
+ * fields are omitted and `raw` is the source of truth.
591
+ *
592
+ * @example
593
+ * ```ts
594
+ * import type { DicomDate } from "@cosyte/dicom";
595
+ * const d: DicomDate = { raw: "20240115", valid: true, year: 2024, month: 1, day: 15 };
596
+ * ```
597
+ */
598
+ interface DicomDate {
599
+ readonly raw: string;
600
+ readonly valid: boolean;
601
+ readonly year?: number;
602
+ readonly month?: number;
603
+ readonly day?: number;
604
+ }
605
+ /**
606
+ * A tolerantly-decoded `TM` (Time) value (PS3.5 §6.2; max length 14 bytes).
607
+ * `fractionalSeconds` is the value after the decimal point as a number in
608
+ * `[0,1)` when present.
609
+ *
610
+ * @example
611
+ * ```ts
612
+ * import type { DicomTime } from "@cosyte/dicom";
613
+ * const t: DicomTime = { raw: "133015.250000", valid: true, hours: 13, minutes: 30, seconds: 15, fractionalSeconds: 0.25 };
614
+ * ```
615
+ */
616
+ interface DicomTime {
617
+ readonly raw: string;
618
+ readonly valid: boolean;
619
+ readonly hours?: number;
620
+ readonly minutes?: number;
621
+ readonly seconds?: number;
622
+ readonly fractionalSeconds?: number;
623
+ }
624
+ /**
625
+ * A tolerantly-decoded `DT` (DateTime) value (PS3.5 §6.2). `offsetMinutes`
626
+ * is the signed UTC offset (`&ZZXX` suffix) in minutes when present.
627
+ *
628
+ * @example
629
+ * ```ts
630
+ * import type { DicomDateTime } from "@cosyte/dicom";
631
+ * const dt: DicomDateTime = { raw: "20240115133015", valid: true, year: 2024, month: 1, day: 15, hours: 13, minutes: 30, seconds: 15 };
632
+ * ```
633
+ */
634
+ interface DicomDateTime {
635
+ readonly raw: string;
636
+ readonly valid: boolean;
637
+ readonly year?: number;
638
+ readonly month?: number;
639
+ readonly day?: number;
640
+ readonly hours?: number;
641
+ readonly minutes?: number;
642
+ readonly seconds?: number;
643
+ readonly fractionalSeconds?: number;
644
+ readonly offsetMinutes?: number;
645
+ }
646
+ /**
647
+ * The lazily-decoded value of an {@link Element}, as a discriminated union
648
+ * on `kind`. See the module doc for the VR → kind mapping. Narrow on `kind`
649
+ * (the `switch-exhaustiveness-check` lint rule keeps consumers honest).
650
+ *
651
+ * @example
652
+ * ```ts
653
+ * import { parseDicom } from "@cosyte/dicom";
654
+ * const ds = parseDicom(buf);
655
+ * const v = ds.get("00100010")?.value; // Patient's Name (PN)
656
+ * if (v?.kind === "personName") {
657
+ * console.log(v.values[0]?.alphabetic.familyName);
658
+ * }
659
+ * ```
660
+ */
661
+ type DicomValue = {
662
+ readonly kind: "empty";
663
+ } | {
664
+ readonly kind: "text";
665
+ readonly value: string;
666
+ readonly warnings?: readonly DicomParseWarning[];
667
+ } | {
668
+ readonly kind: "strings";
669
+ readonly values: readonly string[];
670
+ readonly warnings?: readonly DicomParseWarning[];
671
+ } | {
672
+ readonly kind: "personName";
673
+ readonly values: readonly PersonName[];
674
+ readonly warnings?: readonly DicomParseWarning[];
675
+ } | {
676
+ readonly kind: "numbers";
677
+ readonly values: readonly number[];
678
+ } | {
679
+ readonly kind: "bigints";
680
+ readonly values: readonly bigint[];
681
+ } | {
682
+ readonly kind: "attributeTags";
683
+ readonly values: readonly Tag[];
684
+ } | {
685
+ readonly kind: "decimalString";
686
+ readonly values: readonly (number | null)[];
687
+ readonly warnings?: readonly DicomParseWarning[];
688
+ } | {
689
+ readonly kind: "integerString";
690
+ readonly values: readonly (number | null)[];
691
+ readonly warnings?: readonly DicomParseWarning[];
692
+ } | {
693
+ readonly kind: "dates";
694
+ readonly values: readonly DicomDate[];
695
+ readonly warnings?: readonly DicomParseWarning[];
696
+ } | {
697
+ readonly kind: "times";
698
+ readonly values: readonly DicomTime[];
699
+ readonly warnings?: readonly DicomParseWarning[];
700
+ } | {
701
+ readonly kind: "dateTimes";
702
+ readonly values: readonly DicomDateTime[];
703
+ readonly warnings?: readonly DicomParseWarning[];
704
+ } | {
705
+ readonly kind: "binary";
706
+ readonly bytes: Buffer;
707
+ } | {
708
+ readonly kind: "sequence";
709
+ readonly items: readonly Item[];
710
+ };
711
+
712
+ /**
713
+ * Phase 2 structural `Element` — leaf wrapper over an on-wire DICOM
714
+ * Data Element.
715
+ *
716
+ * Per `02-CONTEXT.md` D-04 + D-42: structural fields ONLY. No `.value`
717
+ * getter, no decoders, no `.items` navigation — those land in Phase 3,
718
+ * which extends this class.
719
+ *
720
+ * Per D-16: `rawBytes` is a `Buffer.subarray()` view over the source
721
+ * buffer by default (zero-copy), pinning the source ArrayBuffer until
722
+ * every Element is GC'd. The `parseDicom` `{ copyValues: true }` option
723
+ * flips production to `Buffer.from(slice)` so the source can be
724
+ * released; producers (parser plans 02-04 onward) honour it.
725
+ *
726
+ * @module
727
+ */
728
+
729
+ /**
730
+ * Initialiser shape accepted by the `Element` constructor.
731
+ *
732
+ * `privateCreator` is omitted (not `undefined`) for standard tags;
733
+ * present only when the parser resolved a `(gggg,EEFF)` private element
734
+ * to its registered Private Creator string per D-33 / D-34.
735
+ *
736
+ * @internal
737
+ */
738
+ interface ElementInit {
739
+ readonly tag: Tag;
740
+ readonly vr: VR;
741
+ readonly vm: number;
742
+ readonly length: number;
743
+ readonly rawBytes: Buffer;
744
+ readonly byteOffset: number;
745
+ readonly privateCreator?: string;
746
+ /**
747
+ * Phase 2 hint set by the parser when an `Element` was promoted from
748
+ * `VR=UN` with undefined length to `VR=SQ` via the CP-246 fallback (D-30).
749
+ * Phase 3 reads this to choose the inner SQ decoder (Implicit VR LE) on
750
+ * first lazy access. Never set on standard SQ elements.
751
+ *
752
+ * @internal
753
+ */
754
+ readonly cp246Promoted?: boolean;
755
+ /**
756
+ * Byte order of the value bytes, set by the parser per transfer syntax
757
+ * (`true` for Implicit/Explicit VR LE + Deflated; `false` for Explicit VR
758
+ * BE). Phase 3 numeric decoders read this — signedness comes from the VR,
759
+ * endianness from here.
760
+ */
761
+ readonly littleEndian: boolean;
762
+ /**
763
+ * The dataset's resolved `(0008,0005)` Specific Character Set terms in
764
+ * effect for this element (parent dataset's, or the item's own override).
765
+ * Omitted when the Default Repertoire (ISO_IR 6) applies. Phase 3
766
+ * charset-dependent text decoders (`LO SH UC LT ST UT PN`) read this.
767
+ */
768
+ readonly specificCharacterSet?: readonly string[];
769
+ /**
770
+ * For `SQ` elements: the parsed sequence items, threaded on by the parser
771
+ * so `Element.value` can expose them without re-parsing. Omitted for
772
+ * non-SQ elements.
773
+ */
774
+ readonly items?: readonly Item[];
775
+ }
776
+ /**
777
+ * One DICOM Data Element as parsed by Phase 2.
778
+ *
779
+ * Phase 2 surface (this plan): `tag`, `vr`, `vm`, `length`, `rawBytes`,
780
+ * `byteOffset`, `privateCreator`. NO `.value` getter, NO decoders, NO
781
+ * navigation methods. Phase 3 (per D-42) extends this class with a lazy,
782
+ * memoized `.value` getter and the VR-aware decoders under
783
+ * `src/dataset/vr/`.
784
+ *
785
+ * `rawBytes` retention behaviour: zero-copy `Buffer.subarray` view by
786
+ * default (pins source ArrayBuffer); pass `{ copyValues: true }` to
787
+ * `parseDicom` for `Buffer.from(slice)` per element when the source
788
+ * needs to be released.
789
+ *
790
+ * @example
791
+ * ```ts
792
+ * import { Buffer } from "node:buffer";
793
+ * import { Element } from "@cosyte/dicom";
794
+ * const el = new Element({
795
+ * tag: "00100010",
796
+ * vr: "PN",
797
+ * vm: 1,
798
+ * length: 8,
799
+ * rawBytes: Buffer.from("DOE^JANE"),
800
+ * byteOffset: 200,
801
+ * littleEndian: true,
802
+ * });
803
+ * // el.tag === "00100010"; el.rawBytes is a Buffer of 8 bytes.
804
+ * // el.value.kind === "personName".
805
+ * ```
806
+ */
807
+ declare class Element {
808
+ readonly tag: Tag;
809
+ readonly vr: VR;
810
+ readonly vm: number;
811
+ readonly length: number;
812
+ readonly rawBytes: Buffer;
813
+ readonly byteOffset: number;
814
+ readonly privateCreator: string | undefined;
815
+ /**
816
+ * Phase 2 hint for Phase 3's lazy SQ decoder per D-30. `true` when this
817
+ * Element was promoted from `VR=UN` with undefined length to `VR=SQ` via
818
+ * the CP-246 fallback. Phase 3 uses this to choose the Implicit VR LE
819
+ * inner decoder. Always `undefined` on standard SQ elements.
820
+ *
821
+ * @internal
822
+ */
823
+ readonly cp246Promoted: boolean | undefined;
824
+ /** Value byte order (per transfer syntax). See {@link ElementInit.littleEndian}. */
825
+ readonly littleEndian: boolean;
826
+ /** In-effect `(0008,0005)` terms, or `undefined` for the Default Repertoire. */
827
+ readonly specificCharacterSet: readonly string[] | undefined;
828
+ /** Parsed items for an `SQ` element; `undefined` otherwise. */
829
+ readonly items: readonly Item[] | undefined;
830
+ /** Lazily-decoded, memoized {@link DicomValue}. */
831
+ private _valueCache;
832
+ /**
833
+ * Construct a new structural `Element`. Producers (parser plans 02-03
834
+ * onward) build these directly from on-wire bytes; consumers receive
835
+ * them via `Dataset` after `parseDicom`.
836
+ *
837
+ * @internal
838
+ */
839
+ constructor(init: ElementInit);
840
+ /**
841
+ * The decoded value of this element, by VR. Decode is lazy (the structural
842
+ * parse stays eager; field decode runs on first access) and memoized — the
843
+ * documented ~30× win on large studies where most fields are never read.
844
+ * Fail-safe: never throws; a malformed value surfaces as typed-absent with
845
+ * the deviation on the returned value's `warnings`.
846
+ *
847
+ * @example
848
+ * ```ts
849
+ * import { parseDicom } from "@cosyte/dicom";
850
+ * const ds = parseDicom(buf);
851
+ * const v = ds.get("00100010")?.value; // Patient's Name (PN)
852
+ * if (v?.kind === "personName") console.log(v.values[0]?.alphabetic.familyName);
853
+ * ```
854
+ */
855
+ get value(): DicomValue;
856
+ }
857
+
858
+ /**
859
+ * Phase 2 structural `FileMeta` view-object shape.
860
+ *
861
+ * Per `02-CONTEXT.md` D-04 + D-17 + FM-02: a plain interface (not a class)
862
+ * since Phase 2 has no methods. Only `transferSyntaxUID` is required —
863
+ * the rest are populated by `parseFileMeta` (plan 02-02) when present
864
+ * but never enforced (Phase 7's `validate()` enforces FM Type-1 fields
865
+ * per D-19). Phase 3 may promote to a class if helpers are added.
866
+ *
867
+ * @module
868
+ */
869
+
870
+ /**
871
+ * A non-modeled `(0002,xxxx)` File Meta element, preserved verbatim so the
872
+ * serializer can re-emit an exotic File Meta group byte-for-byte.
873
+ *
874
+ * The typed {@link FileMeta} fields cover the common Type-1/Type-3 elements;
875
+ * anything else a source file carried — e.g. `(0002,0017)`/`(0002,0018)`
876
+ * Sending/Receiving AE Title, `(0002,0100)` Private Information Creator UID,
877
+ * `(0002,0102)` Private Information — is captured here as raw bytes (the
878
+ * on-wire value, even-length per PS3.5 §6.2) rather than dropped. `value` is a
879
+ * defensive copy, so the view never aliases the parsed input buffer.
880
+ */
881
+ interface FileMetaRawElement {
882
+ /** 8-char uppercase hex tag, e.g. `"00020100"`. */
883
+ readonly tag: Tag;
884
+ /** The element's Value Representation as read under Explicit VR LE. */
885
+ readonly vr: VR;
886
+ /** The raw on-wire value bytes (even-length), copied out of the input. */
887
+ readonly value: Buffer;
888
+ }
889
+ /**
890
+ * The Part-10 File Meta Information group, projected as a typed view
891
+ * over `(0002,xxxx)` elements parsed during the File Meta pre-pass.
892
+ *
893
+ * Only `transferSyntaxUID` is required because it is the dispatch input
894
+ * for the four v1 transfer-syntax parsers; everything else is optional
895
+ * because real-world clinical files routinely omit one or more
896
+ * Type-1 elements. Phase 7's `validate()` adds opinion-bearing checks
897
+ * for those missing elements.
898
+ *
899
+ * @example
900
+ * ```ts
901
+ * import { parseDicom } from "@cosyte/dicom";
902
+ * const ds = parseDicom(buf);
903
+ * if (ds.fileMeta !== undefined) {
904
+ * const ts = ds.fileMeta.transferSyntaxUID; // always present when fileMeta is defined
905
+ * }
906
+ * ```
907
+ */
908
+ interface FileMeta {
909
+ readonly transferSyntaxUID: string;
910
+ readonly mediaStorageSOPClassUID?: string;
911
+ readonly mediaStorageSOPInstanceUID?: string;
912
+ readonly fileMetaInformationVersion?: Buffer;
913
+ readonly implementationClassUID?: string;
914
+ readonly implementationVersionName?: string;
915
+ readonly sourceApplicationEntityTitle?: string;
916
+ /**
917
+ * Any `(0002,xxxx)` elements the source carried that the typed fields above
918
+ * do not model, preserved in tag order so a round-trip re-emits the File Meta
919
+ * group byte-for-byte. Omitted when the group held only modeled elements.
920
+ */
921
+ readonly extraElements?: readonly FileMetaRawElement[];
922
+ }
923
+
924
+ /**
925
+ * Typed view model for the Phase 4 safety-critical domain helpers
926
+ * (`Dataset.patient` / `study` / `series` / `image`).
927
+ *
928
+ * Every field here obeys the §4 fail-safe contract from the dicom roadmap:
929
+ * a missing value is **typed-absent** (`undefined`), never a substituted
930
+ * default; a malformed numeric component stays `null` inside its array
931
+ * rather than being coerced; raw bytes are always still reachable via the
932
+ * underlying `Element`. These views *interpret* nothing about pixels — they
933
+ * surface the metadata a downstream renderer needs to interpret them
934
+ * correctly.
935
+ *
936
+ * The dangerous DICOM failure is the confident, wrong image — so the
937
+ * deliberate omissions are as load-bearing as the fields: there is no
938
+ * `signed` boolean unless `(0028,0103)` was actually present, no rescale
939
+ * slope unless `(0028,1053)` was, and the three pixel-spacing tags are
940
+ * three distinct fields that are never aliased.
941
+ *
942
+ * @module
943
+ */
944
+
945
+ /**
946
+ * One entry of Other Patient IDs Sequence `(0010,1002)` — a `{id, issuer,
947
+ * typeCode}` triple (PS3.3 §10.15, the DICOM analogue of an HL7 v2 CX
948
+ * repetition). Surfaced so a caller never matches on a bare `(0010,0020)`.
949
+ *
950
+ * @example
951
+ * ```ts
952
+ * import type { OtherPatientId } from "@cosyte/dicom";
953
+ * const o: OtherPatientId = { id: "MRN-42", issuer: "HOSP_A", typeCode: "TEXT" };
954
+ * ```
955
+ */
956
+ interface OtherPatientId {
957
+ readonly id?: string;
958
+ readonly issuer?: string;
959
+ readonly typeCode?: string;
960
+ }
961
+ /**
962
+ * Patient & study identity (§4.1 — the wrong-patient failure class).
963
+ *
964
+ * `id` is **not globally unique**; correct cross-system matching needs the
965
+ * `{id, issuerOfId, issuerQualifiers}` tuple plus `otherIds`. `name` keeps
966
+ * its full `PN` component structure, never flattened to a string.
967
+ *
968
+ * @example
969
+ * ```ts
970
+ * import { parseDicom } from "@cosyte/dicom";
971
+ * const p = parseDicom(buf).patient;
972
+ * p.id; // "MRN-42" — meaningless without the issuer
973
+ * p.issuerOfId; // "HOSP_A"
974
+ * p.name?.alphabetic.familyName; // "Doe"
975
+ * ```
976
+ */
977
+ interface PatientView {
978
+ readonly id?: string;
979
+ readonly issuerOfId?: string;
980
+ readonly issuerQualifiers?: readonly Item[];
981
+ readonly otherIds: readonly OtherPatientId[];
982
+ readonly name?: PersonName;
983
+ readonly birthDate?: DicomDate;
984
+ readonly sex?: string;
985
+ }
986
+ /**
987
+ * Study-level identity (§4.1). `instanceUid` is the cross-system study key;
988
+ * `accessionNumber` ties the study to the order.
989
+ *
990
+ * @example
991
+ * ```ts
992
+ * import { parseDicom } from "@cosyte/dicom";
993
+ * const s = parseDicom(buf).study;
994
+ * s.instanceUid; // "1.2.840.113619..."
995
+ * s.accessionNumber; // "ACC123"
996
+ * ```
997
+ */
998
+ interface StudyView {
999
+ readonly instanceUid?: string;
1000
+ readonly id?: string;
1001
+ readonly accessionNumber?: string;
1002
+ readonly date?: DicomDate;
1003
+ readonly time?: DicomTime;
1004
+ readonly description?: string;
1005
+ }
1006
+ /**
1007
+ * Series-level identity & co-registration (§4.1, §4.3). Images sharing a
1008
+ * `frameOfReferenceUid` are spatially co-registered.
1009
+ *
1010
+ * @example
1011
+ * ```ts
1012
+ * import { parseDicom } from "@cosyte/dicom";
1013
+ * const s = parseDicom(buf).series;
1014
+ * s.modality; // "CT"
1015
+ * s.frameOfReferenceUid; // shared ⇒ co-registered
1016
+ * ```
1017
+ */
1018
+ interface SeriesView {
1019
+ readonly instanceUid?: string;
1020
+ readonly number?: number;
1021
+ readonly modality?: string;
1022
+ readonly description?: string;
1023
+ readonly frameOfReferenceUid?: string;
1024
+ }
1025
+ /**
1026
+ * A coded triplet (PS3.16 §8, Table 8-1): `Code Value (0008,0100)`,
1027
+ * `Coding Scheme Designator (0008,0102)`, `Code Meaning (0008,0104)`. The
1028
+ * canonical scheme OID is resolved only for the four standard designators;
1029
+ * legacy SNOMED designators (`SRT`/`SNM3`/`99SDM`) deliberately resolve to
1030
+ * `undefined` because their code values differ from `SCT` (CP-730).
1031
+ *
1032
+ * @example
1033
+ * ```ts
1034
+ * import type { CodedConcept } from "@cosyte/dicom";
1035
+ * const c: CodedConcept = {
1036
+ * codeValue: "C-B1003",
1037
+ * codingSchemeDesignator: "SCT",
1038
+ * codeMeaning: "Hounsfield unit",
1039
+ * schemeUid: "2.16.840.1.113883.6.96",
1040
+ * };
1041
+ * ```
1042
+ */
1043
+ interface CodedConcept {
1044
+ readonly codeValue?: string;
1045
+ readonly codingSchemeDesignator?: string;
1046
+ readonly codeMeaning?: string;
1047
+ readonly schemeUid?: string;
1048
+ }
1049
+ /**
1050
+ * A Real World Value Mapping (§4.5) — slope/intercept bound atomically to
1051
+ * its UCUM measurement-units code, so a number is never detached from its
1052
+ * units. From Real World Value Mapping Sequence `(0040,9096)`.
1053
+ *
1054
+ * @example
1055
+ * ```ts
1056
+ * import type { RealWorldValueMap } from "@cosyte/dicom";
1057
+ * const m: RealWorldValueMap = { slope: 1, intercept: 0, unitsCode: { codeValue: "[hnsf'U]" } };
1058
+ * ```
1059
+ */
1060
+ interface RealWorldValueMap {
1061
+ readonly slope?: number;
1062
+ readonly intercept?: number;
1063
+ readonly unitsCode?: CodedConcept;
1064
+ }
1065
+ /**
1066
+ * The five Enhanced multi-frame functional-group macros, resolved for a
1067
+ * single frame Per-Frame-else-Shared (§4.4, PS3.3 §C.7.6.16). Each macro is
1068
+ * typed-absent when present in neither the per-frame nor the shared group
1069
+ * (the three *geometry* macros are treated as required for an enhanced
1070
+ * object — see {@link ImageView.frame}).
1071
+ *
1072
+ * @example
1073
+ * ```ts
1074
+ * import { parseDicom } from "@cosyte/dicom";
1075
+ * const f = parseDicom(enhancedBuf).image.frame(0);
1076
+ * f.planePosition?.imagePositionPatient; // this frame's [x,y,z]
1077
+ * f.pixelMeasures?.pixelSpacing; // this frame's [row,col] mm
1078
+ * ```
1079
+ */
1080
+ interface FrameFunctionalGroups {
1081
+ readonly index: number;
1082
+ readonly pixelMeasures?: {
1083
+ readonly pixelSpacing?: readonly (number | null)[];
1084
+ readonly sliceThickness?: number;
1085
+ readonly spacingBetweenSlices?: number;
1086
+ };
1087
+ readonly planePosition?: {
1088
+ readonly imagePositionPatient?: readonly (number | null)[];
1089
+ };
1090
+ readonly planeOrientation?: {
1091
+ readonly imageOrientationPatient?: readonly (number | null)[];
1092
+ };
1093
+ readonly pixelValueTransformation?: {
1094
+ readonly rescaleSlope?: number;
1095
+ readonly rescaleIntercept?: number;
1096
+ readonly rescaleType?: string;
1097
+ };
1098
+ readonly frameVoiLut?: {
1099
+ readonly windowCenter?: readonly (number | null)[];
1100
+ readonly windowWidth?: readonly (number | null)[];
1101
+ };
1102
+ }
1103
+ /**
1104
+ * Pixel-interpretation + geometry metadata (§4.2 / §4.3 / §4.4 / §4.5) — the
1105
+ * "wrong pixels look fine" and "looks fine, measures wrong" classes. v1 does
1106
+ * not decode pixels; this view surfaces exactly what a renderer needs so it
1107
+ * does not have to guess.
1108
+ *
1109
+ * Safety-critical omissions are intentional: `rescaleSlope` is absent (not
1110
+ * `1`) when the tag is absent; `signed` is absent (not a guess) unless
1111
+ * `(0028,0103)` was present; `photometricInterpretation` is absent (not
1112
+ * `MONOCHROME2`) when absent; the three pixel-spacing fields are distinct.
1113
+ * When `modalityLutSequence` / `voiLutSequence` are present they are
1114
+ * authoritative over the linear `rescale*` / `window*` pairs.
1115
+ *
1116
+ * @example
1117
+ * ```ts
1118
+ * import { parseDicom } from "@cosyte/dicom";
1119
+ * const img = parseDicom(buf).image;
1120
+ * img.rescaleSlope; // undefined ⇒ caller MUST NOT assume 1
1121
+ * img.signed; // undefined ⇒ signedness unknown, never guess
1122
+ * img.pixelSpacing; // patient-plane mm — distinct from imagerPixelSpacing
1123
+ * ```
1124
+ */
1125
+ interface ImageView {
1126
+ readonly sopInstanceUid?: string;
1127
+ readonly rows?: number;
1128
+ readonly columns?: number;
1129
+ readonly samplesPerPixel?: number;
1130
+ readonly photometricInterpretation?: string;
1131
+ readonly planarConfiguration?: number;
1132
+ readonly bitsAllocated?: number;
1133
+ readonly bitsStored?: number;
1134
+ readonly highBit?: number;
1135
+ /** Raw `(0028,0103)` value: 0 = unsigned, 1 = signed. Absent ⇒ unknown. */
1136
+ readonly pixelRepresentation?: number;
1137
+ /** `true`/`false` only when `(0028,0103)` was 1/0; absent ⇒ never guessed. */
1138
+ readonly signed?: boolean;
1139
+ readonly rescaleSlope?: number;
1140
+ readonly rescaleIntercept?: number;
1141
+ readonly rescaleType?: string;
1142
+ readonly windowCenter?: readonly (number | null)[];
1143
+ readonly windowWidth?: readonly (number | null)[];
1144
+ readonly modalityLutSequence?: readonly Item[];
1145
+ readonly voiLutSequence?: readonly Item[];
1146
+ readonly pixelSpacing?: readonly (number | null)[];
1147
+ readonly imagerPixelSpacing?: readonly (number | null)[];
1148
+ readonly nominalScannedPixelSpacing?: readonly (number | null)[];
1149
+ readonly sliceThickness?: number;
1150
+ readonly spacingBetweenSlices?: number;
1151
+ readonly imagePositionPatient?: readonly (number | null)[];
1152
+ readonly imageOrientationPatient?: readonly (number | null)[];
1153
+ readonly frameOfReferenceUid?: string;
1154
+ readonly numberOfFrames?: number;
1155
+ readonly units?: string;
1156
+ readonly realWorldValueMaps?: readonly RealWorldValueMap[];
1157
+ /** `true` when this object carries Per-Frame/Shared Functional Groups. */
1158
+ readonly isEnhancedMultiFrame: boolean;
1159
+ /**
1160
+ * Resolve the functional-group macros for frame `index` Per-Frame-else-Shared.
1161
+ * Throws {@link DicomValueError} `FRAME_INDEX_OUT_OF_RANGE` for an index
1162
+ * outside `[0, numberOfFrames)` and `MISSING_REQUIRED_FUNCTIONAL_GROUP`
1163
+ * when an enhanced object lacks a required geometry macro in both groups.
1164
+ */
1165
+ frame(index: number): FrameFunctionalGroups;
1166
+ }
1167
+
1168
+ /**
1169
+ * Phase 2 structural `Dataset` — top-level container produced by
1170
+ * `parseDicom`.
1171
+ *
1172
+ * Per `02-CONTEXT.md` D-04 + D-42: only `fileMeta` and `warnings` are
1173
+ * public; the element map is held internally and Phase 3 extends this
1174
+ * class with the `get` / `has` / `elements` / `getAll` / `setElement` /
1175
+ * `addElement` / `removeElement` / `addItem` / `removeItem` navigation
1176
+ * surface.
1177
+ *
1178
+ * The `warnings` array is frozen at the constructor boundary (mirrors
1179
+ * `@cosyte/hl7` sibling `model/message.ts`) so consumers receive
1180
+ * `readonly DicomParseWarning[]` and downstream mutation cannot escape.
1181
+ *
1182
+ * @module
1183
+ */
1184
+
1185
+ /**
1186
+ * Initialiser shape accepted by the `Dataset` (and `Item`) constructor.
1187
+ *
1188
+ * `fileMeta` is optional because nested `Item` datasets do not carry
1189
+ * their own File Meta — only the root `Dataset` returned by
1190
+ * `parseDicom` does.
1191
+ *
1192
+ * @internal
1193
+ */
1194
+ interface DatasetInit {
1195
+ readonly fileMeta?: FileMeta;
1196
+ readonly warnings: readonly DicomParseWarning[];
1197
+ readonly elements: ReadonlyMap<Tag, Element>;
1198
+ }
1199
+ /**
1200
+ * One parsed DICOM dataset — the structural shell shipped by Phase 2.
1201
+ *
1202
+ * Phase 2 public surface: `fileMeta`, `warnings`. The internal element
1203
+ * map is stored on `_elements` (protected) and Phase 3 promotes it to
1204
+ * the public `get` / `has` / `elements` / `getAll` navigation API per
1205
+ * D-42.
1206
+ *
1207
+ * @example
1208
+ * ```ts
1209
+ * import { parseDicom } from "@cosyte/dicom";
1210
+ * const ds = parseDicom(buf);
1211
+ * console.log(ds.fileMeta?.transferSyntaxUID);
1212
+ * for (const w of ds.warnings) {
1213
+ * console.warn(w.code, w.message);
1214
+ * }
1215
+ * ```
1216
+ */
1217
+ declare class Dataset {
1218
+ readonly fileMeta: FileMeta | undefined;
1219
+ readonly warnings: readonly DicomParseWarning[];
1220
+ /**
1221
+ * Element map, keyed by uppercase 8-hex tag. Phase 3 promotes to
1222
+ * public navigation surface; Phase 2 keeps it `protected` so only
1223
+ * subclasses (`Item`, future Phase-3 extensions) can introspect.
1224
+ *
1225
+ * @internal
1226
+ */
1227
+ protected readonly _elements: ReadonlyMap<Tag, Element>;
1228
+ /** Memoised Phase 4 domain views (built once, on first access). */
1229
+ private _patient?;
1230
+ private _study?;
1231
+ private _series?;
1232
+ private _image?;
1233
+ /**
1234
+ * Construct a new structural `Dataset`. Phase 2 freezes the warnings
1235
+ * array at the model boundary (sibling `message.ts` lines 117–118).
1236
+ *
1237
+ * @internal
1238
+ */
1239
+ constructor(init: DatasetInit);
1240
+ /**
1241
+ * Look up a single element by tag. Tags are normalised to 8-char
1242
+ * uppercase hex, so `"7fe00010"` and `"7FE00010"` resolve to the same
1243
+ * element. Returns `undefined` when the tag is absent.
1244
+ *
1245
+ * @example
1246
+ * ```ts
1247
+ * import { parseDicom } from "@cosyte/dicom";
1248
+ * const ds = parseDicom(buf);
1249
+ * const rows = ds.get("00280010"); // Rows
1250
+ * if (rows?.value.kind === "numbers") console.log(rows.value.values[0]);
1251
+ * ```
1252
+ */
1253
+ get(tag: Tag): Element | undefined;
1254
+ /**
1255
+ * `true` when an element with the given tag is present (case-insensitive).
1256
+ *
1257
+ * @example
1258
+ * ```ts
1259
+ * import { parseDicom } from "@cosyte/dicom";
1260
+ * const ds = parseDicom(buf);
1261
+ * if (ds.has("00100010")) console.log("has Patient's Name");
1262
+ * ```
1263
+ */
1264
+ has(tag: Tag): boolean;
1265
+ /**
1266
+ * All elements in this dataset, in parse (insertion) order.
1267
+ *
1268
+ * @example
1269
+ * ```ts
1270
+ * import { parseDicom } from "@cosyte/dicom";
1271
+ * const ds = parseDicom(buf);
1272
+ * for (const el of ds.elements()) console.log(el.tag, el.vr);
1273
+ * ```
1274
+ */
1275
+ elements(): readonly Element[];
1276
+ /**
1277
+ * All elements matching a tag as an array (never `undefined`). A dataset
1278
+ * holds at most one element per tag, so this returns a 0- or 1-length
1279
+ * array — the convenience complement of {@link Dataset.get} for callers
1280
+ * that prefer an always-array shape.
1281
+ *
1282
+ * @example
1283
+ * ```ts
1284
+ * import { parseDicom } from "@cosyte/dicom";
1285
+ * const ds = parseDicom(buf);
1286
+ * for (const el of ds.getAll("00080060")) console.log(el.value);
1287
+ * ```
1288
+ */
1289
+ getAll(tag: Tag): readonly Element[];
1290
+ /**
1291
+ * Patient-identity view (§4.1). Fail-safe typed-absent fields; `id` is
1292
+ * **not** globally unique — match on the `{id, issuerOfId, ...}` tuple
1293
+ * plus `otherIds`, never on a bare `(0010,0020)`. Memoised on first read.
1294
+ *
1295
+ * @example
1296
+ * ```ts
1297
+ * import { parseDicom } from "@cosyte/dicom";
1298
+ * const p = parseDicom(buf).patient;
1299
+ * p.name?.alphabetic.familyName; // structured PN, never flattened
1300
+ * ```
1301
+ */
1302
+ get patient(): PatientView;
1303
+ /**
1304
+ * Study-identity view (§4.1). `instanceUid` is the cross-system study
1305
+ * key; `accessionNumber` ties it to the order. Memoised on first read.
1306
+ *
1307
+ * @example
1308
+ * ```ts
1309
+ * import { parseDicom } from "@cosyte/dicom";
1310
+ * const s = parseDicom(buf).study;
1311
+ * s.instanceUid; // "1.2.840.113619..."
1312
+ * ```
1313
+ */
1314
+ get study(): StudyView;
1315
+ /**
1316
+ * Series-identity & co-registration view (§4.1, §4.3). A shared
1317
+ * `frameOfReferenceUid` means images are spatially co-registered.
1318
+ * Memoised on first read.
1319
+ *
1320
+ * @example
1321
+ * ```ts
1322
+ * import { parseDicom } from "@cosyte/dicom";
1323
+ * const s = parseDicom(buf).series;
1324
+ * s.modality; // "CT"
1325
+ * ```
1326
+ */
1327
+ get series(): SeriesView;
1328
+ /**
1329
+ * Pixel-interpretation + geometry view (§4.2–§4.5). Surfaces exactly
1330
+ * what a renderer needs without guessing: `rescaleSlope`/`signed`/
1331
+ * `photometricInterpretation` stay absent rather than defaulted, and the
1332
+ * three pixel-spacing tags are distinct. Memoised on first read.
1333
+ *
1334
+ * @example
1335
+ * ```ts
1336
+ * import { parseDicom } from "@cosyte/dicom";
1337
+ * const img = parseDicom(buf).image;
1338
+ * img.rescaleSlope; // undefined ⇒ MUST NOT assume 1
1339
+ * ```
1340
+ */
1341
+ get image(): ImageView;
1342
+ }
1343
+
1344
+ /**
1345
+ * Public parser entry — `parseDicom`.
1346
+ *
1347
+ * Pipeline (Phase 2 plan 02-02):
1348
+ * 1. Dual EMPTY_INPUT check (raw input + post-normalize) per CONTEXT D-13.
1349
+ * 2. {@link normalizeInput} converts `Buffer | Uint8Array | ArrayBuffer`
1350
+ * to a zero-copy `Buffer` view per PARSE-04.
1351
+ * 3. {@link parsePart10Header} resolves preamble + DICM framing per D-14.
1352
+ * 4. {@link parseFileMeta} parses the (always Explicit VR LE) File Meta
1353
+ * group per FM-01 / D-17.
1354
+ * 5. {@link TRANSFER_SYNTAX_PARSERS} dispatches on TS UID per D-20;
1355
+ * unsupported UIDs throw `UNSUPPORTED_TRANSFER_SYNTAX` carrying the
1356
+ * `Dictionary.uid(uid)?.name` in `err.snippet`.
1357
+ * 6. The chosen strategy returns a `ReadonlyMap<Tag, Element>` (Phase 2
1358
+ * stubs — real bodies arrive in plans 02-03 / 02-04 / 02-05).
1359
+ * 7. The result is assembled into a structural {@link Dataset}.
1360
+ *
1361
+ * Phase 6 (D-45) wires a source/vendor `Profile` via `ParseOptions.profile`:
1362
+ * its `escalations` / `suppressions` reshape Tier-2 emission at the
1363
+ * {@link makeEmitter} chokepoint, and its private-dictionary overlay resolves
1364
+ * the Implicit VR of vendor private data elements.
1365
+ *
1366
+ * @module
1367
+ */
1368
+
1369
+ /**
1370
+ * Parse a DICOM Part 10 buffer into a structural {@link Dataset}.
1371
+ *
1372
+ * Lenient by default — recoverable deviations (missing preamble, File Meta
1373
+ * group-length mismatch, odd-length value, etc.) are pushed into
1374
+ * `ds.warnings` with stable codes from `WARNING_CODES`. Four unrecoverable
1375
+ * structural failures throw `DicomParseError`:
1376
+ *
1377
+ * - `EMPTY_INPUT` — empty `Buffer | Uint8Array | ArrayBuffer`.
1378
+ * - `NOT_DICOM_PART_10` — input lacks both `DICM` magic at offset 128 and
1379
+ * a recognizable `(0002,0000)` File Meta Group Length at offset 0.
1380
+ * - `INVALID_FILE_META` — File Meta is truncated or `(0002,0010)`
1381
+ * Transfer Syntax UID is missing.
1382
+ * - `UNSUPPORTED_TRANSFER_SYNTAX` — Transfer Syntax UID is not one of the
1383
+ * four v1 UIDs (`1.2.840.10008.1.2`, `…1.2.1`, `…1.2.2`, `…1.2.1.99`).
1384
+ *
1385
+ * Pass `{ strict: true }` to escalate every Tier-2 warning to a thrown
1386
+ * `DicomParseError` carrying the warning code.
1387
+ *
1388
+ * @example
1389
+ * ```ts
1390
+ * import { parseDicom, WARNING_CODES, DicomParseError } from "@cosyte/dicom";
1391
+ * import { readFileSync } from "node:fs";
1392
+ *
1393
+ * // Three input shapes (PARSE-04): Buffer, Uint8Array, ArrayBuffer.
1394
+ * const bytes = readFileSync("study.dcm");
1395
+ * const ds1 = parseDicom(bytes);
1396
+ * const ds2 = parseDicom(new Uint8Array(bytes));
1397
+ * const ds3 = parseDicom(bytes.buffer);
1398
+ *
1399
+ * // Inspect File Meta + warnings.
1400
+ * console.log(ds1.fileMeta?.transferSyntaxUID);
1401
+ * for (const w of ds1.warnings) {
1402
+ * if (w.code === WARNING_CODES.DICOM_MISSING_PREAMBLE) {
1403
+ * console.warn("bare File Meta input at offset", w.position.byteOffset);
1404
+ * }
1405
+ * }
1406
+ *
1407
+ * // Strict mode + onWarning callback.
1408
+ * try {
1409
+ * parseDicom(bytes, {
1410
+ * strict: true,
1411
+ * onWarning: (w) => console.error(w.code, "at offset", w.position.byteOffset),
1412
+ * });
1413
+ * } catch (err) {
1414
+ * if (err instanceof DicomParseError) {
1415
+ * console.error(err.code, err.byteOffset, err.snippet);
1416
+ * }
1417
+ * }
1418
+ * ```
1419
+ */
1420
+ declare function parseDicom(input: Buffer | Uint8Array | ArrayBuffer): Dataset;
1421
+ declare function parseDicom(input: Buffer | Uint8Array | ArrayBuffer, options: ParseOptions): Dataset;
1422
+
1423
+ /**
1424
+ * Phase 2 structural `Sequence` — wrapper around an SQ element's items.
1425
+ *
1426
+ * Per `02-CONTEXT.md` D-04: structural surface only — `items` and
1427
+ * `length`. No navigation methods. Phase 3 adds `Sequence.items[N].get(...)`
1428
+ * via `Item`'s `Dataset`-superclass extension per D-42.
1429
+ *
1430
+ * @module
1431
+ */
1432
+
1433
+ /**
1434
+ * One SQ (Sequence) element's structural body.
1435
+ *
1436
+ * `length` is the on-wire length: a real byte count for defined-length
1437
+ * SQ, or `0xFFFFFFFF` (4_294_967_295) when undefined-length per D-29.
1438
+ *
1439
+ * @example
1440
+ * ```ts
1441
+ * import { Sequence } from "@cosyte/dicom";
1442
+ * // Producers (parser plan 02-04) construct sequences as follows:
1443
+ * // const sq = new Sequence([item0, item1], 0xFFFFFFFF);
1444
+ * ```
1445
+ */
1446
+ declare class Sequence {
1447
+ readonly items: readonly Item[];
1448
+ readonly length: number;
1449
+ /**
1450
+ * Construct a new structural `Sequence`. The `items` array is frozen
1451
+ * at the constructor boundary so downstream mutation cannot escape.
1452
+ *
1453
+ * @internal
1454
+ */
1455
+ constructor(items: readonly Item[], length: number);
1456
+ }
1457
+
1458
+ /**
1459
+ * Fatal error taxonomy for the `@cosyte/dicom` parser pipeline.
1460
+ *
1461
+ * Phase 2 core-parser context:
1462
+ * - D-09 — `FATAL_CODES` is a frozen `as const` registry with EXACTLY
1463
+ * four codes; anything less severe MUST be a Tier-2 warning.
1464
+ * - D-10 — `DicomParseError` carries `code`, `byteOffset`, `snippet`
1465
+ * (up to 16 source bytes, space-separated lowercase hex), and an
1466
+ * optional `contextPath`. The thrown `Error.message` is formatted
1467
+ * `[CODE] msg (offset=N)` with `… in path/segments` appended when
1468
+ * `contextPath` is provided.
1469
+ *
1470
+ * @module
1471
+ */
1472
+
1473
+ /**
1474
+ * Stable string codes for every Tier-3 fatal the parser may throw.
1475
+ *
1476
+ * Locked at four codes per `PROJECT.md` "Fatal errors only for unrecoverable
1477
+ * structural corruption": anything less severe MUST be a Tier-2 warning
1478
+ * (see `./warnings.ts`). Consumers narrow on `err.code` to react to
1479
+ * specific structural failures.
1480
+ *
1481
+ * @example
1482
+ * ```ts
1483
+ * import { parseDicom, FATAL_CODES, DicomParseError } from "@cosyte/dicom";
1484
+ * try {
1485
+ * parseDicom(Buffer.alloc(0));
1486
+ * } catch (err) {
1487
+ * if (err instanceof DicomParseError && err.code === FATAL_CODES.EMPTY_INPUT) {
1488
+ * // handle empty input
1489
+ * }
1490
+ * }
1491
+ * ```
1492
+ */
1493
+ declare const FATAL_CODES: {
1494
+ readonly NOT_DICOM_PART_10: "NOT_DICOM_PART_10";
1495
+ readonly INVALID_FILE_META: "INVALID_FILE_META";
1496
+ readonly UNSUPPORTED_TRANSFER_SYNTAX: "UNSUPPORTED_TRANSFER_SYNTAX";
1497
+ readonly EMPTY_INPUT: "EMPTY_INPUT";
1498
+ };
1499
+ /**
1500
+ * Discriminant type for `DicomParseError.code`. Narrowing a caught error
1501
+ * by this code lets consumers write exhaustive `switch` blocks (enabled
1502
+ * by the `switch-exhaustiveness-check` lint rule) and guarantees a
1503
+ * typo-free comparison against the `FATAL_CODES` registry.
1504
+ *
1505
+ * @example
1506
+ * ```ts
1507
+ * import type { FatalCode } from "@cosyte/dicom";
1508
+ * function describe(code: FatalCode): string {
1509
+ * switch (code) {
1510
+ * case "EMPTY_INPUT":
1511
+ * return "input was empty";
1512
+ * case "NOT_DICOM_PART_10":
1513
+ * return "input is not a DICOM Part 10 file";
1514
+ * case "INVALID_FILE_META":
1515
+ * return "File Meta group is missing or malformed";
1516
+ * case "UNSUPPORTED_TRANSFER_SYNTAX":
1517
+ * return "Transfer Syntax UID is not supported by v1";
1518
+ * }
1519
+ * }
1520
+ * ```
1521
+ */
1522
+ type FatalCode = (typeof FATAL_CODES)[keyof typeof FATAL_CODES];
1523
+ /**
1524
+ * Thrown by `parseDicom` when the input violates one of the four
1525
+ * unrecoverable Tier-3 structural rules — or, under `{ strict: true }`,
1526
+ * when any Tier-2 warning is escalated through the single `emit`
1527
+ * chokepoint (D-35). Carries byte-offset positional context plus a short
1528
+ * source snippet so consumers can log actionable errors.
1529
+ *
1530
+ * Message format: `[CODE] msg (offset=N)`, with `… in a/b/c` appended
1531
+ * when `contextPath` is provided.
1532
+ *
1533
+ * @remarks
1534
+ * Snippets may contain PHI when parsing real clinical files — redact at
1535
+ * the call site if required by your compliance posture. The library does
1536
+ * not redact snippets itself.
1537
+ *
1538
+ * @example
1539
+ * ```ts
1540
+ * import { parseDicom, DicomParseError } from "@cosyte/dicom";
1541
+ * try {
1542
+ * parseDicom(buffer);
1543
+ * } catch (err) {
1544
+ * if (err instanceof DicomParseError && err.code === "NOT_DICOM_PART_10") {
1545
+ * // err.byteOffset, err.snippet, err.contextPath all available
1546
+ * }
1547
+ * }
1548
+ * ```
1549
+ */
1550
+ declare class DicomParseError extends Error {
1551
+ readonly code: FatalCode;
1552
+ readonly byteOffset: number;
1553
+ readonly snippet: string;
1554
+ readonly contextPath: readonly string[] | undefined;
1555
+ /**
1556
+ * Construct a new `DicomParseError`. All fields except `contextPath` are
1557
+ * required so every thrower populates positional context per `TOL-02`.
1558
+ *
1559
+ * @internal
1560
+ */
1561
+ constructor(code: FatalCode, message: string, byteOffset: number, snippet: string, contextPath?: readonly string[]);
1562
+ }
1563
+
1564
+ /**
1565
+ * VR-aware value dispatch — the body behind `Element.value`.
1566
+ *
1567
+ * `decodeElementValue` maps an `Element`'s `vr` + raw bytes to a typed
1568
+ * {@link DicomValue}, applying the fail-safe rules from PS3.5 §6.2:
1569
+ *
1570
+ * - **Signedness / endianness from the VR + transfer syntax**, never
1571
+ * guessed (numeric VRs via `./numeric`).
1572
+ * - **Padding**: `UI` strips trailing NULL (`0x00`); every other string VR
1573
+ * strips trailing SPACE (`0x20`). A mismatched pad is tolerated and
1574
+ * flagged (`DICOM_UI_TRAILING_SPACE` / `DICOM_TRAILING_NULL_IN_TEXT_VR`).
1575
+ * - **Charset**: `LO SH UC LT ST UT PN` decode through `(0008,0005)`;
1576
+ * the remaining string VRs are the Default Repertoire (ASCII, decoded
1577
+ * Latin-1-lenient with `DICOM_NON_ASCII_IN_ASCII_VR` on stray bytes).
1578
+ * - **Numeric strings** `DS`/`IS` → `number | null` (never `NaN`→0).
1579
+ * - **Temporal** `DA`/`TM`/`DT` → tolerant typed values (warn + preserve).
1580
+ * - **Bulk** `OB OD OF OL OV OW UN` → raw `binary` (not interpreted in v1).
1581
+ *
1582
+ * Decode is lazy/post-parse, so any deviation surfaces on the returned
1583
+ * value's `warnings` (it cannot join the frozen `Dataset.warnings`).
1584
+ *
1585
+ * This module is kept OUT of `vr/index.ts` so the dispatch logic is
1586
+ * coverage-measured (barrels are excluded from the gate).
1587
+ *
1588
+ * @module
1589
+ */
1590
+
1591
+ /**
1592
+ * Decode an {@link Element}'s value to a typed {@link DicomValue}. Pure +
1593
+ * fail-safe: it never throws and never coerces a malformed value to a
1594
+ * plausible-but-wrong one. Called lazily (and memoized) by `Element.value`.
1595
+ *
1596
+ * @example
1597
+ * ```ts
1598
+ * import { parseDicom } from "@cosyte/dicom";
1599
+ * const ds = parseDicom(buf);
1600
+ * const v = ds.get("00280010")?.value; // Rows (US)
1601
+ * if (v?.kind === "numbers") console.log(v.values[0]);
1602
+ * ```
1603
+ */
1604
+ declare function decodeElementValue(element: Element): DicomValue;
1605
+
1606
+ /**
1607
+ * Specific Character Set `(0008,0005)` resolution + text decoding for the
1608
+ * Phase 3 value decoders.
1609
+ *
1610
+ * Zero runtime deps: decoding rides on Node 22's full-ICU `TextDecoder`
1611
+ * (the `@cosyte/dicom` ≤3-dep ceiling keeps `iconv-lite` in reserve only if
1612
+ * a future multi-extension CJK case forces it).
1613
+ *
1614
+ * Term-list corrections verified against PS3.3 §C.12.1.1.2 for this build
1615
+ * (the easy-to-get-wrong ones):
1616
+ * - There is **no `ISO_IR 14`** — only `ISO_IR 13` (JIS X 0201 Katakana).
1617
+ * - `IR 87` / `IR 159` are **code-extension-only**: the bare `ISO_IR 87` /
1618
+ * `ISO_IR 159` terms do NOT exist; only `ISO 2022 IR 87` / `ISO 2022 IR
1619
+ * 159` are valid (so they are absent from the single-byte term list).
1620
+ * - **`ISO_IR 203`** (Latin-9 → ISO-8859-15) IS included.
1621
+ *
1622
+ * @module
1623
+ */
1624
+
1625
+ /**
1626
+ * Parse a `(0008,0005)` Specific Character Set value (a `CS`, possibly
1627
+ * multi-valued via `\`) into its trimmed defined terms.
1628
+ *
1629
+ * The first value may be empty (the ISO-2022 "G0 starts as ISO_IR 6" rule);
1630
+ * it is preserved as `""` rather than dropped so {@link resolveDecoderLabel}
1631
+ * can apply the default-repertoire fallback.
1632
+ *
1633
+ * @example
1634
+ * ```ts
1635
+ * import { parseSpecificCharacterSet } from "@cosyte/dicom";
1636
+ * // bytes for "ISO 2022 IR 6\\ISO 2022 IR 87"
1637
+ * const terms = parseSpecificCharacterSet(buf);
1638
+ * ```
1639
+ */
1640
+ declare function parseSpecificCharacterSet(bytes: Buffer): readonly string[];
1641
+ /**
1642
+ * `true` when `term` is a Specific Character Set defined term this build can
1643
+ * map to a decoder (the empty default-repertoire term counts as known).
1644
+ *
1645
+ * @example
1646
+ * ```ts
1647
+ * import { isKnownCharsetTerm } from "@cosyte/dicom";
1648
+ * isKnownCharsetTerm("ISO_IR 192"); // true
1649
+ * isKnownCharsetTerm("ISO_IR 14"); // false — does not exist
1650
+ * ```
1651
+ */
1652
+ declare function isKnownCharsetTerm(term: string): boolean;
1653
+ /**
1654
+ * Resolve the `TextDecoder` label to use for a value, given the dataset's
1655
+ * Specific Character Set terms. Prefers a multibyte/extended decoder when
1656
+ * the term list mixes single-byte and code-extension sets; falls back to
1657
+ * the default repertoire (Latin-1, lenient) when no term resolves.
1658
+ *
1659
+ * @example
1660
+ * ```ts
1661
+ * import { resolveDecoderLabel } from "@cosyte/dicom";
1662
+ * resolveDecoderLabel(["ISO 2022 IR 6", "ISO 2022 IR 87"]); // "iso-2022-jp"
1663
+ * resolveDecoderLabel(undefined); // "latin1"
1664
+ * ```
1665
+ */
1666
+ declare function resolveDecoderLabel(terms: readonly string[] | undefined): string;
1667
+ /**
1668
+ * Decode bytes to a string under the resolved Specific Character Set,
1669
+ * never throwing: an unsupported decoder label falls back to UTF-8, then to
1670
+ * Latin-1 (1:1, cannot fail).
1671
+ *
1672
+ * @example
1673
+ * ```ts
1674
+ * import { decodeText } from "@cosyte/dicom";
1675
+ * const s = decodeText(buf, ["ISO_IR 192"]); // UTF-8
1676
+ * ```
1677
+ */
1678
+ declare function decodeText(bytes: Buffer, terms: readonly string[] | undefined): string;
1679
+
1680
+ /**
1681
+ * `PN` (Person Name) structured decode per PS3.5 §6.2.1.1.
1682
+ *
1683
+ * A PN value has up to three component groups separated by `=` (alphabetic,
1684
+ * ideographic, phonetic); each group has five `^`-delimited components
1685
+ * (family, given, middle, prefix, suffix). Trailing empty components and
1686
+ * groups are dropped on the wire, so decoding fills the missing slots with
1687
+ * the empty string and omits absent ideographic / phonetic groups.
1688
+ *
1689
+ * @module
1690
+ */
1691
+
1692
+ /**
1693
+ * Parse a single (already charset-decoded, pad-trimmed) PN value string into
1694
+ * its structured {@link PersonName} form.
1695
+ *
1696
+ * @example
1697
+ * ```ts
1698
+ * import { parsePersonName } from "@cosyte/dicom";
1699
+ * const pn = parsePersonName("Doe^Jane^^Dr^");
1700
+ * pn.alphabetic.familyName; // "Doe"
1701
+ * pn.alphabetic.namePrefix; // "Dr"
1702
+ * ```
1703
+ */
1704
+ declare function parsePersonName(value: string): PersonName;
1705
+
1706
+ /**
1707
+ * Tolerant temporal decoders for `DA` / `TM` / `DT` (PS3.5 §6.2).
1708
+ *
1709
+ * Every decoder is fail-safe: a malformed value yields `valid: false` with
1710
+ * the raw string preserved and the numeric fields omitted — it NEVER throws
1711
+ * and NEVER coerces to a plausible-but-wrong date. Tolerated legacy forms
1712
+ * (retired dotted `YYYY.MM.DD`, partial precision, non-standard offsets) are
1713
+ * decoded best-effort and flagged to the caller so it can emit the matching
1714
+ * warning. The raw string is the source of truth on any failure.
1715
+ *
1716
+ * @module
1717
+ */
1718
+
1719
+ /**
1720
+ * Decode a single `DA` value. `legacy` is `true` for any tolerated
1721
+ * non-canonical form (retired dotted `YYYY.MM.DD`, or an unparseable value
1722
+ * such as `"ANONYMIZED"`), signalling the caller to emit
1723
+ * `DICOM_DA_LEGACY_FORMAT`.
1724
+ *
1725
+ * @example
1726
+ * ```ts
1727
+ * import { parseDate } from "@cosyte/dicom";
1728
+ * parseDate("20240115").value.valid; // true
1729
+ * parseDate("2024.01.15").legacy; // true
1730
+ * ```
1731
+ */
1732
+ declare function parseDate(raw: string): {
1733
+ value: DicomDate;
1734
+ legacy: boolean;
1735
+ };
1736
+ /**
1737
+ * Decode a single `TM` value (max 14 bytes). Precision may be truncated from
1738
+ * the right (`HH`, `HHMM`, `HHMMSS`, `HHMMSS.FFFFFF`).
1739
+ *
1740
+ * @example
1741
+ * ```ts
1742
+ * import { parseTime } from "@cosyte/dicom";
1743
+ * parseTime("133015.5").value.fractionalSeconds; // 0.5
1744
+ * ```
1745
+ */
1746
+ declare function parseTime(raw: string): {
1747
+ value: DicomTime;
1748
+ };
1749
+ /**
1750
+ * Decode a single `DT` value. `nonstandardOffset` is `true` when a UTC
1751
+ * offset suffix is present but malformed / out of range, signalling the
1752
+ * caller to emit `DICOM_DT_NONSTANDARD_OFFSET`.
1753
+ *
1754
+ * @example
1755
+ * ```ts
1756
+ * import { parseDateTime } from "@cosyte/dicom";
1757
+ * parseDateTime("20240115133015+0100").value.offsetMinutes; // 60
1758
+ * ```
1759
+ */
1760
+ declare function parseDateTime(raw: string): {
1761
+ value: DicomDateTime;
1762
+ nonstandardOffset: boolean;
1763
+ };
1764
+
1765
+ /**
1766
+ * Coded-terminology surfacing (§4.6, PS3.16 §8 Table 8-1).
1767
+ *
1768
+ * A coded concept is the triplet `Code Value (0008,0100)` /
1769
+ * `Coding Scheme Designator (0008,0102)` / `Code Meaning (0008,0104)`. This
1770
+ * module **surfaces** the triplet and resolves the canonical scheme OID for
1771
+ * the four standard designators — it performs **no lookup, validation, or
1772
+ * mapping** (deferred). Detaching a code value from its scheme is unsafe, so
1773
+ * the three parts always travel together.
1774
+ *
1775
+ * @module
1776
+ */
1777
+
1778
+ /**
1779
+ * Canonical coding-scheme OIDs (PS3.16 §8). **Only** the four standard
1780
+ * designators are mapped. Legacy SNOMED designators (`SRT` / `SNM3` /
1781
+ * `99SDM`) are intentionally absent: their code values differ from `SCT`
1782
+ * (CP-730), so resolving them to the SNOMED OID would imply a false
1783
+ * equality.
1784
+ *
1785
+ * @example
1786
+ * ```ts
1787
+ * import { CODING_SCHEME_OIDS } from "@cosyte/dicom";
1788
+ * CODING_SCHEME_OIDS.SCT; // "2.16.840.1.113883.6.96"
1789
+ * ```
1790
+ */
1791
+ declare const CODING_SCHEME_OIDS: {
1792
+ readonly DCM: "1.2.840.10008.2.16.4";
1793
+ readonly SCT: "2.16.840.1.113883.6.96";
1794
+ readonly UCUM: "2.16.840.1.113883.6.8";
1795
+ readonly LN: "2.16.840.1.113883.6.1";
1796
+ };
1797
+ /**
1798
+ * Resolve a coding-scheme designator to its canonical OID, or `undefined`
1799
+ * for any non-standard / legacy designator (including `SRT`/`SNM3`/`99SDM`,
1800
+ * which are NOT treated as `SCT`).
1801
+ *
1802
+ * @example
1803
+ * ```ts
1804
+ * import { codingSchemeOid } from "@cosyte/dicom";
1805
+ * codingSchemeOid("UCUM"); // "2.16.840.1.113883.6.8"
1806
+ * codingSchemeOid("SRT"); // undefined — not SCT (CP-730)
1807
+ * ```
1808
+ */
1809
+ declare function codingSchemeOid(designator: string | undefined): string | undefined;
1810
+ /**
1811
+ * Read the coded triplet off a code-item dataset (e.g. one item of a Code
1812
+ * Sequence). Every part is independently fail-safe; `schemeUid` is the
1813
+ * resolved OID for the designator when standard, else `undefined`.
1814
+ *
1815
+ * @example
1816
+ * ```ts
1817
+ * import { parseDicom, readCode } from "@cosyte/dicom";
1818
+ * const units = parseDicom(buf).image.realWorldValueMaps?.[0]?.unitsCode;
1819
+ * // units?.codeValue / units?.codingSchemeDesignator / units?.codeMeaning
1820
+ * ```
1821
+ */
1822
+ declare function readCode(item: Dataset): CodedConcept;
1823
+
1824
+ /**
1825
+ * Value-layer error taxonomy for the Phase 4 domain helpers.
1826
+ *
1827
+ * This is **separate** from the parser's four-code `FATAL_CODES` /
1828
+ * `DicomParseError` (locked to unrecoverable Part 10 structural corruption
1829
+ * — see `../../parser/errors.ts`). The §4 helpers are otherwise fail-safe:
1830
+ * a missing *value* is typed-absent (`undefined`), never an exception. The
1831
+ * helper layer throws only for a **structural contract violation** the
1832
+ * caller asked it to resolve and that cannot be answered safely:
1833
+ *
1834
+ * - `FRAME_INDEX_OUT_OF_RANGE` — `image.frame(i)` called with an index
1835
+ * outside `[0, numberOfFrames)`. Returning a guessed frame would be a
1836
+ * silent wrong-image, so we refuse.
1837
+ * - `MISSING_REQUIRED_FUNCTIONAL_GROUP` — an Enhanced multi-frame object
1838
+ * lacks a required geometry macro (Pixel Measures / Plane Position /
1839
+ * Plane Orientation) in *both* the Per-Frame and Shared groups, so the
1840
+ * frame cannot be spatially placed.
1841
+ *
1842
+ * @module
1843
+ */
1844
+ /**
1845
+ * Stable string codes the Phase 4 helpers may throw. Narrow on
1846
+ * {@link DicomValueError.code} to react to a specific contract violation.
1847
+ *
1848
+ * @example
1849
+ * ```ts
1850
+ * import { VALUE_ERROR_CODES } from "@cosyte/dicom";
1851
+ * VALUE_ERROR_CODES.FRAME_INDEX_OUT_OF_RANGE; // "FRAME_INDEX_OUT_OF_RANGE"
1852
+ * ```
1853
+ */
1854
+ declare const VALUE_ERROR_CODES: {
1855
+ readonly FRAME_INDEX_OUT_OF_RANGE: "FRAME_INDEX_OUT_OF_RANGE";
1856
+ readonly MISSING_REQUIRED_FUNCTIONAL_GROUP: "MISSING_REQUIRED_FUNCTIONAL_GROUP";
1857
+ };
1858
+ /**
1859
+ * Discriminant for {@link DicomValueError.code}, enabling exhaustive
1860
+ * `switch` narrowing (the `switch-exhaustiveness-check` lint rule).
1861
+ *
1862
+ * @example
1863
+ * ```ts
1864
+ * import type { ValueErrorCode } from "@cosyte/dicom";
1865
+ * function describe(code: ValueErrorCode): string {
1866
+ * switch (code) {
1867
+ * case "FRAME_INDEX_OUT_OF_RANGE":
1868
+ * return "frame index outside [0, numberOfFrames)";
1869
+ * case "MISSING_REQUIRED_FUNCTIONAL_GROUP":
1870
+ * return "enhanced object lacks a required geometry macro";
1871
+ * }
1872
+ * }
1873
+ * ```
1874
+ */
1875
+ type ValueErrorCode = (typeof VALUE_ERROR_CODES)[keyof typeof VALUE_ERROR_CODES];
1876
+ /**
1877
+ * Thrown by the Phase 4 helpers for a structural contract violation that
1878
+ * cannot be answered safely (see module doc). Never carries a decoded
1879
+ * value, so it is safe to log without leaking PHI: the `message` is built
1880
+ * only from the code and structural facts (indices, tag/macro names).
1881
+ *
1882
+ * @example
1883
+ * ```ts
1884
+ * import { parseDicom, DicomValueError } from "@cosyte/dicom";
1885
+ * const img = parseDicom(buf).image;
1886
+ * try {
1887
+ * img.frame(9999);
1888
+ * } catch (err) {
1889
+ * if (err instanceof DicomValueError && err.code === "FRAME_INDEX_OUT_OF_RANGE") {
1890
+ * // handle the out-of-range request
1891
+ * }
1892
+ * }
1893
+ * ```
1894
+ */
1895
+ declare class DicomValueError extends Error {
1896
+ readonly code: ValueErrorCode;
1897
+ /**
1898
+ * Construct a new `DicomValueError`. The `message` MUST be built only
1899
+ * from structural facts (never a decoded attribute value) so the error
1900
+ * is always safe to log.
1901
+ *
1902
+ * @internal
1903
+ */
1904
+ constructor(code: ValueErrorCode, message: string);
1905
+ }
1906
+
1907
+ /**
1908
+ * `serializeDicom` — the Phase 5 Part 10 writer (the conservative half of
1909
+ * Postel's Law).
1910
+ *
1911
+ * Takes a {@link Dataset} (from `parseDicom`, or hand-built) and emits a
1912
+ * spec-clean DICOM Part 10 byte stream:
1913
+ *
1914
+ * 1. 128-byte zero preamble + `DICM` magic (PS3.10 §7.1).
1915
+ * 2. File Meta group `0002`, always Explicit VR LE, with a correct
1916
+ * `(0002,0000)` group length — see {@link encodeFileMeta}.
1917
+ * 3. The dataset body, in the dataset's own transfer syntax (**no
1918
+ * transcode**): Implicit VR LE, Explicit VR LE/BE, or — for the Deflated
1919
+ * syntax — an Explicit VR LE body run through RFC 1951 raw deflate
1920
+ * (`zlib.deflateRawSync`), symmetric to the parser's `inflateRawSync`.
1921
+ *
1922
+ * Conservative behaviour (PS3.5): scalar values are padded to even length on
1923
+ * write (§6.2); retired `(gggg,0000)` group-length elements are omitted from
1924
+ * the dataset (§7.2); short/long-form headers are chosen by VR (§7.1.2);
1925
+ * sequence and encapsulated-pixel-data spans pass through byte-for-byte
1926
+ * (§7.5 / §A.4).
1927
+ *
1928
+ * @module
1929
+ */
1930
+
1931
+ /**
1932
+ * Serialize a {@link Dataset} to a spec-clean DICOM Part 10 `Buffer`.
1933
+ *
1934
+ * The dataset's transfer syntax is preserved (no transcoding): pixel-data
1935
+ * fragments and nested sequences are written back byte-for-byte, while scalar
1936
+ * values are re-emitted with correct even-length padding and File Meta group
1937
+ * length. Pure function — the input `Dataset` is never mutated.
1938
+ *
1939
+ * **Input contract.** The writer is designed for a {@link Dataset} produced by
1940
+ * `parseDicom`: it relies on the parser's `Element.rawBytes` representation
1941
+ * (value-only for scalars and Implicit-LE defined-length `SQ`; full on-wire span
1942
+ * for Explicit `SQ`, undefined-length spans, encapsulated Pixel Data, and the
1943
+ * `UN`/CP-246 fallbacks). A hand-built `Dataset` must follow the same
1944
+ * convention for its bytes to be encoded correctly.
1945
+ *
1946
+ * **Round-trip scope.** `parseDicom(out)` re-reads to a dataset that is equal
1947
+ * over the *modeled* surface (every dataset element + the typed
1948
+ * {@link "../dataset/file-meta".FileMeta} fields plus any non-modeled File Meta
1949
+ * elements preserved on `extraElements`), not a byte-exact copy of the original
1950
+ * file: the 128-byte preamble is normalized to zeros, the File Meta group is
1951
+ * rebuilt in ascending tag order (modeled fields + `extraElements` — see
1952
+ * {@link encodeFileMeta}), odd-length values are padded even, and retired
1953
+ * `(gggg,0000)` group lengths are dropped.
1954
+ *
1955
+ * @throws {@link DicomSerializeError} with code `MISSING_TRANSFER_SYNTAX` when
1956
+ * the dataset has no File Meta Transfer Syntax UID, or
1957
+ * `UNSUPPORTED_TRANSFER_SYNTAX` when that UID is outside the v1 set.
1958
+ *
1959
+ * @example
1960
+ * ```ts
1961
+ * import { parseDicom, serializeDicom } from "@cosyte/dicom";
1962
+ * const ds = parseDicom(buf);
1963
+ * const out = serializeDicom(ds); // spec-clean Part 10, same transfer syntax
1964
+ * // parseDicom(out) re-reads to a structurally-equal dataset.
1965
+ * ```
1966
+ */
1967
+ declare function serializeDicom(ds: Dataset): Buffer;
1968
+
1969
+ /**
1970
+ * Serializer error taxonomy for the Phase 5 Part 10 writer.
1971
+ *
1972
+ * Like the Phase 4 {@link "../dataset/helpers/errors".DicomValueError}, this
1973
+ * is **separate** from the parser's four-code `FATAL_CODES` /
1974
+ * `DicomParseError` (which are locked to *read*-side Part 10 structural
1975
+ * corruption — see `../parser/errors.ts`). The serializer throws only when
1976
+ * it is asked to emit a buffer it cannot make spec-clean:
1977
+ *
1978
+ * - `MISSING_TRANSFER_SYNTAX` — the `Dataset` has no `fileMeta`, or its
1979
+ * `fileMeta.transferSyntaxUID` is empty. The Transfer Syntax UID is the
1980
+ * dispatch input that decides every byte of the encoding, so there is no
1981
+ * safe default to fall back to.
1982
+ * - `UNSUPPORTED_TRANSFER_SYNTAX` — the Transfer Syntax UID is not one of
1983
+ * the four v1 syntaxes the writer (and parser) support. The writer never
1984
+ * transcodes, so it cannot emit a syntax it does not understand.
1985
+ *
1986
+ * The message is built only from the Transfer Syntax UID and structural
1987
+ * facts (never a decoded attribute value), so it is always safe to log.
1988
+ *
1989
+ * @module
1990
+ */
1991
+ /**
1992
+ * Stable string codes the Phase 5 serializer may throw. Narrow on
1993
+ * {@link DicomSerializeError.code} to react to a specific failure.
1994
+ *
1995
+ * @example
1996
+ * ```ts
1997
+ * import { SERIALIZE_ERROR_CODES } from "@cosyte/dicom";
1998
+ * SERIALIZE_ERROR_CODES.MISSING_TRANSFER_SYNTAX; // "MISSING_TRANSFER_SYNTAX"
1999
+ * ```
2000
+ */
2001
+ declare const SERIALIZE_ERROR_CODES: {
2002
+ readonly MISSING_TRANSFER_SYNTAX: "MISSING_TRANSFER_SYNTAX";
2003
+ readonly UNSUPPORTED_TRANSFER_SYNTAX: "UNSUPPORTED_TRANSFER_SYNTAX";
2004
+ };
2005
+ /**
2006
+ * Discriminant for {@link DicomSerializeError.code}, enabling exhaustive
2007
+ * `switch` narrowing (the `switch-exhaustiveness-check` lint rule).
2008
+ *
2009
+ * @example
2010
+ * ```ts
2011
+ * import type { SerializeErrorCode } from "@cosyte/dicom";
2012
+ * function describe(code: SerializeErrorCode): string {
2013
+ * switch (code) {
2014
+ * case "MISSING_TRANSFER_SYNTAX":
2015
+ * return "dataset has no Transfer Syntax UID to serialize under";
2016
+ * case "UNSUPPORTED_TRANSFER_SYNTAX":
2017
+ * return "Transfer Syntax UID is outside the v1 set";
2018
+ * }
2019
+ * }
2020
+ * ```
2021
+ */
2022
+ type SerializeErrorCode = (typeof SERIALIZE_ERROR_CODES)[keyof typeof SERIALIZE_ERROR_CODES];
2023
+ /**
2024
+ * Thrown by `serializeDicom` when a `Dataset`
2025
+ * cannot be emitted as spec-clean Part 10. Never carries a decoded value, so
2026
+ * it is safe to log without leaking PHI: the `message` is built only from the
2027
+ * code and the offending Transfer Syntax UID.
2028
+ *
2029
+ * @example
2030
+ * ```ts
2031
+ * import { parseDicom, serializeDicom, DicomSerializeError } from "@cosyte/dicom";
2032
+ * const ds = parseDicom(buf);
2033
+ * try {
2034
+ * serializeDicom(ds);
2035
+ * } catch (err) {
2036
+ * if (err instanceof DicomSerializeError && err.code === "MISSING_TRANSFER_SYNTAX") {
2037
+ * // dataset is missing the File Meta Transfer Syntax UID
2038
+ * }
2039
+ * }
2040
+ * ```
2041
+ */
2042
+ declare class DicomSerializeError extends Error {
2043
+ readonly code: SerializeErrorCode;
2044
+ /**
2045
+ * Construct a new `DicomSerializeError`. The `message` MUST be built only
2046
+ * from structural facts (never a decoded attribute value) so the error is
2047
+ * always safe to log.
2048
+ *
2049
+ * @internal
2050
+ */
2051
+ constructor(code: SerializeErrorCode, message: string);
2052
+ }
2053
+
2054
+ /**
2055
+ * `defineProfile()` — the public factory for building immutable {@link Profile}
2056
+ * objects (Phase 6). Mirrors the `@cosyte/hl7` sibling's profile factory: a
2057
+ * validated options object in, a frozen `Profile` with a `describe()` method
2058
+ * out. Invalid input throws {@link ProfileDefinitionError} with an actionable
2059
+ * message rather than producing a half-built profile.
2060
+ *
2061
+ * A profile composes via `extends` (single parent or an array). Merge rules:
2062
+ * - `lineage` = parents' lineages then this name, de-duplicated in order.
2063
+ * - `escalations` / `suppressions` = set union of parents' and this
2064
+ * profile's, with the contradiction guard (a code may not be both
2065
+ * escalated and suppressed) applied to the *merged* result.
2066
+ * - `privateDictionary` = parents merged left-to-right, then this profile's
2067
+ * entries layered on top (child wins on a `(creator, key)` collision).
2068
+ * - `description` = this profile's, else the first parent that declares one.
2069
+ *
2070
+ * Zero runtime deps; immutability is enforced at the return boundary via
2071
+ * `Object.freeze` (top-level) over already-frozen inner maps/sets.
2072
+ *
2073
+ * @module
2074
+ */
2075
+
2076
+ /**
2077
+ * One vendor's private-dictionary overlay as authored: canonical
2078
+ * `"GGGGXXLL"` key → definition. Case-insensitive on input; normalized to
2079
+ * uppercase on store.
2080
+ *
2081
+ * @example
2082
+ * ```ts
2083
+ * import type { ProfilePrivateTags } from "@cosyte/dicom";
2084
+ * const csa: ProfilePrivateTags = {
2085
+ * "0029XX10": { vr: "OB", keyword: "CSAImageHeaderInfo", name: "CSA Image Header Info" },
2086
+ * };
2087
+ * ```
2088
+ */
2089
+ type ProfilePrivateTags = Readonly<Record<string, PrivateTagDefinition>>;
2090
+ /**
2091
+ * Options accepted by {@link defineProfile}. Only `name` is required; every
2092
+ * other field defaults to empty. With `exactOptionalPropertyTypes`, omit an
2093
+ * unset key rather than passing `undefined`.
2094
+ *
2095
+ * @example
2096
+ * ```ts
2097
+ * import { defineProfile, WARNING_CODES } from "@cosyte/dicom";
2098
+ * const lenientCd = defineProfile({
2099
+ * name: "lenient-cd",
2100
+ * description: "Tolerant of conformance-loose archive CDs",
2101
+ * suppress: [WARNING_CODES.DICOM_ODD_LENGTH_VALUE_PADDED],
2102
+ * });
2103
+ * ```
2104
+ */
2105
+ interface DefineProfileOptions {
2106
+ readonly name: string;
2107
+ readonly description?: string;
2108
+ readonly escalate?: readonly WarningCode[];
2109
+ readonly suppress?: readonly WarningCode[];
2110
+ readonly privateTags?: Readonly<Record<string, ProfilePrivateTags>>;
2111
+ readonly extends?: Profile | readonly Profile[];
2112
+ }
2113
+ /**
2114
+ * Build a frozen {@link Profile} from a validated options object.
2115
+ *
2116
+ * @example
2117
+ * ```ts
2118
+ * import { defineProfile, parseDicom } from "@cosyte/dicom";
2119
+ * const acme = defineProfile({
2120
+ * name: "acme",
2121
+ * description: "ACME PACS quirks",
2122
+ * privateTags: {
2123
+ * "ACME_PRIV_01": {
2124
+ * "0019XX10": { vr: "DS", keyword: "AcmeDose", name: "ACME Dose" },
2125
+ * },
2126
+ * },
2127
+ * });
2128
+ * const ds = parseDicom(buf, { profile: acme });
2129
+ * console.log(acme.describe?.());
2130
+ * ```
2131
+ */
2132
+ declare function defineProfile(opts: DefineProfileOptions): Profile;
2133
+
2134
+ /**
2135
+ * `ProfileDefinitionError` — the single typed failure surface for
2136
+ * `defineProfile()`. Thrown (never returned) when profile-author input is
2137
+ * malformed: a bad name, an unknown option key, an unknown warning code, an
2138
+ * invalid VR, or a private-tag key that does not match the canonical
2139
+ * `GGGGxxEE` notation.
2140
+ *
2141
+ * This is distinct from the parse-time taxonomy (`DicomParseError` /
2142
+ * `DicomValueError` / `DicomSerializeError`) — a profile is built once, at
2143
+ * author time, long before any byte is parsed, so its errors never carry a
2144
+ * byte offset.
2145
+ *
2146
+ * @module
2147
+ */
2148
+ /**
2149
+ * Thrown by `defineProfile()` when the supplied options are invalid.
2150
+ *
2151
+ * @example
2152
+ * ```ts
2153
+ * import { defineProfile, ProfileDefinitionError } from "@cosyte/dicom";
2154
+ * try {
2155
+ * defineProfile({ name: "" });
2156
+ * } catch (err) {
2157
+ * if (err instanceof ProfileDefinitionError) {
2158
+ * console.error(err.message); // "Profile name must be a non-empty string."
2159
+ * }
2160
+ * }
2161
+ * ```
2162
+ */
2163
+ declare class ProfileDefinitionError extends Error {
2164
+ /**
2165
+ * The offending profile's `name` when known (`undefined` when the failure
2166
+ * is the name itself). Lets callers attribute a composed-lineage failure to
2167
+ * the specific profile that introduced it.
2168
+ */
2169
+ readonly profileName?: string;
2170
+ /**
2171
+ * @param message - Actionable description of what made the options invalid.
2172
+ * @param profileName - The offending profile's `name`, when known.
2173
+ */
2174
+ constructor(message: string, profileName?: string);
2175
+ }
2176
+
2177
+ /**
2178
+ * Frozen namespace of every built-in profile: three vendor private-dictionary
2179
+ * overlays (`ge`, `siemens`, `philips`) and two posture presets (`strict`,
2180
+ * `lenient`). Pass one straight to `parseDicom`.
2181
+ *
2182
+ * @example
2183
+ * ```ts
2184
+ * import { parseDicom, profiles } from "@cosyte/dicom";
2185
+ * const ds = parseDicom(buf, { profile: profiles.siemens });
2186
+ * console.log(profiles.siemens.describe?.());
2187
+ * ```
2188
+ */
2189
+ declare const profiles: Readonly<{
2190
+ ge: Profile;
2191
+ siemens: Profile;
2192
+ philips: Profile;
2193
+ strict: Profile;
2194
+ lenient: Profile;
2195
+ }>;
2196
+
2197
+ /**
2198
+ * PS3.15 Annex E attribute-action table — DICOM Basic Application Confidentiality
2199
+ * Profile + 11 retention/clean option sets.
2200
+ *
2201
+ * Phase 1 deliverable (D-08 / D-09 / D-14). Consumed by Phase 7 `anonymize()`
2202
+ * (ANON-01..ANON-10).
2203
+ *
2204
+ * NOTE: This module is NOT re-exported from the package's `src/index.ts` (per D-10 +
2205
+ * D-27 — Phase 1's external surface is `Dictionary.{lookup,byKeyword,uid}` + `VERSION`
2206
+ * only). Phase 7 imports `annexE` via the `@cosyte/dicom/dictionary/annex-e` internal
2207
+ * path, which the `package.json` `exports` map will admit when Phase 7 lands its plans.
2208
+ *
2209
+ * The 9 *metadata-affecting* PS3.15 Annex E option-set columns (E.3.3–E.3.11 plus the
2210
+ * collapsed E.3.6 "RetainLongitudinalTemporal") populate `AnnexEAction.optionSet`
2211
+ * keys per attribute. The two *pixel-level* options — E.3.1 `CleanPixelData` and
2212
+ * E.3.2 `CleanRecognizableVisual` — are not represented per-attribute (PS3.15 Table
2213
+ * E.1-1 has no column for them); Phase 7 enforces them at the pixel-decode layer.
2214
+ * Both names remain in `AnnexEOption` for completeness and for Phase 7's API.
2215
+ */
2216
+
2217
+ /**
2218
+ * PS3.15 Annex E Table E.1-1 action codes.
2219
+ *
2220
+ * - `D` = replace with dummy value of compatible VR
2221
+ * - `Z` = replace with zero-length value
2222
+ * - `X` = remove element entirely
2223
+ * - `K` = keep original value
2224
+ * - `C` = clean / structured replacement per the action table
2225
+ * - `U` = replace UID with a consistent new UID per session
2226
+ *
2227
+ * Compound codes (`Z/D`, `X/Z/D`, `X/Z/U*`, `C/X`) are preserved verbatim from the
2228
+ * Annex E source; Phase 7 interprets them per the table's per-attribute semantics
2229
+ * (e.g. `Z/D` = `Z` if absent, `D` if present).
2230
+ *
2231
+ * @example
2232
+ * const code: AnnexEActionCode = "Z";
2233
+ */
2234
+ type AnnexEActionCode = "D" | "Z" | "X" | "K" | "C" | "U" | "Z/D" | "X/Z" | "X/D" | "X/Z/D" | "X/Z/U*" | "C/X";
2235
+ /**
2236
+ * One of the 11 PS3.15 Annex E option sets (E.3.1–E.3.11).
2237
+ *
2238
+ * Names match REQUIREMENTS.md ANON-02 verbatim. `CleanPixelData` (E.3.1) and
2239
+ * `CleanRecognizableVisual` (E.3.2) act on pixel data, not metadata, and never
2240
+ * appear as `optionSet` keys in the generated `ANNEX_E` map; they remain in the
2241
+ * union for Phase 7's pixel-decode API.
2242
+ *
2243
+ * @example
2244
+ * const opt: AnnexEOption = "RetainLongitudinalTemporal";
2245
+ */
2246
+ type AnnexEOption = "CleanPixelData" | "CleanRecognizableVisual" | "CleanGraphics" | "CleanStructuredContent" | "CleanDescriptors" | "RetainLongitudinalTemporal" | "RetainPatientCharacteristics" | "RetainDeviceIdentity" | "RetainUIDs" | "RetainSafePrivate" | "RetainInstitutionIdentity";
2247
+
2248
+ /**
2249
+ * Public types + error taxonomy for Phase 7 metadata de-identification
2250
+ * (PS3.15 Annex E).
2251
+ *
2252
+ * @module
2253
+ */
2254
+
2255
+ /**
2256
+ * The PS3.15 Annex E option sets `deidentify` honours — the nine
2257
+ * *metadata-affecting* columns of Table E.1-1. The two pixel-level options
2258
+ * (`CleanPixelData` §E.3.1, `CleanRecognizableVisual` §E.3.2) are deliberately
2259
+ * excluded: this is a metadata-only de-identifier and cannot inspect pixels
2260
+ * (deferred to `@cosyte/dicom-pixel`). When pixel data is present it always
2261
+ * warns rather than claiming the image is clean.
2262
+ *
2263
+ * @example
2264
+ * const retain: DeidentifyOption[] = ["RetainLongitudinalTemporal", "RetainSafePrivate"];
2265
+ */
2266
+ type DeidentifyOption = Exclude<AnnexEOption, "CleanPixelData" | "CleanRecognizableVisual">;
2267
+ /**
2268
+ * The nine metadata option-set names, frozen for runtime validation.
2269
+ *
2270
+ * @example
2271
+ * ```ts
2272
+ * import { DEIDENTIFY_OPTIONS } from "@cosyte/dicom";
2273
+ * DEIDENTIFY_OPTIONS.includes("RetainUIDs"); // true
2274
+ * ```
2275
+ */
2276
+ declare const DEIDENTIFY_OPTIONS: readonly DeidentifyOption[];
2277
+ /**
2278
+ * What `deidentify` actually did to one attribute — the concrete outcome of the
2279
+ * resolved Annex E action.
2280
+ *
2281
+ * - `removed` — the element was deleted (`X`).
2282
+ * - `emptied` — replaced with a zero-length value (`Z`).
2283
+ * - `dummied` — replaced with a non-identifying dummy of compatible VR (`D`).
2284
+ * - `uid-remapped` — UID(s) replaced with internally-consistent UIDs (`U`).
2285
+ * - `cleaned` — conservatively blanked because a safe similar-meaning value
2286
+ * cannot be synthesised at the metadata layer (`C`; see known limitations).
2287
+ * - `kept` — retained, either by an active Retain option or because the SQ was
2288
+ * kept and its items cleaned recursively.
2289
+ *
2290
+ * @example
2291
+ * ```ts
2292
+ * import { deidentify, parseDicom, type AppliedAction } from "@cosyte/dicom";
2293
+ * const { report } = deidentify(parseDicom(buf));
2294
+ * const removed = report.attributes.filter((a) => a.applied === ("removed" satisfies AppliedAction));
2295
+ * ```
2296
+ */
2297
+ type AppliedAction = "removed" | "emptied" | "dummied" | "uid-remapped" | "cleaned" | "kept";
2298
+ /**
2299
+ * One audited attribute outcome. Carries only structural facts — tag, keyword,
2300
+ * the resolved Annex E action code, and the SQ context path — **never** a
2301
+ * decoded value, so a report is always safe to log.
2302
+ *
2303
+ * @example
2304
+ * ```ts
2305
+ * import { deidentify, parseDicom, type DeidentifiedAttribute } from "@cosyte/dicom";
2306
+ * const { report } = deidentify(parseDicom(buf));
2307
+ * report.attributes.forEach((a: DeidentifiedAttribute) => {
2308
+ * console.log(a.keyword, a.action, a.applied); // structural facts only — safe to log
2309
+ * });
2310
+ * ```
2311
+ */
2312
+ interface DeidentifiedAttribute {
2313
+ readonly tag: Tag;
2314
+ readonly keyword: string;
2315
+ /** The resolved single action after collapsing any conditional code. */
2316
+ readonly action: Exclude<AnnexEActionCode, `${string}/${string}`>;
2317
+ readonly applied: AppliedAction;
2318
+ /** Tag/index chain for an attribute inside a sequence; omitted at the root. */
2319
+ readonly contextPath?: readonly string[];
2320
+ }
2321
+ /**
2322
+ * The audit trail returned alongside the de-identified dataset. Contains no
2323
+ * decoded values — only tags, keywords, action codes, and the UID map (whose
2324
+ * keys/values are UIDs, not patient data).
2325
+ *
2326
+ * @example
2327
+ * ```ts
2328
+ * import { deidentify, parseDicom, type DeidentifyReport } from "@cosyte/dicom";
2329
+ * const { report }: { report: DeidentifyReport } = deidentify(parseDicom(buf));
2330
+ * console.log(report.attributes.length, "attributes acted on");
2331
+ * console.log(report.warnings.map((w) => w.code)); // e.g. burned-in annotation
2332
+ * ```
2333
+ */
2334
+ interface DeidentifyReport {
2335
+ /** Per-attribute outcomes for every attribute Annex E acted on. */
2336
+ readonly attributes: readonly DeidentifiedAttribute[];
2337
+ /** Private tags removed under the Basic Profile (kept ones are omitted). */
2338
+ readonly removedPrivateTags: readonly Tag[];
2339
+ /** Source UID → replacement UID, for cross-file consistency. */
2340
+ readonly uidMap: ReadonlyMap<string, string>;
2341
+ /** Safety warnings — notably burned-in-pixel annotation that cannot be cleaned. */
2342
+ readonly warnings: readonly DicomParseWarning[];
2343
+ /** The Retain/Clean options that were active for this run. */
2344
+ readonly retained: readonly DeidentifyOption[];
2345
+ }
2346
+ /**
2347
+ * Options controlling a de-identification run. All optional — the default is
2348
+ * the Basic Application Level Confidentiality Profile with no Retain options.
2349
+ *
2350
+ * @example
2351
+ * ```ts
2352
+ * import { deidentify, parseDicom, type DeidentifyOptions } from "@cosyte/dicom";
2353
+ * const opts: DeidentifyOptions = { retain: ["RetainLongitudinalTemporal", "CleanDescriptors"] };
2354
+ * const { dataset } = deidentify(parseDicom(buf), opts);
2355
+ * ```
2356
+ */
2357
+ interface DeidentifyOptions {
2358
+ /** Annex E option sets to activate (Retain* / Clean*). Default: none. */
2359
+ readonly retain?: readonly DeidentifyOption[];
2360
+ /** Root for generated UIDs (action `U`). Default `"2.25"`. */
2361
+ readonly uidRoot?: string;
2362
+ /**
2363
+ * A caller-owned source→replacement UID cache. Pass one shared map across a
2364
+ * whole study/archive to make UID remapping consistent by construction even
2365
+ * across separate calls (it is consistent anyway — the mapping is content-
2366
+ * derived — but a shared map also makes repeats O(1)).
2367
+ */
2368
+ readonly uidMap?: Map<string, string>;
2369
+ /**
2370
+ * A Phase 6 {@link Profile} whose private-dictionary overlay names the
2371
+ * known-safe private attributes to keep when `RetainSafePrivate` is active.
2372
+ * Without it, `RetainSafePrivate` keeps nothing (fail-safe).
2373
+ */
2374
+ readonly profile?: Profile;
2375
+ /**
2376
+ * Text written to `(0012,0063)` De-identification Method. Default names the
2377
+ * Basic Profile and the active options.
2378
+ */
2379
+ readonly deidentificationMethod?: string;
2380
+ }
2381
+ /**
2382
+ * The result of {@link deidentify}: a new dataset plus its audit report.
2383
+ *
2384
+ * @example
2385
+ * ```ts
2386
+ * import { deidentify, parseDicom, serializeDicom, type DeidentifyResult } from "@cosyte/dicom";
2387
+ * const { dataset, report }: DeidentifyResult<ReturnType<typeof parseDicom>> = deidentify(parseDicom(buf));
2388
+ * const safe = serializeDicom(dataset); // input dataset is never mutated
2389
+ * void report;
2390
+ * ```
2391
+ */
2392
+ interface DeidentifyResult<TDataset> {
2393
+ readonly dataset: TDataset;
2394
+ readonly report: DeidentifyReport;
2395
+ }
2396
+ /**
2397
+ * Stable codes for {@link DeidentifyError}.
2398
+ *
2399
+ * @example
2400
+ * ```ts
2401
+ * import { DEIDENTIFY_ERROR_CODES } from "@cosyte/dicom";
2402
+ * DEIDENTIFY_ERROR_CODES.INVALID_OPTIONS; // "INVALID_OPTIONS"
2403
+ * ```
2404
+ */
2405
+ declare const DEIDENTIFY_ERROR_CODES: Readonly<{
2406
+ readonly INVALID_OPTIONS: "INVALID_OPTIONS";
2407
+ }>;
2408
+ /**
2409
+ * One of the {@link DEIDENTIFY_ERROR_CODES} values.
2410
+ *
2411
+ * @example
2412
+ * ```ts
2413
+ * import { DeidentifyError, type DeidentifyErrorCode } from "@cosyte/dicom";
2414
+ * const code: DeidentifyErrorCode = "INVALID_OPTIONS";
2415
+ * throw new DeidentifyError("unknown retain option", code);
2416
+ * ```
2417
+ */
2418
+ type DeidentifyErrorCode = (typeof DEIDENTIFY_ERROR_CODES)[keyof typeof DEIDENTIFY_ERROR_CODES];
2419
+ /**
2420
+ * Thrown for an author-time misconfiguration of {@link deidentify} (an unknown
2421
+ * Retain option, a malformed UID root). Distinct from the parser's fatal codes,
2422
+ * the value layer's `DicomValueError`, and the serializer's `DicomSerializeError`.
2423
+ * The message carries only structural facts (option names, the UID root) — never
2424
+ * a decoded value.
2425
+ *
2426
+ * @example
2427
+ * ```ts
2428
+ * import { deidentify, DeidentifyError } from "@cosyte/dicom";
2429
+ * try {
2430
+ * // @ts-expect-error — not a valid option
2431
+ * deidentify(ds, { retain: ["RetainEverything"] });
2432
+ * } catch (e) {
2433
+ * if (e instanceof DeidentifyError) console.error(e.code); // "INVALID_OPTIONS"
2434
+ * }
2435
+ * ```
2436
+ */
2437
+ declare class DeidentifyError extends Error {
2438
+ readonly code: DeidentifyErrorCode;
2439
+ /**
2440
+ * @param message Human-readable, PHI-free description.
2441
+ * @param code One of {@link DEIDENTIFY_ERROR_CODES}.
2442
+ */
2443
+ constructor(message: string, code: DeidentifyErrorCode);
2444
+ }
2445
+
2446
+ /**
2447
+ * `deidentify` — PS3.15 Annex E metadata-level de-identification.
2448
+ *
2449
+ * Applies the **Basic Application Level Confidentiality Profile** plus any of the
2450
+ * nine *metadata-affecting* Annex E Options, driven by the generated Table E.1-1
2451
+ * action map ({@link annexE}). It is a **pure** function: the input {@link Dataset}
2452
+ * is never mutated; a fresh `Dataset` (with a rebuilt element map and File Meta)
2453
+ * is returned alongside a value-free {@link DeidentifyReport}.
2454
+ *
2455
+ * **What it does**
2456
+ * - Resolves each attribute's action (basic profile, overridden by an active
2457
+ * Retain/Clean Option), collapsing conditional codes to their leftmost branch
2458
+ * (see {@link resolveAction}), and applies it: `X` remove, `Z` zero-length, `D`
2459
+ * VR-consistent dummy (falling back to `Z` where no safe dummy exists), `C`
2460
+ * conservative blank, `U` deterministic consistent-UID remap, `K` keep.
2461
+ * - Recurses into kept sequences and **re-encodes** them so nested PHI is removed
2462
+ * in the *serialized* bytes too — not just the object model (the Phase 5 writer
2463
+ * blits `SQ` spans verbatim, so a rebuilt `items` array alone would not survive
2464
+ * serialization). Rebuilt sequences are normalized to defined length.
2465
+ * - Removes all private attributes by default; with `RetainSafePrivate` + a
2466
+ * {@link Profile}, keeps the private data elements the profile's overlay names
2467
+ * as safe (and the private-creator elements the profile recognizes).
2468
+ * - Remaps `(0002,0003)` Media Storage SOP Instance UID consistently (unless
2469
+ * `RetainUIDs`), writes `(0012,0062)` Patient Identity Removed = `YES` and
2470
+ * `(0012,0063)` De-identification Method, and warns
2471
+ * (`DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED`) when Pixel Data is present and not
2472
+ * marked free of burned-in annotation — this metadata-only pass cannot clean
2473
+ * pixels (deferred to `@cosyte/dicom-pixel`).
2474
+ *
2475
+ * **Known limitations** (documented, fail-safe toward *more* removal):
2476
+ * - No IOD Type-1 conformance analysis, so conditional codes always take the
2477
+ * most-protective leftmost branch — a Type-1 attribute that strictly needed a
2478
+ * dummy is instead removed/emptied.
2479
+ * - `C` (clean) is a conservative blank, not a meaning-preserving structured
2480
+ * replacement (which needs domain context the metadata layer lacks).
2481
+ * - Pixel-level options (`CleanPixelData`, `CleanRecognizableVisual`) are out of
2482
+ * scope; burned-in text is warned, never cleaned.
2483
+ * - A private data element kept under `RetainSafePrivate` is kept *verbatim* — if
2484
+ * it is itself a sequence carrying standard PHI attributes, that nested content
2485
+ * is not recursed. The profile vouches the element is safe; nest accordingly.
2486
+ * - A sequence whose `items` the parser did not materialize (e.g. an
2487
+ * undefined-length `UN` value stored as an opaque span) is kept verbatim rather
2488
+ * than recursed, so any nested listed attributes are not de-identified.
2489
+ *
2490
+ * @module
2491
+ */
2492
+
2493
+ /**
2494
+ * De-identify a {@link Dataset} per PS3.15 Annex E — the Basic Application Level
2495
+ * Confidentiality Profile, plus any Retain/Clean Options passed in `retain`.
2496
+ *
2497
+ * Pure: `ds` is never mutated. Returns a fresh dataset and a value-free
2498
+ * {@link DeidentifyReport} (tags, keywords, action codes, the UID map, warnings).
2499
+ *
2500
+ * @throws {@link DeidentifyError} (`INVALID_OPTIONS`) for an unknown Retain option
2501
+ * or a malformed `uidRoot`.
2502
+ *
2503
+ * @example
2504
+ * ```ts
2505
+ * import { parseDicom, deidentify, serializeDicom } from "@cosyte/dicom";
2506
+ * const { dataset, report } = deidentify(parseDicom(buf));
2507
+ * const clean = serializeDicom(dataset); // safe to share
2508
+ * console.log(report.attributes.length, "attributes acted on");
2509
+ * ```
2510
+ */
2511
+ declare function deidentify(ds: Dataset, options?: DeidentifyOptions): DeidentifyResult<Dataset>;
2512
+
2513
+ /**
2514
+ * Deterministic UID remapping for PS3.15 Annex E action `U` ("replace with an
2515
+ * internally-consistent UID").
2516
+ *
2517
+ * The replacement is a pure function of the source UID and the chosen root:
2518
+ * `sha256(sourceUid)` reduced to a decimal string under `<root>.…`. Because it
2519
+ * is content-derived, the *same* source UID maps to the *same* replacement
2520
+ * across separate `deidentify` calls and across files in a study set — without
2521
+ * any shared state — so cross-instance referential integrity (Study ⇄ Series ⇄
2522
+ * SOP, Frame of Reference, referenced instances) is preserved. A per-call cache
2523
+ * `Map` makes repeats within one call O(1) and lets a caller thread one map
2524
+ * through a whole archive if they prefer explicit sharing.
2525
+ *
2526
+ * Default root `2.25` is the DICOM-sanctioned UUID-derived arc (PS3.5 §B.2) —
2527
+ * a globally-unique root that needs no registration.
2528
+ *
2529
+ * @module
2530
+ */
2531
+ /**
2532
+ * DICOM `2.25` UUID-derived root (PS3.5 §B.2) — no registration required.
2533
+ *
2534
+ * @example
2535
+ * ```ts
2536
+ * import { makeUidRemapper, DEFAULT_UID_ROOT } from "@cosyte/dicom";
2537
+ * const remap = makeUidRemapper(DEFAULT_UID_ROOT);
2538
+ * remap.map("1.2.840.113619.2.55.3").startsWith("2.25."); // true
2539
+ * ```
2540
+ */
2541
+ declare const DEFAULT_UID_ROOT = "2.25";
2542
+ /**
2543
+ * A UID remapper: a stable `map(src)` plus the backing cache it fills.
2544
+ *
2545
+ * @example
2546
+ * ```ts
2547
+ * import { makeUidRemapper, type UidRemapper } from "@cosyte/dicom";
2548
+ * const remap: UidRemapper = makeUidRemapper();
2549
+ * const replaced = remap.map("1.2.840.10008.5.1.4.1.1.2");
2550
+ * remap.cache.get("1.2.840.10008.5.1.4.1.1.2") === replaced; // true
2551
+ * ```
2552
+ */
2553
+ interface UidRemapper {
2554
+ /** Map one source UID to its deterministic replacement (cached). */
2555
+ readonly map: (sourceUid: string) => string;
2556
+ /** The source→replacement cache, exposed for reporting / reuse. */
2557
+ readonly cache: Map<string, string>;
2558
+ }
2559
+ /**
2560
+ * Build a {@link UidRemapper} rooted at `root`, optionally seeded with (and
2561
+ * writing through to) a caller-owned `cache` for cross-call sharing.
2562
+ *
2563
+ * @param root Dotted-decimal UID root (default {@link DEFAULT_UID_ROOT}).
2564
+ * @param cache Source→replacement map to fill (default a fresh `Map`).
2565
+ * @throws {DeidentifyError} when `root` is not a valid dotted-decimal OID prefix
2566
+ * or leaves no room for a value component within the 64-char UID limit.
2567
+ *
2568
+ * @example
2569
+ * ```ts
2570
+ * import { makeUidRemapper } from "@cosyte/dicom";
2571
+ * const remap = makeUidRemapper();
2572
+ * remap.map("1.2.840.113619.2.55.3") === remap.map("1.2.840.113619.2.55.3"); // true
2573
+ * ```
2574
+ */
2575
+ declare function makeUidRemapper(root?: string, cache?: Map<string, string>): UidRemapper;
2576
+
2577
+ export { type AppliedAction, CODING_SCHEME_OIDS, type CodedConcept, DEFAULT_UID_ROOT, DEIDENTIFY_ERROR_CODES, DEIDENTIFY_OPTIONS, Dataset, type DefineProfileOptions, type DeidentifiedAttribute, DeidentifyError, type DeidentifyErrorCode, type DeidentifyOption, type DeidentifyOptions, type DeidentifyReport, type DeidentifyResult, type DicomDate, type DicomDateTime, DicomParseError, type DicomParseWarning, type DicomPosition, DicomSerializeError, type DicomTime, type DicomValue, DicomValueError, index as Dictionary, Element, FATAL_CODES, type FatalCode, type FileMeta, type FileMetaRawElement, type FrameFunctionalGroups, type ImageView, Item, type OnWarningCallback, type OtherPatientId, type ParseOptions, type PatientView, type PersonName, type PersonNameGroup, type PrivateTagDefinition, type Profile, ProfileDefinitionError, type ProfilePrivateTags, type RealWorldValueMap, SERIALIZE_ERROR_CODES, Sequence, type SerializeErrorCode, type SeriesView, type StudyView, type UidRemapper, VALUE_ERROR_CODES, VERSION, type ValueErrorCode, WARNING_CODES, type WarningCode, codingSchemeOid, decodeElementValue, decodeText, defineProfile, deidentify, isKnownCharsetTerm, makeUidRemapper, parseDate, parseDateTime, parseDicom, parsePersonName, parseSpecificCharacterSet, parseTime, profiles, readCode, resolveDecoderLabel, serializeDicom };