@veda-hq/pariksha 0.5.2 → 0.5.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/README.md +143 -2
- package/dist/cli.js +63 -0
- package/dist/env-gate-switches.js +1041 -0
- package/dist/markdownlint-rules.js +335 -0
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
test-quality ratchets. Part of [Veda](https://github.com/veda-hq/veda).
|
|
5
5
|
|
|
6
6
|
```sh
|
|
7
|
-
npx @veda-hq/pariksha@0.5.
|
|
8
|
-
npx @veda-hq/pariksha@0.5.
|
|
7
|
+
npx @veda-hq/pariksha@0.5.5 <check> [flags]
|
|
8
|
+
npx @veda-hq/pariksha@0.5.5 --version
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
Every check exits 0 when the repository passes, 1 when it refuses something
|
|
@@ -155,6 +155,72 @@ row that is not `` | `path` | bytes | ``, since nothing would enforce it.
|
|
|
155
155
|
The table file is read on every check too, so it must budget itself: a row
|
|
156
156
|
for its own path (relative to `--root`) is required.
|
|
157
157
|
|
|
158
|
+
## `env-gate-switches`
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
pariksha env-gate-switches --config <file> [--mode staged|tree] [--root <dir>]
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
A hook that reads `PUSH_SKIP` from the environment has a bypass anyone can
|
|
165
|
+
type: `PUSH_SKIP=1 git push`. This check refuses gate code that READS an
|
|
166
|
+
environment variable whose name has a switch word (`X_SKIP`, `SKIP_X`,
|
|
167
|
+
`X_PROBE_Y`): writing or deleting one is not a read. The JSON `--config`
|
|
168
|
+
(relative to the root) says where gate code is:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"gatePaths": ["^\\.husky/", "^scripts/", "^\\.github/workflows/"],
|
|
173
|
+
"notGateCode": ["\\.test\\.[cm]?[jt]sx?$", "(?:^|/)fixtures?/", "\\.md$"],
|
|
174
|
+
"notAGate": {
|
|
175
|
+
"SETUP_SKIP_MOBILE": { "files": ["scripts/setup.sh"], "reason": "installs no mobile toolchain; checks nothing" }
|
|
176
|
+
},
|
|
177
|
+
"words": ["SKIP", "PROBE"]
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
- `gatePaths` (required) and `notGateCode` are path regexes: a file is gate
|
|
182
|
+
code when a gate path matches it and no `notGateCode` entry does.
|
|
183
|
+
- `notAGate` allows a switch that turns off something other than a check,
|
|
184
|
+
in the files listed only, with the reason a reviewer reads; the same name
|
|
185
|
+
anywhere else is still refused.
|
|
186
|
+
- `words` (default `SKIP` and `PROBE`) are upper-case words; a name made of
|
|
187
|
+
`_`-joined words, one of them a switch word, is a switch.
|
|
188
|
+
- Shell (`.sh`, and every file under `.husky/`) is read as bash reads it:
|
|
189
|
+
single quotes, escapes, comments and quoted heredoc bodies expand nothing,
|
|
190
|
+
and a bare name bash reads is a read: in arithmetic (`(( X_SKIP ))`,
|
|
191
|
+
`let`, `-eq`, a subscript), `printenv X_SKIP`, and `-v X_SKIP` in `[ ]`,
|
|
192
|
+
`[[ ]]` or `test`. A variable the file initialises at the top level before
|
|
193
|
+
reading it is the script's own decision, not a read; one initialised in a
|
|
194
|
+
branch, loop, function, group, subshell, pipeline or the end of a `&&`
|
|
195
|
+
list, or self-defaulted (`X_SKIP="${X_SKIP:-0}"`), is.
|
|
196
|
+
- TypeScript and JavaScript are read as a syntax tree, with a type checker
|
|
197
|
+
over the one file: dotted, bracketed, optional, destructured (nested too:
|
|
198
|
+
`const { env: { X_SKIP } } = process`) and `in` reads count, through
|
|
199
|
+
`process`, `globalThis.process`, `node:process` and any local bound to the
|
|
200
|
+
environment, with constant keys resolved (`env['X_' + 'SKIP']`); a mention
|
|
201
|
+
in a comment or string does not.
|
|
202
|
+
- Python reads are `os.environ[…]`, `.get(…)`, `.setdefault(…)`, `.pop(…)`,
|
|
203
|
+
`'X' in` / `'X' not in os.environ`, the same on `os.environ.copy()` or
|
|
204
|
+
`dict(os.environ)`, and `os.getenv(…)`, with the bytes forms (`environb`,
|
|
205
|
+
`getenvb`): under `os` by any name, through `from os import environ as e`
|
|
206
|
+
/ `getenv as g`, and through a name assigned one (`env = os.environ`,
|
|
207
|
+
`read = os.getenv`), outside strings and comments (f-string fields are
|
|
208
|
+
code). A key held in a variable is not resolved. Workflow YAML reads are
|
|
209
|
+
`$X`, `env.X` and `env['X']` outside comments.
|
|
210
|
+
|
|
211
|
+
`tree` (the default) reads every tracked file in the working tree, skipping
|
|
212
|
+
one deleted there; when every tracked gate file is deleted, nothing was read
|
|
213
|
+
and it exits 2. `staged` (for a pre-commit hook) reads the files the index
|
|
214
|
+
adds, copies, modifies, renames or changes the type of (a symlink replaced
|
|
215
|
+
by a file), as the index holds them. In either mode, a repository with no
|
|
216
|
+
tracked gate code exits 2, since the config would check nothing, and a gate
|
|
217
|
+
file that is a symlink (in the index for staged, in the working tree for
|
|
218
|
+
tree) exits 2: its text is a path, and its target goes unchecked. A gate
|
|
219
|
+
file that cannot be read (one under a folder the check cannot open
|
|
220
|
+
included), a git call that fails, or a failure while reading a file's code
|
|
221
|
+
exits 2: an unread gate is never clean. Needs `typescript` only when a
|
|
222
|
+
TypeScript or JavaScript file is gate code.
|
|
223
|
+
|
|
158
224
|
## `export-deletions`
|
|
159
225
|
|
|
160
226
|
```sh
|
|
@@ -185,3 +251,78 @@ renames, colour, external diff tools and textconv in a user's git config
|
|
|
185
251
|
do not change what it reads. With no base, or when git cannot read the
|
|
186
252
|
range (no merge base, a shallow clone), it exits 2: nothing was checked,
|
|
187
253
|
so it is not a pass.
|
|
254
|
+
|
|
255
|
+
## markdownlint rules
|
|
256
|
+
|
|
257
|
+
```jsonc
|
|
258
|
+
// .markdownlint-cli2.jsonc
|
|
259
|
+
{
|
|
260
|
+
"customRules": ["@veda-hq/pariksha/markdownlint"],
|
|
261
|
+
"config": { "default": false, "pariksha-markdown": true }
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Not a check but a module: markdownlint custom rules for Markdown that reads
|
|
266
|
+
fine as source and renders mangled, where no built-in rule fails it for that
|
|
267
|
+
reason. `@veda-hq/pariksha/markdownlint` is an ES module whose default export
|
|
268
|
+
is the rules array, which markdownlint-cli2's `customRules` loads by package
|
|
269
|
+
name; `require()` of it returns the array too. Every rule carries the tag
|
|
270
|
+
`pariksha-markdown`, so the config above turns on these rules and nothing
|
|
271
|
+
else. They run on markdownlint's micromark tokens (`parser: "micromark"`)
|
|
272
|
+
and import nothing. Most read prose only, skipping code blocks, HTML blocks
|
|
273
|
+
and code spans; the fence and code-span rules read those,
|
|
274
|
+
`pariksha-replacement-character` reads every line, and
|
|
275
|
+
`pariksha-duplicate-adjacent-line` skips code and HTML blocks but compares
|
|
276
|
+
whole lines, code spans included.
|
|
277
|
+
|
|
278
|
+
| Rule | Fails |
|
|
279
|
+
| ---- | ----- |
|
|
280
|
+
| `pariksha-alert-marker-inline` | `> [!NOTE] body` on one line: GitHub renders the callout only with the marker alone |
|
|
281
|
+
| `pariksha-link-space-before-paren` | `[text] (./a.md)`: the space renders the link as text (targets that look like a URL, path, anchor or file only; task boxes `[ ]` and `[x]` pass) |
|
|
282
|
+
| `pariksha-code-span-crosses-line` | a code span that closes on a later line, or a path span ending in `/` continued in the next line's span |
|
|
283
|
+
| `pariksha-fence-unclosed` | a code fence that never closes |
|
|
284
|
+
| `pariksha-fence-empty` | a fence with nothing but blank lines in it |
|
|
285
|
+
| `pariksha-fence-swallows-heading` | a `# Heading` inside a fence of a language where it cannot be code: the fence above lost its close |
|
|
286
|
+
| `pariksha-table-row-orphaned` | a paragraph made only of `\| … \|` rows: a blank line cut them off their table |
|
|
287
|
+
| `pariksha-duplicate-adjacent-line` | a line of ten or more characters, with a letter, repeating the previous non-blank prose line (table rows may repeat) |
|
|
288
|
+
| `pariksha-replacement-character` | U+FFFD, bytes lost in an encoding round-trip |
|
|
289
|
+
| `pariksha-emphasis-mangle` | `store*id`: a single `*` between letters, where `_` was re-read as emphasis |
|
|
290
|
+
| `pariksha-dunder-mangle` | `**DEV**`: a formatter rewrote `__DEV__` as bold |
|
|
291
|
+
| `pariksha-lettered-step-indent` | `b.` indented unlike the `a.` above it, so it renders under another parent |
|
|
292
|
+
|
|
293
|
+
Options are lists, each replacing its default:
|
|
294
|
+
|
|
295
|
+
| Option | Rule | Default |
|
|
296
|
+
| ------ | ---- | ------- |
|
|
297
|
+
| `alert_types` | `pariksha-alert-marker-inline` | `NOTE`, `TIP`, `IMPORTANT`, `WARNING`, `CAUTION` (matched case-insensitively) |
|
|
298
|
+
| `link_target_extensions` | `pariksha-link-space-before-paren` | `md`, `mdx`, `html`, `htm`, `ts`, `tsx`, `js`, `json`, `yaml`, `yml`, `png`, `svg`, `jpg`, `jpeg`, `pdf` |
|
|
299
|
+
| `dunder_words` | `pariksha-dunder-mangle` | `DEV`, `proto`, `esModule`, `typename`: names never an English word in bold |
|
|
300
|
+
| `no_hash_languages` | `pariksha-fence-swallows-heading` | `ts`, `tsx`, `typescript`, `js`, `jsx`, `javascript`, `json`, `jsonc`, `sql`, `css`, `swift`, `kotlin`, `java`, `go`, `rust`, `prisma` (matched case-insensitively; not `graphql` or `html`, where a `# ` line is a comment or text) |
|
|
301
|
+
|
|
302
|
+
```jsonc
|
|
303
|
+
{
|
|
304
|
+
"config": {
|
|
305
|
+
"default": false,
|
|
306
|
+
"pariksha-markdown": { "dunder_words": ["DEV", "proto", "esModule", "typename", "dirname"] },
|
|
307
|
+
"pariksha-fence-swallows-heading": { "no_hash_languages": ["ts", "py"] }
|
|
308
|
+
},
|
|
309
|
+
"customRules": ["@veda-hq/pariksha/markdownlint"]
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Options set under the tag reach every rule, so every rule accepts all four
|
|
314
|
+
names. A rule's own key replaces the tag's object for that rule (markdownlint
|
|
315
|
+
takes the last key that names a rule, whole), so above
|
|
316
|
+
`pariksha-fence-swallows-heading` keeps the default `dunder_words`, which it
|
|
317
|
+
does not read. The same goes for an option of one rule set under another
|
|
318
|
+
rule's key: a rule cannot tell its own object from the tag's, so it accepts
|
|
319
|
+
every name some rule reads. Otherwise a configuration markdownlint cannot have
|
|
320
|
+
meant is refused, not ignored: an option name no rule reads (an array set as
|
|
321
|
+
the configuration reaches the rules as its indexes, `"0"`, and is refused as
|
|
322
|
+
such), a list option that is not a non-empty array of strings (to turn a rule
|
|
323
|
+
off, set it to `false`), or an entry that could never match (an extension with
|
|
324
|
+
its dot, a dunder word with its underscores, a language with a space). Each
|
|
325
|
+
rule that reads the bad configuration throws, naming itself and the option, so
|
|
326
|
+
markdownlint-cli2 reports up to twelve errors on line 1 of each file and the
|
|
327
|
+
run fails; with `"severity": "warning"` in the configuration they are reported
|
|
328
|
+
as warnings instead.
|
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import { findDenied, hash, isText, parseDenyList } from "./boundary.js";
|
|
|
9
9
|
import { DEFAULT_CONFIG, findGaps, formatGaps, listedPaths, parseConfig, treePaths } from "./coverage-by-type.js";
|
|
10
10
|
import { checkBuild, checkPackage, checkVitest, formatViolations, loaderFailure, loadVitestExclude, missingConfigs, ownedBy, parseConfig as parseExemptions } from "./coverage-exemptions.js";
|
|
11
11
|
import { checkBudgets } from "./doc-budgets.js";
|
|
12
|
+
import { diskReads, formatReads, listFiles, needsTypescript, parseConfig as parseGateConfig, readerFor, readGate } from "./env-gate-switches.js";
|
|
12
13
|
import { loadTypescript, typescriptLoaders } from "./typescript.js";
|
|
13
14
|
import { checkExportDeletions, formatReport } from "./export-deletions.js";
|
|
14
15
|
import { BASE_CANDIDATES, gitAt, gitAtTop, must, resolveBase, topOf } from "./git.js";
|
|
@@ -19,6 +20,7 @@ const USAGE = [
|
|
|
19
20
|
' pariksha coverage-by-type [--config <file>] [--files-from <file>|-] [--root <dir>]',
|
|
20
21
|
' pariksha coverage-exemptions --config <file> [--package <dir>] [--root <dir>]',
|
|
21
22
|
' pariksha doc-budgets --table <file> [--section <name>] [--root <dir>]',
|
|
23
|
+
' pariksha env-gate-switches --config <file> [--mode staged|tree] [--root <dir>]',
|
|
22
24
|
' pariksha export-deletions [--base <ref>] [--root <dir>]',
|
|
23
25
|
' pariksha hash <word>',
|
|
24
26
|
' pariksha --version',
|
|
@@ -254,12 +256,73 @@ const coverageExemptions = (f, io) => {
|
|
|
254
256
|
return 2;
|
|
255
257
|
}
|
|
256
258
|
};
|
|
259
|
+
const envGateSwitches = (f, io) => {
|
|
260
|
+
const P = 'pariksha env-gate-switches:';
|
|
261
|
+
const configPath = f.get('--config');
|
|
262
|
+
const mode = f.get('--mode') ?? 'tree';
|
|
263
|
+
if (configPath === undefined || (mode !== 'staged' && mode !== 'tree')) {
|
|
264
|
+
io.err(USAGE);
|
|
265
|
+
return 2;
|
|
266
|
+
}
|
|
267
|
+
const dir = resolve(f.get('--root') ?? '.');
|
|
268
|
+
const root = topOf(dir);
|
|
269
|
+
if (root === undefined) {
|
|
270
|
+
io.err(`${P} ${dir} is not in a git repository`);
|
|
271
|
+
return 2;
|
|
272
|
+
}
|
|
273
|
+
let reader;
|
|
274
|
+
let words;
|
|
275
|
+
try {
|
|
276
|
+
const config = parseGateConfig(readFileSync(resolve(root, configPath), 'utf8'));
|
|
277
|
+
reader = readerFor(config);
|
|
278
|
+
words = config.words;
|
|
279
|
+
}
|
|
280
|
+
catch (e) {
|
|
281
|
+
io.err(`${P} ${e.message}; nothing was checked, so this is not a pass`);
|
|
282
|
+
return 2;
|
|
283
|
+
}
|
|
284
|
+
const reads = (io.gateReads ?? diskReads)(root);
|
|
285
|
+
let result;
|
|
286
|
+
try {
|
|
287
|
+
const listed = listFiles(reads, mode).filter((l) => reader.isGateCode(l.file));
|
|
288
|
+
// A tree with no gate code is a config whose gatePaths match nothing (a
|
|
289
|
+
// typo): staged mode would pass every commit under it, so both refuse.
|
|
290
|
+
if (!(mode === 'tree' ? listed.length > 0 : listFiles(reads, 'tree').some((l) => reader.isGateCode(l.file)))) {
|
|
291
|
+
io.err(`${P} no tracked file is gate code under the config's gatePaths; nothing was checked, so this is not a pass`);
|
|
292
|
+
return 2;
|
|
293
|
+
}
|
|
294
|
+
const sources = listed.flatMap((entry) => {
|
|
295
|
+
const text = readGate(reads, mode, entry);
|
|
296
|
+
return text === undefined ? [] : [{ file: entry.file, text }];
|
|
297
|
+
});
|
|
298
|
+
if (mode === 'tree' && sources.length === 0) {
|
|
299
|
+
// Staged mode reads the index, which holds every file it lists.
|
|
300
|
+
io.err(`${P} every tracked gate file (${listed.length}) is gone from the working tree; nothing was checked, so this is not a pass`);
|
|
301
|
+
return 2;
|
|
302
|
+
}
|
|
303
|
+
const ts = sources.some((s) => needsTypescript(s.file)) ? (io.typescript ?? (() => loadTypescript(typescriptLoaders(root))))() : undefined;
|
|
304
|
+
if (typeof ts === 'string') {
|
|
305
|
+
io.err(`${P} ${ts}`);
|
|
306
|
+
return 2;
|
|
307
|
+
}
|
|
308
|
+
result = formatReads(sources.flatMap((s) => reader.findSwitchReads(s.file, s.text, ts)), words, sources.length, configPath);
|
|
309
|
+
}
|
|
310
|
+
catch (e) {
|
|
311
|
+
// An unread gate, or one the reader failed on, is unchecked, never clean.
|
|
312
|
+
io.err(`${P} could not read gate code, so it is unchecked: ${e.message}`);
|
|
313
|
+
return 2;
|
|
314
|
+
}
|
|
315
|
+
result.out.forEach(io.out);
|
|
316
|
+
result.err.forEach(io.err);
|
|
317
|
+
return result.ok ? 0 : 1;
|
|
318
|
+
};
|
|
257
319
|
const COMMANDS = {
|
|
258
320
|
'coverage-exemptions': { allowed: ['--config', '--package', '--root'], run: coverageExemptions },
|
|
259
321
|
'authority-citations': { allowed: ['--mode', '--authority', '--issue-prefix', '--adr-dir', '--root'], run: authorityCitations },
|
|
260
322
|
boundary: { allowed: ['--root', '--deny'], run: boundary },
|
|
261
323
|
'coverage-by-type': { allowed: ['--config', '--files-from', '--root'], run: coverageByType },
|
|
262
324
|
'doc-budgets': { allowed: ['--table', '--section', '--root'], run: docBudgets },
|
|
325
|
+
'env-gate-switches': { allowed: ['--config', '--mode', '--root'], run: envGateSwitches },
|
|
263
326
|
'export-deletions': { allowed: ['--base', '--root'], run: exportDeletions },
|
|
264
327
|
};
|
|
265
328
|
export const run = (args, io, version = ownVersion(), env = process.env) => {
|