@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 +174 -2
- package/README.md +423 -17
- package/attw.d.ts +36 -0
- package/attw.js +1679 -0
- package/check-no-emdash.sh +874 -0
- package/internal-refs.d.ts +223 -0
- package/internal-refs.js +1363 -0
- package/package.json +28 -1
- package/phi-scan.d.ts +271 -0
- package/phi-scan.js +1936 -0
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosyte/script-utils",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Zero-dependency helpers for the repo-local gate scripts in @cosyte/* repos.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
7
7
|
"entrypoint",
|
|
8
8
|
"import-meta",
|
|
9
|
+
"phi",
|
|
9
10
|
"scripts",
|
|
10
11
|
"cosyte"
|
|
11
12
|
],
|
|
@@ -24,11 +25,30 @@
|
|
|
24
25
|
".": {
|
|
25
26
|
"types": "./index.d.ts",
|
|
26
27
|
"default": "./index.js"
|
|
28
|
+
},
|
|
29
|
+
"./phi-scan": {
|
|
30
|
+
"types": "./phi-scan.d.ts",
|
|
31
|
+
"default": "./phi-scan.js"
|
|
32
|
+
},
|
|
33
|
+
"./internal-refs": {
|
|
34
|
+
"types": "./internal-refs.d.ts",
|
|
35
|
+
"default": "./internal-refs.js"
|
|
36
|
+
},
|
|
37
|
+
"./attw": {
|
|
38
|
+
"types": "./attw.d.ts",
|
|
39
|
+
"default": "./attw.js"
|
|
27
40
|
}
|
|
28
41
|
},
|
|
29
42
|
"files": [
|
|
30
43
|
"index.js",
|
|
31
44
|
"index.d.ts",
|
|
45
|
+
"phi-scan.js",
|
|
46
|
+
"phi-scan.d.ts",
|
|
47
|
+
"internal-refs.js",
|
|
48
|
+
"internal-refs.d.ts",
|
|
49
|
+
"attw.js",
|
|
50
|
+
"attw.d.ts",
|
|
51
|
+
"check-no-emdash.sh",
|
|
32
52
|
"README.md",
|
|
33
53
|
"CHANGELOG.md"
|
|
34
54
|
],
|
|
@@ -37,5 +57,12 @@
|
|
|
37
57
|
},
|
|
38
58
|
"publishConfig": {
|
|
39
59
|
"access": "public"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"vitest": "4.1.4"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "node ../../scripts/pack-emdash-gate.mjs",
|
|
66
|
+
"test": "vitest run"
|
|
40
67
|
}
|
|
41
68
|
}
|
package/phi-scan.d.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for `@cosyte/script-utils/phi-scan`: the shared machinery of the `@cosyte/*` PHI
|
|
3
|
+
* commit-gate.
|
|
4
|
+
*
|
|
5
|
+
* The engine owns enumeration, the union of the working-tree walk with the bytes git carries,
|
|
6
|
+
* content deduplication, the completeness rule, the refusals and the cross-cutting SSN/email floor.
|
|
7
|
+
* The caller owns the five per-repo axes below and the per-standard field detectors.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A reported PHI finding: a POSITION and a RULE, never the token that matched. `path` is the LOCUS
|
|
12
|
+
* the engine chose, never a path the caller invents.
|
|
13
|
+
*
|
|
14
|
+
* 🛑 THE MATCHED VALUE IS NOT PART OF A FINDING. The report goes to stderr, stderr is a CI log, and
|
|
15
|
+
* a diagnostic about a PHI leak that quotes the leak is a second copy of it somewhere worse
|
|
16
|
+
* (`phi-safety` P4). A detector may pass `value`; the engine drops it at the boundary rather than
|
|
17
|
+
* storing it, so no reporting path downstream can print what no record carries.
|
|
18
|
+
*/
|
|
19
|
+
export interface Hit {
|
|
20
|
+
/** The reported locus: the target's repo-relative path, plus an origin label when it has one. */
|
|
21
|
+
path: string;
|
|
22
|
+
/** A locator inside the target: `(ssn)`, `(email)`, or a field id from a per-standard detector. */
|
|
23
|
+
segment: string;
|
|
24
|
+
/**
|
|
25
|
+
* The offending value, as a detector found it. ACCEPTED AND DISCARDED: the engine neither stores
|
|
26
|
+
* nor prints it, and a hit the engine reports never carries one. It stays in the surface so a
|
|
27
|
+
* detector that has the token in hand can keep saying so at its own call site without the engine
|
|
28
|
+
* becoming the place that publishes it.
|
|
29
|
+
*/
|
|
30
|
+
value?: string;
|
|
31
|
+
/** Why it was raised, in a few words. This is what the report prints beside the locator. */
|
|
32
|
+
reason: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The positive declaration that specific identifiers are synthetic. */
|
|
36
|
+
export interface AllowList {
|
|
37
|
+
/** Uppercase synthetic person-name tokens. Consumed by a caller's structured name detector. */
|
|
38
|
+
names: Set<string>;
|
|
39
|
+
/** Synthetic dates of birth, in whatever form the caller's detector normalises to. */
|
|
40
|
+
dobs: Set<string>;
|
|
41
|
+
/** Synthetic id values (SSN / MRN / member-id shapes), uppercased. Also read by the SSN floor. */
|
|
42
|
+
ids: Set<string>;
|
|
43
|
+
/** Allowed email domains, lowercased. Anything else is a hit. Read by the email floor. */
|
|
44
|
+
emailDomains: Set<string>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A stage-0 index entry: the mode git records, and the object it points at. */
|
|
48
|
+
export interface IndexEntry {
|
|
49
|
+
mode: string;
|
|
50
|
+
oid: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** One thing the scan will read, and where its bytes come from. */
|
|
54
|
+
export interface Target {
|
|
55
|
+
/** Repo-relative, forward-slashed. Every filter, exclusion and completeness tier keys on this. */
|
|
56
|
+
path: string;
|
|
57
|
+
read: () => Buffer;
|
|
58
|
+
/**
|
|
59
|
+
* Where these bytes came from, when it is not simply the file at `path`. Set only by the index
|
|
60
|
+
* union, and it decorates the REPORTED LOCUS ONLY.
|
|
61
|
+
*
|
|
62
|
+
* TWO VALUES, BECAUSE THE REMEDY DIFFERS. `git index` is a path whose working-tree copy this run
|
|
63
|
+
* did not read at all; `git index; the working tree differs` is one whose disk copy WAS read and
|
|
64
|
+
* carries other bytes, so the file a developer opens is clean and re-staging is part of the fix.
|
|
65
|
+
*/
|
|
66
|
+
origin?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** An entry the enumeration reached and cannot scan. Both fields are safe to print. */
|
|
70
|
+
export interface Unscannable {
|
|
71
|
+
path: string;
|
|
72
|
+
/** A token from the engine's own closed set. Never text off the other side of a link. */
|
|
73
|
+
kind: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** What a per-standard field detector is handed for one target. */
|
|
77
|
+
export interface DetectContext {
|
|
78
|
+
/**
|
|
79
|
+
* The reported LOCUS, already carrying any origin label. Raise hits against this and nothing else:
|
|
80
|
+
* a hit naming a bare path whose working-tree copy is clean sends a developer to the wrong file.
|
|
81
|
+
*/
|
|
82
|
+
path: string;
|
|
83
|
+
/** The target's bytes decoded as UTF-8. */
|
|
84
|
+
text: string;
|
|
85
|
+
/** The target's bytes, for a byte-strict format. */
|
|
86
|
+
bytes: Buffer;
|
|
87
|
+
/** The parsed allow-list, so a detector can honour a positive synthetic declaration. */
|
|
88
|
+
allow: AllowList;
|
|
89
|
+
/** Raise a hit. The locus is filled in by the engine. */
|
|
90
|
+
hit: (h: Omit<Hit, "path">) => void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A per-standard field detector. Throwing REFUSES the scan rather than taking node's exit code.
|
|
95
|
+
*
|
|
96
|
+
* 🛑 THE THROWN MESSAGE IS PRINTED VERBATIM, so it reaches CI logs. Name the position, never the
|
|
97
|
+
* content: a parser that interpolates the record it choked on turns a diagnostic into a PHI
|
|
98
|
+
* surface. This is the one place the engine prints text it cannot vouch for, and it is disclosed
|
|
99
|
+
* rather than suppressed, because a refusal nobody can diagnose is its own defect.
|
|
100
|
+
*/
|
|
101
|
+
export type DetectFn = (ctx: DetectContext) => void;
|
|
102
|
+
|
|
103
|
+
/** The three codes this repo's own exit contract assigns. All three must differ. */
|
|
104
|
+
export interface PhiScanExitCodes {
|
|
105
|
+
/**
|
|
106
|
+
* The scan ran, read every target it enumerated, and found nothing. The ONE subtraction is the
|
|
107
|
+
* enumeration TOCTOU window: an UNTRACKED file the walk listed, in `all` mode, that was gone by
|
|
108
|
+
* read time and is still gone at the end of the run, is reported SKIPPED. Git carries no bytes
|
|
109
|
+
* at such a path, so nothing this repository holds went unread. WHEN THAT HAPPENS THE CLEAN LINE
|
|
110
|
+
* ITSELF SAYS SO on stdout, counting the skipped files, so the subtraction is visible to a reader
|
|
111
|
+
* who never sees stderr; with nothing skipped the line is byte-identical to what it always was.
|
|
112
|
+
*/
|
|
113
|
+
clean: number;
|
|
114
|
+
/** This corpus contains something that looks like PHI. */
|
|
115
|
+
hits: number;
|
|
116
|
+
/** Every state the engine RAISES in which the scan cannot account for something. */
|
|
117
|
+
refuse: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface PhiScanConfig {
|
|
121
|
+
/**
|
|
122
|
+
* AXIS 1, REQUIRED. There is deliberately no default: the sibling `@cosyte/*` scanners do not
|
|
123
|
+
* agree on these numbers, and a caller that branches on the code (CI does) must read this repo's
|
|
124
|
+
* own contract rather than an inherited one.
|
|
125
|
+
*/
|
|
126
|
+
exitCodes: PhiScanExitCodes;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* AXIS 2, REQUIRED: the roots `all` mode walks. `["."]` means the whole repository, which is the
|
|
130
|
+
* only honest setting for a repo that has not yet decided what its corpus is. A ROOT IS A SCOPE
|
|
131
|
+
* DECISION AND IT IS THE AXIS MOST LIKELY TO BE WRONG IN A PORT: measure what a narrowing stops
|
|
132
|
+
* reading, rather than assuming it stops reading nothing.
|
|
133
|
+
*
|
|
134
|
+
* Each entry is normalised the way every other path is, so `src`, `./src`, `src/` and an absolute
|
|
135
|
+
* path to it are one root. That normalization is a fix rather than a convenience: `["./src"]`
|
|
136
|
+
* used to walk correctly while matching no index path, which emptied the union, the index
|
|
137
|
+
* non-blob refusal and the unmerged refusal in silence. A root resolving OUTSIDE the repository
|
|
138
|
+
* is refused for the same reason.
|
|
139
|
+
*
|
|
140
|
+
* A ROOT MAY NAME A REGULAR FILE, AND THE KIND IS DERIVED FROM THE FILESYSTEM RATHER THAN
|
|
141
|
+
* DECLARED. This parameter is a plain `string[]` on purpose. One sibling declares its roots as
|
|
142
|
+
* `{ rel, shape: "directory" | "file" }` and lists a single file among them, and that shape is
|
|
143
|
+
* expressible here without the richer type: such a root is scanned as one target. It is a
|
|
144
|
+
* measured decision rather than a preference, because an earlier draft fed every root to
|
|
145
|
+
* `readdirSync` and a file root threw `ENOTDIR`, uncaught, taking node's exit 1, the code this
|
|
146
|
+
* contract reserves for HITS FOUND.
|
|
147
|
+
*
|
|
148
|
+
* WHAT DERIVING GIVES UP: a declaration can notice that a root is not the KIND it was meant to be
|
|
149
|
+
* and derivation cannot, so a root that changes kind is silently treated as what it now is. That
|
|
150
|
+
* is a recorded narrowing rather than an oversight, with its compensating control named:
|
|
151
|
+
* `documentation/decisions/0003-the-phi-scan-engine-gap-settlements.md`, N2. Every TRACKED file
|
|
152
|
+
* that lived under such a root is still read, because the index half of `all` mode is not narrowed
|
|
153
|
+
* by this parameter at all.
|
|
154
|
+
*
|
|
155
|
+
* ALL THREE KINDS HAVE ONE SETTLED OUTCOME. A REGULAR FILE is scanned as one target. A SYMBOLIC
|
|
156
|
+
* LINK is refused rather than followed, and so is anything that is neither a file nor a directory.
|
|
157
|
+
* A DIRECTORY THAT CANNOT BE ENUMERATED is refused at the caller's `refuse` code, naming the path
|
|
158
|
+
* and the errno; it used to escape `readdirSync` uncaught and take node's own exit 1, the code
|
|
159
|
+
* this contract reserves for HITS FOUND. A MISSING root is skipped by the WALK, which is unchanged
|
|
160
|
+
* from the copied scanners, and is then caught by the rule below.
|
|
161
|
+
*
|
|
162
|
+
* 🛑 EVERY ROOT MUST YIELD AT LEAST ONE FILE THAT WAS ACTUALLY READ, OR `all` MODE REFUSES AND
|
|
163
|
+
* NAMES THE STARVED ROOTS. That is what closes the class the paragraph above used to end with:
|
|
164
|
+
* a root that contributes nothing WITHOUT SAYING SO. All three of the known members are in it - a
|
|
165
|
+
* missing root, an UNREADABLE root (reported the same way a missing one is), and a root whose
|
|
166
|
+
* every file the read filter drops, which is what a `.md` file root does under the default
|
|
167
|
+
* `isWalkReadable`. A root is a scope decision, and a scope decision that silently selected
|
|
168
|
+
* nothing is the sweep reporting on a corpus it never had.
|
|
169
|
+
*/
|
|
170
|
+
scanRoots: readonly string[];
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* AXIS 3, REQUIRED: the READ half of scope for `--staged`. Widening it changes what a COMMIT is
|
|
174
|
+
* blocked on, which is a hook decision each repo takes for itself.
|
|
175
|
+
*
|
|
176
|
+
* IT MUST STAY INSIDE `scanRoots`, AND THE ENGINE ENFORCES THAT RATHER THAN ASSUMING IT. This
|
|
177
|
+
* doc used to say "narrower than the root half by construction", and nothing constructed it:
|
|
178
|
+
* these are two independent keys. A reviewer measured the gap with roots at `["src"]` and this
|
|
179
|
+
* filter at `exemptsMarkdown`: a STAGED symbolic link under `test/fixtures/` was outside every
|
|
180
|
+
* scan root, so the non-regular refusal never saw it, and the route read the link's TARGET PATH
|
|
181
|
+
* as if it were content and reported clean at exit 0. A staged path this admits and no scan root
|
|
182
|
+
* covers is now REFUSED, naming the path.
|
|
183
|
+
*
|
|
184
|
+
* 🛑 THIS PREDICATE ALSO DECIDES WHAT THE `--staged` ROUTE REFUSES, which is a change. Its
|
|
185
|
+
* non-regular and unmerged refusals used to key on the ROOT half of scope, so a caller whose
|
|
186
|
+
* staged scope is narrower than its roots had commits blocked that its own gate had always let
|
|
187
|
+
* through. What a commit is blocked on is this key's job. THE COST: a staged non-regular entry
|
|
188
|
+
* this predicate declines is no longer refused HERE. `all` mode still refuses it twice over (the
|
|
189
|
+
* walk classifies it under a scan root, and the index route refuses the tracked record wherever it
|
|
190
|
+
* sits), so a repository's own sweep still does. Recorded as N1 in
|
|
191
|
+
* `documentation/decisions/0003-the-phi-scan-engine-gap-settlements.md`; widen this predicate if
|
|
192
|
+
* you want the wider refusal.
|
|
193
|
+
*/
|
|
194
|
+
isStagedReadable: (relPath: string) => boolean;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* AXIS 2, the subtractive half. Repo-relative paths NO route reads: not the walk, not the index
|
|
198
|
+
* union, not `--staged`.
|
|
199
|
+
*
|
|
200
|
+
* EXCLUDE A LITERAL PATH, NEVER A CLASS. A "skip binary blobs" predicate was measured to drop two
|
|
201
|
+
* of a sibling's hand-written sources, which embed NUL bytes as HMAC domain separators. A literal
|
|
202
|
+
* path is reviewable in a diff; a class quietly grows new members. Each entry is a file the scan
|
|
203
|
+
* has NO verdict about, so each one wants a comment saying why.
|
|
204
|
+
*
|
|
205
|
+
* @default new Set()
|
|
206
|
+
*/
|
|
207
|
+
excludedPaths?: ReadonlySet<string>;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* AXIS 2, the READ half of scope for the two SWEEPING routes. Defaults to the shared Markdown
|
|
211
|
+
* exemption, and the default is the point: moving that boundary is then one change here rather
|
|
212
|
+
* than one edit per repo.
|
|
213
|
+
*
|
|
214
|
+
* @default exemptsMarkdown
|
|
215
|
+
*/
|
|
216
|
+
isWalkReadable?: (relPath: string) => boolean;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* AXIS 4: git's modes for a regular blob. Every other mode names something with no bytes to read
|
|
220
|
+
* at that path, and is refused rather than skipped.
|
|
221
|
+
*
|
|
222
|
+
* @default new Set(["100644", "100755"])
|
|
223
|
+
*/
|
|
224
|
+
regularBlobModes?: ReadonlySet<string>;
|
|
225
|
+
|
|
226
|
+
/** @default process.cwd() */
|
|
227
|
+
repoRoot?: string;
|
|
228
|
+
|
|
229
|
+
/** @default process.argv.slice(2) */
|
|
230
|
+
argv?: string[];
|
|
231
|
+
|
|
232
|
+
/** @default <repoRoot>/scripts/phi-allow-list.txt */
|
|
233
|
+
allowListPath?: string;
|
|
234
|
+
|
|
235
|
+
/** @default <repoRoot>/phi-scan-overrides.md */
|
|
236
|
+
overrideLogPath?: string;
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The per-standard, field-level detection this engine deliberately does not own: names, DOB,
|
|
240
|
+
* MRN / member id, address, phone. Parse the wire format properly rather than bolting a blind text
|
|
241
|
+
* regex onto it; coded values produce false confidence.
|
|
242
|
+
*/
|
|
243
|
+
detect?: DetectFn;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Run the PHI scan and return an exit code drawn from `config.exitCodes`. Nothing here calls
|
|
248
|
+
* `process.exit`, so a test can drive the engine in-process.
|
|
249
|
+
*
|
|
250
|
+
* @throws {TypeError} When a required axis is missing or malformed. That is a misconfigured scanner
|
|
251
|
+
* rather than a scan result, it lands on the author's first run, and it must not be reportable as
|
|
252
|
+
* a clean pass.
|
|
253
|
+
* @example
|
|
254
|
+
* process.exit(
|
|
255
|
+
* runPhiScan({
|
|
256
|
+
* exitCodes: { clean: 0, hits: 1, refuse: 2 },
|
|
257
|
+
* scanRoots: ["."],
|
|
258
|
+
* isStagedReadable: (p) => p.startsWith("test/fixtures/"),
|
|
259
|
+
* }),
|
|
260
|
+
* );
|
|
261
|
+
*/
|
|
262
|
+
export declare function runPhiScan(config: PhiScanConfig): number;
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* The shared Markdown read-exemption, and the default value of `isWalkReadable`.
|
|
266
|
+
*
|
|
267
|
+
* Markdown is documentation rather than fixture data and may legitimately describe a violator value.
|
|
268
|
+
* The consequence, stated because it is route-dependent rather than file-dependent: a tracked `.md`
|
|
269
|
+
* is read by NEITHER sweeping route, while a `.md` named explicitly on argv IS scanned.
|
|
270
|
+
*/
|
|
271
|
+
export declare function exemptsMarkdown(relPath: string): boolean;
|