Repository: stoplightio/spectral
Pull request #2308 (2022-10-14 11:00:57)

feat(core): include error codes in RulesetValidationError
Needed for https://github.com/stoplightio/platform-internal/issues/13221

**Checklist**

- [x] Tests added / updated
- [ ] Docs added / updated

**Does this PR introduce a breaking change?**

- [ ] Yes
- [x] No





Patch:
diff --git a/packages/core/src/ruleset/alias.ts b/packages/core/src/ruleset/alias.ts
index b70de064..fa806961 100644
--- a/packages/core/src/ruleset/alias.ts
+++ b/packages/core/src/ruleset/alias.ts
@@ -44,12 +44,12 @@ function _resolveAlias(
     const alias = ALIAS.exec(expression)?.[1];
 
     if (alias === void 0 || alias === null) {
-      throw new ReferenceError(`Alias must match /^#([A-Za-z0-9_-]+)/`);
+      throw new TypeError(`Alias must match /^#([A-Za-z0-9_-]+)/`);
     }
 
     if (stack.has(alias)) {
       const _stack = [...stack, alias];
-      throw new ReferenceError(`Alias "${_stack[0]}" is circular. Resolution stack: ${_stack.join(' -> ')}`);
+      throw new Error(`Alias "${_stack[0]}" is circular. Resolution stack: ${_stack.join(' -> ')}`);
     }
 
     stack.add(alias);
diff --git a/packages/core/src/ruleset/function.ts b/packages/core/src/ruleset/function.ts
index 14a91de5..b187fc13 100644
--- a/packages/core/src/ruleset/function.ts
+++ b/packages/core/src/ruleset/function.ts
@@ -19,7 +19,11 @@ addFormats(ajv);
 
 export class RulesetFunctionValidationError extends RulesetValidationError {
   constructor(fn: string, error: ErrorObject) {
-    super(RulesetFunctionValidationError.printMessage(fn, error), error.instancePath.slice(1).split('/'));
+    super(
+      'invalid-function-options',
+      RulesetFunctionValidationError.printMessage(fn, error),
+      error.instancePath.slice(1).split('/'),
+    );
   }
 
   private static printMessage(fn: string, error: ErrorObject): string {
@@ -150,7 +154,11 @@ export function createRulesetFunction<I, O>(
     }
 
     if (options === null) {
-      throw new RulesetValidationError(`"${fn.name || '<unknown>'}" function does not accept any options`, []);
+      throw new RulesetValidationError(
+        'invalid-function-options',
+        `"${fn.name || '<unknown>'}" function does not accept any options`,
+        [],
+      );
     } else if (
       'errors' in validateOptions &&
       Array.isArray(validateOptions.errors) &&
@@ -160,7 +168,11 @@ export function createRulesetFunction<I, O>(
         validateOptions.errors.map(error => new RulesetFunctionValidationError(fn.name || '<unknown>', error)),
       );
     } else {
-      throw new RulesetValidationError(`"functionOptions" of "${fn.name || '<unknown>'}" function must be valid`, []);
+      throw new RulesetValidationError(
+        'invalid-function-options',
+        `"functionOptions" of "${fn.name || '<unknown>'}" function must be valid`,
+        [],
+      );
     }
   };
 
diff --git a/packages/core/src/ruleset/meta/ruleset.schema.json b/packages/core/src/ruleset/meta/ruleset.schema.json
index 553aa01f..c5965487 100644
--- a/packages/core/src/ruleset/meta/ruleset.schema.json
+++ b/packages/core/src/ruleset/meta/ruleset.schema.json
@@ -101,7 +101,7 @@
               },
               "required": ["files"],
               "errorMessage": {
-                "type": "must be a override, i.e. { \"files\": [\"v2/**/*.json\"], \"rules\": {} }"
+                "type": "must be an override, i.e. { \"files\": [\"v2/**/*.json\"], \"rules\": {} }"
               }
             },
             {
diff --git a/packages/core/src/ruleset/validation/assertions.ts b/packages/core/src/ruleset/validation/assertions.ts
index 704c1f34..812f3b09 100644
--- a/packages/core/src/ruleset/validation/assertions.ts
+++ b/packages/core/src/ruleset/validation/assertions.ts
@@ -9,11 +9,15 @@ export function assertValidRuleset(
   format: 'js' | 'json' = 'js',
 ): asserts ruleset is RulesetDefinition {
   if (!isPlainObject(ruleset)) {
-    throw new RulesetValidationError('Provided ruleset is not an object', []);
+    throw new RulesetValidationError('invalid-ruleset-definition', 'Provided ruleset is not an object', []);
   }
 
   if (!('rules' in ruleset) && !('extends' in ruleset) && !('overrides' in ruleset)) {
-    throw new RulesetValidationError('Ruleset must have rules or extends or overrides defined', []);
+    throw new RulesetValidationError(
+      'invalid-ruleset-definition',
+      'Ruleset must have rules or extends or overrides defined',
+      [],
+    );
   }
 
   const validate = createValidator(format);
@@ -29,6 +33,6 @@ function isRuleDefinition(rule: FileRuleDefinition): rule is RuleDefinition {
 
 export function assertValidRule(rule: FileRuleDefinition, name: string): asserts rule is RuleDefinition {
   if (!isRuleDefinition(rule)) {
-    throw new RulesetValidationError('Rule definition expected', ['rules', name]);
+    throw new RulesetValidationError('invalid-rule-definition', 'Rule definition expected', ['rules', name]);
   }
 }
diff --git a/packages/core/src/ruleset/validation/errors.ts b/packages/core/src/ruleset/validation/errors.ts
index a80e9da0..af866078 100644
--- a/packages/core/src/ruleset/validation/errors.ts
+++ b/packages/core/src/ruleset/validation/errors.ts
@@ -2,10 +2,31 @@ import type { ErrorObject } from 'ajv';
 import type { IDiagnostic, JsonPath } from '@stoplight/types';
 import { isAggregateError } from '../../guards/isAggregateError';
 
-type RulesetValidationSingleError = Pick<IDiagnostic, 'message' | 'path'>;
+export type RulesetValidationErrorCode =
+  | 'generic-validation-error'
+  | 'invalid-ruleset-definition'
+  | 'invalid-parser-options-definition'
+  | 'invalid-alias-definition'
+  | 'invalid-extend-definition'
+  | 'invalid-rule-definition'
+  | 'invalid-override-definition'
+  | 'invalid-function-options'
+  | 'invalid-given-definition'
+  | 'invalid-severity'
+  | 'invalid-format'
+  | 'undefined-function'
+  | 'undefined-alias';
 
-export class RulesetValidationError extends Error implements RulesetValidationSingleError {
-  constructor(public readonly message: string, public readonly path: JsonPath) {
+interface IRulesetValidationSingleError extends Pick<IDiagnostic, 'message' | 'path'> {
+  code: RulesetValidationErrorCode;
+}
+
+export class RulesetValidationError extends Error implements IRulesetValidationSingleError {
+  constructor(
+    public readonly code: RulesetValidationErrorCode,
+    public readonly message: string,
+    public readonly path: JsonPath,
+  ) {
     super(message);
   }
 }
@@ -55,11 +76,14 @@ export function convertAjvErrors(errors: ErrorObject[]): RulesetValidationError[
     filteredErrors.push(error);
   }
 
-  return filteredErrors.flatMap(error =>
-    error.keyword === 'x-spectral-runtime'
-      ? flatErrors(error.params.errors)
-      : new RulesetValidationError(error.message ?? 'unknown error', error.instancePath.slice(1).split('/')),
-  );
+  return filteredErrors.flatMap(error => {
+    if (error.keyword === 'x-spectral-runtime') {
+      return flatErrors(error.params.errors);
+    }
+
+    const path = error.instancePath.slice(1).split('/');
+    return new RulesetValidationError(inferErrorCode(path, error.keyword), error.message ?? 'unknown error', path);
+  });
 }
 
 function flatErrors(error: RulesetValidationError | AggregateError): RulesetValidationError | RulesetValidationError[] {
@@ -69,3 +93,72 @@ function flatErrors(error: RulesetValidationError | AggregateError): RulesetVali
 
   return error;
 }
+
+function inferErrorCode(path: string[], keyword: string): RulesetValidationErrorCode {
+  if (path.length === 0) {
+    return 'generic-validation-error';
+  }
+
+  if (path.length === 1 && keyword !== 'errorMessage') {
+    return 'invalid-ruleset-definition';
+  }
+
+  switch (path[0]) {
+    case 'rules':
+      return inferErrorCodeFromRulesError(path);
+    case 'parserOptions':
+      return 'invalid-parser-options-definition';
+    case 'aliases':
+      return inferErrorCodeFromAliasesError(path);
+    case 'extends':
+      return 'invalid-extend-definition';
+    case 'overrides':
+      return inferErrorCodeFromOverrideError(path, keyword);
+    case 'formats':
+      if (path.length === 1) {
+        return 'invalid-ruleset-definition';
+      }
+
+      return 'invalid-format';
+    default:
+      return 'generic-validation-error';
+  }
+}
+
+function inferErrorCodeFromRulesError(path: string[]): RulesetValidationErrorCode {
+  if (path.length === 3 && path[2] === 'severity') {
+    return 'invalid-severity';
+  }
+
+  if (path.length === 4 && path[2] === 'formats') {
+    return 'invalid-format';
+  }
+
+  if (path.length === 4 && path[2] === 'given') {
+    return 'invalid-given-definition';
+  }
+
+  return 'invalid-rule-definition';
+}
+
+function inferErrorCodeFromOverrideError(path: string[], keyword: string): RulesetValidationErrorCode {
+  if (path.length >= 3) {
+    return inferErrorCode(path.slice(2), keyword);
+  }
+
+  return 'invalid-override-definition';
+}
+
+function inferErrorCodeFromAliasesError(path: string[]): RulesetValidationErrorCode {
+  if (path.length === 6) {
+    if (path[4] === 'given') {
+      return 'invalid-given-definition';
+    }
+
+    if (path[4] === 'formats') {
+      return 'invalid-format';
+    }
+  }
+
+  return 'invalid-alias-definition';
+}
diff --git a/packages/core/src/ruleset/validation/index.ts b/packages/core/src/ruleset/validation/index.ts
index b7eb566c..1b87362e 100644
--- a/packages/core/src/ruleset/validation/index.ts
+++ b/packages/core/src/ruleset/validation/index.ts
@@ -1,2 +1,2 @@
-export { RulesetValidationError } from './errors';
+export { RulesetValidationError, RulesetValidationErrorCode } from './errors';
 export { assertValidRuleset } from './assertions';
diff --git a/packages/core/src/ruleset/validation/validators/alias.ts b/packages/core/src/ruleset/validation/validators/alias.ts
index 2d6f494e..e7de053a 100644
--- a/packages/core/src/ruleset/validation/validators/alias.ts
+++ b/packages/core/src/ruleset/validation/validators/alias.ts
@@ -2,7 +2,8 @@ import { isPlainObject } from '@stoplight/json';
 import { get } from 'lodash';
 import { resolveAlias } from '../../alias';
 import { Formats } from '../../formats';
-import { wrapError } from './common/error';
+import { toParsedPath, wrapError } from './common/error';
+import { RulesetValidationError } from '../errors';
 
 function getOverrides(overrides: unknown, key: string): Record<string, unknown> | null {
   if (!Array.isArray(overrides)) return null;
@@ -20,8 +21,9 @@ export function validateAlias(
   alias: string,
   path: string,
 ): Error | void {
+  const parsedPath = toParsedPath(path);
+
   try {
-    const parsedPath = path.slice(1).split('/');
     const formats: unknown = get(ruleset, [...parsedPath.slice(0, parsedPath.indexOf('rules') + 2), 'formats']);
 
     const aliases =
@@ -34,6 +36,10 @@ export function validateAlias(
 
     resolveAlias(aliases ?? null, alias, Array.isArray(formats) ? new Formats(formats) : null);
   } catch (ex) {
+    if (ex instanceof ReferenceError) {
+      return new RulesetValidationError('undefined-alias', ex.message, parsedPath);
+    }
+
     return wrapError(ex, path);
   }
 }
diff --git a/packages/core/src/ruleset/validation/validators/common/error.ts b/packages/core/src/ruleset/validation/validators/common/error.ts
index 21406598..278eb2cc 100644
--- a/packages/core/src/ruleset/validation/validators/common/error.ts
+++ b/packages/core/src/ruleset/validation/validators/common/error.ts
@@ -10,11 +10,11 @@ function toRulesetValidationError(this: ReadonlyArray<string>, ex: unknown): Rul
     return ex;
   }
 
-  return new RulesetValidationError(isError(ex) ? ex.message : String(ex), [...this]);
+  return new RulesetValidationError('generic-validation-error', isError(ex) ? ex.message : String(ex), [...this]);
 }
 
 export function wrapError(ex: unknown, path: string): Error {
-  const parsedPath = path.slice(1).split('/');
+  const parsedPath = toParsedPath(path);
 
   if (isAggregateError(ex)) {
     return new AggregateError(ex.errors.map(toRulesetValidationError, parsedPath));
@@ -22,3 +22,7 @@ export function wrapError(ex: unknown, path: string): Error {
 
   return toRulesetValidationError.call(parsedPath, ex);
 }
+
+export function toParsedPath(path: string): string[] {
+  return path.slice(1).split('/');
+}
diff --git a/packages/core/src/ruleset/validation/validators/function.ts b/packages/core/src/ruleset/validation/validators/function.ts
index 2c558d99..83442522 100644
--- a/packages/core/src/ruleset/validation/validators/function.ts
+++ b/packages/core/src/ruleset/validation/validators/function.ts
@@ -1,11 +1,12 @@
 import type { RulesetFunction, RulesetFunctionWithValidator } from '../../../types';
-import { wrapError } from './common/error';
+import { toParsedPath, wrapError } from './common/error';
+import { RulesetValidationError } from '../errors';
 
 function assertRulesetFunction(
   maybeRulesetFunction: unknown,
 ): asserts maybeRulesetFunction is RulesetFunction | RulesetFunctionWithValidator {
   if (typeof maybeRulesetFunction !== 'function') {
-    throw Error('Function is not defined');
+    throw ReferenceError('Function is not defined');
   }
 }
 
@@ -22,6 +23,10 @@ export function validateFunction(
     const validator: RulesetFunctionWithValidator['validator'] = fn.validator.bind(fn);
     validator(opts);
   } catch (ex) {
+    if (ex instanceof ReferenceError) {
+      return new RulesetValidationError('undefined-function', ex.message, toParsedPath(path));
+    }
+
     return wrapError(ex, path);
   }
 }


Test patch:
diff --git a/packages/cli/src/services/__tests__/linter.test.ts b/packages/cli/src/services/__tests__/linter.test.ts
index 118a9354..97f17507 100644
--- a/packages/cli/src/services/__tests__/linter.test.ts
+++ b/packages/cli/src/services/__tests__/linter.test.ts
@@ -229,16 +229,18 @@ describe('Linter service', () => {
           run(`lint ${validCustomOas3SpecPath} -r ${invalidNestedRulesetPath}`),
         ).rejects.toThrowAggregateError(
           new AggregateError([
-            new RulesetValidationError('the rule must have at least "given" and "then" properties', [
-              'rules',
-              'rule-without-given-nor-them',
-            ]),
-            new RulesetValidationError('allowed types are "style" and "validation"', [
+            new RulesetValidationError(
+              'invalid-rule-definition',
+              'the rule must have at least "given" and "then" properties',
+              ['rules', 'rule-without-given-nor-them'],
+            ),
+            new RulesetValidationError('invalid-rule-definition', 'allowed types are "style" and "validation"', [
               'rules',
               'rule-with-invalid-enum',
               'type',
             ]),
             new RulesetValidationError(
+              'invalid-severity',
               'the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"',
               ['rules', 'rule-with-invalid-enum', 'severity'],
             ),
@@ -257,16 +259,18 @@ describe('Linter service', () => {
       it('outputs "invalid ruleset" error', () => {
         return expect(run(`lint ${validOas3SpecPath} -r ${invalidRulesetPath}`)).rejects.toThrowAggregateError(
           new AggregateError([
-            new RulesetValidationError('the rule must have at least "given" and "then" properties', [
-              'rules',
-              'rule-without-given-nor-them',
-            ]),
-            new RulesetValidationError('allowed types are "style" and "validation"', [
+            new RulesetValidationError(
+              'invalid-rule-definition',
+              'the rule must have at least "given" and "then" properties',
+              ['rules', 'rule-without-given-nor-them'],
+            ),
+            new RulesetValidationError('invalid-rule-definition', 'allowed types are "style" and "validation"', [
               'rules',
               'rule-with-invalid-enum',
               'type',
             ]),
             new RulesetValidationError(
+              'invalid-severity',
               'the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"',
               ['rules', 'rule-with-invalid-enum', 'severity'],
             ),
diff --git a/packages/core/src/ruleset/__tests__/ruleset.test.ts b/packages/core/src/ruleset/__tests__/ruleset.test.ts
index 7bfdb813..7068adb2 100644
--- a/packages/core/src/ruleset/__tests__/ruleset.test.ts
+++ b/packages/core/src/ruleset/__tests__/ruleset.test.ts
@@ -262,7 +262,11 @@ describe('Ruleset', () => {
   describe('error handling', () => {
     it('given empty ruleset, should throw a user friendly error', () => {
       expect(() => new Ruleset({})).toThrowError(
-        new RulesetValidationError('Ruleset must have rules or extends or overrides defined', []),
+        new RulesetValidationError(
+          'invalid-ruleset-definition',
+          'Ruleset must have rules or extends or overrides defined',
+          [],
+        ),
       );
     });
   });
@@ -1271,7 +1275,11 @@ describe('Ruleset', () => {
           }),
       ).toThrowAggregateError(
         new AggregateError([
-          new RulesetValidationError('Alias "PathItem-" does not exist', ['rules', 'valid-path', 'given']),
+          new RulesetValidationError('undefined-alias', 'Alias "PathItem-" does not exist', [
+            'rules',
+            'valid-path',
+            'given',
+          ]),
         ]),
       );
     });
@@ -1298,6 +1306,7 @@ describe('Ruleset', () => {
       ).toThrowAggregateError(
         new AggregateError([
           new RulesetValidationError(
+            'generic-validation-error',
             'Alias "Test" is circular. Resolution stack: Test -> Contact -> Info -> Root -> Info',
             ['rules', 'valid-path', 'given'],
           ),
@@ -1335,9 +1344,17 @@ describe('Ruleset', () => {
           }),
       ).toThrowAggregateError(
         new AggregateError([
-          new RulesetValidationError('Alias "PathItem" does not exist', ['rules', 'valid-path', 'given']),
-          new RulesetValidationError('Alias "Name" does not exist', ['rules', 'valid-name-and-description', 'given']),
-          new RulesetValidationError(`Alias "Description" does not exist`, [
+          new RulesetValidationError('undefined-alias', 'Alias "PathItem" does not exist', [
+            'rules',
+            'valid-path',
+            'given',
+          ]),
+          new RulesetValidationError('undefined-alias', 'Alias "Name" does not exist', [
+            'rules',
+            'valid-name-and-description',
+            'given',
+          ]),
+          new RulesetValidationError('undefined-alias', `Alias "Description" does not exist`, [
             'rules',
             'valid-name-and-description',
             'given',
diff --git a/packages/core/src/ruleset/validation/__tests__/validation.test.ts b/packages/core/src/ruleset/validation/__tests__/validation.test.ts
index a729f2f6..a78f8eb6 100644
--- a/packages/core/src/ruleset/validation/__tests__/validation.test.ts
+++ b/packages/core/src/ruleset/validation/__tests__/validation.test.ts
@@ -40,20 +40,21 @@ describe('JS Ruleset Validation', () => {
   it('given invalid ruleset, throws', () => {
     expect(assertValidRuleset.bind(null, invalidRuleset)).toThrowAggregateError(
       new AggregateError([
-        new RulesetValidationError('the rule must have at least "given" and "then" properties', [
-          'rules',
-          'no-given-no-then',
-        ]),
-        new RulesetValidationError('allowed types are "style" and "validation"', [
+        new RulesetValidationError(
+          'invalid-rule-definition',
+          'the rule must have at least "given" and "then" properties',
+          ['rules', 'no-given-no-then'],
+        ),
+        new RulesetValidationError('invalid-rule-definition', 'allowed types are "style" and "validation"', [
           'rules',
           'rule-with-invalid-enum',
           'type',
         ]),
-        new RulesetValidationError('the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"', [
-          'rules',
-          'rule-with-invalid-enum',
-          'severity',
-        ]),
+        new RulesetValidationError(
+          'invalid-severity',
+          'the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"',
+          ['rules', 'rule-with-invalid-enum', 'severity'],
+        ),
       ]),
     );
   });
@@ -66,7 +67,9 @@ describe('JS Ruleset Validation', () => {
     'given invalid %s documentationUrl in a rule, throws',
     documentationUrl => {
       expect(assertValidRuleset.bind(null, { documentationUrl, rules: {} })).toThrowAggregateError(
-        new AggregateError([new RulesetValidationError('must be a valid URL', ['documentationUrl'])]),
+        new AggregateError([
+          new RulesetValidationError('generic-validation-error', 'must be a valid URL', ['documentationUrl']),
+        ]),
       );
 
       expect(
@@ -82,7 +85,13 @@ describe('JS Ruleset Validation', () => {
           },
         }),
       ).toThrowAggregateError(
-        new AggregateError([new RulesetValidationError('must be a valid URL', ['rules', 'rule', 'documentationUrl'])]),
+        new AggregateError([
+          new RulesetValidationError('invalid-rule-definition', 'must be a valid URL', [
+            'rules',
+            'rule',
+            'documentationUrl',
+          ]),
+        ]),
       );
     },
   );
@@ -154,13 +163,23 @@ describe('JS Ruleset Validation', () => {
   it.each<[unknown, RulesetValidationError[]]>([
     [
       [[{ rules: {} }, 'test']],
-      [new RulesetValidationError('allowed types are "off", "recommended" and "all"', ['extends', '0', '1'])],
+      [
+        new RulesetValidationError('invalid-extend-definition', 'allowed types are "off", "recommended" and "all"', [
+          'extends',
+          '0',
+          '1',
+        ]),
+      ],
     ],
     [
       [[{ rules: {} }, 'test'], 'foo'],
       [
-        new RulesetValidationError('must be a valid ruleset', ['extends', '1']),
-        new RulesetValidationError('allowed types are "off", "recommended" and "all"', ['extends', '0', '1']),
+        new RulesetValidationError('invalid-extend-definition', 'must be a valid ruleset', ['extends', '1']),
+        new RulesetValidationError('invalid-extend-definition', 'allowed types are "off", "recommended" and "all"', [
+          'extends',
+          '0',
+          '1',
+        ]),
       ],
     ],
   ])('recognizes invalid array-ish extends syntax %p', (_extends, errors) => {
@@ -184,12 +203,20 @@ describe('JS Ruleset Validation', () => {
     [
       [2, 'a'],
       new AggregateError([
-        new RulesetValidationError('must be a valid format', ['formats', '0']),
-        new RulesetValidationError('must be a valid format', ['formats', '1']),
+        new RulesetValidationError('invalid-format', 'must be a valid format', ['formats', '0']),
+        new RulesetValidationError('invalid-format', 'must be a valid format', ['formats', '1']),
+      ]),
+    ],
+    [
+      2,
+      new AggregateError([
+        new RulesetValidationError('invalid-ruleset-definition', 'must be an array of formats', ['formats']),
       ]),
     ],
-    [2, new AggregateError([new RulesetValidationError('must be an array of formats', ['formats'])])],
-    [[''], new AggregateError([new RulesetValidationError('must be a valid format', ['formats', '0'])])],
+    [
+      [''],
+      new AggregateError([new RulesetValidationError('invalid-format', 'must be a valid format', ['formats', '0'])]),
+    ],
   ])('recognizes invalid ruleset %p formats syntax', (formats, error) => {
     expect(
       assertValidRuleset.bind(null, {
@@ -220,11 +247,20 @@ describe('JS Ruleset Validation', () => {
     [
       [2, 'a'],
       new AggregateError([
-        new RulesetValidationError('must be a valid format', ['rules', 'rule', 'formats', '0']),
-        new RulesetValidationError('must be a valid format', ['rules', 'rule', 'formats', '1']),
+        new RulesetValidationError('invalid-format', 'must be a valid format', ['rules', 'rule', 'formats', '0']),
+        new RulesetValidationError('invalid-format', 'must be a valid format', ['rules', 'rule', 'formats', '1']),
+      ]),
+    ],
+    [
+      2,
+      new AggregateError([
+        new RulesetValidationError('invalid-rule-definition', 'must be an array of formats', [
+          'rules',
+          'rule',
+          'formats',
+        ]),
       ]),
     ],
-    [2, new AggregateError([new RulesetValidationError('must be an array of formats', ['rules', 'rule', 'formats'])])],
   ])('recognizes invalid rule %p formats syntax', (formats, error) => {
     expect(
       assertValidRuleset.bind(null, {
@@ -247,7 +283,9 @@ describe('JS Ruleset Validation', () => {
         assertValidRuleset.bind(null, {
           overrides: null,
         }),
-      ).toThrowAggregateError(new AggregateError([new RulesetValidationError('must be array', ['overrides'])]));
+      ).toThrowAggregateError(
+        new AggregateError([new RulesetValidationError('invalid-ruleset-definition', 'must be array', ['overrides'])]),
+      );
     });
 
     it('given an empty overrides, throws', () => {
@@ -255,7 +293,11 @@ describe('JS Ruleset Validation', () => {
         assertValidRuleset.bind(null, {
           overrides: [],
         }),
-      ).toThrowAggregateError(new AggregateError([new RulesetValidationError('must not be empty', ['overrides'])]));
+      ).toThrowAggregateError(
+        new AggregateError([
+          new RulesetValidationError('invalid-override-definition', 'must not be empty', ['overrides']),
+        ]),
+      );
     });
 
     it('given an invalid pattern, throws', () => {
@@ -265,10 +307,11 @@ describe('JS Ruleset Validation', () => {
         }),
       ).toThrowAggregateError(
         new AggregateError([
-          new RulesetValidationError('must be a override, i.e. { "files": ["v2/**/*.json"], "rules": {} }', [
-            'overrides',
-            '0',
-          ]),
+          new RulesetValidationError(
+            'invalid-override-definition',
+            'must be an override, i.e. { "files": ["v2/**/*.json"], "rules": {} }',
+            ['overrides', '0'],
+          ),
         ]),
       );
     });
@@ -281,25 +324,35 @@ describe('JS Ruleset Validation', () => {
       it.each<[Partial<RulesetDefinition>, RulesetValidationError]>([
         [
           { extends: [rulesetA] },
-          new RulesetValidationError('must contain rules when JSON Pointers are defined', ['overrides', '0']),
+          new RulesetValidationError(
+            'invalid-override-definition',
+            'must contain rules when JSON Pointers are defined',
+            ['overrides', '0'],
+          ),
         ],
         [
           { formats: [formatB] },
-          new RulesetValidationError('must contain rules when JSON Pointers are defined', ['overrides', '0']),
+          new RulesetValidationError(
+            'invalid-override-definition',
+            'must contain rules when JSON Pointers are defined',
+            ['overrides', '0'],
+          ),
         ],
         [
           { rules: {}, formats: [formatB] },
-          new RulesetValidationError('must not override any other property than rules when JSON Pointers are defined', [
-            'overrides',
-            '0',
-          ]),
+          new RulesetValidationError(
+            'invalid-override-definition',
+            'must not override any other property than rules when JSON Pointers are defined',
+            ['overrides', '0'],
+          ),
         ],
         [
           { rules: {}, extends: [rulesetA] },
-          new RulesetValidationError('must not override any other property than rules when JSON Pointers are defined', [
-            'overrides',
-            '0',
-          ]),
+          new RulesetValidationError(
+            'invalid-override-definition',
+            'must not override any other property than rules when JSON Pointers are defined',
+            ['overrides', '0'],
+          ),
         ],
         [
           {
@@ -313,6 +366,7 @@ describe('JS Ruleset Validation', () => {
             },
           },
           new RulesetValidationError(
+            'invalid-rule-definition',
             'the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"',
             ['overrides', '0', 'rules', 'definition'],
           ),
@@ -388,7 +442,9 @@ describe('JS Ruleset Validation', () => {
           rules: {},
           aliases: null,
         }),
-      ).toThrowAggregateError(new AggregateError([new RulesetValidationError('must be object', ['aliases'])]));
+      ).toThrowAggregateError(
+        new AggregateError([new RulesetValidationError('invalid-ruleset-definition', 'must be object', ['aliases'])]),
+      );
     });
 
     it.each([null, 5])('recognizes %p as an invalid type of aliases', value => {
@@ -402,6 +458,7 @@ describe('JS Ruleset Validation', () => {
       ).toThrowAggregateError(
         new AggregateError([
           new RulesetValidationError(
+            'invalid-alias-definition',
             'must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset',
             ['aliases', 'alias', '0'],
           ),
@@ -420,6 +477,7 @@ describe('JS Ruleset Validation', () => {
       ).toThrowAggregateError(
         new AggregateError([
           new RulesetValidationError(
+            'invalid-alias-definition',
             'to avoid confusion the name must match /^[A-Za-z][A-Za-z0-9_-]*$/ regular expression',
             ['aliases'],
           ),
@@ -432,6 +490,7 @@ describe('JS Ruleset Validation', () => {
         [''],
         [
           new RulesetValidationError(
+            'invalid-alias-definition',
             'must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset',
             ['aliases', 'PathItem', '0'],
           ),
@@ -441,16 +500,26 @@ describe('JS Ruleset Validation', () => {
         ['foo'],
         [
           new RulesetValidationError(
+            'invalid-alias-definition',
             'must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset',
             ['aliases', 'PathItem', '0'],
           ),
         ],
       ],
-      [[], [new RulesetValidationError('must be a non-empty array of expressions', ['aliases', 'PathItem'])]],
+      [
+        [],
+        [
+          new RulesetValidationError('invalid-alias-definition', 'must be a non-empty array of expressions', [
+            'aliases',
+            'PathItem',
+          ]),
+        ],
+      ],
       [
         [0],
         [
           new RulesetValidationError(
+            'invalid-alias-definition',
             'must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset',
             ['aliases', 'PathItem', '0'],
           ),
@@ -478,10 +547,11 @@ describe('JS Ruleset Validation', () => {
           }),
         ).toThrowAggregateError(
           new AggregateError([
-            new RulesetValidationError('targets must be present and have at least a single alias definition', [
-              'aliases',
-              'alias',
-            ]),
+            new RulesetValidationError(
+              'invalid-alias-definition',
+              'targets must be present and have at least a single alias definition',
+              ['aliases', 'alias'],
+            ),
           ]),
         );
       });
@@ -551,7 +621,13 @@ describe('JS Ruleset Validation', () => {
             },
           }),
         ).toThrowAggregateError(
-          new AggregateError([new RulesetValidationError('must be array', ['aliases', 'SchemaObject', 'targets'])]),
+          new AggregateError([
+            new RulesetValidationError('invalid-alias-definition', 'must be array', [
+              'aliases',
+              'SchemaObject',
+              'targets',
+            ]),
+          ]),
         );
       });
 
@@ -567,11 +643,11 @@ describe('JS Ruleset Validation', () => {
           }),
         ).toThrowAggregateError(
           new AggregateError([
-            new RulesetValidationError('targets must have at least a single alias definition', [
-              'aliases',
-              'SchemaObject',
-              'targets',
-            ]),
+            new RulesetValidationError(
+              'invalid-alias-definition',
+              'targets must have at least a single alias definition',
+              ['aliases', 'SchemaObject', 'targets'],
+            ),
           ]),
         );
       });
@@ -588,12 +664,11 @@ describe('JS Ruleset Validation', () => {
           }),
         ).toThrowAggregateError(
           new AggregateError([
-            new RulesetValidationError('a valid target must contain given and non-empty formats', [
-              'aliases',
-              'SchemaObject',
-              'targets',
-              '0',
-            ]),
+            new RulesetValidationError(
+              'invalid-alias-definition',
+              'a valid target must contain given and non-empty formats',
+              ['aliases', 'SchemaObject', 'targets', '0'],
+            ),
           ]),
         );
       });
@@ -619,7 +694,7 @@ describe('JS Ruleset Validation', () => {
           }),
         ).toThrowAggregateError(
           new AggregateError([
-            new RulesetValidationError('must be a valid format', [
+            new RulesetValidationError('invalid-format', 'must be a valid format', [
               'aliases',
               'SchemaObject',
               'targets',
@@ -627,7 +702,7 @@ describe('JS Ruleset Validation', () => {
               'formats',
               '0',
             ]),
-            new RulesetValidationError('must be a valid format', [
+            new RulesetValidationError('invalid-format', 'must be a valid format', [
               'aliases',
               'SchemaObject',
               'targets',
@@ -661,6 +736,7 @@ describe('JS Ruleset Validation', () => {
         ).toThrowAggregateError(
           new AggregateError([
             new RulesetValidationError(
+              'invalid-given-definition',
               'must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset',
               ['aliases', 'SchemaObject', 'targets', '1', 'given', '0'],
             ),
@@ -671,35 +747,52 @@ describe('JS Ruleset Validation', () => {
   });
 
   describe('then validation', () => {
-    describe('custom function', () => {
-      it('given valid then, does not complain', () => {
-        expect(
-          assertValidRuleset.bind(null, {
-            rules: {
-              rule: {
-                given: '$',
-                then: {
-                  function: truthy,
-                },
+    it('given undefined function, throws', () => {
+      expect(
+        assertValidRuleset.bind(null, {
+          rules: {
+            rule: {
+              given: '$',
+              then: {
+                function: void 0,
               },
             },
-          }),
-        ).not.toThrow();
+          },
+        }),
+      ).toThrowAggregateError(
+        new AggregateError([
+          new RulesetValidationError('undefined-function', 'Function is not defined', ['rules', 'rule', 'then']),
+        ]),
+      );
+    });
 
-        expect(
-          assertValidRuleset.bind(null, {
-            rules: {
-              rule: {
-                given: '$',
-                then: {
-                  field: 'test',
-                  function: truthy,
-                },
+    it('given valid then, does not complain', () => {
+      expect(
+        assertValidRuleset.bind(null, {
+          rules: {
+            rule: {
+              given: '$',
+              then: {
+                function: truthy,
               },
             },
-          }),
-        ).not.toThrow();
-      });
+          },
+        }),
+      ).not.toThrow();
+
+      expect(
+        assertValidRuleset.bind(null, {
+          rules: {
+            rule: {
+              given: '$',
+              then: {
+                field: 'test',
+                function: truthy,
+              },
+            },
+          },
+        }),
+      ).not.toThrow();
     });
   });
 
@@ -734,13 +827,15 @@ describe('JS Ruleset Validation', () => {
             duplicateKeys: 'foo',
           },
         }),
-      ).toThrowAggregateError(
+      ).toThrow(
         new AggregateError([
           new RulesetValidationError(
+            'invalid-parser-options-definition',
             'the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"',
             ['parserOptions', 'duplicateKeys'],
           ),
           new RulesetValidationError(
+            'invalid-parser-options-definition',
             'the value has to be one of: 0, 1, 2, 3 or "error", "warn", "info", "hint", "off"',
             ['parserOptions', 'incompatibleValues'],
           ),
@@ -768,13 +863,23 @@ describe('JSON Ruleset Validation', () => {
   it.each<[unknown, RulesetValidationError[]]>([
     [
       [['test', 'test']],
-      [new RulesetValidationError('allowed types are "off", "recommended" and "all"', ['extends', '0', '1'])],
+      [
+        new RulesetValidationError('invalid-extend-definition', 'allowed types are "off", "recommended" and "all"', [
+          'extends',
+          '0',
+          '1',
+        ]),
+      ],
     ],
     [
       [['bar', 'test'], {}],
       [
-        new RulesetValidationError('must be string', ['extends', '1']),
-        new RulesetValidationError(`allowed types are "off", "recommended" and "all"`, ['extends', '0', '1']),
+        new RulesetValidationError('invalid-extend-definition', 'must be string', ['extends', '1']),
+        new RulesetValidationError('invalid-extend-definition', `allowed types are "off", "recommended" and "all"`, [
+          'extends',
+          '0',
+          '1',
+        ]),
       ],
     ],
   ])('recognizes invalid array-ish extends syntax %p', (_extends, errors) => {
@@ -806,12 +911,12 @@ describe('JSON Ruleset Validation', () => {
     [
       [2, 'a'],
       [
-        new RulesetValidationError('must be a valid format', ['formats', '0']),
-        new RulesetValidationError('must be a valid format', ['formats', '1']),
+        new RulesetValidationError('invalid-format', 'must be a valid format', ['formats', '0']),
+        new RulesetValidationError('invalid-format', 'must be a valid format', ['formats', '1']),
       ],
     ],
-    [2, [new RulesetValidationError('must be an array of formats', ['formats'])]],
-    [[''], [new RulesetValidationError('must be a valid format', ['formats', '0'])]],
+    [2, [new RulesetValidationError('invalid-ruleset-definition', 'must be an array of formats', ['formats'])]],
+    [[''], [new RulesetValidationError('invalid-format', 'must be a valid format', ['formats', '0'])]],
   ])('recognizes invalid ruleset %p formats syntax', (formats, errors) => {
     expect(
       assertValidRuleset.bind(
@@ -850,11 +955,20 @@ describe('JSON Ruleset Validation', () => {
     [
       [2, 'a'],
       [
-        new RulesetValidationError('must be a valid format', ['rules', 'rule', 'formats', '0']),
-        new RulesetValidationError('must be a valid format', ['rules', 'rule', 'formats', '1']),
+        new RulesetValidationError('invalid-format', 'must be a valid format', ['rules', 'rule', 'formats', '0']),
+        new RulesetValidationError('invalid-format', 'must be a valid format', ['rules', 'rule', 'formats', '1']),
+      ],
+    ],
+    [
+      2,
+      [
+        new RulesetValidationError('invalid-rule-definition', 'must be an array of formats', [
+          'rules',
+          'rule',
+          'formats',
+        ]),
       ],
     ],
-    [2, [new RulesetValidationError('must be an array of formats', ['rules', 'rule', 'formats'])]],
   ])('recognizes invalid rule %p formats syntax', (formats, errors) => {
     expect(
       assertValidRuleset.bind(
diff --git a/packages/functions/src/__tests__/alphabetical.test.ts b/packages/functions/src/__tests__/alphabetical.test.ts
index f2883ba7..fe6c2007 100644
--- a/packages/functions/src/__tests__/alphabetical.test.ts
+++ b/packages/functions/src/__tests__/alphabetical.test.ts
@@ -141,7 +141,7 @@ describe('Core Functions / Alphabetical', () => {
       [{ keyedBy: 2 }, '"alphabetical" function and its "keyedBy" option accepts only the following types: string'],
     ])('given invalid %p options, should throw', async (opts, error) => {
       await expect(runAlphabetical([], opts)).rejects.toThrowAggregateError(
-        new AggregateError([new RulesetValidationError(error, [])]),
+        new AggregateError([new RulesetValidationError('invalid-function-options', error, [])]),
       );
     });
   });
diff --git a/packages/functions/src/__tests__/casing.test.ts b/packages/functions/src/__tests__/casing.test.ts
index 24150bcf..b7860526 100644
--- a/packages/functions/src/__tests__/casing.test.ts
+++ b/packages/functions/src/__tests__/casing.test.ts
@@ -379,6 +379,7 @@ describe('Core Functions / Casing', () => {
         { type: 'foo' },
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"casing" function and its "type" option accept the following values: flat, camel, pascal, kebab, cobol, snake, macro',
             [],
           ),
@@ -386,7 +387,7 @@ describe('Core Functions / Casing', () => {
       ],
       [
         { type: 'macro', foo: true },
-        [new RulesetValidationError('"casing" function does not support "foo" option', [])],
+        [new RulesetValidationError('invalid-function-options', '"casing" function does not support "foo" option', [])],
       ],
       [
         {
@@ -394,7 +395,13 @@ describe('Core Functions / Casing', () => {
           disallowDigits: false,
           separator: {},
         },
-        [new RulesetValidationError('"casing" function is missing "separator.char" option', [])],
+        [
+          new RulesetValidationError(
+            'invalid-function-options',
+            '"casing" function is missing "separator.char" option',
+            [],
+          ),
+        ],
       ],
       [
         {
@@ -402,11 +409,23 @@ describe('Core Functions / Casing', () => {
           disallowDigits: false,
           separator: { allowLeading: true },
         },
-        [new RulesetValidationError('"casing" function is missing "separator.char" option', [])],
+        [
+          new RulesetValidationError(
+            'invalid-function-options',
+            '"casing" function is missing "separator.char" option',
+            [],
+          ),
+        ],
       ],
       [
         { type: 'snake', separator: { char: 'a', foo: true } },
-        [new RulesetValidationError('"casing" function does not support "separator.foo" option', [])],
+        [
+          new RulesetValidationError(
+            'invalid-function-options',
+            '"casing" function does not support "separator.foo" option',
+            [],
+          ),
+        ],
       ],
       [
         {
@@ -417,6 +436,7 @@ describe('Core Functions / Casing', () => {
         },
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"casing" function and its "separator.char" option accepts only char, i.e. "I" or "/"',
             [],
           ),
@@ -431,6 +451,7 @@ describe('Core Functions / Casing', () => {
         },
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"casing" function and its "separator.char" option accepts only char, i.e. "I" or "/"',
             [],
           ),
diff --git a/packages/functions/src/__tests__/defined.test.ts b/packages/functions/src/__tests__/defined.test.ts
index deede0bf..05d3e54b 100644
--- a/packages/functions/src/__tests__/defined.test.ts
+++ b/packages/functions/src/__tests__/defined.test.ts
@@ -24,7 +24,9 @@ describe('Core Functions / Defined', () => {
   describe('validation', () => {
     it.each([{}, 2])('given invalid %p options, should throw', async opts => {
       await expect(runDefined([], opts)).rejects.toThrowAggregateError(
-        new AggregateError([new RulesetValidationError('"defined" function does not accept any options', [])]),
+        new AggregateError([
+          new RulesetValidationError('invalid-function-options', '"defined" function does not accept any options', []),
+        ]),
       );
     });
   });
diff --git a/packages/functions/src/__tests__/enumeration.test.ts b/packages/functions/src/__tests__/enumeration.test.ts
index 23dfe379..ab92c140 100644
--- a/packages/functions/src/__tests__/enumeration.test.ts
+++ b/packages/functions/src/__tests__/enumeration.test.ts
@@ -40,7 +40,13 @@ describe('Core Functions / Enumeration', () => {
           values: ['foo', 2],
           foo: true,
         },
-        [new RulesetValidationError('"enumeration" function does not support "foo" option', [])],
+        [
+          new RulesetValidationError(
+            'invalid-function-options',
+            '"enumeration" function does not support "foo" option',
+            [],
+          ),
+        ],
       ],
       [
         {
@@ -48,6 +54,7 @@ describe('Core Functions / Enumeration', () => {
         },
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"enumeration" and its "values" option support only arrays of primitive values, i.e. ["Berlin", "London", "Paris"]',
             [],
           ),
@@ -57,6 +64,7 @@ describe('Core Functions / Enumeration', () => {
         null,
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"enumeration" function has invalid options specified. Example valid options: { "values": ["Berlin", "London", "Paris"] }, { "values": [2, 3, 5, 8, 13, 21] }',
             [],
           ),
@@ -66,6 +74,7 @@ describe('Core Functions / Enumeration', () => {
         2,
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"enumeration" function has invalid options specified. Example valid options: { "values": ["Berlin", "London", "Paris"] }, { "values": [2, 3, 5, 8, 13, 21] }',
             [],
           ),
diff --git a/packages/functions/src/__tests__/falsy.test.ts b/packages/functions/src/__tests__/falsy.test.ts
index 13f62039..7799b442 100644
--- a/packages/functions/src/__tests__/falsy.test.ts
+++ b/packages/functions/src/__tests__/falsy.test.ts
@@ -24,7 +24,9 @@ describe('Core Functions / Falsy', () => {
   describe('validation', () => {
     it.each([{}, 2])('given invalid %p options, should throw', async opts => {
       await expect(runFalsy([], opts)).rejects.toThrowAggregateError(
-        new AggregateError([new RulesetValidationError('"falsy" function does not accept any options', [])]),
+        new AggregateError([
+          new RulesetValidationError('invalid-function-options', '"falsy" function does not accept any options', []),
+        ]),
       );
     });
   });
diff --git a/packages/functions/src/__tests__/length.test.ts b/packages/functions/src/__tests__/length.test.ts
index 54ffd46f..173a5c1e 100644
--- a/packages/functions/src/__tests__/length.test.ts
+++ b/packages/functions/src/__tests__/length.test.ts
@@ -59,6 +59,7 @@ describe('Core Functions / Length', () => {
         null,
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"length" function has invalid options specified. Example valid options: { "min": 2 }, { "max": 5 }, { "min": 0, "max": 10 }',
             [],
           ),
@@ -68,6 +69,7 @@ describe('Core Functions / Length', () => {
         2,
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"length" function has invalid options specified. Example valid options: { "min": 2 }, { "max": 5 }, { "min": 0, "max": 10 }',
             [],
           ),
@@ -78,12 +80,13 @@ describe('Core Functions / Length', () => {
           min: 2,
           foo: true,
         },
-        [new RulesetValidationError('"length" function does not support "foo" option', [])],
+        [new RulesetValidationError('invalid-function-options', '"length" function does not support "foo" option', [])],
       ],
       [
         { min: '2' },
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"length" function and its "min" option accepts only the following types: number',
             [],
           ),
@@ -94,6 +97,7 @@ describe('Core Functions / Length', () => {
         { max: '2' },
         [
           new RulesetValidationError(
+            'invalid-function-options',
             `"length" function and its "max" option accepts only the following types: number`,
             [],
           ),
@@ -103,10 +107,12 @@ describe('Core Functions / Length', () => {
         { min: '4', max: '2' },
         [
           new RulesetValidationError(
+            'invalid-function-options',
             `"length" function and its "min" option accepts only the following types: number`,
             [],
           ),
           new RulesetValidationError(
+            'invalid-function-options',
             `"length" function and its "max" option accepts only the following types: number`,
             [],
           ),
diff --git a/packages/functions/src/__tests__/pattern.test.ts b/packages/functions/src/__tests__/pattern.test.ts
index 9bcfcc42..74b98e08 100644
--- a/packages/functions/src/__tests__/pattern.test.ts
+++ b/packages/functions/src/__tests__/pattern.test.ts
@@ -57,6 +57,7 @@ describe('Core Functions / Pattern', () => {
         null,
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"pattern" function has invalid options specified. Example valid options: { "match": "^Stoplight" }, { "notMatch": "Swagger" }, { "match": "Stoplight", "notMatch": "Swagger" }',
             [],
           ),
@@ -66,20 +67,37 @@ describe('Core Functions / Pattern', () => {
         {},
         [
           new RulesetValidationError(
+            'invalid-function-options',
             `"pattern" function has invalid options specified. Example valid options: { "match": "^Stoplight" }, { "notMatch": "Swagger" }, { "match": "Stoplight", "notMatch": "Swagger" }`,
             [],
           ),
         ],
       ],
-      [{ foo: true }, [new RulesetValidationError('"pattern" function does not support "foo" option', [])]],
+      [
+        { foo: true },
+        [
+          new RulesetValidationError(
+            'invalid-function-options',
+            '"pattern" function does not support "foo" option',
+            [],
+          ),
+        ],
+      ],
       [
         { match: 2 },
-        [new RulesetValidationError('"pattern" function and its "match" option must be string or RegExp instance', [])],
+        [
+          new RulesetValidationError(
+            'invalid-function-options',
+            '"pattern" function and its "match" option must be string or RegExp instance',
+            [],
+          ),
+        ],
       ],
       [
         { notMatch: null },
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"pattern" function and its "notMatch" option must be string or RegExp instance',
             [],
           ),
@@ -88,8 +106,13 @@ describe('Core Functions / Pattern', () => {
       [
         { match: 4, notMatch: 10 },
         [
-          new RulesetValidationError(`"pattern" function and its "match" option must be string or RegExp instance`, []),
           new RulesetValidationError(
+            'invalid-function-options',
+            `"pattern" function and its "match" option must be string or RegExp instance`,
+            [],
+          ),
+          new RulesetValidationError(
+            'invalid-function-options',
             `"pattern" function and its "notMatch" option must be string or RegExp instance`,
             [],
           ),
diff --git a/packages/functions/src/__tests__/schema.test.ts b/packages/functions/src/__tests__/schema.test.ts
index 562226d5..ee7b5395 100644
--- a/packages/functions/src/__tests__/schema.test.ts
+++ b/packages/functions/src/__tests__/schema.test.ts
@@ -483,6 +483,7 @@ describe('Core Functions / Schema', () => {
         2,
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"schema" function has invalid options specified. Example valid options: { "schema": { /* any JSON Schema can be defined here */ } , { "schema": { "type": "object" }, "dialect": "auto" }',
             [],
           ),
@@ -490,16 +491,23 @@ describe('Core Functions / Schema', () => {
       ],
       [
         { schema: { type: 'object' }, foo: true },
-        [new RulesetValidationError('"schema" function does not support "foo" option', [])],
+        [new RulesetValidationError('invalid-function-options', '"schema" function does not support "foo" option', [])],
       ],
       [
         { schema: { type: 'object' }, oasVersion: 1 },
-        [new RulesetValidationError('"schema" function does not support "oasVersion" option', [])],
+        [
+          new RulesetValidationError(
+            'invalid-function-options',
+            '"schema" function does not support "oasVersion" option',
+            [],
+          ),
+        ],
       ],
       [
         { schema: { type: 'object' }, dialect: 'foo' },
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"schema" function and its "dialect" option accepts only the following values: "auto", "draft4", "draft6", "draft7", "draft2019-09", "draft2020-12"',
             [],
           ),
@@ -509,6 +517,7 @@ describe('Core Functions / Schema', () => {
         { schema: { type: 'object' }, allErrors: null },
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"schema" function and its "allErrors" option accepts only the following types: boolean',
             [],
           ),
@@ -518,10 +527,12 @@ describe('Core Functions / Schema', () => {
         { schema: null, allErrors: null },
         [
           new RulesetValidationError(
+            'invalid-function-options',
             `"schema" function and its "schema" option accepts only the following types: object`,
             [],
           ),
           new RulesetValidationError(
+            'invalid-function-options',
             `"schema" function and its "allErrors" option accepts only the following types: boolean`,
             [],
           ),
diff --git a/packages/functions/src/__tests__/truthy.test.ts b/packages/functions/src/__tests__/truthy.test.ts
index d49ff8ab..b135382a 100644
--- a/packages/functions/src/__tests__/truthy.test.ts
+++ b/packages/functions/src/__tests__/truthy.test.ts
@@ -24,7 +24,9 @@ describe('Core Functions / Truthy', () => {
   describe('validation', () => {
     it.each([{}, 2])('given invalid %p options, should throw', async opts => {
       await expect(runTruthy([], opts)).rejects.toThrowAggregateError(
-        new AggregateError([new RulesetValidationError('"truthy" function does not accept any options', [])]),
+        new AggregateError([
+          new RulesetValidationError('invalid-function-options', '"truthy" function does not accept any options', []),
+        ]),
       );
     });
   });
diff --git a/packages/functions/src/__tests__/unreferencedReusableObject.test.ts b/packages/functions/src/__tests__/unreferencedReusableObject.test.ts
index c49bee68..7fdcbe80 100644
--- a/packages/functions/src/__tests__/unreferencedReusableObject.test.ts
+++ b/packages/functions/src/__tests__/unreferencedReusableObject.test.ts
@@ -32,6 +32,7 @@ describe('Core Functions / UnreferencedReusableObject', () => {
         null,
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"unreferencedReusableObject" function has invalid options specified. Example valid options: { "reusableObjectsLocation": "#/components/schemas" }, { "reusableObjectsLocation": "#/$defs" }',
             [],
           ),
@@ -41,6 +42,7 @@ describe('Core Functions / UnreferencedReusableObject', () => {
         2,
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"unreferencedReusableObject" function has invalid options specified. Example valid options: { "reusableObjectsLocation": "#/components/schemas" }, { "reusableObjectsLocation": "#/$defs" }',
             [],
           ),
@@ -50,6 +52,7 @@ describe('Core Functions / UnreferencedReusableObject', () => {
         {},
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"unreferencedReusableObject" function is missing "reusableObjectsLocation" option. Example valid options: { "reusableObjectsLocation": "#/components/schemas" }, { "reusableObjectsLocation": "#/$defs" }',
             [],
           ),
@@ -60,7 +63,13 @@ describe('Core Functions / UnreferencedReusableObject', () => {
           reusableObjectsLocation: '#',
           foo: true,
         },
-        [new RulesetValidationError('"unreferencedReusableObject" function does not support "foo" option', [])],
+        [
+          new RulesetValidationError(
+            'invalid-function-options',
+            '"unreferencedReusableObject" function does not support "foo" option',
+            [],
+          ),
+        ],
       ],
       [
         {
@@ -68,6 +77,7 @@ describe('Core Functions / UnreferencedReusableObject', () => {
         },
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"unreferencedReusableObject" and its "reusableObjectsLocation" option support only valid JSON Pointer fragments, i.e. "#", "#/foo", "#/paths/~1user"',
             [],
           ),
@@ -79,6 +89,7 @@ describe('Core Functions / UnreferencedReusableObject', () => {
         },
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"unreferencedReusableObject" and its "reusableObjectsLocation" option support only valid JSON Pointer fragments, i.e. "#", "#/foo", "#/paths/~1user"',
             [],
           ),
diff --git a/packages/functions/src/__tests__/xor.test.ts b/packages/functions/src/__tests__/xor.test.ts
index 908f8e3d..2bf7deed 100644
--- a/packages/functions/src/__tests__/xor.test.ts
+++ b/packages/functions/src/__tests__/xor.test.ts
@@ -71,6 +71,7 @@ describe('Core Functions / Xor', () => {
         null,
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"xor" function has invalid options specified. Example valid options: { "properties": ["id", "name"] }, { "properties": ["country", "street"] }',
             [],
           ),
@@ -80,6 +81,7 @@ describe('Core Functions / Xor', () => {
         2,
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"xor" function has invalid options specified. Example valid options: { "properties": ["id", "name"] }, { "properties": ["country", "street"] }',
             [],
           ),
@@ -87,12 +89,13 @@ describe('Core Functions / Xor', () => {
       ],
       [
         { properties: ['foo', 'bar'], foo: true },
-        [new RulesetValidationError('"xor" function does not support "foo" option', [])],
+        [new RulesetValidationError('invalid-function-options', '"xor" function does not support "foo" option', [])],
       ],
       [
         { properties: ['foo', 'bar', 'baz'] },
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]',
             [],
           ),
@@ -102,6 +105,7 @@ describe('Core Functions / Xor', () => {
         { properties: ['foo', {}] },
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]',
             [],
           ),
@@ -111,6 +115,7 @@ describe('Core Functions / Xor', () => {
         { properties: ['foo'] },
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]',
             [],
           ),
@@ -120,6 +125,7 @@ describe('Core Functions / Xor', () => {
         { properties: [] },
         [
           new RulesetValidationError(
+            'invalid-function-options',
             '"xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]',
             [],
           ),