@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 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.5 <check> [flags]
8
- npx @veda-hq/pariksha@0.5.5 --version
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.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
  }