@cosyte/dicom 0.0.7 → 0.0.8
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 +188 -0
- 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 +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,196 @@ All notable changes to `@cosyte/dicom` will be documented in this file. The form
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **The `attw` publish gate exited 0 on a tarball carrying no type declarations, so a broken publish
|
|
10
|
+
read as a pass** (`ATTW-FALSE-GREEN-PORT`). `pnpm attw` now runs `scripts/attw.mjs`, ported from
|
|
11
|
+
`@cosyte/terminology` (#28, `bf153cb`), where the defect was diagnosed; `typecheck:exports` runs
|
|
12
|
+
through the same wrapper with its `--profile node16` forwarded. The code ported, the measurements
|
|
13
|
+
were re-taken on this package rather than carried over.
|
|
14
|
+
|
|
15
|
+
**The cause is unconditional CLI semantics, not a lenient invocation and not concurrency.**
|
|
16
|
+
`getExitCode.js` in `@arethetypeswrong/cli@0.18.4` opens with `if (!analysis.types) return 0`,
|
|
17
|
+
returning before the problem list is read, because an untyped package is a legitimate npm package.
|
|
18
|
+
Reproduced here with **zero concurrency**: on a quiet box `rm -f dist/index.d.ts dist/index.d.cts
|
|
19
|
+
&& pnpm attw` and `rm -rf dist && pnpm attw` both print "This package does not contain types." and
|
|
20
|
+
exit **0**. Concurrency only supplies the condition. `tsup` writes the JS bundles before the
|
|
21
|
+
declaration files, so every build of this package has a window in which `dist/` holds
|
|
22
|
+
`index.mjs`/`index.cjs` and no `index.d.ts`: measured over three clean `pnpm build` runs on an idle
|
|
23
|
+
box, polling every 25ms, at **1.06s, 1.23s and 1.43s** (JS at +4.83s to +5.36s, declarations at
|
|
24
|
+
+6.06s to +6.49s). That is why the remedy is not a lock, a lease or a build queue - a lock would
|
|
25
|
+
leave the defect intact on an idle box, and the gate is supposed to be able to say its own inputs
|
|
26
|
+
were missing whatever removed them.
|
|
27
|
+
|
|
28
|
+
**Two nets, which catch different things.** A **preflight** that every relative path
|
|
29
|
+
`package.json` promises (`main`, `module`, `types`, `typings`, and every string leaf of `exports`,
|
|
30
|
+
which here is `./dist/index.cjs`, `./dist/index.mjs`, `./dist/index.d.ts` and
|
|
31
|
+
`./dist/index.d.cts`) exists and is non-empty; it catches the build window and names the missing
|
|
32
|
+
file rather than leaving a reader to infer it from a sentence about types. And a **post-check**
|
|
33
|
+
that promotes `attw`'s untyped sentence to a failure, which the preflight structurally cannot see:
|
|
34
|
+
the declarations can be on disk and still be absent from the tarball because `files` or
|
|
35
|
+
`.npmignore` left them out. No instance of that second case is on record in this repo. **Neither
|
|
36
|
+
net covers the rest of `files`** (`README.md`, `LICENSE`, `TRADEMARKS.md`, `CHANGELOG.md`), stated
|
|
37
|
+
plainly rather than left to be discovered: `attw` analyses types and never looks at them.
|
|
38
|
+
|
|
39
|
+
**The post-check reads a string, so the routes that would hide that string are refused**, by option
|
|
40
|
+
and not by value. **Eleven were measured here** on a fixture whose tarball genuinely carries no
|
|
41
|
+
types, each restoring the exact exit-0 with the sentence unreadable: `--quiet`, `-q`,
|
|
42
|
+
`--format json`, `-f json`, `--format=json`, `-fjson`, `-qf json`, `-Pfjson`, a `.attw.json`
|
|
43
|
+
setting `quiet` or `format` (`readConfig()` applies config after argv), and **`--config-path`**
|
|
44
|
+
pointing at a file that sets one - that last is the difference from terminology's copy, which
|
|
45
|
+
refused it by inference and said so.
|
|
46
|
+
|
|
47
|
+
**The three cluster forms are why the predicate is not an exact-token set, and a first draft of
|
|
48
|
+
this change got that wrong.** It matched each whole argument, before any `=`, against a set of
|
|
49
|
+
exact spellings; commander lets a short option's value attach to it and lets shorts combine, so
|
|
50
|
+
`-f` is not visible as a token in `-fjson`. Measured on that draft, on the untyped fixture:
|
|
51
|
+
`-fjson` gave **exit 0 with the gate silent**. A single-dash argument is now refused if any
|
|
52
|
+
character in its cluster is `q` or `f`, which is sound because `-f` is `attw`'s only value-taking
|
|
53
|
+
short option.
|
|
54
|
+
|
|
55
|
+
Measured the other way too, and both directions are stated because the bound matters: `--format
|
|
56
|
+
table-flipped` and `--format ascii` still print the sentence and blind nothing, and are refused
|
|
57
|
+
anyway (value-parsing them would be a third moving part in the guard); while `--form json`,
|
|
58
|
+
`--quiet=true` and `-f=json` each look like a route and are not, because commander rejects them
|
|
59
|
+
outright with exit 1. **The over-strictness is bounded and nothing else is refused**: a forwarded
|
|
60
|
+
`--profile node16` or `-P` still reaches `attw`. A forwarded extra positional does not retarget the
|
|
61
|
+
run either - `--pack .` supplies the first positional and `attw` ignores the second, so the
|
|
62
|
+
analysis stays on this package.
|
|
63
|
+
|
|
64
|
+
`test/scripts/attw-gate.test.ts` pins both nets against the real binary: the upstream exit-0
|
|
65
|
+
itself, so an `attw` upgrade that fixes it or rewords the sentence reds instead of letting the net
|
|
66
|
+
go slack; the `tsup` window in this package's own dual ESM/CJS `exports` shape, asserting both that
|
|
67
|
+
bare `attw` passes it with exit 0 and that the wrapper reds naming `./dist/index.d.ts`; a negative
|
|
68
|
+
control on a well-formed package; that a real `attw` failure still fails with `attw`'s own status;
|
|
69
|
+
and that other arguments are still forwarded. **Proved non-vacuous by putting the bare invocation
|
|
70
|
+
back into the script: 15 of the 22 tests red**, re-measured on the file as it stands. The 7 that
|
|
71
|
+
survive are exactly the ones that must pass on both: the upstream exit-0 pin, the well-formed
|
|
72
|
+
negative control, the real-failure parity case, argument forwarding, and the three controls that
|
|
73
|
+
only ever invoke the bare tool.
|
|
74
|
+
`scripts/verify.sh` in the meta-repo needed no change and was not touched, and no lock, lease,
|
|
75
|
+
semaphore or queue was added (ADR 0015).
|
|
76
|
+
|
|
77
|
+
### Changed
|
|
78
|
+
|
|
79
|
+
- `format` and `format:check` now cover `scripts/**/*.mjs` as well as `scripts/**/*.ts`, so the new
|
|
80
|
+
gate script is actually format-gated. Exactly one pre-existing file entered that glob,
|
|
81
|
+
`scripts/sync-version.mjs`, and it was verified Prettier-clean before the glob widened, so nothing
|
|
82
|
+
is red-flagged retroactively. (The tree's other `.mjs` file, `test/smoke/esm/index.mjs`, is under
|
|
83
|
+
`test/` and is not covered by this glob or by the `test/**/*.ts` one.)
|
|
84
|
+
|
|
85
|
+
**`lint` was deliberately NOT widened the same way, and the reason is measured rather than
|
|
86
|
+
assumed.** ESLint applies no rules at all to `.mjs` here: the shared `@cosyte/eslint-config` rule
|
|
87
|
+
blocks are scoped to `**/*.ts`, so `eslint --print-config scripts/attw.mjs` returns only the
|
|
88
|
+
Prettier-conflict disables, and a deliberately seeded unused variable plus a missing semicolon in
|
|
89
|
+
`scripts/attw.mjs` produced **zero** findings. Widening the `lint` glob would have added a
|
|
90
|
+
gate-shaped thing that gates nothing, which is the exact failure this change exists to remove.
|
|
91
|
+
Making ESLint genuinely cover `.mjs` means changing the shared config, and that is its own slice.
|
|
92
|
+
|
|
7
93
|
### Security
|
|
8
94
|
|
|
95
|
+
- **A symbolic link under `test/fixtures/` pointing at a PHI-bearing file scanned CLEAN on BOTH of
|
|
96
|
+
`scripts/phi-scan.ts`'s enumerating routes, so the commit gate passed it twice over**
|
|
97
|
+
(`PHI-SCAN-SYMLINK-BLIND-ON-BOTH-ROUTES`; pre-existing). Reproduced on `0f898be` with a synthetic
|
|
98
|
+
name-bearing payload outside the walk root and a link to it at `test/fixtures/leak.txt`: all-mode
|
|
99
|
+
exited **0** printing `OK - no hits`, `--staged` exited **0** after `git add`, and naming the
|
|
100
|
+
target explicitly exited **1** with three hits (the PN plus both dates). The payload was always
|
|
101
|
+
detectable; the two routes never looked at it.
|
|
102
|
+
|
|
103
|
+
**Two mechanisms, two fixes.** `walk()` enumerates `Dirent.isFile()`, which is an lstat answer, so
|
|
104
|
+
a link is neither a file nor a directory and fell out of the loop silently. `isDirectory()`
|
|
105
|
+
answers false for a **linked directory** too, so a whole subtree vanished the same way (measured:
|
|
106
|
+
a link to a directory holding the payload, exit 0). And `--staged` reads content with
|
|
107
|
+
`git show :<path>`, while git stores a link as its **target path** under mode `120000`, so that
|
|
108
|
+
route was handed the path text and never the target's bytes. `--staged` is this repo's
|
|
109
|
+
`pre-commit` hook.
|
|
110
|
+
|
|
111
|
+
**Neither route is made to follow the link.** Following would read bytes the enumeration does not
|
|
112
|
+
control (outside the repo, a loop, a device, a FIFO that blocks the gate forever), and git does not
|
|
113
|
+
carry those bytes anyway, so a hit on them would be a claim about something no commit contains.
|
|
114
|
+
The enumeration is narrowed instead: an **in-scope entry that is not a regular file refuses the
|
|
115
|
+
scan** (exit 2), naming every offender rather than the first. `--staged` now reads
|
|
116
|
+
`git diff --cached --raw -z` so the destination mode is visible; an unparseable `--raw` record
|
|
117
|
+
refuses too, because a silently shortened list is exactly what this scan must never report clean
|
|
118
|
+
over. **Be precise about which mode was blind:** `120000` was the leak. A staged **gitlink**
|
|
119
|
+
(`160000`) already exited 2 on the base scanner, because `git show :<path>` fails outright on one
|
|
120
|
+
(`fatal: bad object`, exit 128) and the read-error path caught it. Its refusal is now a named one
|
|
121
|
+
with a reason instead of a relayed git error, which is an improvement in the message, not the
|
|
122
|
+
closing of a hole.
|
|
123
|
+
|
|
124
|
+
**`--diff-filter=AMT`, not `AM`, and the one-letter difference is what makes the mode check
|
|
125
|
+
reachable.** Replacing a **tracked** fixture with a link is neither an add nor a modify: git raises
|
|
126
|
+
it as a typechange, `:100644 120000 <sha> <sha> T`. Measured on git 2.39.5, both
|
|
127
|
+
`git diff --cached --raw --diff-filter=AM` and `--name-only --diff-filter=AM` print **nothing** for
|
|
128
|
+
that stage, so the record died before any mode could be read and the hook would have passed a
|
|
129
|
+
mode-`120000` blob green while this entry claimed it refuses one. Typechange carries a single path,
|
|
130
|
+
exactly like `A` and `M`, so admitting it costs the two-field record stride nothing, and the
|
|
131
|
+
reverse typechange (a link replaced by a real file) is now scanned as the file it became.
|
|
132
|
+
|
|
133
|
+
**`--no-renames` as well, and the status filter alone was not enough - the gate's first pass
|
|
134
|
+
claimed otherwise and was refuted by measurement.** Rename detection is on by default, so
|
|
135
|
+
`git mv <link> test/fixtures/<name>` - an ordinary developer action, no crafted input - stages as
|
|
136
|
+
`:120000 120000 <sha> <sha> R100` with **two** paths, which `--diff-filter=AMT` then deletes
|
|
137
|
+
outright: the index held `120000 ... test/fixtures/toplink.txt` and this route printed
|
|
138
|
+
`OK - no hits` and exited **0**. The first pass disclosed that as out of scope on the grounds that
|
|
139
|
+
admitting a rename "needs the two-path record shape handled". It does not. With detection off the
|
|
140
|
+
destination arrives as an ordinary single-path `A` (`:000000 120000 0000000 <sha> A`) and the
|
|
141
|
+
source as a `D` the filter drops, so the two-field stride is untouched and the entry is refused.
|
|
142
|
+
It also makes that stride **structural rather than conditional**: with detection off, no `R` or `C`
|
|
143
|
+
record can be produced whatever the caller's `diff.renames` setting is. The cost is that the
|
|
144
|
+
destination of an ordinary clean rename into `test/fixtures/` is now scanned where it used to be
|
|
145
|
+
dropped, which is more coverage rather than less, and is pinned in both directions.
|
|
146
|
+
|
|
147
|
+
**The fixture root's own path is in scope too, not just what is under it.** A prefix test requiring
|
|
148
|
+
the trailing slash let a staged `test/fixtures` through (measured: mode `120000` at exactly that
|
|
149
|
+
path, exit 0) - the corpus root replaced by a link, so the whole corpus goes unscanned. Git records
|
|
150
|
+
no index entry for a directory, so that path can only mean a blob, a link or a gitlink, and it is
|
|
151
|
+
now refused. Only the "never a directory" half is load-bearing; the other three are all handled.
|
|
152
|
+
|
|
153
|
+
**A refusal names the entry's own repo-relative path and an engine-owned kind token, never the
|
|
154
|
+
link target**, which is working-tree text that can itself carry PHI. That is not hypothetical
|
|
155
|
+
here: on the base scanner, a link whose target filename carried a name and a date made `--staged`
|
|
156
|
+
exit **1** and print a hit whose value was **the date out of the filename** - a report about the
|
|
157
|
+
working tree's own text, not about anything the target contained. For the same reason the shape of
|
|
158
|
+
such a filename is written out in the scanner's docblock rather than exemplified: a diagnostic
|
|
159
|
+
about a PHI leak is itself a PHI surface, and that applies to the prose explaining it.
|
|
160
|
+
|
|
161
|
+
**Almost all of the scope is unchanged, and the two places it moved both admit MORE.** All-mode
|
|
162
|
+
still walks only `test/fixtures/` and still excludes a gitignored entry, by the same rule that
|
|
163
|
+
already excludes a gitignored fixture (`git check-ignore` does not answer for a tracked path, so
|
|
164
|
+
force-adding the link puts it back in scope, and that is pinned). `--staged` still covers only the
|
|
165
|
+
fixture path, and only the staged records git reports as added, modified or typechanged - a
|
|
166
|
+
deletion has no staged blob to scan and an unmerged path has no single one, both pre-existing and
|
|
167
|
+
both stated in the scanner's own banner rather than left to be inferred from the path prefix. The
|
|
168
|
+
two movements are the rename destination and the root path above; neither widens a route to a new
|
|
169
|
+
directory, and a link outside either boundary is still left alone, tested in both directions. The
|
|
170
|
+
`readme.md` exemption deliberately does **not** reach a link: that exemption is a judgement about a
|
|
171
|
+
file whose bytes the walk could have read, and a link's name is no evidence at all about what is on
|
|
172
|
+
the other side.
|
|
173
|
+
|
|
174
|
+
Pinned in `test/scripts/phi-scan.test.ts` with a synthetic name-bearing payload whose target
|
|
175
|
+
filename also carries a name, so the no-echo assertions cannot pass by fixture: **13 of the 31
|
|
176
|
+
cases are red on `0f898be`**, and the four cases covering the three corrections above are red on the
|
|
177
|
+
first pass of this change as well. The file also carries a negative control asserting the scanner
|
|
178
|
+
under test is this package's and not a sibling's.
|
|
179
|
+
|
|
180
|
+
**Not covered, deliberately, each measured rather than assumed.** Explicit-path mode already read
|
|
181
|
+
through a link and is unchanged. The **enumerate-then-read race** is untouched:
|
|
182
|
+
a file that vanishes between `walk()` and `readFileSync` aborts the whole sweep at exit 2, which
|
|
183
|
+
fails closed and is a different defect from this one. **There is still no rule refusing a scan that
|
|
184
|
+
observed nothing, and that has a measured instance worth naming rather than leaving abstract:** if
|
|
185
|
+
`test/fixtures` is itself a DANGLING link, `existsSync` follows it, answers false, and all-mode
|
|
186
|
+
prints `OK - no hits` and exits **0** over a corpus it never opened (identical on the base
|
|
187
|
+
scanner). If it is a link to a regular file, `readdirSync` raises an uncaught `ENOTDIR` - noisy and
|
|
188
|
+
fail-closed, but exit 1, which this contract reserves for "hits found". The `--staged` half of that
|
|
189
|
+
same shape is closed above; the all-mode half needs the observed-nothing rule, which is its own
|
|
190
|
+
slice. Also `--allow-fixture <path>` with no positional path still scans **nothing** and
|
|
191
|
+
prints `OK - no hits` even for a path that does not exist (measured), which makes this suite's
|
|
192
|
+
existing "honors `--allow-fixture` with an override-log entry" case vacuous. All pre-existing,
|
|
193
|
+
disclosed rather than fixed here.
|
|
194
|
+
|
|
195
|
+
No source, parser, de-identify or public API change.
|
|
196
|
+
|
|
9
197
|
- **An element whose on-wire VR is not one of the 34 PS3.5 §6.2 defines was kept verbatim by
|
|
10
198
|
`deidentify()`, carrying a source `(0010,0020)` Patient ID into de-identified output next to the
|
|
11
199
|
`(0012,0062) PatientIdentityRemoved = YES` this library writes** (`DICOM-CARRIER-LEAF-LEAKS`,
|