@cosyte/script-utils 0.1.8 → 0.1.9
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 +9 -0
- package/README.md +99 -3
- package/bin/cosyte-docs-content.js +31 -0
- package/bin/cosyte-phi-scan.js +7 -2
- package/bin/cosyte-ratchet.js +27 -0
- package/bin/script-utils.js +2 -0
- package/changes.d.ts +78 -0
- package/changes.js +269 -0
- package/docs-content.d.ts +182 -0
- package/docs-content.js +833 -0
- package/package.json +28 -5
- package/phi-scan.d.ts +4 -1
- package/phi-scan.js +86 -8
- package/ratchet.d.ts +127 -0
- package/ratchet.js +696 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosyte/script-utils",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Zero-dependency helpers for the repo-local gate scripts in @cosyte/* repos.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"phi",
|
|
11
11
|
"readme",
|
|
12
12
|
"scripts",
|
|
13
|
-
"cosyte"
|
|
13
|
+
"cosyte",
|
|
14
|
+
"coverage",
|
|
15
|
+
"mutation-testing"
|
|
14
16
|
],
|
|
15
17
|
"homepage": "https://github.com/cosyte/config/tree/main/packages/script-utils#readme",
|
|
16
18
|
"repository": {
|
|
@@ -31,7 +33,9 @@
|
|
|
31
33
|
"cosyte-readme": "bin/cosyte-readme.js",
|
|
32
34
|
"cosyte-claude-card": "bin/cosyte-claude-card.js",
|
|
33
35
|
"cosyte-agent-notes": "bin/cosyte-agent-notes.js",
|
|
34
|
-
"cosyte-catalog": "bin/cosyte-catalog.js"
|
|
36
|
+
"cosyte-catalog": "bin/cosyte-catalog.js",
|
|
37
|
+
"cosyte-ratchet": "bin/cosyte-ratchet.js",
|
|
38
|
+
"cosyte-docs-content": "bin/cosyte-docs-content.js"
|
|
35
39
|
},
|
|
36
40
|
"types": "index.d.ts",
|
|
37
41
|
"exports": {
|
|
@@ -66,6 +70,18 @@
|
|
|
66
70
|
"./agent-notes": {
|
|
67
71
|
"types": "./agent-notes.d.ts",
|
|
68
72
|
"default": "./agent-notes.js"
|
|
73
|
+
},
|
|
74
|
+
"./ratchet": {
|
|
75
|
+
"types": "./ratchet.d.ts",
|
|
76
|
+
"default": "./ratchet.js"
|
|
77
|
+
},
|
|
78
|
+
"./changes": {
|
|
79
|
+
"types": "./changes.d.ts",
|
|
80
|
+
"default": "./changes.js"
|
|
81
|
+
},
|
|
82
|
+
"./docs-content": {
|
|
83
|
+
"types": "./docs-content.d.ts",
|
|
84
|
+
"default": "./docs-content.js"
|
|
69
85
|
}
|
|
70
86
|
},
|
|
71
87
|
"files": [
|
|
@@ -86,6 +102,12 @@
|
|
|
86
102
|
"catalog.json",
|
|
87
103
|
"agent-notes.js",
|
|
88
104
|
"agent-notes.d.ts",
|
|
105
|
+
"ratchet.js",
|
|
106
|
+
"ratchet.d.ts",
|
|
107
|
+
"changes.js",
|
|
108
|
+
"changes.d.ts",
|
|
109
|
+
"docs-content.js",
|
|
110
|
+
"docs-content.d.ts",
|
|
89
111
|
"check-no-emdash.sh",
|
|
90
112
|
"bin",
|
|
91
113
|
"README.md",
|
|
@@ -98,10 +120,11 @@
|
|
|
98
120
|
"access": "public"
|
|
99
121
|
},
|
|
100
122
|
"devDependencies": {
|
|
101
|
-
"vitest": "
|
|
123
|
+
"vitest": "5.1.0",
|
|
124
|
+
"@vitest/coverage-v8": "5.1.0"
|
|
102
125
|
},
|
|
103
126
|
"scripts": {
|
|
104
127
|
"build": "node ../../scripts/pack-emdash-gate.mjs",
|
|
105
|
-
"test": "vitest run"
|
|
128
|
+
"test": "vitest run --coverage"
|
|
106
129
|
}
|
|
107
130
|
}
|
package/phi-scan.d.ts
CHANGED
|
@@ -293,7 +293,10 @@ export declare function readPhiExclusions(
|
|
|
293
293
|
* 0 clean, 1 hits, 2 refuse, the whole repository as the scan root, the `--staged` route reading
|
|
294
294
|
* everything but Markdown, and `scripts/phi-scan.exclude`. The cross-cutting floor only: a
|
|
295
295
|
* repository that needs field-level detectors keeps a caller passing `detect`.
|
|
296
|
-
* `
|
|
296
|
+
* `--allow-list <path>` names the allow list to use instead of `scripts/phi-allow-list.txt`
|
|
297
|
+
* (resolved against the working directory, replacing the repository's list); a path that does not
|
|
298
|
+
* exist or cannot be read refuses at 2.
|
|
299
|
+
* `node node_modules/@cosyte/script-utils/phi-scan.js [--allow-list <path>] [--staged | <path>...]`.
|
|
297
300
|
*/
|
|
298
301
|
export declare function runPhiScanCli(argv: string[]): number;
|
|
299
302
|
|
package/phi-scan.js
CHANGED
|
@@ -2102,12 +2102,13 @@ function gitModeKind(mode) {
|
|
|
2102
2102
|
|
|
2103
2103
|
// ---- The command-line form (ST34) ---------------------------------------------------------------
|
|
2104
2104
|
//
|
|
2105
|
-
// `node node_modules/@cosyte/script-utils/phi-scan.js [--staged | <path>...]`
|
|
2106
|
-
// the repository in the working directory with the house settings and no
|
|
2107
|
-
// 0 clean, 1 hits, 2 refuse; the whole repository as the one scan root;
|
|
2108
|
-
// everything but Markdown (`exemptsMarkdown`); the allow-list at
|
|
2109
|
-
// override log at `phi-scan-overrides.md`, the engine's
|
|
2110
|
-
// out, in `scripts/phi-scan.exclude`.
|
|
2105
|
+
// `node node_modules/@cosyte/script-utils/phi-scan.js [--allow-list <path>] [--staged | <path>...]`
|
|
2106
|
+
// runs this engine over the repository in the working directory with the house settings and no
|
|
2107
|
+
// caller script: exit codes 0 clean, 1 hits, 2 refuse; the whole repository as the one scan root;
|
|
2108
|
+
// the `--staged` route reading everything but Markdown (`exemptsMarkdown`); the allow-list at
|
|
2109
|
+
// `scripts/phi-allow-list.txt` and the override log at `phi-scan-overrides.md`, the engine's
|
|
2110
|
+
// defaults; and what the repository leaves out, in `scripts/phi-scan.exclude`. `--allow-list
|
|
2111
|
+
// <path>` names another allow list in place of the repository's (see `runPhiScanCli`).
|
|
2111
2112
|
//
|
|
2112
2113
|
// AN EXCLUSION IS ONE LITERAL TRACKED PATH, as `excludedPaths` requires: no pattern, no directory,
|
|
2113
2114
|
// because a class quietly grows new members. One per line; a line whose first character is `#` is a
|
|
@@ -2150,14 +2151,90 @@ export function readPhiExclusions(text, tracked) {
|
|
|
2150
2151
|
return { paths, problems };
|
|
2151
2152
|
}
|
|
2152
2153
|
|
|
2154
|
+
/**
|
|
2155
|
+
* Take `--allow-list <path>` out of the command line, leaving every other argument in its place.
|
|
2156
|
+
*
|
|
2157
|
+
* IT READS THE ENGINE'S OWN GRAMMAR, NOT A SIMPLER ONE, so it cannot take an argument the engine
|
|
2158
|
+
* would have read as something else: everything after `--` is a path, and the word after
|
|
2159
|
+
* `--allow-fixture` is that bypass's path, even when it reads `--allow-list`.
|
|
2160
|
+
*
|
|
2161
|
+
* @param {string[]} argv
|
|
2162
|
+
* @returns {{ argv: string[], allowList: string | undefined, problem: string | undefined }}
|
|
2163
|
+
*/
|
|
2164
|
+
function takeAllowListOption(argv) {
|
|
2165
|
+
/** @type {string[]} */
|
|
2166
|
+
const rest = [];
|
|
2167
|
+
/** @type {string | undefined} */
|
|
2168
|
+
let allowList;
|
|
2169
|
+
let i = 0;
|
|
2170
|
+
while (i < argv.length) {
|
|
2171
|
+
const a = argv[i];
|
|
2172
|
+
if (a === "--") {
|
|
2173
|
+
rest.push(...argv.slice(i));
|
|
2174
|
+
break;
|
|
2175
|
+
}
|
|
2176
|
+
if (a === "--allow-fixture") {
|
|
2177
|
+
rest.push(...argv.slice(i, i + 2));
|
|
2178
|
+
i += 2;
|
|
2179
|
+
continue;
|
|
2180
|
+
}
|
|
2181
|
+
if (a === "--allow-list") {
|
|
2182
|
+
const value = argv[i + 1];
|
|
2183
|
+
if (value === undefined || value === "") {
|
|
2184
|
+
return { argv, allowList: undefined, problem: "--allow-list requires a path argument" };
|
|
2185
|
+
}
|
|
2186
|
+
if (allowList !== undefined) {
|
|
2187
|
+
return { argv, allowList: undefined, problem: "--allow-list was given more than once" };
|
|
2188
|
+
}
|
|
2189
|
+
allowList = value;
|
|
2190
|
+
i += 2;
|
|
2191
|
+
continue;
|
|
2192
|
+
}
|
|
2193
|
+
if (a !== undefined) rest.push(a);
|
|
2194
|
+
i += 1;
|
|
2195
|
+
}
|
|
2196
|
+
return { argv: rest, allowList, problem: undefined };
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2153
2199
|
/**
|
|
2154
2200
|
* Run the engine from the command line over the repository in the working directory.
|
|
2155
2201
|
*
|
|
2156
|
-
*
|
|
2202
|
+
* `--allow-list <path>` NAMES THE ALLOW LIST INSTEAD OF `scripts/phi-allow-list.txt`, resolved
|
|
2203
|
+
* against the working directory. It is what a tool scanning a repository that keeps no list of its
|
|
2204
|
+
* own passes, usually an empty file: the run is then a scan with no exceptions, where without the
|
|
2205
|
+
* option it refuses for the missing list. The named file REPLACES the repository's list rather than
|
|
2206
|
+
* adding to it. A path that does not exist, or that exists and cannot be read (a directory, no read
|
|
2207
|
+
* permission), refuses at exit 2 before anything is scanned and says which: the engine itself would
|
|
2208
|
+
* let an unreadable list escape to node's own exit 1, the code reserved for hits. Without the option
|
|
2209
|
+
* nothing here changes.
|
|
2210
|
+
*
|
|
2211
|
+
* @param {string[]} argv The engine's own arguments: `--staged`, paths, `--allow-fixture <path>`,
|
|
2212
|
+
* and this form's `--allow-list <path>`.
|
|
2157
2213
|
* @returns {number} 0 clean, 1 hits, 2 the scan could not account for something.
|
|
2158
2214
|
*/
|
|
2159
2215
|
export function runPhiScanCli(argv) {
|
|
2160
2216
|
const repoRoot = process.cwd();
|
|
2217
|
+
const option = takeAllowListOption(argv);
|
|
2218
|
+
if (option.problem !== undefined) {
|
|
2219
|
+
process.stderr.write(`[phi-scan] ${option.problem}\n`);
|
|
2220
|
+
return CLI_EXIT_CODES.refuse;
|
|
2221
|
+
}
|
|
2222
|
+
/** @type {string | undefined} */
|
|
2223
|
+
let allowListPath;
|
|
2224
|
+
if (option.allowList !== undefined) {
|
|
2225
|
+
allowListPath = resolve(repoRoot, option.allowList);
|
|
2226
|
+
try {
|
|
2227
|
+
readFileSync(allowListPath, "utf8");
|
|
2228
|
+
} catch (err) {
|
|
2229
|
+
const code = err instanceof Error && "code" in err ? String(err.code) : "";
|
|
2230
|
+
process.stderr.write(
|
|
2231
|
+
code === "ENOENT"
|
|
2232
|
+
? `[phi-scan] the allow list --allow-list names does not exist: ${allowListPath}\n`
|
|
2233
|
+
: `[phi-scan] the allow list --allow-list names could not be read (${code || "unknown error"}): ${allowListPath}\n`,
|
|
2234
|
+
);
|
|
2235
|
+
return CLI_EXIT_CODES.refuse;
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2161
2238
|
let excludedPaths = new Set();
|
|
2162
2239
|
const file = join(repoRoot, ...PHI_EXCLUDE_PATH.split("/"));
|
|
2163
2240
|
if (existsSync(file)) {
|
|
@@ -2196,7 +2273,8 @@ export function runPhiScanCli(argv) {
|
|
|
2196
2273
|
isStagedReadable: exemptsMarkdown,
|
|
2197
2274
|
excludedPaths,
|
|
2198
2275
|
repoRoot,
|
|
2199
|
-
argv,
|
|
2276
|
+
argv: option.argv,
|
|
2277
|
+
...(allowListPath === undefined ? {} : { allowListPath }),
|
|
2200
2278
|
});
|
|
2201
2279
|
} catch (err) {
|
|
2202
2280
|
// The backstop: an error the engine did not raise as a refusal must still not take node's own
|
package/ratchet.d.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { GitRunner } from "./changes.js";
|
|
2
|
+
|
|
3
|
+
/** The floor file's name, at a repository's root. */
|
|
4
|
+
export declare const FLOOR_FILE: "quality-floor.json";
|
|
5
|
+
|
|
6
|
+
/** The four coverage metrics, in the order they print. */
|
|
7
|
+
export declare const COVERAGE_METRICS: readonly ["lines", "branches", "functions", "statements"];
|
|
8
|
+
|
|
9
|
+
/** Where each report is read from, relative to a package's directory. */
|
|
10
|
+
export declare const REPORTS: Readonly<{
|
|
11
|
+
summary: "coverage/coverage-summary.json";
|
|
12
|
+
lcov: "coverage/lcov.info";
|
|
13
|
+
mutation: "reports/mutation/mutation.json";
|
|
14
|
+
changed: "reports/mutation/changed.json";
|
|
15
|
+
}>;
|
|
16
|
+
|
|
17
|
+
/** One package's floors. */
|
|
18
|
+
export interface FloorEntry {
|
|
19
|
+
coverage?: Partial<Record<"lines" | "branches" | "functions" | "statements", number>>;
|
|
20
|
+
mutation?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A floor file: each package directory, relative to the file, with its floors. */
|
|
24
|
+
export interface Floor {
|
|
25
|
+
$comment?: string;
|
|
26
|
+
packages: Record<string, FloorEntry>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** An inline exception found in a source file. */
|
|
30
|
+
export interface ExceptionComment {
|
|
31
|
+
line: number;
|
|
32
|
+
tool: "coverage" | "mutation";
|
|
33
|
+
directive: string;
|
|
34
|
+
reason: string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** What {@link judgeChangedCode} found in a change's own code. */
|
|
38
|
+
export interface ChangedCodeVerdict {
|
|
39
|
+
uncovered: { file: string; line: number }[];
|
|
40
|
+
survived: { file: string; line: number; mutator: string; status: string }[];
|
|
41
|
+
exceptions: { file: string; line: number; tool: string; what: string; reason: string | null }[];
|
|
42
|
+
unmutated: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Thrown when an input cannot be read or is not what it must be. */
|
|
46
|
+
export declare class RatchetUnreadable extends Error {
|
|
47
|
+
constructor(message: string);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Check a parsed floor file's shape; every key must be known. */
|
|
51
|
+
export declare function parseFloor(value: unknown): Floor;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Compare a floor file with the one it replaces: each floor the earlier one held that is now lower
|
|
55
|
+
* (`after` the lower value) or gone (`after` null). Raised and new floors are not listed.
|
|
56
|
+
*/
|
|
57
|
+
export declare function compareFloors(
|
|
58
|
+
before: Floor,
|
|
59
|
+
after: Floor,
|
|
60
|
+
): { dir: string; metric: string; before: number; after: number | null }[];
|
|
61
|
+
|
|
62
|
+
/** Cut a percentage to two decimals, toward zero. */
|
|
63
|
+
export declare function toFloor(value: number): number;
|
|
64
|
+
|
|
65
|
+
/** Read the four coverage percentages from Istanbul's `json-summary` report. */
|
|
66
|
+
export declare function readCoverageSummary(path: string): Record<string, number>;
|
|
67
|
+
|
|
68
|
+
/** Count a Stryker JSON report's mutants by status and work out its mutation score. */
|
|
69
|
+
export declare function mutationScore(
|
|
70
|
+
report: unknown,
|
|
71
|
+
path: string,
|
|
72
|
+
): { score: number; counts: Record<string, number> };
|
|
73
|
+
|
|
74
|
+
/** Read lcov's line records: absolute file path to line to hits. */
|
|
75
|
+
export declare function parseLcov(text: string, baseDir: string): Map<string, Map<number, number>>;
|
|
76
|
+
|
|
77
|
+
/** Find the inline coverage hints and Stryker directives in a source file. */
|
|
78
|
+
export declare function findExceptionComments(text: string): ExceptionComment[];
|
|
79
|
+
|
|
80
|
+
/** The first and last line a coverage hint takes out of the count. */
|
|
81
|
+
export declare function hintRange(
|
|
82
|
+
hint: { line: number; directive: string },
|
|
83
|
+
lines: string[],
|
|
84
|
+
counted: Map<number, number> | undefined,
|
|
85
|
+
): [number, number];
|
|
86
|
+
|
|
87
|
+
/** Judge a change's own code: its changed lines coverage counts, and the mutants in them. */
|
|
88
|
+
export declare function judgeChangedCode(input: {
|
|
89
|
+
changed: Map<string, Set<number>>;
|
|
90
|
+
lines: Map<string, Map<number, number>>;
|
|
91
|
+
mutants: Map<string, unknown[]> | null;
|
|
92
|
+
read: (file: string) => string;
|
|
93
|
+
}): ChangedCodeVerdict;
|
|
94
|
+
|
|
95
|
+
/** Read a changed-files mutation report into absolute file path to mutants. */
|
|
96
|
+
export declare function readMutants(path: string, packageDir: string): Map<string, unknown[]>;
|
|
97
|
+
|
|
98
|
+
/** Parse the ratchet's command line. */
|
|
99
|
+
export declare function parseArgs(argv: string[]): {
|
|
100
|
+
command: "check" | "raise";
|
|
101
|
+
floor: string;
|
|
102
|
+
base: string | null;
|
|
103
|
+
skip: Set<"coverage" | "mutation">;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Run the ratchet and return its exit code: 0 nothing dropped and changed code is reached, 1 a
|
|
108
|
+
* drop or unreached changed code, 2 an input could not be read.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```js
|
|
112
|
+
* import { runRatchet } from "@cosyte/script-utils/ratchet";
|
|
113
|
+
* process.exitCode = runRatchet({
|
|
114
|
+
* argv: ["check", "--base", "origin/main"],
|
|
115
|
+
* cwd: process.cwd(),
|
|
116
|
+
* out: (text) => process.stdout.write(text),
|
|
117
|
+
* err: (text) => process.stderr.write(text),
|
|
118
|
+
* });
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export declare function runRatchet(opts: {
|
|
122
|
+
argv: string[];
|
|
123
|
+
cwd: string;
|
|
124
|
+
out: (text: string) => void;
|
|
125
|
+
err: (text: string) => void;
|
|
126
|
+
git?: GitRunner;
|
|
127
|
+
}): number;
|