@amritk/lint 0.4.2 → 0.4.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.
Files changed (39) hide show
  1. package/AI.md +16 -2
  2. package/README.md +43 -6
  3. package/dist/core/bounded-cache.d.ts +18 -0
  4. package/dist/core/bounded-cache.js +20 -0
  5. package/dist/core/filter-expression.d.ts +60 -0
  6. package/dist/core/filter-expression.js +320 -0
  7. package/dist/core/filter.d.ts +25 -0
  8. package/dist/core/filter.js +206 -0
  9. package/dist/core/index.d.ts +2 -0
  10. package/dist/core/index.js +4 -0
  11. package/dist/core/jsonpath.d.ts +1 -1
  12. package/dist/core/jsonpath.js +24 -72
  13. package/dist/functions/alphabetical.js +3 -1
  14. package/dist/functions/casing.js +13 -10
  15. package/dist/functions/pattern.js +2 -1
  16. package/dist/index.d.ts +49 -3
  17. package/dist/index.js +82 -23
  18. package/dist/parsers/depth.d.ts +21 -0
  19. package/dist/parsers/depth.js +28 -0
  20. package/dist/parsers/index.d.ts +1 -0
  21. package/dist/parsers/index.js +3 -0
  22. package/dist/parsers/json.js +16 -0
  23. package/dist/parsers/yaml.js +16 -8
  24. package/dist/rules/openapi/functions/oas-schema.d.ts +3 -3
  25. package/dist/rules/openapi/schemas/index.d.ts +3 -3
  26. package/dist/rules/openapi/schemas/index.js +10 -8
  27. package/dist/rules/openapi/schemas/oas20.d.ts +2 -0
  28. package/dist/rules/openapi/schemas/oas20.js +4 -0
  29. package/dist/rules/openapi/schemas/oas30.d.ts +2 -0
  30. package/dist/rules/openapi/schemas/oas30.js +4 -0
  31. package/dist/rules/openapi/schemas/oas31.d.ts +2 -0
  32. package/dist/rules/openapi/schemas/oas31.js +4 -0
  33. package/dist/rules/openapi/schemas/oas32.d.ts +2 -0
  34. package/dist/rules/openapi/schemas/oas32.js +4 -0
  35. package/package.json +6 -4
  36. package/dist/rules/openapi/schemas/oas20.json +0 -1
  37. package/dist/rules/openapi/schemas/oas30.json +0 -1
  38. package/dist/rules/openapi/schemas/oas31.json +0 -1
  39. package/dist/rules/openapi/schemas/oas32.json +0 -1
@@ -0,0 +1,206 @@
1
+ import { createBoundedCache } from "./bounded-cache.js";
2
+ import { parseFilterExpression } from "./filter-expression.js";
3
+ const filterRuntimeError = (message) => new Error(message);
4
+ const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5
+ const readMember = (object, name) => {
6
+ if (object === null || object === void 0) {
7
+ throw filterRuntimeError(`Cannot read "${name}" of ${object === null ? "null" : "undefined"}`);
8
+ }
9
+ if (typeof object === "string") {
10
+ if (name === "length")
11
+ return object.length;
12
+ const index = typeof name === "number" ? name : Number(name);
13
+ return Number.isInteger(index) ? object[index] : void 0;
14
+ }
15
+ if (Array.isArray(object)) {
16
+ if (name === "length")
17
+ return object.length;
18
+ const index = typeof name === "number" ? name : Number(name);
19
+ return Number.isInteger(index) ? object[index] : void 0;
20
+ }
21
+ if (isPlainObject(object))
22
+ return Object.hasOwn(object, name) ? object[name] : void 0;
23
+ return void 0;
24
+ };
25
+ const toStringArgument = (value) => {
26
+ if (typeof value === "string")
27
+ return value;
28
+ if (typeof value === "number" || typeof value === "boolean" || value === null || value === void 0) {
29
+ return String(value);
30
+ }
31
+ throw filterRuntimeError("Expected a string-like argument");
32
+ };
33
+ const toRegExpArgument = (value) => {
34
+ if (value instanceof RegExp)
35
+ return value;
36
+ if (typeof value === "string")
37
+ return new RegExp(value);
38
+ throw filterRuntimeError("Expected a regular expression argument");
39
+ };
40
+ const callMethod = (receiver, name, args) => {
41
+ const [first] = args;
42
+ switch (name) {
43
+ case "indexOf":
44
+ case "lastIndexOf": {
45
+ if (typeof receiver === "string") {
46
+ return name === "indexOf" ? receiver.indexOf(toStringArgument(first)) : receiver.lastIndexOf(toStringArgument(first));
47
+ }
48
+ if (Array.isArray(receiver))
49
+ return name === "indexOf" ? receiver.indexOf(first) : receiver.lastIndexOf(first);
50
+ throw filterRuntimeError(`"${name}" is not a function`);
51
+ }
52
+ case "includes": {
53
+ if (typeof receiver === "string")
54
+ return receiver.includes(toStringArgument(first));
55
+ if (Array.isArray(receiver))
56
+ return receiver.includes(first);
57
+ throw filterRuntimeError('"includes" is not a function');
58
+ }
59
+ case "startsWith":
60
+ if (typeof receiver !== "string")
61
+ throw filterRuntimeError('"startsWith" is not a function');
62
+ return receiver.startsWith(toStringArgument(first));
63
+ case "endsWith":
64
+ if (typeof receiver !== "string")
65
+ throw filterRuntimeError('"endsWith" is not a function');
66
+ return receiver.endsWith(toStringArgument(first));
67
+ case "match":
68
+ if (typeof receiver !== "string")
69
+ throw filterRuntimeError('"match" is not a function');
70
+ return receiver.match(toRegExpArgument(first));
71
+ case "test": {
72
+ if (!(receiver instanceof RegExp))
73
+ throw filterRuntimeError('"test" is not a function');
74
+ receiver.lastIndex = 0;
75
+ return receiver.test(toStringArgument(first));
76
+ }
77
+ case "toLowerCase":
78
+ if (typeof receiver !== "string")
79
+ throw filterRuntimeError('"toLowerCase" is not a function');
80
+ return receiver.toLowerCase();
81
+ case "toUpperCase":
82
+ if (typeof receiver !== "string")
83
+ throw filterRuntimeError('"toUpperCase" is not a function');
84
+ return receiver.toUpperCase();
85
+ case "trim":
86
+ if (typeof receiver !== "string")
87
+ throw filterRuntimeError('"trim" is not a function');
88
+ return receiver.trim();
89
+ default:
90
+ throw filterRuntimeError(`"${name}" is not a function`);
91
+ }
92
+ };
93
+ const looseEquals = (left, right) => {
94
+ if (left === null || left === void 0)
95
+ return right === null || right === void 0;
96
+ if (right === null || right === void 0)
97
+ return false;
98
+ if (typeof left === typeof right)
99
+ return left === right;
100
+ if (typeof left === "object" || typeof right === "object")
101
+ return false;
102
+ return Number(left) === Number(right);
103
+ };
104
+ const compareRelational = (operator, left, right) => {
105
+ if (typeof left === "string" && typeof right === "string") {
106
+ switch (operator) {
107
+ case "<":
108
+ return left < right;
109
+ case "<=":
110
+ return left <= right;
111
+ case ">":
112
+ return left > right;
113
+ default:
114
+ return left >= right;
115
+ }
116
+ }
117
+ const a = Number(left);
118
+ const b = Number(right);
119
+ if (Number.isNaN(a) || Number.isNaN(b))
120
+ return false;
121
+ switch (operator) {
122
+ case "<":
123
+ return a < b;
124
+ case "<=":
125
+ return a <= b;
126
+ case ">":
127
+ return a > b;
128
+ default:
129
+ return a >= b;
130
+ }
131
+ };
132
+ const evaluate = (node, context) => {
133
+ switch (node.kind) {
134
+ case "literal":
135
+ return node.value;
136
+ case "regex":
137
+ return node.value;
138
+ case "context":
139
+ return context[node.ref];
140
+ case "member":
141
+ return readMember(evaluate(node.object, context), node.name);
142
+ case "computed": {
143
+ const index = evaluate(node.index, context);
144
+ if (typeof index !== "string" && typeof index !== "number") {
145
+ throw filterRuntimeError("A computed member must be a string or a number");
146
+ }
147
+ return readMember(evaluate(node.object, context), index);
148
+ }
149
+ case "call":
150
+ return callMethod(evaluate(node.object, context), node.name, node.args.map((argument) => evaluate(argument, context)));
151
+ case "unary": {
152
+ if (node.operator === "!")
153
+ return !evaluate(node.operand, context);
154
+ if (node.operator === "void") {
155
+ evaluate(node.operand, context);
156
+ return void 0;
157
+ }
158
+ return -Number(evaluate(node.operand, context));
159
+ }
160
+ case "logical": {
161
+ const left = evaluate(node.left, context);
162
+ if (node.operator === "&&")
163
+ return left ? evaluate(node.right, context) : left;
164
+ return left ? left : evaluate(node.right, context);
165
+ }
166
+ case "comparison": {
167
+ const left = evaluate(node.left, context);
168
+ const right = evaluate(node.right, context);
169
+ switch (node.operator) {
170
+ case "===":
171
+ return left === right;
172
+ case "!==":
173
+ return left !== right;
174
+ case "==":
175
+ return looseEquals(left, right);
176
+ case "!=":
177
+ return !looseEquals(left, right);
178
+ default:
179
+ return compareRelational(node.operator, left, right);
180
+ }
181
+ }
182
+ }
183
+ };
184
+ const toFilterFn = (parsed) => {
185
+ const { node } = parsed;
186
+ return (value, property, parent, root, path, parentProperty) => {
187
+ try {
188
+ return Boolean(evaluate(node, { value, property, parent, parentProperty, path, root }));
189
+ } catch {
190
+ return false;
191
+ }
192
+ };
193
+ };
194
+ const filterCache = createBoundedCache(500);
195
+ const compileFilter = (source) => {
196
+ const cached = filterCache.get(source);
197
+ if (cached)
198
+ return cached;
199
+ const parsed = parseFilterExpression(source);
200
+ const compiled = "error" in parsed ? { error: parsed.error } : { test: toFilterFn(parsed), usesPath: parsed.usesPath };
201
+ filterCache.set(source, compiled);
202
+ return compiled;
203
+ };
204
+ export {
205
+ compileFilter
206
+ };
@@ -1,4 +1,6 @@
1
+ export { type BoundedCache, createBoundedCache } from './bounded-cache.js';
1
2
  export { createDocument, type Document, type IDocumentOptions } from './document.js';
3
+ export { type CompiledFilter, compileFilter, type FilterFn } from './filter.js';
2
4
  export { detectFormats, type Format } from './formats.js';
3
5
  export { globToRegExp, matchesGlob } from './glob.js';
4
6
  export { type CompiledPath, compileQuery, type IQueryMatch, query, queryCompiled, queryMany } from './jsonpath.js';
@@ -1,4 +1,6 @@
1
+ import { createBoundedCache } from "./bounded-cache.js";
1
2
  import { createDocument } from "./document.js";
3
+ import { compileFilter } from "./filter.js";
2
4
  import { detectFormats } from "./formats.js";
3
5
  import { globToRegExp, matchesGlob } from "./glob.js";
4
6
  import { compileQuery, query, queryCompiled, queryMany } from "./jsonpath.js";
@@ -9,7 +11,9 @@ import { createRuleset } from "./ruleset.js";
9
11
  import { createLinter } from "./runner.js";
10
12
  import { validateRuleset } from "./validate-ruleset.js";
11
13
  export {
14
+ compileFilter,
12
15
  compileQuery,
16
+ createBoundedCache,
13
17
  createDocument,
14
18
  createLinter,
15
19
  createRuleset,
@@ -1,10 +1,10 @@
1
+ import { type FilterFn } from './filter.js';
1
2
  import type { JsonPath } from './types.js';
2
3
  /** A single JSONPath match: the matched value and its concrete path from the root. */
3
4
  export type IQueryMatch = {
4
5
  value: unknown;
5
6
  path: JsonPath;
6
7
  };
7
- type FilterFn = (value: unknown, property: string | number | undefined, parent: unknown, root: unknown, path: string, parentProperty: string | number | undefined) => boolean;
8
8
  type Selector = {
9
9
  kind: 'child';
10
10
  name: string;
@@ -1,3 +1,5 @@
1
+ import { createBoundedCache } from "./bounded-cache.js";
2
+ import { compileFilter } from "./filter.js";
1
3
  const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2
4
  const normalizeSegment = (segment) => {
3
5
  if (typeof segment === "number")
@@ -17,69 +19,7 @@ const pathToJsonPathString = (path) => {
17
19
  }
18
20
  return out;
19
21
  };
20
- const compileCache = /* @__PURE__ */ new Map();
21
- const filterCache = /* @__PURE__ */ new Map();
22
- const substituteContext = (source) => {
23
- let out = "";
24
- let quote = "";
25
- for (let i = 0; i < source.length; i++) {
26
- const ch = source[i];
27
- if (quote) {
28
- out += ch;
29
- if (ch === "\\" && i + 1 < source.length) {
30
- out += source[i + 1];
31
- i++;
32
- } else if (ch === quote) {
33
- quote = "";
34
- }
35
- continue;
36
- }
37
- if (ch === '"' || ch === "'") {
38
- quote = ch;
39
- out += ch;
40
- continue;
41
- }
42
- if (ch === "@") {
43
- const rest = source.slice(i);
44
- if (rest.startsWith("@parentProperty")) {
45
- out += "_pp";
46
- i += "@parentProperty".length - 1;
47
- } else if (rest.startsWith("@parent")) {
48
- out += "_parent";
49
- i += "@parent".length - 1;
50
- } else if (rest.startsWith("@property")) {
51
- out += "_prop";
52
- i += "@property".length - 1;
53
- } else if (rest.startsWith("@path")) {
54
- out += "_path";
55
- i += "@path".length - 1;
56
- } else if (rest.startsWith("@root")) {
57
- out += "_root";
58
- i += "@root".length - 1;
59
- } else {
60
- out += "_v";
61
- }
62
- continue;
63
- }
64
- out += ch;
65
- }
66
- return out;
67
- };
68
- const compileFilter = (source) => {
69
- const cached = filterCache.get(source);
70
- if (cached)
71
- return cached;
72
- const body = substituteContext(source);
73
- let fn;
74
- try {
75
- const compiled = new Function("_v", "_prop", "_parent", "_root", "_path", "_pp", `try { return !!(${body}); } catch (_e) { return false; }`);
76
- fn = compiled;
77
- } catch {
78
- fn = () => false;
79
- }
80
- filterCache.set(source, fn);
81
- return fn;
82
- };
22
+ const compileCache = createBoundedCache(500);
83
23
  const splitUnion = (content) => {
84
24
  const parts = [];
85
25
  let depth = 0;
@@ -168,7 +108,12 @@ const bracketSelector = (content, onError) => {
168
108
  const open = trimmed.indexOf("(");
169
109
  const close = trimmed.lastIndexOf(")");
170
110
  const expr = open !== -1 && close > open ? trimmed.slice(open + 1, close) : trimmed.slice(1);
171
- return { kind: "filter", test: compileFilter(expr), source: expr, usesPath: expr.includes("@path") };
111
+ const filter = compileFilter(expr);
112
+ if ("error" in filter) {
113
+ onError(filter.error);
114
+ return { kind: "none" };
115
+ }
116
+ return { kind: "filter", test: filter.test, source: expr, usesPath: filter.usesPath };
172
117
  }
173
118
  if (trimmed.startsWith("(") && trimmed.endsWith(")")) {
174
119
  const inner = trimmed.slice(1, -1).trim();
@@ -451,14 +396,21 @@ const applySelector = (node, selector, root, out) => {
451
396
  }
452
397
  };
453
398
  const walkDescendants = (node, visit) => {
454
- visit(node);
455
- const value = node.value;
456
- if (Array.isArray(value)) {
457
- for (let idx = 0; idx < value.length; idx++)
458
- walkDescendants({ value: value[idx], parent: node, key: idx }, visit);
459
- } else if (isObject(value)) {
460
- for (const key of Object.keys(value))
461
- walkDescendants({ value: value[key], parent: node, key }, visit);
399
+ const stack = [node];
400
+ while (stack.length > 0) {
401
+ const current = stack.pop();
402
+ visit(current);
403
+ const value = current.value;
404
+ if (Array.isArray(value)) {
405
+ for (let idx = value.length - 1; idx >= 0; idx--)
406
+ stack.push({ value: value[idx], parent: current, key: idx });
407
+ } else if (isObject(value)) {
408
+ const keys = Object.keys(value);
409
+ for (let k = keys.length - 1; k >= 0; k--) {
410
+ const key = keys[k];
411
+ stack.push({ value: value[key], parent: current, key });
412
+ }
413
+ }
462
414
  }
463
415
  };
464
416
  const applySteps = (root, initial, steps) => {
@@ -1,10 +1,12 @@
1
1
  const isRecord = (value) => typeof value === "object" && value !== null;
2
2
  const isStringOrNumber = (value) => typeof value === "string" || typeof value === "number";
3
3
  const isIntegerLike = (value) => typeof value === "string" && /^(?:0|[1-9]\d*)$/.test(value);
4
+ const DECIMAL_NUMBER = /^-?\d+(?:\.\d+)?$/;
5
+ const isNumeric = (value) => typeof value === "number" || typeof value === "string" && DECIMAL_NUMBER.test(value);
4
6
  const compare = (a, b) => {
5
7
  if (isIntegerLike(a) && isIntegerLike(b))
6
8
  return Math.sign(Number(a) - Number(b));
7
- if ((typeof a === "number" || !Number.isNaN(Number(a))) && (typeof b === "number" || !Number.isNaN(Number(b)))) {
9
+ if (isNumeric(a) && isNumeric(b)) {
8
10
  return Math.min(1, Math.max(-1, Number(a) - Number(b)));
9
11
  }
10
12
  if (typeof a !== "string" || typeof b !== "string")
@@ -1,25 +1,28 @@
1
1
  const PATTERNS = {
2
- flat: "[a-z][a-z{d}]*",
3
- camel: "[a-z][a-z{d}]*(?:[A-Z{d}](?:[a-z{d}]+|$))*",
4
- pascal: "[A-Z][a-z{d}]*(?:[A-Z{d}](?:[a-z{d}]+|$))*",
2
+ flat: { body: "[a-z][a-z{d}]*" },
3
+ camel: { body: "[a-z][a-z{d}]*(?:[A-Z][a-z{d}]+)*", tail: "[A-Z]?" },
4
+ pascal: { body: "[A-Z][a-z{d}]*(?:[A-Z][a-z{d}]+)*", tail: "[A-Z]?" },
5
5
  // Segments after a separator may start with a digit, matching Spectral (so
6
6
  // "foo-2fa" is valid kebab case). The sub-pattern is `[a-z{d}]+`, not the
7
7
  // stricter `[a-z][a-z{d}]*` which would require a letter right after the sep.
8
- kebab: "[a-z][a-z{d}]*(?:-[a-z{d}]+)*",
9
- cobol: "[A-Z][A-Z{d}]*(?:-[A-Z{d}]+)*",
10
- snake: "[a-z][a-z{d}]*(?:_[a-z{d}]+)*",
11
- macro: "[A-Z][A-Z{d}]*(?:_[A-Z{d}]+)*"
8
+ kebab: { body: "[a-z][a-z{d}]*(?:-[a-z{d}]+)*", separator: "-" },
9
+ cobol: { body: "[A-Z][A-Z{d}]*(?:-[A-Z{d}]+)*", separator: "-" },
10
+ snake: { body: "[a-z][a-z{d}]*(?:_[a-z{d}]+)*", separator: "_" },
11
+ macro: { body: "[A-Z][A-Z{d}]*(?:_[A-Z{d}]+)*", separator: "_" }
12
12
  };
13
13
  const VALID_TYPES = Object.keys(PATTERNS);
14
14
  const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15
15
  const buildRegExp = (options) => {
16
16
  const digits = options.disallowDigits ? "" : "0-9";
17
- const base = PATTERNS[options.type].replace(/\{d\}/g, digits);
17
+ const pattern = PATTERNS[options.type];
18
+ const base = pattern.body.replace(/\{d\}/g, digits);
19
+ const tail = pattern.tail ?? "";
18
20
  if (!options.separator)
19
- return new RegExp(`^${base}$`);
21
+ return new RegExp(`^${base}${tail}$`);
20
22
  const sep = escapeRegExp(options.separator.char);
21
23
  const leading = options.separator.allowLeading ? `${sep}?` : "";
22
- return new RegExp(`^${leading}${base}(?:${sep}${base})*$`);
24
+ const repeated = options.separator.char === pattern.separator ? "" : `(?:${sep}${base})*`;
25
+ return new RegExp(`^${leading}${base}${repeated}${tail}$`);
23
26
  };
24
27
  const casing = (input, options) => {
25
28
  if (!options?.type)
@@ -1,4 +1,5 @@
1
- const cache = /* @__PURE__ */ new Map();
1
+ import { createBoundedCache } from "../core/bounded-cache.js";
2
+ const cache = createBoundedCache(500);
2
3
  const toRegExp = (pattern2) => {
3
4
  const cached = cache.get(pattern2);
4
5
  if (cached !== void 0) {
package/dist/index.d.ts CHANGED
@@ -5,6 +5,20 @@ export { type AliasDefinition, type CompiledPath, compileQuery, createDocument,
5
5
  export { type AppliedFix, type ApplyFixesOptions, applyFixes, createFixPlugin, type EditOp, FIX_PLUGIN_NAME, type FixContext, type Fixer, type FixerRegistry, type FixPluginData, type FixResult, type ParserFormat, } from './fix/index.js';
6
6
  export { alphabetical, builtinFunctions, type CasingType, casing, defined, enumeration, falsy, type IAlphabeticalOptions, type ICasingOptions, type IOrOptions, type ISchemaOptions, type IUnreferencedReusableObjectOptions, type IXorOptions, length, or, pattern, schema, truthy, typedEnum, undefinedFn, unreferencedReusableObject, xor, } from './functions/index.js';
7
7
  export { detectFormat, parseWithPointers } from './parsers/index.js';
8
+ /**
9
+ * An optional directory that everything a ruleset pulls off disk — `extends`
10
+ * targets and custom function modules — must resolve inside.
11
+ *
12
+ * This is opt-in and off by default, because `basePath` on its own is only a
13
+ * *resolution origin*: an `extends` of `/etc/thing.js` or `../../../elsewhere`
14
+ * resolves and loads exactly as written. See the "Trust boundary" section of the
15
+ * README — a ruleset that can name a `.js` file can run code, restricted root or
16
+ * not. This narrows *which* files it can name; it is not a sandbox.
17
+ */
18
+ export type IRulesetTrustOptions = {
19
+ /** Directory that `extends` files and custom functions must resolve under. */
20
+ restrictTo?: string;
21
+ };
8
22
  /**
9
23
  * Resolves an `extends` reference to a ruleset definition. Supports:
10
24
  * - local file paths (relative to `basePath`, or absolute): `.yaml` / `.yml` / `.json` / `.js`,
@@ -12,22 +26,40 @@ export { detectFormat, parseWithPointers } from './parsers/index.js';
12
26
  *
13
27
  * The engine ships no named built-in rulesets, so every string `extends` target
14
28
  * is a file path or an npm package.
29
+ *
30
+ * Note what this does *not* do by default: `basePath` is where resolution starts,
31
+ * not a boundary. An absolute path or a `../`-escaping one is followed, and a
32
+ * `.js` target is `require`d — meaning it runs. Pass `restrictTo` to confine
33
+ * resolution to one directory tree when the ruleset is not fully trusted.
15
34
  */
16
- export declare const resolveNamedRuleset: (name: string, basePath?: string) => ResolvedExtend;
35
+ export declare const resolveNamedRuleset: (name: string, basePath?: string, options?: IRulesetTrustOptions) => ResolvedExtend;
17
36
  /**
18
37
  * Builds a runnable {@link Ruleset} from a ruleset definition, layering the
19
38
  * built-in functions (plus any custom ones the definition declares via
20
39
  * `functions` / `functionsDir`) over the core engine and wiring up `extends`
21
40
  * resolution against files and npm packages. With no definition it produces an
22
41
  * empty ruleset (no rules run).
42
+ *
43
+ * The result is memoized per `(definition object, basePath, restrictTo)` triple,
44
+ * so linting many documents against the same definition builds it once. Treat a
45
+ * definition you have passed in as frozen: mutating it afterwards will not
46
+ * rebuild the ruleset. Pass a fresh object (or a shallow copy) when you genuinely
47
+ * want a rebuild — for example after a ruleset file on disk has changed.
23
48
  */
24
- export declare const createRuleset: (definition?: RulesetDefinition, basePath?: string) => Ruleset;
49
+ export declare const createRuleset: (definition?: RulesetDefinition, basePath?: string, options?: IRulesetTrustOptions) => Ruleset;
25
50
  /** Options for {@link lintDocument}: the document options plus ruleset controls. */
26
51
  export type ILintOptions = IDocumentOptions & {
27
52
  /** The ruleset definition to evaluate. When omitted, no rules run. */
28
53
  ruleset?: RulesetDefinition;
29
54
  /** Directory that the ruleset's string `extends` references resolve relative to. */
30
55
  rulesetBasePath?: string;
56
+ /**
57
+ * Optional root that every file the ruleset pulls in (`extends` targets,
58
+ * custom functions) must resolve under. Off by default — `rulesetBasePath` is
59
+ * only where resolution starts, not a boundary. See the README's "Trust
60
+ * boundary" section for what this does and does not protect against.
61
+ */
62
+ restrictTo?: string;
31
63
  /**
32
64
  * Produces the resolved (`$ref`-dereferenced) tree for rules with
33
65
  * `resolved: true`. The engine ships no resolver; pass one (for example
@@ -87,10 +119,24 @@ export type IFixResult = {
87
119
  output: string;
88
120
  /** Whether any fix changed the document. */
89
121
  fixed: boolean;
90
- /** The findings that were repaired, across every fix pass. */
122
+ /**
123
+ * The findings that were repaired, de-duplicated by rule code and path. Two
124
+ * fixers that undo each other would otherwise report the same finding once per
125
+ * pass, and a `--fix` summary would claim to have fixed eleven problems when
126
+ * there was only ever one.
127
+ */
91
128
  applied: AppliedFix[];
92
129
  /** Findings that remain after fixing, re-linted against the fixed document. */
93
130
  remaining: IDiagnostic[];
131
+ /**
132
+ * Whether the document stopped changing on its own. `false` means the loop hit
133
+ * {@link MAX_FIX_PASSES} while the document was still changing — usually two
134
+ * fixers undoing each other — and `output` is simply wherever it happened to
135
+ * stop. A caller reporting results should say so rather than claim success.
136
+ */
137
+ converged: boolean;
138
+ /** How many passes actually changed the document. */
139
+ passes: number;
94
140
  };
95
141
  /**
96
142
  * Lints a document and applies the supplied `fixers` repeatedly until the