@cosyte/mllp 0.0.5 → 0.0.7

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
@@ -76,6 +76,76 @@ begins its public history at `0.0.x`, per the cosyte version ladder (`0.0.x` unt
76
76
 
77
77
  ### Security
78
78
 
79
+ - **The PHI scanner refuses a non-regular entry under a scan root, on both routes
80
+ (`PHI-SCAN-SYMLINK-BLIND-ON-BOTH-ROUTES`).** A symbolic link under `test/` or `src/` read CLEAN on
81
+ **both** enumerating routes, so a link pointing at a PHI-bearing file passed the gate twice over.
82
+ Reproduced on `d854e81` with a synthetic name-bearing payload kept outside both walk roots: a link
83
+ to it under `test/` gave `all` mode exit 0 "OK, no hits"; a link to its **directory** did the same
84
+ and took the whole subtree with it; `--staged`, which is this repo's pre-commit gate, exited 0
85
+ after `git add`; and naming the target explicitly exited 1 with every hit. The payload was always
86
+ detectable, the two routes never looked at it.
87
+
88
+ Two mechanisms, two fixes. `walk()` enumerates `Dirent.isFile()`, an **lstat** answer, so a link is
89
+ neither a file nor a directory and fell out of the loop; `isDirectory()` is an lstat answer too,
90
+ which is how a linked directory vanished wholesale. `--staged` read content with
91
+ `git show :<path>`, and git stores a symbolic link as its **target path** under mode `120000`, so
92
+ it scanned the path text.
93
+
94
+ **Neither route follows a link it finds INSIDE a scan root.** Following would read bytes the
95
+ enumeration does not control (outside the repo, a loop, a device, a FIFO that blocks the gate
96
+ forever), and git does not carry those bytes anyway, so a hit on them would be a claim about
97
+ something no commit contains. The enumeration is narrowed instead: a non-regular in-scope entry
98
+ **refuses the scan** (exit 2, the existing "could not complete" code), naming **every** offender.
99
+ `--staged` now reads `git diff --cached --raw -z` so the destination mode is visible. The scan
100
+ roots' own names are matched as well as the prefix, because an entry named exactly `test` or `src`
101
+ does not sit inside a root, it **replaces** one, and a prefix test alone let a staged mode-`120000`
102
+ blob named `test` through at exit 0.
103
+
104
+ **The root itself is the one thing still followed, and a flat "neither route follows a link" would
105
+ be false.** `walk()` opens `test/` and `src/` with `existsSync` + `readdirSync`, and both follow,
106
+ so replacing a walk root with a link to a directory outside the repo makes the walk read straight
107
+ through it. Pre-existing, and the precise reading is **not** "its PHI is reported": the tree beyond
108
+ the link is scanned exactly as the root it replaced would have been, **with that root's own
109
+ limits**. A fixture-like payload behind a linked `test/` is reported (measured, exit 1); the same
110
+ payload behind a linked `src/` gets only the conservative pass and can read clean, exactly as it
111
+ would through a real `src/`. It is link-**neutral**, which is why it is disclosed rather than
112
+ closed: refusing a linked root is a decision about repo layout, not this defect.
113
+
114
+ **`--diff-filter` admits `T` (typechange), and leaving it out would have made the mode check
115
+ unreachable whenever the file being replaced was already tracked.** Replacing a **tracked** regular
116
+ file with a link is neither an add nor a modify; git raises it as `T`, so `--diff-filter=AM` deleted
117
+ the record before any mode could be read and the hook passed a mode-`120000` blob green. Measured on
118
+ git 2.39.5 against a tracked `test/differential/fixtures/*.frame.bin`: with `AM` the raw output for
119
+ that stage is **empty**. Admitting `T` also scans the reverse typechange, a link replaced by a real
120
+ file bearing PHI. Both premises are asserted in the tests rather than trusted.
121
+
122
+ A refusal names the entry's own repo-relative path and an engine-owned kind token, **never the link
123
+ target**, which is working-tree text that can itself carry PHI. Pinned with a synthetic name-bearing
124
+ payload whose target **filename** also carries a name, so the never-echo assertion is not a claim
125
+ about an empty string. 10 of the 14 new cases are red on `d854e81`; the other four are boundary
126
+ controls that must stay green (a gitignored entry stays exempt, an entry outside both roots is not
127
+ refused, the committed corpus still passes, a staged regular file still scans).
128
+
129
+ **Scope, stated rather than left to be read charitably.** "In scope" is each route's own existing
130
+ **root**; the `.ts` / `.md` **name** exemptions deliberately do not carry over to a non-regular
131
+ entry, because they are judgements about a file whose bytes the route could have read. `R`/`C`
132
+ rename and copy are still not enumerated by `--staged` at all: **pre-existing** (`AM` excluded them
133
+ too), disclosed rather than fixed. Explicit-path mode still reads **through** a link and is
134
+ unchanged. The gitlink (mode `160000`) arm is **not** a hole this closes: `--staged`'s scope already
135
+ reached a staged submodule under both roots and `git show` on one fails with `bad object`, so the
136
+ base commit already refused it, and what changed is that the diagnostic now says what the entry is.
137
+ The refuse-a-sweep-that-observed-nothing rule and the vanished-transient tolerance are untouched:
138
+ a non-regular entry that goes away mid-sweep therefore still **refuses**, because that tolerance is
139
+ scoped to the read and these entries are refused at enumeration. That asymmetry is deliberate in
140
+ direction, fail-closed being the correct way for a PHI gate to be wrong, and it is not reachable
141
+ here: nothing in this repo creates a non-regular entry under a scan root, and every one the tests
142
+ create lives in a throwaway repo under `tmpdir()`.
143
+
144
+ **This corrects a claim made under this same heading by the previous scanner change**, which called
145
+ the symlink gap "bounded, because git cannot commit content through a symlink and `--staged` reads
146
+ the index". The first half is true and the conclusion does not follow: the gate's promise is that it
147
+ observed what is under its roots, and both routes reported clean over an entry neither had read.
148
+
79
149
  - **BREAKING: ACK-correlation diagnostics no longer carry the control ID (`PHI-WARNING-MESSAGE-LEAK`).**
80
150
  The opt-in `correlateByControlId` path built its diagnostics by interpolation, and both ends were
81
151
  consumer-controlled and unbounded. The unmatched-ACK path read the **peer's** MSA-2 straight off an
@@ -185,6 +255,147 @@ begins its public history at `0.0.x`, per the cosyte version ladder (`0.0.x` unt
185
255
 
186
256
  ### Fixed
187
257
 
258
+ - **The `attw` publish gate no longer passes on a tarball that carries no types.** `pnpm attw` was
259
+ `attw --pack . --profile node16`, and `@arethetypeswrong/cli@0.18.4`'s `getExitCode.js` opens with
260
+ `if (!analysis.types) return 0`, so the problem list is never read and no `--profile`,
261
+ `--ignore-rules` or config setting reaches that early return. For a package that ships types, "does
262
+ not contain types" means the declarations were not in the tarball, which is a broken publish
263
+ reported as a pass. A false red costs an hour; a false green merges.
264
+ `ATTW-FALSE-GREEN-PORT`, porting the remedy that shipped in `terminology#28` (`bf153cb`).
265
+
266
+ **Measured on this package at `0.0.6` with zero concurrency**, under the old invocation:
267
+ `rm -rf dist && pnpm attw` printed "This package does not contain types." and exited **0**.
268
+ Concurrency supplies only the condition, so the remedy is not a lock, a lease or a build queue: the
269
+ gate has to be able to say its own inputs were missing, whatever removed them.
270
+
271
+ **The exit-0 path needs the tarball to carry no declaration at all**, which is a boundary specific
272
+ to how this package is built and is stated here because it is easy to get wrong. `tsup` emits a
273
+ shared type chunk (`dist/index-<hash>.d.ts`) beside the three entry declarations, so deleting only
274
+ the six entry `.d.ts`/`.d.cts` files leaves that chunk in the tarball and `attw` reds honestly
275
+ (`❌ No types`, UntypedResolution, exit 1); the same removal **plus** `dist/index-*.d.*ts` is what
276
+ exits 0. The build window lands in the exit-0 state regardless, because `tsup` writes JS in one
277
+ pass and every declaration in a later one, so no moment exists where only some are present: polling
278
+ two real clean builds here for `dist/index.mjs` and then the first declaration of any kind gave
279
+ windows of **4.25s and 3.31s**, holding JS and zero declarations throughout. The absolute timings
280
+ move run to run, so no single figure is quoted as "the" window; the stable claim is that the gap is
281
+ seconds rather than milliseconds, which is wide enough for a concurrent build or `pnpm clean` to
282
+ land `attw` in it.
283
+
284
+ `pnpm attw` is now `node scripts/attw.mjs --profile node16`. The wrapper hardcodes `--pack .` and
285
+ forwards the rest, so **`--profile node16` is preserved**, not dropped. Two nets, which catch
286
+ different things: a **preflight** that every relative path `package.json` promises (`main`,
287
+ `module`, `types`, `typings`, and every string leaf of `exports`) exists and is non-empty, which
288
+ catches the build window and names the missing file; and a **post-check** that promotes `attw`'s
289
+ untyped sentence to a failure, which catches declarations present on disk but excluded from the
290
+ tarball by `files`/`.npmignore` (no instance of that is on record here). This package declares
291
+ three subpaths, so the preflight has **twelve** artifact paths to check. Because the post-check
292
+ reads a string, the options that would hide it are refused by name and wholesale rather than by
293
+ value: `--quiet`/`-q`, `--format`/`-f`, `--config-path`, and a `.attw.json` setting `quiet` or
294
+ `format`. The first three of those were re-measured here, on this package's own untyped pack and
295
+ under its own `--profile node16`, each handing back exit 0 with the sentence unreadable;
296
+ `--config-path` is refused by inference, not measurement.
297
+
298
+ **Disclosed gap:** the preflight cannot see that shared type chunk go missing, because
299
+ `package.json` names it nowhere. It is left to the post-check and to `attw` itself rather than
300
+ papered over with a glob. For the same reason the preflight's failure message does not promise
301
+ which way `attw` would have gone, since it cannot tell the two states apart.
302
+
303
+ `test/scripts/attw-gate.test.ts` pins both nets against the real binary, including **the upstream
304
+ exit 0 itself** (so an `attw` upgrade that rewords the sentence or fixes the exit code reds the
305
+ suite rather than letting the net go slack), that the preflight reaches a **subpath's `require`
306
+ branch** and not just the root entry, **the boundary above** (a tarball keeping a shared chunk reds
307
+ on `attw`'s own account, so the failure message must not claim the exit-0 counterfactual), that
308
+ **`--profile node16` survives the wrapper** (the fixture is red under the default profile and green
309
+ under `node16`), a **negative control** on a well-formed package, and that a **real `attw` failure
310
+ still fails** with `attw`'s own exit status. Build and
311
+ packaging only: no runtime, framing, transport, or public-API change.
312
+
313
+ - **The PHI scanner no longer refuses an entire sweep because a transient file went away while it
314
+ was reading.** `all` mode lists `test/` + `src/`, then reads each file; anything created and
315
+ removed inside that window made a read throw `ENOENT` and took the whole scan down with it
316
+ (exit 2, the invocation-error code). The refusal was never wrong, the **enumeration** was, so the
317
+ enumeration is what changed. `PHI-SCAN-ENUMERATE-THEN-READ-CLASS`, porting the remedy that
318
+ shipped in `ccda#80`.
319
+
320
+ **This repo is genuinely reachable, and that was measured rather than read off the code.** With a
321
+ `git` shim first on `PATH` (the scanner runs `git` between the walk and the first read, so the
322
+ shim is a deterministic hook into exactly that gap), a `test/phi-scan-cap-*/capture.txt` removed
323
+ in the window made `pnpm phi-scan` on this checkout exit 2. Those directories are not
324
+ hypothetical: `test/scripts/phi-scan.test.ts` `mkdtemp`s two of them inside `test/`, which IS a
325
+ walk root, and polling the tree during a suite run measured each existing for about **510 ms**
326
+ (about 1.03 s of exposure per `pnpm test`). The siblings are spared only because their walk roots
327
+ are not the repo root, which is where a build transient lands. **The exposure is real but not
328
+ self-triggering:** nothing in this repo runs an `all`-mode sweep beside the suite (CI runs
329
+ `pnpm phi-scan` as a step strictly before `pnpm test` in one sequential job, the pre-commit hook
330
+ runs `--staged` and reads blobs from the index, and inside the test file the sweep is ordered
331
+ before both capture tests), so what is live is a hand-run or agent-run sweep during a test run.
332
+ Racing 88 sweeps against 6 suite runs produced 0 refusals, which is consistent with the measured
333
+ duty cycle: walk 2-4 ms, `git check-ignore` 6-9 ms, reads 11-19 ms, inside a ~4 s process.
334
+
335
+ **Exactly one case is now tolerated:** a file the walk enumerated **itself**, that **git does not
336
+ track**, failing with **`ENOENT`**. It is reported on stderr as skipped, naming the path, and is
337
+ never silent. **Still refusing:** a tracked file that cannot be read, any non-`ENOENT` failure, a
338
+ tolerated file back on disk at sweep end, a `git` that cannot report the tracked set, and an
339
+ **empty** tracked set (a removed `.git/index` exits 0 with no output, which would make every file
340
+ untracked and quietly delete the tracked-file bound; a corrupt one exits 128 and was already
341
+ caught). `all` mode additionally refuses when it observed **no** files, so the tolerance can never
342
+ decay into a clean report of nothing.
343
+
344
+ **Which bounds are pinned, and which are only disclosed.** Pinned by eight tests in
345
+ `test/scripts/phi-scan.test.ts`, each against a throwaway tree under `tmpdir()` (seven of them a
346
+ git repo, one deliberately not) and **five** of them driven by the `git`-shim technique, with no
347
+ sleep and no real build: the tolerance itself; the tracked-file bound; the `ENOENT`-only bound
348
+ (`EISDIR` via a decoy replaced by a directory); git unable to answer; the empty tracked set;
349
+ `observed no files`; a violator in an untracked file that does not vanish; and a violator inside a
350
+ transient that is still there when the read arrives. Each bound was **mutation-tested**: widening
351
+ `tolerateVanish` to `true` fails three of them, dropping the `ENOENT` check fails one, dropping
352
+ the `size > 0` guard fails one, and dropping the `observed === 0` refusal fails one. **Not pinned,
353
+ and measured as not pinned:** the back-on-disk re-check. Stubbing it out leaves all 40 tests
354
+ green. Reaching it needs a timed re-create against a deliberately slowed sweep, and a
355
+ load-sensitive sleep guarding a load-dependent race is the failure mode this defect itself
356
+ teaches; losing that branch loses the re-check, never the tolerance's bounds. **The gate drove
357
+ that branch by hand** (shim anchored on the second git call, `git ls-files`, against a 20,000-file
358
+ read loop with a tuned re-create delay) and confirmed the code right at all three delays, so the
359
+ branch is measured-correct and unpinned rather than unknown.
360
+
361
+ **Two residuals carried, not closed.** The post-sweep re-check is keyed on the enumerated PATH,
362
+ not on content, so an untracked file RENAMED inside the window goes unscanned under a clean
363
+ report; it is bounded (committing the file means `git add`, after which it is tracked and
364
+ untolerable) and closing it needs a content-addressed sweep, a different design rather than a
365
+ wider bound. And `walk()`'s own `existsSync` -> `readdirSync` race, one phase earlier, is
366
+ untouched: a directory removed in that window throws a plain `SystemError` `main()` does not
367
+ convert, so Node exits **1**, the code the contract reserves for "hits found". It matters more
368
+ here than in the siblings because this repo's transient is a **directory**, removed wholesale, and
369
+ it is unpinnable by the same technique because nothing runs before the walk.
370
+
371
+ **The suite keeps writing inside the walk root, deliberately.** The two capture tests exist to
372
+ prove that a file whose repo-relative path starts with `test/` earns the structured scan, so the
373
+ path is the fixture and moving them would delete what they test.
374
+
375
+ **Three things the gate checked hardest and could not break, recorded so they are not re-derived.**
376
+ The tracked-set spelling comparison holds: instrumented against this checkout, **zero** files come
377
+ out `tolerateVanish: true`, so the tolerance is inert on a clean tree, and a purpose-built repo of
378
+ 15 adversarial tracked filenames (space, tab, newline, quote, backslash, `$`, `*`, `;`, non-ASCII,
379
+ CJK, emoji, plus an invalid-UTF-8 name) never marked a tracked file untracked, because `-z`
380
+ suppresses `core.quotePath` on both sides. The tolerance cannot reach `paths` or `staged` mode,
381
+ and `--allow-fixture` forces `mode = "paths"`, so an allow-list subtraction can never be used to
382
+ drive `all` mode to zero observed files. And in `all` mode `report()` is unreachable while
383
+ `observed === 0`, while in every mode a sweep that observed nothing has no hits to publish.
384
+ (`staged` legitimately reaches `report()` at `observed === 0` and prints `OK, no hits` when a
385
+ commit stages nothing in scope, which is why the refusal is scoped to `all`.)
386
+
387
+ **Two scope notes surfaced by the same review, one pre-existing and one introduced here.**
388
+ PRE-EXISTING: `walk()` tests `e.isFile()`, which is false for a symlink Dirent, so a symlinked
389
+ file under `test/` or `src/` is never swept in `all` mode. **The "bounded" reading recorded here
390
+ was wrong and is corrected under Security above (`PHI-SCAN-SYMLINK-BLIND-ON-BOTH-ROUTES`): git
391
+ not committing content through a link does not make a route that reports clean over an unread
392
+ entry safe, and `--staged` reading the index is exactly how the link's target path text got
393
+ scanned in place of any content.** INTRODUCED by this change: a
394
+ tolerated skip exits **0** with only a stderr line, including in CI, where the tree is at rest and
395
+ a skip is therefore anomalous by definition. On the base commit that same input exited 2. Both are
396
+ stated rather than changed: the first is not this defect, and the second **is** the shipped design
397
+ under review.
398
+
188
399
  - **Three doc comments that had outlived the code they described**, all of which shipped into the
189
400
  published type declarations. The `'reconnecting'` payload was documented as carrying
190
401
  `connectionId` only, while `MllpClient` populates `attempt` and `delayMs` when it schedules a
package/dist/index.cjs CHANGED
@@ -3438,7 +3438,7 @@ async function createStarterClient(opts) {
3438
3438
  }
3439
3439
 
3440
3440
  // src/index.ts
3441
- var VERSION = "0.0.5";
3441
+ var VERSION = "0.0.7";
3442
3442
 
3443
3443
  exports.Connection = Connection;
3444
3444
  exports.FrameReader = FrameReader;