Repository: stoplightio/spectral
Pull request #1722 (2021-07-07 12:31:17)

chore(deps-dev): bump @types/xml2js from 0.4.8 to 0.4.9
Bumps [@types/xml2js](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/xml2js) from 0.4.8 to 0.4.9.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/xml2js">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@types/xml2js&package-manager=npm_and_yarn&previous-version=0.4.8&new-version=0.4.9)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 6f2e57ae..98a309be 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -267,7 +267,7 @@ workflows:
   scheduled-release:
     triggers:
       - schedule:
-          cron: "0 * * * *"
+          cron: "0 12 * * *"
           filters:
             branches:
               only:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3c1b835e..79615e73 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,47 @@ 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).
 
+## [6.0.0] - 2021-07-14
+
+### Added
+
+- Ruleset: New alternative ruleset format [#1615](https://github.com/stoplightio/spectral/issues/1615)
+- Ruleset: ESLint-like Overrides [#1021](https://github.com/stoplightio/spectral/issues/1021)
+- Ruleset: Path Aliases [#1680](https://github.com/stoplightio/spectral/issues/1680)
+- Rulesets: OpenAPI (OAS) 3.1 support [#1302](https://github.com/stoplightio/spectral/issues/1302)
+- Functions: `schema` function uses Ajv v8 under the hood and therefore support newer JSON Schema drafts [#1584](https://github.com/stoplightio/spectral/pull/1584)
+- CLI: implement `--stdin-filepath` flag [#1001](https://github.com/stoplightio/spectral/issues/1001)
+- Formats: JSON Schema Draft 2020-12 format [#1556](https://github.com/stoplightio/spectral/issues/1556)
+- CLI: added pretty formatter for colored and wrapped cli output [#1580](https://github.com/stoplightio/spectral/pull/1580)
+- Rulesets: Supports `x-nullable` for OAS2 [#1359](https://github.com/stoplightio/spectral/issues/1359)
+- Rulesets: `oas3-unused-component` rule which detects all orphaned components [#1440](https://github.com/stoplightio/spectral/pull/1440)
+
+### Changed
+
+- Ruleset validation yields more understandable errors [#1637](https://github.com/stoplightio/spectral/issues/1637)
+- CLI: demand some ruleset to be present [#1699](https://github.com/stoplightio/spectral/pull/1699)
+- CLI: require valid commands and options [#1586](https://github.com/stoplightio/spectral/issues/1586)
+- Core: revise `RulesetFunction` aka `IFunction` [#1685](https://github.com/stoplightio/spectral/pull/1685)
+- Core: runtime exceptions are not swallowed [#1618](https://github.com/stoplightio/spectral/issues/1618)
+- Rulesets: Certain oas{2,3}-valid-\*-example rules have been merged [#1313](https://github.com/stoplightio/spectral/pull/1313)
+- Rulesets: more consistent linting messages in AsyncAPI & OpenAPI rulesets [#1619](https://github.com/stoplightio/spectral/issues/1619)
+- Rulesets: `operation-2xx-response` rule has been renamed to `operation-success-response` and is no longer recommended [#1272](https://github.com/stoplightio/spectral/issues/1273)
+- Rulesets: `openapi-tags` rule is no longer recommended [#1132](https://github.com/stoplightio/spectral/issues/1132)
+
+### Fixed
+
+- Core: Incorrect range when semicolons are present in a key [#1697](https://github.com/stoplightio/spectral/issues/1697)
+- Core: Empty $refs should not be ignored [#1515](https://github.com/stoplightio/spectral/issues/1515)
+- Rulesets: validate 'default' in schemas [#1402](https://github.com/stoplightio/spectral/issues/1402)
+
+### Removed
+
+- Support for Node 10.x [#1542](https://github.com/stoplightio/spectral/pull/1542)
+- Functions: `schemaPath` function [#1621](https://github.com/stoplightio/spectral/issues/1621)
+- CLI: `--skip-rule` and `--show-unmatched-globs` flags [#1560](https://github.com/stoplightio/spectral/pull/1560)
+- Core: exceptions [#1675](https://github.com/stoplightio/spectral/pull/1675)
+- Rulesets: `operation-default-response` rule [#1304](https://github.com/stoplightio/spectral/pull/1304)
+
 ## [5.9.1] - 2021-04-08
 
 ### Fixed
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 45d5ca72..388fe63e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -14,26 +14,13 @@ Our Code of Conduct exists because of that dedication, and we do not tolerate ha
 
 ## Development
 
-Yarn is a package manager for your code, similar to npm. While you can use npm to use Spectral in your own project, we use yarn for development of Spectral.
+### Setup
 
-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: Refer to the [installation documentation](https://classic.yarnpkg.com/en/docs/install/) according to your development operating system
-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: `yarn cli 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` and `yarn test.harness`
-9. Update the documentation if appropriate. For example, if you added a new rule to an OpenAPI ruleset,
-add a description of the rule in `docs/reference/openapi-rules.md`.
-
-Now, you are ready to commit & push your changes, and make a pull request to the Spectral repo! 😃
-
-If this is your first Pull Request on GitHub, here's some [help](https://egghead.io/lessons/javascript-how-to-create-a-pull-request-on-github).
-
-> We try to respond to all pull requests and issues within 7 days. We welcome feedback from everyone involved in the project in open pull requests.
+- Install Node.JS, the minimum version we support is 12.20. If you have [nvm](https://github.com/nvm-sh/nvm) installed, execute `nvm use` and it'll pick the right version for you.
+- [Yarn](https://yarnpkg.com/getting-started/install) - Yarn is a package manager for your code, similar to npm. While you can use npm to use Spectral in your own project, we use yarn for development of Spectral.
+- IDE / Editor of your choice - I use WebStorm, but VSCode and many others are fine too.
 
-### Contributing from a Windows based environment
+#### Contributing from a Windows based environment
 
 Spectral is a cross-platform tool and we do our best to ensure it honors specifics
 of the underlying operating system it's being run on.
@@ -57,42 +44,149 @@ Most of the time, working natively from vscode will work fine. However, it may h
 
 Would you want to switch back to the standard experience, using the Command Palette, type `>reopen lo` and select `Remote-Containers: Reopen locally`. Welcome back to the Windows world!
 
-## To run tests
+## Repo overview
 
-We run tests in the two environments that Spectral supports - the browser, and node.js. Browser tests are run in a headless Chrome browser via the Karma test runner, while node.js tests are run via the Jest test runner.
+This repo is a monorepo.
 
-Tests should usually be written for both environments, but there are valid cases when you need to write separate tests for each test runner. To do so, just create a file with `*.karma.test.ts` suffix or `*.jest.test.ts`. A good example of Jest only tests are the tests covering Spectral's CLI functionality - something that obviously is not relevant to the browser context.
+- packages/cli contains everything that's related to CLI of Spectral. This is the only package that's meant to be run exclusively in Node.js environment.
+- packages/core contains all the code that's supposed to parse the input, load & validate the ruleset, run $ref resolver, run the linting process, process errors, etc. It's the heart of Spectral.
+- packages/formats is all about formats, as the title says :). It comes up with a set of officially supported formats you can leverage in your rulesets.
+- packages/functions is a collection of functions maintained by us that you can make use of in your rulesets.
+- packages/parsers is mostly used internally by packages/core. Our parsers live there. Do note that we mostly integrate `@stoplight/yaml` & `@stoplight/json` there.
+- packages/ref-resolver exposes an instance of json-ref-resolver
+- packages/ruleset-migrator
+- packages/rulesets is an array of rulesets backed by us. Currently the list of rulesets consists of OAS and AsyncAPI v2.
+- packages/runtime is a set of utilities you can use in your own custom functions.
 
-Running all tests:
+### General Principles
+
+Since Spectral should be executable in a variety of environments, including browsers & Node.js, we strongly encourage to write code that's portable.
+This implies usage of packages that are not platform-specific, browser globals such as fetch, as well as Node.js modules, such as `fs` or `path`.
+The only exception is the CLI package that's supposed to be exclusive to Node.js.
+To make the whole process easier, we usually have equivalent packages or approaches.
+For instance, `path` can be quite safely replaced with `@stoplight/path`.
+
+### Linting
+
+In comparison with other Stoplight projects, Spectral is the strictest and enforces plenty of rules.
+We use a number of various linters, including ESLint, Prettier or [kacl](https://www.npmjs.com/package/@brightcove/kacl).
+If you're confused about a given linting error, please refer to the documentation provided by the owner of one of these packages, or plugins we use.
+Commit messages follow [conventional-changelog](https://github.com/conventional-changelog/commitlint).
+
+Running all linters:
+
+```bash
+yarn lint
+```
+
+Linting CHANGELOG.md
+
+```bash
+yarn lint.changelog
+```
+
+Linting TS/JS Code
 
 ```bash
-yarn test.prod
+yarn lint.eslint
 ```
 
-Running node.js (jest) tests:
+Linting Documentation / Markdown files
+
+```bash
+yarn lint.prettier
+```
+
+### Tests
+
+We run tests in the two environments that Spectral supports - the browser, and Node.js. Browser tests are run in a headless Chrome browser via the Karma test runner, while Node.js tests are run via the Jest test runner.
+
+Tests should usually be written for both environments, but there are valid cases when you need to write separate tests for each test runner.
+To do so, just create a file with `*.karma.test.ts` suffix or `*.jest.test.ts`. A good example of Jest only tests are the tests covering Spectral's CLI functionality - something that obviously is not relevant to the browser context.
+
+#### Caveats
+
+Since Jest and Jasmine (we use Jasmine framework in Karma) are not fully compatible, one has to keep in mind that certain assertions or methods are not available.
+For example, `toMatchSnapshot` or `toMatchInlineSnapshot` are not available for use. `jest.mock` and similar are also missing.
+This is usually not a problem. Things like `fs` are usually mocked globally (we use aliases to accomplish this), thus you don't need to worry too much.
+If you need to setup HTTP mocks or populate FS with some data, we have a dedicated util to do so.
+
+Example:
+
+```ts
+import { serveAssets } from '@stoplight/spectral-test-utils';
+import * as path from '@stoplight/path';
+
+const cwd = '/tmp/some-fake-path';
+
+serveAssets({
+  'https://library.com/defs.json': { // all requests for given resource will be mocked
+    components: {
+      schemas: {
+        ExternalHttp: {
+          type: 'number',
+        },
+      },
+    },
+  },
+  [path.join(cwd, 'file.json')]: { // fs.read will return this
+    openapi: '3.1.0',
+    info: {
+      title: 'Test file',
+    },
+    paths: {},
+  },
+})
+```
+
+This works both in Karma & Jest.
+
+#### Usage
+
+Running all tests:
 
 ```bash
 yarn test
 ```
 
-Running browser (karma) tests:
+Running Node.js (jest) tests:
 
 ```bash
-yarn test.karma
+yarn test.jest
 ```
 
-Running a specific test:
+Running Browser (karma) tests:
 
 ```bash
-yarn test src/cli/commands/__tests__/lint.test.ts
+yarn test.karma
 ```
 
 Running the harness tests (these must pass or the PR merge will be blocked):
 
 ```bash
+# make sure to build the code beforehand if you haven't done it. To do so execute yarn build && yarn workspace @stoplight/spectral-cli build.binary
 yarn test.harness
 ```
 
+## PR Submission
+
+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: Refer to the [installation documentation](https://classic.yarnpkg.com/en/docs/install/) according to your development operating system
+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 && yarn bui`
+6. Run Spectral from your local installation: `yarn cli 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` and `yarn test.harness`
+9. Update the documentation if appropriate. For example, if you added a new rule to an OpenAPI ruleset,
+   add a description of the rule in `docs/reference/openapi-rules.md`.
+
+Now, you are ready to commit & push your changes, and make a pull request to the Spectral repo! 😃
+
+If this is your first Pull Request on GitHub, here's some [help](https://egghead.io/lessons/javascript-how-to-create-a-pull-request-on-github).
+
+> We try to respond to all pull requests and issues within 7 days. We welcome feedback from everyone involved in the project in open pull requests.
+
 ## Bugs & Feature Requests
 
 We want to keep issues in this repo focused on bug reports and feature requests.
diff --git a/README.md b/README.md
index 11c2c61d..a14bf488 100644
--- a/README.md
+++ b/README.md
@@ -22,11 +22,11 @@
 **Install**
 
 ```bash
-npm install -g @stoplight/spectral
+npm install -g @stoplight/spectral-cli
 
 # OR
 
-yarn global add @stoplight/spectral
+yarn global add @stoplight/spectral-cli
 ```
 
 Find more [installation methods](https://meta.stoplight.io/docs/spectral/docs/getting-started/2-installation.md) in our documentation.
diff --git a/docs/getting-started/2-installation.md b/docs/getting-started/2-installation.md
index 541591fe..b7f3543d 100644
--- a/docs/getting-started/2-installation.md
+++ b/docs/getting-started/2-installation.md
@@ -3,13 +3,13 @@
 For many, the easiest way to install Spectral is as a node module.
 
 ```bash
-npm install -g @stoplight/spectral
+npm install -g @stoplight/spectral-cli
 ```
 
 If you are a Yarn user:
 
 ```bash
-yarn global add @stoplight/spectral
+yarn global add @stoplight/spectral-cli
 ```
 
 ## Executable Binaries
diff --git a/docs/guides/1-workflows.md b/docs/guides/1-workflows.md
index 482fcbd4..6549d461 100644
--- a/docs/guides/1-workflows.md
+++ b/docs/guides/1-workflows.md
@@ -58,7 +58,7 @@ jobs:
       - checkout
       - run:
           name: "API Description Linter"
-          command: npx @stoplight/spectral lint somefile.yaml -- --ruleset=config/custom-ruleset.yaml
+          command: npx @stoplight/spectral-cli lint somefile.yaml -- --ruleset=config/custom-ruleset.yaml
 ```
 
 We plan to add JUnit/xUnit test results in a future version, so tools like CircleCI can show test results in a more visual way. For now, the commands exit code will alert CI that there was a problem, and the console output will say why.
diff --git a/docs/guides/3-javascript.md b/docs/guides/3-javascript.md
index f697ab9d..50cd0b4e 100644
--- a/docs/guides/3-javascript.md
+++ b/docs/guides/3-javascript.md
@@ -4,136 +4,66 @@ The Spectral CLI is a thin wrapper around a JavaScript (TypeScript) API, which c
 
 Assuming it has been installed as a Node module via NPM/Yarn, it can be used to lint YAML and JSON documents from a string, or from an object.
 
+First of all, in order to consume the JS API you need to install the appropriate package(s).
+
+```bash
+npm install -g @stoplight/spectral-core
+```
+
+If you are a Yarn user:
+
+```bash
+yarn global add @stoplight/spectral-core
+```
+
 ## Linting a YAML String
 
 ```js
-const { Spectral, Document, Parsers } = require("@stoplight/spectral");
+const { Spectral, Document } = require("@stoplight/spectral-core");
+const Parsers = require("@stoplight/spectral-parsers"); // make sure to install the package if you intend to use default parsers!
+const { truthy } = require("@stoplight/spectral-functions"); // this has to be installed as well
 
-const myOpenApiDocument = new Document(
-  `responses:
+const myDocument = new Document(
+  `---
+responses:
   '200':
-    description: ''
-    schema:
-      $ref: '#/definitions/error-response'
-`,
+    description: ''`,
   Parsers.Yaml,
+  "/my-file",
 );
 
 const spectral = new Spectral();
-spectral.run(myOpenApiDocument).then(console.log);
-```
-
-This will run Spectral with no formats, rules or functions, so it's not going to do anything besides \$ref resolving.
-Find out how to add formats, rules and functions below.
-
-## Linting an Object
-
-Instead of passing a string to `Document`, you can pass in JavaScript object, with or without `$ref`'s.
-
-```js
-const { Spectral } = require("@stoplight/spectral");
-
-const myOpenApiDocument = {
-  responses: {
-    200: {
-      description: "",
-      schema: {
-        $ref: "#/definitions/error-response",
+spectral.setRuleset({
+  // a ruleset has to be provided
+  rules: {
+    "no-empty-description": {
+      given: "$..description",
+      message: "Description must not be empty",
+      then: {
+        function: truthy,
       },
     },
   },
-};
-
-const spectral = new Spectral();
-spectral.run(myOpenApiDocument).then(console.log);
-```
-
-Note - this usage is discouraged, since you won't get accurate ranges.
-
-## Registering Formats
-
-If you are interested in linting OpenAPI documents or JSON Schema models, you may need to register formats.
-Assuming your rulesets use the built-in Spectral formats, this can be accomplished as follows
-
-- OpenAPI
-
-```js
-const { Spectral, isOpenApiv2, isOpenApiv3 } = require("@stoplight/spectral");
-
-const spectral = new Spectral();
-spectral.registerFormat("oas2", isOpenApiv2);
-spectral.registerFormat("oas3", isOpenApiv3);
-```
-
-- JSON Schema
-
-```js
-const { Spectral, isJSONSchema, isJSONSchemaDraft4, isJSONSchemaDraft6, isJSONSchemaDraft7, isJSONSchemaDraft2019_09, isJSONSchemaLoose } = require("@stoplight/spectral");
-
-const spectral = new Spectral();
-spectral.registerFormat("json-schema", isJSONSchema);
-spectral.registerFormat("json-schema-loose", isJSONSchemaLoose);
-spectral.registerFormat("json-schema-draft4", isJSONSchemaDraft4);
-spectral.registerFormat("json-schema-draft6", isJSONSchemaDraft6);
-spectral.registerFormat("json-schema-draft7", isJSONSchemaDraft7);
-spectral.registerFormat("json-schema-2019-09", isJSONSchemaDraft2019_09);
+});
+spectral.run(myDocument).then(console.log);
 ```
 
-Learn more about predefined formats in the [ruleset documentation](../getting-started/3-rulesets.md#formats).
+This will run Spectral with no formats, rules or functions, so it's not going to do anything besides \$ref resolving.
+Find out how to add formats, rules and functions below.
 
-## Loading Rules
+## Loading Rulesets
 
 Spectral comes with some rulesets that are very specific to OpenAPI v2/v3, and they can be loaded using `Spectral.loadRuleset()`.
 
 ```js
-const { Spectral, isOpenApiv2, isOpenApiv3 } = require("@stoplight/spectral");
+const { Spectral } = require("@stoplight/spectral-core");
+const ruleset = require("./my-ruleset"); // if you use a YAML/JSON ruleset, make sure to use @stoplight/spectral-ruleset-migrator first.
 
 const myOpenApiDocument = `
 openapi: 3.0.0
 # here goes the rest of document
 `;
 
-const spectral = new Spectral();
-spectral.registerFormat("oas2", isOpenApiv2);
-spectral.registerFormat("oas3", isOpenApiv3);
-spectral
-  .loadRuleset("spectral:oas")
-  .then(() => spectral.run(myOpenApiDocument))
-  .then(results => {
-    console.log("here are the results", results);
-  });
-```
-
-The OpenAPI rules are opinionated. There might be some rules that you prefer to change, or disable. We encourage you to create your rules to fit your use case, and we welcome additions to the existing rulesets as well!
-
-Custom rulesets can also be loaded using `spectral.loadRuleset()` by specifying the exact path to the ruleset file.
-
-```js
-const { Spectral, isOpenApiv2, isOpenApiv3 } = require('@stoplight/spectral');
-const { join } = require('path');
-
-const myOpenApiDocument = `
-openapi: 3.0.0
-# here goes the rest of document
-`
-
-const spectral = new Spectral();
-spectral.registerFormat('oas2', isOpenApiv2);
-spectral.registerFormat('oas3', isOpenApiv3);
-
-spectral.loadRuleset(join(__dirname './path/to/my-ruleset.yaml'));
-  .then(() => spectral.run(myOpenApiDocument))
-  .then(results => {
-    console.log('here are the results', results);
-  });
-```
-
-Alternatively, if your ruleset is stored in a plain JSON file that doesn't extend any other rulesets, you can also consider using `setRuleset`, as follows
-
-```js
-const { Spectral } = require("@stoplight/spectral");
-const ruleset = require("./my-ruleset.json");
-
 const spectral = new Spectral();
 spectral.setRuleset(ruleset);
 spectral.run(myOpenApiDocument).then(results => {
@@ -143,86 +73,18 @@ spectral.run(myOpenApiDocument).then(results => {
 
 ## Advanced
 
-### Creating a Custom Format
-
-Spectral supports two core formats: `oas2` and `oas3`. Using `registerFormat` you can add support for auto-detecting other formats. You might want to do this for a ruleset which is run against multiple major versions of description format like RAML v0.8 and v1.0.
-
-```js
-const { Spectral } = require("@stoplight/spectral");
-
-const spectral = new Spectral();
-
-spectral.registerFormat("foo-bar", obj => typeof obj === "object" && obj !== null && "foo-bar" in obj);
-
-spectral.setRuleset({
-  functions: {},
-  rules: {
-    rule1: {
-      given: "$.x",
-      formats: ["foo-bar"],
-      severity: "error",
-      then: {
-        function: "truthy",
-      },
-    },
-  },
-});
-
-spectral
-  .run({
-    "foo-bar": true,
-    x: false,
-  })
-  .then(result => {
-    expect(result).toEqual([
-      expect.objectContaining({
-        code: "rule1",
-      }),
-    ]);
-  });
-```
-
-Alternatively you may lookup for certain format by optional `source`, which could be passed in `run` options.
-
-```js
-const { Document, Spectral } = require("@stoplight/spectral");
-
-const spectral = new Spectral();
-
-spectral.registerFormat("foo-bar", (_, source) => source === "/foo/bar");
-
-spectral.setRuleset({
-  functions: {},
-  rules: {
-    rule1: {
-      given: "$.x",
-      formats: ["foo-bar"],
-      severity: "error",
-      then: {
-        function: "truthy",
-      },
-    },
-  },
-});
-
-spectral.run(new Document(`foo-bar: true\nx: false`, Parsers.Yaml, "/foo/bar")).then(result => {
-  expect(result).toEqual([
-    expect.objectContaining({
-      code: "rule1",
-    }),
-  ]);
-});
-```
-
 ### Using a Proxy
 
-Spectral supports HTTP(S) proxies when fetching remote schemas and rulesets.
+Spectral supports HTTP(S) proxies when fetching remote assets.
 
 ```js
-const { Spectral } = require("@stoplight/spectral");
+const { Spectral } = require("@stoplight/spectral-core");
+const ProxyAgent = require("proxy-agent");
+const { createHttpAndFileResolver } = require("@stoplight/spectral-ref-resolver");
 
-const spectral = new Spectral({ proxyUri: "http://my-proxy:3000" });
-spectral.loadRuleset("https://example.org/my-rules");
+const spectral = new Spectral({
+  resolver: createHttpAndFileResolver({ agent: new ProxyAgent(process.env.PROXY) }),
+});
 
 // lint as usual - $refs and rules will be requested using the proxy
 ```
@@ -239,7 +101,7 @@ the [@stoplight/json-ref-resolver](https://github.com/stoplightio/json-ref-resol
 ```js
 const path = require("path");
 const fs = require("fs");
-const { Spectral } = require("@stoplight/spectral");
+const { Spectral } = require("@stoplight/spectral-cli");
 const { Resolver } = require("@stoplight/json-ref-resolver");
 
 const customFileResolver = new Resolver({
diff --git a/docs/guides/4-custom-rulesets.md b/docs/guides/4-custom-rulesets.md
index a0b1cdf7..914e2766 100644
--- a/docs/guides/4-custom-rulesets.md
+++ b/docs/guides/4-custom-rulesets.md
@@ -175,8 +175,6 @@ Rules by default are considered "recommended" (equivalent to a rule having) `rec
 You can try this out with the core OpenAPI ruleset. If you simply extend the ruleset, by default you will only get the recommended rules.
 
 ```yaml
-extends: spectral:oas
-# is equivalent to
 extends: [[spectral:oas, recommended]]
 ```
 
@@ -200,11 +198,17 @@ To help you create meaningful error messages, Spectral comes with a couple of pl
 
 ```yaml
 message: "{{error}}" # will output the message generated by then.function
+```
 
+```yaml
 message: "The value of '{{property}}' property must equal 'foo'"
+```
 
+```yaml
 message: "{{value}} is greater than 0"
+```
 
+```yaml
 message: "{{path}} cannot point at remote reference"
 ```
 
@@ -265,3 +269,112 @@ Several functions [are provided by default](../reference/functions.md) for your
 ## Custom Functions
 
 If none of the [core functions](../reference/functions.md) do what you want, you can [write your own custom functions](./5-custom-functions.md).
+
+## Alternative JS Ruleset Format
+
+Spectral now supports an alternative format to write rulesets in Javascript using a similar syntax.
+
+### Benifits
+
+- Explicitly load formats or rulesets to get control over versioning.
+- Load common functions from popular JS libraries easily
+
+### Example
+
+```js
+//you can import popular functions from libraries
+import { isObject } from "https://cdn.jsdelivr.net/npm/lodash-es/+esm";
+import { truthy, schema } from "https://cdn.jsdelivr.net/npm/@stoplight/spectral-functions/+esm";
+// you can stick to an older version if you want to for some reason. That's fine
+import { alphabetical } from "https://cdn.jsdelivr.net/npm/@stoplight/spectral-functions@1.0.4/+esm";
+import { oasRuleset } from "https://cdn.jsdelivr.net/npm/@stoplight/spectral-rulesets/+esm";
+import { oas2 } from "https://cdn.jsdelivr.net/npm/@stoplight/spectral-formats/+esm";
+
+import { verifyType } from "./verifyType.mjs";
+
+const $SCHEMA_DRAFT_2020_XX_REGEX = /^https?:\/\/json-schema.org\/draft\/2020-\d\d\/(?:hyper-)?schema#?$/;
+
+const JSONSchemaDraft2020_XX = document => isObject(document) && "$schema" in document && $SCHEMA_DRAFT_2020_XX_REGEX.test(document.$schema);
+
+export default {
+  formats: [oas2, oas3],
+  // No longer accepts functions & functionOptions
+  extends: [oasRuleset],
+  rules: {
+    "valid-rule": {
+      given: "$.info",
+      then: {
+        function: truthy, // instead of 'truthy'
+      },
+    },
+    "only-new-json-schema": {
+      formats: [JSONSchemaDraft2020_XX],
+      given: "$..type",
+      then: {
+        function: verifyType,
+      },
+    },
+  },
+};
+```
+
+## Aliases
+
+Targeting certain parts of an OpenAPI spec is powerful but it can become cumbersome to write and repeat complex JSON path expressions across various rules. Define aliases for commonly used JSON paths on a global level which can then be reused across the ruleset.
+
+Aliases can be defined in an array of KVP on top of the ruleset.
+
+```
+aliases:{
+  {Name}: '{JSONPath}'
+}
+```
+
+### Example:
+
+```json
+{
+  "aliases": {
+    "Info": "$..info",
+    "InfoDescription": "#Info.description",
+    "InfoContact": "#Info.contact",
+    "InfoContactName": "#InfoContact.name"
+  }
+}
+```
+
+> This will be followed by our core rulesets providing a common set of aliases for OpenAPI and AsyncAPI so that our users don't have to do the work at all. If you have ideas about what kind of aliases could be useful leave your thoughts [here](https://roadmap.stoplight.io).
+
+## Overrides
+
+Previously Spectral supported exceptions, which were limited in their ability to target particular rules on specific files or parts of files, or changing parts of a rule. Overrides is the much more powerful version of exceptions, with the ability to customize ruleset usage for different files and projects without having to duplicate any rules.
+
+Overrides can be used to:
+
+- Override rulesets to apply on particular files/folders `files: ['schemas/**/*.draft7.json']`
+- Override rulesets to apply on particular formats `formats: [jsonSchemaDraft7]`
+- Override particular rules
+
+### Example:
+
+```json
+{
+  "overrides": [
+    {
+      "files": ["schemas/**/*.draft7.json"],
+      "formats": ["json-schema-draft7"],
+      "rules": {
+        "valid-number-validation": {
+          "given": ["$..exclusiveMinimum", "$..exclusiveMaximum"],
+          "then": {
+            "function": "schema",
+            "functionOptions": {
+              "type": "number"
+            }
+          }
+        }
+      }
+    }
+  ]
+}
+```
diff --git a/docs/migration-guides/6.0.md b/docs/migration-guides/6.0.md
index f61bf7c7..6c2f0b81 100644
--- a/docs/migration-guides/6.0.md
+++ b/docs/migration-guides/6.0.md
@@ -1,4 +1,16 @@
-# Spectral v5 to v6 Migration Guide (WIP)
+# Spectral v5 to "v6" Migration Guide
+
+## General
+
+1. Spectral has become a monorepo now and CLI <-> Core have been split into separate packages.
+   If you intend to use a CLI version of Spectral make sure to install `@stoplight/spectral-cli`.
+   Otherwise, go with `@stoplight/spectral-core` which is the JS API.
+
+2. No default ruleset is loaded by default as of now, thus if you don't supply a ruleset, Spectral will refuse to lint the document.
+   If you use CLI, make sure to have a valid ruleset in your project or use a `--ruleset` flag.
+   JS API has a method called `setRuleset` which can be used to set the relevant ruleset.
+
+3. Spectral is stricter overall and certain soft errors that previously were marked as warnings will throw now.
 
 ## Core
 
@@ -24,7 +36,7 @@ const doc = new Document(
 await spectral.run(doc);
 ```
 
-2. Spectral constructor
+2. Spectral constructor takes no `proxyUri`.
 
 ```ts
 import { Document, Spectral } from "@stoplight/spectral-core";
@@ -44,6 +56,18 @@ const doc = new Document(
 await spectral.run(doc);
 ```
 
+The workaround here can look as follows
+
+```ts
+import { Spectral } from "@stoplight/spectral-core";
+import { createHttpAndFileResolver } from "@stoplight/spectral-ref-resolver";
+import ProxyAgent from "proxy-agent";
+
+const spectral = new Spectral({
+  resolver: createHttpAndFileResolver({ agent: new ProxyAgent(process.env.PROXY) }),
+});
+```
+
 ## Functions
 
 1. `schema` function uses Ajv v8 in favor of Ajv v6. It is preferred to provide JSON Schema Draft 7 schemas or newer, albeit older drafts are also supported via json-schema-migrate.
@@ -55,7 +79,7 @@ await spectral.run(doc);
 
 1. deprecated `--show-unmatched-globs` flag has been removed. Alternative available: `--fail-on-unmatched-globs`
 
-2. `--skip-rule` has been removed. Use a custom ruleset instead.
+2. `--skip-rule` has been removed. Use a custom ruleset instead - set a severity of a particular rule to "off".
 
 ## Rulesets
 
diff --git a/package.json b/package.json
index c15c2c9d..f2504f0d 100644
--- a/package.json
+++ b/package.json
@@ -100,7 +100,7 @@
     "semantic-release-monorepo": "^7.0.5",
     "ts-jest": "^27.0.3",
     "ts-node": "^10.0.0",
-    "typescript": "^4.3.2"
+    "typescript": "^4.3.5"
   },
   "lint-staged": {
     "*.{ts,js}": [
diff --git a/packages/cli/package.json b/packages/cli/package.json
index a91a9ee1..754367b9 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@stoplight/spectral-cli",
-  "version": "1.2.0",
+  "version": "1.2.1",
   "homepage": "https://github.com/stoplightio/spectral",
   "bugs": "https://github.com/stoplightio/spectral/issues",
   "author": "Stoplight <support@stoplight.io>",
@@ -53,7 +53,7 @@
     "yargs": "17.0.1"
   },
   "devDependencies": {
-    "@types/xml2js": "^0.4.8",
+    "@types/xml2js": "^0.4.9",
     "@types/yargs": "^17.0.2",
     "copyfiles": "^2.4.1",
     "jest-when": "^3.3.1",
diff --git a/packages/core/package.json b/packages/core/package.json
index 455ef522..177b6035 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@stoplight/spectral-core",
-  "version": "1.2.0",
+  "version": "1.3.0",
   "main": "dist/index.js",
   "types": "dist/index.d.ts",
   "sideEffects": false,
@@ -28,7 +28,7 @@
     "@stoplight/path": "1.3.2",
     "@stoplight/spectral-parsers": "^1.0.0",
     "@stoplight/spectral-ref-resolver": "*",
-    "@stoplight/spectral-runtime": "*",
+    "@stoplight/spectral-runtime": "^1.0.0",
     "@stoplight/types": "12.3.0",
     "ajv": "~8.6.0",
     "ajv-errors": "~3.0.0",
diff --git a/packages/core/src/ruleset/rule/rule.ts b/packages/core/src/ruleset/rule/rule.ts
index c749d787..c371abbf 100644
--- a/packages/core/src/ruleset/rule/rule.ts
+++ b/packages/core/src/ruleset/rule/rule.ts
@@ -10,6 +10,7 @@ import { Ruleset } from '../ruleset';
 import { Format } from '../format';
 import { HumanReadableDiagnosticSeverity, IRuleThen, RuleDefinition } from '../types';
 import { minimatch } from '../utils/minimatch';
+import { printValue } from '@stoplight/spectral-runtime';
 
 const ALIAS = /^#([A-Za-z0-9_-]+)/;
 
@@ -26,6 +27,13 @@ export interface IRule {
   given: string[];
 }
 
+export type StringifiedRule = Omit<IRule, 'formats' | 'then'> & {
+  name: string;
+  formats: string[] | null;
+  then: (Pick<IRuleThen, 'field'> & { function: string; functionOptions?: string })[];
+  owner: number;
+};
+
 export class Rule implements IRule {
   public description: string | null;
   public message: string | null;
@@ -218,6 +226,27 @@ export class Rule implements IRule {
       };
     }
   }
+
+  public toJSON(): StringifiedRule {
+    return {
+      name: this.name,
+      recommended: this.recommended,
+      enabled: this.enabled,
+      description: this.description,
+      message: this.message,
+      documentationUrl: this.documentationUrl,
+      severity: this.severity,
+      resolved: this.resolved,
+      formats: this.formats === null ? null : Array.from(this.formats).map(fn => fn.displayName ?? fn.name),
+      then: this.then.map(then => ({
+        ...then.function,
+        function: then.function.name,
+        ...('functionOptions' in then ? { functionOptions: printValue(then.functionOptions) } : null),
+      })),
+      given: this.#given,
+      owner: this.owner.id,
+    };
+  }
 }
 
 function stub(): void {
diff --git a/packages/core/src/ruleset/ruleset.ts b/packages/core/src/ruleset/ruleset.ts
index a4fc1560..117a4e77 100644
--- a/packages/core/src/ruleset/ruleset.ts
+++ b/packages/core/src/ruleset/ruleset.ts
@@ -1,6 +1,6 @@
 import { dirname, relative } from '@stoplight/path';
 import { minimatch } from './utils/minimatch';
-import { Rule } from './rule/rule';
+import { Rule, StringifiedRule } from './rule/rule';
 import {
   FileRulesetSeverityDefinition,
   ParserOptions,
@@ -25,8 +25,23 @@ type RulesetContext = {
   readonly [STACK_SYMBOL]?: Map<RulesetDefinition, Ruleset>;
 };
 
+let SEED = 1;
+
+export type StringifiedRuleset = {
+  id: number;
+  extends: StringifiedRuleset[] | null;
+  source: string | null;
+  aliases: RulesetAliasesDefinition | null;
+  formats: string[] | null;
+  rules: Record<string, StringifiedRule>;
+  overrides: RulesetOverridesDefinition | null;
+  parserOptions: ParserOptions;
+};
+
 export class Ruleset {
-  protected extends!: Ruleset[];
+  public readonly id = SEED++;
+
+  protected readonly extends: Ruleset[] | null;
   public readonly formats = new Set<Format>();
   public readonly overrides: RulesetOverridesDefinition | null;
   public readonly aliases: RulesetAliasesDefinition | null;
@@ -84,7 +99,7 @@ export class Ruleset {
             },
             [],
           )
-        : [];
+        : null;
 
     if (stack.size === 1 && definition.overrides) {
       this.overrides = definition.overrides;
@@ -100,9 +115,11 @@ export class Ruleset {
       }
     }
 
-    for (const { formats } of this.extends) {
-      for (const format of formats) {
-        this.formats.add(format);
+    if (Array.isArray(this.extends)) {
+      for (const { formats } of this.extends) {
+        for (const format of formats) {
+          this.formats.add(format);
+        }
       }
     }
 
@@ -220,7 +237,7 @@ export class Ruleset {
   #getRules(): Record<string, Rule> {
     const rules: Record<string, Rule> = {};
 
-    if (this.extends.length > 0) {
+    if (this.extends !== null && this.extends.length > 0) {
       for (const extendedRuleset of this.extends) {
         if (extendedRuleset === this) continue;
         for (const rule of Object.values(extendedRuleset.rules)) {
@@ -265,4 +282,20 @@ export class Ruleset {
   public static isDefaultRulesetFile(uri: string): boolean {
     return DEFAULT_RULESET_FILE.test(uri);
   }
+
+  public toJSON(): Omit<StringifiedRuleset, 'extends' | 'rules'> & {
+    extends: Ruleset['extends'];
+    rules: Ruleset['rules'];
+  } {
+    return {
+      id: this.id,
+      extends: this.extends,
+      source: this.source,
+      aliases: this.aliases,
+      formats: this.formats.size === 0 ? null : Array.from(this.formats).map(fn => fn.displayName ?? fn.name),
+      rules: this.rules,
+      overrides: this.overrides,
+      parserOptions: this.parserOptions,
+    };
+  }
 }
diff --git a/packages/formats/package.json b/packages/formats/package.json
index 5af7efdc..83095cc9 100644
--- a/packages/formats/package.json
+++ b/packages/formats/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@stoplight/spectral-formats",
-  "version": "1.0.0",
+  "version": "1.0.1",
   "sideEffects": false,
   "homepage": "https://github.com/stoplightio/spectral",
   "bugs": "https://github.com/stoplightio/spectral/issues",
diff --git a/packages/formats/src/asyncapi.ts b/packages/formats/src/asyncapi.ts
index 17f25e1c..8a1a6dde 100644
--- a/packages/formats/src/asyncapi.ts
+++ b/packages/formats/src/asyncapi.ts
@@ -20,3 +20,5 @@ export const asyncApi2: Format = document => {
 };
 
 asyncApi2.displayName = 'AsyncAPI 2.x';
+
+export { asyncApi2 as asyncapi2 };
diff --git a/packages/functions/package.json b/packages/functions/package.json
index 077b5796..9a41d984 100644
--- a/packages/functions/package.json
+++ b/packages/functions/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@stoplight/spectral-functions",
-  "version": "1.1.0",
+  "version": "1.1.1",
   "sideEffects": false,
   "homepage": "https://github.com/stoplightio/spectral",
   "bugs": "https://github.com/stoplightio/spectral/issues",
diff --git a/packages/ref-resolver/package.json b/packages/ref-resolver/package.json
index 28de00ec..a0e5b8b9 100644
--- a/packages/ref-resolver/package.json
+++ b/packages/ref-resolver/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@stoplight/spectral-ref-resolver",
-  "version": "0.0.0",
+  "version": "1.0.0",
   "homepage": "https://github.com/stoplightio/spectral",
   "bugs": "https://github.com/stoplightio/spectral/issues",
   "author": "Stoplight <support@stoplight.io>",
diff --git a/packages/ruleset-migrator/README.md b/packages/ruleset-migrator/README.md
index d1263596..09c57edc 100644
--- a/packages/ruleset-migrator/README.md
+++ b/packages/ruleset-migrator/README.md
@@ -91,3 +91,13 @@ migrateRuleset(path.join(__dirname, "spectral.json"), {
   format: "commonjs", // esm available too, but not recommended for now
 }).then(fs.promises.writeFile.bind(fs.promises, path.join(__dirname, ".spectral.js")));
 ```
+
+### Caveats
+
+#### YAML/JSON extending JS rulesets
+
+It's not disallowed for a YAML/JSON ruleset to extend another ruleset expressed in JS,
+albeit one has to notice that the formats have to be matched.
+In other words if you select "commonjs" as your target format, your extended rulesets have to follow,
+otherwise Node.js or any other tool you use in between may refuse to load the resulting code.
+If that happens to be the limitation for you, try your luck with [esm](https://www.npmjs.com/package/esm), [Babel](https://babeljs.io/) or a bundler such as [rollup.js](https://rollupjs.org/).
diff --git a/packages/ruleset-migrator/package.json b/packages/ruleset-migrator/package.json
index 229b4fca..2eeae3f8 100644
--- a/packages/ruleset-migrator/package.json
+++ b/packages/ruleset-migrator/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@stoplight/spectral-ruleset-migrator",
-  "version": "1.1.0",
+  "version": "1.4.0",
   "homepage": "https://github.com/stoplightio/spectral",
   "bugs": "https://github.com/stoplightio/spectral/issues",
   "author": "Stoplight <support@stoplight.io>",
@@ -31,11 +31,13 @@
     "@types/node": "*",
     "ajv": "^8.6.0",
     "ast-types": "0.14.2",
-    "astring": "^1.7.5"
+    "astring": "^1.7.5",
+    "reserved": "0.1.2"
   },
   "devDependencies": {
     "@stoplight/spectral-core": "^1.1.0",
     "@stoplight/spectral-rulesets": "^1.1.0",
+    "fetch-mock": "^9.11.0",
     "json-schema-to-typescript": "^10.1.4",
     "memfs": "^3.2.2",
     "prettier": "^2.3.2"
diff --git a/packages/ruleset-migrator/scripts/compile-schemas.ts b/packages/ruleset-migrator/scripts/compile-schemas.ts
index 9901985a..e8beda4f 100644
--- a/packages/ruleset-migrator/scripts/compile-schemas.ts
+++ b/packages/ruleset-migrator/scripts/compile-schemas.ts
@@ -1,10 +1,11 @@
 import * as fs from 'fs';
 import * as path from '@stoplight/path';
 import { compile } from 'json-schema-to-typescript';
+import type { JSONSchema4 } from 'json-schema';
 
 import schema from '../src/validation/schema';
 
-compile(schema, 'Ruleset', {
+compile(<JSONSchema4>schema, 'Ruleset', {
   bannerComment: '/*eslint-disable*/',
   style: {
     singleQuote: true,
diff --git a/packages/ruleset-migrator/src/index.ts b/packages/ruleset-migrator/src/index.ts
index 0e1ace3a..d6fc8c8d 100644
--- a/packages/ruleset-migrator/src/index.ts
+++ b/packages/ruleset-migrator/src/index.ts
@@ -1,8 +1,8 @@
 import { parseWithPointers as parseJsonWithPointers, safeStringify } from '@stoplight/json';
 import { parseWithPointers as parseYamlWithPointers } from '@stoplight/yaml';
+import { fetch as defaultFetch } from '@stoplight/spectral-runtime';
 import { dirname, extname, isURL } from '@stoplight/path';
-import { fetch } from '@stoplight/spectral-runtime';
-import { Hook, MigrationOptions, TransformerCtx } from './types';
+import { Fetch, Hook, MigrationOptions, TransformerCtx } from './types';
 import transformers from './transformers';
 import { Scope, Tree } from './tree';
 import { builders as b, namedTypes } from 'ast-types';
@@ -11,7 +11,7 @@ import { assertRuleset } from './validation';
 import requireResolve from './requireResolve';
 import { Ruleset } from './validation/types';
 
-async function read(filepath: string, fs: MigrationOptions['fs']): Promise<Ruleset> {
+async function read(filepath: string, fs: MigrationOptions['fs'], fetch: Fetch): Promise<Ruleset> {
   const input = isURL(filepath)
     ? await (await fetch(filepath)).text()
     : await fs.promises.readFile(requireResolve?.(filepath) ?? filepath, 'utf8');
@@ -25,22 +25,22 @@ async function read(filepath: string, fs: MigrationOptions['fs']): Promise<Rules
 }
 
 export async function migrateRuleset(filepath: string, opts: MigrationOptions): Promise<string> {
-  const { fs, format, npmRegistry, scope = new Scope() } = opts;
+  const { fs, fetch = defaultFetch, format, npmRegistry } = opts;
   const cwd = dirname(filepath);
   const tree = new Tree({
     cwd,
     format,
     npmRegistry,
-    scope,
+    scope: new Scope(),
   });
 
-  const ruleset = await read(filepath, fs);
+  const ruleset = await read(filepath, fs, fetch);
   const hooks = new Set<Hook>();
   const ctx: TransformerCtx = {
     cwd,
     tree,
     opts: {
-      scope,
+      fetch,
       ...opts,
     },
     hooks,
@@ -98,6 +98,6 @@ async function _process(input: unknown, hooks: Set<Hook>, path: string): Promise
   );
 }
 
-export async function process(input: unknown, hooks: Set<Hook>): Promise<namedTypes.ObjectExpression> {
+export async function process(input: Ruleset, hooks: Set<Hook>): Promise<namedTypes.ObjectExpression> {
   return (await _process(input, hooks, '')) as namedTypes.ObjectExpression;
 }
diff --git a/packages/ruleset-migrator/src/transformers/extends.ts b/packages/ruleset-migrator/src/transformers/extends.ts
index 2afb1163..e94044cc 100644
--- a/packages/ruleset-migrator/src/transformers/extends.ts
+++ b/packages/ruleset-migrator/src/transformers/extends.ts
@@ -10,6 +10,8 @@ const REPLACEMENTS = {
   'spectral:asyncapi': 'asyncapi',
 };
 
+const KNOWN_JS_EXTS = /^\.[cm]?js$/;
+
 export { transformer as default };
 
 async function processExtend(
@@ -21,11 +23,19 @@ async function processExtend(
   }
 
   const filepath = ctx.tree.resolveModule(name);
+
+  if (KNOWN_JS_EXTS.test(path.extname(filepath))) {
+    return ctx.tree.addImport(`${path.basename(filepath, true)}_${path.extname(filepath)}`, filepath, true);
+  }
+
   const existingCwd = ctx.cwd;
+  const scope = ctx.tree.scope;
   try {
     ctx.cwd = path.dirname(filepath);
-    return await process(await ctx.read(filepath, ctx.opts.fs), ctx.hooks);
+    ctx.tree.scope = ctx.tree.scope.fork();
+    return await process(await ctx.read(filepath, ctx.opts.fs, ctx.opts.fetch), ctx.hooks);
   } finally {
+    ctx.tree.scope = scope;
     ctx.cwd = existingCwd;
   }
 }
@@ -41,18 +51,17 @@ const transformer: Transformer = function (ctx) {
       }
 
       assertArray(_extends);
+      const extendedRulesets: (namedTypes.ArrayExpression | namedTypes.ObjectExpression | namedTypes.Identifier)[] = [];
+
+      for (const ruleset of _extends) {
+        if (typeof ruleset === 'string') {
+          extendedRulesets.push(await processExtend(ctx, ruleset));
+        } else {
+          extendedRulesets.push(b.arrayExpression([await processExtend(ctx, ruleset[0]), b.literal(ruleset[1])]));
+        }
+      }
 
-      return b.arrayExpression(
-        await Promise.all(
-          _extends.map(async ruleset => {
-            if (typeof ruleset === 'string') {
-              return await processExtend(ctx, ruleset);
-            }
-
-            return b.arrayExpression([await processExtend(ctx, ruleset[0]), b.literal(ruleset[1])]);
-          }),
-        ),
-      );
+      return b.arrayExpression(extendedRulesets);
     },
   ]);
 };
diff --git a/packages/ruleset-migrator/src/transformers/formats.ts b/packages/ruleset-migrator/src/transformers/formats.ts
index ffaf6dbd..ffb5ade2 100644
--- a/packages/ruleset-migrator/src/transformers/formats.ts
+++ b/packages/ruleset-migrator/src/transformers/formats.ts
@@ -2,32 +2,34 @@ import { builders as b, namedTypes } from 'ast-types';
 import { Transformer, TransformerCtx } from '../types';
 import { assertArray, assertString } from '../validation';
 
-const REPLACEMENTS = [
-  'oas2',
-  'oas3',
-  'oas3.0',
-  'oas3.1',
-  'asyncapi2',
-  'json-schema',
-  'json-schema-loose',
-  'json-schema-draft4',
-  'json-schema-draft6',
-  'json-schema-draft7',
-  'json-schema-2019-09',
-  'json-schema-2020-12',
-].reduce((replacements, id) => {
-  replacements[id] = id.replace(/\./g, '_').replace(/-([a-z])/g, (match, char) => String(char).toUpperCase());
-  return replacements;
-}, {});
+import schema from '../validation/schema';
+
+const ALIASES: Record<string, string> = {
+  'json-schema-2019-09': 'json-schema-draft-2019-09',
+  'json-schema-2020-12': 'json-schema-draft-2020-12',
+};
+
+const REPLACEMENTS = Object.fromEntries(
+  schema.properties.formats.items.enum.map(format => [
+    format,
+    (ALIASES[format] ?? format)
+      .replace(/\.|(?<=[0-9])-(?=[0-9])/g, '_')
+      .replace(/-([0-9a-z])/g, (match, char) => String(char).toUpperCase()),
+  ]),
+);
 
 function transform(ctx: TransformerCtx, input: unknown): namedTypes.ArrayExpression {
   assertArray(input);
 
   return b.arrayExpression(
-    input.map(format => {
-      assertString(format);
-      return ctx.tree.addImport(REPLACEMENTS[format], '@stoplight/spectral-formats');
-    }),
+    Array.from(
+      new Set(
+        input.map(format => {
+          assertString(format);
+          return ctx.tree.addImport(REPLACEMENTS[format], '@stoplight/spectral-formats');
+        }),
+      ),
+    ),
   );
 }
 
diff --git a/packages/ruleset-migrator/src/transformers/functions.ts b/packages/ruleset-migrator/src/transformers/functions.ts
index a9b78c81..7549f3fa 100644
--- a/packages/ruleset-migrator/src/transformers/functions.ts
+++ b/packages/ruleset-migrator/src/transformers/functions.ts
@@ -1,5 +1,4 @@
 import * as path from '@stoplight/path';
-
 import type { Transformer } from '../types';
 import { assertArray, assertString } from '../validation';
 import { Ruleset } from '../validation/types';
@@ -27,7 +26,9 @@ const transformer: Transformer = function (ctx) {
 
       for (const fn of value) {
         assertString(fn);
-        ctx.tree.addImport(path.basename(fn, true), fn, true);
+        const fnName = path.basename(fn, true);
+        const identifier = ctx.tree.addImport(fnName, fn, true);
+        ctx.tree.scope.store(`function-${fnName}`, identifier.name);
       }
 
       return null;
diff --git a/packages/ruleset-migrator/src/transformers/rules.ts b/packages/ruleset-migrator/src/transformers/rules.ts
index 82401d46..7d70fe9f 100644
--- a/packages/ruleset-migrator/src/transformers/rules.ts
+++ b/packages/ruleset-migrator/src/transformers/rules.ts
@@ -57,12 +57,15 @@ const transformer: Transformer = function (ctx) {
 
   ctx.hooks.add([
     /^\/rules\/[^/]+\/then\/(?:[0-9]+\/)?function$/,
-    (value): namedTypes.Identifier => {
+    (value): namedTypes.Identifier | namedTypes.UnaryExpression => {
       assertString(value);
 
-      return KNOWN_FUNCTIONS.includes(value)
-        ? ctx.tree.addImport(value, '@stoplight/spectral-functions')
-        : b.identifier(value);
+      if (KNOWN_FUNCTIONS.includes(value)) {
+        return ctx.tree.addImport(value, '@stoplight/spectral-functions');
+      }
+
+      const alias = ctx.tree.scope.load(`function-${value}`);
+      return alias !== void 0 ? b.identifier(alias) : b.unaryExpression('void', b.literal(0));
     },
   ]);
 };
diff --git a/packages/ruleset-migrator/src/tree/index.ts b/packages/ruleset-migrator/src/tree/index.ts
index 48a3376f..27c3cd86 100644
--- a/packages/ruleset-migrator/src/tree/index.ts
+++ b/packages/ruleset-migrator/src/tree/index.ts
@@ -6,8 +6,9 @@ import { commonjs } from './commonjs';
 import { esm } from './esm';
 import { IModule } from './types';
 import requireResolve from '../requireResolve';
+import { Scope } from './scope';
 
-export class Scope extends Set<string> {}
+export { Scope };
 
 export class Tree {
   readonly #importDeclarations = new Map<
@@ -20,7 +21,7 @@ export class Tree {
   readonly #cwd: string;
 
   public ruleset?: namedTypes.ObjectExpression;
-  public readonly scope: Scope;
+  public scope: Scope;
 
   constructor({
     cwd,
@@ -37,12 +38,13 @@ export class Tree {
   addImport(specifier: string, source: string, _default = false): namedTypes.Identifier {
     const existingImportDeclaration = this.#importDeclarations.get(source);
 
+    const scope = source.startsWith('@stoplight/') ? this.scope.global : this.scope;
+
     if (existingImportDeclaration === void 0) {
-      const identifier = this.identifier(specifier);
+      const identifier = Tree.identifier(specifier, scope);
       this.#importDeclarations.set(source, [
         { imported: b.identifier(specifier), local: identifier, default: _default },
       ]);
-      this.scope.add(specifier);
       return identifier;
     } else {
       for (const declaration of existingImportDeclaration) {
@@ -51,8 +53,7 @@ export class Tree {
         }
       }
 
-      const identifier = this.identifier(specifier);
-      this.scope.add(specifier);
+      const identifier = Tree.identifier(specifier, scope);
       existingImportDeclaration.push({ imported: identifier, local: identifier, default: _default });
       return identifier;
     }
@@ -84,7 +85,7 @@ export class Tree {
               : <namedTypes.ImportDeclaration[]>[]),
             ...identifiers
               .filter(({ default: _default }) => _default)
-              .flatMap(({ imported }) => this.#module.importDefaultDeclaration(imported, resolvedSource)),
+              .flatMap(({ local }) => this.#module.importDefaultDeclaration(local, resolvedSource)),
           ];
         }),
         this.#module.exportDefaultDeclaration(this.ruleset),
@@ -93,13 +94,16 @@ export class Tree {
     );
   }
 
-  public identifier(name: string): namedTypes.Identifier {
-    let uniqName = name;
+  public static identifier(name: string, scope: Scope): namedTypes.Identifier {
+    const baseName = name.replace(/[^$_0-9A-Za-z]/g, '').replace(/^([0-9])/, '_$1');
+    let uniqName = baseName;
     let i = 0;
-    while (this.scope.has(uniqName)) {
-      uniqName = `${name}_${i++}`;
+    while (scope.has(uniqName)) {
+      uniqName = `${baseName}$${i++}`;
     }
 
+    scope.add(uniqName);
+
     return b.identifier(uniqName);
   }
 
diff --git a/packages/ruleset-migrator/src/tree/scope.ts b/packages/ruleset-migrator/src/tree/scope.ts
new file mode 100644
index 00000000..7e262c35
--- /dev/null
+++ b/packages/ruleset-migrator/src/tree/scope.ts
@@ -0,0 +1,56 @@
+import * as reserved from 'reserved';
+
+const REGISTERED_WORDS = [
+  ...reserved,
+  'await', // not listed among reserved
+
+  // NodeJS CJS
+  'require',
+  'module',
+  '__dirname',
+  '__filename',
+
+  // Our own interop
+  '_interopDefault',
+];
+
+let i = 0;
+
+export class Scope {
+  public readonly id;
+  readonly #parentScope: Scope | null;
+  readonly #uniqueIdentifiers: Set<string>;
+  readonly #aliases: Map<string, string>;
+
+  constructor(parentScope: Scope | null = null) {
+    this.id = i++;
+    this.#parentScope = parentScope;
+    this.#uniqueIdentifiers = parentScope === null ? new Set(REGISTERED_WORDS) : new Set<string>();
+    this.#aliases = new Map();
+  }
+
+  get global(): Scope {
+    return this.#parentScope === null ? this : this.#parentScope.global;
+  }
+
+  has(local: string): boolean {
+    return this.#parentScope?.has(local) ?? this.#uniqueIdentifiers.has(local);
+  }
+
+  add(imported: string): void {
+    this.#uniqueIdentifiers.add(imported);
+    this.#parentScope?.add(imported);
+  }
+
+  store(name: string, imported: string): void {
+    this.#aliases.set(name, imported);
+  }
+
+  load(name: string): string | undefined {
+    return this.#aliases.get(name);
+  }
+
+  fork(): Scope {
+    return new Scope(this);
+  }
+}
diff --git a/packages/ruleset-migrator/src/types.ts b/packages/ruleset-migrator/src/types.ts
index 67a42f42..ee902a2c 100644
--- a/packages/ruleset-migrator/src/types.ts
+++ b/packages/ruleset-migrator/src/types.ts
@@ -1,16 +1,20 @@
-import type { Tree, Scope } from './tree';
+/// <reference lib="dom" />
+
+import type { Tree } from './tree';
 import type { ExpressionKind } from 'ast-types/gen/kinds';
 import { Ruleset } from './validation/types';
 
+export type Fetch = Window['fetch'] | typeof import('@stoplight/spectral-runtime').fetch;
+
 export type MigrationOptions = {
   fs: {
     promises: {
       readFile: typeof import('fs').promises.readFile;
     };
   };
+  fetch?: Fetch;
   npmRegistry?: string;
   format?: 'esm' | 'commonjs';
-  scope?: Scope;
 };
 
 export type Hook = [
@@ -22,8 +26,10 @@ export type Transformer = (ctx: TransformerCtx) => void;
 
 export type TransformerCtx = {
   readonly tree: Tree;
-  readonly opts: MigrationOptions;
+  readonly opts: MigrationOptions & {
+    fetch: Fetch;
+  };
   readonly hooks: Set<Hook>;
   cwd: string;
-  read(filepath: string, fs: MigrationOptions['fs']): Promise<Ruleset>;
+  read(filepath: string, fs: MigrationOptions['fs'], fetch: Fetch): Promise<Ruleset>;
 };
diff --git a/packages/ruleset-migrator/src/validation/schema.ts b/packages/ruleset-migrator/src/validation/schema.ts
index 5c82ed51..be57b3ab 100644
--- a/packages/ruleset-migrator/src/validation/schema.ts
+++ b/packages/ruleset-migrator/src/validation/schema.ts
@@ -1,8 +1,6 @@
-import type { JSONSchema4 } from 'json-schema';
-
 export { schema as default };
 
-const schema: JSONSchema4 = {
+const schema = {
   $schema: 'http://json-schema.org/draft-07/schema#',
   type: 'object',
   properties: {
@@ -59,7 +57,9 @@ const schema: JSONSchema4 = {
           'json-schema-draft4',
           'json-schema-draft6',
           'json-schema-draft7',
+          'json-schema-draft-2019-09',
           'json-schema-2019-09',
+          'json-schema-draft-2020-12',
           'json-schema-2020-12',
         ],
       },
diff --git a/packages/ruleset-migrator/src/validation/types.ts b/packages/ruleset-migrator/src/validation/types.ts
index 401691fa..4e687184 100644
--- a/packages/ruleset-migrator/src/validation/types.ts
+++ b/packages/ruleset-migrator/src/validation/types.ts
@@ -16,7 +16,9 @@ export interface Ruleset {
     | 'json-schema-draft4'
     | 'json-schema-draft6'
     | 'json-schema-draft7'
+    | 'json-schema-draft-2019-09'
     | 'json-schema-2019-09'
+    | 'json-schema-draft-2020-12'
     | 'json-schema-2020-12'
   )[];
   functions?: string[];
@@ -33,7 +35,9 @@ export interface Ruleset {
       | 'json-schema-draft4'
       | 'json-schema-draft6'
       | 'json-schema-draft7'
+      | 'json-schema-draft-2019-09'
       | 'json-schema-2019-09'
+      | 'json-schema-draft-2020-12'
       | 'json-schema-2020-12'
     )[];
     [k: string]: unknown;
diff --git a/packages/ruleset-migrator/tsconfig.build.json b/packages/ruleset-migrator/tsconfig.build.json
index e4d27ceb..1049bfe9 100644
--- a/packages/ruleset-migrator/tsconfig.build.json
+++ b/packages/ruleset-migrator/tsconfig.build.json
@@ -1,6 +1,6 @@
 {
   "extends": "../../tsconfig.build.json",
-  "include": ["src"],
+  "include": ["src", "types"],
   "compilerOptions": {
     "composite": true,
     "rootDir": "src",
diff --git a/packages/ruleset-migrator/types/reserved.d.ts b/packages/ruleset-migrator/types/reserved.d.ts
new file mode 100644
index 00000000..8ea54317
--- /dev/null
+++ b/packages/ruleset-migrator/types/reserved.d.ts
@@ -0,0 +1,7 @@
+declare module 'reserved' {
+  export declare const reserved: string[];
+  export declare const builtins: string[];
+
+  declare const reserved: string[];
+  export = reserved;
+}
diff --git a/packages/rulesets/package.json b/packages/rulesets/package.json
index cef45894..e1df5f57 100644
--- a/packages/rulesets/package.json
+++ b/packages/rulesets/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@stoplight/spectral-rulesets",
-  "version": "1.2.0",
+  "version": "1.2.1",
   "homepage": "https://github.com/stoplightio/spectral",
   "bugs": "https://github.com/stoplightio/spectral/issues",
   "author": "Stoplight <support@stoplight.io>",
diff --git a/packages/runtime/package.json b/packages/runtime/package.json
index 38fd85dc..689ba2db 100644
--- a/packages/runtime/package.json
+++ b/packages/runtime/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@stoplight/spectral-runtime",
-  "version": "0.0.0",
+  "version": "1.1.0",
   "homepage": "https://github.com/stoplightio/spectral",
   "bugs": "https://github.com/stoplightio/spectral/issues",
   "author": "Stoplight <support@stoplight.io>",
diff --git a/yarn.lock b/yarn.lock
index 6eda9dea..512f026d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2106,12 +2106,7 @@
     "@types/node" "*"
     form-data "^3.0.0"
 
-"@types/node@*", "@types/node@>=10.0.0":
-  version "14.14.31"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055"
-  integrity sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g==
-
-"@types/node@^15.12.4":
+"@types/node@*", "@types/node@>=10.0.0", "@types/node@^15.12.4":
   version "15.12.4"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.4.tgz#e1cf817d70a1e118e81922c4ff6683ce9d422e26"
   integrity sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA==
@@ -2166,10 +2161,10 @@
   resolved "https://registry.yarnpkg.com/@types/urijs/-/urijs-1.19.14.tgz#1c714cfbc23eb9e3f840dd02b7e8284ab85f1a6b"
   integrity sha512-Ds9OMd4xZqI2zZtoOicASAi0SvFPyNPgkfgPrPeUTQwcJOX1w6Mwkpq8ClI4ZP11nsEI6akvKqRDV+epA8yzRw==
 
-"@types/xml2js@^0.4.8":
-  version "0.4.8"
-  resolved "https://registry.yarnpkg.com/@types/xml2js/-/xml2js-0.4.8.tgz#84c120c864a5976d0b5cf2f930a75d850fc2b03a"
-  integrity sha512-EyvT83ezOdec7BhDaEcsklWy7RSIdi6CNe95tmOAK0yx/Lm30C9K75snT3fYayK59ApC2oyW+rcHErdG05FHJA==
+"@types/xml2js@^0.4.9":
+  version "0.4.9"
+  resolved "https://registry.yarnpkg.com/@types/xml2js/-/xml2js-0.4.9.tgz#a38267d8c2fe121c96922b12ee3bd89a58a6e20e"
+  integrity sha512-CHiCKIihl1pychwR2RNX5mAYmJDACgFVCMT5OArMaO3erzwXVcBqPcusr+Vl8yeeXukxZqtF8mZioqX+mpjjdw==
   dependencies:
     "@types/node" "*"
 
@@ -2399,9 +2394,9 @@ ajv@^8.0.0, ajv@^8.0.1:
     uri-js "^4.2.2"
 
 ajv@^8.6.0, ajv@~8.6.0:
-  version "8.6.0"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.0.tgz#60cc45d9c46a477d80d92c48076d972c342e5720"
-  integrity sha512-cnUG4NSBiM4YFBxgZIj/In3/6KX+rQ2l2YPRVcvAMQGWEPKuXoPIhxzwqh31jA3IPbI4qEOp/5ILI4ynioXsGQ==
+  version "8.6.1"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.1.tgz#ae65764bf1edde8cd861281cda5057852364a295"
+  integrity sha512-42VLtQUOLefAvKFAQIxIZDaThq6om/PrfP0CYk3/vn+y4BMNkKnbli8ON2QCiHov4KkzOSJ/xSoBJdayiiYvVQ==
   dependencies:
     fast-deep-equal "^3.1.1"
     json-schema-traverse "^1.0.0"
@@ -9105,6 +9100,11 @@ requires-port@^1.0.0:
   resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
   integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
 
+reserved@0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/reserved/-/reserved-0.1.2.tgz#707b1246a3269f755da7cfcf9af6f4983bef105c"
+  integrity sha1-cHsSRqMmn3Vdp8/Pmvb0mDvvEFw=
+
 resolve-cwd@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
@@ -10362,10 +10362,10 @@ typedarray-to-buffer@^3.1.5:
   dependencies:
     is-typedarray "^1.0.0"
 
-typescript@^4.3.2:
-  version "4.3.4"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.4.tgz#3f85b986945bcf31071decdd96cf8bfa65f9dcbc"
-  integrity sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==
+typescript@^4.3.5:
+  version "4.3.5"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4"
+  integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==
 
 ua-parser-js@^0.7.23:
   version "0.7.24"


Test patch:
diff --git a/packages/core/src/ruleset/__tests__/__fixtures__/overrides/serializing.ts b/packages/core/src/ruleset/__tests__/__fixtures__/overrides/serializing.ts
new file mode 100644
index 00000000..534ce48a
--- /dev/null
+++ b/packages/core/src/ruleset/__tests__/__fixtures__/overrides/serializing.ts
@@ -0,0 +1,34 @@
+import _base from './_base';
+import { RulesetDefinition } from '../../../types';
+
+export { ruleset as default };
+
+const ruleset: RulesetDefinition = {
+  rules: {
+    ..._base.rules,
+  },
+  overrides: [
+    {
+      files: ['legacy/**/*.json'],
+      rules: {
+        'description-matches-stoplight': 'off',
+        'title-matches-stoplight': 'warn',
+        'contact-name-matches-stoplight': true,
+      },
+    },
+    {
+      files: ['v2/**/*.json'],
+      rules: {
+        'description-matches-stoplight': 'error',
+        'title-matches-stoplight': 'hint',
+      },
+    },
+    {
+      files: ['**/*.json'],
+      rules: {
+        'description-matches-stoplight': 'info',
+        'title-matches-stoplight': 'off',
+      },
+    },
+  ],
+};
diff --git a/packages/core/src/ruleset/__tests__/ruleset.test.ts b/packages/core/src/ruleset/__tests__/ruleset.test.ts
index db9ed0d9..523387ce 100644
--- a/packages/core/src/ruleset/__tests__/ruleset.test.ts
+++ b/packages/core/src/ruleset/__tests__/ruleset.test.ts
@@ -19,21 +19,21 @@ describe('Ruleset', () => {
     }
 
     it('given ruleset with extends set to recommended, should enable recommended rules', async () => {
-      const { rules } = await loadRuleset(import('./__fixtures__/severity/recommended'));
-      expect(Object.keys(rules)).toEqual([
+      const ruleset = await loadRuleset(import('./__fixtures__/severity/recommended'));
+      expect(Object.keys(ruleset.rules)).toEqual([
         'description-matches-stoplight',
         'title-matches-stoplight',
         'contact-name-matches-stoplight',
         'overridable-rule',
       ]);
 
-      expect(getEnabledRules(rules)).toEqual([
+      expect(getEnabledRules(ruleset.rules)).toEqual([
         'description-matches-stoplight',
         'title-matches-stoplight',
         'overridable-rule',
       ]);
 
-      expect(rules).toStrictEqual((await loadRuleset(import('./__fixtures__/severity/implicit'))).rules);
+      expect(print(ruleset)).toEqual(print(await loadRuleset(import('./__fixtures__/severity/implicit'))));
     });
 
     it('given ruleset with extends set to all, should enable all rules but explicitly disabled', async () => {
@@ -203,6 +203,41 @@ describe('Ruleset', () => {
       └─ severity: 1
 `);
     });
+
+    it('should be serializable', async () => {
+      expect(JSON.parse(JSON.stringify(await loadRuleset(import('./__fixtures__/circularity/direct'))))).toEqual({
+        id: expect.any(Number),
+        source: null,
+        aliases: null,
+        extends: [],
+        formats: null,
+        overrides: null,
+        parserOptions: {
+          duplicateKeys: DiagnosticSeverity.Error,
+          incompatibleValues: DiagnosticSeverity.Error,
+        },
+        rules: {
+          'foo-rule': {
+            description: null,
+            documentationUrl: null,
+            enabled: true,
+            formats: null,
+            given: ['$'],
+            message: null,
+            name: 'foo-rule',
+            owner: expect.any(Number),
+            recommended: true,
+            resolved: true,
+            severity: 1,
+            then: [
+              {
+                function: 'falsy',
+              },
+            ],
+          },
+        },
+      });
+    });
   });
 
   describe('error handling', () => {
@@ -474,6 +509,105 @@ describe('Ruleset', () => {
 `);
     });
 
+    it('should be serializable', async () => {
+      const ruleset = await loadRuleset(import('./__fixtures__/overrides/serializing'), path.join(cwd, 'serializing'));
+
+      expect(JSON.parse(JSON.stringify(ruleset))).toEqual({
+        id: expect.any(Number),
+        source: path.join(cwd, 'serializing'),
+        aliases: null,
+        extends: null,
+        formats: null,
+        overrides: [
+          {
+            files: ['legacy/**/*.json'],
+            rules: {
+              'contact-name-matches-stoplight': true,
+              'description-matches-stoplight': 'off',
+              'title-matches-stoplight': 'warn',
+            },
+          },
+          {
+            files: ['v2/**/*.json'],
+            rules: {
+              'description-matches-stoplight': 'error',
+              'title-matches-stoplight': 'hint',
+            },
+          },
+          {
+            files: ['**/*.json'],
+            rules: {
+              'description-matches-stoplight': 'info',
+              'title-matches-stoplight': 'off',
+            },
+          },
+        ],
+        parserOptions: {
+          duplicateKeys: DiagnosticSeverity.Error,
+          incompatibleValues: DiagnosticSeverity.Error,
+        },
+        rules: {
+          'contact-name-matches-stoplight': {
+            description: null,
+            documentationUrl: null,
+            enabled: false,
+            formats: null,
+            given: ['$.info.contact'],
+            message: 'Contact name must contain Stoplight',
+            name: 'contact-name-matches-stoplight',
+            owner: expect.any(Number),
+            recommended: false,
+            resolved: true,
+            severity: DiagnosticSeverity.Warning,
+            then: [
+              {
+                function: 'pattern',
+                functionOptions: 'Object{}',
+              },
+            ],
+          },
+          'description-matches-stoplight': {
+            description: null,
+            documentationUrl: null,
+            enabled: true,
+            formats: null,
+            given: ['$.info'],
+            message: 'Description must contain Stoplight',
+            name: 'description-matches-stoplight',
+            owner: expect.any(Number),
+            recommended: true,
+            resolved: true,
+            severity: DiagnosticSeverity.Error,
+            then: [
+              {
+                function: 'pattern',
+                functionOptions: 'Object{}',
+              },
+            ],
+          },
+          'title-matches-stoplight': {
+            description: null,
+            documentationUrl: null,
+            enabled: true,
+            formats: null,
+            given: ['$.info'],
+            message: 'Title must contain Stoplight',
+            name: 'title-matches-stoplight',
+            owner: expect.any(Number),
+            recommended: true,
+            resolved: true,
+            severity: DiagnosticSeverity.Warning,
+            then: [
+              {
+                function: 'pattern',
+                functionOptions: 'Object{}',
+              },
+            ],
+          },
+        },
+      });
+    });
+
     it('should support new rule definitions', async () => {
       const ruleset = await loadRuleset(
         import('./__fixtures__/overrides/new-definitions'),
@@ -922,6 +1056,120 @@ describe('Ruleset', () => {
 `);
     });
 
+    it('should be serializable', () => {
+      expect(
+        JSON.parse(
+          JSON.stringify(
+            new Ruleset({
+              aliases: {
+                Info: '$.info',
+                PathItem: '$.paths[*][*]',
+                Description: '$..description',
+                Name: '$..name',
+              },
+
+              rules: {
+                'valid-path': {
+                  given: '#PathItem',
+                  then: {
+                    function: truthy,
+                  },
+                },
+
+                'valid-name-and-description': {
+                  given: ['#Name', '#Description'],
+                  then: {
+                    function: truthy,
+                  },
+                },
+
+                'valid-contact': {
+                  given: '#Info.contact',
+                  then: {
+                    function: truthy,
+                  },
+                },
+              },
+            }),
+          ),
+        ),
+      ).toEqual({
+        extends: null,
+        source: null,
+        id: expect.any(Number),
+        formats: null,
+        overrides: null,
+        parserOptions: {
+          duplicateKeys: DiagnosticSeverity.Error,
+          incompatibleValues: DiagnosticSeverity.Error,
+        },
+        aliases: {
+          Info: '$.info',
+          PathItem: '$.paths[*][*]',
+          Description: '$..description',
+          Name: '$..name',
+        },
+        rules: {
+          'valid-path': {
+            description: null,
+            documentationUrl: null,
+            enabled: true,
+            formats: null,
+            given: ['#PathItem'],
+            message: null,
+            name: 'valid-path',
+            owner: expect.any(Number),
+            recommended: true,
+            resolved: true,
+            severity: DiagnosticSeverity.Warning,
+            then: [
+              {
+                function: 'truthy',
+              },
+            ],
+          },
+
+          'valid-name-and-description': {
+            description: null,
+            documentationUrl: null,
+            enabled: true,
+            formats: null,
+            given: ['#Name', '#Description'],
+            message: null,
+            name: 'valid-name-and-description',
+            owner: expect.any(Number),
+            recommended: true,
+            resolved: true,
+            severity: DiagnosticSeverity.Warning,
+            then: [
+              {
+                function: 'truthy',
+              },
+            ],
+          },
+
+          'valid-contact': {
+            description: null,
+            documentationUrl: null,
+            enabled: true,
+            formats: null,
+            given: ['#Info.contact'],
+            message: null,
+            name: 'valid-contact',
+            owner: expect.any(Number),
+            recommended: true,
+            resolved: true,
+            severity: DiagnosticSeverity.Warning,
+            then: [
+              {
+                function: 'truthy',
+              },
+            ],
+          },
+        },
+      });
+    });
+
     it('should resolve nested aliases', () => {
       expect(
         print(
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-6/assets/ruleset.cjs b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-6/assets/ruleset.cjs
new file mode 100644
index 00000000..f8c3771e
--- /dev/null
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-6/assets/ruleset.cjs
@@ -0,0 +1,3 @@
+module.exports = {
+  rules: {},
+};
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-6/assets/ruleset.js b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-6/assets/ruleset.js
new file mode 100644
index 00000000..f8c3771e
--- /dev/null
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-6/assets/ruleset.js
@@ -0,0 +1,3 @@
+module.exports = {
+  rules: {},
+};
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-6/assets/ruleset.mjs b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-6/assets/ruleset.mjs
new file mode 100644
index 00000000..f8c3771e
--- /dev/null
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-6/assets/ruleset.mjs
@@ -0,0 +1,3 @@
+module.exports = {
+  rules: {},
+};
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-6/output.cjs b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-6/output.cjs
new file mode 100644
index 00000000..d143df98
--- /dev/null
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-6/output.cjs
@@ -0,0 +1,9 @@
+const ruleset_js = _interopDefault(require('/.tmp/spectral/extends-variant-6/assets/ruleset.js'));
+const ruleset_cjs = _interopDefault(require('/.tmp/spectral/extends-variant-6/assets/ruleset.cjs'));
+const ruleset_mjs = _interopDefault(require('/.tmp/spectral/extends-variant-6/assets/ruleset.mjs'));
+module.exports = {
+  extends: [ruleset_js, ruleset_cjs, ruleset_mjs],
+};
+function _interopDefault(ex) {
+  return ex && typeof ex === 'object' && 'default' in ex ? ex['default'] : ex;
+}
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-6/output.mjs b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-6/output.mjs
new file mode 100644
index 00000000..c24b17b9
--- /dev/null
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-6/output.mjs
@@ -0,0 +1,6 @@
+import ruleset_js from '/.tmp/spectral/extends-variant-6/assets/ruleset.js';
+import ruleset_cjs from '/.tmp/spectral/extends-variant-6/assets/ruleset.cjs';
+import ruleset_mjs from '/.tmp/spectral/extends-variant-6/assets/ruleset.mjs';
+export default {
+  extends: [ruleset_js, ruleset_cjs, ruleset_mjs],
+};
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-6/ruleset.yaml b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-6/ruleset.yaml
new file mode 100644
index 00000000..40644289
--- /dev/null
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-6/ruleset.yaml
@@ -0,0 +1,4 @@
+extends:
+  - ./assets/ruleset.js
+  - ./assets/ruleset.cjs
+  - ./assets/ruleset.mjs
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-7/assets/ruleset-2.yaml b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-7/assets/ruleset-2.yaml
new file mode 100644
index 00000000..2a61b40c
--- /dev/null
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-7/assets/ruleset-2.yaml
@@ -0,0 +1,9 @@
+formats:
+  - oas3.0
+functions:
+  - pascalCase
+rules:
+  my-rule:
+    given: $
+    then:
+      function: pascalCase
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-7/assets/ruleset-3.yaml b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-7/assets/ruleset-3.yaml
new file mode 100644
index 00000000..0a88d104
--- /dev/null
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-7/assets/ruleset-3.yaml
@@ -0,0 +1,8 @@
+functions:
+  - pascalCase
+functionsDir: ./fns
+rules:
+  my-rule:
+    given: $
+    then:
+      function: pascalCase
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-7/assets/ruleset.yaml b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-7/assets/ruleset.yaml
new file mode 100644
index 00000000..12b6b3f2
--- /dev/null
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-7/assets/ruleset.yaml
@@ -0,0 +1,11 @@
+formats:
+  - oas2
+  - oas3.1
+functions:
+  - pascalCase
+  - oas3
+rules:
+  my-rule:
+    given: $
+    then:
+      function: truthy
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-7/output.cjs b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-7/output.cjs
new file mode 100644
index 00000000..19f5e57d
--- /dev/null
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-7/output.cjs
@@ -0,0 +1,54 @@
+const { oas2: oas2, oas3: oas3, oas3_1: oas3_1, oas3_0: oas3_0 } = require('@stoplight/spectral-formats');
+const pascalCase = _interopDefault(require('/.tmp/spectral/extends-variant-7/functions/pascalCase.js'));
+const pascalCase$0 = _interopDefault(require('/.tmp/spectral/extends-variant-7/assets/functions/pascalCase.js'));
+const oas3$0 = _interopDefault(require('/.tmp/spectral/extends-variant-7/assets/functions/oas3.js'));
+const { truthy: truthy } = require('@stoplight/spectral-functions');
+const pascalCase$1 = _interopDefault(require('/.tmp/spectral/extends-variant-7/assets/fns/pascalCase.js'));
+module.exports = {
+  formats: [oas2, oas3],
+  extends: [
+    {
+      formats: [oas2, oas3_1],
+      rules: {
+        'my-rule': {
+          given: '$',
+          then: {
+            function: truthy,
+          },
+        },
+      },
+    },
+    {
+      formats: [oas3_0],
+      rules: {
+        'my-rule': {
+          given: '$',
+          then: {
+            function: pascalCase$0,
+          },
+        },
+      },
+    },
+    {
+      rules: {
+        'my-rule': {
+          given: '$',
+          then: {
+            function: pascalCase$1,
+          },
+        },
+      },
+    },
+  ],
+  rules: {
+    'my-rule': {
+      given: '$',
+      then: {
+        function: pascalCase,
+      },
+    },
+  },
+};
+function _interopDefault(ex) {
+  return ex && typeof ex === 'object' && 'default' in ex ? ex['default'] : ex;
+}
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-7/output.mjs b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-7/output.mjs
new file mode 100644
index 00000000..51d738f7
--- /dev/null
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-7/output.mjs
@@ -0,0 +1,51 @@
+import { oas2, oas3, oas3_1, oas3_0 } from '@stoplight/spectral-formats';
+import pascalCase from '/.tmp/spectral/extends-variant-7/functions/pascalCase.js';
+import pascalCase$0 from '/.tmp/spectral/extends-variant-7/assets/functions/pascalCase.js';
+import oas3$0 from '/.tmp/spectral/extends-variant-7/assets/functions/oas3.js';
+import { truthy } from '@stoplight/spectral-functions';
+import pascalCase$1 from '/.tmp/spectral/extends-variant-7/assets/fns/pascalCase.js';
+export default {
+  formats: [oas2, oas3],
+  extends: [
+    {
+      formats: [oas2, oas3_1],
+      rules: {
+        'my-rule': {
+          given: '$',
+          then: {
+            function: truthy,
+          },
+        },
+      },
+    },
+    {
+      formats: [oas3_0],
+      rules: {
+        'my-rule': {
+          given: '$',
+          then: {
+            function: pascalCase$0,
+          },
+        },
+      },
+    },
+    {
+      rules: {
+        'my-rule': {
+          given: '$',
+          then: {
+            function: pascalCase$1,
+          },
+        },
+      },
+    },
+  ],
+  rules: {
+    'my-rule': {
+      given: '$',
+      then: {
+        function: pascalCase,
+      },
+    },
+  },
+};
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-7/ruleset.yaml b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-7/ruleset.yaml
new file mode 100644
index 00000000..002bc40d
--- /dev/null
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/extends-variant-7/ruleset.yaml
@@ -0,0 +1,15 @@
+formats:
+  - oas2
+  - oas3
+extends:
+  - ./assets/ruleset.yaml
+  - ./assets/ruleset-2.yaml
+  - ./assets/ruleset-3.yaml
+functions:
+  - pascalCase
+rules:
+  my-rule:
+    given: $
+    then:
+      function: pascalCase
+
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-1/output.cjs b/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-1/output.cjs
index 1036e72b..6656acfa 100644
--- a/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-1/output.cjs
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-1/output.cjs
@@ -3,8 +3,9 @@ const {
   oas3_1: oas3_1,
   oas3_0: oas3_0,
   jsonSchemaLoose: jsonSchemaLoose,
+  jsonSchemaDraft2019_09: jsonSchemaDraft2019_09,
 } = require('@stoplight/spectral-formats');
 module.exports = {
-  formats: [oas2, oas3_1, oas3_0, jsonSchemaLoose],
+  formats: [oas2, oas3_1, oas3_0, jsonSchemaLoose, jsonSchemaDraft2019_09],
   rules: {},
 };
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-1/output.mjs b/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-1/output.mjs
index 202c73a0..1d56b389 100644
--- a/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-1/output.mjs
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-1/output.mjs
@@ -1,5 +1,5 @@
-import { oas2, oas3_1, oas3_0, jsonSchemaLoose } from '@stoplight/spectral-formats';
+import { oas2, oas3_1, oas3_0, jsonSchemaLoose, jsonSchemaDraft2019_09 } from '@stoplight/spectral-formats';
 export default {
-  formats: [oas2, oas3_1, oas3_0, jsonSchemaLoose],
+  formats: [oas2, oas3_1, oas3_0, jsonSchemaLoose, jsonSchemaDraft2019_09],
   rules: {},
 };
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-1/ruleset.yaml b/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-1/ruleset.yaml
index bb2ac9d9..36897a4b 100644
--- a/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-1/ruleset.yaml
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-1/ruleset.yaml
@@ -1,2 +1,2 @@
-formats: [oas2, oas3.1, oas3.0, json-schema-loose]
+formats: [oas2, oas3.1, oas3.0, json-schema-loose, json-schema-draft-2019-09]
 rules: {}
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-2/output.cjs b/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-2/output.cjs
index 0f551197..c146c1dd 100644
--- a/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-2/output.cjs
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-2/output.cjs
@@ -8,7 +8,7 @@ const {
 } = require('@stoplight/spectral-formats');
 const { truthy: truthy } = require('@stoplight/spectral-functions');
 module.exports = {
-  formats: [oas2, oas3_1, oas3_0, jsonSchemaLoose, jsonSchemaLoose],
+  formats: [oas2, oas3_1, oas3_0, jsonSchemaLoose],
   rules: {
     test: {
       given: '$',
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-2/output.mjs b/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-2/output.mjs
index 854ea70b..65c86bde 100644
--- a/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-2/output.mjs
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-2/output.mjs
@@ -1,7 +1,7 @@
 import { oas2, oas3_1, oas3_0, jsonSchemaLoose, asyncapi2, oas3 } from '@stoplight/spectral-formats';
 import { truthy } from '@stoplight/spectral-functions';
 export default {
-  formats: [oas2, oas3_1, oas3_0, jsonSchemaLoose, jsonSchemaLoose],
+  formats: [oas2, oas3_1, oas3_0, jsonSchemaLoose],
   rules: {
     test: {
       given: '$',
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-3/output.cjs b/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-3/output.cjs
new file mode 100644
index 00000000..c129fb6e
--- /dev/null
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-3/output.cjs
@@ -0,0 +1,16 @@
+const {
+  jsonSchemaDraft2019_09: jsonSchemaDraft2019_09,
+  jsonSchemaDraft2020_12: jsonSchemaDraft2020_12,
+} = require('@stoplight/spectral-formats');
+const { truthy: truthy } = require('@stoplight/spectral-functions');
+module.exports = {
+  formats: [jsonSchemaDraft2019_09, jsonSchemaDraft2020_12],
+  rules: {
+    test: {
+      given: '$',
+      then: {
+        function: truthy,
+      },
+    },
+  },
+};
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-3/output.mjs b/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-3/output.mjs
new file mode 100644
index 00000000..8e2735b9
--- /dev/null
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-3/output.mjs
@@ -0,0 +1,13 @@
+import { jsonSchemaDraft2019_09, jsonSchemaDraft2020_12 } from '@stoplight/spectral-formats';
+import { truthy } from '@stoplight/spectral-functions';
+export default {
+  formats: [jsonSchemaDraft2019_09, jsonSchemaDraft2020_12],
+  rules: {
+    test: {
+      given: '$',
+      then: {
+        function: truthy,
+      },
+    },
+  },
+};
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-3/ruleset.yaml b/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-3/ruleset.yaml
new file mode 100644
index 00000000..d43d00ca
--- /dev/null
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/formats-variant-3/ruleset.yaml
@@ -0,0 +1,10 @@
+formats:
+  - json-schema-2019-09
+  - json-schema-draft-2019-09
+  - json-schema-draft-2020-12
+  - json-schema-2020-12
+rules:
+  test:
+    given: $
+    then:
+      function: truthy
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/functions-variant-3/output.cjs b/packages/ruleset-migrator/src/__tests__/__fixtures__/functions-variant-3/output.cjs
new file mode 100644
index 00000000..7a89c40d
--- /dev/null
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/functions-variant-3/output.cjs
@@ -0,0 +1,32 @@
+const uppercase = _interopDefault(require('/.tmp/spectral/functions-variant-3/functions/upper-case.js'));
+const no = _interopDefault(require('/.tmp/spectral/functions-variant-3/functions/no-@.js'));
+const _400response = _interopDefault(require('/.tmp/spectral/functions-variant-3/functions/400-response.js'));
+const import$0 = _interopDefault(require('/.tmp/spectral/functions-variant-3/functions/import.js'));
+const require$0 = _interopDefault(require('/.tmp/spectral/functions-variant-3/functions/require.js'));
+module.exports = {
+  rules: {
+    rule: {
+      given: '$',
+      then: [
+        {
+          function: uppercase,
+        },
+        {
+          function: no,
+        },
+        {
+          function: _400response,
+        },
+        {
+          function: import$0,
+        },
+        {
+          function: require$0,
+        },
+      ],
+    },
+  },
+};
+function _interopDefault(ex) {
+  return ex && typeof ex === 'object' && 'default' in ex ? ex['default'] : ex;
+}
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/functions-variant-3/output.mjs b/packages/ruleset-migrator/src/__tests__/__fixtures__/functions-variant-3/output.mjs
new file mode 100644
index 00000000..2720ab10
--- /dev/null
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/functions-variant-3/output.mjs
@@ -0,0 +1,29 @@
+import uppercase from '/.tmp/spectral/functions-variant-3/functions/upper-case.js';
+import no from '/.tmp/spectral/functions-variant-3/functions/no-@.js';
+import _400response from '/.tmp/spectral/functions-variant-3/functions/400-response.js';
+import import$0 from '/.tmp/spectral/functions-variant-3/functions/import.js';
+import require$0 from '/.tmp/spectral/functions-variant-3/functions/require.js';
+export default {
+  rules: {
+    rule: {
+      given: '$',
+      then: [
+        {
+          function: uppercase,
+        },
+        {
+          function: no,
+        },
+        {
+          function: _400response,
+        },
+        {
+          function: import$0,
+        },
+        {
+          function: require$0,
+        },
+      ],
+    },
+  },
+};
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/functions-variant-3/ruleset.yaml b/packages/ruleset-migrator/src/__tests__/__fixtures__/functions-variant-3/ruleset.yaml
new file mode 100644
index 00000000..257f8357
--- /dev/null
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/functions-variant-3/ruleset.yaml
@@ -0,0 +1,15 @@
+functions:
+  - upper-case
+  - no-@
+  - 400-response
+  - import
+  - require
+rules:
+  rule:
+    given: $
+    then:
+      - function: upper-case
+      - function: no-@
+      - function: 400-response
+      - function: import
+      - function: require
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/functions-variant-4/output.cjs b/packages/ruleset-migrator/src/__tests__/__fixtures__/functions-variant-4/output.cjs
new file mode 100644
index 00000000..a4eedd7a
--- /dev/null
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/functions-variant-4/output.cjs
@@ -0,0 +1,18 @@
+const { oas3: oas3, oas3_1: oas3_1 } = require('@stoplight/spectral-formats');
+const oas3$0 = _interopDefault(require('/.tmp/spectral/functions-variant-4/functions/oas3.js'));
+module.exports = {
+  formats: [oas3, oas3_1],
+  rules: {
+    rule: {
+      given: '$',
+      then: [
+        {
+          function: oas3$0,
+        },
+      ],
+    },
+  },
+};
+function _interopDefault(ex) {
+  return ex && typeof ex === 'object' && 'default' in ex ? ex['default'] : ex;
+}
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/functions-variant-4/output.mjs b/packages/ruleset-migrator/src/__tests__/__fixtures__/functions-variant-4/output.mjs
new file mode 100644
index 00000000..0561856a
--- /dev/null
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/functions-variant-4/output.mjs
@@ -0,0 +1,15 @@
+import { oas3, oas3_1 } from '@stoplight/spectral-formats';
+import oas3$0 from '/.tmp/spectral/functions-variant-4/functions/oas3.js';
+export default {
+  formats: [oas3, oas3_1],
+  rules: {
+    rule: {
+      given: '$',
+      then: [
+        {
+          function: oas3$0,
+        },
+      ],
+    },
+  },
+};
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/functions-variant-4/ruleset.yaml b/packages/ruleset-migrator/src/__tests__/__fixtures__/functions-variant-4/ruleset.yaml
new file mode 100644
index 00000000..c62d519f
--- /dev/null
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/functions-variant-4/ruleset.yaml
@@ -0,0 +1,10 @@
+formats:
+  - oas3
+  - oas3.1
+functions:
+  - oas3
+rules:
+  rule:
+    given: $
+    then:
+      - function: oas3
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/rules-variant-1/output.cjs b/packages/ruleset-migrator/src/__tests__/__fixtures__/rules-variant-1/output.cjs
index e9b51a69..4c2cb477 100644
--- a/packages/ruleset-migrator/src/__tests__/__fixtures__/rules-variant-1/output.cjs
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/rules-variant-1/output.cjs
@@ -11,7 +11,7 @@ module.exports = {
       given:
         "$.paths.*[?( @property === 'get' || @property === 'put' || @property === 'post' || @property === 'delete' || @property === 'options' || @property === 'head' || @property === 'patch' || @property === 'trace' )]",
       then: {
-        function: oasOpFormDataConsumeCheck,
+        function: void 0,
         functionOptions: null,
       },
     },
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/rules-variant-1/output.mjs b/packages/ruleset-migrator/src/__tests__/__fixtures__/rules-variant-1/output.mjs
index ae942a29..5bcb8249 100644
--- a/packages/ruleset-migrator/src/__tests__/__fixtures__/rules-variant-1/output.mjs
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/rules-variant-1/output.mjs
@@ -11,7 +11,7 @@ export default {
       given:
         "$.paths.*[?( @property === 'get' || @property === 'put' || @property === 'post' || @property === 'delete' || @property === 'options' || @property === 'head' || @property === 'patch' || @property === 'trace' )]",
       then: {
-        function: oasOpFormDataConsumeCheck,
+        function: void 0,
         functionOptions: null,
       },
     },
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/rules-variant-2/output.cjs b/packages/ruleset-migrator/src/__tests__/__fixtures__/rules-variant-2/output.cjs
index e3a47f8a..046b3c37 100644
--- a/packages/ruleset-migrator/src/__tests__/__fixtures__/rules-variant-2/output.cjs
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/rules-variant-2/output.cjs
@@ -1,4 +1,4 @@
-const { truthy: truthy } = require('@stoplight/spectral-functions');
+const { undefined: undefined$0 } = require('@stoplight/spectral-functions');
 module.exports = {
   rules: {
     'oas3-unused-components': 'error',
@@ -7,7 +7,7 @@ module.exports = {
       given:
         "$.paths.*[?( @property === 'get' || @property === 'put' || @property === 'post' || @property === 'delete' || @property === 'options' || @property === 'head' || @property === 'patch' || @property === 'trace' )]",
       then: {
-        function: truthy,
+        function: undefined$0,
       },
     },
   },
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/rules-variant-2/output.mjs b/packages/ruleset-migrator/src/__tests__/__fixtures__/rules-variant-2/output.mjs
index c4a28ea5..6e3c2ab0 100644
--- a/packages/ruleset-migrator/src/__tests__/__fixtures__/rules-variant-2/output.mjs
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/rules-variant-2/output.mjs
@@ -1,4 +1,4 @@
-import { truthy } from "@stoplight/spectral-functions";
+import { undefined as undefined$0 } from "@stoplight/spectral-functions";
 export default {
   rules: {
     'oas3-unused-components': 'error',
@@ -7,7 +7,7 @@ export default {
       given:
         "$.paths.*[?( @property === 'get' || @property === 'put' || @property === 'post' || @property === 'delete' || @property === 'options' || @property === 'head' || @property === 'patch' || @property === 'trace' )]",
       then: {
-        function: truthy,
+        function: undefined$0,
       },
     },
   },
diff --git a/packages/ruleset-migrator/src/__tests__/__fixtures__/rules-variant-2/ruleset.yaml b/packages/ruleset-migrator/src/__tests__/__fixtures__/rules-variant-2/ruleset.yaml
index 09bfcd39..f9de4fee 100644
--- a/packages/ruleset-migrator/src/__tests__/__fixtures__/rules-variant-2/ruleset.yaml
+++ b/packages/ruleset-migrator/src/__tests__/__fixtures__/rules-variant-2/ruleset.yaml
@@ -6,4 +6,4 @@ rules:
       "$.paths.*[?( @property === 'get' || @property === 'put' || @property === 'post' || @property === 'delete' ||
       @property === 'options' || @property === 'head' || @property === 'patch' || @property === 'trace' )]"
     then:
-      function: truthy
+      function: undefined
diff --git a/packages/ruleset-migrator/src/__tests__/ruleset.test.ts b/packages/ruleset-migrator/src/__tests__/ruleset.test.ts
index b84ea78b..6ef14b88 100644
--- a/packages/ruleset-migrator/src/__tests__/ruleset.test.ts
+++ b/packages/ruleset-migrator/src/__tests__/ruleset.test.ts
@@ -4,6 +4,7 @@ import * as prettier from 'prettier/standalone';
 import * as parserBabel from 'prettier/parser-babel';
 import { Ruleset } from '@stoplight/spectral-core';
 import { DiagnosticSeverity } from '@stoplight/types';
+import * as fetchMock from 'fetch-mock';
 
 import { migrateRuleset } from '..';
 import * as fixtures from './__fixtures__/.cache/index.json';
@@ -106,6 +107,57 @@ describe('migrator', () => {
     expect(ruleset.rules['valid-type'].severity).toEqual(DiagnosticSeverity.Error);
   });
 
+  it('should accept custom fetch implementation', async () => {
+    // something is off with default module interop in Karma :man_shrugging:
+    const fetch = ((fetchMock as { default?: typeof import('fetch-mock') }).default ?? fetchMock).sandbox();
+
+    await fs.promises.writeFile(
+      path.join(cwd, 'ruleset.json'),
+      JSON.stringify({
+        extends: ['https://spectral.stoplight.io/ruleset'],
+        rules: {
+          'valid-type': 'error',
+        },
+      }),
+    );
+
+    fetch.get('https://spectral.stoplight.io/ruleset', {
+      body: {
+        rules: {
+          'valid-type': {
+            given: '$',
+            function: {
+              then: 'truthy',
+            },
+          },
+        },
+      },
+    });
+
+    expect(
+      await migrateRuleset(path.join(cwd, 'ruleset.json'), {
+        format: 'esm',
+        fs: fs as any,
+        fetch,
+      }),
+    ).toEqual(`export default {
+  "extends": [{
+    "rules": {
+      "valid-type": {
+        "given": "$",
+        "function": {
+          "then": "truthy"
+        }
+      }
+    }
+  }],
+  "rules": {
+    "valid-type": "error"
+  }
+};
+`);
+  });
+
   describe('error handling', () => {
     it('given unknown format, should throw', async () => {
       await fs.promises.writeFile(path.join(cwd, 'unknown-format.json'), `{ "formats": ["json-schema-draft-2"] }`);