@cosyte/script-utils 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.5
4
+
5
+ ### Patch Changes
6
+
7
+ - f5e1a1f: The internal-refs phase rule lets two kinds of ordinary prose through. `phase` as the second half of a compound word is no longer a hit (`connect-phase socket`, `two-phase state`, `read-phase timeout`), and neither is `phase` or `phases` followed by `where`, `are`, `was`, `were`, `during` or `at` (`All 5 phases are defined`, `Connection lifecycle phase where the error occurred`). A phase named beside those words is still a hit (`shipped in phase 2`, `Phase 3 was where it failed`, `Phase-L`), and both guards' passages are in the rule's self-test floor, so every run proves them. The catalog is regenerated from each library's current package.json: the terminology, deid and synth descriptions take the serial comma, the cli record carries cli's current description, and cli's engines are `>=22.0.0`.
8
+
9
+ ## 0.1.4
10
+
11
+ ### Patch Changes
12
+
13
+ - b9b2a7c: Every gate is now a bin, so a repository with no `package.json` can run it through npx: `cosyte-no-emdash`, `cosyte-attw`, `cosyte-phi-scan`, `cosyte-internal-refs`, `cosyte-readme`, `cosyte-claude-card`, `cosyte-agent-notes` and `cosyte-catalog`, each with `--help`, and `script-utils`, the dispatcher npx runs by the package's name (`npx -y @cosyte/script-utils@0.1 no-emdash`). The dash gate also matches every form the marketing and consulting scanner caught, in any case: the characters, URL encoding, the backslash-u escape (plain, braced and eight-digit), the braced backslash-x escape, the CSS escape, the Unicode name, and the named, decimal and hexadecimal HTML entities with or without the semicolon and with leading zeros. It now reads every tracked file's name as well as its content, and answers `--help`. A tracked file or name carrying one of the newly matched forms is refused where 0.1.3 printed OK.
14
+
3
15
  ## 0.1.3
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -247,6 +247,31 @@ Each shared gate runs from a repository's package script with no file of its own
247
247
  notes), with exit codes 0 clean, 1 hits, 2 the gate could not run. The file may not set
248
248
  `exitCodes`, `repoRoot` or `write`, which the command-line form owns.
249
249
 
250
+ ### The bins, for a repository with no package.json
251
+
252
+ Every gate is also a bin (D56), so a repository with no `package.json`, or none that depends on this
253
+ package, runs it through npx. `script-utils` is named after the package, so npx runs it by the
254
+ package's name and it dispatches to the gate named first:
255
+
256
+ ```sh
257
+ npx -y @cosyte/script-utils@0.1 no-emdash # the dispatcher: script-utils <command>
258
+ npx -y -p @cosyte/script-utils@0.1 cosyte-readme # or one gate's own bin
259
+ ```
260
+
261
+ | bin | `script-utils` command | the gate |
262
+ | ---------------------- | ---------------------- | --------------------------------------------- |
263
+ | `cosyte-no-emdash` | `no-emdash` | the em and en dash gate, over files and names |
264
+ | `cosyte-attw` | `attw` | the attw publish gate, for the package here |
265
+ | `cosyte-phi-scan` | `phi-scan` | the PHI commit-gate |
266
+ | `cosyte-internal-refs` | `internal-refs` | the internal-reference gate |
267
+ | `cosyte-readme` | `readme` | the README gate |
268
+ | `cosyte-claude-card` | `claude-card` | the CLAUDE.md card checker |
269
+ | `cosyte-agent-notes` | `agent-notes` | the agent-notes checker |
270
+ | `cosyte-catalog` | `catalog` | the catalog check |
271
+
272
+ Each runs over the working directory, takes the arguments its gate takes, exits with the gate's own
273
+ code, and prints what it does with `--help`. `script-utils` with no command prints the list and exits 2.
274
+
250
275
  ### The em-dash gate, which is a file rather than a subpath
251
276
 
252
277
  This package also ships `check-no-emdash.sh`, the one implementation of the em-dash gate for every
@@ -275,9 +300,13 @@ A repository narrows WHAT IS SCANNED, and only that, through a tracked file at i
275
300
  `scripts/check-no-emdash.exclude`: one entry per line, `#` comments and blank lines ignored, an entry
276
301
  ending in `/` covering every tracked path beneath it, and every entry anchored at the repository
277
302
  root. An entry that matches no tracked path is refused rather than ignored, because an exclusion that
278
- has outlived its subject is a hole nobody is looking at. Nothing narrows WHAT IS MATCHED: the twelve
279
- banned forms are fixed in the gate. They are the em dash (U+2014) and the en dash (U+2013), each as
280
- the literal character, its URL encoding, its JS escape and three HTML entity spellings.
303
+ has outlived its subject is a hole nobody is looking at; an excluded path's name is left out with its
304
+ content. Nothing narrows WHAT IS MATCHED: the banned forms are fixed in the gate. They are the em
305
+ dash (U+2014) and the en dash (U+2013), in any case, each as the literal character, its URL encoding,
306
+ its backslash-u escape (plain, braced and eight-digit), its braced backslash-x escape, its CSS escape
307
+ (plain and zero-padded), its Unicode name escape (a backslash, `N` and the character's name in braces), and its named, decimal and hexadecimal
308
+ HTML entities, with or without the semicolon and with leading zeros. The default scan also reads
309
+ every tracked file's NAME, because a name is copy too.
281
310
 
282
311
  ### The README gate
283
312
 
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ // cosyte-agent-notes: the agent-notes checker for the repository in the working directory.
3
+ // A bin of @cosyte/script-utils (D56), so a repository with no package.json can run it with
4
+ // `npx -y -p @cosyte/script-utils cosyte-agent-notes`. It forwards every argument to the gate and exits with
5
+ // the gate's own code; `--help` prints what it does and exits 0.
6
+
7
+ import { runAgentNotesCheck } from "../agent-notes.js";
8
+
9
+ const HELP = `cosyte-agent-notes: check a repository's agent notes and every pointer into them.
10
+
11
+ cosyte-agent-notes [--dir <repository root>]
12
+
13
+ The notes are one file, documentation/agent-notes.md, and CLAUDE.md points into it as
14
+ documentation/agent-notes.md#<anchor>. Every pointer in a tracked file must name a heading or an
15
+ explicit anchor. Exit 0 clean, 1 a finding, 2 the check could not run.
16
+ `;
17
+
18
+ const args = process.argv.slice(2);
19
+ if (args.includes("--help") || args.includes("-h")) {
20
+ process.stdout.write(HELP);
21
+ } else {
22
+ process.exitCode = runAgentNotesCheck({ argv: args });
23
+ }
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ // cosyte-attw: the attw publish gate for the package in the working directory.
3
+ // A bin of @cosyte/script-utils (D56), so a repository with no package.json can run it with
4
+ // `npx -y -p @cosyte/script-utils cosyte-attw`. It forwards every argument to the gate and exits with
5
+ // the gate's own code; `--help` prints what it does and exits 0.
6
+
7
+ import { runAttwCli } from "../attw.js";
8
+
9
+ const HELP = `cosyte-attw: run the attw publish gate for the package in the working directory.
10
+
11
+ cosyte-attw [--no-definitely-typed]
12
+
13
+ It runs this package's own node_modules/.bin/attw over the tarball npm would publish, with four
14
+ nets attw lacks. scripts/attw.exclude names the resolutions the package leaves out, one per line:
15
+ node10 selects attw's node16 profile, node10 with node16-cjs selects esm-only. Exit 0 every net
16
+ passed; attw's own status when attw judged the package; 1 a refusal.
17
+ `;
18
+
19
+ const args = process.argv.slice(2);
20
+ if (args.includes("--help") || args.includes("-h")) {
21
+ process.stdout.write(HELP);
22
+ } else {
23
+ process.exitCode = runAttwCli(args);
24
+ }
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ // cosyte-catalog: the catalog check for the library in the working directory.
3
+ // A bin of @cosyte/script-utils (D56), so a repository with no package.json can run it with
4
+ // `npx -y -p @cosyte/script-utils cosyte-catalog`. It forwards every argument to the gate and exits with
5
+ // the gate's own code; `--help` prints what it does and exits 0.
6
+
7
+ import { runCatalogCheck } from "../catalog.js";
8
+
9
+ const HELP = `cosyte-catalog: check that a library's package.json description is the catalog's.
10
+
11
+ cosyte-catalog [--dir <package directory>]
12
+
13
+ The catalog is the one list of our libraries, generated in cosyte/config from each package.json.
14
+ Exit 0 the description is the catalog's, 1 it is not (both values printed), 2 the package is not
15
+ listed or package.json cannot be read.
16
+ `;
17
+
18
+ const args = process.argv.slice(2);
19
+ if (args.includes("--help") || args.includes("-h")) {
20
+ process.stdout.write(HELP);
21
+ } else {
22
+ process.exitCode = runCatalogCheck({ argv: args });
23
+ }
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ // cosyte-claude-card: the CLAUDE.md card checker for the repository in the working directory.
3
+ // A bin of @cosyte/script-utils (D56), so a repository with no package.json can run it with
4
+ // `npx -y -p @cosyte/script-utils cosyte-claude-card`. It forwards every argument to the gate and exits with
5
+ // the gate's own code; `--help` prints what it does and exits 0.
6
+
7
+ import { runClaudeCardGate } from "../claude-card.js";
8
+
9
+ const HELP = `cosyte-claude-card: check a repository's CLAUDE.md against the card spec.
10
+
11
+ cosyte-claude-card [--dir <repository root>]
12
+
13
+ Gate, Hazard and Rules in order, at most 60 lines of at most 100 characters and 5,120 bytes, and an
14
+ @AGENTS.md import when an AGENTS.md sits beside it. Exit 0 the card follows the spec, 1 a finding,
15
+ 2 CLAUDE.md could not be read.
16
+ `;
17
+
18
+ const args = process.argv.slice(2);
19
+ if (args.includes("--help") || args.includes("-h")) {
20
+ process.stdout.write(HELP);
21
+ } else {
22
+ process.exitCode = runClaudeCardGate({ argv: args });
23
+ }
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ // cosyte-internal-refs: the internal-reference gate over the repository in the working directory.
3
+ // A bin of @cosyte/script-utils (D56), so a repository with no package.json can run it with
4
+ // `npx -y -p @cosyte/script-utils cosyte-internal-refs`. It forwards every argument to the gate and exits with
5
+ // the gate's own code; `--help` prints what it does and exits 0.
6
+
7
+ import { runInternalRefsCli } from "../internal-refs.js";
8
+
9
+ const HELP = `cosyte-internal-refs: run the internal-reference gate over the repository in the working directory.
10
+
11
+ cosyte-internal-refs [--config <path>]
12
+
13
+ It refuses internal bookkeeping on a public surface: item identifiers, phase and wave language, ADR
14
+ numbers, internal repo paths. The axes come from scripts/internal-refs.json (or --config). Exit 0
15
+ clean, 1 hits, 2 the gate could not run.
16
+ `;
17
+
18
+ const args = process.argv.slice(2);
19
+ if (args.includes("--help") || args.includes("-h")) {
20
+ process.stdout.write(HELP);
21
+ } else {
22
+ process.exitCode = runInternalRefsCli(args);
23
+ }
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ // cosyte-no-emdash: the em and en dash gate over the repository in the working directory.
3
+ // A bin of @cosyte/script-utils (D56), so a repository with no package.json can run it with
4
+ // `npx -y -p @cosyte/script-utils cosyte-no-emdash`. The gate is the shell script beside this
5
+ // package's entry points; this runs it with bash, forwards every argument (`--help` included, which
6
+ // the gate answers itself) and exits with its code.
7
+
8
+ import { spawnSync } from "node:child_process";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ const gate = fileURLToPath(new URL("../check-no-emdash.sh", import.meta.url));
12
+ const run = spawnSync("bash", [gate, ...process.argv.slice(2)], { stdio: "inherit" });
13
+ if (run.error) {
14
+ process.stderr.write(`cosyte-no-emdash: could not run bash ${gate}: ${run.error.message}\n`);
15
+ process.exitCode = 1;
16
+ } else {
17
+ process.exitCode = run.status ?? 1;
18
+ }
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ // cosyte-phi-scan: the PHI commit-gate over the repository in the working directory.
3
+ // A bin of @cosyte/script-utils (D56), so a repository with no package.json can run it with
4
+ // `npx -y -p @cosyte/script-utils cosyte-phi-scan`. It forwards every argument to the gate and exits with
5
+ // the gate's own code; `--help` prints what it does and exits 0.
6
+
7
+ import { runPhiScanCli } from "../phi-scan.js";
8
+
9
+ const HELP = `cosyte-phi-scan: run the PHI commit-gate over the repository in the working directory.
10
+
11
+ cosyte-phi-scan scan the whole repository
12
+ cosyte-phi-scan --staged scan what is staged, Markdown excepted (the pre-commit route)
13
+ cosyte-phi-scan <path>... scan these paths
14
+
15
+ It carries the cross-cutting floor (a dashed SSN shape, an email at an undeclared domain) and
16
+ reports a place and a rule, never the matched value. scripts/phi-allow-list.txt declares synthetic
17
+ values, and scripts/phi-scan.exclude names literal tracked paths to leave out. Exit 0 clean, 1 hits,
18
+ 2 the scan could not account for something.
19
+ `;
20
+
21
+ const args = process.argv.slice(2);
22
+ if (args.includes("--help") || args.includes("-h")) {
23
+ process.stdout.write(HELP);
24
+ } else {
25
+ process.exitCode = runPhiScanCli(args);
26
+ }
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ // cosyte-readme: the README gate for the library in the working directory.
3
+ // A bin of @cosyte/script-utils (D56), so a repository with no package.json can run it with
4
+ // `npx -y -p @cosyte/script-utils cosyte-readme`. It forwards every argument to the gate and exits with
5
+ // the gate's own code; `--help` prints what it does and exits 0.
6
+
7
+ import { runReadmeGate } from "../readme.js";
8
+
9
+ const HELP = `cosyte-readme: check a library README against the house skeleton and npm's size limit.
10
+
11
+ cosyte-readme [--dir <package directory>]
12
+
13
+ It reads README.md and package.json. Exit 0 the README follows the skeleton, 1 a finding (each
14
+ names its line and rule), 2 the package could not be read.
15
+ `;
16
+
17
+ const args = process.argv.slice(2);
18
+ if (args.includes("--help") || args.includes("-h")) {
19
+ process.stdout.write(HELP);
20
+ } else {
21
+ process.exitCode = runReadmeGate({ argv: args });
22
+ }
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ // script-utils: one entry for every gate in @cosyte/script-utils (D56). npx runs the bin named
3
+ // after the package, so `npx -y @cosyte/script-utils@<version> <command> [args]` reaches each gate
4
+ // from a repository with no package.json. `<command> --help` is the gate's own help.
5
+
6
+ import { spawnSync } from "node:child_process";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const COMMANDS = {
10
+ "no-emdash": ["cosyte-no-emdash", "the em and en dash gate, over tracked files and their names"],
11
+ attw: ["cosyte-attw", "the attw publish gate, for the package here"],
12
+ "phi-scan": ["cosyte-phi-scan", "the PHI commit-gate, over the repository here"],
13
+ "internal-refs": [
14
+ "cosyte-internal-refs",
15
+ "the internal-reference gate, over the repository here",
16
+ ],
17
+ readme: ["cosyte-readme", "the README gate, for the library here"],
18
+ "claude-card": ["cosyte-claude-card", "the CLAUDE.md card checker"],
19
+ "agent-notes": ["cosyte-agent-notes", "the agent-notes checker"],
20
+ catalog: ["cosyte-catalog", "the catalog check, for the library here"],
21
+ };
22
+
23
+ const HELP = `script-utils: run one of the @cosyte/script-utils gates over the working directory.
24
+
25
+ script-utils <command> [arguments]
26
+ script-utils <command> --help
27
+
28
+ ${Object.entries(COMMANDS)
29
+ .map(([command, [bin, what]]) => ` ${command.padEnd(14)} ${what} (also the bin ${bin})`)
30
+ .join("\n")}
31
+ `;
32
+
33
+ const [command, ...rest] = process.argv.slice(2);
34
+ if (command === undefined || command === "--help" || command === "-h") {
35
+ process.stdout.write(HELP);
36
+ process.exitCode = command === undefined ? 2 : 0;
37
+ } else if (!(command in COMMANDS)) {
38
+ process.stderr.write(`script-utils: ${JSON.stringify(command)} is not a command.\n\n${HELP}`);
39
+ process.exitCode = 2;
40
+ } else {
41
+ const bin = fileURLToPath(new URL(`./${COMMANDS[command][0]}.js`, import.meta.url));
42
+ const run = spawnSync(process.execPath, [bin, ...rest], { stdio: "inherit" });
43
+ process.exitCode = run.error ? 2 : (run.status ?? 2);
44
+ }
package/catalog.json CHANGED
@@ -116,7 +116,7 @@
116
116
  {
117
117
  "name": "@cosyte/terminology",
118
118
  "slug": "terminology",
119
- "description": "Terminology engine for US healthcare code systems in Node.js and TypeScript: FHIR $translate, $lookup, $validate-code and $expand over code systems, value sets and maps you supply.",
119
+ "description": "Terminology engine for US healthcare code systems in Node.js and TypeScript: FHIR $translate, $lookup, $validate-code, and $expand over code systems, value sets, and maps you supply.",
120
120
  "line": "0.1",
121
121
  "engines": {
122
122
  "node": ">=22.0.0"
@@ -142,7 +142,7 @@
142
142
  {
143
143
  "name": "@cosyte/deid",
144
144
  "slug": "deid",
145
- "description": "Healthcare de-identification for Node.js and TypeScript: applies a HIPAA Safe Harbor policy to HL7 v2, C-CDA, FHIR R4, X12, NCPDP Telecom and DICOM, fails closed, and emits a value-free manifest.",
145
+ "description": "Healthcare de-identification for Node.js and TypeScript: applies a HIPAA Safe Harbor policy to HL7 v2, C-CDA, FHIR R4, X12, NCPDP Telecom, and DICOM, fails closed, and emits a value-free manifest.",
146
146
  "line": "0.1",
147
147
  "engines": {
148
148
  "node": ">=22.0.0"
@@ -155,7 +155,7 @@
155
155
  {
156
156
  "name": "@cosyte/synth",
157
157
  "slug": "synth",
158
- "description": "Deterministic, seedable synthetic healthcare test fixtures for Node.js and TypeScript: HL7 v2, FHIR R4 and US Core, C-CDA, X12, NCPDP and ASTM, spec-clean by construction and never real PHI.",
158
+ "description": "Deterministic, seedable synthetic healthcare test fixtures for Node.js and TypeScript: HL7 v2, FHIR R4 and US Core, C-CDA, X12, NCPDP, and ASTM, spec-clean by construction and never real PHI.",
159
159
  "line": "0.1",
160
160
  "engines": {
161
161
  "node": ">=22.0.0"
@@ -181,10 +181,10 @@
181
181
  {
182
182
  "name": "@cosyte/cli",
183
183
  "slug": "cli",
184
- "description": "The cosyte CLI: a PHI-safe developer front door over the @cosyte/* healthcare parsers. Parse HL7 v2 / FHIR from a file or stdin to typed JSON, with a documented exit-code contract.",
184
+ "description": "The Cosyte command line for Node.js and TypeScript: one command over HL7 v2, FHIR R4, X12, ASTM, NCPDP, C-CDA, DICOM, and MLLP, with HL7 v2 to FHIR conversion from transform, code mapping from terminology, and the cosyte-mcp MCP server for agents. Value-free diagnostics and a documented exit-code contract.",
185
185
  "line": "0.1",
186
186
  "engines": {
187
- "node": ">=22.0.0 <26.0.0"
187
+ "node": ">=22.0.0"
188
188
  },
189
189
  "repository": "https://github.com/cosyte/cli",
190
190
  "docs": "https://docs.cosyte.com/cli",
@@ -228,19 +228,32 @@ unset -f grep xargs sed awk 2>/dev/null || true
228
228
  # refuse_if_incomplete reads. That branch only refines wording, never the red.
229
229
  export LC_ALL=C.UTF-8
230
230
 
231
- # Matches U+2014 as the literal character and as its encodings: %E2%80%94 (URL), the
232
- # JS backslash-u escape, and the &mdash; / &#8212; / &#x2014; HTML entities. The
233
- # escape is not theoretical in this repo: it is what the parser-template manifest
234
- # carried, and a literal-only sweep passed straight over it. Matches U+2013 the same
235
- # way: %E2%80%93, the backslash-u escape, and the &ndash; / &#8211; / &#x2013; HTML
236
- # entities.
237
- #
238
- # THE MATCH VOCABULARY IS FROZEN at these twelve forms, six for each dash. A consumer
239
- # may narrow WHAT IS SCANNED through the declaration above; nothing may narrow WHAT IS
240
- # MATCHED, and widening it is a change every consuming repo inherits at once and is
241
- # specced as one. The en dash's six were that change (operator decision, 2026-11-07).
242
- PATTERN='\x{2014}|%E2%80%94|\\u2014|&mdash;|&#8212;|&#x2014;'
243
- PATTERN+='|\x{2013}|%E2%80%93|\\u2013|&ndash;|&#8211;|&#x2013;'
231
+ # What counts as a dash, for U+2014 and U+2013 alike. Case never matters, so `&MDASH;`
232
+ # and `%e2%80%94` count too (D56: the forms the marketing and consulting scanner caught,
233
+ # which this gate now matches so those repos lose nothing by moving to it):
234
+ #
235
+ # the characters U+2014 U+2013
236
+ # HTML named entities, ; optional &mdash; &ndash;
237
+ # HTML decimal references &#8212; &#8211; leading zeros and a missing ; too
238
+ # HTML hex references &#x2014; &#x2013; leading zeros and a missing ; too
239
+ # URL encoding of the UTF-8 bytes %E2%80%94 %E2%80%93
240
+ # backslash-u escapes (JS, JSON, Java, \u2014 \u{2014} \U00002014
241
+ # C#, Python, Rust, Swift)
242
+ # backslash-x escapes (PCRE, Perl) \x{2014}
243
+ # CSS escapes \2014 \002014
244
+ # Unicode names (Python, Perl) \N{EM DASH} \N{EN DASH}
245
+ #
246
+ # The lookaheads keep the near misses out: &#82120; and the CSS escape \20140 name other
247
+ # characters, and &mdashboard is not an entity. The escape forms are not theoretical: the
248
+ # parser-template manifest once carried a backslash-u em dash, and a literal-only sweep
249
+ # passed straight over it.
250
+ #
251
+ # A consumer may narrow WHAT IS SCANNED through the declaration above; nothing may narrow
252
+ # WHAT IS MATCHED, and widening it is a change every consuming repo inherits at once. The
253
+ # en dash's forms (operator decision, 2026-11-07) and these (D56) were such changes.
254
+ PATTERN='(?i)\x{2014}|\x{2013}|&[mn]dash(?![a-z])|&#0*821[12](?![0-9])|&#x0*201[34](?![0-9a-f])'
255
+ PATTERN+='|%E2%80%9[34]|\\u(?:0000)?201[34]|\\u\{0*201[34]\}|\\x\{0*201[34]\}'
256
+ PATTERN+='|\\0{0,2}201[34](?![0-9a-f])|\\N\{E[MN] DASH\}'
244
257
 
245
258
  # The declaration's path, relative to the top level of the repo under scan. Deliberately
246
259
  # NOT relative to this file: in a consuming repo this file lives under `node_modules/`
@@ -281,10 +294,11 @@ SCAN_MODE=files
281
294
 
282
295
  refuse_arguments() {
283
296
  echo "ERROR: check-no-emdash - $1" >&2
284
- echo " The modes are: (no argument) to scan every tracked file; --stdin LABEL" >&2
285
- echo " to scan the text arriving on stdin; --list-scanned to write the paths the" >&2
286
- echo " default scan reads; --self-id to write the sha256 of the implementation" >&2
287
- echo " that is executing. Refusing to run rather than guess which was meant." >&2
297
+ echo " The modes are: (no argument) to scan every tracked file and its name;" >&2
298
+ echo " --stdin LABEL to scan the text arriving on stdin; --list-scanned to write the" >&2
299
+ echo " paths the default scan reads; --self-id to write the sha256 of the" >&2
300
+ echo " implementation that is executing; --help for this. Refusing to run rather" >&2
301
+ echo " than guess which was meant." >&2
288
302
  exit 1
289
303
  }
290
304
 
@@ -324,6 +338,22 @@ case "${1:-}" in
324
338
  refuse_arguments "--self-id takes no further argument, and this was given: ${2}"
325
339
  fi
326
340
  ;;
341
+ --help | -h)
342
+ cat << 'USAGE'
343
+ check-no-emdash: refuse the em dash (U+2014) and the en dash (U+2013), as the characters and
344
+ in every encoded form, in the tracked files of the repository it runs in and in their names.
345
+
346
+ check-no-emdash.sh scan every tracked file and every tracked file name
347
+ check-no-emdash.sh --stdin LABEL scan the text on stdin (a PR title, body, commit message)
348
+ check-no-emdash.sh --list-scanned write the NUL-separated paths the default scan reads
349
+ check-no-emdash.sh --self-id write the sha256 of the implementation that is executing
350
+
351
+ A repository leaves paths out in scripts/check-no-emdash.exclude: one per line, an entry ending
352
+ in / covers everything beneath it, # starts a comment. Exit 0 means the mode completed and found
353
+ nothing banned; exit 1 means anything else.
354
+ USAGE
355
+ exit 0
356
+ ;;
327
357
  *)
328
358
  refuse_arguments "unrecognised argument: ${1}"
329
359
  ;;
@@ -341,6 +371,7 @@ GITERR=$(mktemp)
341
371
  FILELIST=$(mktemp)
342
372
  SCANLIST=$(mktemp)
343
373
  PLAINLIST=$(mktemp)
374
+ NAMELIST=$(mktemp)
344
375
  STDINBUF=$(mktemp)
345
376
  BINPROBE=$(mktemp)
346
377
  PROBEERR=$(mktemp)
@@ -353,8 +384,8 @@ PROBEERR=$(mktemp)
353
384
  # not lost with it.
354
385
  close_exit_vocabulary() {
355
386
  local code=$?
356
- rm -f "$ERRLOG" "$GITERR" "$FILELIST" "$SCANLIST" "$PLAINLIST" "$STDINBUF" "$BINPROBE" \
357
- "$PROBEERR"
387
+ rm -f "$ERRLOG" "$GITERR" "$FILELIST" "$SCANLIST" "$PLAINLIST" "$NAMELIST" "$STDINBUF" \
388
+ "$BINPROBE" "$PROBEERR"
358
389
  if [ "$code" -ne 0 ] && [ "$code" -ne 1 ]; then
359
390
  echo "ERROR: check-no-emdash - the run ended with status ${code}. This gate reports 0" >&2
360
391
  echo " for a mode that completed clean and 1 for everything else, so it is" >&2
@@ -816,6 +847,7 @@ esac
816
847
 
817
848
  : > "$SCANLIST"
818
849
  : > "$PLAINLIST"
850
+ : > "$NAMELIST"
819
851
  gitlinks=0
820
852
  scanned=0
821
853
  self_excluded=0
@@ -831,11 +863,16 @@ for i in "${!TRACKED[@]}"; do
831
863
 
832
864
  # What the repo declared it leaves out. Checked before the entry is opened, because an
833
865
  # excluded path is not read and its type on disk is therefore not this gate's business.
866
+ # It is not scanned at all, so its NAME is left out with its content.
834
867
  if is_excluded "$f"; then
835
868
  declared_excluded=$((declared_excluded + 1))
836
869
  continue
837
870
  fi
838
871
 
872
+ # A NAME IS COPY TOO (D56): every tracked path the declaration does not leave out has its
873
+ # name scanned, a gitlink's included, before any test of what is on disk.
874
+ printf '%s\0' "$f" >> "$NAMELIST"
875
+
839
876
  # `git ls-files` lists a submodule as a gitlink, which on disk is a REAL directory.
840
877
  # config has none today (checked: `git ls-files -s` lists no mode 160000 entry). The
841
878
  # skip is keyed on the INDEX MODE and never on the type on disk, because the two
@@ -886,12 +923,22 @@ if [ "$MODE" = list ]; then
886
923
  exit 0
887
924
  fi
888
925
 
926
+ # File names first: NUL-separated in, one per line out. `-a` because a name is text whatever
927
+ # bytes it holds, and the stderr capture refuses a name scan that could not complete.
928
+ NAME_HITS=$(grep -z -a -P -e "$PATTERN" < "$NAMELIST" 2>> "$ERRLOG" | tr '\0' '\n' || true)
929
+ refuse_if_incomplete
930
+
889
931
  HITS=$(xargs -0 -r grep -H -nP -e "$PATTERN" -- < "$SCANLIST" 2>> "$ERRLOG" || true)
890
932
 
891
933
  refuse_if_incomplete
892
934
 
935
+ if [ -n "$NAME_HITS" ] && [ -n "$HITS" ]; then
936
+ fail_with_hits "the tracked file names and the tracked files listed above" \
937
+ "$NAME_HITS"$'\n'"$HITS"
938
+ fi
939
+ [ -n "$NAME_HITS" ] && fail_with_hits "the tracked file names listed above" "$NAME_HITS"
893
940
  [ -n "$HITS" ] && fail_with_hits "the tracked files listed above" "$HITS"
894
941
 
895
- echo "check-no-emdash: OK (no em or en dashes in ${scanned} of ${#TRACKED[@]} tracked file(s);" \
896
- "${gitlinks} gitlink(s) skipped, ${self_excluded} excluded as the implementation under" \
897
- "scan, ${declared_excluded} excluded by ${DECLARATION})"
942
+ echo "check-no-emdash: OK (no em or en dashes in ${scanned} of ${#TRACKED[@]} tracked file(s)" \
943
+ "or their names; ${gitlinks} gitlink(s) skipped, ${self_excluded} excluded as the" \
944
+ "implementation under scan, ${declared_excluded} excluded by ${DECLARATION})"
package/internal-refs.js CHANGED
@@ -230,10 +230,28 @@ const PHASE_NOT_CLINICAL =
230
230
  "(?<!study )(?<!clinical )(?<!trial )(?<!acute )(?<!chronic )(?<!luteal )(?<!follicular )" +
231
231
  "(?<!liquid )(?<!gas )";
232
232
 
233
- /** Lookahead that drops the clinical-study field-name tails and the trial roman numerals. */
233
+ /**
234
+ * Lookbehind that drops `phase` as the second half of a compound word.
235
+ *
236
+ * `connect-phase socket`, `two-phase state` and `read-phase timeout` describe a protocol, not a
237
+ * roadmap: a letter and a hyphen in front of `phase` make it part of an adjective. Only the
238
+ * characters BEFORE the word are read, so `Phase-L` (the hyphen after it) is still a hit, and so is
239
+ * a phase after a digit and a hyphen.
240
+ */
241
+ const PHASE_NOT_COMPOUND = "(?<![A-Za-z]-)";
242
+
243
+ /**
244
+ * Lookahead that drops the clinical-study field-name tails, the trial roman numerals, and the
245
+ * words that make `phase` a noun in a sentence rather than the name of one.
246
+ *
247
+ * `All 5 phases are defined`, `the lifecycle phase where the error occurred`, `the phase was`, `the
248
+ * phases were`, `the phase during which` and `the phase at which` say what a phase is. A roadmap
249
+ * phase is named by what follows the word, and none of these words names one.
250
+ */
234
251
  const PHASE_NOT_FIELD =
235
252
  "(?!of\\b|with\\b|in\\b|out\\b|the\\b|and\\b|is\\b|for\\b|to\\b|identifier\\b|start\\b|end\\b|" +
236
- "evaluability\\b|number\\b|(?:I{1,3}|IV)\\s+(?:trial|stud|clinical|oncolog))";
253
+ "evaluability\\b|number\\b|where\\b|are\\b|was\\b|were\\b|during\\b|at\\b|" +
254
+ "(?:I{1,3}|IV)\\s+(?:trial|stud|clinical|oncolog))";
237
255
 
238
256
  /**
239
257
  * Imaging vocabulary that follows `slice` in reference material rather than in our jargon.
@@ -340,7 +358,8 @@ export function compileCanonicalRules(resolved) {
340
358
  name: "phase or wave language",
341
359
  patterns: [
342
360
  new RegExp(
343
- `\\b(?:roadmap phases?\\b[ ]?[A-Za-z0-9]*|${PHASE_NOT_CLINICAL}phases?[ -]${PHASE_NOT_FIELD}` +
361
+ `\\b(?:roadmap phases?\\b[ ]?[A-Za-z0-9]*|${PHASE_NOT_CLINICAL}${PHASE_NOT_COMPOUND}phases?[ -]` +
362
+ `${PHASE_NOT_FIELD}` +
344
363
  `[A-Za-z0-9]+[a-z]?\\b|wave \\d+\\b|the \\w+ and final phase\\b|` +
345
364
  `documentation residual\\b|${ORDINAL} (?:slice|wave)\\b)`,
346
365
  "gi",
@@ -350,12 +369,22 @@ export function compileCanonicalRules(resolved) {
350
369
  "Phase 5b closes it (Phase W, Phase-L and the thirteenth slice landed earlier, in wave 2)",
351
370
  // The plural, which ten of the eleven per-repository copies matched (`phases?[ -]`).
352
371
  "Phases 1 to 6 shipped together",
372
+ // What the two guards must still catch: a phase named next to a compound word, and a
373
+ // phase named before the words that make a bare `phase` a noun.
374
+ "The connect-phase socket shipped in phase 2",
375
+ "Phase 3 was where the error occurred",
353
376
  ],
354
377
  negatives: [
355
378
  "CSP-1 Study Phase Identifier, CSP-2 Study Phase Start Date/Time, CSP-3 Study Phase End " +
356
379
  "Date/Time, CSP-4 Study Phase Evaluability; a Phase III oncology trial and a Phase II " +
357
380
  "study; the acute phase reactant; the adapter stays in phase with the source system " +
358
381
  "and is out of phase",
382
+ // The compound-word guard: `phase` as the second half of an adjective.
383
+ "The connect-phase socket, the two-phase state and the read-phase timeout",
384
+ // The sentence guard: `phase` as a noun followed by what it is, not by its name.
385
+ "All 5 phases are defined; Connection lifecycle phase where the error occurred; the " +
386
+ "phase was open, the phases were closed, and a phase during which and a phase at which " +
387
+ "the socket failed",
359
388
  ],
360
389
  },
361
390
  {
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@cosyte/script-utils",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Zero-dependency helpers for the repo-local gate scripts in @cosyte/* repos.",
5
5
  "keywords": [
6
6
  "cli",
7
+ "bin",
7
8
  "entrypoint",
8
9
  "import-meta",
9
10
  "phi",
@@ -21,6 +22,17 @@
21
22
  "author": "Cosyte <hello@cosyte.com> (https://cosyte.com)",
22
23
  "type": "module",
23
24
  "main": "index.js",
25
+ "bin": {
26
+ "script-utils": "bin/script-utils.js",
27
+ "cosyte-no-emdash": "bin/cosyte-no-emdash.js",
28
+ "cosyte-attw": "bin/cosyte-attw.js",
29
+ "cosyte-phi-scan": "bin/cosyte-phi-scan.js",
30
+ "cosyte-internal-refs": "bin/cosyte-internal-refs.js",
31
+ "cosyte-readme": "bin/cosyte-readme.js",
32
+ "cosyte-claude-card": "bin/cosyte-claude-card.js",
33
+ "cosyte-agent-notes": "bin/cosyte-agent-notes.js",
34
+ "cosyte-catalog": "bin/cosyte-catalog.js"
35
+ },
24
36
  "types": "index.d.ts",
25
37
  "exports": {
26
38
  ".": {
@@ -75,6 +87,7 @@
75
87
  "agent-notes.js",
76
88
  "agent-notes.d.ts",
77
89
  "check-no-emdash.sh",
90
+ "bin",
78
91
  "README.md",
79
92
  "CHANGELOG.md"
80
93
  ],
@@ -85,7 +98,7 @@
85
98
  "access": "public"
86
99
  },
87
100
  "devDependencies": {
88
- "vitest": "4.1.4"
101
+ "vitest": "4.1.12"
89
102
  },
90
103
  "scripts": {
91
104
  "build": "node ../../scripts/pack-emdash-gate.mjs",