@cosyte/script-utils 0.0.1 → 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 CHANGED
@@ -2,12 +2,184 @@
2
2
 
3
3
  All notable changes to `@cosyte/script-utils` are documented here, following
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions are managed with Changesets;
5
- this file is hand-maintained (Changesets' own changelog generation is disabled). The package stays on
6
- the **`0.0.x`-until-first-alpha** ladder.
5
+ this file is hand-maintained (Changesets' own changelog generation is disabled). The package is on the
6
+ **`0.1.x`** line: its surface is settled, and bump types follow ordinary semver rather than a
7
+ pre-alpha rule. See
8
+ [ADR 0002](../../documentation/decisions/0002-the-0-1-0-version-line.md).
7
9
 
8
10
  > Because the generator is disabled, **`[Unreleased]` is promoted to a version heading BY HAND**, in
9
11
  > the pull request that adds the changeset. Nothing does it automatically.
10
12
 
13
+ ## [Unreleased]
14
+
15
+ ## [0.1.0] - Unreleased
16
+
17
+ ### Changed
18
+
19
+ - **The package leaves the pre-alpha version ladder for the `0.1.x` line.** `isCliEntrypoint`
20
+ exports exactly what `0.0.2` exports. The phi-scan engine does NOT: see the entries below, which
21
+ are what this release actually carries on that subpath. The version policy itself is unchanged in
22
+ substance and the reasoning is in
23
+ [ADR 0002](../../documentation/decisions/0002-the-0-1-0-version-line.md). A consumer pinned at
24
+ `^0.0.2` does not resolve this release and has to widen its range once, which is recorded as break
25
+ candidate BC-2 in `documentation/release-0.1.0-audit.md`.
26
+ - **`phi-scan`: `all` mode refuses unless EVERY scan root yielded a file that was read**, naming the
27
+ starved roots. The completeness rule asks whether every target was read; nothing asked whether a
28
+ root produced a target at all, and one productive root made the whole run look productive.
29
+ Measured before the change: a caller supplying an `isWalkReadable` that admits nothing read no
30
+ file on either sweeping route and printed `OK: no hits` at the CLEAN code over a tracked file
31
+ carrying a live dashed identifier.
32
+ - **`phi-scan`: an UNTRACKED file removed mid-run is reported SKIPPED rather than refusing.** A
33
+ target the walk listed and that was gone by read time refused unconditionally, which made an
34
+ ordinary mid-run deletion look like an unaccounted-for file. Git carries no bytes at such a path,
35
+ so nothing the repository holds went unread. The exception is bounded: a tracked file, a
36
+ non-`ENOENT` failure, a path named on argv and a path that came BACK before the run ended all
37
+ still refuse.
38
+ - **`phi-scan`: a finding names a position and stops there.** A hit report prints the locus, the
39
+ locator inside it and the rule that fired, and no longer the token that matched:
40
+ `segment=(ssn) (dashed SSN pattern)` where it used to carry the identifier as well. stderr is a CI
41
+ log, and a diagnostic ABOUT a PHI leak that quotes the leak is a second copy of it somewhere with
42
+ no retention policy. `Hit.value` is now optional, a hit the engine reports never carries one, and
43
+ **a gate test asserting that the matched identifier reaches stderr asserts on the locator and the
44
+ rule instead.**
45
+ - **`phi-scan`: `all` mode reads the bytes git carries at EVERY tracked path.** `scanRoots` bounds
46
+ the WALK, which answers what is on disk under the roots you declared; the index answers what your
47
+ repository CARRIES, and a root list was narrowing that too. Measured on the first consumer to
48
+ adopt this engine: three real messages under an undeclared top-level directory were read by
49
+ neither route, and a tracked symbolic link outside every root was reached by neither. **A
50
+ repository whose roots are narrower than itself has more of it read after this release.** The two
51
+ READ filters are unchanged and still apply on both sweeping routes, so a tracked `.md` and an
52
+ `excludedPaths` entry mean what they meant. `--staged` is untouched.
53
+ - **`phi-scan`: the `--staged` route refuses what `isStagedReadable` admits, and nothing else.** Its
54
+ non-regular and unmerged refusals used to fire for any path under a scan root, so a repository
55
+ whose staged scope is narrower than its roots had commits blocked that its own gate had always let
56
+ through: what a commit is blocked on is a hook decision each repository takes for itself. **This
57
+ refuses LESS at the pre-commit hook.** A staged entry your staged scope declines is still refused
58
+ by `all` mode on both of its routes, so your own sweep catches it; widen `isStagedReadable` if you
59
+ want the hook to refuse it too. Recorded with its compensating control as N1 in
60
+ [ADR 0003](../../documentation/decisions/0003-the-phi-scan-engine-gap-settlements.md).
61
+ - **`phi-scan`: a hit in the bytes git carries says WHICH kind it is.** The single
62
+ `(as git carries it)` label becomes `(git index)` for a path whose working-tree copy the run never
63
+ read and `(git index; the working tree differs)` for one whose disk copy carries other bytes, with
64
+ a footer counting how many reported hits are in those bytes. The second kind reads clean in the
65
+ file a developer opens, so re-staging is part of its remedy and one label hid that. **A gate test
66
+ asserting the old label asserts one of the two new ones.**
67
+ - **`phi-scan`: an index-route refusal no longer discards the hits already in hand.** An unmerged
68
+ index entry or a tracked link refused the whole sweep before anything was read, so a consumer saw
69
+ a refusal with no indication that PHI had already been found. The walk sweeps first, prints what
70
+ it found, and the refusal follows at the same code.
71
+ - **`phi-scan`: a clean run that skipped a file says so on stdout**, as
72
+ `OK: no hits (N untracked file(s) skipped, see stderr)`. With nothing skipped the line is
73
+ byte-identical to what it was.
74
+ - **`phi-scan`: a scan root has one settled outcome per KIND.** A regular file is scanned as one
75
+ target, a symbolic link is refused rather than followed, and a directory that cannot be listed is
76
+ refused at your `refuse` code naming the path: that one used to escape `readdirSync` uncaught and
77
+ take node's own exit 1, the code this contract reserves for HITS FOUND. The kind stays DERIVED
78
+ from the filesystem rather than declared, and what that gives up is recorded as N2 in ADR 0003.
79
+ - **`phi-scan`: two refusals reworded because their remedies differ.** "git could not read this
80
+ repository's git index" and "the git index holds no entries" were one sentence. The starved-root
81
+ refusal says how many of how many roots went unobserved and names them.
82
+ - **`phi-scan`: which tier refuses `--allow-fixture` follows the MODE.** A bypass beside a positional
83
+ path names something the run does not enumerate; a lone bypass declares a path an `all`-mode run
84
+ would have read and is refused by the completeness rule. Both name the path, and neither reaches a
85
+ clean run, which is unchanged.
86
+ - The `README.md` in the tarball now opens its `## Status` section on the settled-line sentence
87
+ instead of the pre-alpha ladder one, so the policy text a consumer reads agrees with the version
88
+ printed beside it. `scripts/readme-check.mjs` grades that sentence against the release line the
89
+ pending changesets resolve to.
90
+
91
+ ## [0.0.2] - 2026-08-11
92
+
93
+ ### Added
94
+
95
+ - **`@cosyte/script-utils/phi-scan`: the shared machinery of the `@cosyte/*` PHI commit-gate, as a
96
+ parameterised engine.** `scripts/parser-template/` is a SCAFFOLD rather than a dependency, so
97
+ `scripts/phi-scan.ts` was COPIED into every parser repo. **Thirteen repos, thirteen byte-distinct
98
+ copies**, so a newly-found escape cost one pull request and one adversarial review PER REPO, and
99
+ three escape classes had already been paid for that way. `runPhiScan(config)` owns argument
100
+ parsing, the allow-list and override log, target enumeration on all three routes, the union of the
101
+ working-tree walk with the bytes git carries, content deduplication under git's own
102
+ `blob <len>\0` framing, the completeness rule, every refusal, and the cross-cutting SSN/email
103
+ floor.
104
+
105
+ **THE FIVE PER-REPO AXES ARE PARAMETERS, NOT FORKS**, and which ones are required is itself the
106
+ design. `exitCodes`, `scanRoots` and `isStagedReadable` are **REQUIRED**: the siblings genuinely
107
+ disagree on all three, and a default would be the porting mistake the gate exists to catch (at
108
+ least one sibling uses 2 where another uses 1). `excludedPaths`, `isWalkReadable` (the Markdown
109
+ read exemption) and `regularBlobModes` are **DEFAULTED**, so moving one of those boundaries is a
110
+ change to this package plus a version bump rather than an edit in thirteen repos. EOL
111
+ normalization has **no parameter at all** and stays machinery: the walk/index deduplication is by
112
+ CONTENT, so where a `text` attribute or `core.autocrlf` makes the index carry LF and the working
113
+ tree CRLF, BOTH forms are scanned.
114
+
115
+ **Per-standard field detection stays with the caller**, through `detect`. It is handed the
116
+ reported LOCUS rather than the target path, so a hit found in the bytes git carries cannot be
117
+ reported against a working-tree copy a developer would open and find clean. That used to be a
118
+ sentence in a comment; it is now the only path a caller can reach.
119
+
120
+ - **A scan root may name a regular FILE, and the kind is DERIVED from the filesystem rather than
121
+ declared**, which is what keeps the parameter a plain `string[]`. Measured against the thirteen
122
+ live copies: they declare roots in at least six different shapes, and one declares
123
+ `{ rel, shape: "directory" | "file" }` with a single file among them. An earlier draft fed every
124
+ root to `readdirSync`, so such a root threw `ENOTDIR`, uncaught, and the run took node's exit 1,
125
+ the code this contract reserves for HITS FOUND. **What deriving gives up is stated rather than
126
+ left to be found**: a declaration can notice a root is not the KIND it was meant to be and
127
+ derivation cannot. A root that is neither a file nor a directory is still refused, a root naming a
128
+ symbolic link is refused rather than followed, and a MISSING root is skipped, which is unchanged
129
+ from the copied scanners and is named as the one remaining silently-empty root state.
130
+ - **A scan root of `"."` means the whole repository**, with gitignored directories pruned during
131
+ descent and `.git` skipped by literal name, which is what makes a whole-repository root usable at
132
+ all rather than a walk through `node_modules`. Pruning is equivalent to filtering afterwards
133
+ because **`git check-ignore` is index-aware at directory granularity**, measured both ways on git
134
+ 2.39.5: with nothing tracked underneath, a gitignored directory is reported ignored and pruned;
135
+ with one file force-added underneath it is reported NOT ignored, so the walk descends and still
136
+ reads it. An earlier draft justified this with the gitignore-pattern rule that a path under an
137
+ excluded directory cannot be re-included, which does not settle it, because the filter being
138
+ replaced asked `check-ignore` too.
139
+
140
+ ### Changed
141
+
142
+ - **Two containments the first draft ASSERTED are now ENFORCED**, both falsified by this slice's
143
+ adversarial review and both reproduced against the pre-consolidation scanner too, so neither is a
144
+ regression this package introduced. What was introduced was the sentence claiming they held, on the
145
+ API contract thirteen migrations are about to be written against.
146
+ - **A staged path `isStagedReadable` admits that no scan root covers is REFUSED.** The type said
147
+ the `--staged` filter was "narrower than the root half by construction"; nothing constructed it,
148
+ because they are two independent keys. Measured with roots at `["src"]` and the filter at the
149
+ shared Markdown exemption: a STAGED mode-120000 entry under `test/fixtures/` was outside every
150
+ scan root, so the non-regular refusal never saw it, and the route enumerated it, READ it, handed
151
+ the link's TARGET PATH to the detector as if it were content, counted the scan complete and
152
+ printed `OK: no hits` at exit 0. Narrowing silently to the intersection would have been the wrong
153
+ repair: it hides a misconfiguration in the one place the gate blocks a commit.
154
+ - **A scan root is normalised the way every other path is, and one resolving outside the repository
155
+ is refused.** `"./src"` is a spelling the type documents as valid; it walked correctly while
156
+ `isUnderScanRoot` compared it against the normalized index path and never matched, emptying the
157
+ union, the index non-blob refusal and the unmerged refusal in silence. Measured: `["src"]`
158
+ refused a tracked mode-120000 entry at exit 2 while `["./src"]` reported clean at exit 0 over the
159
+ same repository.
160
+ - **The optional axes are shape-checked.** `excludedPaths: ["a"]`, a plausible reading of
161
+ "repo-relative paths", used to survive normalization, reach `.has(...)` inside enumeration, and
162
+ take node's exit 1 from there, which the exit contract reserves for HITS FOUND.
163
+ - **The floor's dashed-SSN branch now consults `allow.ids`, in either rendering.** This is a
164
+ correction rather than a port: with the whole-file `--allow-fixture` bypass closed, a detector
165
+ that consults nothing leaves a developer with a hit and **no remedy at all**, and a sibling
166
+ shipped a footer claiming the token allow-list was the only remedy that reaches a clean run while
167
+ that was false for exactly this branch. One `ID` entry now covers both the dashed and the undashed
168
+ rendering, so a repo does not have to guess which one a fixture uses.
169
+ - **A fatal partway through the sweep prints the hits already found BEFORE the refusal**, instead of
170
+ discarding them. Measured on a mutant that throws after the reads: the old ordering produced a
171
+ refusal with **zero** `HIT:` lines over a corpus containing a real hit. The refusal still wins the
172
+ exit code and the clean line is still unreachable from there, so nothing is reported as accounted
173
+ for that is not.
174
+ - **The hit footer is scoped to what the engine can know.** It says the cross-cutting floor consults
175
+ the allow-list and that a repo's own detectors are answerable that way **only if** they consult
176
+ it. The engine cannot see inside a caller's `detect`, so a wider claim would be one it has no
177
+ evidence for.
178
+ - **A detector that throws REFUSES the scan** rather than escaping to node's own exit code, which
179
+ this contract reserves for HITS FOUND. A misconfigured scanner throws a `TypeError` rather than
180
+ returning a code: at the point a required axis is missing, `exitCodes` is itself the thing that
181
+ was not supplied, so there is no trustworthy code to return.
182
+
11
183
  ## [0.0.1] - 2026-08-05
12
184
 
13
185
  ### Added