@cosyte/cli 0.0.5 → 0.1.0
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 +416 -0
- package/README.md +111 -45
- package/dist/bin/cosyte-mcp.cjs +461 -106
- package/dist/bin/cosyte-mcp.cjs.map +1 -1
- package/dist/bin/cosyte-mcp.mjs +461 -106
- package/dist/bin/cosyte-mcp.mjs.map +1 -1
- package/dist/bin/cosyte.cjs +824 -140
- package/dist/bin/cosyte.cjs.map +1 -1
- package/dist/bin/cosyte.mjs +825 -141
- package/dist/bin/cosyte.mjs.map +1 -1
- package/dist/index.cjs +592 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +515 -281
- package/dist/index.d.ts +515 -281
- package/dist/index.mjs +574 -104
- package/dist/index.mjs.map +1 -1
- package/dist/io-C9bSG2zF.d.cts +381 -0
- package/dist/io-C9bSG2zF.d.ts +381 -0
- package/dist/mcp.cjs +435 -103
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.d.cts +70 -9
- package/dist/mcp.d.ts +70 -9
- package/dist/mcp.mjs +435 -103
- package/dist/mcp.mjs.map +1 -1
- package/package.json +61 -37
- package/dist/io-B2EobRzC.d.cts +0 -57
- package/dist/io-B2EobRzC.d.ts +0 -57
package/CHANGELOG.md
CHANGED
|
@@ -15,8 +15,424 @@ still do. Each entry was assigned to the release whose tag first contains it, re
|
|
|
15
15
|
|
|
16
16
|
## [Unreleased]
|
|
17
17
|
|
|
18
|
+
**What 0.1 means for you.** This is the first release whose commands, output and exit codes we treat
|
|
19
|
+
as settled: command names, flags, the JSON output shapes, the exit-code contract (`0`, `1`, `2`,
|
|
20
|
+
`65`, `66`, `69`, `70`, `74`) and the diagnostic codes are the surface we keep stable. It covers the
|
|
21
|
+
`cosyte` command over all eight cosyte formats (`parse`, `validate`, `inspect`, `fmt`, `convert`,
|
|
22
|
+
`map-codes`, `redact` / `deid`, `completion`), value-free diagnostics with `--unsafe-show-values` as
|
|
23
|
+
the one opt-in, the `cosyte-mcp` stdio server and the programmatic `core` API. While the package is
|
|
24
|
+
below 1.0, a breaking change bumps the minor version and is called out here; a fix that changes no
|
|
25
|
+
output ships as a patch. Not covered yet: DICOM `parse` and `fmt`, C-CDA `parse`, and MLLP `fmt` and
|
|
26
|
+
`validate` (`CLI_FORMAT_UNSUPPORTED`), `validate --profile` (`CLI_NOT_IMPLEMENTED`), `redact` for
|
|
27
|
+
`astm`, `dicom`, `mllp` and `ncpdp`, a remote MCP transport and MCP tools for `redact` and
|
|
28
|
+
`map-codes`. The FHIR commands and `convert` use `@cosyte/fhir` as the peer of the optional
|
|
29
|
+
`@cosyte/transform`, and report `CLI_PARSER_UNAVAILABLE` (exit `69`) when a package manager leaves
|
|
30
|
+
it out.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **A downstream consumer that closes the CLI's stdout now ends the run quietly, under a documented
|
|
35
|
+
code of its own: `74` (`IOERR`, the value `sysexits.h` assigns `EX_IOERR`).** A pipeline into
|
|
36
|
+
`head`, a pager you quit, or any early-exiting reader gets the stable value-free
|
|
37
|
+
`CLI_OUTPUT_WRITE_FAILED` diagnostic and that exit code: no stack trace, no platform error text,
|
|
38
|
+
and no byte of the input on stderr. The contract is additive; no existing code is renamed,
|
|
39
|
+
repurposed or removed, and no existing code's meaning changes.
|
|
40
|
+
- **The code was chosen on a measurement, not a citation.** A write to a closed stdout is a Node
|
|
41
|
+
`EPIPE` on the stream, which is fatal when nothing is listening: measured here, the unguarded
|
|
42
|
+
shape prints a ten-frame stack trace and exits `1`. It is a handled condition the consumer owns,
|
|
43
|
+
so reporting it as `70` told an operator their pipeline had hit a bug in the CLI. The one
|
|
44
|
+
condition that moves onto the new code is that write failure itself, which previously reported
|
|
45
|
+
`70` on the record-stream path.
|
|
46
|
+
- **Neither write path reports a success over output that reached nobody.** The whole-result write
|
|
47
|
+
in the `cosyte` bin bypassed the guarded output sink altogether, and the record stream's sink
|
|
48
|
+
only consulted a flag the platform sets asynchronously, so a short enough run could have every
|
|
49
|
+
line enqueued before the closed consumer was reported. Either way a parse piped into a reader
|
|
50
|
+
that stopped early could print a summary and exit `0` over output that was never delivered.
|
|
51
|
+
Every chunk bound for stdout now waits for the platform's acknowledgement before the run may
|
|
52
|
+
resolve, so an undelivered result resolves to `74` whatever the command had computed, and the
|
|
53
|
+
summary describing that computed outcome is withdrawn with it.
|
|
54
|
+
- **`cosyte-mcp` terminates quietly when its client goes away.** A closed stdout used to reach the
|
|
55
|
+
process as an unhandled stream error, printing a Node stack trace naming this machine's install
|
|
56
|
+
paths. It is now a value-free diagnostic and the same exit code, and the server ends rather than
|
|
57
|
+
serving a channel nobody is reading.
|
|
58
|
+
- **A closed stderr cannot turn either termination into an unhandled error.** Both bins hear the
|
|
59
|
+
diagnostic channel's own error and fall back to the exit code as the whole signal.
|
|
60
|
+
- `test/pipe-close.test.ts` spawns real processes and closes their real streams, which nothing in
|
|
61
|
+
this suite did before: the existing coverage drove an injected sink inside the test process and
|
|
62
|
+
could say nothing about the process adapters. Its record-stream cases run at one and two records
|
|
63
|
+
on purpose, because a long stream fails on a later write and so is caught however the earlier
|
|
64
|
+
ones were handled, and `scripts/smoke.mjs` asks the same question of the PACKAGED bin under
|
|
65
|
+
plain `node`, where a faster start widens the window further.
|
|
66
|
+
|
|
67
|
+
- **The release pipeline now installs the packed tarball from outside this repository, and runs both
|
|
68
|
+
bins from that installed copy, before anything can publish.** `0.0.1` and `0.0.2` are on npm
|
|
69
|
+
permanently uninstallable, and the check that catches that shape was a sentence in a human
|
|
70
|
+
checklist: a `--dry-run` packs a tarball but never resolves its dependencies from a registry. It is
|
|
71
|
+
now the `install-gate` job in the release workflow, which the publishing job `needs:`, so a red
|
|
72
|
+
gate stops the publish rather than reporting on one that already happened.
|
|
73
|
+
- **It refuses a local-path dependency specifier before it packs anything.** A `file:`, `link:` or
|
|
74
|
+
bare filesystem path in `dependencies`, `optionalDependencies` or `peerDependencies` is what made
|
|
75
|
+
those two versions uninstallable, and it is now a refusal naming the field and the specifier.
|
|
76
|
+
`devDependencies` is deliberately exempt and reported rather than skipped silently: a consumer
|
|
77
|
+
never installs them, which is why this package can carry one on purpose.
|
|
78
|
+
- **Both bins are executed, not stat-ed.** `cosyte` must exit `0` from the installed copy, and
|
|
79
|
+
`cosyte-mcp`, a stdio server that never exits on its own, must not exit non-zero inside a bounded
|
|
80
|
+
window. A bin whose file is missing from the tarball is reported as a packaging defect, in words
|
|
81
|
+
that do not read like an install failure.
|
|
82
|
+
- **It fails closed.** An install that exceeds its time bound, a registry that does not answer and
|
|
83
|
+
a throw inside the gate each refuse with their own reason rather than warning and continuing. The
|
|
84
|
+
accepted cost is that a transient fault delays a correct release until the run is repeated.
|
|
85
|
+
- No dependency was added, no manifest field moved, and nothing a consumer receives changed.
|
|
86
|
+
|
|
87
|
+
- **Every MCP tool now publishes an `outputSchema`, and every tool result conforms to it.** An agent
|
|
88
|
+
calling a cosyte tool used to receive `structuredContent: { exit, ok }` with no schema to check it
|
|
89
|
+
against, so deciding whether a result held data or a diagnostic meant pattern-matching a text blob.
|
|
90
|
+
Each of the four tools now advertises an output schema and a `title` on `tools/list`, and every
|
|
91
|
+
dispatch path (success, negative verdict, hard failure, usage error, internal error) returns
|
|
92
|
+
structured content conforming to the schema its own tool declared.
|
|
93
|
+
- **The result carries `ok`, `status`, `exit`, `code` and `data`.** `status` is `success`,
|
|
94
|
+
`verdict` or `failed`, and it is the property that separates a negative verdict about the message
|
|
95
|
+
(the tool ran, the payload is present, the call is not an error) from a call that produced
|
|
96
|
+
nothing: the distinction no text blob could make reliably. `code` is the stable diagnostic code on
|
|
97
|
+
a failed call. `data` is the tool's own payload: the parsed model and warnings (or a record stream
|
|
98
|
+
for a multi-record input), the validation verdict and findings, the value-free structural summary,
|
|
99
|
+
or the converted `Bundle`.
|
|
100
|
+
- **On a failed call the structured result is value-free by construction**, not by review: every
|
|
101
|
+
property is drawn from a fixed set (the outcome vocabulary, the exit-code contract, the diagnostic
|
|
102
|
+
code registry), so no part of the caller's input can appear in it. One consequence is deliberate:
|
|
103
|
+
an unknown tool name is no longer echoed back, because a tool name is caller-supplied text like
|
|
104
|
+
any other argument.
|
|
105
|
+
- Tool names, tool count, `tools/list` ordering, input schemas, the exit-code contract, and the rule
|
|
106
|
+
that a parsed-but-invalid `validate` is a successful call are all unchanged, and no dependency was
|
|
107
|
+
added. The suite validates each emitted result against its own tool's declared schema with a
|
|
108
|
+
dependency-free checker that refuses any schema keyword it does not implement.
|
|
109
|
+
|
|
110
|
+
- **`redact` / `deid` produces a real de-identified copy, delegated whole to `@cosyte/deid`.** The
|
|
111
|
+
command that shipped as an honest `CLI_NOT_IMPLEMENTED` is wired. For `ccda`, `fhir`, `hl7` and
|
|
112
|
+
`x12` stdout carries the de-identified document (serialized exactly as `cosyte fmt` serializes that
|
|
113
|
+
format) and stderr carries the library's own value-free manifest, one line per locus with its
|
|
114
|
+
category, transform, structural path, count, disposition and stable code, plus the library's own
|
|
115
|
+
published output label and version. The CLI adds no policy, no locus map, no transform and no
|
|
116
|
+
fallback scrub, and asserts no de-identification standard of its own.
|
|
117
|
+
- **Every other outcome emits nothing at all**, because a partial pass offered as a de-identified
|
|
118
|
+
copy is the hazard the command exists to prevent. `astm`, `mllp` and `ncpdp` have no adapter in
|
|
119
|
+
that library: `CLI_NOT_IMPLEMENTED` (`69`). `dicom` is covered there, but its de-identified form
|
|
120
|
+
is a Part 10 byte stream this text stdout cannot carry: `CLI_FORMAT_UNSUPPORTED` (`65`), the
|
|
121
|
+
CLI's own limit, deliberately not blamed on the library. Any locus the library reports it could
|
|
122
|
+
not handle is the new `CLI_DEID_INCOMPLETE` diagnostic and exit `1`, with the blocked paths and
|
|
123
|
+
their codes named on stderr.
|
|
124
|
+
- **`@cosyte/deid` is an `optionalDependency`.** An install without it degrades to a value-free
|
|
125
|
+
`CLI_PARSER_UNAVAILABLE` (`69`), decided **before the input is read**, so a copy that cannot
|
|
126
|
+
de-identify never touches the bytes it cannot strip. No other command loads it: the boundary is
|
|
127
|
+
gated statically and observed at runtime in `test/deid-isolation.test.ts`.
|
|
128
|
+
- **Identifier surrogates are keyed with a per-invocation ephemeral key.** The library's default
|
|
129
|
+
policy pseudonymizes MRN / account / beneficiary numbers, and a keyed transform with no key is a
|
|
130
|
+
fatal there, never an unkeyed fallback. The CLI holds no key material and adds no key surface, so
|
|
131
|
+
it keys each run with a fresh random value: surrogates are consistent within one output and
|
|
132
|
+
deliberately not stable across runs. Stated on stderr and in the docs rather than discovered.
|
|
133
|
+
- **`redact` does not honour `--unsafe-show-values`.** The opt-in excerpt exists for debugging a
|
|
134
|
+
rejected parse; on this command an excerpt of the un-stripped input is precisely the leak it
|
|
135
|
+
exists to prevent, so its diagnostics stay value-free under every flag. The PHI-leak sentinel
|
|
136
|
+
matrix now carries a redact row per mode (covered, refused, blocked, library absent, `--format`
|
|
137
|
+
given and omitted, file and stdin).
|
|
138
|
+
- No published exit value moved and no existing `CLI_*` code was renamed. `redact` is still
|
|
139
|
+
deliberately absent from the MCP tool surface.
|
|
140
|
+
|
|
141
|
+
- **The two-file agent-guidance contract is now gated (`pnpm check:agent-notes`).** `CLAUDE.md` was
|
|
142
|
+
split from `documentation/agent-notes.md` on 2026-08-04, which made every anchor between them
|
|
143
|
+
load-bearing, and nothing checked them. `scripts/check-agent-notes.ts` now verifies that the
|
|
144
|
+
narrative file is tracked, that no section is emptied down to its heading (a container whose body is
|
|
145
|
+
its subsections is exempt and counted), and that every pointer at it resolves. It runs from
|
|
146
|
+
`test/scripts/agent-notes.test.ts`, so it rides the required `ci / verify` contexts and
|
|
147
|
+
`prepublishOnly` rather than adding a fourth workflow.
|
|
148
|
+
- **The matcher was derived by counting this tree, not by porting a sibling's.** Two pointer
|
|
149
|
+
spellings are live across these repositories, and porting without re-counting has produced a false
|
|
150
|
+
green twice: a qualified-only matcher would have covered 3 of `ncpdp`'s 38 pointers, and would
|
|
151
|
+
have matched nothing at all in `terminology` (42 bare, zero qualified) while still exiting 0.
|
|
152
|
+
**Measured here: every pointer is the qualified `documentation/agent-notes.md#<anchor>` form and no
|
|
153
|
+
bare pointer exists**, so the gate matches that form only. No count is written into the gate's
|
|
154
|
+
prose as a promise; the OK line prints all of them on every run, because it measures rather than
|
|
155
|
+
remembers.
|
|
156
|
+
- **A bare-form census keeps that scope honest.** Matching one spelling is safe only while the other
|
|
157
|
+
stays absent, so in **every opened file** each backticked `#<anchor>` span is enumerated: a
|
|
158
|
+
digits-only one is a pull-request reference and is counted and reported, and **any other one
|
|
159
|
+
refuses the run at exit 2** with an instruction to re-derive the matcher. Refusal rather than a
|
|
160
|
+
finding, because the tree has not necessarily broken but the evidence the scope rested on has.
|
|
161
|
+
A first draft scoped the census to the pair and justified that with tree files it claimed a
|
|
162
|
+
widening would red; **running it tree-wide proved none of them can, and that the only files that
|
|
163
|
+
could were the gate's own source and test**, which wrote bare spans out literally. That made the
|
|
164
|
+
narrow scope a self-exemption for the very files where a broken pointer would hide, so the samples
|
|
165
|
+
are now assembled from parts and the census covers the corpus. It closes the hole a pair-scoped
|
|
166
|
+
census leaves: a bare pointer in a third file was covered by neither the matcher nor the census.
|
|
167
|
+
- **Three claims were withdrawn after review rather than shipped.** The opening promise said the
|
|
168
|
+
gate catches "a rename"; it does not, because both halves match on **basename** and the directory
|
|
169
|
+
is never compared, so moving the file while pointers keep their prefix exits 0 with every rendered
|
|
170
|
+
link broken. That is now a narrowed promise and a disclosed miss rather than a bigger guard. A
|
|
171
|
+
**heading inside an HTML comment** mints a phantom anchor, which the list had omitted; it is now
|
|
172
|
+
disclosed and pinned. And a written-down count of sibling repos lacking the file was already
|
|
173
|
+
wrong, so **the number was dropped in favour of the class**.
|
|
174
|
+
- **Two further claims were withdrawn on later review passes, each arriving inside the fix for the
|
|
175
|
+
one before it.** The first fix asserted the HTML-comment miss is disclosed in "every sibling
|
|
176
|
+
copy"; that phrasing came from a review rather than from the tree, and it is false. `ncpdp`,
|
|
177
|
+
`terminology` and `astm` disclose it; `mllp`, `ccda`, `transform` and `docs` do not, and `mllp`
|
|
178
|
+
is the copy this gate's disclosed-miss block is transcribed from, so the shared ancestor never
|
|
179
|
+
carried the entry. The second fix then said the four that lack it are all owed it. Also false:
|
|
180
|
+
`mllp`, `ccda` and `transform` derive anchors by **slugging headings** and are owed it, while
|
|
181
|
+
`docs` resolves explicit `<a id>` anchors and cannot have the miss at all. **A claim about
|
|
182
|
+
another repository is not checkable from inside this one**, so it is measured against that
|
|
183
|
+
repository's source or not made.
|
|
184
|
+
- **It refuses rather than reporting green over a corpus it never opened.** There is no declared
|
|
185
|
+
scan root to be wrong about: the corpus is `git ls-files`, reconciled as **sets of paths**, and
|
|
186
|
+
zero pointers, zero tracked files, an unmerged path, a symlink, a non-regular file or two files
|
|
187
|
+
carrying the contract basename each refuse at exit 2. This repository has already shipped the
|
|
188
|
+
opposite defect once, in `phi-scan`, which printed `OK, no hits` over a root it never walked.
|
|
189
|
+
- **Every claim was watched to fail on a clone of the real tree** before the gate was believed: a
|
|
190
|
+
misspelled real anchor, an emptied real section, the narrative file deleted, a bare pointer that
|
|
191
|
+
resolves, a neutered matcher, and a tree with every pointer rewritten out of range. The last two
|
|
192
|
+
are the `terminology` scenario, and both refuse instead of going green.
|
|
193
|
+
- **Exit codes and corpus handling were re-derived from this repository, not inherited.** `0`, `1`
|
|
194
|
+
for a finding and `2` for a refusal come from `scripts/phi-scan.ts`. An unmerged path **refuses**
|
|
195
|
+
here even though `phi-scan` leaves that status unenumerated, because its reasoning (`git commit`
|
|
196
|
+
refuses an unmerged index) covers a staged route this gate does not have.
|
|
197
|
+
- **The NUL skip is a disclosed miss, not a pass**, and is required rather than tidy: the tree
|
|
198
|
+
tracks vendored `@cosyte/*` tarballs and a synthetic DICOM fixture, none readable as markdown or
|
|
199
|
+
editable to clear a red. The tell is the skipped count on the OK line. A draft claimed this
|
|
200
|
+
partition differs from `check-no-emdash.sh`'s; **that was false and was corrected by reading that
|
|
201
|
+
gate's own OK line.** Both key on an actual NUL byte; the wider set is git's own binary
|
|
202
|
+
classification, which is why neither gate may be reduced to `grep -I`.
|
|
203
|
+
|
|
204
|
+
- **`parse` has a documented input-size limit: 67108864 bytes (64 MiB) per invocation, refused as a
|
|
205
|
+
data error.** Node has two hard allocation ceilings (`buffer.constants.MAX_LENGTH` and
|
|
206
|
+
`buffer.constants.MAX_STRING_LENGTH`), and a legitimately large input that crossed one of them threw
|
|
207
|
+
past every handler and was reported as `CLI_INTERNAL` / exit `70`, which says "this is a bug in the
|
|
208
|
+
tool" about an input that is merely big. The CLI now declares its own limit far below both, checks it
|
|
209
|
+
against the **running byte count as the input arrives**, and refuses with a value-free
|
|
210
|
+
`CLI_INPUT_TOO_LARGE` naming the limit and exit `65`. The number is rendered from one constant into
|
|
211
|
+
`cosyte --help` and the command reference, and a test reds if the two ever disagree.
|
|
212
|
+
- The refusal fires before anything allocates memory proportional to the oversized input, which is
|
|
213
|
+
what makes it a refusal rather than a slower way to reach the same crash. The regression suite
|
|
214
|
+
proves it with sources that would run past `MAX_STRING_LENGTH` if anything drained them.
|
|
215
|
+
- The MLLP frame reader's own smaller default ceiling is raised to the CLI's limit on this path, so
|
|
216
|
+
the documented number is the binding one rather than an undocumented number underneath it.
|
|
217
|
+
|
|
218
|
+
### Changed
|
|
219
|
+
|
|
220
|
+
- **The CLI now runs on the 0.1 releases of every `@cosyte` library it uses except `@cosyte/deid`.**
|
|
221
|
+
The two required dependencies, `@cosyte/hl7` and `@cosyte/terminology`, and the optional
|
|
222
|
+
`@cosyte/astm`, `@cosyte/ccda`, `@cosyte/dicom`, `@cosyte/mllp`, `@cosyte/ncpdp`,
|
|
223
|
+
`@cosyte/transform` and `@cosyte/x12` move from their `0.0.x` releases to `^0.1.0`, so an install
|
|
224
|
+
takes their `0.1.x` patch releases and stops before `0.2.0`. `@cosyte/deid` follows in the next
|
|
225
|
+
entry, because it changes what `redact` does. The CLI's commands, flags, exit codes and diagnostic
|
|
226
|
+
codes are unchanged.
|
|
227
|
+
- **`redact` now delegates to the 0.1 releases of `@cosyte/deid`, and refuses more HL7 messages.**
|
|
228
|
+
The optional `@cosyte/deid` moves from `^0.0.9` (which pinned exactly `0.0.9`) to `^0.1.0`. The
|
|
229
|
+
library's default policy is the one `redact` applies, unmodified, and under it:
|
|
230
|
+
- **An HL7 visit number or order number refuses the run.** `PV1-19`, and a placer or filler order
|
|
231
|
+
number (`ORC-2`/`ORC-3`, `OBR-2`/`OBR-3`), fall to the catch-all identifier category, which the
|
|
232
|
+
default policy blocks (`DEID_LOCUS_BLOCKED`). `redact` answers with its existing refusal:
|
|
233
|
+
`CLI_DEID_INCOMPLETE`, exit `1`, nothing on stdout, and each blocked locus named on stderr with
|
|
234
|
+
the policy still attributed to the library. An ADT message that carries a visit number, which
|
|
235
|
+
`redact` used to de-identify, is now refused. The CLI passes no policy of its own to turn the
|
|
236
|
+
block into a removal: the library's own profile API rejects that weakening.
|
|
237
|
+
- **Medical record, account and member numbers are removed** (`DEID_CATEGORY_REMOVED`) rather than
|
|
238
|
+
replaced by a keyed surrogate. The per-invocation ephemeral key and its stderr disclosure stay,
|
|
239
|
+
so any keyed transform the library applies is never unkeyed.
|
|
240
|
+
- **HL7 `MSH-7` and `EVN-2` timestamps are now generalized.** The `0.0.9` policy left them
|
|
241
|
+
unchanged; a guard in the suite reds if a delegate that does so is ever resolved again.
|
|
242
|
+
- No exit code, diagnostic code or flag changed. The README and the command reference, limitations
|
|
243
|
+
and troubleshooting pages say which inputs produce output and which are refused.
|
|
244
|
+
- **The `js-yaml` dependency override now covers the advisory's extended range.** The pin moves from
|
|
245
|
+
`4.2.0` on `>=4.0.0 <4.2.0` to `4.3.0` on `>=4.0.0 <4.3.0`, so a transitive resolution landing
|
|
246
|
+
inside the newly covered window is remediated instead of being admitted silently. The `esbuild`
|
|
247
|
+
override is untouched, and the lockfile records the new pin.
|
|
248
|
+
- **`pnpm-workspace.yaml` declares the install hardening this package relies on**: a
|
|
249
|
+
`minimumReleaseAge` of 1440 minutes, which is the cooling-off window that blunts a compromised
|
|
250
|
+
release, and a `trustPolicy` of `no-downgrade`.
|
|
251
|
+
- **The `packageManager` pin moves to a pnpm release that honours both keys.** The previous pin
|
|
252
|
+
predates them, so it would have ignored the settings file entirely: a file that decorates rather
|
|
253
|
+
than defends is worse than an absent one, because it reads as a control that is in place.
|
|
254
|
+
- **Nothing an install resolves changed beyond the override above.** The full pre-publish ladder
|
|
255
|
+
(clean, typecheck, lint, test, build and the types check) runs green on the new pin.
|
|
256
|
+
- **The always-read agent guide is inside its declared line budget, and nothing it said was lost.**
|
|
257
|
+
Every rule and every trap stays there as a one-line imperative carrying a resolving pointer, and
|
|
258
|
+
the sentences that explained each one moved into the narrative document verbatim. The two-file
|
|
259
|
+
contract gate checks on every run that each pointer still lands on a section with a body, so the
|
|
260
|
+
compression cannot quietly become a deletion.
|
|
261
|
+
- **The declared Node range now matches the release lines this package is tested on.**
|
|
262
|
+
`engines.node` narrows from `>=22.0.0` to `>=22.0.0 <26.0.0`. Node 26 is supported upstream and
|
|
263
|
+
was admitted by the old open-ended range while the test matrix exercises 22 and 24 only, so an
|
|
264
|
+
install on Node 26 was a combination nobody here had run and the manifest said nothing about it.
|
|
265
|
+
The claim narrows to the tested set rather than the tested set widening to the claim: every matrix
|
|
266
|
+
cell is a required status check whose name is fixed by a repository setting no commit can edit, so
|
|
267
|
+
adding a line is a change that has to start outside this repository.
|
|
268
|
+
- **The tested set is stated here now.** The CI caller passes the Node majors explicitly rather
|
|
269
|
+
than inheriting them from the shared pipeline's input default. The same two versions run and
|
|
270
|
+
every status-check name is byte-identical; what changed is that the set is readable and diffable
|
|
271
|
+
in this repository instead of resolved somewhere else.
|
|
272
|
+
- **A guard reds when the two drift apart again.** It derives the lines upstream still supports
|
|
273
|
+
from a release schedule committed beside it, evaluated at the run date rather than pinned to a
|
|
274
|
+
number typed into a test, and reports a supported line the range admits but the matrix skips, a
|
|
275
|
+
matrix cell the range refuses, a range admitting a line the schedule does not carry, and any
|
|
276
|
+
prose surface restating the range or the matrix differently. It refuses to report agreement over
|
|
277
|
+
an absent, empty or provenance-less schedule.
|
|
278
|
+
- **The installation prerequisite states the upper bound too**, so the published documentation and
|
|
279
|
+
the published manifest cannot disagree about which runtimes are supported.
|
|
280
|
+
- **The MCP text content block is now the serialized JSON of the structured result**, replacing the
|
|
281
|
+
command's raw stdout (on a success) or its stderr diagnostic (on a failure). A client that reads
|
|
282
|
+
only text now sees exactly the value a schema-aware client validates: there is one value, serialized
|
|
283
|
+
once, so the two channels cannot disagree. A client that read the text block expecting the bare
|
|
284
|
+
command output will find the same payload one level down, under `data`.
|
|
285
|
+
- **Multi-record `parse` output (`--ndjson` and MLLP) is now emitted record by record, as each record
|
|
286
|
+
is parsed**, instead of being accumulated and written once at the end. The first line reaches stdout
|
|
287
|
+
before the rest of the input has been read, so a bulk batch pipes into the next process instead of
|
|
288
|
+
waiting on the whole file. Per-record isolation and the exit-code contract are unchanged: a record
|
|
289
|
+
that fails to parse is still a value-free `{ record, error }` line, the stream still continues, and
|
|
290
|
+
any failed record still resolves the invocation to exit `65`.
|
|
291
|
+
- **A fatal condition part way through keeps the lines already written and still exits non-zero.**
|
|
292
|
+
A truncated MLLP stream is the visible case: the frames that completed have already been emitted
|
|
293
|
+
when the unterminated one is detected at end of stream, so `stdout` is no longer empty for that
|
|
294
|
+
input. It was never a success and still is not: the exit code carries the failure, and a partial
|
|
295
|
+
record stream is never reported as a complete one.
|
|
296
|
+
- **A downstream consumer that closes the pipe part way through is now a value-free
|
|
297
|
+
`CLI_OUTPUT_WRITE_FAILED`** rather than an unhandled write error. One write per record is a
|
|
298
|
+
failure surface a single write did not have.
|
|
299
|
+
|
|
300
|
+
- **`CLAUDE.md` narrative was relocated into `documentation/agent-notes.md` to make room for the gate's
|
|
301
|
+
rules.** The branch-protection, PHI-scanner-residual and em-dash blocks were compressed to their
|
|
302
|
+
imperatives; every trap keeps a one-line rule and a pointer, and the reasoning each one compresses
|
|
303
|
+
was already in the narrative file. No trap was deleted and no ceiling was raised.
|
|
304
|
+
|
|
18
305
|
### Fixed
|
|
19
306
|
|
|
307
|
+
- **The PHI scanner reported on a run that had enumerated a target and never read it.** A logged
|
|
308
|
+
`--allow-fixture` bypass withdraws a file after the scan has already named it on the command line,
|
|
309
|
+
and the run then reported on whatever was left as though the corpus were whole. Over a corpus whose
|
|
310
|
+
only violator was withdrawn, that reads as a clean verdict about a file nobody opened.
|
|
311
|
+
- **The run now refuses, with a status distinct from both the clean status and the hits status.**
|
|
312
|
+
The refusal names every enumerated path that went unread and points at the token-level allow-list
|
|
313
|
+
as the remedy, because declaring an identifier keeps the file inside the scan while withdrawing it
|
|
314
|
+
removes the only evidence the run could have had about it.
|
|
315
|
+
- **Hits are reported FIRST, always.** A refusal raised before the findings were written would
|
|
316
|
+
discard every hit the run did find, which turns a stricter gate into a quieter one. The report
|
|
317
|
+
runs first and the refusal changes only the exit status.
|
|
318
|
+
- **The question is answered as a set difference, never as a count.** A count counts the targets
|
|
319
|
+
that DID get read, so it reads healthy at the exact moment a withdrawal empties the corpus, and it
|
|
320
|
+
cannot say which paths were missed.
|
|
321
|
+
- **An honest run is untouched.** With nothing withdrawn, the read set is the enumerated set, so the
|
|
322
|
+
pre-commit hook, the whole-tree sweep and a plain scan of named paths keep the exit codes they
|
|
323
|
+
already had.
|
|
324
|
+
- **The exit-code contract labelled exit `2` as `EX_USAGE`, and `sysexits.h` assigns `EX_USAGE` the
|
|
325
|
+
value `64` (EXIT-CODE-SYSEXITS-LABEL).** The label sat in two places in `src/core/exit-codes.ts`,
|
|
326
|
+
the module's contract table and the `USAGE` member's JSDoc, so it compiled into `dist/*.d.ts` and
|
|
327
|
+
rendered on a consumer's hover. `README.md` and the concepts page grounded their whole table in
|
|
328
|
+
that header while listing `1` and `2`, which it does not define at all. A developer aligning their
|
|
329
|
+
own tooling to `sysexits.h` therefore learned a false fact about it from our documentation.
|
|
330
|
+
- **Exit `2` now carries no sysexits constant, and `1` and `2` are both stated as this CLI's own
|
|
331
|
+
values.** The four labels that were already correct (`EX_DATAERR` 65, `EX_NOINPUT` 66,
|
|
332
|
+
`EX_UNAVAILABLE` 69, `EX_SOFTWARE` 70) are untouched.
|
|
333
|
+
- **No published number moved, and no member of the exported `EXIT` map was added, removed or
|
|
334
|
+
renamed.** This is a documentation correction and nothing else: `2` is still `2`, every command
|
|
335
|
+
still exits with the status it exited with before, and the golden exit-code matrix is unchanged.
|
|
336
|
+
A consumer who hard-coded `EX_USAGE = 2` against this CLI keeps working.
|
|
337
|
+
- **`test/exit-code-docs.test.ts` reds if a sysexits constant is ever again attached to a number
|
|
338
|
+
that header does not assign it**, across every source file, the README and every published docs
|
|
339
|
+
page, naming the offending surface, line and code. It carries the header's own table, reports a
|
|
340
|
+
constant that table does not define rather than passing it through, and is proved against the
|
|
341
|
+
exact text that shipped before this correction.
|
|
342
|
+
- **`pnpm phi-scan`'s all-mode walk was rooted at `test/__fixtures__` and `src` only, so 89 of this
|
|
343
|
+
repository's 123 tracked files were scanned by NEITHER of its two routes
|
|
344
|
+
(PHI-SCAN-WALK-ROOT-SCOPE).** The walk now roots at **`src`, `test` and `scripts`**, which opens 72
|
|
345
|
+
tracked files instead of 34.
|
|
346
|
+
- **Measured back to back on the base commit, and re-derived for this repository rather than ported
|
|
347
|
+
from a sibling.** A dashed SSN and an off-domain address written into `test/planted.test.ts`, in
|
|
348
|
+
this repository's own inline-message shape (a whole HL7 message as one `.ts` string literal with
|
|
349
|
+
`\r` escapes between its segments), exited **0** with `OK, no hits` in all mode, while naming the
|
|
350
|
+
same file in paths mode reported both at **exit 1** over the same bytes. A file written under
|
|
351
|
+
`scripts/` behaved identically. Both routes now report both.
|
|
352
|
+
- **All 38 newly opened files were hand-read.** Every message literal is a placeholder and the only
|
|
353
|
+
SSN/email shapes anywhere are the scanner's own declared synthetic payload, so the gap was one of
|
|
354
|
+
**enumeration**, not a live exposure: the defect was that the gate could not see those files, so
|
|
355
|
+
nothing would have caught a real value if one appeared.
|
|
356
|
+
- **`test` REPLACES `test/__fixtures__` rather than joining it.** Roots must stay disjoint: each is
|
|
357
|
+
walked independently and the results concatenated, so a nested root would enumerate every file
|
|
358
|
+
beneath it twice and report each hit twice. The fixture directory is still watched, through the
|
|
359
|
+
observation rule's other condition, **wherever git tracks files under it** (here, seven); only the
|
|
360
|
+
root a refusal is filed under changed.
|
|
361
|
+
- **One cover was LOST, and it is stated rather than implied away:** where git tracks NOTHING under
|
|
362
|
+
`test/__fixtures__`, an empty one no longer refuses. As a declared root it refused by the
|
|
363
|
+
opened-nothing floor whatever git carried; as an ordinary directory it contributes no entry and the
|
|
364
|
+
reconciliation has no expected path to miss. A test pins it.
|
|
365
|
+
- **`scripts/` is a root because the scanner, its allow-list and its override log live there**, so
|
|
366
|
+
the one directory guaranteed to hold PHI-shaped text was the one nothing enumerated. All nine
|
|
367
|
+
files there were measured against the detector before the root was declared: no hits.
|
|
368
|
+
- **The cost of that root nobody would guess: the allow-list's own bytes.** The allow-list documents
|
|
369
|
+
`ID <value>` as a synthetic id "matching an SSN / MRN / member-id shape", and the dashed-SSN check
|
|
370
|
+
consults no allow-list, so an `ID` entry written in that dashed shape now reds the gate on the
|
|
371
|
+
allow-list itself. Latent today (the shipped file declares its only id in the `MRN-` form, and a
|
|
372
|
+
test pins that). **The remedy is to spell the id in a shape the check does not match**, never to
|
|
373
|
+
make that check consult the allow-list (which would delete a detection) and never to exempt the
|
|
374
|
+
file (which would leave the likeliest place for a real value unswept).
|
|
375
|
+
- **A narrowing that came with it:** `test/__fixtures__` is no longer a declared root, so a live or
|
|
376
|
+
dangling symbolic link at that path is now an enumerated entry and is refused outright, whatever
|
|
377
|
+
it points at. Only the three top-level roots can still be followed.
|
|
378
|
+
- **`test/scripts/phi-scan.test.ts` is exempt from the sweep, and is the only exempt path.** It
|
|
379
|
+
carries violator literals on purpose, as the positive half of the scanner's own tests. The
|
|
380
|
+
exemption is applied **after the read**, so the file still counts as observed and an unreadable
|
|
381
|
+
one still refuses; it is **scoped to the sweep**, so naming the file explicitly still reports
|
|
382
|
+
every hit; and it is **per path, never a pattern**.
|
|
383
|
+
- **What this does NOT buy, stated because a wider reading would be false:** the detector is still
|
|
384
|
+
the cross-cutting SSN + email floor, over 38 more files. Structured, field-level detection remains
|
|
385
|
+
unimplemented, and a test now pins that limit rather than leaving it as prose.
|
|
386
|
+
- **`--staged` is deliberately unchanged**, because widening it changes what a commit is blocked on.
|
|
387
|
+
The two routes therefore differ widely, and the scanner's own documentation says by how much.
|
|
388
|
+
- **Four claims about which directories are deliberately NOT scan roots are now checks rather than
|
|
389
|
+
prose**, after two of them were measured false: `docs-content/` and `.changeset/` each carry one
|
|
390
|
+
tracked non-markdown file, and the repository root is not clean under the detector (the `author`
|
|
391
|
+
field carries a real off-domain contact address). `documentation/` and `.github/` were correct.
|
|
392
|
+
|
|
393
|
+
- **`pnpm phi-scan` printed `OK, no hits` and exited 0 over a corpus it never opened
|
|
394
|
+
(PHI-SCAN-OBSERVED-NOTHING-IS-GLOBAL).** A declared scan root that the walk never observed is now
|
|
395
|
+
a refusal at **exit 2**, in the all-mode sweep CI runs. Each root's walk is reconciled against
|
|
396
|
+
`git ls-files`, and two independent conditions refuse: the root contributed **nothing**, or git
|
|
397
|
+
tracks an in-scope file under the root that the walk did not open.
|
|
398
|
+
- **Six states measured on this repository, every one of them previously exiting 0 with
|
|
399
|
+
`OK, no hits`:** the root missing; the root emptied; the root a **dangling** symbolic link; the
|
|
400
|
+
root a **live** symbolic link to a directory outside the repository; a single tracked fixture
|
|
401
|
+
removed from the working tree with the rest of the root still opened; and `src` moved away.
|
|
402
|
+
- **The dangling case is why a kind check cannot stand in for this rule.** `existsSync` follows
|
|
403
|
+
the link and answers false, so `walk()` returns before `readdirSync` and the existing
|
|
404
|
+
not-a-regular-file refusal never fires. Nothing about the entry is ever inspected, so no check
|
|
405
|
+
on its kind can reach it. Refusing on what was **observed** needs no opinion about the entry.
|
|
406
|
+
- **A denominator is deliberately not what this is.** A count counts the roots and the files that
|
|
407
|
+
did exist, so a healthy-looking total is exactly what a starved root produces. This scanner
|
|
408
|
+
prints no file count and one was not added.
|
|
409
|
+
- **Existence is not observation, which is why both conditions ship.** Refusing only a missing
|
|
410
|
+
root leaves the emptied one open; refusing only an empty result leaves the swapped one open,
|
|
411
|
+
because a root pointed at another directory opens plenty. Neither subsumes the other.
|
|
412
|
+
- **Exit 2 was derived from this scanner's own contract, not ported from a sibling.** `1` means
|
|
413
|
+
"hits found" here; `walk()` already raises an unreadable root as an invocation failure, and a
|
|
414
|
+
root replaced by a regular file already exits 2 through `readdirSync`. Sibling scanners disagree
|
|
415
|
+
on this code, and carrying one across would have been the defect.
|
|
416
|
+
- **`git ls-files` failing refuses rather than answering the empty set**, because an empty answer
|
|
417
|
+
is indistinguishable from "this root tracks nothing" and would switch the rule off in silence.
|
|
418
|
+
- **The rule is one-directional on purpose.** A tracked in-scope file the walk missed refuses; an
|
|
419
|
+
untracked working-tree file the walk found does not, because scanning more than git carries is
|
|
420
|
+
the safe direction.
|
|
421
|
+
- **Scope, stated rather than left to be inferred:** all-mode only. `--staged` is a diff and not a
|
|
422
|
+
corpus, and widening it would change what a **commit** is blocked on, which is a separate
|
|
423
|
+
decision. Naming paths explicitly is unchanged.
|
|
424
|
+
- **Narrowed, not closed, and still disclosed in the module header:** a scan root that is itself a
|
|
425
|
+
live link is still followed. It is now refused whenever git tracks an in-scope file under the
|
|
426
|
+
root that the link's target does not also carry **at the same relative path**, so a link to an
|
|
427
|
+
unrelated directory refuses; the reconciliation compares path **sets** rather than the bytes git
|
|
428
|
+
carries at those paths, so a target mirroring the tracked names still passes at exit 0, and a
|
|
429
|
+
root git tracks nothing under is the degenerate case of that rather than the whole of it. An
|
|
430
|
+
**ancestor** of a scan root remains out of the staged route's scope, and paths mode still follows
|
|
431
|
+
a link a caller names.
|
|
432
|
+
- **A present-but-unreadable `phi-scan-overrides.md` exited 1, the code reserved for "hits found".**
|
|
433
|
+
`loadOverrideLog` threw a raw filesystem error past every handler while its sibling reader,
|
|
434
|
+
`loadAllowList`, had already been wrapped. It now exits 2 with a diagnostic. A caller branching on
|
|
435
|
+
the exit code read a broken invocation as a PHI finding.
|
|
20
436
|
- **A red pre-publish gate showed a red X and merged anyway, because `ci / prepublish` was a
|
|
21
437
|
required check nowhere (CI-REQUIRED-CHECKS).** The shared pipeline this repo calls grew a
|
|
22
438
|
`prepublish` job on 2026-08-05 (`cosyte/.github#35`, `6142ac4`; its second layer defaulted on in
|