From e0da467755b6e1c87fa14fa03ed5a42fb12edc1b Mon Sep 17 00:00:00 2001 From: Julius Jogela Date: Mon, 27 Jul 2026 14:06:39 +0100 Subject: [PATCH 1/5] fix(ipa): allow Operations endpoint paths in IPA-102 alternation The LRO standardization work introduces read-only Operations endpoints nested under a parent resource: /operations /operations/{operationId} /{resourceId}/operations /{resourceId}/operations/{operationId} xgen-IPA-102-path-alternate-resource-name-path-param rejected the collection-scoped shapes, because `operations` sits where the rule expects a path parameter. Treat a trailing `operations` or `operations/{operationId}` as a parity-neutral suffix: strip it, then run the existing alternation check unchanged. A positional carve-out is not sufficient, since an odd-indexed non-param segment shifts the parity of every segment after it, so `/operations/{operationId}` would still fail. The exemption requires a parent segment, so a bare `/operations` at the API root is unaffected; rejecting unscoped Operations endpoints belongs to IPA-132, not IPA-102. Verified additive: replaying the old and new logic over all 367 paths in openapi/.raw/v2.yaml produces zero behaviour changes. --- ...tesBetweenResourceNameAndPathParam.test.js | 50 +++++++++++++++++++ tools/spectral/ipa/rulesets/IPA-102.yaml | 4 +- tools/spectral/ipa/rulesets/README.md | 4 +- ...ternatesBetweenResourceNameAndPathParam.js | 33 +++++++++++- 4 files changed, 88 insertions(+), 3 deletions(-) diff --git a/tools/spectral/ipa/__tests__/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.test.js b/tools/spectral/ipa/__tests__/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.test.js index 9bb8a2cc91..61d0fe93ea 100644 --- a/tools/spectral/ipa/__tests__/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.test.js +++ b/tools/spectral/ipa/__tests__/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.test.js @@ -32,6 +32,24 @@ testRule('xgen-IPA-102-path-alternate-resource-name-path-param', [ }, errors: [], }, + { + name: 'valid paths - IPA-132 Operations endpoints', + document: { + paths: { + // Collection-scoped Operations endpoints, nested directly under the parent collection + '/api/atlas/v2/resourceName/operations': {}, + '/api/atlas/v2/resourceName/operations/{operationId}': {}, + '/api/atlas/v2/resourceName1/{pathParam}/resourceName2/operations': {}, + '/api/atlas/v2/resourceName1/{pathParam}/resourceName2/operations/{operationId}': {}, + // Instance-scoped Operations endpoints, nested under the parent resource instance + '/api/atlas/v2/resourceName/{pathParam}/operations': {}, + '/api/atlas/v2/resourceName/{pathParam}/operations/{operationId}': {}, + '/api/atlas/v2/unauth/resourceName/operations': {}, + '/api/atlas/v2/unauth/resourceName/operations/{operationId}': {}, + }, + }, + errors: [], + }, { name: 'invalid paths - api/atlas/v2', document: { @@ -134,6 +152,38 @@ testRule('xgen-IPA-102-path-alternate-resource-name-path-param', [ }, ], }, + { + name: 'invalid paths - only a trailing Operations suffix is exempt', + document: { + paths: { + // `operations` is only exempt as the final segment, optionally followed by its path param + '/api/atlas/v2/resourceName/operations/foo': {}, + '/api/atlas/v2/resourceName/operations/{operationId}/details': {}, + // A non-alternating segment which is not `operations` is still a violation + '/api/atlas/v2/resourceName/customAction': {}, + }, + }, + errors: [ + { + code: 'xgen-IPA-102-path-alternate-resource-name-path-param', + message: 'API paths must alternate between resource name and path params.', + path: ['paths', '/api/atlas/v2/resourceName/operations/foo'], + severity: DiagnosticSeverity.Error, + }, + { + code: 'xgen-IPA-102-path-alternate-resource-name-path-param', + message: 'API paths must alternate between resource name and path params.', + path: ['paths', '/api/atlas/v2/resourceName/operations/{operationId}/details'], + severity: DiagnosticSeverity.Error, + }, + { + code: 'xgen-IPA-102-path-alternate-resource-name-path-param', + message: 'API paths must alternate between resource name and path params.', + path: ['paths', '/api/atlas/v2/resourceName/customAction'], + severity: DiagnosticSeverity.Error, + }, + ], + }, { name: 'invalid paths with exceptions', document: { diff --git a/tools/spectral/ipa/rulesets/IPA-102.yaml b/tools/spectral/ipa/rulesets/IPA-102.yaml index d573295097..4a1287be01 100644 --- a/tools/spectral/ipa/rulesets/IPA-102.yaml +++ b/tools/spectral/ipa/rulesets/IPA-102.yaml @@ -35,9 +35,11 @@ rules: ##### Implementation details Rule checks for the following conditions: - - Paths must follow a pattern where resource names and path parameters strictly alternate + - Paths should alternate between resource names and path parameters - Even-indexed path segments should be resource names (not path parameters) - Odd-indexed path segments should be path parameters + - A trailing `operations` or `operations/{operationId}` suffix is exempt, for the Operations + endpoints defined by IPA-132 - Paths with `x-xgen-IPA-exception` for this rule are excluded from validation - If any parent path has an exception for this rule, the exception will be inherited. diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index 43ba12b55d..74fca1ff9f 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -56,9 +56,11 @@ Paths should alternate between resource names and path params. ##### Implementation details Rule checks for the following conditions: - - Paths must follow a pattern where resource names and path parameters strictly alternate + - Paths should alternate between resource names and path parameters - Even-indexed path segments should be resource names (not path parameters) - Odd-indexed path segments should be path parameters + - A trailing `operations` or `operations/{operationId}` suffix is exempt, for the Operations + endpoints defined by IPA-132 - Paths with `x-xgen-IPA-exception` for this rule are excluded from validation - If any parent path has an exception for this rule, the exception will be inherited. diff --git a/tools/spectral/ipa/rulesets/functions/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.js b/tools/spectral/ipa/rulesets/functions/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.js index 71f5b5acb7..76fd4db8f6 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.js +++ b/tools/spectral/ipa/rulesets/functions/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.js @@ -15,8 +15,39 @@ const getPrefix = (path) => { return null; }; +const OPERATIONS_SEGMENT = 'operations'; + +/** + * Removes a trailing Operations suffix (`operations` or `operations/{operationId}`) so the rest of + * the path can be checked for strict alternation. The Operations collection defined by IPA-132 is + * nested directly under its parent resource and intentionally does not alternate. + * + * @param {string[]} elements - The path segments + * @returns {string[]} The path segments without a trailing Operations suffix + */ +const stripOperationsSuffix = (elements) => { + const last = elements[elements.length - 1]; + const secondToLast = elements[elements.length - 2]; + + let suffixLength; + if (secondToLast === OPERATIONS_SEGMENT && isPathParam(last)) { + suffixLength = 2; // `.../operations/{operationId}` + } else if (last === OPERATIONS_SEGMENT) { + suffixLength = 1; // `.../operations` + } else { + return elements; + } + + // The exemption only applies to an Operations collection nested under a parent resource. An + // unscoped `/api/atlas/v2/operations` keeps its segments and is checked as an ordinary + // collection -- rejecting unscoped Operations endpoints belongs to IPA-132, not IPA-102. + // (Both unscoped shapes happen to alternate anyway, so this is a statement of intent.) + const parent = elements.slice(0, elements.length - suffixLength); + return parent.length > 0 ? parent : elements; +}; + const validatePathStructure = (elements) => { - return elements.every((element, index) => { + return stripOperationsSuffix(elements).every((element, index) => { const isEvenIndex = index % 2 === 0; return isEvenIndex ? !isPathParam(element) : isPathParam(element); }); From effe9d83a0d8ca19b732360938db41a6ee1558f4 Mon Sep 17 00:00:00 2001 From: Julius Jogela Date: Wed, 29 Jul 2026 09:41:14 +0100 Subject: [PATCH 2/5] docs(ipa): restore original IPA-102 alternation wording The Operations exemption is described by its own bullet, so the existing 'must follow a pattern where resource names and path parameters strictly alternate' line does not need softening. --- tools/spectral/ipa/rulesets/IPA-102.yaml | 2 +- tools/spectral/ipa/rulesets/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/spectral/ipa/rulesets/IPA-102.yaml b/tools/spectral/ipa/rulesets/IPA-102.yaml index 4a1287be01..dd512191e9 100644 --- a/tools/spectral/ipa/rulesets/IPA-102.yaml +++ b/tools/spectral/ipa/rulesets/IPA-102.yaml @@ -35,7 +35,7 @@ rules: ##### Implementation details Rule checks for the following conditions: - - Paths should alternate between resource names and path parameters + - Paths must follow a pattern where resource names and path parameters strictly alternate - Even-indexed path segments should be resource names (not path parameters) - Odd-indexed path segments should be path parameters - A trailing `operations` or `operations/{operationId}` suffix is exempt, for the Operations diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index 74fca1ff9f..f5a4957968 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -56,7 +56,7 @@ Paths should alternate between resource names and path params. ##### Implementation details Rule checks for the following conditions: - - Paths should alternate between resource names and path parameters + - Paths must follow a pattern where resource names and path parameters strictly alternate - Even-indexed path segments should be resource names (not path parameters) - Odd-indexed path segments should be path parameters - A trailing `operations` or `operations/{operationId}` suffix is exempt, for the Operations From 679c957d4c2dc114a21525191974a20cb467795f Mon Sep 17 00:00:00 2001 From: Julius Jogela Date: Wed, 29 Jul 2026 09:48:49 +0100 Subject: [PATCH 3/5] test(ipa): group unauth Operations paths under the collection-scoped comment The two unauth paths are collection-scoped, not instance-scoped, so they sat under the wrong comment. --- ...2EachPathAlternatesBetweenResourceNameAndPathParam.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/spectral/ipa/__tests__/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.test.js b/tools/spectral/ipa/__tests__/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.test.js index 61d0fe93ea..9b2bb394a7 100644 --- a/tools/spectral/ipa/__tests__/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.test.js +++ b/tools/spectral/ipa/__tests__/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.test.js @@ -41,11 +41,11 @@ testRule('xgen-IPA-102-path-alternate-resource-name-path-param', [ '/api/atlas/v2/resourceName/operations/{operationId}': {}, '/api/atlas/v2/resourceName1/{pathParam}/resourceName2/operations': {}, '/api/atlas/v2/resourceName1/{pathParam}/resourceName2/operations/{operationId}': {}, + '/api/atlas/v2/unauth/resourceName/operations': {}, + '/api/atlas/v2/unauth/resourceName/operations/{operationId}': {}, // Instance-scoped Operations endpoints, nested under the parent resource instance '/api/atlas/v2/resourceName/{pathParam}/operations': {}, '/api/atlas/v2/resourceName/{pathParam}/operations/{operationId}': {}, - '/api/atlas/v2/unauth/resourceName/operations': {}, - '/api/atlas/v2/unauth/resourceName/operations/{operationId}': {}, }, }, errors: [], From 3266668754891af4f3d6924d2b5502a41db493c0 Mon Sep 17 00:00:00 2001 From: Julius Jogela Date: Wed, 29 Jul 2026 11:39:11 +0100 Subject: [PATCH 4/5] refactor(ipa): drop the redundant root guard in stripOperationsSuffix Slicing an unscoped Operations path down to no segments already passes the alternation check vacuously, so the ternary fallback and the comment explaining it were describing a distinction with no observable effect. --- ...02EachPathAlternatesBetweenResourceNameAndPathParam.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tools/spectral/ipa/rulesets/functions/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.js b/tools/spectral/ipa/rulesets/functions/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.js index 76fd4db8f6..db28e96f1a 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.js +++ b/tools/spectral/ipa/rulesets/functions/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.js @@ -38,12 +38,8 @@ const stripOperationsSuffix = (elements) => { return elements; } - // The exemption only applies to an Operations collection nested under a parent resource. An - // unscoped `/api/atlas/v2/operations` keeps its segments and is checked as an ordinary - // collection -- rejecting unscoped Operations endpoints belongs to IPA-132, not IPA-102. - // (Both unscoped shapes happen to alternate anyway, so this is a statement of intent.) - const parent = elements.slice(0, elements.length - suffixLength); - return parent.length > 0 ? parent : elements; + // An unscoped `/api/atlas/v2/operations` strips to nothing and passes; rejecting it is IPA-132's job. + return elements.slice(0, elements.length - suffixLength); }; const validatePathStructure = (elements) => { From 0819e22ddc5d5057e8b4eb2e4d11f23c39b39385 Mon Sep 17 00:00:00 2001 From: Julius Jogela Date: Wed, 29 Jul 2026 12:05:27 +0100 Subject: [PATCH 5/5] test(ipa): cover exceptions on Operations paths An Operations path that now passes but still carries an x-xgen-IPA-exception gets an unnecessary-exception error, which is a direct consequence of this change and worth pinning. Drop the customAction case, which duplicates the existing resourceName1/ resourceName2 coverage. --- ...tesBetweenResourceNameAndPathParam.test.js | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/tools/spectral/ipa/__tests__/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.test.js b/tools/spectral/ipa/__tests__/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.test.js index 9b2bb394a7..45b105fcee 100644 --- a/tools/spectral/ipa/__tests__/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.test.js +++ b/tools/spectral/ipa/__tests__/IPA102EachPathAlternatesBetweenResourceNameAndPathParam.test.js @@ -159,8 +159,6 @@ testRule('xgen-IPA-102-path-alternate-resource-name-path-param', [ // `operations` is only exempt as the final segment, optionally followed by its path param '/api/atlas/v2/resourceName/operations/foo': {}, '/api/atlas/v2/resourceName/operations/{operationId}/details': {}, - // A non-alternating segment which is not `operations` is still a violation - '/api/atlas/v2/resourceName/customAction': {}, }, }, errors: [ @@ -176,14 +174,47 @@ testRule('xgen-IPA-102-path-alternate-resource-name-path-param', [ path: ['paths', '/api/atlas/v2/resourceName/operations/{operationId}/details'], severity: DiagnosticSeverity.Error, }, + ], + }, + { + name: 'Operations endpoints no longer need an exception', + document: { + paths: { + '/api/atlas/v2/resourceName/operations': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-102-path-alternate-resource-name-path-param': 'reason', + }, + }, + }, + }, + errors: [ { code: 'xgen-IPA-102-path-alternate-resource-name-path-param', - message: 'API paths must alternate between resource name and path params.', - path: ['paths', '/api/atlas/v2/resourceName/customAction'], + message: 'This component adopts the rule and does not need an exception. Please remove the exception.', + path: [ + 'paths', + '/api/atlas/v2/resourceName/operations', + 'x-xgen-IPA-exception', + 'xgen-IPA-102-path-alternate-resource-name-path-param', + ], severity: DiagnosticSeverity.Error, }, ], }, + { + name: 'Operations paths which are still violations may keep an exception', + document: { + paths: { + // `operations` is not the final segment, so the path is a violation the exception suppresses + '/api/atlas/v2/resourceName/operations/foo': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-102-path-alternate-resource-name-path-param': 'reason', + }, + }, + }, + }, + errors: [], + }, { name: 'invalid paths with exceptions', document: {