@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/CHANGELOG.md +453 -3
- package/README.md +9 -7
- package/dist/index.cjs +626 -273
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +257 -85
- package/dist/index.d.ts +257 -85
- package/dist/index.mjs +627 -274
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
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
|
-
*
|
|
316
|
-
*
|
|
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.
|
|
2352
|
-
*
|
|
2353
|
-
*
|
|
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); //
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
2393
|
-
*
|
|
2394
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
2417
|
-
*
|
|
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
|
-
*
|
|
2437
|
-
*
|
|
2438
|
-
*
|
|
2439
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
2518
|
-
*
|
|
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
|
-
/**
|
|
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,
|
|
2557
|
-
* **
|
|
2558
|
-
*
|
|
2559
|
-
*
|
|
2560
|
-
*
|
|
2561
|
-
*
|
|
2562
|
-
*
|
|
2563
|
-
*
|
|
2564
|
-
*
|
|
2565
|
-
*
|
|
2566
|
-
*
|
|
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
|
|
2624
|
-
* `SQ` a {@link Profile} vouches for under `RetainSafePrivate`
|
|
2625
|
-
* verbatim and never
|
|
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
|
-
*
|
|
2655
|
-
*
|
|
2656
|
-
*
|
|
2657
|
-
*
|
|
2658
|
-
*
|
|
2659
|
-
*
|
|
2660
|
-
*
|
|
2661
|
-
*
|
|
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
|
|
2843
|
-
*
|
|
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*
|
|
2881
|
-
* it
|
|
2882
|
-
* is
|
|
2883
|
-
*
|
|
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
|
-
* -
|
|
2895
|
-
*
|
|
2896
|
-
*
|
|
2897
|
-
*
|
|
2898
|
-
*
|
|
2899
|
-
*
|
|
2900
|
-
*
|
|
2901
|
-
*
|
|
2902
|
-
*
|
|
2903
|
-
*
|
|
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`. **
|
|
2933
|
-
*
|
|
2934
|
-
* descent,
|
|
2935
|
-
*
|
|
2936
|
-
*
|
|
2937
|
-
*
|
|
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
|
|
2954
|
-
*
|
|
2955
|
-
*
|
|
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
|
*/
|