@cosyte/script-utils 0.1.4 → 0.1.6

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,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 239ac9a: The catalog check a library runs is now advisory. `cosyte-catalog` (and `catalog.js`) still compares the package.json description with the catalog this release carries, but a disagreement, or a package the catalog does not list, prints both values as a note and exits 0: a library that changes its description is no longer held red until the next script-utils release. `--strict` (or `{ strict: true }`) restores the gate, 1 for a disagreement and 2 for an unlisted package; 2 still means the check could not run. Keeping the catalog fresh moves to cosyte/config, which compares it with every library's latest release on npm (its description, release line and engines range) nightly and before each config release, and regenerates it from there.
8
+ - 4e30735: The dash gate reads a changelog's dated archive the way the repositories that keep one already do. A line in `scripts/check-no-emdash.exclude` of the form `CHANGELOG.md em-only below ## Released before this file was generated` reads the file from that exact line to its end for the em dash alone, in every form, so an en dash in an old range passes while an em dash still reds; `CHANGELOG.md unread below <line>` does not read the archive at all. Everything above the line is read for both dashes as before, the file's name is still scanned, and a boundary line the file no longer carries is refused rather than ignored. The test suite now spells out every encoded form terminology's own scanner carried (the braced escape, lower-case URL encoding, entities with no semicolon or with leading zeros, and the upper-case X), all of which 0.1.4 already refused. The internal-refs slice rule no longer counts `the` or `each` as a determiner: a fixed-width reader's field slice (`the field slices`, `the slice holding the concept code`) is the reader's vocabulary. `this`, `that`, `another`, `previous`, `next`, `final` and `current` still red, so `the final slice` and `the next slice` are still hits, but a bare `the slice` and `each slice` no longer are.
9
+ - 409ef6c: Under CI, every gate's `--help` and `-h` now exit non-zero instead of 0: the bins exit 2 and the dash gate exits 1, after printing the help to stderr with a line saying why. Help checks nothing, so a package script such as `"attw": "cosyte-attw --help"` used to pass a required check having run no gate. CI is read the way the common runners set it: `CI` set to anything but an empty string, `false` or `0`. At a terminal `--help` prints to stdout and exits 0 as before.
10
+ - 91f8d76: The internal-refs canonical rules take what generalizes from dicom's own gate. An umbrella spec id (`S0228-github-profile-ci-drift`, never a HCPCS code such as `S9083`) is an identifier. The phase rule also catches the adjective forms (`in subsequent phases`, `a later wave`, `waves 2 and 3`) while letting the clinical senses through (`a later phase of the disease`), and a citation of the roadmap (`tracked by the roadmap`). The ADR rule reads the plural, `#` and two-digit forms (`ADRs 0015`, `ADR #0018`, `ADR 23`). The jargon rule catches process commentary (`a later PR`, `this changeset`, `out of scope for this change`) but not `this spec`, `this session` or `this port`, which are reader words in a parser, and lets an image slice in its volume through. The repo-path rule catches the planning files a session writes (`CONTEXT.md`, `REQUIREMENTS.md`, `PITFALLS.md`), the meta-repo or umbrella repo named as a place, and its `work/` and `cards/` directories, and a `decisions/NNNN-` path wherever it sits. Measured on the pinned libraries, this adds 12 hits, all of them real (in hl7, x12 and ncpdp), and removes none. dicom's decision-number, issue-number, `WORD-N` and bare section rules stay dicom's: each collides with reference material another library publishes.
11
+ - d75579c: The internal-refs gate gains a source string-literal pass, `sourceStringLiterals: { enabled, paths, extraRules }`, off by default like the doc-comment pass. It reads the double-quoted, single-quoted and template literals in the tracked source under `paths`, decoded, plus the literals on each line joined, because a string literal reaches a consumer as error and warning text. It never reads a comment, so an item identifier in a `//` note or a `/* */` block stays allowed, and it skips regular-expression literals. In both source passes a NUL byte now reads as a space instead of refusing the file: a raw separator inside a literal is legitimate source, and a space keeps the word boundary beside it. The public-surface pass still refuses a NUL-bearing target.
12
+ - d30eca0: The PHI engine reads a JavaScript or TypeScript source as the document its string literals spell, as well as its bytes. A literal can spell a dashed identifier's separators as `-`, `\u{2D}` or `\x2D`, and the raw floor read straight past it. Each `.ts`, `.js`, `.mts`, `.cts`, `.mjs`, `.cjs`, `.tsx` and `.jsx` target is now also decoded (an escaped backslash first, so a doubled backslash stays text), and the floor and the caller's `detect` run over that view too, so a field-level detector can parse a record whose separators are escapes. The decoded view adds only what the raw view did not report, and says `escape-encoded in a source literal` on the hit. It is a view, not a parser, so it also decodes a `String.raw` template or a comment quoting an escape; an `ID <value>` declaration in the allow-list clears such a hit. `decodeSourceLiterals` and `isSourceLiteralContainer` are exported.
13
+
14
+ ## 0.1.5
15
+
16
+ ### Patch Changes
17
+
18
+ - 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`.
19
+
3
20
  ## 0.1.4
4
21
 
5
22
  ### Patch Changes
package/README.md CHANGED
@@ -271,6 +271,9 @@ npx -y -p @cosyte/script-utils@0.1 cosyte-readme # or one gate's own bin
271
271
 
272
272
  Each runs over the working directory, takes the arguments its gate takes, exits with the gate's own
273
273
  code, and prints what it does with `--help`. `script-utils` with no command prints the list and exits 2.
274
+ Under CI (`CI` set to anything but empty, `false` or `0`, as GitHub Actions sets it) `--help` and
275
+ `-h` print to stderr and exit non-zero (2, or 1 for the dash gate, whose only codes are 0 and 1): help
276
+ checks nothing, so a package script that ends in `--help` must not pass a required check.
274
277
 
275
278
  ### The em-dash gate, which is a file rather than a subpath
276
279
 
@@ -308,6 +311,16 @@ its backslash-u escape (plain, braced and eight-digit), its braced backslash-x e
308
311
  HTML entities, with or without the semicolon and with leading zeros. The default scan also reads
309
312
  every tracked file's NAME, because a name is copy too.
310
313
 
314
+ A changelog whose dated archive predates the generated part names that archive with a boundary line
315
+ in the same file, `CHANGELOG.md em-only below ## Released before this file was generated` or
316
+ `CHANGELOG.md unread below ## Released before this file was generated`. The text after `below` is a
317
+ whole line of that file, matched exactly; from it to the end of the file is the archive. `em-only`
318
+ reads the archive for the em dash alone, in every form, so an en dash in an old range passes and an em
319
+ dash still fails; `unread` does not read the archive. Above the line the file is read for both dashes
320
+ like any other, and its name is scanned as always. A boundary line the file no longer carries is
321
+ refused, so a lost heading fails closed, and a file cannot be both left out whole and given a
322
+ boundary.
323
+
311
324
  ### The README gate
312
325
 
313
326
  `@cosyte/script-utils/readme` checks a library's README against the house skeleton, and against a
@@ -362,18 +375,28 @@ record is generated from the package's own `package.json` (name, description, re
362
375
  engines, repository, docs and npm URLs) and carries its tier; the data ships as `catalog.json`
363
376
  beside the module, and `CATALOG` is that file, frozen.
364
377
 
365
- Each package.json description is that library's one canonical line (ST14), so every library runs
366
- the check that its description is the catalog's, with no file of its own:
378
+ Each package.json description is that library's one canonical line (ST14). Two sides check it, and
379
+ each checks one thing:
380
+
381
+ - **cosyte/config keeps the catalog fresh.** `node scripts/catalog.mjs check --npm` compares the
382
+ committed catalog with every library's latest release on npm. It runs nightly and before each
383
+ config release, and fails when a library has released a description, release line or engines
384
+ range the catalog does not carry; `write --npm` regenerates it. npm keeps 255 characters of a
385
+ description, so a longer one is fresh when npm holds its first 255, and the run notes it.
386
+ - **A library can check its own line, advisory.** With no file of its own:
367
387
 
368
388
  ```sh
369
389
  node node_modules/@cosyte/script-utils/catalog.js # the package in this directory
370
390
  node node_modules/@cosyte/script-utils/catalog.js --dir <path> # another package directory
391
+ node node_modules/@cosyte/script-utils/catalog.js --strict # a disagreement fails
371
392
  ```
372
393
 
373
- Exit 0 means the description is the catalog's, 1 that it is not (both values are printed), and 2
374
- that the check could not run (no package.json, or a package the catalog does not list). A changed
375
- description therefore goes through the catalog: regenerate it in cosyte/config
376
- (`node scripts/catalog.mjs write`), release this package, then move the library to that release.
394
+ It compares the package.json description with the catalog this release of `@cosyte/script-utils`
395
+ carries. A disagreement, or a package the catalog does not list, prints both values as a note and
396
+ exits 0: a library that changes its description is not held red until the next script-utils
397
+ release, because config's check against npm picks the change up once the library is released. Exit
398
+ 2 means the check could not run (no package.json, or an argument it does not accept). `--strict`
399
+ restores the gate: 1 for a disagreement, 2 for an unlisted package.
377
400
 
378
401
  ### The agent-notes checker
379
402
 
@@ -528,6 +551,13 @@ every `scanRoots` entry yielded a file the WALK read, naming the starved roots);
528
551
  cross-cutting detection floor that finds a dashed SSN shape and an email at an undeclared domain over
529
552
  every file, always.
530
553
 
554
+ **A JavaScript or TypeScript source is read twice**: as its bytes, and as the document its string
555
+ literals spell, with `\uXXXX`, `\u{...}`, `\xHH` and the single-character escapes decoded (an
556
+ escaped backslash first, so a doubled backslash before `u002D` stays text). The floor and your
557
+ `detect` run over both views, and the decoded view adds only what the raw view did not report, with
558
+ `, escape-encoded in a source literal` on its reason. It is a view, not a parser, so it also decodes a
559
+ `String.raw` template or a comment quoting an escape; an `ID <value>` declaration clears such a hit.
560
+
531
561
  **The RULES, local to your repo, never moved and never shipped in this package.**
532
562
  `scripts/phi-allow-list.txt` (the positive declaration that a token is synthetic, tagged `NAME`,
533
563
  `DOB`, `ADDR`, `ID`, `EMAILDOMAIN`); `phi-scan-overrides.md` (the `--allow-fixture` audit log, and
@@ -593,7 +623,7 @@ still refuses such an entry on both of its routes.
593
623
  `@cosyte/script-utils/internal-refs` keeps our own bookkeeping off every surface a consumer reads:
594
624
  item identifiers, phase and wave language, ADR numbers, internal repository paths and traceability
595
625
  markers, on the pages, the npm metadata and, optionally, the doc comments that compile into shipped
596
- type declarations.
626
+ type declarations and the string literals a consumer reads as error and warning text.
597
627
 
598
628
  ```ts
599
629
  import { runInternalRefsScan } from "@cosyte/script-utils/internal-refs";
@@ -605,6 +635,7 @@ process.exit(
605
635
  surfacePaths: ["README.md", "TRADEMARKS.md", "LICENSE", "docs-content"],
606
636
  accountedTarballFiles: ["CHANGELOG.md", "dist"],
607
637
  sourceDocComments: { enabled: true, paths: ["src/*.ts", "src/**/*.ts"] },
638
+ sourceStringLiterals: { enabled: true, paths: ["src/*.ts", "src/**/*.ts"] },
608
639
  }),
609
640
  );
610
641
  ```
@@ -632,14 +663,23 @@ gets wrong, and a default would be one repository's answer imposed on every othe
632
663
  | 3 Public surface | `surfacePaths` | **Required**, empty refused, every entry must be tracked. |
633
664
  | 4 Tarball accounting | `accountedTarballFiles` | **Required.** The `files` entries already accounted for. |
634
665
  | 5 Source doc comments | `sourceDocComments` | Defaulted OFF. Enable where the build copies doc text verbatim. |
635
- | 6 Exit codes | `exitCodes` | Defaulted `{ clean: 0, hits: 1, refuse: 1 }`. `clean` must stay 0. |
636
- | 7 Added detection | `extraRules` | Defaulted `[]`. Each added rule brings its own samples. |
637
- | 8 Added self-test material | `extraSamples` | Defaulted `{}`. Additive only. |
638
- | 9 Repository under test | `repoRoot` | Defaulted to the working directory, anchored at its git top level. |
639
- | 10 Report destination | `write` | Defaulted to the process streams. |
666
+ | 6 Source string literals | `sourceStringLiterals` | Defaulted OFF. Enable where messages reach a consumer. No comments. |
667
+ | 7 Exit codes | `exitCodes` | Defaulted `{ clean: 0, hits: 1, refuse: 1 }`. `clean` must stay 0. |
668
+ | 8 Added detection | `extraRules` | Defaulted `[]`. Each added rule brings its own samples. |
669
+ | 9 Added self-test material | `extraSamples` | Defaulted `{}`. Additive only. |
670
+ | 10 Repository under test | `repoRoot` | Defaulted to the working directory, anchored at its git top level. |
671
+ | 11 Report destination | `write` | Defaulted to the process streams. |
640
672
 
641
673
  `CONFIG_AXES` is the same table as data, so a consumer can enumerate it rather than transcribe it.
642
674
 
675
+ The string-literal pass reads double-quoted, single-quoted and template literals (a multi-line
676
+ template whole, a `${...}` as a space), decodes their escapes, and also reads the literals on one
677
+ source line joined, so a phrase split across `"a " + "b"` is seen. It never reads a comment: a `//`
678
+ line, a trailing `//` and a `/* */` block are skipped, so an item identifier in a maintainer's note
679
+ stays allowed. A regular-expression literal is skipped too. In both source passes a NUL byte reads as
680
+ a space instead of refusing the file, since a raw separator inside a literal is legitimate source;
681
+ the clean line counts them.
682
+
643
683
  #### Two things worth knowing before you adopt this one
644
684
 
645
685
  **The prefix set has to be extended by hand, and that is the cheaper mistake.** A new programme
@@ -2,9 +2,10 @@
2
2
  // cosyte-agent-notes: the agent-notes checker for the repository in the working directory.
3
3
  // A bin of @cosyte/script-utils (D56), so a repository with no package.json can run it with
4
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.
5
+ // the gate's own code; `--help` prints what it does and exits 0, or 2 under CI.
6
6
 
7
7
  import { runAgentNotesCheck } from "../agent-notes.js";
8
+ import { answerHelp } from "./help.js";
8
9
 
9
10
  const HELP = `cosyte-agent-notes: check a repository's agent notes and every pointer into them.
10
11
 
@@ -17,7 +18,7 @@ explicit anchor. Exit 0 clean, 1 a finding, 2 the check could not run.
17
18
 
18
19
  const args = process.argv.slice(2);
19
20
  if (args.includes("--help") || args.includes("-h")) {
20
- process.stdout.write(HELP);
21
+ process.exitCode = answerHelp("cosyte-agent-notes", HELP);
21
22
  } else {
22
23
  process.exitCode = runAgentNotesCheck({ argv: args });
23
24
  }
@@ -2,9 +2,10 @@
2
2
  // cosyte-attw: the attw publish gate for the package in the working directory.
3
3
  // A bin of @cosyte/script-utils (D56), so a repository with no package.json can run it with
4
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.
5
+ // the gate's own code; `--help` prints what it does and exits 0, or 2 under CI.
6
6
 
7
7
  import { runAttwCli } from "../attw.js";
8
+ import { answerHelp } from "./help.js";
8
9
 
9
10
  const HELP = `cosyte-attw: run the attw publish gate for the package in the working directory.
10
11
 
@@ -18,7 +19,7 @@ passed; attw's own status when attw judged the package; 1 a refusal.
18
19
 
19
20
  const args = process.argv.slice(2);
20
21
  if (args.includes("--help") || args.includes("-h")) {
21
- process.stdout.write(HELP);
22
+ process.exitCode = answerHelp("cosyte-attw", HELP);
22
23
  } else {
23
24
  process.exitCode = runAttwCli(args);
24
25
  }
@@ -2,22 +2,24 @@
2
2
  // cosyte-catalog: the catalog check for the library in the working directory.
3
3
  // A bin of @cosyte/script-utils (D56), so a repository with no package.json can run it with
4
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.
5
+ // the gate's own code; `--help` prints what it does and exits 0, or 2 under CI.
6
6
 
7
7
  import { runCatalogCheck } from "../catalog.js";
8
+ import { answerHelp } from "./help.js";
8
9
 
9
10
  const HELP = `cosyte-catalog: check that a library's package.json description is the catalog's.
10
11
 
11
- cosyte-catalog [--dir <package directory>]
12
+ cosyte-catalog [--dir <package directory>] [--strict]
12
13
 
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.
14
+ The catalog is the one list of our libraries, generated in cosyte/config, which keeps it fresh
15
+ against npm. This check is advisory: exit 0 when the description is the catalog's, and 0 with both
16
+ values printed when it is not or the package is unlisted; 2 when package.json cannot be read. With
17
+ --strict a disagreement exits 1 and an unlisted package 2.
16
18
  `;
17
19
 
18
20
  const args = process.argv.slice(2);
19
21
  if (args.includes("--help") || args.includes("-h")) {
20
- process.stdout.write(HELP);
22
+ process.exitCode = answerHelp("cosyte-catalog", HELP);
21
23
  } else {
22
24
  process.exitCode = runCatalogCheck({ argv: args });
23
25
  }
@@ -2,9 +2,10 @@
2
2
  // cosyte-claude-card: the CLAUDE.md card checker for the repository in the working directory.
3
3
  // A bin of @cosyte/script-utils (D56), so a repository with no package.json can run it with
4
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.
5
+ // the gate's own code; `--help` prints what it does and exits 0, or 2 under CI.
6
6
 
7
7
  import { runClaudeCardGate } from "../claude-card.js";
8
+ import { answerHelp } from "./help.js";
8
9
 
9
10
  const HELP = `cosyte-claude-card: check a repository's CLAUDE.md against the card spec.
10
11
 
@@ -17,7 +18,7 @@ Gate, Hazard and Rules in order, at most 60 lines of at most 100 characters and
17
18
 
18
19
  const args = process.argv.slice(2);
19
20
  if (args.includes("--help") || args.includes("-h")) {
20
- process.stdout.write(HELP);
21
+ process.exitCode = answerHelp("cosyte-claude-card", HELP);
21
22
  } else {
22
23
  process.exitCode = runClaudeCardGate({ argv: args });
23
24
  }
@@ -2,9 +2,10 @@
2
2
  // cosyte-internal-refs: the internal-reference gate over the repository in the working directory.
3
3
  // A bin of @cosyte/script-utils (D56), so a repository with no package.json can run it with
4
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.
5
+ // the gate's own code; `--help` prints what it does and exits 0, or 2 under CI.
6
6
 
7
7
  import { runInternalRefsCli } from "../internal-refs.js";
8
+ import { answerHelp } from "./help.js";
8
9
 
9
10
  const HELP = `cosyte-internal-refs: run the internal-reference gate over the repository in the working directory.
10
11
 
@@ -17,7 +18,7 @@ clean, 1 hits, 2 the gate could not run.
17
18
 
18
19
  const args = process.argv.slice(2);
19
20
  if (args.includes("--help") || args.includes("-h")) {
20
- process.stdout.write(HELP);
21
+ process.exitCode = answerHelp("cosyte-internal-refs", HELP);
21
22
  } else {
22
23
  process.exitCode = runInternalRefsCli(args);
23
24
  }
@@ -2,9 +2,10 @@
2
2
  // cosyte-phi-scan: the PHI commit-gate over the repository in the working directory.
3
3
  // A bin of @cosyte/script-utils (D56), so a repository with no package.json can run it with
4
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.
5
+ // the gate's own code; `--help` prints what it does and exits 0, or 2 under CI.
6
6
 
7
7
  import { runPhiScanCli } from "../phi-scan.js";
8
+ import { answerHelp } from "./help.js";
8
9
 
9
10
  const HELP = `cosyte-phi-scan: run the PHI commit-gate over the repository in the working directory.
10
11
 
@@ -20,7 +21,7 @@ values, and scripts/phi-scan.exclude names literal tracked paths to leave out. E
20
21
 
21
22
  const args = process.argv.slice(2);
22
23
  if (args.includes("--help") || args.includes("-h")) {
23
- process.stdout.write(HELP);
24
+ process.exitCode = answerHelp("cosyte-phi-scan", HELP);
24
25
  } else {
25
26
  process.exitCode = runPhiScanCli(args);
26
27
  }
@@ -2,9 +2,10 @@
2
2
  // cosyte-readme: the README gate for the library in the working directory.
3
3
  // A bin of @cosyte/script-utils (D56), so a repository with no package.json can run it with
4
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.
5
+ // the gate's own code; `--help` prints what it does and exits 0, or 2 under CI.
6
6
 
7
7
  import { runReadmeGate } from "../readme.js";
8
+ import { answerHelp } from "./help.js";
8
9
 
9
10
  const HELP = `cosyte-readme: check a library README against the house skeleton and npm's size limit.
10
11
 
@@ -16,7 +17,7 @@ names its line and rule), 2 the package could not be read.
16
17
 
17
18
  const args = process.argv.slice(2);
18
19
  if (args.includes("--help") || args.includes("-h")) {
19
- process.stdout.write(HELP);
20
+ process.exitCode = answerHelp("cosyte-readme", HELP);
20
21
  } else {
21
22
  process.exitCode = runReadmeGate({ argv: args });
22
23
  }
package/bin/help.js ADDED
@@ -0,0 +1,41 @@
1
+ // Not a bin: the one answer every gate bin gives to `--help` and `-h`, written once beside them.
2
+ //
3
+ // A gate asked for its help checks nothing. At a terminal that is what was asked for, so the help
4
+ // goes to stdout and the exit is 0. Under CI it is a trap: a package script that ends in `--help`
5
+ // passes a required check having checked nothing (`"attw": "cosyte-attw --help"` did exactly that).
6
+ // So when `CI` is set to anything but an empty string, `false` or `0`, which is how GitHub Actions
7
+ // and the other common runners announce themselves, the help goes to stderr, a refusal follows it,
8
+ // and the bin exits 2, the code every gate here uses for "did not run".
9
+
10
+ /**
11
+ * Whether this process runs under CI, read the way the common runners set it.
12
+ *
13
+ * @param {Record<string, string | undefined>} [env] The environment to read.
14
+ * @returns {boolean} True when `CI` is set to anything but an empty string, `false` or `0`.
15
+ */
16
+ export function underCi(env = process.env) {
17
+ const value = env.CI;
18
+ return value !== undefined && value !== "" && value !== "false" && value !== "0";
19
+ }
20
+
21
+ /**
22
+ * Answer `--help`: print the help and return 0, or under CI print it to stderr with a refusal and
23
+ * return 2.
24
+ *
25
+ * @param {string} name The bin's name, for the refusal line.
26
+ * @param {string} help The help text.
27
+ * @param {Record<string, string | undefined>} [env] The environment to read.
28
+ * @returns {number} The exit code.
29
+ */
30
+ export function answerHelp(name, help, env = process.env) {
31
+ if (!underCi(env)) {
32
+ process.stdout.write(help);
33
+ return 0;
34
+ }
35
+ process.stderr.write(help);
36
+ process.stderr.write(
37
+ `${name}: refusing --help under CI (CI=${env.CI}). It prints this text and checks nothing, ` +
38
+ "so a CI step that runs it must not pass. Run the gate without --help.\n",
39
+ );
40
+ return 2;
41
+ }
@@ -1,11 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  // script-utils: one entry for every gate in @cosyte/script-utils (D56). npx runs the bin named
3
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.
4
+ // from a repository with no package.json. `<command> --help` is the gate's own help; under CI
5
+ // every `--help` exits 2, because it checks nothing.
5
6
 
6
7
  import { spawnSync } from "node:child_process";
7
8
  import { fileURLToPath } from "node:url";
8
9
 
10
+ import { answerHelp } from "./help.js";
11
+
9
12
  const COMMANDS = {
10
13
  "no-emdash": ["cosyte-no-emdash", "the em and en dash gate, over tracked files and their names"],
11
14
  attw: ["cosyte-attw", "the attw publish gate, for the package here"],
@@ -31,9 +34,11 @@ ${Object.entries(COMMANDS)
31
34
  `;
32
35
 
33
36
  const [command, ...rest] = process.argv.slice(2);
34
- if (command === undefined || command === "--help" || command === "-h") {
37
+ if (command === undefined) {
35
38
  process.stdout.write(HELP);
36
- process.exitCode = command === undefined ? 2 : 0;
39
+ process.exitCode = 2;
40
+ } else if (command === "--help" || command === "-h") {
41
+ process.exitCode = answerHelp("script-utils", HELP);
37
42
  } else if (!(command in COMMANDS)) {
38
43
  process.stderr.write(`script-utils: ${JSON.stringify(command)} is not a command.\n\n${HELP}`);
39
44
  process.exitCode = 2;
package/catalog.d.ts CHANGED
@@ -99,12 +99,19 @@ export declare function compareWithCatalog(
99
99
  export interface CatalogCheckOptions {
100
100
  /** The package directory holding package.json. Default: the working directory. */
101
101
  dir?: string;
102
- /** CLI arguments; only `--dir <path>` is accepted. */
102
+ /** CLI arguments; `--dir <path>` and `--strict` are accepted. */
103
103
  argv?: string[];
104
+ /**
105
+ * Fail on a disagreement (1) or an unlisted package (2) instead of printing a note and exiting
106
+ * 0. Default: false, because keeping the catalog fresh is cosyte/config's job.
107
+ */
108
+ strict?: boolean;
104
109
  }
105
110
 
106
111
  /**
107
- * Run the check over one package directory and return the exit code: 0 the description is the
108
- * catalog's, 1 it is not, 2 the check could not run. It never exits the process.
112
+ * Run the check over one package directory and return the exit code. Advisory by default: 0 when
113
+ * the description is the catalog's, and 0 with a note when it is not or the package is unlisted.
114
+ * With `strict`, 1 for a disagreement and 2 for an unlisted package. 2 whenever the check could not
115
+ * run. It never exits the process.
109
116
  */
110
117
  export declare function runCatalogCheck(options?: CatalogCheckOptions): 0 | 1 | 2;
package/catalog.js CHANGED
@@ -8,16 +8,20 @@
8
8
  // A site, the org profile, a deck or a knowledgebase page reads that one list instead of keeping
9
9
  // its own roster.
10
10
  //
11
- // IT ALSO CARRIES THE CHECK EVERY LIBRARY RUNS. Each package.json description is that library's one
12
- // canonical line (ST14), so the description in a library's package.json must be the catalog's.
13
- // When a library changes its description, its check fails until the catalog is regenerated and a
14
- // script-utils release carries it: the catalog and the package cannot quietly disagree.
11
+ // IT ALSO CARRIES THE CHECK A LIBRARY CAN RUN, AND THAT CHECK IS ADVISORY. Each package.json
12
+ // description is that library's one canonical line (ST14). Keeping the catalog fresh is cosyte/config's
13
+ // job: `scripts/catalog.mjs check --npm` compares the catalog with every library's latest npm release,
14
+ // nightly and before each config release. So a library that changes its description is not held red
15
+ // until the next script-utils release: this check prints both values as a note and exits 0. `--strict`
16
+ // makes a disagreement exit 1 and an unlisted package exit 2, for a caller that wants the old gate.
15
17
  //
16
18
  // node node_modules/@cosyte/script-utils/catalog.js # the package in this directory
17
19
  // node node_modules/@cosyte/script-utils/catalog.js --dir <path> # another package directory
20
+ // node node_modules/@cosyte/script-utils/catalog.js --strict # a disagreement fails
18
21
  //
19
- // EXIT CODES, the contract every gate here keeps: 0 the description agrees, 1 it does not, 2 the
20
- // check could not run (no package.json, or a package the catalog does not list).
22
+ // EXIT CODES: 0 the description agrees, or (advisory) it does not and a note says so; 1 it does not,
23
+ // with --strict; 2 the check could not run (no package.json, a bad argument, or, with --strict, a
24
+ // package the catalog does not list).
21
25
  //
22
26
  // ZERO DEPENDENCIES AND NO BUILD STEP, like the rest of this package.
23
27
 
@@ -192,21 +196,29 @@ export function compareWithCatalog(manifest, catalog = CATALOG) {
192
196
  /**
193
197
  * Run the check over one package directory and return the exit code. It never exits the process.
194
198
  *
195
- * @param {{ dir?: string, argv?: string[] }} [options] `dir` is the package directory (default:
196
- * the working directory). `argv` is the CLI arguments, read only for `--dir <path>`.
197
- * @returns {0 | 1 | 2} 0 agree, 1 disagree, 2 the check could not run.
199
+ * ADVISORY BY DEFAULT: a disagreement or an unlisted package is printed as a note and exits 0,
200
+ * because keeping the catalog fresh is cosyte/config's job (`scripts/catalog.mjs check --npm`).
201
+ * `strict` (or `--strict`) restores the gate: 1 for a disagreement, 2 for an unlisted package.
202
+ *
203
+ * @param {{ dir?: string, argv?: string[], strict?: boolean }} [options] `dir` is the package
204
+ * directory (default: the working directory). `argv` is the CLI arguments, read for
205
+ * `--dir <path>` and `--strict`.
206
+ * @returns {0 | 1 | 2} 0 agree (or advisory), 1 disagree under strict, 2 the check could not run.
198
207
  */
199
208
  export function runCatalogCheck(options = {}) {
200
209
  let dir = options.dir ?? process.cwd();
210
+ let strict = options.strict === true;
201
211
  const argv = options.argv ?? [];
202
212
  for (let i = 0; i < argv.length; i += 1) {
203
213
  if (argv[i] === "--dir" && typeof argv[i + 1] === "string") {
204
214
  dir = argv[i + 1];
205
215
  i += 1;
216
+ } else if (argv[i] === "--strict") {
217
+ strict = true;
206
218
  } else if (argv[i] !== "--") {
207
219
  process.stderr.write(
208
220
  `catalog check: ${JSON.stringify(argv[i])} is not an argument this check accepts (only ` +
209
- `--dir <package directory>). Nothing was checked.\n`,
221
+ `--dir <package directory> and --strict). Nothing was checked.\n`,
210
222
  );
211
223
  return 2;
212
224
  }
@@ -223,8 +235,15 @@ export function runCatalogCheck(options = {}) {
223
235
  try {
224
236
  differences = compareWithCatalog(manifest);
225
237
  } catch (err) {
226
- process.stderr.write(`catalog check: ${err.message} Nothing was checked.\n`);
227
- return 2;
238
+ if (strict) {
239
+ process.stderr.write(`catalog check: ${err.message} Nothing was checked.\n`);
240
+ return 2;
241
+ }
242
+ process.stderr.write(
243
+ `catalog check: note: ${err.message} The catalog is cosyte/config's to keep fresh; this ` +
244
+ `check is advisory, so it exits 0.\n`,
245
+ );
246
+ return 0;
228
247
  }
229
248
  if (differences.length === 0) {
230
249
  process.stdout.write(`catalog check: ${manifest.name}'s description is the catalog's.\n`);
@@ -237,11 +256,21 @@ export function runCatalogCheck(options = {}) {
237
256
  ` catalog: ${JSON.stringify(d.catalog)}`,
238
257
  )
239
258
  .join("\n");
259
+ if (!strict) {
260
+ process.stderr.write(
261
+ `catalog check: note: ${manifest.name}'s package.json and the catalog this release of ` +
262
+ `@cosyte/script-utils carries disagree.\n${report}\n` +
263
+ `This is advisory and exits 0. Once this package is released with the new description, ` +
264
+ `cosyte/config's nightly check against npm (scripts/catalog.mjs check --npm) flags the ` +
265
+ `catalog, and the next script-utils release carries it.\n`,
266
+ );
267
+ return 0;
268
+ }
240
269
  process.stderr.write(
241
270
  `catalog check: ${manifest.name}'s package.json and the catalog disagree.\n${report}\n` +
242
- `The catalog is generated from package.json in cosyte/config (scripts/catalog.mjs) and ` +
243
- `ships in @cosyte/script-utils. Change both together: regenerate the catalog, release ` +
244
- `script-utils, then move this package to that release.\n`,
271
+ `The catalog is generated in cosyte/config (scripts/catalog.mjs) and ships in ` +
272
+ `@cosyte/script-utils. Regenerate it there, release script-utils, then move this package to ` +
273
+ `that release.\n`,
245
274
  );
246
275
  return 1;
247
276
  }
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",