@cosyte/dicom 0.0.4 → 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 +276 -182
- package/README.md +24 -14
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,85 @@ 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
|
+
|
|
9
88
|
- **The element registry is now generated from NEMA's PS3.6 DocBook, the normative publication**
|
|
10
89
|
(`DICOM-UPSTREAM-EDITION-LAG`). `vendor/nema/part06/` pins `part06.xml` (PS3.6 **2026c**) by
|
|
11
90
|
SHA-256, and `scripts/generate-dictionary.ts` applies it as a **per-field overlay** over the
|
|
@@ -49,69 +128,56 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
49
128
|
_content_, documented in `vendor/nema/README.md`. What is gated in CI is unchanged and offline:
|
|
50
129
|
the committed dictionary must be byte-for-byte what the pinned inputs produce.
|
|
51
130
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
`
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
five **Core Concepts** notes (the object model, the tolerance/warning model, the typed value
|
|
103
|
-
layer, the safety-critical views, and the source-profile system), a **Guides** cookbook (four
|
|
104
|
-
recipes: re-serialize, de-identify, read raw pixel data, triage warnings), and a
|
|
105
|
-
**Troubleshooting & known limitations** reference. Every documented capability is grounded in the
|
|
106
|
-
package's actually-shipped surface; the metadata-first boundary is stated explicitly, with the
|
|
107
|
-
permanent non-goals named in Troubleshooting (**no pixel decode, no DIMSE networking, no
|
|
108
|
-
DICOMweb**, no pixel-level de-identification).
|
|
109
|
-
- **Doc/code-agreement gate (`test/docs-content.test.ts`).** Every ` ```ts runnable ` snippet in
|
|
110
|
-
`docs-content/` is extracted, compiled, and executed against the **built** package via
|
|
111
|
-
`docSnippetSuite()` from `@cosyte/vitest-config/snippets`, with its inline `// =>` assertions
|
|
112
|
-
checked, so a documented example can never silently drift from the code. All examples use
|
|
113
|
-
synthetic, base64-encoded Part 10 objects (invented patient, fake UIDs); no real PHI, no `.dcm`
|
|
114
|
-
file on disk.
|
|
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.
|
|
115
181
|
|
|
116
182
|
### Fixed
|
|
117
183
|
|
|
@@ -274,56 +340,84 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
274
340
|
and a chained clean would leave the working tree empty and the build broken. The gate does its own
|
|
275
341
|
delete rather than trusting the script under test.
|
|
276
342
|
|
|
277
|
-
|
|
343
|
+
## [0.0.3] - 2026-07-27
|
|
278
344
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
`
|
|
289
|
-
`
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
`
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
+
|
|
351
|
+
- **Em-dash brand gate in CI (`EMDASH-CONFORMANCE`).** The founder directive of 2026-07-24
|
|
352
|
+
(`knowledgebase/06-brand/voice-and-tone.md`, "No em dashes. Ever.") bans `U+2014` outright across
|
|
353
|
+
every cosyte surface and names commit messages explicitly, and the meta-repo's
|
|
354
|
+
`documentation/conventions.md` has stated for weeks that the rule is CI-gated. It now actually is,
|
|
355
|
+
here: `scripts/check-no-emdash.sh` (`pnpm check:no-emdash`) plus a dedicated
|
|
356
|
+
`.github/workflows/no-emdash.yml` job that scans **both** halves the rule covers, the tracked files
|
|
357
|
+
**and** the PR title, body, and commit messages. The workflow carries the non-default `edited`
|
|
358
|
+
pull-request trigger, which is load-bearing: this repo squash-merges under
|
|
359
|
+
`squash_merge_commit_title: COMMIT_OR_PR_TITLE` and `squash_merge_commit_message: COMMIT_MESSAGES`
|
|
360
|
+
(read off the repo, not assumed), so the subject that lands on `main` is the PR title, or the lone
|
|
361
|
+
commit's subject when the branch has exactly one, and the body is the branch commit messages.
|
|
362
|
+
Without `edited` a title changed after the last push would never be re-checked. The PR body does
|
|
363
|
+
**not** land; the gate scans it anyway, as deliberate over-strictness on a surface that costs
|
|
364
|
+
nothing to cover. It is a separate workflow rather than a job in `ci.yml` because that trigger
|
|
365
|
+
would otherwise re-run the whole Node 22 + 24 matrix on every typo fix, and because the shared
|
|
366
|
+
`cosyte/.github` pipeline runs no arbitrary repo script.
|
|
367
|
+
**This one did change content, unlike the earlier ports.** An ecosystem survey had measured
|
|
368
|
+
markdown only (0 of 25 `.md` files) and read dicom as already clean. Measuring all 178 tracked
|
|
369
|
+
files found six live em dashes in four non-markdown files, all removed here (see Changed, below).
|
|
370
|
+
Measure every tracked file, because that is what the scan covers.
|
|
371
|
+
The script is the **text-only** variant, taken from `ncpdp` (PR #34, `39212bb`) rather than from
|
|
372
|
+
the older `knowledgebase` copy, so it carries `ncpdp`'s two fixes to the shared shape instead of
|
|
373
|
+
inheriting the holes: a tracked file named `-` was read by `grep` as standard input (which `xargs`
|
|
374
|
+
points at `/dev/null`) and so was never opened, and `-d skip` silently passed a tracked symlink to
|
|
375
|
+
a directory. Both are re-proved here rather than taken on faith: with the `./` path prefix removed
|
|
376
|
+
the gate prints OK and exits 0 over a live em dash in a file named `-`, and with `-d skip` restored
|
|
377
|
+
it goes green over an unread symlink.
|
|
378
|
+
It deliberately omits `grep -I`, and in this repo that is the most load-bearing flag choice in the
|
|
379
|
+
file. `src/dataset/vr/charset.ts` holds a functional NUL byte inside `/[\x00 ]+$/u`, the regex that
|
|
380
|
+
strips DICOM's own padding, so grep classifies it binary. It carries no em dash today and scans
|
|
381
|
+
green, which is the whole answer to the belief that this port was blocked: the red would come from
|
|
382
|
+
a match, never from the NUL. **If it ever gains one, the gate reds.** Measured with GNU grep 3.8:
|
|
383
|
+
grep exits 0, writes nothing to stdout, and writes `binary file matches` to stderr, so the hit
|
|
384
|
+
never reaches the hit list and the stderr capture fails the run instead. With `-I` added, the same
|
|
385
|
+
edit goes green, which is why `-I` stays out. The failure message names that case explicitly rather
|
|
386
|
+
than blaming an I/O error that did not happen.
|
|
387
|
+
A gate that prints OK when it did not read its input is worse than no gate, so nine routes by which
|
|
388
|
+
a dead or blind scan could still report green are each checked red, not assumed: a corrupt git
|
|
389
|
+
index, an unreadable tracked file, a tracked file named `-q`, a C-quoted non-ASCII path, a
|
|
390
|
+
mis-encoded text file, an empty tracked-file list, a tracked file named `-`, a tracked symlink to a
|
|
391
|
+
directory, and the NUL-bearing source file gaining an em dash.
|
|
392
|
+
Known limits are documented in the script header and inherited knowingly from the shared shape
|
|
393
|
+
rather than patched in this copy alone: encoded-form matching is literal (so `—` without the
|
|
394
|
+
semicolon, and lowercase `%e2%80%94`, pass), stderr capture binds to the scanning `grep` rather
|
|
395
|
+
than to the filters ahead of it, an em dash encoded in a non-UTF-8 charset is not matched (a live
|
|
396
|
+
possibility here, given `(0008,0005)` Specific Character Set fixtures), and the scan reads file
|
|
397
|
+
**contents** only, so a tracked path that itself carries an em dash passes. Tooling only: no
|
|
398
|
+
runtime, public-API, or parse-behavior change.
|
|
399
|
+
- **`docs-content/` now covers the full canonical Diátaxis spine** (`DOCS-CONTENT-P6`). Beyond the
|
|
400
|
+
existing Overview (`intro`), the sidebar gains **Installation** and **Quickstart** (tutorials),
|
|
401
|
+
five **Core Concepts** notes (the object model, the tolerance/warning model, the typed value
|
|
402
|
+
layer, the safety-critical views, and the source-profile system), a **Guides** cookbook (four
|
|
403
|
+
recipes: re-serialize, de-identify, read raw pixel data, triage warnings), and a
|
|
404
|
+
**Troubleshooting & known limitations** reference. Every documented capability is grounded in the
|
|
405
|
+
package's actually-shipped surface; the metadata-first boundary is stated explicitly, with the
|
|
406
|
+
permanent non-goals named in Troubleshooting (**no pixel decode, no DIMSE networking, no
|
|
407
|
+
DICOMweb**, no pixel-level de-identification).
|
|
408
|
+
- **Doc/code-agreement gate (`test/docs-content.test.ts`).** Every ` ```ts runnable ` snippet in
|
|
409
|
+
`docs-content/` is extracted, compiled, and executed against the **built** package via
|
|
410
|
+
`docSnippetSuite()` from `@cosyte/vitest-config/snippets`, with its inline `// =>` assertions
|
|
411
|
+
checked, so a documented example can never silently drift from the code. All examples use
|
|
412
|
+
synthetic, base64-encoded Part 10 objects (invented patient, fake UIDs); no real PHI, no `.dcm`
|
|
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.
|
|
419
|
+
|
|
420
|
+
### Changed
|
|
327
421
|
|
|
328
422
|
- **Removed the six em dashes the new gate found (`EMDASH-CONFORMANCE`).** Four tracked files, none
|
|
329
423
|
of them markdown: `.github/CODEOWNERS` (2), `.github/workflows/release.yml` (2),
|
|
@@ -379,7 +473,8 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
379
473
|
therefore "a no-op until that flag is removed". `package.json` carries no `private` field, the repo
|
|
380
474
|
is public, and the package ships on npm at `0.0.1`, so the comment described the release pipeline as
|
|
381
475
|
inert when it is live. Documentation only, no behavior change, but a misleading note on a workflow
|
|
382
|
-
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.)
|
|
383
478
|
|
|
384
479
|
- **Corrected stale publish-status language in `docs-content/` (`README-ORG-SWEEP`).**
|
|
385
480
|
`installation.md` claimed the package was "not yet published to npm" and that the install command
|
|
@@ -387,57 +482,13 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
387
482
|
published … not on npm; the first provenance publish is gated on the coordinated public launch"
|
|
388
483
|
non-goal. `@cosyte/dicom` is published on npm at `0.0.1` and public. Both now state the truth
|
|
389
484
|
(published, public, still pre-alpha on the `0.0.x`-until-first-alpha ladder), and the troubleshooting
|
|
390
|
-
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.)
|
|
391
487
|
|
|
392
|
-
|
|
393
|
-
scaffold commit and was never explained; `changeset publish` silently skips a private package, so
|
|
394
|
-
this repo was the one parser that could not reach npm even once its pipeline worked. It also
|
|
395
|
-
contradicted the `publishConfig: { access: "public" }` in the same file. Removed as part of the
|
|
396
|
-
coordinated public launch (`PUB-FLIP`).
|
|
397
|
-
|
|
398
|
-
- **The release can actually bump the version.** `package.json` had no `version` script, so the
|
|
399
|
-
shared pipeline's `pnpm run version` failed with `Command "version" not found` and the release
|
|
400
|
-
aborted before opening a "Version Packages" PR. Adds `scripts/sync-version.mjs` (the `hl7`
|
|
401
|
-
reference, retargeted at `src/version.ts`) and the `version` script that runs it after
|
|
402
|
-
`changeset version`, so the bump and the `VERSION` constant land in the same commit.
|
|
403
|
-
- **`VERSION` is no longer typed as a string literal.** It was declared `export const VERSION =
|
|
404
|
-
"0.0.0"`, giving it the literal type `"0.0.0"`, so the exported type would change on every
|
|
405
|
-
release, making each version bump a breaking type change. Now annotated `: string`, matching the
|
|
406
|
-
`hl7` reference. Type-only; the runtime value is unchanged. Done now because the package is
|
|
407
|
-
unpublished. After the first publish this would itself be a breaking change.
|
|
408
|
-
|
|
409
|
-
- **The Release workflow can actually start.** `.github/workflows/release.yml` calls the shared
|
|
410
|
-
`cosyte/.github` pipeline, which requests `contents`/`id-token`/`pull-requests: write`, but declared
|
|
411
|
-
no `permissions:` of its own, so it inherited the repo default of `contents: read`. A called
|
|
412
|
-
workflow may only downgrade the caller's `GITHUB_TOKEN`, never escalate it, so GitHub rejected the
|
|
413
|
-
workflow at startup (~1s, no jobs, no logs). Every Release run from June 2026 until now failed this
|
|
414
|
-
way, unnoticed, because a `startup_failure` produces no logs to read. The caller job now declares
|
|
415
|
-
the three scopes explicitly. CI-only: no runtime or API change.
|
|
416
|
-
|
|
417
|
-
### Security
|
|
418
|
-
|
|
419
|
-
- **Dev-dependency advisory remediation (no runtime impact: the published
|
|
420
|
-
artifact is unchanged).** Added scoped `pnpm.overrides` pinning two
|
|
421
|
-
transitive **dev/build-time** packages to their patched releases: `esbuild`
|
|
422
|
-
(`>=0.27.3 <0.28.1` → `0.28.1`; GHSA dev-server path-traversal, not
|
|
423
|
-
reachable here: the library builds via `tsup`/`vitest` and never runs
|
|
424
|
-
`esbuild serve`) and the `@changesets/parse` copy of `js-yaml`
|
|
425
|
-
(`>=4.0.0 <4.2.0` → `4.2.0`; GHSA-h67p-54hq-rp68 merge-key DoS). The
|
|
426
|
-
`js-yaml@3.14.2` pulled by `read-yaml-file@1.1.0` (via
|
|
427
|
-
`@manypkg/get-packages` → `@changesets/cli`) is **intentionally left**: it
|
|
428
|
-
calls `yaml.safeLoad`, removed/throwing in js-yaml 4, so it cannot be
|
|
429
|
-
force-upgraded without breaking the release tooling, and it only parses
|
|
430
|
-
trusted local repo YAML at release time. This is the shared canonical
|
|
431
|
-
override block, enforced suite-wide by the `@cosyte/config` drift check.
|
|
488
|
+
## [0.0.1] - 2026-07-17
|
|
432
489
|
|
|
433
490
|
### Added
|
|
434
491
|
|
|
435
|
-
- **Trademark notice (`TRADEMARKS.md`).** This package names third-party systems to describe what it
|
|
436
|
-
interoperates with; the notice records that cosyte is not affiliated with, endorsed by, or
|
|
437
|
-
sponsored by any of them, that every reference is descriptive, and that the built-in profiles are
|
|
438
|
-
authored from public sources only. Added to `files` so it ships inside the published tarball, not
|
|
439
|
-
just on GitHub. Documentation only: no runtime or API change.
|
|
440
|
-
|
|
441
492
|
- **Lossless File Meta round-trip.** The parser now retains non-modeled
|
|
442
493
|
`(0002,xxxx)` File Meta elements (e.g. `(0002,0017)`/`(0002,0018)`
|
|
443
494
|
Sending/Receiving AE Title, `(0002,0100)` Private Information Creator UID,
|
|
@@ -519,28 +570,6 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
519
570
|
built only from the code + the offending Transfer Syntax UID (structural facts), never a decoded
|
|
520
571
|
value, so it is always safe to log. New public exports: `serializeDicom`, `DicomSerializeError`,
|
|
521
572
|
`SERIALIZE_ERROR_CODES`, `SerializeErrorCode`.
|
|
522
|
-
|
|
523
|
-
### Known limitations
|
|
524
|
-
|
|
525
|
-
- **File Meta round-trip is over the modeled surface, not byte-exact.** Only the typed `FileMeta`
|
|
526
|
-
fields round-trip; any other `(0002,xxxx)` element a source file carried (e.g. `(0002,0100)` Private
|
|
527
|
-
Information Creator UID) is dropped at _parse_ time (the Phase 2 `FileMeta` view does not model it)
|
|
528
|
-
and so cannot be re-emitted. The preamble is normalized to zeros and odd-length values are padded
|
|
529
|
-
even. The output stays spec-clean but is not a byte-identical copy of a non-conformant input.
|
|
530
|
-
|
|
531
|
-
### Tests
|
|
532
|
-
|
|
533
|
-
- **Enhanced multi-frame coverage (DICOM-COV).** Closed the Per-Frame-else-Shared branch gaps left by
|
|
534
|
-
the Phase 4 functional-group resolver (`functional-groups.ts`: ~53% → 100% branch): both optional
|
|
535
|
-
macros (Pixel Value Transformation `(0028,9145)`, Frame VOI LUT `(0028,9132)`), Pixel Measures
|
|
536
|
-
`spacingBetweenSlices`, shared-only resolution (no Per-Frame Functional Groups Sequence), the
|
|
537
|
-
lenient inner-attribute-absence paths (a macro item present but its attributes omitted ⇒ typed-absent,
|
|
538
|
-
never coerced), and all three `MISSING_REQUIRED_FUNCTIONAL_GROUP` throws (Pixel Measures / Plane
|
|
539
|
-
Position / Plane Orientation). Synthetic fixtures only; no public-surface change. Per-directory
|
|
540
|
-
coverage now sits genuinely ≥ 90 on every gated directory (global branches 93.2%).
|
|
541
|
-
|
|
542
|
-
### Added
|
|
543
|
-
|
|
544
573
|
- **Safety-critical domain helpers (Phase 4).** New `Dataset` accessors `patient` / `study` /
|
|
545
574
|
`series` / `image` return typed, fail-safe views over the DICOM §4 safety-critical attributes
|
|
546
575
|
(memoized on first access). `patient` surfaces the `{id, issuerOfId, issuerQualifiers}` identity
|
|
@@ -605,3 +634,68 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
605
634
|
- CI/release workflows reduced to thin callers of the reusable `cosyte/.github` pipelines
|
|
606
635
|
(`ci.yml` runs the shared PHI scan; `release.yml` targets `@cosyte/dicom`). The repo-specific
|
|
607
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.
|