@cosyte/dicom 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -74,7 +74,13 @@ interface DictionaryEntry {
74
74
  readonly repeatingGroup?: boolean;
75
75
  }
76
76
  /**
77
- * One DICOM UID as published in PS3.6 / PS3.4 (UID registry).
77
+ * One DICOM UID as published in PS3.6 Annex A (Tables A-1 and A-2).
78
+ *
79
+ * `name` is the normative UID Name, with two deliberate conveniences. Retirement is carried in
80
+ * `retired` rather than as the trailing `(Retired)` every retired Annex A row carries in its UID
81
+ * Name, and four transfer syntaxes carry the short form every DICOM toolkit prints rather than
82
+ * PS3.6's longer `...: Default Transfer Syntax for ...` form. Every other name is the normative
83
+ * text.
78
84
  *
79
85
  * @example
80
86
  * import { Dictionary } from "@cosyte/dicom";
@@ -141,9 +147,11 @@ declare function byKeyword(keyword: string): DictionaryEntry | undefined;
141
147
  /**
142
148
  * Look up a DICOM UID by its dotted-decimal value.
143
149
  *
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).
150
+ * Covers PS3.6 Annex A whole: transfer syntaxes, SOP and Meta SOP Classes,
151
+ * well-known SOP Instances and Frames of Reference, coding schemes and the
152
+ * rest, current and retired alike. Returns `undefined` for unknown UIDs or
153
+ * malformed input. Used by `parseDicom()` (Phase 2) to render human-readable
154
+ * Transfer Syntax names (DICT-06).
147
155
  *
148
156
  * @example
149
157
  * import { Dictionary } from "@cosyte/dicom";
@@ -307,6 +315,32 @@ interface ParseOptions {
307
315
  * When `true`, every Tier-2 warning is escalated to a thrown
308
316
  * `DicomParseError` carrying the warning code. Default `false`.
309
317
  *
318
+ * 🛑 **THE ESCALATED DIAGNOSTIC CARRIES SOURCE BYTES THAT THE WARNING DOES
319
+ * NOT.** A `DicomParseWarning.message` is a frozen registry string with only
320
+ * structural tokens filled in, so it is safe to log whole; the
321
+ * `DicomParseError` this option raises in its place also carries `snippet`,
322
+ * **16 raw bytes, unredacted** (D-10), read at the warning's own `byteOffset`.
323
+ * A message-only PHI review of the lenient path therefore does not transfer to
324
+ * the strict one. Log `err.code`, `err.byteOffset` and `err.message`; treat
325
+ * `err.snippet` as PHI.
326
+ *
327
+ * **The snippet is cut in the SAME FRAME the `byteOffset` is counted in**, so
328
+ * it is the bytes at the offset the diagnostic names: file-absolute at the
329
+ * root, relative to the enclosing slice inside a defined-length Sequence or
330
+ * Item, and into the inflated stream under Deflated Explicit VR LE. It was not
331
+ * always: until `DICOM-FATAL-MESSAGE-REGISTRY` the offset moved with the frame
332
+ * while the cut was always taken from the whole file, so inside a
333
+ * defined-length Item the 16 bytes were **an unrelated element's** - a
334
+ * diagnostic disclosing data from a part of the document the reader was never
335
+ * asked about. That is closed. **What is NOT closed, and never was a defect:
336
+ * the bytes are still raw source bytes.** Reading them as safe because the
337
+ * message beside them is registry-bound is the mistake this whole paragraph
338
+ * exists to prevent.
339
+ *
340
+ * **`byteOffset` still carries no frame-of-reference contract**, and closing
341
+ * the mismatch did not give it one: a nested offset is still not a key you can
342
+ * look up against the root. Measure it rather than reasoning about it.
343
+ *
310
344
  * Omit (do not pass `undefined`) to use the default.
311
345
  */
312
346
  readonly strict?: boolean;
@@ -420,6 +454,7 @@ declare const WARNING_CODES: {
420
454
  readonly DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED: "DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED";
421
455
  readonly DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED: "DICOM_DEIDENT_EMBEDDED_ATTRIBUTE_REMOVED";
422
456
  readonly DICOM_DEIDENT_METHOD_NOT_ADDED: "DICOM_DEIDENT_METHOD_NOT_ADDED";
457
+ readonly DICOM_DEIDENT_METHOD_NOT_LO: "DICOM_DEIDENT_METHOD_NOT_LO";
423
458
  readonly DICOM_DEIDENT_METHOD_PRIOR_RETAINED: "DICOM_DEIDENT_METHOD_PRIOR_RETAINED";
424
459
  readonly DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE: "DICOM_DEIDENT_SEQUENCE_NOT_AUDITABLE";
425
460
  readonly DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE: "DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE";
@@ -2327,16 +2362,23 @@ declare const DEIDENTIFY_OPTIONS: readonly DeidentifyOption[];
2327
2362
  */
2328
2363
  type AppliedAction = "removed" | "emptied" | "dummied" | "uid-remapped" | "cleaned" | "kept";
2329
2364
  /**
2330
- * One audited attribute outcome. Carries only structural facts - tag, keyword,
2331
- * the resolved Annex E action code, and the SQ context path - **never** a
2332
- * decoded value, so a report is always safe to log.
2365
+ * One audited attribute outcome. `tag`, `keyword`, `action`, `applied` and
2366
+ * `repeatingGroup` carry **never** a decoded value: `keyword`, `action` and
2367
+ * `repeatingGroup` come from the Part 6 and Annex E tables, and `tag` is bound to
2368
+ * a tag those tables carry a row for, which is membership in a closed table
2369
+ * rather than a shape test.
2370
+ *
2371
+ * **🩺 `contextPath` is NOT in that class and this docstring used to say it was.**
2372
+ * It is a chain of tags read off the wire, bound by nothing - see
2373
+ * {@link DeidentifiedAttribute.contextPath} and the note on
2374
+ * {@link DeidentifyReport}.
2333
2375
  *
2334
2376
  * @example
2335
2377
  * ```ts
2336
2378
  * import { deidentify, parseDicom, type DeidentifiedAttribute } from "@cosyte/dicom";
2337
2379
  * const { report } = deidentify(parseDicom(buf));
2338
2380
  * report.attributes.forEach((a: DeidentifiedAttribute) => {
2339
- * console.log(a.keyword, a.action, a.applied); // structural facts only - safe to log
2381
+ * console.log(a.keyword, a.action, a.applied); // composed from tables - safe to log
2340
2382
  * });
2341
2383
  * ```
2342
2384
  */
@@ -2346,7 +2388,45 @@ interface DeidentifiedAttribute {
2346
2388
  /** The resolved single action after collapsing any conditional code. */
2347
2389
  readonly action: Exclude<AnnexEActionCode, `${string}/${string}`>;
2348
2390
  readonly applied: AppliedAction;
2349
- /** Tag/index chain for an attribute inside a sequence; omitted at the root. */
2391
+ /**
2392
+ * Tag/index chain for an attribute inside a sequence; omitted at the root.
2393
+ *
2394
+ * **🩺 Each segment is `TAG[index]`, and the `TAG` half is read off the wire
2395
+ * with no table behind it.** It is whatever tag the descent walked, so it is
2396
+ * bound by neither a shape test nor membership in a closed one - which is what
2397
+ * separates it from `tag`, `keyword`, `action` and `repeatingGroup`. On a file
2398
+ * where an under-declared Value Length desynchronized the reader onto four
2399
+ * bytes sitting **inside** somebody's value, those four bytes become a segment
2400
+ * here. It is not the only identifier on this report read off the wire -
2401
+ * {@link DeidentifyReport.removedPrivateTags} and
2402
+ * {@link UnauditableSequenceFinding.tag} are too - but those two are disclosed
2403
+ * as such, and this one was documented as structural.
2404
+ *
2405
+ * Measured on a synthetic `LO` carrier holding `"MRS BRAIN SMITHSON"` that
2406
+ * under-declares by four: the reader resynchronizes onto a fabricated `SQ`
2407
+ * header, descends it, and the report reads `contextPath: ["53484E4F[0]"]` -
2408
+ * `"HSON"` in wire order, recovered by writing the two halves back with
2409
+ * `writeUInt16LE`. **No warning is raised and every finding array on the
2410
+ * report is empty.** Change the surname and the published segment changes with
2411
+ * it. `PRE-EXISTING`, on every release that has shipped the field.
2412
+ *
2413
+ * **🛑 IT IS NOT THE ONLY PLACE THOSE BYTES SURFACE, AND AN EARLIER DRAFT OF
2414
+ * THIS NOTE SAID IT WAS. A GRADED PASS REFUTED THAT AND IT MUST NOT COME
2415
+ * BACK.** On the same file the de-identified `Dataset` still carries the
2416
+ * fabricated `(5348,4E4F)`, so `serializeDicom` writes its header back out in
2417
+ * full - `"HSON"` included - inside an object stamped
2418
+ * `(0012,0062) Patient Identity Removed = YES`. That re-emission belongs to
2419
+ * the disclosed under-declare carrier class, not to this field, and neither is
2420
+ * a bound on the other: **redacting `contextPath` from a log does not make the
2421
+ * object safe to share.** Pinned in
2422
+ * `test/integration/phi-diagnostic-surface.test.ts`.
2423
+ *
2424
+ * It is published anyway, on the same footing as
2425
+ * {@link DeidentifyReport.removedPrivateTags}: **where** an attribute sat is
2426
+ * the whole audit value of the field, and withholding it would destroy that on
2427
+ * every well-formed file in order to bound a malformed one. **Treat it as PHI
2428
+ * when the source is untrusted.**
2429
+ */
2350
2430
  readonly contextPath?: readonly string[];
2351
2431
  /**
2352
2432
  * Present when the action came from a Table E.1-1 row that names a
@@ -2368,9 +2448,15 @@ interface DeidentifiedAttribute {
2368
2448
  * (PS3.15 §E.1 "all instances"; §E.3.5 is the standard's own precedent for
2369
2449
  * removing identifying information embedded inside a string attribute).
2370
2450
  *
2371
- * Every field is structural: `tag` and `vr` are the carrier's, and `hidden`
2372
- * holds tags composed from four bytes each. No decoded value appears here, so
2373
- * this is safe to log.
2451
+ * `tag` and `vr` are the carrier's own and are structural. **`hidden` is not,
2452
+ * and calling this finding "safe to log" was wrong.** Each entry is composed
2453
+ * from four bytes that were sitting *inside* the carrier's value - that is the
2454
+ * whole reason the field exists - so on any file that populates it those bytes
2455
+ * are document content. Measured: a `CS` carrier over-declaring over the bytes
2456
+ * `"SABC"` reports `hidden: ["41534342"]`, which is `"SABC"` in wire order.
2457
+ * `PRE-EXISTING` and identical on every release that has shipped the field.
2458
+ * Treat `hidden` at the sensitivity of the file, and see
2459
+ * {@link DeidentifyReport} for the other fields in this class.
2374
2460
  *
2375
2461
  * @example
2376
2462
  * ```ts
@@ -2388,12 +2474,31 @@ interface EmbeddedAttributeFinding {
2388
2474
  readonly vr: VR;
2389
2475
  /** The tags of the Data Elements found inside the carrier's value, in wire order. */
2390
2476
  readonly hidden: readonly Tag[];
2391
- /** Tag/index chain when the carrier is inside a sequence item; omitted at the root. */
2477
+ /**
2478
+ * Tag/index chain when the carrier is inside a sequence item; omitted at the
2479
+ * root. **Built by the same descent as
2480
+ * {@link DeidentifiedAttribute.contextPath} and carrying the same caveat:
2481
+ * each segment's tag is read off the wire, bound by nothing, so on a
2482
+ * desynchronized read it can be four bytes of a value.** Read that field's
2483
+ * note before logging this one.
2484
+ */
2392
2485
  readonly contextPath?: readonly string[];
2393
2486
  }
2394
2487
  /**
2395
- * One `SQ` element that was emptied because the parser never materialized its
2396
- * items, so the de-identifier had no Data Sets to walk.
2488
+ * One Sequence-of-Items carrier that was emptied because this run had no item
2489
+ * stream to walk, so the de-identifier had no Data Sets to reach.
2490
+ *
2491
+ * **Two producers, and the second is not a parsed `SQ`.** The ordinary one is an
2492
+ * `SQ` element whose `items` the parser never materialized. The other is a
2493
+ * private data element retained under `RetainSafePrivate` whose `Profile` entry
2494
+ * declares it `SQ` while the parse tree says otherwise - `UN` under Implicit VR
2495
+ * LE when the profile was passed to `deidentify()` but not to `parseDicom`, or
2496
+ * whatever binary VR the sender wrote under Explicit VR, which wins in the
2497
+ * parser. The profile is the authority that retained the element, and it has
2498
+ * said the value is a Sequence of Items; with no items on the tree, the §E.1.1
2499
+ * obligation below falls on the carrier just the same. Such an element keeps its
2500
+ * parsed VR in the output and is emptied rather than re-typed to `SQ`
2501
+ * (`DICOM-PRIVATE-SQ-PARSE-VR`).
2397
2502
  *
2398
2503
  * PS3.5 2026c §7.5.1 "Item Encoding Rules" states that "Each Item Value shall
2399
2504
  * contain a DICOM Data Set composed of Data Elements", so an `SQ` element's
@@ -2412,12 +2517,26 @@ interface EmbeddedAttributeFinding {
2412
2517
  * the carrier, not as a rule about this case.)
2413
2518
  *
2414
2519
  * Both fields are structural: `tag` is the carrier's and `byteLength` is the
2415
- * declared size of the value that was dropped. No decoded value appears here,
2416
- * so this is safe to log.
2417
- *
2418
- * The parser always announces the underlying refusal first, on
2520
+ * recorded span of the value that was dropped. **No _decoded_ value appears
2521
+ * here, and that is not the same as "safe to log".** On the second producer
2522
+ * `tag` can be four bytes of another element's value: a length under-declared
2523
+ * upstream resynchronizes the reader mid-value, and if the bytes it lands on
2524
+ * spell a private block this caller's profile declares `SQ`, followed by a VR
2525
+ * that is one of the 34, the fabricated header is what you get here. The
2526
+ * package's answer to that class is normally `report.undefinedVrElements`,
2527
+ * which names a byte offset and **no tag**, and it still answers the case where
2528
+ * the fabricated VR is outside the 34 - but it cannot answer this one, because
2529
+ * a fabricated `OB` header and a genuine one are byte-identical. So this shares
2530
+ * the standing exception `report.removedPrivateTags` and `uidMap` already have:
2531
+ * a `DeidentifyReport` is **not** a value-free surface. Treat it as document
2532
+ * content, at the sensitivity of the file it came from.
2533
+ *
2534
+ * For the first producer the parser announces the underlying refusal on
2419
2535
  * `Dataset.warnings`: `DICOM_SQ_NOT_DESCENDED` for a defined-length Implicit VR
2420
- * LE value whose dictionary-resolved `SQ` was not a valid item stream.
2536
+ * LE value whose dictionary-resolved `SQ` was not a valid item stream. **Do not
2537
+ * generalise that to the second.** There the file may be entirely conformant -
2538
+ * an honest defined-length `OB` carrier raises nothing at all - so this report
2539
+ * field, not `Dataset.warnings`, is where that drop is visible.
2421
2540
  *
2422
2541
  * @example
2423
2542
  * ```ts
@@ -2433,7 +2552,14 @@ interface UnauditableSequenceFinding {
2433
2552
  readonly tag: Tag;
2434
2553
  /** Byte length of the value field that was dropped. Structural, never a value. */
2435
2554
  readonly byteLength: number;
2436
- /** Tag/index chain when the carrier is inside a sequence item; omitted at the root. */
2555
+ /**
2556
+ * Tag/index chain when the carrier is inside a sequence item; omitted at the
2557
+ * root. **Built by the same descent as
2558
+ * {@link DeidentifiedAttribute.contextPath} and carrying the same caveat:
2559
+ * each segment's tag is read off the wire, bound by nothing, so on a
2560
+ * desynchronized read it can be four bytes of a value.** Read that field's
2561
+ * note before logging this one.
2562
+ */
2437
2563
  readonly contextPath?: readonly string[];
2438
2564
  }
2439
2565
  /**
@@ -2493,8 +2619,14 @@ interface UnauditableSequenceFinding {
2493
2619
  * finding rather than an inconsistency.
2494
2620
  *
2495
2621
  * `byteOffset` locates the element instead - a position this parser counted.
2496
- * Nothing here renders a document byte: an offset the parser counted, a decoded
2497
- * length, and the structural `contextPath`. Safe to log.
2622
+ *
2623
+ * **🩺 THAT WITHHOLDING IS NOT WHOLE, AND THIS PARAGRAPH USED TO CLAIM IT WAS.**
2624
+ * It said "nothing here renders a document byte ... and the structural
2625
+ * `contextPath`. Safe to log." `contextPath` is not structural: its segments are
2626
+ * tags read off the wire by the same descent, so the header this type refuses to
2627
+ * name by `tag` can be named by the `contextPath` of a finding one level down.
2628
+ * See {@link DeidentifiedAttribute.contextPath} for the measurement. `byteOffset`
2629
+ * and `byteLength` are unaffected and the reasoning above them still stands.
2498
2630
  *
2499
2631
  * @example
2500
2632
  * ```ts
@@ -2525,24 +2657,62 @@ interface UndefinedVrFinding {
2525
2657
  * as "structural, never a value" the way its siblings are described.
2526
2658
  */
2527
2659
  readonly byteLength: number;
2528
- /** Tag/index chain when the carrier is inside a sequence item; omitted at the root. */
2660
+ /**
2661
+ * Tag/index chain when the carrier is inside a sequence item; omitted at the
2662
+ * root. **Built by the same descent as
2663
+ * {@link DeidentifiedAttribute.contextPath} and carrying the same caveat:
2664
+ * each segment's tag is read off the wire, bound by nothing, so on a
2665
+ * desynchronized read it can be four bytes of a value.** Read that field's
2666
+ * note before logging this one.
2667
+ */
2529
2668
  readonly contextPath?: readonly string[];
2530
2669
  }
2531
2670
  /**
2532
2671
  * The audit trail returned alongside the de-identified dataset.
2533
2672
  *
2534
2673
  * Most fields are composed from static tables: Part 6 keywords, Annex E action
2535
- * codes, structural `TAG[index]` sequence paths, and registry warning messages.
2536
- * **Two are not, and both are named here rather than in a footnote.**
2537
- *
2538
- * 1. **`uidMap`** - its keys are the source UIDs read out of the file, kept so a
2539
- * caller can make UID replacement consistent across a study or an archive. A
2540
- * Study or SOP Instance UID is a unique identifying number, so treat it as
2541
- * PHI.
2542
- * 2. **`removedPrivateTags`** - see the field's own note. On a *well-formed*
2543
- * file these are the sender's own private tag numbers and carry nothing; on a
2544
- * malformed one a tag can be four bytes of a value, and it is measured, not
2545
- * theoretical.
2674
+ * codes, the active option names.
2675
+ * **Several fields are not, and they are named here rather than in a footnote.**
2676
+ * 🛑 **Do not quote a COUNT of them, here or anywhere else.** The count read
2677
+ * "one" and then "two" and then "three", and was wrong every time it was read,
2678
+ * because each correction bumped the numeral without re-deriving the list.
2679
+ * **The list below carried its own numerals until one had to be added, which is
2680
+ * the same disease one step removed, so they are gone.** Treat it as the current
2681
+ * reading of a surface that has grown, not as a proof of exhaustiveness.
2682
+ *
2683
+ * - **`uidMap`** - its keys are the source UIDs read out of the file, kept so a
2684
+ * caller can make UID replacement consistent across a study or an archive. A
2685
+ * Study or SOP Instance UID is a unique identifying number, so treat it as
2686
+ * PHI.
2687
+ * - **`removedPrivateTags`** - see the field's own note. On a *well-formed*
2688
+ * file these are the sender's own private tag numbers and carry nothing; on a
2689
+ * malformed one a tag can be four bytes of a value, and it is measured, not
2690
+ * theoretical.
2691
+ * - **`unauditableSequences[].tag`** - see
2692
+ * {@link UnauditableSequenceFinding}. Same shape as `removedPrivateTags` by a
2693
+ * narrower route: a private carrier a {@link Profile} declares `SQ` is named
2694
+ * there, and an under-declared length upstream can resynchronize the reader
2695
+ * onto four bytes that spell such a block. The package's usual answer to a
2696
+ * fabricated header, `undefinedVrElements`, carries a byte offset and no tag,
2697
+ * and still answers it whenever the fabricated VR is outside the 34 PS3.5
2698
+ * §6.2 defines. It cannot when the fabricated VR is one of them, because those
2699
+ * two files are byte-identical.
2700
+ * - **`embeddedAttributes[].hidden`** - see {@link EmbeddedAttributeFinding}.
2701
+ * Each entry is a tag composed from four bytes that were sitting **inside**
2702
+ * a value, which is the position this type exists to distrust, so on a file
2703
+ * that populates the field at all they are document content by
2704
+ * construction. `PRE-EXISTING` and identical on every release that has
2705
+ * shipped the field; it is disclosed here and its remedy is its own slice,
2706
+ * alongside the cap that field is also missing.
2707
+ * - **`contextPath`, on all four findings that carry one** - see
2708
+ * {@link DeidentifiedAttribute.contextPath}, which holds the measurement. The
2709
+ * segment tags come off the wire with no table behind them, so a fabricated
2710
+ * `SQ` header the reader descended is named there, `PRE-EXISTING`, with **no
2711
+ * warning and no finding array to correlate it with**. This is the field the
2712
+ * rest of this docstring, the tolerance table and the troubleshooting guide
2713
+ * all called structural. **It is a logging hazard and nothing more: on that
2714
+ * same file the de-identified object itself re-emits the fabricated header, so
2715
+ * redacting this field does not make the object safe.**
2546
2716
  *
2547
2717
  * So "the report is safe to log apart from `uidMap`" is **not** an accurate
2548
2718
  * description of this type, and was corrected rather than kept convenient.
@@ -2599,9 +2769,11 @@ interface DeidentifyReport {
2599
2769
  * sequence is still emptied; an array exactly that long means "at least this
2600
2770
  * many", so read it as truncated rather than as a total.
2601
2771
  *
2602
- * **It is not a complete list of what went un-audited, either**: a private
2603
- * `SQ` a {@link Profile} vouches for under `RetainSafePrivate` is kept
2604
- * verbatim and never appears here.
2772
+ * **It covers private sequences too, since `DICOM-PRIVATE-SQ-CARVE-OUT`.** A
2773
+ * private `SQ` a {@link Profile} vouches for under `RetainSafePrivate` used to
2774
+ * be kept verbatim and never appear here; it is now emptied and listed on the
2775
+ * same terms as any other, because the profile's licence under PS3.15 §E.3.10
2776
+ * is over a private attribute and not over an item stream nothing could read.
2605
2777
  */
2606
2778
  readonly unauditableSequences: readonly UnauditableSequenceFinding[];
2607
2779
  /**
@@ -2630,14 +2802,17 @@ interface DeidentifyReport {
2630
2802
  * {@link UndefinedVrFinding}: the tag of a fabricated header is itself part of
2631
2803
  * some element's value.
2632
2804
  *
2633
- * Unlike its sibling this list has **no carve-out**, and the reason is
2634
- * structural rather than a promise: `keepOrEmpty` is the **only** path that
2635
- * writes a source value into de-identified output unchanged, and the test sits
2636
- * at the top of it. Every other outcome - `X` remove, `Z`/`C` empty, `D` dummy,
2637
- * `U` remap, and a private tag the Basic Profile drops - already replaces the
2638
- * value. So a `RetainSafePrivate` element a {@link Profile} vouches for still
2639
- * reaches this test and is still emptied, which is where the sibling
2640
- * `SQ`-with-no-items rule has a real carve-out and this one does not.
2805
+ * This list has **no carve-out**, and the reason is structural rather than a
2806
+ * promise: `keepOrEmpty` is the **only** path that writes a source value into
2807
+ * de-identified output unchanged, and the test sits at the top of it. Every
2808
+ * other outcome - `X` remove, `Z`/`C` empty, `D` dummy, `U` remap, and a
2809
+ * private tag the Basic Profile drops - already replaces the value. So a
2810
+ * `RetainSafePrivate` element a {@link Profile} vouches for still reaches this
2811
+ * test and is still emptied. Its sibling
2812
+ * {@link DeidentifyReport.unauditableSequences} had a real one until
2813
+ * `DICOM-PRIVATE-SQ-CARVE-OUT`; neither has one now, and they arrive at that by
2814
+ * different routes - this one because nothing bypasses `keepOrEmpty`, that one
2815
+ * because a vouched-for private `SQ` is routed into the ordinary `SQ` branches.
2641
2816
  */
2642
2817
  readonly undefinedVrElements: readonly UndefinedVrFinding[];
2643
2818
  /**
@@ -2692,6 +2867,19 @@ interface DeidentifyOptions {
2692
2867
  * Text **added to** `(0012,0063)` De-identification Method. Default names the
2693
2868
  * Basic Profile and the active options.
2694
2869
  *
2870
+ * **The default is multi-valued, one Value per name, and that is a conformance
2871
+ * fix rather than a style choice.** PS3.5 2026c Table 6.2-1 caps an `LO` at
2872
+ * **64 characters per Value**, and `(0012,0063)` is `1-n`; the single-value
2873
+ * default this replaced measured 76 characters with no options and 272 with
2874
+ * all nine, so every run this library ever made wrote a value no `LO` may
2875
+ * legally carry. The Profile name is now one Value of 61 characters and each
2876
+ * active option is its own, so no option subset can exceed the maximum.
2877
+ *
2878
+ * **Your string is not bounded here.** A value of your own longer than 64
2879
+ * characters is written through as given, with no warning: it is yours, and
2880
+ * splitting or truncating it would invent a record you did not write. Split it
2881
+ * on `\` yourself if a strict receiver is in your path.
2882
+ *
2695
2883
  * PS3.15 E.1.1 says this string is "inserted in or added to" the attribute, so
2696
2884
  * a value the incoming Data Set already carried is kept and this one is
2697
2885
  * appended after a `\` as a further value of the `1-n` attribute - the
@@ -2721,7 +2909,10 @@ interface DeidentifyOptions {
2721
2909
  *
2722
2910
  * `DICOM_DEIDENT_METHOD_NOT_ADDED` means "the length ceiling was reached", never
2723
2911
  * "every fallback is disclosed": a `(0012,0063)` a file encoded under a VR other
2724
- * than `LO` is also replaced, and that one is silent.
2912
+ * than `LO` is also replaced, and that one raises `DICOM_DEIDENT_METHOD_NOT_LO`.
2913
+ * Two codes rather than one because the causes are unrelated - the chain outgrew
2914
+ * the VR, or the bytes were never in that VR at all - and a prior value that is
2915
+ * empty or padding only raises neither, because nothing was lost.
2725
2916
  *
2726
2917
  * When the prior value **is** kept, `report.warnings` carries
2727
2918
  * `DICOM_DEIDENT_METHOD_PRIOR_RETAINED`: `(0012,0063)` is not in Table E.1-1, so
@@ -2802,8 +2993,10 @@ declare class DeidentifyError extends Error {
2802
2993
  * nine *metadata-affecting* Annex E Options, driven by the generated Table E.1-1
2803
2994
  * action map ({@link annexE}). It is a **pure** function: the input {@link Dataset}
2804
2995
  * is never mutated; a fresh `Dataset` (with a rebuilt element map and File Meta)
2805
- * is returned alongside a {@link DeidentifyReport} whose two non-value-free
2806
- * fields are named on that type: `uidMap` and `removedPrivateTags`.
2996
+ * is returned alongside a {@link DeidentifyReport} whose non-value-free fields
2997
+ * are named on that type: `uidMap`, `removedPrivateTags`,
2998
+ * `unauditableSequences[].tag` and `embeddedAttributes[].hidden`. Read the list
2999
+ * there, never a count quoted elsewhere.
2807
3000
  *
2808
3001
  * **What it does**
2809
3002
  * - Resolves each attribute's action (basic profile, overridden by an active
@@ -2840,10 +3033,22 @@ declare class DeidentifyError extends Error {
2840
3033
  * replacement (which needs domain context the metadata layer lacks).
2841
3034
  * - Pixel-level options (`CleanPixelData`, `CleanRecognizableVisual`) are out of
2842
3035
  * scope; burned-in text is warned, never cleaned.
2843
- * - A private data element kept under `RetainSafePrivate` is kept *verbatim* - if
2844
- * it is itself a sequence carrying standard PHI attributes, that nested content
2845
- * is not recursed. The profile vouches the element is safe; nest accordingly.
2846
- * - A **standard** (non-private) `SQ` whose `items` the parser did not
3036
+ * - A private data element kept under `RetainSafePrivate` is kept *verbatim*
3037
+ * **unless it carries a Sequence of Items**. A private `SQ` the profile
3038
+ * vouches for is retained as an element but its items are **walked**, so
3039
+ * standard PHI attributes nested inside it are de-identified like any others
3040
+ * (`DICOM-PRIVATE-SQ-CARVE-OUT`; it was kept whole and unexamined through
3041
+ * `0.0.10`). The profile vouches that the *private attribute* is safe, which
3042
+ * PS3.15 §E.3.10 is about; it cannot vouch for a Data Set nested in its value,
3043
+ * which §E.1.1 covers "whether contained in the top level Data Set or embedded
3044
+ * in an Item of a Sequence of Items". **"Carries a Sequence" is decided by the
3045
+ * profile as well as by the parse tree**, because the two disagree about the
3046
+ * same bytes whenever the profile did not reach `parseDicom` (Implicit VR LE
3047
+ * writes no VR) or the sender wrote a binary VR (Explicit VR, where the wire
3048
+ * wins). A carrier the profile declares `SQ` and the tree did not resolve is
3049
+ * emptied rather than kept, keeping its parsed VR
3050
+ * (`DICOM-PRIVATE-SQ-PARSE-VR`). See {@link keepRetainedPrivate}.
3051
+ * - A `SQ` whose `items` the parser did not
2847
3052
  * materialize is **emptied**, not kept:
2848
3053
  * its value is by PS3.5 §7.5.1 a stream of Data Sets, and a run that cannot
2849
3054
  * enumerate them cannot discharge §E.1.1's obligation inside them, so the
@@ -2854,16 +3059,18 @@ declare class DeidentifyError extends Error {
2854
3059
  * `(0010,0020)` Patient IDs among them - into output stamped
2855
3060
  * `(0012,0062) PatientIdentityRemoved = YES`
2856
3061
  * (`DICOM-DEIDENT-RAWBYTES-PASSTHROUGH`, live through `0.0.6`).
2857
- * - Two things are still kept verbatim, deliberately. A **private** `SQ`
2858
- * retained under `RetainSafePrivate` + a {@link Profile}, where the profile has
2859
- * vouched for the element by creator and tag - the pre-existing "kept
2860
- * verbatim" limitation above, unchanged. And an undefined-length **`UN`** whose
2861
- * CP-246 descent was refused: it keeps `vr === "UN"`, and since every ordinary
2862
- * `UN` element also has no items, the test above cannot be applied there
2863
- * without emptying every unknown-VR element in every file. That one is
2864
- * measured and **still leaks** (`PRE-EXISTING`); the reliable consumer-side
2865
- * test remains `el.items === undefined` on a `UN` element you are trusting a
2866
- * report about.
3062
+ * - One thing is still kept verbatim, and it is **not** the private `SQ`: an
3063
+ * undefined-length **`UN`** whose CP-246 descent was refused. It keeps
3064
+ * `vr === "UN"`, and since every ordinary `UN` element also has no items, the
3065
+ * test above cannot be applied there without emptying every unknown-VR element
3066
+ * in every file. That one is measured and **still leaks** (`PRE-EXISTING`);
3067
+ * the reliable consumer-side test remains `el.items === undefined` on a `UN`
3068
+ * element you are trusting a report about. `RetainSafePrivate` no longer
3069
+ * exempts anything from the rule above it - a vouched-for private `SQ` with no
3070
+ * materialized items is emptied on the same terms as any other. The one place
3071
+ * a `UN` **is** answered is the retained-private route, and only because a
3072
+ * profile named it: a private `UN` whose profile entry declares `SQ` is
3073
+ * emptied there. That is a statement about that route and not about `UN`.
2867
3074
  * - An element whose **on-wire VR is not one of the 34** PS3.5 §6.2 defines is
2868
3075
  * **emptied**, not kept (`DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE` +
2869
3076
  * `report.undefinedVrElements`). Such an element is what an *under*-declared
@@ -2892,12 +3099,14 @@ declare class DeidentifyError extends Error {
2892
3099
  * which removal is the branch available here (`(0008,0307)` is not
2893
3100
  * implemented). Every private element the recursion **reaches** in such an
2894
3101
  * Item, at any depth below it, is removed and named in
2895
- * `report.removedPrivateTags`. **There is one carve-out, and it is the same one
2896
- * `#54` was refused for asserting away**: `keepsPrivate` decides *before* the
2897
- * descent, so a **private `SQ`** the profile vouches for is kept **verbatim**,
2898
- * its items are never walked, and this rule is never consulted inside it -
2899
- * measured, and it still leaks a private value absorbed from the root
2900
- * (`PRE-EXISTING`, identical on `164eb39`, its own item). That costs content
3102
+ * `report.removedPrivateTags`. **The carve-out `#54` was refused for asserting
3103
+ * away is CLOSED** (`DICOM-PRIVATE-SQ-CARVE-OUT`): `keepsPrivate` still decides
3104
+ * before the descent, but a **private `SQ`** it vouches for now routes through
3105
+ * {@link keepRetainedPrivate} into the ordinary `SQ` branches instead of being
3106
+ * kept verbatim, so its items **are** walked and this rule **is** consulted
3107
+ * inside them. Read that as "the retention decision no longer decides the fate
3108
+ * of the Data Sets below it", never as "the class is closed" - the residual
3109
+ * list in `CLAUDE.md` is the census. That costs content
2901
3110
  * on a malformed file and nothing at all on a conformant one; without it a
2902
3111
  * private value the sender wrote **outside** the sequence was retained on the
2903
3112
  * Item's reservation and written into output stamped `PatientIdentityRemoved =
@@ -2913,10 +3122,10 @@ declare class DeidentifyError extends Error {
2913
3122
  * inner sequence ejecting a creator into the still-usable Item that encloses it
2914
3123
  * is measured and pinned. The cut is positional, so a reservation the sender
2915
3124
  * wrote **ahead of** the offending sequence is untouched. Every private element
2916
- * after that point is removed and named in `report.removedPrivateTags`, and the
2917
- * **private-`SQ` carve-out above still applies**: a private `SQ` inside the
2918
- * settled run that the profile vouches for is kept verbatim and never walked.
2919
- * See {@link settledBound}.
3125
+ * after that point is removed and named in `report.removedPrivateTags`. A
3126
+ * private `SQ` inside the settled run that the profile vouches for is retained
3127
+ * and **walked**, so the rule reaches its items too - it no longer stops at the
3128
+ * carrier. See {@link settledBound} and {@link keepRetainedPrivate}.
2920
3129
  *
2921
3130
  * @module
2922
3131
  */