Repository: Redocly/redocly-cli
Pull request #894 (2022-10-03 10:50:46)

feat: new assertion syntax 
## What/Why/How?
This is rough implementation of new assertion syntax according to ~[docs](https://github.com/Redocly/redocly-cli/pull/857)~ [new docs](https://github.com/Redocly/redocly-cli/pull/917).


## Reference

Also closes https://github.com/Redocly/redocly-cli/issues/915

## Testing

## Screenshots (optional)

## Check yourself

- [x] Code is linted
- [x] Tested with redoc/reference-docs/workflows
- [x] All new/updated code is covered with tests

## Security

- [x] Security impact of change has been considered
- [x] Code follows company security practices and guidelines


github-actions[bot]: <!-- jest coverage report action for options with hash 3339ae53a38c44382c0fb4491bdb282d -->

## Coverage report 

❌ An unexpected error occurred. For more details, [check console](https://github.com/Redocly/redocly-cli/actions/runs/3173549268) 
```
Error: The process '/usr/local/bin/npm' failed with exit code 1
```

| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | Category   | Percentage                                                     | Covered / Total |
| :----------------------------------------------------------------------------------------------------------------------------------------------------: | :--------- | :------------------------------------------------------------- | :-------------: |
|                                                                           🟡                                                                           | Statements | 72.75%                                                         |    3255/4474    |
|                                                                           🟡                                                                           | Branches   | 64.28%                                                         |    1814/2822    |
|                                                                           🟡                                                                           | Functions  | <div title="Base coverage is: 65.55%">64.38% (-1.17% 🔻)</div> |     544/845     |
|                                                                           🟡                                                                           | Lines      | 72.73%                                                         |    3024/4158    |


<details><summary>Show files with reduced coverage 🔻</summary>
<br/>

| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | File                                                                               | Statements                                                   | Branches                                                      | Functions                                                       | Lines                                                        |
| :----------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------- | :----------------------------------------------------------- | :------------------------------------------------------------ | :-------------------------------------------------------------- | :----------------------------------------------------------- |
|                                                                           🟡                                                                           | core/src/utils.ts                                                                  | 74.55%                                                       | <div title="Base coverage is: 60%">58.82% (-1.18% 🔻)</div>   | 71.79%                                                          | 75.49%                                                       |
|                                                                           🔴                                                                           | <div title="core/src/rules/common/assertions/utils.ts">`...` / utils.ts</div>      | <div title="Base coverage is: 52.94%">40% (-12.94% 🔻)</div> | <div title="Base coverage is: 24.66%">17.2% (-7.45% 🔻)</div> | <div title="Base coverage is: 45.45%">23.08% (-22.38% 🔻)</div> | <div title="Base coverage is: 51.81%">41% (-10.81% 🔻)</div> |
|                                                                           🟢                                                                           | <div title="core/src/config/config-resolvers.ts">`...` / config-resolvers.ts</div> | 86%                                                          | 76.75%                                                        | 100%                                                            | 86.11%                                                       |

</details>

## Test suite run failed

<details><summary>Failed tests: 3/509. Failed suites: 2/86.</summary>
<br/>

```
  ● Oas3 assertions › Utils › buildVisitorObject › should return a consistent visitor structure

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `Oas3 assertions Utils buildVisitorObject should return a consistent visitor structure 1`

    - Snapshot  -  7
    + Received  + 19

      Object {
    -   "Foo": Object {
    -     "Bar": Object {
    -       "Roof": Object {
    -         "Bar": [Function],
    -         "skip": [Function],
    +   "undefined": Object {
    +     "enter": Array [
    +       Object {
    +         "matchParentKeys": Array [
    +           "test",
    +         ],
    +         "type": "Foo",
            },
    -       "skip": [Function],
    +       Object {
    +         "matchParentKeys": Array [
    +           "test",
    +         ],
    +         "type": "Bar",
            },
    -     "skip": [Function],
    +       Object {
    +         "matchParentKeys": Array [
    +           "test",
    +         ],
    +         "type": "Roof",
    +       },
    +     ],
        },
      }

      43 |         const visitors = buildVisitorObject('Bar', context, () => {}) as any;
      44 |
    > 45 |         expect(visitors).toMatchInlineSnapshot(`
         |                          ^
      46 |           Object {
      47 |             "Foo": Object {
      48 |               "Bar": Object {

      at Object.<anonymous> (packages/core/src/rules/common/assertions/__tests__/utils.test.ts:45:26)

  ● Oas3 assertions › Utils › buildVisitorObject › should return the right visitor structure

    expect(received).toMatchInlineSnapshot(snapshot)

    Snapshot name: `Oas3 assertions Utils buildVisitorObject should return the right visitor structure 1`

    - Snapshot  -  5
    + Received  + 15

      Object {
    -   "Operation": Object {
    -     "ResponsesMap": Object {
    -       "MediaTypesMap": [Function],
    -       "skip": [Function],
    +   "undefined": Object {
    +     "enter": Array [
    +       Object {
    +         "matchParentKeys": Array [
    +           "put",
    +         ],
    +         "type": "Operation",
            },
    -     "skip": [Function],
    +       Object {
    +         "matchParentKeys": Array [
    +           201,
    +           200,
    +         ],
    +         "type": "ResponsesMap",
    +       },
    +     ],
        },
      }

      73 |         const visitors = buildVisitorObject('MediaTypesMap', context, () => {}) as any;
      74 |
    > 75 |         expect(visitors).toMatchInlineSnapshot(`
         |                          ^
      76 |           Object {
      77 |             "Operation": Object {
      78 |               "ResponsesMap": Object {

      at Object.<anonymous> (packages/core/src/rules/common/assertions/__tests__/utils.test.ts:75:26)

```
---
```
  ● Oas3 assertions › should return the right visitor structure

    assertion #3: 'subject' (String) is required

      42 |
      43 |     if (!isString(assertion.subject)) {
    > 44 |       throw new Error(`${assertId}: 'subject' (String) is required`);
         |             ^
      45 |     }
      46 |
      47 |     const subjectVisitor = buildSubjectVisitor(assertId, assertion);

      at Object.Assertions (packages/core/src/rules/common/assertions/index.ts:44:13)
      at Object.<anonymous> (packages/core/src/rules/common/assertions/__tests__/index.test.ts:38:22)

```

</details>


<p align="right">Report generated by <a href="https://github.com/ArtiomTr/jest-coverage-report-action">🧪jest coverage report action</a> from 50f5b3f89a31421f6b4a6d4de88fae0bf2535530</p>


Patch:
diff --git a/packages/core/src/config/config-resolvers.ts b/packages/core/src/config/config-resolvers.ts
index a2856c6f..b010e808 100644
--- a/packages/core/src/config/config-resolvers.ts
+++ b/packages/core/src/config/config-resolvers.ts
@@ -22,10 +22,16 @@ import type {
   DeprecatedInRawConfig,
 } from './types';
 import { isBrowser } from '../env';
-import { isNotString, isString, isDefined, parseYaml } from '../utils';
+import { isNotString, isString, isDefined, parseYaml, keysOf } from '../utils';
 import { Config } from './config';
 import { colorize, logger } from '../logger';
-import { asserts, buildAssertCustomFunction } from '../rules/common/assertions/asserts';
+import {
+  Asserts,
+  AssertionFn,
+  asserts,
+  buildAssertCustomFunction,
+} from '../rules/common/assertions/asserts';
+import type { Assertion, AssertionDefinition, RawAssertion } from '../rules/common/assertions';
 
 export async function resolveConfig(rawConfig: RawConfig, configPath?: string): Promise<Config> {
   if (rawConfig.styleguide?.extends?.some(isNotString)) {
@@ -409,26 +415,17 @@ function groupStyleguideAssertionRules({
   const transformedRules: Record<string, RuleConfig> = {};
 
   // Collect assertion rules
-  const assertions = [];
+  const assertions: Assertion[] = [];
   for (const [ruleKey, rule] of Object.entries(rules)) {
     if (ruleKey.startsWith('assert/') && typeof rule === 'object' && rule !== null) {
-      const assertion = rule;
+      const assertion = rule as RawAssertion;
+
       if (plugins) {
-        for (const field of Object.keys(assertion)) {
-          const [pluginId, fn] = field.split('/');
-          if (!pluginId || !fn) continue;
-          const plugin = plugins.find((plugin) => plugin.id === pluginId);
-          if (!plugin) {
-            throw Error(colorize.red(`Plugin ${colorize.blue(pluginId)} isn't found.`));
-          }
-          if (!plugin.assertions || !plugin.assertions[fn]) {
-            throw Error(
-              `Plugin ${colorize.red(
-                pluginId
-              )} doesn't export assertions function with name ${colorize.red(fn)}.`
-            );
-          }
-          asserts[field] = buildAssertCustomFunction(plugin.assertions[fn]);
+        registerCustomAssertions(plugins, assertion);
+
+        // We may have custom assertion inside where block
+        for (const context of assertion.where || []) {
+          registerCustomAssertions(plugins, context);
         }
       }
       assertions.push({
@@ -446,3 +443,29 @@ function groupStyleguideAssertionRules({
 
   return transformedRules;
 }
+
+function registerCustomAssertions(plugins: Plugin[], assertion: AssertionDefinition) {
+  for (const field of keysOf(assertion.assertions)) {
+    const [pluginId, fn] = field.split('/');
+
+    if (!pluginId || !fn) continue;
+
+    const plugin = plugins.find((plugin) => plugin.id === pluginId);
+
+    if (!plugin) {
+      throw Error(colorize.red(`Plugin ${colorize.blue(pluginId)} isn't found.`));
+    }
+
+    if (!plugin.assertions || !plugin.assertions[fn]) {
+      throw Error(
+        `Plugin ${colorize.red(
+          pluginId
+        )} doesn't export assertions function with name ${colorize.red(fn)}.`
+      );
+    }
+
+    (asserts as Asserts & { [name: string]: AssertionFn })[field] = buildAssertCustomFunction(
+      plugin.assertions[fn]
+    );
+  }
+}
diff --git a/packages/core/src/rules/common/assertions/asserts.ts b/packages/core/src/rules/common/assertions/asserts.ts
index b45578fe..58a9b5e7 100644
--- a/packages/core/src/rules/common/assertions/asserts.ts
+++ b/packages/core/src/rules/common/assertions/asserts.ts
@@ -9,12 +9,33 @@ import {
   regexFromString,
 } from './utils';
 
-type Asserts = Record<
-  string,
-  (value: any, condition: any, baseLocation: Location, rawValue?: any) => AssertResult[]
->;
+export type AssertionFn = (
+  value: any,
+  condition: any,
+  baseLocation: Location,
+  rawValue?: any
+) => AssertResult[];
 
-export const runOnKeysSet = new Set([
+export type Asserts = {
+  pattern: AssertionFn;
+  enum: AssertionFn;
+  defined: AssertionFn;
+  required: AssertionFn;
+  disallowed: AssertionFn;
+  undefined: AssertionFn;
+  nonEmpty: AssertionFn;
+  minLength: AssertionFn;
+  maxLength: AssertionFn;
+  casing: AssertionFn;
+  sortOrder: AssertionFn;
+  mutuallyExclusive: AssertionFn;
+  mutuallyRequired: AssertionFn;
+  requireAny: AssertionFn;
+  ref: AssertionFn;
+  const: AssertionFn;
+};
+
+export const runOnKeysSet = new Set<keyof Asserts>([
   'mutuallyExclusive',
   'mutuallyRequired',
   'enum',
@@ -27,8 +48,10 @@ export const runOnKeysSet = new Set([
   'required',
   'requireAny',
   'ref',
+  'const',
+  'defined', // In case if `property` for assertions is not added
 ]);
-export const runOnValuesSet = new Set([
+export const runOnValuesSet = new Set<keyof Asserts>([
   'pattern',
   'enum',
   'defined',
@@ -39,6 +62,7 @@ export const runOnValuesSet = new Set([
   'casing',
   'sortOrder',
   'ref',
+  'const',
 ]);
 
 export const asserts: Asserts = {
@@ -106,6 +130,34 @@ export const asserts: Asserts = {
       )
       .filter(isTruthy);
   },
+  const: (
+    value: string | number | boolean | string[] | number[],
+    condition: string | number | boolean,
+    baseLocation: Location
+  ) => {
+    if (typeof value === 'undefined') return [];
+
+    if (Array.isArray(value)) {
+      return value
+        .map(
+          (_val) =>
+            condition !== _val && {
+              message: `"${_val}" should be equal ${condition} `,
+              location: runOnValue(value) ? baseLocation : baseLocation.child(_val).key(),
+            }
+        )
+        .filter(isTruthy);
+    } else {
+      return value !== condition
+        ? [
+            {
+              message: `${value} should be equal ${condition}`,
+              location: baseLocation,
+            },
+          ]
+        : [];
+    }
+  },
   undefined: (value: any, condition: boolean = true, baseLocation: Location) => {
     const isUndefined = typeof value === 'undefined';
     const isValid = condition ? isUndefined : !isUndefined;
@@ -210,7 +262,7 @@ export const asserts: Asserts = {
           },
         ];
   },
-  ref: (_value: any, condition: string | boolean, baseLocation, rawValue: any) => {
+  ref: (_value: any, condition: string | boolean, baseLocation: Location, rawValue: any) => {
     if (typeof rawValue === 'undefined') return []; // property doesn't exist, no need to lint it with this assert
     const hasRef = rawValue.hasOwnProperty('$ref');
     if (typeof condition === 'boolean') {
@@ -237,7 +289,7 @@ export const asserts: Asserts = {
   },
 };
 
-export function buildAssertCustomFunction(fn: CustomFunction) {
+export function buildAssertCustomFunction(fn: CustomFunction): AssertionFn {
   return (value: string[], options: any, baseLocation: Location) =>
     fn.call(null, value, options, baseLocation);
 }
diff --git a/packages/core/src/rules/common/assertions/index.ts b/packages/core/src/rules/common/assertions/index.ts
index 9d082a92..a6cb3b39 100644
--- a/packages/core/src/rules/common/assertions/index.ts
+++ b/packages/core/src/rules/common/assertions/index.ts
@@ -1,65 +1,55 @@
-import { asserts, runOnKeysSet, runOnValuesSet } from './asserts';
-import { AssertToApply, buildSubjectVisitor, buildVisitorObject } from './utils';
-import { Oas2Rule, Oas3Rule } from '../../../visitors';
+import { asserts, AssertionFn } from './asserts';
+import { buildSubjectVisitor, buildVisitorObject } from './utils';
+import { Oas2Visitor, Oas3Visitor } from '../../../visitors';
+import { RuleSeverity } from '../../../config';
+import { isString } from '../../../utils';
+
+export type AssertionLocators = {
+  filterInParentKeys?: (string | number)[];
+  filterOutParentKeys?: (string | number)[];
+  matchParentKeys?: string;
+};
+
+export type AssertionDefinition = {
+  subject: {
+    type: string;
+    property?: string | string[];
+  } & AssertionLocators;
+  assertions: { [name in keyof typeof asserts]?: AssertionFn };
+};
 
-export const Assertions: Oas3Rule | Oas2Rule = (opts: object) => {
-  const visitors: any[] = [];
+export type RawAssertion = AssertionDefinition & {
+  where?: AssertionDefinition[];
+  message?: string;
+  suggest?: string[];
+  severity?: RuleSeverity;
+};
+
+export type Assertion = RawAssertion & { assertionId: string };
+
+export const Assertions = (opts: Record<string, Assertion>) => {
+  const visitors: (Oas2Visitor | Oas3Visitor)[] = [];
 
   // As 'Assertions' has an array of asserts,
   // that array spreads into an 'opts' object on init rules phase here
   // https://github.com/Redocly/redocly-cli/blob/main/packages/core/src/config/config.ts#L311
   // that is why we need to iterate through 'opts' values;
   // before - filter only object 'opts' values
-  const assertions: any[] = Object.values(opts).filter(
+  const assertions: Assertion[] = Object.values(opts).filter(
     (opt: unknown) => typeof opt === 'object' && opt !== null
   );
 
   for (const [index, assertion] of assertions.entries()) {
     const assertId =
       (assertion.assertionId && `${assertion.assertionId} assertion`) || `assertion #${index + 1}`;
-    if (!assertion.subject) {
-      throw new Error(`${assertId}: 'subject' is required`);
-    }
 
-    const subjects: string[] = Array.isArray(assertion.subject)
-      ? assertion.subject
-      : [assertion.subject];
-
-    const assertsToApply: AssertToApply[] = Object.keys(asserts)
-      .filter((assertName: string) => assertion[assertName] !== undefined)
-      .map((assertName: string) => {
-        return {
-          name: assertName,
-          conditions: assertion[assertName],
-          runsOnKeys: runOnKeysSet.has(assertName),
-          runsOnValues: runOnValuesSet.has(assertName),
-        };
-      });
-
-    const shouldRunOnKeys: AssertToApply | undefined = assertsToApply.find(
-      (assert: AssertToApply) => assert.runsOnKeys && !assert.runsOnValues
-    );
-    const shouldRunOnValues: AssertToApply | undefined = assertsToApply.find(
-      (assert: AssertToApply) => assert.runsOnValues && !assert.runsOnKeys
-    );
-
-    if (shouldRunOnValues && !assertion.property) {
-      throw new Error(
-        `${shouldRunOnValues.name} can't be used on all keys. Please provide a single property.`
-      );
+    if (!isString(assertion.subject.type)) {
+      throw new Error(`${assertId}: 'type' (String) is required`);
     }
 
-    if (shouldRunOnKeys && assertion.property) {
-      throw new Error(
-        `${shouldRunOnKeys.name} can't be used on a single property. Please use 'property'.`
-      );
-    }
-
-    for (const subject of subjects) {
-      const subjectVisitor = buildSubjectVisitor(assertId, assertion, assertsToApply);
-      const visitorObject = buildVisitorObject(subject, assertion.context, subjectVisitor);
-      visitors.push(visitorObject);
-    }
+    const subjectVisitor = buildSubjectVisitor(assertId, assertion);
+    const visitorObject = buildVisitorObject(assertion, subjectVisitor);
+    visitors.push(visitorObject);
   }
 
   return visitors;
diff --git a/packages/core/src/rules/common/assertions/utils.ts b/packages/core/src/rules/common/assertions/utils.ts
index a000ed2e..433c5450 100644
--- a/packages/core/src/rules/common/assertions/utils.ts
+++ b/packages/core/src/rules/common/assertions/utils.ts
@@ -1,8 +1,15 @@
-import type { AssertResult, RuleSeverity } from '../../../config';
+import { asserts, runOnKeysSet, runOnValuesSet, Asserts } from './asserts';
 import { colorize } from '../../../logger';
 import { isRef, Location } from '../../../ref-utils';
-import { UserContext } from '../../../walk';
-import { asserts } from './asserts';
+import { isTruthy, keysOf, isString } from '../../../utils';
+import type { AssertResult } from '../../../config';
+import type { Assertion, AssertionDefinition, AssertionLocators } from '.';
+import type {
+  Oas2Visitor,
+  Oas3Visitor,
+  SkipFunctionContext,
+  VisitFunction,
+} from '../../../visitors';
 
 export type OrderDirection = 'asc' | 'desc';
 
@@ -11,166 +18,236 @@ export type OrderOptions = {
   property: string;
 };
 
-type Assertion = {
-  property: string | string[];
-  context?: Record<string, any>[];
-  severity?: RuleSeverity;
-  suggest?: any[];
-  message?: string;
-  subject: string;
-};
-
 export type AssertToApply = {
-  name: string;
+  name: keyof Asserts;
   conditions: any;
   runsOnKeys: boolean;
   runsOnValues: boolean;
 };
 
+type AssertionContext = SkipFunctionContext & {
+  node: any;
+};
+
+const assertionMessageTemplates = {
+  problems: '{{problems}}',
+};
+
+function getPredicatesFromLocators(
+  locators: AssertionLocators
+): ((key: string | number) => boolean)[] {
+  const { filterInParentKeys, filterOutParentKeys, matchParentKeys } = locators;
+
+  const keyMatcher = matchParentKeys && regexFromString(matchParentKeys);
+  const matchKeysPredicate =
+    keyMatcher && ((key: string | number) => keyMatcher.test(key.toString()));
+
+  const filterInPredicate =
+    Array.isArray(filterInParentKeys) &&
+    ((key: string | number) => filterInParentKeys.includes(key.toString()));
+
+  const filterOutPredicate =
+    Array.isArray(filterOutParentKeys) &&
+    ((key: string | number) => !filterOutParentKeys.includes(key.toString()));
+
+  return [matchKeysPredicate, filterInPredicate, filterOutPredicate].filter(isTruthy);
+}
+
+export function getAssertsToApply(assertion: AssertionDefinition): AssertToApply[] {
+  const assertsToApply = keysOf(asserts)
+    .filter((assertName) => assertion.assertions[assertName] !== undefined)
+    .map((assertName) => {
+      return {
+        name: assertName,
+        conditions: assertion.assertions[assertName],
+        runsOnKeys: runOnKeysSet.has(assertName),
+        runsOnValues: runOnValuesSet.has(assertName),
+      };
+    });
+
+  const shouldRunOnKeys: AssertToApply | undefined = assertsToApply.find(
+    (assert: AssertToApply) => assert.runsOnKeys && !assert.runsOnValues
+  );
+  const shouldRunOnValues: AssertToApply | undefined = assertsToApply.find(
+    (assert: AssertToApply) => assert.runsOnValues && !assert.runsOnKeys
+  );
+
+  if (shouldRunOnValues && !assertion.subject.property) {
+    throw new Error(
+      `${shouldRunOnValues.name} can't be used on all keys. Please provide a single property`
+    );
+  }
+
+  if (shouldRunOnKeys && assertion.subject.property) {
+    throw new Error(
+      `${shouldRunOnKeys.name} can't be used on a single property. Please use 'property'.`
+    );
+  }
+
+  return assertsToApply;
+}
+
+function getAssertionProperties({ subject }: AssertionDefinition): string[] {
+  return (Array.isArray(subject.property) ? subject.property : [subject?.property]).filter(
+    Boolean
+  ) as string[];
+}
+
+function applyAssertions(
+  assertionDefinition: AssertionDefinition,
+  asserts: AssertToApply[],
+  { rawLocation, rawNode, resolve, location, node }: AssertionContext
+): AssertResult[] {
+  const properties = getAssertionProperties(assertionDefinition);
+  const assertResults: Array<AssertResult[]> = [];
+
+  for (const assert of asserts) {
+    const currentLocation = assert.name === 'ref' ? rawLocation : location;
+
+    if (properties.length) {
+      for (const property of properties) {
+        // we can have resolvable scalar so need to resolve value here.
+        const value = isRef(node[property]) ? resolve(node[property])?.node : node[property];
+        assertResults.push(
+          runAssertion({
+            values: value,
+            rawValues: rawNode[property],
+            assert,
+            location: currentLocation.child(property),
+          })
+        );
+      }
+    } else {
+      const value = assert.name === 'ref' ? rawNode : Object.keys(node);
+      assertResults.push(
+        runAssertion({
+          values: Object.keys(node),
+          rawValues: value,
+          assert,
+          location: currentLocation,
+        })
+      );
+    }
+  }
+
+  return assertResults.flat();
+}
+
 export function buildVisitorObject(
-  subject: string,
-  context: Record<string, any>[],
-  subjectVisitor: any
-) {
-  if (!context) {
-    return { [subject]: subjectVisitor };
+  assertion: Assertion,
+  subjectVisitor: VisitFunction<any>
+): Oas2Visitor | Oas3Visitor {
+  const targetVisitorLocatorPredicates = getPredicatesFromLocators(assertion.subject);
+  const targetVisitorSkipFunction = targetVisitorLocatorPredicates.length
+    ? (node: any, key: string | number) =>
+        !targetVisitorLocatorPredicates.every((predicate) => predicate(key))
+    : undefined;
+  const targetVisitor: Oas2Visitor | Oas3Visitor = {
+    [assertion.subject.type]: {
+      enter: subjectVisitor,
+      ...(targetVisitorSkipFunction && { skip: targetVisitorSkipFunction }),
+    },
+  };
+
+  if (!Array.isArray(assertion.where)) {
+    return targetVisitor;
   }
 
   let currentVisitorLevel: Record<string, any> = {};
   const visitor: Record<string, any> = currentVisitorLevel;
+  const context = assertion.where;
 
   for (let index = 0; index < context.length; index++) {
-    const node = context[index];
-    if (context.length === index + 1 && node.type === subject) {
-      // Visitors don't work properly for the same type nested nodes, so
-      // as a workaround for that we don't create separate visitor for the last element
-      // which is the same as subject;
-      // we will check includes/excludes it in the last visitor.
-      continue;
-    }
-    const matchParentKeys = node.matchParentKeys;
-    const excludeParentKeys = node.excludeParentKeys;
+    const assertionDefinitionNode = context[index];
 
-    if (matchParentKeys && excludeParentKeys) {
+    if (!isString(assertionDefinitionNode.subject?.type)) {
       throw new Error(
-        `Both 'matchParentKeys' and 'excludeParentKeys' can't be under one context item`
+        `${assertion.assertionId} -> where -> [${index}]: 'type' (String) is required`
       );
     }
 
-    if (matchParentKeys || excludeParentKeys) {
-      currentVisitorLevel[node.type] = {
-        skip: (_value: any, key: string) => {
-          if (matchParentKeys) {
-            return !matchParentKeys.includes(key);
-          }
-          if (excludeParentKeys) {
-            return excludeParentKeys.includes(key);
-          }
-        },
+    const locatorPredicates = getPredicatesFromLocators(assertionDefinitionNode.subject);
+    const assertsToApply = getAssertsToApply(assertionDefinitionNode);
+
+    const skipFunction = (
+      node: unknown,
+      key: string | number,
+      { location, rawLocation, resolve, rawNode }: SkipFunctionContext
+    ): boolean =>
+      !locatorPredicates.every((predicate) => predicate(key)) ||
+      !!applyAssertions(assertionDefinitionNode, assertsToApply, {
+        location,
+        node,
+        rawLocation,
+        rawNode,
+        resolve,
+      }).length;
+
+    const nodeVisitor = {
+      ...((locatorPredicates.length || assertsToApply.length) && { skip: skipFunction }),
+    };
+
+    if (
+      assertionDefinitionNode.subject.type === assertion.subject.type &&
+      index === context.length - 1
+    ) {
+      // We have to merge the visitors if the last node inside the `where` is the same as the subject.
+      targetVisitor[assertion.subject.type] = {
+        enter: subjectVisitor,
+        ...((nodeVisitor.skip && { skip: nodeVisitor.skip }) ||
+          (targetVisitorSkipFunction && {
+            skip: (
+              node,
+              key,
+              ctx // We may have locators defined on assertion level and on where level for the same node type
+            ) => !!(nodeVisitor.skip?.(node, key, ctx) || targetVisitorSkipFunction?.(node, key)),
+          })),
       };
     } else {
-      currentVisitorLevel[node.type] = {};
+      currentVisitorLevel = currentVisitorLevel[assertionDefinitionNode.subject?.type] =
+        nodeVisitor;
     }
-    currentVisitorLevel = currentVisitorLevel[node.type];
   }
 
-  currentVisitorLevel[subject] = subjectVisitor;
+  currentVisitorLevel[assertion.subject.type] = targetVisitor[assertion.subject.type];
 
   return visitor;
 }
 
-export function buildSubjectVisitor(
-  assertId: string,
-  assertion: Assertion,
-  asserts: AssertToApply[]
-) {
-  return (
-    node: any,
-    { report, location, rawLocation, key, type, resolve, rawNode }: UserContext
-  ) => {
-    let properties = assertion.property;
-    // We need to check context's last node if it has the same type as subject node;
-    // if yes - that means we didn't create context's last node visitor,
-    // so we need to handle 'matchParentKeys' and 'excludeParentKeys' conditions here;
-    if (assertion.context) {
-      const lastContextNode = assertion.context[assertion.context.length - 1];
-      if (lastContextNode.type === type.name) {
-        const matchParentKeys = lastContextNode.matchParentKeys;
-        const excludeParentKeys = lastContextNode.excludeParentKeys;
-
-        if (matchParentKeys && !matchParentKeys.includes(key)) {
-          return;
-        }
-        if (excludeParentKeys && excludeParentKeys.includes(key)) {
-          return;
-        }
-      }
-    }
-
-    if (properties) {
-      properties = Array.isArray(properties) ? properties : [properties];
-    }
+export function buildSubjectVisitor(assertId: string, assertion: Assertion): VisitFunction<any> {
+  return (node: any, { report, location, rawLocation, resolve, rawNode }) => {
+    const properties = getAssertionProperties(assertion);
 
     const defaultMessage = `${colorize.blue(assertId)} failed because the ${colorize.blue(
-      assertion.subject
-    )}${colorize.blue(
-      properties ? ` ${(properties as string[]).join(', ')}` : ''
-    )} didn't meet the assertions: {{problems}}`;
-
-    const assertResults: Array<AssertResult[]> = [];
-    for (const assert of asserts) {
-      const currentLocation = assert.name === 'ref' ? rawLocation : location;
-      if (properties) {
-        for (const property of properties) {
-          // we can have resolvable scalar so need to resolve value here.
-          const value = isRef(node[property]) ? resolve(node[property])?.node : node[property];
-          assertResults.push(
-            runAssertion({
-              values: value,
-              rawValues: rawNode[property],
-              assert,
-              location: currentLocation.child(property),
-            })
-          );
-        }
-      } else {
-        const value = assert.name === 'ref' ? rawNode : Object.keys(node);
-        assertResults.push(
-          runAssertion({
-            values: Object.keys(node),
-            rawValues: value,
-            assert,
-            location: currentLocation,
-          })
-        );
-      }
-    }
+      assertion.subject.type
+    )} ${colorize.blue(properties.join(', '))} didn't meet the assertions: ${
+      assertionMessageTemplates.problems
+    }`.replace(/ +/g, ' ');
+
+    const problems = applyAssertions(assertion, getAssertsToApply(assertion), {
+      rawLocation,
+      rawNode,
+      resolve,
+      location,
+      node,
+    });
 
-    const problems = assertResults.flat();
     if (problems.length) {
       const message = assertion.message || defaultMessage;
-
-      report({
-        message: message.replace('{{problems}}', getProblemsMessage(problems)),
-        location: getProblemsLocation(problems) || location,
-        forceSeverity: assertion.severity || 'error',
-        suggest: assertion.suggest || [],
-        ruleId: assertId,
-      });
+      for (const problem of problems) {
+        const problemMessage = problem.message ? problem.message : defaultMessage;
+        report({
+          message: message.replace(assertionMessageTemplates.problems, problemMessage),
+          location: problem.location || location,
+          forceSeverity: assertion.severity || 'error',
+          suggest: assertion.suggest || [],
+          ruleId: assertId,
+        });
+      }
     }
   };
 }
 
-function getProblemsLocation(problems: AssertResult[]) {
-  return problems.length ? problems[0].location : undefined;
-}
-
-function getProblemsMessage(problems: AssertResult[]) {
-  return problems.length === 1
-    ? problems[0].message ?? ''
-    : problems.map((problem) => `\n- ${problem.message ?? ''}`).join('');
-}
-
 export function getIntersectionLength(keys: string[], properties: string[]): number {
   const props = new Set(properties);
   let count = 0;
diff --git a/packages/core/src/types/redocly-yaml.ts b/packages/core/src/types/redocly-yaml.ts
index ff90b24e..53d2d56e 100644
--- a/packages/core/src/types/redocly-yaml.ts
+++ b/packages/core/src/types/redocly-yaml.ts
@@ -238,15 +238,9 @@ const ObjectRule: NodeType = {
   required: ['severity'],
 };
 
-const Assert: NodeType = {
+const AssertionDefinitionSubject: NodeType = {
   properties: {
-    subject: (value: unknown) => {
-      if (Array.isArray(value)) {
-        return { type: 'array', items: { enum: nodeTypesList } };
-      } else {
-        return { enum: nodeTypesList };
-      }
-    },
+    type: { enum: nodeTypesList },
     property: (value: unknown) => {
       if (Array.isArray(value)) {
         return { type: 'array', items: { type: 'string' } };
@@ -256,10 +250,15 @@ const Assert: NodeType = {
         return { type: 'string' };
       }
     },
-    context: listOf('Context'),
-    message: { type: 'string' },
-    suggest: { type: 'array', items: { type: 'string' } },
-    severity: { enum: ['error', 'warn', 'off'] },
+    filterInParentKeys: { type: 'array', items: { type: 'string' } },
+    filterOutParentKeys: { type: 'array', items: { type: 'string' } },
+    matchParentKeys: { type: 'string' },
+  },
+  required: ['type'],
+};
+
+const AssertionDefinitionAssertions: NodeType = {
+  properties: {
     enum: { type: 'array', items: { type: 'string' } },
     pattern: { type: 'string' },
     casing: {
@@ -279,27 +278,50 @@ const Assert: NodeType = {
     requireAny: { type: 'array', items: { type: 'string' } },
     disallowed: { type: 'array', items: { type: 'string' } },
     defined: { type: 'boolean' },
-    undefined: { type: 'boolean' },
+    // undefined: { type: 'boolean' }, // TODO: Remove `undefined` assertion from codebase overall
     nonEmpty: { type: 'boolean' },
     minLength: { type: 'integer' },
     maxLength: { type: 'integer' },
     ref: (value: string | boolean) =>
       typeof value === 'string' ? { type: 'string' } : { type: 'boolean' },
+    const: (value: string | boolean | number) => {
+      if (typeof value === 'string') {
+        return { type: 'string' };
+      }
+      if (typeof value === 'number') {
+        return { type: 'number' };
+      }
+      if (typeof value === 'boolean') {
+        return { type: 'boolean' };
+      } else {
+        return;
+      }
+    },
   },
   additionalProperties: (_value: unknown, key: string) => {
     if (/^\w+\/\w+$/.test(key)) return { type: 'object' };
     return;
   },
-  required: ['subject'],
 };
 
-const Context: NodeType = {
+const AssertDefinition: NodeType = {
   properties: {
-    type: { enum: nodeTypesList },
-    matchParentKeys: { type: 'array', items: { type: 'string' } },
-    excludeParentKeys: { type: 'array', items: { type: 'string' } },
+    subject: 'AssertionDefinitionSubject',
+    assertions: 'AssertionDefinitionAssertions',
   },
-  required: ['type'],
+  required: ['subject', 'assertions'],
+};
+
+const Assert: NodeType = {
+  properties: {
+    subject: 'AssertionDefinitionSubject',
+    assertions: 'AssertionDefinitionAssertions',
+    where: listOf('AssertDefinition'),
+    message: { type: 'string' },
+    suggest: { type: 'array', items: { type: 'string' } },
+    severity: { enum: ['error', 'warn', 'off'] },
+  },
+  required: ['subject', 'assertions'],
 };
 
 const ConfigLanguage: NodeType = {
@@ -925,7 +947,7 @@ export const ConfigTypes: Record<string, NodeType> = {
   ConfigSidebarLinks,
   CommonConfigSidebarLinks,
   ConfigTheme,
-  Context,
+  AssertDefinition,
   ThemeColors,
   CommonThemeColors,
   BorderThemeColors,
@@ -972,4 +994,6 @@ export const ConfigTypes: Record<string, NodeType> = {
   Sidebar,
   Heading,
   Typography,
+  AssertionDefinitionAssertions,
+  AssertionDefinitionSubject,
 };
diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts
index 20a6cc13..45d69b70 100644
--- a/packages/core/src/utils.ts
+++ b/packages/core/src/utils.ts
@@ -232,6 +232,11 @@ export function identity<T>(value: T): T {
   return value;
 }
 
+export function keysOf<T>(obj: T) {
+  if (!obj) return [];
+  return Object.keys(obj) as (keyof T)[];
+}
+
 export function pickDefined<T extends Record<string, unknown>>(
   obj?: T
 ): Record<string, unknown> | undefined {
diff --git a/packages/core/src/visitors.ts b/packages/core/src/visitors.ts
index a304a6c3..ae97a6df 100644
--- a/packages/core/src/visitors.ts
+++ b/packages/core/src/visitors.ts
@@ -50,6 +50,11 @@ import type { Stack } from './utils';
 import type { UserContext, ResolveResult, ProblemSeverity } from './walk';
 import type { Location } from './ref-utils';
 
+export type SkipFunctionContext = Pick<
+  UserContext,
+  'location' | 'rawNode' | 'resolve' | 'rawLocation'
+>;
+
 export type VisitFunction<T> = (
   node: T,
   ctx: UserContext & { ignoreNextVisitorsOnNode: () => void },
@@ -59,7 +64,7 @@ export type VisitFunction<T> = (
 
 type VisitRefFunction = (node: OasRef, ctx: UserContext, resolved: ResolveResult<any>) => void;
 
-type SkipFunction<T> = (node: T, key: string | number) => boolean;
+type SkipFunction<T> = (node: T, key: string | number, ctx: SkipFunctionContext) => boolean;
 
 type VisitObject<T> = {
   enter?: VisitFunction<T>;
@@ -210,6 +215,7 @@ const legacyTypesMap = {
   HeadersMap: 'HeaderMap',
   LinksMap: 'LinkMap',
   OAuth2Flows: 'SecuritySchemeFlows',
+  Responses: 'ResponsesMap',
 };
 
 type Oas3NestedVisitor = {
diff --git a/packages/core/src/walk.ts b/packages/core/src/walk.ts
index 25762da4..df0a9c8f 100644
--- a/packages/core/src/walk.ts
+++ b/packages/core/src/walk.ts
@@ -226,7 +226,14 @@ export function walkDocument<T>(opts: {
               nextLevelTypeActivated: null,
               withParentNode: context.parent?.activatedOn?.value.node,
               skipped:
-                (context.parent?.activatedOn?.value.skipped || skip?.(resolvedNode, key)) ?? false,
+                (context.parent?.activatedOn?.value.skipped ||
+                  skip?.(resolvedNode, key, {
+                    location,
+                    rawLocation,
+                    resolve,
+                    rawNode: node,
+                  })) ??
+                false,
             };
 
             context.activatedOn = pushStack<any>(context.activatedOn, activatedOn);


Test patch:
diff --git a/__tests__/commands.test.ts b/__tests__/commands.test.ts
index 22b945ae..b8ca6616 100644
--- a/__tests__/commands.test.ts
+++ b/__tests__/commands.test.ts
@@ -35,7 +35,7 @@ describe('E2E', () => {
       { dirName: 'invalid-config--lint-config-off', option: 'off' },
       { dirName: 'invalid-config--lint-config-warn', option: 'warn' },
       { dirName: 'invalid-config--lint-config-error', option: 'error' },
-      { dirName: 'invlid-lint-config-saverity', option: 'something' },
+      { dirName: 'invalid-lint-config-severity', option: 'something' },
       { dirName: 'invalid-config--no-option', option: null },
       { dirName: 'invalid-config-assertation-name', option: 'error' },
       { dirName: 'invalid-config-assertation-config-type', option: 'error' },
diff --git a/__tests__/lint-config/invalid-config-assertation-config-type/.redocly.yaml b/__tests__/lint-config/invalid-config-assertation-config-type/.redocly.yaml
index d622da32..c8102def 100644
--- a/__tests__/lint-config/invalid-config-assertation-config-type/.redocly.yaml
+++ b/__tests__/lint-config/invalid-config-assertation-config-type/.redocly.yaml
@@ -4,8 +4,16 @@ apis:
 
 rules:
   assert/path-item-mutually-required:
-    context:
-      - type: Invalid-type
-    subject: Operation
-    message: Operation should have summary and security fields simultaneously
+    where:
+      - subject:
+          type: Invalid-type
+          property: property
+        assertions:
+          defined: true
+    subject:
+      type: Operation
+      property: property
+    assertions:
+      defined: true
+
 extends: []
diff --git a/__tests__/lint-config/invalid-config-assertation-config-type/snapshot.js b/__tests__/lint-config/invalid-config-assertation-config-type/snapshot.js
index 39bfa7a0..1e979d41 100644
--- a/__tests__/lint-config/invalid-config-assertation-config-type/snapshot.js
+++ b/__tests__/lint-config/invalid-config-assertation-config-type/snapshot.js
@@ -2,16 +2,16 @@
 
 exports[`E2E lint-config test with option: { dirName: 'invalid-config-assertation-config-type', option: 'error' } 1`] = `
 
-[1] .redocly.yaml:9:17 at #/styleguide/rules/assert~1path-item-mutually-required/context/0/type
+[1] .redocly.yaml:10:19 at #/styleguide/rules/assert~1path-item-mutually-required/where/0/subject/type
 
 \`type\` can be one of the following only: "Root", "Tag", "ExternalDocs", "Server", "ServerVariable", "ServerVariablesMap", "SecurityRequirement", "Info", "Contact", "License", "Paths", "PathItem", "Parameter", "Operation", "Callback", "CallbacksMap", "RequestBody", "MediaTypesMap", "MediaType", "Example", "ExamplesMap", "Encoding", "EncodingMap", "Header", "HeadersMap", "Responses", "Response", "Link", "LinksMap", "Schema", "Xml", "SchemaProperties", "DiscriminatorMapping", "Discriminator", "Components", "NamedSchemas", "NamedResponses", "NamedParameters", "NamedExamples", "NamedRequestBodies", "NamedHeaders", "NamedSecuritySchemes", "NamedLinks", "NamedCallbacks", "ImplicitFlow", "PasswordFlow", "ClientCredentials", "AuthorizationCode", "OAuth2Flows", "SecurityScheme", "XCodeSample", "WebhooksMap".
 
- 7 | assert/path-item-mutually-required:
- 8 |   context:
- 9 |     - type: Invalid-type
+ 8 | where:
+ 9 |   - subject:
+10 |       type: Invalid-type
    |             ^^^^^^^^^^^^
-10 |   subject: Operation
-11 |   message: Operation should have summary and security fields simultaneously
+11 |       property: property
+12 |     assertions:
 
 Error was generated by the configuration spec rule.
 
diff --git a/__tests__/lint-config/invalid-config-assertation-name/.redocly.yaml b/__tests__/lint-config/invalid-config-assertation-name/.redocly.yaml
index f59e5a90..671ba6be 100644
--- a/__tests__/lint-config/invalid-config-assertation-name/.redocly.yaml
+++ b/__tests__/lint-config/invalid-config-assertation-name/.redocly.yaml
@@ -7,5 +7,5 @@ rules:
     context:
       - type: PathItem
     subject: Operation
-    message: Operation should have summary and security fields simultaneously
+
 extends: []
diff --git a/__tests__/lint-config/invalid-lint-config-severity/.redocly.yaml b/__tests__/lint-config/invalid-lint-config-severity/.redocly.yaml
new file mode 100644
index 00000000..85bdce7c
--- /dev/null
+++ b/__tests__/lint-config/invalid-lint-config-severity/.redocly.yaml
@@ -0,0 +1,12 @@
+apis:
+  main:
+    root: ./openapi.yaml
+
+rules:
+  assert/path-item-mutually-required:
+    subject: 
+      type: Operation
+    assertions:
+      defined: true
+
+extends: []
diff --git a/__tests__/lint-config/invlid-lint-config-saverity/snapshot.js b/__tests__/lint-config/invalid-lint-config-severity/snapshot.js
similarity index 92%
rename from __tests__/lint-config/invlid-lint-config-saverity/snapshot.js
rename to __tests__/lint-config/invalid-lint-config-severity/snapshot.js
index 8be9a6c2..6c199d55 100644
--- a/__tests__/lint-config/invlid-lint-config-saverity/snapshot.js
+++ b/__tests__/lint-config/invalid-lint-config-severity/snapshot.js
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`E2E lint-config test with option: { dirName: 'invlid-lint-config-saverity', option: 'something' } 1`] = `
+exports[`E2E lint-config test with option: { dirName: 'invalid-lint-config-severity', option: 'something' } 1`] = `
 
 index.ts lint [apis...]
 
diff --git a/__tests__/lint-config/invlid-lint-config-saverity/.redocly.yaml b/__tests__/lint-config/invlid-lint-config-saverity/.redocly.yaml
deleted file mode 100644
index 8054578d..00000000
--- a/__tests__/lint-config/invlid-lint-config-saverity/.redocly.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-apis:
-  main:
-    root: ./openapi.yaml
-
-rules:
-  assert/path-item-mutually-required:
-    context:
-      - type: PathItem
-    subject: Operation
-    message: Operation should have summary and security fields simultaneously
-extends: []
diff --git a/__tests__/lint/assertions-camel-case-twice/.redocly.yaml b/__tests__/lint/assertions-camel-case-twice/.redocly.yaml
new file mode 100644
index 00000000..3546bbc5
--- /dev/null
+++ b/__tests__/lint/assertions-camel-case-twice/.redocly.yaml
@@ -0,0 +1,14 @@
+apis:
+  main:
+    root: ./openapi.yaml
+
+rules:
+  assert/named-parameters-camelCase:
+    subject:
+      type: NamedParameters
+    message: Parameters must use camelCase
+    severity: warn
+    assertions:
+      casing: camelCase
+
+extends: []
diff --git a/__tests__/lint/assertions-camel-case-twice/openapi.yaml b/__tests__/lint/assertions-camel-case-twice/openapi.yaml
new file mode 100644
index 00000000..14924ee4
--- /dev/null
+++ b/__tests__/lint/assertions-camel-case-twice/openapi.yaml
@@ -0,0 +1,39 @@
+openapi: 3.0.0
+info:
+  description: Description
+  license:
+    name: MIT
+    url: example.com
+tags:
+- name: sometag
+  description: sometag
+servers:
+- url: https://example.com
+security: []
+paths:
+  /someGet:
+    get:
+      operationId: operationIdGet
+      parameters: 
+      - $ref: "#/components/parameters/header_Accept"
+      responses:
+        '200':
+          description: ''
+        '400':
+          description: ''
+components:
+  parameters:
+    header_roleCode:
+      description: Some role description
+      name: roleCode
+      in: header
+      required: true
+      schema:
+        type: string
+    header_Accept:
+      name: Accept
+      in: header
+      description: Some header description
+      required: false
+      schema:
+        type: string
diff --git a/__tests__/lint/assertions-camel-case-twice/snapshot.js b/__tests__/lint/assertions-camel-case-twice/snapshot.js
new file mode 100644
index 00000000..8f4fc10e
--- /dev/null
+++ b/__tests__/lint/assertions-camel-case-twice/snapshot.js
@@ -0,0 +1,40 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`E2E lint assertions-camel-case-twice 1`] = `
+
+validating /openapi.yaml...
+[1] openapi.yaml:26:5 at #/components/parameters/header_roleCode
+
+Parameters must use camelCase
+
+24 | components:
+25 |   parameters:
+26 |     header_roleCode:
+   |     ^^^^^^^^^^^^^^^
+27 |       description: Some role description
+28 |       name: roleCode
+
+Warning was generated by the named-parameters-camelCase assertion rule.
+
+
+[2] openapi.yaml:33:5 at #/components/parameters/header_Accept
+
+Parameters must use camelCase
+
+31 |   schema:
+32 |     type: string
+33 | header_Accept:
+   | ^^^^^^^^^^^^^
+34 |   name: Accept
+35 |   in: header
+
+Warning was generated by the named-parameters-camelCase assertion rule.
+
+
+/openapi.yaml: validated in <test>ms
+
+Woohoo! Your OpenAPI definition is valid. 🎉
+You have 2 warnings.
+
+
+`;
diff --git a/__tests__/lint/assertions-casing-camel-case-error/.redocly.yaml b/__tests__/lint/assertions-casing-camel-case-error/.redocly.yaml
index 78cf3cf3..2c99ff6a 100644
--- a/__tests__/lint/assertions-casing-camel-case-error/.redocly.yaml
+++ b/__tests__/lint/assertions-casing-camel-case-error/.redocly.yaml
@@ -4,20 +4,29 @@ apis:
 
 rules:
   assert/operation-get:
-    context:
-      - type: Operation
-        matchParentKeys: [get]
-    subject: Operation
-    property: operationId
+    subject: 
+      type: Operation
+      filterInParentKeys: [get]
+      property: operationId
+    assertions:
+      defined: true
+      casing: camelCase
     message: Operation id for get operation should be camelCase
-    casing: camelCase
+
+   
   assert/operation-id-camel-case:
-    subject: Operation
-    property: operationId
+    subject:
+      type: Operation
+      property: operationId
     message: Operation id should be camelCase
-    casing: camelCase
+    assertions: 
+      casing: camelCase
+
   assert/camel-case-on-value:
-    subject: NamedParameters
-    casing: camelCase
+    subject: 
+      type: NamedParameters
+    assertions:
+      casing: camelCase
     message: Named Parameters should be camelCase
+
 extends: []
diff --git a/__tests__/lint/assertions-casing-kebab-case-error/.redocly.yaml b/__tests__/lint/assertions-casing-kebab-case-error/.redocly.yaml
index 44f9485e..f2f6fad9 100644
--- a/__tests__/lint/assertions-casing-kebab-case-error/.redocly.yaml
+++ b/__tests__/lint/assertions-casing-kebab-case-error/.redocly.yaml
@@ -4,16 +4,20 @@ apis:
 
 rules:
   assert/operation-get:
-    context:
-      - type: Operation
-        matchParentKeys: [get]
-    subject: Operation
-    property: operationId
+    subject:
+      type: Operation
+      property: operationId
+      filterInParentKeys: [get]
+    assertions:
+      casing: kebab-case
     message: Operation id should be kebab-case
-    casing: kebab-case
+
   assert/operation-id-kebab-case:
-    subject: Operation
-    property: operationId
+    subject:
+      type: Operation
+      property: operationId
     message: Operation id should be kebab-case
-    casing: kebab-case
-extends: []
\ No newline at end of file
+    assertions:
+      casing: kebab-case
+
+extends: []
diff --git a/__tests__/lint/assertions-casing-pascal-case-error/.redocly.yaml b/__tests__/lint/assertions-casing-pascal-case-error/.redocly.yaml
index 52c5a2ef..5d1d216a 100644
--- a/__tests__/lint/assertions-casing-pascal-case-error/.redocly.yaml
+++ b/__tests__/lint/assertions-casing-pascal-case-error/.redocly.yaml
@@ -2,19 +2,22 @@ apis:
   main:
     root: ./openapi.yaml
 
-
 rules:
   assert/operation-get:
-    context:
-      - type: Operation
-        matchParentKeys: [get]
-    subject: Operation
-    property: operationId
+    subject:
+      type: Operation
+      property: operationId
+      filterInParentKeys: [get]
+    assertions:
+      casing: PascalCase
     message: Operation id should be PascalCase
-    casing: PascalCase
+
   assert/operation-id-pascal-case:
-    subject: Operation
-    property: operationId
+    subject:
+      type: Operation
+      property: operationId
     message: Operation id should be PascalCase
-    casing: PascalCase
-extends: []
\ No newline at end of file
+    assertions:
+      casing: PascalCase
+
+extends: []
diff --git a/__tests__/lint/assertions-casing-snake-case-error/.redocly.yaml b/__tests__/lint/assertions-casing-snake-case-error/.redocly.yaml
index 508d9a45..693de1cd 100644
--- a/__tests__/lint/assertions-casing-snake-case-error/.redocly.yaml
+++ b/__tests__/lint/assertions-casing-snake-case-error/.redocly.yaml
@@ -2,19 +2,22 @@ apis:
   main:
     root: ./openapi.yaml
 
-
 rules:
   assert/operation-id-pascal-case:
-    context:
-      - type: Operation
-        matchParentKeys: [get]
-    subject: Operation
-    property: operationId
+    subject: 
+      type: Operation
+      property: operationId
+      filterInParentKeys: [get]
     message: Operation id for get requests should be PascalCase
-    casing: PascalCase
+    assertions:
+      casing: PascalCase
+
   assert/operation-id-snake-case:
-    subject: Operation
-    property: operationId
+    subject: 
+      type: Operation
+      property: operationId
     message: Operation id should be snake_case
-    casing: snake_case
-extends: []
\ No newline at end of file
+    assertions: 
+      casing: snake_case
+
+extends: []
diff --git a/__tests__/lint/assertions-custom-function-options/.redocly.yaml b/__tests__/lint/assertions-custom-function-options/.redocly.yaml
index cf7e8b69..3e614950 100644
--- a/__tests__/lint/assertions-custom-function-options/.redocly.yaml
+++ b/__tests__/lint/assertions-custom-function-options/.redocly.yaml
@@ -5,38 +5,60 @@ plugins:
   - ./plugin.js
 rules:
   assert/operation-summary-length-custom:
-    subject: Operation
-    property: summary
+    subject: 
+      type: Operation
+      property: summary
     message: Custom message {{problems}}
-    minLength: 16
-    local/checkWordsStarts: 
-      words: 
-        - Create
-        - Retrieve
-        - Merge
-        - Delete
-        - List
-        - Upsert
-        - Update
-        - Approve
-        - Reject
-    local/checkWordsCount: 
-      min: 3
+    assertions:
+      minLength: 16
+      local/checkWordsStarts: 
+        words: 
+          - Create
+          - Retrieve
+          - Merge
+          - Delete
+          - List
+          - Upsert
+          - Update
+          - Approve
+          - Reject
+      local/checkWordsCount: 
+        min: 3
+
+
   assert/operation-summary-length:
-    subject: Operation
-    property: summary
-    minLength: 16
-    local/checkWordsStarts: 
-      words: 
-        - Create
-        - Retrieve
-        - Merge
-        - Delete
-        - List
-        - Upsert
-        - Update
-        - Approve
-        - Reject
-    local/checkWordsCount: 
-      min: 3
-  extends: []
+    subject: 
+      type: Operation
+      property: summary
+    assertions: 
+      minLength: 16
+      local/checkWordsStarts: 
+        words: 
+          - Create
+          - Retrieve
+          - Merge
+          - Delete
+          - List
+          - Upsert
+          - Update
+          - Approve
+          - Reject
+      local/checkWordsCount: 
+        min: 3
+
+  assert/operation-summary-length-with-where:
+    where:
+      - subject: 
+          type: Operation
+          property: summary
+        assertions:
+          local/checkWordsCount: 
+            min: 3
+    subject: 
+      type: Schema
+      property: type
+    assertions:
+      defined: true
+      const: integer
+    
+extends: []
diff --git a/__tests__/lint/assertions-custom-function-options/snapshot.js b/__tests__/lint/assertions-custom-function-options/snapshot.js
index ab21c346..d7f5a4aa 100644
--- a/__tests__/lint/assertions-custom-function-options/snapshot.js
+++ b/__tests__/lint/assertions-custom-function-options/snapshot.js
@@ -2,23 +2,6 @@
 
 exports[`E2E lint assertions-custom-function-options 1`] = `
 
-[1] .redocly.yaml:43:5 at #/styleguide/rules/extends
-
-Property \`extends\` is not expected here.
-
-41 |   local/checkWordsCount:
-42 |     min: 3
-43 | extends: []
-   | ^^^^^^^
-44 |
-
-Warning was generated by the configuration spec rule.
-
-
-You have 1 warning.
-No configurations were defined in extends -- using built in recommended configuration by default.
-Warning! This default behavior is going to be deprecated soon.
-
 validating /openapi.yaml...
 [1] openapi.yaml:20:16 at #/paths/~1pet~1findByStatus/get/summary
 
@@ -48,11 +31,23 @@ operation-summary-length assertion failed because the Operation summary didn't m
 Error was generated by the operation-summary-length assertion rule.
 
 
-[3] openapi.yaml:36:16 at #/paths/~1pet~1findByStatus/post/summary
+[3] openapi.yaml:31:23 at #/paths/~1pet~1findByStatus/get/responses/200/content/application~1json/schema/type
+
+operation-summary-length-with-where assertion failed because the Schema type didn't meet the assertions: string should be equal integer
 
-Custom message 
-- Should start with one of Create, Retrieve, Merge, Delete, List, Upsert, Update, Approve, Reject
-- Should have at least 3 words
+29 |     application/json:
+30 |       schema:
+31 |         type: 'string'
+   |               ^^^^^^^^
+32 | '404':
+33 |   description: example description
+
+Error was generated by the operation-summary-length-with-where assertion rule.
+
+
+[4] openapi.yaml:36:16 at #/paths/~1pet~1findByStatus/post/summary
+
+Custom message Should start with one of Create, Retrieve, Merge, Delete, List, Upsert, Update, Approve, Reject
 
 34 | post:
 35 |   operationId: example-post
@@ -64,11 +59,9 @@ Custom message
 Error was generated by the operation-summary-length-custom assertion rule.
 
 
-[4] openapi.yaml:36:16 at #/paths/~1pet~1findByStatus/post/summary
+[5] openapi.yaml:36:16 at #/paths/~1pet~1findByStatus/post/summary
 
-operation-summary-length assertion failed because the Operation summary didn't meet the assertions: 
-- Should start with one of Create, Retrieve, Merge, Delete, List, Upsert, Update, Approve, Reject
-- Should have at least 3 words
+Custom message Should have at least 3 words
 
 34 | post:
 35 |   operationId: example-post
@@ -77,40 +70,40 @@ operation-summary-length assertion failed because the Operation summary didn't m
 37 |   tags:
 38 |     - foo
 
-Error was generated by the operation-summary-length assertion rule.
+Error was generated by the operation-summary-length-custom assertion rule.
 
 
-[5] openapi.yaml:18:5 at #/paths/~1pet~1findByStatus/get
+[6] openapi.yaml:36:16 at #/paths/~1pet~1findByStatus/post/summary
 
-Every operation should have security defined on it or on the root level.
+operation-summary-length assertion failed because the Operation summary didn't meet the assertions: Should start with one of Create, Retrieve, Merge, Delete, List, Upsert, Update, Approve, Reject
 
-16 | paths:
-17 |   /pet/findByStatus:
-18 |     get:
-   |     ^^^
-19 |       operationId: example
-20 |       summary: List of pets
+34 | post:
+35 |   operationId: example-post
+36 |   summary: summary looooong
+   |            ^^^^^^^^^^^^^^^^
+37 |   tags:
+38 |     - foo
 
-Error was generated by the security-defined rule.
+Error was generated by the operation-summary-length assertion rule.
 
 
-[6] openapi.yaml:34:5 at #/paths/~1pet~1findByStatus/post
+[7] openapi.yaml:36:16 at #/paths/~1pet~1findByStatus/post/summary
 
-Every operation should have security defined on it or on the root level.
+operation-summary-length assertion failed because the Operation summary didn't meet the assertions: Should have at least 3 words
 
-32 |     '404':
-33 |       description: example description
 34 | post:
-   | ^^^^
 35 |   operationId: example-post
 36 |   summary: summary looooong
+   |            ^^^^^^^^^^^^^^^^
+37 |   tags:
+38 |     - foo
 
-Error was generated by the security-defined rule.
+Error was generated by the operation-summary-length assertion rule.
 
 
 /openapi.yaml: validated in <test>ms
 
-❌ Validation failed with 6 errors.
+❌ Validation failed with 7 errors.
 run \`openapi lint --generate-ignore-file\` to add all problems to the ignore file.
 
 
diff --git a/__tests__/lint/assertions-defined-error/.redocly.yaml b/__tests__/lint/assertions-defined-error/.redocly.yaml
index a669caea..0627c2f0 100644
--- a/__tests__/lint/assertions-defined-error/.redocly.yaml
+++ b/__tests__/lint/assertions-defined-error/.redocly.yaml
@@ -2,19 +2,21 @@ apis:
   main:
     root: ./openapi.yaml
 
-
 rules:
   assert/response-content-defined:
-    context:
-    - type: Response
-      excludeParentKeys: ['204']
-    subject: Response
-    property: content
+    subject:
+      type: Response
+      property: content
+      filterOutParentKeys: ['204']
     message: Response content should be defined
-    defined: true
+    assertions:
+      defined: true
   assert/operation-security:
-    subject: Operation
-    property: security
+    subject:
+      type: Operation
+      property: security
     message: Operation security should be defined
-    defined: true
+    assertions:
+      defined: true
+
 extends: []
diff --git a/__tests__/lint/assertions-enum-error/.redocly.yaml b/__tests__/lint/assertions-enum-error/.redocly.yaml
index f9398073..8d78bb9d 100644
--- a/__tests__/lint/assertions-enum-error/.redocly.yaml
+++ b/__tests__/lint/assertions-enum-error/.redocly.yaml
@@ -5,13 +5,15 @@ apis:
 rules:
   assert/operation-summary-value:
     subject:
-    - Operation
-    property: summary
+      type: Operation
+      property: summary
     message: Operation summary value should be among of predefined values
-    enum:
+    suggest:
       - test summary
       - test example
-    suggest:
+    assertions:
+      enum:
       - test summary
       - test example
-extends: []
\ No newline at end of file
+
+extends: []
diff --git a/__tests__/lint/assertions-enum-on-keys-error/.redocly.yaml b/__tests__/lint/assertions-enum-on-keys-error/.redocly.yaml
index bd5f7ddf..11223393 100644
--- a/__tests__/lint/assertions-enum-on-keys-error/.redocly.yaml
+++ b/__tests__/lint/assertions-enum-on-keys-error/.redocly.yaml
@@ -5,8 +5,10 @@ apis:
 rules:
   assert/media-type-application-json:
     subject:
-    - MediaTypesMap
+      type: MediaTypesMap
     message: Only application/json can be used
-    enum:
+    assertions: 
+      enum:
       - application/json
+
 extends: []
diff --git a/__tests__/lint/assertions-error/.redocly.yaml b/__tests__/lint/assertions-error/.redocly.yaml
index 6b59f3fb..244f2a6b 100644
--- a/__tests__/lint/assertions-error/.redocly.yaml
+++ b/__tests__/lint/assertions-error/.redocly.yaml
@@ -2,156 +2,206 @@ apis:
   main:
     root: ./openapi.yaml
 
-extends:
-  - recommended
+extends: []
+
 rules:
   assert/tag-description-pattern:
-    subject: Tag
-    property: description
+    subject: 
+      type: Tag
+      property: description
     message: Tag description must be at least 13 characters and end with a full stop.
     severity: error
-    minLength: 13
-    pattern: /\.$/
+    assertions:
+      minLength: 13
+      pattern: /\.$/
   # property example
   assert/path-item-get-defined:
-    subject: PathItem
-    property: get
+    subject: 
+      type: PathItem
+      property: get
     message: Every path item must have a GET operation.
-    defined: true
+    assertions: 
+      defined: true
   assert/path-item-get-required:
-    subject: PathItem
+    subject: 
+      type: PathItem
     message: Every path item must have a GET(required) operation.
-    required:
+    assertions:
+      required:
       - get
   assert/tag-name-and-desc:
-    subject: Tag
-    property:
-      - name
-      - description
+    subject: 
+      type: Tag
+      property:
+        - name
+        - description
     message: Every tag must have a name and description.
-    defined: true
+    assertions:
+      defined: true
   # context example
   assert/operation-w-context:
     subject:
-      - MediaTypesMap
-    context:
-      - type: Operation
-        excludeParentKeys:
+      type: MediaTypesMap
+    where:
+      - subject:
+          type: Operation
+          filterOutParentKeys:
           - put
-      - type: Response
-        matchParentKeys: ['201', '200']
-    disallowed: [ 'application/pdf' ]
+        assertions:
+          defined: true
+      - subject:
+          type: Response
+          filterInParentKeys: ['201', '200']
+        assertions:
+          defined: true
+    assertions:
+      disallowed: [ 'application/pdf' ]
     message: Media type should not be pdf
+
   # enum example
   assert/media-type-pdf:
-    subject: MediaTypesMap
+    subject: 
+      type: MediaTypesMap
     message: Only application/pdf can be used
     severity: error
-    enum:
+    assertions:
+      enum:
       - application/pdf
   assert/operation-summary-suggest:
-    subject: Operation
-    property: summary
+    subject: 
+      type: Operation
+      property: summary
     message: Summary must be one of the predefined values
     suggest:
       - change to 'My resource'
       - change to 'My collection'
     severity: error
-    enum:
+    assertions: 
+      enum:
       - My resource
       - My collection
   # pattern example
   assert/operation-summary-pattern:
-    subject: Operation
-    property: summary
+    subject: 
+      type: Operation
+      property: summary
     message: Summary should match a regex
     severity: error
-    pattern: /resource/
+    assertions:
+      pattern: /resource/
   # casing
   assert/operation-id-camel-case:
-    subject: Operation
-    property: operationId
+    subject: 
+      type: Operation
+      property: operationId
     message: NamedExamples key must be in camelCase
     severity: error
-    casing: camelCase
+    assertions:
+      casing: camelCase
   # mutuallyExclusive example
   assert/operation-mutually-exclusive:
-    subject: Operation
+    subject: 
+      type: Operation
     message: "Operation must not define both properties together: description and externalDocs"
     severity: error
-    mutuallyExclusive:
+    assertions:
+      mutuallyExclusive:
       - description
       - externalDocs
   # mutuallyRequired example
   assert/operation-mutually-required:
-    subject: SchemaProperties
-    context:
-      - type: Response
+    subject:
+      type: SchemaProperties
+    where:
+      - subject:
+          type: Response
+        assertions:
+          defined: true
     message: The created_at and updated_at properties are mutually required
     severity: error
-    mutuallyRequired:
+    assertions: 
+      mutuallyRequired:
       - created_at
       - updated_at
   # mutuallyRequired example with context
   assert/operation-mutually-required-w-context:
-    subject: Responses
-    context:
-        - type: Operation
-          matchParentKeys:
+    subject: 
+      type: Responses
+    where:
+        - subject:
+            type: Operation
+            filterInParentKeys:
             - put
+          assertions:
+            defined: true
     message: Must mutually define 200 and 201 responses for PUT requests.
     severity: error
-    mutuallyRequired:
+    assertions: 
+      mutuallyRequired:
       - '200'
       - '201'
   # disallowed example
   assert/operation-disallowed:
-    subject: Operation
+    subject: 
+      type: Operation
     message: x-code-samples and x-internal must not be defined
     severity: error
-    disallowed:
+    assertions: 
+      disallowed:
       - x-code-samples
       - x-internal
   # defined example
   assert/operation-x-code-samples-defined:
-    subject: Operation
-    property: x-codeSamples
+    subject: 
+      type: Operation
+      property: x-codeSamples
     message: x-codeSamples must be defined
     severity: error
-    defined: true
+    assertions: 
+      defined: true
   # undefined example
   assert/operation-x-code-samples-undefined:
-    subject: Operation
-    property: x-code-samples
+    subject: 
+      type: Operation
+      property: x-code-samples
     message: x-code-samples is deprecated
     suggest:
       - x-codeSamples instead of x-code-samples
     severity: error
-    undefined: true
+    assertions: 
+      defined: false
   # nonEmpty example
   assert/operation-summary-non-empty:
-    subject: Operation
-    property: summary
+    subject: 
+      type: Operation
+      property: summary
     message: Operation summary should not be empty
     severity: error
-    nonEmpty: true
+    assertions: 
+      nonEmpty: true
   # minLength example
   assert/operation-summary-min-length:
-    subject: Operation
-    property: summary
+    subject: 
+      type: Operation
+      property: summary
     message: Operation summary must have minimum of 2 chars length
     severity: error
-    minLength: 2
+    assertions: 
+      minLength: 2
   # maxLength example
   assert/operation-summary-max-length:
-    subject: Operation
-    property: summary
+    subject: 
+      type: Operation
+      property: summary
     message: Operation summary must have a maximum of 2 characters
     severity: error
-    maxLength: 2
+    assertions:
+      maxLength: 2
   # Example map subject example
   assert/exampleMap:
-    subject: ExamplesMap
+    subject: 
+      type: ExamplesMap
     message: Examples names must use camelCase
-    casing: camelCase
+    assertions:
+      casing: camelCase
     severity: error
diff --git a/__tests__/lint/assertions-error/snapshot.js b/__tests__/lint/assertions-error/snapshot.js
index b737977b..e0b10769 100644
--- a/__tests__/lint/assertions-error/snapshot.js
+++ b/__tests__/lint/assertions-error/snapshot.js
@@ -293,21 +293,7 @@ Operation summary must have minimum of 2 chars length
 Error was generated by the operation-summary-min-length assertion rule.
 
 
-[20] openapi.yaml:59:7 at #/paths/~1pet~1findByStatus/post/summary
-
-Operation object \`summary\` must be non-empty string.
-
-57 | post:
-58 |   operationId: EXAMPLE
-59 |   summary: ''
-   |   ^^^^^^^
-60 |   responses:
-61 |     '201':
-
-Error was generated by the operation-summary rule.
-
-
-[21] openapi.yaml:64:13 at #/paths/~1pet~1findByStatus/post/responses/201/content/application~1pdf
+[20] openapi.yaml:64:13 at #/paths/~1pet~1findByStatus/post/responses/201/content/application~1pdf
 
 Media type should not be pdf
 
@@ -321,7 +307,7 @@ Media type should not be pdf
 Error was generated by the operation-w-context assertion rule.
 
 
-[22] openapi.yaml:77:17 at #/paths/~1pet~1findByStatus/post/responses/404/content/application~1pdf/examples/credit-file-identity-address
+[21] openapi.yaml:77:17 at #/paths/~1pet~1findByStatus/post/responses/404/content/application~1pdf/examples/credit-file-identity-address
 
 Examples names must use camelCase
 
@@ -335,39 +321,9 @@ Examples names must use camelCase
 Error was generated by the exampleMap assertion rule.
 
 
-[23] openapi.yaml:21:5 at #/tags/2/description
-
-Tag object should contain \`description\` field.
-
-19 |   - name: about
-20 |     description: Everything about your Pets
-21 |   - name: Without description
-   |     ^^^^^^^^^^^^^^^^^^^^^^^^^
-22 | paths:
-23 |   /pet/findByStatus:
-
-Warning was generated by the tag-description rule.
-
-
-[24] openapi.yaml:78:19 at #/paths/~1pet~1findByStatus/post/responses/404/content/application~1pdf/examples/credit-file-identity-address/value
-
-Example value must conform to the schema: type must be string.
-
-76 |         examples:
-77 |           credit-file-identity-address:
-78 |             summary: Credit file with fallback
-   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-79 | x-code-samples:
-80 |   - lang: 'C#'
-
-referenced from openapi.yaml:74:15 at #/paths/~1pet~1findByStatus/post/responses/404/content/application~1pdf 
-
-Warning was generated by the no-invalid-media-type-examples rule.
-
-
 /openapi.yaml: validated in <test>ms
 
-❌ Validation failed with 22 errors and 2 warnings.
+❌ Validation failed with 21 errors.
 run \`openapi lint --generate-ignore-file\` to add all problems to the ignore file.
 
 
diff --git a/__tests__/lint/assertions-map-types/.redocly.yaml b/__tests__/lint/assertions-map-types/.redocly.yaml
index 88a5898f..b56ec8a5 100644
--- a/__tests__/lint/assertions-map-types/.redocly.yaml
+++ b/__tests__/lint/assertions-map-types/.redocly.yaml
@@ -5,22 +5,32 @@ apis:
 extends: []
 rules:
   assert/headerMap:
-    subject: HeadersMap
-    property: x-next
-    defined: false
+    subject: 
+      type: HeadersMap 
+      property: x-next
+    assertions:
+      defined: false
 
   assert/encodingMap:
-    subject: EncodingMap
-    casing: kebab-case
+    subject: 
+      type: EncodingMap
+    assertions:
+      casing: kebab-case
 
   assert/linkMap:
-    subject: LinksMap
-    pattern: /^pet/
+    subject: 
+      type: LinksMap
+    assertions: 
+      pattern: /^pet/
 
   assert/serverVariableMap:
-    subject: ServerVariablesMap
-    casing: flatcase
+    subject: 
+      type: ServerVariablesMap
+    assertions: 
+      casing: flatcase
 
   assert/callbackMap:
-    subject: CallbacksMap
-    casing: snake_case
+    subject: 
+      type: CallbacksMap
+    assertions:
+      casing: snake_case
diff --git a/__tests__/lint/assertions-match-parent-keys/.redocly.yaml b/__tests__/lint/assertions-match-parent-keys/.redocly.yaml
new file mode 100644
index 00000000..b0e7dda3
--- /dev/null
+++ b/__tests__/lint/assertions-match-parent-keys/.redocly.yaml
@@ -0,0 +1,22 @@
+apis:
+  main:
+    root: ./openapi.yaml
+rules:
+  assert/match-parent-keys-with-defined:
+    subject:
+      type: Operation
+      matchParentKeys: /^p/
+      property: x-test
+    assertions:
+      defined: true
+      const: true
+
+  assert/match-parent-keys-example-without-defined:
+    subject:
+      type: Operation
+      matchParentKeys: /^p/
+      property: x-test
+    assertions:
+      const: true
+
+extends: []
diff --git a/__tests__/lint/assertions-match-parent-keys/openapi.yaml b/__tests__/lint/assertions-match-parent-keys/openapi.yaml
new file mode 100644
index 00000000..8f31da48
--- /dev/null
+++ b/__tests__/lint/assertions-match-parent-keys/openapi.yaml
@@ -0,0 +1,47 @@
+openapi: 3.1.0
+servers:
+  - url: http://example.xyz
+info:
+  license:
+    name: test licence
+    url: http://example.xyz
+  title: Example OpenAPI 3 definition.
+  description: Example description
+  version: '1.0'
+  contact:
+    name: qa
+    url: https://swagger.io/specification/#definitions
+    email: email@redoc.ly
+
+paths:
+  /pet/findByStatus:
+    get:
+      operationId: getExample
+      responses:
+        '200':
+          description: example description
+        '404':
+          description: example description
+    post:
+      operationId: postExample
+      x-test: false
+      responses:
+        '200':
+          description: example description
+        '404':
+          description: example description
+    put:
+      operationId: putExample
+      x-test: true
+      responses:
+        '200':
+          description: example description
+        '404':
+          description: example description
+    patch:
+      operationId: putExample
+      responses:
+        '200':
+          description: example description
+        '404':
+          description: example description
diff --git a/__tests__/lint/assertions-match-parent-keys/snapshot.js b/__tests__/lint/assertions-match-parent-keys/snapshot.js
new file mode 100644
index 00000000..65f6b944
--- /dev/null
+++ b/__tests__/lint/assertions-match-parent-keys/snapshot.js
@@ -0,0 +1,58 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`E2E lint assertions-match-parent-keys 1`] = `
+
+validating /openapi.yaml...
+[1] openapi.yaml:27:15 at #/paths/~1pet~1findByStatus/post/x-test
+
+match-parent-keys-with-defined assertion failed because the Operation x-test didn't meet the assertions: false should be equal true
+
+25 | post:
+26 |   operationId: postExample
+27 |   x-test: false
+   |           ^^^^^
+28 |   responses:
+29 |     '200':
+
+Error was generated by the match-parent-keys-with-defined assertion rule.
+
+
+[2] openapi.yaml:27:15 at #/paths/~1pet~1findByStatus/post/x-test
+
+match-parent-keys-example-without-defined assertion failed because the Operation x-test didn't meet the assertions: false should be equal true
+
+25 | post:
+26 |   operationId: postExample
+27 |   x-test: false
+   |           ^^^^^
+28 |   responses:
+29 |     '200':
+
+Error was generated by the match-parent-keys-example-without-defined assertion rule.
+
+
+[3] openapi.yaml:42:7 at #/paths/~1pet~1findByStatus/patch/x-test
+
+match-parent-keys-with-defined assertion failed because the Operation x-test didn't meet the assertions: Should be defined
+
+40 |       description: example description
+41 | patch:
+42 |   operationId: putExample
+   |   ^^^^^^^^^^^^^^^^^^^^^^^
+43 |   responses:
+   |   ^^^^^^^^^^
+ … |   < 3 more lines >
+47 |       description: example description
+   |       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+48 |
+
+Error was generated by the match-parent-keys-with-defined assertion rule.
+
+
+/openapi.yaml: validated in <test>ms
+
+❌ Validation failed with 3 errors.
+run \`openapi lint --generate-ignore-file\` to add all problems to the ignore file.
+
+
+`;
diff --git a/__tests__/lint/assertions-max-length-array-error/.redocly.yaml b/__tests__/lint/assertions-max-length-array-error/.redocly.yaml
index 5106f2f1..cbd5c10b 100644
--- a/__tests__/lint/assertions-max-length-array-error/.redocly.yaml
+++ b/__tests__/lint/assertions-max-length-array-error/.redocly.yaml
@@ -4,8 +4,11 @@ apis:
 
 rules:
   assert/tags-max-length:
-    subject: Operation
-    property: tags
+    subject: 
+      type: Operation
+      property: tags
     message: Operation tags should have maximum 2 items
-    maxLength: 2
+    assertions:
+      maxLength: 2
+
 extends: []
diff --git a/__tests__/lint/assertions-max-length-string-error/.redocly.yaml b/__tests__/lint/assertions-max-length-string-error/.redocly.yaml
index ea990c07..a3fd2b47 100644
--- a/__tests__/lint/assertions-max-length-string-error/.redocly.yaml
+++ b/__tests__/lint/assertions-max-length-string-error/.redocly.yaml
@@ -4,8 +4,11 @@ apis:
 
 rules:
   assert/description-max-length:
-    subject: Info
-    property: description
+    subject: 
+      type: Info
+      property: description
     message: Info description should be maximum 10 chars length
-    maxLength: 10
+    assertions:
+      maxLength: 10
+
 extends: []
diff --git a/__tests__/lint/assertions-min-length-array-error/.redocly.yaml b/__tests__/lint/assertions-min-length-array-error/.redocly.yaml
index 4f3c0d68..a08dcb8b 100644
--- a/__tests__/lint/assertions-min-length-array-error/.redocly.yaml
+++ b/__tests__/lint/assertions-min-length-array-error/.redocly.yaml
@@ -4,8 +4,11 @@ apis:
 
 rules:
   assert/tags-min-length:
-    subject: Operation
-    property: tags
+    subject: 
+      type: Operation
+      property: tags
     message: Operation tags should have minimum 4 items
-    minLength: 4
+    assertions: 
+      minLength: 4
+
 extends: []
diff --git a/__tests__/lint/assertions-min-length-string-error/.redocly.yaml b/__tests__/lint/assertions-min-length-string-error/.redocly.yaml
index 1d69bbf6..c171b4b5 100644
--- a/__tests__/lint/assertions-min-length-string-error/.redocly.yaml
+++ b/__tests__/lint/assertions-min-length-string-error/.redocly.yaml
@@ -4,8 +4,11 @@ apis:
 
 rules:
   assert/description-min-length:
-    subject: Info
-    property: description
+    subject: 
+      type: Info
+      property: description
     message: Info description should be minimum 20 chars length
-    minLength: 20
+    assertions: 
+      minLength: 20
+
 extends: []
diff --git a/__tests__/lint/assertions-mutually-exclusive-error/.redocly.yaml b/__tests__/lint/assertions-mutually-exclusive-error/.redocly.yaml
index aa1fb302..0927c0a3 100644
--- a/__tests__/lint/assertions-mutually-exclusive-error/.redocly.yaml
+++ b/__tests__/lint/assertions-mutually-exclusive-error/.redocly.yaml
@@ -4,11 +4,18 @@ apis:
 
 rules:
   assert/path-item-mutually-exclusive:
-    context:
-      - type: PathItem
-    subject: Operation
+    where:
+      - subject:
+          type: PathItem
+          property: get
+        assertions:
+          defined: true
+    subject: 
+      type: Operation
     message: Operation should not have summary and tags fields simultaneously
-    mutuallyExclusive:
+    assertions:
+      mutuallyExclusive:
       - summary
       - tags
+
 extends: []
diff --git a/__tests__/lint/assertions-mutually-required-error/.redocly.yaml b/__tests__/lint/assertions-mutually-required-error/.redocly.yaml
index 261ab35e..8933e15e 100644
--- a/__tests__/lint/assertions-mutually-required-error/.redocly.yaml
+++ b/__tests__/lint/assertions-mutually-required-error/.redocly.yaml
@@ -4,11 +4,18 @@ apis:
 
 rules:
   assert/path-item-mutually-required:
-    context:
-      - type: PathItem
-    subject: Operation
+    where:
+      - subject:
+          type: PathItem
+          property: get
+        assertions:
+          defined: true
+    subject: 
+      type: Operation
     message: Operation should have summary and security fields simultaneously
-    mutuallyRequired:
+    assertions:
+      mutuallyRequired:
       - summary
       - security
+
 extends: []
diff --git a/__tests__/lint/assertions-non-empty-error/.redocly.yaml b/__tests__/lint/assertions-non-empty-error/.redocly.yaml
index ad986963..651857e8 100644
--- a/__tests__/lint/assertions-non-empty-error/.redocly.yaml
+++ b/__tests__/lint/assertions-non-empty-error/.redocly.yaml
@@ -4,8 +4,11 @@ apis:
 
 rules:
   assert/summary-non-empty:
-    subject: Operation
-    property: summary
+    subject: 
+      type: Operation
+      property: summary
     message: Operation summary should not be empty
-    nonEmpty: true
+    assertions: 
+      nonEmpty: true
+
 extends: []
diff --git a/__tests__/lint/assertions-non-empty-off/.redocly.yaml b/__tests__/lint/assertions-non-empty-off/.redocly.yaml
index 854a79d9..454132a7 100644
--- a/__tests__/lint/assertions-non-empty-off/.redocly.yaml
+++ b/__tests__/lint/assertions-non-empty-off/.redocly.yaml
@@ -4,9 +4,12 @@ apis:
 
 rules:
   assert/summary-non-empty:
-    subject: Operation
-    property: summary
+    subject: 
+      type: Operation
+      property: summary
     message: Operation summary should not be empty
-    nonEmpty: true
+    assertions: 
+      nonEmpty: true
     severity: off
+
 extends: []
diff --git a/__tests__/lint/assertions-non-empty-warn/.redocly.yaml b/__tests__/lint/assertions-non-empty-warn/.redocly.yaml
index 3bbb2824..f3098420 100644
--- a/__tests__/lint/assertions-non-empty-warn/.redocly.yaml
+++ b/__tests__/lint/assertions-non-empty-warn/.redocly.yaml
@@ -4,9 +4,12 @@ apis:
 
 rules:
   assert/summary-non-empty:
-    subject: Operation
-    property: summary
+    subject: 
+      type: Operation
+      property: summary
     message: Operation summary should not be empty
-    nonEmpty: true
+    assertions: 
+      nonEmpty: true
     severity: warn
+
 extends: []
diff --git a/__tests__/lint/assertions-pattern-error/.redocly.yaml b/__tests__/lint/assertions-pattern-error/.redocly.yaml
index f83ffbb7..4115759b 100644
--- a/__tests__/lint/assertions-pattern-error/.redocly.yaml
+++ b/__tests__/lint/assertions-pattern-error/.redocly.yaml
@@ -4,8 +4,11 @@ apis:
 
 rules:
   assert/summary-pattern:
-    subject: Operation
-    property: summary
+    subject: 
+      type: Operation
+      property: summary
     message: Operation summary should have 'test' word
-    pattern: '/test/'
+    assertions: 
+      pattern: '/test/'
+
 extends: []
diff --git a/__tests__/lint/assertions-query-in-parameters/.redocly.yaml b/__tests__/lint/assertions-query-in-parameters/.redocly.yaml
new file mode 100644
index 00000000..789ade17
--- /dev/null
+++ b/__tests__/lint/assertions-query-in-parameters/.redocly.yaml
@@ -0,0 +1,17 @@
+apis:
+  main:
+    root: ./openapi.yaml
+rules: 
+  assert/query-in-parameters:
+    where:
+      - subject: 
+          type: Parameter
+          property: name
+        assertions:
+          const: statusId
+    subject: 
+      type: Parameter
+      property: in
+    assertions: 
+      const: path
+extends: []
\ No newline at end of file
diff --git a/__tests__/lint/assertions-query-in-parameters/openapi.yaml b/__tests__/lint/assertions-query-in-parameters/openapi.yaml
new file mode 100644
index 00000000..7c37192c
--- /dev/null
+++ b/__tests__/lint/assertions-query-in-parameters/openapi.yaml
@@ -0,0 +1,31 @@
+openapi: 3.1.0
+servers:
+  - url: http://example.xyz
+info:
+  license:
+    name: test licence
+    url: http://example.xyz
+  title: Example OpenAPI 3 definition.
+  description: Example description
+  version: '1.0'
+  contact:
+    name: qa
+    url: https://swagger.io/specification/#definitions
+    email: email@redoc.ly
+
+paths:
+  /pet/findByStatus:
+    get:
+      operationId: GetExampleId
+      summary: summary example
+      parameters: 
+        - name: statusId
+          in: query
+        - name: otherParam
+          in: query
+      responses:
+        '200':
+          description: example description
+        '404':
+          description: example description
+    
\ No newline at end of file
diff --git a/__tests__/lint/assertions-query-in-parameters/snapshot.js b/__tests__/lint/assertions-query-in-parameters/snapshot.js
new file mode 100644
index 00000000..548ab781
--- /dev/null
+++ b/__tests__/lint/assertions-query-in-parameters/snapshot.js
@@ -0,0 +1,26 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`E2E lint assertions-query-in-parameters 1`] = `
+
+validating /openapi.yaml...
+[1] openapi.yaml:23:15 at #/paths/~1pet~1findByStatus/get/parameters/0/in
+
+query-in-parameters assertion failed because the Parameter in didn't meet the assertions: query should be equal path
+
+21 | parameters: 
+22 |   - name: statusId
+23 |     in: query
+   |         ^^^^^
+24 |   - name: otherParam
+25 |     in: query
+
+Error was generated by the query-in-parameters assertion rule.
+
+
+/openapi.yaml: validated in <test>ms
+
+❌ Validation failed with 1 error.
+run \`openapi lint --generate-ignore-file\` to add all problems to the ignore file.
+
+
+`;
diff --git a/__tests__/lint/assertions-ref-forbidden-error/.redocly.yaml b/__tests__/lint/assertions-ref-forbidden-error/.redocly.yaml
index 31fb6d80..59533029 100644
--- a/__tests__/lint/assertions-ref-forbidden-error/.redocly.yaml
+++ b/__tests__/lint/assertions-ref-forbidden-error/.redocly.yaml
@@ -4,14 +4,22 @@ apis:
 
 rules:
   assert/ref-forbidden:
-    context:
-      - type: Response
-    subject: MediaType
-    property: schema
+    where:
+      - subject:
+          type: Response
+        assertions: 
+          defined: true
+    subject:
+      type: MediaType
+      property: schema
     message: Response MediaType schema should NOT have a ref
-    ref: false
+    assertions: 
+      ref: false
   assert/ref-forbidden-no-property:
-    subject: PathItem
+    subject: 
+      type: PathItem
     message: PathItems should NOT should have a ref
-    ref: false
+    assertions: 
+      ref: false
+
 extends: []
diff --git a/__tests__/lint/assertions-ref-pattern-error/.redocly.yaml b/__tests__/lint/assertions-ref-pattern-error/.redocly.yaml
index fd117fd0..e2c93ac1 100644
--- a/__tests__/lint/assertions-ref-pattern-error/.redocly.yaml
+++ b/__tests__/lint/assertions-ref-pattern-error/.redocly.yaml
@@ -4,14 +4,22 @@ apis:
 
 rules:
   assert/ref-pattern:
-    context:
-      - type: Response
-    subject: MediaType
-    property: schema
+    where:
+      - subject:
+          type: Response
+        assertions:
+          defined: true
+    subject: 
+      type: MediaType
+      property: schema
     message: Response MediaType schema should contain ref to components/schemas folder
-    ref: ^(\.\/)?components\/schemas\/.*\.yaml$
+    assertions:
+      ref: ^(\.\/)?components\/schemas\/.*\.yaml$
   assert/ref-pattern-no-properties:
-    subject: PathItem
+    subject: 
+      type: PathItem
     message: PathItem should contain ref to components/paths folder
-    ref: ^(\.\/)?components\/paths\/.*\.yaml$
+    assertions:
+      ref: ^(\.\/)?components\/paths\/.*\.yaml$
+
 extends: []
diff --git a/__tests__/lint/assertions-ref-required-error/.redocly.yaml b/__tests__/lint/assertions-ref-required-error/.redocly.yaml
index 1369922b..51c0ed1f 100644
--- a/__tests__/lint/assertions-ref-required-error/.redocly.yaml
+++ b/__tests__/lint/assertions-ref-required-error/.redocly.yaml
@@ -4,14 +4,22 @@ apis:
 
 rules:
   assert/ref-required:
-    context:
-      - type: Response
-    subject: MediaType
-    property: schema
+    where:
+      - subject:
+          type: Response
+        assertions:
+          defined: true
+    subject: 
+      type: MediaType
+      property: schema
     message: Response MediaType schema should have a ref
-    ref: true
+    assertions:
+      ref: true
   assert/ref-required-no-property:
-    subject: PathItem
+    subject: 
+      type: PathItem
     message: PathItems should have refs
-    ref: true
+    assertions:
+      ref: true
+
 extends: []
diff --git a/__tests__/lint/assertions-require-any-error/.redocly.yaml b/__tests__/lint/assertions-require-any-error/.redocly.yaml
index 558b0321..8d5ab790 100644
--- a/__tests__/lint/assertions-require-any-error/.redocly.yaml
+++ b/__tests__/lint/assertions-require-any-error/.redocly.yaml
@@ -4,10 +4,13 @@ apis:
 
 rules:
   assert/operation-require-any-description-or-external:
-    subject: Operation
+    subject: 
+      type: Operation
     message: 'Operation must have one of properties defined: description or externalDocs'
     severity: error
-    requireAny:
+    assertions:
+      requireAny:
       - description
       - externalDocs
+
 extends: []
diff --git a/__tests__/lint/assertions-show-suggestions/.redocly.yaml b/__tests__/lint/assertions-show-suggestions/.redocly.yaml
index 28311164..e8d4ad30 100644
--- a/__tests__/lint/assertions-show-suggestions/.redocly.yaml
+++ b/__tests__/lint/assertions-show-suggestions/.redocly.yaml
@@ -4,13 +4,20 @@ apis:
 
 rules:
   assert/path-item-suggest:
-    context:
-      - type: PathItem
-    subject: Operation
-    property: summary
+    where:
+      - subject:
+          type: PathItem
+          property: get
+        assertions:
+          defined: true
+    subject: 
+      type: Operation
+      property: summary
     message: Operation summary should have at least 10 chars length
     suggest:
       - Suggestion 1
       - Suggestion 2
-    minLength: 20
+    assertions:
+      minLength: 20
+
 extends: []
diff --git a/__tests__/lint/assertions-type-integer-in-parameter/.redocly.yaml b/__tests__/lint/assertions-type-integer-in-parameter/.redocly.yaml
new file mode 100644
index 00000000..952cd228
--- /dev/null
+++ b/__tests__/lint/assertions-type-integer-in-parameter/.redocly.yaml
@@ -0,0 +1,20 @@
+apis:
+  main:
+    root: ./openapi.yaml
+rules:
+  assert/no-type-integer-in-parameter:
+    where:
+      - subject:
+          type: Parameter
+          property: example
+        assertions:
+          defined: true
+          enum:
+            - en-US
+    subject: 
+      type: Schema
+      property: type
+    assertions:
+      enum:
+        - string
+extends: []
diff --git a/__tests__/lint/assertions-type-integer-in-parameter/openapi.yaml b/__tests__/lint/assertions-type-integer-in-parameter/openapi.yaml
new file mode 100644
index 00000000..e5959dd4
--- /dev/null
+++ b/__tests__/lint/assertions-type-integer-in-parameter/openapi.yaml
@@ -0,0 +1,32 @@
+openapi: 3.1.0
+servers:
+  - url: http://example.xyz
+info:
+  license:
+    name: test licence
+    url: http://example.xyz
+  title: Example OpenAPI 3 definition.
+  description: Example description
+  version: '1.0'
+  contact:
+    name: qa
+    url: https://swagger.io/specification/#definitions
+    email: email@redoc.ly
+
+paths:
+  /pet/findByStatus:
+    get:
+      operationId: GetExampleId
+      summary: summary example
+      parameters: 
+        - name: statusId
+          in: path
+          example: en-US
+          schema: 
+            type: integer
+      responses:
+        '200':
+          description: example description
+        '404':
+          description: example description
+    
\ No newline at end of file
diff --git a/__tests__/lint/assertions-type-integer-in-parameter/snapshot.js b/__tests__/lint/assertions-type-integer-in-parameter/snapshot.js
new file mode 100644
index 00000000..c1f069dc
--- /dev/null
+++ b/__tests__/lint/assertions-type-integer-in-parameter/snapshot.js
@@ -0,0 +1,26 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`E2E lint assertions-type-integer-in-parameter 1`] = `
+
+validating /openapi.yaml...
+[1] openapi.yaml:26:19 at #/paths/~1pet~1findByStatus/get/parameters/0/schema/type
+
+no-type-integer-in-parameter assertion failed because the Schema type didn't meet the assertions: "integer" should be one of the predefined values
+
+24 |     example: en-US
+25 |     schema: 
+26 |       type: integer
+   |             ^^^^^^^
+27 | responses:
+28 |   '200':
+
+Error was generated by the no-type-integer-in-parameter assertion rule.
+
+
+/openapi.yaml: validated in <test>ms
+
+❌ Validation failed with 1 error.
+run \`openapi lint --generate-ignore-file\` to add all problems to the ignore file.
+
+
+`;
diff --git a/__tests__/lint/assertions-type-integer-in-schema-response/.redocly.yaml b/__tests__/lint/assertions-type-integer-in-schema-response/.redocly.yaml
new file mode 100644
index 00000000..7da7782b
--- /dev/null
+++ b/__tests__/lint/assertions-type-integer-in-schema-response/.redocly.yaml
@@ -0,0 +1,30 @@
+apis:
+  main:
+    root: ./openapi.yaml
+rules:
+  assert/type-integer-in-schema-response:
+    where:
+      - subject:
+          type: Operation
+          filterInParentKeys: [get]
+        assertions:
+          defined: true
+      - subject: 
+          type: Response
+          filterInParentKeys: ['201', '200']
+        assertions: 
+          defined: true
+      - subject: 
+          type: MediaType
+          property: example
+        assertions: 
+          defined: true
+          const: example
+          
+    subject: 
+      type: Schema
+      property: type
+    assertions: 
+      const: string
+
+extends: []
diff --git a/__tests__/lint/assertions-type-integer-in-schema-response/openapi.yaml b/__tests__/lint/assertions-type-integer-in-schema-response/openapi.yaml
new file mode 100644
index 00000000..7be48651
--- /dev/null
+++ b/__tests__/lint/assertions-type-integer-in-schema-response/openapi.yaml
@@ -0,0 +1,45 @@
+openapi: 3.1.0
+servers:
+  - url: http://example.xyz
+info:
+  license:
+    name: test licence
+    url: http://example.xyz
+  title: Example OpenAPI 3 definition.
+  description: Example description
+  version: '1.0'
+  contact:
+    name: qa
+    url: https://swagger.io/specification/#definitions
+    email: email@redoc.ly
+
+paths:
+  /pet/findByStatus:
+    get:
+      operationId: GetExampleId
+      summary: summary example
+      responses:
+        '200':
+          content: 
+            application/json:
+              example: example
+              schema: 
+                type: string
+          description: example description
+        '404':
+          description: example description
+  /pet/{id}:
+    post:
+      operationId: addExample
+      summary: summary example
+      responses:
+        '200':
+          content: 
+            application/json:
+              example: example
+              schema: 
+                type: integer
+          description: example description
+        '404':
+          description: example description
+    
\ No newline at end of file
diff --git a/__tests__/lint/assertions-type-integer-in-schema-response/snapshot.js b/__tests__/lint/assertions-type-integer-in-schema-response/snapshot.js
new file mode 100644
index 00000000..10d184ef
--- /dev/null
+++ b/__tests__/lint/assertions-type-integer-in-schema-response/snapshot.js
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`E2E lint assertions-type-integer-in-schema-response 1`] = `
+
+validating /openapi.yaml...
+/openapi.yaml: validated in <test>ms
+
+Woohoo! Your OpenAPI definition is valid. 🎉
+
+
+`;
diff --git a/__tests__/lint/assertions-undefined-error/.redocly.yaml b/__tests__/lint/assertions-undefined-error/.redocly.yaml
index eba8d6bd..40e3e44d 100644
--- a/__tests__/lint/assertions-undefined-error/.redocly.yaml
+++ b/__tests__/lint/assertions-undefined-error/.redocly.yaml
@@ -4,8 +4,10 @@ apis:
 
 rules:
   assert/summary-undefined:
-    subject: Operation
-    property: summary
+    subject: 
+      type: Operation
+      property: summary
     message: Operation summary should be undefined
-    undefined: true
+    assertions:
+      defined: false
 extends: []
diff --git a/__tests__/lint/assertions/.redocly.yaml b/__tests__/lint/assertions/.redocly.yaml
index fa4f6c6a..6863566b 100644
--- a/__tests__/lint/assertions/.redocly.yaml
+++ b/__tests__/lint/assertions/.redocly.yaml
@@ -5,161 +5,215 @@ apis:
 extends: []
 rules:
   assert/tag-description:
-    subject: Tag
-    property: description
+    subject:
+      type: Tag
+      property: description
     message: Tag description must be at least 13 characters and end with a full stop.
     severity: error
-    minLength: 13
-    pattern: /\.$/
+    assertions:
+      minLength: 13
+      pattern: /\.$/
   # property example
   assert/path-item-get-defined:
-    subject: PathItem
-    property: get
+    subject: 
+      type: PathItem
+      property: get
     message: Every path item must have a GET operation.
-    defined: true
+    assertions:
+      defined: true
   assert/path-item-get-required:
-    subject: PathItem
+    subject: 
+      type: PathItem
     message: Every path item must have a GET operation.
-    required:
-      - get
+    assertions:
+      required:
+        - get
   assert/tag-name-and-desc:
-    subject: Tag
-    property:
-      - name
-      - description
+    subject: 
+      type: Tag
+      property:
+        - name
+        - description
     message: Every tag must have a name and description.
-    defined: true
+    assertions:
+      defined: true
+
   # context example
   assert/media-type-map-not-pdf:
     subject:
-      - MediaTypesMap
-    context:
-      - type: Operation
-        excludeParentKeys:
-          - put
-      - type: Response
-        matchParentKeys: ['201', '200']
-    disallowed: ['application/pdf']
+      type: MediaTypesMap
+    assertions:
+      disallowed: ['application/pdf']
     message: Media type should not be pdf
+    where: 
+      - subject: 
+          type: Operation
+          filterOutParentKeys:
+            - put
+        assertions: 
+          defined: true
+      - subject:
+          type: Response
+          filterInParentKeys: ['201', '200']
+          property: description
+        assertions: 
+          defined: true
+   
   # enum example
   assert/media-type-map-enum:
-    subject: MediaTypesMap
+    subject: 
+      type: MediaTypesMap
     message: Only application/json and application/pdf can be used
     severity: error
-    enum:
-      - application/json
-      - application/pdf
+    assertions:
+      enum:
+        - application/json
+        - application/pdf
+
   assert/operation-summary-suggest:
-    subject: Operation
-    property: summary
+    subject: 
+      type: Operation
+      property: summary
     message: Summary must be one of the predefined values
     suggest:
       - change to 'My resource'
       - change to 'My collection'
     severity: error
-    enum:
-      - My resource
-      - My collection
+    assertions: 
+      enum:
+        - My resource
+        - My collection
+
   # pattern example
   assert/operation-summary-pattern:
-    subject: Operation
-    property: summary
+    subject: 
+      type: Operation
+      property: summary
     message: Summary should match a regex
     severity: error
-    pattern: /resource/
+    assertions:
+      pattern: /resource/
   # casing
   assert/operation-id-casing:
-    subject: Operation
-    property: operationId
+    subject: 
+      type: Operation
+      property: operationId
     message: NamedExamples key must be in camelCase
     severity: error
-    casing: camelCase
+    assertions:
+      casing: camelCase
   # mutuallyExclusive example
   assert/operation-mutually-exclusive:
-    subject: Operation
+    subject: 
+      type: Operation
     message: 'Operation must not define both properties together: description and externalDocs'
     severity: error
-    mutuallyExclusive:
-      - description
-      - externalDocs
+    assertions:
+      mutuallyExclusive:
+        - description
+        - externalDocs
   # mutuallyRequired example
   assert/schema-properties-mutually-required:
-    subject: SchemaProperties
-    context:
-      - type: Response
+    subject: 
+      type: SchemaProperties
+    assertions:
+      mutuallyRequired:
+        - created_at
+        - updated_at
     message: The created_at and updated_at properties are mutually required
     severity: error
-    mutuallyRequired:
-      - created_at
-      - updated_at
+
+    where:
+      - subject: 
+          type: Response
+        assertions:
+          defined: true
+    
   # mutuallyRequired example with context
   assert/response-map-required-with-context:
-    subject: Responses
-    context:
-      - type: Operation
-        matchParentKeys:
-          - put
+    subject: 
+      type: Responses
+    assertions: 
+      mutuallyRequired:
+        - '200'
+        - '201'
+    where:
+      - subject: 
+          type: Operation
+          filterInParentKeys:
+            - put
+        assertions: 
+          defined: true
     message: Must mutually define 200 and 201 responses for PUT requests.
     severity: error
-    mutuallyRequired:
-      - '200'
-      - '201'
+  
   # requireAny example
   assert/operation-require-any-description-or-external:
-    subject: Operation
+    subject: 
+      type: Operation
     message: 'Operation must have one of properties defined: description or externalDocs'
     severity: error
-    requireAny:
-      - description
-      - externalDocs
+    assertions:
+      requireAny:
+        - description
+        - externalDocs
+
   # disallowed example
   assert/operation-disallowed:
-    subject: Operation
+    subject: 
+      type: Operation
     message: x-code-samples and x-internal must not be defined
     severity: error
-    disallowed:
-      - x-code-samples
-      - x-internal
+    assertions: 
+      disallowed:
+        - x-code-samples
+        - x-internal
+
   # defined example
   assert/operation-defined:
-    subject: Operation
-    property: x-codeSamples
+    subject:
+      type: Operation
+      property: x-codeSamples
     message: x-codeSamples must be defined
     severity: error
-    defined: true
-  # undefined example
-  assert/operation-undefined:
-    subject: Operation
-    property: x-code-samples
-    message: x-code-samples is deprecated
-    suggest:
-      - x-codeSamples instead of x-code-samples
-    severity: error
-    undefined: true
+    assertions: 
+      defined: true
+
   # nonEmpty example
   assert/operation-non-empty:
-    subject: Operation
-    property: summary
+    subject: 
+      type: Operation
+      property: summary
     message: Operation summary should not be empty
     severity: error
-    nonEmpty: true
+    assertions:
+      nonEmpty: true
+
   # minLength example
   assert/operation-min-length:
-    subject: Operation
-    property: summary
+    subject: 
+      type: Operation
+      property: summary
     message: Operation summary must have minimum of 2 chars length
     severity: error
-    minLength: 2
+    assertions:
+      minLength: 2
+
   # maxLength example
   assert/operation-max-length:
-    subject: Operation
-    property: summary
+    subject: 
+      type: Operation
+      property: summary
     message: Operation summary must have a maximum of 2 characters
     severity: error
-    maxLength: 20
+    assertions:
+      maxLength: 20
+
   # ref example
   assert/ref:
-    subject: PathItem
+    subject: 
+      type: PathItem
     message: No refs on Path Items
     severity: error
-    ref: false
+    assertions:
+      ref: false
diff --git a/packages/core/src/__tests__/lint.test.ts b/packages/core/src/__tests__/lint.test.ts
index 3aadaa6e..f974407d 100644
--- a/packages/core/src/__tests__/lint.test.ts
+++ b/packages/core/src/__tests__/lint.test.ts
@@ -61,11 +61,13 @@ describe('lint', () => {
           path-http-verbs-order: error
           boolean-parameter-prefixes: off
           assert/operation-summary-length:
-            subject: Operation
-            property: summary
+            subject: 
+              type: Operation
+              property: summary
             message: Operation summary should start with an active verb
-            local/checkWordsCount: 
-              min: 3
+            assertions:
+              local/checkWordsCount: 
+                min: 3
       features.openapi:
         showConsole: true
         layout:
diff --git a/packages/core/src/config/__tests__/config-resolvers.test.ts b/packages/core/src/config/__tests__/config-resolvers.test.ts
index 45138d56..3d728a89 100644
--- a/packages/core/src/config/__tests__/config-resolvers.test.ts
+++ b/packages/core/src/config/__tests__/config-resolvers.test.ts
@@ -1,5 +1,5 @@
 import { colorize } from '../../logger';
-import { asserts } from '../../rules/common/assertions/asserts';
+import { Asserts, asserts } from '../../rules/common/assertions/asserts';
 import { resolveStyleguideConfig, resolveApis, resolveConfig } from '../config-resolvers';
 const path = require('path');
 
@@ -143,7 +143,7 @@ describe('resolveStyleguideConfig', () => {
 
     expect(plugins).toBeDefined();
     expect(plugins?.length).toBe(2);
-    expect(asserts['test-plugin/checkWordsCount']).toBeDefined();
+    expect(asserts['test-plugin/checkWordsCount' as keyof Asserts]).toBeDefined();
   });
 
   it('should throw error when custom assertion load not exist plugin', async () => {
@@ -163,7 +163,7 @@ describe('resolveStyleguideConfig', () => {
       );
     }
 
-    expect(asserts['test-plugin/checkWordsCount']).toBeDefined();
+    expect(asserts['test-plugin/checkWordsCount' as keyof Asserts]).toBeDefined();
   });
 
   it('should correctly merge assertions from nested config', async () => {
diff --git a/packages/core/src/config/__tests__/fixtures/resolve-config/local-config-with-custom-function.yaml b/packages/core/src/config/__tests__/fixtures/resolve-config/local-config-with-custom-function.yaml
index 5e3376f0..35f4defc 100644
--- a/packages/core/src/config/__tests__/fixtures/resolve-config/local-config-with-custom-function.yaml
+++ b/packages/core/src/config/__tests__/fixtures/resolve-config/local-config-with-custom-function.yaml
@@ -3,12 +3,14 @@ lint:
     no-invalid-media-type-examples: warn
     operation-4xx-response: off
     assert/tag-description:
-      subject: Tag
-      property: description
+      subject: 
+        type: Tag
+        property: description
       message: Tag description must have at least 3 words.
       severity: error
-      test-plugin/checkWordsCount:
-        min: 3
+      assertions:
+        test-plugin/checkWordsCount:
+          min: 3
   plugins:
     - plugin.js
   extends:
diff --git a/packages/core/src/rules/common/assertions/__tests__/asserts.test.ts b/packages/core/src/rules/common/assertions/__tests__/asserts.test.ts
index 91080d26..acaefa8a 100644
--- a/packages/core/src/rules/common/assertions/__tests__/asserts.test.ts
+++ b/packages/core/src/rules/common/assertions/__tests__/asserts.test.ts
@@ -1,6 +1,6 @@
 import { Location } from '../../../../ref-utils';
 import { Source } from '../../../../resolve';
-import { asserts, buildAssertCustomFunction } from '../asserts';
+import { Asserts, asserts, buildAssertCustomFunction } from '../asserts';
 
 let baseLocation = new Location(jest.fn() as any as Source, 'pointer');
 
@@ -674,9 +674,13 @@ describe('oas3 assertions', () => {
           }
           return [];
         });
-        asserts['local/customFn'] = buildAssertCustomFunction(customFn);
+        asserts['local/customFn' as keyof Asserts] = buildAssertCustomFunction(customFn);
         expect(
-          asserts['local/customFn'](Object.keys(fakeNode), { word: 'foo' }, baseLocation)
+          asserts['local/customFn' as keyof Asserts](
+            Object.keys(fakeNode),
+            { word: 'foo' },
+            baseLocation
+          )
         ).toEqual([
           {
             message: 'First value should be foo',
diff --git a/packages/core/src/rules/common/assertions/__tests__/index.test.ts b/packages/core/src/rules/common/assertions/__tests__/index.test.ts
index 47f3c500..a1f5bcfa 100644
--- a/packages/core/src/rules/common/assertions/__tests__/index.test.ts
+++ b/packages/core/src/rules/common/assertions/__tests__/index.test.ts
@@ -1,61 +1,82 @@
-import { Assertions } from '../.';
+import { Assertion, Assertions } from '../.';
 
 const opts = {
   '0': {
-    subject: 'Operation',
-    property: 'summary',
+    subject: {
+      type: 'Operation',
+      property: 'summary',
+    },
     description: 'example warn text',
     severity: 'warn',
-    pattern: '/example/',
+    assertions: { pattern: '/example/' },
   },
   '1': {
-    subject: 'PathItem',
-    context: [{ type: 'Operation', matchParentKeys: ['post'] }],
+    subject: {
+      type: 'PathItem',
+    },
+    where: [
+      {
+        subject: { type: 'Operation', filterInParentKeys: ['post'], property: 'responses' },
+        assertions: { defined: true },
+      },
+    ],
     description: 'example warn text',
     severity: 'warn',
-    mutuallyExclusive: ['summary', 'security'],
+    assertions: { mutuallyExclusive: ['summary', 'security'] },
   },
   '2': {
-    subject: ['PathItem'],
-    context: [{ type: 'Operation' }],
-    property: 'tags',
+    subject: { type: 'PathItem', property: 'tags' },
+    where: [
+      { subject: { type: 'Operation', property: 'responses' }, assertions: { defined: true } },
+    ],
     description: 'example warn text',
     severity: 'warn',
-    sortOrder: 'desc',
+    assertions: { sortOrder: 'desc' },
   },
   '3': {
-    subject: ['Foo'],
-    context: [{ type: 'Bar' }, { type: 'Baz' }],
-    property: 'test',
+    subject: { type: 'Foo', property: 'test' },
+    where: [
+      { subject: { type: 'Bar' }, assertions: {} },
+      { subject: { type: 'Baz' }, assertions: {} },
+    ],
     description: 'example warn text',
     severity: 'warn',
-    sortOrder: 'desc',
+    assertions: { sortOrder: 'desc' },
   },
 };
 
 describe('Oas3 assertions', () => {
   it('should return the right visitor structure', () => {
-    const visitors = Assertions(opts) as any;
+    const visitors = Assertions(opts as any);
     expect(visitors).toMatchInlineSnapshot(`
       Array [
         Object {
-          "Operation": [Function],
+          "Operation": Object {
+            "enter": [Function],
+          },
         },
         Object {
           "Operation": Object {
-            "PathItem": [Function],
+            "PathItem": Object {
+              "enter": [Function],
+            },
             "skip": [Function],
           },
         },
         Object {
           "Operation": Object {
-            "PathItem": [Function],
+            "PathItem": Object {
+              "enter": [Function],
+            },
+            "skip": [Function],
           },
         },
         Object {
           "Bar": Object {
             "Baz": Object {
-              "Foo": [Function],
+              "Foo": Object {
+                "enter": [Function],
+              },
             },
           },
         },
diff --git a/packages/core/src/rules/common/assertions/__tests__/utils.test.ts b/packages/core/src/rules/common/assertions/__tests__/utils.test.ts
index 20879e31..c69b2456 100644
--- a/packages/core/src/rules/common/assertions/__tests__/utils.test.ts
+++ b/packages/core/src/rules/common/assertions/__tests__/utils.test.ts
@@ -1,3 +1,4 @@
+import { Assertion, AssertionDefinition } from '..';
 import { isOrdered, buildVisitorObject, getIntersectionLength } from '../utils';
 
 describe('Oas3 assertions', () => {
@@ -25,29 +26,43 @@ describe('Oas3 assertions', () => {
 
     describe('buildVisitorObject', () => {
       it('should return a consistent visitor structure', () => {
-        const context = [
+        const where: AssertionDefinition[] = [
           {
-            type: 'Foo',
-            matchParentKeys: ['test'],
+            subject: {
+              type: 'Foo',
+              filterInParentKeys: ['test'],
+            },
+            assertions: {},
           },
           {
-            type: 'Bar',
-            matchParentKeys: ['test'],
+            subject: {
+              type: 'Bar',
+              filterInParentKeys: ['test'],
+            },
+            assertions: {},
           },
           {
-            type: 'Roof',
-            matchParentKeys: ['test'],
+            subject: {
+              type: 'Roof',
+              filterInParentKeys: ['test'],
+            },
+            assertions: {},
           },
-        ];
+        ] as AssertionDefinition[];
 
-        const visitors = buildVisitorObject('Bar', context, () => {}) as any;
+        const visitors = buildVisitorObject(
+          { subject: { type: 'Bar' }, where, assertions: {} } as Assertion,
+          () => {}
+        );
 
         expect(visitors).toMatchInlineSnapshot(`
           Object {
             "Foo": Object {
               "Bar": Object {
                 "Roof": Object {
-                  "Bar": [Function],
+                  "Bar": Object {
+                    "enter": [Function],
+                  },
                   "skip": [Function],
                 },
                 "skip": [Function],
@@ -59,24 +74,35 @@ describe('Oas3 assertions', () => {
       });
 
       it('should return the right visitor structure', () => {
-        const context = [
+        const where = [
           {
-            type: 'Operation',
-            matchParentKeys: ['put'],
+            subject: {
+              type: 'Operation',
+              filterInParentKeys: ['put'],
+            },
+            assertions: {},
           },
           {
-            type: 'Responses',
-            matchParentKeys: [201, 200],
+            subject: {
+              type: 'Responses',
+              filterInParentKeys: [201, 200],
+            },
+            assertions: {},
           },
         ];
 
-        const visitors = buildVisitorObject('MediaTypesMap', context, () => {}) as any;
+        const visitors = buildVisitorObject(
+          { subject: { type: 'MediaTypesMap' }, where, assertions: {} } as Assertion,
+          () => {}
+        );
 
         expect(visitors).toMatchInlineSnapshot(`
           Object {
             "Operation": Object {
               "Responses": Object {
-                "MediaTypesMap": [Function],
+                "MediaTypesMap": Object {
+                  "enter": [Function],
+                },
                 "skip": [Function],
               },
               "skip": [Function],