@cosyte/dicom 0.0.3 → 0.0.5
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 +417 -70
- package/README.md +28 -14
- package/dist/index.cjs +641 -190
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.mjs +641 -190
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,348 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
6
6
|
|
|
7
7
|
### Added
|
|
8
8
|
|
|
9
|
+
- **`README.md` now opens with the shared Cosyte lockup, which follows the reader's color scheme**
|
|
10
|
+
(`ASSETS-P8`). A `<picture>` block above the H1 carries the dark-ground org tile behind a
|
|
11
|
+
`prefers-color-scheme: dark` media query and the light-ground tile as the inner `<img>`, so the
|
|
12
|
+
mark sits on a ground that matches the page it is read on. It replaces the per-package banner,
|
|
13
|
+
which baked the package name and the one-line tagline into pixels while the two lines directly
|
|
14
|
+
beneath it repeated both: the lockup reads "Cosyte" where the H1 reads `@cosyte/dicom`, so the
|
|
15
|
+
duplication goes and the heading stays. The alt text is content rather than decoration, because it
|
|
16
|
+
is what a screen reader reads on the npm page, and it describes the mark itself rather than
|
|
17
|
+
repeating the heading below it.
|
|
18
|
+
|
|
19
|
+
**One stated reason is corrected rather than dropped.** The per-package banner was chosen as a
|
|
20
|
+
plain markdown image, with no `<img>` and no `<picture>`, expressly because whether npm's markdown
|
|
21
|
+
sanitizer preserves a `<picture>` was unverified. It has since been measured on a published
|
|
22
|
+
package page: the `<img>` is hoisted out of its `<picture>` by npm's anchor wrapper, so the light
|
|
23
|
+
cut renders there, and npmjs.com has no dark mode, so that is the correct cut. A renderer that
|
|
24
|
+
strips `<source>` still renders the inner `<img>`, so the worst case is a light-ground mark on a
|
|
25
|
+
dark page, never a missing or broken image. Both tile URLs were rechecked with `curl -I` as
|
|
26
|
+
`200 image/png`, 10513 and 10455 bytes, before this landed, rather than taken from the `live` flag
|
|
27
|
+
in `assets/published-urls.json`, whose own `$fields.status` note says to read `live` for what it
|
|
28
|
+
is: a declaration made on evidence from another repo, never a fact checked there.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- **`CHANGELOG.md` now records what each released version contained.** Every entry in this file sat
|
|
33
|
+
under `[Unreleased]` while npm served `0.0.1`, `0.0.3` and `0.0.4`, and `CHANGELOG.md` is in
|
|
34
|
+
`package.json#files`, so the file shipped inside the tarball telling a consumer that the version
|
|
35
|
+
they had installed was unreleased. That is a false claim on a distributed surface rather than
|
|
36
|
+
untidy bookkeeping. No entry's substance is rewritten: each is moved to the version that actually
|
|
37
|
+
shipped it, reconstructed from the tags, the GitHub releases, and the changesets each
|
|
38
|
+
"Version Packages" commit consumed. `0.0.2` gets no heading because it was never published: it was
|
|
39
|
+
version-bumped on 2026-07-27 and the publish that followed did not run, so its four changesets
|
|
40
|
+
reached npm inside `0.0.3` and are recorded there. That is also why npm's version list skips
|
|
41
|
+
`0.0.2`.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- **`README.md` documented an element-access API that does not exist.** `Dataset.get` / `has` /
|
|
46
|
+
`getAll` take the 8-character `(group,element)` tag (`"00080060"`, case-insensitive) and nothing
|
|
47
|
+
else, but the README's quickstart, feature list, access-pattern section, typed-value examples and
|
|
48
|
+
Philips vendor note all showed `ds.get("Modality")` and `ds.get("(0008,0060)")`. Both forms return
|
|
49
|
+
`undefined`, and because the `Tag` type is `string` the compiler catches neither: the first line a
|
|
50
|
+
reader copied out of the quickstart silently produced nothing. Measured against the built package
|
|
51
|
+
rather than read off the source. The identical defect was found and corrected in
|
|
52
|
+
`docs-content/intro.md` before `0.0.3` (see below) and the README was not swept with it, which is
|
|
53
|
+
the reason to state the rule once here: `get` takes a tag, and a keyword is resolved to its tag
|
|
54
|
+
through `Dictionary.byKeyword(...)?.tag` first. Every example is corrected and the section is
|
|
55
|
+
retitled from "By keyword or tag" to "By tag". `docs-content/spec-notes-model.md` carried a
|
|
56
|
+
neighbouring defect and is corrected with it: it said `getAll` "returns every element at a
|
|
57
|
+
repeating tag", where a `Dataset` holds at most one element per tag, so it returns 0 or 1.
|
|
58
|
+
- **`README.md` claimed compressed transfer syntaxes were readable at the structural level.** It
|
|
59
|
+
said the supported set was the four v1 syntaxes "and any compressed syntax at the structural level
|
|
60
|
+
(fragments preserved)". The dispatch table holds exactly four entries and any other Transfer
|
|
61
|
+
Syntax UID is the fatal `UNSUPPORTED_TRANSFER_SYNTAX`, so a JPEG, JPEG-LS, JPEG2000, RLE or HTJ2K
|
|
62
|
+
object does not parse at all. Confirmed by parsing a synthetic object in `1.2.840.10008.1.2.4.50`
|
|
63
|
+
and catching the throw. Deflated Explicit VR LE is the one compressed syntax that is supported: it
|
|
64
|
+
deflates the dataset stream rather than the pixels, and the corrected text says so rather than
|
|
65
|
+
calling every compressed syntax unreadable. This mattered most in the "index a folder of studies"
|
|
66
|
+
recipe, whose closing line promised that "nothing here throws on a quirky file" while a real
|
|
67
|
+
archive is full of pixel-compressed objects. That recipe now names all four Tier-3 conditions a
|
|
68
|
+
folder walk meets (`UNSUPPORTED_TRANSFER_SYNTAX`, `INVALID_FILE_META` for a truncated or
|
|
69
|
+
partly-copied file, `NOT_DICOM_PART_10` for a non-DICOM file in the folder, `EMPTY_INPUT` for a
|
|
70
|
+
zero-byte one) and says to catch `DicomParseError` per file and skip. The non-goals entry no
|
|
71
|
+
longer implies a compressed object is read structurally either.
|
|
72
|
+
- **`README.md` counted four typed errors where five are exported.** The Error Handling section
|
|
73
|
+
introduced "four typed errors" and then documented `DicomParseError`, `DicomValueError`,
|
|
74
|
+
`DicomSerializeError`, `ProfileDefinitionError` and `DeidentifyError`. The count is now five. The
|
|
75
|
+
same paragraph said warnings are "never thrown", which a profile's `escalate` list contradicts. It
|
|
76
|
+
now says warnings are data unless you ask otherwise, and names `escalate` as the route that asks.
|
|
77
|
+
- **`docs-content/` stated the published version as `0.0.1`, two published releases stale.**
|
|
78
|
+
`installation.md`'s status note and `troubleshooting.md`'s scope entry both named `0.0.1` while
|
|
79
|
+
npm served `0.0.4`. Neither quotes a version any more. Both send the reader to the registry, which
|
|
80
|
+
is the only thing that cannot go stale, by naming the command that reads it.
|
|
81
|
+
`troubleshooting.md` also stopped sending readers to `CLAUDE.md`, an internal file that does not
|
|
82
|
+
ship in the tarball.
|
|
83
|
+
|
|
84
|
+
## [0.0.4] - 2026-07-30
|
|
85
|
+
|
|
86
|
+
### Added
|
|
87
|
+
|
|
88
|
+
- **The element registry is now generated from NEMA's PS3.6 DocBook, the normative publication**
|
|
89
|
+
(`DICOM-UPSTREAM-EDITION-LAG`). `vendor/nema/part06/` pins `part06.xml` (PS3.6 **2026c**) by
|
|
90
|
+
SHA-256, and `scripts/generate-dictionary.ts` applies it as a **per-field overlay** over the
|
|
91
|
+
Innolitics mirror: for a tag both sources carry, PS3.6 wins on name, keyword, VR, VM and
|
|
92
|
+
retirement; a tag PS3.6 carries and the mirror does not is added; a tag the mirror carries and
|
|
93
|
+
PS3.6 does not is **kept**, because PS3.6 retires elements rather than deleting them, so an absence
|
|
94
|
+
is far more likely to be a parse gap here than a withdrawal there and dropping the entry would turn
|
|
95
|
+
a decoded element into an unknown one (that set is empty today, and the generator prints its size).
|
|
96
|
+
The overlay is scoped to the four registry tables (6-1, 7-1, 8-1, 9-1). UIDs stay on the
|
|
97
|
+
`sops.json` + curated path deliberately: `uids.ts` carries the four short forms every DICOM toolkit
|
|
98
|
+
uses instead of PS3.6's "...: Default Transfer Syntax for ..." clauses, and retirement as a
|
|
99
|
+
structured `retired` boolean rather than a trailing " (Retired)" glued into the name, and an
|
|
100
|
+
overlay would undo both. PS3.15 Annex E is a different part and a different generator, unchanged.
|
|
101
|
+
This is the durable fix for the edition lag recorded in `vendor/innolitics/README.md`: the mirror's
|
|
102
|
+
pin is exactly current against its own upstream, but that upstream last refreshed its data on
|
|
103
|
+
2024-04-18, so the tables sat on PS3.6 2024b against a current 2026c and no re-pinning discipline
|
|
104
|
+
could have moved them. The registry no longer comes from a source whose regeneration cadence is not
|
|
105
|
+
ours.
|
|
106
|
+
**The pin is a precondition, not a comment.** The generator recomputes the SHA-256 of the file it
|
|
107
|
+
reads and refuses to generate on a mismatch, reads the edition from the document's own
|
|
108
|
+
`<subtitle>` rather than asserting it, and fails loudly rather than emitting a thinner dictionary
|
|
109
|
+
on a row that is not six cells, a malformed `(gggg,eeee)` tag cell, a non-identifier keyword, an
|
|
110
|
+
unrecognized VR token, or a total under 5,000 registry rows. Two traps in the DocBook are handled
|
|
111
|
+
explicitly and covered by tests: the keyword column carries **13,470 ZERO WIDTH SPACE** line-break
|
|
112
|
+
hints in 2026c, one of which left in produces a keyword that looks right and never matches; and the
|
|
113
|
+
sixth column carries `DICOS` and `DICONDE` dictionary markers alongside `RET (edition)`, so reading
|
|
114
|
+
it as a boolean would retire **391** live tags. Only a leading `RET` retires. Row accounting is
|
|
115
|
+
the part that had to be got right rather than merely asserted: every `<tr>` in a registry table
|
|
116
|
+
must resolve to a matched body row or a header row, which covers unrecognized row markup, a second
|
|
117
|
+
`<tbody>`, and a row outside any `<tbody>` in one check. Counting row opens inside the already
|
|
118
|
+
truncated first-`<tbody>` slice, as an earlier draft did, structurally cannot see the rows it
|
|
119
|
+
dropped: splitting Table 6-1 in two made that version read 207 rows instead of 5,309. Cell text
|
|
120
|
+
is likewise stripped of markup to a fixpoint and then refused if any `<` or `>` survives, rather
|
|
121
|
+
than stripped in a single pass that can leave a residue which reassembles into another tag.
|
|
122
|
+
Every run prints the overlay it applied (shared / added / mirror-only, and the fields overridden
|
|
123
|
+
broken out by field, **with every VR override listed individually**), so a future re-pin of either
|
|
124
|
+
source shows what moved instead of a 5,000-line diff. There is deliberately **no staleness clock**:
|
|
125
|
+
a date-based gate would fire the day it was written, demand an action nobody can take on demand
|
|
126
|
+
(NEMA publishes when NEMA publishes), red unrelated pull requests, and train people to bump a date
|
|
127
|
+
instead of re-deriving anything. "Has NEMA published a new edition" is one command against
|
|
128
|
+
_content_, documented in `vendor/nema/README.md`. What is gated in CI is unchanged and offline:
|
|
129
|
+
the committed dictionary must be byte-for-byte what the pinned inputs produce.
|
|
130
|
+
|
|
131
|
+
### Changed
|
|
132
|
+
|
|
133
|
+
- **The PS3.5 repeating-group bound is now derived from pinned normative documents rather than
|
|
134
|
+
transcribed (`DICOM-PS35-NOT-VENDORED`). No runtime impact: the emitted bound is identical and no
|
|
135
|
+
public API moves.** `deidentify()` expands PS3.15 Table E.1-1's `(50xx,xxxx)`, `(60xx,3000)` and
|
|
136
|
+
`(60xx,4000)` rows over the concrete groups PS3.5 section 7.6 admits, sixteen even groups per mask
|
|
137
|
+
(`5000`-`501E`, `6000`-`601E`), not 256. That bound is a safety limit in **both** directions, and it
|
|
138
|
+
was the last input here that lived as a **quotation in a source file** while PS3.6 and PS3.15 were
|
|
139
|
+
SHA-pinned and re-hashed before use, so the generator's guard could catch a new mask _prefix_ but
|
|
140
|
+
never a changed _bound_.
|
|
141
|
+
`vendor/nema/part05/` pins `part05.xml` (PS3.5 **2026c**) and `vendor/nema/part05-2004/` pins
|
|
142
|
+
`04_05pu.pdf`, both re-hashed as a precondition, and `scripts/generate-repeating-groups.ts` emits
|
|
143
|
+
`src/dictionary/generated/repeating-groups.ts`, which `src/dictionary/repeating-groups.ts`
|
|
144
|
+
re-exports. **Two documents, because the bound is split across two editions:** the current one
|
|
145
|
+
states the overlay bound normatively and excludes the odd `6001`-`601F`, but says nothing about the
|
|
146
|
+
curve bound, having retired curve encoding and delegated it to PS3.5-2004 by URL in section 7.6's
|
|
147
|
+
own Note. So the 2004 PDF is the authority the edition in force names, not a convenience copy. The
|
|
148
|
+
generator **proves that delegation** (section 7.6 must link exactly the vendored URL) instead of
|
|
149
|
+
assuming it, and because both editions state the overlay bound it **requires them to agree**.
|
|
150
|
+
`gen:repeating-groups` runs first in `gen:all`, since the Annex E generator expands family rows
|
|
151
|
+
through this bound.
|
|
152
|
+
**The values did not change; their falsifiability did.** Moving the overlay bound in either
|
|
153
|
+
vendored edition reds the cross-check, moving the curve bound in the 2004 edition moves the emitted
|
|
154
|
+
artifact (caught by the byte-identical regen gate), and removing the delegation link stops the 2004
|
|
155
|
+
document being used at all. Each was confirmed non-vacuous by disabling the guard and watching the
|
|
156
|
+
test fail. Reading a 2004 PDF needs a PDF reader; the one in that generator is deliberately minimal
|
|
157
|
+
(Node `zlib` only, one sentence recovered, matched against a precise expected shape) and is not to
|
|
158
|
+
be grown into a general parser. There is no staleness clock, for the same reasons as PS3.6 and
|
|
159
|
+
PS3.15.
|
|
160
|
+
- **Recorded what the vendor pin is actually current against (`DICOM-DICT-CURRENCY`).**
|
|
161
|
+
`vendor/innolitics/README.md` gains a measured Currency section and drops the "re-pin monthly"
|
|
162
|
+
policy, which nothing ran and which measured the wrong thing. The pin is exactly current against
|
|
163
|
+
upstream: `git ls-remote` resolves `master` to the pinned SHA and all four vendored files are
|
|
164
|
+
byte-identical to upstream at it. Upstream itself is the stale link, having last changed
|
|
165
|
+
`attributes.json` on 2024-04-18 ("Update standard to rev2024b"), so the tag tables are grounded in
|
|
166
|
+
PS3.6 2024b while the current edition is PS3.6 2026c. The drift is now measured rather than
|
|
167
|
+
assumed, against the NEMA DocBook source for 2026c, and every non-additive difference is named in
|
|
168
|
+
that file. Figures on `86ab6c1` (`origin/main` at measurement time; this slice does not touch
|
|
169
|
+
`tags.ts` or `keywords.ts`): all **5,129** committed tags are shared with PS3.6 2026c, and across
|
|
170
|
+
them there are **zero VR differences**, zero name differences and zero VM differences, with 2
|
|
171
|
+
keyword and 2 retirement-status differences. **Every tag this dictionary carries is still in
|
|
172
|
+
PS3.6 2026c**; the drift is otherwise purely additive, 180 tags the standard has gained. VR is
|
|
173
|
+
what turns bytes into a value, so zero VR drift is the result that matters for reading a real
|
|
174
|
+
study. Nothing in the dictionary is hand-edited to close the gap: it is generated, and the
|
|
175
|
+
remaining differences are recorded for the next re-pin. One method note lives with the
|
|
176
|
+
measurement, because getting it wrong is easy and was got wrong on the first run: DICOM tag values
|
|
177
|
+
are hexadecimal and their case is not semantic, so tag keys must be compared case-insensitively.
|
|
178
|
+
PS3.6 prints them uniformly uppercase and `tags.ts` agrees on 1,540 of its 5,129 keys, but
|
|
179
|
+
`tags.ts` lowercases the 8 repeating-group keys whose trailing digits are hex letters and no
|
|
180
|
+
others, so a verbatim comparison mis-classifies exactly those 8.
|
|
181
|
+
|
|
182
|
+
### Fixed
|
|
183
|
+
|
|
184
|
+
- **`deidentify()` never removed and never reported the repeating-group attributes PS3.15 marks `X`**
|
|
185
|
+
(`DICOM-REPEATING-GROUP-DEID-GAP`). Table E.1-1 states three of its rows as a group mask rather
|
|
186
|
+
than a single tag: `(50xx,xxxx)` Curve Data, `(60xx,3000)` Overlay Data and `(60xx,4000)` Overlay
|
|
187
|
+
Comments, all three marked **X** (remove). The matcher keyed attributes by exact tag, which cannot
|
|
188
|
+
express a mask, so all three rows were unreachable; `deidentify()` reads "not in the table" as
|
|
189
|
+
"not listed, keep", so a file carrying `(6000,4000)` free text came back with a **clean report and
|
|
190
|
+
the text still in it**. Overlay comments are a classic burned-in-identifier carrier, and
|
|
191
|
+
`DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED` gives no coverage here: it keys on `(7FE0,0010)` +
|
|
192
|
+
`(0028,0301)`, which describe the image, not the overlay planes. This shipped at `0.0.3`, and
|
|
193
|
+
`deid`'s `/dicom` adapter delegates here, so it had the same hole.
|
|
194
|
+
The three rows are now generated from the pinned PS3.15 DocBook as pattern rules and consulted on
|
|
195
|
+
an exact-tag miss. A matched element is **removed and reported**: the report entry carries the
|
|
196
|
+
concrete tag, the family's attribute name, and a new `DeidentifiedAttribute.repeatingGroup` field
|
|
197
|
+
naming the mask that matched, so an audit distinguishes a mask hit from a single-tag hit. The
|
|
198
|
+
`CleanGraphics` column is honoured on these rows exactly as the table states it. **The exact-tag
|
|
199
|
+
table wins** over a mask that would also cover the tag, as the more specific statement the standard
|
|
200
|
+
makes about it; 2026c publishes no such overlap and the generator counts and prints it every run.
|
|
201
|
+
**The mask covers the groups PS3.5 bounds it to, not any four hex digits.** PS3.5 §7.6: repeating
|
|
202
|
+
groups "shall only be allowed in the even numbered Groups 6000-601E", and PS3.5-2004 §7.6, which
|
|
203
|
+
the current edition's note delegates to for curves, states "even Groups (5000-501E,eeee)". Sixteen
|
|
204
|
+
groups per mask, not 256, and the same section says of the odd ones that there is "no implication
|
|
205
|
+
of repeating semantics". Both bounds are load-bearing in opposite directions: a hex wildcard would
|
|
206
|
+
remove attributes the standard never marked (data loss on a call asked to be conservative), and an
|
|
207
|
+
exact tag matched nothing. Odd groups in the overlay range are private and still go through the
|
|
208
|
+
private-attribute path.
|
|
209
|
+
**The generator now refuses a masked row on a prefix PS3.5 does not define as a repeating group**,
|
|
210
|
+
rather than printing it and carrying on, which is what let these three rows go missing. Proven by
|
|
211
|
+
mutation rather than asserted: against a Table E.1-1 carrying an injected `(7Fxx,0010)` family row
|
|
212
|
+
the pre-remedy generator exits 0 and drops it, the post-remedy one exits 1 and names the prefix. A
|
|
213
|
+
second mutation moves the Overlay Comments Basic Profile code `X` -> `K` and asserts the emitted
|
|
214
|
+
rule moves with it, so the action codes are read from the document rather than assumed. Every run
|
|
215
|
+
prints the rules emitted, the concrete groups each covers, the private family row it does not
|
|
216
|
+
emit, and the shadowing count.
|
|
217
|
+
- **`RetainLongitudinalTemporal` now documents which E.3.6 sub-option a caller actually gets.**
|
|
218
|
+
Behaviour is unchanged: PS3.15 §E.3.6 is two options and this package exposes one name carrying the
|
|
219
|
+
**full-dates** column, the less protective branch. It is `K` on all **169** rows where
|
|
220
|
+
modified-dates says `C`. The option's JSDoc and `docs-content/troubleshooting.md` now say so, and
|
|
221
|
+
say that date shifting is not performed at this layer. Splitting the option is a public-surface
|
|
222
|
+
change and is deliberately not made here.
|
|
223
|
+
- **`deidentify()` retained 35 patient-identifying attributes and reported nothing**
|
|
224
|
+
(`DICOM-ANNEX-E-DEID-LAG`). The PS3.15 Annex E action table came from a third-party mirror alone,
|
|
225
|
+
pinned at a snapshot whose Table E.1-1 data is 2024b-era. Current PS3.15 publishes 652 concrete
|
|
226
|
+
attributes; that snapshot carried 617. Because `annexE()` returns `undefined` for a tag it does not
|
|
227
|
+
carry and `deidentify()` reads `undefined` as "not listed, keep", every missing attribute came
|
|
228
|
+
through the call verbatim, and the `DeidentifyReport`, whose whole job is to say what was done,
|
|
229
|
+
said nothing about them. The caller's only signal was a clean return. This shipped at `0.0.3`.
|
|
230
|
+
**32 of the 35 are marked X** (remove) by the standard; the other three are `(0040,B020)` Waveform
|
|
231
|
+
Annotation Sequence (`X/D`), `(0070,0006)` Unformatted Text Value (`D`), and `(300A,0054)` Table Top
|
|
232
|
+
Position Alignment UID (`U`). The removals include `(0010,0011)` through `(0010,0016)`,
|
|
233
|
+
the person-names-to-use and pronoun block, of which `(0010,0012)` **Name to Use is a patient's
|
|
234
|
+
preferred name and survived verbatim**; `(0010,0041)` through `(0010,0047)`, the gender-identity
|
|
235
|
+
and sex-parameters-for-clinical-use attributes; `(0010,2161)` `EthnicGroupCodeSequence` and
|
|
236
|
+
`(0010,2162)` `EthnicGroups`, the two attributes that replaced the retired `(0010,2160)`
|
|
237
|
+
`EthnicGroup`; the four `(0008,130x)` diagnosis code sequences; and the waveform, montage,
|
|
238
|
+
acquisition-context and display-URI attributes at `(003A,xxxx)`, `(0040,Axxx)`, `(0040,Bxxx)` and
|
|
239
|
+
`(0040,E012)`. `(0032,1033)` `RequestingService` also gains the `CleanDescriptors: C` column the
|
|
240
|
+
mirror had dropped.
|
|
241
|
+
The fix is the same authority rule the data dictionary already uses, pointed at PS3.15.
|
|
242
|
+
`vendor/nema/part15/` pins `part15.xml` (PS3.15 **2026c**) by SHA-256, and
|
|
243
|
+
`scripts/generate-annex-e.ts` applies Table E.1-1 as a **per-field overlay** over the mirror: for a
|
|
244
|
+
tag both sources carry, PS3.15 wins on attribute name, Basic Profile action code, and all nine
|
|
245
|
+
metadata-affecting option columns; a tag PS3.15 carries and the mirror does not is added; a tag the
|
|
246
|
+
mirror carries and PS3.15 does not is **kept**, because the standard retires rather than deletes,
|
|
247
|
+
so an absence is far more likely to be a parse gap here than a withdrawal there and dropping the
|
|
248
|
+
entry would turn an attribute the de-identifier acts on into one it silently keeps. That set is
|
|
249
|
+
empty today, and its size prints on every run so the assumption stays observable. Nothing is
|
|
250
|
+
hand-corrected: every value is derived from the fetched normative bytes and reproduced by every
|
|
251
|
+
regen. No Basic Profile code changed for a tag both sources carried, which is the reassuring half
|
|
252
|
+
of the finding: the mirror was not wrong about what it had, only silent about what it lacked.
|
|
253
|
+
**The pin is a precondition, not a comment.** The generator recomputes the SHA-256 of the file it
|
|
254
|
+
reads and refuses to generate on a mismatch, reads the edition from the document's own
|
|
255
|
+
`<subtitle>` rather than asserting it, and fails loudly rather than emitting a thinner table on a
|
|
256
|
+
header row whose 15 column labels are not where the generator's indices expect them, a body row that
|
|
257
|
+
is not 15 cells, a tag cell it does not recognize, an action code outside Table E.1-1a in any
|
|
258
|
+
non-empty action column, an empty Basic Profile cell, a `<tr>` that resolves to neither a matched
|
|
259
|
+
body row nor a header row, or a total under 600 concrete rows. The header check is the one that
|
|
260
|
+
catches a column **reorder**, which a cell count cannot see and which would read one option's action
|
|
261
|
+
code as another's. Every run prints the overlay
|
|
262
|
+
it applied, **with every action-code override listed individually**, because a changed action code
|
|
263
|
+
is the one difference that decides whether an identifier survives.
|
|
264
|
+
Two exclusions are deliberate and now **printed on every run** instead of being assumed. Four rows
|
|
265
|
+
of Table E.1-1 state a family rather than a single tag (`(50xx,xxxx)` Curve Data, `(60xx,3000)`
|
|
266
|
+
Overlay Data, `(60xx,4000)` Overlay Comments, and `(gggg,eeee) where gggg is odd` Private
|
|
267
|
+
Attributes); an exact-tag map cannot key them, private attributes are removed through their own
|
|
268
|
+
path, and the three repeating-group rows remain a stated gap. And PS3.15's E.3.6 is two options,
|
|
269
|
+
full dates and modified dates, against this package's single `RetainLongitudinalTemporal`, which
|
|
270
|
+
carries the full-dates column; the two columns diverge on **169** of the 652 rows in 2026c, and
|
|
271
|
+
that count now prints rather than living in a comment that guessed "usually not".
|
|
272
|
+
There is deliberately **no staleness clock**, for the same reasons as PS3.6: a date gate fires the
|
|
273
|
+
day it is written, demands an action nobody can take on demand, and reds unrelated pull requests.
|
|
274
|
+
What CI gates is unchanged and offline: the committed table must be byte-for-byte what the pinned
|
|
275
|
+
inputs produce.
|
|
276
|
+
- **Three defects in the shipped data dictionary, all resolved from normative bytes**
|
|
277
|
+
(`DICOM-UPSTREAM-EDITION-LAG`). None is hand-corrected; each is what PS3.6 2026c prints, and each
|
|
278
|
+
is reproduced by every regen.
|
|
279
|
+
- **`(0010,2160)` `EthnicGroup` was marked current although PS3.6 retired it in 2025a**, and its
|
|
280
|
+
replacements were absent entirely. It is now `retired: true`, and `(0010,2161)`
|
|
281
|
+
`EthnicGroupCodeSequence` (`SQ`, VM 1) and `(0010,2162)` `EthnicGroups` (`UC`, VM 1-n) are
|
|
282
|
+
present. This is a real demographic-attribute change: an application reading only `EthnicGroup`
|
|
283
|
+
silently misses what current instances actually carry. Retired does not mean removed, so the
|
|
284
|
+
keyword still resolves and an older study still reads; the caller is told both what the tag is
|
|
285
|
+
and that it is no longer current.
|
|
286
|
+
- **`(3004,0012)` `DoseValue` was marked retired although PS3.6 defines it.** The `RET (2022d)`
|
|
287
|
+
marker belongs to the preceding row, `(3004,0010)` `RTDoseROISequence`, which remains retired.
|
|
288
|
+
`DoseValue` is now `retired: false`. A dose attribute wrongly flagged retired is the dangerous
|
|
289
|
+
direction of that error: it is the one that talks a caller out of reading a real RT dose.
|
|
290
|
+
- **`(003A,0320)` and `(003A,0325)` carried truncated keywords**, `SummarizedFilterLookupTable` and
|
|
291
|
+
`AnalogFilterType`, so `byKeyword` missed on the spelling PS3.6 publishes. They are now
|
|
292
|
+
`SummarizedFilterLookupTableSequence` and `AnalogFilterTypeCodeSequence`. The truncated forms are
|
|
293
|
+
**gone rather than kept as aliases**: they were never PS3.6 keywords, and carrying them would
|
|
294
|
+
preserve the defect under a nicer name.
|
|
295
|
+
- Alongside these, **180 tags PS3.6 has gained are now known**, so `Dictionary.lookup` names them
|
|
296
|
+
and Implicit VR parsing resolves their VR from the dictionary instead of falling back to `UN`.
|
|
297
|
+
The registry goes from **5,129 to 5,309** tags and `byKeyword` from **5,035 to 5,214** keywords.
|
|
298
|
+
Zero VR, name and VM differences existed on the 5,129 shared tags and zero tags were dropped, so
|
|
299
|
+
**no previously-decoded element decodes differently**; the change is corrections plus additions.
|
|
300
|
+
|
|
301
|
+
- **174 SOP Class UID names in `UIDS` were wrong (`DICOM-DICT-CURRENCY`).** The dictionary generator
|
|
302
|
+
appended `" Storage"` to every name it took from the vendor `sops.json`, but that field is already
|
|
303
|
+
the full PS3.6 Table A-1 UID Name. The result was a doubled suffix on the 164 names that already
|
|
304
|
+
ended in "Storage", so `UIDS["1.2.840.10008.5.1.4.1.1.2"].name` read **"CT Image Storage Storage"**,
|
|
305
|
+
and an equally wrong tail on 10 of the other 11 ("Digital X-Ray Image Storage - For Presentation
|
|
306
|
+
Storage"). Every one shipped in `0.0.1` through `0.0.3`. The suffix is gone and the names now come
|
|
307
|
+
through verbatim.
|
|
308
|
+
The rule was wrong for all 175 entries it touched but landed on the right string for exactly one,
|
|
309
|
+
so removing it needed a compensating correction rather than a second blanket rule:
|
|
310
|
+
`1.2.840.10008.5.1.4.1.1.79.1` is the single UID whose vendor name genuinely lacks the suffix, and
|
|
311
|
+
it is now pinned in the generator's curated table to the "Macular Grid Thickness and Volume Report
|
|
312
|
+
Storage" that PS3.6 2026c Table A-1 gives it.
|
|
313
|
+
One further name is corrected: `1.2.840.10008.1.2.6.1`, the retired RFC 2557 transfer syntax, read
|
|
314
|
+
"RFC 2557 MIME Encapsulation" where PS3.6 prints "RFC 2557 MIME encapsulation".
|
|
315
|
+
Measured against PS3.6 2026c on the branch head: of the **261** UIDs shared with Table A-1,
|
|
316
|
+
**240 match the `UID Name` column byte for byte**; a further **17** differ only in that Table A-1
|
|
317
|
+
writes retirement into the name as a trailing " (Retired)" where this dictionary carries a
|
|
318
|
+
structured `retired` boolean, which gives **257** when those are read as matches and **zero**
|
|
319
|
+
retirement-flag disagreements; the remaining **4** are the deliberate transfer-syntax short forms
|
|
320
|
+
tabulated in `vendor/innolitics/README.md`; **0** are unexplained. All **7** well-known frames of
|
|
321
|
+
reference match Table A-2 byte for byte. `uids.ts` changes on 175 lines in total.
|
|
322
|
+
Name only: no UID value, `type`, or `retired` flag changed. Transfer-syntax dispatch keys off the
|
|
323
|
+
UID **value**, never the name, so no parse or de-identification behavior changes. The one place a
|
|
324
|
+
name reaches a caller at runtime is the human-readable `snippet` on the fatal
|
|
325
|
+
`UNSUPPORTED_TRANSFER_SYNTAX` error, which is improved by the correction rather than altered in
|
|
326
|
+
meaning.
|
|
327
|
+
- **The byte-identical regen gate could go green while generating nothing (`DICOM-DICT-CURRENCY`).**
|
|
328
|
+
Two ways, both fixed by making the gate delete `src/dictionary/generated/` (except the hand-written
|
|
329
|
+
`README.md`) before running `pnpm gen:all`, so it measures what the generators produce instead of
|
|
330
|
+
what happens to be on disk. A stale orphan, an artifact a generator used to emit and no longer
|
|
331
|
+
does, was never rewritten and never diffed, so it passed indefinitely. And gutting `gen:all` to a
|
|
332
|
+
no-op wrote nothing, left every committed artifact untouched, and produced an empty `git diff`:
|
|
333
|
+
permanently green. `package.json` also joins the workflow's `paths` filters, without which that
|
|
334
|
+
second case never triggered the gate at all. Both now red, proved by seeding each defect and
|
|
335
|
+
watching the run fail. The delete is `-mindepth 1` rather than `-type f`, so a stale symlink or
|
|
336
|
+
subdirectory cannot become an orphan the gate is blind to, and the step asserts that only
|
|
337
|
+
`README.md` survives instead of printing an expectation nothing checks. `pnpm gen:clean` exposes
|
|
338
|
+
the same delete for local use but is deliberately **not** chained into `gen:all`: the generators
|
|
339
|
+
throw on malformed or missing vendor input, which is the normal state part-way through a re-pin,
|
|
340
|
+
and a chained clean would leave the working tree empty and the build broken. The gate does its own
|
|
341
|
+
delete rather than trusting the script under test.
|
|
342
|
+
|
|
343
|
+
## [0.0.3] - 2026-07-27
|
|
344
|
+
|
|
345
|
+
This release carries two version bumps' worth of change. `0.0.2` was bumped on the same day and
|
|
346
|
+
never published, so the four changesets it consumed reached npm here, in `0.0.3`, together with
|
|
347
|
+
the regen-gate repair below.
|
|
348
|
+
|
|
349
|
+
### Added
|
|
350
|
+
|
|
9
351
|
- **Em-dash brand gate in CI (`EMDASH-CONFORMANCE`).** The founder directive of 2026-07-24
|
|
10
352
|
(`knowledgebase/06-brand/voice-and-tone.md`, "No em dashes. Ever.") bans `U+2014` outright across
|
|
11
353
|
every cosyte surface and names commit messages explicitly, and the meta-repo's
|
|
@@ -69,6 +411,11 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
69
411
|
checked, so a documented example can never silently drift from the code. All examples use
|
|
70
412
|
synthetic, base64-encoded Part 10 objects (invented patient, fake UIDs); no real PHI, no `.dcm`
|
|
71
413
|
file on disk.
|
|
414
|
+
- **Trademark notice (`TRADEMARKS.md`).** This package names third-party systems to describe what it
|
|
415
|
+
interoperates with; the notice records that cosyte is not affiliated with, endorsed by, or
|
|
416
|
+
sponsored by any of them, that every reference is descriptive, and that the built-in profiles are
|
|
417
|
+
authored from public sources only. Added to `files` so it ships inside the published tarball, not
|
|
418
|
+
just on GitHub. Documentation only: no runtime or API change.
|
|
72
419
|
|
|
73
420
|
### Changed
|
|
74
421
|
|
|
@@ -126,7 +473,8 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
126
473
|
therefore "a no-op until that flag is removed". `package.json` carries no `private` field, the repo
|
|
127
474
|
is public, and the package ships on npm at `0.0.1`, so the comment described the release pipeline as
|
|
128
475
|
inert when it is live. Documentation only, no behavior change, but a misleading note on a workflow
|
|
129
|
-
that really does publish is worth more than a stale one elsewhere.
|
|
476
|
+
that really does publish is worth more than a stale one elsewhere. (`0.0.1` was the published
|
|
477
|
+
version on that date.)
|
|
130
478
|
|
|
131
479
|
- **Corrected stale publish-status language in `docs-content/` (`README-ORG-SWEEP`).**
|
|
132
480
|
`installation.md` claimed the package was "not yet published to npm" and that the install command
|
|
@@ -134,57 +482,13 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
134
482
|
published … not on npm; the first provenance publish is gated on the coordinated public launch"
|
|
135
483
|
non-goal. `@cosyte/dicom` is published on npm at `0.0.1` and public. Both now state the truth
|
|
136
484
|
(published, public, still pre-alpha on the `0.0.x`-until-first-alpha ladder), and the troubleshooting
|
|
137
|
-
bullet becomes an honest pin-your-version pre-alpha caveat rather than a stale non-goal.
|
|
485
|
+
bullet becomes an honest pin-your-version pre-alpha caveat rather than a stale non-goal. (`0.0.1`
|
|
486
|
+
was the published version on that date; `npm view @cosyte/dicom version` is the current one.)
|
|
138
487
|
|
|
139
|
-
|
|
140
|
-
scaffold commit and was never explained; `changeset publish` silently skips a private package, so
|
|
141
|
-
this repo was the one parser that could not reach npm even once its pipeline worked. It also
|
|
142
|
-
contradicted the `publishConfig: { access: "public" }` in the same file. Removed as part of the
|
|
143
|
-
coordinated public launch (`PUB-FLIP`).
|
|
144
|
-
|
|
145
|
-
- **The release can actually bump the version.** `package.json` had no `version` script, so the
|
|
146
|
-
shared pipeline's `pnpm run version` failed with `Command "version" not found` and the release
|
|
147
|
-
aborted before opening a "Version Packages" PR. Adds `scripts/sync-version.mjs` (the `hl7`
|
|
148
|
-
reference, retargeted at `src/version.ts`) and the `version` script that runs it after
|
|
149
|
-
`changeset version`, so the bump and the `VERSION` constant land in the same commit.
|
|
150
|
-
- **`VERSION` is no longer typed as a string literal.** It was declared `export const VERSION =
|
|
151
|
-
"0.0.0"`, giving it the literal type `"0.0.0"`, so the exported type would change on every
|
|
152
|
-
release, making each version bump a breaking type change. Now annotated `: string`, matching the
|
|
153
|
-
`hl7` reference. Type-only; the runtime value is unchanged. Done now because the package is
|
|
154
|
-
unpublished. After the first publish this would itself be a breaking change.
|
|
155
|
-
|
|
156
|
-
- **The Release workflow can actually start.** `.github/workflows/release.yml` calls the shared
|
|
157
|
-
`cosyte/.github` pipeline, which requests `contents`/`id-token`/`pull-requests: write`, but declared
|
|
158
|
-
no `permissions:` of its own, so it inherited the repo default of `contents: read`. A called
|
|
159
|
-
workflow may only downgrade the caller's `GITHUB_TOKEN`, never escalate it, so GitHub rejected the
|
|
160
|
-
workflow at startup (~1s, no jobs, no logs). Every Release run from June 2026 until now failed this
|
|
161
|
-
way, unnoticed, because a `startup_failure` produces no logs to read. The caller job now declares
|
|
162
|
-
the three scopes explicitly. CI-only: no runtime or API change.
|
|
163
|
-
|
|
164
|
-
### Security
|
|
165
|
-
|
|
166
|
-
- **Dev-dependency advisory remediation (no runtime impact: the published
|
|
167
|
-
artifact is unchanged).** Added scoped `pnpm.overrides` pinning two
|
|
168
|
-
transitive **dev/build-time** packages to their patched releases: `esbuild`
|
|
169
|
-
(`>=0.27.3 <0.28.1` → `0.28.1`; GHSA dev-server path-traversal, not
|
|
170
|
-
reachable here: the library builds via `tsup`/`vitest` and never runs
|
|
171
|
-
`esbuild serve`) and the `@changesets/parse` copy of `js-yaml`
|
|
172
|
-
(`>=4.0.0 <4.2.0` → `4.2.0`; GHSA-h67p-54hq-rp68 merge-key DoS). The
|
|
173
|
-
`js-yaml@3.14.2` pulled by `read-yaml-file@1.1.0` (via
|
|
174
|
-
`@manypkg/get-packages` → `@changesets/cli`) is **intentionally left**: it
|
|
175
|
-
calls `yaml.safeLoad`, removed/throwing in js-yaml 4, so it cannot be
|
|
176
|
-
force-upgraded without breaking the release tooling, and it only parses
|
|
177
|
-
trusted local repo YAML at release time. This is the shared canonical
|
|
178
|
-
override block, enforced suite-wide by the `@cosyte/config` drift check.
|
|
488
|
+
## [0.0.1] - 2026-07-17
|
|
179
489
|
|
|
180
490
|
### Added
|
|
181
491
|
|
|
182
|
-
- **Trademark notice (`TRADEMARKS.md`).** This package names third-party systems to describe what it
|
|
183
|
-
interoperates with; the notice records that cosyte is not affiliated with, endorsed by, or
|
|
184
|
-
sponsored by any of them, that every reference is descriptive, and that the built-in profiles are
|
|
185
|
-
authored from public sources only. Added to `files` so it ships inside the published tarball, not
|
|
186
|
-
just on GitHub. Documentation only: no runtime or API change.
|
|
187
|
-
|
|
188
492
|
- **Lossless File Meta round-trip.** The parser now retains non-modeled
|
|
189
493
|
`(0002,xxxx)` File Meta elements (e.g. `(0002,0017)`/`(0002,0018)`
|
|
190
494
|
Sending/Receiving AE Title, `(0002,0100)` Private Information Creator UID,
|
|
@@ -266,28 +570,6 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
266
570
|
built only from the code + the offending Transfer Syntax UID (structural facts), never a decoded
|
|
267
571
|
value, so it is always safe to log. New public exports: `serializeDicom`, `DicomSerializeError`,
|
|
268
572
|
`SERIALIZE_ERROR_CODES`, `SerializeErrorCode`.
|
|
269
|
-
|
|
270
|
-
### Known limitations
|
|
271
|
-
|
|
272
|
-
- **File Meta round-trip is over the modeled surface, not byte-exact.** Only the typed `FileMeta`
|
|
273
|
-
fields round-trip; any other `(0002,xxxx)` element a source file carried (e.g. `(0002,0100)` Private
|
|
274
|
-
Information Creator UID) is dropped at _parse_ time (the Phase 2 `FileMeta` view does not model it)
|
|
275
|
-
and so cannot be re-emitted. The preamble is normalized to zeros and odd-length values are padded
|
|
276
|
-
even. The output stays spec-clean but is not a byte-identical copy of a non-conformant input.
|
|
277
|
-
|
|
278
|
-
### Tests
|
|
279
|
-
|
|
280
|
-
- **Enhanced multi-frame coverage (DICOM-COV).** Closed the Per-Frame-else-Shared branch gaps left by
|
|
281
|
-
the Phase 4 functional-group resolver (`functional-groups.ts`: ~53% → 100% branch): both optional
|
|
282
|
-
macros (Pixel Value Transformation `(0028,9145)`, Frame VOI LUT `(0028,9132)`), Pixel Measures
|
|
283
|
-
`spacingBetweenSlices`, shared-only resolution (no Per-Frame Functional Groups Sequence), the
|
|
284
|
-
lenient inner-attribute-absence paths (a macro item present but its attributes omitted ⇒ typed-absent,
|
|
285
|
-
never coerced), and all three `MISSING_REQUIRED_FUNCTIONAL_GROUP` throws (Pixel Measures / Plane
|
|
286
|
-
Position / Plane Orientation). Synthetic fixtures only; no public-surface change. Per-directory
|
|
287
|
-
coverage now sits genuinely ≥ 90 on every gated directory (global branches 93.2%).
|
|
288
|
-
|
|
289
|
-
### Added
|
|
290
|
-
|
|
291
573
|
- **Safety-critical domain helpers (Phase 4).** New `Dataset` accessors `patient` / `study` /
|
|
292
574
|
`series` / `image` return typed, fail-safe views over the DICOM §4 safety-critical attributes
|
|
293
575
|
(memoized on first access). `patient` surfaces the `{id, issuerOfId, issuerQualifiers}` identity
|
|
@@ -352,3 +634,68 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
352
634
|
- CI/release workflows reduced to thin callers of the reusable `cosyte/.github` pipelines
|
|
353
635
|
(`ci.yml` runs the shared PHI scan; `release.yml` targets `@cosyte/dicom`). The repo-specific
|
|
354
636
|
byte-identical dictionary-regen workflow is kept and bumped to Node 22.
|
|
637
|
+
|
|
638
|
+
### Fixed
|
|
639
|
+
|
|
640
|
+
- **`private: true` removed: `@cosyte/dicom` can publish.** The flag dated to the very first
|
|
641
|
+
scaffold commit and was never explained; `changeset publish` silently skips a private package, so
|
|
642
|
+
this repo was the one parser that could not reach npm even once its pipeline worked. It also
|
|
643
|
+
contradicted the `publishConfig: { access: "public" }` in the same file. Removed as part of the
|
|
644
|
+
coordinated public launch (`PUB-FLIP`).
|
|
645
|
+
|
|
646
|
+
- **The release can actually bump the version.** `package.json` had no `version` script, so the
|
|
647
|
+
shared pipeline's `pnpm run version` failed with `Command "version" not found` and the release
|
|
648
|
+
aborted before opening a "Version Packages" PR. Adds `scripts/sync-version.mjs` (the `hl7`
|
|
649
|
+
reference, retargeted at `src/version.ts`) and the `version` script that runs it after
|
|
650
|
+
`changeset version`, so the bump and the `VERSION` constant land in the same commit.
|
|
651
|
+
- **`VERSION` is no longer typed as a string literal.** It was declared `export const VERSION =
|
|
652
|
+
"0.0.0"`, giving it the literal type `"0.0.0"`, so the exported type would change on every
|
|
653
|
+
release, making each version bump a breaking type change. Now annotated `: string`, matching the
|
|
654
|
+
`hl7` reference. Type-only; the runtime value is unchanged. Done now because the package is
|
|
655
|
+
unpublished. After the first publish this would itself be a breaking change.
|
|
656
|
+
|
|
657
|
+
- **The Release workflow can actually start.** `.github/workflows/release.yml` calls the shared
|
|
658
|
+
`cosyte/.github` pipeline, which requests `contents`/`id-token`/`pull-requests: write`, but declared
|
|
659
|
+
no `permissions:` of its own, so it inherited the repo default of `contents: read`. A called
|
|
660
|
+
workflow may only downgrade the caller's `GITHUB_TOKEN`, never escalate it, so GitHub rejected the
|
|
661
|
+
workflow at startup (~1s, no jobs, no logs). Every Release run from June 2026 until now failed this
|
|
662
|
+
way, unnoticed, because a `startup_failure` produces no logs to read. The caller job now declares
|
|
663
|
+
the three scopes explicitly. CI-only: no runtime or API change.
|
|
664
|
+
|
|
665
|
+
### Security
|
|
666
|
+
|
|
667
|
+
- **Dev-dependency advisory remediation (no runtime impact: the published
|
|
668
|
+
artifact is unchanged).** Added scoped `pnpm.overrides` pinning two
|
|
669
|
+
transitive **dev/build-time** packages to their patched releases: `esbuild`
|
|
670
|
+
(`>=0.27.3 <0.28.1` → `0.28.1`; GHSA dev-server path-traversal, not
|
|
671
|
+
reachable here: the library builds via `tsup`/`vitest` and never runs
|
|
672
|
+
`esbuild serve`) and the `@changesets/parse` copy of `js-yaml`
|
|
673
|
+
(`>=4.0.0 <4.2.0` → `4.2.0`; GHSA-h67p-54hq-rp68 merge-key DoS). The
|
|
674
|
+
`js-yaml@3.14.2` pulled by `read-yaml-file@1.1.0` (via
|
|
675
|
+
`@manypkg/get-packages` → `@changesets/cli`) is **intentionally left**: it
|
|
676
|
+
calls `yaml.safeLoad`, removed/throwing in js-yaml 4, so it cannot be
|
|
677
|
+
force-upgraded without breaking the release tooling, and it only parses
|
|
678
|
+
trusted local repo YAML at release time. This is the shared canonical
|
|
679
|
+
override block, enforced suite-wide by the `@cosyte/config` drift check.
|
|
680
|
+
|
|
681
|
+
### Tests
|
|
682
|
+
|
|
683
|
+
- **Enhanced multi-frame coverage (DICOM-COV).** Closed the Per-Frame-else-Shared branch gaps left by
|
|
684
|
+
the Phase 4 functional-group resolver (`functional-groups.ts`: ~53% → 100% branch): both optional
|
|
685
|
+
macros (Pixel Value Transformation `(0028,9145)`, Frame VOI LUT `(0028,9132)`), Pixel Measures
|
|
686
|
+
`spacingBetweenSlices`, shared-only resolution (no Per-Frame Functional Groups Sequence), the
|
|
687
|
+
lenient inner-attribute-absence paths (a macro item present but its attributes omitted ⇒ typed-absent,
|
|
688
|
+
never coerced), and all three `MISSING_REQUIRED_FUNCTIONAL_GROUP` throws (Pixel Measures / Plane
|
|
689
|
+
Position / Plane Orientation). Synthetic fixtures only; no public-surface change. Per-directory
|
|
690
|
+
coverage now sits genuinely ≥ 90 on every gated directory (global branches 93.2%).
|
|
691
|
+
|
|
692
|
+
### Known limitations
|
|
693
|
+
|
|
694
|
+
- **File Meta round-trip is over the modeled surface, not byte-exact.** Only the typed `FileMeta`
|
|
695
|
+
fields round-trip; any other `(0002,xxxx)` element a source file carried (e.g. `(0002,0100)` Private
|
|
696
|
+
Information Creator UID) is dropped at _parse_ time (the Phase 2 `FileMeta` view does not model it)
|
|
697
|
+
and so cannot be re-emitted. The preamble is normalized to zeros and odd-length values are padded
|
|
698
|
+
even. The output stays spec-clean but is not a byte-identical copy of a non-conformant input.
|
|
699
|
+
**Superseded within this same release** by the lossless File Meta round-trip above, which landed
|
|
700
|
+
after the serializer and before `0.0.1` was cut: as published, `0.0.1` preserves and re-emits non-modeled
|
|
701
|
+
`(0002,xxxx)` elements. The entry is kept because it is what the serializer shipped with.
|