@veda-hq/pariksha 0.5.20 → 0.6.4

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.20 <check> [flags]
8
- npx @veda-hq/pariksha@0.5.20 --version
7
+ npx @veda-hq/pariksha@0.6.4 <check> [flags]
8
+ npx @veda-hq/pariksha@0.6.4 --version
9
9
  ```
10
10
 
11
11
  Every check exits 0 when the repository passes, 1 when it refuses something
@@ -72,19 +72,32 @@ step's `if:`, or any `${{ … }}` in the job, outside quoted literals. Text in
72
72
  a `run:` script and an action input named `if` are not reads. A non-gating
73
73
  job (a change detector) need not be gated, but must be declared once read.
74
74
 
75
+ The same command refuses a workflow that holds a gate and can also be
76
+ dispatched (`workflow_dispatch` in `on:`, as one event, in a list or as a
77
+ key), one line per gate under its own id, `dispatch-on-gate-workflow`. A
78
+ dispatch run from a branch with an open pull request posts every job's check
79
+ on the head commit, the gate among them as skipped, and that replaces the
80
+ pull request run's success: a merge queue requiring the gate reads it as
81
+ failed and dequeues the pull request. Ad-hoc runs belong in a separate
82
+ workflow with no gate-named job.
83
+
75
84
  ```json
76
- { "id": "CI-001", "workflowsDir": ".github/workflows", "gateName": "gate$", "nonGating": ["detect-changes"] }
85
+ { "id": "CI-001", "workflowsDir": ".github/workflows", "gateName": "gate$", "nonGating": ["detect-changes"], "dispatchAllowed": [] }
77
86
  ```
78
87
 
79
88
  - `id` (default `aggregate-gate-needs`) names the check in its messages: a
80
- project's rule id, say.
89
+ project's rule id, say. The dispatch refusal keeps its own id.
81
90
  - `workflowsDir` (default `.github/workflows`, relative to the root).
82
91
  - `gateName` (default `gate$`) is a regex, matched case-insensitively.
83
92
  - `nonGating` (default none) lists job ids that gate nothing.
93
+ - `dispatchAllowed` (default none) lists workflow file names (`release.yml`)
94
+ that may be dispatched beside a gate: one no merge queue requires, such as
95
+ an approval gate in a release workflow.
84
96
 
85
97
  A workflow that does not parse is reported as a gap, never passed. No
86
98
  workflow to read exits 1 (`no workflow files found — the lookup failed`),
87
- as the original did; a bad config exits 2.
99
+ as the original did; a bad config exits 2. The clean line is unchanged: it
100
+ prints only when both refusals find nothing.
88
101
 
89
102
  ## `authority-citations`
90
103
 
@@ -1030,6 +1043,38 @@ Parsing those is the job of a command guard such as kavach's `git_safety`.
1030
1043
  project's settings and then also skips the user's own `defaultMode`, so it
1031
1044
  belongs in user settings only.
1032
1045
 
1046
+ ## `schema-roots`
1047
+
1048
+ ```sh
1049
+ pariksha schema-roots [--files-from <file>|-] [--root <dir>] [<file>...]
1050
+ ```
1051
+
1052
+ The Anthropic API refuses a tool's `input_schema`, or a structured output's
1053
+ schema, with `allOf`, `anyOf` or `oneOf` at its root, answering 400:
1054
+ "input_schema does not support oneOf, allOf, or anyOf at the top level". The
1055
+ request fails before the first tool call, so an agent built on that schema
1056
+ dies at its start every time. This reads the JSON files it is given and refuses each such
1057
+ schema:
1058
+
1059
+ - the value under `input_schema`, `inputSchema`, `output_schema`,
1060
+ `outputSchema` or `json_schema`, and the `schema` inside a `json_schema`,
1061
+ at any depth, in objects and arrays (a request's `tools` list, say);
1062
+ - the `schema` of an object whose `type` is `json_schema`, a structured
1063
+ output format;
1064
+ - the root of a file named `*.schema.json`.
1065
+
1066
+ A combinator below a schema's root is allowed, and a schema found is not read
1067
+ further: a property inside it named `input_schema` is a property. Each
1068
+ finding goes to stderr as `<file>: <json path>: top-level <keyword>`, with
1069
+ the fix: express the condition in code after the call returns.
1070
+
1071
+ The files are the ones named on the command line and the ones `--files-from`
1072
+ lists, one path per line (a file, or `-` for stdin); relative paths are read
1073
+ from the root. Only `.json` files are read, so a list of changed files can
1074
+ hold others; YAML is not read. No file to read passes. The first `.json`
1075
+ file that cannot be read or is not plain JSON (one with comments, say) exits
1076
+ 2, naming it, and no finding is printed; so does a list that cannot be read.
1077
+
1033
1078
  ## `shell-gates`
1034
1079
 
1035
1080
  ```sh
@@ -15,20 +15,32 @@
15
15
  // `${{ … }}` in the job. Not read: text in a `run:` script, quoted literals in
16
16
  // an expression, and an action input that happens to be called `if`. A
17
17
  // workflow that does not parse is reported, never read as having no gaps.
18
+ //
19
+ // The same workflows are also refused a `workflow_dispatch` trigger beside a
20
+ // gate (dispatch-on-gate-workflow): a dispatch run from a branch with an open
21
+ // pull request posts every job's check on the head commit, the gate among
22
+ // them as skipped, and that replaces the pull request run's success. A merge
23
+ // queue requiring the gate then reads it as failed and dequeues the pull
24
+ // request. `dispatchAllowed` names the workflows (by file name) where that
25
+ // cannot happen, for a project with no merge queue on the gate.
18
26
  import { readdirSync, readFileSync } from 'node:fs';
19
- import { join } from 'node:path';
27
+ import { basename, join } from 'node:path';
20
28
  import { load as parseYaml } from 'js-yaml';
21
29
  export const DEFAULT_GATE_CONFIG = {
22
30
  workflowsDir: '.github/workflows',
23
31
  gateName: /gate$/i,
24
32
  nonGating: new Set(),
25
33
  id: 'aggregate-gate-needs',
34
+ dispatchAllowed: new Set(),
26
35
  };
36
+ /** What the messages call the dispatch refusal: its own rule, not the project's needs rule. */
37
+ export const DISPATCH_ID = 'dispatch-on-gate-workflow';
27
38
  const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
28
39
  /**
29
40
  * `{ "id"?: "<label>", "workflowsDir"?: "<dir>", "gateName"?: "<regex>",
30
- * "nonGating"?: ["<job id>", …] }`; `gateName` matches case-insensitively.
31
- * Absent keys take the defaults; anything else throws.
41
+ * "nonGating"?: ["<job id>", …], "dispatchAllowed"?: ["<file name>", …] }`;
42
+ * `gateName` matches case-insensitively. Absent keys take the defaults;
43
+ * anything else throws.
32
44
  */
33
45
  export const parseGateConfig = (text) => {
34
46
  let raw;
@@ -40,7 +52,7 @@ export const parseGateConfig = (text) => {
40
52
  }
41
53
  if (!isRecord(raw))
42
54
  throw new Error('the config must be a JSON object');
43
- const unknown = Object.keys(raw).find((k) => !['id', 'workflowsDir', 'gateName', 'nonGating'].includes(k));
55
+ const unknown = Object.keys(raw).find((k) => !['id', 'workflowsDir', 'gateName', 'nonGating', 'dispatchAllowed'].includes(k));
44
56
  if (unknown !== undefined)
45
57
  throw new Error(`unknown key "${unknown}"`);
46
58
  const text_ = (key, fallback) => {
@@ -60,10 +72,15 @@ export const parseGateConfig = (text) => {
60
72
  catch (e) {
61
73
  throw new Error(`"gateName" is not a regex: ${e.message}`);
62
74
  }
63
- const nonGating = 'nonGating' in raw ? raw['nonGating'] : [];
64
- if (!Array.isArray(nonGating) || nonGating.some((j) => typeof j !== 'string' || j === ''))
65
- throw new Error('"nonGating" must be a list of job ids');
66
- return { id, workflowsDir, gateName, nonGating: new Set(nonGating) };
75
+ const names = (key, what) => {
76
+ const value = key in raw ? raw[key] : [];
77
+ if (!Array.isArray(value) || value.some((j) => typeof j !== 'string' || j === ''))
78
+ throw new Error(`"${key}" must be a list of ${what}`);
79
+ return new Set(value);
80
+ };
81
+ const nonGating = names('nonGating', 'job ids');
82
+ const dispatchAllowed = names('dispatchAllowed', 'workflow file names');
83
+ return { id, workflowsDir, gateName, nonGating, dispatchAllowed };
67
84
  };
68
85
  /** The `.yml` and `.yaml` files in the workflows folder, as root-relative paths; none when it cannot be read. */
69
86
  export const workflowFiles = (root, dir) => {
@@ -107,23 +124,31 @@ export const consumedNeeds = (job) => {
107
124
  return [...ids];
108
125
  };
109
126
  const asArray = (needs) => (needs === undefined ? [] : Array.isArray(needs) ? needs : [needs]);
127
+ /** The workflow at `relative`, or undefined when it does not parse; a scalar or null document is no job map, read as empty. */
128
+ const readWorkflow = (root, relative) => {
129
+ let parsed;
130
+ try {
131
+ parsed = parseYaml(readFileSync(join(root, relative), 'utf8'));
132
+ }
133
+ catch {
134
+ return undefined;
135
+ }
136
+ return typeof parsed === 'object' && parsed !== null ? parsed : {};
137
+ };
138
+ /** The ids of the workflow's gates: jobs whose `name:` (or id), or id, matches `gateName`. */
139
+ const gatesOf = (jobs, config) => Object.keys(jobs).filter((id) => config.gateName.test(jobs[id]?.name ?? id) || config.gateName.test(id));
110
140
  /** Each gate, in each of `files` (root-relative), whose `needs:` leaves out a job it gates or reads. */
111
141
  export const findGateGaps = (root, files, config) => {
112
142
  const gaps = [];
113
143
  for (const relative of files) {
114
- let document;
115
- try {
116
- // A workflow that parses to a scalar or null is no job map: read as empty.
117
- const parsed = parseYaml(readFileSync(join(root, relative), 'utf8'));
118
- document = typeof parsed === 'object' && parsed !== null ? parsed : {};
119
- }
120
- catch {
144
+ const document = readWorkflow(root, relative);
145
+ if (document === undefined) {
121
146
  gaps.push({ workflow: relative, gate: '(unparseable)', missing: ['<file did not parse>'] });
122
147
  continue;
123
148
  }
124
149
  const jobs = document.jobs ?? {};
125
150
  const jobIds = Object.keys(jobs);
126
- const gateIds = jobIds.filter((id) => config.gateName.test(jobs[id]?.name ?? id) || config.gateName.test(id));
151
+ const gateIds = gatesOf(jobs, config);
127
152
  for (const gateId of gateIds) {
128
153
  const declared = new Set(asArray(jobs[gateId]?.needs));
129
154
  const missing = jobIds.filter((id) => id !== gateId && !gateIds.includes(id) && !config.nonGating.has(id) && !declared.has(id));
@@ -135,19 +160,53 @@ export const findGateGaps = (root, files, config) => {
135
160
  }
136
161
  return gaps;
137
162
  };
138
- /** The verdict on the workflows under `root`, worded as the original check worded it. */
163
+ /** Whether the workflow's `on:` (one event, a list, or a map) holds `workflow_dispatch`; a map's value is usually null. */
164
+ export const hasDispatch = (on) => {
165
+ if (typeof on === 'string')
166
+ return on === 'workflow_dispatch';
167
+ if (Array.isArray(on))
168
+ return on.includes('workflow_dispatch');
169
+ return isRecord(on) && Object.hasOwn(on, 'workflow_dispatch');
170
+ };
171
+ /**
172
+ * Each gate, in each of `files` (root-relative), whose workflow can also be
173
+ * dispatched, outside `dispatchAllowed`. A workflow that does not parse is
174
+ * left to findGateGaps, which reports it.
175
+ */
176
+ export const findDispatchedGates = (root, files, config) => {
177
+ const found = [];
178
+ for (const relative of files) {
179
+ if (config.dispatchAllowed.has(basename(relative)))
180
+ continue;
181
+ const document = readWorkflow(root, relative);
182
+ if (document === undefined || !hasDispatch(document.on))
183
+ continue;
184
+ for (const gate of gatesOf(document.jobs ?? {}, config))
185
+ found.push({ workflow: relative, gate });
186
+ }
187
+ return found;
188
+ };
189
+ /** The refusal of one dispatched gate, one line. */
190
+ export const dispatchMessage = (found) => `❌ ${DISPATCH_ID}: ${found.workflow}: workflow_dispatch on the workflow holding the required gate ${found.gate}. ` +
191
+ "A dispatch run posts the gate as skipped on the PR's head commit, which a merge queue reads as failed and dequeues the PR. " +
192
+ 'Move ad-hoc runs to a separate workflow with no gate-named job.';
193
+ /** The verdict on the workflows under `root`: the needs gaps worded as the original check worded them, then each dispatched gate. */
139
194
  export const checkGates = (root, config) => {
140
195
  const files = workflowFiles(root, config.workflowsDir);
141
196
  if (files.length === 0)
142
197
  return { ok: false, out: [], err: [`❌ ${config.id}: no workflow files found — the lookup failed`] };
143
198
  const gaps = findGateGaps(root, files, config);
144
- if (gaps.length === 0)
199
+ const dispatched = findDispatchedGates(root, files, config).map(dispatchMessage);
200
+ if (gaps.length === 0 && dispatched.length === 0)
145
201
  return { ok: true, out: [`✅ ${config.id}: every aggregate gate depends on all its jobs (${files.length} workflow(s))`], err: [] };
146
- const err = [`\n❌ ${config.id}: ${gaps.length} aggregate gate(s) with an incomplete needs list\n`];
147
- for (const gap of gaps)
148
- err.push(` ${gap.workflow} → job "${gap.gate}"`, ` not depended on: ${gap.missing.join(', ')}\n`);
149
- err.push('The merge queue checks only the gate. A job absent from its `needs:` cannot\n' +
150
- 'block a merge however it fails — and a failure upstream turns into a SKIP\n' +
151
- 'downstream, which the gate then accepts.\n');
152
- return { ok: false, out: [], err };
202
+ const err = [];
203
+ if (gaps.length > 0) {
204
+ err.push(`\n❌ ${config.id}: ${gaps.length} aggregate gate(s) with an incomplete needs list\n`);
205
+ for (const gap of gaps)
206
+ err.push(` ${gap.workflow} → job "${gap.gate}"`, ` not depended on: ${gap.missing.join(', ')}\n`);
207
+ err.push('The merge queue checks only the gate. A job absent from its `needs:` cannot\n' +
208
+ 'block a merge however it fails — and a failure upstream turns into a SKIP\n' +
209
+ 'downstream, which the gate then accepts.\n');
210
+ }
211
+ return { ok: false, out: [], err: [...err, ...dispatched] };
153
212
  };
package/dist/cli.js CHANGED
@@ -32,6 +32,7 @@ import { checkClaims, formatOutcome, loadEslint, parseConfig as parseClaimsConfi
32
32
  import { parseConfig as parseDuplicateConfig, runDuplicateTests } from "./duplicate-tests.js";
33
33
  import { DEFAULT_RUNS, npxVitest, parseConfig as parseFlakyConfig, parseRuns, runFlakyTests } from "./flaky-tests.js";
34
34
  import { inheritRunner, runPythonFloor } from "./python-floor.js";
35
+ import { checkSchemaRoots, jsonFiles, readDocuments } from "./schema-roots.js";
35
36
  export const DENY_FILE = '.veda/deny.sha256';
36
37
  const USAGE = [
37
38
  'usage: pariksha authority-citations [--mode staged|tree] [--authority <a,b>] [--issue-prefix <A,B>] [--adr-dir <dir>] [--root <dir>]',
@@ -54,6 +55,7 @@ const USAGE = [
54
55
  ' pariksha pnpm-settings [--config <file>] [--root <dir>]',
55
56
  ' pariksha python-floor --python <interpreter> --floor-file <path> [--exclude <regex>] [--selector <path>] [--root <dir>]',
56
57
  ' pariksha required-settings --require <file> [--settings <file>] [--root <dir>]',
58
+ ' pariksha schema-roots [--files-from <file>|-] [--root <dir>] [<file>...]',
57
59
  ' pariksha shell-gates [--config <file>] [--staged | --all | --base <ref> | --files-from <file>|-] [--only <checks>] [--root <dir>]',
58
60
  ' pariksha hash <word>',
59
61
  ' pariksha --version',
@@ -791,6 +793,52 @@ const pythonFloor = (f, io) => {
791
793
  };
792
794
  return runPythonFloor(options, io.python ?? inheritRunner, io.err);
793
795
  };
796
+ /** `rest` split into its `--name value` pairs and the bare words around them, the files a command names. */
797
+ export const splitFiles = (rest) => {
798
+ const flagArgs = [];
799
+ const files = [];
800
+ for (let i = 0; i < rest.length; i += 1) {
801
+ const word = rest[i];
802
+ if (!word.startsWith('--')) {
803
+ files.push(word);
804
+ continue;
805
+ }
806
+ flagArgs.push(...rest.slice(i, i + 2));
807
+ i += 1;
808
+ }
809
+ return { flagArgs, files };
810
+ };
811
+ /**
812
+ * schema-roots: the `.json` files named on the command line and in the
813
+ * `--files-from` list, relative ones read from the root; other files are
814
+ * not read, and none at all is a pass. The first file that cannot be read or
815
+ * parsed exits 2, naming it, with no finding printed.
816
+ */
817
+ const schemaRoots = (rest, io) => {
818
+ const P = 'pariksha schema-roots:';
819
+ const { flagArgs, files: named } = splitFiles(rest);
820
+ const f = flags(flagArgs, ['--files-from', '--root']);
821
+ if (!f) {
822
+ io.err(USAGE);
823
+ return 2;
824
+ }
825
+ const root = resolve(f.get('--root') ?? '.');
826
+ const from = f.get('--files-from');
827
+ let documents;
828
+ try {
829
+ const listed = [...named, ...(from === undefined ? [] : entries(listFrom(from, io)))];
830
+ const files = [...new Set(jsonFiles(listed).map((p) => relative(root, resolve(root, p))))];
831
+ documents = readDocuments(files, (file) => readFileSync(resolve(root, file), 'utf8'));
832
+ }
833
+ catch (e) {
834
+ io.err(`${P} ${e.message}; nothing was checked, so this is not a pass`);
835
+ return 2;
836
+ }
837
+ const result = checkSchemaRoots(documents);
838
+ result.out.forEach(io.out);
839
+ result.err.forEach(io.err);
840
+ return result.ok ? 0 : 1;
841
+ };
794
842
  const COMMANDS = {
795
843
  'codec-registry': { allowed: ['--config', '--base', '--root'], switches: ['--no-base'], run: codecRegistry },
796
844
  'coverage-exemptions': { allowed: ['--config', '--package', '--root'], run: coverageExemptions },
@@ -825,6 +873,9 @@ export const run = (args, io, version = ownVersion(), env = process.env) => {
825
873
  io.out(hash(rest[0]));
826
874
  return 0;
827
875
  }
876
+ // The one check that takes files as bare words, so not through `flags`.
877
+ if (command === 'schema-roots')
878
+ return schemaRoots(rest, io);
828
879
  const spec = command !== undefined && Object.hasOwn(COMMANDS, command) ? COMMANDS[command] : undefined;
829
880
  const f = spec && flags(rest, spec.allowed, spec.switches);
830
881
  if (!spec || !f) {
@@ -0,0 +1,92 @@
1
+ // schema-roots: no tool or output schema with a combinator at its root.
2
+ //
3
+ // The Anthropic API refuses a tool's `input_schema`, or a structured output's
4
+ // schema, whose root holds `allOf`, `anyOf` or `oneOf` (a 400, "input_schema
5
+ // does not support oneOf, allOf, or anyOf at the top level"). The request
6
+ // fails before the model's first tool call, so an agent built on that schema
7
+ // dies at its start, every time, with nothing wrong in its own code. This
8
+ // reads JSON documents and refuses each such schema it finds:
9
+ //
10
+ // - the value under `input_schema`, `inputSchema`, `output_schema`,
11
+ // `outputSchema` or `json_schema`, and the `schema` inside a `json_schema`;
12
+ // - the `schema` of an object whose `type` is `json_schema` (a structured
13
+ // output format: `{ "type": "json_schema", "schema": {…} }`);
14
+ // - the root of a file named `*.schema.json`.
15
+ //
16
+ // A schema found is not read further: a combinator below its root is allowed,
17
+ // and a property inside it that happens to be called `input_schema` is a
18
+ // property, not a tool.
19
+ import { basename } from 'node:path';
20
+ export const COMBINATORS = ['allOf', 'anyOf', 'oneOf'];
21
+ /** The keys whose value is a tool's or an output's schema. */
22
+ export const SCHEMA_KEYS = ['input_schema', 'inputSchema', 'output_schema', 'outputSchema', 'json_schema'];
23
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
24
+ /** `path` one step further: `[0]`, `.name`, or `["a key"]` for a key that is no identifier. */
25
+ const step = (path, key) => typeof key === 'number' ? `${path}[${key}]` : /^[A-Za-z_$][\w$]*$/.test(key) ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]`;
26
+ /** Each tool or output schema in `document`, the JSON of `file`, whose root holds a combinator. */
27
+ export const findRootCombinators = (file, document) => {
28
+ const found = [];
29
+ const atRoot = (schema, path) => {
30
+ if (!isRecord(schema))
31
+ return;
32
+ for (const keyword of COMBINATORS)
33
+ if (Object.hasOwn(schema, keyword))
34
+ found.push({ file, path, keyword });
35
+ };
36
+ const walk = (value, path) => {
37
+ if (Array.isArray(value)) {
38
+ value.forEach((item, i) => walk(item, step(path, i)));
39
+ return;
40
+ }
41
+ if (!isRecord(value))
42
+ return;
43
+ const format = value['type'] === 'json_schema' && isRecord(value['schema']);
44
+ for (const [key, child] of Object.entries(value)) {
45
+ const at = step(path, key);
46
+ if (format && key === 'schema')
47
+ atRoot(child, at);
48
+ else if (SCHEMA_KEYS.includes(key)) {
49
+ atRoot(child, at);
50
+ if (key === 'json_schema' && isRecord(child))
51
+ atRoot(child['schema'], step(at, 'schema'));
52
+ }
53
+ else
54
+ walk(child, at);
55
+ }
56
+ };
57
+ if (basename(file).endsWith('.schema.json'))
58
+ atRoot(document, '$');
59
+ else
60
+ walk(document, '$');
61
+ return found;
62
+ };
63
+ /** The refusal of one schema, one line. */
64
+ export const rootMessage = (found) => `pariksha schema-roots: ${found.file}: ${found.path}: top-level ${found.keyword}. ` +
65
+ 'The API rejects tool and output schemas with oneOf, allOf or anyOf at the root; express the condition in code after the call returns.';
66
+ /** The JSON files among `paths`: the ones this check reads. */
67
+ export const jsonFiles = (paths) => paths.filter((p) => /\.json$/i.test(p));
68
+ /** A file's text as JSON, a leading byte-order mark dropped; throws `not JSON: …`. */
69
+ export const parseJson = (text) => {
70
+ try {
71
+ return JSON.parse(text.startsWith('') ? text.slice(1) : text);
72
+ }
73
+ catch (e) {
74
+ throw new Error(`not JSON: ${e.message}`);
75
+ }
76
+ };
77
+ /** Each of `files`, read by `read` and parsed; throws on the first that cannot be, naming it. */
78
+ export const readDocuments = (files, read) => files.map((file) => {
79
+ try {
80
+ return { file, json: parseJson(read(file)) };
81
+ }
82
+ catch (e) {
83
+ throw new Error(`${file}: ${e.message}`);
84
+ }
85
+ });
86
+ /** The verdict on `documents`, each a file's path and its parsed JSON. */
87
+ export const checkSchemaRoots = (documents) => {
88
+ const found = documents.flatMap((d) => findRootCombinators(d.file, d.json));
89
+ if (found.length > 0)
90
+ return { ok: false, out: [], err: found.map(rootMessage) };
91
+ return { ok: true, out: [`pariksha schema-roots: ${documents.length} JSON file(s), no tool or output schema with oneOf, allOf or anyOf at its root`], err: [] };
92
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@veda-hq/pariksha",
3
- "version": "0.5.20",
4
- "description": "Repository checks: doc drift, rule enforcement, test-quality ratchets. Checks: absolute-paths, aggregate-gate-needs, authority-citations, boundary, codec-registry, coverage-by-type, coverage-exemptions, doc-budgets, doc-references, duplicate-tests, enforcement-claims, env-gate-switches, export-deletions, fault-injection-registry, flaky-tests, masked-exit-codes, non-synced-resources, pnpm-settings, python-floor, required-settings, shell-gates. Also markdownlint rules for Markdown that renders mangled: @veda-hq/pariksha/markdownlint.",
3
+ "version": "0.6.4",
4
+ "description": "Repository checks: doc drift, rule enforcement, test-quality ratchets. Checks: absolute-paths, aggregate-gate-needs, authority-citations, boundary, codec-registry, coverage-by-type, coverage-exemptions, doc-budgets, doc-references, duplicate-tests, enforcement-claims, env-gate-switches, export-deletions, fault-injection-registry, flaky-tests, masked-exit-codes, non-synced-resources, pnpm-settings, python-floor, required-settings, schema-roots, shell-gates. Also markdownlint rules for Markdown that renders mangled: @veda-hq/pariksha/markdownlint.",
5
5
  "keywords": [
6
6
  "repository checks",
7
7
  "lint",