@cosyte/mllp 0.0.7 → 0.0.9
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 +182 -2
- 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
|
@@ -9,8 +9,12 @@ this file is maintained by hand (Changesets handles the version bump and publish
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
`@cosyte/mllp` is published on npm and stays on the `0.0.x`-until-first-alpha ladder, so the API can
|
|
13
|
+
change with no deprecation cycle. The sentence this replaces said the first pre-alpha release "will
|
|
14
|
+
ship" the surface below, which stopped being true the day the package first published and had been
|
|
15
|
+
false ever since. **No version literal is written here, on purpose**: what is published is a fact
|
|
16
|
+
about the registry (`npm view @cosyte/mllp version`) and about `package.json`, and a number copied
|
|
17
|
+
into prose here is a claim that goes stale on its own.
|
|
14
18
|
|
|
15
19
|
### Added
|
|
16
20
|
|
|
@@ -76,6 +80,76 @@ begins its public history at `0.0.x`, per the cosyte version ladder (`0.0.x` unt
|
|
|
76
80
|
|
|
77
81
|
### Security
|
|
78
82
|
|
|
83
|
+
- **The PHI scanner enumerates a staged rename, reads a blob that replaced a walk root, and
|
|
84
|
+
refuses a root that is not a directory (`PHI-SCAN-RENAME-BLIND-AT-PRECOMMIT`).** Three holes, two
|
|
85
|
+
of them on `--staged`, which is this repo's **pre-commit** gate. All three were reproduced on
|
|
86
|
+
`2252d33` before anything was touched, with a synthetic name-bearing payload kept outside both
|
|
87
|
+
walk roots.
|
|
88
|
+
|
|
89
|
+
**The rename hole.** `R` (rename) and `C` (copy) are the only `--raw` statuses carrying a second
|
|
90
|
+
path, and `--diff-filter=AMT` deletes such a record outright, so the route never saw one.
|
|
91
|
+
Measured: `git mv <link> test/<name>` staged as `:120000 120000 <sha> <sha> R100` and `--staged`
|
|
92
|
+
printed `OK, no hits` with a mode-`120000` entry sitting under a scan root; a rename that also
|
|
93
|
+
substituted a real name staged as a scored rename and passed the same way, over live `PID-5` / `PID-7` /
|
|
94
|
+
`PID-3` values in the destination blob. The remedy is **`--no-renames`**, and it needs no work on
|
|
95
|
+
the record shape at all: the destination arrives as an ordinary single-path `A` and the source as a
|
|
96
|
+
`D` the filter already drops. The enumeration is a strict **superset** of the previous one, and
|
|
97
|
+
that is pinned on the records rather than on an exit code: a stage with no rename in it hands the
|
|
98
|
+
scanner byte-identical raw output either way. The two-field
|
|
99
|
+
stride becomes **structural** rather than conditional, because with detection off git cannot emit
|
|
100
|
+
an `R` or a `C` at all. Verified under `diff.renames` set to `true`, `copies`, `false` and `1`,
|
|
101
|
+
and under `diff.renameLimit=1`, so the caller's own configuration cannot reopen it. **The earlier
|
|
102
|
+
disclosure that admitting these "needs the two-path record shape handled, which is a scope
|
|
103
|
+
decision" was wrong**, and it was wrong because it was carried in from a sibling rather than
|
|
104
|
+
measured here.
|
|
105
|
+
|
|
106
|
+
**The root-replacing blob.** The refusal path already matched a scan root's own name, because an
|
|
107
|
+
entry at exactly `test` REPLACES the root instead of sitting in it. The read filter did not: it
|
|
108
|
+
wanted the `test/` prefix. So a mode-`100644` blob staged at exactly `test` was in scope for one
|
|
109
|
+
and out of scope for the other, and `--staged` exited 0 over the same three fields. Both read
|
|
110
|
+
predicates now admit the root's own path. **Admitting it to the read set was only half of it**:
|
|
111
|
+
`looksLikeHl7` decides what scan a target earns and a path named `test` matches none of the
|
|
112
|
+
fixture-like extensions, so a draft read the blob and still reported clean over a `PID`. An entry
|
|
113
|
+
that replaces a root is judged with **that root's own limits**, so `test` earns the structured
|
|
114
|
+
HL7 scan while `src` keeps the conservative dashed-SSN + email pass, exactly as a file inside
|
|
115
|
+
either root would. Both directions are pinned.
|
|
116
|
+
|
|
117
|
+
**The non-directory root.** A walk root that resolves to a **file** (a regular file at `test`, or
|
|
118
|
+
a link to one) threw `ENOTDIR` straight out of `readdirSync`, uncaught. Node exits **1** on an
|
|
119
|
+
uncaught throw, which is the code this contract reserves for "hits found", so the gate published
|
|
120
|
+
a finding it had never made: a false positive that reads as actionable, which is a different and
|
|
121
|
+
worse failure than a crash. A **dangling** link at a root was the silent half of the same shape,
|
|
122
|
+
because `existsSync` follows: the walk returned and the sweep reported `OK` over the entire
|
|
123
|
+
corpus that root stands for, with the `observed === 0` backstop unable to fire while the other
|
|
124
|
+
root still had files. Both refuse now (exit 2), naming the root and its kind and **never the link
|
|
125
|
+
target**. Reading whatever sits there instead was refused as the remedy: what is missing in the
|
|
126
|
+
working tree is a **tree**, and one file read in its place is evidence about that file rather
|
|
127
|
+
than about the corpus it replaced. `staged` mode does read such a blob, because the index holds
|
|
128
|
+
no directories to lose. `walk()` also no longer lets any `readdir` failure leave the process:
|
|
129
|
+
everything but `ENOENT` is now a refusal that names the directory.
|
|
130
|
+
|
|
131
|
+
**Two more routes into the same false finding, both `PRE-EXISTING`, both closed here.** A
|
|
132
|
+
**missing** allow-list threw its refusal past every `catch` in `main`, and an **unreadable**
|
|
133
|
+
allow-list or override log threw a raw `EACCES`: both exited 1. Nothing ever passed the gate that
|
|
134
|
+
way, because non-zero still blocks the commit, but the code said the wrong thing. All three exit
|
|
135
|
+
2 now: the missing-allow-list route is answered where it arises, and the two unreadable ones reach
|
|
136
|
+
a process-level guard that turns anything still unaccounted for into exit 2. A bad root and an
|
|
137
|
+
unscannable entry under the other root also come out in one refusal now, rather than one per
|
|
138
|
+
run.
|
|
139
|
+
|
|
140
|
+
**What did not change, deliberately, and is pinned so it stays a decision.** A root that is a
|
|
141
|
+
link to a **directory** is still followed and is still link-neutral (the tree beyond it is
|
|
142
|
+
scanned as the root it replaced would have been, with that root's own limits). An **absent** root
|
|
143
|
+
is still legitimate and still exits 0, which is the control that separates it from a dangling
|
|
144
|
+
one. Explicit-path mode still reads through a link. And the `test/` walk still excludes `.ts`
|
|
145
|
+
sources, which is a separate open question about walk-root scope and is not touched here.
|
|
146
|
+
|
|
147
|
+
16 new cases; **12 of them run red against the base scanner** and the other four are deliberate
|
|
148
|
+
controls (an ordinary stage, an absent root, a linked-directory root, and the record-identity
|
|
149
|
+
comparison, which is a statement about git and holds on both trees). Negative-controlled across packages too: the identical stage reports
|
|
150
|
+
four HL7 field hits through this scanner and nothing through a sibling's, so the evidence is
|
|
151
|
+
about this package rather than about the harness.
|
|
152
|
+
|
|
79
153
|
- **The PHI scanner refuses a non-regular entry under a scan root, on both routes
|
|
80
154
|
(`PHI-SCAN-SYMLINK-BLIND-ON-BOTH-ROUTES`).** A symbolic link under `test/` or `src/` read CLEAN on
|
|
81
155
|
**both** enumerating routes, so a link pointing at a PHI-bearing file passed the gate twice over.
|
|
@@ -255,6 +329,112 @@ begins its public history at `0.0.x`, per the cosyte version ladder (`0.0.x` unt
|
|
|
255
329
|
|
|
256
330
|
### Fixed
|
|
257
331
|
|
|
332
|
+
- **The test suite stopped assuming an idle machine, and the fix was mostly to make it cheaper
|
|
333
|
+
rather than to move a number.** `vitest.config.ts` carried `testTimeout: 10_000` and
|
|
334
|
+
`hookTimeout: 10_000`; the shared `@cosyte/vitest-config` sets neither. Test-only change: no
|
|
335
|
+
runtime, public API or published artifact is affected. `PARSER-TESTTIMEOUT-ASSERTS-AN-IDLE-BOX`.
|
|
336
|
+
|
|
337
|
+
**What the numbers below were measured under, because the condition is the finding.** Every
|
|
338
|
+
figure comes from `pnpm test:coverage`, run on a shared box that had other repos' suites on it
|
|
339
|
+
throughout, with base and head **interleaved** (alternating within the same minutes, three rounds
|
|
340
|
+
each) so contention cancels instead of being attributed to the change. The load-sensitive figures
|
|
341
|
+
additionally come from **four concurrent coverage suites**, which is what a parallel session here
|
|
342
|
+
actually does. Single figures are medians; peaks are called peaks.
|
|
343
|
+
|
|
344
|
+
**The headline, and it is a pass/fail rather than a stopwatch reading: under four concurrent
|
|
345
|
+
coverage suites the base commit FAILED three runs out of three, on correct code, and head passed
|
|
346
|
+
three out of three.** Base red on `test/framing/byte-fidelity.test.ts` (the 1 MiB corpus, 31.1 s,
|
|
347
|
+
over its **own** 30 s budget), on `test/conformance/quirk-corpus.test.ts` (§3.7 large payload,
|
|
348
|
+
10.7 s and 14.2 s) and on `test/scripts/phi-scan.test.ts` (`--staged` negative control, 14.6 s).
|
|
349
|
+
All three were spending that time on **cost rather than subject**, which is why the remedy is the
|
|
350
|
+
trim and not a number.
|
|
351
|
+
|
|
352
|
+
**`hookTimeout: 10_000` was a verbatim no-op and is gone.** Vitest 4.1.4 resolves
|
|
353
|
+
`hookTimeout ??= 1e4` and `testTimeout ??= 5e3` for a non-browser run, read out of this repo's own
|
|
354
|
+
installed copy. The line restated the default exactly.
|
|
355
|
+
|
|
356
|
+
**`testTimeout: 10_000` stays, and the reason is not the one this change set out to give.** After
|
|
357
|
+
the trim, the slowest case in the suite that carries **no budget of its own** is a `fast-check`
|
|
358
|
+
property sweep at **2.6 s peak** under four concurrent coverage suites, and across three loaded
|
|
359
|
+
runs **not one** unbudgeted case passed 5 s. (Every `attw` gate case is budgeted for this count,
|
|
360
|
+
in the bare-trailing-number form described later in this entry, which is why the suite's
|
|
361
|
+
slowest cases are not in it.) So neither 10,000 nor Vitest's 5,000 is what stands
|
|
362
|
+
between this suite and a false red any more, and an earlier draft of this entry was wrong to say
|
|
363
|
+
the framework default would sit close to reddening correct code. The literal is left alone because
|
|
364
|
+
no measurement asks it to move: halving a documented ceiling with nothing near it is churn, and
|
|
365
|
+
`PARSER-TESTTIMEOUT-ASSERTS-AN-IDLE-BOX` is satisfied by removing what was near it. The literal was
|
|
366
|
+
never what asserted an idle box.
|
|
367
|
+
|
|
368
|
+
**Two sites gain a budget, and no pre-existing number changes value.** The larger is `test/tls/**`,
|
|
369
|
+
where every case generates an RSA key pair and completes a real TLS handshake, both CPU-bound and
|
|
370
|
+
proportional to machine load, while what each case asserts is an outcome (a classification, a
|
|
371
|
+
flag, a frame delivered) and never a duration. Over 96 TLS case-runs under four concurrent
|
|
372
|
+
coverage suites the WANT-authorized regression **peaked at 9.70 s**, against the 10 s ceiling it
|
|
373
|
+
used to run under. No TLS case was observed timing out, and the budget is not claimed to fix an
|
|
374
|
+
observed red: a 3 % margin on a load-proportional case is a coin flip, and the budget is what buys
|
|
375
|
+
it out of the shared ceiling. The smaller is the one case this slice adds, the `tsx`/`node` parity
|
|
376
|
+
check below: it is the only case in the suite still paying a `tsx` cold start, and it **peaked at
|
|
377
|
+
9.20 s** under the same four concurrent suites, which is the same argument at 92 % of the ceiling.
|
|
378
|
+
On a quiet box it runs in under 2 s, so the budget looks gratuitous there and is not.
|
|
379
|
+
|
|
380
|
+
**Trim first: the phi-scan suite was paying a `tsx` start per case.** It spawns the scanner dozens
|
|
381
|
+
of times through two helpers, and on this scanner `tsx` costs **466 ms** against **137 ms** for a
|
|
382
|
+
bare `node` (medians of seven, same clean fixture). The suite now spawns `node` directly and relies
|
|
383
|
+
on node's native type stripping; the file went from **29.7 s to 12.4 s** **while gaining a test**.
|
|
384
|
+
The gained test is the backstop: `pnpm phi-scan` still runs `tsx scripts/phi-scan.ts`, so one case
|
|
385
|
+
spawns both runners on a framed violator and on a clean file and requires they agree on exit code,
|
|
386
|
+
stdout and stderr byte for byte. Without it the cheap runner would be testing something the commit
|
|
387
|
+
gate does not run.
|
|
388
|
+
|
|
389
|
+
**Trim second, and this one was the surprise: `expect(a).toEqual(b)` on a megabyte of Buffer was
|
|
390
|
+
the whole cost of the framing corpus test.** `toEqual` walks two Buffers element by element in JS.
|
|
391
|
+
Timed around the assertion alone on a 1 MiB pair it took **8.46 s under coverage** (4.32 s without
|
|
392
|
+
it), against **0.5 ms** for `Buffer.equals` on the same pair and tens of milliseconds for the
|
|
393
|
+
encode and decode round trip the test exists to exercise. The two large-payload assertions (the
|
|
394
|
+
1 MiB byte-fidelity corpus and the quirk corpus's accumulator-growth case) now use a native
|
|
395
|
+
`Buffer.equals` through `test/helpers/bytes.ts`, which reports the first differing offset and both
|
|
396
|
+
bytes on failure, so the diagnostic is better than the truncated structural diff it replaced. Those
|
|
397
|
+
two files together went from **14.9 s to 1.2 s**. The helper's failure paths are pinned by their
|
|
398
|
+
own tests, since a byte comparison that cannot fail is worse than the assertion it replaced.
|
|
399
|
+
|
|
400
|
+
**Said plainly, because it is easy to oversell:** the whole-suite wall moved only from **33.3 s to
|
|
401
|
+
27.8 s** (median of three interleaved rounds). The suite's critical path is
|
|
402
|
+
`test/scripts/attw-gate.test.ts`, which runs a real `npm pack` per case, peaked at 43.9 s in one
|
|
403
|
+
case under four concurrent suites, was not touched here, and already carries a 120 s per-case
|
|
404
|
+
budget. The per-file cuts are large; the total is bounded by a file this change deliberately left
|
|
405
|
+
alone.
|
|
406
|
+
|
|
407
|
+
**One assumption carried in from a sibling repo was measured here and is false here.**
|
|
408
|
+
`pnpm test:coverage` is **not** materially slower than a bare `pnpm test` on this suite: four
|
|
409
|
+
interleaved rounds put the two within noise of each other, because the critical path is
|
|
410
|
+
`attw-gate`'s real `npm pack` subprocesses, which v8 coverage does not instrument. Coverage is
|
|
411
|
+
still the right thing to measure under, for the different reason that CI runs **both** `pnpm test`
|
|
412
|
+
and `pnpm test:coverage`, so the suite is paid for twice per matrix leg.
|
|
413
|
+
|
|
414
|
+
**The rule this leaves behind, stated once:** a case that needs more than the shared ceiling
|
|
415
|
+
states its own budget at its own site and says why. **No list of those sites is kept anywhere,
|
|
416
|
+
because three drafts of this entry kept one and all three named the wrong set.** Read them off the
|
|
417
|
+
tests, and note there are **two spellings**: the options object (`it(name, { timeout: N }, fn)`,
|
|
418
|
+
`describe(name, { timeout: N }, fn)`) and a **bare trailing number** (`it(name, fn, N)`, and the
|
|
419
|
+
same on `describe`), which is
|
|
420
|
+
the form the `attw` gate uses through a named constant. A search for `timeout:` alone finds
|
|
421
|
+
neither the second form nor the constant, and does turn up `spawnSync`'s unrelated child-process
|
|
422
|
+
kill timeout. The quirk corpus's large-payload case was briefly given a budget and then had it
|
|
423
|
+
taken away again, because after the trim it no longer needed one, and a budget that changes
|
|
424
|
+
nothing is the same defect as the `hookTimeout` line above.
|
|
425
|
+
|
|
426
|
+
**Disclosed, not fixed, and it outranks everything above: the tightest idle-box assumption in
|
|
427
|
+
`test/tls/**` is not a test budget at all.** Both TLS files poll with a local
|
|
428
|
+
`waitFor(cond, timeoutMs = 3000)` helper, and two cases pass 5,000 ms explicitly. Those are
|
|
429
|
+
wall-clock ceilings on a loaded box exactly the way `testTimeout` was, they are unchanged here and
|
|
430
|
+
unchanged from base, and no per-suite budget relaxes them. They did not red in any run measured
|
|
431
|
+
for this slice. Tightening them is its own slice; widening them is not obviously right either,
|
|
432
|
+
since a poll ceiling is also what stops a hung case from burning the whole 60 s budget.
|
|
433
|
+
|
|
434
|
+
**Disclosed, not fixed.** `engines.node` says `>=22.0.0`, while node's type stripping is on by
|
|
435
|
+
default only from 22.18, so the phi-scan suite now needs the newer 22 that CI's 22 and 24 matrix
|
|
436
|
+
already resolves to. Narrowing `engines` is a consumer-facing change and belongs in its own slice.
|
|
437
|
+
|
|
258
438
|
- **The `attw` publish gate no longer passes on a tarball that carries no types.** `pnpm attw` was
|
|
259
439
|
`attw --pack . --profile node16`, and `@arethetypeswrong/cli@0.18.4`'s `getExitCode.js` opens with
|
|
260
440
|
`if (!analysis.types) return 0`, so the problem list is never read and no `--profile`,
|
package/dist/index.cjs
CHANGED