@veda-hq/pariksha 0.5.7 → 0.5.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/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.7 <check> [flags]
8
- npx @veda-hq/pariksha@0.5.7 --version
7
+ npx @veda-hq/pariksha@0.5.9 <check> [flags]
8
+ npx @veda-hq/pariksha@0.5.9 --version
9
9
  ```
10
10
 
11
11
  Every check exits 0 when the repository passes, 1 when it refuses something
@@ -62,6 +62,114 @@ the repository's deny list.
62
62
  - Binary files (any NUL byte) are skipped. Untracked files are not read.
63
63
  Line 0 in a finding is the file's path.
64
64
 
65
+ ## `codec-registry`
66
+
67
+ ```sh
68
+ pariksha codec-registry --config <file> [--base <ref> | --no-base] [--root <dir>]
69
+ ```
70
+
71
+ A module that parses markdown with its own regexes passes every example its
72
+ author thought of, and misses the setext heading nobody did. So every module
73
+ that hand-parses a format must be held by a property suite to the format's
74
+ reference parser, and every exported writer/reader pair (`writeX`/`readX`,
75
+ `renderX`/`parseX`, `formatX`/`loadX`…) by a suite that feeds real writer
76
+ output through the real reader. Discovery reads tracked source under `roots`
77
+ as a TypeScript syntax tree (tests, declarations, fixtures and mocks are not
78
+ read):
79
+
80
+ - a hand parser is a regex literal that reads a format's syntax (an ATX
81
+ heading or a fence, a frontmatter delimiter, a tag, an import statement), a
82
+ string literal that delimits one by itself (`---`, a fence, `<script`), a
83
+ comma split in a module that says `csv`, or a tokenizer tracking both quote
84
+ kinds and the backslash in a module about shell commands: one id per module
85
+ and format, `path#format`, in the formats `oracles` names only;
86
+ - a pair is an exported producer and consumer of the same stem,
87
+ `path#writeRow->readRow` (an `export { … }` list counts; a class, a constant
88
+ or a type-only export does not), and `path#digest` for a module that calls
89
+ `createHash` beside a numeric `*SCHEMA*` export or a `'schema'` argument.
90
+
91
+ Each id is registered, exempt with a reason, or listed in the baseline, and
92
+ the baseline lists exactly what is still unregistered. The JSON `--config`
93
+ (relative to the root):
94
+
95
+ ```json
96
+ {
97
+ "baseline": "scripts/codec-registry.baseline.json",
98
+ "roots": ["scripts", "src"],
99
+ "oracles": { "markdown": "mdast-util-from-markdown", "yaml": "js-yaml" },
100
+ "collected": ["^scripts/.+\\.test\\.ts$"],
101
+ "surfaces": ["scripts/**/*.{differential,roundtrip}.properties.test.ts"],
102
+ "library": "fast-check",
103
+ "markers": { "seed": "seed: SEED", "lineEndings": "LINE_ENDINGS", "golden": "assertGolden" },
104
+ "handParsers": [{ "source": "scripts/doc.ts", "formats": ["markdown"], "suite": "scripts/doc.differential.properties.test.ts", "outOfModel": "lists and block quotes", "motivatedBy": "M1234567890" }],
105
+ "pairs": [{ "id": "scripts/rows.ts#writeRow->readRow", "kind": "round-trip", "suite": "scripts/rows.roundtrip.properties.test.ts" }],
106
+ "exempt": [{ "id": "scripts/main.ts#csv", "reason": "a comma list in an env var, not CSV" }],
107
+ "minNoteLength": 21,
108
+ "examples": { "catalogue": { "path": "checks.json", "slice": "C-20" }, "fixtures": { "EDGE-12": { "example": "V3586514538", "test": "scripts/edge-12.examples.test.ts" } } },
109
+ "findingIds": { "motivatedBy": "^M\\d{10}$", "example": "^[MV]\\d{10}$" }
110
+ }
111
+ ```
112
+
113
+ - A hand-parser suite sits where a `collected` pattern matches it and on a
114
+ `surfaces` glob, imports its module by a relative specifier that resolves
115
+ to it, imports `library` (default `fast-check`), names each format's oracle
116
+ module, holds the `seed` marker, and holds the `lineEndings` marker unless
117
+ `lfOnly` says why the suite runs LF only. `outOfModel` (what the generators
118
+ leave out) is required.
119
+ - A pair's `kind` follows from its id and is refused otherwise: a
120
+ `producer->consumer` pair is `round-trip`, a `#digest` is `golden`. A
121
+ `round-trip` suite does what a hand-parser suite does less the oracle and
122
+ line endings; a `golden` one needs only its module and the `golden` marker.
123
+ - A suite's imports are read from its syntax tree: import declarations,
124
+ `export … from` and `import('…')` of a literal. A specifier in a comment
125
+ or another string, or a type-only import, is no import.
126
+ - `minNoteLength` is the fewest characters (trimmed) an exemption `reason`,
127
+ an `outOfModel` or an `lfOnly` may have: at 21, as here, `"reason": "not
128
+ csv"` is refused and the reason above passes. At 1 only a blank is refused.
129
+ - `examples` holds a fixture per check: its suite exists, is collected and
130
+ names the finding id; with a `catalogue`, the fixtures cover exactly that
131
+ JSON's checks for its `slice` (`{ "checks": [{ "id", "slice", "example" }]
132
+ }`, each example opening with `<finding id>:`), and `"catalogue": null`
133
+ checks the fixtures alone.
134
+ - `findingIds.motivatedBy` holds every `motivatedBy`, and `findingIds.example`
135
+ every example fixture's id, to its own pattern.
136
+ - `surfaces`, `examples`, `findingIds` and each pattern in it are required, so
137
+ a forgotten key never drops a check silently. `null` opts a check out, and
138
+ the run prints it on its own line: `not checked: surfaces, examples`.
139
+ - Messages name the kind (`hand parser scripts/doc.ts: …`, `pair
140
+ scripts/rows.ts#writeRow->readRow: …`), never a repository's check ids.
141
+
142
+ Refused (exit 1): a discovered id that is unaccounted; a registered or exempt
143
+ id discovery does not find; a suite that is missing, uncollected, off the
144
+ surface or missing any part above; a baseline entry that is registered,
145
+ exempt, gone or in the wrong list (each list is held to what discovery finds
146
+ of its kind); and a baseline entry the merge base of HEAD and the base did
147
+ not have, in any list, unless its file only moved (git's rename detection
148
+ over the working tree; the file is the id before its last `#`, so a path may
149
+ hold one). Without `--base`, the base is the first of `origin/main`,
150
+ `origin/develop`, `origin/master` and `main` that exists.
151
+
152
+ Growth is read against the merge base, so growth committed on the branch is
153
+ refused like growth in the working tree. The merge base's baseline is read
154
+ against today's lists: a list it lacks is empty (an entry in it today is
155
+ growth) and a list no longer named is ignored. Where the merge base has no
156
+ file at `baseline`, the baseline is being introduced and nothing has grown;
157
+ so pointing `baseline` at a new path also compares no growth for that run.
158
+ `--base HEAD` compares the tree with HEAD alone: committed growth passes and
159
+ only uncommitted growth is refused. `--no-base` compares no growth and needs
160
+ no base; the run prints `not checked: baseline growth (--no-base)`. `--base`
161
+ with `--no-base`, and an empty `--base ""`, are usage errors.
162
+
163
+ Exit 2, since nothing was checked: a config with an unknown, missing or bad
164
+ key, a path that is not a normalized root-relative POSIX path (a backslash or
165
+ a NUL included), a pair `kind` its id does not fit, an exemption,
166
+ `outOfModel` or `lfOnly` shorter than `minNoteLength`, an id registered twice; a baseline that is missing or not `{ "handParsers":
167
+ [ids], "pairs": [ids] }` with no other list; a merge-base baseline whose
168
+ lists are malformed; a root holding no tracked file; discovery finding no
169
+ hand parser, or no pair, since a kind found nowhere is checked nowhere; no
170
+ base without `--no-base`; a git call that fails. Needs `typescript`, as
171
+ `coverage-by-type` does.
172
+
65
173
  ## `coverage-by-type`
66
174
 
67
175
  ```sh
@@ -252,6 +360,83 @@ do not change what it reads. With no base, or when git cannot read the
252
360
  range (no merge base, a shallow clone), it exits 2: nothing was checked,
253
361
  so it is not a pass.
254
362
 
363
+ ## `fault-injection-registry`
364
+
365
+ ```sh
366
+ pariksha fault-injection-registry --config <file> [--base <ref> | --no-base] [--root <dir>]
367
+ ```
368
+
369
+ A CLI whose ledger write fails after it published a plan, or a service whose
370
+ best-effort cache write fails after its database commit, behaves in a way no
371
+ happy-path test reaches. So every such unit runs under the repository's
372
+ fault-injection harness, which fails its dependencies call by call, and its
373
+ suite asserts each declared check (an exit contract, containment, rerun
374
+ convergence) held to zero violations. Discovery reads the tracked files under
375
+ each `discover` group's `roots` that match its `include` and are not tests,
376
+ declarations or under a `fixtures/` folder, and keeps those whose text
377
+ matches its `marker`. Each found source is registered, exempt with a reason,
378
+ or in the baseline, which has one list per group and lists exactly what is
379
+ still unregistered. The JSON `--config` (relative to the root):
380
+
381
+ ```json
382
+ {
383
+ "baseline": "scripts/fault-injection.baseline.json",
384
+ "discover": {
385
+ "cli": { "roots": ["scripts"], "include": "\\.(ts|mjs)$", "marker": { "pattern": "isCliEntry\\(|process\\.argv\\b|^main\\(", "flags": "m" } },
386
+ "server": { "roots": ["apps/api/src"], "include": "\\.ts$", "marker": { "pattern": "best[- ]effort|fire-and-forget", "flags": "i" } }
387
+ },
388
+ "collected": ["^scripts/.+\\.test\\.ts$", "^apps/api/src/.+\\.test\\.ts$"],
389
+ "harness": {
390
+ "module": "packages/test-utils/src/fault-injection/index.ts",
391
+ "imports": ["@acme/test-utils/fault-injection"],
392
+ "checks": { "ERR-09": "checkExitContract", "ERR-10": "checkContainment", "STATE-11": "checkRerunConvergence" },
393
+ "heldBy": "assertNoViolations"
394
+ },
395
+ "consumers": [{ "source": "scripts/publish.ts", "unit": "main", "checks": ["ERR-09", "STATE-11"], "test": "scripts/publish.faults.test.ts", "uncovered": "the entry block, which only parses flags", "motivatedBy": "M3933356096" }],
396
+ "exempt": [{ "source": "scripts/report.ts", "reason": "writes no file and holds no state" }],
397
+ "exemptIn": ["cli"],
398
+ "minNoteLength": 21,
399
+ "examples": { "catalogue": { "path": "checks.json", "slice": "C-18" }, "fixtures": { "ERR-09": { "example": "M3933356096", "test": "packages/test-utils/src/fault-injection/err-09.examples.test.ts" } } },
400
+ "findingIds": { "motivatedBy": "^[MV]\\d{10}$", "example": "^[MV]\\d{10}$" }
401
+ }
402
+ ```
403
+
404
+ - The harness module exists and exports each `checks` function and `heldBy`
405
+ as a function. Its exports are read with TypeScript: `export { … } from`,
406
+ `export *` and a local name bound by an import (a default import, or
407
+ `export { default as … }`, follows the module's default export) are
408
+ followed through relative specifiers to the declaration, which must be a
409
+ function (a declaration, or a constant holding an arrow or function
410
+ expression), not a type or another value. A package specifier is not followed, and a name it
411
+ would have to supply is refused, naming what was not followed. The harness
412
+ is never imported or run.
413
+ - A consumer's suite exists where a `collected` pattern matches it, imports
414
+ the harness (a relative specifier resolving to `module` or its folder, or
415
+ one of `imports`) and the consumer's source, its imports read from its
416
+ syntax tree as in `codec-registry`, calls each declared check's function and
417
+ `heldBy`, and the name `unit` opens with appears in the source.
418
+ - `exemptIn` names the groups an exemption may sit in: an exempt source must
419
+ be discovered in one of them, so a source only another group finds (here a
420
+ best-effort server step) cannot be exempted away.
421
+ - `minNoteLength` is the fewest characters (trimmed) an exemption `reason`
422
+ or an `uncovered` may have.
423
+ - `examples`, `findingIds`, the opt-out with `null` and the `not checked`
424
+ line work as in `codec-registry`, and each fixture suite also calls its
425
+ check's function.
426
+
427
+ Refused (exit 1): a discovered source that is unaccounted; an exemption
428
+ discovery does not find in an `exemptIn` group; a harness or consumer suite
429
+ missing any part above; a stale baseline entry, or one in the list of a group
430
+ that does not discover it; and growth against the merge base, read as in
431
+ `codec-registry` (the same `--base`, `--base HEAD`,
432
+ `--no-base`, first introduction and merge-base lists). Exit 2: a bad config
433
+ (an unknown, missing or bad key, a path that is not a normalized
434
+ root-relative POSIX path, a check not in `checks`, an `exemptIn` group
435
+ `discover` lacks, a reason or `uncovered` shorter than
436
+ `minNoteLength`, a source registered twice), a missing or malformed
437
+ baseline, a root holding no tracked file, a group discovering nothing, no
438
+ base without `--no-base`, a failed git call, or no `typescript`.
439
+
255
440
  ## `required-settings`
256
441
 
257
442
  ```sh
@@ -267,10 +452,26 @@ value it must hold and why:
267
452
  { "advisorModel": { "value": "fable", "why": "sessions work with the advisor on" } }
268
453
  ```
269
454
 
455
+ A list the project only adds to, such as a permission list, takes
456
+ `"includes"` instead of `"value"`: the key must be a list holding each entry,
457
+ among any others.
458
+
270
459
  The check refuses a settings file (default `.claude/settings.json`) that lacks
271
- a key or holds another value, compared as JSON, naming the key, the value and
272
- the reason. A missing settings file is a refusal; a requirements file it cannot
273
- read or parse, or one listing no key, exits 2.
460
+ a key, holds another value, or lacks a required list entry, compared as JSON,
461
+ naming the key, what is wrong and the reason. A missing settings file is a
462
+ refusal; a requirements file it cannot read or parse, or one listing no key,
463
+ exits 2.
464
+
465
+ `baselines/claude-settings.json` (`@veda-hq/pariksha/baselines/claude-settings.json`)
466
+ is the permission baseline every repository agents work in keeps: allow
467
+ `mcp__ccd_pr__set_monitor`, deny `Bash(*--no-verify*)`, ask
468
+ `Bash(*git push*--force*)` and `Bash(*git push* -f*)`. Point `--require` at it
469
+ in the package's install. Permission globs are a coarse backstop: they cannot
470
+ tell `git commit -n` from `git log -n 5`, nor see `git -C dir push --force`.
471
+ Parsing those is the job of a command guard such as kavach's `git_safety`.
472
+ `permissions.defaultMode: "auto"` is not in it: Claude Code ignores `auto` in a
473
+ project's settings and then also skips the user's own `defaultMode`, so it
474
+ belongs in user settings only.
274
475
 
275
476
  ## markdownlint rules
276
477
 
@@ -0,0 +1,14 @@
1
+ {
2
+ "permissions.allow": {
3
+ "includes": ["mcp__ccd_pr__set_monitor"],
4
+ "why": "an agent turns on the desktop app's CI monitor for the pull request it opens without a prompt"
5
+ },
6
+ "permissions.deny": {
7
+ "includes": ["Bash(*--no-verify*)"],
8
+ "why": "no git command skips the repository's hooks"
9
+ },
10
+ "permissions.ask": {
11
+ "includes": ["Bash(*git push*--force*)", "Bash(*git push* -f*)"],
12
+ "why": "a force push rewrites published history, so a person approves each one"
13
+ }
14
+ }
package/dist/cli.js CHANGED
@@ -13,16 +13,21 @@ import { diskReads, formatReads, listFiles, needsTypescript, parseConfig as pars
13
13
  import { loadTypescript, typescriptLoaders } from "./typescript.js";
14
14
  import { checkExportDeletions, formatReport } from "./export-deletions.js";
15
15
  import { findMissing, formatFindings, parseRequirements } from "./required-settings.js";
16
- import { BASE_CANDIDATES, gitAt, gitAtTop, must, resolveBase, topOf } from "./git.js";
16
+ import { BASE_CANDIDATES, gitAt, gitAtTop, must, resolveBase, topOf, trackedUnder } from "./git.js";
17
+ import { discover as discoverCodecs, fileOf as codecFileOf, findings as codecFindings, formatFindings as formatCodecFindings, parseBaseline as parseCodecBaseline, parseConfig as parseCodecConfig } from "./codec-registry.js";
18
+ import { discoverGroup, findings as faultFindings, formatFindings as formatFaultFindings, parseBaseline as parseFaultBaseline, parseConfig as parseFaultConfig } from "./fault-injection-registry.js";
19
+ import { emptyRoots, growthSince, readBaseline, wholeId } from "./registry-ratchet.js";
17
20
  export const DENY_FILE = '.veda/deny.sha256';
18
21
  const USAGE = [
19
22
  'usage: pariksha authority-citations [--mode staged|tree] [--authority <a,b>] [--issue-prefix <A,B>] [--adr-dir <dir>] [--root <dir>]',
20
23
  ' pariksha boundary [--root <dir>] [--deny <file>]',
24
+ ' pariksha codec-registry --config <file> [--base <ref> | --no-base] [--root <dir>]',
21
25
  ' pariksha coverage-by-type [--config <file>] [--files-from <file>|-] [--root <dir>]',
22
26
  ' pariksha coverage-exemptions --config <file> [--package <dir>] [--root <dir>]',
23
27
  ' pariksha doc-budgets --table <file> [--section <name>] [--root <dir>]',
24
28
  ' pariksha env-gate-switches --config <file> [--mode staged|tree] [--root <dir>]',
25
29
  ' pariksha export-deletions [--base <ref>] [--root <dir>]',
30
+ ' pariksha fault-injection-registry --config <file> [--base <ref> | --no-base] [--root <dir>]',
26
31
  ' pariksha required-settings --require <file> [--settings <file>] [--root <dir>]',
27
32
  ' pariksha hash <word>',
28
33
  ' pariksha --version',
@@ -45,15 +50,27 @@ export const trackedFiles = (root) => {
45
50
  }
46
51
  return files;
47
52
  };
48
- /** `--name value` pairs, each name allowed once and from `allowed`; undefined on anything else. */
49
- export const flags = (rest, allowed) => {
53
+ /**
54
+ * `--name value` pairs, each name allowed once and from `allowed`, and bare
55
+ * `switches` (`--no-base`), each once, held as ''; undefined on anything else.
56
+ * An empty value is refused: `--base ""` names no ref.
57
+ */
58
+ export const flags = (rest, allowed, switches = []) => {
50
59
  const out = new Map();
51
- for (let i = 0; i < rest.length; i += 2) {
60
+ for (let i = 0; i < rest.length;) {
52
61
  const name = rest[i];
62
+ if (name === undefined || out.has(name))
63
+ return undefined;
64
+ if (switches.includes(name)) {
65
+ out.set(name, '');
66
+ i += 1;
67
+ continue;
68
+ }
53
69
  const value = rest[i + 1];
54
- if (name === undefined || !allowed.includes(name) || !value || out.has(name))
70
+ if (!allowed.includes(name) || !value)
55
71
  return undefined;
56
72
  out.set(name, value);
73
+ i += 2;
57
74
  }
58
75
  return out;
59
76
  };
@@ -318,6 +335,90 @@ const envGateSwitches = (f, io) => {
318
335
  result.err.forEach(io.err);
319
336
  return result.ok ? 0 : 1;
320
337
  };
338
+ /** Thrown when TypeScript cannot be loaded; carries the loader's message. */
339
+ class NoTypescript extends Error {
340
+ }
341
+ const runRatchet = (spec, f, io) => {
342
+ const P = `pariksha ${spec.name}:`;
343
+ const configPath = f.get('--config');
344
+ const noBase = f.has('--no-base');
345
+ if (configPath === undefined || (noBase && f.has('--base'))) {
346
+ io.err(USAGE);
347
+ return 2;
348
+ }
349
+ const dir = resolve(f.get('--root') ?? '.');
350
+ const root = topOf(dir);
351
+ if (root === undefined) {
352
+ io.err(`${P} ${dir} is not in a git repository`);
353
+ return 2;
354
+ }
355
+ const git = gitAt(root);
356
+ try {
357
+ const config = spec.parse(readFileSync(resolve(root, configPath), 'utf8'));
358
+ const baseline = readBaseline(root, config.baseline, (source, where) => spec.parseBaseline(source, where, config, false));
359
+ const roots = spec.roots(config);
360
+ const files = trackedUnder(git, root, roots);
361
+ const empty = emptyRoots(files, roots);
362
+ if (empty.length > 0)
363
+ throw new Error(`no tracked file lies under ${empty.join(', ')}, so discovery there would read nothing`);
364
+ const ts = () => {
365
+ const loaded = (io.typescript ?? (() => loadTypescript(typescriptLoaders(root))))();
366
+ if (typeof loaded === 'string')
367
+ throw new NoTypescript(loaded);
368
+ return loaded;
369
+ };
370
+ const found = spec.discover(root, files, config, ts);
371
+ // Each kind discovery reads must find something: one finding nothing is a
372
+ // config (or a discovery) that checks nothing of that kind, never a pass.
373
+ const none = Object.entries(found).find(([, ids]) => ids.length === 0)?.[0];
374
+ if (none !== undefined) {
375
+ const { noun, roots: under } = spec.kind(none, config);
376
+ throw new Error(`discovery finds no ${noun} under ${under.join(', ')}, so nothing of that kind would be checked`);
377
+ }
378
+ let grown = [];
379
+ if (!noBase) {
380
+ const asked = f.get('--base');
381
+ const base = asked === undefined ? resolveBase(git) : resolveBase(git, [asked]);
382
+ if (base === undefined) {
383
+ throw new Error(asked === undefined ? `none of ${BASE_CANDIDATES.join(', ')} names a commit, so the baseline cannot be held to its merge base; fetch one, pass --base, or pass --no-base` : `--base ${asked} names no commit`);
384
+ }
385
+ grown = growthSince(git, base, config.baseline, baseline, (source, where) => spec.parseBaseline(source, where, config, true), spec.fileOf);
386
+ }
387
+ const notChecked = [...config.skipped, ...(noBase ? ['baseline growth (--no-base)'] : [])];
388
+ const result = spec.format(spec.findings(root, config, found, baseline, grown, ts), found, baseline, notChecked, !noBase);
389
+ result.out.forEach(io.out);
390
+ result.err.forEach(io.err);
391
+ return result.ok ? 0 : 1;
392
+ }
393
+ catch (e) {
394
+ io.err(e instanceof NoTypescript ? `${P} ${e.message}` : `${P} ${e.message}; nothing was checked, so this is not a pass`);
395
+ return 2;
396
+ }
397
+ };
398
+ const CODEC_REGISTRY = {
399
+ name: 'codec-registry',
400
+ parse: parseCodecConfig,
401
+ parseBaseline: (source, where, _config, atBase) => parseCodecBaseline(source, where, atBase),
402
+ roots: (config) => config.roots,
403
+ discover: (root, files, config, ts) => discoverCodecs(ts(), root, files, new Set(Object.keys(config.oracles))),
404
+ kind: (list, config) => ({ noun: list === 'handParsers' ? 'hand parser' : 'producer/consumer pair', roots: config.roots }),
405
+ fileOf: codecFileOf,
406
+ findings: (root, config, found, baseline, grown, ts) => codecFindings(ts(), root, config, found, baseline, grown),
407
+ format: (list, found, baseline, notChecked, compared) => formatCodecFindings(list, found, baseline, notChecked, compared),
408
+ };
409
+ const FAULT_INJECTION_REGISTRY = {
410
+ name: 'fault-injection-registry',
411
+ parse: parseFaultConfig,
412
+ parseBaseline: parseFaultBaseline,
413
+ roots: (config) => [...new Set(Object.values(config.discover).flatMap((g) => g.roots))],
414
+ discover: (root, files, config) => Object.fromEntries(Object.entries(config.discover).map(([name, group]) => [name, discoverGroup(root, files.filter((file) => group.roots.some((r) => file === r || file.startsWith(`${r}/`))), group)])),
415
+ kind: (list, config) => ({ noun: `${list} source`, roots: config.discover[list]?.roots ?? [] }),
416
+ fileOf: wholeId,
417
+ findings: (root, config, found, baseline, grown, ts) => faultFindings(ts(), root, config, found, baseline, grown),
418
+ format: formatFaultFindings,
419
+ };
420
+ const codecRegistry = (f, io) => runRatchet(CODEC_REGISTRY, f, io);
421
+ const faultInjectionRegistry = (f, io) => runRatchet(FAULT_INJECTION_REGISTRY, f, io);
321
422
  export const DEFAULT_SETTINGS = '.claude/settings.json';
322
423
  const requiredSettings = (f, io) => {
323
424
  const P = 'pariksha required-settings:';
@@ -366,7 +467,9 @@ const requiredSettings = (f, io) => {
366
467
  return result.ok ? 0 : 1;
367
468
  };
368
469
  const COMMANDS = {
470
+ 'codec-registry': { allowed: ['--config', '--base', '--root'], switches: ['--no-base'], run: codecRegistry },
369
471
  'coverage-exemptions': { allowed: ['--config', '--package', '--root'], run: coverageExemptions },
472
+ 'fault-injection-registry': { allowed: ['--config', '--base', '--root'], switches: ['--no-base'], run: faultInjectionRegistry },
370
473
  'authority-citations': { allowed: ['--mode', '--authority', '--issue-prefix', '--adr-dir', '--root'], run: authorityCitations },
371
474
  boundary: { allowed: ['--root', '--deny'], run: boundary },
372
475
  'coverage-by-type': { allowed: ['--config', '--files-from', '--root'], run: coverageByType },
@@ -386,7 +489,7 @@ export const run = (args, io, version = ownVersion(), env = process.env) => {
386
489
  return 0;
387
490
  }
388
491
  const spec = command !== undefined && Object.hasOwn(COMMANDS, command) ? COMMANDS[command] : undefined;
389
- const f = spec && flags(rest, spec.allowed);
492
+ const f = spec && flags(rest, spec.allowed, spec.switches);
390
493
  if (!spec || !f) {
391
494
  io.err(USAGE);
392
495
  return 2;