@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/README.md
CHANGED
|
@@ -1,10 +1,41 @@
|
|
|
1
|
+
<a href="https://cosyte.com">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cosyte.com/tile/cosyte-lockup-tile-on-dark-1200x300.png">
|
|
4
|
+
<img alt="The Cosyte logo on its own white ground: the icon beside the word Cosyte." src="https://cosyte.com/tile/cosyte-lockup-tile-on-light-1200x300.png">
|
|
5
|
+
</picture>
|
|
6
|
+
</a>
|
|
7
|
+
|
|
1
8
|
# @cosyte/script-utils
|
|
2
9
|
|
|
3
|
-
|
|
10
|
+
> The two things every repo-local gate script gets wrong, fixed once and shared.
|
|
11
|
+
|
|
12
|
+
[](https://www.npmjs.com/package/@cosyte/script-utils)
|
|
13
|
+
[](https://github.com/cosyte/config/actions/workflows/ci.yml)
|
|
14
|
+
[](https://github.com/cosyte/config/blob/main/LICENSE)
|
|
15
|
+
[](https://nodejs.org)
|
|
16
|
+
|
|
17
|
+
Zero-dependency helpers for the repo-local gate scripts in @cosyte/\* repos.
|
|
18
|
+
|
|
19
|
+
## Why this exists
|
|
20
|
+
|
|
21
|
+
A gate script that exits 0 without having checked anything is worse than no gate, because the run
|
|
22
|
+
conclusion is the only thing anyone reads. Two ways of producing one turned out to be universal
|
|
23
|
+
across the cosyte repositories: an entrypoint guard written by hand that answers `false` for ordinary
|
|
24
|
+
invocations, and thirteen byte-distinct copies of one PHI scanner, each of which had to be fixed
|
|
25
|
+
separately when an escape was found.
|
|
26
|
+
|
|
27
|
+
The nearest alternative is `scripts/parser-template/`, and it is deliberately not this. A scaffold is
|
|
28
|
+
COPIED, so fixing the template fixes no repository that already exists. Here a fix is one pull
|
|
29
|
+
request and a version bump.
|
|
30
|
+
|
|
31
|
+
## Status
|
|
4
32
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
33
|
+
`@cosyte/script-utils` is on the cosyte 0.1.x line: the public API is settled and bump types follow ordinary semver.
|
|
34
|
+
|
|
35
|
+
Still moving: the `runPhiScan` option surface, which is where the per-repo axes below are still being
|
|
36
|
+
argued about, and the cross-cutting detection floor, which gains patterns as escapes are found. A new
|
|
37
|
+
pattern is a new red in a consumer that was passing, so pin an exact version if a green commit gate
|
|
38
|
+
matters more to you than a new detection.
|
|
8
39
|
|
|
9
40
|
## Install
|
|
10
41
|
|
|
@@ -12,13 +43,17 @@ whether a release gate runs.
|
|
|
12
43
|
pnpm add -D @cosyte/script-utils
|
|
13
44
|
```
|
|
14
45
|
|
|
46
|
+
Node `>=22.14`. ESM only, no runtime dependencies, and no build step. Both of those are load-bearing
|
|
47
|
+
rather than minimalism: the gates in `config` itself run **before `pnpm install`** on purpose, so
|
|
48
|
+
that a broken or hostile install cannot decide whether a release gate runs.
|
|
49
|
+
|
|
15
50
|
### If your gate runs before `pnpm install`, do not use the bare specifier
|
|
16
51
|
|
|
17
52
|
Read this before adopting the package, because it is the one thing that does not generalise.
|
|
18
53
|
|
|
19
54
|
A bare `import ... from "@cosyte/script-utils"` resolves through `node_modules`, so it only works
|
|
20
|
-
once the install has happened. `config`'s own
|
|
21
|
-
|
|
55
|
+
once the install has happened. `config`'s own gates run before their install and therefore import
|
|
56
|
+
this file by **relative path** instead, which needs nothing on disk but the checkout.
|
|
22
57
|
|
|
23
58
|
So check when your gate runs, and pick accordingly:
|
|
24
59
|
|
|
@@ -29,26 +64,187 @@ So check when your gate runs, and pick accordingly:
|
|
|
29
64
|
|
|
30
65
|
Installing the package does not, on its own, give a pre-install gate the property described above.
|
|
31
66
|
|
|
32
|
-
|
|
67
|
+
Every entry point is published as source with no build step, so the relative-path route works for
|
|
68
|
+
all three: the files are `index.js`, `phi-scan.js` and `internal-refs.js`, each beside its own
|
|
69
|
+
hand-written `.d.ts`. A vendored copy is one of those files and nothing else.
|
|
33
70
|
|
|
34
|
-
|
|
71
|
+
## Usage
|
|
35
72
|
|
|
36
|
-
|
|
73
|
+
Guard a script's CLI side effect so a test can import it for its exports. That guard wraps the exit,
|
|
74
|
+
so this is the whole shape of a real gate:
|
|
75
|
+
|
|
76
|
+
```ts runnable
|
|
37
77
|
import { isCliEntrypoint } from "@cosyte/script-utils";
|
|
38
78
|
|
|
39
|
-
|
|
40
|
-
|
|
79
|
+
/** Your gate's body, exported so a test can import it without the CLI running. */
|
|
80
|
+
export function main(argv: string[]): number {
|
|
81
|
+
return argv.length;
|
|
41
82
|
}
|
|
42
83
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
84
|
+
// This module was imported, not pointed at, so the guard is closed: `main` does not run and nothing
|
|
85
|
+
// exits. The assertion is deliberately ahead of the branch, so the example cannot exit a test
|
|
86
|
+
// process even if that answer ever changed.
|
|
87
|
+
const runAsCli = isCliEntrypoint(import.meta.url);
|
|
88
|
+
runAsCli; // => false
|
|
89
|
+
if (runAsCli) process.exit(main(process.argv.slice(2)));
|
|
46
90
|
```
|
|
47
91
|
|
|
48
92
|
That guard is what lets a test import the module for its exports without the CLI executing as a side
|
|
49
93
|
effect, while the script still runs normally when invoked.
|
|
50
94
|
|
|
51
|
-
|
|
95
|
+
The PHI scanner is the other half, on its own subpath. Its shared read filter is exported so a
|
|
96
|
+
caller can compose with it rather than restate it:
|
|
97
|
+
|
|
98
|
+
```ts runnable
|
|
99
|
+
import { exemptsMarkdown } from "@cosyte/script-utils/phi-scan";
|
|
100
|
+
|
|
101
|
+
exemptsMarkdown("docs/adopting.md"); // => false
|
|
102
|
+
exemptsMarkdown("src/patient.ts"); // => true
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The internal-reference gate is the third, and it is configured entirely through one object. The
|
|
106
|
+
axis names below are the whole interface a consumer needs, so they are checked here rather than
|
|
107
|
+
described:
|
|
108
|
+
|
|
109
|
+
```ts runnable
|
|
110
|
+
import { canonicalRuleNames, resolveConfig } from "@cosyte/script-utils/internal-refs";
|
|
111
|
+
|
|
112
|
+
const config = {
|
|
113
|
+
projectPrefixes: ["HL7", "CCDA"],
|
|
114
|
+
standardsDesignations: ["HL7-(?:V2|V3|CDA)"],
|
|
115
|
+
surfacePaths: ["README.md", "docs-content"],
|
|
116
|
+
accountedTarballFiles: ["CHANGELOG.md", "dist"],
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Hand that to `runInternalRefsScan(config)` and it returns an exit code. Checked here without
|
|
120
|
+
// touching a repository, so this example reds if an axis is ever renamed under a consumer.
|
|
121
|
+
resolveConfig(config).problems; // => undefined
|
|
122
|
+
canonicalRuleNames().length; // => 6
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The `attw` publish gate is the fourth. It exists because `attw` prints "This package does not
|
|
126
|
+
contain types." and exits **0**: for a package that ships types, that sentence means the
|
|
127
|
+
declarations were not in the tarball, which is a broken publish reported as a pass. Your repo keeps
|
|
128
|
+
`scripts/attw.mjs` and an `attw` script whose body is exactly `node scripts/attw.mjs`; that file is
|
|
129
|
+
a caller, and this is the whole of it:
|
|
130
|
+
|
|
131
|
+
```ts runnable
|
|
132
|
+
import { runAttwGate } from "@cosyte/script-utils/attw";
|
|
133
|
+
|
|
134
|
+
// The one line a consuming repo's `scripts/attw.mjs` carries:
|
|
135
|
+
//
|
|
136
|
+
// process.exitCode = runAttwGate({ callerUrl: import.meta.url });
|
|
137
|
+
//
|
|
138
|
+
// `callerUrl` is required and has no default. `attw` is resolved as `../node_modules/.bin/attw`
|
|
139
|
+
// from it, so the binary that runs is YOUR package's own; defaulted to this module's location it
|
|
140
|
+
// would resolve inside `node_modules/@cosyte/`, where your package has no attw at all. The gate
|
|
141
|
+
// RETURNS the code to exit with and never calls `process.exit`, so it is testable in-process.
|
|
142
|
+
typeof runAttwGate; // => "function"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Entry points
|
|
146
|
+
|
|
147
|
+
| entry point | what it is |
|
|
148
|
+
| ------------------------------------ | ------------------------------------------------------------------------ |
|
|
149
|
+
| `@cosyte/script-utils` | `isCliEntrypoint(moduleUrl)`, the entry-point guard |
|
|
150
|
+
| `@cosyte/script-utils/phi-scan` | `runPhiScan(config)` and `exemptsMarkdown(relPath)`, the shared PHI gate |
|
|
151
|
+
| `@cosyte/script-utils/internal-refs` | `runInternalRefsScan(config)`, the shared internal-reference gate |
|
|
152
|
+
| `@cosyte/script-utils/attw` | `runAttwGate(options)`, the shared `attw` publish gate |
|
|
153
|
+
|
|
154
|
+
They are separate subpaths because they are separately adoptable: a repo can take the entry-point
|
|
155
|
+
guard without taking a position on PHI scanning, a repo can take the internal-reference gate without
|
|
156
|
+
taking either, and importing one never loads another.
|
|
157
|
+
|
|
158
|
+
### The em-dash gate, which is a file rather than a subpath
|
|
159
|
+
|
|
160
|
+
This package also ships `check-no-emdash.sh`, the one implementation of the em-dash gate for every
|
|
161
|
+
`@cosyte/*` repository. It is a shell script and not a module, so it is not in the `exports` map:
|
|
162
|
+
nothing imports it, a repository EXECUTES it.
|
|
163
|
+
|
|
164
|
+
```sh
|
|
165
|
+
# The package script every consuming repo keeps, which forwards its arguments:
|
|
166
|
+
# "check:no-emdash": "bash node_modules/@cosyte/script-utils/check-no-emdash.sh"
|
|
167
|
+
bash node_modules/@cosyte/script-utils/check-no-emdash.sh # scan every tracked file
|
|
168
|
+
bash node_modules/@cosyte/script-utils/check-no-emdash.sh --stdin PR # scan text on stdin
|
|
169
|
+
bash node_modules/@cosyte/script-utils/check-no-emdash.sh --list-scanned
|
|
170
|
+
bash node_modules/@cosyte/script-utils/check-no-emdash.sh --self-id
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Exit 0 means the mode completed and found nothing banned. Exit 1 means anything else: a banned form
|
|
174
|
+
was found, an input could not be read, an input was not scanned, a declaration was unusable, or an
|
|
175
|
+
argument was not recognised. There is no third code. Hits and diagnostics go to stderr in every mode.
|
|
176
|
+
|
|
177
|
+
`--self-id` prints the sha256 of the copy that is executing, so a repository can prove it is running
|
|
178
|
+
these bytes rather than a fork of them. `--list-scanned` prints the NUL-separated, repo-root-relative
|
|
179
|
+
path of every tracked file the default scan reads, so a repository adopting the gate can compare its
|
|
180
|
+
coverage against whatever it ran before rather than compare two filter expressions.
|
|
181
|
+
|
|
182
|
+
A repository narrows WHAT IS SCANNED, and only that, through a tracked file at its own top level,
|
|
183
|
+
`scripts/check-no-emdash.exclude`: one entry per line, `#` comments and blank lines ignored, an entry
|
|
184
|
+
ending in `/` covering every tracked path beneath it, and every entry anchored at the repository
|
|
185
|
+
root. An entry that matches no tracked path is refused rather than ignored, because an exclusion that
|
|
186
|
+
has outlived its subject is a hole nobody is looking at. Nothing narrows WHAT IS MATCHED: the six
|
|
187
|
+
banned forms are fixed in the gate.
|
|
188
|
+
|
|
189
|
+
## Overrides
|
|
190
|
+
|
|
191
|
+
`isCliEntrypoint` has no options and cannot be overridden. It answers one question and its
|
|
192
|
+
tie-breaking direction is the whole design, so a knob to invert it would be a knob to turn the gate
|
|
193
|
+
off.
|
|
194
|
+
|
|
195
|
+
`runPhiScan` is configured entirely through its `config` argument, along the five axes tabulated
|
|
196
|
+
under [the five per-repo axes](#the-five-per-repo-axes) below: three are required because they are
|
|
197
|
+
the ones a port gets wrong, and the defaulted ones move for every repo at once through a version
|
|
198
|
+
bump.
|
|
199
|
+
|
|
200
|
+
The engine's own cross-cutting detection floor is not on that list, and it is not overridable.
|
|
201
|
+
Neither is the completeness rule that refuses a run which enumerated a target and never read it. A
|
|
202
|
+
caller can widen what is scanned and can add detection, and cannot subtract either of those two.
|
|
203
|
+
|
|
204
|
+
`runInternalRefsScan` follows the same bargain along [its own axes](#the-internal-reference-axes):
|
|
205
|
+
four required, six defaulted, and every per-repository difference supplied by the caller. What it
|
|
206
|
+
will not accept is a configuration that SUBTRACTS. The canonical rule set, the self-test floor and
|
|
207
|
+
every completeness refusal are not caller inputs, and an option that reaches for one of them is
|
|
208
|
+
refused by name rather than ignored. So is an option the gate has never heard of: an ignored setting
|
|
209
|
+
reads, from the caller's side, exactly like an honoured one.
|
|
210
|
+
|
|
211
|
+
`runAttwGate` takes the narrowest bargain of the four, and deliberately. A caller supplies where it
|
|
212
|
+
lives (`callerUrl`, required, no default) and may supply the arguments to forward; everything else
|
|
213
|
+
is the gate's. The arguments it will forward are an ALLOW-LIST of two (`--profile` with its value,
|
|
214
|
+
and `--no-definitely-typed`), because a deny-list of blinding spellings bought exactly one more
|
|
215
|
+
evasion per round: each of `--quiet`, `-q`, `--format json`, `-fjson`, `-qP`, `--config-path`,
|
|
216
|
+
`--help` and `--version` was measured making attw exit 0 with nothing for the gate to read.
|
|
217
|
+
Widening that set is a deliberate one-line edit in this package, reviewed here, and not something a
|
|
218
|
+
caller can do. No net can be switched off.
|
|
219
|
+
|
|
220
|
+
## PHI and safety
|
|
221
|
+
|
|
222
|
+
This package ships the machinery of the PHI commit-gate, so it is the one package here that reads
|
|
223
|
+
files which may contain patient data. What it does with them, stated narrowly:
|
|
224
|
+
|
|
225
|
+
- It reads your repository's own files, in your own process, to look for PHI-shaped content. It sends
|
|
226
|
+
nothing anywhere and opens no network connection.
|
|
227
|
+
- It **does not log, echo or persist a matched value**. A finding names a path, a locator inside it
|
|
228
|
+
(`(ssn)`, `(email)`, or the field id your own detector raised it against) and the rule that fired;
|
|
229
|
+
the matched bytes are not written into the report, because a gate that prints the secret it found
|
|
230
|
+
has published it into a CI log. A `value` your detector passes to `ctx.hit` is dropped at the
|
|
231
|
+
boundary rather than stored, so nothing downstream can print what no record carries.
|
|
232
|
+
- The allow-list and the override log record DECLARATIONS about paths, never content.
|
|
233
|
+
- Detection is a floor, not a proof: this package owns a dashed SSN shape and an email at an
|
|
234
|
+
undeclared domain, and everything field-level is yours to supply through `detect`. A clean run
|
|
235
|
+
means nothing this configuration looked for was found, which is not the same as no PHI.
|
|
236
|
+
|
|
237
|
+
The consumer still owns the rest: deciding what a PHI-bearing field is for its standard, keeping real
|
|
238
|
+
patient data out of the repository in the first place, and never illustrating a rule, a fixture or an
|
|
239
|
+
issue report with a real value.
|
|
240
|
+
|
|
241
|
+
## API
|
|
242
|
+
|
|
243
|
+
### `isCliEntrypoint(import.meta.url)`
|
|
244
|
+
|
|
245
|
+
Is this module the file Node was pointed at, rather than one imported by something else?
|
|
246
|
+
|
|
247
|
+
#### Why not compare the strings
|
|
52
248
|
|
|
53
249
|
The obvious spelling is a one-liner, and several cosyte repos wrote it by hand:
|
|
54
250
|
|
|
@@ -72,7 +268,7 @@ the defect the gate was written to catch, because the run conclusion is the only
|
|
|
72
268
|
`isCliEntrypoint` compares canonical paths, resolves symlinks, and treats an `argv[1]` that names no
|
|
73
269
|
file on disk as a specifier Node had to resolve.
|
|
74
270
|
|
|
75
|
-
|
|
271
|
+
#### Which way it errs
|
|
76
272
|
|
|
77
273
|
When the answer is genuinely ambiguous it returns `true` and the CLI runs. A false positive runs a
|
|
78
274
|
gate during an import, which is loud and immediately visible; a false negative skips a gate and
|
|
@@ -81,6 +277,216 @@ exits 0, which is silent. Those are not symmetric, so the tie goes to running.
|
|
|
81
277
|
It throws a `TypeError` rather than returning `false` when handed something that is not a non-empty
|
|
82
278
|
string, for the same reason.
|
|
83
279
|
|
|
280
|
+
### `runPhiScan(config)`
|
|
281
|
+
|
|
282
|
+
`@cosyte/script-utils/phi-scan` is the shared machinery of the `@cosyte/*` PHI commit-gate.
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import { exemptsMarkdown, runPhiScan } from "@cosyte/script-utils/phi-scan";
|
|
286
|
+
|
|
287
|
+
process.exit(
|
|
288
|
+
runPhiScan({
|
|
289
|
+
exitCodes: { clean: 0, hits: 1, refuse: 2 },
|
|
290
|
+
scanRoots: ["."],
|
|
291
|
+
isStagedReadable: exemptsMarkdown,
|
|
292
|
+
detect: (ctx) => {
|
|
293
|
+
// Your standard's field-level detection. Check every PHI-bearing field
|
|
294
|
+
// against ctx.allow, and raise findings with ctx.hit(...).
|
|
295
|
+
},
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
It returns an exit code rather than calling `process.exit`, so a test can drive it in process.
|
|
301
|
+
|
|
302
|
+
#### Why it is a dependency and not a template file
|
|
303
|
+
|
|
304
|
+
`scripts/parser-template/` is a **scaffold**, not a dependency: the scaffolder copies it, so fixing
|
|
305
|
+
the template fixes no existing repo. That produced thirteen byte-distinct copies of one scanner, and
|
|
306
|
+
a newly-found escape therefore cost one pull request and one adversarial review **per repo**. Three
|
|
307
|
+
escape classes were paid for that way before this package existed. Here it is one pull request and a
|
|
308
|
+
version bump.
|
|
309
|
+
|
|
310
|
+
#### The boundary: what the engine owns, and what stays in your repo
|
|
311
|
+
|
|
312
|
+
**The ENGINE, shared and shipped here.** Argument parsing and the three modes (`--staged`, explicit
|
|
313
|
+
paths, and the `all`-mode sweep); reading the allow-list and the override log; target enumeration;
|
|
314
|
+
the union of the working-tree walk with the bytes git carries at every index path, deduplicated **by
|
|
315
|
+
content** under git's own `blob <len>\0` framing; the completeness rule (a target the run enumerated
|
|
316
|
+
and never read refuses, naming the paths); the per-root observation rule (`all` mode refuses unless
|
|
317
|
+
every `scanRoots` entry yielded a file that was read, naming the starved roots); every refusal; and a
|
|
318
|
+
cross-cutting detection floor that finds a dashed SSN shape and an email at an undeclared domain over
|
|
319
|
+
every file, always.
|
|
320
|
+
|
|
321
|
+
**The RULES, local to your repo, never moved and never shipped in this package.**
|
|
322
|
+
`scripts/phi-allow-list.txt` (the positive declaration that a token is synthetic, tagged `NAME`,
|
|
323
|
+
`DOB`, `ADDR`, `ID`, `EMAILDOMAIN`); `phi-scan-overrides.md` (the `--allow-fixture` audit log, and
|
|
324
|
+
whatever category-to-field map your detector reads); and the per-standard field detector you supply
|
|
325
|
+
through `detect` (names, DOB, MRN / member id, address, phone, parsed out of your own wire format).
|
|
326
|
+
|
|
327
|
+
The split is not a convenience. **This package's `files` list ships no repo's rules**, so a
|
|
328
|
+
declaration one consumer makes can never travel to another consumer through a version bump: with no
|
|
329
|
+
file at `allowListPath` the engine treats every PHI-shaped token as undeclared, and refuses rather
|
|
330
|
+
than reporting clean. A rules file is a reviewed, committed act in the repository it describes.
|
|
331
|
+
|
|
332
|
+
#### The five per-repo axes
|
|
333
|
+
|
|
334
|
+
Which ones are required is the design, not an oversight.
|
|
335
|
+
|
|
336
|
+
| Axis | Option | Required? |
|
|
337
|
+
| ------------------- | --------------------------------- | --------------------------------------------------------------- |
|
|
338
|
+
| 1 Exit codes | `exitCodes` | **Required.** The siblings disagree; a default would be a port. |
|
|
339
|
+
| 2 Roots | `scanRoots` | **Required.** `["."]` is the whole repository. |
|
|
340
|
+
| 2 Roots (subtract) | `excludedPaths`, `isWalkReadable` | Defaulted. Moving the shared boundary is one change here. |
|
|
341
|
+
| 3 `--staged` scope | `isStagedReadable` | **Required.** It decides what a commit is blocked on. |
|
|
342
|
+
| 4 Gitlinks | `regularBlobModes` | Defaulted to git's two regular-blob modes. |
|
|
343
|
+
| 5 EOL normalization | none | Machinery. A port must check it, not set it. |
|
|
344
|
+
|
|
345
|
+
A required axis has no default **because** it is the thing a port gets wrong: carrying an exit code
|
|
346
|
+
across a repo boundary is how a caller ends up branching on a meaning that repo never assigned.
|
|
347
|
+
A defaulted axis is the opposite case: every repo wants the same answer, so the answer lives here
|
|
348
|
+
and reaches all of them through a version bump.
|
|
349
|
+
|
|
350
|
+
#### Two things worth knowing before you adopt
|
|
351
|
+
|
|
352
|
+
**A detector that consults nothing has no remedy.** The whole-file `--allow-fixture` bypass is
|
|
353
|
+
recorded and then refused, so it cannot reach a clean run. Check every PHI-bearing field against
|
|
354
|
+
`ctx.allow`, or a developer meeting your detector has nowhere to go. The engine's own floor does
|
|
355
|
+
this on both branches.
|
|
356
|
+
|
|
357
|
+
**`all` mode needs a git index.** It refuses when git cannot name the index or names it empty,
|
|
358
|
+
because without it the sweep is the working-tree walk's word alone. A freshly scaffolded repo has to
|
|
359
|
+
`git init` and commit before an `all`-mode run means anything.
|
|
360
|
+
|
|
361
|
+
**Every scan root has to produce something.** `all` mode refuses when a `scanRoots` entry yielded no
|
|
362
|
+
file that was read, and names it. A root that is missing, unreadable, or whose every file your read
|
|
363
|
+
filter drops used to leave the sweep quietly narrower than its own configuration said, and one
|
|
364
|
+
productive root made the whole run look productive. If you narrow `scanRoots`, this is the tier that
|
|
365
|
+
tells you what the narrowing stopped reading. A root naming a regular file is scanned as one target;
|
|
366
|
+
one naming a symbolic link is refused rather than followed; one naming a directory that cannot be
|
|
367
|
+
listed is refused, naming the path.
|
|
368
|
+
|
|
369
|
+
**`scanRoots` bounds the WALK, not the index.** `all` mode reads the bytes git carries at every
|
|
370
|
+
tracked path, whatever your roots say, because a root list answers what is on disk under it and the
|
|
371
|
+
index answers what your repository carries. A hit found there is labelled `(git index)` or
|
|
372
|
+
`(git index; the working tree differs)`, with a footer counting them: the second means the file on
|
|
373
|
+
disk reads clean, so re-staging is part of the remedy.
|
|
374
|
+
|
|
375
|
+
**`isStagedReadable` decides what `--staged` REFUSES, not only what it reads.** A staged entry that
|
|
376
|
+
is not a regular blob, or that has no stage-0 blob, is refused when this predicate admits it and left
|
|
377
|
+
alone when it does not, because what a commit is blocked on is your repository's decision. `all` mode
|
|
378
|
+
still refuses such an entry on both of its routes.
|
|
379
|
+
|
|
380
|
+
### `runInternalRefsScan(config)`
|
|
381
|
+
|
|
382
|
+
`@cosyte/script-utils/internal-refs` keeps our own bookkeeping off every surface a consumer reads:
|
|
383
|
+
item identifiers, phase and wave language, ADR numbers, internal repository paths and traceability
|
|
384
|
+
markers, on the pages, the npm metadata and, optionally, the doc comments that compile into shipped
|
|
385
|
+
type declarations.
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
import { runInternalRefsScan } from "@cosyte/script-utils/internal-refs";
|
|
389
|
+
|
|
390
|
+
process.exit(
|
|
391
|
+
runInternalRefsScan({
|
|
392
|
+
projectPrefixes: ["HL7", "CCDA", "MLLP"],
|
|
393
|
+
standardsDesignations: ["HL7-(?:V2|V3|CDA)", "FHIR-R\\d[A-Z]?"],
|
|
394
|
+
surfacePaths: ["README.md", "TRADEMARKS.md", "LICENSE", "docs-content"],
|
|
395
|
+
accountedTarballFiles: ["CHANGELOG.md", "dist"],
|
|
396
|
+
sourceDocComments: { enabled: true, paths: ["src/*.ts", "src/**/*.ts"] },
|
|
397
|
+
}),
|
|
398
|
+
);
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
It returns an exit code rather than calling `process.exit`, so a test can drive it in process.
|
|
402
|
+
|
|
403
|
+
#### What exit 0 means, and what it does not
|
|
404
|
+
|
|
405
|
+
Exit 0 means the run completed, enumerated its configured surface, read every target it enumerated,
|
|
406
|
+
passed its own self-tests and found nothing. A hit, an enumerated target that was never read, an
|
|
407
|
+
unreadable input, an input a text matcher classifies as binary, a configured surface path the
|
|
408
|
+
repository does not track, a `files` entry the configuration does not account for, and a failed
|
|
409
|
+
self-test are each a non-zero exit with the cause on stderr. A hit report names the file, the line
|
|
410
|
+
and the rule.
|
|
411
|
+
|
|
412
|
+
#### The internal-reference axes
|
|
413
|
+
|
|
414
|
+
Four are required, and which four is the design rather than an oversight: each is the axis a port
|
|
415
|
+
gets wrong, and a default would be one repository's answer imposed on every other.
|
|
416
|
+
|
|
417
|
+
| Axis | Option | Required? |
|
|
418
|
+
| -------------------------- | ----------------------- | --------------------------------------------------------------------- |
|
|
419
|
+
| 1 Project prefixes | `projectPrefixes` | **Required**, empty refused. Keying on these is why `MSH-2` survives. |
|
|
420
|
+
| 2 Standards designations | `standardsDesignations` | **Required.** Never flagged. An empty list declares no collisions. |
|
|
421
|
+
| 3 Public surface | `surfacePaths` | **Required**, empty refused, every entry must be tracked. |
|
|
422
|
+
| 4 Tarball accounting | `accountedTarballFiles` | **Required.** The `files` entries already accounted for. |
|
|
423
|
+
| 5 Source doc comments | `sourceDocComments` | Defaulted OFF. Enable where the build copies doc text verbatim. |
|
|
424
|
+
| 6 Exit codes | `exitCodes` | Defaulted `{ clean: 0, hits: 1, refuse: 1 }`. `clean` must stay 0. |
|
|
425
|
+
| 7 Added detection | `extraRules` | Defaulted `[]`. Each added rule brings its own samples. |
|
|
426
|
+
| 8 Added self-test material | `extraSamples` | Defaulted `{}`. Additive only. |
|
|
427
|
+
| 9 Repository under test | `repoRoot` | Defaulted to the working directory, anchored at its git top level. |
|
|
428
|
+
| 10 Report destination | `write` | Defaulted to the process streams. |
|
|
429
|
+
|
|
430
|
+
`CONFIG_AXES` is the same table as data, so a consumer can enumerate it rather than transcribe it.
|
|
431
|
+
|
|
432
|
+
#### Two things worth knowing before you adopt this one
|
|
433
|
+
|
|
434
|
+
**The prefix set has to be extended by hand, and that is the cheaper mistake.** A new programme
|
|
435
|
+
means adding its prefix, and nothing catches it until someone does. The alternative is a `WORD-N`
|
|
436
|
+
shape rule, which flags the segment-field references a parser's documentation exists to provide.
|
|
437
|
+
|
|
438
|
+
**Every configured prefix has to be reachable.** The self-test floor proves each one still matches,
|
|
439
|
+
so a prefix shadowed by a standards designation refuses the run instead of silently contributing
|
|
440
|
+
nothing. That is the axis a caller edits most, and a dead entry there is a rule that stopped seeing.
|
|
441
|
+
|
|
442
|
+
### `runAttwGate(options)`
|
|
443
|
+
|
|
444
|
+
The `attw` publish gate, run in the working directory of the package being checked. It returns the
|
|
445
|
+
exit code a caller should exit with: 0 when every net passed, attw's own status when attw itself
|
|
446
|
+
judged the package, and 1 for every refusal.
|
|
447
|
+
|
|
448
|
+
`attw --pack .` prints "This package does not contain types." and exits **0**, because
|
|
449
|
+
`getExitCode()` returns before it reads the problem list whenever the analysis found no types at
|
|
450
|
+
all. For a package that ships types, that is a broken publish reported as a pass, and the window it
|
|
451
|
+
arrives through is ordinary: `tsup` emits JS in one pass and declarations in a later one, so every
|
|
452
|
+
build has a moment where `dist/` holds `.mjs`/`.cjs` and no `.d.ts`.
|
|
453
|
+
|
|
454
|
+
Four nets, each catching something the others cannot:
|
|
455
|
+
|
|
456
|
+
1. **Preflight**, over every relative artifact path the manifest promises. A path that is missing or
|
|
457
|
+
empty reds and is named. A manifest that declares NOTHING is refused rather than reported as
|
|
458
|
+
checked: three of the four nets grade that one set, so an empty set makes them vacuous at once.
|
|
459
|
+
2. **The document attw prints**, never its prose. The gate forces `--format json` and asserts
|
|
460
|
+
`analysis.types.kind === "included"`, so a route that hides or reformats the output leaves stdout
|
|
461
|
+
unparseable, and unparseable fails closed.
|
|
462
|
+
3. **npm's own pack listing.** A declared path can be on disk and out of the tarball, which net 1
|
|
463
|
+
cannot see and which a committed `.attw.json` can hide from attw's exit code. This net asks npm.
|
|
464
|
+
4. **The manifest pnpm WOULD PUBLISH.** pnpm applies `publishConfig` as publish-time overrides and
|
|
465
|
+
rewrites the manifest inside the tarball; `npm pack` does not. This org publishes with pnpm, so
|
|
466
|
+
the document net 3 grades is not the one that ships.
|
|
467
|
+
|
|
468
|
+
What each net does and does not claim, and every measurement behind them, is in `attw.js`'s own
|
|
469
|
+
docblock. It is not restated here: a claim written down twice is a claim that drifts.
|
|
470
|
+
|
|
471
|
+
## Compatibility
|
|
472
|
+
|
|
473
|
+
Node `>=22.14`, ESM only, published as source (`index.js`, `phi-scan.js`, `internal-refs.js` and
|
|
474
|
+
`attw.js` with hand-written `.d.ts` files) with no build step, so a consumer can import it by
|
|
475
|
+
relative path from a checkout as well as by specifier. `runPhiScan` shells out to `git` for the
|
|
476
|
+
index-backed half of its sweep, so `all` mode needs git on the path, `runInternalRefsScan` shells
|
|
477
|
+
out to `git` for its whole enumeration, and `runAttwGate` spawns the consuming package's own `attw`
|
|
478
|
+
binary, `npm pack` and, where a `publishConfig` object is present, `pnpm pack`.
|
|
479
|
+
|
|
480
|
+
## Contributing
|
|
481
|
+
|
|
482
|
+
Questions, bug reports and proposals go to
|
|
483
|
+
[the issue tracker](https://github.com/cosyte/config/issues). Pull requests are welcome, in
|
|
484
|
+
[cosyte/config](https://github.com/cosyte/config), where this package lives.
|
|
485
|
+
|
|
486
|
+
A change has to clear the required `verify` job, and a change to the scan engine needs a changeset:
|
|
487
|
+
a new detection pattern reds a consumer that was passing, and a removed one silently stops catching
|
|
488
|
+
something. Never open an issue containing a real PHI value; describe the shape instead.
|
|
489
|
+
|
|
84
490
|
## License
|
|
85
491
|
|
|
86
|
-
MIT
|
|
492
|
+
MIT, copyright Cosyte. See [LICENSE](https://github.com/cosyte/config/blob/main/LICENSE).
|
package/attw.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for `@cosyte/script-utils/attw`, the shared `attw` publish gate.
|
|
3
|
+
*
|
|
4
|
+
* The behaviour, every net, every refusal and every measurement behind them are documented in
|
|
5
|
+
* `attw.js`'s own docblock, which is the single authoritative description. Nothing is restated
|
|
6
|
+
* here: a claim written down twice is a claim that drifts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** What a caller must tell the gate, and the one thing it may. */
|
|
10
|
+
export interface AttwGateOptions {
|
|
11
|
+
/**
|
|
12
|
+
* The CALLER's `import.meta.url`. REQUIRED, and deliberately without a default.
|
|
13
|
+
*
|
|
14
|
+
* `attw` is resolved as `../node_modules/.bin/attw` from this URL, so the binary the gate spawns
|
|
15
|
+
* is the CONSUMING package's own. Defaulted to this module's URL it would resolve inside
|
|
16
|
+
* `node_modules/@cosyte/`, where a consumer has no attw at all. A call that omits it is refused.
|
|
17
|
+
*/
|
|
18
|
+
callerUrl: string;
|
|
19
|
+
/**
|
|
20
|
+
* The arguments to grade against the allow-list and forward to attw. Defaults to this process's
|
|
21
|
+
* own (`process.argv.slice(2)`). Only `--profile` (with its value) and `--no-definitely-typed`
|
|
22
|
+
* are accepted; everything else is refused by the allow-list rather than forwarded.
|
|
23
|
+
*/
|
|
24
|
+
argv?: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Run the gate in the current working directory, which must be the package being checked.
|
|
29
|
+
*
|
|
30
|
+
* Nothing here calls `process.exit`: the code is RETURNED, and a caller is expected to set
|
|
31
|
+
* `process.exitCode` from it.
|
|
32
|
+
*
|
|
33
|
+
* @returns 0 when every net passed, attw's own status when attw itself judged the package, and 1
|
|
34
|
+
* for every refusal the gate raises.
|
|
35
|
+
*/
|
|
36
|
+
export declare function runAttwGate(options: AttwGateOptions): number;
|