@cosyte/script-utils 0.0.1 → 0.0.2
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 +92 -0
- package/README.md +71 -0
- package/package.json +8 -1
- package/phi-scan.d.ts +224 -0
- package/phi-scan.js +1543 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,98 @@ the **`0.0.x`-until-first-alpha** ladder.
|
|
|
8
8
|
> Because the generator is disabled, **`[Unreleased]` is promoted to a version heading BY HAND**, in
|
|
9
9
|
> the pull request that adds the changeset. Nothing does it automatically.
|
|
10
10
|
|
|
11
|
+
## [0.0.2] - 2026-08-11
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`@cosyte/script-utils/phi-scan`: the shared machinery of the `@cosyte/*` PHI commit-gate, as a
|
|
16
|
+
parameterised engine.** `scripts/parser-template/` is a SCAFFOLD rather than a dependency, so
|
|
17
|
+
`scripts/phi-scan.ts` was COPIED into every parser repo. **Thirteen repos, thirteen byte-distinct
|
|
18
|
+
copies**, so a newly-found escape cost one pull request and one adversarial review PER REPO, and
|
|
19
|
+
three escape classes had already been paid for that way. `runPhiScan(config)` owns argument
|
|
20
|
+
parsing, the allow-list and override log, target enumeration on all three routes, the union of the
|
|
21
|
+
working-tree walk with the bytes git carries, content deduplication under git's own
|
|
22
|
+
`blob <len>\0` framing, the completeness rule, every refusal, and the cross-cutting SSN/email
|
|
23
|
+
floor.
|
|
24
|
+
|
|
25
|
+
**THE FIVE PER-REPO AXES ARE PARAMETERS, NOT FORKS**, and which ones are required is itself the
|
|
26
|
+
design. `exitCodes`, `scanRoots` and `isStagedReadable` are **REQUIRED**: the siblings genuinely
|
|
27
|
+
disagree on all three, and a default would be the porting mistake the gate exists to catch (at
|
|
28
|
+
least one sibling uses 2 where another uses 1). `excludedPaths`, `isWalkReadable` (the Markdown
|
|
29
|
+
read exemption) and `regularBlobModes` are **DEFAULTED**, so moving one of those boundaries is a
|
|
30
|
+
change to this package plus a version bump rather than an edit in thirteen repos. EOL
|
|
31
|
+
normalization has **no parameter at all** and stays machinery: the walk/index deduplication is by
|
|
32
|
+
CONTENT, so where a `text` attribute or `core.autocrlf` makes the index carry LF and the working
|
|
33
|
+
tree CRLF, BOTH forms are scanned.
|
|
34
|
+
|
|
35
|
+
**Per-standard field detection stays with the caller**, through `detect`. It is handed the
|
|
36
|
+
reported LOCUS rather than the target path, so a hit found in the bytes git carries cannot be
|
|
37
|
+
reported against a working-tree copy a developer would open and find clean. That used to be a
|
|
38
|
+
sentence in a comment; it is now the only path a caller can reach.
|
|
39
|
+
|
|
40
|
+
- **A scan root may name a regular FILE, and the kind is DERIVED from the filesystem rather than
|
|
41
|
+
declared**, which is what keeps the parameter a plain `string[]`. Measured against the thirteen
|
|
42
|
+
live copies: they declare roots in at least six different shapes, and one declares
|
|
43
|
+
`{ rel, shape: "directory" | "file" }` with a single file among them. An earlier draft fed every
|
|
44
|
+
root to `readdirSync`, so such a root threw `ENOTDIR`, uncaught, and the run took node's exit 1,
|
|
45
|
+
the code this contract reserves for HITS FOUND. **What deriving gives up is stated rather than
|
|
46
|
+
left to be found**: a declaration can notice a root is not the KIND it was meant to be and
|
|
47
|
+
derivation cannot. A root that is neither a file nor a directory is still refused, a root naming a
|
|
48
|
+
symbolic link is refused rather than followed, and a MISSING root is skipped, which is unchanged
|
|
49
|
+
from the copied scanners and is named as the one remaining silently-empty root state.
|
|
50
|
+
- **A scan root of `"."` means the whole repository**, with gitignored directories pruned during
|
|
51
|
+
descent and `.git` skipped by literal name, which is what makes a whole-repository root usable at
|
|
52
|
+
all rather than a walk through `node_modules`. Pruning is equivalent to filtering afterwards
|
|
53
|
+
because **`git check-ignore` is index-aware at directory granularity**, measured both ways on git
|
|
54
|
+
2.39.5: with nothing tracked underneath, a gitignored directory is reported ignored and pruned;
|
|
55
|
+
with one file force-added underneath it is reported NOT ignored, so the walk descends and still
|
|
56
|
+
reads it. An earlier draft justified this with the gitignore-pattern rule that a path under an
|
|
57
|
+
excluded directory cannot be re-included, which does not settle it, because the filter being
|
|
58
|
+
replaced asked `check-ignore` too.
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- **Two containments the first draft ASSERTED are now ENFORCED**, both falsified by this slice's
|
|
63
|
+
adversarial review and both reproduced against the pre-consolidation scanner too, so neither is a
|
|
64
|
+
regression this package introduced. What was introduced was the sentence claiming they held, on the
|
|
65
|
+
API contract thirteen migrations are about to be written against.
|
|
66
|
+
- **A staged path `isStagedReadable` admits that no scan root covers is REFUSED.** The type said
|
|
67
|
+
the `--staged` filter was "narrower than the root half by construction"; nothing constructed it,
|
|
68
|
+
because they are two independent keys. Measured with roots at `["src"]` and the filter at the
|
|
69
|
+
shared Markdown exemption: a STAGED mode-120000 entry under `test/fixtures/` was outside every
|
|
70
|
+
scan root, so the non-regular refusal never saw it, and the route enumerated it, READ it, handed
|
|
71
|
+
the link's TARGET PATH to the detector as if it were content, counted the scan complete and
|
|
72
|
+
printed `OK: no hits` at exit 0. Narrowing silently to the intersection would have been the wrong
|
|
73
|
+
repair: it hides a misconfiguration in the one place the gate blocks a commit.
|
|
74
|
+
- **A scan root is normalised the way every other path is, and one resolving outside the repository
|
|
75
|
+
is refused.** `"./src"` is a spelling the type documents as valid; it walked correctly while
|
|
76
|
+
`isUnderScanRoot` compared it against the normalized index path and never matched, emptying the
|
|
77
|
+
union, the index non-blob refusal and the unmerged refusal in silence. Measured: `["src"]`
|
|
78
|
+
refused a tracked mode-120000 entry at exit 2 while `["./src"]` reported clean at exit 0 over the
|
|
79
|
+
same repository.
|
|
80
|
+
- **The optional axes are shape-checked.** `excludedPaths: ["a"]`, a plausible reading of
|
|
81
|
+
"repo-relative paths", used to survive normalization, reach `.has(...)` inside enumeration, and
|
|
82
|
+
take node's exit 1 from there, which the exit contract reserves for HITS FOUND.
|
|
83
|
+
- **The floor's dashed-SSN branch now consults `allow.ids`, in either rendering.** This is a
|
|
84
|
+
correction rather than a port: with the whole-file `--allow-fixture` bypass closed, a detector
|
|
85
|
+
that consults nothing leaves a developer with a hit and **no remedy at all**, and a sibling
|
|
86
|
+
shipped a footer claiming the token allow-list was the only remedy that reaches a clean run while
|
|
87
|
+
that was false for exactly this branch. One `ID` entry now covers both the dashed and the undashed
|
|
88
|
+
rendering, so a repo does not have to guess which one a fixture uses.
|
|
89
|
+
- **A fatal partway through the sweep prints the hits already found BEFORE the refusal**, instead of
|
|
90
|
+
discarding them. Measured on a mutant that throws after the reads: the old ordering produced a
|
|
91
|
+
refusal with **zero** `HIT:` lines over a corpus containing a real hit. The refusal still wins the
|
|
92
|
+
exit code and the clean line is still unreachable from there, so nothing is reported as accounted
|
|
93
|
+
for that is not.
|
|
94
|
+
- **The hit footer is scoped to what the engine can know.** It says the cross-cutting floor consults
|
|
95
|
+
the allow-list and that a repo's own detectors are answerable that way **only if** they consult
|
|
96
|
+
it. The engine cannot see inside a caller's `detect`, so a wider claim would be one it has no
|
|
97
|
+
evidence for.
|
|
98
|
+
- **A detector that throws REFUSES the scan** rather than escaping to node's own exit code, which
|
|
99
|
+
this contract reserves for HITS FOUND. A misconfigured scanner throws a `TypeError` rather than
|
|
100
|
+
returning a code: at the point a required axis is missing, `exitCodes` is itself the thing that
|
|
101
|
+
was not supplied, so there is no trustworthy code to return.
|
|
102
|
+
|
|
11
103
|
## [0.0.1] - 2026-08-05
|
|
12
104
|
|
|
13
105
|
### Added
|
package/README.md
CHANGED
|
@@ -81,6 +81,77 @@ exits 0, which is silent. Those are not symmetric, so the tie goes to running.
|
|
|
81
81
|
It throws a `TypeError` rather than returning `false` when handed something that is not a non-empty
|
|
82
82
|
string, for the same reason.
|
|
83
83
|
|
|
84
|
+
## `runPhiScan(config)`
|
|
85
|
+
|
|
86
|
+
`@cosyte/script-utils/phi-scan` is the shared machinery of the `@cosyte/*` PHI commit-gate.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { exemptsMarkdown, runPhiScan } from "@cosyte/script-utils/phi-scan";
|
|
90
|
+
|
|
91
|
+
process.exit(
|
|
92
|
+
runPhiScan({
|
|
93
|
+
exitCodes: { clean: 0, hits: 1, refuse: 2 },
|
|
94
|
+
scanRoots: ["."],
|
|
95
|
+
isStagedReadable: exemptsMarkdown,
|
|
96
|
+
detect: (ctx) => {
|
|
97
|
+
// Your standard's field-level detection. Check every PHI-bearing field
|
|
98
|
+
// against ctx.allow, and raise findings with ctx.hit(...).
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
It returns an exit code rather than calling `process.exit`, so a test can drive it in process.
|
|
105
|
+
|
|
106
|
+
### Why it is a dependency and not a template file
|
|
107
|
+
|
|
108
|
+
`scripts/parser-template/` is a **scaffold**, not a dependency: the scaffolder copies it, so fixing
|
|
109
|
+
the template fixes no existing repo. That produced thirteen byte-distinct copies of one scanner, and
|
|
110
|
+
a newly-found escape therefore cost one pull request and one adversarial review **per repo**. Three
|
|
111
|
+
escape classes were paid for that way before this package existed. Here it is one pull request and a
|
|
112
|
+
version bump.
|
|
113
|
+
|
|
114
|
+
### What it owns
|
|
115
|
+
|
|
116
|
+
Argument parsing and the three modes (`--staged`, explicit paths, and the `all`-mode sweep); the
|
|
117
|
+
allow-list and the override log; target enumeration; the union of the working-tree walk with the
|
|
118
|
+
bytes git carries, deduplicated **by content** under git's own `blob <len>\0` framing; the
|
|
119
|
+
completeness rule (a target the run enumerated and never read refuses, naming the paths); every
|
|
120
|
+
refusal; and a cross-cutting floor that detects a dashed SSN shape and an email at an undeclared
|
|
121
|
+
domain.
|
|
122
|
+
|
|
123
|
+
It does **not** own per-standard field detection: names, DOB, MRN / member id, address, phone.
|
|
124
|
+
Those differ per healthcare standard and are supplied through `detect`.
|
|
125
|
+
|
|
126
|
+
### The five per-repo axes
|
|
127
|
+
|
|
128
|
+
Which ones are required is the design, not an oversight.
|
|
129
|
+
|
|
130
|
+
| Axis | Option | Required? |
|
|
131
|
+
| ------------------- | --------------------------------- | --------------------------------------------------------------- |
|
|
132
|
+
| 1 Exit codes | `exitCodes` | **Required.** The siblings disagree; a default would be a port. |
|
|
133
|
+
| 2 Roots | `scanRoots` | **Required.** `["."]` is the whole repository. |
|
|
134
|
+
| 2 Roots (subtract) | `excludedPaths`, `isWalkReadable` | Defaulted. Moving the shared boundary is one change here. |
|
|
135
|
+
| 3 `--staged` scope | `isStagedReadable` | **Required.** It decides what a commit is blocked on. |
|
|
136
|
+
| 4 Gitlinks | `regularBlobModes` | Defaulted to git's two regular-blob modes. |
|
|
137
|
+
| 5 EOL normalization | none | Machinery. A port must check it, not set it. |
|
|
138
|
+
|
|
139
|
+
A required axis has no default **because** it is the thing a port gets wrong: carrying an exit code
|
|
140
|
+
across a repo boundary is how a caller ends up branching on a meaning that repo never assigned.
|
|
141
|
+
A defaulted axis is the opposite case: every repo wants the same answer, so the answer lives here
|
|
142
|
+
and reaches all of them through a version bump.
|
|
143
|
+
|
|
144
|
+
### Two things worth knowing before you adopt
|
|
145
|
+
|
|
146
|
+
**A detector that consults nothing has no remedy.** The whole-file `--allow-fixture` bypass is
|
|
147
|
+
recorded and then refused, so it cannot reach a clean run. Check every PHI-bearing field against
|
|
148
|
+
`ctx.allow`, or a developer meeting your detector has nowhere to go. The engine's own floor does
|
|
149
|
+
this on both branches.
|
|
150
|
+
|
|
151
|
+
**`all` mode needs a git index.** It refuses when git cannot name the index or names it empty,
|
|
152
|
+
because without it the sweep is the working-tree walk's word alone. A freshly scaffolded repo has to
|
|
153
|
+
`git init` and commit before an `all`-mode run means anything.
|
|
154
|
+
|
|
84
155
|
## License
|
|
85
156
|
|
|
86
157
|
MIT
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosyte/script-utils",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
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,17 @@
|
|
|
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"
|
|
27
32
|
}
|
|
28
33
|
},
|
|
29
34
|
"files": [
|
|
30
35
|
"index.js",
|
|
31
36
|
"index.d.ts",
|
|
37
|
+
"phi-scan.js",
|
|
38
|
+
"phi-scan.d.ts",
|
|
32
39
|
"README.md",
|
|
33
40
|
"CHANGELOG.md"
|
|
34
41
|
],
|
package/phi-scan.d.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
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
|
+
/** A reported PHI finding. `path` is the LOCUS the engine chose, never a path the caller invents. */
|
|
11
|
+
export interface Hit {
|
|
12
|
+
/** The reported locus: the target's repo-relative path, plus an origin label when it has one. */
|
|
13
|
+
path: string;
|
|
14
|
+
/** A locator inside the target: `(ssn)`, `(email)`, or a field id from a per-standard detector. */
|
|
15
|
+
segment: string;
|
|
16
|
+
/** The offending value, as found. */
|
|
17
|
+
value: string;
|
|
18
|
+
/** Why it was raised, in a few words. */
|
|
19
|
+
reason: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The positive declaration that specific identifiers are synthetic. */
|
|
23
|
+
export interface AllowList {
|
|
24
|
+
/** Uppercase synthetic person-name tokens. Consumed by a caller's structured name detector. */
|
|
25
|
+
names: Set<string>;
|
|
26
|
+
/** Synthetic dates of birth, in whatever form the caller's detector normalises to. */
|
|
27
|
+
dobs: Set<string>;
|
|
28
|
+
/** Synthetic id values (SSN / MRN / member-id shapes), uppercased. Also read by the SSN floor. */
|
|
29
|
+
ids: Set<string>;
|
|
30
|
+
/** Allowed email domains, lowercased. Anything else is a hit. Read by the email floor. */
|
|
31
|
+
emailDomains: Set<string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A stage-0 index entry: the mode git records, and the object it points at. */
|
|
35
|
+
export interface IndexEntry {
|
|
36
|
+
mode: string;
|
|
37
|
+
oid: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** One thing the scan will read, and where its bytes come from. */
|
|
41
|
+
export interface Target {
|
|
42
|
+
/** Repo-relative, forward-slashed. Every filter, exclusion and completeness tier keys on this. */
|
|
43
|
+
path: string;
|
|
44
|
+
read: () => Buffer;
|
|
45
|
+
/**
|
|
46
|
+
* Where these bytes came from, when it is not simply the file at `path`. Set only by the index
|
|
47
|
+
* union, and it decorates the REPORTED LOCUS ONLY.
|
|
48
|
+
*/
|
|
49
|
+
origin?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** An entry the enumeration reached and cannot scan. Both fields are safe to print. */
|
|
53
|
+
export interface Unscannable {
|
|
54
|
+
path: string;
|
|
55
|
+
/** A token from the engine's own closed set. Never text off the other side of a link. */
|
|
56
|
+
kind: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** What a per-standard field detector is handed for one target. */
|
|
60
|
+
export interface DetectContext {
|
|
61
|
+
/**
|
|
62
|
+
* The reported LOCUS, already carrying any origin label. Raise hits against this and nothing else:
|
|
63
|
+
* a hit naming a bare path whose working-tree copy is clean sends a developer to the wrong file.
|
|
64
|
+
*/
|
|
65
|
+
path: string;
|
|
66
|
+
/** The target's bytes decoded as UTF-8. */
|
|
67
|
+
text: string;
|
|
68
|
+
/** The target's bytes, for a byte-strict format. */
|
|
69
|
+
bytes: Buffer;
|
|
70
|
+
/** The parsed allow-list, so a detector can honour a positive synthetic declaration. */
|
|
71
|
+
allow: AllowList;
|
|
72
|
+
/** Raise a hit. The locus is filled in by the engine. */
|
|
73
|
+
hit: (h: Omit<Hit, "path">) => void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A per-standard field detector. Throwing REFUSES the scan rather than taking node's exit code.
|
|
78
|
+
*
|
|
79
|
+
* 🛑 THE THROWN MESSAGE IS PRINTED VERBATIM, so it reaches CI logs. Name the position, never the
|
|
80
|
+
* content: a parser that interpolates the record it choked on turns a diagnostic into a PHI
|
|
81
|
+
* surface. This is the one place the engine prints text it cannot vouch for, and it is disclosed
|
|
82
|
+
* rather than suppressed, because a refusal nobody can diagnose is its own defect.
|
|
83
|
+
*/
|
|
84
|
+
export type DetectFn = (ctx: DetectContext) => void;
|
|
85
|
+
|
|
86
|
+
/** The three codes this repo's own exit contract assigns. All three must differ. */
|
|
87
|
+
export interface PhiScanExitCodes {
|
|
88
|
+
/** The scan ran, read every target it enumerated, and found nothing. */
|
|
89
|
+
clean: number;
|
|
90
|
+
/** This corpus contains something that looks like PHI. */
|
|
91
|
+
hits: number;
|
|
92
|
+
/** Every state the engine RAISES in which the scan cannot account for something. */
|
|
93
|
+
refuse: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface PhiScanConfig {
|
|
97
|
+
/**
|
|
98
|
+
* AXIS 1, REQUIRED. There is deliberately no default: the sibling `@cosyte/*` scanners do not
|
|
99
|
+
* agree on these numbers, and a caller that branches on the code (CI does) must read this repo's
|
|
100
|
+
* own contract rather than an inherited one.
|
|
101
|
+
*/
|
|
102
|
+
exitCodes: PhiScanExitCodes;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* AXIS 2, REQUIRED: the roots `all` mode walks. `["."]` means the whole repository, which is the
|
|
106
|
+
* only honest setting for a repo that has not yet decided what its corpus is. A ROOT IS A SCOPE
|
|
107
|
+
* DECISION AND IT IS THE AXIS MOST LIKELY TO BE WRONG IN A PORT: measure what a narrowing stops
|
|
108
|
+
* reading, rather than assuming it stops reading nothing.
|
|
109
|
+
*
|
|
110
|
+
* Each entry is normalised the way every other path is, so `src`, `./src`, `src/` and an absolute
|
|
111
|
+
* path to it are one root. That normalization is a fix rather than a convenience: `["./src"]`
|
|
112
|
+
* used to walk correctly while matching no index path, which emptied the union, the index
|
|
113
|
+
* non-blob refusal and the unmerged refusal in silence. A root resolving OUTSIDE the repository
|
|
114
|
+
* is refused for the same reason.
|
|
115
|
+
*
|
|
116
|
+
* A ROOT MAY NAME A REGULAR FILE, AND THE KIND IS DERIVED FROM THE FILESYSTEM RATHER THAN
|
|
117
|
+
* DECLARED. This parameter is a plain `string[]` on purpose. One sibling declares its roots as
|
|
118
|
+
* `{ rel, shape: "directory" | "file" }` and lists a single file among them, and that shape is
|
|
119
|
+
* expressible here without the richer type: such a root is scanned as one target. It is a
|
|
120
|
+
* measured decision rather than a preference, because an earlier draft fed every root to
|
|
121
|
+
* `readdirSync` and a file root threw `ENOTDIR`, uncaught, taking node's exit 1, the code this
|
|
122
|
+
* contract reserves for HITS FOUND.
|
|
123
|
+
*
|
|
124
|
+
* WHAT DERIVING GIVES UP: a declaration can notice that a root is not the KIND it was meant to be
|
|
125
|
+
* and derivation cannot, so a root that changes kind is silently treated as what it now is. A root
|
|
126
|
+
* that is NEITHER a file nor a directory is still refused, and a root naming a symbolic link is
|
|
127
|
+
* refused rather than followed. A MISSING root is skipped, which is unchanged from the copied
|
|
128
|
+
* scanners. It is not the last state in which a root contributes nothing without saying so, and
|
|
129
|
+
* two others are known: a file root the read filter drops (a `.md` file root reads nothing under
|
|
130
|
+
* the default `isWalkReadable`, so porting one means overriding that filter too), and an
|
|
131
|
+
* UNREADABLE root, which is reported the same way a missing one is.
|
|
132
|
+
*/
|
|
133
|
+
scanRoots: readonly string[];
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* AXIS 3, REQUIRED: the READ half of scope for `--staged`. Widening it changes what a COMMIT is
|
|
137
|
+
* blocked on, which is a hook decision each repo takes for itself.
|
|
138
|
+
*
|
|
139
|
+
* IT MUST STAY INSIDE `scanRoots`, AND THE ENGINE ENFORCES THAT RATHER THAN ASSUMING IT. This
|
|
140
|
+
* doc used to say "narrower than the root half by construction", and nothing constructed it:
|
|
141
|
+
* these are two independent keys. A reviewer measured the gap with roots at `["src"]` and this
|
|
142
|
+
* filter at `exemptsMarkdown`: a STAGED symbolic link under `test/fixtures/` was outside every
|
|
143
|
+
* scan root, so the non-regular refusal never saw it, and the route read the link's TARGET PATH
|
|
144
|
+
* as if it were content and reported clean at exit 0. A staged path this admits and no scan root
|
|
145
|
+
* covers is now REFUSED, naming the path.
|
|
146
|
+
*/
|
|
147
|
+
isStagedReadable: (relPath: string) => boolean;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* AXIS 2, the subtractive half. Repo-relative paths NO route reads: not the walk, not the index
|
|
151
|
+
* union, not `--staged`.
|
|
152
|
+
*
|
|
153
|
+
* EXCLUDE A LITERAL PATH, NEVER A CLASS. A "skip binary blobs" predicate was measured to drop two
|
|
154
|
+
* of a sibling's hand-written sources, which embed NUL bytes as HMAC domain separators. A literal
|
|
155
|
+
* path is reviewable in a diff; a class quietly grows new members. Each entry is a file the scan
|
|
156
|
+
* has NO verdict about, so each one wants a comment saying why.
|
|
157
|
+
*
|
|
158
|
+
* @default new Set()
|
|
159
|
+
*/
|
|
160
|
+
excludedPaths?: ReadonlySet<string>;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* AXIS 2, the READ half of scope for the two SWEEPING routes. Defaults to the shared Markdown
|
|
164
|
+
* exemption, and the default is the point: moving that boundary is then one change here rather
|
|
165
|
+
* than one edit per repo.
|
|
166
|
+
*
|
|
167
|
+
* @default exemptsMarkdown
|
|
168
|
+
*/
|
|
169
|
+
isWalkReadable?: (relPath: string) => boolean;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* AXIS 4: git's modes for a regular blob. Every other mode names something with no bytes to read
|
|
173
|
+
* at that path, and is refused rather than skipped.
|
|
174
|
+
*
|
|
175
|
+
* @default new Set(["100644", "100755"])
|
|
176
|
+
*/
|
|
177
|
+
regularBlobModes?: ReadonlySet<string>;
|
|
178
|
+
|
|
179
|
+
/** @default process.cwd() */
|
|
180
|
+
repoRoot?: string;
|
|
181
|
+
|
|
182
|
+
/** @default process.argv.slice(2) */
|
|
183
|
+
argv?: string[];
|
|
184
|
+
|
|
185
|
+
/** @default <repoRoot>/scripts/phi-allow-list.txt */
|
|
186
|
+
allowListPath?: string;
|
|
187
|
+
|
|
188
|
+
/** @default <repoRoot>/phi-scan-overrides.md */
|
|
189
|
+
overrideLogPath?: string;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The per-standard, field-level detection this engine deliberately does not own: names, DOB,
|
|
193
|
+
* MRN / member id, address, phone. Parse the wire format properly rather than bolting a blind text
|
|
194
|
+
* regex onto it; coded values produce false confidence.
|
|
195
|
+
*/
|
|
196
|
+
detect?: DetectFn;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Run the PHI scan and return an exit code drawn from `config.exitCodes`. Nothing here calls
|
|
201
|
+
* `process.exit`, so a test can drive the engine in-process.
|
|
202
|
+
*
|
|
203
|
+
* @throws {TypeError} When a required axis is missing or malformed. That is a misconfigured scanner
|
|
204
|
+
* rather than a scan result, it lands on the author's first run, and it must not be reportable as
|
|
205
|
+
* a clean pass.
|
|
206
|
+
* @example
|
|
207
|
+
* process.exit(
|
|
208
|
+
* runPhiScan({
|
|
209
|
+
* exitCodes: { clean: 0, hits: 1, refuse: 2 },
|
|
210
|
+
* scanRoots: ["."],
|
|
211
|
+
* isStagedReadable: (p) => p.startsWith("test/fixtures/"),
|
|
212
|
+
* }),
|
|
213
|
+
* );
|
|
214
|
+
*/
|
|
215
|
+
export declare function runPhiScan(config: PhiScanConfig): number;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The shared Markdown read-exemption, and the default value of `isWalkReadable`.
|
|
219
|
+
*
|
|
220
|
+
* Markdown is documentation rather than fixture data and may legitimately describe a violator value.
|
|
221
|
+
* The consequence, stated because it is route-dependent rather than file-dependent: a tracked `.md`
|
|
222
|
+
* is read by NEITHER sweeping route, while a `.md` named explicitly on argv IS scanned.
|
|
223
|
+
*/
|
|
224
|
+
export declare function exemptsMarkdown(relPath: string): boolean;
|