Repository: Redocly/redocly-cli
Pull request #1713 (2024-09-05 12:24:57)

feat: adding spot arazzo rules
## What/Why/How?

Adding Spot Arazzo rules and docs:

- [x]  Unique criteria rule is missing
- [x]  not suport actions type end
- [x]  No support for XPath.

## Reference

## Testing

## Screenshots (optional)

## Check yourself

- [ ] Code changed? - Tested with redoc/reference-docs/workflows (internal)
- [x] All new/updated code is covered with tests
- [ ] New package installed? - Tested in different environments (browser/node)

## Security

- [x] Security impact of change has been considered
- [x] Code follows company security practices and guidelines


changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 91fd8c63d2a2364e98be8744c8dce94be158c771

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/Redocly/redocly-cli/new/adding-new-spot-arazzo-rules?filename=.changeset/many-dingos-march.md&value=---%0A%22%40redocly%2Fopenapi-core%22%3A%20patch%0A---%0A%0Afeat%3A%20adding%20spot%20arazzo%20rules%0A)


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

## Coverage report 



| <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 | 78.13%     |    4883/6250    |
|                                                                           🟡                                                                           | Branches   | 66.53%     |    2014/3027    |
|                                                                           🟡                                                                           | Functions  | 72.57%     |     807/1112    |
|                                                                           🟡                                                                           | Lines      | 78.42%     |    4603/5870    |

<details><summary>Show new covered files 🐣</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 |
| :----------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------- | :--------- | :------- | :-------- | :---- |
|                                                                           🟢                                                                           | <div title="core/src/rules/spot/no-criteria-xpath.ts">`...` / no-criteria-xpath.ts</div> | 100%       | 100%     | 100%      | 100%  |

</details>


## Test suite run success
788 tests passing in 119 suites.

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


Patch:
diff --git a/.changeset/green-penguins-press.md b/.changeset/green-penguins-press.md
new file mode 100644
index 00000000..beffdd85
--- /dev/null
+++ b/.changeset/green-penguins-press.md
@@ -0,0 +1,6 @@
+---
+"@redocly/openapi-core": minor
+"@redocly/cli": minor
+---
+
+Added Spot and Arazzo rules: `no-criteria-xpath`, `no-actions-type-end`, `criteria-unique`.
diff --git a/docs/guides/lint-arazzo.md b/docs/guides/lint-arazzo.md
index e0cbef81..1efa1e22 100644
--- a/docs/guides/lint-arazzo.md
+++ b/docs/guides/lint-arazzo.md
@@ -84,6 +84,9 @@ The rules available for linting Arazzo are:
 - `step-onSuccess-unique`: the `onSuccess` actions of the `step` object must be unique.
 - `step-onFailure-unique`: the `onFailure` actions of the `step` object must be unique.
 - `requestBody-replacements-unique`: the `replacements` of the `requestBody` object must be unique.
+- `no-criteria-xpath`: the `xpath` type criteria is not supported by Spot.
+- `no-actions-type-end`: the `end` type action is not supported by Spot.
+- `criteria-unique`: the criteria list must not contain duplicated assertions.
 
 Add the rules to `redocly.yaml`, but for Arazzo specifications, the rules go in their own configuration section called `arazzoRules`.
 The following example shows configuration for the minimal ruleset with some additional rules configuration:
diff --git a/docs/rules/arazzo/criteria-unique.md b/docs/rules/arazzo/criteria-unique.md
new file mode 100644
index 00000000..9cff899c
--- /dev/null
+++ b/docs/rules/arazzo/criteria-unique.md
@@ -0,0 +1,60 @@
+---
+slug: /docs/cli/rules/arazzo/criteria-unique
+---
+
+# criteria-unique
+
+The criteria list must not contain duplicated assertions.
+
+| Arazzo | Compatibility |
+| ------ | ------------- |
+| 1.0.0  | ✅            |
+
+## API design principles
+
+The criteria list must not contain duplicated assertions.
+
+## Configuration
+
+| Option   | Type   | Description                                             |
+| -------- | ------ | ------------------------------------------------------- |
+| severity | string | Possible values: `off`, `warn`, `error`. Default `off`. |
+
+An example configuration:
+
+```yaml
+arazzoRules:
+  criteria-unique: error
+```
+
+## Examples
+
+Given the following configuration:
+
+```yaml
+arazzoRules:
+  criteria-unique: error
+```
+
+Example of a criteria list:
+
+```yaml Object example
+successCriteria:
+  - condition: $statusCode == 200
+onSuccess:
+  - name: 'onSuccessActionName'
+    type: 'goto'
+    stepId: 'buy-ticket'
+    criteria:
+      - condition: $response.body.open == true
+onFailure:
+  - name: 'onFailureActionName'
+    type: 'goto'
+    stepId: 'buy-ticket'
+    criteria:
+      - condition: $response.body.open == true
+```
+
+## Resources
+
+- [Rule source](https://github.com/Redocly/redocly-cli/blob/main/packages/core/src/rules/arazzo/criteria-unique.ts)
diff --git a/docs/rules/spot/no-criteria-xpath.md b/docs/rules/spot/no-criteria-xpath.md
new file mode 100644
index 00000000..596d7747
--- /dev/null
+++ b/docs/rules/spot/no-criteria-xpath.md
@@ -0,0 +1,54 @@
+---
+slug: /docs/cli/rules/spot/no-criteria-xpath
+---
+
+# no-criteria-xpath
+
+The `xpath` type criteria is not supported by Spot.
+
+| Arazzo | Compatibility |
+| ------ | ------------- |
+| 1.0.0  | ✅            |
+
+## API design principles
+
+This is `Spot` specific rule.
+The `xpath` type criteria is not supported by Spot.
+
+## Configuration
+
+| Option   | Type   | Description                                             |
+| -------- | ------ | ------------------------------------------------------- |
+| severity | string | Possible values: `off`, `warn`, `error`. Default `off`. |
+
+An example configuration:
+
+```yaml
+arazzoRules:
+  no-criteria-xpath: error
+```
+
+## Examples
+
+Given the following configuration:
+
+```yaml
+arazzoRules:
+  no-criteria-xpath: error
+```
+
+Example of criteria:
+
+```yaml Object example
+successCriteria:
+  - condition: $statusCode == 201
+  - context: $response.body
+    condition: $.name == 'Mermaid Treasure Identification and Analysis'
+    type:
+      type: jsonpath
+      version: draft-goessner-dispatch-jsonpath-00
+```
+
+## Resources
+
+- [Rule source](https://github.com/Redocly/redocly-cli/blob/main/packages/core/src/rules/spot/no-criteria-xpath.ts)
diff --git a/packages/core/src/config/all.ts b/packages/core/src/config/all.ts
index 61bec163..a6dd21f8 100644
--- a/packages/core/src/config/all.ts
+++ b/packages/core/src/config/all.ts
@@ -139,6 +139,9 @@ const all: PluginStyleguideConfig<'built-in'> = {
     'step-onSuccess-unique': 'error',
     'step-onFailure-unique': 'error',
     'requestBody-replacements-unique': 'error',
+    'no-criteria-xpath': 'error',
+    'no-actions-type-end': 'error',
+    'criteria-unique': 'error',
   },
 };
 
diff --git a/packages/core/src/config/minimal.ts b/packages/core/src/config/minimal.ts
index 2a15423c..f1ae082e 100644
--- a/packages/core/src/config/minimal.ts
+++ b/packages/core/src/config/minimal.ts
@@ -121,6 +121,9 @@ const minimal: PluginStyleguideConfig<'built-in'> = {
     'step-onSuccess-unique': 'off',
     'step-onFailure-unique': 'off',
     'requestBody-replacements-unique': 'off',
+    'no-criteria-xpath': 'off',
+    'no-actions-type-end': 'off',
+    'criteria-unique': 'off',
   },
 };
 
diff --git a/packages/core/src/config/recommended-strict.ts b/packages/core/src/config/recommended-strict.ts
index 33bd5b17..9cd52552 100644
--- a/packages/core/src/config/recommended-strict.ts
+++ b/packages/core/src/config/recommended-strict.ts
@@ -121,6 +121,9 @@ const recommendedStrict: PluginStyleguideConfig<'built-in'> = {
     'step-onSuccess-unique': 'error',
     'step-onFailure-unique': 'error',
     'requestBody-replacements-unique': 'error',
+    'no-criteria-xpath': 'error',
+    'no-actions-type-end': 'error',
+    'criteria-unique': 'error',
   },
 };
 
diff --git a/packages/core/src/config/recommended.ts b/packages/core/src/config/recommended.ts
index 484e35ac..3a155d80 100644
--- a/packages/core/src/config/recommended.ts
+++ b/packages/core/src/config/recommended.ts
@@ -121,6 +121,9 @@ const recommended: PluginStyleguideConfig<'built-in'> = {
     'step-onSuccess-unique': 'warn',
     'step-onFailure-unique': 'warn',
     'requestBody-replacements-unique': 'warn',
+    'no-criteria-xpath': 'warn',
+    'no-actions-type-end': 'warn',
+    'criteria-unique': 'warn',
   },
 };
 
diff --git a/packages/core/src/rules/arazzo/criteria-unique.ts b/packages/core/src/rules/arazzo/criteria-unique.ts
new file mode 100644
index 00000000..d1947c06
--- /dev/null
+++ b/packages/core/src/rules/arazzo/criteria-unique.ts
@@ -0,0 +1,63 @@
+import type { ArazzoRule } from '../../visitors';
+import type { UserContext } from '../../walk';
+
+export const CriteriaUnique: ArazzoRule = () => {
+  return {
+    FailureActionObject: {
+      enter(action, { report, location }: UserContext) {
+        const criterias = action.criteria;
+        const seen = new Set<string>();
+        for (const criteria of criterias) {
+          const key = JSON.stringify(criteria);
+          if (seen.has(key)) {
+            report({
+              message: 'The FailureAction criteria items must be unique.',
+              location: location.child(['criteria', criterias.indexOf(criteria)]),
+            });
+          } else {
+            seen.add(key);
+          }
+        }
+      },
+    },
+    SuccessActionObject: {
+      enter(action, { report, location }: UserContext) {
+        const criterias = action.criteria;
+        const seen = new Set<string>();
+        for (const criteria of criterias) {
+          const key = JSON.stringify(criteria);
+          if (seen.has(key)) {
+            report({
+              message: 'The SuccessAction criteria items must be unique.',
+              location: location.child(['criteria', criterias.indexOf(criteria)]),
+            });
+          } else {
+            seen.add(key);
+          }
+        }
+      },
+    },
+    Step: {
+      enter(step, { report, location }: UserContext) {
+        if (!step.successCriteria) {
+          return;
+        }
+
+        const successCriterias = step.successCriteria;
+        const seen = new Set<string>();
+
+        for (const criteria of successCriterias) {
+          const key = JSON.stringify(criteria);
+          if (seen.has(key)) {
+            report({
+              message: 'The Step SuccessCriteria items must be unique.',
+              location: location.child(['successCriteria', successCriterias.indexOf(criteria)]),
+            });
+          } else {
+            seen.add(key);
+          }
+        }
+      },
+    },
+  };
+};
diff --git a/packages/core/src/rules/arazzo/index.ts b/packages/core/src/rules/arazzo/index.ts
index ffdaadfe..c00261a2 100644
--- a/packages/core/src/rules/arazzo/index.ts
+++ b/packages/core/src/rules/arazzo/index.ts
@@ -11,6 +11,9 @@ import { ParametersUnique } from './parameters-unique';
 import { StepOnSuccessUnique } from './step-onSuccess-unique';
 import { StepOnFailureUnique } from './step-onFailure-unique';
 import { RequestBodyReplacementsUnique } from './requestBody-replacements-unique';
+import { NoCriteriaXpath } from '../spot/no-criteria-xpath';
+import { NoActionsTypeEnd } from '../spot/no-actions-type-end';
+import { CriteriaUnique } from './criteria-unique';
 
 import type { ArazzoRule } from '../../visitors';
 import type { ArazzoRuleSet } from '../../oas-types';
@@ -29,6 +32,9 @@ export const rules: ArazzoRuleSet<'built-in'> = {
   'step-onSuccess-unique': StepOnSuccessUnique as ArazzoRule,
   'step-onFailure-unique': StepOnFailureUnique as ArazzoRule,
   'requestBody-replacements-unique': RequestBodyReplacementsUnique as ArazzoRule,
+  'no-criteria-xpath': NoCriteriaXpath as ArazzoRule,
+  'no-actions-type-end': NoActionsTypeEnd as ArazzoRule,
+  'criteria-unique': CriteriaUnique as ArazzoRule,
 };
 
 export const preprocessors = {};
diff --git a/packages/core/src/rules/spot/no-actions-type-end.ts b/packages/core/src/rules/spot/no-actions-type-end.ts
new file mode 100644
index 00000000..f0e816d2
--- /dev/null
+++ b/packages/core/src/rules/spot/no-actions-type-end.ts
@@ -0,0 +1,27 @@
+import type { ArazzoRule } from '../../visitors';
+import type { UserContext } from '../../walk';
+
+export const NoActionsTypeEnd: ArazzoRule = () => {
+  return {
+    FailureActionObject: {
+      enter(action, { report, location }: UserContext) {
+        if (action.type === 'end') {
+          report({
+            message: 'The `end` type action is not supported by Spot.',
+            location: location.child(['type']),
+          });
+        }
+      },
+    },
+    SuccessActionObject: {
+      enter(action, { report, location }: UserContext) {
+        if (action.type === 'end') {
+          report({
+            message: 'The `end` type action is not supported by Spot.',
+            location: location.child(['type']),
+          });
+        }
+      },
+    },
+  };
+};
diff --git a/packages/core/src/rules/spot/no-criteria-xpath.ts b/packages/core/src/rules/spot/no-criteria-xpath.ts
new file mode 100644
index 00000000..dac5a950
--- /dev/null
+++ b/packages/core/src/rules/spot/no-criteria-xpath.ts
@@ -0,0 +1,20 @@
+import type { ArazzoRule } from '../../visitors';
+import type { UserContext } from '../../walk';
+
+export const NoCriteriaXpath: ArazzoRule = () => {
+  return {
+    CriterionObject: {
+      enter(criteria, { report, location }: UserContext) {
+        if (!criteria.type) {
+          return;
+        }
+        if (criteria?.type?.type === 'xpath' || criteria?.type === 'xpath') {
+          report({
+            message: 'The `xpath` type criteria is not supported by Spot.',
+            location: location.child(['type']),
+          });
+        }
+      },
+    },
+  };
+};
diff --git a/packages/core/src/types/arazzo.ts b/packages/core/src/types/arazzo.ts
index c330d3a1..a435af9e 100755
--- a/packages/core/src/types/arazzo.ts
+++ b/packages/core/src/types/arazzo.ts
@@ -205,7 +205,7 @@ const CriterionObject: NodeType = {
         return undefined;
       } else if (typeof value === 'string') {
         return { enum: ['regex', 'jsonpath', 'simple', 'xpath'] };
-      } else if (value.type === 'jsonpath') {
+      } else if (value?.type === 'jsonpath') {
         return 'JSONPathCriterion';
       } else {
         return 'XPathCriterion';
diff --git a/packages/core/src/types/redocly-yaml.ts b/packages/core/src/types/redocly-yaml.ts
index 7887ae2a..dc8675ef 100644
--- a/packages/core/src/types/redocly-yaml.ts
+++ b/packages/core/src/types/redocly-yaml.ts
@@ -122,6 +122,9 @@ const builtInArazzoRules = [
   'step-onSuccess-unique',
   'step-onFailure-unique',
   'requestBody-replacements-unique',
+  'no-criteria-xpath',
+  'no-actions-type-end',
+  'criteria-unique',
 ] as const;
 
 export type BuiltInArazzoRuleId = typeof builtInArazzoRules[number];
diff --git a/resources/arazzo.yaml b/resources/arazzo.yaml
index 348a20e9..db32f08d 100644
--- a/resources/arazzo.yaml
+++ b/resources/arazzo.yaml
@@ -68,7 +68,9 @@ workflows:
           - condition: $statusCode == 201
           - context: $response.body
             condition: $.name == 'Mermaid Treasure Identification and Analysis'
-            type: jsonpath
+            type:
+              type: jsonpath
+              version: draft-goessner-dispatch-jsonpath-00
         outputs:
           createdEventId: $response.body.eventId
           name: $response.body.name


Test patch:
diff --git a/docs/rules/spot/no-actions-type-end.test.md b/docs/rules/spot/no-actions-type-end.test.md
new file mode 100644
index 00000000..3c28bab8
--- /dev/null
+++ b/docs/rules/spot/no-actions-type-end.test.md
@@ -0,0 +1,55 @@
+---
+slug: /docs/cli/rules/spot/no-actions-type-end
+---
+
+# no-actions-type-end
+
+The `end` type action is not supported by Spot.
+
+| Arazzo | Compatibility |
+| ------ | ------------- |
+| 1.0.0  | ✅            |
+
+## API design principles
+
+This is a `Spot`-specific rule.
+The `end` type action is not supported by Spot.
+
+## Configuration
+
+| Option   | Type   | Description                                             |
+| -------- | ------ | ------------------------------------------------------- |
+| severity | string | Possible values: `off`, `warn`, `error`. Default `off`. |
+
+An example configuration:
+
+```yaml
+arazzoRules:
+  no-actions-type-end: error
+```
+
+## Examples
+
+Given the following configuration:
+
+```yaml
+arazzoRules:
+  no-actions-type-end: error
+```
+
+Example of an action:
+
+```yaml Object example
+onSuccess:
+  - name: 'onSuccessActionName'
+    type: 'goto'
+    stepId: 'buy-ticket'
+onFailure:
+  - name: 'onFailureActionName'
+    type: 'goto'
+    stepId: 'buy-ticket'
+```
+
+## Resources
+
+- [Rule source](https://github.com/Redocly/redocly-cli/blob/main/packages/core/src/rules/spot/no-actions-type-end.ts)
diff --git a/packages/core/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap b/packages/core/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap
index 7bc2a92e..d28851a8 100644
--- a/packages/core/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap
+++ b/packages/core/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap
@@ -5,6 +5,9 @@ exports[`resolveConfig should ignore minimal from the root and read local file 1
   "arazzoDecorators": {},
   "arazzoPreprocessors": {},
   "arazzoRules": {
+    "criteria-unique": "warn",
+    "no-actions-type-end": "warn",
+    "no-criteria-xpath": "warn",
     "parameters-not-in-body": "warn",
     "parameters-unique": "error",
     "requestBody-replacements-unique": "warn",
@@ -147,6 +150,9 @@ exports[`resolveStyleguideConfig should resolve extends with local file config w
   "arazzoDecorators": {},
   "arazzoPreprocessors": {},
   "arazzoRules": {
+    "criteria-unique": "warn",
+    "no-actions-type-end": "warn",
+    "no-criteria-xpath": "warn",
     "parameters-not-in-body": "warn",
     "parameters-unique": "error",
     "requestBody-replacements-unique": "warn",
diff --git a/packages/core/src/rules/arazzo/__tests__/criteria-unique.test.ts b/packages/core/src/rules/arazzo/__tests__/criteria-unique.test.ts
new file mode 100644
index 00000000..a1dd0753
--- /dev/null
+++ b/packages/core/src/rules/arazzo/__tests__/criteria-unique.test.ts
@@ -0,0 +1,161 @@
+import { outdent } from 'outdent';
+import { lintDocument } from '../../../lint';
+import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils';
+import { BaseResolver } from '../../../resolve';
+
+describe('Arazzo criteria-unique', () => {
+  const document = parseYamlToDocument(
+    outdent`
+      arazzo: '1.0.0'
+      info:
+        title: Cool API
+        version: 1.0.0
+        description: A cool API
+      sourceDescriptions:
+        - name: museum-api
+          type: openapi
+          url: openapi.yaml
+      workflows:
+        - workflowId: get-museum-hours
+          description: This workflow demonstrates how to get the museum opening hours and buy tickets.
+          parameters:
+            - in: header
+              name: Authorization
+              value: Basic Og==
+          steps:
+            - stepId: create-event
+              description: >-
+                Create a new special event.
+              operationPath: $sourceDescriptions.museum-api#/paths/~1special-events/post
+              requestBody:
+                payload:
+                  name: 'Mermaid Treasure Identification and Analysis'
+                  location: 'Under the seaaa 🦀 🎶 🌊.'
+                  eventDescription: 'Join us as we review and classify a rare collection of 20 thingamabobs, gadgets, gizmos, whoosits, and whatsits, kindly donated by Ariel.'
+                  dates:
+                    - '2023-09-05'
+                    - '2023-09-08'
+                  price: 0
+              successCriteria:
+                - condition: $statusCode == 200
+                - condition: $statusCode == 200
+                - context: $response.body
+                  condition: $.name == 'Mermaid Treasure Identification and Analysis'
+                  type: jsonpath
+                - context: $response.body
+                  condition: $.name == 'Mermaid Treasure Identification and Analysis'
+                  type: jsonpath
+              onSuccess:
+                - name: 'onSuccessActionName'
+                  type: 'goto'
+                  stepId: 'buy-ticket'
+                  criteria:
+                    - condition: $response.body.open == true
+                    - condition: $response.body.open == true
+              onFailure:
+                - name: 'onFailureActionName'
+                  type: 'goto'
+                  stepId: 'buy-ticket'
+                  criteria:
+                    - condition: $response.body.open == true
+                    - condition: $response.body.open == true
+              outputs:
+                createdEventId: $response.body.eventId
+                name: $response.body.name
+        - workflowId: get-museum-hours-2
+          description: This workflow demonstrates how to get the museum opening hours and buy tickets.
+          parameters:
+            - in: header
+              name: Authorization
+              value: Basic Og==
+          steps:
+            - stepId: get-museum-hours
+              description: >-
+                Get museum hours by resolving request details with getMuseumHours operationId from openapi.yaml description.
+              operationId: museum-api.getMuseumHours
+              successCriteria:
+                - condition: $statusCode == 200
+    `,
+    'arazzo.yaml'
+  );
+
+  it('should report when the duplicated criteria exists', async () => {
+    const results = await lintDocument({
+      externalRefResolver: new BaseResolver(),
+      document,
+      config: await makeConfig({
+        rules: {},
+        arazzoRules: { 'criteria-unique': 'error' },
+      }),
+    });
+
+    expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
+      [
+        {
+          "location": [
+            {
+              "pointer": "#/workflows/0/steps/0/successCriteria/1",
+              "reportOnKey": false,
+              "source": "arazzo.yaml",
+            },
+          ],
+          "message": "The Step SuccessCriteria items must be unique.",
+          "ruleId": "criteria-unique",
+          "severity": "error",
+          "suggest": [],
+        },
+        {
+          "location": [
+            {
+              "pointer": "#/workflows/0/steps/0/successCriteria/3",
+              "reportOnKey": false,
+              "source": "arazzo.yaml",
+            },
+          ],
+          "message": "The Step SuccessCriteria items must be unique.",
+          "ruleId": "criteria-unique",
+          "severity": "error",
+          "suggest": [],
+        },
+        {
+          "location": [
+            {
+              "pointer": "#/workflows/0/steps/0/onSuccess/0/criteria/1",
+              "reportOnKey": false,
+              "source": "arazzo.yaml",
+            },
+          ],
+          "message": "The SuccessAction criteria items must be unique.",
+          "ruleId": "criteria-unique",
+          "severity": "error",
+          "suggest": [],
+        },
+        {
+          "location": [
+            {
+              "pointer": "#/workflows/0/steps/0/onFailure/0/criteria/1",
+              "reportOnKey": false,
+              "source": "arazzo.yaml",
+            },
+          ],
+          "message": "The FailureAction criteria items must be unique.",
+          "ruleId": "criteria-unique",
+          "severity": "error",
+          "suggest": [],
+        },
+      ]
+    `);
+  });
+
+  it('should not report when the duplicated criteria exists', async () => {
+    const results = await lintDocument({
+      externalRefResolver: new BaseResolver(),
+      document,
+      config: await makeConfig({
+        rules: {},
+      }),
+    });
+
+    expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
+  });
+});
diff --git a/packages/core/src/rules/arazzo/__tests__/no-actions-type-end.test.ts b/packages/core/src/rules/arazzo/__tests__/no-actions-type-end.test.ts
new file mode 100644
index 00000000..d2625119
--- /dev/null
+++ b/packages/core/src/rules/arazzo/__tests__/no-actions-type-end.test.ts
@@ -0,0 +1,122 @@
+import { outdent } from 'outdent';
+import { lintDocument } from '../../../lint';
+import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils';
+import { BaseResolver } from '../../../resolve';
+
+describe('Arazzo no-actions-type-end', () => {
+  const document = parseYamlToDocument(
+    outdent`
+      arazzo: '1.0.0'
+      info:
+        title: Cool API
+        version: 1.0.0
+        description: A cool API
+      sourceDescriptions:
+        - name: museum-api
+          type: openapi
+          url: openapi.yaml
+      workflows:
+        - workflowId: get-museum-hours
+          description: This workflow demonstrates how to get the museum opening hours and buy tickets.
+          parameters:
+            - in: header
+              name: Authorization
+              value: Basic Og==
+          steps:
+            - stepId: create-event
+              description: >-
+                Create a new special event.
+              operationPath: $sourceDescriptions.museum-api#/paths/~1special-events/post
+              requestBody:
+                payload:
+                  name: 'Mermaid Treasure Identification and Analysis'
+                  location: 'Under the seaaa 🦀 🎶 🌊.'
+                  eventDescription: 'Join us as we review and classify a rare collection of 20 thingamabobs, gadgets, gizmos, whoosits, and whatsits, kindly donated by Ariel.'
+                  dates:
+                    - '2023-09-05'
+                    - '2023-09-08'
+                  price: 0
+              successCriteria:
+                - condition: $statusCode == 201
+              onSuccess:
+                - name: 'onSuccessActionName'
+                  type: 'end'
+                  stepId: 'buy-ticket'
+              onFailure:
+                - name: 'onFailureActionName'
+                  type: 'end'
+                  stepId: 'buy-ticket'
+              outputs:
+                createdEventId: $response.body.eventId
+                name: $response.body.name
+        - workflowId: get-museum-hours-2
+          description: This workflow demonstrates how to get the museum opening hours and buy tickets.
+          parameters:
+            - in: header
+              name: Authorization
+              value: Basic Og==
+          steps:
+            - stepId: get-museum-hours
+              description: >-
+                Get museum hours by resolving request details with getMuseumHours operationId from openapi.yaml description.
+              operationId: museum-api.getMuseumHours
+              successCriteria:
+                - condition: $statusCode == 200
+    `,
+    'arazzo.yaml'
+  );
+
+  it('should report when the type `end` action exists', async () => {
+    const results = await lintDocument({
+      externalRefResolver: new BaseResolver(),
+      document,
+      config: await makeConfig({
+        rules: {},
+        arazzoRules: { 'no-actions-type-end': 'error' },
+      }),
+    });
+
+    expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
+      [
+        {
+          "location": [
+            {
+              "pointer": "#/workflows/0/steps/0/onSuccess/0/type",
+              "reportOnKey": false,
+              "source": "arazzo.yaml",
+            },
+          ],
+          "message": "The \`end\` type action is not supported by Spot.",
+          "ruleId": "no-actions-type-end",
+          "severity": "error",
+          "suggest": [],
+        },
+        {
+          "location": [
+            {
+              "pointer": "#/workflows/0/steps/0/onFailure/0/type",
+              "reportOnKey": false,
+              "source": "arazzo.yaml",
+            },
+          ],
+          "message": "The \`end\` type action is not supported by Spot.",
+          "ruleId": "no-actions-type-end",
+          "severity": "error",
+          "suggest": [],
+        },
+      ]
+    `);
+  });
+
+  it('should not report when the type `end` action exists', async () => {
+    const results = await lintDocument({
+      externalRefResolver: new BaseResolver(),
+      document,
+      config: await makeConfig({
+        rules: {},
+      }),
+    });
+
+    expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
+  });
+});
diff --git a/packages/core/src/rules/arazzo/__tests__/no-criteria-xpath.test.ts b/packages/core/src/rules/arazzo/__tests__/no-criteria-xpath.test.ts
new file mode 100644
index 00000000..6d0e845c
--- /dev/null
+++ b/packages/core/src/rules/arazzo/__tests__/no-criteria-xpath.test.ts
@@ -0,0 +1,127 @@
+import { outdent } from 'outdent';
+import { lintDocument } from '../../../lint';
+import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils';
+import { BaseResolver } from '../../../resolve';
+
+describe('Arazzo no-criteria-xpath', () => {
+  const document = parseYamlToDocument(
+    outdent`
+      arazzo: '1.0.0'
+      info:
+        title: Cool API
+        version: 1.0.0
+        description: A cool API
+      sourceDescriptions:
+        - name: museum-api
+          type: openapi
+          url: openapi.yaml
+      workflows:
+        - workflowId: get-museum-hours
+          description: This workflow demonstrates how to get the museum opening hours and buy tickets.
+          parameters:
+            - in: header
+              name: Authorization
+              value: Basic Og==
+          steps:
+            - stepId: create-event
+              description: >-
+                Create a new special event.
+              operationPath: $sourceDescriptions.museum-api#/paths/~1special-events/post
+              requestBody:
+                payload:
+                  name: 'Mermaid Treasure Identification and Analysis'
+                  location: 'Under the seaaa 🦀 🎶 🌊.'
+                  eventDescription: 'Join us as we review and classify a rare collection of 20 thingamabobs, gadgets, gizmos, whoosits, and whatsits, kindly donated by Ariel.'
+                  dates:
+                    - '2023-09-05'
+                    - '2023-09-08'
+                  price: 0
+              successCriteria:
+                - condition: $statusCode == 201
+                - context: $response.body
+                  condition: $.name == 'Mermaid Treasure Identification and Analysis'
+                  type:
+                    type: jsonpath
+                    version: draft-goessner-dispatch-jsonpath-00
+                - context: $response.body
+                  condition: $.name == 'Orca Identification and Analysis'
+                  type: xpath
+                - context: $response.body
+                  condition: $.name == 'Mermaid Treasure Identification and Analysis'
+                  type:
+                    type: xpath
+                    version: xpath-30
+              outputs:
+                createdEventId: $response.body.eventId
+                name: $response.body.name
+        - workflowId: get-museum-hours-2
+          description: This workflow demonstrates how to get the museum opening hours and buy tickets.
+          parameters:
+            - in: header
+              name: Authorization
+              value: Basic Og==
+          steps:
+            - stepId: get-museum-hours
+              description: >-
+                Get museum hours by resolving request details with getMuseumHours operationId from openapi.yaml description.
+              operationId: museum-api.getMuseumHours
+              successCriteria:
+                - condition: $statusCode == 200
+    `,
+    'arazzo.yaml'
+  );
+
+  it('should report when the `xpath` criteria exists', async () => {
+    const results = await lintDocument({
+      externalRefResolver: new BaseResolver(),
+      document,
+      config: await makeConfig({
+        rules: {},
+        arazzoRules: { 'no-criteria-xpath': 'error' },
+      }),
+    });
+
+    expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
+      [
+        {
+          "location": [
+            {
+              "pointer": "#/workflows/0/steps/0/successCriteria/2/type",
+              "reportOnKey": false,
+              "source": "arazzo.yaml",
+            },
+          ],
+          "message": "The \`xpath\` type criteria is not supported by Spot.",
+          "ruleId": "no-criteria-xpath",
+          "severity": "error",
+          "suggest": [],
+        },
+        {
+          "location": [
+            {
+              "pointer": "#/workflows/0/steps/0/successCriteria/3/type",
+              "reportOnKey": false,
+              "source": "arazzo.yaml",
+            },
+          ],
+          "message": "The \`xpath\` type criteria is not supported by Spot.",
+          "ruleId": "no-criteria-xpath",
+          "severity": "error",
+          "suggest": [],
+        },
+      ]
+    `);
+  });
+
+  it('should not report when the `xpath` criteria exists', async () => {
+    const results = await lintDocument({
+      externalRefResolver: new BaseResolver(),
+      document,
+      config: await makeConfig({
+        rules: {},
+      }),
+    });
+
+    expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
+  });
+});