@cosyte/dicom 0.0.12 → 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";
@@ -312,21 +320,27 @@ interface ParseOptions {
312
320
  * structural tokens filled in, so it is safe to log whole; the
313
321
  * `DicomParseError` this option raises in its place also carries `snippet`,
314
322
  * **16 raw bytes, unredacted** (D-10), read at the warning's own `byteOffset`.
315
- * **Which element those bytes belong to is not contracted**: that offset's
316
- * frame follows where the element was read - file-absolute at the root,
317
- * relative to the enclosing slice inside a defined-length Sequence or Item,
318
- * and into the inflated stream under Deflated Explicit VR LE - while the
319
- * snippet is cut from whichever buffer the parse is holding, so the two can
320
- * disagree and the bytes can be an unrelated element's value. Do not reason
321
- * from a code's message to what its snippet holds - **measure it, and treat
322
- * every one of them as document content.**
323
- *
324
- * That is the documented design of `snippet` rather than a defect in any one
325
- * code, and turning this option on does not change what any of them means -
326
- * but a message-only PHI review of the lenient path does not transfer to the
327
- * strict one. Log `err.code`, `err.byteOffset` and `err.message`; treat
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
328
325
  * `err.snippet` as PHI.
329
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
+ *
330
344
  * Omit (do not pass `undefined`) to use the default.
331
345
  */
332
346
  readonly strict?: boolean;
@@ -2348,16 +2362,23 @@ declare const DEIDENTIFY_OPTIONS: readonly DeidentifyOption[];
2348
2362
  */
2349
2363
  type AppliedAction = "removed" | "emptied" | "dummied" | "uid-remapped" | "cleaned" | "kept";
2350
2364
  /**
2351
- * One audited attribute outcome. Carries only structural facts - tag, keyword,
2352
- * the resolved Annex E action code, and the SQ context path - **never** a
2353
- * 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}.
2354
2375
  *
2355
2376
  * @example
2356
2377
  * ```ts
2357
2378
  * import { deidentify, parseDicom, type DeidentifiedAttribute } from "@cosyte/dicom";
2358
2379
  * const { report } = deidentify(parseDicom(buf));
2359
2380
  * report.attributes.forEach((a: DeidentifiedAttribute) => {
2360
- * 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
2361
2382
  * });
2362
2383
  * ```
2363
2384
  */
@@ -2367,7 +2388,45 @@ interface DeidentifiedAttribute {
2367
2388
  /** The resolved single action after collapsing any conditional code. */
2368
2389
  readonly action: Exclude<AnnexEActionCode, `${string}/${string}`>;
2369
2390
  readonly applied: AppliedAction;
2370
- /** 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
+ */
2371
2430
  readonly contextPath?: readonly string[];
2372
2431
  /**
2373
2432
  * Present when the action came from a Table E.1-1 row that names a
@@ -2389,9 +2448,15 @@ interface DeidentifiedAttribute {
2389
2448
  * (PS3.15 §E.1 "all instances"; §E.3.5 is the standard's own precedent for
2390
2449
  * removing identifying information embedded inside a string attribute).
2391
2450
  *
2392
- * Every field is structural: `tag` and `vr` are the carrier's, and `hidden`
2393
- * holds tags composed from four bytes each. No decoded value appears here, so
2394
- * 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.
2395
2460
  *
2396
2461
  * @example
2397
2462
  * ```ts
@@ -2409,12 +2474,31 @@ interface EmbeddedAttributeFinding {
2409
2474
  readonly vr: VR;
2410
2475
  /** The tags of the Data Elements found inside the carrier's value, in wire order. */
2411
2476
  readonly hidden: readonly Tag[];
2412
- /** 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
+ */
2413
2485
  readonly contextPath?: readonly string[];
2414
2486
  }
2415
2487
  /**
2416
- * One `SQ` element that was emptied because the parser never materialized its
2417
- * 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`).
2418
2502
  *
2419
2503
  * PS3.5 2026c §7.5.1 "Item Encoding Rules" states that "Each Item Value shall
2420
2504
  * contain a DICOM Data Set composed of Data Elements", so an `SQ` element's
@@ -2433,12 +2517,26 @@ interface EmbeddedAttributeFinding {
2433
2517
  * the carrier, not as a rule about this case.)
2434
2518
  *
2435
2519
  * Both fields are structural: `tag` is the carrier's and `byteLength` is the
2436
- * declared size of the value that was dropped. No decoded value appears here,
2437
- * so this is safe to log.
2438
- *
2439
- * 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
2440
2535
  * `Dataset.warnings`: `DICOM_SQ_NOT_DESCENDED` for a defined-length Implicit VR
2441
- * 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.
2442
2540
  *
2443
2541
  * @example
2444
2542
  * ```ts
@@ -2454,7 +2552,14 @@ interface UnauditableSequenceFinding {
2454
2552
  readonly tag: Tag;
2455
2553
  /** Byte length of the value field that was dropped. Structural, never a value. */
2456
2554
  readonly byteLength: number;
2457
- /** 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
+ */
2458
2563
  readonly contextPath?: readonly string[];
2459
2564
  }
2460
2565
  /**
@@ -2514,8 +2619,14 @@ interface UnauditableSequenceFinding {
2514
2619
  * finding rather than an inconsistency.
2515
2620
  *
2516
2621
  * `byteOffset` locates the element instead - a position this parser counted.
2517
- * Nothing here renders a document byte: an offset the parser counted, a decoded
2518
- * 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.
2519
2630
  *
2520
2631
  * @example
2521
2632
  * ```ts
@@ -2546,24 +2657,62 @@ interface UndefinedVrFinding {
2546
2657
  * as "structural, never a value" the way its siblings are described.
2547
2658
  */
2548
2659
  readonly byteLength: number;
2549
- /** 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
+ */
2550
2668
  readonly contextPath?: readonly string[];
2551
2669
  }
2552
2670
  /**
2553
2671
  * The audit trail returned alongside the de-identified dataset.
2554
2672
  *
2555
2673
  * Most fields are composed from static tables: Part 6 keywords, Annex E action
2556
- * codes, structural `TAG[index]` sequence paths, and registry warning messages.
2557
- * **Two are not, and both are named here rather than in a footnote.**
2558
- *
2559
- * 1. **`uidMap`** - its keys are the source UIDs read out of the file, kept so a
2560
- * caller can make UID replacement consistent across a study or an archive. A
2561
- * Study or SOP Instance UID is a unique identifying number, so treat it as
2562
- * PHI.
2563
- * 2. **`removedPrivateTags`** - see the field's own note. On a *well-formed*
2564
- * file these are the sender's own private tag numbers and carry nothing; on a
2565
- * malformed one a tag can be four bytes of a value, and it is measured, not
2566
- * 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.**
2567
2716
  *
2568
2717
  * So "the report is safe to log apart from `uidMap`" is **not** an accurate
2569
2718
  * description of this type, and was corrected rather than kept convenient.
@@ -2620,9 +2769,11 @@ interface DeidentifyReport {
2620
2769
  * sequence is still emptied; an array exactly that long means "at least this
2621
2770
  * many", so read it as truncated rather than as a total.
2622
2771
  *
2623
- * **It is not a complete list of what went un-audited, either**: a private
2624
- * `SQ` a {@link Profile} vouches for under `RetainSafePrivate` is kept
2625
- * 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.
2626
2777
  */
2627
2778
  readonly unauditableSequences: readonly UnauditableSequenceFinding[];
2628
2779
  /**
@@ -2651,14 +2802,17 @@ interface DeidentifyReport {
2651
2802
  * {@link UndefinedVrFinding}: the tag of a fabricated header is itself part of
2652
2803
  * some element's value.
2653
2804
  *
2654
- * Unlike its sibling this list has **no carve-out**, and the reason is
2655
- * structural rather than a promise: `keepOrEmpty` is the **only** path that
2656
- * writes a source value into de-identified output unchanged, and the test sits
2657
- * at the top of it. Every other outcome - `X` remove, `Z`/`C` empty, `D` dummy,
2658
- * `U` remap, and a private tag the Basic Profile drops - already replaces the
2659
- * value. So a `RetainSafePrivate` element a {@link Profile} vouches for still
2660
- * reaches this test and is still emptied, which is where the sibling
2661
- * `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.
2662
2816
  */
2663
2817
  readonly undefinedVrElements: readonly UndefinedVrFinding[];
2664
2818
  /**
@@ -2839,8 +2993,10 @@ declare class DeidentifyError extends Error {
2839
2993
  * nine *metadata-affecting* Annex E Options, driven by the generated Table E.1-1
2840
2994
  * action map ({@link annexE}). It is a **pure** function: the input {@link Dataset}
2841
2995
  * is never mutated; a fresh `Dataset` (with a rebuilt element map and File Meta)
2842
- * is returned alongside a {@link DeidentifyReport} whose two non-value-free
2843
- * 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.
2844
3000
  *
2845
3001
  * **What it does**
2846
3002
  * - Resolves each attribute's action (basic profile, overridden by an active
@@ -2877,10 +3033,22 @@ declare class DeidentifyError extends Error {
2877
3033
  * replacement (which needs domain context the metadata layer lacks).
2878
3034
  * - Pixel-level options (`CleanPixelData`, `CleanRecognizableVisual`) are out of
2879
3035
  * scope; burned-in text is warned, never cleaned.
2880
- * - A private data element kept under `RetainSafePrivate` is kept *verbatim* - if
2881
- * it is itself a sequence carrying standard PHI attributes, that nested content
2882
- * is not recursed. The profile vouches the element is safe; nest accordingly.
2883
- * - 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
2884
3052
  * materialize is **emptied**, not kept:
2885
3053
  * its value is by PS3.5 §7.5.1 a stream of Data Sets, and a run that cannot
2886
3054
  * enumerate them cannot discharge §E.1.1's obligation inside them, so the
@@ -2891,16 +3059,18 @@ declare class DeidentifyError extends Error {
2891
3059
  * `(0010,0020)` Patient IDs among them - into output stamped
2892
3060
  * `(0012,0062) PatientIdentityRemoved = YES`
2893
3061
  * (`DICOM-DEIDENT-RAWBYTES-PASSTHROUGH`, live through `0.0.6`).
2894
- * - Two things are still kept verbatim, deliberately. A **private** `SQ`
2895
- * retained under `RetainSafePrivate` + a {@link Profile}, where the profile has
2896
- * vouched for the element by creator and tag - the pre-existing "kept
2897
- * verbatim" limitation above, unchanged. And an undefined-length **`UN`** whose
2898
- * CP-246 descent was refused: it keeps `vr === "UN"`, and since every ordinary
2899
- * `UN` element also has no items, the test above cannot be applied there
2900
- * without emptying every unknown-VR element in every file. That one is
2901
- * measured and **still leaks** (`PRE-EXISTING`); the reliable consumer-side
2902
- * test remains `el.items === undefined` on a `UN` element you are trusting a
2903
- * 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`.
2904
3074
  * - An element whose **on-wire VR is not one of the 34** PS3.5 §6.2 defines is
2905
3075
  * **emptied**, not kept (`DICOM_DEIDENT_UNDEFINED_VR_NOT_AUDITABLE` +
2906
3076
  * `report.undefinedVrElements`). Such an element is what an *under*-declared
@@ -2929,12 +3099,14 @@ declare class DeidentifyError extends Error {
2929
3099
  * which removal is the branch available here (`(0008,0307)` is not
2930
3100
  * implemented). Every private element the recursion **reaches** in such an
2931
3101
  * Item, at any depth below it, is removed and named in
2932
- * `report.removedPrivateTags`. **There is one carve-out, and it is the same one
2933
- * `#54` was refused for asserting away**: `keepsPrivate` decides *before* the
2934
- * descent, so a **private `SQ`** the profile vouches for is kept **verbatim**,
2935
- * its items are never walked, and this rule is never consulted inside it -
2936
- * measured, and it still leaks a private value absorbed from the root
2937
- * (`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
2938
3110
  * on a malformed file and nothing at all on a conformant one; without it a
2939
3111
  * private value the sender wrote **outside** the sequence was retained on the
2940
3112
  * Item's reservation and written into output stamped `PatientIdentityRemoved =
@@ -2950,10 +3122,10 @@ declare class DeidentifyError extends Error {
2950
3122
  * inner sequence ejecting a creator into the still-usable Item that encloses it
2951
3123
  * is measured and pinned. The cut is positional, so a reservation the sender
2952
3124
  * wrote **ahead of** the offending sequence is untouched. Every private element
2953
- * after that point is removed and named in `report.removedPrivateTags`, and the
2954
- * **private-`SQ` carve-out above still applies**: a private `SQ` inside the
2955
- * settled run that the profile vouches for is kept verbatim and never walked.
2956
- * 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}.
2957
3129
  *
2958
3130
  * @module
2959
3131
  */