Repository: stoplightio/spectral
Pull request #631 (2019-10-03 16:03:35)

Release 4.2


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 357370db..bb780824 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -22,16 +22,7 @@ commands:
     steps:
       - checkout
       - cached-dependencies
-      - run:
-          name: Download cc-test-reporter
-          command: |
-            curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
-            chmod +x ./cc-test-reporter
       - run: yarn
-      - run:
-          name: cc-before
-          command: |
-            ./cc-test-reporter before-build
       - run:
           name: Build the code
           command: yarn build
@@ -53,10 +44,6 @@ commands:
       - run:
           name: Lint the code
           command: yarn lint
-      - run:
-          name: cc-after
-          command: |
-            ./cc-test-reporter after-build --coverage-input-type lcov --exit-code $?
 
 jobs:
   test-node-latest:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1985dc44..0b14b356 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,38 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [4.2.0] - 2019-10-08
+
+### Features
+- CLI: glob patterns and multiple paths are allowed [#534](https://github.com/stoplightio/spectral/issues/534)
+- CLI: control fail severity and result display [#368](https://github.com/stoplightio/spectral/issues/368)
+- CLI: new formatter - JUnit [#478](https://github.com/stoplightio/spectral/issues/478)
+- CLI: add possibility to proxy requests [#446](https://github.com/stoplightio/spectral/issues/446)
+- Built-in ruleset formats targeting JSON Schema files [#571](https://github.com/stoplightio/spectral/issues/571)
+- `{{value}}` and `{{path}}` can be used in messages [#520](https://github.com/stoplightio/spectral/issues/520) [#572](https://github.com/stoplightio/spectral/issues/572)
+
+### Deprecated
+- `when` Rule property is deprecated [#585](https://github.com/stoplightio/spectral/issues/585) 
+
+### Changed
+- Validation results produced by `alphabetical` function are more meaningful [#613](https://github.com/stoplightio/spectral/pull/613)
+- Enhanced JSON Schema enum validation [#579](https://github.com/stoplightio/spectral/pull/579) 
+- Improved messages generated by `oasPathParam` function [#537](https://github.com/stoplightio/spectral/issues/537) 
+- CLI: the amount of enabled rules is now displayed if you run Spectral with `--verbose` flag [#435](https://github.com/stoplightio/spectral/issues/435)
+- Stricter source matching for errors [#615](https://github.com/stoplightio/spectral/pull/615)
+
+### Fixed
+- `schema` function can validate falsy values [10e5d1c](https://github.com/stoplightio/spectral/commit/10e5d1c0262790ad8349e25b2e5517e7ae15402c)
+- `schema` function can validate Draft 6 and Draft 7 JSON Schemas [ea2ddff](https://github.com/stoplightio/spectral/commit/ea2ddffd58a2a92f483147fec195e0a8fe80c07b)
+- Parameters in links objects are not linted for not having a description property. [#272](https://github.com/stoplightio/spectral/issues/272)
+- More accurate ranges for errors occurring in referenced files [6986b82](https://github.com/stoplightio/spectral/commit/6986b82bee725aa8733c2d07ccc65a99e14c22c6)
+- CLI: stylish formatter reports `info` and `hint` severity levels correctly [#565](https://github.com/stoplightio/spectral/issues/565)
+
+## [4.1.1] - 2019-09-05
+
+### Fixed
+CLI: missing tslib [#524](https://github.com/stoplightio/spectral/issues/524)
+
 ## [4.1.0] - 2019-09-04
 
 ### Added
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c21a5781..36ba260e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -18,10 +18,10 @@ Yarn is a package manager for your code, similar to npm. While you can use npm t
 
 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your computer.
 2. Install yarn: `npm install -g yarn`
-3. In your terminal, navigate to the directory you cloned Spectral into.
+3. In your terminal, navigate to the directory you cloned Spectral into (check that you are on the `develop` branch).
 4. Install the dependencies: `yarn`
 5. Build Spectral: `yarn build`
-6. Run Spectral from your local installation: `bin/run lint openapi.yml`
+6. Run Spectral from your local installation: `node dist/cli/index.js lint [openapi_spec_file]`
 7. Create a new branch for your work: `git checkout -b [name_of_your_new_branch]`
 8. Make changes, add tests, and then run the tests: `yarn test.prod`
 
diff --git a/README.md b/README.md
index 1a7bedd7..89b80d3f 100644
--- a/README.md
+++ b/README.md
@@ -74,10 +74,11 @@ If you are using Spectral in your project and want to be listed in the examples
 - [stoplightio/json](https://github.com/stoplightio/json), a library of useful functions for when working with JSON
 - [stoplightio/yaml](https://github.com/stoplightio/yaml), a library of useful functions for when working with YAML, including parsing YAML into JSON, and a few helper functions such as `getJsonPathForPosition` or `getLocationForJsonPath`
 
-## Thanks :)
+## Thanks
 
-- [Phil Sturgeon](https://github.com/philsturgeon) for collaboration and creating Speccy
 - [Mike Ralphson](https://github.com/MikeRalphson) for kicking off the Spectral CLI and his work on Speccy
+- [Jamund Ferguson](https://github.com/xjamundx) for JUnit formatter
+- [Sindre Sorhus](https://github.com/sindresorhus) for Stylish formatter
 
 ## Support
 
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index b83b7d79..c4de087a 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -6,6 +6,11 @@ trigger:
     include:
       - master
       - develop
+pr:
+  branches:
+    include:
+      - master
+      - develop
 jobs:
   - job: test
     displayName: Test
diff --git a/docs/getting-started/rulesets.md b/docs/getting-started/rulesets.md
index f8f530ee..9052e852 100644
--- a/docs/getting-started/rulesets.md
+++ b/docs/getting-started/rulesets.md
@@ -12,6 +12,7 @@ rules:
     description: Tags must have a description.
     given: $.tags[*]
     severity: error
+    recommended: true
     then:
       field: description
       function: truthy
@@ -60,11 +61,17 @@ You might find `resolved` useful if your rule requires access to $refs.
 
 ## Formats
 
-Formats are an optional way to specify which API description formats a rule, or ruleset, is applicable to. Currently Spectral supports these two formats:
+Formats are an optional way to specify which API description formats a rule, or ruleset, is applicable to. Currently Spectral supports these formats:
 
 - `oas2` (this is OpenAPI v2.0 - formerly known as Swagger)
 - `oas3` (this is OpenAPI v3.0)
-
+- `json-schema` (this is JSON Schema, detection based on the value of $schema property)
+- `json-schema-loose` (this is JSON Schema, loose check, no $schema required)
+- `json-schema-draft4` (this is JSON Schema Draft 4, detection based on the value of $schema property)
+- `json-schema-draft6` (this is JSON Schema Draft 6, detection based on the value of $schema property)
+- `json-schema-draft7` (this is JSON Schema Draft 7, detection based on the value of $schema property)
+- `json-schema-2019-09` (this is JSON Schema 2019-09, detection based on the value of $schema property)
+  
 Specifying the format is optional, so you can completely ignore this if all the rules you are writing apply to any document you lint, or if you have specific rulesets for different formats. If you'd like to use one ruleset for multiple formats, the formats key is here to help.
 
 ```yaml
@@ -219,4 +226,4 @@ Available severity levels are `error`, `warn`, `info`, `hint`, and `off`.
 
 ## Creating custom functions
 
-Learn more about [custom functions](./custom-functions.md). 
+Learn more about [custom functions](../guides/custom-functions.md).
diff --git a/docs/guides/cli.md b/docs/guides/cli.md
index 395409dd..8e2e4a02 100644
--- a/docs/guides/cli.md
+++ b/docs/guides/cli.md
@@ -6,19 +6,52 @@ Once installed, Spectral can be run via the command-line:
 spectral lint petstore.yaml
 ```
 
+You can lint multiple files at the same time by passing on multiple arguments:
+
+```bash
+spectral lint petstore.yaml https://example.com/petstore/openapi-v2.json https://example.com/todos/openapi-v3.json
+```
+
+Alternatively you can use [glob syntax](https://github.com/mrmlnc/fast-glob#basic-syntax) to match multiple files at once:
+
+```bash
+spectral lint ./reference/**/*.oas*.{json,yml,yaml}
+```
+
 Other options include:
 
 ``` text
-  -e, --encoding=encoding      text encoding to use
-  -f, --format=json|stylish    formatter to use for outputting results
-  -h, --help                   show CLI help
-  -o, --output=output          output to a file instead of stdout
-  -q, --quiet                  no logging - output only
-  -r, --ruleset=ruleset        path to a ruleset file (supports remote files)
-  -s, --skip-rule=skip-rule    ignore certain rules if they are causing trouble
-  -v, --verbose                increase verbosity
+  --version                    Show version number                                          [boolean]
+  --help                       Show help                                                    [boolean]
+  --encoding, -e               text encoding to use                        [string] [default: "utf8"]
+  --format, -f                 formatter to use for outputting results  [string] [default: "stylish"]
+  --output, -o                 output to a file instead of stdout                            [string]
+  --ruleset, -r                path/URL to a ruleset file                                    [string]
+  --skip-rule, -s              ignore certain rules if they are causing trouble              [string]
+  --fail-severity, -F          results of this level or above will trigger a failure exit code
+                                [string] [choices: "error", "warn", "info", "hint"] [default: "hint"]
+  --display-only-failures, -D  only output results equal to or greater than --fail-severity
+                                                                           [boolean] [default: false]
+  --verbose, -v                increase verbosity                                           [boolean]
+  --quiet, -q                  no logging - output only                                     [boolean]
 ```
 
-> Note: The Spectral CLI supports both YAML and JSON.
+The Spectral CLI supports loading documents as YAML or JSON, and validation of OpenAPI v2/v3 documents via our built-in ruleset. 
+
+You can also provide your own ruleset file. By default, the Spectral CLI will look for a ruleset file called `.spectral.yml` or `.spectral.json` in the current working directory. You can tell spectral to use a different file by using the `--ruleset` CLI option.
+
+Here you can build a [custom ruleset](../getting-started/rulesets.md), or extend and modify our [core OpenAPI ruleset](https://stoplight.io/p/docs/gh/stoplightio/spectral/docs/reference/openapi-rules.md).
+
+## Error Results
+
+Spectral has a few different error severities: `error`, `warn`, `info` and `hint`, and they are in "order" from highest to lowest. By default, all results will be shown regardless of severity, and the presence of any results will cause a failure status code of 1.
+
+The default behavior is can be modified with the `--fail-severity=` option. Setting fail severity to `--fail-severity=warn` would return a status code of 1 for any warning results or higher, so that would also include error. Using `--fail-severity=error` will only show errors.
+
+Changing the fail severity will not effect output. To change what results Spectral CLI prints to the screen, add the `--display-only-failures` switch (or just `-D` for short). This will strip out any results which are below the fail severity.
+
+## Proxying
+
+To have requests made from Spectral be proxied through a server, you'd need to specify PROXY environment variable:
 
-Currently, Spectral CLI supports validation of OpenAPI v2/v3 documents via our built-in ruleset, or you can create [custom rulesets](../getting-started/rulesets.md) to work with any JSON/YAML documents.
+`PROXY=<<PROXY_SERVER_ADDRESS>> spectral lint spec.yaml`
diff --git a/docs/guides/custom-functions.md b/docs/guides/custom-functions.md
index cee4d5c6..6c30fcba 100644
--- a/docs/guides/custom-functions.md
+++ b/docs/guides/custom-functions.md
@@ -48,26 +48,42 @@ rules:
       function: "abc"
 ```
 
-If your function accepts options, you should provide a JSON Schema that describes those options.
+If you are writing a function that accepts options, you should provide a JSON Schema that describes those options.
 
 You can do it as follows:
 
 ```yaml
 functions:
-- - abc
+- - equals
   # can be any valid JSONSchema7
   - properties:
-      sort:
-        type: boolean
-        description: enforces that abc is sorted
+      value:
+        type: string
+        description: Value to check equality for
 rules:
   my-rule:
     message: "{{error}}"
     given: "$.info"
     then:
-      function: "abc"
+      function: "equals"
       functionOptions:
-        sort: true
+        value: "abc"
+```
+
+Where the function **functions/equals.js** might look like:
+
+```js
+module.exports = (targetVal, opts) => {
+  const { value } = opts;
+
+  if (targetVal !== value) {
+    return [
+      {
+        message: `Value must equal {value}.`,
+      },
+    ];
+  }
+};
 ```
 
 If for some reason, you do not want to place your functions in a directory called `functions`, you can specify a custom directory.
diff --git a/docs/guides/workflows.md b/docs/guides/workflows.md
index ad60d623..3faaee90 100644
--- a/docs/guides/workflows.md
+++ b/docs/guides/workflows.md
@@ -34,8 +34,8 @@ Folks will forget to run Spectral, and that means they can commit broken or (low
 {
   "husky": {
     "hooks": {
-      "pre-commit": "spectral lint openapi.yaml",
-      "pre-push": "spectral lint openapi.yaml",
+      "pre-commit": "spectral lint ./reference/**/*.{json,yml,yaml}",
+      "pre-push": "spectral lint ./reference/**/*.{json,yml,yaml}",
       "...": "..."
     }
   }
diff --git a/docs/reference/openapi-rules.md b/docs/reference/openapi-rules.md
index ba03c78e..4442a4cc 100644
--- a/docs/reference/openapi-rules.md
+++ b/docs/reference/openapi-rules.md
@@ -334,12 +334,6 @@ Operation should have non-empty `tags` array.
 
 **Recommended:** Yes
 
-### parameter-description
-
-Parameter objects should have a `description`.
-
-**Recommended:** No
-
 ### path-declarations-must-exist
 
 Path parameter declarations cannot be empty, ex.`/given/{}` is invalid.
@@ -451,6 +445,12 @@ Validate structure of OpenAPI v2 specification.
 
 **Recommended:** Yes
 
+### oas2-parameter-description
+
+Parameter objects should have a `description`.
+
+**Recommended:** No
+
 ## oas3
 
 These OpenAPI v3.0-only rules can be loaded with `extends: "spectral: oas3"` and include all the oas rules.
@@ -543,3 +543,9 @@ Examples must be valid against their defined schema.
 Validate structure of OpenAPI v3 specification.
 
 **Recommended:** Yes
+
+### oas3-parameter-description
+
+Parameter objects should have a `description`.
+
+**Recommended:** No
\ No newline at end of file
diff --git a/karma.conf.ts b/karma.conf.ts
index 80c41c84..186db628 100644
--- a/karma.conf.ts
+++ b/karma.conf.ts
@@ -14,8 +14,7 @@ module.exports = (config: any) => {
     files: ['./karma-jest.ts', './setupKarma.ts', 'src/**/*.ts'],
 
     // list of files / patterns to exclude
-    // unit tests are excluded because most of them make use of jest.mock
-    exclude: ['src/cli/**', 'src/config/__tests__/config.test.ts', 'src/**/*.jest.test.ts'],
+    exclude: ['src/cli/**', 'src/formatters/**', 'src/config/__tests__/config.test.ts', 'src/**/*.jest.test.ts'],
 
     // preprocess matching files before serving them to the browser
     // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
diff --git a/package.json b/package.json
index 394c53c4..750af32e 100644
--- a/package.json
+++ b/package.json
@@ -33,9 +33,11 @@
     "node": ">=8.3.0"
   },
   "scripts": {
-    "build.binary": "pkg . --targets linux,macos --out-path ./binaries",
+    "build.binary": "pkg . --output ./binaries/spectral",
     "build.oas-functions": "rollup -c",
     "build": "tsc -p ./tsconfig.build.json",
+    "cli": "node -r ts-node/register -r tsconfig-paths/register src/cli/index.ts",
+    "cli:debug": "node -r ts-node/register -r tsconfig-paths/register --inspect-brk src/cli/index.ts",
     "compile-rulesets": "node ./scripts/compile-rulesets.js",
     "inline-version": "./scripts/inline-version.js",
     "lint.fix": "yarn lint --fix",
@@ -62,13 +64,15 @@
     "abort-controller": "^3.0.0",
     "ajv": "^6.7",
     "ajv-oai": "^1.1.1",
-    "better-ajv-errors": "stoplightio/better-ajv-errors#0c5be246c12c87993ba07b47479cff8257b9579b",
+    "better-ajv-errors": "^0.6.7",
     "chalk": "^2.4.2",
     "deprecated-decorator": "^0.1.6",
+    "fast-glob": "^3.0.4",
     "jsonpath-plus": "~1.0",
     "lodash": ">=4.17.5",
     "nanoid": "^2.0.3",
     "node-fetch": "^2.6",
+    "proxy-agent": "^3.1.0",
     "strip-ansi": "^5.2",
     "text-table": "^0.2",
     "tslib": "^1.10.0",
@@ -83,9 +87,11 @@
     "@types/jest-when": "^2.4.0",
     "@types/lodash": "^4.14.136",
     "@types/nanoid": "^2.0.0",
-    "@types/nock": "^10.0.3",
+    "@types/nock": "^11.1.0",
     "@types/node": "~12.7",
+    "@types/node-fetch": "^2.5.0",
     "@types/text-table": "^0.2.0",
+    "@types/xml2js": "^0.4.4",
     "@types/yaml": "^1.0.2",
     "copyfiles": "^2.1.1",
     "fetch-mock": "^7.3.9",
@@ -93,13 +99,13 @@
     "husky": "^3.0.0",
     "jest": "~24.9",
     "jest-mock": "^24.8.0",
-    "jest-when": "~2.6",
+    "jest-when": "~2.7",
     "karma": "^4.2.0",
     "karma-chrome-launcher": "^3.0.0",
     "karma-jasmine": "^2.0.1",
     "karma-typescript": "^4.1.1",
     "lint-staged": "^9.0.2",
-    "nock": "~10.0.6",
+    "nock": "~11.4.0",
     "pkg": "^4.4.0",
     "recast": "^0.18.1",
     "rollup": "^1.19.4",
@@ -110,10 +116,10 @@
     "ts-jest": "^24.0.2",
     "ts-node": "^8.1.0",
     "tsconfig-paths": "^3.8.0",
-    "tslint": "~5.19",
+    "tslint": "~5.20",
     "tslint-config-stoplight": "~1.3",
     "typescript": "^3.5.3",
-    "yaml": "~1.6"
+    "xml2js": "^0.4.21"
   },
   "lint-staged": {
     "*.{ts,tsx}": [
diff --git a/src/cli/commands/lint.ts b/src/cli/commands/lint.ts
index 4ad2c6e2..3cedc924 100644
--- a/src/cli/commands/lint.ts
+++ b/src/cli/commands/lint.ts
@@ -2,26 +2,31 @@ import { Dictionary } from '@stoplight/types';
 import { CommandModule, showHelp } from 'yargs';
 
 import { pick } from 'lodash';
-import { ILintConfig, OutputFormat } from '../../types/config';
+import { getDiagnosticSeverity } from '../../rulesets/severity';
+import { IRuleResult } from '../../types';
+import { FailSeverity, ILintConfig, OutputFormat } from '../../types/config';
 import { lint } from '../services/linter';
 import { formatOutput, writeOutput } from '../services/output';
 
 const toArray = (args: unknown) => (Array.isArray(args) ? args : [args]);
 
+const formatOptions = Object.values(OutputFormat);
+
 const lintCommand: CommandModule = {
-  describe: 'lint a JSON/YAML document from a file or URL',
-  command: 'lint <document>',
+  describe: 'lint JSON/YAML documents from files or URLs',
+  command: 'lint <documents..>',
   builder: yargs =>
     yargs
-      .positional('document', {
-        description: 'Location of a JSON/YAML document. Can be either a file or a fetchable resource on the web.',
+      .positional('documents', {
+        description:
+          'Location of JSON/YAML documents. Can be either a file, a glob or fetchable resource(s) on the web.',
         type: 'string',
       })
       .fail(() => {
         showHelp();
       })
       .check((argv: Dictionary<unknown>) => {
-        if (argv.format !== void 0 && argv.format !== OutputFormat.JSON && argv.format !== OutputFormat.STYLISH) {
+        if (argv.format !== void 0 && !(formatOptions as string[]).includes(String(argv.format))) {
           return false;
         }
 
@@ -37,7 +42,7 @@ const lintCommand: CommandModule = {
         format: {
           alias: 'f',
           description: 'formatter to use for outputting results',
-          options: [OutputFormat.STYLISH, OutputFormat.JSON],
+          options: formatOptions,
           default: OutputFormat.STYLISH,
           type: 'string',
         },
@@ -58,6 +63,19 @@ const lintCommand: CommandModule = {
           type: 'string',
           coerce: toArray,
         },
+        'fail-severity': {
+          alias: 'F',
+          description: 'results of this level or above will trigger a failure exit code',
+          choices: ['error', 'warn', 'info', 'hint'],
+          default: 'hint', // TODO: BREAKING: raise this to warn in 5.0
+          type: 'string',
+        },
+        'display-only-failures': {
+          alias: 'D',
+          description: 'only output results equal to or greater than --fail-severity',
+          type: 'boolean',
+          default: false,
+        },
         verbose: {
           alias: 'v',
           description: 'increase verbosity',
@@ -71,20 +89,33 @@ const lintCommand: CommandModule = {
       }),
 
   handler: args => {
-    const { document, ruleset, format, output, encoding, ...config } = (args as unknown) as ILintConfig & {
-      document: string;
+    const {
+      documents,
+      failSeverity,
+      displayOnlyFailures,
+      ruleset,
+      format,
+      output,
+      encoding,
+      ...config
+    } = (args as unknown) as ILintConfig & {
+      documents: string[];
+      failSeverity: FailSeverity;
+      displayOnlyFailures: boolean;
     };
 
-    return lint(
-      document,
-      { format, output, encoding, ...pick(config, ['ruleset', 'skipRule', 'verbose', 'quiet']) },
-      ruleset,
-    )
+    return lint(documents, { format, output, encoding, ruleset, ...pick(config, ['skipRule', 'verbose', 'quiet']) })
+      .then(results => {
+        if (displayOnlyFailures) {
+          return filterResultsBySeverity(results, failSeverity);
+        }
+        return results;
+      })
       .then(results => {
         if (results.length) {
-          process.exitCode = 1;
+          process.exitCode = severeEnoughToFail(results, failSeverity) ? 1 : 0;
         } else if (!config.quiet) {
-          console.log('No errors or warnings found!');
+          console.log(`No results with a severity of '${failSeverity}' or higher found!`);
         }
         const formattedOutput = formatOutput(results, format);
         return writeOutput(formattedOutput, output);
@@ -93,9 +124,19 @@ const lintCommand: CommandModule = {
   },
 };
 
-function fail(err: Error) {
+const fail = (err: Error) => {
   console.error(err);
   process.exitCode = 2;
-}
+};
+
+const filterResultsBySeverity = (results: IRuleResult[], failSeverity: FailSeverity): IRuleResult[] => {
+  const diagnosticSeverity = getDiagnosticSeverity(failSeverity);
+  return results.filter(r => r.severity <= diagnosticSeverity);
+};
+
+const severeEnoughToFail = (results: IRuleResult[], failSeverity: FailSeverity): boolean => {
+  const diagnosticSeverity = getDiagnosticSeverity(failSeverity);
+  return results.some(r => r.severity <= diagnosticSeverity);
+};
 
 export default lintCommand;
diff --git a/src/cli/index.ts b/src/cli/index.ts
index 1a5a4cbb..47884a57 100644
--- a/src/cli/index.ts
+++ b/src/cli/index.ts
@@ -2,8 +2,14 @@
 
 import * as yargs from 'yargs';
 
+import { DEFAULT_REQUEST_OPTIONS } from '../request';
 import lintCommand from './commands/lint';
 
+if (process.env.PROXY) {
+  const ProxyAgent = require('proxy-agent');
+  DEFAULT_REQUEST_OPTIONS.agent = new ProxyAgent(process.env.PROXY);
+}
+
 export default yargs
   .scriptName('spectral')
   .parserConfiguration({
diff --git a/src/cli/services/linter.ts b/src/cli/services/linter.ts
deleted file mode 100644
index 482e9428..00000000
--- a/src/cli/services/linter.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-import { isAbsolute, resolve } from '@stoplight/path';
-import { IParserResult, Optional } from '@stoplight/types';
-import { getLocationForJsonPath, parseWithPointers } from '@stoplight/yaml';
-
-import { readParsable } from '../../fs/reader';
-import { httpAndFileResolver } from '../../resolvers/http-and-file';
-import { getDefaultRulesetFile } from '../../rulesets/loader';
-import { isOpenApiv2, isOpenApiv3 } from '../../rulesets/lookups';
-import { readRuleset } from '../../rulesets/reader';
-import { Spectral } from '../../spectral';
-import { IParsedResult, RuleCollection } from '../../types';
-import { ILintConfig } from '../../types/config';
-import { IRuleset } from '../../types/ruleset';
-
-export async function loadRulesets(cwd: string, rulesetFiles: string[]): Promise<IRuleset> {
-  if (rulesetFiles.length === 0) {
-    return {
-      functions: {},
-      rules: {},
-    };
-  }
-
-  return readRuleset(rulesetFiles.map(file => (isAbsolute(file) ? file : resolve(cwd, file))));
-}
-
-export async function lint(name: string, flags: ILintConfig, rulesetFile: Optional<string[]>) {
-  if (flags.verbose) {
-    console.info(`Linting ${name}`);
-  }
-
-  const targetUri = !/^https?:\/\//.test(name) && !isAbsolute(name) ? resolve(process.cwd(), name) : name;
-
-  const spec: IParserResult = parseWithPointers(await readParsable(targetUri, { encoding: flags.encoding }), {
-    ignoreDuplicateKeys: false,
-    mergeKeys: true,
-  });
-
-  const rulesetFiles = rulesetFile || (await getDefaultRulesetFile(process.cwd()));
-  const ruleset = await (rulesetFiles
-    ? loadRulesets(process.cwd(), Array.isArray(rulesetFiles) ? rulesetFiles : [rulesetFiles])
-    : readRuleset('spectral:oas'));
-
-  const spectral = new Spectral({ resolver: httpAndFileResolver });
-
-  if (flags.verbose) {
-    if (ruleset) {
-      console.info(`Found ${Object.keys(ruleset.rules).length} rules`);
-    } else {
-      console.info('No rules loaded, attempting to detect document type');
-    }
-  }
-
-  spectral.registerFormat('oas2', document => {
-    if (isOpenApiv2(document)) {
-      if (!flags.quiet) {
-        console.log('OpenAPI 2.0 (Swagger) detected');
-      }
-
-      return true;
-    }
-    return false;
-  });
-
-  spectral.registerFormat('oas3', document => {
-    if (isOpenApiv3(document)) {
-      if (!flags.quiet) {
-        console.log('OpenAPI 3.x detected');
-      }
-
-      return true;
-    }
-    return false;
-  });
-
-  spectral.setRuleset(ruleset);
-
-  if (flags.skipRule) {
-    spectral.setRules(skipRules(ruleset.rules, flags));
-  }
-
-  const parsedResult: IParsedResult = {
-    source: targetUri,
-    parsed: spec,
-    getLocationForJsonPath,
-  };
-
-  return spectral.run(parsedResult, {
-    resolve: {
-      documentUri: targetUri,
-    },
-  });
-}
-
-const skipRules = (rules: RuleCollection, flags: ILintConfig): RuleCollection => {
-  const skippedRules: string[] = [];
-  const invalidRules: string[] = [];
-
-  if (flags.skipRule !== undefined) {
-    for (const rule of Array.isArray(flags.skipRule) ? flags.skipRule : [flags.skipRule]) {
-      if (rule in rules) {
-        delete rules[rule];
-        skippedRules.push(rule);
-      } else {
-        invalidRules.push(rule);
-      }
-    }
-  }
-
-  if (invalidRules.length !== 0 && !flags.quiet) {
-    console.warn(`ignoring invalid ${invalidRules.length > 1 ? 'rules' : 'rule'} "${invalidRules.join(', ')}"`);
-  }
-
-  if (skippedRules.length !== 0 && flags.verbose) {
-    console.info(`skipping ${skippedRules.length > 1 ? 'rules' : 'rule'} "${skippedRules.join(', ')}"`);
-  }
-
-  return rules;
-};
diff --git a/src/cli/services/linter/index.ts b/src/cli/services/linter/index.ts
new file mode 100644
index 00000000..449df5db
--- /dev/null
+++ b/src/cli/services/linter/index.ts
@@ -0,0 +1,1 @@
+export * from './linter';
diff --git a/src/cli/services/linter/linter.ts b/src/cli/services/linter/linter.ts
new file mode 100644
index 00000000..0520072b
--- /dev/null
+++ b/src/cli/services/linter/linter.ts
@@ -0,0 +1,95 @@
+import { IParserResult } from '@stoplight/types';
+import { getLocationForJsonPath, parseWithPointers } from '@stoplight/yaml';
+
+import {
+  isJSONSchema,
+  isJSONSchemaDraft2019_09,
+  isJSONSchemaDraft4,
+  isJSONSchemaDraft6,
+  isJSONSchemaDraft7,
+  isJSONSchemaLoose,
+  isOpenApiv2,
+  isOpenApiv3,
+} from '../../../formats';
+import { readParsable } from '../../../fs/reader';
+import { httpAndFileResolver } from '../../../resolvers/http-and-file';
+import { isRuleEnabled } from '../../../runner';
+import { IRuleResult, Spectral } from '../../../spectral';
+import { FormatLookup, IParsedResult } from '../../../types';
+import { ILintConfig } from '../../../types/config';
+import { getRuleset, listFiles, skipRules } from './utils';
+
+const KNOWN_FORMATS: Array<[string, FormatLookup, string]> = [
+  ['oas2', isOpenApiv2, 'OpenAPI 2.0 (Swagger) detected'],
+  ['oas3', isOpenApiv3, 'OpenAPI 3.x detected'],
+  ['json-schema', isJSONSchema, 'JSON Schema detected'],
+  ['json-schema-loose', isJSONSchemaLoose, 'JSON Schema (loose) detected'],
+  ['json-schema-draft4', isJSONSchemaDraft4, 'JSON Schema Draft 4 detected'],
+  ['json-schema-draft6', isJSONSchemaDraft6, 'JSON Schema Draft 6 detected'],
+  ['json-schema-draft7', isJSONSchemaDraft7, 'JSON Schema Draft 7 detected'],
+  ['json-schema-2019-09', isJSONSchemaDraft2019_09, 'JSON Schema Draft 2019-09 detected'],
+];
+
+export async function lint(documents: string[], flags: ILintConfig) {
+  const spectral = new Spectral({ resolver: httpAndFileResolver });
+
+  const ruleset = await getRuleset(flags.ruleset);
+  spectral.setRuleset(ruleset);
+
+  for (const [format, lookup, message] of KNOWN_FORMATS) {
+    spectral.registerFormat(format, document => {
+      if (lookup(document)) {
+        if (!flags.quiet) {
+          console.log(message);
+        }
+
+        return true;
+      }
+
+      return false;
+    });
+  }
+
+  if (flags.verbose) {
+    if (ruleset) {
+      const rules = Object.values(spectral.rules);
+      console.info(`Found ${rules.length} rules (${rules.filter(isRuleEnabled).length} enabled)`);
+    } else {
+      console.info('No rules loaded, attempting to detect document type');
+    }
+  }
+
+  if (flags.skipRule) {
+    spectral.setRules(skipRules(ruleset.rules, flags));
+  }
+
+  const targetUris = await listFiles(documents);
+  const results: IRuleResult[] = []; // todo: shall we display results as they come in?
+
+  for (const targetUri of targetUris) {
+    if (flags.verbose) {
+      console.info(`Linting ${targetUri}`);
+    }
+
+    const spec: IParserResult = parseWithPointers(await readParsable(targetUri, { encoding: flags.encoding }), {
+      ignoreDuplicateKeys: false,
+      mergeKeys: true,
+    });
+
+    const parsedResult: IParsedResult = {
+      source: targetUri,
+      parsed: spec,
+      getLocationForJsonPath,
+    };
+
+    results.push(
+      ...(await spectral.run(parsedResult, {
+        resolve: {
+          documentUri: targetUri,
+        },
+      })),
+    );
+  }
+
+  return results;
+}
diff --git a/src/cli/services/linter/utils/getRuleset.ts b/src/cli/services/linter/utils/getRuleset.ts
new file mode 100644
index 00000000..223bdd53
--- /dev/null
+++ b/src/cli/services/linter/utils/getRuleset.ts
@@ -0,0 +1,24 @@
+import { isAbsolute, resolve } from '@stoplight/path';
+import { Optional } from '@stoplight/types';
+import { readRuleset } from '../../../../rulesets';
+import { getDefaultRulesetFile } from '../../../../rulesets/loader';
+import { IRuleset } from '../../../../types/ruleset';
+
+async function loadRulesets(cwd: string, rulesetFiles: string[]): Promise<IRuleset> {
+  if (rulesetFiles.length === 0) {
+    return {
+      functions: {},
+      rules: {},
+    };
+  }
+
+  return readRuleset(rulesetFiles.map(file => (isAbsolute(file) ? file : resolve(cwd, file))));
+}
+
+export async function getRuleset(rulesetFile: Optional<string[]>) {
+  const rulesetFiles = rulesetFile || (await getDefaultRulesetFile(process.cwd()));
+
+  return await (rulesetFiles
+    ? loadRulesets(process.cwd(), Array.isArray(rulesetFiles) ? rulesetFiles : [rulesetFiles])
+    : readRuleset('spectral:oas'));
+}
diff --git a/src/cli/services/linter/utils/index.ts b/src/cli/services/linter/utils/index.ts
new file mode 100644
index 00000000..4912e9c0
--- /dev/null
+++ b/src/cli/services/linter/utils/index.ts
@@ -0,0 +1,3 @@
+export * from './getRuleset';
+export * from './listFiles';
+export * from './skipRules';
diff --git a/src/cli/services/linter/utils/listFiles.ts b/src/cli/services/linter/utils/listFiles.ts
new file mode 100644
index 00000000..0f57cd7c
--- /dev/null
+++ b/src/cli/services/linter/utils/listFiles.ts
@@ -0,0 +1,22 @@
+import { normalize } from '@stoplight/path';
+import * as fg from 'fast-glob';
+
+export async function listFiles(pattens: string[]): Promise<string[]> {
+  const { files, urls } = pattens.reduce<{ files: string[]; urls: string[] }>(
+    (group, pattern) => {
+      if (!/^https?:\/\//.test(pattern)) {
+        group.files.push(pattern);
+      } else {
+        group.urls.push(pattern);
+      }
+
+      return group;
+    },
+    {
+      files: [],
+      urls: [],
+    },
+  );
+
+  return [...urls, ...(await fg(files, { dot: true, absolute: true })).map(normalize)]; // let's normalize OS paths produced by fast-glob to have consistent paths across all platforms
+}
diff --git a/src/cli/services/linter/utils/skipRules.ts b/src/cli/services/linter/utils/skipRules.ts
new file mode 100644
index 00000000..a937e437
--- /dev/null
+++ b/src/cli/services/linter/utils/skipRules.ts
@@ -0,0 +1,28 @@
+import { RuleCollection } from '../../../../types';
+import { ILintConfig } from '../../../../types/config';
+
+export const skipRules = (rules: RuleCollection, flags: ILintConfig): RuleCollection => {
+  const skippedRules: string[] = [];
+  const invalidRules: string[] = [];
+
+  if (flags.skipRule !== undefined) {
+    for (const rule of Array.isArray(flags.skipRule) ? flags.skipRule : [flags.skipRule]) {
+      if (rule in rules) {
+        delete rules[rule];
+        skippedRules.push(rule);
+      } else {
+        invalidRules.push(rule);
+      }
+    }
+  }
+
+  if (invalidRules.length !== 0 && !flags.quiet) {
+    console.warn(`ignoring invalid ${invalidRules.length > 1 ? 'rules' : 'rule'} "${invalidRules.join(', ')}"`);
+  }
+
+  if (skippedRules.length !== 0 && flags.verbose) {
+    console.info(`skipping ${skippedRules.length > 1 ? 'rules' : 'rule'} "${skippedRules.join(', ')}"`);
+  }
+
+  return rules;
+};
diff --git a/src/cli/services/output.ts b/src/cli/services/output.ts
index 0a5418b5..cdbe02af 100644
--- a/src/cli/services/output.ts
+++ b/src/cli/services/output.ts
@@ -1,16 +1,21 @@
+import { Dictionary } from '@stoplight/types';
 import { writeFile } from 'fs';
 import { promisify } from 'util';
-import { json, stylish } from '../../formatters';
+import { json, junit, stylish } from '../../formatters';
+import { Formatter } from '../../formatters/types';
 import { IRuleResult } from '../../types';
 import { OutputFormat } from '../../types/config';
 
 const writeFileAsync = promisify(writeFile);
 
+const formatters: Dictionary<Formatter, OutputFormat> = {
+  json,
+  stylish,
+  junit,
+};
+
 export function formatOutput(results: IRuleResult[], format: OutputFormat): string {
-  return {
-    json: () => json(results),
-    stylish: () => stylish(results),
-  }[format]();
+  return formatters[format](results);
 }
 
 export async function writeOutput(outputStr: string, outputFile?: string) {
diff --git a/src/formats/index.ts b/src/formats/index.ts
new file mode 100644
index 00000000..54f1929e
--- /dev/null
+++ b/src/formats/index.ts
@@ -0,0 +1,2 @@
+export * from './openapi';
+export * from './json-schema';
diff --git a/src/formats/json-schema.ts b/src/formats/json-schema.ts
new file mode 100644
index 00000000..5d75a25f
--- /dev/null
+++ b/src/formats/json-schema.ts
@@ -0,0 +1,41 @@
+import { JSONSchema4, JSONSchema6, JSONSchema7 } from 'json-schema';
+import { isObject } from '../utils/isObject';
+
+type JSONSchema = JSONSchema4 | JSONSchema6 | JSONSchema7;
+
+const KNOWN_JSON_SCHEMA_TYPES = ['array', 'boolean', 'integer', 'null', 'number', 'object', 'string'];
+const KNOWN_JSON_SCHEMA_COMBINERS = ['allOf', 'oneOf', 'anyOf'];
+const $SCHEMA_DRAFT4_REGEX = /^https?:\/\/json-schema.org\/draft-04\/(?:hyper-)?schema#?$/;
+const $SCHEMA_DRAFT6_REGEX = /^https?:\/\/json-schema.org\/draft-06\/(?:hyper-)?schema#?$/;
+const $SCHEMA_DRAFT7_REGEX = /^https?:\/\/json-schema.org\/draft-07\/(?:hyper-)?schema#?$/;
+const $SCHEMA_DRAFT_2019_09_REGEX = /^https?:\/\/json-schema.org\/draft\/2019-09\/(?:hyper-)?schema#?$/;
+
+const hasValidJSONSchemaType = (document: object | Partial<{ type: unknown }>) =>
+  'type' in document && typeof document.type === 'string' && KNOWN_JSON_SCHEMA_TYPES.includes(document.type);
+
+const hasValidJSONSchemaCombiner = (document: object) =>
+  KNOWN_JSON_SCHEMA_COMBINERS.some(combiner => combiner in document && isObject(document[combiner]));
+
+export const isJSONSchema = (document: unknown): document is JSONSchema & { $schema: string } =>
+  isObject(document) &&
+  '$schema' in document &&
+  String((document as Partial<Pick<JSONSchema, '$schema'>>).$schema).includes('//json-schema.org/');
+
+export const isJSONSchemaLoose = (document: unknown) =>
+  isObject(document) &&
+  (isJSONSchema(document) ||
+    hasValidJSONSchemaType(document) ||
+    hasValidJSONSchemaCombiner(document) ||
+    'not' in document);
+
+export const isJSONSchemaDraft4 = (document: unknown) =>
+  isJSONSchema(document) && $SCHEMA_DRAFT4_REGEX.test(document.$schema);
+
+export const isJSONSchemaDraft6 = (document: unknown) =>
+  isJSONSchema(document) && $SCHEMA_DRAFT6_REGEX.test(document.$schema);
+
+export const isJSONSchemaDraft7 = (document: unknown) =>
+  isJSONSchema(document) && $SCHEMA_DRAFT7_REGEX.test(document.$schema);
+
+export const isJSONSchemaDraft2019_09 = (document: unknown) =>
+  isJSONSchema(document) && $SCHEMA_DRAFT_2019_09_REGEX.test(document.$schema);
diff --git a/src/rulesets/lookups.ts b/src/formats/openapi.ts
similarity index 85%
rename from src/rulesets/lookups.ts
rename to src/formats/openapi.ts
index ac166e4e..9f5c7c23 100644
--- a/src/rulesets/lookups.ts
+++ b/src/formats/openapi.ts
@@ -1,4 +1,4 @@
-const isObject = (thing: unknown): thing is object => thing !== null && typeof thing === 'object';
+import { isObject } from '../utils/isObject';
 
 type MaybeOAS2 = Partial<{ swagger: unknown }>;
 type MaybeOAS3 = Partial<{ openapi: unknown }>;
diff --git a/src/formatters/index.ts b/src/formatters/index.ts
index 074d5806..c51aab7a 100644
--- a/src/formatters/index.ts
+++ b/src/formatters/index.ts
@@ -1,2 +1,3 @@
 export * from './json';
 export * from './stylish';
+export * from './junit';
diff --git a/src/formatters/json.ts b/src/formatters/json.ts
index c3f3ead0..4ff9fbce 100644
--- a/src/formatters/json.ts
+++ b/src/formatters/json.ts
@@ -1,6 +1,6 @@
-import { IRuleResult } from '../types';
+import { Formatter } from './types';
 
-export const json = (results: IRuleResult[]): string => {
+export const json: Formatter = results => {
   const outputJson = results.map(result => {
     return {
       code: result.code,
diff --git a/src/formatters/junit.ts b/src/formatters/junit.ts
new file mode 100644
index 00000000..543da66e
--- /dev/null
+++ b/src/formatters/junit.ts
@@ -0,0 +1,81 @@
+/**
+ * Copyright JS Foundation and other contributors, https://js.foundation
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @fileoverview jUnit Reporter
+ * @author Jamund Ferguson
+ */
+
+import { encodePointerFragment } from '@stoplight/json';
+import { extname } from '@stoplight/path';
+import { DiagnosticSeverity, JsonPath } from '@stoplight/types';
+import { escapeRegExp } from 'lodash';
+import { Formatter } from './types';
+import { groupBySource } from './utils/groupBySource';
+import { xmlEscape } from './utils/xmlEscape';
+
+function stringifyPath(path: JsonPath) {
+  return ['#', ...path.map(encodePointerFragment)].join('/');
+}
+
+export const junit: Formatter = results => {
+  let output = '';
+
+  output += '<?xml version="1.0" encoding="utf-8"?>\n';
+  output += '<testsuites>\n';
+
+  const groupedResults = groupBySource(results);
+
+  for (const [source, validationResults] of Object.entries(groupedResults)) {
+    const classname = source.replace(new RegExp(`${escapeRegExp(extname(source))}$`), '');
+
+    if (validationResults.length > 0) {
+      const filteredValidationResults = validationResults.filter(
+        result => result.severity === DiagnosticSeverity.Error,
+      );
+
+      output += `<testsuite package="org.spectral" time="0" tests="${
+        filteredValidationResults.length
+      }" errors="0" failures="${filteredValidationResults.length}" name="${source}">\n`;
+
+      for (const result of filteredValidationResults) {
+        output += `<testcase time="0" name="org.spectral.${result.code || 'unknown'}" classname="${classname}">`;
+        output += `<failure message="${xmlEscape(result.message)}">`;
+        output += '<![CDATA[';
+        output += `line ${result.range.start.line + 1}, col ${result.range.start.character + 1}, `;
+        output += `${xmlEscape(result.message)} (${result.code}) `;
+        output += `at path ${xmlEscape(stringifyPath(result.path))}`;
+        output += ']]>';
+        output += `</failure>`;
+        output += '</testcase>\n';
+      }
+
+      output += '</testsuite>\n';
+    } else {
+      output += `<testsuite package="org.spectral" time="0" tests="1" errors="0" name="${source}">\n`;
+      output += `<testcase time="0" name="${source}" classname="${classname}" />\n`;
+      output += '</testsuite>\n';
+    }
+  }
+
+  output += '</testsuites>\n';
+
+  return output;
+};
diff --git a/src/formatters/stylish.ts b/src/formatters/stylish.ts
index 4e7615cf..cc7b355f 100644
--- a/src/formatters/stylish.ts
+++ b/src/formatters/stylish.ts
@@ -27,8 +27,13 @@ import chalk from 'chalk';
 import stripAnsi from 'strip-ansi';
 import * as table from 'text-table';
 
-import { DiagnosticSeverity, Dictionary, IRange } from '@stoplight/types';
+import { DiagnosticSeverity, IRange } from '@stoplight/types';
 import { IRuleResult } from '../types';
+import { Formatter } from './types';
+import { getHighestSeverity } from './utils/getHighestSeverity';
+import { groupBySeverity } from './utils/groupBySeverity';
+import { groupBySource } from './utils/groupBySource';
+import { sortResults } from './utils/sortResults';
 
 // -----------------------------------------------------------------------------
 // Helpers
@@ -44,50 +49,74 @@ function pluralize(word: string, count: number): string {
   return count === 1 ? word : `${word}s`;
 }
 
+function formatRange(range?: IRange): string {
+  if (!range) return '';
+
+  return ` ${range.start.line + 1}:${range.start.character + 1}`;
+}
+
+const SEVERITY_COLORS = {
+  [DiagnosticSeverity.Error]: 'red',
+  [DiagnosticSeverity.Warning]: 'yellow',
+  [DiagnosticSeverity.Information]: 'blue',
+  [DiagnosticSeverity.Hint]: 'white',
+};
+
+function getColorForSeverity(severity: DiagnosticSeverity) {
+  return SEVERITY_COLORS[severity];
+}
+
+function getMessageType(severity: DiagnosticSeverity) {
+  const color = getColorForSeverity(severity);
+
+  switch (severity) {
+    case DiagnosticSeverity.Error:
+      return chalk[color]('error');
+    case DiagnosticSeverity.Warning:
+      return chalk[color]('warning');
+    case DiagnosticSeverity.Information:
+      return chalk[color]('information');
+    default:
+      return chalk[color]('hint');
+  }
+}
+
 // -----------------------------------------------------------------------------
 // Public Interface
 // -----------------------------------------------------------------------------
 
-export const stylish = (results: IRuleResult[]): string => {
+export const stylish: Formatter = results => {
   let output = '\n';
   let errorCount = 0;
   let warningCount = 0;
   let infoCount = 0;
-  let summaryColor = 'white';
+  let hintCount = 0;
+  const summaryColor = getColorForSeverity(getHighestSeverity(results));
 
   const groupedResults = groupBySource(results);
   Object.keys(groupedResults).map((path, index) => {
     const pathResults = groupedResults[path];
 
-    const errors = pathResults.filter((result: IRuleResult) => result.severity === DiagnosticSeverity.Error);
-    const warnings = pathResults.filter((result: IRuleResult) => result.severity === DiagnosticSeverity.Warning);
-    const infos = pathResults.filter((result: IRuleResult) => result.severity === DiagnosticSeverity.Information);
+    const {
+      [DiagnosticSeverity.Error]: errors,
+      [DiagnosticSeverity.Warning]: warnings,
+      [DiagnosticSeverity.Information]: infos,
+      [DiagnosticSeverity.Hint]: hints,
+    } = groupBySeverity(pathResults);
 
     errorCount += errors.length;
     warningCount += warnings.length;
     infoCount += infos.length;
+    hintCount += hints.length;
 
     output += `${chalk.underline(path)}\n`;
 
-    const pathTableData = sortResults(pathResults).map((result: IRuleResult) => {
-      let messageType;
-
-      if (result.severity === DiagnosticSeverity.Error) {
-        messageType = chalk.red('error');
-        summaryColor = 'red';
-      } else if (result.severity === DiagnosticSeverity.Warning) {
-        messageType = chalk.yellow('warning');
-
-        // we should always display red color for summary if there are any errors
-        if (summaryColor !== 'red') {
-          summaryColor = 'yellow';
-        }
-      } else {
-        messageType = chalk.yellow('white');
-      }
-
-      return [formatRange(result.range), messageType, result.code !== undefined ? result.code : '', result.message];
-    });
+    const pathTableData = sortResults(pathResults).map((result: IRuleResult) => [
+      formatRange(result.range),
+      getMessageType(result.severity),
+      result.code !== undefined ? result.code : '',
+      result.message,
+    ]);
 
     output += `${table(pathTableData, {
       align: ['c', 'r', 'l'],
@@ -102,7 +131,7 @@ export const stylish = (results: IRuleResult[]): string => {
       .join('\n')}\n\n`;
   });
 
-  const total = errorCount + warningCount + infoCount;
+  const total = errorCount + warningCount + infoCount + hintCount;
 
   if (total > 0) {
     output += chalk[summaryColor].bold(
@@ -119,6 +148,9 @@ export const stylish = (results: IRuleResult[]): string => {
         ', ',
         infoCount,
         pluralize(' info', infoCount),
+        ', ',
+        hintCount,
+        pluralize(' hint', hintCount),
         ')\n',
       ].join(''),
     );
@@ -126,28 +158,3 @@ export const stylish = (results: IRuleResult[]): string => {
 
   return total > 0 ? output : '';
 };
-
-const groupBySource = (results: IRuleResult[]): Dictionary<IRuleResult[]> => {
-  return results.reduce((grouped: Dictionary<IRuleResult[]>, result: IRuleResult) => {
-    (grouped[result.source!] = grouped[result.source!] || []).push(result);
-    return grouped;
-  }, {});
-};
-
-const formatRange = (range?: IRange): string => {
-  if (!range) return '';
-
-  return ` ${range.start.line + 1}:${range.start.character + 1}`;
-};
-
-const sortResults = (results: IRuleResult[]) => {
-  return [...results].sort((resultA, resultB) => {
-    const diff = resultA.range.start.line - resultB.range.start.line;
-
-    if (diff === 0) {
-      return resultA.range.start.character - resultB.range.start.character;
-    }
-
-    return diff;
-  });
-};
diff --git a/src/formatters/types.ts b/src/formatters/types.ts
new file mode 100644
index 00000000..3632c747
--- /dev/null
+++ b/src/formatters/types.ts
@@ -0,0 +1,3 @@
+import { IRuleResult } from '../types';
+
+export type Formatter = (results: IRuleResult[]) => string;
diff --git a/src/formatters/utils/getHighestSeverity.ts b/src/formatters/utils/getHighestSeverity.ts
new file mode 100644
index 00000000..46672dc8
--- /dev/null
+++ b/src/formatters/utils/getHighestSeverity.ts
@@ -0,0 +1,5 @@
+import { DiagnosticSeverity } from '@stoplight/types';
+import { IRuleResult } from '../../types';
+
+export const getHighestSeverity = (results: IRuleResult[]) =>
+  results.length === 0 ? DiagnosticSeverity.Hint : Math.min(...results.map(({ severity }) => severity));
diff --git a/src/formatters/utils/groupBySeverity.ts b/src/formatters/utils/groupBySeverity.ts
new file mode 100644
index 00000000..f1d5f85b
--- /dev/null
+++ b/src/formatters/utils/groupBySeverity.ts
@@ -0,0 +1,16 @@
+import { DiagnosticSeverity, Dictionary } from '@stoplight/types';
+import { IRuleResult } from '../../types';
+
+export const groupBySeverity = (results: IRuleResult[]) =>
+  results.reduce<Dictionary<IRuleResult[], DiagnosticSeverity>>(
+    (group, result: IRuleResult) => {
+      group[result.severity].push(result);
+      return group;
+    },
+    {
+      [DiagnosticSeverity.Error]: [],
+      [DiagnosticSeverity.Warning]: [],
+      [DiagnosticSeverity.Hint]: [],
+      [DiagnosticSeverity.Information]: [],
+    },
+  );
diff --git a/src/formatters/utils/groupBySource.ts b/src/formatters/utils/groupBySource.ts
new file mode 100644
index 00000000..e285dd47
--- /dev/null
+++ b/src/formatters/utils/groupBySource.ts
@@ -0,0 +1,9 @@
+import { Dictionary } from '@stoplight/types';
+import { IRuleResult } from '../../types';
+
+export const groupBySource = (results: IRuleResult[]): Dictionary<IRuleResult[]> => {
+  return results.reduce<Dictionary<IRuleResult[]>>((grouped, result) => {
+    (grouped[result.source!] = grouped[result.source!] || []).push(result);
+    return grouped;
+  }, {});
+};
diff --git a/src/formatters/utils/sortResults.ts b/src/formatters/utils/sortResults.ts
new file mode 100644
index 00000000..b096f768
--- /dev/null
+++ b/src/formatters/utils/sortResults.ts
@@ -0,0 +1,13 @@
+import { IRuleResult } from '../../types';
+
+export const sortResults = (results: IRuleResult[]) => {
+  return [...results].sort((resultA, resultB) => {
+    const diff = resultA.range.start.line - resultB.range.start.line;
+
+    if (diff === 0) {
+      return resultA.range.start.character - resultB.range.start.character;
+    }
+
+    return diff;
+  });
+};
diff --git a/src/formatters/utils/xmlEscape.ts b/src/formatters/utils/xmlEscape.ts
new file mode 100644
index 00000000..18238183
--- /dev/null
+++ b/src/formatters/utils/xmlEscape.ts
@@ -0,0 +1,49 @@
+/**
+ * Copyright JS Foundation and other contributors, https://js.foundation
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @fileoverview XML character escaper
+ * @author George Chung
+ */
+
+/**
+ * Returns the escaped value for a character
+ * @param {string} s string to examine
+ * @returns {string} severity level
+ * @private
+ */
+export const xmlEscape = (s: string) => {
+  return `${s}`.replace(/[<>&"'\x00-\x1F\x7F\u0080-\uFFFF]/gu, c => {
+    switch (c) {
+      case '<':
+        return '&lt;';
+      case '>':
+        return '&gt;';
+      case '&':
+        return '&amp;';
+      case '"':
+        return '&quot;';
+      case "'":
+        return '&apos;';
+      default:
+        return `&#${c.charCodeAt(0)};`;
+    }
+  });
+};
diff --git a/src/fs/reader.ts b/src/fs/reader.ts
index d63aeddc..dcc5a0a9 100644
--- a/src/fs/reader.ts
+++ b/src/fs/reader.ts
@@ -1,26 +1,26 @@
 import { isURL } from '@stoplight/path';
-import * as fs from 'fs';
-const fetch = require('node-fetch');
 import AbortController from 'abort-controller';
+import * as fs from 'fs';
+import request from '../request';
 
 export interface IReadOptions {
   encoding: string;
   timeout?: number;
 }
 
-export async function readFile(name: string, opts: IReadOptions) {
+export async function readFile(name: string, opts: IReadOptions): Promise<string> {
   if (isURL(name)) {
     let response;
-    let timeout: NodeJS.Timeout | null = null;
+    let timeout: NodeJS.Timeout | number | null = null;
     try {
       if (opts.timeout) {
         const controller = new AbortController();
         timeout = setTimeout(() => {
           controller.abort();
         }, opts.timeout);
-        response = await fetch(name, { signal: controller.signal });
+        response = await request(name, { signal: controller.signal });
       } else {
-        response = await fetch(name);
+        response = await request(name);
       }
 
       if (!response.ok) throw new Error(response.statusText);
diff --git a/src/functions/alphabetical.ts b/src/functions/alphabetical.ts
index 27c5e87e..5c797772 100644
--- a/src/functions/alphabetical.ts
+++ b/src/functions/alphabetical.ts
@@ -1,51 +1,66 @@
-import { isEqual } from 'lodash';
-
 import { IAlphaRuleOptions, IFunction, IFunctionResult } from '../types';
+import { isObject } from '../utils';
 
-export const alphabetical: IFunction<IAlphaRuleOptions> = (targetVal, opts) => {
-  const results: IFunctionResult[] = [];
+const compare = (a: unknown, b: unknown): number => {
+  if ((typeof a === 'number' || Number.isNaN(Number(a))) && (typeof b === 'number' || !Number.isNaN(Number(b)))) {
+    return Math.min(1, Math.max(-1, Number(a) - Number(b)));
+  }
 
-  if (!targetVal) {
-    return results;
+  if (typeof a !== 'string' || typeof b !== 'string') {
+    return 0;
   }
 
-  let targetArray: any[] = targetVal;
-  if (!Array.isArray(targetVal)) {
-    targetArray = Object.keys(targetVal);
+  return a.localeCompare(b);
+};
+
+const getUnsortedItems = <T>(arr: T[], compareFn: (a: T, B: T) => number): null | [number, number] => {
+  for (let i = 0; i < arr.length - 1; i += 1) {
+    if (compareFn(arr[i], arr[i + 1]) >= 1) {
+      return [i, i + 1];
+    }
   }
 
-  // don't mutate original array
-  const copiedArray = targetArray.slice();
+  return null;
+};
+
+export const alphabetical: IFunction<IAlphaRuleOptions> = (targetVal, opts, paths) => {
+  const results: IFunctionResult[] = [];
 
-  if (copiedArray.length < 2) {
+  if (!isObject(targetVal)) {
     return results;
   }
 
-  const { keyedBy } = opts;
+  const targetArray: any[] | string[] = Array.isArray(targetVal) ? targetVal : Object.keys(targetVal);
 
-  // If we aren't expecting an object keyed by a specific property, then treat the
-  // object as a simple array.
-  if (keyedBy) {
-    copiedArray.sort((a, b) => {
-      if (typeof a !== 'object') {
-        return 0;
-      }
-
-      if (a[keyedBy] < b[keyedBy]) {
-        return -1;
-      } else if (a[keyedBy] > b[keyedBy]) {
-        return 1;
-      }
-
-      return 0;
-    });
-  } else {
-    copiedArray.sort();
+  if (targetArray.length < 2) {
+    return results;
   }
 
-  if (!isEqual(targetArray, copiedArray)) {
+  const { keyedBy } = opts;
+
+  const unsortedItems = getUnsortedItems<unknown>(
+    targetArray,
+    keyedBy
+      ? (a, b) => {
+          if (!isObject(a) || !isObject(b)) return 0;
+
+          return compare(a[keyedBy], b[keyedBy]);
+        }
+      : // If we aren't expecting an object keyed by a specific property, then treat the
+        // object as a simple array.
+        compare,
+  );
+
+  if (unsortedItems != null) {
+    const path = paths.target || paths.given;
+
     results.push({
-      message: 'properties are not in alphabetical order',
+      ...(!keyedBy && { path: [...path, Array.isArray(targetVal) ? unsortedItems[0] : targetArray[unsortedItems[0]]] }),
+      message: keyedBy
+        ? 'properties are not in alphabetical order'
+        : `at least 2 properties are not in alphabetical order: "${
+            targetArray[unsortedItems[0]]
+          }" should be placed after "${targetArray[unsortedItems[1]]}"`,
     });
   }
 
diff --git a/src/functions/schema.ts b/src/functions/schema.ts
index 0b2173d3..3b2055a4 100644
--- a/src/functions/schema.ts
+++ b/src/functions/schema.ts
@@ -1,8 +1,13 @@
+import { decodePointerFragment } from '@stoplight/json';
+import { Optional } from '@stoplight/types';
 import * as AJV from 'ajv';
 import { ValidateFunction } from 'ajv';
 import * as jsonSpecv4 from 'ajv/lib/refs/json-schema-draft-04.json';
+import * as jsonSpecv6 from 'ajv/lib/refs/json-schema-draft-06.json';
+import * as jsonSpecv7 from 'ajv/lib/refs/json-schema-draft-07.json';
 import { IOutputError } from 'better-ajv-errors';
 import { JSONSchema4, JSONSchema6 } from 'json-schema';
+import { escapeRegExp } from 'lodash';
 import { IFunction, IFunctionResult, ISchemaOptions } from '../types';
 const oasFormatValidator = require('ajv-oai/lib/format-validator');
 const betterAjvErrors = require('better-ajv-errors/lib/modern');
@@ -31,6 +36,8 @@ const ajv = new AJV({
   logger,
 });
 ajv.addMetaSchema(jsonSpecv4);
+ajv.addMetaSchema(jsonSpecv6);
+ajv.addMetaSchema(jsonSpecv7);
 // @ts-ignore
 ajv._opts.defaultMeta = jsonSpecv4.id;
 // @ts-ignore
@@ -71,14 +78,18 @@ const validators = new class extends WeakMap<JSONSchema4 | JSONSchema6, Validate
   }
 }();
 
-const cleanAJVErrorMessage = (message: string) => message.trim().replace(/^[^:]*:\s*/, '');
+const cleanAJVErrorMessage = (message: string, path: Optional<string>, suggestion: Optional<string>) => {
+  const cleanMessage =
+    typeof path === 'string' ? message.trim().replace(new RegExp(`^${escapeRegExp(path)}:\\s*`), '') : message.trim();
+  return `${cleanMessage}${typeof suggestion === 'string' && suggestion.length > 0 ? `. ${suggestion}` : ''}`;
+};
 
 export const schema: IFunction<ISchemaOptions> = (targetVal, opts, paths) => {
   const results: IFunctionResult[] = [];
 
   const path = paths.target || paths.given;
 
-  if (!targetVal)
+  if (targetVal === void 0)
     return [
       {
         path,
@@ -93,16 +104,29 @@ export const schema: IFunction<ISchemaOptions> = (targetVal, opts, paths) => {
     // we used the compiled validation now, hence this lookup here (see the logic above for more info)
     const validator = validators.get(schemaObj);
     if (!validator(targetVal) && validator.errors) {
-      results.push(
-        ...(betterAjvErrors(schemaObj, targetVal, validator.errors, { format: 'js' }) as IAJVOutputError[]).map(
-          ({ error, path: errorPath }) => {
-            return {
-              message: cleanAJVErrorMessage(error),
+      try {
+        results.push(
+          ...(betterAjvErrors(schemaObj, targetVal, validator.errors, { format: 'js' }) as IAJVOutputError[]).map(
+            ({ suggestion, error, path: errorPath }) => ({
+              message: cleanAJVErrorMessage(error, errorPath, suggestion),
               path: [...path, ...(errorPath ? errorPath.replace(/^\//, '').split('/') : [])],
-            };
-          },
-        ),
-      );
+            }),
+          ),
+        );
+      } catch {
+        results.push(
+          ...validator.errors.map(({ message, dataPath }) => ({
+            message: message ? cleanAJVErrorMessage(message, dataPath, void 0) : '',
+            path: [
+              ...path,
+              ...dataPath
+                .split('/')
+                .slice(1)
+                .map(decodePointerFragment),
+            ],
+          })),
+        );
+      }
     }
   } catch (ex) {
     if (ex instanceof AJV.MissingRefError) {
diff --git a/src/index.ts b/src/index.ts
index 9e3cb6c4..6463d20d 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,1 +1,2 @@
 export * from './spectral';
+export * from './formats';
diff --git a/src/linter.ts b/src/linter.ts
index ff738866..de25f634 100644
--- a/src/linter.ts
+++ b/src/linter.ts
@@ -3,11 +3,12 @@ import { get, has } from 'lodash';
 
 const { JSONPath } = require('jsonpath-plus');
 
-import { decodePointerFragment } from '@stoplight/json';
+import { decodePointerFragment, pathToPointer } from '@stoplight/json';
 import { Resolved } from './resolved';
 import { message } from './rulesets/message';
 import { getDiagnosticSeverity } from './rulesets/severity';
 import { IFunction, IGivenNode, IRuleResult, IRunRule, IThen } from './types';
+import { isObject } from './utils';
 
 // TODO(SO-23): unit test but mock whatShouldBeLinted
 export const lintNode = (
@@ -96,10 +97,11 @@ export const lintNode = (
     results = results.concat(
       targetResults.map<IRuleResult>(result => {
         const escapedJsonPath = (result.path || targetPath).map(segment => decodePointerFragment(String(segment)));
-        const path = getRealJsonPath(
+        const path = getClosestJsonPath(
           rule.resolved === false ? resolved.unresolved : resolved.resolved,
           escapedJsonPath,
         );
+        // todo: https://github.com/stoplightio/spectral/issues/608
         const location = resolved.getLocationForJsonPath(path, true);
 
         return {
@@ -111,23 +113,22 @@ export const lintNode = (
               : message(rule.message, {
                   error: result.message,
                   property: path.length > 0 ? path[path.length - 1] : '',
+                  path: pathToPointer(path),
                   description: rule.description,
+                  get value() {
+                    // let's make it `value` lazy
+                    const value = resolved.getValueForJsonPath(path);
+                    if (isObject(value)) {
+                      return Array.isArray(value) ? 'Array[]' : 'Object{}';
+                    }
+
+                    return JSON.stringify(value);
+                  },
                 }),
           path,
           severity: getDiagnosticSeverity(rule.severity),
           source: location.uri,
-          ...(location || {
-            range: {
-              start: {
-                character: 0,
-                line: 0,
-              },
-              end: {
-                character: 0,
-                line: 0,
-              },
-            },
-          }),
+          range: location.range,
         };
       }),
     );
@@ -210,7 +211,8 @@ function keyAndOptionalPattern(key: string | number, pattern: string, value: any
   };
 }
 
-function getRealJsonPath(data: unknown, path: JsonPath) {
+// todo: revisit -> https://github.com/stoplightio/spectral/issues/608
+function getClosestJsonPath(data: unknown, path: JsonPath) {
   if (data === null || typeof data !== 'object') return [];
 
   while (path.length > 0 && !has(data, path)) {
diff --git a/src/request.ts b/src/request.ts
new file mode 100644
index 00000000..6188e03c
--- /dev/null
+++ b/src/request.ts
@@ -0,0 +1,7 @@
+import fetch, { RequestInit } from 'node-fetch';
+
+export const DEFAULT_REQUEST_OPTIONS: RequestInit = {};
+
+export default async (uri: string, opts: RequestInit = {}) => {
+  return fetch(uri, { ...opts, ...DEFAULT_REQUEST_OPTIONS });
+};
diff --git a/src/resolved.ts b/src/resolved.ts
index 42058a7e..37027bd4 100644
--- a/src/resolved.ts
+++ b/src/resolved.ts
@@ -1,22 +1,58 @@
-import { IResolveError, IResolveResult } from '@stoplight/json-ref-resolver/types';
-import { Dictionary, ILocation, JsonPath, Segment } from '@stoplight/types';
+import { decodePointerFragment, pointerToPath } from '@stoplight/json';
+import { IResolveError } from '@stoplight/json-ref-resolver/types';
+import { Dictionary, ILocation, IRange, JsonPath, Segment } from '@stoplight/types';
 import { get } from 'lodash';
-import { IParseMap, REF_METADATA } from './spectral';
+import { IParseMap, REF_METADATA, ResolveResult } from './spectral';
 import { IParsedResult } from './types';
+import { hasRef, isObject } from './utils';
+
+const getDefaultRange = (): IRange => ({
+  start: {
+    line: 0,
+    character: 0,
+  },
+  end: {
+    line: 0,
+    character: 0,
+  },
+});
 
 export class Resolved {
-  public refMap: Dictionary<string>;
-  public resolved: unknown;
-  public unresolved: unknown;
-  public errors: IResolveError[];
-  public format?: string | null;
+  public readonly refMap: Dictionary<string>;
+  public readonly resolved: unknown;
+  public readonly unresolved: unknown;
+  public readonly errors: IResolveError[];
+  public formats?: string[] | null;
+
+  constructor(public spec: IParsedResult, resolveResult: ResolveResult, public parsedMap: IParseMap) {
+    this.unresolved = spec.parsed.data;
+    this.formats = spec.formats;
 
-  constructor(public spec: IParsedResult, resolveResult: IResolveResult, public parsedMap: IParseMap) {
     this.refMap = resolveResult.refMap;
     this.resolved = resolveResult.result;
-    this.unresolved = spec.parsed.data;
     this.errors = resolveResult.errors;
-    this.format = spec.format;
+  }
+
+  public doesBelongToDoc(path: JsonPath): boolean {
+    if (path.length === 0) {
+      // todo: each rule and their function should be context-aware, meaning they should aware of the fact they operate on resolved content
+      // let's assume the error was reported correctly by any custom rule /shrug
+      return true;
+    }
+
+    let piece = this.unresolved;
+
+    for (let i = 0; i < path.length; i++) {
+      if (!isObject(piece)) return false;
+
+      if (path[i] in piece) {
+        piece = piece[path[i]];
+      } else if (hasRef(piece)) {
+        return this.doesBelongToDoc([...pointerToPath(piece.$ref), ...path.slice(i)]);
+      }
+    }
+
+    return true;
   }
 
   public getParsedForJsonPath(path: JsonPath) {
@@ -36,11 +72,15 @@ export class Resolved {
 
     if (target && target[REF_METADATA]) {
       return {
-        path: [...get(target, [REF_METADATA, 'root'], []), ...newPath],
+        path: [...get(target, [REF_METADATA, 'root'], []).map(decodePointerFragment), ...newPath],
         doc: get(this.parsedMap.parsed, get(target, [REF_METADATA, 'ref']), this.spec),
       };
     }
 
+    if (!this.doesBelongToDoc(path)) {
+      return null;
+    }
+
     return {
       path,
       doc: this.spec,
@@ -49,23 +89,22 @@ export class Resolved {
 
   public getLocationForJsonPath(path: JsonPath, closest?: boolean): ILocation {
     const parsedResult = this.getParsedForJsonPath(path);
+    if (parsedResult === null) {
+      return {
+        range: getDefaultRange(),
+      };
+    }
+
     const location = parsedResult.doc.getLocationForJsonPath(parsedResult.doc.parsed, parsedResult.path, closest);
 
     return {
       ...(parsedResult.doc.source && { uri: parsedResult.doc.source }),
-      range:
-        location !== undefined
-          ? location.range
-          : {
-              start: {
-                line: 0,
-                character: 0,
-              },
-              end: {
-                line: 0,
-                character: 0,
-              },
-            },
+      range: location !== void 0 ? location.range : getDefaultRange(),
     };
   }
+
+  public getValueForJsonPath(path: JsonPath): unknown {
+    const parsedResult = this.getParsedForJsonPath(path);
+    return parsedResult === null ? void 0 : get(parsedResult.doc.parsed.data, parsedResult.path);
+  }
 }
diff --git a/src/resolvers/http.ts b/src/resolvers/http.ts
index f2dc4f98..eacdc627 100644
--- a/src/resolvers/http.ts
+++ b/src/resolvers/http.ts
@@ -1,10 +1,10 @@
 import { Resolver } from '@stoplight/json-ref-resolver';
 
-const fetch = require('node-fetch');
+import request from '../request';
 
 export const httpReader = {
   async resolve(ref: any) {
-    return (await fetch(String(ref))).text();
+    return (await request(String(ref))).text();
   },
 };
 
diff --git a/src/rulesets/evaluators.ts b/src/rulesets/evaluators.ts
index 275561b8..5b395b5d 100644
--- a/src/rulesets/evaluators.ts
+++ b/src/rulesets/evaluators.ts
@@ -1,6 +1,7 @@
 import { Optional } from '@stoplight/types';
 import { JSONSchema7 } from 'json-schema';
 import { IFunction } from '../types';
+import { isObject } from '../utils/isObject';
 import { decorateIFunctionWithSchemaValidation } from './validation';
 
 export type CJSExport = Partial<{ exports: object | ESCJSCompatibleExport }>;
@@ -28,8 +29,6 @@ const createDefine = (exports: CJSExport) => {
   return define;
 };
 
-const isObject = (thing: unknown): thing is object => thing !== null && typeof thing === 'object';
-
 const isESCJSCompatibleExport = (obj: unknown): obj is ESCJSCompatibleExport => isObject(obj) && 'default' in obj;
 
 // note: this code is hand-crafted and cover cases we want to support
diff --git a/src/rulesets/mergers/functions.ts b/src/rulesets/mergers/functions.ts
index 466cced1..4637fa09 100644
--- a/src/rulesets/mergers/functions.ts
+++ b/src/rulesets/mergers/functions.ts
@@ -25,7 +25,7 @@ export function mergeFunctions(
     if (typeof rule === 'object') {
       const ruleThen = Array.isArray(rule.then) ? rule.then : [rule.then];
       for (const then of ruleThen) {
-        // note: if function relies on global function, it will take the most recent defined one
+        // if function relies on global function, it will take the most recently defined one
         if (then.function in map) {
           then.function = map[then.function];
         }
diff --git a/src/rulesets/message.ts b/src/rulesets/message.ts
index e3c4f5a4..c0f2967c 100644
--- a/src/rulesets/message.ts
+++ b/src/rulesets/message.ts
@@ -1,7 +1,11 @@
+import { Segment } from '@stoplight/types';
+
 export interface IMessageVars {
-  property: string | number;
+  property: Segment;
   error: string;
   description?: string;
+  value: unknown;
+  path: string;
 }
 
 export type MessageInterpolator = (str: string, values: IMessageVars) => string;
diff --git a/src/rulesets/oas/functions/oasOp2xxResponse.ts b/src/rulesets/oas/functions/oasOp2xxResponse.ts
index bfe4fd48..5d8ae7ca 100644
--- a/src/rulesets/oas/functions/oasOp2xxResponse.ts
+++ b/src/rulesets/oas/functions/oasOp2xxResponse.ts
@@ -1,16 +1,17 @@
 import { IFunction, IFunctionResult, Rule } from '../../../types';
 
 export const oasOp2xxResponse: IFunction<Rule> = targetVal => {
-  const results: IFunctionResult[] = [];
+  if (!targetVal) {
+    return;
+  }
 
+  const results: IFunctionResult[] = [];
   const responses = Object.keys(targetVal);
-
   if (responses.filter(response => Number(response) >= 200 && Number(response) < 300).length === 0) {
     results.push({
       message: 'operations must define at least one 2xx response',
     });
   }
-
   return results;
 };
 
diff --git a/src/rulesets/oas/functions/oasPathParam.ts b/src/rulesets/oas/functions/oasPathParam.ts
index 134a9254..ac908550 100644
--- a/src/rulesets/oas/functions/oasPathParam.ts
+++ b/src/rulesets/oas/functions/oasPathParam.ts
@@ -30,12 +30,10 @@ export const oasPathParam: IFunction<Rule> = (targetVal, _options, paths, vals)
     const normalized = path.replace(pathRegex, '%'); // '%' is used here since its invalid in paths
     if (uniquePaths[normalized]) {
       results.push(
-        generateResult(
-          `The paths "**${uniquePaths[normalized]}**" and "**${path}**" are equivalent.
-
-To fix, remove one of the paths or merge them together.`,
-          [...paths.given, 'paths'],
-        ),
+        generateResult(`The paths \`${uniquePaths[normalized]}\` and \`${path}\` are equivalent.`, [
+          ...paths.given,
+          'paths',
+        ]),
       );
     } else {
       uniquePaths[normalized] = path;
@@ -51,11 +49,7 @@ To fix, remove one of the paths or merge them together.`,
         if (pathElements[p]) {
           results.push(
             generateResult(
-              `The path "**${path}**" uses the parameter "**{${p}}**" multiple times.
-
-Path parameters must be unique.
-
-To fix, update the path so that all parameter names are unique.`,
+              `The path \`${path}\` uses the parameter \`{${p}}\` multiple times. Path parameters must be unique.`,
               [...paths.given, 'paths', path],
             ),
           );
@@ -134,9 +128,7 @@ To fix, update the path so that all parameter names are unique.`,
       if (!topParams[p] && !operationParams[p]) {
         results.push(
           generateResult(
-            `The path "**${path}**" uses a parameter "**{${p}}**" that does not have a corresponding definition.
-
-To fix, add a path parameter with the name "**${p}**".`,
+            `The path \`${path}\` uses a parameter \`{${p}}\` that does not have a corresponding definition.`,
             [...paths.given, 'paths', path],
           ),
         );
@@ -152,14 +144,7 @@ To fix, add a path parameter with the name "**${p}**".`,
         if (!pathElements[p]) {
           const resPath = paramObj[p];
           results.push(
-            generateResult(
-              `Parameter "**${p}**" is not used in the path "**${path}**".
-
-Unused parameters are not allowed.
-
-To fix, remove this parameter.`,
-              [...paths.given, ...resPath],
-            ),
+            generateResult(`Parameter \`${p}\` is not used in the path \`${path}\`.`, [...paths.given, ...resPath]),
           );
         }
       }
@@ -176,16 +161,10 @@ function generateResult(message: string, path: Array<string | number>): IFunctio
   };
 }
 
-const requiredMessage = (
-  name: string,
-) => `Path parameter "**${name}**" must have a \`required\` that is set to \`true\`.
-
-To fix, mark this parameter as required.`;
-
-const uniqueDefinitionMessage = (name: string) => `Path parameter '**${name}**' is defined multiple times.
-
-Path parameters must be unique.
+const requiredMessage = (name: string) =>
+  `Path parameter \`${name}\` must have a \`required\` property that is set to \`true\`.`;
 
-To fix, remove the duplicate parameters.`;
+const uniqueDefinitionMessage = (name: string) =>
+  `Path parameter \`${name}\` is defined multiple times. Path parameters must be unique.`;
 
 export default oasPathParam;
diff --git a/src/rulesets/oas/functions/refSiblings.ts b/src/rulesets/oas/functions/refSiblings.ts
index 7527629f..1b7e991a 100644
--- a/src/rulesets/oas/functions/refSiblings.ts
+++ b/src/rulesets/oas/functions/refSiblings.ts
@@ -1,14 +1,14 @@
 import { JsonPath } from '@stoplight/types';
 import { IFunction, IFunctionResult } from '../../../types';
+import { hasRef } from '../../../utils/hasRef';
 
 // function is needed because `$..$ref` or `$..[?(@.$ref)]` are not parsed correctly
 // and therefore lead to infinite recursion due to the dollar sign ('$' in '$ref')
 function* siblingIterator(obj: object, path: JsonPath): IterableIterator<JsonPath> {
-  const hasRef = '$ref' in obj;
   for (const key in obj) {
     if (!Object.hasOwnProperty.call(obj, key)) continue;
     const scopedPath = [...path, key];
-    if (hasRef && key !== '$ref') {
+    if (hasRef(obj) && key !== '$ref') {
       yield scopedPath;
     }
 
diff --git a/src/rulesets/oas/index.json b/src/rulesets/oas/index.json
index 13fff27f..4a5c40e5 100644
--- a/src/rulesets/oas/index.json
+++ b/src/rulesets/oas/index.json
@@ -338,23 +338,6 @@
         "operation"
       ]
     },
-    "parameter-description": {
-      "description": "Parameter objects should have a `description`.",
-      "recommended": false,
-      "type": "style",
-      "given": "$..parameters[*]",
-      "when": {
-        "field": "@key",
-        "pattern": "^(?!.*(\\$ref))"
-      },
-      "then": {
-        "field": "description",
-        "function": "truthy"
-      },
-      "tags": [
-        "parameters"
-      ]
-    },
     "path-declarations-must-exist": {
       "description": "Path parameter declarations cannot be empty, ex.`/given/{}` is invalid.",
       "recommended": true,
diff --git a/src/rulesets/oas2/index.json b/src/rulesets/oas2/index.json
index 7504368b..3c5d2f11 100644
--- a/src/rulesets/oas2/index.json
+++ b/src/rulesets/oas2/index.json
@@ -73,6 +73,19 @@
         "function": "truthy"
       }
     },
+    "oas2-parameter-description": {
+      "description": "Parameter objects should have a `description`.",
+      "recommended": false,
+      "type": "style",
+      "given": "$..[?(@parentProperty === 'parameters' && @.in)]",
+      "then": {
+        "field": "description",
+        "function": "truthy"
+      },
+      "tags": [
+        "parameters"
+      ]
+    },
     "oas2-operation-security-defined": {
       "description": "Operation `security` values must match a scheme defined in the `securityDefinitions` object.",
       "recommended": true,
diff --git a/src/rulesets/oas3/index.json b/src/rulesets/oas3/index.json
index f9929a87..fa1e6f85 100644
--- a/src/rulesets/oas3/index.json
+++ b/src/rulesets/oas3/index.json
@@ -52,6 +52,19 @@
         "operation"
       ]
     },
+    "oas3-parameter-description": {
+      "description": "Parameter objects should have a `description`.",
+      "recommended": false,
+      "type": "style",
+      "given": "$..[?(@parentProperty !== 'links' && @.parameters)]['parameters'].[?(@.in)]",
+      "then": {
+        "field": "description",
+        "function": "truthy"
+      },
+      "tags": [
+        "parameters"
+      ]
+    },
     "server-not-example.com": {
       "description": "Server URL should not point at `example.com`.",
       "recommended": false,
diff --git a/src/runner.ts b/src/runner.ts
index 941405f8..f3d64e02 100644
--- a/src/runner.ts
+++ b/src/runner.ts
@@ -4,7 +4,10 @@ const { JSONPath } = require('jsonpath-plus');
 
 import { lintNode } from './linter';
 import { getDiagnosticSeverity } from './rulesets/severity';
-import { FunctionCollection, IGivenNode, IRuleResult, IRunRule, RunRuleCollection } from './types';
+import { FunctionCollection, IGivenNode, IRule, IRuleResult, IRunRule, RunRuleCollection } from './types';
+import { hasIntersectingElement } from './utils/';
+
+export const isRuleEnabled = (rule: IRule) => rule.severity !== void 0 && getDiagnosticSeverity(rule.severity) !== -1;
 
 export const runRules = (
   resolved: Resolved,
@@ -21,11 +24,12 @@ export const runRules = (
 
     if (
       rule.formats !== void 0 &&
-      (resolved.format === null || (resolved.format !== void 0 && !rule.formats.includes(resolved.format)))
+      (resolved.formats === null ||
+        (resolved.formats !== void 0 && !hasIntersectingElement(rule.formats, resolved.formats)))
     )
       continue;
 
-    if (rule.severity !== undefined && getDiagnosticSeverity(rule.severity) === -1) {
+    if (!isRuleEnabled(rule)) {
       continue;
     }
 
diff --git a/src/spectral.ts b/src/spectral.ts
index 80b56daf..3c4fc545 100644
--- a/src/spectral.ts
+++ b/src/spectral.ts
@@ -4,8 +4,8 @@ import {
   parseWithPointers as parseJSONWithPointers,
   safeStringify,
 } from '@stoplight/json';
-import { Cache, Resolver } from '@stoplight/json-ref-resolver';
-import { IUriParser } from '@stoplight/json-ref-resolver/types';
+import { Resolver } from '@stoplight/json-ref-resolver';
+import { ICache, IUriParser } from '@stoplight/json-ref-resolver/types';
 import { extname } from '@stoplight/path';
 import { Dictionary } from '@stoplight/types';
 import {
@@ -30,6 +30,7 @@ import {
   FunctionCollection,
   IConstructorOpts,
   IParsedResult,
+  IResolver,
   IRuleResult,
   IRunOpts,
   ISpectralFullResult,
@@ -43,8 +44,9 @@ import { IRuleset } from './types/ruleset';
 export * from './types';
 
 export class Spectral {
-  private _resolver: Resolver;
-  private _uriCache: Cache;
+  private readonly _resolver: IResolver;
+  private readonly _parsedMap: IParseMap;
+  private static readonly _parsedCache = new WeakMap<ICache | IResolver, IParseMap>();
   public functions: FunctionCollection = { ...defaultFunctions };
   public rules: RunRuleCollection = {};
 
@@ -53,7 +55,20 @@ export class Spectral {
   constructor(opts?: IConstructorOpts) {
     this._resolver = opts && opts.resolver ? opts.resolver : new Resolver();
     this.formats = {};
-    this._uriCache = new Cache();
+
+    const cacheKey = this._resolver instanceof Resolver ? this._resolver.uriCache : this._resolver;
+    const _parsedMap = Spectral._parsedCache.get(cacheKey);
+    if (_parsedMap) {
+      this._parsedMap = _parsedMap;
+    } else {
+      this._parsedMap = {
+        refs: {},
+        parsed: {},
+        pointers: {},
+      };
+
+      Spectral._parsedCache.set(cacheKey, this._parsedMap);
+    }
   }
 
   public async runWithResolved(
@@ -83,16 +98,15 @@ export class Spectral {
     const resolved = new Resolved(
       parsedResult,
       await this._resolver.resolve(parsedResult.parsed.data, {
-        uriCache: this._uriCache,
         baseUri: documentUri,
         parseResolveResult: this._parseResolveResult(refDiagnostics),
       }),
       this._parsedMap,
     );
 
-    if (resolved.format === void 0) {
-      const foundFormat = Object.keys(this.formats).find(format => this.formats[format](resolved.resolved));
-      resolved.format = foundFormat === void 0 ? null : foundFormat;
+    if (resolved.formats === void 0) {
+      const foundFormats = Object.keys(this.formats).filter(format => this.formats[format](resolved.resolved));
+      resolved.formats = foundFormats.length === 0 ? null : foundFormats;
     }
 
     const validationResults = [
@@ -202,12 +216,6 @@ export class Spectral {
     this.formats[format] = fn;
   }
 
-  private _parsedMap: IParseMap = {
-    refs: {},
-    parsed: {},
-    pointers: {},
-  };
-
   private _processExternalRef(parsedResult: IParsedResult, opts: IUriParser) {
     const ref = opts.targetAuthority.toString();
     this._parsedMap.parsed[ref] = parsedResult;
diff --git a/src/types/config.ts b/src/types/config.ts
index 8f9a02a7..c6c524f1 100644
--- a/src/types/config.ts
+++ b/src/types/config.ts
@@ -1,6 +1,11 @@
+import { HumanReadableDiagnosticSeverity } from './rule';
+
+export type FailSeverity = HumanReadableDiagnosticSeverity;
+
 export enum OutputFormat {
   JSON = 'json',
   STYLISH = 'stylish',
+  JUNIT = 'junit',
 }
 
 export interface ILintConfig {
diff --git a/src/types/spectral.ts b/src/types/spectral.ts
index c85ac6fe..4cbcc4e3 100644
--- a/src/types/spectral.ts
+++ b/src/types/spectral.ts
@@ -1,4 +1,4 @@
-import { Resolver } from '@stoplight/json-ref-resolver';
+import { IResolveOpts, IResolveResult } from '@stoplight/json-ref-resolver/types';
 import {
   DiagnosticSeverity,
   Dictionary,
@@ -29,7 +29,7 @@ export type SpectralDiagnosticSeverity = DiagnosticSeverity | -1;
 export type RuleDeclarationCollection = Dictionary<boolean, string>;
 
 export interface IConstructorOpts {
-  resolver?: Resolver;
+  resolver?: IResolver;
 }
 
 export interface IRunOpts {
@@ -56,7 +56,13 @@ export interface IParsedResult<R extends IParserResult = IParserResult<unknown,
   parsed: IParserResult;
   getLocationForJsonPath: GetLocationForJsonPath<R>;
   source?: string;
-  format?: string;
+  formats?: string[];
+}
+
+export type ResolveResult = Omit<IResolveResult, 'runner'>;
+
+export interface IResolver {
+  resolve(source: unknown, opts?: IResolveOpts): Promise<ResolveResult>;
 }
 
 export type FormatLookup = (document: unknown) => boolean;
diff --git a/src/utils/hasIntersectingElement.ts b/src/utils/hasIntersectingElement.ts
new file mode 100644
index 00000000..5121a9b9
--- /dev/null
+++ b/src/utils/hasIntersectingElement.ts
@@ -0,0 +1,1 @@
+export const hasIntersectingElement = (left: unknown[], right: unknown[]) => left.some(item => right.includes(item));
diff --git a/src/utils/hasRef.ts b/src/utils/hasRef.ts
new file mode 100644
index 00000000..3d42e5f2
--- /dev/null
+++ b/src/utils/hasRef.ts
@@ -0,0 +1,2 @@
+export const hasRef = (obj: object): obj is object & { $ref: string } =>
+  '$ref' in obj && typeof (obj as Partial<{ $ref: unknown }>).$ref === 'string';
diff --git a/src/utils/index.ts b/src/utils/index.ts
new file mode 100644
index 00000000..85f1d7b0
--- /dev/null
+++ b/src/utils/index.ts
@@ -0,0 +1,3 @@
+export * from './hasRef';
+export * from './hasIntersectingElement';
+export * from './isObject';
diff --git a/src/utils/isObject.ts b/src/utils/isObject.ts
new file mode 100644
index 00000000..d76de650
--- /dev/null
+++ b/src/utils/isObject.ts
@@ -0,0 +1,1 @@
+export const isObject = (thing: unknown): thing is object => thing !== null && typeof thing === 'object';
diff --git a/yarn.lock b/yarn.lock
index d138eb49..815acf77 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -103,10 +103,10 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.0.0"
 
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.4.5":
-  version "7.5.5"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
-  integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
+"@babel/runtime@^7.0.0":
+  version "7.6.0"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.0.tgz#4fc1d642a9fd0299754e8b5de62c631cf5568205"
+  integrity sha512-89eSBLJsxNxOERC0Op4vd+0Bqm6wRMqMbFtV3i0/fbaWw/mJ8Q3eBvgX0G4SyrOOLCtbu98HspF8o09MRT+KzQ==
   dependencies:
     regenerator-runtime "^0.13.2"
 
@@ -159,9 +159,9 @@
     minimist "^1.2.0"
 
 "@commitlint/config-conventional@^8.1.0":
-  version "8.1.0"
-  resolved "https://registry.yarnpkg.com/@commitlint/config-conventional/-/config-conventional-8.1.0.tgz#ba61fbf0ad4df52da2b5ee3034470371a2cbf039"
-  integrity sha512-/JY+FNBnrT91qzDVIoV1Buiigvj7Le7ezFw+oRqu0nYREX03k7xnaG/7t7rUSvm7hM6dnLSOlaUsevjgMI9AEw==
+  version "8.2.0"
+  resolved "https://registry.yarnpkg.com/@commitlint/config-conventional/-/config-conventional-8.2.0.tgz#886a5538e3708e017ec2871e0cbce00f635d3102"
+  integrity sha512-HuwlHQ3DyVhpK9GHgTMhJXD8Zp8PGIQVpQGYh/iTrEU6TVxdRC61BxIDZvfWatCaiG617Z/U8maRAFrqFM4TqA==
 
 "@jest/console@^24.7.1", "@jest/console@^24.9.0":
   version "24.9.0"
@@ -319,30 +319,30 @@
     call-me-maybe "^1.0.1"
     glob-to-regexp "^0.3.0"
 
-"@nodelib/fs.scandir@2.1.2":
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.2.tgz#1f981cd5b83e85cfdeb386fc693d4baab392fa54"
-  integrity sha512-wrIBsjA5pl13f0RN4Zx4FNWmU71lv03meGKnqRUoCyan17s4V3WL92f3w3AIuWbNnpcrQyFBU5qMavJoB8d27w==
+"@nodelib/fs.scandir@2.1.3":
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b"
+  integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==
   dependencies:
-    "@nodelib/fs.stat" "2.0.2"
+    "@nodelib/fs.stat" "2.0.3"
     run-parallel "^1.1.9"
 
-"@nodelib/fs.stat@2.0.2", "@nodelib/fs.stat@^2.0.1":
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.2.tgz#2762aea8fe78ea256860182dcb52d61ee4b8fda6"
-  integrity sha512-z8+wGWV2dgUhLqrtRYa03yDx4HWMvXKi1z8g3m2JyxAx8F7xk74asqPk5LAETjqDSGLFML/6CDl0+yFunSYicw==
+"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2":
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3"
+  integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==
 
 "@nodelib/fs.stat@^1.1.2":
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
   integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
 
-"@nodelib/fs.walk@^1.2.1":
-  version "1.2.3"
-  resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.3.tgz#a555dc256acaf00c62b0db29529028dd4d4cb141"
-  integrity sha512-l6t8xEhfK9Sa4YO5mIRdau7XSOADfmh3jCr0evNHdY+HNkW6xuQhgMH7D73VV6WpZOagrW0UludvMTiifiwTfA==
+"@nodelib/fs.walk@^1.2.3":
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976"
+  integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==
   dependencies:
-    "@nodelib/fs.scandir" "2.1.2"
+    "@nodelib/fs.scandir" "2.1.3"
     fastq "^1.6.0"
 
 "@samverschueren/stream-to-observable@^0.3.0":
@@ -366,20 +366,10 @@
     urijs "~1.19.1"
     vscode-uri "^2.0.3"
 
-"@stoplight/json@^3.1.0":
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/@stoplight/json/-/json-3.1.0.tgz#a03bd8d065a62eb00cd42cf003df431f23d7a5dd"
-  integrity sha512-8Fev+RW+UJNYtcpDgvQPH6MuHEf5cr95KnAyMBNNXlVIeRAIJqYK3r5FWZv4/KFH08ozQy4QpMM9MdsR8J3cZQ==
-  dependencies:
-    "@stoplight/types" "^11.0.0"
-    jsonc-parser "~2.1.1"
-    lodash "^4.17.15"
-    safe-stable-stringify "^1.1"
-
-"@stoplight/json@^3.1.1":
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/@stoplight/json/-/json-3.1.1.tgz#531d966a62247bf2ab1e8a06a685fa62e36cf890"
-  integrity sha512-DK2ufmnPvfW9Mv0q4DYtldjicTTcBY+j4kiEUOEmc9hL+mzMDV08+5ix4tmPtheUDZH6OtEqEmGcf8ZKCHFbpg==
+"@stoplight/json@^3.1.0", "@stoplight/json@^3.1.1":
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/@stoplight/json/-/json-3.1.2.tgz#ae89d1974f0b6f8bc5778f792aeed4664c95ec62"
+  integrity sha512-7zt0zCN94m90X1sL7FrD7jXhPtkIup07MyG/00sELVrCvzb2wgNaiE+x2N7WSbk3Z0wS08e8B6kow81GQY+Isw==
   dependencies:
     "@stoplight/types" "^11.0.0"
     jsonc-parser "~2.1.1"
@@ -387,26 +377,30 @@
     safe-stable-stringify "^1.1"
 
 "@stoplight/path@^1.2.0":
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/@stoplight/path/-/path-1.2.0.tgz#0d0b828e10c2a492ab12c5fcffda2a4b4a4785b0"
-  integrity sha512-0GE2585RbYhR7HXTRK+AB/BBwOrqMAdAdBTzuJ5itFQwx3b/Qo0qi0y0Qis5vX6pswVm/6vlx5xBfSBYysI9BQ==
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/@stoplight/path/-/path-1.2.1.tgz#f39f408377a0d1a9c33a04ce08eb3a1691c457d6"
+  integrity sha512-W/wBpTw6ThKZGPcb1ztKz6ULnMVnLE/ygzqIjnqnmKcpNrKkSuBLtL7+oQYZSkteZfJAryloAt3plU3sTiekcQ==
 
-"@stoplight/types@^11.0.0":
-  version "11.1.0"
-  resolved "https://registry.yarnpkg.com/@stoplight/types/-/types-11.1.0.tgz#b29ecd8c8ad27bb564c7820c99f93c27f1d11bf1"
-  integrity sha512-z4VqnYIDTcj9R0f8AIDitBWkiAKKJ4W/Z5GJz/3fnBZBa/TD9Oak0xHbicXFXUvqTVCv/7D32PjPYiUX6W/EfQ==
+"@stoplight/types@^11.0.0", "@stoplight/types@^11.1.1":
+  version "11.1.1"
+  resolved "https://registry.yarnpkg.com/@stoplight/types/-/types-11.1.1.tgz#a92d1833adb580a72439f42ba73de8f560fd68b4"
+  integrity sha512-IU8U9y/uO548z15DX/Jl053u9VQG8gCwNtypuD4RtskUA7pvHZl4+zzGK3klgIcO6Ql3Jk4/fcrFaN9vjmdEWg==
   dependencies:
     "@types/json-schema" "^7.0.3"
 
+"@stoplight/yaml-ast-parser@0.0.44":
+  version "0.0.44"
+  resolved "https://registry.yarnpkg.com/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.44.tgz#ed3c962564283e9983f7895a6effc3994286df5e"
+  integrity sha512-PdY8p2Ufgtorf4d2DbKMfknILMa8KwuyyMMR/2lgK1mLaU8F5PKWYc+h9hIzC+ar0bh7m9h2rINo32m7ADfVyA==
+
 "@stoplight/yaml@^3.1.0":
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/@stoplight/yaml/-/yaml-3.1.0.tgz#1026dd99512b987f6686e9daa52f67bd36ab4285"
-  integrity sha512-Db+f9AJO95S+bWL4ODdItLNXaBE/ey5nYAB9K/i9IgBZgJ+lyQ1+L0HSduX627uzwZCqgMo0tySWrgVTi2scBQ==
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/@stoplight/yaml/-/yaml-3.3.2.tgz#001049ed4a8733fca43cc60efe6bcd046abef210"
+  integrity sha512-KfrEsl3bA8mtoIklVvS4Hg8OrOYYtqi+K0IsQ7lJbZLVaUA4wMwDGwz85a6YWeo1OuVe8tumM6OynrFIjFutNA==
   dependencies:
-    "@stoplight/types" "^11.0.0"
-    "@types/js-yaml" "3.12.1"
+    "@stoplight/types" "^11.1.1"
+    "@stoplight/yaml-ast-parser" "0.0.44"
     lodash "^4.17.15"
-    yaml-ast-parser "~0.0.43"
 
 "@types/babel__core@^7.1.0":
   version "7.1.2"
@@ -498,9 +492,9 @@
   integrity sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==
 
 "@types/jest-when@^2.4.0":
-  version "2.4.1"
-  resolved "https://registry.yarnpkg.com/@types/jest-when/-/jest-when-2.4.1.tgz#a382017a8410ad73c4274c27edbc890de97346e7"
-  integrity sha512-6t3Wu+sPv6GqKSrTfIh3csy0syDrwXni85dowkBWbvl6oBXoEpSX389yELKFnPN9MSTPLGBfXw1/XytBh3Yxlg==
+  version "2.7.0"
+  resolved "https://registry.yarnpkg.com/@types/jest-when/-/jest-when-2.7.0.tgz#538d5a3d069aedb496ce42aed76404fc8673bf27"
+  integrity sha512-oahwfICJSaEmmtyN7JeOg6P7ey+GKigzk26zdKLxYLewmr2F3WeDPo/U+kVpIxPuZG3EiVFdRy97q9N9DoEkSg==
   dependencies:
     "@types/jest" "*"
 
@@ -511,11 +505,6 @@
   dependencies:
     "@types/jest-diff" "*"
 
-"@types/js-yaml@3.12.1":
-  version "3.12.1"
-  resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.1.tgz#5c6f4a1eabca84792fbd916f0cb40847f123c656"
-  integrity sha512-SGGAhXLHDx+PK4YLNcNGa6goPf9XRWQNAUUbffkwVGGXIxmDKWyGGL4inzq2sPmExu431Ekb9aEMn9BkPqEYFA==
-
 "@types/json-schema@^7.0.3":
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636"
@@ -527,9 +516,9 @@
   integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
 
 "@types/lodash@^4.14.136":
-  version "4.14.136"
-  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.136.tgz#413e85089046b865d960c9ff1d400e04c31ab60f"
-  integrity sha512-0GJhzBdvsW2RUccNHOBkabI8HZVdOXmXbXhuKlDEd5Vv12P7oAVGfomGp3Ne21o5D/qu1WmthlNKFaoZJJeErA==
+  version "4.14.141"
+  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.141.tgz#d81f4d0c562abe28713406b571ffb27692a82ae6"
+  integrity sha512-v5NYIi9qEbFEUpCyikmnOYe4YlP8BMUdTcNCAquAKzu+FA7rZ1onj9x80mbnDdOW/K5bFf3Tv5kJplP33+gAbQ==
 
 "@types/minimatch@*":
   version "3.0.3"
@@ -537,23 +526,35 @@
   integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
 
 "@types/nanoid@^2.0.0":
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/@types/nanoid/-/nanoid-2.0.0.tgz#b59002c475e6dfcc26e67ba563ff61b512e5ebf8"
-  integrity sha512-NtwPHfAyU3IDXdKAB2OMPpAauHBg9gUjpOYr3FAzI84D70nWdS8k5mryteLvT/s1ACeAFAkGg132/XJVN4qx/w==
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/@types/nanoid/-/nanoid-2.1.0.tgz#41edfda78986e9127d0dc14de982de766f994020"
+  integrity sha512-xdkn/oRTA0GSNPLIKZgHWqDTWZsVrieKomxJBOQUK9YDD+zfSgmwD5t4WJYra5S7XyhTw7tfvwznW+pFexaepQ==
   dependencies:
     "@types/node" "*"
 
-"@types/nock@^10.0.3":
-  version "10.0.3"
-  resolved "https://registry.yarnpkg.com/@types/nock/-/nock-10.0.3.tgz#dab1d18ffbccfbf2db811dab9584304eeb6e1c4c"
-  integrity sha512-OthuN+2FuzfZO3yONJ/QVjKmLEuRagS9TV9lEId+WHL9KhftYG+/2z+pxlr0UgVVXSpVD8woie/3fzQn8ft/Ow==
+"@types/nock@^11.1.0":
+  version "11.1.0"
+  resolved "https://registry.yarnpkg.com/@types/nock/-/nock-11.1.0.tgz#0a8c1056a31ba32a959843abccf99626dd90a538"
+  integrity sha512-jI/ewavBQ7X5178262JQR0ewicPAcJhXS/iFaNJl0VHLfyosZ/kwSrsa6VNQNSO8i9d8SqdRgOtZSOKJ/+iNMw==
+  dependencies:
+    nock "*"
+
+"@types/node-fetch@^2.5.0":
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.2.tgz#76906dea5b3d6901e50e63e15249c9bcd6e9676e"
+  integrity sha512-djYYKmdNRSBtL1x4CiE9UJb9yZhwtI1VC+UxZD0psNznrUj80ywsxKlEGAE+QL1qvLjPbfb24VosjkYM6W4RSQ==
   dependencies:
     "@types/node" "*"
 
-"@types/node@*", "@types/node@^12.7.2", "@types/node@~12.7":
-  version "12.7.3"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.3.tgz#27b3f40addaf2f580459fdb405222685542f907a"
-  integrity sha512-3SiLAIBkDWDg6vFo0+5YJyHPWU9uwu40Qe+v+0MH8wRKYBimHvvAOyk3EzMrD/TrIlLYfXrqDqrg913PynrMJQ==
+"@types/node@*", "@types/node@^12.7.5", "@types/node@~12.7":
+  version "12.7.11"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.11.tgz#be879b52031cfb5d295b047f5462d8ef1a716446"
+  integrity sha512-Otxmr2rrZLKRYIybtdG/sgeO+tHY20GxeDjcGmUnmmlCWyEnv2a2x1ZXBo3BTec4OiTXMQCiazB8NMBf0iRlFw==
+
+"@types/node@^8.0.7":
+  version "8.10.54"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.54.tgz#1c88eb253ac1210f1a5876953fb70f7cc4928402"
+  integrity sha512-kaYyLYf6ICn6/isAyD4K1MyWWd5Q3JgH6bnMN089LUx88+s4W8GvK9Q6JMBVu5vsFFp7pMdSxdKmlBXwH/VFRg==
 
 "@types/normalize-package-data@^2.4.0":
   version "2.4.0"
@@ -577,6 +578,13 @@
   resolved "https://registry.yarnpkg.com/@types/text-table/-/text-table-0.2.1.tgz#39c4d4a058a82f677392dfd09976e83d9b4c9264"
   integrity sha512-dchbFCWfVgUSWEvhOkXGS7zjm+K7jCUvGrQkAHPk2Fmslfofp4HQTH2pqnQ3Pw5GPYv0zWa2AQjKtsfZThuemQ==
 
+"@types/xml2js@^0.4.4":
+  version "0.4.5"
+  resolved "https://registry.yarnpkg.com/@types/xml2js/-/xml2js-0.4.5.tgz#d21759b056f282d9c7066f15bbf5c19b908f22fa"
+  integrity sha512-yohU3zMn0fkhlape1nxXG2bLEGZRc1FeqF80RoHaYXJN7uibaauXfhzhOJr1Xh36sn+/tx21QAOf07b/xYVk1w==
+  dependencies:
+    "@types/node" "*"
+
 "@types/yaml@^1.0.2":
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/@types/yaml/-/yaml-1.0.2.tgz#bba080d64714c6ef3eaa023e235dacd2cfa3c938"
@@ -657,6 +665,20 @@ after@0.8.2:
   resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
   integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=
 
+agent-base@4, agent-base@^4.2.0, agent-base@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
+  integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==
+  dependencies:
+    es6-promisify "^5.0.0"
+
+agent-base@~4.2.1:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9"
+  integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==
+  dependencies:
+    es6-promisify "^5.0.0"
+
 aggregate-error@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.0.0.tgz#5b5a3c95e9095f311c9ab16c19fb4f3527cd3f79"
@@ -903,7 +925,7 @@ assign-symbols@^1.0.0:
   resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
   integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
 
-ast-types@0.13.2:
+ast-types@0.13.2, ast-types@0.x.x:
   version "0.13.2"
   resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.2.tgz#df39b677a911a83f3a049644fb74fdded23cea48"
   integrity sha512-uWMHxJxtfj/1oZClOxDEV1sQ1HCDkA4MG8Gr69KKeBjEVH0R84WlejZ0y2DcwyBlpAEMltmVYkVgqfLFb2oyiA==
@@ -1064,17 +1086,18 @@ bcrypt-pbkdf@^1.0.0:
   dependencies:
     tweetnacl "^0.14.3"
 
-better-ajv-errors@stoplightio/better-ajv-errors#0c5be246c12c87993ba07b47479cff8257b9579b:
-  version "0.6.4"
-  resolved "https://codeload.github.com/stoplightio/better-ajv-errors/tar.gz/0c5be246c12c87993ba07b47479cff8257b9579b"
+better-ajv-errors@^0.6.7:
+  version "0.6.7"
+  resolved "https://registry.yarnpkg.com/better-ajv-errors/-/better-ajv-errors-0.6.7.tgz#b5344af1ce10f434fe02fc4390a5a9c811e470d1"
+  integrity sha512-PYgt/sCzR4aGpyNy5+ViSQ77ognMnWq7745zM+/flYO4/Yisdtp9wDQW2IKCyVYPUxQt3E/b5GBSwfhd1LPdlg==
   dependencies:
     "@babel/code-frame" "^7.0.0"
     "@babel/runtime" "^7.0.0"
     chalk "^2.4.1"
-    core-js "^2.5.7"
+    core-js "^3.2.1"
     json-to-ast "^2.0.3"
     jsonpointer "^4.0.1"
-    leven "^2.1.0"
+    leven "^3.1.0"
 
 better-assert@~1.0.0:
   version "1.0.2"
@@ -1681,12 +1704,12 @@ copyfiles@^2.1.1:
     through2 "^2.0.1"
     yargs "^13.2.4"
 
-core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.7, core-js@^2.6.9:
+core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.9:
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2"
   integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==
 
-core-js@^3.1.3:
+core-js@^3.1.3, core-js@^3.2.1:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.2.1.tgz#cd41f38534da6cc59f7db050fe67307de9868b09"
   integrity sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw==
@@ -1796,6 +1819,13 @@ dashdash@^1.12.0:
   dependencies:
     assert-plus "^1.0.0"
 
+data-uri-to-buffer@2:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-2.0.1.tgz#ca8f56fe38b1fd329473e9d1b4a9afcd8ce1c045"
+  integrity sha512-OkVVLrerfAKZlW2ZZ3Ve2y65jgiWqBKsTfUIAFbn8nVbPcCZg6l6gikKlEYv0kXcmzqGm6mFq/Jf2vriuEkv8A==
+  dependencies:
+    "@types/node" "^8.0.7"
+
 data-urls@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe"
@@ -1835,26 +1865,26 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
   dependencies:
     ms "2.0.0"
 
-debug@^3.0.0, debug@^3.2.6:
-  version "3.2.6"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
-  integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
+debug@3.1.0, debug@~3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+  integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
   dependencies:
-    ms "^2.1.1"
+    ms "2.0.0"
 
-debug@^4.1.0, debug@^4.1.1:
+debug@4, debug@^4.1.0, debug@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
   integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
   dependencies:
     ms "^2.1.1"
 
-debug@~3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
-  integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
+debug@^3.0.0, debug@^3.1.0, debug@^3.2.6:
+  version "3.2.6"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
+  integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
   dependencies:
-    ms "2.0.0"
+    ms "^2.1.1"
 
 decamelize@^1.1.2, decamelize@^1.2.0:
   version "1.2.0"
@@ -1883,18 +1913,6 @@ deep-eql@^3.0.1:
   dependencies:
     type-detect "^4.0.0"
 
-deep-equal@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.0.tgz#3103cdf8ab6d32cf4a8df7865458f2b8d33f3745"
-  integrity sha512-ZbfWJq/wN1Z273o7mUSjILYqehAktR2NVoSrOukDkU9kg2v/Uv89yU4Cvz8seJeAmtN5oqiefKq8FPuXOboqLw==
-  dependencies:
-    is-arguments "^1.0.4"
-    is-date-object "^1.0.1"
-    is-regex "^1.0.4"
-    object-is "^1.0.1"
-    object-keys "^1.1.1"
-    regexp.prototype.flags "^1.2.0"
-
 deep-extend@^0.6.0:
   version "0.6.0"
   resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
@@ -1905,11 +1923,6 @@ deep-is@~0.1.3:
   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
   integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
 
-deepmerge@^2.0.1:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
-  integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
-
 defaults@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d"
@@ -1946,6 +1959,15 @@ define-property@^2.0.2:
     is-descriptor "^1.0.2"
     isobject "^3.0.1"
 
+degenerator@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-1.0.4.tgz#fcf490a37ece266464d9cc431ab98c5819ced095"
+  integrity sha1-/PSQo37OJmRk2cxDGrmMWBnO0JU=
+  dependencies:
+    ast-types "0.x.x"
+    escodegen "1.x.x"
+    esprima "3.x.x"
+
 del@^5.0.0:
   version "5.1.0"
   resolved "https://registry.yarnpkg.com/del/-/del-5.1.0.tgz#d9487c94e367410e6eff2925ee58c0c84a75b3a7"
@@ -2218,6 +2240,18 @@ es6-object-assign@^1.1.0:
   resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c"
   integrity sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw=
 
+es6-promise@^4.0.3:
+  version "4.2.8"
+  resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
+  integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
+
+es6-promisify@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
+  integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=
+  dependencies:
+    es6-promise "^4.0.3"
+
 escape-html@~1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
@@ -2240,7 +2274,7 @@ escodegen@1.8.x:
   optionalDependencies:
     source-map "~0.2.0"
 
-escodegen@^1.9.1:
+escodegen@1.x.x, escodegen@^1.9.1:
   version "1.12.0"
   resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.12.0.tgz#f763daf840af172bb3a2b6dd7219c0e17f7ff541"
   integrity sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg==
@@ -2277,7 +2311,7 @@ esprima@2.7.x, esprima@^2.7.1:
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
   integrity sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=
 
-esprima@^3.1.3:
+esprima@3.x.x, esprima@^3.1.3:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
   integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=
@@ -2499,16 +2533,15 @@ fast-glob@^2.2.6:
     merge2 "^1.2.3"
     micromatch "^3.1.10"
 
-fast-glob@^3.0.3:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.0.4.tgz#d484a41005cb6faeb399b951fd1bd70ddaebb602"
-  integrity sha512-wkIbV6qg37xTJwqSsdnIphL1e+LaGz4AIQqr00mIubMaEhv1/HEmJ0uuCGZRNRUkZZmOB5mJKO0ZUTVq+SxMQg==
+fast-glob@^3.0.3, fast-glob@^3.0.4:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.1.0.tgz#77375a7e3e6f6fc9b18f061cddd28b8d1eec75ae"
+  integrity sha512-TrUz3THiq2Vy3bjfQUB2wNyPdGBeGmdjbzzBLhfHN4YFurYptCKwGq/TfiRavbGywFRzY6U2CdmQ1zmsY5yYaw==
   dependencies:
-    "@nodelib/fs.stat" "^2.0.1"
-    "@nodelib/fs.walk" "^1.2.1"
-    glob-parent "^5.0.0"
-    is-glob "^4.0.1"
-    merge2 "^1.2.3"
+    "@nodelib/fs.stat" "^2.0.2"
+    "@nodelib/fs.walk" "^1.2.3"
+    glob-parent "^5.1.0"
+    merge2 "^1.3.0"
     micromatch "^4.0.2"
 
 fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0:
@@ -2541,13 +2574,14 @@ fb-watchman@^2.0.0:
     bser "^2.0.0"
 
 fetch-mock@^7.3.9:
-  version "7.3.9"
-  resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-7.3.9.tgz#a80fd2a1728f72e0634ef7a9734bc61200096487"
-  integrity sha512-PgsTbiQBNapFz2P2UwDl3gowK3nZqfV4HdyDZ1dI4eTGGH9MLAeBglIPbyDbbNQoGYBOfla6/9uaiq7az2z4Aw==
+  version "7.4.0"
+  resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-7.4.0.tgz#34f2f5cca03de93af9b253ebb8a678b72563cb02"
+  integrity sha512-q2zrAJh2CcPf6yrDIRxAo18vs1yohETzTNVvtJsqznAAWS4XuJ5M8uaIfrw8odHm7wZJIfMdA6P3RObMNYSWXQ==
   dependencies:
     babel-polyfill "^6.26.0"
     core-js "^2.6.9"
     glob-to-regexp "^0.4.0"
+    lodash.isequal "^4.5.0"
     path-to-regexp "^2.2.1"
     whatwg-url "^6.5.0"
 
@@ -2566,6 +2600,11 @@ figures@^2.0.0:
   dependencies:
     escape-string-regexp "^1.0.5"
 
+file-uri-to-path@1:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
+  integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
+
 filename-regex@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
@@ -2760,6 +2799,14 @@ fsevents@^2.0.6:
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.0.7.tgz#382c9b443c6cbac4c57187cdda23aa3bf1ccfc2a"
   integrity sha512-a7YT0SV3RB+DjYcppwVDLtn13UQnmg0SWZS7ezZD0UjnLwXmy8Zm21GMVGLaFGimIqcvyMQaOJBrop8MyOp1kQ==
 
+ftp@~0.3.10:
+  version "0.3.10"
+  resolved "https://registry.yarnpkg.com/ftp/-/ftp-0.3.10.tgz#9197d861ad8142f3e63d5a83bfe4c59f7330885d"
+  integrity sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=
+  dependencies:
+    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"
@@ -2818,6 +2865,18 @@ get-stream@^5.0.0:
   dependencies:
     pump "^3.0.0"
 
+get-uri@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-2.0.3.tgz#fa13352269781d75162c6fc813c9e905323fbab5"
+  integrity sha512-x5j6Ks7FOgLD/GlvjKwgu7wdmMR55iuRHhn8hj/+gA+eSbxQvZ+AEomq+3MgVEZj1vpi738QahGbCCSIDtXtkw==
+  dependencies:
+    data-uri-to-buffer "2"
+    debug "4"
+    extend "~3.0.2"
+    file-uri-to-path "1"
+    ftp "~0.3.10"
+    readable-stream "3"
+
 get-value@^1.1.5:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/get-value/-/get-value-1.3.1.tgz#8ac7ef4f20382392b2646548f9b9ad2dc6c89642"
@@ -2916,10 +2975,10 @@ glob-parent@^3.1.0:
     is-glob "^3.1.0"
     path-dirname "^1.0.0"
 
-glob-parent@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.0.0.tgz#1dc99f0f39b006d3e92c2c284068382f0c20e954"
-  integrity sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==
+glob-parent@^5.0.0, glob-parent@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2"
+  integrity sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==
   dependencies:
     is-glob "^4.0.1"
 
@@ -3189,6 +3248,25 @@ http-errors@1.7.2:
     statuses ">= 1.5.0 < 2"
     toidentifier "1.0.0"
 
+http-errors@1.7.3:
+  version "1.7.3"
+  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
+  integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
+  dependencies:
+    depd "~1.1.2"
+    inherits "2.0.4"
+    setprototypeof "1.1.1"
+    statuses ">= 1.5.0 < 2"
+    toidentifier "1.0.0"
+
+http-proxy-agent@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405"
+  integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==
+  dependencies:
+    agent-base "4"
+    debug "3.1.0"
+
 http-proxy@^1.13.0:
   version "1.17.0"
   resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a"
@@ -3212,10 +3290,18 @@ https-browserify@^1.0.0:
   resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
   integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
 
+https-proxy-agent@^2.2.1:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.2.tgz#271ea8e90f836ac9f119daccd39c19ff7dfb0793"
+  integrity sha512-c8Ndjc9Bkpfx/vCJueCPy0jlP4ccCCSNDp8xwCZzPjKJUm+B+u9WX2x98Qx4n1PiMNTWo3D7KK5ifNV/yJyRzg==
+  dependencies:
+    agent-base "^4.3.0"
+    debug "^3.1.0"
+
 husky@^3.0.0:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/husky/-/husky-3.0.4.tgz#10a48ac11ab50859b0939750fa0b4e07ad0bf669"
-  integrity sha512-7Rnt8aJfy+MlV28snmYK7O7vWwtOfeVxV6KhLpUFXlmx5ukQ1nQmNUB7QsAwSgdySB5X+bm7q7JIRgazqBUzKA==
+  version "3.0.8"
+  resolved "https://registry.yarnpkg.com/husky/-/husky-3.0.8.tgz#8de3fed26ce9b43034ef51013c4ad368b6b74ea8"
+  integrity sha512-HFOsgcyrX3qe/rBuqyTt+P4Gxn5P0seJmr215LAZ/vnwK3jWB3r0ck7swbzGRUbufCf9w/lgHPVbF/YXQALgfQ==
   dependencies:
     chalk "^2.4.2"
     cosmiconfig "^5.2.1"
@@ -3309,7 +3395,7 @@ inflight@^1.0.4:
     once "^1.3.0"
     wrappy "1"
 
-inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
+inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -3346,6 +3432,11 @@ invariant@^2.2.4:
   dependencies:
     loose-envify "^1.0.0"
 
+ip@^1.1.5:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
+  integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=
+
 is-absolute@^0.2.2:
   version "0.2.6"
   resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-0.2.6.tgz#20de69f3db942ef2d87b9c2da36f172235b1b5eb"
@@ -4162,10 +4253,10 @@ jest-watcher@^24.9.0:
     jest-util "^24.9.0"
     string-length "^2.0.0"
 
-jest-when@~2.6:
-  version "2.6.2"
-  resolved "https://registry.yarnpkg.com/jest-when/-/jest-when-2.6.2.tgz#ba0e0a1ad497387350e70c2eeb783008b198a079"
-  integrity sha512-fivO1rRScZVEkEQe2BcURx18wD8SbbcX3U/4mTmFRSA9CkPoNs492qakwnM+grLkXDV7ZURmyH1gXPbfF9/uXQ==
+jest-when@~2.7:
+  version "2.7.0"
+  resolved "https://registry.yarnpkg.com/jest-when/-/jest-when-2.7.0.tgz#9549185ae8847b47d5d40262f1c59a5143e89a0c"
+  integrity sha512-psU0pXdomBORY9TGuSut/k8vViVki9l92WggL0m5/Lk8zTrDYtcCpPIFdZQDKqXvmW5Jzoh7SCsLKITvBJ0jyQ==
   dependencies:
     bunyan "^1.8.12"
     expect "^24.8.0"
@@ -4500,11 +4591,6 @@ left-pad@^1.3.0:
   resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e"
   integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==
 
-leven@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
-  integrity sha1-wuep93IJTe6dNCAq6KzORoeHVYA=
-
 leven@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
@@ -4524,9 +4610,9 @@ lines-and-columns@^1.1.6:
   integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
 
 lint-staged@^9.0.2:
-  version "9.2.5"
-  resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-9.2.5.tgz#5a3e1e0a539a403bd7f88542bc3d34ce52efdbb3"
-  integrity sha512-d99gTBFMJ29159+9iRvaMEQstmNcPAbQbhHSYw6D/1FncvFdIj8lWHztaq3Uq+tbZPABHXQ/fyN7Rp1QwF8HIw==
+  version "9.4.1"
+  resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-9.4.1.tgz#60c0f85745bd398e6460aa7f5adb3cad3a2b862c"
+  integrity sha512-zFRbo1bAJEVf1m33paTTjDVfy2v3lICCqHfmQSgNoI+lWpi7HPG5y/R2Y7Whdce+FKxlZYs/U1sDSx8+nmQdDA==
   dependencies:
     chalk "^2.4.2"
     commander "^2.20.0"
@@ -4623,6 +4709,16 @@ locate-path@^5.0.0:
   dependencies:
     p-locate "^4.1.0"
 
+lodash.isequal@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
+  integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
+
+lodash.memoize@4.x:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+  integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
+
 lodash.memoize@~3.0.3:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f"
@@ -4633,12 +4729,7 @@ lodash.sortby@^4.7.0:
   resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
   integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
 
-lodash@>=4.17.5, lodash@^4.17.0, lodash@^4.17.11, lodash@^4.17.5:
-  version "4.17.11"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
-  integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
-
-lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15:
+lodash@>=4.17.5, lodash@^4.17.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15:
   version "4.17.15"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
   integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@@ -4692,7 +4783,7 @@ loud-rejection@^1.0.0:
     currently-unhandled "^0.4.1"
     signal-exit "^3.0.0"
 
-lru-cache@4.1.x:
+lru-cache@4.1.x, lru-cache@^4.1.2:
   version "4.1.5"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
   integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
@@ -4791,10 +4882,10 @@ merge-stream@^2.0.0:
   resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
   integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
 
-merge2@^1.2.3:
-  version "1.2.4"
-  resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.4.tgz#c9269589e6885a60cf80605d9522d4b67ca646e3"
-  integrity sha512-FYE8xI+6pjFOhokZu0We3S5NKCirLbCzSh2Usf3qEyr4X8U+0jNg9P8RZ4qz+V2UoECLVwSyzU3LxXBaLGtD3A==
+merge2@^1.2.3, merge2@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81"
+  integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==
 
 micromatch@^2.3.11, micromatch@^2.3.7:
   version "2.3.11"
@@ -5001,9 +5092,9 @@ nan@^2.12.1, nan@^2.14.0:
   integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
 
 nanoid@^2.0.3:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.0.4.tgz#4889355c9ce8e24efad7c65945a4a2875ac3e8f4"
-  integrity sha512-sOJnBmY3TJQBVIBqKHoifuwygrocXg3NjS9rZSMnVl05XWSHK7Qxb177AIZQyMDjP86bz+yneozj/h9qsPLcCA==
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.2.tgz#5c63a913f4fbf4afff2004c7bb42dee8e627baf4"
+  integrity sha512-q0iKJHcLc9rZg/qtJ/ioG5s6/5357bqvkYCpqXJxpcyfK7L5us8+uJllZosqPWou7l6E1lY2Qqoq5ce+AMbFuQ==
 
 nanomatch@^1.2.9:
   version "1.2.13"
@@ -5051,25 +5142,27 @@ neo-async@^2.6.0:
   resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
   integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
 
+netmask@^1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35"
+  integrity sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=
+
 nice-try@^1.0.4:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
   integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
 
-nock@~10.0.6:
-  version "10.0.6"
-  resolved "https://registry.yarnpkg.com/nock/-/nock-10.0.6.tgz#e6d90ee7a68b8cfc2ab7f6127e7d99aa7d13d111"
-  integrity sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w==
+nock@*, nock@~11.4.0:
+  version "11.4.0"
+  resolved "https://registry.yarnpkg.com/nock/-/nock-11.4.0.tgz#68c2f9cf533f13ecb1a43e956823dbfba5593d17"
+  integrity sha512-UrVEbEAvhyDoUttrS0fv3znhZ5nEJvlxqgmrC6Gb2Mf9cFci65RMK17e6EjDDQB57g5iwZw1TFnVvyeL0eUlhQ==
   dependencies:
     chai "^4.1.2"
     debug "^4.1.0"
-    deep-equal "^1.0.0"
     json-stringify-safe "^5.0.1"
-    lodash "^4.17.5"
+    lodash "^4.17.13"
     mkdirp "^0.5.0"
-    propagate "^1.0.0"
-    qs "^6.5.1"
-    semver "^5.5.0"
+    propagate "^2.0.0"
 
 node-fetch@^2.6:
   version "2.6.0"
@@ -5239,7 +5332,7 @@ object-is@^1.0.1:
   resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6"
   integrity sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=
 
-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==
@@ -5440,6 +5533,31 @@ p-try@^2.0.0:
   resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
   integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
 
+pac-proxy-agent@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-3.0.0.tgz#11d578b72a164ad74bf9d5bac9ff462a38282432"
+  integrity sha512-AOUX9jES/EkQX2zRz0AW7lSx9jD//hQS8wFXBvcnd/J2Py9KaMJMqV/LPqJssj1tgGufotb2mmopGPR15ODv1Q==
+  dependencies:
+    agent-base "^4.2.0"
+    debug "^3.1.0"
+    get-uri "^2.0.0"
+    http-proxy-agent "^2.1.0"
+    https-proxy-agent "^2.2.1"
+    pac-resolver "^3.0.0"
+    raw-body "^2.2.0"
+    socks-proxy-agent "^4.0.1"
+
+pac-resolver@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-3.0.0.tgz#6aea30787db0a891704deb7800a722a7615a6f26"
+  integrity sha512-tcc38bsjuE3XZ5+4vP96OfhOugrX+JcnpUbhfuc4LuXBLQhoTthOstZeoQJBDnQUDYzYmdImKsbz0xSl1/9qeA==
+  dependencies:
+    co "^4.6.0"
+    degenerator "^1.0.4"
+    ip "^1.1.5"
+    netmask "^1.0.6"
+    thunkify "^2.1.2"
+
 pad@^3.2.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/pad/-/pad-3.2.0.tgz#be7a1d1cb6757049b4ad5b70e71977158fea95d1"
@@ -5813,10 +5931,29 @@ prompts@^2.0.1:
     kleur "^3.0.3"
     sisteransi "^1.0.3"
 
-propagate@^1.0.0:
+propagate@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45"
+  integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==
+
+proxy-agent@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-3.1.0.tgz#3cf86ee911c94874de4359f37efd9de25157c113"
+  integrity sha512-IkbZL4ClW3wwBL/ABFD2zJ8iP84CY0uKMvBPk/OceQe/cEjrxzN1pMHsLwhbzUoRhG9QbSxYC+Z7LBkTiBNvrA==
+  dependencies:
+    agent-base "^4.2.0"
+    debug "^3.1.0"
+    http-proxy-agent "^2.1.0"
+    https-proxy-agent "^2.2.1"
+    lru-cache "^4.1.2"
+    pac-proxy-agent "^3.0.0"
+    proxy-from-env "^1.0.0"
+    socks-proxy-agent "^4.0.1"
+
+proxy-from-env@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.yarnpkg.com/propagate/-/propagate-1.0.0.tgz#00c2daeedda20e87e3782b344adba1cddd6ad709"
-  integrity sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=
+  resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
+  integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=
 
 pseudomap@^1.0.2:
   version "1.0.2"
@@ -5873,11 +6010,6 @@ qs@6.7.0:
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
   integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
 
-qs@^6.5.1:
-  version "6.8.0"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.8.0.tgz#87b763f0d37ca54200334cd57bb2ef8f68a1d081"
-  integrity sha512-tPSkj8y92PfZVbinY1n84i1Qdx75lZjMQYx9WZhnkofyxzw2r7Ho39G3/aEvSUdebxpnnM4LZJCtvE/Aq3+s9w==
-
 qs@~6.5.2:
   version "6.5.2"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
@@ -5932,6 +6064,16 @@ raw-body@2.4.0:
     iconv-lite "0.4.24"
     unpipe "1.0.0"
 
+raw-body@^2.2.0:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c"
+  integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==
+  dependencies:
+    bytes "3.1.0"
+    http-errors "1.7.3"
+    iconv-lite "0.4.24"
+    unpipe "1.0.0"
+
 rc@^1.2.7:
   version "1.2.8"
   resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
@@ -5991,7 +6133,17 @@ read-pkg@^5.1.1:
     parse-json "^5.0.0"
     type-fest "^0.6.0"
 
-"readable-stream@2 || 3", readable-stream@^3.0.6, readable-stream@^3.1.1:
+readable-stream@1.1.x:
+  version "1.1.14"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
+  integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk=
+  dependencies:
+    core-util-is "~1.0.0"
+    inherits "~2.0.1"
+    isarray "0.0.1"
+    string_decoder "~0.10.x"
+
+"readable-stream@2 || 3", readable-stream@3, readable-stream@^3.0.6, readable-stream@^3.1.1:
   version "3.4.0"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc"
   integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==
@@ -6085,13 +6237,6 @@ regex-not@^1.0.0, regex-not@^1.0.2:
     extend-shallow "^3.0.2"
     safe-regex "^1.1.0"
 
-regexp.prototype.flags@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz#6b30724e306a27833eeb171b66ac8890ba37e41c"
-  integrity sha512-ztaw4M1VqgMwl9HlPpOuiYgItcHlunW0He2fE6eNfT6E/CF2FtYi9ofOYe4mKntstYk0Fyh/rDRBdS3AnxjlrA==
-  dependencies:
-    define-properties "^1.1.2"
-
 relative@^3.0.1:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/relative/-/relative-3.0.2.tgz#0dcd8ec54a5d35a3c15e104503d65375b5a5367f"
@@ -6315,9 +6460,9 @@ rollup-plugin-node-resolve@^5.2.0:
     rollup-pluginutils "^2.8.1"
 
 rollup-plugin-terser@^5.1.1:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-5.1.1.tgz#e9d2545ec8d467f96ba99b9216d2285aad8d5b66"
-  integrity sha512-McIMCDEY8EU6Y839C09UopeRR56wXHGdvKKjlfiZG/GrP6wvZQ62u2ko/Xh1MNH2M9WDL+obAAHySljIZYCuPQ==
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-5.1.2.tgz#3e41256205cb75f196fc70d4634227d1002c255c"
+  integrity sha512-sWKBCOS+vUkRtHtEiJPAf+WnBqk/C402fBD9AVHxSIXMqjsY7MnYWKYEUqGixtr0c8+1DjzUEPlNgOYQPVrS1g==
   dependencies:
     "@babel/code-frame" "^7.0.0"
     jest-worker "^24.6.0"
@@ -6326,9 +6471,9 @@ rollup-plugin-terser@^5.1.1:
     terser "^4.1.0"
 
 rollup-plugin-typescript2@^0.24.0:
-  version "0.24.0"
-  resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.24.0.tgz#27ce5459d03e095236bdbcda3ca68fe289718d07"
-  integrity sha512-yp7z9hZ5kXjOGXKXkfTRJuIPyLvKtUWfAGmreilnYn+qIVhE5CgE7SFvzLKHggsAXPaCAflHRiEj0l71WXWJkA==
+  version "0.24.3"
+  resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.24.3.tgz#276fa33a9d584d500da62d3e5400307f4a46bdf2"
+  integrity sha512-D7yovQlhnRoz7pG/RF0ni+koxgzEShwfAGuOq6OVqKzcATHOvmUt2ePeYVdc9N0adcW1PcTzklUEM0oNWE/POw==
   dependencies:
     find-cache-dir "^3.0.0"
     fs-extra "8.1.0"
@@ -6344,12 +6489,12 @@ rollup-pluginutils@2.8.1, rollup-pluginutils@^2.8.1:
     estree-walker "^0.6.1"
 
 rollup@^1.19.4:
-  version "1.20.3"
-  resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.20.3.tgz#6243f6c118ca05f56b2d9433112400cd834a1eb8"
-  integrity sha512-/OMCkY0c6E8tleeVm4vQVDz24CkVgvueK3r8zTYu2AQNpjrcaPwO9hE+pWj5LTFrvvkaxt4MYIp2zha4y0lRvg==
+  version "1.21.4"
+  resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.21.4.tgz#00a41a30f90095db890301b226cbe2918e4cf54d"
+  integrity sha512-Pl512XVCmVzgcBz5h/3Li4oTaoDcmpuFZ+kdhS/wLreALz//WuDAMfomD3QEYl84NkDu6Z6wV9twlcREb4qQsw==
   dependencies:
     "@types/estree" "0.0.39"
-    "@types/node" "^12.7.2"
+    "@types/node" "^12.7.5"
     acorn "^7.0.0"
 
 rsvp@^4.8.4:
@@ -6421,7 +6566,7 @@ sane@^4.0.3:
     minimist "^1.1.1"
     walker "~1.0.5"
 
-sax@^1.2.4:
+sax@>=0.6.0, sax@^1.2.4:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
   integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
@@ -6534,6 +6679,11 @@ slice-ansi@0.0.4:
   resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
   integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=
 
+smart-buffer@4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.0.2.tgz#5207858c3815cc69110703c6b94e46c15634395d"
+  integrity sha512-JDhEpTKzXusOqXZ0BUIdH+CjFdO/CR3tLlf5CN34IypI+xMmXW1uB16OOY8z3cICbJlDAVJzNbwBhNO0wt9OAw==
+
 snapdragon-node@^2.0.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@@ -6610,6 +6760,22 @@ socket.io@2.1.1:
     socket.io-client "2.1.1"
     socket.io-parser "~3.2.0"
 
+socks-proxy-agent@^4.0.1:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz#3c8991f3145b2799e70e11bd5fbc8b1963116386"
+  integrity sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==
+  dependencies:
+    agent-base "~4.2.1"
+    socks "~2.3.2"
+
+socks@~2.3.2:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/socks/-/socks-2.3.2.tgz#ade388e9e6d87fdb11649c15746c578922a5883e"
+  integrity sha512-pCpjxQgOByDHLlNqlnh/mNSAxIUkyBBuwwhTcV+enZGbDaClPvHdvm6uvOwZfFJkam7cGhBNbb4JxiP8UZkRvQ==
+  dependencies:
+    ip "^1.1.5"
+    smart-buffer "4.0.2"
+
 source-map-resolve@^0.5.0:
   version "0.5.2"
   resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259"
@@ -6999,6 +7165,11 @@ through2@^2.0.0, through2@^2.0.1:
     readable-stream "~2.3.6"
     xtend "~4.0.1"
 
+thunkify@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/thunkify/-/thunkify-2.1.2.tgz#faa0e9d230c51acc95ca13a361ac05ca7e04553d"
+  integrity sha1-+qDp0jDFGsyVyhOjYawFyn4EVT0=
+
 timers-browserify@^2.0.10:
   version "2.0.11"
   resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f"
@@ -7106,14 +7277,15 @@ trim-right@^1.0.1:
   integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=
 
 ts-jest@^24.0.2:
-  version "24.0.2"
-  resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-24.0.2.tgz#8dde6cece97c31c03e80e474c749753ffd27194d"
-  integrity sha512-h6ZCZiA1EQgjczxq+uGLXQlNgeg02WWJBbeT8j6nyIBRQdglqbvzDoHahTEIiS6Eor6x8mK6PfZ7brQ9Q6tzHw==
+  version "24.1.0"
+  resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-24.1.0.tgz#2eaa813271a2987b7e6c3fefbda196301c131734"
+  integrity sha512-HEGfrIEAZKfu1pkaxB9au17b1d9b56YZSqz5eCVE8mX68+5reOvlM93xGOzzCREIov9mdH7JBG+s0UyNAqr0tQ==
   dependencies:
     bs-logger "0.x"
     buffer-from "1.x"
     fast-json-stable-stringify "2.x"
     json5 "2.x"
+    lodash.memoize "4.x"
     make-error "1.x"
     mkdirp "0.x"
     resolve "1.x"
@@ -7121,9 +7293,9 @@ ts-jest@^24.0.2:
     yargs-parser "10.x"
 
 ts-node@^8.1.0:
-  version "8.3.0"
-  resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.3.0.tgz#e4059618411371924a1fb5f3b125915f324efb57"
-  integrity sha512-dyNS/RqyVTDcmNM4NIBAeDMpsAdaQ+ojdf0GOLqE6nwJOgzEkdRNzJywhDfwnuvB10oa6NLVG1rUJQCpRN7qoQ==
+  version "8.4.1"
+  resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.4.1.tgz#270b0dba16e8723c9fa4f9b4775d3810fd994b4f"
+  integrity sha512-5LpRN+mTiCs7lI5EtbXmF/HfMeCjzt7DH9CZwtkr6SywStrNQC723wG+aOWFiLNn7zT3kD/RnFqi3ZUfr4l5Qw==
   dependencies:
     arg "^4.1.0"
     diff "^4.0.1"
@@ -7132,12 +7304,11 @@ ts-node@^8.1.0:
     yn "^3.0.0"
 
 tsconfig-paths@^3.8.0:
-  version "3.8.0"
-  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.8.0.tgz#4e34202d5b41958f269cf56b01ed95b853d59f72"
-  integrity sha512-zZEYFo4sjORK8W58ENkRn9s+HmQFkkwydDG7My5s/fnfr2YYCaiyXe/HBUcIgU8epEKOXwiahOO+KZYjiXlWyQ==
+  version "3.9.0"
+  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b"
+  integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==
   dependencies:
     "@types/json5" "^0.0.29"
-    deepmerge "^2.0.1"
     json5 "^1.0.1"
     minimist "^1.2.0"
     strip-bom "^3.0.0"
@@ -7189,16 +7360,16 @@ tslint@5.11.x:
     tslib "^1.8.0"
     tsutils "^2.27.2"
 
-tslint@~5.19:
-  version "5.19.0"
-  resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.19.0.tgz#a2cbd4a7699386da823f6b499b8394d6c47bb968"
-  integrity sha512-1LwwtBxfRJZnUvoS9c0uj8XQtAnyhWr9KlNvDIdB+oXyT+VpsOAaEhEgKi1HrZ8rq0ki/AAnbGSv4KM6/AfVZw==
+tslint@~5.20:
+  version "5.20.0"
+  resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.20.0.tgz#fac93bfa79568a5a24e7be9cdde5e02b02d00ec1"
+  integrity sha512-2vqIvkMHbnx8acMogAERQ/IuINOq6DFqgF8/VDvhEkBqQh/x6SP0Y+OHnKth9/ZcHQSroOZwUQSN18v8KKF0/g==
   dependencies:
     "@babel/code-frame" "^7.0.0"
     builtin-modules "^1.1.1"
     chalk "^2.3.0"
     commander "^2.12.1"
-    diff "^3.2.0"
+    diff "^4.0.1"
     glob "^7.1.1"
     js-yaml "^3.13.1"
     minimatch "^3.0.4"
@@ -7269,9 +7440,9 @@ typescript-json-schema@~0.40:
     yargs "^14.0.0"
 
 typescript@^3.5.3:
-  version "3.6.2"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.2.tgz#105b0f1934119dde543ac8eb71af3a91009efe54"
-  integrity sha512-lmQ4L+J6mnu3xweP8+rOrUwzmN+MRAj7TgtJtDaXE5PMyX2kCrklhg3rvOsOIfNeAWMQWO2F1GPc1kMD2vLAfw==
+  version "3.6.3"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.3.tgz#fea942fabb20f7e1ca7164ff626f1a9f3f70b4da"
+  integrity sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==
 
 uglify-js@^3.1.4:
   version "3.6.0"
@@ -7376,7 +7547,7 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1:
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
   integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
 
-util.promisify@^1.0.0:
+util.promisify@^1.0.0, util.promisify@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030"
   integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==
@@ -7574,11 +7745,30 @@ xml-name-validator@^3.0.0:
   resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
   integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
 
+xml2js@^0.4.21:
+  version "0.4.22"
+  resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.22.tgz#4fa2d846ec803237de86f30aa9b5f70b6600de02"
+  integrity sha512-MWTbxAQqclRSTnehWWe5nMKzI3VmJ8ltiJEco8akcC6j3miOhjjfzKum5sId+CWhfxdOs/1xauYr8/ZDBtQiRw==
+  dependencies:
+    sax ">=0.6.0"
+    util.promisify "~1.0.0"
+    xmlbuilder "~11.0.0"
+
+xmlbuilder@~11.0.0:
+  version "11.0.1"
+  resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"
+  integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==
+
 xmlhttprequest-ssl@~1.5.4:
   version "1.5.5"
   resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e"
   integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=
 
+xregexp@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"
+  integrity sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=
+
 xtend@^4.0.0, xtend@~4.0.1:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
@@ -7599,18 +7789,6 @@ yallist@^3.0.0, yallist@^3.0.3:
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9"
   integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==
 
-yaml-ast-parser@~0.0.43:
-  version "0.0.43"
-  resolved "https://registry.yarnpkg.com/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz#e8a23e6fb4c38076ab92995c5dca33f3d3d7c9bb"
-  integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==
-
-yaml@~1.6:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.6.0.tgz#d8a985cfb26086dd73f91c637f6e6bc909fddd3c"
-  integrity sha512-iZfse3lwrJRoSlfs/9KQ9iIXxs9++RvBFVzAqbbBiFT+giYtyanevreF9r61ZTbGMgWQBxAua3FzJiniiJXWWw==
-  dependencies:
-    "@babel/runtime" "^7.4.5"
-
 yargs-parser@10.x:
   version "10.1.0"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8"


Test patch:
diff --git a/src/__tests__/linter.jest.test.ts b/src/__tests__/linter.jest.test.ts
index 60cd9fae..cbc74449 100644
--- a/src/__tests__/linter.jest.test.ts
+++ b/src/__tests__/linter.jest.test.ts
@@ -1,5 +1,8 @@
 import * as path from '@stoplight/path';
-import { isOpenApiv2, isOpenApiv3 } from '../rulesets/lookups';
+import { join } from '@stoplight/path';
+import { DiagnosticSeverity } from '@stoplight/types';
+import { isOpenApiv2, isOpenApiv3 } from '../formats';
+import { httpAndFileResolver } from '../resolvers/http-and-file';
 import { Spectral } from '../spectral';
 
 const customFunctionOASRuleset = path.join(__dirname, './__fixtures__/custom-functions-oas-ruleset.json');
@@ -74,4 +77,66 @@ describe('Linter', () => {
       }),
     ]);
   });
+
+  describe('evaluate {{value}} in validation messages', () => {
+    test('should print correct values for referenced files', async () => {
+      spectral = new Spectral({ resolver: httpAndFileResolver });
+
+      spectral.setRules({
+        'empty-is-falsy': {
+          severity: DiagnosticSeverity.Error,
+          recommended: true,
+          description: 'Should be falsy',
+          message: 'Value {{value}} should be falsy',
+          given: '$..empty',
+          then: {
+            function: 'falsy',
+          },
+        },
+      });
+
+      return expect(
+        spectral.run(
+          {
+            empty: {
+              $ref: './__fixtures__/petstore.merge.keys.oas3.json#/info/contact/url',
+            },
+            bar: {
+              empty: {
+                $ref: './__fixtures__/petstore.oas3.json#/servers',
+              },
+            },
+            foo: {
+              empty: {
+                $ref: './__fixtures__/petstore.oas3.json#/info',
+              },
+            },
+          },
+          {
+            resolve: {
+              documentUri: join(__dirname, 'foo.json'),
+            },
+          },
+        ),
+      ).resolves.toEqual(
+        expect.arrayContaining([
+          expect.objectContaining({
+            code: 'empty-is-falsy',
+            message: 'Value "https://example.com" should be falsy',
+            path: ['empty'],
+          }),
+          expect.objectContaining({
+            code: 'empty-is-falsy',
+            message: 'Value Array[] should be falsy',
+            path: ['bar', 'empty'],
+          }),
+          expect.objectContaining({
+            code: 'empty-is-falsy',
+            message: 'Value Object{} should be falsy',
+            path: ['foo', 'empty'],
+          }),
+        ]),
+      );
+    });
+  });
 });
diff --git a/src/__tests__/linter.test.ts b/src/__tests__/linter.test.ts
index b7b5a696..36b6c651 100644
--- a/src/__tests__/linter.test.ts
+++ b/src/__tests__/linter.test.ts
@@ -2,8 +2,8 @@ import { getLocationForJsonPath, parseWithPointers } from '@stoplight/json';
 import { Resolver } from '@stoplight/json-ref-resolver';
 import { DiagnosticSeverity } from '@stoplight/types';
 import { parse } from '@stoplight/yaml';
+import { isOpenApiv2, isOpenApiv3 } from '../formats';
 import { mergeRules, readRuleset } from '../rulesets';
-import { isOpenApiv2, isOpenApiv3 } from '../rulesets/lookups';
 import { RuleCollection, Spectral } from '../spectral';
 
 const invalidSchema = JSON.stringify(require('./__fixtures__/petstore.invalid-schema.oas3.json'));
@@ -366,7 +366,7 @@ describe('linter', () => {
     expect(result).toEqual([]);
   });
 
-  test('should prefer the first matched format', async () => {
+  test('should execute rules matching all found formats', async () => {
     spectral.registerFormat('foo-bar', obj => typeof obj === 'object' && obj !== null && 'foo-bar' in obj);
     spectral.registerFormat('baz', () => true);
 
@@ -399,10 +399,13 @@ describe('linter', () => {
       expect.objectContaining({
         code: 'rule1',
       }),
+      expect.objectContaining({
+        code: 'rule2',
+      }),
     ]);
   });
 
-  test('should not run any rule with defined formats if some formats are are registered but document format could not be associated', async () => {
+  test('should not run any rule with defined formats if some formats are registered but document format could not be associated', async () => {
     spectral.registerFormat('foo-bar', obj => typeof obj === 'object' && obj !== null && 'foo-bar' in obj);
 
     spectral.setRules({
@@ -870,6 +873,122 @@ responses:: !!foo
     });
   });
 
+  describe('evaluate {{value}} in validation messages', () => {
+    test('should print primitive values', () => {
+      spectral = new Spectral();
+      spectral.setRules({
+        'header-parameter-names-kebab-case': {
+          severity: DiagnosticSeverity.Error,
+          recommended: true,
+          description: 'A parameter in the header should be written in kebab-case',
+          message: '{{value}} is not kebab-cased: {{error}}',
+          given: "$..parameters[?(@.in === 'header')]",
+          then: {
+            field: 'name',
+            function: 'pattern',
+            functionOptions: {
+              match: '^[a-z0-9]+((-[a-z0-9]+)+)?$',
+            },
+          },
+        },
+      });
+
+      return expect(
+        spectral.run({
+          parameters: [
+            {
+              in: 'header',
+              name: 'fooA',
+            },
+          ],
+          foo: {
+            parameters: [
+              {
+                in: 'header',
+                name: 'd 1',
+              },
+            ],
+          },
+        }),
+      ).resolves.toEqual([
+        expect.objectContaining({
+          code: 'header-parameter-names-kebab-case',
+          message: '"fooA" is not kebab-cased: must match the pattern \'^[a-z0-9]+((-[a-z0-9]+)+)?$\'',
+          path: ['parameters', '0', 'name'],
+        }),
+
+        expect.objectContaining({
+          code: 'header-parameter-names-kebab-case',
+          message: '"d 1" is not kebab-cased: must match the pattern \'^[a-z0-9]+((-[a-z0-9]+)+)?$\'',
+          path: ['foo', 'parameters', '0', 'name'],
+        }),
+      ]);
+    });
+
+    test('should not attempt to print complex values', () => {
+      spectral = new Spectral();
+      spectral.setRules({
+        'empty-is-falsy': {
+          severity: DiagnosticSeverity.Error,
+          recommended: true,
+          description: 'Should be falsy',
+          message: 'Value {{value}} should be falsy',
+          given: '$..empty',
+          then: {
+            function: 'falsy',
+          },
+        },
+      });
+
+      return expect(
+        spectral.run({
+          empty: {
+            a: 'b',
+          },
+          bar: {
+            empty: [13, { empty: 123 }],
+          },
+        }),
+      ).resolves.toEqual([
+        expect.objectContaining({
+          code: 'empty-is-falsy',
+          message: 'Value Object{} should be falsy',
+          path: ['empty'],
+        }),
+        expect.objectContaining({
+          code: 'empty-is-falsy',
+          message: 'Value Array[] should be falsy',
+          path: ['bar', 'empty'],
+        }),
+        expect.objectContaining({
+          code: 'empty-is-falsy',
+          message: 'Value 123 should be falsy',
+          path: ['bar', 'empty', '1', 'empty'], // fixme: 1 should be a number
+        }),
+      ]);
+    });
+  });
+
+  test('should evaluate {{path}} in validation messages', async () => {
+    await spectral.loadRuleset('spectral:oas3');
+    spectral.setRules({
+      'oas3-schema': {
+        ...spectral.rules['oas3-schema'],
+        message: 'Schema error at {{path}}',
+      },
+    });
+
+    return expect(spectral.run(invalidSchema)).resolves.toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({
+          code: 'oas3-schema',
+          message: 'Schema error at #/paths/~1pets/get/responses/200',
+          path: ['paths', '/pets', 'get', 'responses', '200'],
+        }),
+      ]),
+    );
+  });
+
   test('should report ref siblings', async () => {
     await spectral.loadRuleset('spectral:oas');
 
diff --git a/src/__tests__/request.jest.test.ts b/src/__tests__/request.jest.test.ts
new file mode 100644
index 00000000..d6a5b671
--- /dev/null
+++ b/src/__tests__/request.jest.test.ts
@@ -0,0 +1,82 @@
+import * as http from 'http';
+import * as url from 'url';
+import { DEFAULT_REQUEST_OPTIONS } from '../request';
+import { httpAndFileResolver } from '../resolvers/http-and-file';
+import { Spectral } from '../spectral';
+const ProxyAgent = require('proxy-agent');
+
+const PORT = 4001;
+
+describe('request', () => {
+  describe('when agent is set', () => {
+    let server: http.Server;
+
+    beforeAll(() => {
+      // nock cannot mock proxied requests
+      server = http
+        .createServer((req, res) => {
+          const { pathname } = url.parse(String(req.url));
+          if (pathname === '/custom-ruleset') {
+            res.writeHead(403);
+          } else if (pathname === '/ok.json') {
+            res.writeHead(200, { 'Content-Type': 'application/json' });
+            res.write(
+              JSON.stringify({
+                info: {
+                  title: '',
+                  description: 'Foo',
+                },
+              }),
+            );
+          } else {
+            res.writeHead(404);
+          }
+
+          res.end();
+        })
+        .listen(PORT, '0.0.0.0');
+    });
+
+    afterAll(() => {
+      server.close();
+    });
+
+    beforeEach(() => {
+      DEFAULT_REQUEST_OPTIONS.agent = new ProxyAgent(`http://localhost:${PORT}`);
+    });
+
+    afterEach(() => {
+      delete DEFAULT_REQUEST_OPTIONS.agent;
+    });
+
+    describe('loading a ruleset', () => {
+      it('proxies the request', () => {
+        const s = new Spectral();
+
+        return expect(s.loadRuleset('http://localhost:4000/custom-ruleset')).rejects.toHaveProperty(
+          'message',
+          'Could not parse http://localhost:4000/custom-ruleset: Forbidden',
+        );
+      });
+    });
+
+    describe('loading a $ref', () => {
+      it('proxies the request', () => {
+        const doc = {
+          info: {
+            $ref: 'http://localhost:8089/ok.json#/info',
+          },
+        };
+
+        const s = new Spectral({ resolver: httpAndFileResolver });
+
+        return expect(s.runWithResolved(doc)).resolves.toHaveProperty('resolved', {
+          info: {
+            title: '',
+            description: 'Foo',
+          },
+        });
+      });
+    });
+  });
+});
diff --git a/src/__tests__/spectral.jest.test.ts b/src/__tests__/spectral.jest.test.ts
index 0c2bef31..73bc3e8c 100644
--- a/src/__tests__/spectral.jest.test.ts
+++ b/src/__tests__/spectral.jest.test.ts
@@ -3,8 +3,8 @@ import { DiagnosticSeverity, Dictionary } from '@stoplight/types';
 import * as fs from 'fs';
 import * as nock from 'nock';
 import * as path from 'path';
+import { isOpenApiv2 } from '../formats';
 import { httpAndFileResolver } from '../resolvers/http-and-file';
-import { isOpenApiv2 } from '../rulesets/lookups';
 import { Spectral } from '../spectral';
 
 const oasRuleset = require('../rulesets/oas/index.json');
diff --git a/src/__tests__/spectral.test.ts b/src/__tests__/spectral.test.ts
index c2ddb094..1b29053d 100644
--- a/src/__tests__/spectral.test.ts
+++ b/src/__tests__/spectral.test.ts
@@ -1,7 +1,7 @@
-import { Cache } from '@stoplight/json-ref-resolver';
+import { getLocationForJsonPath, parseWithPointers } from '@stoplight/json';
 import { DiagnosticSeverity, Dictionary } from '@stoplight/types';
 import { isParsedResult, Spectral } from '../spectral';
-import { IParsedResult, RuleFunction } from '../types';
+import { IParsedResult, IResolver, RuleFunction } from '../types';
 
 const merge = require('lodash/merge');
 
@@ -125,23 +125,146 @@ describe('spectral', () => {
   });
 
   describe('when a $ref appears', () => {
-    test('will call the resolver with target', async () => {
-      const fakeResolver = {
-        resolve: jest.fn(() => Promise.resolve([])),
-      };
+    describe('and a custom resolver is provided', () => {
+      test('will call the resolver with target', async () => {
+        const customResolver: IResolver = {
+          resolve: jest.fn(async () => ({
+            result: {},
+            refMap: {},
+            errors: [],
+          })),
+        };
+
+        const s = new Spectral({
+          resolver: customResolver,
+        });
+
+        const target = { foo: 'bar' };
 
-      const s = new Spectral({
-        resolver: fakeResolver as any,
+        await s.run(target);
+
+        expect(customResolver.resolve).toBeCalledWith(target, {
+          authority: undefined,
+          parseResolveResult: expect.any(Function),
+        });
       });
 
-      const target = { foo: 'bar' };
+      test('should handle lack of information about $refs gracefully', () => {
+        const customResolver: IResolver = {
+          resolve: jest.fn(async () => ({
+            result: {
+              foo: {
+                bar: {
+                  baz: '',
+                },
+              },
+            },
+            refMap: {},
+            errors: [],
+          })),
+        };
+
+        const s = new Spectral({
+          resolver: customResolver,
+        });
 
-      await s.run(target);
+        s.setRules({
+          'truthy-baz': {
+            given: '$.foo.bar.baz',
+            message: 'Baz must be truthy',
+            severity: DiagnosticSeverity.Error,
+            recommended: true,
+            then: {
+              function: 'truthy',
+            },
+          },
+        });
 
-      expect(fakeResolver.resolve).toBeCalledWith(target, {
-        authority: undefined,
-        uriCache: expect.any(Cache),
-        parseResolveResult: expect.any(Function),
+        const target: IParsedResult = {
+          parsed: parseWithPointers(`{"foo":"bar"}`),
+          getLocationForJsonPath,
+          source: 'foo',
+        };
+
+        return expect(s.run(target)).resolves.toStrictEqual([
+          {
+            code: 'truthy-baz',
+            message: 'Baz must be truthy',
+            path: ['foo', 'bar', 'baz'],
+            range: {
+              end: {
+                character: 0,
+                line: 0,
+              },
+              start: {
+                character: 0,
+                line: 0,
+              },
+            },
+            severity: DiagnosticSeverity.Error,
+            source: void 0,
+          },
+        ]);
+      });
+
+      test('should recognize the source of local $refs', () => {
+        const s = new Spectral();
+        const source = 'foo.yaml';
+
+        const parsedResult: IParsedResult = {
+          getLocationForJsonPath,
+          source,
+          parsed: parseWithPointers(
+            JSON.stringify(
+              {
+                paths: {
+                  '/agreements': {
+                    get: {
+                      description: 'Get some Agreements',
+                      responses: {
+                        '200': {
+                          $ref: '#/responses/GetAgreementsOk',
+                        },
+                        default: {},
+                      },
+                      summary: 'List agreements',
+                      tags: ['agreements', 'pagination'],
+                    },
+                  },
+                },
+                responses: {
+                  GetAgreementsOk: {
+                    description: 'Successful operation',
+                    headers: {},
+                  },
+                },
+              },
+              null,
+              2,
+            ),
+          ),
+        };
+
+        s.setRules({
+          'pagination-responses-have-x-next-token': {
+            description: 'All collection endpoints have the X-Next-Token parameter in responses',
+            given: "$.paths..get.responses['200'].headers",
+            severity: 'error',
+            recommended: true,
+            then: { field: 'X-Next-Token', function: 'truthy' },
+          },
+        });
+
+        return expect(s.run(parsedResult)).resolves.toEqual([
+          {
+            code: 'pagination-responses-have-x-next-token',
+            message: 'All collection endpoints have the X-Next-Token parameter in responses',
+            path: ['paths', '/agreements', 'get', 'responses', '200', 'headers'],
+            range: expect.any(Object),
+            severity: DiagnosticSeverity.Error,
+            source,
+          },
+        ]);
       });
     });
   });
diff --git a/src/cli/commands/__tests__/lint.test.ts b/src/cli/commands/__tests__/lint.test.ts
index ff56b7f2..044d5b11 100644
--- a/src/cli/commands/__tests__/lint.test.ts
+++ b/src/cli/commands/__tests__/lint.test.ts
@@ -59,98 +59,100 @@ describe('lint', () => {
 
   it('shows help when no document argument is passed', async () => {
     const output = await run('lint');
-    expect(output).toContain('document  Location of a JSON/YAML document');
+    expect(output).toContain('documents  Location of JSON/YAML documents');
   });
 
   it('shows help when invalid arguments are passed', async () => {
     const output = await run('lint --foo');
-    expect(output).toContain('lint a JSON/YAML document from a file or URL');
+    expect(output).toContain('documents  Location of JSON/YAML documents. Can be either a file, a glob or');
   });
 
   it('calls lint with document and default options', async () => {
     const doc = './__fixtures__/empty-oas2-document.json';
-    const ruleset = undefined;
     await run(`lint ${doc}`);
-    expect(lint).toBeCalledWith(
-      doc,
-      {
-        encoding: 'utf8',
-        format: 'stylish',
-      },
-      ruleset,
-    );
+    expect(lint).toBeCalledWith([doc], {
+      encoding: 'utf8',
+      format: 'stylish',
+    });
   });
 
   it('calls lint with document and custom encoding', async () => {
     const doc = './__fixtures__/empty-oas2-document.json';
-    const ruleset = undefined;
     await run(`lint --encoding utf16 ${doc}`);
-    expect(lint).toBeCalledWith(
-      doc,
-      {
-        encoding: 'utf16',
-        format: 'stylish',
-      },
-      ruleset,
-    );
+    expect(lint).toBeCalledWith([doc], {
+      encoding: 'utf16',
+      format: 'stylish',
+    });
   });
 
   it('calls lint with document and custom encoding and format', async () => {
     const doc = './__fixtures__/empty-oas2-document.json';
-    const ruleset = undefined;
     await run(`lint -f json --encoding utf16 ${doc}`);
-    expect(lint).toBeCalledWith(
-      doc,
-      {
-        encoding: 'utf16',
-        format: 'json',
-      },
-      ruleset,
-    );
+    expect(lint).toBeCalledWith([doc], {
+      encoding: 'utf16',
+      format: 'json',
+    });
   });
 
   it('calls lint with document and custom ruleset', async () => {
     const doc = './__fixtures__/empty-oas2-document.json';
     const ruleset = 'custom-ruleset.json';
     await run(`lint -r ${ruleset} ${doc}`);
-    expect(lint).toBeCalledWith(doc, expect.any(Object), [ruleset]);
+    expect(lint).toBeCalledWith(
+      [doc],
+      expect.objectContaining({
+        ruleset: [ruleset],
+      }),
+    );
   });
 
   it('calls lint with document and multiple custom rulesets', async () => {
     const doc = './__fixtures__/empty-oas2-document.json';
     const ruleset = 'custom-ruleset.json';
     const ruleset2 = 'custom-ruleset-2.json';
-    await run(`lint -r ${ruleset} -r ${ruleset2} ${doc}`);
-    expect(lint).toBeCalledWith(doc, expect.any(Object), [ruleset, ruleset2]);
+    await run(`lint --r ${ruleset} -r ${ruleset2} ${doc}`);
+    expect(lint).toBeCalledWith(
+      [doc],
+      expect.objectContaining({
+        ruleset: [ruleset, ruleset2],
+      }),
+    );
   });
 
   it.each(['json', 'stylish'])('calls formatOutput with %s format', async format => {
     await run(`lint -f ${format} ./__fixtures__/empty-oas2-document.json`);
-    expect(formatOutput).toBeCalledWith(results, format);
+    // needed by Node 8 (different ticking?) - can be simplified once we drop support for version 8
+    await new Promise(resolve => {
+      setImmediate(() => {
+        expect(formatOutput).toBeCalledWith(results, format);
+        resolve();
+      });
+    });
   });
 
   it('writes formatted output to a file', async () => {
     await run(`lint -o foo.json ./__fixtures__/empty-oas2-document.json`);
-    expect(writeOutput).toBeCalledWith('<formatted output>', 'foo.json');
+    // needed by Node 8 (different ticking?) - can be simplified once we drop support for version 8
+    await new Promise(resolve => {
+      setImmediate(() => {
+        expect(writeOutput).toBeCalledWith('<formatted output>', 'foo.json');
+        resolve();
+      });
+    });
   });
 
   it('passes skip-rule to lint', async () => {
-    const ruleset = undefined;
     await run('lint --skip-rule foo --skip-rule bar ./__fixtures__/empty-oas2-document.json');
-    expect(lint).toHaveBeenCalledWith(
-      expect.any(String),
-      {
-        skipRule: ['foo', 'bar'],
-        encoding: 'utf8',
-        format: 'stylish',
-      },
-      ruleset,
-    );
+    expect(lint).toHaveBeenCalledWith([expect.any(String)], {
+      skipRule: ['foo', 'bar'],
+      encoding: 'utf8',
+      format: 'stylish',
+    });
   });
 
   it('shows help if unknown format is passed', () => {
     return expect(run('lint -f foo ./__fixtures__/empty-oas2-document.json')).resolves.toContain(
-      'lint a JSON/YAML document from a file or URL',
+      'documents  Location of JSON/YAML documents. Can be either a file, a glob or',
     );
   });
 
diff --git a/src/cli/services/__tests__/__fixtures__/draft-nested-ref.oas2.json b/src/cli/services/__tests__/__fixtures__/draft-nested-ref.oas2.json
index 441e923e..da6d298d 100644
--- a/src/cli/services/__tests__/__fixtures__/draft-nested-ref.oas2.json
+++ b/src/cli/services/__tests__/__fixtures__/draft-nested-ref.oas2.json
@@ -3,5 +3,10 @@
   "host": "d",
   "info": {
     "$ref": "./refs/info-ref.json"
+  },
+  "paths": {
+    "/test": {
+      "$ref": "./refs/paths.json#/paths/~1test"
+    }
   }
 }
diff --git a/src/cli/services/__tests__/__fixtures__/refs/paths.json b/src/cli/services/__tests__/__fixtures__/refs/paths.json
new file mode 100644
index 00000000..fceaaf76
--- /dev/null
+++ b/src/cli/services/__tests__/__fixtures__/refs/paths.json
@@ -0,0 +1,9 @@
+{
+  "paths": {
+    "/test": {
+      "get": {
+        "response": "bar"
+      }
+    }
+  }
+}
diff --git a/src/cli/services/__tests__/linter.test.ts b/src/cli/services/__tests__/linter.test.ts
index 1b518bb9..58c90962 100644
--- a/src/cli/services/__tests__/linter.test.ts
+++ b/src/cli/services/__tests__/linter.test.ts
@@ -1,4 +1,4 @@
-import { resolve } from '@stoplight/path';
+import { join, resolve } from '@stoplight/path';
 import * as nock from 'nock';
 import * as yargs from 'yargs';
 import { ValidationError } from '../../../rulesets/validation';
@@ -23,8 +23,8 @@ const invalidOas3SpecPath = resolve(__dirname, '__fixtures__/openapi-3.0-no-cont
 
 function run(command: string) {
   const parser = yargs.command(lintCommand);
-  const { document, ruleset, ...opts } = (parser.parse(command) as unknown) as ILintConfig & { document: string };
-  return lint(document, opts, ruleset);
+  const { documents, ...opts } = (parser.parse(command) as unknown) as ILintConfig & { documents: string[] };
+  return lint(documents, opts);
 }
 
 describe('Linter service', () => {
@@ -144,6 +144,228 @@ describe('Linter service', () => {
     });
   });
 
+  describe('when a list of files is provided', () => {
+    // cwd is set to root of Spectral
+    const documents = [
+      join(process.cwd(), `src/__tests__/__fixtures__/petstore.invalid-schema.oas3.json`),
+      join(process.cwd(), `src/__tests__/__fixtures__/petstore.oas3.json`),
+    ];
+
+    it('outputs issues for each file', () => {
+      return expect(run(['lint', ...documents].join(' '))).resolves.toEqual(
+        expect.arrayContaining([
+          expect.objectContaining({
+            code: 'invalid-ref',
+            path: ['paths', '/pets', 'get', 'responses', '200', 'content', 'application/json', 'schema', '$ref'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.invalid-schema.oas3.json'),
+          }),
+          expect.objectContaining({
+            code: 'invalid-ref',
+            path: ['paths', '/pets', 'get', 'responses', 'default', 'content', 'application/json', 'schema', '$ref'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.invalid-schema.oas3.json'),
+          }),
+          expect.objectContaining({
+            code: 'valid-example-in-schemas',
+            path: ['components', 'schemas', 'foo', 'example'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.invalid-schema.oas3.json'),
+          }),
+          expect.objectContaining({
+            code: 'info-contact',
+            path: ['info'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas3.json'),
+          }),
+          expect.objectContaining({
+            code: 'info-description',
+            path: ['info'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas3.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-description',
+            path: ['paths', '/pets', 'get'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas3.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-description',
+            path: ['paths', '/pets', 'post'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas3.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-description',
+            path: ['paths', '/pets/{petId}', 'get'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas3.json'),
+          }),
+        ]),
+      );
+    });
+  });
+
+  describe('when glob is provided', () => {
+    // cwd is set to root of Spectral
+    const documents = `src/__tests__/__fixtures__/petstore*.json`;
+
+    it('outputs issues for each file', () => {
+      return expect(run(`lint ${documents}`)).resolves.toEqual(
+        expect.arrayContaining([
+          expect.objectContaining({
+            code: 'invalid-ref',
+            path: ['paths', '/pets', 'get', 'responses', '200', 'content', 'application/json', 'schema', '$ref'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.invalid-schema.oas3.json'),
+          }),
+          expect.objectContaining({
+            code: 'invalid-ref',
+            path: ['paths', '/pets', 'get', 'responses', 'default', 'content', 'application/json', 'schema', '$ref'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.invalid-schema.oas3.json'),
+          }),
+          expect.objectContaining({
+            code: 'valid-example-in-schemas',
+            path: ['components', 'schemas', 'foo', 'example'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.invalid-schema.oas3.json'),
+          }),
+          expect.objectContaining({
+            code: 'oas3-schema',
+            path: ['paths', '/pets', 'get', 'responses', '200'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.invalid-schema.oas3.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-2xx-response',
+            path: ['paths', '/pet', 'post', 'responses'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas2.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-2xx-response',
+            path: ['paths', '/pet', 'put', 'responses'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas2.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-2xx-response',
+            path: ['paths', '/pet/{petId}', 'post', 'responses'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas2.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-2xx-response',
+            path: ['paths', '/pet/{petId}', 'delete', 'responses'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas2.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-2xx-response',
+            path: ['paths', '/store/order/{orderId}', 'delete', 'responses'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas2.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-2xx-response',
+            path: ['paths', '/user', 'post', 'responses'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas2.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-2xx-response',
+            path: ['paths', '/user/createWithArray', 'post', 'responses'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas2.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-2xx-response',
+            path: ['paths', '/user/createWithList', 'post', 'responses'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas2.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-2xx-response',
+            path: ['paths', '/user/logout', 'get', 'responses'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas2.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-2xx-response',
+            path: ['paths', '/user/{username}', 'put', 'responses'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas2.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-2xx-response',
+            path: ['paths', '/user/{username}', 'delete', 'responses'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas2.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-description',
+            path: ['paths', '/pet', 'post', 'description'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas2.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-description',
+            path: ['paths', '/pet', 'put', 'description'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas2.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-description',
+            path: ['paths', '/pet/{petId}', 'post', 'description'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas2.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-description',
+            path: ['paths', '/pet/{petId}', 'delete', 'description'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas2.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-description',
+            path: ['paths', '/pet/{petId}/uploadImage', 'post', 'description'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas2.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-description',
+            path: ['paths', '/store/order', 'post', 'description'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas2.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-description',
+            path: ['paths', '/user/createWithArray', 'post', 'description'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas2.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-description',
+            path: ['paths', '/user/createWithList', 'post', 'description'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas2.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-description',
+            path: ['paths', '/user/login', 'get', 'description'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas2.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-description',
+            path: ['paths', '/user/logout', 'get', 'description'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas2.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-description',
+            path: ['paths', '/user/{username}', 'get', 'description'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas2.json'),
+          }),
+          expect.objectContaining({
+            code: 'info-contact',
+            path: ['info'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas3.json'),
+          }),
+          expect.objectContaining({
+            code: 'info-description',
+            path: ['info'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas3.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-description',
+            path: ['paths', '/pets', 'get'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas3.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-description',
+            path: ['paths', '/pets', 'post'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas3.json'),
+          }),
+          expect.objectContaining({
+            code: 'operation-description',
+            path: ['paths', '/pets/{petId}', 'get'],
+            source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.oas3.json'),
+          }),
+        ]),
+      );
+    });
+  });
+
   describe('--ruleset', () => {
     describe('extends feature', () => {
       it('extends a valid relative ruleset', () => {
@@ -380,6 +602,54 @@ describe('Linter service', () => {
           },
           source: expect.stringContaining('__tests__/__fixtures__/refs/contact.json'),
         }),
+        expect.objectContaining({
+          code: 'operation-description',
+          message: 'Operation `description` must be present and non-empty string.',
+          path: ['paths', '/test', 'get'],
+          range: {
+            end: {
+              character: 7,
+              line: 5,
+            },
+            start: {
+              character: 13,
+              line: 3,
+            },
+          },
+          source: expect.stringContaining('__tests__/__fixtures__/refs/paths.json'),
+        }),
+        expect.objectContaining({
+          code: 'operation-operationId',
+          message: 'Operation should have an `operationId`.',
+          path: ['paths', '/test', 'get'],
+          range: {
+            end: {
+              character: 7,
+              line: 5,
+            },
+            start: {
+              character: 13,
+              line: 3,
+            },
+          },
+          source: expect.stringContaining('__tests__/__fixtures__/refs/paths.json'),
+        }),
+        expect.objectContaining({
+          code: 'operation-tags',
+          message: 'Operation should have non-empty `tags` array.',
+          path: ['paths', '/test', 'get'],
+          range: {
+            end: {
+              character: 7,
+              line: 5,
+            },
+            start: {
+              character: 13,
+              line: 3,
+            },
+          },
+          source: expect.stringContaining('__tests__/__fixtures__/refs/paths.json'),
+        }),
       ]);
     });
   });
diff --git a/src/formats/__tests__/json-schema.spec.ts b/src/formats/__tests__/json-schema.spec.ts
new file mode 100644
index 00000000..04a5bdaf
--- /dev/null
+++ b/src/formats/__tests__/json-schema.spec.ts
@@ -0,0 +1,234 @@
+import {
+  isJSONSchema,
+  isJSONSchemaDraft2019_09,
+  isJSONSchemaDraft4,
+  isJSONSchemaDraft6,
+  isJSONSchemaDraft7,
+  isJSONSchemaLoose,
+} from '../json-schema';
+
+describe('JSON Schema format', () => {
+  describe('JSON Schema strict', () => {
+    it.each([
+      'http://json-schema.org/schema#',
+      'https://json-schema.org/schema#',
+      'http://json-schema.org/hyper-schema#',
+      'http://json-schema.org/draft-04/schema#',
+      'http://json-schema.org/draft-04/hyper-schema#',
+      'http://json-schema.org/draft-06/schema#',
+      'http://json-schema.org/draft-06/hyper-schema#',
+    ])('recognizes %s schema correctly', $schema => {
+      expect(isJSONSchema({ $schema })).toBe(true);
+    });
+
+    it('does not recognize invalid document', () => {
+      expect(isJSONSchema({ $schema: '2.0' })).toBe(false);
+      expect(isJSONSchema({ $schema: 'json-schema' })).toBe(false);
+      expect(isJSONSchema({ $schema: 2 })).toBe(false);
+      expect(isJSONSchema({ swagger: null })).toBe(false);
+      expect(isJSONSchema({ allOf: [] })).toBe(false);
+      expect(isJSONSchema({ type: 'string' })).toBe(false);
+      expect(isJSONSchema({})).toBe(false);
+      expect(isJSONSchema(null)).toBe(false);
+    });
+  });
+
+  describe('JSON Schema loose', () => {
+    describe('by $schema', () => {
+      it.each([
+        'http://json-schema.org/schema#',
+        'https://json-schema.org/schema#',
+        'http://json-schema.org/hyper-schema#',
+        'http://json-schema.org/draft-04/schema#',
+        'http://json-schema.org/draft-04/hyper-schema#',
+        'http://json-schema.org/draft-06/schema#',
+        'http://json-schema.org/draft-06/hyper-schema#',
+      ])('recognizes %s schema correctly', $schema => {
+        expect(isJSONSchemaLoose({ $schema })).toBe(true);
+      });
+    });
+
+    describe('by type', () => {
+      it.each(['array', 'boolean', 'integer', 'null', 'number', 'object', 'string'])(
+        'recognizes %s type correctly',
+        type => {
+          expect(isJSONSchemaLoose({ type })).toBe(true);
+        },
+      );
+
+      it.each(['foo', 'bar', 1, 2, void 0, null])('does not recognize invalid %s type', type => {
+        expect(isJSONSchemaLoose({ type })).toBe(false);
+      });
+    });
+
+    describe('by combiner', () => {
+      it.each(['allOf', 'oneOf', 'anyOf'])('recognizes %s combiner correctly', combiner => {
+        expect(isJSONSchemaLoose({ [combiner]: [] })).toBe(true);
+      });
+
+      it.each(['allOf', 'oneOf', 'anyOf'])('does not %s combiner that is not an object', combiner => {
+        expect(isJSONSchemaLoose({ [combiner]: void 0 })).toBe(false);
+        expect(isJSONSchemaLoose({ [combiner]: 0 })).toBe(false);
+        expect(isJSONSchemaLoose({ [combiner]: '' })).toBe(false);
+        expect(isJSONSchemaLoose({ [combiner]: null })).toBe(false);
+      });
+    });
+
+    it('recognizes by the presence of "not"', () => {
+      expect(isJSONSchemaLoose({ not: {} })).toBe(true);
+    });
+
+    describe('mixed', () => {
+      it('invalid type but valid combiner', () => {
+        expect(isJSONSchemaLoose({ type: 'foo', allOf: [] })).toBe(true);
+      });
+
+      it('valid type but invalid combiner', () => {
+        expect(isJSONSchemaLoose({ type: 'string', allOf: null })).toBe(true);
+      });
+    });
+  });
+
+  describe('JSON Schema Draft 4', () => {
+    it.each([
+      'http://json-schema.org/draft-04/schema#',
+      'https://json-schema.org/draft-04/schema#',
+      'http://json-schema.org/draft-04/schema',
+      'https://json-schema.org/draft-04/schema',
+      'http://json-schema.org/draft-04/hyper-schema#',
+      'https://json-schema.org/draft-04/hyper-schema#',
+      'http://json-schema.org/draft-04/hyper-schema',
+      'https://json-schema.org/draft-04/hyper-schema',
+    ])('recognizes %s schema correctly', $schema => {
+      expect(isJSONSchemaDraft4({ $schema })).toBe(true);
+    });
+
+    it.each([
+      'http://json-schema.org/schema#',
+      'https://json-schema.org/schema#',
+      'http://json-schema.org/hyper-schema#',
+      'http://json-schema.org/draft-06/schema#',
+      'http://json-schema.org/draft-06/hyper-schema#',
+    ])('does not recognize %s schema', $schema => {
+      expect(isJSONSchemaDraft4({ $schema })).toBe(false);
+    });
+
+    it('does not recognize invalid document', () => {
+      expect(isJSONSchemaDraft4({ $schema: '2.0' })).toBe(false);
+      expect(isJSONSchemaDraft4({ $schema: 'json-schema' })).toBe(false);
+      expect(isJSONSchemaDraft4({ $schema: 2 })).toBe(false);
+      expect(isJSONSchemaDraft4({ swagger: null })).toBe(false);
+      expect(isJSONSchemaDraft4({ allOf: [] })).toBe(false);
+      expect(isJSONSchemaDraft4({ type: 'string' })).toBe(false);
+      expect(isJSONSchemaDraft4({})).toBe(false);
+      expect(isJSONSchemaDraft4(null)).toBe(false);
+    });
+  });
+  describe('JSON Schema Draft 6', () => {
+    it.each([
+      'http://json-schema.org/draft-06/schema#',
+      'https://json-schema.org/draft-06/schema#',
+      'http://json-schema.org/draft-06/schema',
+      'https://json-schema.org/draft-06/schema',
+      'http://json-schema.org/draft-06/hyper-schema#',
+      'https://json-schema.org/draft-06/hyper-schema#',
+      'http://json-schema.org/draft-06/hyper-schema',
+      'https://json-schema.org/draft-06/hyper-schema',
+    ])('recognizes %s schema correctly', $schema => {
+      expect(isJSONSchemaDraft6({ $schema })).toBe(true);
+    });
+
+    it.each([
+      'http://json-schema.org/schema#',
+      'https://json-schema.org/schema#',
+      'http://json-schema.org/hyper-schema#',
+      'http://json-schema.org/draft-04/schema#',
+      'http://json-schema.org/draft-04/hyper-schema#',
+    ])('does not recognize %s schema', $schema => {
+      expect(isJSONSchemaDraft6({ $schema })).toBe(false);
+    });
+
+    it('does not recognize invalid document', () => {
+      expect(isJSONSchemaDraft6({ $schema: '2.0' })).toBe(false);
+      expect(isJSONSchemaDraft6({ $schema: 'json-schema' })).toBe(false);
+      expect(isJSONSchemaDraft6({ $schema: 2 })).toBe(false);
+      expect(isJSONSchemaDraft6({ swagger: null })).toBe(false);
+      expect(isJSONSchemaDraft6({ allOf: [] })).toBe(false);
+      expect(isJSONSchemaDraft6({ type: 'string' })).toBe(false);
+      expect(isJSONSchemaDraft6({})).toBe(false);
+      expect(isJSONSchemaDraft6(null)).toBe(false);
+    });
+  });
+
+  describe('JSON Schema Draft 7', () => {
+    it.each([
+      'http://json-schema.org/draft-07/schema#',
+      'https://json-schema.org/draft-07/schema#',
+      'http://json-schema.org/draft-07/schema',
+      'https://json-schema.org/draft-07/schema',
+      'http://json-schema.org/draft-07/hyper-schema#',
+      'https://json-schema.org/draft-07/hyper-schema#',
+      'http://json-schema.org/draft-07/hyper-schema',
+      'https://json-schema.org/draft-07/hyper-schema',
+    ])('recognizes %s schema correctly', $schema => {
+      expect(isJSONSchemaDraft7({ $schema })).toBe(true);
+    });
+
+    it.each([
+      'http://json-schema.org/schema#',
+      'https://json-schema.org/schema#',
+      'http://json-schema.org/hyper-schema#',
+      'http://json-schema.org/draft-06/schema#',
+      'http://json-schema.org/draft-06/hyper-schema#',
+    ])('does not recognize %s schema', $schema => {
+      expect(isJSONSchemaDraft7({ $schema })).toBe(false);
+    });
+
+    it('does not recognize invalid document', () => {
+      expect(isJSONSchemaDraft7({ $schema: '2.0' })).toBe(false);
+      expect(isJSONSchemaDraft7({ $schema: 'json-schema' })).toBe(false);
+      expect(isJSONSchemaDraft7({ $schema: 2 })).toBe(false);
+      expect(isJSONSchemaDraft7({ swagger: null })).toBe(false);
+      expect(isJSONSchemaDraft7({ allOf: [] })).toBe(false);
+      expect(isJSONSchemaDraft7({ type: 'string' })).toBe(false);
+      expect(isJSONSchemaDraft7({})).toBe(false);
+      expect(isJSONSchemaDraft7(null)).toBe(false);
+    });
+  });
+
+  describe('JSON Schema Draft 2019-09', () => {
+    it.each([
+      'http://json-schema.org/draft/2019-09/schema#',
+      'https://json-schema.org/draft/2019-09/schema#',
+      'http://json-schema.org/draft/2019-09/schema',
+      'https://json-schema.org/draft/2019-09/schema',
+      'http://json-schema.org/draft/2019-09/hyper-schema#',
+      'https://json-schema.org/draft/2019-09/hyper-schema#',
+      'http://json-schema.org/draft/2019-09/hyper-schema',
+      'https://json-schema.org/draft/2019-09/hyper-schema',
+    ])('recognizes %s schema correctly', $schema => {
+      expect(isJSONSchemaDraft2019_09({ $schema })).toBe(true);
+    });
+
+    it.each([
+      'http://json-schema.org/schema#',
+      'https://json-schema.org/schema#',
+      'http://json-schema.org/hyper-schema#',
+      'http://json-schema.org/draft-06/schema#',
+      'http://json-schema.org/draft-06/hyper-schema#',
+    ])('does not recognize %s schema', $schema => {
+      expect(isJSONSchemaDraft2019_09({ $schema })).toBe(false);
+    });
+
+    it('does not recognize invalid document', () => {
+      expect(isJSONSchemaDraft2019_09({ $schema: '2.0' })).toBe(false);
+      expect(isJSONSchemaDraft2019_09({ $schema: 'json-schema' })).toBe(false);
+      expect(isJSONSchemaDraft2019_09({ $schema: 2 })).toBe(false);
+      expect(isJSONSchemaDraft2019_09({ swagger: null })).toBe(false);
+      expect(isJSONSchemaDraft2019_09({ allOf: [] })).toBe(false);
+      expect(isJSONSchemaDraft2019_09({ type: 'string' })).toBe(false);
+      expect(isJSONSchemaDraft2019_09({})).toBe(false);
+      expect(isJSONSchemaDraft2019_09(null)).toBe(false);
+    });
+  });
+});
diff --git a/src/rulesets/__tests__/lookups.spec.ts b/src/formats/__tests__/openapi.spec.ts
similarity index 94%
rename from src/rulesets/__tests__/lookups.spec.ts
rename to src/formats/__tests__/openapi.spec.ts
index 6bf9491c..fb275473 100644
--- a/src/rulesets/__tests__/lookups.spec.ts
+++ b/src/formats/__tests__/openapi.spec.ts
@@ -1,6 +1,6 @@
-import { isOpenApiv2, isOpenApiv3, isOpenApiv3_1 } from '../lookups';
+import { isOpenApiv2, isOpenApiv3, isOpenApiv3_1 } from '../openapi';
 
-describe('Format lookups', () => {
+describe('OpenAPI format', () => {
   describe('OpenAPI 2.0 aka Swagger', () => {
     it.each(['2.0.0', '2', '2.0'])('recognizes %s version correctly', version => {
       expect(isOpenApiv2({ swagger: version })).toBe(true);
diff --git a/src/formatters/__tests__/__fixtures__/mixed-errors-with-different-paths.json b/src/formatters/__tests__/__fixtures__/mixed-errors-with-different-paths.json
new file mode 100644
index 00000000..017825b9
--- /dev/null
+++ b/src/formatters/__tests__/__fixtures__/mixed-errors-with-different-paths.json
@@ -0,0 +1,79 @@
+[
+  {
+    "code": "info-contact",
+    "path": [],
+    "message": "Info object should contain `contact` object.",
+    "severity": 3,
+    "range": {
+      "start": {
+        "line": 2,
+        "character": 9
+      },
+      "end": {
+        "line": 6,
+        "character": 19
+      }
+    },
+    "source": "/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json"
+  },
+  {
+    "code": "info-description",
+    "path": [
+      ""
+    ],
+    "message": "OpenAPI object info `description` must be present and non-empty string.",
+    "severity": 1,
+    "range": {
+      "start": {
+        "line": 2,
+        "character": 9
+      },
+      "end": {
+        "line": 6,
+        "character": 19
+      }
+    },
+    "source": "/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json"
+  },
+  {
+    "code": "operation-description",
+    "path": [
+      "paths",
+      "/pets",
+      "post"
+    ],
+    "message": "Operation `description` must be present and non-empty string.",
+    "severity": 2,
+    "range": {
+      "start": {
+        "line": 63,
+        "character": 13
+      },
+      "end": {
+        "line": 76,
+        "character": 54
+      }
+    },
+    "source": "/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json"
+  },
+  {
+    "code": "info-matches-stoplight",
+    "path": [
+      "info",
+      "title"
+    ],
+    "message": "Info must contain Stoplight",
+    "severity": 0,
+    "range": {
+      "start": {
+        "line": 4,
+        "character": 13
+      },
+      "end": {
+        "line": 4,
+        "character": 31
+      }
+    },
+    "source": "/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json"
+  }
+]
diff --git a/src/formatters/__tests__/__fixtures__/mixed-errors.json b/src/formatters/__tests__/__fixtures__/mixed-errors.json
new file mode 100644
index 00000000..f9b9b31a
--- /dev/null
+++ b/src/formatters/__tests__/__fixtures__/mixed-errors.json
@@ -0,0 +1,123 @@
+[
+  {
+    "code": "info-contact",
+    "path": [
+      "info"
+    ],
+    "message": "Info object should contain `contact` object.",
+    "severity": 3,
+    "range": {
+      "start": {
+        "line": 2,
+        "character": 9
+      },
+      "end": {
+        "line": 6,
+        "character": 19
+      }
+    },
+    "source": "/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json"
+  },
+  {
+    "code": "info-description",
+    "path": [
+      "info"
+    ],
+    "message": "OpenAPI object info `description` must be present and non-empty string.",
+    "severity": 1,
+    "range": {
+      "start": {
+        "line": 2,
+        "character": 9
+      },
+      "end": {
+        "line": 6,
+        "character": 19
+      }
+    },
+    "source": "/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json"
+  },
+  {
+    "code": "operation-description",
+    "path": [
+      "paths",
+      "/pets",
+      "get"
+    ],
+    "message": "Operation `description` must be present and non-empty string.",
+    "severity": 2,
+    "range": {
+      "start": {
+        "line": 16,
+        "character": 12
+      },
+      "end": {
+        "line": 56,
+        "character": 54
+      }
+    },
+    "source": "/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json"
+  },
+  {
+    "code": "operation-description",
+    "path": [
+      "paths",
+      "/pets",
+      "post"
+    ],
+    "message": "Operation `description` must be present and non-empty string.",
+    "severity": 2,
+    "range": {
+      "start": {
+        "line": 63,
+        "character": 13
+      },
+      "end": {
+        "line": 76,
+        "character": 54
+      }
+    },
+    "source": "/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json"
+  },
+  {
+    "code": "operation-description",
+    "path": [
+      "paths",
+      "/pets/{petId}",
+      "get"
+    ],
+    "message": "Operation `description` must be present and non-empty string.",
+    "severity": 2,
+    "range": {
+      "start": {
+        "line": 85,
+        "character": 12
+      },
+      "end": {
+        "line": 116,
+        "character": 54
+      }
+    },
+    "source": "/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json"
+  },
+  {
+    "code": "info-matches-stoplight",
+    "path": [
+      "info",
+      "title"
+    ],
+    "message": "Info must contain Stoplight",
+    "severity": 0,
+    "range": {
+      "start": {
+        "line": 4,
+        "character": 13
+      },
+      "end": {
+        "line": 4,
+        "character": 31
+      }
+    },
+    "source": "/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json"
+  }
+]
diff --git a/src/formatters/__tests__/__fixtures__/oas3-schema-errors.json b/src/formatters/__tests__/__fixtures__/oas3-schema-errors.json
new file mode 100644
index 00000000..c43eaf08
--- /dev/null
+++ b/src/formatters/__tests__/__fixtures__/oas3-schema-errors.json
@@ -0,0 +1,77 @@
+[
+  {
+    "code": "oas3-schema",
+    "message": "should NOT have additional properties: type",
+    "path": [
+      "paths",
+      "/pets",
+      "get",
+      "responses",
+      "200",
+      "headers",
+      "header-1"
+    ],
+    "severity": 0,
+    "source": "/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.invalid-schema.oas3.yaml",
+    "range": {
+      "start": {
+        "line": 35,
+        "character": 21
+      },
+      "end": {
+        "line": 37,
+        "character": 21
+      }
+    }
+  },
+  {
+    "code": "oas3-schema",
+    "message": "should match exactly one schema in oneOf",
+    "path": [
+      "paths",
+      "/pets",
+      "get",
+      "responses",
+      "200",
+      "headers",
+      "header-1"
+    ],
+    "severity": 0,
+    "source": "/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.invalid-schema.oas3.yaml",
+    "range": {
+      "start": {
+        "line": 35,
+        "character": 21
+      },
+      "end": {
+        "line": 37,
+        "character": 21
+      }
+    }
+  },
+  {
+    "code": "oas3-schema",
+    "message": "should have required property '$ref'",
+    "path": [
+      "paths",
+      "/pets",
+      "get",
+      "responses",
+      "200",
+      "headers",
+      "header-1"
+    ],
+    "severity": 0,
+    "source": "/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.invalid-schema.oas3.yaml",
+    "range": {
+      "start": {
+        "line": 35,
+        "character": 21
+      },
+      "end": {
+        "line": 37,
+        "character": 21
+      }
+    }
+  }
+]
diff --git a/src/formatters/__tests__/junit.test.ts b/src/formatters/__tests__/junit.test.ts
new file mode 100644
index 00000000..1019b62a
--- /dev/null
+++ b/src/formatters/__tests__/junit.test.ts
@@ -0,0 +1,122 @@
+import { promisify } from 'util';
+import { Parser } from 'xml2js';
+import { junit } from '../junit';
+
+const oas3SchemaErrors = require('./__fixtures__/oas3-schema-errors.json');
+const mixedErrors = require('./__fixtures__/mixed-errors-with-different-paths.json');
+
+describe('JUnit formatter', () => {
+  let parse: Parser['parseString'];
+
+  beforeEach(() => {
+    const parser = new Parser();
+    parse = promisify(parser.parseString.bind(parser));
+  });
+
+  test('should produce valid report', async () => {
+    const result = await parse(junit(oas3SchemaErrors));
+    expect(result).toEqual({
+      testsuites: {
+        testsuite: [
+          {
+            $: {
+              errors: '0',
+              failures: '3',
+              name: '/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.invalid-schema.oas3.yaml',
+              package: 'org.spectral',
+              tests: '3',
+              time: '0',
+            },
+            testcase: [
+              {
+                $: {
+                  classname: '/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.invalid-schema.oas3',
+                  name: 'org.spectral.oas3-schema',
+                  time: '0',
+                },
+                failure: [
+                  {
+                    $: {
+                      message: 'should NOT have additional properties: type',
+                    },
+                    _:
+                      'line 36, col 22, should NOT have additional properties: type (oas3-schema) at path #/paths/~1pets/get/responses/200/headers/header-1',
+                  },
+                ],
+              },
+              {
+                $: {
+                  classname: '/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.invalid-schema.oas3',
+                  name: 'org.spectral.oas3-schema',
+                  time: '0',
+                },
+                failure: [
+                  {
+                    $: {
+                      message: 'should match exactly one schema in oneOf',
+                    },
+                    _:
+                      'line 36, col 22, should match exactly one schema in oneOf (oas3-schema) at path #/paths/~1pets/get/responses/200/headers/header-1',
+                  },
+                ],
+              },
+              {
+                $: {
+                  classname: '/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.invalid-schema.oas3',
+                  name: 'org.spectral.oas3-schema',
+                  time: '0',
+                },
+                failure: [
+                  {
+                    $: {
+                      message: "should have required property '$ref'",
+                    },
+                    _:
+                      'line 36, col 22, should have required property &apos;$ref&apos; (oas3-schema) at path #/paths/~1pets/get/responses/200/headers/header-1',
+                  },
+                ],
+              },
+            ],
+          },
+        ],
+      },
+    });
+  });
+
+  test('should filter out non-error validation results', async () => {
+    const result = await parse(junit(mixedErrors));
+    expect(result).toEqual({
+      testsuites: {
+        testsuite: [
+          {
+            $: {
+              errors: '0',
+              failures: '1',
+              name: '/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json',
+              package: 'org.spectral',
+              tests: '1',
+              time: '0',
+            },
+            testcase: [
+              {
+                $: {
+                  classname: '/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3',
+                  name: 'org.spectral.info-matches-stoplight',
+                  time: '0',
+                },
+                failure: [
+                  {
+                    $: {
+                      message: 'Info must contain Stoplight',
+                    },
+                    _: 'line 5, col 14, Info must contain Stoplight (info-matches-stoplight) at path #/info/title',
+                  },
+                ],
+              },
+            ],
+          },
+        ],
+      },
+    });
+  });
+});
diff --git a/src/formatters/__tests__/stylish.test.ts b/src/formatters/__tests__/stylish.test.ts
index 79796c23..fcf507d1 100644
--- a/src/formatters/__tests__/stylish.test.ts
+++ b/src/formatters/__tests__/stylish.test.ts
@@ -1,67 +1,33 @@
+import chalk from 'chalk';
 import { stylish } from '../stylish';
 
-const results = [
-  {
-    code: 'oas3-schema',
-    summary: 'Validate structure of OpenAPIv3 specification.',
-    message: 'should NOT have additional properties: type',
-    path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1'],
-    severity: 0,
-    source: '/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.invalid-schema.oas3.yaml',
-    range: {
-      start: {
-        line: 35,
-        character: 21,
-      },
-      end: {
-        line: 37,
-        character: 21,
-      },
-    },
-  },
-  {
-    code: 'oas3-schema',
-    summary: 'Validate structure of OpenAPIv3 specification.',
-    message: 'should match exactly one schema in oneOf',
-    path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1'],
-    severity: 0,
-    source: '/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.invalid-schema.oas3.yaml',
-    range: {
-      start: {
-        line: 35,
-        character: 21,
-      },
-      end: {
-        line: 37,
-        character: 21,
-      },
-    },
-  },
-  {
-    code: 'oas3-schema',
-    summary: 'Validate structure of OpenAPIv3 specification.',
-    message: "should have required property '$ref'",
-    path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1'],
-    severity: 0,
-    source: '/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.invalid-schema.oas3.yaml',
-    range: {
-      start: {
-        line: 35,
-        character: 21,
-      },
-      end: {
-        line: 37,
-        character: 21,
-      },
-    },
-  },
-];
+const oas3SchemaErrors = require('./__fixtures__/oas3-schema-errors.json');
+const mixedErrors = require('./__fixtures__/mixed-errors.json');
 
 describe('Stylish formatter', () => {
   test('should prefer message for oas-schema errors', () => {
-    const result = stylish(results);
+    const result = stylish(oas3SchemaErrors);
     expect(result).toContain('oas3-schema  should NOT have additional properties: type\n');
     expect(result).toContain('oas3-schema  should match exactly one schema in oneOf');
     expect(result).toContain("oas3-schema  should have required property '$ref'\n");
   });
+
+  test('should display proper severity level', () => {
+    const result = stylish(mixedErrors);
+    expect(result).toContain(`
+  3:10         ${chalk.white('hint')}  info-contact            Info object should contain \`contact\` object.
+  3:10      ${chalk.yellow(
+    'warning',
+  )}  info-description        OpenAPI object info \`description\` must be present and non-empty string.
+  5:14        ${chalk.red('error')}  info-matches-stoplight  Info must contain Stoplight
+ 17:13  ${chalk.blue(
+   'information',
+ )}  operation-description   Operation \`description\` must be present and non-empty string.
+ 64:14  ${chalk.blue(
+   'information',
+ )}  operation-description   Operation \`description\` must be present and non-empty string.
+ 86:13  ${chalk.blue(
+   'information',
+ )}  operation-description   Operation \`description\` must be present and non-empty string.`);
+  });
 });
diff --git a/src/functions/__tests__/alphabetical.test.ts b/src/functions/__tests__/alphabetical.test.ts
index ce5d1104..bafe2493 100644
--- a/src/functions/__tests__/alphabetical.test.ts
+++ b/src/functions/__tests__/alphabetical.test.ts
@@ -21,7 +21,8 @@ describe('alphabetical', () => {
       }),
     ).toEqual([
       {
-        message: 'properties are not in alphabetical order',
+        message: 'at least 2 properties are not in alphabetical order: "c" should be placed after "b"',
+        path: ['$', 'c'],
       },
     ]);
   });
@@ -30,7 +31,8 @@ describe('alphabetical', () => {
     test('given an unsorted array of strings should return error', () => {
       expect(runAlphabetical(['b', 'a'])).toEqual([
         {
-          message: 'properties are not in alphabetical order',
+          message: 'at least 2 properties are not in alphabetical order: "b" should be placed after "a"',
+          path: ['$', 0],
         },
       ]);
     });
@@ -42,15 +44,8 @@ describe('alphabetical', () => {
     test('given an unsorted array of numbers should return error', () => {
       expect(runAlphabetical([10, 1])).toEqual([
         {
-          message: 'properties are not in alphabetical order',
-        },
-      ]);
-    });
-
-    test('given an unsorted array of numbers should return error', () => {
-      expect(runAlphabetical([10, 1])).toEqual([
-        {
-          message: 'properties are not in alphabetical order',
+          message: 'at least 2 properties are not in alphabetical order: "10" should be placed after "1"',
+          path: ['$', 0],
         },
       ]);
     });
diff --git a/src/functions/__tests__/schema.test.ts b/src/functions/__tests__/schema.test.ts
index 1bb63a9a..8a59e2fd 100644
--- a/src/functions/__tests__/schema.test.ts
+++ b/src/functions/__tests__/schema.test.ts
@@ -6,6 +6,60 @@ function runSchema(target: any, schemaObj: object) {
 }
 
 describe('schema', () => {
+  describe('validates falsy values such as', () => {
+    test('empty string', () => {
+      const testSchema: JSONSchema6 = {
+        type: 'number',
+      };
+
+      expect(runSchema('', testSchema)).toEqual([
+        {
+          message: 'type should be number',
+          path: [],
+        },
+      ]);
+    });
+
+    test('zero', () => {
+      const testSchema: JSONSchema6 = {
+        type: 'string',
+      };
+
+      expect(runSchema(0, testSchema)).toEqual([
+        {
+          message: 'type should be string',
+          path: [],
+        },
+      ]);
+    });
+
+    test('false', () => {
+      const testSchema: JSONSchema6 = {
+        type: 'string',
+      };
+
+      expect(runSchema(false, testSchema)).toEqual([
+        {
+          message: 'type should be string',
+          path: [],
+        },
+      ]);
+    });
+
+    test('null', () => {
+      const testSchema: JSONSchema6 = {
+        type: 'string',
+      };
+
+      expect(runSchema(null, testSchema)).toEqual([
+        {
+          message: 'type should be string',
+          path: [],
+        },
+      ]);
+    });
+  });
+
   describe('when schema defines unknown format', () => {
     const testSchema = {
       type: 'string',
@@ -144,7 +198,7 @@ describe('schema', () => {
     });
   });
 
-  describe('handles duplicate JSONSchema4 ids', () => {
+  test('handles duplicate JSONSchema Draft 4 ids', () => {
     const testSchema: JSONSchema4 = {
       id: 'test',
       type: 'string',
@@ -164,7 +218,7 @@ describe('schema', () => {
     expect(runSchema('a', testSchema2)).toEqual([]);
   });
 
-  describe('handles duplicate JSONSchema6 ids', () => {
+  test('handles duplicate JSONSchema Draft 6 and 7 $ids', () => {
     const testSchema: JSONSchema6 = {
       $id: 'test',
       type: 'string',
@@ -183,4 +237,82 @@ describe('schema', () => {
     ]);
     expect(runSchema('a', testSchema2)).toEqual([]);
   });
+
+  test.each([4, 6, 7])('accepts draft %d', draft => {
+    const testSchema: JSONSchema6 = {
+      $schema: `http://json-schema.org/draft-0${draft}/schema#`,
+      type: 'string',
+    };
+
+    expect(runSchema.bind(null, 'd', testSchema)).not.toThrow();
+  });
+
+  describe('given a primitive value', () => {
+    describe('and an enum consisting of string values', () => {
+      const testSchema: JSONSchema6 = {
+        $schema: `http://json-schema.org/draft-06/schema#`,
+        type: 'string',
+        enum: ['foo', 'bar'],
+      };
+
+      it('reports pretty enum errors for a string', () => {
+        expect(runSchema('baz', testSchema)).toEqual([
+          {
+            message: 'should be equal to one of the allowed values: foo, bar. Did you mean bar?',
+            path: [],
+          },
+        ]);
+      });
+
+      it('reports pretty enum errors for a number', () => {
+        expect(runSchema(2, testSchema)).toEqual([
+          {
+            message: 'type should be string',
+            path: [],
+          },
+        ]);
+      });
+    });
+
+    describe('and an enum consisting of integer values', () => {
+      const testSchema: JSONSchema6 = {
+        $schema: `http://json-schema.org/draft-06/schema#`,
+        type: 'integer',
+        enum: [1, 3, 5, 10, 12],
+      };
+
+      it('reports pretty enum errors for a string', () => {
+        expect(runSchema('baz', testSchema)).toEqual([
+          {
+            message: 'type should be integer',
+            path: [],
+          },
+        ]);
+      });
+
+      it('reports pretty enum errors for a number', () => {
+        expect(runSchema(2, testSchema)).toEqual([
+          {
+            message: 'should be equal to one of the allowed values: 1, 3, 5, 10, 12',
+            path: [],
+          },
+        ]);
+      });
+    });
+  });
+
+  test('reports slightly less pretty enum errors for primitive values that are not similar to any values in enum', () => {
+    const testSchema: JSONSchema6 = {
+      $schema: `http://json-schema.org/draft-06/schema#`,
+      type: 'string',
+      enum: ['foo', 'bar'],
+    };
+
+    expect(runSchema('three', testSchema)).toEqual([
+      {
+        message: 'should be equal to one of the allowed values: foo, bar',
+        path: [],
+      },
+    ]);
+  });
 });
diff --git a/src/rulesets/__tests__/reader.jest.test.ts b/src/rulesets/__tests__/reader.jest.test.ts
index 603dd0e0..ebb724e7 100644
--- a/src/rulesets/__tests__/reader.jest.test.ts
+++ b/src/rulesets/__tests__/reader.jest.test.ts
@@ -485,6 +485,10 @@ describe('Rulesets reader', () => {
   });
 
   it('should not fail if function cannot be loaded', () => {
+    nock('https://unpkg.com')
+      .get('/boo.js')
+      .reply(404);
+
     return expect(readRuleset(rulesetWithMissingFunctions)).resolves.toEqual({
       rules: {},
       functions: {},
diff --git a/src/rulesets/oas/__tests__/parameter-description.ts b/src/rulesets/__tests__/shared/_parameter-description.ts
similarity index 73%
rename from src/rulesets/oas/__tests__/parameter-description.ts
rename to src/rulesets/__tests__/shared/_parameter-description.ts
index 37de60f4..2b0170e7 100644
--- a/src/rulesets/oas/__tests__/parameter-description.ts
+++ b/src/rulesets/__tests__/shared/_parameter-description.ts
@@ -1,16 +1,7 @@
-import { RuleType, Spectral } from '../../../spectral';
-import * as ruleset from '../index.json';
-
-describe('parameter-description', () => {
-  const s = new Spectral();
-
-  s.setRules({
-    'parameter-description': Object.assign(ruleset.rules['parameter-description'], {
-      recommended: true,
-      type: RuleType[ruleset.rules['parameter-description'].type],
-    }),
-  });
+import { DiagnosticSeverity } from '@stoplight/types';
+import { Spectral } from '../../../spectral';
 
+export default (s: Spectral, oasVersion: number) => {
   test('should work for shared level parameters', async () => {
     const results = await s.run({
       swagger: '2.0',
@@ -66,27 +57,6 @@ describe('parameter-description', () => {
     expect(results.length).toEqual(0);
   });
 
-  test('return errors if shared level parameter description is missing', async () => {
-    const results = await s.run({
-      swagger: '2.0',
-      parameters: {
-        limit: {
-          name: 'limit',
-          in: 'query',
-          type: 'integer',
-        },
-      },
-    });
-    expect(results).toEqual([
-      expect.objectContaining({
-        code: 'parameter-description',
-        message: 'Parameter objects should have a `description`.',
-        path: ['parameters', 'limit'],
-        severity: 1,
-      }),
-    ]);
-  });
-
   test('return errors if top level path parameter description is missing', async () => {
     const results = await s.run({
       swagger: '2.0',
@@ -104,10 +74,10 @@ describe('parameter-description', () => {
     });
     expect(results).toEqual([
       expect.objectContaining({
-        code: 'parameter-description',
+        code: `oas${oasVersion}-parameter-description`,
         message: 'Parameter objects should have a `description`.',
         path: ['paths', '/todos', 'parameters', '0'],
-        severity: 1,
+        severity: DiagnosticSeverity.Warning,
       }),
     ]);
   });
@@ -131,7 +101,7 @@ describe('parameter-description', () => {
     });
     expect(results).toEqual([
       expect.objectContaining({
-        code: 'parameter-description',
+        code: `oas${oasVersion}-parameter-description`,
         message: 'Parameter objects should have a `description`.',
         path: ['paths', '/todos', 'get', 'parameters', '0'],
         severity: 1,
@@ -155,4 +125,4 @@ describe('parameter-description', () => {
       }),
     ).not.rejects;
   });
-});
+};
diff --git a/src/rulesets/oas/__tests__/message.spec.ts b/src/rulesets/oas/__tests__/message.spec.ts
index 352f7e75..c235ea77 100644
--- a/src/rulesets/oas/__tests__/message.spec.ts
+++ b/src/rulesets/oas/__tests__/message.spec.ts
@@ -7,6 +7,8 @@ describe('message util', () => {
       message(template, {
         property: 'description',
         error: 'expected property to be truthy',
+        path: '',
+        value: void 0,
       }),
     ).toEqual('oops... "description" is missing;error: expected property to be truthy');
   });
@@ -17,6 +19,8 @@ describe('message util', () => {
       message(template, {
         property: 'baz',
         error: 'foo',
+        path: '',
+        value: void 0,
       }),
     ).toEqual('foofoobazfoofoo');
   });
@@ -27,6 +31,8 @@ describe('message util', () => {
       message(template, {
         property: 'description',
         error: 'expected property to be truthy',
+        path: '',
+        value: void 0,
       }),
     ).toEqual('missing :(');
   });
diff --git a/src/rulesets/oas/functions/__tests__/oasOp2xxResponse.test.ts b/src/rulesets/oas/functions/__tests__/oasOp2xxResponse.test.ts
index 4e5a3e8c..1523672a 100644
--- a/src/rulesets/oas/functions/__tests__/oasOp2xxResponse.test.ts
+++ b/src/rulesets/oas/functions/__tests__/oasOp2xxResponse.test.ts
@@ -102,4 +102,20 @@ describe('oasOp2xxResponse', () => {
       },
     ]);
   });
+
+  test('does not complain when no $.responses property', async () => {
+    const results = await s.run({
+      paths: {
+        '/test': {
+          get: {
+            operationId: '123',
+          },
+          post: {
+            operationId: '123',
+          },
+        },
+      },
+    });
+    expect(results).toEqual([]);
+  });
 });
diff --git a/src/rulesets/oas/functions/__tests__/oasPathParam.test.ts b/src/rulesets/oas/functions/__tests__/oasPathParam.test.ts
index 4dae5813..ba8c02bc 100644
--- a/src/rulesets/oas/functions/__tests__/oasPathParam.test.ts
+++ b/src/rulesets/oas/functions/__tests__/oasPathParam.test.ts
@@ -35,8 +35,7 @@ describe('oasPathParam', () => {
     expect(results).toEqual([
       {
         code: 'path-params',
-        message:
-          'The path "**/foo/{bar}**" uses a parameter "**{bar}**" that does not have a corresponding definition.\n\nTo fix, add a path parameter with the name "**bar**".',
+        message: 'The path `/foo/{bar}` uses a parameter `{bar}` that does not have a corresponding definition.',
         path: ['paths', '/foo/{bar}'],
         range: {
           end: {
@@ -131,9 +130,7 @@ describe('oasPathParam', () => {
     expect(results).toEqual([
       {
         code: 'path-params',
-        message: `The path "**/foo/{bar}/{bar}**" uses the parameter "**{bar}**" multiple times.\n
-Path parameters must be unique.\n
-To fix, update the path so that all parameter names are unique.`,
+        message: `The path \`/foo/{bar}/{bar}\` uses the parameter \`{bar}\` multiple times. Path parameters must be unique.`,
         path: ['paths', '/foo/{bar}/{bar}'],
         range: {
           end: {
@@ -174,7 +171,7 @@ To fix, update the path so that all parameter names are unique.`,
     expect(results).toEqual([
       {
         code: 'path-params',
-        message: `Path parameter \"**bar**\" must have a \`required\` that is set to \`true\`.\n\nTo fix, mark this parameter as required.`,
+        message: `Path parameter \`bar\` must have a \`required\` property that is set to \`true\`.`,
         path: ['paths', '/foo/{bar}', 'parameters'],
         range: {
           end: {
@@ -220,7 +217,7 @@ To fix, update the path so that all parameter names are unique.`,
     expect(results).toEqual([
       {
         code: 'path-params',
-        message: `The paths \"**/foo/{boo}**\" and \"**/foo/{bar}**\" are equivalent.\n\nTo fix, remove one of the paths or merge them together.`,
+        message: `The paths \`/foo/{boo}\` and \`/foo/{bar}\` are equivalent.`,
         path: ['paths'],
         range: {
           end: {
diff --git a/src/rulesets/oas2/__tests__/oas2-parameter-description.ts b/src/rulesets/oas2/__tests__/oas2-parameter-description.ts
new file mode 100644
index 00000000..1187c2e4
--- /dev/null
+++ b/src/rulesets/oas2/__tests__/oas2-parameter-description.ts
@@ -0,0 +1,38 @@
+import { DiagnosticSeverity } from '@stoplight/types';
+import { RuleType, Spectral } from '../../../spectral';
+import testParameterDescription from '../../__tests__/shared/_parameter-description';
+import * as ruleset from '../index.json';
+
+describe('oas2-parameter-description', () => {
+  const s = new Spectral();
+
+  s.setRules({
+    'oas2-parameter-description': Object.assign(ruleset.rules['oas2-parameter-description'], {
+      recommended: true,
+      type: RuleType[ruleset.rules['oas2-parameter-description'].type],
+    }),
+  });
+
+  testParameterDescription(s, 2);
+
+  test('return errors if shared level parameter description is missing', async () => {
+    const results = await s.run({
+      swagger: '2.0',
+      parameters: {
+        limit: {
+          name: 'limit',
+          in: 'query',
+          type: 'integer',
+        },
+      },
+    });
+    expect(results).toEqual([
+      expect.objectContaining({
+        code: 'oas2-parameter-description',
+        message: 'Parameter objects should have a `description`.',
+        path: ['parameters', 'limit'],
+        severity: DiagnosticSeverity.Warning,
+      }),
+    ]);
+  });
+});
diff --git a/src/rulesets/oas3/__tests__/oas3-parameter-description.ts b/src/rulesets/oas3/__tests__/oas3-parameter-description.ts
new file mode 100644
index 00000000..908b622b
--- /dev/null
+++ b/src/rulesets/oas3/__tests__/oas3-parameter-description.ts
@@ -0,0 +1,95 @@
+import { DiagnosticSeverity } from '@stoplight/types';
+import { RuleType, Spectral } from '../../../spectral';
+import testParameterDescription from '../../__tests__/shared/_parameter-description';
+import * as ruleset from '../index.json';
+
+describe('oas3-parameter-description', () => {
+  const s = new Spectral();
+
+  s.setRules({
+    'oas3-parameter-description': Object.assign(ruleset.rules['oas3-parameter-description'], {
+      recommended: true,
+      type: RuleType[ruleset.rules['oas3-parameter-description'].type],
+    }),
+  });
+
+  testParameterDescription(s, 3);
+
+  describe('$.components.parameters', () => {
+    it('validates description', async () => {
+      const results = await s.run({
+        openapi: '3.0.2',
+        components: {
+          parameters: {
+            address: {
+              in: 'body',
+            },
+          },
+        },
+      });
+
+      expect(results).toEqual([
+        expect.objectContaining({
+          code: 'oas3-parameter-description',
+          message: 'Parameter objects should have a `description`.',
+          path: ['components', 'parameters', 'address'],
+          severity: DiagnosticSeverity.Warning,
+        }),
+      ]);
+    });
+  });
+
+  describe('description for parameters in links', () => {
+    describe('$.components.links', () => {
+      it('does not validate description', async () => {
+        const results = await s.run({
+          openapi: '3.0.2',
+          components: {
+            links: {
+              address: {
+                operationId: 'getUserAddressByUUID',
+                parameters: {
+                  param: {
+                    value: 'value',
+                    in: 'header',
+                  },
+                },
+              },
+            },
+          },
+        });
+
+        expect(results).toEqual([]);
+      });
+    });
+
+    describe('links in a response', () => {
+      it('does not validate description', async () => {
+        const results = await s.run({
+          paths: {
+            '/pets': {
+              get: {
+                responses: {
+                  '200': {
+                    links: {
+                      abc: {
+                        parameters: {
+                          param: {
+                            in: 'body',
+                            val: 2,
+                          },
+                        },
+                      },
+                    },
+                  },
+                },
+              },
+            },
+          },
+        });
+
+        expect(results).toEqual([]);
+      });
+    });
+  });
+});
diff --git a/src/utils/__tests__/hasIntersectingElement.spec.ts b/src/utils/__tests__/hasIntersectingElement.spec.ts
new file mode 100644
index 00000000..7e8aa49e
--- /dev/null
+++ b/src/utils/__tests__/hasIntersectingElement.spec.ts
@@ -0,0 +1,26 @@
+import { hasIntersectingElement } from '../hasIntersectingElement';
+
+describe('hasIntersectingElement util', () => {
+  it('returns true if intersecting element is found', () => {
+    expect(hasIntersectingElement([1, 0], [0])).toBe(true);
+    expect(hasIntersectingElement(['a', 'b', 'c', 'd'], ['c', 'a', 'b', 'c'])).toBe(true);
+    const obj = {};
+    expect(hasIntersectingElement([{}, obj], [obj])).toBe(true);
+    expect(hasIntersectingElement([NaN], [NaN])).toBe(true);
+  });
+
+  it('returns false if intersecting element cannot be found', () => {
+    expect(hasIntersectingElement([1, 0], [2])).toBe(false);
+    expect(hasIntersectingElement(['a', 'b', 'c', 'd'], ['e', 'f', 'g'])).toBe(false);
+    expect(hasIntersectingElement([{}], [{}])).toBe(false);
+  });
+
+  it('returns false if any set is empty', () => {
+    expect(hasIntersectingElement([0], [])).toBe(false);
+    expect(hasIntersectingElement([], [1])).toBe(false);
+  });
+
+  it('returns false for empty sets', () => {
+    expect(hasIntersectingElement([], [])).toBe(false);
+  });
+});
diff --git a/test-harness/README.md b/test-harness/README.md
index 9b3a3880..81fa1c64 100644
--- a/test-harness/README.md
+++ b/test-harness/README.md
@@ -40,18 +40,32 @@ lint --foo {document} --bar
 expected output
 ====stderr====
 optional/alternative to stdout for expected errors
+====status====
+(optional) expected exit code (`0` or `1`)
 ```
 
 ### A real example?
 
 ```
 ====test====
-Hello
+scenario-example
 ====document====
 openapi: 3.0.2
+servers:
+  - url: "localhost"
+info:
+  title: "my api"
+  description: "An example"
+  contact:
+    name: "Stoplight"
+  version: "1.0"
 paths:
   /todos:
     get:
+      tags:
+        - "example"
+      description: "Example endpoint"
+      operationId: getTodos
       responses:
         200:
           description: Get Todo Items
@@ -59,19 +73,23 @@ paths:
             'application/json':
               example: hello
 ====command====
-lint {document} | grep "something"
+lint {document}
 ====stdout====
+OpenAPI 3.x detected
+
+{document}
  2:6  warning  some-rule  This rule is complaining about something.
+
+✖ 1 problems (1 error, 0 warning, 0 infos, 0 hints)
 ```
 
 #### Things to keep in mind when creating the files:
 
 * 1 test per file, we do not support multiple splitting.
-* Be precise with the separators. They shuold be 4 *before* **AND** *after* the word. `====`
-* The 4 keywords are `test,spec,server,command,expect,expect-loose`, nothing else at the moment
+* Be precise with the separators. They should be 4 *before* **AND** *after* the word. `====`
+* The keywords are `test`, `document`, `command`, `status`, `stdout`, and `stderr`, nothing else at the moment
 * You can run all the tests on the same port `4010`, but you can also choose another one
-* The `curl` command does not support piping stuff into other tools; so if you're trying to be cool and do `curl | grep`, well maybe next time.
-* All the `curl` commands **must** have the `-i` flag, otherwise the trace parser won't understand the output
+* You can pipe your command to grep. For example, using `lint {document} | grep 'expected-rule-name'` can be used to test for the presence of a specific violation. But, beware of being overly specific here.
 
 ## Technical details
 
diff --git a/test-harness/helpers.ts b/test-harness/helpers.ts
index b75561e6..527c6796 100644
--- a/test-harness/helpers.ts
+++ b/test-harness/helpers.ts
@@ -1,18 +1,33 @@
 export function parseScenarioFile(data: string) {
-  const regex = /====(test|document|command|stdout|stderr)====\r?\n/gi;
+  const regex = /====(test|document|command|status|stdout|stderr|env)====\r?\n/gi;
   const split = data.split(regex);
 
   const testIndex = split.findIndex(t => t === 'test');
   const documentIndex = split.findIndex(t => t === 'document');
   const commandIndex = split.findIndex(t => t === 'command');
+  const statusIndex = split.findIndex(t => t === 'status');
   const stdoutIndex = split.findIndex(t => t === 'stdout');
   const stderrIndex = split.findIndex(t => t === 'stderr');
+  const envIndex = split.findIndex(t => t === 'env');
 
   return {
     test: split[1 + testIndex],
     document: split[1 + documentIndex],
     command: split[1 + commandIndex],
+    status: split[1 + statusIndex],
     stdout: split[1 + stdoutIndex],
     stderr: split[1 + stderrIndex],
+    env: envIndex === -1 ? process.env : getEnv(split[1 + envIndex]),
   };
 }
+
+function getEnv(env: string): NodeJS.ProcessEnv {
+  return env.split(/\r?\n/).reduce(
+    (envs, line) => {
+      const [key, value = ''] = line.split('=');
+      envs[key] = value;
+      return envs;
+    },
+    { ...process.env },
+  );
+}
diff --git a/test-harness/index.ts b/test-harness/index.ts
index 3c8baf84..8ce384a4 100644
--- a/test-harness/index.ts
+++ b/test-harness/index.ts
@@ -10,7 +10,7 @@ import { parseScenarioFile } from './helpers';
 
 const glob = globFs({ gitignore: true });
 
-const spectralBin = path.join(__dirname, '../binaries/spectral-linux');
+const spectralBin = path.join(__dirname, '../binaries/spectral');
 
 type Replacement = {
   from: RegExp;
@@ -21,13 +21,12 @@ function replaceVars(string: string, replacements: Replacement[]) {
   return replacements.reduce((str, replace) => str.replace(replace.from, replace.to), string);
 }
 
-describe('cli e2e tests', () => {
-  const files = process.env.TESTS
-    ? String(process.env.TESTS).split(',')
-    : glob.readdirSync('**/*.scenario', { cwd: path.join(__dirname, './scenarios') });
+describe('cli acceptance tests', () => {
+  const cwd = path.join(__dirname, './scenarios');
+  const files = process.env.TESTS ? String(process.env.TESTS).split(',') : glob.readdirSync('**/*.scenario', { cwd });
 
   files.forEach((file: string) => {
-    const data = fs.readFileSync(path.join(__dirname, './scenarios/', file), { encoding: 'utf8' });
+    const data = fs.readFileSync(path.join(cwd, file), { encoding: 'utf8' });
     const scenario = parseScenarioFile(data);
     const replacements: Replacement[] = [];
 
@@ -50,6 +49,11 @@ describe('cli e2e tests', () => {
           to: tmpFileHandle.name,
         });
 
+        replacements.push({
+          from: /\{documentWithoutExt\}/g,
+          to: tmpFileHandle.name.replace(/\.yml$/, ''),
+        });
+
         fs.writeFileSync(tmpFileHandle.name, scenario.document, { encoding: 'utf8' });
       }
     });
@@ -60,7 +64,7 @@ describe('cli e2e tests', () => {
       }
     });
 
-    test(`${file}${os.EOL}${scenario.test}`, () => {
+    test(`./test-harness/scenarios/${file}${os.EOL}${scenario.test}`, () => {
       // TODO split on " " is going to break quoted args
       const args = scenario.command.split(' ').map(t => {
         const arg = t.trim();
@@ -74,12 +78,15 @@ describe('cli e2e tests', () => {
         shell: true,
         encoding: 'utf8',
         windowsVerbatimArguments: false,
+        env: scenario.env,
       });
 
+      const expectedStatus = replaceVars(scenario.status.trim(), replacements);
+      const expectedStdout = replaceVars(scenario.stdout.trim(), replacements);
+      const expectedStderr = replaceVars(scenario.stderr.trim(), replacements);
+      const status = commandHandle.status;
       const stderr = commandHandle.stderr.trim();
       const stdout = commandHandle.stdout.trim();
-      const expectedStderr = replaceVars(scenario.stderr.trim(), replacements);
-      const expectedStdout = replaceVars(scenario.stdout.trim(), replacements);
 
       if (expectedStderr) {
         expect(stderr).toEqual(expectedStderr);
@@ -87,9 +94,13 @@ describe('cli e2e tests', () => {
         throw new Error(stderr);
       }
 
-      if (stdout) {
+      if (stdout || expectedStdout) {
         expect(stdout).toEqual(expectedStdout);
       }
+
+      if (expectedStatus !== '') {
+        expect(`status:${status}`).toEqual(`status:${expectedStatus}`);
+      }
     });
   });
 });
diff --git a/test-harness/scenarios/enabled-rules-amount.oas3.scenario b/test-harness/scenarios/enabled-rules-amount.oas3.scenario
new file mode 100644
index 00000000..8052a526
--- /dev/null
+++ b/test-harness/scenarios/enabled-rules-amount.oas3.scenario
@@ -0,0 +1,36 @@
+====test====
+The amount of enabled rules is printed in a parenthesis.
+====document====
+openapi: 3.0.2
+paths:
+  /pets:
+    parameters:
+      - name: abc
+        in: body
+    get:
+      parameters:
+        - name: ok
+          in: header
+      responses:
+        200:
+          description: abc
+components:
+  parameters:
+    skipParam:
+      name: skip
+      in: query
+      schema:
+        type: integer
+====command====
+lint {document} --ruleset ./test-harness/scenarios/rulesets/parameter-description.oas3.yaml -v
+====stdout====
+Found 52 rules (1 enabled)
+Linting {document}
+OpenAPI 3.x detected
+
+{document}
+  5:9   warning  oas3-parameter-description  Parameter objects should have a `description`.
+  9:11  warning  oas3-parameter-description  Parameter objects should have a `description`.
+ 16:15  warning  oas3-parameter-description  Parameter objects should have a `description`.
+
+✖ 3 problems (0 errors, 3 warnings, 0 infos, 0 hints)
diff --git a/test-harness/scenarios/external-schemas-ruleset.scenario b/test-harness/scenarios/external-schemas-ruleset.scenario
new file mode 100644
index 00000000..2b0d1415
--- /dev/null
+++ b/test-harness/scenarios/external-schemas-ruleset.scenario
@@ -0,0 +1,17 @@
+====test====
+Schemas referenced via $refs are resolved and used
+====document====
+openapi: 3.0.2
+info:
+  title: foo
+  description: baz
+====command====
+lint {document} --ruleset ./test-harness/scenarios/rulesets/external-schemas-ruleset.yaml
+====stdout====
+OpenAPI 3.x detected
+
+{document}
+ 3:10  error  info-title        should be equal to one of the allowed values: Stoplight, Stoplight.io, StoplightIO. Did you mean Stoplight?
+ 4:16  error  info-description  should be equal to one of the allowed values: foo, foo-bar, bar-foo
+
+✖ 2 problems (2 errors, 0 warnings, 0 infos, 0 hints)
diff --git a/test-harness/scenarios/help-no-document.scenario b/test-harness/scenarios/help-no-document.scenario
index 57635fc6..c3527e63 100644
--- a/test-harness/scenarios/help-no-document.scenario
+++ b/test-harness/scenarios/help-no-document.scenario
@@ -3,20 +3,22 @@ Errors when no document is provided
 ====command====
 lint
 ====stderr====
-spectral lint <document>
+spectral lint <documents..>
 
-lint a JSON/YAML document from a file or URL
+lint JSON/YAML documents from files or URLs
 
 Positionals:
-  document  Location of a JSON/YAML document. Can be either a file or a fetchable resource on the web.  [string] [required]
+  documents  Location of JSON/YAML documents. Can be either a file, a glob or fetchable resource(s) on the web.  [array] [required] [default: []]
 
 Options:
-  --version        Show version number  [boolean]
-  --help           Show help  [boolean]
-  --encoding, -e   text encoding to use  [string] [default: "utf8"]
-  --format, -f     formatter to use for outputting results  [string] [default: "stylish"]
-  --output, -o     output to a file instead of stdout  [string]
-  --ruleset, -r    path/URL to a ruleset file  [string]
-  --skip-rule, -s  ignore certain rules if they are causing trouble  [string]
-  --verbose, -v    increase verbosity  [boolean]
-  --quiet, -q      no logging - output only  [boolean]
\ No newline at end of file
+  --version                    Show version number  [boolean]
+  --help                       Show help  [boolean]
+  --encoding, -e               text encoding to use  [string] [default: "utf8"]
+  --format, -f                 formatter to use for outputting results  [string] [default: "stylish"]
+  --output, -o                 output to a file instead of stdout  [string]
+  --ruleset, -r                path/URL to a ruleset file  [string]
+  --skip-rule, -s              ignore certain rules if they are causing trouble  [string]
+  --fail-severity, -F          results of this level or above will trigger a failure exit code  [string] [choices: "error", "warn", "info", "hint"] [default: "hint"]
+  --display-only-failures, -D  only output results equal to or greater than --fail-severity  [boolean] [default: false]
+  --verbose, -v                increase verbosity  [boolean]
+  --quiet, -q                  no logging - output only  [boolean]
diff --git a/test-harness/scenarios/parameter-description-links.oas3.scenario b/test-harness/scenarios/parameter-description-links.oas3.scenario
new file mode 100644
index 00000000..ef440516
--- /dev/null
+++ b/test-harness/scenarios/parameter-description-links.oas3.scenario
@@ -0,0 +1,28 @@
+====test====
+Parameters in links are not validated to have description.
+====document====
+openapi: 3.0.2
+paths:
+  /pets:
+    get:
+      responses:
+        200:
+          description: Expected response to a valid request
+          links:
+            abc:
+              parameters:
+                xyz: 123
+                abc:
+                  in: header
+                  val: abc
+components:
+  links:
+    address:
+      operationId: getUserAddressByUUID
+      parameters:
+        userUuid: abc
+====command====
+lint --ruleset ./test-harness/scenarios/rulesets/parameter-description.oas3.yaml {document}
+====stdout====
+OpenAPI 3.x detected
+No results with a severity of 'hint' or higher found!
diff --git a/test-harness/scenarios/parameter-description-parameters.oas2.scenario b/test-harness/scenarios/parameter-description-parameters.oas2.scenario
new file mode 100644
index 00000000..048fbfc3
--- /dev/null
+++ b/test-harness/scenarios/parameter-description-parameters.oas2.scenario
@@ -0,0 +1,47 @@
+====test====
+Parameters - Parameter Objects - are validated to have description.
+====document====
+swagger: "2.0"
+paths:
+  /pets:
+    parameters:
+      - name: abc
+        type: string
+        in: header
+      - name: xyz
+        type: string
+        in: header
+        description: xyz
+    get:
+      parameters:
+        - name: ok
+          type: string
+          in: query
+        - name: abc
+          type: string
+          in: query
+          description: abc
+      responses:
+        200:
+          description: abc
+parameters:
+  skipParam:
+    name: skip
+    in: query
+    type: boolean
+  status:
+    name: status
+    in: query
+    type: string
+    description: status
+====command====
+lint {document} --ruleset ./test-harness/scenarios/rulesets/parameter-description.oas2.yaml
+====stdout====
+OpenAPI 2.0 (Swagger) detected
+
+{document}
+  5:9   warning  oas2-parameter-description  Parameter objects should have a `description`.
+ 14:11  warning  oas2-parameter-description  Parameter objects should have a `description`.
+ 25:13  warning  oas2-parameter-description  Parameter objects should have a `description`.
+
+✖ 3 problems (0 errors, 3 warnings, 0 infos, 0 hints)
diff --git a/test-harness/scenarios/parameter-description-parameters.oas3.scenario b/test-harness/scenarios/parameter-description-parameters.oas3.scenario
new file mode 100644
index 00000000..b387376e
--- /dev/null
+++ b/test-harness/scenarios/parameter-description-parameters.oas3.scenario
@@ -0,0 +1,34 @@
+====test====
+Parameters - Parameter Objects - are validated to have description.
+====document====
+openapi: 3.0.2
+paths:
+  /pets:
+    parameters:
+      - name: abc
+        in: body
+    get:
+      parameters:
+        - name: ok
+          in: header
+      responses:
+        200:
+          description: abc
+components:
+  parameters:
+    skipParam:
+      name: skip
+      in: query
+      schema:
+        type: integer
+====command====
+lint {document} --ruleset ./test-harness/scenarios/rulesets/parameter-description.oas3.yaml
+====stdout====
+OpenAPI 3.x detected
+
+{document}
+  5:9   warning  oas3-parameter-description  Parameter objects should have a `description`.
+  9:11  warning  oas3-parameter-description  Parameter objects should have a `description`.
+ 16:15  warning  oas3-parameter-description  Parameter objects should have a `description`.
+
+✖ 3 problems (0 errors, 3 warnings, 0 infos, 0 hints)
diff --git a/test-harness/scenarios/proxy-agent.scenario b/test-harness/scenarios/proxy-agent.scenario
new file mode 100644
index 00000000..3906ad30
--- /dev/null
+++ b/test-harness/scenarios/proxy-agent.scenario
@@ -0,0 +1,15 @@
+====test====
+Requests for $refs are proxied when PROXY env variable is set
+====document====
+foo:
+  $ref: http://localhost:3002/foo.json#/ref
+====env====
+PROXY=http://localhost:3001
+====command====
+lint {document}
+====stdout====
+
+{document}
+ 2:9  error  invalid-ref  FetchError: request to http://localhost:3002/foo.json failed, reason: connect ECONNREFUSED 127.0.0.1:3001
+
+✖ 1 problem (1 error, 0 warnings, 0 infos, 0 hints)
diff --git a/test-harness/scenarios/results-default-output.oas3.scenario b/test-harness/scenarios/results-default-output.oas3.scenario
index 59916507..29b34533 100644
--- a/test-harness/scenarios/results-default-output.oas3.scenario
+++ b/test-harness/scenarios/results-default-output.oas3.scenario
@@ -15,4 +15,4 @@ lint {document} --output=./test-harness/tmp/results-default-output.txt > /dev/nu
  2:6  warning  info-contact      Info object should contain `contact` object.
  2:6  warning  info-description  OpenAPI object info `description` must be present and non-empty string.
 
-✖ 3 problems (0 errors, 3 warnings, 0 infos)
\ No newline at end of file
+✖ 3 problems (0 errors, 3 warnings, 0 infos, 0 hints)
diff --git a/test-harness/scenarios/results-default.oas3.scenario b/test-harness/scenarios/results-default.oas3.scenario
index 370c7393..e77c7d12 100644
--- a/test-harness/scenarios/results-default.oas3.scenario
+++ b/test-harness/scenarios/results-default.oas3.scenario
@@ -16,4 +16,4 @@ OpenAPI 3.x detected
  2:6  warning  info-contact      Info object should contain `contact` object.
  2:6  warning  info-description  OpenAPI object info `description` must be present and non-empty string.
 
-✖ 3 problems (0 errors, 3 warnings, 0 infos)
\ No newline at end of file
+✖ 3 problems (0 errors, 3 warnings, 0 infos, 0 hints)
diff --git a/test-harness/scenarios/results-format-junit.oas3.scenario b/test-harness/scenarios/results-format-junit.oas3.scenario
new file mode 100644
index 00000000..948dc69e
--- /dev/null
+++ b/test-harness/scenarios/results-format-junit.oas3.scenario
@@ -0,0 +1,19 @@
+====test====
+Invalid OAS3 document outputs results when --format=junit
+====document====
+openapi: 3.0.0
+info:
+  version: 1.0.0
+  title: Stoplight
+  foo:
+paths: {}
+====command====
+lint {document} --format=junit
+====stdout====
+OpenAPI 3.x detected
+<?xml version="1.0" encoding="utf-8"?>
+<testsuites>
+<testsuite package="org.spectral" time="0" tests="1" errors="0" failures="1" name="{document}">
+<testcase time="0" name="org.spectral.oas3-schema" classname="{documentWithoutExt}"><failure message="/info Property foo is not expected to be here"><![CDATA[line 2, col 6, /info Property foo is not expected to be here (oas3-schema) at path #/info]]></failure></testcase>
+</testsuite>
+</testsuites>
diff --git a/test-harness/scenarios/results-format-stylish.oas3.scenario b/test-harness/scenarios/results-format-stylish.oas3.scenario
index 2c6cf814..480367a7 100644
--- a/test-harness/scenarios/results-format-stylish.oas3.scenario
+++ b/test-harness/scenarios/results-format-stylish.oas3.scenario
@@ -16,4 +16,4 @@ OpenAPI 3.x detected
  2:6  warning  info-contact      Info object should contain `contact` object.
  2:6  warning  info-description  OpenAPI object info `description` must be present and non-empty string.
 
-✖ 3 problems (0 errors, 3 warnings, 0 infos)
\ No newline at end of file
+✖ 3 problems (0 errors, 3 warnings, 0 infos, 0 hints)
diff --git a/test-harness/scenarios/results-skip-rule.oas3.scenario b/test-harness/scenarios/results-skip-rule.oas3.scenario
index 55bd5b35..8b60c124 100644
--- a/test-harness/scenarios/results-skip-rule.oas3.scenario
+++ b/test-harness/scenarios/results-skip-rule.oas3.scenario
@@ -15,4 +15,4 @@ OpenAPI 3.x detected
  1:1  warning  api-servers       OpenAPI `servers` must be present and non-empty array.
  2:6  warning  info-description  OpenAPI object info `description` must be present and non-empty string.
 
-✖ 2 problems (0 errors, 2 warnings, 0 infos)
\ No newline at end of file
+✖ 2 problems (0 errors, 2 warnings, 0 infos, 0 hints)
diff --git a/test-harness/scenarios/results-skip-rules-multiple.oas3.scenario b/test-harness/scenarios/results-skip-rules-multiple.oas3.scenario
index 4ac8e7a9..2024a99f 100644
--- a/test-harness/scenarios/results-skip-rules-multiple.oas3.scenario
+++ b/test-harness/scenarios/results-skip-rules-multiple.oas3.scenario
@@ -14,4 +14,4 @@ OpenAPI 3.x detected
 {document}
  1:1  warning  api-servers  OpenAPI `servers` must be present and non-empty array.
 
-✖ 1 problem (0 errors, 1 warning, 0 infos)
\ No newline at end of file
+✖ 1 problem (0 errors, 1 warning, 0 infos, 0 hints)
diff --git a/test-harness/scenarios/rulesets/external-schemas-ruleset.yaml b/test-harness/scenarios/rulesets/external-schemas-ruleset.yaml
new file mode 100644
index 00000000..f9324e4b
--- /dev/null
+++ b/test-harness/scenarios/rulesets/external-schemas-ruleset.yaml
@@ -0,0 +1,23 @@
+rules:
+  info-description:
+    message: "{{error}}"
+    severity: error
+    given: $.info.description
+    type: style
+    recommended: true
+    then:
+      function: schema # note, this rule could use pattern function, but for test purposes I made the rule use schema function
+      functionOptions:
+        schema:
+          $ref: ./schemas/foo-description.yaml#
+  info-title:
+    message: "{{error}}"
+    severity: error
+    given: $.info.title
+    type: style
+    recommended: true
+    then:
+      function: schema # note, as above, this rule could use pattern function, but for test purposes I made the rule use schema function
+      functionOptions:
+        schema:
+          $ref: ./schemas/stoplight-title.yaml#
diff --git a/test-harness/scenarios/rulesets/loose-schema-ruleset.json b/test-harness/scenarios/rulesets/loose-schema-ruleset.json
new file mode 100644
index 00000000..d527e94f
--- /dev/null
+++ b/test-harness/scenarios/rulesets/loose-schema-ruleset.json
@@ -0,0 +1,27 @@
+{
+  "formats": ["json-schema-loose"],
+  "rules": {
+    "no-root-all-of": {
+      "description": "No root allOf combiner",
+      "message": "{{description}}",
+      "recommended": true,
+      "severity": "error",
+      "type": "validation",
+      "given": "$",
+      "then": {
+        "field": "allOf",
+        "function": "falsy"
+      }
+    },
+    "no-empty-description": {
+      "description": "Description must not be empty",
+      "message": "{{description}}",
+      "recommended": true,
+      "type": "validation",
+      "given": "$..description",
+      "then": {
+        "function": "truthy"
+      }
+    }
+  }
+}
diff --git a/test-harness/scenarios/rulesets/oas-mixed-severity.json b/test-harness/scenarios/rulesets/oas-mixed-severity.json
new file mode 100644
index 00000000..244a278b
--- /dev/null
+++ b/test-harness/scenarios/rulesets/oas-mixed-severity.json
@@ -0,0 +1,21 @@
+{
+  "extends": ["spectral:oas"],
+  "rules": {
+    "info-contact": "error",
+    "operation-description": "info",
+    "info-matches-stoplight": {
+      "message": "Info must contain Stoplight",
+      "given": "$.info",
+      "severity": "hint",
+      "type": "style",
+      "recommended": true,
+      "then": {
+        "field": "title",
+        "function": "pattern",
+        "functionOptions": {
+          "match": "Stoplight"
+        }
+      }
+    }
+  }
+}
diff --git a/test-harness/scenarios/rulesets/parameter-description.oas2.yaml b/test-harness/scenarios/rulesets/parameter-description.oas2.yaml
new file mode 100644
index 00000000..dc82fa3a
--- /dev/null
+++ b/test-harness/scenarios/rulesets/parameter-description.oas2.yaml
@@ -0,0 +1,3 @@
+extends: [[spectral:oas2, off]]
+rules:
+  oas2-parameter-description: true
diff --git a/test-harness/scenarios/rulesets/parameter-description.oas3.yaml b/test-harness/scenarios/rulesets/parameter-description.oas3.yaml
new file mode 100644
index 00000000..dbc1f0fd
--- /dev/null
+++ b/test-harness/scenarios/rulesets/parameter-description.oas3.yaml
@@ -0,0 +1,3 @@
+extends: [[spectral:oas3, off]]
+rules:
+  oas3-parameter-description: true
diff --git a/test-harness/scenarios/rulesets/schemas/foo-description.yaml b/test-harness/scenarios/rulesets/schemas/foo-description.yaml
new file mode 100644
index 00000000..f5fe075e
--- /dev/null
+++ b/test-harness/scenarios/rulesets/schemas/foo-description.yaml
@@ -0,0 +1,6 @@
+$schema: http://json-schema.org/draft-07/schema#
+type: string
+enum:
+  - foo
+  - foo-bar
+  - bar-foo
diff --git a/test-harness/scenarios/rulesets/schemas/stoplight-title.yaml b/test-harness/scenarios/rulesets/schemas/stoplight-title.yaml
new file mode 100644
index 00000000..8bdabebb
--- /dev/null
+++ b/test-harness/scenarios/rulesets/schemas/stoplight-title.yaml
@@ -0,0 +1,6 @@
+$schema: http://json-schema.org/draft-06/schema#
+type: string
+enum:
+  - Stoplight
+  - Stoplight.io
+  - StoplightIO
diff --git a/test-harness/scenarios/severity/display-errors.oas3.scenario b/test-harness/scenarios/severity/display-errors.oas3.scenario
new file mode 100644
index 00000000..9faa2210
--- /dev/null
+++ b/test-harness/scenarios/severity/display-errors.oas3.scenario
@@ -0,0 +1,47 @@
+====test====
+Request only errors be shown, but no errors exist
+====document====
+openapi: '3.0.0'
+info:
+  version: 1.0.0
+  title: Swagger Petstore
+  license:
+    name: MIT
+paths:
+  /pets/{petId}:
+    get:
+      summary: Info for a specific pet
+      operationId: showPetById
+      tags:
+        - pets
+      parameters:
+        - name: petId
+          in: path
+          required: true
+          description: The id of the pet to retrieve
+          schema:
+            type: string
+      responses:
+        '200':
+          description: Expected response to a valid request
+          content:
+            application/json:
+              schema:
+                required:
+                  - id
+                  - name
+                properties:
+                  id:
+                    type: integer
+                    format: int64
+                  name:
+                    type: string
+                  tag:
+                    type: string
+====command====
+lint {document} --fail-severity=error -D
+====status====
+0
+====stdout====
+OpenAPI 3.x detected
+No results with a severity of 'error' or higher found!
diff --git a/test-harness/scenarios/severity/display-warnings.oas3.scenario b/test-harness/scenarios/severity/display-warnings.oas3.scenario
new file mode 100644
index 00000000..d752660a
--- /dev/null
+++ b/test-harness/scenarios/severity/display-warnings.oas3.scenario
@@ -0,0 +1,55 @@
+====test====
+Fail severity is set to error but only warnings exist, 
+so status should be success and output should show warnings
+====document====
+openapi: '3.0.0'
+info:
+  version: 1.0.0
+  title: Swagger Petstore
+  license:
+    name: MIT
+paths:
+  /pets/{petId}:
+    get:
+      summary: Info for a specific pet
+      operationId: showPetById
+      tags:
+        - pets
+      parameters:
+        - name: petId
+          in: path
+          required: true
+          description: The id of the pet to retrieve
+          schema:
+            type: string
+      responses:
+        '200':
+          description: Expected response to a valid request
+          content:
+            application/json:
+              schema:
+                required:
+                  - id
+                  - name
+                properties:
+                  id:
+                    type: integer
+                    format: int64
+                  name:
+                    type: string
+                  tag:
+                    type: string
+====command====
+lint {document} --fail-severity=error
+====status====
+0
+====stdout====
+OpenAPI 3.x detected
+
+{document}
+ 1:1  warning  api-servers            OpenAPI `servers` must be present and non-empty array.
+ 2:6  warning  info-contact           Info object should contain `contact` object.
+ 2:6  warning  info-description       OpenAPI object info `description` must be present and non-empty string.
+ 9:9  warning  operation-description  Operation `description` must be present and non-empty string.
+
+✖ 4 problems (0 errors, 4 warnings, 0 infos, 0 hints)
diff --git a/test-harness/scenarios/severity/fail-on-error-no-error.scenario b/test-harness/scenarios/severity/fail-on-error-no-error.scenario
new file mode 100644
index 00000000..23dc7e78
--- /dev/null
+++ b/test-harness/scenarios/severity/fail-on-error-no-error.scenario
@@ -0,0 +1,36 @@
+====test====
+Will only fail if there is an error, and there is not. Can still see all warnings.
+====document====
+openapi: 3.0.0
+info:
+  version: 1.0.0
+  title: Unique Operations
+paths:
+  /test:
+    get:
+      operationId: foo
+      responses:
+        200:
+          description: ok
+    post:
+      operationId: bar
+      responses:
+        200:
+          description: ok
+====command====
+lint {document} --fail-severity=error
+====status====
+0
+====stdout====
+OpenAPI 3.x detected
+
+{document}
+  1:1   warning  api-servers            OpenAPI `servers` must be present and non-empty array.
+  2:6   warning  info-contact           Info object should contain `contact` object.
+  2:6   warning  info-description       OpenAPI object info `description` must be present and non-empty string.
+  7:9   warning  operation-description  Operation `description` must be present and non-empty string.
+  7:9   warning  operation-tags         Operation should have non-empty `tags` array.
+ 12:10  warning  operation-description  Operation `description` must be present and non-empty string.
+ 12:10  warning  operation-tags         Operation should have non-empty `tags` array.
+
+✖ 7 problems (0 errors, 7 warnings, 0 infos, 0 hints)
diff --git a/test-harness/scenarios/severity/fail-on-error.oas3.scenario b/test-harness/scenarios/severity/fail-on-error.oas3.scenario
new file mode 100644
index 00000000..23e99563
--- /dev/null
+++ b/test-harness/scenarios/severity/fail-on-error.oas3.scenario
@@ -0,0 +1,38 @@
+====test====
+Will fail and return 1 as exit code because errors exist
+====document====
+openapi: 3.0.0
+info:
+  version: 1.0.0
+  title: Clashing Operations
+paths:
+  /test:
+    get:
+      operationId: foo
+      responses:
+        200:
+          description: ok
+    post:
+      operationId: foo
+      responses:
+        200:
+          description: ok
+====command====
+lint {document} --fail-severity=error
+====status====
+1
+====stdout====
+OpenAPI 3.x detected
+
+{document}
+  1:1   warning  api-servers                   OpenAPI `servers` must be present and non-empty array.
+  2:6   warning  info-contact                  Info object should contain `contact` object.
+  2:6   warning  info-description              OpenAPI object info `description` must be present and non-empty string.
+  7:9   warning  operation-description         Operation `description` must be present and non-empty string.
+  7:9   warning  operation-tags                Operation should have non-empty `tags` array.
+  8:20    error  operation-operationId-unique  Every operation must have a unique `operationId`.
+ 12:10  warning  operation-description         Operation `description` must be present and non-empty string.
+ 12:10  warning  operation-tags                Operation should have non-empty `tags` array.
+ 13:20    error  operation-operationId-unique  Every operation must have a unique `operationId`.
+
+✖ 9 problems (2 errors, 7 warnings, 0 infos, 0 hints)
diff --git a/test-harness/scenarios/severity/stylish-display-proper-names.scenario b/test-harness/scenarios/severity/stylish-display-proper-names.scenario
new file mode 100644
index 00000000..1f0efe02
--- /dev/null
+++ b/test-harness/scenarios/severity/stylish-display-proper-names.scenario
@@ -0,0 +1,101 @@
+====test====
+The name of severity levels are display correctly by stylish formatter
+====document====
+{
+  "openapi": "3.0.0",
+  "info": {
+    "version": "1.0.0",
+    "title": "Swagger Petstore",
+    "license": {
+      "name": "MIT"
+    }
+  },
+  "paths": {
+    "/pets": {
+      "get": {
+        "summary": "List all pets",
+        "operationId": "listPets",
+        "tags": ["pets"],
+        "parameters": [
+          {
+            "name": "limit",
+            "in": "query",
+            "description": "How many items to return at one time (max 100)",
+            "required": false,
+            "schema": {
+              "type": "integer",
+              "format": "int32"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "A paged array of pets",
+            "headers": {
+              "x-next": {
+                "description": "A link to the next page of responses",
+                "schema": {
+                  "type": "string"
+                }
+              }
+            },
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Pets"
+                }
+              }
+            }
+          },
+          "default": {
+            "description": "unexpected error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                }
+              }
+            }
+          }
+        }
+      },
+      "post": {
+        "summary": "Create a pet",
+        "operationId": "createPets",
+        "tags": ["pets"],
+        "responses": {
+          "201": {
+            "description": "Null response"
+          },
+          "default": {
+            "description": "unexpected error",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+====command====
+lint --ruleset ./test-harness/scenarios/rulesets/oas-mixed-severity.json {document}
+====stdout====
+OpenAPI 3.x detected
+
+{document}
+  1:1       warning  api-servers             OpenAPI `servers` must be present and non-empty array.
+  3:10        error  info-contact            Info object should contain `contact` object.
+  3:10      warning  info-description        OpenAPI object info `description` must be present and non-empty string.
+  5:14         hint  info-matches-stoplight  Info must contain Stoplight
+ 12:13  information  operation-description   Operation `description` must be present and non-empty string.
+ 42:27        error  invalid-ref             '#/components/schemas/Pets' does not exist
+ 52:27        error  invalid-ref             '#/components/schemas/Error' does not exist
+ 59:14  information  operation-description   Operation `description` must be present and non-empty string.
+
+✖ 8 problems (3 errors, 2 warnings, 2 infos, 1 hint)
diff --git a/test-harness/scenarios/todo-full-loose-schema.scenario b/test-harness/scenarios/todo-full-loose-schema.scenario
new file mode 100644
index 00000000..d0efd1f2
--- /dev/null
+++ b/test-harness/scenarios/todo-full-loose-schema.scenario
@@ -0,0 +1,67 @@
+====test====
+Loose JSON Schema can be validated
+====document====
+{
+  "title": "Todo Full",
+  "allOf": [
+    {
+      "title": "Todo Partial",
+      "description": "",
+      "type": "object",
+      "properties": {
+        "name": {
+          "type": "string"
+        },
+        "completed": {
+          "type": [
+            "boolean",
+            "null"
+          ]
+        }
+      },
+      "required": [
+        "name",
+        "completed"
+      ]
+    },
+    {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "integer",
+          "minimum": 0,
+          "maximum": 1000000
+        },
+        "completed_at": {
+          "type": [
+            "string",
+            "null"
+          ],
+          "format": "date-time"
+        },
+        "created_at": {
+          "type": "string",
+          "format": "date-time"
+        },
+        "updated_at": {
+          "type": "string",
+          "format": "date-time"
+        }
+      },
+      "required": [
+        "id",
+        "user"
+      ]
+    }
+  ]
+}
+====command====
+lint {document} --ruleset ./test-harness/scenarios/rulesets/loose-schema-ruleset.json
+====stdout====
+JSON Schema (loose) detected
+
+{document}
+ 3:11    error  no-root-all-of        No root allOf combiner
+ 6:22  warning  no-empty-description  Description must not be empty
+
+✖ 2 problems (1 error, 1 warning, 0 infos, 0 hints)
diff --git a/test-harness/scenarios/valid-no-errors.oas2.scenario b/test-harness/scenarios/valid-no-errors.oas2.scenario
index 131fd549..eb00d47f 100644
--- a/test-harness/scenarios/valid-no-errors.oas2.scenario
+++ b/test-harness/scenarios/valid-no-errors.oas2.scenario
@@ -16,4 +16,4 @@ paths: {}
 lint {document}
 ====stdout====
 OpenAPI 2.0 (Swagger) detected
-No errors or warnings found!
\ No newline at end of file
+No results with a severity of 'hint' or higher found!