From 55c2dcf298382071c0928eff84dc4cfafb3bf4c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20Felleg?= Date: Tue, 23 Jun 2026 15:04:13 +0200 Subject: [PATCH 01/14] feat: add documentation link to result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: András Felleg --- .../validator/src/cli-validator/utils/print-results.js | 1 + .../src/markdown-report/tables/rule-violation-details.js | 9 +++++---- packages/validator/src/schemas/results-object.yaml | 3 +++ packages/validator/src/spectral/index.js | 6 ++++++ 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/validator/src/cli-validator/utils/print-results.js b/packages/validator/src/cli-validator/utils/print-results.js index daf5ec3e9..1703075c8 100644 --- a/packages/validator/src/cli-validator/utils/print-results.js +++ b/packages/validator/src/cli-validator/utils/print-results.js @@ -38,6 +38,7 @@ module.exports = function print(context, results) { console.log(chalk[color](` Message : ${result.message}`)); console.log(chalk[color](` Rule : ${result.rule}`)); console.log(chalk[color](` Path : ${result.path.join('.')}`)); + console.log(chalk[color](` DocLink : ${result.docLink}`)); console.log(chalk[color](` Line : ${result.line}`)); console.log(''); }); diff --git a/packages/validator/src/markdown-report/tables/rule-violation-details.js b/packages/validator/src/markdown-report/tables/rule-violation-details.js index f73051dfa..1be0ebee4 100644 --- a/packages/validator/src/markdown-report/tables/rule-violation-details.js +++ b/packages/validator/src/markdown-report/tables/rule-violation-details.js @@ -10,16 +10,17 @@ function getTable({ error, warning }) { 'Rule', 'Message', 'Path', + 'docLink', 'Line', 'Severity' ); - error.results.forEach(({ message, path, rule, line }) => { - table.addRow(rule, message, path.join('.'), line, 'error'); + error.results.forEach(({ message, path, rule, line, docLink }) => { + table.addRow(rule, message, path.join('.'), docLink, line, 'error'); }); - warning.results.forEach(({ message, path, rule, line }) => { - table.addRow(rule, message, path.join('.'), line, 'warning'); + warning.results.forEach(({ message, path, rule, line, docLink }) => { + table.addRow(rule, message, path.join('.'), docLink, line, 'warning'); }); return table.render(); diff --git a/packages/validator/src/schemas/results-object.yaml b/packages/validator/src/schemas/results-object.yaml index d398b0094..0f2065fe2 100644 --- a/packages/validator/src/schemas/results-object.yaml +++ b/packages/validator/src/schemas/results-object.yaml @@ -139,6 +139,9 @@ $defs: rule: type: string description: The rule identifier in the Spectral ruleset + docLink: + type: string + description: The documentation link to the problem discovered in the API line: type: integer description: The line number in the original file that the problem diff --git a/packages/validator/src/spectral/index.js b/packages/validator/src/spectral/index.js index 8b91af070..6ddf4e59e 100644 --- a/packages/validator/src/spectral/index.js +++ b/packages/validator/src/spectral/index.js @@ -89,6 +89,7 @@ function convertResults(spectralResults, { config, logger }) { message: r.message, path: r.path, rule: r.code, + docLink: getDocumentationURL(r.code), line: r.range.start.line + 1, }); @@ -233,3 +234,8 @@ function convertSpectralSeverity(s) { const mapping = { 0: 'error', 1: 'warning', 2: 'info', 3: 'hint' }; return mapping[s]; } + +function getDocumentationURL(ruleCode) { + const baseUrl = 'https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md'; + return `${baseUrl}#${ruleCode}`; +} From 4570692ec9ade9955ed4589ad5286a6171990bf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20Felleg?= Date: Wed, 24 Jun 2026 13:36:40 +0200 Subject: [PATCH 02/14] test: extend tests with url in result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: András Felleg --- .../tests/expected-output.test.js | 46 ++++++++++--------- .../tables/rule-violation-details.test.js | 10 ++-- .../test/test-utils/mock-json-output.json | 3 ++ 3 files changed, 33 insertions(+), 26 deletions(-) diff --git a/packages/validator/test/cli-validator/tests/expected-output.test.js b/packages/validator/test/cli-validator/tests/expected-output.test.js index 9d27a4e65..14c201488 100644 --- a/packages/validator/test/cli-validator/tests/expected-output.test.js +++ b/packages/validator/test/cli-validator/tests/expected-output.test.js @@ -87,39 +87,39 @@ describe('Expected output tests', function () { // errors const errorStart = 3; - expect(capturedText[errorStart + 5].match(/\S+/g)[2]).toEqual('52'); - expect(capturedText[errorStart + 10].match(/\S+/g)[2]).toEqual('96'); - expect(capturedText[errorStart + 15].match(/\S+/g)[2]).toEqual('103'); + expect(capturedText[errorStart + 6].match(/\S+/g)[2]).toEqual('52'); + expect(capturedText[errorStart + 12].match(/\S+/g)[2]).toEqual('96'); + expect(capturedText[errorStart + 18].match(/\S+/g)[2]).toEqual('103'); // Specifically verify that the no-$ref-siblings error occurred. // We do this because this rule is inherited from Spectral's oas ruleset, // but we modify the rule definition in ibmoas.js so that it is run // against both OpenAPI 3.0 and 3.1 documents. - expect(capturedText[errorStart + 17].split(':')[1].trim()).toEqual( + expect(capturedText[errorStart + 20].split(':')[1].trim()).toEqual( '$ref must not be placed next to any other properties' ); - expect(capturedText[errorStart + 18].split(':')[1].trim()).toEqual( + expect(capturedText[errorStart + 21].split(':')[1].trim()).toEqual( 'no-$ref-siblings' ); - expect(capturedText[errorStart + 19].split(':')[1].trim()).toEqual( + expect(capturedText[errorStart + 22].split(':')[1].trim()).toEqual( 'components.schemas.Pet.properties.category.description' ); - expect(capturedText[errorStart + 20].match(/\S+/g)[2]).toEqual('184'); + expect(capturedText[errorStart + 24].match(/\S+/g)[2]).toEqual('184'); - // warnings - const warningStart = 25; + // warnings - each warning block is now 6 lines (5 content + 1 blank) + const warningStart = 30; expect(capturedText[warningStart + 5].match(/\S+/g)[2]).toEqual('22'); - expect(capturedText[warningStart + 10].match(/\S+/g)[2]).toEqual('24'); - expect(capturedText[warningStart + 15].match(/\S+/g)[2]).toEqual('40'); - expect(capturedText[warningStart + 20].match(/\S+/g)[2]).toEqual('41'); - expect(capturedText[warningStart + 25].match(/\S+/g)[2]).toEqual('52'); - expect(capturedText[warningStart + 30].match(/\S+/g)[2]).toEqual('56'); - expect(capturedText[warningStart + 35].match(/\S+/g)[2]).toEqual('57'); - expect(capturedText[warningStart + 40].match(/\S+/g)[2]).toEqual('59'); - expect(capturedText[warningStart + 45].match(/\S+/g)[2]).toEqual('61'); - expect(capturedText[warningStart + 50].match(/\S+/g)[2]).toEqual('96'); - // Skip a few, then verify the last one. - expect(capturedText[warningStart + 145].match(/\S+/g)[2]).toEqual( + expect(capturedText[warningStart + 11].match(/\S+/g)[2]).toEqual('24'); + expect(capturedText[warningStart + 17].match(/\S+/g)[2]).toEqual('40'); + expect(capturedText[warningStart + 23].match(/\S+/g)[2]).toEqual('41'); + expect(capturedText[warningStart + 29].match(/\S+/g)[2]).toEqual('52'); + expect(capturedText[warningStart + 35].match(/\S+/g)[2]).toEqual('56'); + expect(capturedText[warningStart + 41].match(/\S+/g)[2]).toEqual('57'); + expect(capturedText[warningStart + 47].match(/\S+/g)[2]).toEqual('59'); + expect(capturedText[warningStart + 53].match(/\S+/g)[2]).toEqual('61'); + expect(capturedText[warningStart + 59].match(/\S+/g)[2]).toEqual('96'); + // Skip a few, then verify the last one (29 warnings × 6 lines = 174 lines) + expect(capturedText[warningStart + 173].match(/\S+/g)[2]).toEqual( '210' ); } @@ -214,7 +214,10 @@ describe('Expected output tests', function () { [] ); - allMessages.forEach(msg => expect(msg).toHaveProperty('rule')); + allMessages.forEach(msg => { + expect(msg).toHaveProperty('rule'); + expect(msg).toHaveProperty('docLink'); + }); } ); @@ -244,6 +247,7 @@ describe('Expected output tests', function () { const warningToCheck = jsonOutput.warning.results[0]; expect(warningToCheck.rule).toEqual('ibm-prefer-token-pagination'); + expect(warningToCheck.docLink).toContain('ibm-prefer-token-pagination'); expect(warningToCheck.path.join('.')).toBe('paths./letters.get'); expect(warningToCheck.line).toEqual(20); } diff --git a/packages/validator/test/markdown-report/tables/rule-violation-details.test.js b/packages/validator/test/markdown-report/tables/rule-violation-details.test.js index f140dc1ca..108211a27 100644 --- a/packages/validator/test/markdown-report/tables/rule-violation-details.test.js +++ b/packages/validator/test/markdown-report/tables/rule-violation-details.test.js @@ -11,16 +11,16 @@ describe('ruleViolationDetails table tests', function () { const tableRows = ruleViolationDetails(validatorResults).split('\n'); expect(tableRows).toHaveLength(5); - expect(tableRows[0]).toBe('| Rule | Message | Path | Line | Severity |'); - expect(tableRows[1]).toBe('| --- | --- | --- | --- | --- |'); + expect(tableRows[0]).toBe('| Rule | Message | Path | docLink | Line | Severity |'); + expect(tableRows[1]).toBe('| --- | --- | --- | --- | --- | --- |'); expect(tableRows[2]).toBe( - '| ibm-no-consecutive-path-parameter-segments | Path contains two or more consecutive path parameter references: /pets/{pet_id}/{id} | paths./pets/{pet_id}/{id} | 84 | error |' + '| ibm-no-consecutive-path-parameter-segments | Path contains two or more consecutive path parameter references: /pets/{pet_id}/{id} | paths./pets/{pet_id}/{id} | https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md#ibm-no-consecutive-path-parameter-segments | 84 | error |' ); expect(tableRows[3]).toBe( - "| ibm-integer-attributes | Integer schemas should define property 'minimum' | components.schemas.Pet.properties.id | 133 | error |" + "| ibm-integer-attributes | Integer schemas should define property 'minimum' | components.schemas.Pet.properties.id | https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md#ibm-integer-attributes | 133 | error |" ); expect(tableRows[4]).toBe( - "| ibm-anchored-patterns | A regular expression used in a 'pattern' attribute should be anchored with ^ and $ | components.schemas.Error.properties.message.pattern | 233 | warning |" + "| ibm-anchored-patterns | A regular expression used in a 'pattern' attribute should be anchored with ^ and $ | components.schemas.Error.properties.message.pattern | https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md#ibm-anchored-patterns | 233 | warning |" ); }); }); diff --git a/packages/validator/test/test-utils/mock-json-output.json b/packages/validator/test/test-utils/mock-json-output.json index 6e264c798..7c7ef1203 100644 --- a/packages/validator/test/test-utils/mock-json-output.json +++ b/packages/validator/test/test-utils/mock-json-output.json @@ -8,6 +8,7 @@ "/pets/{pet_id}/{id}" ], "rule": "ibm-no-consecutive-path-parameter-segments", + "docLink": "https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md#ibm-no-consecutive-path-parameter-segments", "line": 84 }, { @@ -20,6 +21,7 @@ "id" ], "rule": "ibm-integer-attributes", + "docLink": "https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md#ibm-integer-attributes", "line": 133 } ], @@ -52,6 +54,7 @@ "pattern" ], "rule": "ibm-anchored-patterns", + "docLink": "https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md#ibm-anchored-patterns", "line": 233 } ], From e81b3439d9776a84e559d954d902b5a938c938d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20Felleg?= Date: Thu, 25 Jun 2026 14:58:21 +0200 Subject: [PATCH 03/14] fix: include spectral links correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: András Felleg --- packages/validator/src/spectral/index.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/validator/src/spectral/index.js b/packages/validator/src/spectral/index.js index 6ddf4e59e..11a617fea 100644 --- a/packages/validator/src/spectral/index.js +++ b/packages/validator/src/spectral/index.js @@ -236,6 +236,11 @@ function convertSpectralSeverity(s) { } function getDocumentationURL(ruleCode) { - const baseUrl = 'https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md'; - return `${baseUrl}#${ruleCode}`; + if(ruleCode.includes('ibm')) { + const baseUrl = 'https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md'; + return `${baseUrl}#${ruleCode}`; + } else { + const baseUrl = 'https://meta.stoplight.io/docs/spectral/4dec24461f3af-open-api-rules' + return `${baseUrl}#${ruleCode}` + } } From 9973ce3d8fdabb0be7b876aefb1d3ea7ecc30330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20Felleg?= Date: Thu, 25 Jun 2026 15:31:58 +0200 Subject: [PATCH 04/14] fix: make docLink required in validation result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: András Felleg --- packages/validator/src/schemas/results-object.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/validator/src/schemas/results-object.yaml b/packages/validator/src/schemas/results-object.yaml index 0f2065fe2..bcd6f842c 100644 --- a/packages/validator/src/schemas/results-object.yaml +++ b/packages/validator/src/schemas/results-object.yaml @@ -125,6 +125,7 @@ $defs: - message - path - rule + - docLink - line properties: message: @@ -172,3 +173,4 @@ $defs: type: number description: A number describing the demerit quality a rule has on an API minimum: 0.01 + From e0f4fd214e6af87da9fa010f2b3c1385cc64c547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20Felleg?= Date: Thu, 25 Jun 2026 15:36:06 +0200 Subject: [PATCH 05/14] feat: make scoring information collapsible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: András Felleg --- packages/validator/src/markdown-report/report.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/validator/src/markdown-report/report.js b/packages/validator/src/markdown-report/report.js index 4a02cf7d8..1b5f88330 100644 --- a/packages/validator/src/markdown-report/report.js +++ b/packages/validator/src/markdown-report/report.js @@ -34,8 +34,14 @@ inherently weighted by the scoring algorithm, so that security violations are 5 as usability violations, evolution 3 times, and robustness 2 times. ## Scoring information +
+ +Information about how the scoring gets calculated + ${scoringData(results)} +
+ ## Error summary ${errorSummary(results)} From 7758b9e6543247cba3b25e485a675e480d670e79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20Felleg?= Date: Fri, 26 Jun 2026 11:42:07 +0200 Subject: [PATCH 06/14] feat: group result by rule violation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: András Felleg --- .../tables/rule-violation-details.js | 105 +++++++++++++++--- 1 file changed, 90 insertions(+), 15 deletions(-) diff --git a/packages/validator/src/markdown-report/tables/rule-violation-details.js b/packages/validator/src/markdown-report/tables/rule-violation-details.js index 1be0ebee4..9a8950ec6 100644 --- a/packages/validator/src/markdown-report/tables/rule-violation-details.js +++ b/packages/validator/src/markdown-report/tables/rule-violation-details.js @@ -5,25 +5,100 @@ const MarkdownTable = require('../markdown-table'); -function getTable({ error, warning }) { - const table = new MarkdownTable( - 'Rule', - 'Message', - 'Path', - 'docLink', - 'Line', - 'Severity' - ); - - error.results.forEach(({ message, path, rule, line, docLink }) => { - table.addRow(rule, message, path.join('.'), docLink, line, 'error'); +function splitMessage(message) { + const colonIdx = message.indexOf(':'); + if (colonIdx === -1) { + return { base: message, detail: null }; + } + return { + base: message.slice(0, colonIdx).trim(), + detail: message.slice(colonIdx + 1).trim() || null, + }; +} + +function groupResultsByRule(results) { + const groupedByRule = {}; + + results.forEach(result => { + const { base, detail } = splitMessage(result.message); + + if (!groupedByRule[result.rule]) { + groupedByRule[result.rule] = { + base, + docLink: result.docLink, + violations: [], + }; + } + + const pathStr = result.path.join('.'); + + if (!groupedByRule[result.rule].violations.some( + v => v.line === result.line && v.path === pathStr + )) { + groupedByRule[result.rule].violations.push({ + line: result.line, + path: pathStr, + detail, + }); + } }); - warning.results.forEach(({ message, path, rule, line, docLink }) => { - table.addRow(rule, message, path.join('.'), docLink, line, 'warning'); + return groupedByRule; +} + +function renderRuleGroups(groupedByRule) { + const sections = []; + + Object.entries(groupedByRule).forEach(([rule, data]) => { + // Create heading with link + sections.push(`### [${rule}](${data.docLink})`); + sections.push(''); + + // Add the base message as italic description + sections.push(`_${data.base}_`); + sections.push(''); + + // Use a 3-col table when any violation carries extra detail + const hasDetail = data.violations.some(v => v.detail); + const table = hasDetail + ? new MarkdownTable('Line', 'Path', 'Detail') + : new MarkdownTable('Line', 'Path'); + + data.violations.forEach(({ line, path, detail }) => { + if (hasDetail) { + table.addRow(line, path, detail); + } else { + table.addRow(line, path); + } + }); + + sections.push(table.render()); + sections.push(''); }); - return table.render(); + return sections.join('\n'); +} + +function getTable({ error, warning }) { + const sections = []; + + // Process errors + if (error.results.length > 0) { + sections.push('## Errors'); + sections.push(''); + const errorGroups = groupResultsByRule(error.results); + sections.push(renderRuleGroups(errorGroups)); + } + + // Process warnings + if (warning.results.length > 0) { + sections.push('## Warnings'); + sections.push(''); + const warningGroups = groupResultsByRule(warning.results); + sections.push(renderRuleGroups(warningGroups)); + } + + return sections.join('\n').trim(); } module.exports = getTable; From 69e866c6fc8a6ddeb0c1ce1b1cce7e078246f697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20Felleg?= Date: Fri, 26 Jun 2026 11:42:47 +0200 Subject: [PATCH 07/14] test: update markdown test with new output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: András Felleg --- .../tables/rule-violation-details.test.js | 142 ++++++++++++++++-- 1 file changed, 131 insertions(+), 11 deletions(-) diff --git a/packages/validator/test/markdown-report/tables/rule-violation-details.test.js b/packages/validator/test/markdown-report/tables/rule-violation-details.test.js index 108211a27..d38088c05 100644 --- a/packages/validator/test/markdown-report/tables/rule-violation-details.test.js +++ b/packages/validator/test/markdown-report/tables/rule-violation-details.test.js @@ -7,20 +7,140 @@ const { ruleViolationDetails } = require('../../../src/markdown-report/tables'); const validatorResults = require('../../test-utils/mock-json-output.json'); describe('ruleViolationDetails table tests', function () { - it('should produce a table with all rule violations from the results', function () { - const tableRows = ruleViolationDetails(validatorResults).split('\n'); + it('should produce grouped sections for errors and warnings', function () { + const output = ruleViolationDetails(validatorResults); + const lines = output.split('\n'); - expect(tableRows).toHaveLength(5); - expect(tableRows[0]).toBe('| Rule | Message | Path | docLink | Line | Severity |'); - expect(tableRows[1]).toBe('| --- | --- | --- | --- | --- | --- |'); - expect(tableRows[2]).toBe( - '| ibm-no-consecutive-path-parameter-segments | Path contains two or more consecutive path parameter references: /pets/{pet_id}/{id} | paths./pets/{pet_id}/{id} | https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md#ibm-no-consecutive-path-parameter-segments | 84 | error |' + // Errors heading + expect(lines[0]).toBe('## Errors'); + expect(lines[1]).toBe(''); + + // 3-col table + expect(lines[2]).toBe( + '### [ibm-no-consecutive-path-parameter-segments](https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md#ibm-no-consecutive-path-parameter-segments)' + ); + expect(lines[3]).toBe(''); + expect(lines[4]).toBe( + '_Path contains two or more consecutive path parameter references_' + ); + expect(lines[5]).toBe(''); + expect(lines[6]).toBe('| Line | Path | Detail |'); + expect(lines[7]).toBe('| --- | --- | --- |'); + expect(lines[8]).toBe( + '| 84 | paths./pets/{pet_id}/{id} | /pets/{pet_id}/{id} |' + ); + expect(lines[9]).toBe(''); + + // 2-col table + expect(lines[10]).toBe( + '### [ibm-integer-attributes](https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md#ibm-integer-attributes)' ); - expect(tableRows[3]).toBe( - "| ibm-integer-attributes | Integer schemas should define property 'minimum' | components.schemas.Pet.properties.id | https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md#ibm-integer-attributes | 133 | error |" + expect(lines[11]).toBe(''); + expect(lines[12]).toBe( + "_Integer schemas should define property 'minimum'_" ); - expect(tableRows[4]).toBe( - "| ibm-anchored-patterns | A regular expression used in a 'pattern' attribute should be anchored with ^ and $ | components.schemas.Error.properties.message.pattern | https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md#ibm-anchored-patterns | 233 | warning |" + expect(lines[13]).toBe(''); + expect(lines[14]).toBe('| Line | Path |'); + expect(lines[15]).toBe('| --- | --- |'); + expect(lines[16]).toBe('| 133 | components.schemas.Pet.properties.id |'); + expect(lines[17]).toBe(''); + + // Warnings heading + expect(lines[18]).toBe('## Warnings'); + expect(lines[19]).toBe(''); + + // 2-col table + expect(lines[20]).toBe( + '### [ibm-anchored-patterns](https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md#ibm-anchored-patterns)' + ); + expect(lines[21]).toBe(''); + expect(lines[22]).toBe( + "_A regular expression used in a 'pattern' attribute should be anchored with ^ and $_" ); + expect(lines[23]).toBe(''); + expect(lines[24]).toBe('| Line | Path |'); + expect(lines[25]).toBe('| --- | --- |'); + expect(lines[26]).toBe( + '| 233 | components.schemas.Error.properties.message.pattern |' + ); + + expect(lines).toHaveLength(27); + }); + + it('should return an empty string when there are no results', function () { + const empty = { + error: { results: [] }, + warning: { results: [] }, + }; + expect(ruleViolationDetails(empty)).toBe(''); + }); + + it('should only render the Errors section when there are no warnings', function () { + const errorsOnly = { + error: { results: validatorResults.error.results }, + warning: { results: [] }, + }; + const output = ruleViolationDetails(errorsOnly); + expect(output).toContain('## Errors'); + expect(output).not.toContain('## Warnings'); + }); + + it('should only render the Warnings section when there are no errors', function () { + const warningsOnly = { + error: { results: [] }, + warning: { results: validatorResults.warning.results }, + }; + const output = ruleViolationDetails(warningsOnly); + expect(output).not.toContain('## Errors'); + expect(output).toContain('## Warnings'); + }); + + it('should deduplicate violations with the same line and path', function () { + const duplicate = { + error: { + results: [ + { + message: 'Some error message', + path: ['paths', '/foo'], + rule: 'some-rule', + docLink: 'https://example.com', + line: 10, + }, + { + message: 'Some error message', + path: ['paths', '/foo'], + rule: 'some-rule', + docLink: 'https://example.com', + line: 10, + }, + ], + }, + warning: { results: [] }, + }; + const output = ruleViolationDetails(duplicate); + const dataRows = output + .split('\n') + .filter(l => l.startsWith('| 10 |')); + expect(dataRows).toHaveLength(1); + }); + + it('should use a 3-column table when at least one violation has a detail', function () { + const withDetail = { + error: { + results: [ + { + message: 'Base message: detail value', + path: ['paths', '/foo'], + rule: 'rule-with-detail', + docLink: 'https://example.com', + line: 5, + }, + ], + }, + warning: { results: [] }, + }; + const output = ruleViolationDetails(withDetail); + expect(output).toContain('| Line | Path | Detail |'); + expect(output).toContain('| 5 | paths./foo | detail value |'); }); }); From a6fb05556844785697647276a3127e62d919930c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20Felleg?= Date: Fri, 26 Jun 2026 13:55:54 +0200 Subject: [PATCH 08/14] test: fix failing test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: András Felleg --- .../markdown-report/tables/rule-violation-details.js | 4 ++-- .../validator/test/markdown-report/report.test.js | 2 +- .../tables/rule-violation-details.test.js | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/validator/src/markdown-report/tables/rule-violation-details.js b/packages/validator/src/markdown-report/tables/rule-violation-details.js index 9a8950ec6..062f7326f 100644 --- a/packages/validator/src/markdown-report/tables/rule-violation-details.js +++ b/packages/validator/src/markdown-report/tables/rule-violation-details.js @@ -84,7 +84,7 @@ function getTable({ error, warning }) { // Process errors if (error.results.length > 0) { - sections.push('## Errors'); + sections.push('### Errors'); sections.push(''); const errorGroups = groupResultsByRule(error.results); sections.push(renderRuleGroups(errorGroups)); @@ -92,7 +92,7 @@ function getTable({ error, warning }) { // Process warnings if (warning.results.length > 0) { - sections.push('## Warnings'); + sections.push('### Warnings'); sections.push(''); const warningGroups = groupResultsByRule(warning.results); sections.push(renderRuleGroups(warningGroups)); diff --git a/packages/validator/test/markdown-report/report.test.js b/packages/validator/test/markdown-report/report.test.js index 5877f713d..debfe4a8e 100644 --- a/packages/validator/test/markdown-report/report.test.js +++ b/packages/validator/test/markdown-report/report.test.js @@ -18,7 +18,7 @@ describe('getReport tests', function () { // Check all subtitle-level headers. const headers = report .split('\n') - .filter(l => l.startsWith('##')) + .filter(l => /^## /.test(l)) .map(l => l.slice(3)); expect(headers).toEqual([ 'Quick view', diff --git a/packages/validator/test/markdown-report/tables/rule-violation-details.test.js b/packages/validator/test/markdown-report/tables/rule-violation-details.test.js index d38088c05..85152137b 100644 --- a/packages/validator/test/markdown-report/tables/rule-violation-details.test.js +++ b/packages/validator/test/markdown-report/tables/rule-violation-details.test.js @@ -12,7 +12,7 @@ describe('ruleViolationDetails table tests', function () { const lines = output.split('\n'); // Errors heading - expect(lines[0]).toBe('## Errors'); + expect(lines[0]).toBe('### Errors'); expect(lines[1]).toBe(''); // 3-col table @@ -46,7 +46,7 @@ describe('ruleViolationDetails table tests', function () { expect(lines[17]).toBe(''); // Warnings heading - expect(lines[18]).toBe('## Warnings'); + expect(lines[18]).toBe('### Warnings'); expect(lines[19]).toBe(''); // 2-col table @@ -81,8 +81,8 @@ describe('ruleViolationDetails table tests', function () { warning: { results: [] }, }; const output = ruleViolationDetails(errorsOnly); - expect(output).toContain('## Errors'); - expect(output).not.toContain('## Warnings'); + expect(output).toContain('### Errors'); + expect(output).not.toContain('### Warnings'); }); it('should only render the Warnings section when there are no errors', function () { @@ -91,8 +91,8 @@ describe('ruleViolationDetails table tests', function () { warning: { results: validatorResults.warning.results }, }; const output = ruleViolationDetails(warningsOnly); - expect(output).not.toContain('## Errors'); - expect(output).toContain('## Warnings'); + expect(output).not.toContain('### Errors'); + expect(output).toContain('### Warnings'); }); it('should deduplicate violations with the same line and path', function () { From 82e14f9fd9704c5165f1cf9e890bf5efaf553bf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20Felleg?= Date: Fri, 26 Jun 2026 14:02:42 +0200 Subject: [PATCH 09/14] chore: fix linter issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: András Felleg --- .../markdown-report/tables/rule-violation-details.js | 6 ++++-- packages/validator/src/spectral/index.js | 10 ++++++---- .../tables/rule-violation-details.test.js | 4 +--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/validator/src/markdown-report/tables/rule-violation-details.js b/packages/validator/src/markdown-report/tables/rule-violation-details.js index 062f7326f..ecb13f80c 100644 --- a/packages/validator/src/markdown-report/tables/rule-violation-details.js +++ b/packages/validator/src/markdown-report/tables/rule-violation-details.js @@ -32,9 +32,11 @@ function groupResultsByRule(results) { const pathStr = result.path.join('.'); - if (!groupedByRule[result.rule].violations.some( + if ( + !groupedByRule[result.rule].violations.some( v => v.line === result.line && v.path === pathStr - )) { + ) + ) { groupedByRule[result.rule].violations.push({ line: result.line, path: pathStr, diff --git a/packages/validator/src/spectral/index.js b/packages/validator/src/spectral/index.js index 11a617fea..9c0f52062 100644 --- a/packages/validator/src/spectral/index.js +++ b/packages/validator/src/spectral/index.js @@ -236,11 +236,13 @@ function convertSpectralSeverity(s) { } function getDocumentationURL(ruleCode) { - if(ruleCode.includes('ibm')) { - const baseUrl = 'https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md'; + if (ruleCode.includes('ibm')) { + const baseUrl = + 'https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md'; return `${baseUrl}#${ruleCode}`; } else { - const baseUrl = 'https://meta.stoplight.io/docs/spectral/4dec24461f3af-open-api-rules' - return `${baseUrl}#${ruleCode}` + const baseUrl = + 'https://meta.stoplight.io/docs/spectral/4dec24461f3af-open-api-rules'; + return `${baseUrl}#${ruleCode}`; } } diff --git a/packages/validator/test/markdown-report/tables/rule-violation-details.test.js b/packages/validator/test/markdown-report/tables/rule-violation-details.test.js index 85152137b..f61e15e6b 100644 --- a/packages/validator/test/markdown-report/tables/rule-violation-details.test.js +++ b/packages/validator/test/markdown-report/tables/rule-violation-details.test.js @@ -118,9 +118,7 @@ describe('ruleViolationDetails table tests', function () { warning: { results: [] }, }; const output = ruleViolationDetails(duplicate); - const dataRows = output - .split('\n') - .filter(l => l.startsWith('| 10 |')); + const dataRows = output.split('\n').filter(l => l.startsWith('| 10 |')); expect(dataRows).toHaveLength(1); }); From e22b8e4e4497ad8d81727c63bc596d8cdecb5414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20Felleg?= Date: Fri, 24 Jul 2026 12:11:58 +0200 Subject: [PATCH 10/14] fix: update docs link to GH pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: András Felleg --- packages/validator/src/spectral/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/validator/src/spectral/index.js b/packages/validator/src/spectral/index.js index 9c0f52062..f7043614b 100644 --- a/packages/validator/src/spectral/index.js +++ b/packages/validator/src/spectral/index.js @@ -238,7 +238,7 @@ function convertSpectralSeverity(s) { function getDocumentationURL(ruleCode) { if (ruleCode.includes('ibm')) { const baseUrl = - 'https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md'; + 'https://ibm.github.io/openapi-validator/docs/ibm-cloud-rules.html'; return `${baseUrl}#${ruleCode}`; } else { const baseUrl = From aa75bef20a08ba535130b7832ddbe4d64d68e204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20Felleg?= Date: Mon, 21 Sep 2026 16:39:24 +0200 Subject: [PATCH 11/14] fix: apply changes requested in PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: András Felleg --- packages/ruleset/src/ibm-oas.js | 4 +++- packages/validator/src/spectral/index.js | 14 +------------- .../tables/rule-violation-details.test.js | 6 +++--- .../test/test-utils/mock-json-output.json | 6 +++--- 4 files changed, 10 insertions(+), 20 deletions(-) diff --git a/packages/ruleset/src/ibm-oas.js b/packages/ruleset/src/ibm-oas.js index 6a8895343..7b50fe243 100644 --- a/packages/ruleset/src/ibm-oas.js +++ b/packages/ruleset/src/ibm-oas.js @@ -12,11 +12,13 @@ const ibmRules = require('./rules'); // However, we want to enable this rule also for OpenAPI 3.1.x documents, // so we'll just tweak Spectral's rule definition here. oas.rules['no-$ref-siblings'].formats = [oas3]; +oas.documentationUrl = + 'https://meta.stoplight.io/docs/spectral/4dec24461f3af-open-api-rules'; module.exports = { extends: oas, documentationUrl: - 'https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md', + 'https://ibm.github.io/openapi-validator/docs/ibm-cloud-rules.html', formats: [oas3], rules: { // Original list created from Spectral with: diff --git a/packages/validator/src/spectral/index.js b/packages/validator/src/spectral/index.js index f7043614b..18456b1bf 100644 --- a/packages/validator/src/spectral/index.js +++ b/packages/validator/src/spectral/index.js @@ -89,7 +89,7 @@ function convertResults(spectralResults, { config, logger }) { message: r.message, path: r.path, rule: r.code, - docLink: getDocumentationURL(r.code), + docLink: r.documentationUrl, line: r.range.start.line + 1, }); @@ -234,15 +234,3 @@ function convertSpectralSeverity(s) { const mapping = { 0: 'error', 1: 'warning', 2: 'info', 3: 'hint' }; return mapping[s]; } - -function getDocumentationURL(ruleCode) { - if (ruleCode.includes('ibm')) { - const baseUrl = - 'https://ibm.github.io/openapi-validator/docs/ibm-cloud-rules.html'; - return `${baseUrl}#${ruleCode}`; - } else { - const baseUrl = - 'https://meta.stoplight.io/docs/spectral/4dec24461f3af-open-api-rules'; - return `${baseUrl}#${ruleCode}`; - } -} diff --git a/packages/validator/test/markdown-report/tables/rule-violation-details.test.js b/packages/validator/test/markdown-report/tables/rule-violation-details.test.js index f61e15e6b..d4fcf4cfc 100644 --- a/packages/validator/test/markdown-report/tables/rule-violation-details.test.js +++ b/packages/validator/test/markdown-report/tables/rule-violation-details.test.js @@ -17,7 +17,7 @@ describe('ruleViolationDetails table tests', function () { // 3-col table expect(lines[2]).toBe( - '### [ibm-no-consecutive-path-parameter-segments](https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md#ibm-no-consecutive-path-parameter-segments)' + '### [ibm-no-consecutive-path-parameter-segments](https://ibm.github.io/openapi-validator/docs/ibm-cloud-rules.html#ibm-no-consecutive-path-parameter-segments)' ); expect(lines[3]).toBe(''); expect(lines[4]).toBe( @@ -33,7 +33,7 @@ describe('ruleViolationDetails table tests', function () { // 2-col table expect(lines[10]).toBe( - '### [ibm-integer-attributes](https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md#ibm-integer-attributes)' + '### [ibm-integer-attributes](https://ibm.github.io/openapi-validator/docs/ibm-cloud-rules.html#ibm-integer-attributes)' ); expect(lines[11]).toBe(''); expect(lines[12]).toBe( @@ -51,7 +51,7 @@ describe('ruleViolationDetails table tests', function () { // 2-col table expect(lines[20]).toBe( - '### [ibm-anchored-patterns](https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md#ibm-anchored-patterns)' + '### [ibm-anchored-patterns](https://ibm.github.io/openapi-validator/docs/ibm-cloud-rules.html#ibm-anchored-patterns)' ); expect(lines[21]).toBe(''); expect(lines[22]).toBe( diff --git a/packages/validator/test/test-utils/mock-json-output.json b/packages/validator/test/test-utils/mock-json-output.json index 7c7ef1203..d4ddf2664 100644 --- a/packages/validator/test/test-utils/mock-json-output.json +++ b/packages/validator/test/test-utils/mock-json-output.json @@ -8,7 +8,7 @@ "/pets/{pet_id}/{id}" ], "rule": "ibm-no-consecutive-path-parameter-segments", - "docLink": "https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md#ibm-no-consecutive-path-parameter-segments", + "docLink": "https://ibm.github.io/openapi-validator/docs/ibm-cloud-rules.html#ibm-no-consecutive-path-parameter-segments", "line": 84 }, { @@ -21,7 +21,7 @@ "id" ], "rule": "ibm-integer-attributes", - "docLink": "https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md#ibm-integer-attributes", + "docLink": "https://ibm.github.io/openapi-validator/docs/ibm-cloud-rules.html#ibm-integer-attributes", "line": 133 } ], @@ -54,7 +54,7 @@ "pattern" ], "rule": "ibm-anchored-patterns", - "docLink": "https://github.com/IBM/openapi-validator/blob/main/docs/ibm-cloud-rules.md#ibm-anchored-patterns", + "docLink": "https://ibm.github.io/openapi-validator/docs/ibm-cloud-rules.html#ibm-anchored-patterns", "line": 233 } ], From 4593822776e3576b604b52e381413b224879e451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20Felleg?= Date: Tue, 22 Sep 2026 15:29:07 +0200 Subject: [PATCH 12/14] fix: set doclink during spectral setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: András Felleg --- packages/validator/src/spectral/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/validator/src/spectral/index.js b/packages/validator/src/spectral/index.js index 18456b1bf..62d7a9c30 100644 --- a/packages/validator/src/spectral/index.js +++ b/packages/validator/src/spectral/index.js @@ -47,10 +47,10 @@ const runSpectral = async function ({ originalFile, validFile }, context) { // Save the resolved API definition for use in the scoring tool logic. context.apiDefinition = doc.data; - return convertResults(spectralResults, context); + return convertResults(spectralResults, context, spectral.ruleset.rules); }; -function convertResults(spectralResults, { config, logger }) { +function convertResults(spectralResults, { config, logger }, rules) { const { errorsOnly } = config; // This structure must match the JSON Schema defined for JSON output @@ -89,7 +89,7 @@ function convertResults(spectralResults, { config, logger }) { message: r.message, path: r.path, rule: r.code, - docLink: r.documentationUrl, + docLink: rules[r.code]?.documentationUrl, line: r.range.start.line + 1, }); From d935b368cd435d0917495a0f88611d5996440d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20Felleg?= Date: Tue, 22 Sep 2026 15:54:02 +0200 Subject: [PATCH 13/14] build: update ruleset package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: András Felleg --- package-lock.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 062bcb223..459247d88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14041,13 +14041,13 @@ }, "packages/ruleset": { "name": "@ibm-cloud/openapi-ruleset", - "version": "1.33.15", + "version": "1.33.9", "license": "Apache-2.0", "dependencies": { - "@ibm-cloud/openapi-ruleset-utilities": "1.9.4", - "@stoplight/spectral-formats": "1.8.5", - "@stoplight/spectral-functions": "1.10.5", - "@stoplight/spectral-rulesets": "1.22.7", + "@ibm-cloud/openapi-ruleset-utilities": "1.9.1", + "@stoplight/spectral-formats": "1.8.2", + "@stoplight/spectral-functions": "1.9.3", + "@stoplight/spectral-rulesets": "1.21.3", "chalk": "4.1.2", "inflected": "2.1.0", "jsonschema": "1.5.0", @@ -14103,7 +14103,7 @@ }, "packages/utilities": { "name": "@ibm-cloud/openapi-ruleset-utilities", - "version": "1.9.4", + "version": "1.9.1", "license": "Apache-2.0", "devDependencies": { "@stoplight/spectral-core": "1.23.1", @@ -14116,13 +14116,13 @@ }, "packages/validator": { "name": "ibm-openapi-validator", - "version": "1.38.4", + "version": "1.37.13", "license": "Apache-2.0", "dependencies": { - "@ibm-cloud/openapi-ruleset": "1.33.15", - "@ibm-cloud/openapi-ruleset-utilities": "1.9.4", - "@stoplight/spectral-cli": "6.16.3", - "@stoplight/spectral-core": "1.23.1", + "@ibm-cloud/openapi-ruleset": "1.33.9", + "@ibm-cloud/openapi-ruleset-utilities": "1.9.1", + "@stoplight/spectral-cli": "6.14.2", + "@stoplight/spectral-core": "1.19.4", "@stoplight/spectral-parsers": "1.0.5", "@stoplight/spectral-ref-resolver": "1.0.5", "ajv": "8.20.0", From 313dfbaa5915bb102c5d374de36977033433f796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20Felleg?= Date: Wed, 23 Sep 2026 13:24:48 +0200 Subject: [PATCH 14/14] build: update packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: András Felleg --- package-lock.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 459247d88..062bcb223 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14041,13 +14041,13 @@ }, "packages/ruleset": { "name": "@ibm-cloud/openapi-ruleset", - "version": "1.33.9", + "version": "1.33.15", "license": "Apache-2.0", "dependencies": { - "@ibm-cloud/openapi-ruleset-utilities": "1.9.1", - "@stoplight/spectral-formats": "1.8.2", - "@stoplight/spectral-functions": "1.9.3", - "@stoplight/spectral-rulesets": "1.21.3", + "@ibm-cloud/openapi-ruleset-utilities": "1.9.4", + "@stoplight/spectral-formats": "1.8.5", + "@stoplight/spectral-functions": "1.10.5", + "@stoplight/spectral-rulesets": "1.22.7", "chalk": "4.1.2", "inflected": "2.1.0", "jsonschema": "1.5.0", @@ -14103,7 +14103,7 @@ }, "packages/utilities": { "name": "@ibm-cloud/openapi-ruleset-utilities", - "version": "1.9.1", + "version": "1.9.4", "license": "Apache-2.0", "devDependencies": { "@stoplight/spectral-core": "1.23.1", @@ -14116,13 +14116,13 @@ }, "packages/validator": { "name": "ibm-openapi-validator", - "version": "1.37.13", + "version": "1.38.4", "license": "Apache-2.0", "dependencies": { - "@ibm-cloud/openapi-ruleset": "1.33.9", - "@ibm-cloud/openapi-ruleset-utilities": "1.9.1", - "@stoplight/spectral-cli": "6.14.2", - "@stoplight/spectral-core": "1.19.4", + "@ibm-cloud/openapi-ruleset": "1.33.15", + "@ibm-cloud/openapi-ruleset-utilities": "1.9.4", + "@stoplight/spectral-cli": "6.16.3", + "@stoplight/spectral-core": "1.23.1", "@stoplight/spectral-parsers": "1.0.5", "@stoplight/spectral-ref-resolver": "1.0.5", "ajv": "8.20.0",