@cosyte/script-utils 0.1.5 → 0.1.7
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 +18 -0
- package/README.md +86 -54
- package/agent-notes.d.ts +60 -9
- package/agent-notes.js +51 -14
- package/bin/cosyte-agent-notes.js +16 -6
- package/bin/cosyte-attw.js +3 -2
- package/bin/cosyte-catalog.js +8 -6
- package/bin/cosyte-claude-card.js +3 -2
- package/bin/cosyte-internal-refs.js +3 -2
- package/bin/cosyte-phi-scan.js +3 -2
- package/bin/cosyte-readme.js +3 -2
- package/bin/help.js +41 -0
- package/bin/script-utils.js +9 -4
- package/catalog.d.ts +10 -3
- package/catalog.js +44 -15
- package/check-no-emdash.sh +180 -15
- package/claude-card.js +11 -2
- package/internal-refs.d.ts +31 -0
- package/internal-refs.js +461 -14
- package/package.json +1 -1
- package/phi-scan.d.ts +11 -0
- package/phi-scan.js +174 -23
package/bin/cosyte-phi-scan.js
CHANGED
|
@@ -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.
|
|
24
|
+
process.exitCode = answerHelp("cosyte-phi-scan", HELP);
|
|
24
25
|
} else {
|
|
25
26
|
process.exitCode = runPhiScanCli(args);
|
|
26
27
|
}
|
package/bin/cosyte-readme.js
CHANGED
|
@@ -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.
|
|
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
|
+
}
|
package/bin/script-utils.js
CHANGED
|
@@ -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"],
|
|
@@ -16,7 +19,7 @@ const COMMANDS = {
|
|
|
16
19
|
],
|
|
17
20
|
readme: ["cosyte-readme", "the README gate, for the library here"],
|
|
18
21
|
"claude-card": ["cosyte-claude-card", "the CLAUDE.md card checker"],
|
|
19
|
-
"agent-notes": ["cosyte-agent-notes", "the agent-notes checker"],
|
|
22
|
+
"agent-notes": ["cosyte-agent-notes", "the agent-notes checker, deprecated by D62"],
|
|
20
23
|
catalog: ["cosyte-catalog", "the catalog check, for the library here"],
|
|
21
24
|
};
|
|
22
25
|
|
|
@@ -31,9 +34,11 @@ ${Object.entries(COMMANDS)
|
|
|
31
34
|
`;
|
|
32
35
|
|
|
33
36
|
const [command, ...rest] = process.argv.slice(2);
|
|
34
|
-
if (command === undefined
|
|
37
|
+
if (command === undefined) {
|
|
35
38
|
process.stdout.write(HELP);
|
|
36
|
-
process.exitCode =
|
|
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;
|
|
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
|
|
108
|
-
* catalog's,
|
|
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
|
|
12
|
-
// canonical line (ST14)
|
|
13
|
-
//
|
|
14
|
-
//
|
|
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
|
|
20
|
-
// check could not run (no package.json,
|
|
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
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
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
|
-
|
|
227
|
-
|
|
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
|
|
243
|
-
|
|
244
|
-
`
|
|
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/check-no-emdash.sh
CHANGED
|
@@ -118,6 +118,22 @@
|
|
|
118
118
|
# * A repo with no such file declares no exclusions.
|
|
119
119
|
# * The declaration file is itself tracked and is SCANNED LIKE ANY OTHER TRACKED
|
|
120
120
|
# FILE. It is not self-excluding.
|
|
121
|
+
# * A BOUNDARY is the one entry that names part of a file rather than a whole one:
|
|
122
|
+
#
|
|
123
|
+
# CHANGELOG.md em-only below ## Released before this file was generated
|
|
124
|
+
# CHANGELOG.md unread below ## Released before this file was generated
|
|
125
|
+
#
|
|
126
|
+
# It is for a changelog whose dated archive predates the generated part and is kept
|
|
127
|
+
# verbatim. The path is ONE EXACT TRACKED PATH (no `/` prefix entry, no spaces), and
|
|
128
|
+
# the text after `below ` is a WHOLE LINE of that file, matched exactly. From the
|
|
129
|
+
# first line equal to it to the end of the file is the archive: `em-only` reads the
|
|
130
|
+
# archive for the em dash alone, in every form, so an en dash in an old range passes
|
|
131
|
+
# and an em dash still reds; `unread` does not read the archive at all. Everything
|
|
132
|
+
# above the boundary line is read for both dashes like any other file, and the file's
|
|
133
|
+
# NAME is scanned as always. A boundary line the file does not contain is a stale
|
|
134
|
+
# entry and the run exits 1, so a lost heading fails closed instead of widening what
|
|
135
|
+
# is left out. A path may carry one boundary, and a path the declaration also leaves
|
|
136
|
+
# out whole is refused, since it cannot be both.
|
|
121
137
|
#
|
|
122
138
|
# AN ENTRY IS THE LINE VERBATIM: nothing is trimmed off either end, because a path is
|
|
123
139
|
# bytes and trimming would invent a second spelling for one entry. A line carrying
|
|
@@ -255,6 +271,14 @@ PATTERN='(?i)\x{2014}|\x{2013}|&[mn]dash(?![a-z])|�*821[12](?![0-9])|�*201[
|
|
|
255
271
|
PATTERN+='|%E2%80%9[34]|\\u(?:0000)?201[34]|\\u\{0*201[34]\}|\\x\{0*201[34]\}'
|
|
256
272
|
PATTERN+='|\\0{0,2}201[34](?![0-9a-f])|\\N\{E[MN] DASH\}'
|
|
257
273
|
|
|
274
|
+
# The em dash's half of PATTERN, form for form, for the archive a declared `em-only`
|
|
275
|
+
# boundary reads. The self-test below holds it to matching the em dash and NOT the en
|
|
276
|
+
# dash, and test/check-no-emdash.test.ts holds every em form to it, so it cannot drift
|
|
277
|
+
# from PATTERN without a red.
|
|
278
|
+
EM_PATTERN='(?i)\x{2014}|&mdash(?![a-z])|�*8212(?![0-9])|�*2014(?![0-9a-f])'
|
|
279
|
+
EM_PATTERN+='|%E2%80%94|\\u(?:0000)?2014|\\u\{0*2014\}|\\x\{0*2014\}'
|
|
280
|
+
EM_PATTERN+='|\\0{0,2}2014(?![0-9a-f])|\\N\{EM DASH\}'
|
|
281
|
+
|
|
258
282
|
# The declaration's path, relative to the top level of the repo under scan. Deliberately
|
|
259
283
|
# NOT relative to this file: in a consuming repo this file lives under `node_modules/`
|
|
260
284
|
# and the declaration is the consumer's own tracked file.
|
|
@@ -314,6 +338,25 @@ if [ "$#" -gt 0 ] && [ "$1" = '--' ]; then
|
|
|
314
338
|
shift
|
|
315
339
|
fi
|
|
316
340
|
|
|
341
|
+
usage() {
|
|
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. A changelog archive is named by a boundary,
|
|
353
|
+
`PATH em-only below LINE` or `PATH unread below LINE`: from the first line of PATH equal to LINE to
|
|
354
|
+
the end, em-only reads for the em dash alone and unread reads nothing; above it, both dashes as
|
|
355
|
+
usual. Exit 0 means the mode completed and found nothing banned; exit 1 means anything else. Under
|
|
356
|
+
CI, --help exits 1: it checks nothing.
|
|
357
|
+
USAGE
|
|
358
|
+
}
|
|
359
|
+
|
|
317
360
|
case "${1:-}" in
|
|
318
361
|
'')
|
|
319
362
|
MODE=files
|
|
@@ -339,19 +382,17 @@ case "${1:-}" in
|
|
|
339
382
|
fi
|
|
340
383
|
;;
|
|
341
384
|
--help | -h)
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
nothing banned; exit 1 means anything else.
|
|
354
|
-
USAGE
|
|
385
|
+
# Under CI the help is a refusal: it prints this text and checks nothing, so a CI step that
|
|
386
|
+
# runs it must not pass. `CI` set to anything but empty, `false` or `0` is how GitHub Actions
|
|
387
|
+
# and the other common runners announce themselves.
|
|
388
|
+
if [ -n "${CI:-}" ] && [ "${CI}" != false ] && [ "${CI}" != 0 ]; then
|
|
389
|
+
usage >&2
|
|
390
|
+
echo "ERROR: check-no-emdash - refusing --help under CI (CI=${CI}). It prints this text and" >&2
|
|
391
|
+
echo " checks nothing, so a CI step that runs it must not pass. Run the gate without" >&2
|
|
392
|
+
echo " --help." >&2
|
|
393
|
+
exit 1
|
|
394
|
+
fi
|
|
395
|
+
usage
|
|
355
396
|
exit 0
|
|
356
397
|
;;
|
|
357
398
|
*)
|
|
@@ -557,6 +598,13 @@ entry_matches_a_tracked_path() {
|
|
|
557
598
|
|
|
558
599
|
# ---- the exclusion declaration ---------------------------------------------------
|
|
559
600
|
EXCLUDES=()
|
|
601
|
+
# A declared boundary, one index per entry: the path, `em-only` or `unread`, the boundary
|
|
602
|
+
# line as written, and the number of the first line of the file equal to it.
|
|
603
|
+
BOUND_PATH=()
|
|
604
|
+
BOUND_MODE=()
|
|
605
|
+
BOUND_TEXT=()
|
|
606
|
+
BOUND_LINE=()
|
|
607
|
+
BOUNDARY_SHAPE='^([^ ]+) (em-only|unread) below (.+)$'
|
|
560
608
|
refuse_declaration() {
|
|
561
609
|
local line="$1" entry="$2" what="$3"
|
|
562
610
|
echo "ERROR: check-no-emdash - unusable entry in ${DECLARATION}, line ${line}: ${entry}" >&2
|
|
@@ -589,6 +637,45 @@ if is_tracked "$DECLARATION"; then
|
|
|
589
637
|
# The entry is the line VERBATIM. See the header: nothing is trimmed, because a path
|
|
590
638
|
# is bytes. The anchor rules are the whole of what makes an entry usable on its face.
|
|
591
639
|
entry=$line
|
|
640
|
+
|
|
641
|
+
# A BOUNDARY names part of one file. It is recognised by its shape and validated on its
|
|
642
|
+
# own terms, so nothing below it can mistake it for a path with spaces in it.
|
|
643
|
+
if [[ $entry =~ $BOUNDARY_SHAPE ]]; then
|
|
644
|
+
b_path=${BASH_REMATCH[1]}
|
|
645
|
+
b_mode=${BASH_REMATCH[2]}
|
|
646
|
+
b_text=${BASH_REMATCH[3]}
|
|
647
|
+
case $b_path in
|
|
648
|
+
/* | ./* | *..* | */)
|
|
649
|
+
refuse_declaration "$lineno" "$entry" \
|
|
650
|
+
"A boundary names ONE tracked file, anchored at the repository root: not absolute, not ./, no .., and not a directory."
|
|
651
|
+
;;
|
|
652
|
+
esac
|
|
653
|
+
if ! is_tracked "$b_path"; then
|
|
654
|
+
refuse_declaration "$lineno" "$entry" \
|
|
655
|
+
"No tracked file is spelled ${b_path}. A boundary that has outlived its file is a hole nobody is looking at."
|
|
656
|
+
fi
|
|
657
|
+
for i in "${!BOUND_PATH[@]}"; do
|
|
658
|
+
if [ "${BOUND_PATH[$i]}" = "$b_path" ]; then
|
|
659
|
+
refuse_declaration "$lineno" "$entry" \
|
|
660
|
+
"${b_path} already carries a boundary. A file has one archive, so it has one boundary."
|
|
661
|
+
fi
|
|
662
|
+
done
|
|
663
|
+
if [ ! -f "$b_path" ] || [ ! -r "$b_path" ]; then
|
|
664
|
+
refuse_declaration "$lineno" "$entry" \
|
|
665
|
+
"${b_path} is not a readable regular file, so its boundary line cannot be looked for."
|
|
666
|
+
fi
|
|
667
|
+
b_line=$(grep -n -m1 -xF -e "$b_text" -- "$b_path" 2> /dev/null || true)
|
|
668
|
+
b_line=${b_line%%:*}
|
|
669
|
+
if [ -z "$b_line" ]; then
|
|
670
|
+
refuse_declaration "$lineno" "$entry" \
|
|
671
|
+
"${b_path} has no line that is exactly: ${b_text}. A lost boundary fails closed rather than leaving out more than was declared: restore the line, or correct or delete the entry."
|
|
672
|
+
fi
|
|
673
|
+
BOUND_PATH+=("$b_path")
|
|
674
|
+
BOUND_MODE+=("$b_mode")
|
|
675
|
+
BOUND_TEXT+=("$b_text")
|
|
676
|
+
BOUND_LINE+=("$b_line")
|
|
677
|
+
continue
|
|
678
|
+
fi
|
|
592
679
|
case $entry in
|
|
593
680
|
/*)
|
|
594
681
|
refuse_declaration "$lineno" "$entry" \
|
|
@@ -648,6 +735,30 @@ is_excluded() {
|
|
|
648
735
|
return 1
|
|
649
736
|
}
|
|
650
737
|
|
|
738
|
+
# A path is either left out whole or read up to its boundary. Both at once would leave the
|
|
739
|
+
# reader unable to say which one the gate honoured, so it is refused, whichever line came
|
|
740
|
+
# first.
|
|
741
|
+
for i in "${!BOUND_PATH[@]}"; do
|
|
742
|
+
if is_excluded "${BOUND_PATH[$i]}"; then
|
|
743
|
+
echo "ERROR: check-no-emdash - ${DECLARATION} both leaves out ${BOUND_PATH[$i]} and" >&2
|
|
744
|
+
echo " declares a boundary in it. A file is either left out whole or read up to its" >&2
|
|
745
|
+
echo " boundary; delete one of the two entries." >&2
|
|
746
|
+
exit 1
|
|
747
|
+
fi
|
|
748
|
+
done
|
|
749
|
+
|
|
750
|
+
# The index of the boundary declared for a path, or nothing.
|
|
751
|
+
boundary_index() {
|
|
752
|
+
local p="$1" i
|
|
753
|
+
for i in "${!BOUND_PATH[@]}"; do
|
|
754
|
+
if [ "${BOUND_PATH[$i]}" = "$p" ]; then
|
|
755
|
+
printf '%s' "$i"
|
|
756
|
+
return 0
|
|
757
|
+
fi
|
|
758
|
+
done
|
|
759
|
+
return 1
|
|
760
|
+
}
|
|
761
|
+
|
|
651
762
|
# ---- --self-id: which bytes am I ------------------------------------------------
|
|
652
763
|
#
|
|
653
764
|
# Answered before the scanner assertions, because this mode reads no scanned content and
|
|
@@ -736,6 +847,20 @@ if [ "$MODE" = files ] || [ "$MODE" = stdin ]; then
|
|
|
736
847
|
echo " Refusing to report findings from a scanner that flags what the rule allows." >&2
|
|
737
848
|
exit 1
|
|
738
849
|
fi
|
|
850
|
+
# The archive reading a declared `em-only` boundary uses: it must still see the em dash,
|
|
851
|
+
# and it must NOT see the en dash, or `em-only` would mean something other than it says.
|
|
852
|
+
if [ "${#BOUND_PATH[@]}" -gt 0 ]; then
|
|
853
|
+
if ! printf 'a\xe2\x80\x94b\n' | grep -qP "$EM_PATTERN"; then
|
|
854
|
+
echo "ERROR: check-no-emdash - the archive scanner cannot match a known em dash." >&2
|
|
855
|
+
echo " Refusing to read a declared boundary's archive with a scanner that cannot see." >&2
|
|
856
|
+
exit 1
|
|
857
|
+
fi
|
|
858
|
+
if printf 'a\xe2\x80\x93b\n' | grep -qP "$EM_PATTERN"; then
|
|
859
|
+
echo "ERROR: check-no-emdash - the archive scanner matched an en dash, so it is not reading" >&2
|
|
860
|
+
echo " for the em dash alone. Refusing to report what an em-only boundary allows." >&2
|
|
861
|
+
exit 1
|
|
862
|
+
fi
|
|
863
|
+
fi
|
|
739
864
|
|
|
740
865
|
# SCANNER VISIBILITY PROBE: prove the scanner can still READ a file before believing a
|
|
741
866
|
# word it says about one. The self-test above asserts what the tool MATCHES; this
|
|
@@ -901,12 +1026,20 @@ for i in "${!TRACKED[@]}"; do
|
|
|
901
1026
|
exit 1
|
|
902
1027
|
fi
|
|
903
1028
|
|
|
1029
|
+
# A file with a declared boundary is read on its own below, in two halves. It is still
|
|
1030
|
+
# READ, so it is listed for --list-scanned and counted as scanned.
|
|
1031
|
+
if boundary_index "$f" > /dev/null; then
|
|
1032
|
+
printf '%s\0' "$f" >> "$PLAINLIST"
|
|
1033
|
+
scanned=$((scanned + 1))
|
|
1034
|
+
continue
|
|
1035
|
+
fi
|
|
1036
|
+
|
|
904
1037
|
printf './%s\0' "$f" >> "$SCANLIST"
|
|
905
1038
|
printf '%s\0' "$f" >> "$PLAINLIST"
|
|
906
1039
|
scanned=$((scanned + 1))
|
|
907
1040
|
done
|
|
908
1041
|
|
|
909
|
-
if [ ! -s "$SCANLIST" ]; then
|
|
1042
|
+
if [ ! -s "$SCANLIST" ] && [ "${#BOUND_PATH[@]}" -eq 0 ]; then
|
|
910
1043
|
echo "ERROR: check-no-emdash - no tracked files left to scan. Refusing to report" >&2
|
|
911
1044
|
echo " green from a scan that read nothing." >&2
|
|
912
1045
|
exit 1
|
|
@@ -932,6 +1065,38 @@ HITS=$(xargs -0 -r grep -H -nP -e "$PATTERN" -- < "$SCANLIST" 2>> "$ERRLOG" || t
|
|
|
932
1065
|
|
|
933
1066
|
refuse_if_incomplete
|
|
934
1067
|
|
|
1068
|
+
# The files with a declared boundary. Every hit is found with the full pattern first, so
|
|
1069
|
+
# the line numbers are the file's own; a hit above the boundary line stands, and a hit at
|
|
1070
|
+
# or below it stands only for `em-only` and only when the line holds an em dash.
|
|
1071
|
+
bounded_note=''
|
|
1072
|
+
for i in "${!BOUND_PATH[@]}"; do
|
|
1073
|
+
b_path=${BOUND_PATH[$i]}
|
|
1074
|
+
b_line=${BOUND_LINE[$i]}
|
|
1075
|
+
b_mode=${BOUND_MODE[$i]}
|
|
1076
|
+
b_hits=$(grep -H -nP -e "$PATTERN" -- "./${b_path}" 2>> "$ERRLOG" || true)
|
|
1077
|
+
refuse_if_incomplete
|
|
1078
|
+
while IFS= read -r hit; do
|
|
1079
|
+
[ -n "$hit" ] || continue
|
|
1080
|
+
n=${hit#"./${b_path}:"}
|
|
1081
|
+
n=${n%%:*}
|
|
1082
|
+
keep=0
|
|
1083
|
+
if [ "$n" -lt "$b_line" ]; then
|
|
1084
|
+
keep=1
|
|
1085
|
+
elif [ "$b_mode" = em-only ] && printf '%s\n' "${hit#"./${b_path}:${n}:"}" | grep -qP -e "$EM_PATTERN"; then
|
|
1086
|
+
keep=1
|
|
1087
|
+
fi
|
|
1088
|
+
if [ "$keep" -eq 1 ]; then
|
|
1089
|
+
HITS="${HITS:+${HITS}
|
|
1090
|
+
}${hit}"
|
|
1091
|
+
fi
|
|
1092
|
+
done <<< "$b_hits"
|
|
1093
|
+
if [ "$b_mode" = em-only ]; then
|
|
1094
|
+
bounded_note="${bounded_note}, ${b_path} from line ${b_line} read for the em dash only"
|
|
1095
|
+
else
|
|
1096
|
+
bounded_note="${bounded_note}, ${b_path} from line ${b_line} not read"
|
|
1097
|
+
fi
|
|
1098
|
+
done
|
|
1099
|
+
|
|
935
1100
|
if [ -n "$NAME_HITS" ] && [ -n "$HITS" ]; then
|
|
936
1101
|
fail_with_hits "the tracked file names and the tracked files listed above" \
|
|
937
1102
|
"$NAME_HITS"$'\n'"$HITS"
|
|
@@ -941,4 +1106,4 @@ fi
|
|
|
941
1106
|
|
|
942
1107
|
echo "check-no-emdash: OK (no em or en dashes in ${scanned} of ${#TRACKED[@]} tracked file(s)" \
|
|
943
1108
|
"or their names; ${gitlinks} gitlink(s) skipped, ${self_excluded} excluded as the" \
|
|
944
|
-
"implementation under scan, ${declared_excluded} excluded by ${DECLARATION})"
|
|
1109
|
+
"implementation under scan, ${declared_excluded} excluded by ${DECLARATION}${bounded_note})"
|
package/claude-card.js
CHANGED
|
@@ -49,6 +49,14 @@ export const CARD_SECTIONS = Object.freeze(["Gate", "Hazard", "Rules"]);
|
|
|
49
49
|
/** The line that imports AGENTS.md into the card. */
|
|
50
50
|
export const AGENTS_IMPORT = "@AGENTS.md";
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Where what does not fit on the card goes, from the spec's "Where the detail goes". The card stands
|
|
54
|
+
* on its own: there is no notes file beside it to send detail to (D62).
|
|
55
|
+
*/
|
|
56
|
+
const DETAIL_GOES =
|
|
57
|
+
"where it is used: a test, a comment at the code it guards, an ADR, the README or docs, or a " +
|
|
58
|
+
"runbook";
|
|
59
|
+
|
|
52
60
|
/** Every rule id a finding can carry, with what it requires. */
|
|
53
61
|
export const CARD_RULES = Object.freeze({
|
|
54
62
|
title: "The card opens with `# <name>`, after a YAML front matter block if it has one.",
|
|
@@ -140,7 +148,8 @@ export function checkClaudeCard(text, context = {}) {
|
|
|
140
148
|
"bytes",
|
|
141
149
|
0,
|
|
142
150
|
`the card is ${bytes.toLocaleString("en-US")} bytes; the limit is ` +
|
|
143
|
-
`${CARD_MAX_BYTES.toLocaleString("en-US")}. Move detail to
|
|
151
|
+
`${CARD_MAX_BYTES.toLocaleString("en-US")}. Move detail to ${DETAIL_GOES}; git and the ` +
|
|
152
|
+
`PRs keep the history.`,
|
|
144
153
|
);
|
|
145
154
|
}
|
|
146
155
|
lines.forEach((line, index) => {
|
|
@@ -185,7 +194,7 @@ export function checkClaudeCard(text, context = {}) {
|
|
|
185
194
|
"section-extra",
|
|
186
195
|
heading.line + 1,
|
|
187
196
|
`"## ${heading.text}" is not one of the card's sections (${CARD_SECTIONS.join(", ")}); ` +
|
|
188
|
-
`make it a ### inside one of them, or move it to
|
|
197
|
+
`make it a ### inside one of them, or move it to ${DETAIL_GOES}.`,
|
|
189
198
|
);
|
|
190
199
|
} else if (!found.has(word)) {
|
|
191
200
|
found.set(word, heading);
|
package/internal-refs.d.ts
CHANGED
|
@@ -68,6 +68,16 @@ export interface SourceDocCommentsAxis {
|
|
|
68
68
|
extraRules?: ExtraRule[];
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/** Whether the source string-literal pass runs, over what, and with which extra rules. */
|
|
72
|
+
export interface SourceStringLiteralsAxis {
|
|
73
|
+
/** @default false */
|
|
74
|
+
enabled: boolean;
|
|
75
|
+
/** Tracked pathspecs holding the source whose string literals reach a consumer as messages. */
|
|
76
|
+
paths?: string[];
|
|
77
|
+
/** Detection that applies to string literals only. */
|
|
78
|
+
extraRules?: ExtraRule[];
|
|
79
|
+
}
|
|
80
|
+
|
|
71
81
|
export interface InternalRefsConfig {
|
|
72
82
|
/**
|
|
73
83
|
* AXIS 1, REQUIRED. The project and programme prefixes an identifier is keyed on, and an empty
|
|
@@ -113,6 +123,15 @@ export interface InternalRefsConfig {
|
|
|
113
123
|
*/
|
|
114
124
|
sourceDocComments?: SourceDocCommentsAxis;
|
|
115
125
|
|
|
126
|
+
/**
|
|
127
|
+
* DEFAULTED OFF. The source string-literal pass: double-quoted, single-quoted and template
|
|
128
|
+
* literals, decoded, with every comment skipped, because a string literal reaches a consumer as
|
|
129
|
+
* error and warning text and a comment does not. A NUL byte in source reads as a space.
|
|
130
|
+
*
|
|
131
|
+
* @default { enabled: false }
|
|
132
|
+
*/
|
|
133
|
+
sourceStringLiterals?: SourceStringLiteralsAxis;
|
|
134
|
+
|
|
116
135
|
/**
|
|
117
136
|
* AXIS 6, DEFAULTED. `clean` must be 0 and the other two must not be, which is checked: a mapping
|
|
118
137
|
* that put `hits` on 0 would turn every finding into a green run.
|
|
@@ -207,6 +226,18 @@ export declare function runSelfTests(rules: CompiledRule[]): string[];
|
|
|
207
226
|
/** Join a document the way markdown renders it, so a violation that straddles a wrap is visible. */
|
|
208
227
|
export declare function reflowParagraphs(text: string): string[];
|
|
209
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Extract the string literals of one JavaScript or TypeScript source file, decoded, with every
|
|
231
|
+
* comment skipped: one entry per literal, and one per source line holding two or more of them.
|
|
232
|
+
*/
|
|
233
|
+
export declare function extractStringLiterals(
|
|
234
|
+
text: string,
|
|
235
|
+
file: string,
|
|
236
|
+
): {
|
|
237
|
+
literals: { text: string; where: string }[];
|
|
238
|
+
lines: { text: string; where: string }[];
|
|
239
|
+
};
|
|
240
|
+
|
|
210
241
|
/** Extract doc-comment text from one source file, with each line's location beside it. */
|
|
211
242
|
export declare function extractDocComments(
|
|
212
243
|
text: string,
|