Files
jiapuapp/tests/g12-generation-poem-openapi-contract.ps1
T
2026-07-23 17:21:33 +08:00

1400 lines
70 KiB
PowerShell

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) + '/(?<name>[^/]+)$'
$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/(?<section>schemas|parameters|headers|requestBodies|responses|callbacks|securitySchemes)/(?<name>[^/]+)$')
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'