@veda-hq/pariksha 0.5.7 → 0.5.8

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.8 <check> [flags]
8
+ npx @veda-hq/pariksha@0.5.8 --version
9
9
  ```
10
10
 
11
11
  Every check exits 0 when the repository passes, 1 when it refuses something
@@ -267,10 +267,26 @@ value it must hold and why:
267
267
  { "advisorModel": { "value": "fable", "why": "sessions work with the advisor on" } }
268
268
  ```
269
269
 
270
+ A list the project only adds to, such as a permission list, takes
271
+ `"includes"` instead of `"value"`: the key must be a list holding each entry,
272
+ among any others.
273
+
270
274
  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.
275
+ a key, holds another value, or lacks a required list entry, compared as JSON,
276
+ naming the key, what is wrong and the reason. A missing settings file is a
277
+ refusal; a requirements file it cannot read or parse, or one listing no key,
278
+ exits 2.
279
+
280
+ `baselines/claude-settings.json` (`@veda-hq/pariksha/baselines/claude-settings.json`)
281
+ is the permission baseline every repository agents work in keeps: allow
282
+ `mcp__ccd_pr__set_monitor`, deny `Bash(*--no-verify*)`, ask
283
+ `Bash(*git push*--force*)` and `Bash(*git push* -f*)`. Point `--require` at it
284
+ in the package's install. Permission globs are a coarse backstop: they cannot
285
+ tell `git commit -n` from `git log -n 5`, nor see `git -C dir push --force`.
286
+ Parsing those is the job of a command guard such as kavach's `git_safety`.
287
+ `permissions.defaultMode: "auto"` is not in it: Claude Code ignores `auto` in a
288
+ project's settings and then also skips the user's own `defaultMode`, so it
289
+ belongs in user settings only.
274
290
 
275
291
  ## markdownlint rules
276
292
 
@@ -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
+ }
@@ -3,11 +3,13 @@
3
3
  // A setting a project depends on (Claude Code's `advisorModel`, say) lives in
4
4
  // one committed line, and one edit deletes it without any test noticing. The
5
5
  // project lists what it requires, with why, and this check refuses a settings
6
- // file that lacks a key or holds another value.
6
+ // file that lacks a key, holds another value, or drops an entry from a list
7
+ // (a permission rule, say) the project requires.
7
8
  import { isDeepStrictEqual } from 'node:util';
8
9
  const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
9
10
  /**
10
- * Parses the requirements file: `{ "<key>": { "value": <any JSON>, "why": "<reason>" }, … }`.
11
+ * Parses the requirements file:
12
+ * `{ "<key>": { "value": <any JSON>, "why": "…" } | { "includes": [<any JSON>, …], "why": "…" }, … }`.
11
13
  * Throws on anything else, so a malformed list never passes as an empty one.
12
14
  */
13
15
  export function parseRequirements(text) {
@@ -26,14 +28,26 @@ export function parseRequirements(text) {
26
28
  continue;
27
29
  if (key === '' || key.split('.').some((part) => part === ''))
28
30
  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');
31
+ const shape = `"${key}" needs { "value": …, "why": "…" } or { "includes": […], "why": "…" }`;
32
+ if (!isRecord(spec))
33
+ throw new Error(shape);
34
+ const hasValue = Object.hasOwn(spec, 'value');
35
+ const hasIncludes = Object.hasOwn(spec, 'includes');
36
+ if (hasValue === hasIncludes)
37
+ throw new Error(shape);
38
+ const extra = Object.keys(spec).filter((k) => k !== 'value' && k !== 'includes' && k !== 'why');
32
39
  if (extra.length > 0)
33
40
  throw new Error(`"${key}": unknown key ${extra.map((k) => `"${k}"`).join(', ')}`);
34
41
  if (typeof spec.why !== 'string' || spec.why.trim() === '')
35
42
  throw new Error(`"${key}" needs a "why"`);
36
- out.push({ key, value: spec.value, why: spec.why });
43
+ if (!hasIncludes) {
44
+ out.push({ key, kind: 'value', expected: spec.value, why: spec.why });
45
+ continue;
46
+ }
47
+ const includes = spec.includes;
48
+ if (!Array.isArray(includes) || includes.length === 0)
49
+ throw new Error(`"${key}": "includes" must list at least one entry`);
50
+ out.push({ key, kind: 'includes', expected: includes, why: spec.why });
37
51
  }
38
52
  if (out.length === 0)
39
53
  throw new Error('the requirements list no key');
@@ -49,7 +63,25 @@ const lookup = (settings, key) => {
49
63
  }
50
64
  return { missing: false, value: at };
51
65
  };
52
- /** Every requirement the settings break: a missing key or a different value. */
66
+ /** The finding for one requirement, or undefined when the settings meet it. */
67
+ const judge = (settings, r) => {
68
+ const got = lookup(settings, r.key);
69
+ const base = { key: r.key, kind: r.kind, why: r.why };
70
+ if (r.kind === 'value') {
71
+ if (got.missing)
72
+ return { ...base, expected: r.expected };
73
+ return isDeepStrictEqual(got.value, r.expected) ? undefined : { ...base, expected: r.expected, found: got.value };
74
+ }
75
+ const wanted = r.expected;
76
+ if (got.missing)
77
+ return { ...base, expected: wanted };
78
+ if (!Array.isArray(got.value))
79
+ return { ...base, expected: wanted, found: got.value };
80
+ const list = got.value;
81
+ const lacking = wanted.filter((entry) => !list.some((have) => isDeepStrictEqual(have, entry)));
82
+ return lacking.length === 0 ? undefined : { ...base, expected: lacking, found: got.value };
83
+ };
84
+ /** Every requirement the settings break: a missing key, a different value, or a list without a required entry. */
53
85
  export function findMissing(settingsText, requirements) {
54
86
  let settings;
55
87
  try {
@@ -60,16 +92,21 @@ export function findMissing(settingsText, requirements) {
60
92
  }
61
93
  if (!isRecord(settings))
62
94
  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;
95
+ return requirements.flatMap((r) => judge(settings, r) ?? []);
72
96
  }
97
+ /** One finding as a line: what is wrong and how to fix it. */
98
+ const describe = (f) => {
99
+ if (f.kind === 'value') {
100
+ const has = Object.hasOwn(f, 'found') ? `holds ${JSON.stringify(f.found)}` : 'is missing';
101
+ return `"${f.key}" ${has}; it must be ${JSON.stringify(f.expected)}`;
102
+ }
103
+ const entries = f.expected.map((e) => JSON.stringify(e)).join(', ');
104
+ if (!Object.hasOwn(f, 'found'))
105
+ return `"${f.key}" is missing; it must be a list holding ${entries}`;
106
+ if (!Array.isArray(f.found))
107
+ return `"${f.key}" holds ${JSON.stringify(f.found)}, not a list; it must be a list holding ${entries}`;
108
+ return `"${f.key}" lacks ${entries}`;
109
+ };
73
110
  /** The check's lines: one per finding on err, or one pass line on out. */
74
111
  export function formatFindings(findings, count, settingsPath, requirePath) {
75
112
  const P = 'pariksha required-settings:';
@@ -78,9 +115,6 @@ export function formatFindings(findings, count, settingsPath, requirePath) {
78
115
  return {
79
116
  ok: false,
80
117
  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
- }),
118
+ err: findings.map((f) => `${P} ${settingsPath}: ${describe(f)} (${f.why}). Put it back; to change the requirement, edit ${requirePath}.`),
85
119
  };
86
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veda-hq/pariksha",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
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",
@@ -31,10 +31,12 @@
31
31
  "exports": {
32
32
  "./markdownlint": "./dist/markdownlint-rules.js",
33
33
  "./dist/*": "./dist/*",
34
+ "./baselines/*": "./baselines/*",
34
35
  "./package.json": "./package.json"
35
36
  },
36
37
  "files": [
37
- "dist"
38
+ "dist",
39
+ "baselines"
38
40
  ],
39
41
  "peerDependencies": {
40
42
  "typescript": ">=5.4 <6"
@@ -52,7 +54,7 @@
52
54
  "build": "tsc -p tsconfig.build.json",
53
55
  "typecheck": "tsc -p tsconfig.json",
54
56
  "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 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');\"",
57
+ "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 dist/cli.js required-settings --require packages/pariksha/baselines/claude-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
58
  "check": "pnpm typecheck && pnpm test && pnpm build && pnpm smoke"
57
59
  }
58
60
  }