@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 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`,
package/dist/index.cjs CHANGED
@@ -11,7 +11,7 @@ var __export = (target, all) => {
11
11
  };
12
12
 
13
13
  // src/version.ts
14
- var VERSION = "0.0.7";
14
+ var VERSION = "0.0.8";
15
15
 
16
16
  // src/dictionary/index.ts
17
17
  var dictionary_exports = {};