param( [object]$InputDocument, [switch]$SkipParity, [switch]$ReturnIssues ) $ErrorActionPreference = 'Stop' $root = Split-Path -Parent $PSScriptRoot $jsonPath = Join-Path $root 'APP.openapi.json' $document = if ($PSBoundParameters.ContainsKey('InputDocument')) { $InputDocument } else { Get-Content -Raw -Encoding UTF8 -LiteralPath $jsonPath | ConvertFrom-Json } $issues = New-Object System.Collections.Generic.List[string] $poemPath = '/genealogy/app/genealogies/{genealogyId}/generation-poems' $httpMethods = @('get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace') $identifierPattern = '^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$' $generationTextPattern = '^(?!.*[\u0000-\u001F\u007F-\u009F\u061C\u200B-\u200F\u2028-\u202E\u2060\u2066-\u2069\uFEFF])\S(?:[\s\S]*\S)?$' $generationPoemLexicalPattern = '(?i)(generation[-_ ]?poem|poem[-_ ]?set|poemSetVersion|poemId|generationText|\u5B57\u8F88)' function Add-Issue([string]$Message) { $script:issues.Add($Message) } function Test-IsJsonArray([object]$Value) { return $null -ne $Value -and $Value.GetType().IsArray } function Test-IsJsonBoolean([object]$Value, [bool]$Expected) { return $Value -is [System.Boolean] -and $Value -ceq $Expected } function Get-ExactProperty([object]$Object, [string]$Name) { if ($null -eq $Object) { return $null } $matches = @($Object.PSObject.Properties | Where-Object { $_.Name -ceq $Name }) if ($matches.Count -eq 1) { return $matches[0] } return $null } function Test-ContainsExact([object[]]$Values, [object]$Value) { return @($Values | Where-Object { $_ -ceq $Value }).Count -gt 0 } function Assert-CanonicalKeyCasing([object]$Object, [string]$Label, [string[]]$CanonicalNames) { if (-not $Object) { return } foreach ($property in @($Object.PSObject.Properties)) { $canonical = @($CanonicalNames | Where-Object { $_ -ieq $property.Name }) if ($canonical.Count -eq 1 -and $property.Name -cne $canonical[0]) { Add-Issue "$Label contains non-canonical object keyword casing: $($property.Name); expected: $($canonical[0])" } } } function Get-OrdinalMapKey([string]$Value) { return [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Value)) } function Test-IsNonNullable([object]$Schema) { if (-not $Schema) { return $false } $nullable = Get-ExactProperty $Schema 'nullable' return -not $nullable -or (Test-IsJsonBoolean $nullable.Value $false) } function Assert-AllowedObjectKeys { param( [object]$Object, [string]$Label, [string[]]$Allowed, [string[]]$Required = @(), [string[]]$AllowedExtensions = @() ) if (-not $Object) { return } foreach ($property in @($Object.PSObject.Properties)) { if ($property.Name -clike 'x-*') { if (-not (Test-ContainsExact $AllowedExtensions $property.Name)) { Add-Issue "$Label contains an unowned semantic extension: $($property.Name)" } continue } if (-not (Test-ContainsExact $Allowed $property.Name)) { Add-Issue "$Label contains an unowned object keyword: $($property.Name)" } } foreach ($name in $Required) { if (-not (Get-ExactProperty $Object $name)) { Add-Issue "$Label missing required object keyword: $name" } } } function Assert-NoConflictingSchemaKeywords([object]$Schema, [string]$Label, [string[]]$Allowed = @()) { if (-not $Schema) { return } foreach ($keyword in @('not', 'allOf', 'anyOf', 'oneOf', 'const', 'enum')) { if (-not (Test-ContainsExact $Allowed $keyword) -and (Get-ExactProperty $Schema $keyword)) { Add-Issue "$Label must not define conflicting schema keyword: $keyword" } } } function Assert-AllowedSchemaKeywords([object]$Schema, [string]$Label, [string[]]$Allowed) { if (-not $Schema) { return } $annotations = @('title', 'description', 'example', 'deprecated') foreach ($property in @($Schema.PSObject.Properties)) { if ((Test-ContainsExact $annotations $property.Name) -or (Test-ContainsExact $Allowed $property.Name)) { continue } Add-Issue "$Label contains an unowned schema keyword: $($property.Name)" } } function Assert-AllowedSchemaKeywordsWithExtensions { param( [object]$Schema, [string]$Label, [string[]]$Allowed, [string[]]$AllowedExtensions ) if (-not $Schema) { return } $annotations = @('title', 'description', 'example', 'deprecated') foreach ($property in @($Schema.PSObject.Properties)) { if ($property.Name -clike 'x-*') { if (-not (Test-ContainsExact $AllowedExtensions $property.Name)) { Add-Issue "$Label contains an unowned semantic extension: $($property.Name)" } continue } if ((Test-ContainsExact $annotations $property.Name) -or (Test-ContainsExact $Allowed $property.Name)) { continue } Add-Issue "$Label contains an unowned schema keyword: $($property.Name)" } } function Get-LocalComponentName([string]$Ref, [string]$Section, [string]$Label) { $pattern = '^#/components/' + [regex]::Escape($Section) + '/(?[^/]+)$' $match = [regex]::Match($Ref, $pattern) if (-not $match.Success) { Add-Issue "$Label must use an exact local #/components/$Section/... ref; actual: $Ref" return '' } return $match.Groups['name'].Value } function Test-IsPureSchemaRef([object]$Schema, [string]$ExpectedRef, [string]$Label) { if (-not $Schema) { return $false } $properties = @($Schema.PSObject.Properties.Name) $actualRef = [string]$Schema.'$ref' if ($properties.Count -ne 1 -or $properties[0] -cne '$ref' -or $actualRef -cne $ExpectedRef) { Add-Issue "$Label must be the sole exact local schema ref $ExpectedRef; actual: $actualRef" return $false } [void](Get-LocalComponentName $actualRef 'schemas' $Label) return $true } function Get-ComponentSection([string]$Section) { $componentsProperty = Get-ExactProperty $document 'components' $components = if ($componentsProperty) { $componentsProperty.Value } else { $null } $sectionProperty = Get-ExactProperty $components $Section return $(if ($sectionProperty) { $sectionProperty.Value } else { $null }) } function Resolve-PureLocalReference { param( [object]$Object, [string]$Section, [string]$Label, [hashtable]$Seen = @{} ) if (-not $Object) { return $null } $refProperty = Get-ExactProperty $Object '$ref' if (-not $refProperty) { return $Object } $keys = @($Object.PSObject.Properties.Name) if ($keys.Count -ne 1 -or $keys[0] -cne '$ref') { Add-Issue "$Label OpenAPI 3.0.1 Reference Object must contain only `$ref" } $ref = [string]$refProperty.Value $name = Get-LocalComponentName $ref $Section $Label if (-not $name) { return $null } $key = "$Section/$name" $seenKey = Get-OrdinalMapKey $key if ($Seen.ContainsKey($seenKey)) { Add-Issue "$Label contains a cyclic local reference: $ref" return $null } $Seen[$seenKey] = $true $sectionOwner = Get-ComponentSection $Section $ownerProperty = Get-ExactProperty $sectionOwner $name if (-not $ownerProperty) { Add-Issue "$Label references missing $Section owner: $name" return $null } return Resolve-PureLocalReference $ownerProperty.Value $Section "$Label->$name" $Seen } function Assert-ParameterObject([object]$Parameter, [string]$Label) { if (-not $Parameter) { return } Assert-AllowedObjectKeys $Parameter $Label @('name', 'in', 'required', 'schema', 'description', 'example', 'examples') @('name', 'in', 'required', 'schema') if (Get-ExactProperty $Parameter 'content') { Add-Issue "$Label Parameter Object must use schema only, never content" } if ((Get-ExactProperty $Parameter 'example') -and (Get-ExactProperty $Parameter 'examples')) { Add-Issue "$Label Parameter Object must not define both example and examples" } } function Assert-HeaderObject([object]$Header, [string]$Label) { if (-not $Header) { return } Assert-AllowedObjectKeys $Header $Label @('schema', 'description', 'example', 'examples') @('schema') if (Get-ExactProperty $Header 'content') { Add-Issue "$Label Header Object must use schema only, never content" } if ((Get-ExactProperty $Header 'example') -and (Get-ExactProperty $Header 'examples')) { Add-Issue "$Label Header Object must not define both example and examples" } } function Assert-MediaTypeObject([object]$MediaType, [string]$Label) { if (-not $MediaType) { return } Assert-AllowedObjectKeys $MediaType $Label @('schema', 'example', 'examples') @('schema') if ((Get-ExactProperty $MediaType 'example') -and (Get-ExactProperty $MediaType 'examples')) { Add-Issue "$Label MediaType Object must not define both example and examples" } } function Assert-RequestBodyObject([object]$RequestBody, [string]$Label) { if (-not $RequestBody) { return } Assert-AllowedObjectKeys $RequestBody $Label @('required', 'content', 'description') @('required', 'content') } function Assert-ResponseObject([object]$Response, [string]$Label) { if (-not $Response) { return } Assert-AllowedObjectKeys $Response $Label @('description', 'headers', 'content') @('description', 'headers', 'content') } function Resolve-Parameter([object]$Parameter, [string]$Label) { $resolved = Resolve-PureLocalReference $Parameter 'parameters' $Label @{} Assert-ParameterObject $resolved $Label return $resolved } function Resolve-Response([object]$Response, [string]$Label) { $resolved = Resolve-PureLocalReference $Response 'responses' $Label @{} Assert-ResponseObject $resolved $Label return $resolved } function Resolve-Header([object]$Header, [string]$Label) { $resolved = Resolve-PureLocalReference $Header 'headers' $Label @{} Assert-HeaderObject $resolved $Label return $resolved } function Get-Schema([string]$Name) { $componentsProperty = Get-ExactProperty $document 'components' $schemasProperty = if ($componentsProperty) { Get-ExactProperty $componentsProperty.Value 'schemas' } else { $null } $property = if ($schemasProperty) { Get-ExactProperty $schemasProperty.Value $Name } else { $null } if (-not $property) { Add-Issue "missing schema owner: $Name" return $null } return $property.Value } function Resolve-RequestBody([object]$RequestBody, [string]$Label) { $resolved = Resolve-PureLocalReference $RequestBody 'requestBodies' $Label @{} Assert-RequestBodyObject $resolved $Label return $resolved } function Test-ObjectGraphLooksLikeGenerationPoem { param( [object]$Value, [hashtable]$Seen, [string]$Label, [string]$ExpectedRefSection = '', [ValidateSet('Any', 'Mutation', 'CanonicalRead')] [string]$OwnerKind = 'Any' ) if ($null -eq $Value) { return $false } if ($Value -is [string]) { return $false } if ($Value -is [ValueType]) { return $false } if (Test-IsJsonArray $Value) { $arrayMatched = $false foreach ($item in @($Value)) { if (Test-ObjectGraphLooksLikeGenerationPoem $item $Seen "$Label[]" $ExpectedRefSection $OwnerKind) { $arrayMatched = $true } } return $arrayMatched } $refProperty = Get-ExactProperty $Value '$ref' if ($refProperty) { $keys = @($Value.PSObject.Properties.Name) $hasReferenceSiblings = $keys.Count -ne 1 -or $keys[0] -cne '$ref' $ref = [string]$refProperty.Value $match = [regex]::Match($ref, '^#/components/(?
schemas|parameters|headers|requestBodies|responses|callbacks|securitySchemes)/(?[^/]+)$') if (-not $match.Success) { Add-Issue "$Label contains an uninspectable external or wrong-section ref: $ref" return $ref -cmatch $generationPoemLexicalPattern } $section = $match.Groups['section'].Value $name = $match.Groups['name'].Value if ($ExpectedRefSection -and $section -cne $ExpectedRefSection) { Add-Issue "$Label uses wrong-section ref $ref; expected components/$ExpectedRefSection" return $ref -cmatch $generationPoemLexicalPattern } $key = "$section/$name" $seenKey = Get-OrdinalMapKey $key if ($Seen.ContainsKey($seenKey)) { return $false } $Seen[$seenKey] = $true $sectionOwner = Get-ComponentSection $section $ownerProperty = Get-ExactProperty $sectionOwner $name if (-not $ownerProperty) { Add-Issue "$Label references missing $section owner: $name" return $name -cmatch $generationPoemLexicalPattern } $named = switch ($OwnerKind) { 'Mutation' { $name -cmatch '^(AppGenerationPoemSetUpdateBody|AppGenerationPoemSetItem|GenerationPoemText|GenerationPoemBody|GenerationPoemBatchBody)$' } 'CanonicalRead' { $name -cmatch '^(RGenerationPoemSetSnapshot|GenerationPoemSetSnapshot|RGenerationPoemList|GenerationPoemListResult)$' } default { $name -cmatch $generationPoemLexicalPattern } } $nested = Test-ObjectGraphLooksLikeGenerationPoem $ownerProperty.Value $Seen "$Label->$name" $section $OwnerKind if (($named -or $nested) -and $hasReferenceSiblings) { Add-Issue "$Label G12-related OpenAPI 3.0.1 Reference Object must contain only `$ref" } return $named -or $nested } $propertyNames = @($Value.PSObject.Properties.Name) $mutationSignature = (Test-ContainsExact $propertyNames 'poemText') -or (Test-ContainsExact $propertyNames 'disableMissing') -or ((Test-ContainsExact $propertyNames 'generationText') -and (Test-ContainsExact $propertyNames 'generationNo')) $readSignature = (Test-ContainsExact $propertyNames 'poemSetVersion') -and (Test-ContainsExact $propertyNames 'items') $signature = switch ($OwnerKind) { 'Mutation' { $mutationSignature } 'CanonicalRead' { $readSignature } default { $mutationSignature -or $readSignature } } $matched = [bool]$signature if (Get-ExactProperty $Value 'responses') { $operationMetadata = @( [string]$Value.operationId, [string]$Value.summary, [string]$Value.description, (@($Value.tags) -join ' ') ) -join ' ' if ($operationMetadata -cmatch $generationPoemLexicalPattern) { $matched = $true } } foreach ($property in @($Value.PSObject.Properties)) { $name = $property.Name $child = $property.Value if (($name -cmatch '^/|^\{') -and $name -cmatch $generationPoemLexicalPattern) { $matched = $true } if ($name -ceq 'properties') { $schemaFieldNames = @($child.PSObject.Properties.Name) $schemaMutationSignature = (Test-ContainsExact $schemaFieldNames 'poemText') -or (Test-ContainsExact $schemaFieldNames 'disableMissing') -or ((Test-ContainsExact $schemaFieldNames 'generationText') -and (Test-ContainsExact $schemaFieldNames 'generationNo')) $schemaReadSignature = (Test-ContainsExact $schemaFieldNames 'poemSetVersion') -and (Test-ContainsExact $schemaFieldNames 'items') $schemaSignature = switch ($OwnerKind) { 'Mutation' { $schemaMutationSignature } 'CanonicalRead' { $schemaReadSignature } default { $schemaMutationSignature -or $schemaReadSignature } } if ($schemaSignature) { $matched = $true } foreach ($schemaProperty in @($child.PSObject.Properties)) { if (Test-ObjectGraphLooksLikeGenerationPoem $schemaProperty.Value $Seen "$Label.properties.$($schemaProperty.Name)" 'schemas' $OwnerKind) { $matched = $true } } continue } if (Test-ContainsExact @('schema', 'items', 'additionalProperties', 'not') $name) { if ($child -isnot [System.Boolean] -and (Test-ObjectGraphLooksLikeGenerationPoem $child $Seen "$Label.$name" 'schemas' $OwnerKind)) { $matched = $true } continue } if (Test-ContainsExact @('allOf', 'oneOf', 'anyOf') $name) { foreach ($branch in @($child)) { if (Test-ObjectGraphLooksLikeGenerationPoem $branch $Seen "$Label.$name" 'schemas' $OwnerKind) { $matched = $true } } continue } if ($name -ceq 'parameters') { foreach ($parameter in @($child)) { if (Test-ObjectGraphLooksLikeGenerationPoem $parameter $Seen "$Label.parameters" 'parameters' $OwnerKind) { $matched = $true } } continue } if ($name -ceq 'requestBody') { if (Test-ObjectGraphLooksLikeGenerationPoem $child $Seen "$Label.requestBody" 'requestBodies' $OwnerKind) { $matched = $true } continue } if ($name -ceq 'responses') { foreach ($response in @($child.PSObject.Properties)) { if (Test-ObjectGraphLooksLikeGenerationPoem $response.Value $Seen "$Label.responses.$($response.Name)" 'responses' $OwnerKind) { $matched = $true } } continue } if ($name -ceq 'headers') { foreach ($header in @($child.PSObject.Properties)) { if (Test-ObjectGraphLooksLikeGenerationPoem $header.Value $Seen "$Label.headers.$($header.Name)" 'headers' $OwnerKind) { $matched = $true } } continue } if ($name -ceq 'callbacks') { foreach ($callback in @($child.PSObject.Properties)) { if (Test-ObjectGraphLooksLikeGenerationPoem $callback.Value $Seen "$Label.callbacks.$($callback.Name)" 'callbacks' $OwnerKind) { $matched = $true } } continue } if ($name -ceq 'mapping') { foreach ($mapping in @($child.PSObject.Properties)) { $mappingRef = [pscustomobject]@{ '$ref' = [string]$mapping.Value } if (Test-ObjectGraphLooksLikeGenerationPoem $mappingRef $Seen "$Label.mapping.$($mapping.Name)" 'schemas' $OwnerKind) { $matched = $true } } continue } if ($name -clike 'x-*' -or (Test-ContainsExact @('title', 'summary', 'description', 'example', 'examples', 'default', 'externalDocs', 'xml') $name)) { continue } if (Test-ObjectGraphLooksLikeGenerationPoem $child $Seen "$Label.$name" '' $OwnerKind) { $matched = $true } } return $matched } function Test-OperationHasGenerationPoemIdentity([string]$Path, [object]$Operation) { $identity = @( $Path, [string]$Operation.operationId, (@($Operation.tags) -join ' ') ) -join ' ' return $identity -cmatch $generationPoemLexicalPattern } function Get-GlobalGenerationPoemOwners([ValidateSet('Mutation', 'CanonicalRead')][string]$OwnerKind) { $owners = New-Object System.Collections.Generic.List[string] $pathsProperty = Get-ExactProperty $document 'paths' $paths = if ($pathsProperty) { $pathsProperty.Value } else { $null } foreach ($pathProperty in @($paths.PSObject.Properties | Where-Object { $_.Name -cmatch '^/genealogy/app(?:/|$)' })) { $pathItem = $pathProperty.Value if (Get-ExactProperty $pathItem '$ref') { Add-Issue "Path Item $($pathProperty.Name) must not use `$ref in the OpenAPI 3.0.1 APP owner graph" } $methods = if ($OwnerKind -ceq 'Mutation') { @('post', 'put', 'patch', 'delete') } else { @('get', 'head') } foreach ($methodProperty in @($pathItem.PSObject.Properties | Where-Object { Test-ContainsExact $methods $_.Name })) { $label = "$($methodProperty.Name.ToUpperInvariant()) $($pathProperty.Name)" $operation = $methodProperty.Value $matched = Test-OperationHasGenerationPoemIdentity $pathProperty.Name $operation if ($OwnerKind -ceq 'Mutation') { if ((Get-ExactProperty $operation 'requestBody') -and (Test-ObjectGraphLooksLikeGenerationPoem $operation.requestBody @{} "$label requestBody" 'requestBodies' 'Mutation')) { $matched = $true } foreach ($parameter in @(@($pathItem.parameters) + @($operation.parameters))) { if (Test-ObjectGraphLooksLikeGenerationPoem $parameter @{} "$label parameter" 'parameters' 'Mutation') { $matched = $true } } } else { if ((Get-ExactProperty $operation 'responses') -and (Test-ObjectGraphLooksLikeGenerationPoem $operation.responses @{} "$label responses" '' 'CanonicalRead')) { $matched = $true } } if ((Get-ExactProperty $operation 'callbacks') -and (Test-ObjectGraphLooksLikeGenerationPoem $operation.callbacks @{} "$label callbacks" '' 'Any')) { $matched = $true } if ($matched) { $owners.Add($label) } } foreach ($methodProperty in @($pathItem.PSObject.Properties | Where-Object { Test-ContainsExact @('options', 'trace') $_.Name })) { $label = "$($methodProperty.Name.ToUpperInvariant()) $($pathProperty.Name)" $operation = $methodProperty.Value $matched = Test-OperationHasGenerationPoemIdentity $pathProperty.Name $operation if (Test-ObjectGraphLooksLikeGenerationPoem $operation @{} $label '' 'Any') { $matched = $true } if ($matched) { Add-Issue "$label is a forbidden G12 contract surface" } } } return @($owners | Sort-Object -Unique) } function Get-GlobalGenerationPoemMutationOwners { return @(Get-GlobalGenerationPoemOwners 'Mutation') } function Get-GlobalGenerationPoemReadOwners { return @(Get-GlobalGenerationPoemOwners 'CanonicalRead') } function Get-Operation([string]$Path, [string]$Method) { $pathsProperty = Get-ExactProperty $document 'paths' $pathProperty = if ($pathsProperty) { Get-ExactProperty $pathsProperty.Value $Path } else { $null } if (-not $pathProperty) { Add-Issue "missing path: $Path" return $null } $operation = Get-ExactProperty $pathProperty.Value $Method if (-not $operation) { Add-Issue "missing $($Method.ToUpperInvariant()) $Path" return $null } return $operation.Value } function Assert-OnlyMethods([string]$Path, [string[]]$Expected) { $pathsProperty = Get-ExactProperty $document 'paths' $pathProperty = if ($pathsProperty) { Get-ExactProperty $pathsProperty.Value $Path } else { $null } if (-not $pathProperty) { return } Assert-CanonicalKeyCasing $pathProperty.Value "$Path Path Item" @('$ref', 'summary', 'description', 'get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace', 'servers', 'parameters') $actual = @($pathProperty.Value.PSObject.Properties.Name | Where-Object { Test-ContainsExact $httpMethods $_ } | Sort-Object) $wanted = @($Expected | Sort-Object) if (($actual -join ',') -cne ($wanted -join ',')) { Add-Issue "$Path must expose exactly $($wanted -join ','); actual: $($actual -join ',')" } } function Assert-GlobalOperationId([string]$OperationId, [string]$ExpectedOwner) { $owners = New-Object System.Collections.Generic.List[string] foreach ($pathProperty in @($document.paths.PSObject.Properties)) { foreach ($methodProperty in @($pathProperty.Value.PSObject.Properties | Where-Object { Test-ContainsExact $httpMethods $_.Name })) { if ([string]$methodProperty.Value.operationId -ceq $OperationId) { $owners.Add("$($methodProperty.Name.ToUpperInvariant()) $($pathProperty.Name)") } } } if ($owners.Count -ne 1 -or $owners[0] -cne $ExpectedOwner) { Add-Issue "operationId $OperationId must have one global owner $ExpectedOwner; actual: $($owners -join ',')" } } function Get-OperationParameters([string]$Path, [object]$Operation, [string]$Label) { if (-not $Operation) { return @() } $resolved = @() $pathsProperty = Get-ExactProperty $document 'paths' $pathProperty = if ($pathsProperty) { Get-ExactProperty $pathsProperty.Value $Path } else { $null } $pathParametersProperty = if ($pathProperty) { Get-ExactProperty $pathProperty.Value 'parameters' } else { $null } $operationParametersProperty = Get-ExactProperty $Operation 'parameters' $pathParameters = if ($pathParametersProperty) { @($pathParametersProperty.Value) } else { @() } $operationParameters = if ($operationParametersProperty) { @($operationParametersProperty.Value) } else { @() } foreach ($scope in @($pathParameters, $operationParameters)) { foreach ($parameter in $scope) { $value = Resolve-Parameter $parameter "$Label parameter" if ($value) { $resolved += $value } } } return $resolved } function Assert-ExactParameters([object[]]$Parameters, [string]$Label, [string[]]$Expected) { $actual = @($Parameters | ForEach-Object { "$($_.in):$($_.name)" } | Sort-Object) $wanted = @($Expected | Sort-Object) if (($actual -join ',') -cne ($wanted -join ',')) { Add-Issue "$Label parameters must be exactly $($wanted -join ','); actual: $($actual -join ',')" } } function Get-Parameter([object[]]$Parameters, [string]$Name, [string]$In, [string]$Label) { $matches = @($Parameters | Where-Object { [string]$_.name -ceq $Name -and [string]$_.in -ceq $In }) if ($matches.Count -ne 1) { Add-Issue "$Label must declare exactly one $In parameter: $Name" return $null } return $matches[0] } function Assert-SaToken([object]$Operation, [string]$Label) { if (-not $Operation) { return } if (-not (Test-IsJsonArray $Operation.security)) { Add-Issue "$Label must require exactly one SaToken security alternative" return } $security = @($Operation.security) $saTokenProperty = if ($security.Count -eq 1) { Get-ExactProperty $security[0] 'SaToken' } else { $null } if ($security.Count -ne 1 -or $security[0].PSObject.Properties.Count -ne 1 -or -not $saTokenProperty -or -not (Test-IsJsonArray $saTokenProperty.Value) -or @($saTokenProperty.Value).Count -ne 0) { Add-Issue "$Label must require exactly one SaToken security alternative" } } function Assert-SaTokenOwner { $securitySchemes = Get-ComponentSection 'securitySchemes' $ownerProperty = Get-ExactProperty $securitySchemes 'SaToken' if (-not $ownerProperty) { Add-Issue 'missing security scheme owner: SaToken' return } $owner = Resolve-PureLocalReference $ownerProperty.Value 'securitySchemes' 'SaToken security scheme owner' @{} if (-not $owner) { return } Assert-AllowedObjectKeys $owner 'SaToken security scheme owner' @('type', 'in', 'name', 'description') @('type', 'in', 'name') if ([string]$owner.type -cne 'apiKey' -or [string]$owner.in -cne 'header' -or [string]$owner.name -cne 'Authorization') { Add-Issue 'SaToken must be the header apiKey security scheme named Authorization' } } function Assert-AppGatewayCorsPolicyOwner { $policiesProperty = Get-ExactProperty $document 'x-app-gateway-policies' $policies = if ($policiesProperty) { $policiesProperty.Value } else { $null } $policyProperty = Get-ExactProperty $policies 'APP_GATEWAY_PREFLIGHT' if (-not $policyProperty) { Add-Issue 'missing top-level gateway CORS policy owner: APP_GATEWAY_PREFLIGHT' return } $policy = $policyProperty.Value Assert-AllowedObjectKeys $policy 'APP_GATEWAY_PREFLIGHT gateway policy' @('allowedRequestHeaders', 'allowedMethods', 'originPolicy', 'maxAgeSeconds') @('allowedRequestHeaders', 'allowedMethods', 'originPolicy', 'maxAgeSeconds') if (-not (Test-IsJsonArray $policy.allowedRequestHeaders) -or (@($policy.allowedRequestHeaders) -join ',') -cne 'Authorization,Content-Type,If-Match,clientid' -or -not (Test-IsJsonArray $policy.allowedMethods) -or (@($policy.allowedMethods) -join ',') -cne 'GET,PUT,OPTIONS' -or [string]$policy.originPolicy -cne 'EXPLICIT_DEPLOYMENT_ALLOWLIST' -or ($policy.maxAgeSeconds -isnot [int] -and $policy.maxAgeSeconds -isnot [long]) -or [int64]$policy.maxAgeSeconds -ne 600) { Add-Issue 'APP_GATEWAY_PREFLIGHT must own the exact G12 browser preflight policy' } } function Assert-OperationObject([object]$Operation, [string]$Label, [string[]]$AllowedExtensions, [bool]$RequiresRequestBody) { if (-not $Operation) { return } $allowed = @('tags', 'summary', 'description', 'operationId', 'parameters', 'responses', 'security', 'deprecated') $required = @('description', 'operationId', 'responses', 'security') if ($RequiresRequestBody) { $allowed += 'requestBody' $required += 'requestBody' } Assert-AllowedObjectKeys $Operation $Label $allowed $required $AllowedExtensions if ((Get-ExactProperty $Operation 'deprecated') -and -not (Test-IsJsonBoolean $Operation.deprecated $false)) { Add-Issue "$Label must not publish the canonical owner as deprecated" } } function Assert-ExactResponseSet([object]$Operation, [string]$Label, [string[]]$Expected) { if (-not $Operation) { return } $actual = @($Operation.responses.PSObject.Properties.Name | Sort-Object) $wanted = @($Expected | Sort-Object) if (($actual -join ',') -cne ($wanted -join ',')) { Add-Issue "$Label responses must be exactly $($wanted -join ','); actual: $($actual -join ',')" } foreach ($status in $actual) { if ($status -ceq 'default' -or $status -cmatch '^3' -or $status -ceq '412') { Add-Issue "$Label must not use default, 3xx, or 412 responses" } } } function Get-Response([object]$Operation, [string]$Status, [string]$Label) { if (-not $Operation) { return $null } $property = Get-ExactProperty $Operation.responses $Status if (-not $property) { Add-Issue "$Label missing response: $Status" return $null } return Resolve-Response $property.Value "$Label $Status" } function Get-JsonResponseSchemaRef([object]$Response, [string]$Label) { if (-not $Response) { return '' } $media = if ($Response.content) { @($Response.content.PSObject.Properties) } else { @() } if ($media.Count -ne 1 -or $media[0].Name -cne 'application/json') { Add-Issue "$Label must expose only application/json" return '' } Assert-MediaTypeObject $media[0].Value "$Label application/json" $schema = $media[0].Value.schema $ref = [string]$schema.'$ref' if (-not $ref) { Add-Issue "$Label must use a component schema ref" return '' } [void](Get-LocalComponentName $ref 'schemas' "$Label schema") $keywords = @($schema.PSObject.Properties.Name) if ($keywords.Count -ne 1 -or $keywords[0] -cne '$ref') { Add-Issue "$Label response schema must contain only its exact local schema ref" } return $ref } function Assert-ResponseHeaders([object]$Response, [string]$Status, [string]$Label) { if (-not $Response) { return } $headers = if ($Response.headers) { @($Response.headers.PSObject.Properties.Name) } else { @() } $cacheProperty = Get-ExactProperty $Response.headers 'Cache-Control' if (-not $cacheProperty) { Add-Issue "$Label must use the shared PrivateNoStore Cache-Control header" } else { $cacheRef = [string]$cacheProperty.Value.'$ref' $refKeys = @($cacheProperty.Value.PSObject.Properties.Name) if ($cacheRef -cne '#/components/headers/PrivateNoStore' -or $refKeys.Count -ne 1 -or $refKeys[0] -cne '$ref') { Add-Issue "$Label Cache-Control must be the sole exact local PrivateNoStore Reference Object" } [void](Get-LocalComponentName $cacheRef 'headers' "$Label Cache-Control") } if ($Status -ceq '429') { $retryProperty = Get-ExactProperty $Response.headers 'Retry-After' if (-not $retryProperty) { Add-Issue "$Label must use the shared RetryAfter header" } else { $retryRef = [string]$retryProperty.Value.'$ref' $refKeys = @($retryProperty.Value.PSObject.Properties.Name) if ($retryRef -cne '#/components/headers/RetryAfter' -or $refKeys.Count -ne 1 -or $refKeys[0] -cne '$ref') { Add-Issue "$Label Retry-After must be the sole exact local RetryAfter Reference Object" } [void](Get-LocalComponentName $retryRef 'headers' "$Label Retry-After") } } if (@($headers | Where-Object { $_ -ceq 'ETag' }).Count -gt 0) { Add-Issue "$Label must not publish ETag; poemSetVersion is the sole concurrency owner" } $allowed = @('Cache-Control', 'traceparent', 'tracestate', 'x-request-id', 'x-correlation-id') if ($Status -ceq '429') { $allowed += 'Retry-After' } $unexpected = @($headers | Where-Object { -not (Test-ContainsExact $allowed $_) }) if ($unexpected.Count -gt 0) { Add-Issue "$Label has forbidden semantic response headers: $($unexpected -join ',')" } foreach ($traceName in @('traceparent', 'tracestate', 'x-request-id', 'x-correlation-id')) { $property = Get-ExactProperty $Response.headers $traceName if (-not $property) { continue } $header = Resolve-Header $property.Value "$Label $traceName" if (-not $header) { continue } if (-not $header.schema -or [string]$header.schema.type -cne 'string' -or -not (Test-IsNonNullable $header.schema)) { Add-Issue "$Label $traceName must resolve to a Header Object with a non-null string schema" continue } Assert-NoConflictingSchemaKeywords $header.schema "$Label $traceName schema" Assert-AllowedSchemaKeywords $header.schema "$Label $traceName schema" @('type', 'nullable') } } function Assert-ExactObject([object]$Schema, [string]$Name, [string[]]$Properties, [string[]]$Required, [string[]]$AllowedExtensions = @()) { if (-not $Schema) { return } $actualProperties = if ($Schema.properties) { @($Schema.properties.PSObject.Properties.Name | Sort-Object) } else { @() } $actualRequired = @($Schema.required | Sort-Object) $expectedProperties = @($Properties | Sort-Object) $expectedRequired = @($Required | Sort-Object) if ([string]$Schema.type -cne 'object' -or -not (Test-IsNonNullable $Schema) -or -not (Test-IsJsonBoolean $Schema.additionalProperties $false) -or -not (Test-IsJsonArray $Schema.required) -or ($actualProperties -join ',') -cne ($expectedProperties -join ',') -or ($actualRequired -join ',') -cne ($expectedRequired -join ',')) { Add-Issue "$Name must be a non-null closed object with properties [$($expectedProperties -join ',')] and required [$($expectedRequired -join ',')]" } Assert-NoConflictingSchemaKeywords $Schema $Name Assert-AllowedSchemaKeywordsWithExtensions $Schema $Name @('type', 'properties', 'required', 'additionalProperties', 'nullable') $AllowedExtensions } function Assert-PropertyRef([object]$Schema, [string]$SchemaName, [string]$Field, [string]$ExpectedRef) { if (-not $Schema) { return } $property = Get-ExactProperty $Schema.properties $Field if (-not $property) { Add-Issue "$SchemaName missing property: $Field" return } [void](Test-IsPureSchemaRef $property.Value $ExpectedRef "$SchemaName.$Field") } function Assert-SingleEnum([object]$Schema, [string]$Label, [object]$Value) { if (-not $Schema) { return } $values = @($Schema.enum) if (-not (Test-IsJsonArray $Schema.enum) -or $values.Count -ne 1 -or $values[0] -cne $Value) { Add-Issue "$Label must use the single enum value: $Value" } } function Assert-IntegerEnumScalar([object]$Schema, [string]$Label, [int]$Value) { if (-not $Schema) { return } $formatProperty = Get-ExactProperty $Schema 'format' if ([string]$Schema.type -cne 'integer' -or -not (Test-IsNonNullable $Schema) -or ($formatProperty -and [string]$formatProperty.Value -cne 'int32')) { Add-Issue "$Label must be a non-null integer scalar" } Assert-SingleEnum $Schema $Label $Value Assert-NoConflictingSchemaKeywords $Schema $Label @('enum') Assert-AllowedSchemaKeywords $Schema $Label @('type', 'format', 'enum', 'nullable') } function Assert-StringEnumScalar([object]$Schema, [string]$Label, [string[]]$Values) { if (-not $Schema) { return } $actual = @($Schema.enum | Sort-Object) $expected = @($Values | Sort-Object) if ([string]$Schema.type -cne 'string' -or -not (Test-IsNonNullable $Schema) -or -not (Test-IsJsonArray $Schema.enum) -or ($actual -join ',') -cne ($expected -join ',')) { Add-Issue "$Label must be a non-null string enum exactly [$($expected -join ',')]" } Assert-NoConflictingSchemaKeywords $Schema $Label @('enum') Assert-AllowedSchemaKeywords $Schema $Label @('type', 'enum', 'nullable') } function Assert-BoundedStringScalar([object]$Schema, [string]$Label, [int]$Maximum, [string]$Pattern = '') { if (-not $Schema) { return } if ([string]$Schema.type -cne 'string' -or -not (Test-IsNonNullable $Schema) -or -not (Get-ExactProperty $Schema 'minLength') -or [int]$Schema.minLength -ne 1 -or -not (Get-ExactProperty $Schema 'maxLength') -or [int]$Schema.maxLength -ne $Maximum -or ($Pattern -and [string]$Schema.pattern -cne $Pattern)) { Add-Issue "$Label must be a bounded non-null string scalar" } Assert-NoConflictingSchemaKeywords $Schema $Label $allowed = @('type', 'minLength', 'maxLength', 'nullable') if ($Pattern) { $allowed += 'pattern' } Assert-AllowedSchemaKeywords $Schema $Label $allowed } function Assert-SuccessEnvelope([object]$Schema) { Assert-ExactObject $Schema 'RGenerationPoemSetSnapshot' @('code', 'data') @('code', 'data') if (-not $Schema) { return } Assert-IntegerEnumScalar $Schema.properties.code 'RGenerationPoemSetSnapshot.code' 200 Assert-PropertyRef $Schema 'RGenerationPoemSetSnapshot' 'data' '#/components/schemas/GenerationPoemSetSnapshot' } function Assert-FixedError([string]$Name, [int]$Status, [string[]]$BusinessCodes) { $schema = Get-Schema $Name Assert-ExactObject $schema $Name @('code', 'businessCode', 'message') @('code', 'businessCode', 'message') if (-not $schema) { return } $code = $schema.properties.code $businessCode = $schema.properties.businessCode $message = $schema.properties.message Assert-IntegerEnumScalar $code "$Name.code" $Status Assert-StringEnumScalar $businessCode "$Name.businessCode" $BusinessCodes Assert-BoundedStringScalar $message "$Name.message" 200 } function Assert-Union([object]$Schema, [string]$Name, [hashtable]$Branches, [string[]]$AllowedExtensions = @()) { if (-not $Schema) { return } Assert-AllowedObjectKeys $Schema $Name @('discriminator', 'oneOf') @('discriminator', 'oneOf') $AllowedExtensions $discriminatorKeys = if ($Schema.discriminator) { @($Schema.discriminator.PSObject.Properties.Name | Sort-Object) } else { @() } if (($discriminatorKeys -join ',') -cne 'mapping,propertyName' -or -not (Test-IsJsonArray $Schema.oneOf) -or @($Schema.oneOf).Count -ne $Branches.Count -or [string]$Schema.discriminator.propertyName -cne 'businessCode') { Add-Issue "$Name must be a pure businessCode discriminator/oneOf union" } $actualMappingKeys = if ($Schema.discriminator.mapping) { @($Schema.discriminator.mapping.PSObject.Properties.Name | Sort-Object) } else { @() } $expectedMappingKeys = @($Branches.Keys | Sort-Object) if (($actualMappingKeys -join ',') -cne ($expectedMappingKeys -join ',')) { Add-Issue "$Name discriminator mapping keys must be exactly $($expectedMappingKeys -join ',')" } foreach ($entry in $Branches.GetEnumerator()) { $mappingProperty = Get-ExactProperty $Schema.discriminator.mapping ([string]$entry.Key) if (-not $mappingProperty -or [string]$mappingProperty.Value -cne $entry.Value) { Add-Issue "$Name discriminator mapping drifted for $($entry.Key)" } [void](Get-LocalComponentName ([string]$mappingProperty.Value) 'schemas' "$Name discriminator mapping $($entry.Key)") } $actualRefs = @($Schema.oneOf | ForEach-Object { [string]$_.'$ref' } | Sort-Object) $expectedRefs = @($Branches.Values | Sort-Object) if (($actualRefs -join ',') -cne ($expectedRefs -join ',')) { Add-Issue "$Name oneOf refs must be exactly $($expectedRefs -join ',')" } foreach ($branch in @($Schema.oneOf)) { if ($branch) { [void](Test-IsPureSchemaRef $branch ([string]$branch.'$ref') "$Name branch") } } } function Assert-SharedHeaderOwners { $componentsProperty = Get-ExactProperty $document 'components' $headersProperty = if ($componentsProperty) { Get-ExactProperty $componentsProperty.Value 'headers' } else { $null } $headers = if ($headersProperty) { $headersProperty.Value } else { $null } $privateProperty = Get-ExactProperty $headers 'PrivateNoStore' if (-not $privateProperty) { Add-Issue 'missing shared header owner: PrivateNoStore' } else { $header = Resolve-Header $privateProperty.Value 'PrivateNoStore header owner' if ($header) { $values = @($header.schema.enum) if ([string]$header.schema.type -cne 'string' -or -not (Test-IsNonNullable $header.schema) -or -not (Test-IsJsonArray $header.schema.enum) -or $values.Count -ne 1 -or [string]$values[0] -cne 'private, no-store') { Add-Issue 'PrivateNoStore must be a fixed non-null string enum [private, no-store]' } Assert-NoConflictingSchemaKeywords $header.schema 'PrivateNoStore schema' @('enum') Assert-AllowedSchemaKeywords $header.schema 'PrivateNoStore schema' @('type', 'enum', 'nullable') } } $retryProperty = Get-ExactProperty $headers 'RetryAfter' if (-not $retryProperty) { Add-Issue 'missing shared header owner: RetryAfter' } else { $header = Resolve-Header $retryProperty.Value 'RetryAfter header owner' if ($header) { if ([string]$header.schema.type -cne 'integer' -or -not (Test-IsNonNullable $header.schema) -or [int]$header.schema.minimum -ne 1 -or [int]$header.schema.maximum -lt 1 -or [int]$header.schema.maximum -gt 120) { Add-Issue 'RetryAfter must be a non-null integer bounded to 1..120 seconds' } Assert-NoConflictingSchemaKeywords $header.schema 'RetryAfter schema' Assert-AllowedSchemaKeywords $header.schema 'RetryAfter schema' @('type', 'minimum', 'maximum', 'nullable') } } } $openapiProperty = Get-ExactProperty $document 'openapi' if (-not $openapiProperty -or [string]$openapiProperty.Value -cne '3.0.1') { Add-Issue "G12 gate requires the exact OpenAPI 3.0.1 dialect; actual: $($document.openapi)" } if (Get-ExactProperty $document 'webhooks') { Add-Issue 'OpenAPI 3.0.1 must not define top-level webhooks; migrate dialect and owner graph together' } if (-not $SkipParity) { $parityScript = Join-Path $PSScriptRoot 'openapi-yaml-json-parity-runtime-smoke.js' $parityOutput = @(& node $parityScript 2>&1) if ($LASTEXITCODE -ne 0 -or 'OPENAPI-YAML-JSON-PARITY PASS' -notin $parityOutput) { Add-Issue "protected JSON/YAML semantic parity failed: $($parityOutput -join ' | ')" } } Assert-SaTokenOwner Assert-AppGatewayCorsPolicyOwner Assert-SharedHeaderOwners $poemPaths = @($document.paths.PSObject.Properties.Name | Where-Object { $_ -match '^/genealogy/app/genealogies/\{genealogyId\}/generation-poems(?:/|$)' } | Sort-Object) if ($poemPaths.Count -ne 1 -or $poemPaths[0] -cne $poemPath) { Add-Issue "generation-poem APP API must have the sole aggregate path $poemPath; actual: $($poemPaths -join ',')" } foreach ($path in $poemPaths) { if ($path -cne $poemPath) { Add-Issue "legacy generation-poem path must be removed: $path" } } Assert-OnlyMethods $poemPath @('get', 'put') $globalMutationOwners = @(Get-GlobalGenerationPoemMutationOwners) if ($globalMutationOwners.Count -ne 1 -or $globalMutationOwners[0] -cne "PUT $poemPath") { Add-Issue "generation-poem APP mutation must have one global owner PUT $poemPath; actual: $($globalMutationOwners -join ',')" } $globalReadOwners = @(Get-GlobalGenerationPoemReadOwners) if ($globalReadOwners.Count -ne 1 -or $globalReadOwners[0] -cne "GET $poemPath") { Add-Issue "generation-poem APP canonical read must have one global owner GET $poemPath; actual: $($globalReadOwners -join ',')" } $callbackOwners = Get-ComponentSection 'callbacks' foreach ($callbackProperty in @($callbackOwners.PSObject.Properties)) { if (Test-ObjectGraphLooksLikeGenerationPoem $callbackProperty.Value @{} "components.callbacks.$($callbackProperty.Name)" 'callbacks') { Add-Issue "unowned component callback exposes the G12 contract: $($callbackProperty.Name)" } } $legacySchemas = @( 'GenerationPoemBatchBody', 'GenerationPoemBody', 'RGenerationPoem', 'GenerationPoemView', 'RGenerationPoemList', 'RGenerationPoemBatchPreview', 'GenerationPoemBatchPreviewView', 'GenerationPoemBatchItemView' ) foreach ($name in $legacySchemas) { $schemasSection = Get-ComponentSection 'schemas' if (Get-ExactProperty $schemasSection $name) { Add-Issue "legacy generation-poem schema owner must be removed: $name" } } foreach ($name in @('GenerationPoemResult', 'GenerationPoemListResult', 'GenerationPoemBatchPreviewResult')) { $responsesSection = Get-ComponentSection 'responses' if (Get-ExactProperty $responsesSection $name) { Add-Issue "legacy generation-poem response owner must be removed: $name" } } $getOperation = Get-Operation $poemPath 'get' $putOperation = Get-Operation $poemPath 'put' $getOperationExtensions = @( 'x-projection', 'x-sort-order', 'x-cors-required-request-headers', 'x-cors-policy-owner' ) $putOperationExtensions = @( 'x-cas-owner', 'x-revalidates', 'x-candidate-steps', 'x-generation-slot-collision-policy', 'x-swap-declaration-policy', 'x-swap-write-policy', 'x-transaction-failure-policy', 'x-transaction-effects', 'x-semantic-noop', 'x-version-reuse', 'x-outcome-unknown-reconciliation', 'x-unknown-precedence', 'x-unknown-overlap-policy', 'x-unknown-existing-match-fields', 'x-unknown-new-match-fields', 'x-unknown-new-id-policy', 'x-unknown-set-equality', 'x-unknown-version-comparison', 'x-unknown-version-policy', 'x-unknown-target-construction-fields', 'x-unknown-semantic-fields', 'x-cors-required-request-headers', 'x-cors-policy-owner' ) Assert-OperationObject $getOperation "GET $poemPath" $getOperationExtensions $false Assert-OperationObject $putOperation "PUT $poemPath" $putOperationExtensions $true $operationContracts = @( [pscustomobject]@{ Method = 'GET' Operation = $getOperation OperationId = 'appGetGenerationPoemSet' Parameters = @('header:clientid', 'path:genealogyId') Responses = [ordered]@{ '200' = '#/components/schemas/RGenerationPoemSetSnapshot' '400' = '#/components/schemas/RGenerationPoemBadRequest' '401' = '#/components/schemas/RGenerationPoemUnauthorized' '404' = '#/components/schemas/RGenerationPoemNotFound' '429' = '#/components/schemas/RGenerationPoemRateLimited' '500' = '#/components/schemas/RGenerationPoemReadUnavailable' } }, [pscustomobject]@{ Method = 'PUT' Operation = $putOperation OperationId = 'appUpdateGenerationPoemSet' Parameters = @('header:If-Match', 'header:clientid', 'path:genealogyId') Responses = [ordered]@{ '200' = '#/components/schemas/RGenerationPoemSetSnapshot' '400' = '#/components/schemas/RGenerationPoemBadRequest' '401' = '#/components/schemas/RGenerationPoemUnauthorized' '403' = '#/components/schemas/RGenerationPoemForbidden' '404' = '#/components/schemas/RGenerationPoemNotFound' '409' = '#/components/schemas/RGenerationPoemSetConflict' '422' = '#/components/schemas/RGenerationPoemUnprocessable' '429' = '#/components/schemas/RGenerationPoemRateLimited' '500' = '#/components/schemas/RGenerationPoemOutcomeUnknown' } } ) foreach ($contract in $operationContracts) { $label = "$($contract.Method) $poemPath" $operation = $contract.Operation if (-not $operation) { continue } if ([string]$operation.operationId -cne $contract.OperationId) { Add-Issue "$label operationId must be $($contract.OperationId)" } Assert-GlobalOperationId $contract.OperationId $label Assert-SaToken $operation $label $parameters = @(Get-OperationParameters $poemPath $operation $label) Assert-ExactParameters $parameters $label $contract.Parameters $clientid = Get-Parameter $parameters 'clientid' 'header' $label if ($clientid -and (-not (Test-IsJsonBoolean $clientid.required $true) -or [string]$clientid.schema.type -cne 'string' -or -not (Test-IsNonNullable $clientid.schema) -or [int]$clientid.schema.minLength -ne 1 -or [int]$clientid.schema.maxLength -ne 128)) { Add-Issue "$label clientid must be a required non-null string bounded to 1..128" } if ($clientid) { Assert-NoConflictingSchemaKeywords $clientid.schema "$label clientid" Assert-AllowedSchemaKeywords $clientid.schema "$label clientid" @('type', 'minLength', 'maxLength', 'nullable') } $genealogyId = Get-Parameter $parameters 'genealogyId' 'path' $label if ($genealogyId -and -not (Test-IsJsonBoolean $genealogyId.required $true)) { Add-Issue "$label genealogyId must be required" } if ($genealogyId) { [void](Test-IsPureSchemaRef $genealogyId.schema '#/components/schemas/GenealogyId' "$label genealogyId") } Assert-ExactResponseSet $operation $label @($contract.Responses.Keys) foreach ($status in @($contract.Responses.Keys)) { $response = Get-Response $operation $status $label $actualRef = Get-JsonResponseSchemaRef $response "$label $status" if ($actualRef -cne $contract.Responses[$status]) { Add-Issue "$label $status must return $($contract.Responses[$status]); actual: $actualRef" } Assert-ResponseHeaders $response $status "$label $status" } } if ($getOperation) { if (Get-ExactProperty $getOperation 'requestBody') { Add-Issue "GET $poemPath must not define a request body" } foreach ($pattern in @('canView', 'ACTIVE[- ]only', 'generationNo.*ascending', 'non-disclosing.*404')) { if ([string]$getOperation.description -notmatch "(?i)$pattern") { Add-Issue "GET $poemPath description misses: $pattern" } } if ([string]$getOperation.'x-projection' -cne 'ACTIVE_ONLY' -or -not (Test-IsJsonArray $getOperation.'x-sort-order') -or (@($getOperation.'x-sort-order') -join ',') -cne 'generationNo:asc' -or -not (Test-IsJsonArray $getOperation.'x-cors-required-request-headers') -or (@($getOperation.'x-cors-required-request-headers') -join ',') -cne 'Authorization,clientid' -or [string]$getOperation.'x-cors-policy-owner' -cne 'APP_GATEWAY_PREFLIGHT') { Add-Issue "GET $poemPath must machine-bind ACTIVE_ONLY, generationNo ascending, and gateway CORS headers" } } if ($putOperation) { $putParameters = @(Get-OperationParameters $poemPath $putOperation "PUT $poemPath") $ifMatch = Get-Parameter $putParameters 'If-Match' 'header' "PUT $poemPath" if ($ifMatch -and -not (Test-IsJsonBoolean $ifMatch.required $true)) { Add-Issue 'PUT generation-poem If-Match must be required' } if ($ifMatch) { [void](Test-IsPureSchemaRef $ifMatch.schema '#/components/schemas/GenerationPoemSetVersion' 'PUT generation-poem If-Match') } $requestBody = Resolve-RequestBody $putOperation.requestBody 'PUT generation-poem request body' if (-not $requestBody -or -not (Test-IsJsonBoolean $requestBody.required $true)) { Add-Issue 'PUT generation-poem request body must be required' } $requestMedia = if ($requestBody -and $requestBody.content) { @($requestBody.content.PSObject.Properties) } else { @() } if ($requestMedia.Count -ne 1 -or $requestMedia[0].Name -cne 'application/json') { Add-Issue 'PUT generation-poem request body must expose only application/json' } else { Assert-MediaTypeObject $requestMedia[0].Value 'PUT generation-poem request body application/json' [void](Test-IsPureSchemaRef $requestMedia[0].Value.schema '#/components/schemas/AppGenerationPoemSetUpdateBody' 'PUT generation-poem request body') } foreach ($pattern in @( 'canEditContent.*READY.*transaction', 'If-Match.*poemSetVersion', 'full candidate.*before.*write', 'disableMissing=false.*preserve.*unrepresented.*ACTIVE', 'disableMissing=true.*soft-disable.*unrepresented.*ACTIVE', 'never physical.*delete', 'swap.*no observable intermediate', 'semantic no-op.*keep.*version', 'unknown.*fresh GET.*target.*old.*current', 'current.*target.*not.*prove.*request', 'no automatic PUT' )) { if ([string]$putOperation.description -notmatch "(?i)$pattern") { Add-Issue "PUT $poemPath description misses: $pattern" } } $revalidates = @($putOperation.'x-revalidates') $candidateSteps = @($putOperation.'x-candidate-steps') $effects = @($putOperation.'x-transaction-effects') $unknownTargetFields = @($putOperation.'x-unknown-target-construction-fields') $unknownFields = @($putOperation.'x-unknown-semantic-fields') if ([string]$putOperation.'x-cas-owner' -cne 'GenerationPoemSetVersion' -or -not (Test-IsJsonArray $putOperation.'x-revalidates') -or ($revalidates -join ',') -cne 'tenantScope,canEditContent,poemSetVersion,genealogyState' -or -not (Test-IsJsonArray $putOperation.'x-candidate-steps') -or ($candidateSteps -join ',') -cne 'RESOLVE_BASELINE_ACTIVE_IDS,VALIDATE_DECLARED_STRICT_ORDER,BUILD_DECLARED_TARGET,APPLY_DISABLE_MISSING_POLICY,ORDER_MERGED_CANDIDATE_BY_GENERATION_NO,VALIDATE_ID_AND_GENERATION_UNIQUENESS,VALIDATE_GENERATION_SLOT_COLLISIONS,VALIDATE_CONTIGUITY,VALIDATE_FINAL_ACTIVE_CAPACITY,ALLOCATE_UNIQUE_NEW_IDS,VALIDATE_ALLOCATED_IDS_NONEMPTY_UNIQUE_NOT_IN_BASELINE,WRITE_ATOMICALLY' -or [string]$putOperation.'x-generation-slot-collision-policy' -cne 'REJECT_422_ZERO_DOMAIN_WRITE' -or [string]$putOperation.'x-swap-declaration-policy' -cne 'ALL_AFFECTED_ROWS_REQUIRED' -or -not (Test-IsJsonArray $putOperation.'x-transaction-effects') -or ($effects -join ',') -cne 'APPLY_VALIDATED_CANDIDATE_ATOMICALLY,APPLY_DISABLE_MISSING_POLICY,UPDATE_VERSION_ON_SEMANTIC_CHANGE' -or [string]$putOperation.'x-swap-write-policy' -cne 'ATOMIC_NO_OBSERVABLE_INTERMEDIATE_STATE' -or [string]$putOperation.'x-transaction-failure-policy' -cne 'ZERO_DOMAIN_WRITE' -or [string]$putOperation.'x-semantic-noop' -cne 'RETURN_200_KEEP_VERSION' -or [string]$putOperation.'x-version-reuse' -cne 'FORBIDDEN' -or [string]$putOperation.'x-outcome-unknown-reconciliation' -cne 'FRESH_GET_THREE_WAY_NO_AUTO_PUT' -or [string]$putOperation.'x-unknown-precedence' -cne 'CURRENT_EQUALS_TARGET_THEN_CURRENT_EQUALS_OLD_THEN_DIVERGED' -or [string]$putOperation.'x-unknown-overlap-policy' -cne 'CURRENT_EQUALS_TARGET_FIRST_NO_ATTRIBUTION' -or -not (Test-IsJsonArray $putOperation.'x-unknown-existing-match-fields') -or (@($putOperation.'x-unknown-existing-match-fields') -join ',') -cne 'poemId,generationNo,generationText' -or -not (Test-IsJsonArray $putOperation.'x-unknown-new-match-fields') -or (@($putOperation.'x-unknown-new-match-fields') -join ',') -cne 'generationNo,generationText' -or [string]$putOperation.'x-unknown-new-id-policy' -cne 'NON_EMPTY_UNIQUE_NOT_IN_BASELINE' -or [string]$putOperation.'x-unknown-set-equality' -cne 'SAME_CARDINALITY_NO_EXTRA_ACTIVE_ROWS' -or [string]$putOperation.'x-unknown-version-comparison' -cne 'IGNORE_FOR_SEMANTIC_EQUALITY' -or [string]$putOperation.'x-unknown-version-policy' -cne 'CHANGED_TARGET_REQUIRES_FRESH_NONREUSED_VERSION' -or -not (Test-IsJsonArray $putOperation.'x-unknown-target-construction-fields') -or ($unknownTargetFields -join ',') -cne 'genealogyId,baselinePoemSetVersion,baselineOrderedItems,declaredItems,disableMissing' -or -not (Test-IsJsonArray $putOperation.'x-unknown-semantic-fields') -or ($unknownFields -join ',') -cne 'genealogyId,effectiveTargetOrderedItems' -or -not (Test-IsJsonArray $putOperation.'x-cors-required-request-headers') -or (@($putOperation.'x-cors-required-request-headers') -join ',') -cne 'Authorization,Content-Type,If-Match,clientid' -or [string]$putOperation.'x-cors-policy-owner' -cne 'APP_GATEWAY_PREFLIGHT') { Add-Issue 'PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted' } } $genealogyIdOwner = Get-Schema 'GenealogyId' if ($genealogyIdOwner -and ([string]$genealogyIdOwner.type -cne 'string' -or -not (Test-IsNonNullable $genealogyIdOwner) -or [int]$genealogyIdOwner.minLength -ne 1 -or [int]$genealogyIdOwner.maxLength -ne 128 -or [string]$genealogyIdOwner.pattern -cne $identifierPattern)) { Add-Issue 'GenealogyId must be a non-null 1..128 lexical opaque identifier' } if ($genealogyIdOwner) { Assert-NoConflictingSchemaKeywords $genealogyIdOwner 'GenealogyId' Assert-AllowedSchemaKeywords $genealogyIdOwner 'GenealogyId' @('type', 'minLength', 'maxLength', 'pattern', 'nullable') } $poemIdOwner = Get-Schema 'GenerationPoemId' if ($poemIdOwner -and ([string]$poemIdOwner.type -cne 'string' -or -not (Test-IsNonNullable $poemIdOwner) -or [int]$poemIdOwner.minLength -ne 1 -or [int]$poemIdOwner.maxLength -ne 128 -or [string]$poemIdOwner.pattern -cne $identifierPattern -or -not (Test-IsJsonBoolean $poemIdOwner.'x-opaque' $true) -or [string]$poemIdOwner.'x-client-semantics' -cne 'COMPARE_ONLY')) { Add-Issue 'GenerationPoemId must be a non-null opaque lexical identifier used only for equality' } if ($poemIdOwner) { Assert-NoConflictingSchemaKeywords $poemIdOwner 'GenerationPoemId' Assert-AllowedSchemaKeywordsWithExtensions $poemIdOwner 'GenerationPoemId' @('type', 'minLength', 'maxLength', 'pattern', 'nullable') @('x-opaque', 'x-client-semantics') } $versionOwner = Get-Schema 'GenerationPoemSetVersion' if ($versionOwner -and ([string]$versionOwner.type -cne 'string' -or -not (Test-IsNonNullable $versionOwner) -or [int]$versionOwner.minLength -ne 1 -or [int]$versionOwner.maxLength -ne 128 -or [string]$versionOwner.pattern -cne $identifierPattern -or -not (Test-IsJsonBoolean $versionOwner.'x-opaque' $true) -or -not (Test-IsJsonArray $versionOwner.'x-version-scope-fields') -or (@($versionOwner.'x-version-scope-fields') -join ',') -cne 'poemId,generationNo,generationText,activeState' -or [string]$versionOwner.'x-version-change-policy' -cne 'ACTIVE_SEMANTIC_CHANGE_ONLY' -or [string]$versionOwner.'x-version-reuse' -cne 'FORBIDDEN')) { Add-Issue 'GenerationPoemSetVersion must be one opaque, non-reused ACTIVE semantic version owner' } if ($versionOwner) { Assert-NoConflictingSchemaKeywords $versionOwner 'GenerationPoemSetVersion' Assert-AllowedSchemaKeywordsWithExtensions $versionOwner 'GenerationPoemSetVersion' @('type', 'minLength', 'maxLength', 'pattern', 'nullable') @('x-opaque', 'x-version-scope-fields', 'x-version-change-policy', 'x-version-reuse') } $generationNoOwner = Get-Schema 'GenerationPoemGenerationNo' if ($generationNoOwner -and ([string]$generationNoOwner.type -cne 'integer' -or -not (Test-IsNonNullable $generationNoOwner) -or ((Get-ExactProperty $generationNoOwner 'format') -and [string]$generationNoOwner.format -cne 'int32') -or -not (Get-ExactProperty $generationNoOwner 'minimum') -or -not (Get-ExactProperty $generationNoOwner 'maximum') -or [int64]$generationNoOwner.minimum -ne 1 -or [int64]$generationNoOwner.maximum -ne 2147483647)) { Add-Issue 'GenerationPoemGenerationNo must be a non-null safe int32-range JSON integer 1..2147483647 with absent or int32 format' } if ($generationNoOwner) { Assert-NoConflictingSchemaKeywords $generationNoOwner 'GenerationPoemGenerationNo' Assert-AllowedSchemaKeywords $generationNoOwner 'GenerationPoemGenerationNo' @('type', 'format', 'minimum', 'maximum', 'nullable') } $textOwner = Get-Schema 'GenerationPoemText' $forbiddenTextClasses = 'C0,C1,CR,LF,TAB,ARABIC_LETTER_MARK,LTR_RTL_MARK,BIDI_OVERRIDE_OR_ISOLATE,WORD_JOINER,ZERO_WIDTH_OR_BOM,LINE_PARAGRAPH_SEPARATOR,UNPAIRED_SURROGATE' if ($textOwner -and ([string]$textOwner.type -cne 'string' -or -not (Test-IsNonNullable $textOwner) -or [int]$textOwner.minLength -ne 1 -or [int]$textOwner.maxLength -ne 50 -or [string]$textOwner.pattern -cne $generationTextPattern -or [string]$textOwner.'x-normalization' -cne 'NFC' -or [string]$textOwner.'x-length-unit' -cne 'UNICODE_CODE_POINT' -or -not (Test-IsJsonBoolean $textOwner.'x-well-formed-unicode' $true) -or [string]$textOwner.'x-boundary-whitespace' -cne 'REJECT' -or -not (Test-IsJsonArray $textOwner.'x-forbidden-code-point-classes') -or (@($textOwner.'x-forbidden-code-point-classes') -join ',') -cne $forbiddenTextClasses)) { Add-Issue 'GenerationPoemText must be NFC, 1..50 Unicode code points, boundary-clean, and free of control/bidi/zero-width/malformed Unicode' } if ($textOwner) { Assert-NoConflictingSchemaKeywords $textOwner 'GenerationPoemText' Assert-AllowedSchemaKeywordsWithExtensions $textOwner 'GenerationPoemText' @('type', 'minLength', 'maxLength', 'pattern', 'nullable') @('x-normalization', 'x-length-unit', 'x-well-formed-unicode', 'x-boundary-whitespace', 'x-forbidden-code-point-classes') } $requestItem = Get-Schema 'AppGenerationPoemSetItem' Assert-ExactObject $requestItem 'AppGenerationPoemSetItem' @('poemId', 'generationNo', 'generationText') @('generationNo', 'generationText') @('x-existing-id-policy', 'x-new-id-policy', 'x-id-move-policy', 'x-disabled-id-policy') Assert-PropertyRef $requestItem 'AppGenerationPoemSetItem' 'poemId' '#/components/schemas/GenerationPoemId' Assert-PropertyRef $requestItem 'AppGenerationPoemSetItem' 'generationNo' '#/components/schemas/GenerationPoemGenerationNo' Assert-PropertyRef $requestItem 'AppGenerationPoemSetItem' 'generationText' '#/components/schemas/GenerationPoemText' if ($requestItem -and ([string]$requestItem.'x-existing-id-policy' -cne 'CURRENT_BASELINE_ACTIVE_ONLY' -or [string]$requestItem.'x-new-id-policy' -cne 'OMIT_POEM_ID' -or [string]$requestItem.'x-id-move-policy' -cne 'ALLOW_EXPLICIT_TARGET_GENERATION' -or [string]$requestItem.'x-disabled-id-policy' -cne 'ROW_NOT_AVAILABLE')) { Add-Issue 'AppGenerationPoemSetItem identity/new/move/disabled policies drifted' } $responseItem = Get-Schema 'GenerationPoemSetItem' Assert-ExactObject $responseItem 'GenerationPoemSetItem' @('poemId', 'generationNo', 'generationText') @('poemId', 'generationNo', 'generationText') Assert-PropertyRef $responseItem 'GenerationPoemSetItem' 'poemId' '#/components/schemas/GenerationPoemId' Assert-PropertyRef $responseItem 'GenerationPoemSetItem' 'generationNo' '#/components/schemas/GenerationPoemGenerationNo' Assert-PropertyRef $responseItem 'GenerationPoemSetItem' 'generationText' '#/components/schemas/GenerationPoemText' $body = Get-Schema 'AppGenerationPoemSetUpdateBody' Assert-ExactObject $body 'AppGenerationPoemSetUpdateBody' @('items', 'disableMissing') @('items', 'disableMissing') @('x-request-order', 'x-false-policy', 'x-true-policy', 'x-empty-false-policy', 'x-empty-true-policy', 'x-final-active-invariant', 'x-candidate-validation', 'x-duplicate-generation-text') if ($body) { $items = $body.properties.items $disableMissing = $body.properties.disableMissing if ([string]$items.type -cne 'array' -or -not (Test-IsNonNullable $items) -or -not (Get-ExactProperty $items 'minItems') -or [int]$items.minItems -ne 0 -or -not (Get-ExactProperty $items 'maxItems') -or [int]$items.maxItems -ne 500) { Add-Issue 'AppGenerationPoemSetUpdateBody.items must be a non-null array with 0..500 declared rows' } if ($items) { [void](Test-IsPureSchemaRef $items.items '#/components/schemas/AppGenerationPoemSetItem' 'AppGenerationPoemSetUpdateBody.items.items') Assert-NoConflictingSchemaKeywords $items 'AppGenerationPoemSetUpdateBody.items' Assert-AllowedSchemaKeywords $items 'AppGenerationPoemSetUpdateBody.items' @('type', 'items', 'minItems', 'maxItems', 'nullable') } if ([string]$disableMissing.type -cne 'boolean' -or -not (Test-IsNonNullable $disableMissing)) { Add-Issue 'AppGenerationPoemSetUpdateBody.disableMissing must be a required non-null boolean' } if ($disableMissing) { Assert-NoConflictingSchemaKeywords $disableMissing 'AppGenerationPoemSetUpdateBody.disableMissing' Assert-AllowedSchemaKeywords $disableMissing 'AppGenerationPoemSetUpdateBody.disableMissing' @('type', 'nullable') } if ([string]$body.'x-request-order' -cne 'STRICT_GENERATION_NO_ASC_NO_SERVER_SORT' -or [string]$body.'x-false-policy' -cne 'PRESERVE_ALL_UNREPRESENTED_BASELINE_ACTIVE' -or [string]$body.'x-true-policy' -cne 'SOFT_DISABLE_ALL_UNREPRESENTED_BASELINE_ACTIVE' -or [string]$body.'x-empty-false-policy' -cne 'SEMANTIC_NOOP' -or [string]$body.'x-empty-true-policy' -cne 'SOFT_DISABLE_ALL_ACTIVE' -or [string]$body.'x-final-active-invariant' -cne 'EMPTY_OR_CONTIGUOUS_INTERVAL' -or [string]$body.'x-candidate-validation' -cne 'BEFORE_ANY_DOMAIN_WRITE' -or [string]$body.'x-duplicate-generation-text' -cne 'ALLOWED') { Add-Issue 'AppGenerationPoemSetUpdateBody ordering/omission/empty/candidate invariants drifted' } } $snapshot = Get-Schema 'GenerationPoemSetSnapshot' Assert-ExactObject $snapshot 'GenerationPoemSetSnapshot' @('genealogyId', 'poemSetVersion', 'items') @('genealogyId', 'poemSetVersion', 'items') @('x-projection', 'x-order', 'x-active-invariant', 'x-identity-uniqueness') Assert-PropertyRef $snapshot 'GenerationPoemSetSnapshot' 'genealogyId' '#/components/schemas/GenealogyId' Assert-PropertyRef $snapshot 'GenerationPoemSetSnapshot' 'poemSetVersion' '#/components/schemas/GenerationPoemSetVersion' if ($snapshot) { $items = $snapshot.properties.items if ([string]$items.type -cne 'array' -or -not (Test-IsNonNullable $items) -or -not (Get-ExactProperty $items 'minItems') -or [int]$items.minItems -ne 0 -or -not (Get-ExactProperty $items 'maxItems') -or [int]$items.maxItems -ne 500) { Add-Issue 'GenerationPoemSetSnapshot.items must be the non-null 0..500 canonical ACTIVE array' } if ($items) { [void](Test-IsPureSchemaRef $items.items '#/components/schemas/GenerationPoemSetItem' 'GenerationPoemSetSnapshot.items.items') Assert-NoConflictingSchemaKeywords $items 'GenerationPoemSetSnapshot.items' Assert-AllowedSchemaKeywords $items 'GenerationPoemSetSnapshot.items' @('type', 'items', 'minItems', 'maxItems', 'nullable') } if ([string]$snapshot.'x-projection' -cne 'ACTIVE_ONLY' -or [string]$snapshot.'x-order' -cne 'STRICT_GENERATION_NO_ASC' -or [string]$snapshot.'x-active-invariant' -cne 'EMPTY_OR_CONTIGUOUS_INTERVAL' -or [string]$snapshot.'x-identity-uniqueness' -cne 'POEM_ID_AND_GENERATION_NO') { Add-Issue 'GenerationPoemSetSnapshot ACTIVE/order/continuity/identity invariants drifted' } } $successEnvelope = Get-Schema 'RGenerationPoemSetSnapshot' Assert-SuccessEnvelope $successEnvelope Assert-FixedError 'RGenerationPoemBadRequest' 400 @('GENERATION_POEM_REQUEST_INVALID') Assert-FixedError 'RGenerationPoemUnauthorized' 401 @('AUTH_REQUIRED') Assert-FixedError 'RGenerationPoemForbidden' 403 @('GENERATION_POEM_EDIT_FORBIDDEN') Assert-FixedError 'RGenerationPoemNotFound' 404 @('GENEALOGY_NOT_AVAILABLE') Assert-FixedError 'RGenerationPoemRateLimited' 429 @('RATE_LIMITED') Assert-FixedError 'RGenerationPoemReadUnavailable' 500 @('GENERATION_POEM_READ_UNAVAILABLE') Assert-FixedError 'RGenerationPoemOutcomeUnknown' 500 @('GENERATION_POEM_OUTCOME_UNKNOWN') $versionConflict = Get-Schema 'GenerationPoemSetVersionConflict' Assert-ExactObject $versionConflict 'GenerationPoemSetVersionConflict' @('code', 'businessCode', 'message', 'current') @('code', 'businessCode', 'message', 'current') if ($versionConflict) { Assert-IntegerEnumScalar $versionConflict.properties.code 'GenerationPoemSetVersionConflict.code' 409 Assert-StringEnumScalar $versionConflict.properties.businessCode 'GenerationPoemSetVersionConflict.businessCode' @('POEM_SET_VERSION_CONFLICT') Assert-BoundedStringScalar $versionConflict.properties.message 'GenerationPoemSetVersionConflict.message' 200 Assert-PropertyRef $versionConflict 'GenerationPoemSetVersionConflict' 'current' '#/components/schemas/GenerationPoemSetSnapshot' } $notReadyConflict = Get-Schema 'GenerationPoemGenealogyNotReadyConflict' Assert-ExactObject $notReadyConflict 'GenerationPoemGenealogyNotReadyConflict' @('code', 'businessCode', 'message') @('code', 'businessCode', 'message') if ($notReadyConflict) { Assert-IntegerEnumScalar $notReadyConflict.properties.code 'GenerationPoemGenealogyNotReadyConflict.code' 409 Assert-StringEnumScalar $notReadyConflict.properties.businessCode 'GenerationPoemGenealogyNotReadyConflict.businessCode' @('GENEALOGY_NOT_READY') Assert-BoundedStringScalar $notReadyConflict.properties.message 'GenerationPoemGenealogyNotReadyConflict.message' 200 } $conflict = Get-Schema 'RGenerationPoemSetConflict' Assert-Union $conflict 'RGenerationPoemSetConflict' @{ POEM_SET_VERSION_CONFLICT = '#/components/schemas/GenerationPoemSetVersionConflict' GENEALOGY_NOT_READY = '#/components/schemas/GenerationPoemGenealogyNotReadyConflict' } @('x-conflict-precedence') if ($conflict -and (-not (Test-IsJsonArray $conflict.'x-conflict-precedence') -or (@($conflict.'x-conflict-precedence') -join ',') -cne 'VERSION,READY')) { Add-Issue 'RGenerationPoemSetConflict precedence must be VERSION then READY after authorization' } $fieldError = Get-Schema 'GenerationPoemFieldError' Assert-ExactObject $fieldError 'GenerationPoemFieldError' @('path', 'message') @('path', 'message') if ($fieldError) { $path = $fieldError.properties.path $message = $fieldError.properties.message $fieldPathPattern = '^(items|items\[(?:[0-9]|[1-9][0-9]|[1-4][0-9]{2})\]\.(poemId|generationNo|generationText)|disableMissing)$' if ([string]$path.type -cne 'string' -or -not (Test-IsNonNullable $path) -or [int]$path.minLength -ne 1 -or [int]$path.maxLength -ne 128 -or [string]$path.pattern -cne $fieldPathPattern) { Add-Issue 'GenerationPoemFieldError.path must identify only the closed request fields' } Assert-NoConflictingSchemaKeywords $path 'GenerationPoemFieldError.path' Assert-AllowedSchemaKeywords $path 'GenerationPoemFieldError.path' @('type', 'minLength', 'maxLength', 'pattern', 'nullable') Assert-BoundedStringScalar $message 'GenerationPoemFieldError.message' 200 } $unprocessable = Get-Schema 'RGenerationPoemUnprocessable' Assert-ExactObject $unprocessable 'RGenerationPoemUnprocessable' @('code', 'businessCode', 'message', 'fieldErrors') @('code', 'businessCode', 'message', 'fieldErrors') if ($unprocessable) { Assert-IntegerEnumScalar $unprocessable.properties.code 'RGenerationPoemUnprocessable.code' 422 $expectedCodes = @( 'DUPLICATE_GENERATION_NO', 'DUPLICATE_POEM_ID', 'GENERATION_POEM_CAPACITY_EXCEEDED', 'GENERATION_POEM_REQUEST_ORDER_INVALID', 'GENERATION_POEM_TEXT_INVALID', 'GENERATION_SLOT_CONFLICT', 'NON_CONTIGUOUS_GENERATIONS', 'POEM_ROW_NOT_AVAILABLE' ) Assert-StringEnumScalar $unprocessable.properties.businessCode 'RGenerationPoemUnprocessable.businessCode' $expectedCodes Assert-BoundedStringScalar $unprocessable.properties.message 'RGenerationPoemUnprocessable.message' 200 $fieldErrors = $unprocessable.properties.fieldErrors if ([string]$fieldErrors.type -cne 'array' -or -not (Test-IsNonNullable $fieldErrors) -or -not (Get-ExactProperty $fieldErrors 'minItems') -or [int]$fieldErrors.minItems -ne 0 -or -not (Get-ExactProperty $fieldErrors 'maxItems') -or [int]$fieldErrors.maxItems -ne 500) { Add-Issue 'RGenerationPoemUnprocessable.fieldErrors must be a bounded non-null array' } if ($fieldErrors) { [void](Test-IsPureSchemaRef $fieldErrors.items '#/components/schemas/GenerationPoemFieldError' 'RGenerationPoemUnprocessable.fieldErrors.items') Assert-NoConflictingSchemaKeywords $fieldErrors 'RGenerationPoemUnprocessable.fieldErrors' Assert-AllowedSchemaKeywords $fieldErrors 'RGenerationPoemUnprocessable.fieldErrors' @('type', 'items', 'minItems', 'maxItems', 'nullable') } } if ($ReturnIssues) { return [string[]]@($issues) } if ($issues.Count -gt 0) { Write-Output 'G12-GENERATION-POEM-OPENAPI-CONTRACT BLOCKED' Write-Output "Issues: $($issues.Count)" $issues | ForEach-Object { Write-Output "- $_" } Write-Output '- Keep G12 on its explicit local preview until this protected OpenAPI gate passes from one backend JSON/YAML export.' Write-Output '- After OpenAPI passes, start with executable production normalizer/coordinator tests; do not add a placeholder client gate.' exit 1 } Write-Output 'G12-GENERATION-POEM-OPENAPI-CONTRACT PASS'