Repository: stoplightio/spectral
Pull request #1112 (2020-04-20 23:13:23)

refactor: introduce Rule
**Checklist**

- [x] Tests added / updated
- [ ] Docs added / updated

**Does this PR introduce a breaking change?**

- [ ] Yes
- [x] No

**Additional context**

There are not so amny changes taking aplce here.
The only actual notable changes are:
- linter and runners have been moved and their implementations have been slightly modified, but semantically they are almost equal
- introduced Rule that is a more unified IRunRule that is more convienent to use during the linting phase, i.e. exposes `enabled`, `matchesFormat`, etc. Plus, the data types are more strict, so less duck-typing or other checks are required

This one of the chunks of "json-path on rails" (#437) chore.
I'll split it into smaller pieces, as certain changes are not directly related.



Patch:
diff --git a/package.json b/package.json
index aaa0f2fd..03db49a6 100644
--- a/package.json
+++ b/package.json
@@ -77,7 +77,6 @@
     "lodash": "4.17.15",
     "nanoid": "2.1.11",
     "node-fetch": "2.6",
-    "promise.allsettled": "1.0.2",
     "proxy-agent": "3.1.1",
     "strip-ansi": "6.0",
     "text-table": "0.2",
diff --git a/src/cli/services/linter/linter.ts b/src/cli/services/linter/linter.ts
index e0473d3a..6ba5a6b1 100644
--- a/src/cli/services/linter/linter.ts
+++ b/src/cli/services/linter/linter.ts
@@ -12,7 +12,6 @@ import {
 } from '../../../formats';
 import { readParsable } from '../../../fs/reader';
 import * as Parsers from '../../../parsers';
-import { isRuleEnabled } from '../../../runner';
 import { IRuleResult, Spectral } from '../../../spectral';
 import { FormatLookup } from '../../../types';
 import { ILintConfig } from '../../../types/config';
@@ -56,7 +55,7 @@ export async function lint(documents: Array<number | string>, flags: ILintConfig
   if (flags.verbose) {
     if (ruleset) {
       const rules = Object.values(spectral.rules);
-      console.info(`Found ${rules.length} rules (${rules.filter(isRuleEnabled).length} enabled)`);
+      console.info(`Found ${rules.length} rules (${rules.filter(rule => rule.enabled).length} enabled)`);
     } else {
       console.info('No rules loaded, attempting to detect document type');
     }
diff --git a/src/functions/schema-path.ts b/src/functions/schema-path.ts
index 66a279d6..853813d1 100644
--- a/src/functions/schema-path.ts
+++ b/src/functions/schema-path.ts
@@ -11,8 +11,8 @@ import { JSONPath } from 'jsonpath-plus';
 
 import { Optional } from '@stoplight/types';
 
+import { getLintTargets } from '../runner/utils/getLintTargets';
 import { IFunction, IFunctionResult } from '../types';
-import { getLintTargets } from '../utils';
 import { schema } from './schema';
 
 export interface ISchemaPathOptions {
diff --git a/src/index.ts b/src/index.ts
index a7fd476c..9969157d 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -2,5 +2,6 @@ import * as Parsers from './parsers';
 
 export * from './spectral';
 export * from './formats';
+export { Rule } from './rule';
 export { Parsers };
 export { Document, ParsedDocument } from './document';
diff --git a/src/linter.ts b/src/linter.ts
deleted file mode 100644
index dc47d335..00000000
--- a/src/linter.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import { decodePointerFragment } from '@stoplight/json';
-import { get } from 'lodash';
-
-import { JsonPath, Optional } from '@stoplight/types';
-import { Document } from './document';
-import { IMessageVars, message } from './rulesets/message';
-import { getDiagnosticSeverity } from './rulesets/severity';
-import { IRunningContext } from './runner';
-import { IGivenNode, IRuleResult, IRunRule } from './types';
-import { getClosestJsonPath, getLintTargets, printPath, PrintStyle } from './utils';
-import { IExceptionLocation } from './utils/pivotExceptions';
-
-const arePathsEqual = (one: JsonPath, another: JsonPath): boolean => {
-  if (one.length !== another.length) {
-    return false;
-  }
-
-  for (let i = 0; i < one.length; i++) {
-    if (one[i] !== another[i]) {
-      return false;
-    }
-  }
-
-  return true;
-};
-
-const isAKnownException = (
-  path: JsonPath,
-  source: Optional<string | null>,
-  locations: IExceptionLocation[],
-): boolean => {
-  for (const location of locations) {
-    if (source !== location.source) {
-      continue;
-    }
-
-    if (arePathsEqual(path, location.path)) {
-      return true;
-    }
-  }
-
-  return false;
-};
-
-export const lintNode = async (
-  context: IRunningContext,
-  node: IGivenNode,
-  rule: IRunRule,
-  exceptionLocations: Optional<IExceptionLocation[]>,
-): Promise<IRuleResult[]> => {
-  const results: IRuleResult[] = [];
-
-  for (const then of Array.isArray(rule.then) ? rule.then : [rule.then]) {
-    const func = context.functions[then.function];
-    if (typeof func !== 'function') {
-      throw new ReferenceError(`Function ${then.function} not found. Called by rule ${rule.name}.`);
-    }
-
-    const givenPath = node.path[0] === '$' ? node.path.slice(1) : node.path;
-    const targets = getLintTargets(node.value, then.field);
-
-    for (const target of targets) {
-      const targetPath = [...givenPath, ...target.path];
-
-      const targetResults = await func(
-        target.value,
-        then.functionOptions || {},
-        {
-          given: givenPath,
-          target: targetPath,
-        },
-        {
-          original: node.value,
-          given: node.value,
-          documentInventory: context.documentInventory,
-        },
-      );
-
-      if (targetResults === void 0) continue;
-
-      for (const result of targetResults) {
-        const escapedJsonPath = (result.path || targetPath).map(segment => decodePointerFragment(String(segment)));
-        const associatedItem = context.documentInventory.findAssociatedItemForPath(
-          escapedJsonPath,
-          rule.resolved !== false,
-        );
-        const path = associatedItem?.path || getClosestJsonPath(context.documentInventory.resolved, escapedJsonPath);
-        const source = associatedItem?.document.source;
-
-        if (exceptionLocations !== void 0 && isAKnownException(path, source, exceptionLocations)) {
-          continue;
-        }
-
-        const document = associatedItem?.document || context.documentInventory.document;
-        const range = document.getRangeForJsonPath(path, true) || Document.DEFAULT_RANGE;
-        const value = path.length === 0 ? document.data : get(document.data, path);
-
-        const vars: IMessageVars = {
-          property:
-            associatedItem?.missingPropertyPath && associatedItem.missingPropertyPath.length > path.length
-              ? printPath(associatedItem.missingPropertyPath.slice(path.length - 1), PrintStyle.Dot)
-              : path.length > 0
-              ? path[path.length - 1]
-              : '',
-          error: result.message,
-          path: printPath(path, PrintStyle.EscapedPointer),
-          description: rule.description,
-          value,
-        };
-
-        const resultMessage = message(result.message, vars);
-        vars.error = resultMessage;
-
-        results.push({
-          code: rule.name,
-          message: (rule.message === void 0 ? rule.description ?? resultMessage : message(rule.message, vars)).trim(),
-          path,
-          severity: getDiagnosticSeverity(rule.severity),
-          ...(source !== null && { source }),
-          range,
-        });
-      }
-    }
-  }
-
-  return results;
-};
diff --git a/src/rule.ts b/src/rule.ts
new file mode 100644
index 00000000..29b35436
--- /dev/null
+++ b/src/rule.ts
@@ -0,0 +1,42 @@
+import { Optional } from '@stoplight/types';
+
+import { IDocument } from './document';
+import { DEFAULT_SEVERITY_LEVEL, getDiagnosticSeverity } from './rulesets/severity';
+import { IRule, IThen, SpectralDiagnosticSeverity } from './types';
+import { hasIntersectingElement } from './utils';
+
+export class Rule {
+  public readonly name: string;
+  public readonly description: string | null;
+  public readonly message: string | null;
+  public readonly severity: SpectralDiagnosticSeverity;
+  public readonly resolved: boolean;
+  public readonly formats: Optional<string[]>;
+
+  public readonly then: IThen[];
+  public readonly given: string[];
+
+  public get enabled() {
+    return this.severity !== -1;
+  }
+
+  constructor(name: string, rule: IRule) {
+    this.name = name;
+    this.description = rule.description ?? null;
+    this.message = rule.message ?? null;
+    this.severity = rule.severity === void 0 ? DEFAULT_SEVERITY_LEVEL : getDiagnosticSeverity(rule.severity);
+    this.resolved = rule.resolved !== false;
+    this.formats = rule.formats;
+
+    this.then = Array.isArray(rule.then) ? rule.then : [rule.then];
+    this.given = Array.isArray(rule.given) ? rule.given : [rule.given];
+  }
+
+  public matchesFormat(formats: IDocument['formats']) {
+    if (this.formats === void 0) {
+      return true;
+    }
+
+    return Array.isArray(formats) && hasIntersectingElement(this.formats, formats);
+  }
+}
diff --git a/src/rulesets/message.ts b/src/rulesets/message.ts
index 969c1f5f..96ef2a8e 100644
--- a/src/rulesets/message.ts
+++ b/src/rulesets/message.ts
@@ -5,7 +5,7 @@ import { Replacer } from '../utils/replacer';
 export interface IMessageVars {
   property: Segment;
   error: string;
-  description?: string;
+  description: string | null;
   value: unknown;
   path: string;
 }
diff --git a/src/runner.ts b/src/runner.ts
deleted file mode 100644
index 23b5747b..00000000
--- a/src/runner.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-const allSettled = require('promise.allsettled');
-const { JSONPath } = require('jsonpath-plus');
-import { DiagnosticSeverity, Optional } from '@stoplight/types';
-import { flatMap } from 'lodash';
-import { STDIN } from './document';
-import { DocumentInventory } from './documentInventory';
-import { lintNode } from './linter';
-import { getDiagnosticSeverity } from './rulesets/severity';
-import { FunctionCollection, IRule, IRuleResult, IRunRule, RunRuleCollection } from './types';
-import { RulesetExceptionCollection } from './types/ruleset';
-import { hasIntersectingElement } from './utils/';
-import { generateDocumentWideResult } from './utils/generateDocumentWideResult';
-import { IExceptionLocation, pivotExceptions } from './utils/pivotExceptions';
-
-export const isRuleEnabled = (rule: IRule) => rule.severity !== void 0 && getDiagnosticSeverity(rule.severity) !== -1;
-
-const isStdInSource = (inventory: DocumentInventory): boolean => {
-  return inventory.document.source === STDIN;
-};
-
-export interface IRunningContext {
-  documentInventory: DocumentInventory;
-  rules: RunRuleCollection;
-  functions: FunctionCollection;
-  exceptions: RulesetExceptionCollection;
-}
-
-const generateDefinedExceptionsButStdIn = (documentInventory: DocumentInventory): IRuleResult => {
-  return generateDocumentWideResult(
-    documentInventory.document,
-    'The ruleset contains `except` entries. However, they cannot be enforced when the input is passed through stdin.',
-    DiagnosticSeverity.Warning,
-    'except-but-stdin',
-  );
-};
-
-export const runRules = async (context: IRunningContext): Promise<IRuleResult[]> => {
-  const { documentInventory, rules, exceptions } = context;
-
-  const results: Array<IRuleResult | Promise<IRuleResult[]>> = [];
-  const isStdIn = isStdInSource(documentInventory);
-  const exceptRuleByLocations = isStdIn ? {} : pivotExceptions(exceptions, rules);
-
-  if (isStdIn && Object.keys(exceptions).length > 0) {
-    results.push(generateDefinedExceptionsButStdIn(documentInventory));
-  }
-
-  for (const rule of Object.values(rules)) {
-    if (!isRuleEnabled(rule)) continue;
-
-    if (
-      rule.formats !== void 0 &&
-      (documentInventory.formats === null ||
-        (documentInventory.formats !== void 0 && !hasIntersectingElement(rule.formats, documentInventory.formats)))
-    ) {
-      continue;
-    }
-
-    runRule(context, rule, exceptRuleByLocations[rule.name], results);
-  }
-
-  return flatMap<PromiseSettledResult<IRuleResult[]>, IRuleResult>(await allSettled(results), result => {
-    if (result.status === 'fulfilled') {
-      return result.value;
-    } else {
-      // todo: use @stoplight/reporter
-      console.warn(result.reason);
-      return [];
-    }
-  });
-};
-
-const runRule = (
-  context: IRunningContext,
-  rule: IRunRule,
-  exceptRuleByLocations: Optional<IExceptionLocation[]>,
-  results: Array<IRuleResult | Promise<IRuleResult[]>>,
-): void => {
-  const target = rule.resolved === false ? context.documentInventory.unresolved : context.documentInventory.resolved;
-
-  for (const given of Array.isArray(rule.given) ? rule.given : [rule.given]) {
-    // don't have to spend time running jsonpath if given is $ - can just use the root object
-    if (given === '$') {
-      results.push(
-        lintNode(
-          context,
-          {
-            path: ['$'],
-            value: target,
-          },
-          rule,
-          exceptRuleByLocations,
-        ),
-      );
-    } else {
-      JSONPath({
-        path: given,
-        json: target,
-        resultType: 'all',
-        callback: (result: any) => {
-          results.push(
-            lintNode(
-              context,
-              {
-                path: JSONPath.toPathArray(result.path),
-                value: result.value,
-              },
-              rule,
-              exceptRuleByLocations,
-            ),
-          );
-        },
-      });
-    }
-  }
-};
diff --git a/src/runner/lintNode.ts b/src/runner/lintNode.ts
new file mode 100644
index 00000000..90eb6490
--- /dev/null
+++ b/src/runner/lintNode.ts
@@ -0,0 +1,139 @@
+import { JsonPath, Optional } from '@stoplight/types';
+import { get } from 'lodash';
+
+import { Document } from '../document';
+import { Rule } from '../rule';
+import { IMessageVars, message } from '../rulesets/message';
+import { getDiagnosticSeverity } from '../rulesets/severity';
+import { IFunctionResult, IGivenNode } from '../types';
+import { decodeSegmentFragment, getClosestJsonPath, printPath, PrintStyle } from '../utils';
+import { IRunnerInternalContext } from './types';
+import { getLintTargets, IExceptionLocation, isAKnownException } from './utils';
+
+export const lintNode = (
+  context: IRunnerInternalContext,
+  node: IGivenNode,
+  rule: Rule,
+  exceptionLocations: Optional<IExceptionLocation[]>,
+): void => {
+  const fnContext = {
+    original: node.value,
+    given: node.value,
+    documentInventory: context.documentInventory,
+  };
+
+  const givenPath = node.path.length > 0 && node.path[0] === '$' ? node.path.slice(1) : node.path;
+
+  for (const then of rule.then) {
+    const func = context.functions[then.function];
+    if (typeof func !== 'function') {
+      console.warn(`Function ${then.function} not found. Called by rule ${rule.name}.`);
+      continue;
+    }
+
+    const targets = getLintTargets(node.value, then.field);
+
+    for (const target of targets) {
+      const targetPath = target.path.length > 0 ? [...givenPath, ...target.path] : givenPath;
+
+      let targetResults;
+      try {
+        targetResults = func(
+          target.value,
+          then.functionOptions || {},
+          {
+            given: givenPath,
+            target: targetPath,
+          },
+          fnContext,
+        );
+      } catch (ex) {
+        // todo: use reporter or sth
+        console.warn(ex);
+      }
+
+      if (targetResults === void 0) continue;
+
+      if ('then' in targetResults) {
+        context.promises.push(
+          targetResults
+            .then(results =>
+              results === void 0
+                ? void 0
+                : void processTargetResults(
+                    context,
+                    results,
+                    rule,
+                    exceptionLocations,
+                    targetPath, // todo: get rid of it somehow.
+                  ),
+            )
+            .catch(ex => {
+              // todo: use reporter or sth
+              console.warn(ex.message);
+            }),
+        );
+      } else {
+        processTargetResults(
+          context,
+          targetResults,
+          rule,
+          exceptionLocations,
+          targetPath, // todo: get rid of it somehow.
+        );
+      }
+    }
+  }
+};
+
+function processTargetResults(
+  context: IRunnerInternalContext,
+  results: IFunctionResult[],
+  rule: Rule,
+  exceptionLocations: Optional<IExceptionLocation[]>,
+  targetPath: JsonPath,
+): void {
+  for (const result of results) {
+    const escapedJsonPath = (result.path || targetPath).map(decodeSegmentFragment);
+    const associatedItem = context.documentInventory.findAssociatedItemForPath(
+      escapedJsonPath,
+      rule.resolved !== false,
+    );
+    const path = associatedItem?.path || getClosestJsonPath(context.documentInventory.resolved, escapedJsonPath);
+    const source = associatedItem?.document.source;
+
+    if (exceptionLocations !== void 0 && isAKnownException(path, source, exceptionLocations)) {
+      continue;
+    }
+
+    const document = associatedItem?.document || context.documentInventory.document;
+    const range = document.getRangeForJsonPath(path, true) || Document.DEFAULT_RANGE;
+    const value = path.length === 0 ? document.data : get(document.data, path);
+
+    const vars: IMessageVars = {
+      property:
+        associatedItem?.missingPropertyPath && associatedItem.missingPropertyPath.length > path.length
+          ? printPath(associatedItem.missingPropertyPath.slice(path.length - 1), PrintStyle.Dot)
+          : path.length > 0
+          ? path[path.length - 1]
+          : '',
+      error: result.message,
+      path: printPath(path, PrintStyle.EscapedPointer),
+      description: rule.description,
+      value,
+    };
+
+    const resultMessage = message(result.message, vars);
+    vars.error = resultMessage;
+
+    context.results.push({
+      code: rule.name,
+      // todo: rule.isInterpolable
+      message: (rule.message === null ? rule.description ?? resultMessage : message(rule.message, vars)).trim(),
+      path,
+      severity: getDiagnosticSeverity(rule.severity),
+      ...(source !== null && { source }),
+      range,
+    });
+  }
+}
diff --git a/src/runner/runner.ts b/src/runner/runner.ts
new file mode 100644
index 00000000..d7704b71
--- /dev/null
+++ b/src/runner/runner.ts
@@ -0,0 +1,96 @@
+import { DiagnosticSeverity, Optional } from '@stoplight/types';
+import { JSONPathCallback } from 'jsonpath-plus';
+import { STDIN } from '../document';
+import { DocumentInventory } from '../documentInventory';
+import { Rule } from '../rule';
+import { IRuleResult } from '../types';
+import { generateDocumentWideResult } from '../utils/generateDocumentWideResult';
+import { lintNode } from './lintNode';
+import { IRunnerInternalContext, IRunnerPublicContext } from './types';
+import { IExceptionLocation, pivotExceptions } from './utils/pivotExceptions';
+
+const { JSONPath } = require('jsonpath-plus');
+
+const isStdInSource = (inventory: DocumentInventory): boolean => {
+  return inventory.document.source === STDIN;
+};
+
+const generateDefinedExceptionsButStdIn = (documentInventory: DocumentInventory): IRuleResult => {
+  return generateDocumentWideResult(
+    documentInventory.document,
+    'The ruleset contains `except` entries. However, they cannot be enforced when the input is passed through stdin.',
+    DiagnosticSeverity.Warning,
+    'except-but-stdin',
+  );
+};
+
+export const runRules = async (context: IRunnerPublicContext): Promise<IRuleResult[]> => {
+  const { documentInventory, rules, exceptions } = context;
+
+  const runnerContext: IRunnerInternalContext = {
+    ...context,
+    results: [],
+    promises: [],
+  };
+
+  const isStdIn = isStdInSource(documentInventory);
+  const exceptRuleByLocations = isStdIn ? {} : pivotExceptions(exceptions, rules);
+
+  if (isStdIn && Object.keys(exceptions).length > 0) {
+    runnerContext.results.push(generateDefinedExceptionsButStdIn(documentInventory));
+  }
+
+  const relevantRules = Object.values(rules).filter(
+    rule => rule.enabled && rule.matchesFormat(documentInventory.formats),
+  );
+
+  for (const rule of relevantRules) {
+    runRule(runnerContext, rule, exceptRuleByLocations[rule.name]);
+  }
+
+  if (runnerContext.promises.length > 0) {
+    await Promise.all(runnerContext.promises);
+  }
+
+  return runnerContext.results;
+};
+
+const runRule = (
+  context: IRunnerInternalContext,
+  rule: Rule,
+  exceptRuleByLocations: Optional<IExceptionLocation[]>,
+): void => {
+  const target = rule.resolved ? context.documentInventory.resolved : context.documentInventory.unresolved;
+
+  for (const given of rule.given) {
+    // don't have to spend time running jsonpath if given is $ - can just use the root object
+    if (given === '$') {
+      lintNode(
+        context,
+        {
+          path: ['$'],
+          value: target,
+        },
+        rule,
+        exceptRuleByLocations,
+      );
+    } else {
+      JSONPath({
+        path: given,
+        json: target,
+        resultType: 'all',
+        callback: (result => {
+          lintNode(
+            context,
+            {
+              path: JSONPath.toPathArray(result.path),
+              value: result.value,
+            },
+            rule,
+            exceptRuleByLocations,
+          );
+        }) as JSONPathCallback,
+      });
+    }
+  }
+};
diff --git a/src/runner/types.ts b/src/runner/types.ts
new file mode 100644
index 00000000..b564e587
--- /dev/null
+++ b/src/runner/types.ts
@@ -0,0 +1,15 @@
+import { DocumentInventory } from '../documentInventory';
+import { FunctionCollection, IRuleResult, RunRuleCollection } from '../types';
+import { RulesetExceptionCollection } from '../types/ruleset';
+
+export interface IRunnerPublicContext {
+  documentInventory: DocumentInventory;
+  rules: RunRuleCollection;
+  functions: FunctionCollection;
+  exceptions: RulesetExceptionCollection;
+}
+
+export interface IRunnerInternalContext extends IRunnerPublicContext {
+  results: IRuleResult[];
+  promises: Array<Promise<void>>;
+}
diff --git a/src/runner/utils/arePathsEqual.ts b/src/runner/utils/arePathsEqual.ts
new file mode 100644
index 00000000..d141d893
--- /dev/null
+++ b/src/runner/utils/arePathsEqual.ts
@@ -0,0 +1,15 @@
+import { JsonPath } from '@stoplight/types';
+
+export const arePathsEqual = (one: JsonPath, another: JsonPath): boolean => {
+  if (one.length !== another.length) {
+    return false;
+  }
+
+  for (let i = 0; i < one.length; i++) {
+    if (one[i] !== another[i]) {
+      return false;
+    }
+  }
+
+  return true;
+};
diff --git a/src/utils/getLintTargets.ts b/src/runner/utils/getLintTargets.ts
similarity index 100%
rename from src/utils/getLintTargets.ts
rename to src/runner/utils/getLintTargets.ts
diff --git a/src/runner/utils/index.ts b/src/runner/utils/index.ts
new file mode 100644
index 00000000..c2b28a37
--- /dev/null
+++ b/src/runner/utils/index.ts
@@ -0,0 +1,4 @@
+export * from './arePathsEqual';
+export * from './getLintTargets';
+export * from './isAKnownException';
+export * from './pivotExceptions';
diff --git a/src/runner/utils/isAKnownException.ts b/src/runner/utils/isAKnownException.ts
new file mode 100644
index 00000000..caa71a89
--- /dev/null
+++ b/src/runner/utils/isAKnownException.ts
@@ -0,0 +1,21 @@
+import { JsonPath, Optional } from '@stoplight/types';
+import { arePathsEqual } from './arePathsEqual';
+import { IExceptionLocation } from './pivotExceptions';
+
+export const isAKnownException = (
+  path: JsonPath,
+  source: Optional<string | null>,
+  locations: IExceptionLocation[],
+): boolean => {
+  for (const location of locations) {
+    if (source !== location.source) {
+      continue;
+    }
+
+    if (arePathsEqual(path, location.path)) {
+      return true;
+    }
+  }
+
+  return false;
+};
diff --git a/src/utils/pivotExceptions.ts b/src/runner/utils/pivotExceptions.ts
similarity index 82%
rename from src/utils/pivotExceptions.ts
rename to src/runner/utils/pivotExceptions.ts
index ade1b395..df549041 100644
--- a/src/utils/pivotExceptions.ts
+++ b/src/runner/utils/pivotExceptions.ts
@@ -1,8 +1,8 @@
 import { extractPointerFromRef, extractSourceFromRef, pointerToPath } from '@stoplight/json';
 import { Dictionary, JsonPath } from '@stoplight/types';
-import { InvalidUriError } from '../rulesets/mergers/exceptions';
-import { IRunRule } from '../types';
-import { RulesetExceptionCollection } from '../types/ruleset';
+import { InvalidUriError } from '../../rulesets/mergers/exceptions';
+import { RunRuleCollection } from '../../types';
+import { RulesetExceptionCollection } from '../../types/ruleset';
 
 export interface IExceptionLocation {
   source: string;
@@ -11,7 +11,7 @@ export interface IExceptionLocation {
 
 export const pivotExceptions = (
   exceptions: RulesetExceptionCollection,
-  runRules: Dictionary<IRunRule, string>,
+  runRules: RunRuleCollection,
 ): Dictionary<IExceptionLocation[], string> => {
   const dic: Dictionary<IExceptionLocation[], string> = {};
 
diff --git a/src/spectral.ts b/src/spectral.ts
index 2b88084b..d98f2f74 100644
--- a/src/spectral.ts
+++ b/src/spectral.ts
@@ -10,12 +10,12 @@ import { DocumentInventory } from './documentInventory';
 import { CoreFunctions, functions as coreFunctions } from './functions';
 import * as Parsers from './parsers';
 import request from './request';
+import { Rule } from './rule';
 import { readRuleset } from './rulesets';
 import { compileExportedFunction, setFunctionContext } from './rulesets/evaluators';
 import { mergeExceptions } from './rulesets/mergers/exceptions';
 import { IRulesetReadOptions } from './rulesets/reader';
-import { DEFAULT_SEVERITY_LEVEL, getDiagnosticSeverity } from './rulesets/severity';
-import { runRules } from './runner';
+import { runRules } from './runner/runner';
 import {
   FormatLookup,
   FunctionCollection,
@@ -126,25 +126,14 @@ export class Spectral {
   public setRules(rules: RuleCollection) {
     empty(this.rules);
 
-    for (const name in rules) {
-      if (!rules.hasOwnProperty(name)) continue;
-      const rule = rules[name];
-
-      this.rules[name] = {
-        name,
-        ...rule,
-        severity: rule.severity === void 0 ? DEFAULT_SEVERITY_LEVEL : getDiagnosticSeverity(rule.severity),
-      };
+    for (const [name, rule] of Object.entries(rules)) {
+      this.rules[name] = new Rule(name, rule);
     }
   }
 
   public mergeRules(rules: PartialRuleCollection) {
-    for (const name in rules) {
-      if (!rules.hasOwnProperty(name)) continue;
-      const rule = rules[name];
-      if (rule) {
-        this.rules[name] = merge(this.rules[name], rule);
-      }
+    for (const [name, rule] of Object.entries(rules)) {
+      this.rules[name] = merge(this.rules[name], rule);
     }
   }
 
diff --git a/src/types/spectral.ts b/src/types/spectral.ts
index f5478327..36b78684 100644
--- a/src/types/spectral.ts
+++ b/src/types/spectral.ts
@@ -2,17 +2,13 @@ import { IResolveOpts, IResolveResult } from '@stoplight/json-ref-resolver/types
 import { DiagnosticSeverity, Dictionary, IDiagnostic, JsonPath } from '@stoplight/types';
 import { JSONSchema4, JSONSchema6, JSONSchema7 } from 'json-schema';
 import { IFunction, IRule } from '.';
+import { Rule } from '../rule';
 import { ComputeFingerprintFunc } from '../utils';
 
 export type FunctionCollection = Dictionary<IFunction, string>;
 export type RuleCollection = Dictionary<IRule, string>;
 export type PartialRuleCollection = Dictionary<Partial<IRule>, string>;
-export type RunRuleCollection = Dictionary<IRunRule, string>;
-
-export interface IRunRule extends IRule {
-  name: string;
-  severity: SpectralDiagnosticSeverity;
-}
+export type RunRuleCollection = Dictionary<Rule, string>;
 
 export type SpectralDiagnosticSeverity = DiagnosticSeverity | -1;
 
diff --git a/src/utils/decodeSegmentFragment.ts b/src/utils/decodeSegmentFragment.ts
new file mode 100644
index 00000000..83ea4956
--- /dev/null
+++ b/src/utils/decodeSegmentFragment.ts
@@ -0,0 +1,6 @@
+import { decodePointerFragment } from '@stoplight/json';
+import { Segment } from '@stoplight/types';
+
+export function decodeSegmentFragment(segment: Segment): string {
+  return typeof segment !== 'string' ? String(segment) : decodePointerFragment(segment);
+}
diff --git a/src/utils/index.ts b/src/utils/index.ts
index be867a2b..e19c3913 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,5 +1,5 @@
 export * from './empty';
-export * from './getLintTargets';
+export * from './decodeSegmentFragment';
 export * from './hasIntersectingElement';
 export * from './refs';
 export * from './prepareResults';
diff --git a/yarn.lock b/yarn.lock
index 052f51d4..db0e0f56 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -419,7 +419,7 @@
     "@types/istanbul-reports" "^1.1.1"
     "@types/yargs" "^13.0.0"
 
-"@jest/types@^25.3.0", "@jest/types@^25.4.0":
+"@jest/types@^25.4.0":
   version "25.4.0"
   resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.4.0.tgz#5afeb8f7e1cba153a28e5ac3c9fe3eede7206d59"
   integrity sha512-XBeaWNzw2PPnGW5aXvZt3+VO60M+34RY3XDsCK5tW7kyj3RK0XClRutCfjqcBuaR2aBQTbluEDME9b5MB9UAPw==
@@ -1004,16 +1004,6 @@ array-unique@^0.3.2:
   resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
   integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
 
-array.prototype.map@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/array.prototype.map/-/array.prototype.map-1.0.2.tgz#9a4159f416458a23e9483078de1106b2ef68f8ec"
-  integrity sha512-Az3OYxgsa1g7xDYp86l0nnN4bcmuEITGe1rbdEBVkrqkzMgDcbdQ2R7r41pNzti+4NMces3H8gMmuioZUilLgw==
-  dependencies:
-    define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.1"
-    es-array-method-boxes-properly "^1.0.0"
-    is-string "^1.0.4"
-
 arraybuffer.slice@~0.0.7:
   version "0.0.7"
   resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675"
@@ -2057,7 +2047,7 @@ defaults@^1.0.3:
   dependencies:
     clone "^1.0.2"
 
-define-properties@^1.1.1, define-properties@^1.1.2, define-properties@^1.1.3:
+define-properties@^1.1.1:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
   integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
@@ -2322,50 +2312,6 @@ error-ex@^1.3.1:
   dependencies:
     is-arrayish "^0.2.1"
 
-es-abstract@^1.17.0-next.1, es-abstract@^1.17.4, es-abstract@^1.17.5:
-  version "1.17.5"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9"
-  integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==
-  dependencies:
-    es-to-primitive "^1.2.1"
-    function-bind "^1.1.1"
-    has "^1.0.3"
-    has-symbols "^1.0.1"
-    is-callable "^1.1.5"
-    is-regex "^1.0.5"
-    object-inspect "^1.7.0"
-    object-keys "^1.1.1"
-    object.assign "^4.1.0"
-    string.prototype.trimleft "^2.1.1"
-    string.prototype.trimright "^2.1.1"
-
-es-array-method-boxes-properly@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e"
-  integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==
-
-es-get-iterator@^1.0.2:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8"
-  integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==
-  dependencies:
-    es-abstract "^1.17.4"
-    has-symbols "^1.0.1"
-    is-arguments "^1.0.4"
-    is-map "^2.0.1"
-    is-set "^2.0.1"
-    is-string "^1.0.5"
-    isarray "^2.0.5"
-
-es-to-primitive@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
-  integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
-  dependencies:
-    is-callable "^1.1.4"
-    is-date-object "^1.0.1"
-    is-symbol "^1.0.2"
-
 es6-object-assign@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c"
@@ -2916,11 +2862,6 @@ ftp@~0.3.10:
     readable-stream "1.1.x"
     xregexp "2.0.0"
 
-function-bind@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
-  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
-
 gensync@^1.0.0-beta.1:
   version "1.0.0-beta.1"
   resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
@@ -3179,11 +3120,6 @@ has-flag@^4.0.0:
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
   integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
 
-has-symbols@^1.0.0, has-symbols@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
-  integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
-
 has-value@^0.3.1:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
@@ -3215,13 +3151,6 @@ has-values@^1.0.0:
     is-number "^3.0.0"
     kind-of "^4.0.0"
 
-has@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
-  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
-  dependencies:
-    function-bind "^1.1.1"
-
 hash-base@^3.0.0:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918"
@@ -3492,11 +3421,6 @@ is-buffer@^1.0.2, is-buffer@^1.1.5:
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
   integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
 
-is-callable@^1.1.4, is-callable@^1.1.5:
-  version "1.1.5"
-  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
-  integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==
-
 is-ci@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
@@ -3518,11 +3442,6 @@ is-data-descriptor@^1.0.0:
   dependencies:
     kind-of "^6.0.0"
 
-is-date-object@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
-  integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
-
 is-descriptor@^0.1.0:
   version "0.1.6"
   resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
@@ -3628,11 +3547,6 @@ is-glob@^4.0.1, is-glob@~4.0.1:
   dependencies:
     is-extglob "^2.1.1"
 
-is-map@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1"
-  integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==
-
 is-module@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
@@ -3710,13 +3624,6 @@ is-reference@^1.1.2:
   dependencies:
     "@types/estree" "0.0.39"
 
-is-regex@^1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae"
-  integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==
-  dependencies:
-    has "^1.0.3"
-
 is-regexp@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069"
@@ -3729,11 +3636,6 @@ is-relative@^0.2.1:
   dependencies:
     is-unc-path "^0.1.1"
 
-is-set@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43"
-  integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==
-
 is-stream@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
@@ -3744,18 +3646,6 @@ is-stream@^2.0.0:
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
   integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==
 
-is-string@^1.0.4, is-string@^1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
-  integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
-
-is-symbol@^1.0.2:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
-  integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
-  dependencies:
-    has-symbols "^1.0.1"
-
 is-typedarray@^1.0.0, is-typedarray@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
@@ -3803,11 +3693,6 @@ isarray@2.0.1:
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e"
   integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=
 
-isarray@^2.0.5:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
-  integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
-
 isbinaryfile@^3.0.0:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.3.tgz#5d6def3edebf6e8ca8cae9c30183a804b5f8be80"
@@ -3886,19 +3771,6 @@ istanbul-reports@^3.0.0, istanbul-reports@^3.0.2:
     html-escaper "^2.0.0"
     istanbul-lib-report "^3.0.0"
 
-iterate-iterator@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/iterate-iterator/-/iterate-iterator-1.0.1.tgz#1693a768c1ddd79c969051459453f082fe82e9f6"
-  integrity sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw==
-
-iterate-value@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/iterate-value/-/iterate-value-1.0.2.tgz#935115bd37d006a52046535ebc8d07e9c9337f57"
-  integrity sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==
-  dependencies:
-    es-get-iterator "^1.0.2"
-    iterate-iterator "^1.0.1"
-
 jasmine-core@^3.5.0:
   version "3.5.0"
   resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.5.0.tgz#132c23e645af96d85c8bca13c8758b18429fc1e4"
@@ -3966,17 +3838,7 @@ jest-diff@^24.9.0:
     jest-get-type "^24.9.0"
     pretty-format "^24.9.0"
 
-jest-diff@^25.2.1:
-  version "25.3.0"
-  resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.3.0.tgz#0d7d6f5d6171e5dacde9e05be47b3615e147c26f"
-  integrity sha512-vyvs6RPoVdiwARwY4kqFWd4PirPLm2dmmkNzKqo38uZOzJvLee87yzDjIZLmY1SjM3XR5DwsUH+cdQ12vgqi1w==
-  dependencies:
-    chalk "^3.0.0"
-    diff-sequences "^25.2.6"
-    jest-get-type "^25.2.6"
-    pretty-format "^25.3.0"
-
-jest-diff@^25.4.0:
+jest-diff@^25.2.1, jest-diff@^25.4.0:
   version "25.4.0"
   resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.4.0.tgz#260b70f19a46c283adcad7f081cae71eb784a634"
   integrity sha512-kklLbJVXW0y8UKOWOdYhI6TH5MG6QAxrWiBMgQaPIuhj3dNFGirKCd+/xfplBXICQ7fI+3QcqHm9p9lWu1N6ug==
@@ -5239,17 +5101,12 @@ object-copy@^0.1.0:
     define-property "^0.2.5"
     kind-of "^3.0.3"
 
-object-inspect@^1.7.0:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
-  integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==
-
 object-is@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6"
   integrity sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=
 
-object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
+object-keys@^1.0.12:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
   integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
@@ -5268,16 +5125,6 @@ object-visit@^1.0.0:
   dependencies:
     isobject "^3.0.0"
 
-object.assign@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
-  integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
-  dependencies:
-    define-properties "^1.1.2"
-    function-bind "^1.1.1"
-    has-symbols "^1.0.0"
-    object-keys "^1.0.11"
-
 object.omit@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-1.1.0.tgz#9d17ea16778e5057deba7752c6f55f1496829e94"
@@ -5705,17 +5552,7 @@ pretty-format@^24.9.0:
     ansi-styles "^3.2.0"
     react-is "^16.8.4"
 
-pretty-format@^25.2.1, pretty-format@^25.3.0:
-  version "25.3.0"
-  resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.3.0.tgz#d0a4f988ff4a6cd350342fdabbb809aeb4d49ad5"
-  integrity sha512-wToHwF8bkQknIcFkBqNfKu4+UZqnrLn/Vr+wwKQwwvPzkBfDDKp/qIabFqdgtoi5PEnM8LFByVsOrHoa3SpTVA==
-  dependencies:
-    "@jest/types" "^25.3.0"
-    ansi-regex "^5.0.0"
-    ansi-styles "^4.0.0"
-    react-is "^16.12.0"
-
-pretty-format@^25.4.0:
+pretty-format@^25.2.1, pretty-format@^25.4.0:
   version "25.4.0"
   resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.4.0.tgz#c58801bb5c4926ff4a677fe43f9b8b99812c7830"
   integrity sha512-PI/2dpGjXK5HyXexLPZU/jw5T9Q6S1YVXxxVxco+LIqzUFHXIbKZKdUVt7GcX7QUCr31+3fzhi4gN4/wUYPVxQ==
@@ -5745,17 +5582,6 @@ progress@^2.0.3:
   resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
   integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
 
-promise.allsettled@1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/promise.allsettled/-/promise.allsettled-1.0.2.tgz#d66f78fbb600e83e863d893e98b3d4376a9c47c9"
-  integrity sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg==
-  dependencies:
-    array.prototype.map "^1.0.1"
-    define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.1"
-    function-bind "^1.1.1"
-    iterate-value "^1.0.0"
-
 prompts@^2.0.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.2.1.tgz#f901dd2a2dfee080359c0e20059b24188d75ad35"
@@ -6754,40 +6580,6 @@ string-width@^4.1.0, string-width@^4.2.0:
     is-fullwidth-code-point "^3.0.0"
     strip-ansi "^6.0.0"
 
-string.prototype.trimend@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.0.tgz#ee497fd29768646d84be2c9b819e292439614373"
-  integrity sha512-EEJnGqa/xNfIg05SxiPSqRS7S9qwDhYts1TSLR1BQfYUfPe1stofgGKvwERK9+9yf+PpfBMlpBaCHucXGPQfUA==
-  dependencies:
-    define-properties "^1.1.3"
-    es-abstract "^1.17.5"
-
-string.prototype.trimleft@^2.1.1:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz#4408aa2e5d6ddd0c9a80739b087fbc067c03b3cc"
-  integrity sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==
-  dependencies:
-    define-properties "^1.1.3"
-    es-abstract "^1.17.5"
-    string.prototype.trimstart "^1.0.0"
-
-string.prototype.trimright@^2.1.1:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz#c76f1cef30f21bbad8afeb8db1511496cfb0f2a3"
-  integrity sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==
-  dependencies:
-    define-properties "^1.1.3"
-    es-abstract "^1.17.5"
-    string.prototype.trimend "^1.0.0"
-
-string.prototype.trimstart@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.0.tgz#afe596a7ce9de905496919406c9734845f01a2f2"
-  integrity sha512-iCP8g01NFYiiBOnwG1Xc3WZLyoo+RuBymwIlWncShXDDJYWN6DbnM3odslBJdgCdRlq94B5s63NWAZlcn2CS4w==
-  dependencies:
-    define-properties "^1.1.3"
-    es-abstract "^1.17.5"
-
 string_decoder@^1.1.1, string_decoder@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"


Test patch:
diff --git a/setupTests.ts b/setupTests.ts
index e01b4134..b5b133ba 100644
--- a/setupTests.ts
+++ b/setupTests.ts
@@ -1,7 +1,6 @@
 import { RulesetExceptionCollection } from './src/types/ruleset';
 
-import { Dictionary } from '@stoplight/types';
-import { IRule, IRunRule, isAsyncApiv2, Spectral } from './src';
+import { IRule, isAsyncApiv2, Rule, RuleCollection, Spectral } from './src';
 import { rules as asyncApiRules } from './src/rulesets/asyncapi/index.json';
 
 export const buildRulesetExceptionCollectionFrom = (
@@ -20,17 +19,17 @@ const removeAllRulesBut = (spectral: Spectral, ruleName: string) => {
 
   const rawRule = asyncApiRules[ruleName];
 
-  const patchedRule: IRule = Object.assign(rule1, {
+  const patchedRule = Object.assign(rule1, {
     recommended: true,
     severity: rawRule.severity,
-  });
+  }) as IRule;
 
-  const rules: Dictionary<IRule, string> = {};
+  const rules: RuleCollection = {};
   rules[ruleName] = patchedRule;
   spectral.setRules(rules);
 };
 
-export const buildTestSpectralWithAsyncApiRule = async (ruleName: string): Promise<[Spectral, IRunRule]> => {
+export const buildTestSpectralWithAsyncApiRule = async (ruleName: string): Promise<[Spectral, Rule]> => {
   const s = new Spectral();
   s.registerFormat('asyncapi2', isAsyncApiv2);
   await s.loadRuleset('spectral:asyncapi');
@@ -39,7 +38,7 @@ export const buildTestSpectralWithAsyncApiRule = async (ruleName: string): Promi
   expect(Object.keys(s.rules)).toEqual([ruleName]);
 
   const rule = s.rules[ruleName];
-  expect(rule.recommended).not.toBe(false);
+  expect(rule.enabled).not.toBe(false);
   expect(rule.severity).not.toBeUndefined();
   expect(rule.severity).not.toEqual(-1);
   expect(rule.formats).not.toBeUndefined();
diff --git a/src/__tests__/__fixtures__/bare-oas-ruleset.json b/src/__tests__/__fixtures__/bare-oas-ruleset.json
index 3c980184..a4983421 100644
--- a/src/__tests__/__fixtures__/bare-oas-ruleset.json
+++ b/src/__tests__/__fixtures__/bare-oas-ruleset.json
@@ -13,5 +13,12 @@
         }
       }
     }
+  },
+  "except": {
+    "/test/file.json#/info": ["info-contact", "info-description"],
+    "/test/file.json#": [ "oas3-api-servers"],
+    "/test/file.json#/paths/~1a.two/get": ["operation-2xx-response"],
+    "/test/file.json#/paths/~1b.three/get": ["operation-2xx-response"],
+    "another.yaml#": ["dummy-rule", "info-contact"]
   }
 }
diff --git a/src/__tests__/functions.test.ts b/src/__tests__/functions.test.ts
index 4bd7fe97..0b1fb595 100644
--- a/src/__tests__/functions.test.ts
+++ b/src/__tests__/functions.test.ts
@@ -1,4 +1,5 @@
-import { IRule, Spectral } from '../spectral';
+import { IRule } from '..';
+import { Spectral } from '../spectral';
 import { IRuleResult } from '../types';
 
 const applyRuleToObject = async (r: IRule, o: object): Promise<IRuleResult[]> => {
diff --git a/src/__tests__/linter.jest.test.ts b/src/__tests__/linter.jest.test.ts
index 3be14228..181a08c0 100644
--- a/src/__tests__/linter.jest.test.ts
+++ b/src/__tests__/linter.jest.test.ts
@@ -735,7 +735,7 @@ console.log(this.cache.get('test') || this.cache.set('test', []).get('test'));
 
     expect(Object.keys(spectral.rules)).toHaveLength(3);
 
-    expect(Object.entries(spectral.rules).map(([name, rule]) => [name, rule.recommended])).toEqual([
+    expect(Object.entries(spectral.rules).map(([name, rule]) => [name, rule.enabled])).toEqual([
       ['explicitly-recommended', true],
       ['implicitly-recommended', true],
       ['explicitly-not-recommended', false],
diff --git a/src/__tests__/linter.test.ts b/src/__tests__/linter.test.ts
index 0e215338..f003808b 100644
--- a/src/__tests__/linter.test.ts
+++ b/src/__tests__/linter.test.ts
@@ -1,6 +1,7 @@
 import { Resolver } from '@stoplight/json-ref-resolver';
 import { DiagnosticSeverity } from '@stoplight/types';
 import { parse } from '@stoplight/yaml';
+import { omit } from 'lodash';
 import { IParsedResult } from '../document';
 import { isOpenApiv2, isOpenApiv3 } from '../formats';
 import { mergeRules, readRuleset } from '../rulesets';
@@ -150,16 +151,20 @@ describe('linter', () => {
   test('should not report anything for disabled rules', async () => {
     await spectral.loadRuleset('spectral:oas');
     const { rules: oasRules } = await readRuleset('spectral:oas');
-    spectral.setRules(
-      mergeRules(oasRules, {
+    spectral.setRules({
+      ...mergeRules(oasRules, {
         'oas3-valid-schema-example': 'off',
         'operation-2xx-response': -1,
         'openapi-tags': 'off',
-      }) as RuleCollection,
-    );
-
-    // @ts-ignore
-    spectral.rules['oas3-schema'].then.function = 'oasDocumentSchema';
+        'operation-tag-defined': 'off',
+      }),
+      ...omit(spectral.rules, [
+        'oas3-valid-schema-example',
+        'operation-2xx-response',
+        'openapi-tags',
+        'operation-tag-defined',
+      ]),
+    } as RuleCollection);
 
     const result = await spectral.run(invalidSchema);
 
@@ -530,9 +535,7 @@ describe('linter', () => {
   });
 
   test('should include parser diagnostics', async () => {
-    await spectral.loadRuleset('spectral:oas');
-
-    const responses = `openapi: 2.0.0
+    const responses = `
 responses:: !!foo
   400:
     description: a
@@ -542,7 +545,7 @@ responses:: !!foo
      description: c
 `;
 
-    const result = await spectral.run(responses);
+    const result = await spectral.run(responses, { ignoreUnknownFormat: true });
 
     expect(result).toEqual(
       expect.arrayContaining([
@@ -671,26 +674,22 @@ responses:: !!foo
   });
 
   test('should report invalid $refs', async () => {
-    await spectral.loadRuleset('spectral:oas');
-
     const result = await spectral.run(invalidSchema);
 
-    expect(result).toEqual(
-      expect.arrayContaining([
-        expect.objectContaining({
-          code: 'invalid-ref',
-          message: "No resolver defined for scheme 'file' in ref ./models/pet.yaml",
-          path: ['paths', '/pets', 'get', 'responses', '200', 'content', 'application/json', 'schema', '$ref'],
-          severity: DiagnosticSeverity.Error,
-        }),
-        expect.objectContaining({
-          code: 'invalid-ref',
-          message: "No resolver defined for scheme 'file' in ref ../common/models/error.yaml",
-          path: ['paths', '/pets', 'get', 'responses', 'default', 'content', 'application/json', 'schema', '$ref'],
-          severity: DiagnosticSeverity.Error,
-        }),
-      ]),
-    );
+    expect(result).toEqual([
+      expect.objectContaining({
+        code: 'invalid-ref',
+        message: "No resolver defined for scheme 'file' in ref ./models/pet.yaml",
+        path: ['paths', '/pets', 'get', 'responses', '200', 'content', 'application/json', 'schema', '$ref'],
+        severity: DiagnosticSeverity.Error,
+      }),
+      expect.objectContaining({
+        code: 'invalid-ref',
+        message: "No resolver defined for scheme 'file' in ref ../common/models/error.yaml",
+        path: ['paths', '/pets', 'get', 'responses', 'default', 'content', 'application/json', 'schema', '$ref'],
+        severity: DiagnosticSeverity.Error,
+      }),
+    ]);
   });
 
   test('should support YAML merge keys', async () => {
@@ -698,6 +697,8 @@ responses:: !!foo
     spectral.setRules({
       'operation-tag-defined': {
         ...spectral.rules['operation-tag-defined'],
+        message: spectral.rules['operation-tag-defined'].message ?? '',
+        description: spectral.rules['operation-tag-defined'].description ?? '',
         severity: 'off',
       },
     });
@@ -1024,6 +1025,7 @@ responses:: !!foo
     spectral.setRules({
       'oas3-schema': {
         ...spectral.rules['oas3-schema'],
+        description: spectral.rules['oas3-schema'].description ?? '',
         message: 'Schema error at {{path}}',
       },
     });
diff --git a/src/__tests__/spectral.jest.test.ts b/src/__tests__/spectral.jest.test.ts
index 6cb2ab2d..e5dbeb2c 100644
--- a/src/__tests__/spectral.jest.test.ts
+++ b/src/__tests__/spectral.jest.test.ts
@@ -1,5 +1,5 @@
 import { normalize } from '@stoplight/path';
-import { DiagnosticSeverity, Dictionary } from '@stoplight/types';
+import { DiagnosticSeverity } from '@stoplight/types';
 import * as fs from 'fs';
 import * as nock from 'nock';
 import * as path from 'path';
@@ -9,11 +9,9 @@ import { isOpenApiv2 } from '../formats';
 import { pattern } from '../functions/pattern';
 import * as Parsers from '../parsers';
 import { httpAndFileResolver } from '../resolvers/http-and-file';
-import { IRunRule, Spectral } from '../spectral';
+import { Spectral } from '../spectral';
 
-const oasRuleset = require('../rulesets/oas/index.json');
-const oasRulesetRules: Dictionary<IRunRule, string> = oasRuleset.rules;
-const customOASRuleset = require('./__fixtures__/custom-oas-ruleset.json');
+const bareRuleset = require('./__fixtures__/bare-oas-ruleset.json');
 
 describe('Spectral', () => {
   afterEach(() => {
@@ -23,29 +21,16 @@ describe('Spectral', () => {
   describe('loadRuleset', () => {
     test('should support loading rulesets from filesystem', async () => {
       const s = new Spectral();
-      await s.loadRuleset(path.join(__dirname, '__fixtures__/custom-oas-ruleset.json'));
+      await s.loadRuleset(path.join(__dirname, '__fixtures__/bare-oas-ruleset.json'));
 
-      expect(s.rules).toEqual(
-        expect.objectContaining({
-          ...[...Object.entries(oasRulesetRules)].reduce<Dictionary<IRunRule, string>>((oasRules, [name, rule]) => {
-            oasRules[name] = {
-              name,
-              ...rule,
-              formats: expect.arrayContaining([expect.any(String)]),
-              recommended: expect.any(Boolean),
-              severity: expect.any(Number),
-              then: expect.any(Object),
-            };
-
-            return oasRules;
-          }, {}),
-          'info-matches-stoplight': {
-            ...customOASRuleset.rules['info-matches-stoplight'],
-            name: 'info-matches-stoplight',
-            severity: DiagnosticSeverity.Warning,
-          },
+      expect(s.rules).toEqual({
+        'info-matches-stoplight': expect.objectContaining({
+          message: bareRuleset.rules['info-matches-stoplight'].message,
+          name: 'info-matches-stoplight',
+          given: [bareRuleset.rules['info-matches-stoplight'].given],
+          severity: DiagnosticSeverity.Warning,
         }),
-      );
+      });
 
       Object.keys(s.exceptions).forEach(p => expect(path.isAbsolute(p)).toEqual(true));
 
@@ -84,12 +69,12 @@ describe('Spectral', () => {
       await s.loadRuleset('https://localhost:4000/custom-ruleset');
 
       expect(s.rules).toEqual({
-        'info-matches-stoplight': {
-          ...ruleset.rules['info-matches-stoplight'],
+        'info-matches-stoplight': expect.objectContaining({
+          message: bareRuleset.rules['info-matches-stoplight'].message,
           name: 'info-matches-stoplight',
-          recommended: true,
+          given: [bareRuleset.rules['info-matches-stoplight'].given],
           severity: DiagnosticSeverity.Warning,
-        },
+        }),
       });
     });
   });
diff --git a/src/__tests__/spectral.karma.test.ts b/src/__tests__/spectral.karma.test.ts
index b446f714..48f20584 100644
--- a/src/__tests__/spectral.karma.test.ts
+++ b/src/__tests__/spectral.karma.test.ts
@@ -42,12 +42,13 @@ describe('Spectral', () => {
       await s.loadRuleset('https://localhost:4000/custom-ruleset');
 
       expect(s.rules).toEqual({
-        'info-matches-stoplight': {
-          ...ruleset.rules['info-matches-stoplight'],
+        'info-matches-stoplight': expect.objectContaining({
+          given: ['$.info'],
           name: 'info-matches-stoplight',
-          recommended: true,
+          message: 'Info must contain Stoplight',
+          enabled: true,
           severity: DiagnosticSeverity.Warning,
-        },
+        }),
       });
     });
   });
diff --git a/src/__tests__/spectral.test.ts b/src/__tests__/spectral.test.ts
index 20c8fbd8..51325510 100644
--- a/src/__tests__/spectral.test.ts
+++ b/src/__tests__/spectral.test.ts
@@ -1,20 +1,19 @@
 import { IGraphNodeData } from '@stoplight/json-ref-resolver/types';
-import { DiagnosticSeverity, Dictionary } from '@stoplight/types';
+import { DiagnosticSeverity } from '@stoplight/types';
 import { DepGraph } from 'dependency-graph';
 import { escapeRegExp, merge } from 'lodash';
 
+import { buildRulesetExceptionCollectionFrom } from '../../setupTests';
 import { Document } from '../document';
 import * as Parsers from '../parsers';
-import { Spectral } from '../spectral';
-import { IResolver, IRunRule } from '../types';
+import { RunRuleCollection, Spectral } from '../spectral';
+import { IResolver, RuleCollection } from '../types';
 import { RulesetExceptionCollection } from '../types/ruleset';
 
-import { buildRulesetExceptionCollectionFrom } from '../../setupTests';
-
 const oasRuleset = JSON.parse(JSON.stringify(require('../rulesets/oas/index.json')));
 const asyncApiRuleset = JSON.parse(JSON.stringify(require('../rulesets/asyncapi/index.json')));
-const oasRulesetRules: Dictionary<IRunRule, string> = oasRuleset.rules;
-const asyncApiRulesetRules: Dictionary<IRunRule, string> = asyncApiRuleset.rules;
+const oasRulesetRules: RuleCollection = oasRuleset.rules;
+const asyncApiRulesetRules: RuleCollection = asyncApiRuleset.rules;
 
 describe('spectral', () => {
   describe('loadRuleset', () => {
@@ -27,15 +26,17 @@ describe('spectral', () => {
 
       expect(s.rules).toEqual(
         expect.objectContaining(
-          Object.entries(rules).reduce<Dictionary<IRunRule, string>>((oasRules, [name, rule]) => {
-            oasRules[name] = {
+          Object.entries(rules).reduce<RunRuleCollection>((oasRules, [name, rule]) => {
+            oasRules[name] = expect.objectContaining({
               name,
-              ...rule,
+              given: expect.anything(),
               formats: expect.arrayContaining([expect.any(String)]),
-              recommended: expect.any(Boolean),
+              enabled: expect.any(Boolean),
               severity: expect.any(Number),
-              then: expect.any(Object),
-            };
+              then: expect.any(Array),
+              message: rule.message ?? null,
+              description: rule.description ?? null,
+            });
 
             return oasRules;
           }, {}),
@@ -51,15 +52,17 @@ describe('spectral', () => {
       await s.loadRuleset([rulesetName, rulesetName]);
 
       expect(s.rules).toEqual(
-        Object.entries(expectedRules).reduce<Dictionary<IRunRule, string>>((oasRules, [name, rule]) => {
-          oasRules[name] = {
+        Object.entries(expectedRules).reduce<RunRuleCollection>((oasRules, [name, rule]) => {
+          oasRules[name] = expect.objectContaining({
             name,
-            ...rule,
+            given: expect.anything(),
             formats: expect.arrayContaining([expect.any(String)]),
-            recommended: expect.any(Boolean),
+            enabled: expect.any(Boolean),
             severity: expect.any(Number),
-            then: expect.any(Object),
-          };
+            then: expect.any(Array),
+            message: rule.message ?? null,
+            description: rule.description ?? null,
+          });
 
           return oasRules;
         }, {}),
diff --git a/src/rulesets/__tests__/message.test.ts b/src/rulesets/__tests__/message.test.ts
index 0b5f4c07..e51bfc46 100644
--- a/src/rulesets/__tests__/message.test.ts
+++ b/src/rulesets/__tests__/message.test.ts
@@ -8,6 +8,7 @@ describe('message util', () => {
         property: 'description',
         error: 'expected property to be truthy',
         path: '',
+        description: null,
         value: void 0,
       }),
     ).toEqual('oops... "description" is missing;error: expected property to be truthy');
@@ -20,6 +21,7 @@ describe('message util', () => {
         property: 'description',
         error: 'expected property to be truthy',
         path: '',
+        description: null,
         value,
       }),
     ).toEqual(`Value must not equal ${value}`);
@@ -32,6 +34,7 @@ describe('message util', () => {
         property: 'baz',
         error: 'foo',
         path: '',
+        description: null,
         value: void 0,
       }),
     ).toEqual('foofoobazfoofoo');
@@ -44,6 +47,7 @@ describe('message util', () => {
         property: 'description',
         error: 'expected property to be truthy',
         path: '',
+        description: null,
         value: void 0,
       }),
     ).toEqual('missing :(');
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-channel-no-empty-parameter.ts b/src/rulesets/asyncapi/__tests__/asyncapi-channel-no-empty-parameter.ts
index fda96a40..7748cf3f 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-channel-no-empty-parameter.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-channel-no-empty-parameter.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-channel-no-empty-parameter';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-channel-no-query-nor-fragment.ts b/src/rulesets/asyncapi/__tests__/asyncapi-channel-no-query-nor-fragment.ts
index a541eb97..7fb084ec 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-channel-no-query-nor-fragment.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-channel-no-query-nor-fragment.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-channel-no-query-nor-fragment';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-channel-no-trailing-slash.ts b/src/rulesets/asyncapi/__tests__/asyncapi-channel-no-trailing-slash.ts
index ad944000..6125e7ee 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-channel-no-trailing-slash.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-channel-no-trailing-slash.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-channel-no-trailing-slash';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-headers-schema-type-object.ts b/src/rulesets/asyncapi/__tests__/asyncapi-headers-schema-type-object.ts
index bde140ba..7b01ad4d 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-headers-schema-type-object.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-headers-schema-type-object.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-headers-schema-type-object';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-info-contact-properties.ts b/src/rulesets/asyncapi/__tests__/asyncapi-info-contact-properties.ts
index 22b2f964..536e087c 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-info-contact-properties.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-info-contact-properties.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-info-contact-properties';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-info-contact.ts b/src/rulesets/asyncapi/__tests__/asyncapi-info-contact.ts
index 8b0dc6db..bf05e52d 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-info-contact.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-info-contact.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-info-contact';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-info-description.ts b/src/rulesets/asyncapi/__tests__/asyncapi-info-description.ts
index df2d9ce5..b34322e4 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-info-description.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-info-description.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-info-description';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-info-license-url.ts b/src/rulesets/asyncapi/__tests__/asyncapi-info-license-url.ts
index 22f6f009..bf251066 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-info-license-url.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-info-license-url.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-info-license-url';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-info-license.ts b/src/rulesets/asyncapi/__tests__/asyncapi-info-license.ts
index bfbc01d2..a447b526 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-info-license.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-info-license.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-info-license';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-operation-description.ts b/src/rulesets/asyncapi/__tests__/asyncapi-operation-description.ts
index f329e40b..968801e8 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-operation-description.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-operation-description.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-operation-description';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-operation-operationId.ts b/src/rulesets/asyncapi/__tests__/asyncapi-operation-operationId.ts
index 29b48303..fafbe31b 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-operation-operationId.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-operation-operationId.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-operation-operationId';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-parameter-description.ts b/src/rulesets/asyncapi/__tests__/asyncapi-parameter-description.ts
index 74ac28eb..e4560b6a 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-parameter-description.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-parameter-description.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-parameter-description';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-payload-default.ts b/src/rulesets/asyncapi/__tests__/asyncapi-payload-default.ts
index 5240e15f..9b45b0cc 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-payload-default.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-payload-default.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-payload-default';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-payload-examples.ts b/src/rulesets/asyncapi/__tests__/asyncapi-payload-examples.ts
index f5c89109..d67b8470 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-payload-examples.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-payload-examples.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-payload-examples';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-payload-unsupported-schemaFormat.ts b/src/rulesets/asyncapi/__tests__/asyncapi-payload-unsupported-schemaFormat.ts
index 416bdf17..6d8873bb 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-payload-unsupported-schemaFormat.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-payload-unsupported-schemaFormat.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-payload-unsupported-schemaFormat';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-payload.ts b/src/rulesets/asyncapi/__tests__/asyncapi-payload.ts
index 13fab5ca..77955dcb 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-payload.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-payload.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-payload';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-schema-default.ts b/src/rulesets/asyncapi/__tests__/asyncapi-schema-default.ts
index 4dec1912..3b0ba591 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-schema-default.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-schema-default.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-schema-default';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-schema-examples.ts b/src/rulesets/asyncapi/__tests__/asyncapi-schema-examples.ts
index 963e61b1..322b32dd 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-schema-examples.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-schema-examples.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-schema-examples';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-schema.ts b/src/rulesets/asyncapi/__tests__/asyncapi-schema.ts
index 43bcf1f6..ffc38b7a 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-schema.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-schema.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-schema';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-server-no-empty-variable.ts b/src/rulesets/asyncapi/__tests__/asyncapi-server-no-empty-variable.ts
index d0c07054..a1f135de 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-server-no-empty-variable.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-server-no-empty-variable.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-server-no-empty-variable';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-server-no-trailing-slash.ts b/src/rulesets/asyncapi/__tests__/asyncapi-server-no-trailing-slash.ts
index 0684f92f..e3b03109 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-server-no-trailing-slash.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-server-no-trailing-slash.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-server-no-trailing-slash';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-server-not-example-com.ts b/src/rulesets/asyncapi/__tests__/asyncapi-server-not-example-com.ts
index c8eee20d..136a7b3f 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-server-not-example-com.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-server-not-example-com.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-server-not-example-com';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-servers.ts b/src/rulesets/asyncapi/__tests__/asyncapi-servers.ts
index f42e13cc..c4748026 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-servers.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-servers.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-servers';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-tag-description.ts b/src/rulesets/asyncapi/__tests__/asyncapi-tag-description.ts
index d65ad29f..2db2adb4 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-tag-description.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-tag-description.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-tag-description';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-tags-alphabetical.ts b/src/rulesets/asyncapi/__tests__/asyncapi-tags-alphabetical.ts
index 129f8e1f..4b997130 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-tags-alphabetical.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-tags-alphabetical.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-tags-alphabetical';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-tags.ts b/src/rulesets/asyncapi/__tests__/asyncapi-tags.ts
index 6fb880ce..9d77920b 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-tags.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-tags.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-tags';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/asyncapi/__tests__/asyncapi-unused-components-schema.ts b/src/rulesets/asyncapi/__tests__/asyncapi-unused-components-schema.ts
index 80bbbf7e..caab3a42 100644
--- a/src/rulesets/asyncapi/__tests__/asyncapi-unused-components-schema.ts
+++ b/src/rulesets/asyncapi/__tests__/asyncapi-unused-components-schema.ts
@@ -1,12 +1,12 @@
 import { cloneDeep } from 'lodash';
 
 import { buildTestSpectralWithAsyncApiRule } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 import { Spectral } from '../../../spectral';
-import { IRunRule } from '../../../types';
 
 const ruleName = 'asyncapi-unused-components-schema';
 let s: Spectral;
-let rule: IRunRule;
+let rule: Rule;
 
 describe(`Rule '${ruleName}'`, () => {
   beforeEach(async () => {
diff --git a/src/rulesets/oas/__tests__/oas2-valid-definition-example.ts b/src/rulesets/oas/__tests__/oas2-valid-definition-example.ts
index e1e44c1b..1e8dd22d 100644
--- a/src/rulesets/oas/__tests__/oas2-valid-definition-example.ts
+++ b/src/rulesets/oas/__tests__/oas2-valid-definition-example.ts
@@ -3,13 +3,17 @@ import { RuleType, Spectral } from '../../../spectral';
 import * as ruleset from '../index.json';
 
 describe('oas2-valid-definition-example', () => {
-  const s = new Spectral();
-  s.registerFormat('oas2', () => true);
-  s.setRules({
-    'oas2-valid-definition-example': Object.assign(ruleset.rules['oas2-valid-definition-example'], {
-      recommended: true,
-      type: RuleType[ruleset.rules['oas2-valid-definition-example'].type],
-    }),
+  let s: Spectral;
+
+  beforeEach(() => {
+    s = new Spectral();
+    s.registerFormat('oas2', () => true);
+    s.setRules({
+      'oas2-valid-definition-example': Object.assign(ruleset.rules['oas2-valid-definition-example'], {
+        recommended: true,
+        type: RuleType[ruleset.rules['oas2-valid-definition-example'].type],
+      }),
+    });
   });
 
   test('will pass when simple example is valid', async () => {
diff --git a/src/rulesets/oas/__tests__/oas2-valid-parameter-example.ts b/src/rulesets/oas/__tests__/oas2-valid-parameter-example.ts
index cd762581..48956eab 100644
--- a/src/rulesets/oas/__tests__/oas2-valid-parameter-example.ts
+++ b/src/rulesets/oas/__tests__/oas2-valid-parameter-example.ts
@@ -3,13 +3,17 @@ import { RuleType, Spectral } from '../../../spectral';
 import * as ruleset from '../index.json';
 
 describe('oas2-valid-parameter-example', () => {
-  const s = new Spectral();
-  s.registerFormat('oas2', () => true);
-  s.setRules({
-    'oas2-valid-parameter-example': Object.assign(ruleset.rules['oas2-valid-parameter-example'], {
-      recommended: true,
-      type: RuleType[ruleset.rules['oas2-valid-parameter-example'].type],
-    }),
+  let s: Spectral;
+
+  beforeEach(() => {
+    s = new Spectral();
+    s.registerFormat('oas2', () => true);
+    s.setRules({
+      'oas2-valid-parameter-example': Object.assign(ruleset.rules['oas2-valid-parameter-example'], {
+        recommended: true,
+        type: RuleType[ruleset.rules['oas2-valid-parameter-example'].type],
+      }),
+    });
   });
 
   test('will pass when simple example is valid', async () => {
diff --git a/src/rulesets/oas/__tests__/oas3-valid-oas-parameter-example.ts b/src/rulesets/oas/__tests__/oas3-valid-oas-parameter-example.ts
index 00aebdb1..dcb4046d 100644
--- a/src/rulesets/oas/__tests__/oas3-valid-oas-parameter-example.ts
+++ b/src/rulesets/oas/__tests__/oas3-valid-oas-parameter-example.ts
@@ -3,13 +3,18 @@ import { RuleType, Spectral } from '../../../spectral';
 import * as ruleset from '../index.json';
 
 describe('oas3-valid-oas-parameter-example', () => {
-  const s = new Spectral();
-  s.registerFormat('oas3', () => true);
-  s.setRules({
-    'oas3-valid-oas-parameter-example': Object.assign(ruleset.rules['oas3-valid-oas-parameter-example'], {
-      recommended: true,
-      type: RuleType[ruleset.rules['oas3-valid-oas-parameter-example'].type],
-    }),
+  let s: Spectral;
+
+  beforeEach(() => {
+    s = new Spectral();
+
+    s.registerFormat('oas3', () => true);
+    s.setRules({
+      'oas3-valid-oas-parameter-example': Object.assign(ruleset.rules['oas3-valid-oas-parameter-example'], {
+        recommended: true,
+        type: RuleType[ruleset.rules['oas3-valid-oas-parameter-example'].type],
+      }),
+    });
   });
 
   test('will pass when simple example is valid', async () => {
diff --git a/src/rulesets/oas/__tests__/oas3-valid-parameter-schema-example.ts b/src/rulesets/oas/__tests__/oas3-valid-parameter-schema-example.ts
index 941ecdec..a89a7186 100644
--- a/src/rulesets/oas/__tests__/oas3-valid-parameter-schema-example.ts
+++ b/src/rulesets/oas/__tests__/oas3-valid-parameter-schema-example.ts
@@ -3,13 +3,17 @@ import { RuleType, Spectral } from '../../../spectral';
 import * as ruleset from '../index.json';
 
 describe('oas3-valid-parameter-schema-example', () => {
-  const s = new Spectral();
-  s.registerFormat('oas3', () => true);
-  s.setRules({
-    'oas3-valid-parameter-schema-example': Object.assign(ruleset.rules['oas3-valid-parameter-schema-example'], {
-      recommended: true,
-      type: RuleType[ruleset.rules['oas3-valid-parameter-schema-example'].type],
-    }),
+  let s: Spectral;
+
+  beforeEach(() => {
+    s = new Spectral();
+    s.registerFormat('oas3', () => true);
+    s.setRules({
+      'oas3-valid-parameter-schema-example': Object.assign(ruleset.rules['oas3-valid-parameter-schema-example'], {
+        recommended: true,
+        type: RuleType[ruleset.rules['oas3-valid-parameter-schema-example'].type],
+      }),
+    });
   });
 
   test('will pass when simple example is valid', async () => {
diff --git a/src/rulesets/oas/__tests__/oas3-valid-schema-example.ts b/src/rulesets/oas/__tests__/oas3-valid-schema-example.ts
index 95aef983..3330e2be 100644
--- a/src/rulesets/oas/__tests__/oas3-valid-schema-example.ts
+++ b/src/rulesets/oas/__tests__/oas3-valid-schema-example.ts
@@ -3,13 +3,17 @@ import { RuleType, Spectral } from '../../../spectral';
 import * as ruleset from '../index.json';
 
 describe('oas3-valid-schema-example', () => {
-  const s = new Spectral();
-  s.registerFormat('oas3', () => true);
-  s.setRules({
-    'oas3-valid-schema-example': Object.assign(ruleset.rules['oas3-valid-schema-example'], {
-      recommended: true,
-      type: RuleType[ruleset.rules['oas3-valid-schema-example']!.type],
-    }),
+  let s: Spectral;
+
+  beforeEach(() => {
+    s = new Spectral();
+    s.registerFormat('oas3', () => true);
+    s.setRules({
+      'oas3-valid-schema-example': Object.assign(ruleset.rules['oas3-valid-schema-example'], {
+        recommended: true,
+        type: RuleType[ruleset.rules['oas3-valid-schema-example']!.type],
+      }),
+    });
   });
 
   test('will pass when simple example is valid', async () => {
diff --git a/src/rulesets/oas/__tests__/templates/_oas-example.ts b/src/rulesets/oas/__tests__/templates/_oas-example.ts
index b72598bc..c6ebe383 100644
--- a/src/rulesets/oas/__tests__/templates/_oas-example.ts
+++ b/src/rulesets/oas/__tests__/templates/_oas-example.ts
@@ -3,13 +3,17 @@ import { RuleType, Spectral } from '../../../../spectral';
 import { rules } from '../../index.json';
 
 export default (ruleName: string, path: string) => {
-  const s = new Spectral();
-  s.registerFormat('oas3', () => true);
-  s.setRules({
-    [ruleName]: Object.assign(rules[ruleName], {
-      recommended: true,
-      type: RuleType[rules[ruleName].type],
-    }),
+  let s: Spectral;
+
+  beforeEach(() => {
+    s = new Spectral();
+    s.registerFormat('oas3', () => true);
+    s.setRules({
+      [ruleName]: Object.assign(rules[ruleName], {
+        recommended: true,
+        type: RuleType[rules[ruleName].type],
+      }),
+    });
   });
 
   test('will pass when simple example is valid', async () => {
diff --git a/src/rulesets/oas/__tests__/templates/_schema-example.ts b/src/rulesets/oas/__tests__/templates/_schema-example.ts
index 9c488455..a7c0211f 100644
--- a/src/rulesets/oas/__tests__/templates/_schema-example.ts
+++ b/src/rulesets/oas/__tests__/templates/_schema-example.ts
@@ -3,13 +3,17 @@ import { RuleType, Spectral } from '../../../../spectral';
 import { rules } from '../../index.json';
 
 export default (ruleName: string, path: string) => {
-  const s = new Spectral();
-  s.registerFormat('oas3', () => true);
-  s.setRules({
-    [ruleName]: Object.assign(rules[ruleName], {
-      recommended: true,
-      type: RuleType[rules[ruleName].type],
-    }),
+  let s: Spectral;
+
+  beforeEach(() => {
+    s = new Spectral();
+    s.registerFormat('oas3', () => true);
+    s.setRules({
+      [ruleName]: Object.assign(rules[ruleName], {
+        recommended: true,
+        type: RuleType[rules[ruleName].type],
+      }),
+    });
   });
 
   test('will pass when simple example is valid', async () => {
diff --git a/src/utils/__tests__/getLintTargets.test.ts b/src/runner/utils/__tests__/getLintTargets.test.ts
similarity index 100%
rename from src/utils/__tests__/getLintTargets.test.ts
rename to src/runner/utils/__tests__/getLintTargets.test.ts
diff --git a/src/utils/__tests__/pivotExceptions.test.ts b/src/runner/utils/__tests__/pivotExceptions.test.ts
similarity index 80%
rename from src/utils/__tests__/pivotExceptions.test.ts
rename to src/runner/utils/__tests__/pivotExceptions.test.ts
index 282f8906..17539596 100644
--- a/src/utils/__tests__/pivotExceptions.test.ts
+++ b/src/runner/utils/__tests__/pivotExceptions.test.ts
@@ -1,19 +1,22 @@
 import { DiagnosticSeverity, Dictionary } from '@stoplight/types';
-import { InvalidUriError } from '../../rulesets/mergers/exceptions';
-import { IRunRule } from '../../types';
+import { InvalidUriError } from '../../../rulesets/mergers/exceptions';
 import { IExceptionLocation, pivotExceptions } from '../pivotExceptions';
 
-import { buildRulesetExceptionCollectionFrom } from '../../../setupTests';
+import { buildRulesetExceptionCollectionFrom } from '../../../../setupTests';
+import { Rule } from '../../../rule';
 
 describe('pivotExceptions', () => {
-  const dummyRule: IRunRule = {
-    name: '',
-    severity: DiagnosticSeverity.Error,
-    given: '',
-    then: {
-      function: 'truthy',
-    },
-  };
+  let dummyRule: Rule;
+
+  beforeEach(() => {
+    dummyRule = new Rule('', {
+      severity: DiagnosticSeverity.Error,
+      given: '',
+      then: {
+        function: 'truthy',
+      },
+    });
+  });
 
   it('ignores exceptions for rules that are not part of the run', () => {
     const exceptions = {