Repository navigation
Version Packages - #115
Merged
Merged
Version Packages#115
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or setup this action to publish automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
@cosyte/script-utils@0.1.1
Changelog
All notable changes to
@cosyte/script-utilsare documented here, followingKeep a Changelog. Versions are managed with Changesets;
this file is hand-maintained (Changesets' own changelog generation is disabled). The package is on the
0.1.xline: its surface is settled, and bump types follow ordinary semver rather than apre-alpha rule. See
ADR 0002.
[Unreleased]
[0.1.1] - Unreleased
Fixed
phi-scan: the per-root observation rule is answered from the WALK's reads only. It wasanswered from every read the run took, including the index-union reads of the bytes git carries,
so a scan root that was missing or empty on disk was credited whenever git tracked a file under
it. In a real repository every root holds tracked files, so the rule never fired for the state it
exists to catch. Measured before the change: two repositories with
srcemptied on disk, differingonly in whether
src/index.tswas tracked, exited at the refuse code and at the clean code withOK: no hits. Such a root now refuses at therefusecode, naming it. Those tracked files arestill read from the bytes git carries and a hit in them is printed before the refusal; the
completeness rule still counts every read, and a root the walk reads is not refused.
[0.1.0] - Unreleased
Changed
0.1.xline.isCliEntrypointexports exactly what
0.0.2exports. The phi-scan engine does NOT: see the entries below, whichare what this release actually carries on that subpath. The version policy itself is unchanged in
substance and the reasoning is in
ADR 0002. A consumer pinned at
^0.0.2does not resolve this release and has to widen its range once, which is recorded as breakcandidate BC-2 in
documentation/release-0.1.0-audit.md.phi-scan:allmode refuses unless EVERY scan root yielded a file that was read, naming thestarved roots. The completeness rule asks whether every target was read; nothing asked whether a
root produced a target at all, and one productive root made the whole run look productive.
Measured before the change: a caller supplying an
isWalkReadablethat admits nothing read nofile on either sweeping route and printed
OK: no hitsat the CLEAN code over a tracked filecarrying a live dashed identifier.
phi-scan: an UNTRACKED file removed mid-run is reported SKIPPED rather than refusing. Atarget the walk listed and that was gone by read time refused unconditionally, which made an
ordinary mid-run deletion look like an unaccounted-for file. Git carries no bytes at such a path,
so nothing the repository holds went unread. The exception is bounded: a tracked file, a
non-
ENOENTfailure, a path named on argv and a path that came BACK before the run ended allstill refuse.
phi-scan: a finding names a position and stops there. A hit report prints the locus, thelocator inside it and the rule that fired, and no longer the token that matched:
segment=(ssn) (dashed SSN pattern)where it used to carry the identifier as well. stderr is a CIlog, and a diagnostic ABOUT a PHI leak that quotes the leak is a second copy of it somewhere with
no retention policy.
Hit.valueis now optional, a hit the engine reports never carries one, anda gate test asserting that the matched identifier reaches stderr asserts on the locator and the
rule instead.
phi-scan:allmode reads the bytes git carries at EVERY tracked path.scanRootsboundsthe WALK, which answers what is on disk under the roots you declared; the index answers what your
repository CARRIES, and a root list was narrowing that too. Measured on the first consumer to
adopt this engine: three real messages under an undeclared top-level directory were read by
neither route, and a tracked symbolic link outside every root was reached by neither. A
repository whose roots are narrower than itself has more of it read after this release. The two
READ filters are unchanged and still apply on both sweeping routes, so a tracked
.mdand anexcludedPathsentry mean what they meant.--stagedis untouched.phi-scan: the--stagedroute refuses whatisStagedReadableadmits, and nothing else. Itsnon-regular and unmerged refusals used to fire for any path under a scan root, so a repository
whose staged scope is narrower than its roots had commits blocked that its own gate had always let
through: what a commit is blocked on is a hook decision each repository takes for itself. This
refuses LESS at the pre-commit hook. A staged entry your staged scope declines is still refused
by
allmode on both of its routes, so your own sweep catches it; widenisStagedReadableif youwant the hook to refuse it too. Recorded with its compensating control as N1 in
ADR 0003.
phi-scan: a hit in the bytes git carries says WHICH kind it is. The single(as git carries it)label becomes(git index)for a path whose working-tree copy the run neverread and
(git index; the working tree differs)for one whose disk copy carries other bytes, witha footer counting how many reported hits are in those bytes. The second kind reads clean in the
file a developer opens, so re-staging is part of its remedy and one label hid that. A gate test
asserting the old label asserts one of the two new ones.
phi-scan: an index-route refusal no longer discards the hits already in hand. An unmergedindex entry or a tracked link refused the whole sweep before anything was read, so a consumer saw
a refusal with no indication that PHI had already been found. The walk sweeps first, prints what
it found, and the refusal follows at the same code.
phi-scan: a clean run that skipped a file says so on stdout, asOK: no hits (N untracked file(s) skipped, see stderr). With nothing skipped the line isbyte-identical to what it was.
phi-scan: a scan root has one settled outcome per KIND. A regular file is scanned as onetarget, a symbolic link is refused rather than followed, and a directory that cannot be listed is
refused at your
refusecode naming the path: that one used to escapereaddirSyncuncaught andtake node's own exit 1, the code this contract reserves for HITS FOUND. The kind stays DERIVED
from the filesystem rather than declared, and what that gives up is recorded as N2 in ADR 0003.
phi-scan: two refusals reworded because their remedies differ. "git could not read thisrepository's git index" and "the git index holds no entries" were one sentence. The starved-root
refusal says how many of how many roots went unobserved and names them.
phi-scan: which tier refuses--allow-fixturefollows the MODE. A bypass beside a positionalpath names something the run does not enumerate; a lone bypass declares a path an
all-mode runwould have read and is refused by the completeness rule. Both name the path, and neither reaches a
clean run, which is unchanged.
README.mdin the tarball now opens its## Statussection on the settled-line sentenceinstead of the pre-alpha ladder one, so the policy text a consumer reads agrees with the version
printed beside it.
scripts/readme-check.mjsgrades that sentence against the release line thepending changesets resolve to.
[0.0.2] - 2026-08-11
Added
@cosyte/script-utils/phi-scan: the shared machinery of the@cosyte/*PHI commit-gate, as aparameterised engine.
scripts/parser-template/is a SCAFFOLD rather than a dependency, soscripts/phi-scan.tswas COPIED into every parser repo. Thirteen repos, thirteen byte-distinctcopies, so a newly-found escape cost one pull request and one adversarial review PER REPO, and
three escape classes had already been paid for that way.
runPhiScan(config)owns argumentparsing, the allow-list and override log, target enumeration on all three routes, the union of the
working-tree walk with the bytes git carries, content deduplication under git's own
blob <len>\0framing, the completeness rule, every refusal, and the cross-cutting SSN/emailfloor.
THE FIVE PER-REPO AXES ARE PARAMETERS, NOT FORKS, and which ones are required is itself the
design.
exitCodes,scanRootsandisStagedReadableare REQUIRED: the siblings genuinelydisagree on all three, and a default would be the porting mistake the gate exists to catch (at
least one sibling uses 2 where another uses 1).
excludedPaths,isWalkReadable(the Markdownread exemption) and
regularBlobModesare DEFAULTED, so moving one of those boundaries is achange to this package plus a version bump rather than an edit in thirteen repos. EOL
normalization has no parameter at all and stays machinery: the walk/index deduplication is by
CONTENT, so where a
textattribute orcore.autocrlfmakes the index carry LF and the workingtree CRLF, BOTH forms are scanned.
Per-standard field detection stays with the caller, through
detect. It is handed thereported LOCUS rather than the target path, so a hit found in the bytes git carries cannot be
reported against a working-tree copy a developer would open and find clean. That used to be a
sentence in a comment; it is now the only path a caller can reach.
A scan root may name a regular FILE, and the kind is DERIVED from the filesystem rather than
declared, which is what keeps the parameter a plain
string[]. Measured against the thirteenlive copies: they declare roots in at least six different shapes, and one declares
{ rel, shape: "directory" | "file" }with a single file among them. An earlier draft fed everyroot to
readdirSync, so such a root threwENOTDIR, uncaught, and the run took node's exit 1,the code this contract reserves for HITS FOUND. What deriving gives up is stated rather than
left to be found: a declaration can notice a root is not the KIND it was meant to be and
derivation cannot. A root that is neither a file nor a directory is still refused, a root naming a
symbolic link is refused rather than followed, and a MISSING root is skipped, which is unchanged
from the copied scanners and is named as the one remaining silently-empty root state.
A scan root of
"."means the whole repository, with gitignored directories pruned duringdescent and
.gitskipped by literal name, which is what makes a whole-repository root usable atall rather than a walk through
node_modules. Pruning is equivalent to filtering afterwardsbecause
git check-ignoreis index-aware at directory granularity, measured both ways on git2.39.5: with nothing tracked underneath, a gitignored directory is reported ignored and pruned;
with one file force-added underneath it is reported NOT ignored, so the walk descends and still
reads it. An earlier draft justified this with the gitignore-pattern rule that a path under an
excluded directory cannot be re-included, which does not settle it, because the filter being
replaced asked
check-ignoretoo.Changed
adversarial review and both reproduced against the pre-consolidation scanner too, so neither is a
regression this package introduced. What was introduced was the sentence claiming they held, on the
API contract thirteen migrations are about to be written against.
isStagedReadableadmits that no scan root covers is REFUSED. The type saidthe
--stagedfilter was "narrower than the root half by construction"; nothing constructed it,because they are two independent keys. Measured with roots at
["src"]and the filter at theshared Markdown exemption: a STAGED mode-120000 entry under
test/fixtures/was outside everyscan root, so the non-regular refusal never saw it, and the route enumerated it, READ it, handed
the link's TARGET PATH to the detector as if it were content, counted the scan complete and
printed
OK: no hitsat exit 0. Narrowing silently to the intersection would have been the wrongrepair: it hides a misconfiguration in the one place the gate blocks a commit.
is refused.
"./src"is a spelling the type documents as valid; it walked correctly whileisUnderScanRootcompared it against the normalized index path and never matched, emptying theunion, the index non-blob refusal and the unmerged refusal in silence. Measured:
["src"]refused a tracked mode-120000 entry at exit 2 while
["./src"]reported clean at exit 0 over thesame repository.
excludedPaths: ["a"], a plausible reading of"repo-relative paths", used to survive normalization, reach
.has(...)inside enumeration, andtake node's exit 1 from there, which the exit contract reserves for HITS FOUND.
allow.ids, in either rendering. This is acorrection rather than a port: with the whole-file
--allow-fixturebypass closed, a detectorthat consults nothing leaves a developer with a hit and no remedy at all, and a sibling
shipped a footer claiming the token allow-list was the only remedy that reaches a clean run while
that was false for exactly this branch. One
IDentry now covers both the dashed and the undashedrendering, so a repo does not have to guess which one a fixture uses.
discarding them. Measured on a mutant that throws after the reads: the old ordering produced a
refusal with zero
HIT:lines over a corpus containing a real hit. The refusal still wins theexit code and the clean line is still unreachable from there, so nothing is reported as accounted
for that is not.
the allow-list and that a repo's own detectors are answerable that way only if they consult
it. The engine cannot see inside a caller's
detect, so a wider claim would be one it has noevidence for.
this contract reserves for HITS FOUND. A misconfigured scanner throws a
TypeErrorrather thanreturning a code: at the point a required axis is missing,
exitCodesis itself the thing thatwas not supplied, so there is no trustworthy code to return.
[0.0.1] - 2026-08-05
Added
First release.
isCliEntrypoint(import.meta.url)answers "is this module the file Node waspointed at", for the
scripts/*.mjsgates that must not run their CLI body when a test importsthem for their exports.
It replaces the raw string comparison those gates were spelling by hand,
import.meta.url === `file://${resolve(process.argv[1])}`, which compares two strings ratherthan two paths and therefore answers
falsefor three ordinary invocations: an extension-lessspecifier that Node or
tsxresolved (node scripts/gaterunningscripts/gate.js), a checkoutunder a path containing a space (
import.meta.urlpercent-encodes and the concatenation doesnot), and a symlinked invocation (Node resolves the main module to its real path). In each the
gate exits 0 having checked nothing.
Those three forms also COMBINE, and the combination is the one that hides: an extension-less
argv[1]names no file, so it cannot be resolved directly, and any symlinked ancestor then putsthe two sides in different places.
isCliEntrypointresolves the deepest ancestor that doesexist and keeps the rest verbatim, so
tsx scripts/gateanswers correctly under a symlinkedcheckout.
The package carries no runtime dependencies and no build step, because the gates that import it
run before
pnpm installon purpose.