@veda-hq/pariksha 0.5.5 → 0.5.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/README.md +22 -2
- package/dist/cli.js +50 -0
- package/dist/required-settings.js +86 -0
- package/package.json +3 -3
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.7 <check> [flags]
|
|
8
|
+
npx @veda-hq/pariksha@0.5.7 --version
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
Every check exits 0 when the repository passes, 1 when it refuses something
|
|
@@ -252,6 +252,26 @@ do not change what it reads. With no base, or when git cannot read the
|
|
|
252
252
|
range (no merge base, a shallow clone), it exits 2: nothing was checked,
|
|
253
253
|
so it is not a pass.
|
|
254
254
|
|
|
255
|
+
## `required-settings`
|
|
256
|
+
|
|
257
|
+
```sh
|
|
258
|
+
pariksha required-settings --require <file> [--settings <file>] [--root <dir>]
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
A setting a project depends on, such as Claude Code's `advisorModel`, is one
|
|
262
|
+
committed line, and one edit deletes it without any test noticing. The
|
|
263
|
+
requirements file lists each key (a dotted path for a nested one) with the
|
|
264
|
+
value it must hold and why:
|
|
265
|
+
|
|
266
|
+
```json
|
|
267
|
+
{ "advisorModel": { "value": "fable", "why": "sessions work with the advisor on" } }
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
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.
|
|
274
|
+
|
|
255
275
|
## markdownlint rules
|
|
256
276
|
|
|
257
277
|
```jsonc
|
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { checkBudgets } from "./doc-budgets.js";
|
|
|
12
12
|
import { diskReads, formatReads, listFiles, needsTypescript, parseConfig as parseGateConfig, readerFor, readGate } from "./env-gate-switches.js";
|
|
13
13
|
import { loadTypescript, typescriptLoaders } from "./typescript.js";
|
|
14
14
|
import { checkExportDeletions, formatReport } from "./export-deletions.js";
|
|
15
|
+
import { findMissing, formatFindings, parseRequirements } from "./required-settings.js";
|
|
15
16
|
import { BASE_CANDIDATES, gitAt, gitAtTop, must, resolveBase, topOf } from "./git.js";
|
|
16
17
|
export const DENY_FILE = '.veda/deny.sha256';
|
|
17
18
|
const USAGE = [
|
|
@@ -22,6 +23,7 @@ const USAGE = [
|
|
|
22
23
|
' pariksha doc-budgets --table <file> [--section <name>] [--root <dir>]',
|
|
23
24
|
' pariksha env-gate-switches --config <file> [--mode staged|tree] [--root <dir>]',
|
|
24
25
|
' pariksha export-deletions [--base <ref>] [--root <dir>]',
|
|
26
|
+
' pariksha required-settings --require <file> [--settings <file>] [--root <dir>]',
|
|
25
27
|
' pariksha hash <word>',
|
|
26
28
|
' pariksha --version',
|
|
27
29
|
].join('\n');
|
|
@@ -316,6 +318,53 @@ const envGateSwitches = (f, io) => {
|
|
|
316
318
|
result.err.forEach(io.err);
|
|
317
319
|
return result.ok ? 0 : 1;
|
|
318
320
|
};
|
|
321
|
+
export const DEFAULT_SETTINGS = '.claude/settings.json';
|
|
322
|
+
const requiredSettings = (f, io) => {
|
|
323
|
+
const P = 'pariksha required-settings:';
|
|
324
|
+
const requirePath = f.get('--require');
|
|
325
|
+
if (requirePath === undefined) {
|
|
326
|
+
io.err(USAGE);
|
|
327
|
+
return 2;
|
|
328
|
+
}
|
|
329
|
+
const root = resolve(f.get('--root') ?? '.');
|
|
330
|
+
const settingsPath = f.get('--settings') ?? DEFAULT_SETTINGS;
|
|
331
|
+
const read = (path, what) => {
|
|
332
|
+
try {
|
|
333
|
+
return readFileSync(resolve(root, path), 'utf8');
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
io.err(`${P} cannot read the ${what} ${resolve(root, path)}; nothing was checked, so this is not a pass`);
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
const requireText = read(requirePath, 'requirements');
|
|
341
|
+
if (requireText === undefined)
|
|
342
|
+
return 2;
|
|
343
|
+
let requirements;
|
|
344
|
+
try {
|
|
345
|
+
requirements = parseRequirements(requireText);
|
|
346
|
+
}
|
|
347
|
+
catch (e) {
|
|
348
|
+
io.err(`${P} ${requirePath}: ${e.message}; nothing was checked, so this is not a pass`);
|
|
349
|
+
return 2;
|
|
350
|
+
}
|
|
351
|
+
const settingsText = read(settingsPath, 'settings');
|
|
352
|
+
// A missing settings file lacks every required key: a refusal, not a usage error.
|
|
353
|
+
if (settingsText === undefined)
|
|
354
|
+
return 1;
|
|
355
|
+
let findings;
|
|
356
|
+
try {
|
|
357
|
+
findings = findMissing(settingsText, requirements);
|
|
358
|
+
}
|
|
359
|
+
catch (e) {
|
|
360
|
+
io.err(`${P} ${settingsPath}: ${e.message}`);
|
|
361
|
+
return 1;
|
|
362
|
+
}
|
|
363
|
+
const result = formatFindings(findings, requirements.length, settingsPath, requirePath);
|
|
364
|
+
result.out.forEach(io.out);
|
|
365
|
+
result.err.forEach(io.err);
|
|
366
|
+
return result.ok ? 0 : 1;
|
|
367
|
+
};
|
|
319
368
|
const COMMANDS = {
|
|
320
369
|
'coverage-exemptions': { allowed: ['--config', '--package', '--root'], run: coverageExemptions },
|
|
321
370
|
'authority-citations': { allowed: ['--mode', '--authority', '--issue-prefix', '--adr-dir', '--root'], run: authorityCitations },
|
|
@@ -324,6 +373,7 @@ const COMMANDS = {
|
|
|
324
373
|
'doc-budgets': { allowed: ['--table', '--section', '--root'], run: docBudgets },
|
|
325
374
|
'env-gate-switches': { allowed: ['--config', '--mode', '--root'], run: envGateSwitches },
|
|
326
375
|
'export-deletions': { allowed: ['--base', '--root'], run: exportDeletions },
|
|
376
|
+
'required-settings': { allowed: ['--require', '--settings', '--root'], run: requiredSettings },
|
|
327
377
|
};
|
|
328
378
|
export const run = (args, io, version = ownVersion(), env = process.env) => {
|
|
329
379
|
const [command, ...rest] = args;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// required-settings: a settings file carries every value a project requires of it.
|
|
2
|
+
//
|
|
3
|
+
// A setting a project depends on (Claude Code's `advisorModel`, say) lives in
|
|
4
|
+
// one committed line, and one edit deletes it without any test noticing. The
|
|
5
|
+
// project lists what it requires, with why, and this check refuses a settings
|
|
6
|
+
// file that lacks a key or holds another value.
|
|
7
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
8
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
9
|
+
/**
|
|
10
|
+
* Parses the requirements file: `{ "<key>": { "value": <any JSON>, "why": "<reason>" }, … }`.
|
|
11
|
+
* Throws on anything else, so a malformed list never passes as an empty one.
|
|
12
|
+
*/
|
|
13
|
+
export function parseRequirements(text) {
|
|
14
|
+
let raw;
|
|
15
|
+
try {
|
|
16
|
+
raw = JSON.parse(text);
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
throw new Error(`the requirements are not JSON (${e.message})`);
|
|
20
|
+
}
|
|
21
|
+
if (!isRecord(raw))
|
|
22
|
+
throw new Error('the requirements must be an object of keys');
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const [key, spec] of Object.entries(raw)) {
|
|
25
|
+
if (key === '$schema')
|
|
26
|
+
continue;
|
|
27
|
+
if (key === '' || key.split('.').some((part) => part === ''))
|
|
28
|
+
throw new Error(`"${key}" is not a key or dotted path`);
|
|
29
|
+
if (!isRecord(spec) || !Object.hasOwn(spec, 'value'))
|
|
30
|
+
throw new Error(`"${key}" needs { "value": …, "why": "…" }`);
|
|
31
|
+
const extra = Object.keys(spec).filter((k) => k !== 'value' && k !== 'why');
|
|
32
|
+
if (extra.length > 0)
|
|
33
|
+
throw new Error(`"${key}": unknown key ${extra.map((k) => `"${k}"`).join(', ')}`);
|
|
34
|
+
if (typeof spec.why !== 'string' || spec.why.trim() === '')
|
|
35
|
+
throw new Error(`"${key}" needs a "why"`);
|
|
36
|
+
out.push({ key, value: spec.value, why: spec.why });
|
|
37
|
+
}
|
|
38
|
+
if (out.length === 0)
|
|
39
|
+
throw new Error('the requirements list no key');
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/** The value at a dotted path, or `{ missing: true }`. */
|
|
43
|
+
const lookup = (settings, key) => {
|
|
44
|
+
let at = settings;
|
|
45
|
+
for (const part of key.split('.')) {
|
|
46
|
+
if (!isRecord(at) || !Object.hasOwn(at, part))
|
|
47
|
+
return { missing: true };
|
|
48
|
+
at = at[part];
|
|
49
|
+
}
|
|
50
|
+
return { missing: false, value: at };
|
|
51
|
+
};
|
|
52
|
+
/** Every requirement the settings break: a missing key or a different value. */
|
|
53
|
+
export function findMissing(settingsText, requirements) {
|
|
54
|
+
let settings;
|
|
55
|
+
try {
|
|
56
|
+
settings = JSON.parse(settingsText);
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
throw new Error(`the settings are not JSON (${e.message})`);
|
|
60
|
+
}
|
|
61
|
+
if (!isRecord(settings))
|
|
62
|
+
throw new Error('the settings must hold an object');
|
|
63
|
+
const findings = [];
|
|
64
|
+
for (const r of requirements) {
|
|
65
|
+
const got = lookup(settings, r.key);
|
|
66
|
+
if (got.missing)
|
|
67
|
+
findings.push({ key: r.key, expected: r.value, why: r.why });
|
|
68
|
+
else if (!isDeepStrictEqual(got.value, r.value))
|
|
69
|
+
findings.push({ key: r.key, expected: r.value, found: got.value, why: r.why });
|
|
70
|
+
}
|
|
71
|
+
return findings;
|
|
72
|
+
}
|
|
73
|
+
/** The check's lines: one per finding on err, or one pass line on out. */
|
|
74
|
+
export function formatFindings(findings, count, settingsPath, requirePath) {
|
|
75
|
+
const P = 'pariksha required-settings:';
|
|
76
|
+
if (findings.length === 0)
|
|
77
|
+
return { ok: true, out: [`${P} ${settingsPath} carries all ${count} required settings (${requirePath})`], err: [] };
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
out: [],
|
|
81
|
+
err: findings.map((f) => {
|
|
82
|
+
const has = Object.hasOwn(f, 'found') ? `holds ${JSON.stringify(f.found)}` : 'is missing';
|
|
83
|
+
return `${P} ${settingsPath}: "${f.key}" ${has}; it must be ${JSON.stringify(f.expected)} (${f.why}). Put it back; to change the requirement, edit ${requirePath}.`;
|
|
84
|
+
}),
|
|
85
|
+
};
|
|
86
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veda-hq/pariksha",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "Repository checks: doc drift, rule enforcement, test-quality ratchets. Checks: authority-citations, boundary, coverage-by-type, coverage-exemptions, doc-budgets, env-gate-switches, export-deletions. Also markdownlint rules for Markdown that renders mangled: @veda-hq/pariksha/markdownlint.",
|
|
3
|
+
"version": "0.5.7",
|
|
4
|
+
"description": "Repository checks: doc drift, rule enforcement, test-quality ratchets. Checks: authority-citations, boundary, coverage-by-type, coverage-exemptions, doc-budgets, env-gate-switches, export-deletions, required-settings. Also markdownlint rules for Markdown that renders mangled: @veda-hq/pariksha/markdownlint.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"repository checks",
|
|
7
7
|
"lint",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"build": "tsc -p tsconfig.build.json",
|
|
53
53
|
"typecheck": "tsc -p tsconfig.json",
|
|
54
54
|
"test": "node --test --experimental-test-coverage --test-coverage-lines=95 --test-coverage-functions=95 --test-coverage-branches=90 --test-coverage-exclude=src/**/*.test.ts src/**/*.test.ts",
|
|
55
|
-
"smoke": "node dist/cli.js --version && node dist/cli.js boundary --root ../.. && node --input-type=module -e \"import { createRequire } from 'node:module'; import { pathToFileURL } from 'node:url'; const id = '@veda-hq/pariksha/markdownlint'; const req = createRequire(process.cwd() + '/'); const required = req(id); const imported = (await import(pathToFileURL(req.resolve(id, { paths: [process.cwd()] })).href)).default; if (!Array.isArray(required) || required !== imported || required.length !== 12) throw new Error(id + ' does not load as markdownlint-cli2 loads it');\"",
|
|
55
|
+
"smoke": "node dist/cli.js --version && node dist/cli.js boundary --root ../.. && node dist/cli.js required-settings --require .veda/required-settings.json --root ../.. &&node --input-type=module -e \"import { createRequire } from 'node:module'; import { pathToFileURL } from 'node:url'; const id = '@veda-hq/pariksha/markdownlint'; const req = createRequire(process.cwd() + '/'); const required = req(id); const imported = (await import(pathToFileURL(req.resolve(id, { paths: [process.cwd()] })).href)).default; if (!Array.isArray(required) || required !== imported || required.length !== 12) throw new Error(id + ' does not load as markdownlint-cli2 loads it');\"",
|
|
56
56
|
"check": "pnpm typecheck && pnpm test && pnpm build && pnpm smoke"
|
|
57
57
|
}
|
|
58
58
|
}
|