@cosyte/astm 0.0.10 → 0.0.11

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
@@ -11,6 +11,88 @@ this file is maintained by hand (Changesets handles the version bump and publish
11
11
 
12
12
  ### Fixed
13
13
 
14
+ - **A symbolic link under a scan root read CLEAN on BOTH of the PHI scanner's enumerating routes, so
15
+ a link pointing at a file full of real identifiers passed the gate twice over**
16
+ (`PHI-SCAN-SYMLINK-BLIND-ON-BOTH-ROUTES`). Development tooling only: `scripts/phi-scan.ts` ships in
17
+ no tarball and the package's public surface is unchanged.
18
+
19
+ Reproduced on `8e8012d` before any fix, with a synthetic name-bearing payload (an ASTM `P` record
20
+ carrying a patient name, a mother's maiden name and a birthdate, plus a dashed SSN and an email at
21
+ a non-test domain) placed outside the walk roots and a link to it at `src/leak.ts`. `pnpm phi-scan`
22
+ printed `OK: no hits` and exited **0**; `pnpm phi-scan --staged`, after `git add`, printed
23
+ `OK: no hits` and exited **0**. Naming the link's target explicitly on the command line reported
24
+ **six** hits and exited 1, so the payload was always detectable, and detectable by this package's
25
+ own structured `P`-record detectors rather than only by the cross-cutting SSN/email floor. The two
26
+ routes simply never looked at it.
27
+
28
+ The two blindnesses are separate mechanisms and needed separate fixes. The walk enumerates
29
+ `Dirent.isFile()`, which is an **lstat** answer, so a link is neither a file nor a directory and
30
+ fell out of the loop with no record that anything had been skipped, and `isDirectory()` answers
31
+ false for a linked directory too, so a whole subtree could disappear the same way. The `--staged`
32
+ route reads content with `git show :<path>`, and **git stores a symbolic link as its target path
33
+ under mode `120000`**, so it was handed the target's path text and scanned that. That second route
34
+ is this repo's `pre-commit` hook, which is exactly where the claim that `--staged` covers a link
35
+ would have been trusted.
36
+
37
+ **Neither route is made to follow the link.** Following would read bytes the enumeration does not
38
+ control (outside the repo, a loop, a device, a FIFO that blocks the gate forever) and git does not
39
+ carry those bytes anyway, so a hit on them would be a claim about something no commit contains.
40
+ Instead the enumeration is narrowed: an entry under a scan root that is not a regular file
41
+ **refuses the scan** (exit 2, the existing "could not complete" code), naming every offender rather
42
+ than the first. The walk classifies by `Dirent` (symbolic link, FIFO, socket, block device,
43
+ character device); `--staged` now reads `git diff --cached --raw -z` instead of `--name-only` so
44
+ the destination mode is visible, and refuses mode `120000` and `160000` (a gitlink) wherever that
45
+ route's own path scope reaches them. A `--raw` record that does not parse refuses as well, rather
46
+ than being skipped into a silently shortened list.
47
+
48
+ **`T` (typechange) is in the `--diff-filter`, and leaving it out would have made the mode check
49
+ unreachable whenever the file being replaced was already tracked.** Replacing a **tracked** regular
50
+ file with a link is neither an add nor a modify: git raises `:100644 120000 <sha> <sha> T`, so
51
+ `--diff-filter=AM` deletes the record before any mode can be read. Measured on git 2.39.5 on this
52
+ tree: with `AM` the raw output for that stage is empty, and the unfixed scanner passed that stage
53
+ green. Typechange carries a single path, exactly like `A` and `M`, so admitting it costs the record
54
+ stride nothing, and it means the reverse typechange, a link replaced by a real file, is now scanned
55
+ as the ordinary file it became.
56
+
57
+ **A refusal names the entry's own repo-relative path and an engine-owned token for its kind, and
58
+ never the link target**, because a target path is text off the working tree and can itself carry
59
+ PHI. That is asserted rather than argued: the pinning payload and the target's own filename both
60
+ carry a synthetic person name, and every refusal message is checked to contain none of it. 10 of
61
+ the file's 30 cases are red on `8e8012d`.
62
+
63
+ **What this does not cover, stated narrowly, each measured.** Explicit-path mode already read
64
+ through a link and reported the target's hits; it is unchanged. The `--staged` path scope is
65
+ unchanged, still `test/fixtures/**` and `src/**.ts`, so a staged link outside it is still not
66
+ looked at; narrowing what a scope admits is not widening the scope. That scope also bounds the
67
+ gitlink half: a submodule staged at `test/fixtures/nested` is refused, while one at `src/nested` is
68
+ a directory name that fails the `.ts` suffix and is not looked at. `R` (rename) and `C` (copy) are
69
+ still **not** enumerated by `--staged` at all, so a staged rename that also appends PHI passes that
70
+ route; pre-existing, unchanged here, and admitting it needs the two-path record shape handled,
71
+ which is a scope decision rather than this one. A tracked file **absent from the worktree** is
72
+ still caught at `git add` time only.
73
+
74
+ **And this scanner still has no rule that a sweep observing ZERO targets must refuse**, which is
75
+ worth naming rather than leaving to be discovered. Measured: run from a tree with no `src/` and no
76
+ `test/fixtures/`, all-mode prints `OK: no hits` and exits 0, so a clean report can be a report
77
+ about a tree the scan never looked at, and **nothing backstops it**: the all-mode sweep is the
78
+ route with the hole, so it cannot be its own compensating control, and neither the `pre-commit`
79
+ hook nor CI asserts that the sweep observed anything. Five of the sibling scanners carry that rule
80
+ (`ccda`, `hl7`, `mllp`, `ncpdp`, `synth`) and the rest, this one included, do not; do not read that
81
+ as a majority practice. It is unchanged by this slice in either direction, and adding it is its own
82
+ slice, because the threshold is a judgement about this repo's corpus rather than a consequence of
83
+ anything here.
84
+
85
+ **The enumerate-then-read race is deliberately NOT closed here, and the deferral is measured
86
+ rather than assumed.** The scanner still has no tolerance for a file that vanishes between
87
+ enumeration and read, so a transient appearing under a walk root can refuse a whole sweep (exit 2,
88
+ fails closed). This repo's walk roots are `test/fixtures/` and `src/`, and nothing was observed
89
+ landing in either: polling both roots continuously across a real `pnpm build` saw only the tracked
90
+ corpus, while `tsup.config.bundled_*.mjs` (the transient that blocked a sibling's publish) appeared
91
+ at the **repo root**, which is not a walk root here. This repo's own suite writes its throwaway
92
+ trees under the OS temp directory, not under a scan root. So the shape is present and its trigger
93
+ is not, which makes closing it a separate slice with its own argument rather than a rider on a PHI
94
+ blindness fix. It is a different defect and it fails in the safe direction.
95
+
14
96
  - **The frame encoder no longer writes a different character than the one it was handed**
15
97
  (`ASTM-FRAME-BYTE-RESIDUALS`, closing known defect 7). `composeAstmFrames` turned a record given
16
98
  as a `string` into bytes with `charCodeAt(i) & 0xff`, so any character above `U+00FF` was replaced
@@ -76,6 +158,44 @@ this file is maintained by hand (Changesets handles the version bump and publish
76
158
  refuse the record string or reproduce it byte for byte" is asserted over 2,000 generated cases,
77
159
  with its bound written into the test and a non-vacuity check that both sides of it are reached.
78
160
 
161
+ - **The `attw` publish gate now fails when the packed tarball carries no types, where the CLI exits
162
+ `0`** (`ATTW-FALSE-GREEN-PORT`). The `attw` script was the bare `attw --pack .`.
163
+ `@arethetypeswrong/cli` opens `getExitCode()` with `if (!analysis.types) return 0`, returning
164
+ before it ever reads the problem list, because an untyped package is a legitimate npm package and
165
+ the CLI treats "no types at all" as a description rather than a problem. No `--profile`,
166
+ `--ignore-rules` or config setting reaches that branch. For a package that ships types the same
167
+ result means the declarations were **not in the tarball**, which is a broken publish reported as a
168
+ pass. A false red costs an hour; a false green merges.
169
+
170
+ Measured on this package at `0.0.9`, on a quiet box with **no** concurrency, both states printing
171
+ "This package does not contain types." and exiting `0`: `rm -rf dist`, and
172
+ `rm -f dist/index.d.ts dist/index.d.cts`. The second is the realistic one, and the trigger is the
173
+ build rather than any race: timed on one real `tsup` run here, `dist/index.mjs` and
174
+ `dist/index.cjs` appeared at 1,176 ms and `dist/index.d.ts` and `dist/index.d.cts` at 3,063 ms, a
175
+ **1,887 ms window** in which `dist/` held JS and no declarations. A concurrent build or `clean` in
176
+ the same working tree lands the gate in that window. This is deliberately **not** answered with a
177
+ lock or a build queue: the gate has to be able to report that its own inputs were missing,
178
+ whatever removed them.
179
+
180
+ `attw` is now `node scripts/attw.mjs`, which keeps two nets because they catch different things. A
181
+ **preflight** checks that every relative path `package.json` promises (`main`, `module`, `types`,
182
+ `typings`, and every string leaf of `exports`) exists and is non-empty, and names the missing file
183
+ rather than leaving it to be inferred. A **post-check** promotes `attw`'s untyped sentence to a
184
+ failure, catching what the preflight structurally cannot: declarations present on disk but
185
+ excluded from the tarball by `files` or an `.npmignore`. No instance of that second case is on
186
+ record in this repo. Because the post-check reads a printed string, the routes that would hide it
187
+ are refused rather than tolerated: `--quiet`, `--format`, a `.attw.json` setting either, and
188
+ `--config-path` at a file setting either were each measured here to restore the exact exit `0`. The
189
+ refusal is by option name, wholesale, not by value, and the list is a record of what is closed
190
+ rather than a proof that the post-check cannot be blinded at all: `--definitely-typed` suppresses
191
+ the sentence by making the analysis typed, is equally true of the bare CLI, and is deliberately not
192
+ refused. The preflight is the net that reads no string. Other arguments are forwarded, so
193
+ `--profile node16` still works. `test/scripts/attw-gate.test.ts` pins the upstream exit `0` itself,
194
+ so an `attw` upgrade that fixes it or rewords the sentence reds the suite instead of letting the
195
+ net go quietly slack, and it carries a negative control plus a real `attw` failure, because a gate
196
+ that only ever fails is not a gate and one that swallows the status is not one either. **No
197
+ runtime code changed and the built output is identical.**
198
+
79
199
  ### Added
80
200
 
81
201
  - **`ASTM_RECORD_FIELDS_UNSEPARATED`: a record that lost its fields no longer loses them in
package/dist/index.cjs CHANGED
@@ -2315,7 +2315,7 @@ function concatBytes3(chunks) {
2315
2315
  }
2316
2316
 
2317
2317
  // src/index.ts
2318
- var VERSION = "0.0.10";
2318
+ var VERSION = "0.0.11";
2319
2319
 
2320
2320
  exports.ALL_ASTM_WARNING_CODES = ALL_ASTM_WARNING_CODES;
2321
2321
  exports.AMBIGUOUS_CODES = AMBIGUOUS_CODES;