$ErrorActionPreference = 'Stop' $gatePath = Join-Path $PSScriptRoot 'g12-generation-poem-openapi-contract.ps1' function New-SeedDocument { return New-CompletePassingDocument } function Copy-Document([object]$Value) { return $Value | ConvertTo-Json -Depth 100 -Compress | ConvertFrom-Json } function Set-Property([object]$Object, [string]$Name, [object]$Value) { $Object | Add-Member -MemberType NoteProperty -Name $Name -Value $Value -Force } function Rename-Property([object]$Object, [string]$OldName, [string]$NewName) { $property = @($Object.PSObject.Properties | Where-Object { $_.Name -ceq $OldName }) if ($property.Count -ne 1) { throw "cannot rename missing exact property: $OldName" } $value = $property[0].Value $Object.PSObject.Properties.Remove($OldName) Set-Property $Object $NewName $value } function Get-Issues([object]$Document) { return @(& $gatePath -InputDocument $Document -SkipParity -ReturnIssues) } function Assert-MutationRejected { param( [string]$Name, [scriptblock]$Setup, [scriptblock]$Mutate, [string]$Pattern ) $baselineDocument = New-SeedDocument & $Setup $baselineDocument $before = @(Get-Issues $baselineDocument) if ($before.Count -ne 0) { throw "$Name setup must have zero issues: $($before -join ' | ')" } $mutant = Copy-Document $baselineDocument & $Mutate $mutant $after = @(Get-Issues $mutant) $newIssues = @($after | Where-Object { $_ -notin $before }) if ($after.Count -le $before.Count -or -not ($newIssues | Where-Object { $_ -match $Pattern })) { throw "$Name fake-green. New issues: $($newIssues -join ' | ')" } } function Assert-LocalBaselineAccepted { param( [string]$Name, [scriptblock]$Setup, [string]$ForbiddenPattern ) $document = New-SeedDocument & $Setup $document $issues = @(Get-Issues $document) if ($issues.Count -ne 0 -or $issues | Where-Object { $_ -match $ForbiddenPattern }) { throw "$Name false-positive: $($issues -join ' | ')" } } function New-SecurityRequirement { return @([pscustomobject]@{ SaToken = @() }) } function New-MinimalOperation([string]$OperationId, [string]$Description) { return [pscustomobject]@{ description = $Description operationId = $OperationId parameters = @() responses = [pscustomobject]@{} security = @((New-SecurityRequirement)) } } function Add-ShadowMutation([object]$Document, [string]$Path, [object]$Schema) { $operation = New-MinimalOperation 'shadowMutation' 'unrelated shadow mutation' Set-Property $operation 'requestBody' ([pscustomobject]@{ required = $true content = [pscustomobject]@{ 'application/json' = [pscustomobject]@{ schema = $Schema } } }) Set-Property $Document.paths $Path ([pscustomobject]@{ put = $operation }) } function Add-TargetGetWithValidExtensions([object]$Document) { $operation = New-MinimalOperation 'appGetGenerationPoemSet' 'canView ACTIVE-only generationNo ascending non-disclosing 404' Set-Property $operation 'x-projection' 'ACTIVE_ONLY' Set-Property $operation 'x-sort-order' @('generationNo:asc') Set-Property $operation 'x-cors-required-request-headers' @('Authorization', 'clientid') Set-Property $operation 'x-cors-policy-owner' 'APP_GATEWAY_PREFLIGHT' $pathName = '/genealogy/app/genealogies/{genealogyId}/generation-poems' $pathProperty = $Document.paths.PSObject.Properties[$pathName] if ($pathProperty) { Set-Property $pathProperty.Value 'get' $operation } else { Set-Property $Document.paths $pathName ([pscustomobject]@{ get = $operation }) } } function Add-TargetPutWithValidExtensions([object]$Document) { $operation = New-MinimalOperation 'appUpdateGenerationPoemSet' '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' Set-Property $operation 'requestBody' ([pscustomobject]@{ required = $true; content = [pscustomobject]@{} }) $values = [ordered]@{ 'x-cas-owner' = 'GenerationPoemSetVersion' 'x-revalidates' = @('tenantScope', 'canEditContent', 'poemSetVersion', 'genealogyState') 'x-candidate-steps' = @('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') 'x-generation-slot-collision-policy' = 'REJECT_422_ZERO_DOMAIN_WRITE' 'x-swap-declaration-policy' = 'ALL_AFFECTED_ROWS_REQUIRED' 'x-swap-write-policy' = 'ATOMIC_NO_OBSERVABLE_INTERMEDIATE_STATE' 'x-transaction-failure-policy' = 'ZERO_DOMAIN_WRITE' 'x-transaction-effects' = @('APPLY_VALIDATED_CANDIDATE_ATOMICALLY', 'APPLY_DISABLE_MISSING_POLICY', 'UPDATE_VERSION_ON_SEMANTIC_CHANGE') 'x-semantic-noop' = 'RETURN_200_KEEP_VERSION' 'x-version-reuse' = 'FORBIDDEN' 'x-outcome-unknown-reconciliation' = 'FRESH_GET_THREE_WAY_NO_AUTO_PUT' 'x-unknown-precedence' = 'CURRENT_EQUALS_TARGET_THEN_CURRENT_EQUALS_OLD_THEN_DIVERGED' 'x-unknown-overlap-policy' = 'CURRENT_EQUALS_TARGET_FIRST_NO_ATTRIBUTION' 'x-unknown-existing-match-fields' = @('poemId', 'generationNo', 'generationText') 'x-unknown-new-match-fields' = @('generationNo', 'generationText') 'x-unknown-new-id-policy' = 'NON_EMPTY_UNIQUE_NOT_IN_BASELINE' 'x-unknown-set-equality' = 'SAME_CARDINALITY_NO_EXTRA_ACTIVE_ROWS' 'x-unknown-version-comparison' = 'IGNORE_FOR_SEMANTIC_EQUALITY' 'x-unknown-version-policy' = 'CHANGED_TARGET_REQUIRES_FRESH_NONREUSED_VERSION' 'x-unknown-target-construction-fields' = @('genealogyId', 'baselinePoemSetVersion', 'baselineOrderedItems', 'declaredItems', 'disableMissing') 'x-unknown-semantic-fields' = @('genealogyId', 'effectiveTargetOrderedItems') 'x-cors-required-request-headers' = @('Authorization', 'Content-Type', 'If-Match', 'clientid') 'x-cors-policy-owner' = 'APP_GATEWAY_PREFLIGHT' } foreach ($entry in $values.GetEnumerator()) { Set-Property $operation $entry.Key $entry.Value } $pathName = '/genealogy/app/genealogies/{genealogyId}/generation-poems' $pathProperty = $Document.paths.PSObject.Properties[$pathName] if ($pathProperty) { Set-Property $pathProperty.Value 'put' $operation } else { Set-Property $Document.paths $pathName ([pscustomobject]@{ put = $operation }) } } function New-Ref([string]$Ref) { return [pscustomobject]@{ '$ref' = $Ref } } function New-FixedErrorSchema([int]$Status, [string]$BusinessCode) { return [pscustomobject]@{ type = 'object' nullable = $false additionalProperties = $false required = @('code', 'businessCode', 'message') properties = [pscustomobject]@{ code = [pscustomobject]@{ type = 'integer'; format = 'int32'; enum = @($Status); nullable = $false } businessCode = [pscustomobject]@{ type = 'string'; enum = @($BusinessCode); nullable = $false } message = [pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 200; nullable = $false } } } } function New-ApiResponse([string]$SchemaRef, [bool]$RetryAfter) { $headers = [pscustomobject]@{ 'Cache-Control' = (New-Ref '#/components/headers/PrivateNoStore') } if ($RetryAfter) { Set-Property $headers 'Retry-After' (New-Ref '#/components/headers/RetryAfter') } return [pscustomobject]@{ description = 'typed response' headers = $headers content = [pscustomobject]@{ 'application/json' = [pscustomobject]@{ schema = (New-Ref $SchemaRef) } } } } function New-ClientIdParameter { return [pscustomobject]@{ name = 'clientid' in = 'header' required = $true description = 'client instance' schema = [pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 128; nullable = $false } } } function New-GenealogyIdParameter { return [pscustomobject]@{ name = 'genealogyId' in = 'path' required = $true description = 'genealogy identity' schema = (New-Ref '#/components/schemas/GenealogyId') } } function New-IfMatchParameter { return [pscustomobject]@{ name = 'If-Match' in = 'header' required = $true description = 'poem set version' schema = (New-Ref '#/components/schemas/GenerationPoemSetVersion') } } function New-CompletePassingDocument { $identifierPattern = '^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$' $textPattern = '^(?!.*[\u0000-\u001F\u007F-\u009F\u061C\u200B-\u200F\u2028-\u202E\u2060\u2066-\u2069\uFEFF])\S(?:[\s\S]*\S)?$' $fieldPathPattern = '^(items|items\[(?:[0-9]|[1-9][0-9]|[1-4][0-9]{2})\]\.(poemId|generationNo|generationText)|disableMissing)$' $schemas = [pscustomobject]@{ GenealogyId = [pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 128; pattern = $identifierPattern; nullable = $false } GenerationPoemId = [pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 128; pattern = $identifierPattern; nullable = $false 'x-opaque' = $true 'x-client-semantics' = 'COMPARE_ONLY' } GenerationPoemSetVersion = [pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 128; pattern = $identifierPattern; nullable = $false 'x-opaque' = $true 'x-version-scope-fields' = @('poemId', 'generationNo', 'generationText', 'activeState') 'x-version-change-policy' = 'ACTIVE_SEMANTIC_CHANGE_ONLY' 'x-version-reuse' = 'FORBIDDEN' } GenerationPoemGenerationNo = [pscustomobject]@{ type = 'integer'; format = 'int32'; minimum = 1; maximum = 2147483647; nullable = $false } GenerationPoemText = [pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 50; pattern = $textPattern; nullable = $false 'x-normalization' = 'NFC' 'x-length-unit' = 'UNICODE_CODE_POINT' 'x-well-formed-unicode' = $true 'x-boundary-whitespace' = 'REJECT' 'x-forbidden-code-point-classes' = @('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') } AppGenerationPoemSetItem = [pscustomobject]@{ type = 'object'; nullable = $false; additionalProperties = $false required = @('generationNo', 'generationText') properties = [pscustomobject]@{ poemId = (New-Ref '#/components/schemas/GenerationPoemId') generationNo = (New-Ref '#/components/schemas/GenerationPoemGenerationNo') generationText = (New-Ref '#/components/schemas/GenerationPoemText') } 'x-existing-id-policy' = 'CURRENT_BASELINE_ACTIVE_ONLY' 'x-new-id-policy' = 'OMIT_POEM_ID' 'x-id-move-policy' = 'ALLOW_EXPLICIT_TARGET_GENERATION' 'x-disabled-id-policy' = 'ROW_NOT_AVAILABLE' } GenerationPoemSetItem = [pscustomobject]@{ type = 'object'; nullable = $false; additionalProperties = $false required = @('poemId', 'generationNo', 'generationText') properties = [pscustomobject]@{ poemId = (New-Ref '#/components/schemas/GenerationPoemId') generationNo = (New-Ref '#/components/schemas/GenerationPoemGenerationNo') generationText = (New-Ref '#/components/schemas/GenerationPoemText') } } AppGenerationPoemSetUpdateBody = [pscustomobject]@{ type = 'object'; nullable = $false; additionalProperties = $false required = @('items', 'disableMissing') properties = [pscustomobject]@{ items = [pscustomobject]@{ type = 'array'; minItems = 0; maxItems = 500; nullable = $false items = (New-Ref '#/components/schemas/AppGenerationPoemSetItem') } disableMissing = [pscustomobject]@{ type = 'boolean'; nullable = $false } } 'x-request-order' = 'STRICT_GENERATION_NO_ASC_NO_SERVER_SORT' 'x-false-policy' = 'PRESERVE_ALL_UNREPRESENTED_BASELINE_ACTIVE' 'x-true-policy' = 'SOFT_DISABLE_ALL_UNREPRESENTED_BASELINE_ACTIVE' 'x-empty-false-policy' = 'SEMANTIC_NOOP' 'x-empty-true-policy' = 'SOFT_DISABLE_ALL_ACTIVE' 'x-final-active-invariant' = 'EMPTY_OR_CONTIGUOUS_INTERVAL' 'x-candidate-validation' = 'BEFORE_ANY_DOMAIN_WRITE' 'x-duplicate-generation-text' = 'ALLOWED' } GenerationPoemSetSnapshot = [pscustomobject]@{ type = 'object'; nullable = $false; additionalProperties = $false required = @('genealogyId', 'poemSetVersion', 'items') properties = [pscustomobject]@{ genealogyId = (New-Ref '#/components/schemas/GenealogyId') poemSetVersion = (New-Ref '#/components/schemas/GenerationPoemSetVersion') items = [pscustomobject]@{ type = 'array'; minItems = 0; maxItems = 500; nullable = $false items = (New-Ref '#/components/schemas/GenerationPoemSetItem') } } 'x-projection' = 'ACTIVE_ONLY' 'x-order' = 'STRICT_GENERATION_NO_ASC' 'x-active-invariant' = 'EMPTY_OR_CONTIGUOUS_INTERVAL' 'x-identity-uniqueness' = 'POEM_ID_AND_GENERATION_NO' } RGenerationPoemSetSnapshot = [pscustomobject]@{ type = 'object'; nullable = $false; additionalProperties = $false required = @('code', 'data') properties = [pscustomobject]@{ code = [pscustomobject]@{ type = 'integer'; format = 'int32'; enum = @(200); nullable = $false } data = (New-Ref '#/components/schemas/GenerationPoemSetSnapshot') } } RGenerationPoemBadRequest = (New-FixedErrorSchema 400 'GENERATION_POEM_REQUEST_INVALID') RGenerationPoemUnauthorized = (New-FixedErrorSchema 401 'AUTH_REQUIRED') RGenerationPoemForbidden = (New-FixedErrorSchema 403 'GENERATION_POEM_EDIT_FORBIDDEN') RGenerationPoemNotFound = (New-FixedErrorSchema 404 'GENEALOGY_NOT_AVAILABLE') RGenerationPoemRateLimited = (New-FixedErrorSchema 429 'RATE_LIMITED') RGenerationPoemReadUnavailable = (New-FixedErrorSchema 500 'GENERATION_POEM_READ_UNAVAILABLE') RGenerationPoemOutcomeUnknown = (New-FixedErrorSchema 500 'GENERATION_POEM_OUTCOME_UNKNOWN') GenerationPoemSetVersionConflict = [pscustomobject]@{ type = 'object'; nullable = $false; additionalProperties = $false required = @('code', 'businessCode', 'message', 'current') properties = [pscustomobject]@{ code = [pscustomobject]@{ type = 'integer'; format = 'int32'; enum = @(409); nullable = $false } businessCode = [pscustomobject]@{ type = 'string'; enum = @('POEM_SET_VERSION_CONFLICT'); nullable = $false } message = [pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 200; nullable = $false } current = (New-Ref '#/components/schemas/GenerationPoemSetSnapshot') } } GenerationPoemGenealogyNotReadyConflict = [pscustomobject]@{ type = 'object'; nullable = $false; additionalProperties = $false required = @('code', 'businessCode', 'message') properties = [pscustomobject]@{ code = [pscustomobject]@{ type = 'integer'; format = 'int32'; enum = @(409); nullable = $false } businessCode = [pscustomobject]@{ type = 'string'; enum = @('GENEALOGY_NOT_READY'); nullable = $false } message = [pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 200; nullable = $false } } } RGenerationPoemSetConflict = [pscustomobject]@{ oneOf = @( (New-Ref '#/components/schemas/GenerationPoemSetVersionConflict'), (New-Ref '#/components/schemas/GenerationPoemGenealogyNotReadyConflict') ) discriminator = [pscustomobject]@{ propertyName = 'businessCode' mapping = [pscustomobject]@{ POEM_SET_VERSION_CONFLICT = '#/components/schemas/GenerationPoemSetVersionConflict' GENEALOGY_NOT_READY = '#/components/schemas/GenerationPoemGenealogyNotReadyConflict' } } 'x-conflict-precedence' = @('VERSION', 'READY') } GenerationPoemFieldError = [pscustomobject]@{ type = 'object'; nullable = $false; additionalProperties = $false required = @('path', 'message') properties = [pscustomobject]@{ path = [pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 128; pattern = $fieldPathPattern; nullable = $false } message = [pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 200; nullable = $false } } } RGenerationPoemUnprocessable = [pscustomobject]@{ type = 'object'; nullable = $false; additionalProperties = $false required = @('code', 'businessCode', 'message', 'fieldErrors') properties = [pscustomobject]@{ code = [pscustomobject]@{ type = 'integer'; format = 'int32'; enum = @(422); nullable = $false } businessCode = [pscustomobject]@{ type = 'string'; nullable = $false enum = @('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') } message = [pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 200; nullable = $false } fieldErrors = [pscustomobject]@{ type = 'array'; minItems = 0; maxItems = 500; nullable = $false items = (New-Ref '#/components/schemas/GenerationPoemFieldError') } } } } $document = [pscustomobject]@{ openapi = '3.0.1' 'x-app-gateway-policies' = [pscustomobject]@{ APP_GATEWAY_PREFLIGHT = [pscustomobject]@{ allowedRequestHeaders = @('Authorization', 'Content-Type', 'If-Match', 'clientid') allowedMethods = @('GET', 'PUT', 'OPTIONS') originPolicy = 'EXPLICIT_DEPLOYMENT_ALLOWLIST' maxAgeSeconds = 600 } } paths = [pscustomobject]@{} components = [pscustomobject]@{ schemas = $schemas parameters = [pscustomobject]@{} requestBodies = [pscustomobject]@{} responses = [pscustomobject]@{} callbacks = [pscustomobject]@{} headers = [pscustomobject]@{ PrivateNoStore = [pscustomobject]@{ schema = [pscustomobject]@{ type = 'string'; enum = @('private, no-store'); nullable = $false } } RetryAfter = [pscustomobject]@{ schema = [pscustomobject]@{ type = 'integer'; minimum = 1; maximum = 120; nullable = $false } } } securitySchemes = [pscustomobject]@{ SaToken = [pscustomobject]@{ type = 'apiKey'; in = 'header'; name = 'Authorization' } } } } Add-TargetGetWithValidExtensions $document Add-TargetPutWithValidExtensions $document $pathItem = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems' $get = $pathItem.get $put = $pathItem.put Set-Property $get 'deprecated' $false Set-Property $put 'deprecated' $false $get.parameters = @((New-ClientIdParameter), (New-GenealogyIdParameter)) $put.parameters = @((New-IfMatchParameter), (New-ClientIdParameter), (New-GenealogyIdParameter)) $put.requestBody = [pscustomobject]@{ required = $true content = [pscustomobject]@{ 'application/json' = [pscustomobject]@{ schema = (New-Ref '#/components/schemas/AppGenerationPoemSetUpdateBody') } } } $getResponses = [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' } $putResponses = [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 ($entry in $getResponses.GetEnumerator()) { Set-Property $get.responses $entry.Key (New-ApiResponse $entry.Value ($entry.Key -eq '429')) } foreach ($entry in $putResponses.GetEnumerator()) { Set-Property $put.responses $entry.Key (New-ApiResponse $entry.Value ($entry.Key -eq '429')) } return $document } $passingSeedIssues = @(Get-Issues (New-SeedDocument)) if ($passingSeedIssues.Count -ne 0) { throw "complete G12 passing seed must have zero issues: $($passingSeedIssues -join ' | ')" } $noSetup = { param($document) } Assert-MutationRejected 'Path Item parameters keyword casing' { param($document) $pathItem = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems' $genealogyId = @($pathItem.get.parameters | Where-Object { $_.name -ceq 'genealogyId' })[0] Set-Property $pathItem 'parameters' @($genealogyId) $pathItem.get.parameters = @($pathItem.get.parameters | Where-Object { $_.name -cne 'genealogyId' }) $pathItem.put.parameters = @($pathItem.put.parameters | Where-Object { $_.name -cne 'genealogyId' }) } { param($document) $pathItem = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems' Rename-Property $pathItem 'parameters' 'Parameters' } 'Path Item.*non-canonical object keyword casing: Parameters' Assert-MutationRejected 'second write through allOf' $noSetup { param($document) Add-ShadowMutation $document '/genealogy/app/poems-shadow' ([pscustomobject]@{ allOf = @([pscustomobject]@{ '$ref' = '#/components/schemas/AppGenerationPoemSetUpdateBody' }) }) } 'generation-poem APP mutation.*poems-shadow' Assert-MutationRejected 'second write through two-hop alias' $noSetup { param($document) Set-Property $document.components.schemas 'ShadowAliasA' (New-Ref '#/components/schemas/ShadowAliasB') Set-Property $document.components.schemas 'ShadowAliasB' ([pscustomobject]@{ allOf = @((New-Ref '#/components/schemas/AppGenerationPoemSetUpdateBody')) }) Add-ShadowMutation $document '/genealogy/app/two-hop-shadow' (New-Ref '#/components/schemas/ShadowAliasA') } 'generation-poem APP mutation.*two-hop-shadow' Assert-MutationRejected 'two-hop external schema ref' $noSetup { param($document) Set-Property $document.components.schemas 'ExternalAliasA' (New-Ref '#/components/schemas/ExternalAliasB') Set-Property $document.components.schemas 'ExternalAliasB' (New-Ref 'https://evil.test/openapi.json#/components/schemas/Foo') Add-ShadowMutation $document '/genealogy/app/two-hop-external' (New-Ref '#/components/schemas/ExternalAliasA') } 'two-hop-external.*uninspectable external' Assert-MutationRejected 'cycle plus target remains inspectable' $noSetup { param($document) Set-Property $document.components.schemas 'CycleAlias' ([pscustomobject]@{ allOf = @( (New-Ref '#/components/schemas/AppGenerationPoemSetUpdateBody'), (New-Ref '#/components/schemas/CycleAlias') ) }) Add-ShadowMutation $document '/genealogy/app/cycle-shadow' (New-Ref '#/components/schemas/CycleAlias') } 'generation-poem APP mutation.*cycle-shadow' Assert-MutationRejected 'external generic schema ref' $noSetup { param($document) Add-ShadowMutation $document '/genealogy/app/external-shadow' ([pscustomobject]@{ '$ref' = 'https://evil.test/openapi.json#/components/schemas/Foo' }) } 'external-shadow.*uninspectable external' Assert-MutationRejected 'wrong-section generic schema ref' $noSetup { param($document) Add-ShadowMutation $document '/genealogy/app/wrong-section-shadow' ([pscustomobject]@{ '$ref' = '#/components/responses/Foo' }) } 'wrong-section-shadow.*wrong-section ref' Assert-MutationRejected 'no-body lexical delete' $noSetup { param($document) $operation = New-MinimalOperation 'deleteGenerationPoemShadow' 'delete a generation poem shadow row' Set-Property $document.paths '/genealogy/app/poems/{poemId}' ([pscustomobject]@{ delete = $operation }) } 'generation-poem APP mutation.*DELETE /genealogy/app/poems/\{poemId\}' Assert-MutationRejected 'read owner hidden in 206' $noSetup { param($document) $operation = New-MinimalOperation 'shadowRead' 'unrelated read' Set-Property $operation.responses '206' ([pscustomobject]@{ description = 'partial' headers = [pscustomobject]@{} content = [pscustomobject]@{ 'application/json' = [pscustomobject]@{ schema = [pscustomobject]@{ type = 'object' properties = [pscustomobject]@{ genealogyId = [pscustomobject]@{}; poemSetVersion = [pscustomobject]@{}; items = [pscustomobject]@{} } } } } }) Set-Property $document.paths '/genealogy/app/shadow-read' ([pscustomobject]@{ get = $operation }) } 'canonical read.*shadow-read' Assert-MutationRejected 'path item ref' $noSetup { param($document) Set-Property $document.paths '/genealogy/app/path-item-shadow' ([pscustomobject]@{ '$ref' = 'https://evil.test/path-item.yaml' }) } 'Path Item.*path-item-shadow.*must not use' Assert-MutationRejected 'HEAD canonical read owner' $noSetup { param($document) $operation = New-MinimalOperation 'shadowHead' 'head response' Set-Property $operation.responses '200' (New-ApiResponse '#/components/schemas/RGenerationPoemSetSnapshot' $false) Set-Property $document.paths '/genealogy/app/head-shadow' ([pscustomobject]@{ head = $operation }) } 'canonical read.*HEAD /genealogy/app/head-shadow' Assert-MutationRejected 'OPTIONS G12 surface' $noSetup { param($document) $operation = New-MinimalOperation 'shadowOptions' 'options response' Set-Property $operation 'requestBody' ([pscustomobject]@{ required = $true content = [pscustomobject]@{ 'application/json' = [pscustomobject]@{ schema = (New-Ref '#/components/schemas/AppGenerationPoemSetUpdateBody') } } }) Set-Property $document.paths '/genealogy/app/options-shadow' ([pscustomobject]@{ options = $operation }) } 'OPTIONS /genealogy/app/options-shadow is a forbidden G12 contract surface' Assert-MutationRejected 'TRACE G12 surface' $noSetup { param($document) $operation = New-MinimalOperation 'traceGenerationPoemShadow' 'trace response' Set-Property $document.paths '/genealogy/app/trace-shadow' ([pscustomobject]@{ trace = $operation }) } 'TRACE /genealogy/app/trace-shadow is a forbidden G12 contract surface' Assert-MutationRejected 'requestBody ref sibling' { param($document) Set-Property $document.components.requestBodies 'PoemBodyAlias' ([pscustomobject]@{ required = $true content = [pscustomobject]@{ 'application/json' = [pscustomobject]@{ schema = [pscustomobject]@{ '$ref' = '#/components/schemas/AppGenerationPoemSetUpdateBody' } } } }) } { param($document) $operation = New-MinimalOperation 'shadowRefSibling' 'unrelated write' Set-Property $operation 'requestBody' ([pscustomobject]@{ '$ref' = '#/components/requestBodies/PoemBodyAlias'; description = 'illegal sibling' }) Set-Property $document.paths '/genealogy/app/ref-sibling-shadow' ([pscustomobject]@{ put = $operation }) } 'ref-sibling-shadow.*Reference Object must contain only' Assert-MutationRejected 'parameter ref sibling' { param($document) Set-Property $document.components.parameters 'GenealogyIdPath' (New-GenealogyIdParameter) $get = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get $get.parameters[1] = New-Ref '#/components/parameters/GenealogyIdPath' } { param($document) $ref = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get.parameters[1] Set-Property $ref 'description' 'illegal sibling' } 'parameter OpenAPI 3.0.1 Reference Object must contain only' Assert-MutationRejected 'response ref sibling' { param($document) $get = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get Set-Property $document.components.responses 'G12GetSuccessResponse' $get.responses.'200' $get.responses.'200' = New-Ref '#/components/responses/G12GetSuccessResponse' } { param($document) $ref = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get.responses.'200' Set-Property $ref 'description' 'illegal sibling' } '200 OpenAPI 3.0.1 Reference Object must contain only' Assert-MutationRejected 'header ref sibling' $noSetup { param($document) $ref = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get.responses.'200'.headers.'Cache-Control' Set-Property $ref 'description' 'illegal sibling' } 'Cache-Control must be the sole exact local PrivateNoStore Reference Object' Assert-MutationRejected 'MediaType example conflict' $noSetup { param($document) $media = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put.requestBody.content.'application/json' Set-Property $media 'example' ([pscustomobject]@{}) Set-Property $media 'examples' ([pscustomobject]@{}) } 'MediaType Object must not define both example and examples' Assert-MutationRejected 'SaToken drift' $noSetup { param($document) $document.components.securitySchemes.SaToken.type = 'oauth2' } 'SaToken must be the header apiKey' Assert-MutationRejected 'SaToken nonempty scopes' $noSetup { param($document) $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get.security[0].SaToken = @('read') } 'must require exactly one SaToken security alternative' Assert-MutationRejected 'security object instead of array' $noSetup { param($document) $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get.security = [pscustomobject]@{ SaToken = @() } } 'must require exactly one SaToken security alternative' Assert-MutationRejected 'SaToken requirement key casing' $noSetup { param($document) $requirement = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get.security[0] Rename-Property $requirement 'SaToken' 'satoken' } 'must require exactly one SaToken security alternative' Assert-MutationRejected 'top-level gateway policy key casing' $noSetup { param($document) Rename-Property $document 'x-app-gateway-policies' 'X-App-Gateway-Policies' } 'missing top-level gateway CORS policy owner' Assert-MutationRejected 'gateway policy owner key casing' $noSetup { param($document) Rename-Property $document.'x-app-gateway-policies' 'APP_GATEWAY_PREFLIGHT' 'app_gateway_preflight' } 'missing top-level gateway CORS policy owner' Assert-MutationRejected 'gateway policy field key casing' $noSetup { param($document) Rename-Property $document.'x-app-gateway-policies'.APP_GATEWAY_PREFLIGHT 'allowedMethods' 'AllowedMethods' } 'APP_GATEWAY_PREFLIGHT gateway policy.*(unowned object keyword|missing required object keyword)' Assert-MutationRejected 'operation CORS extension key casing' $noSetup { param($document) $operation = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put Rename-Property $operation 'x-cors-policy-owner' 'X-Cors-Policy-Owner' } 'unowned object keyword: X-Cors-Policy-Owner|PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted' Assert-MutationRejected 'operation CORS owner value casing' $noSetup { param($document) $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put.'x-cors-policy-owner' = 'app_gateway_preflight' } 'PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted' Assert-MutationRejected 'component owner key casing' $noSetup { param($document) Rename-Property $document.components.schemas 'GenerationPoemText' 'generationPoemText' } 'missing schema owner: GenerationPoemText|references missing schemas owner: GenerationPoemText' Assert-MutationRejected 'canonical ref target casing' $noSetup { param($document) $document.components.schemas.GenerationPoemSetItem.properties.generationText.'$ref' = '#/components/schemas/generationPoemText' } 'GenerationPoemSetItem.generationText must be the sole exact local schema ref|references missing schemas owner: generationPoemText' Assert-MutationRejected 'reference keyword casing' $noSetup { param($document) $reference = $document.components.schemas.GenerationPoemSetItem.properties.generationText Rename-Property $reference '$ref' '$Ref' } 'GenerationPoemSetItem.generationText must be the sole exact local schema ref' Assert-MutationRejected 'candidate extension key casing' $noSetup { param($document) $operation = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put Rename-Property $operation 'x-candidate-steps' 'X-Candidate-Steps' } 'unowned object keyword: X-Candidate-Steps|PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted' Assert-MutationRejected 'candidate step value casing' $noSetup { param($document) $steps = @($document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put.'x-candidate-steps') $steps[0] = $steps[0].ToLowerInvariant() $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put.'x-candidate-steps' = $steps } 'PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted' Assert-MutationRejected 'candidate adjacent step swap' $noSetup { param($document) $steps = @($document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put.'x-candidate-steps') $first = $steps[0] $steps[0] = $steps[1] $steps[1] = $first $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put.'x-candidate-steps' = $steps } 'PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted' Assert-MutationRejected 'operationId keyword casing' $noSetup { param($document) $operation = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get Rename-Property $operation 'operationId' 'OperationId' } 'unowned object keyword: OperationId|missing required object keyword: operationId' Assert-MutationRejected 'schema type keyword casing' $noSetup { param($document) Rename-Property $document.components.schemas.GenealogyId 'type' 'Type' } 'GenealogyId contains an unowned schema keyword: Type' Assert-MutationRejected 'schema type value casing' $noSetup { param($document) $document.components.schemas.GenealogyId.type = 'String' } 'GenealogyId must be a non-null 1\.\.128 lexical opaque identifier' Assert-MutationRejected 'top-level webhooks in 3.0.1' $noSetup { param($document) Set-Property $document 'webhooks' ([pscustomobject]@{}) } 'must not define top-level webhooks' Assert-MutationRejected 'component callback owner' $noSetup { param($document) Set-Property $document.components.callbacks 'PoemCallback' ([pscustomobject]@{ '{$request.body#/callbackUrl}' = [pscustomobject]@{ post = [pscustomobject]@{ requestBody = [pscustomobject]@{ required = $true content = [pscustomobject]@{ 'application/json' = [pscustomobject]@{ schema = [pscustomobject]@{ '$ref' = '#/components/schemas/AppGenerationPoemSetUpdateBody' } } } } } } }) } 'unowned component callback exposes the G12 contract' Assert-MutationRejected 'Header Object content conflict' $noSetup { param($document) Set-Property $document.components.headers.PrivateNoStore 'content' ([pscustomobject]@{}) } 'PrivateNoStore header owner contains an unowned object keyword: content' Assert-MutationRejected 'gateway CORS policy drift' $noSetup { param($document) $document.'x-app-gateway-policies'.APP_GATEWAY_PREFLIGHT.allowedRequestHeaders = @('Authorization', 'Content-Type', 'clientid') } 'APP_GATEWAY_PREFLIGHT must own the exact G12 browser preflight policy' Assert-MutationRejected 'GET CORS header loss' $noSetup { param($document) $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get.'x-cors-required-request-headers' = @('Authorization') } 'GET .*gateway CORS headers' Assert-MutationRejected 'PUT candidate order drift' $noSetup { param($document) $operation = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put $operation.'x-candidate-steps' = @('WRITE_ATOMICALLY') } 'PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted' Assert-MutationRejected 'PUT final capacity step missing' $noSetup { param($document) $operation = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put $operation.'x-candidate-steps' = @($operation.'x-candidate-steps' | Where-Object { $_ -ne 'VALIDATE_FINAL_ACTIVE_CAPACITY' }) } 'PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted' Assert-MutationRejected 'PUT allocated ID validation missing' $noSetup { param($document) $operation = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put $operation.'x-candidate-steps' = @($operation.'x-candidate-steps' | Where-Object { $_ -ne 'VALIDATE_ALLOCATED_IDS_NONEMPTY_UNIQUE_NOT_IN_BASELINE' }) } 'PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted' Assert-MutationRejected 'PUT unknown target omits disableMissing' $noSetup { param($document) $operation = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put $operation.'x-unknown-target-construction-fields' = @('genealogyId', 'declaredItems') } 'PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted' Assert-MutationRejected 'PUT unknown NEW ID policy drift' $noSetup { param($document) $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put.'x-unknown-new-id-policy' = 'ALLOW_BASELINE_REUSE' } 'PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted' Assert-MutationRejected 'PUT unknown set equality drift' $noSetup { param($document) $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put.'x-unknown-set-equality' = 'ALLOW_EXTRA_ROWS' } 'PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted' Assert-MutationRejected 'PUT unknown overlap precedence drift' $noSetup { param($document) $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put.'x-unknown-overlap-policy' = 'ATTRIBUTE_TO_REQUEST' } 'PUT generation-poem CAS/transaction/swap/unknown/CORS extensions drifted' Assert-MutationRejected 'unowned operation extension' $noSetup { param($document) $operation = $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.get Set-Property $operation 'x-conflict-policy' 'LAST_WINS' } 'unowned semantic extension: x-conflict-policy' Assert-MutationRejected 'bad 409 scalar' { param($document) Set-Property $document.components.schemas 'GenerationPoemSetVersionConflict' ([pscustomobject]@{ type = 'object'; nullable = $false; additionalProperties = $false required = @('code', 'businessCode', 'message', 'current') properties = [pscustomobject]@{ code = [pscustomobject]@{ type = 'integer'; enum = @(409); nullable = $false } businessCode = [pscustomobject]@{ type = 'string'; enum = @('POEM_SET_VERSION_CONFLICT'); nullable = $false } message = [pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 200; nullable = $false } current = [pscustomobject]@{ '$ref' = '#/components/schemas/GenerationPoemSetSnapshot' } } }) } { param($document) $document.components.schemas.GenerationPoemSetVersionConflict.properties.code.type = 'string' } 'GenerationPoemSetVersionConflict.code must be a non-null integer scalar' Assert-MutationRejected 'nullable 409 current ref sibling' $noSetup { param($document) $current = $document.components.schemas.GenerationPoemSetVersionConflict.properties.current Set-Property $current 'nullable' $true } 'GenerationPoemSetVersionConflict.current must be the sole exact local schema ref' Assert-MutationRejected 'extra discriminator mapping' { param($document) Set-Property $document.components.schemas 'RGenerationPoemSetConflict' ([pscustomobject]@{ oneOf = @( [pscustomobject]@{ '$ref' = '#/components/schemas/GenerationPoemSetVersionConflict' }, [pscustomobject]@{ '$ref' = '#/components/schemas/GenerationPoemGenealogyNotReadyConflict' } ) discriminator = [pscustomobject]@{ propertyName = 'businessCode' mapping = [pscustomobject]@{ POEM_SET_VERSION_CONFLICT = '#/components/schemas/GenerationPoemSetVersionConflict' GENEALOGY_NOT_READY = '#/components/schemas/GenerationPoemGenealogyNotReadyConflict' } } 'x-conflict-precedence' = @('VERSION', 'READY') }) } { param($document) Set-Property $document.components.schemas.RGenerationPoemSetConflict.discriminator.mapping 'EVIL' 'https://evil.test/Foo' } 'discriminator mapping keys must be exactly' Assert-MutationRejected 'unreachable 422 soft-disable confirmation code' { param($document) Set-Property $document.components.schemas 'RGenerationPoemUnprocessable' ([pscustomobject]@{ type = 'object'; nullable = $false; additionalProperties = $false required = @('code', 'businessCode', 'message', 'fieldErrors') properties = [pscustomobject]@{ code = [pscustomobject]@{ type = 'integer'; enum = @(422); nullable = $false } businessCode = [pscustomobject]@{ type = 'string'; nullable = $false enum = @('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') } message = [pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 200; nullable = $false } fieldErrors = [pscustomobject]@{ type = 'array'; minItems = 0; maxItems = 500; nullable = $false items = [pscustomobject]@{ '$ref' = '#/components/schemas/GenerationPoemFieldError' } } } }) } { param($document) $codes = @($document.components.schemas.RGenerationPoemUnprocessable.properties.businessCode.enum) $document.components.schemas.RGenerationPoemUnprocessable.properties.businessCode.enum = @($codes + 'SOFT_DISABLE_CONFIRMATION_REQUIRED') } 'RGenerationPoemUnprocessable.businessCode must be a non-null string enum exactly' Assert-MutationRejected 'nullable 422 businessCode' $noSetup { param($document) $document.components.schemas.RGenerationPoemUnprocessable.properties.businessCode.nullable = $true } 'RGenerationPoemUnprocessable.businessCode must be a non-null string enum exactly' Assert-MutationRejected '422 fieldErrors union wrapper' $noSetup { param($document) Set-Property $document.components.schemas.RGenerationPoemUnprocessable.properties.fieldErrors 'oneOf' @([pscustomobject]@{}) } 'RGenerationPoemUnprocessable.fieldErrors must not define conflicting schema keyword: oneOf' Assert-MutationRejected 'unowned body extension' { param($document) Set-Property $document.components.schemas 'AppGenerationPoemSetUpdateBody' ([pscustomobject]@{ type = 'object'; nullable = $false; additionalProperties = $false required = @('items', 'disableMissing') properties = [pscustomobject]@{ items = [pscustomobject]@{ type = 'array'; minItems = 0; maxItems = 500; nullable = $false; items = [pscustomobject]@{ '$ref' = '#/components/schemas/AppGenerationPoemSetItem' } } disableMissing = [pscustomobject]@{ type = 'boolean'; nullable = $false } } 'x-request-order' = 'STRICT_GENERATION_NO_ASC_NO_SERVER_SORT' 'x-false-policy' = 'PRESERVE_ALL_UNREPRESENTED_BASELINE_ACTIVE' 'x-true-policy' = 'SOFT_DISABLE_ALL_UNREPRESENTED_BASELINE_ACTIVE' 'x-empty-false-policy' = 'SEMANTIC_NOOP' 'x-empty-true-policy' = 'SOFT_DISABLE_ALL_ACTIVE' 'x-final-active-invariant' = 'EMPTY_OR_CONTIGUOUS_INTERVAL' 'x-candidate-validation' = 'BEFORE_ANY_DOMAIN_WRITE' 'x-duplicate-generation-text' = 'ALLOWED' }) } { param($document) Set-Property $document.components.schemas.AppGenerationPoemSetUpdateBody 'x-server-sort' $true } 'AppGenerationPoemSetUpdateBody contains an unowned semantic extension: x-server-sort' Assert-MutationRejected 'Unicode pattern drift' { param($document) Set-Property $document.components.schemas 'GenerationPoemText' ([pscustomobject]@{ type = 'string'; minLength = 1; maxLength = 50; nullable = $false pattern = '^(?!.*[\u0000-\u001F\u007F-\u009F\u061C\u200B-\u200F\u2028-\u202E\u2060\u2066-\u2069\uFEFF])\S(?:[\s\S]*\S)?$' 'x-normalization' = 'NFC' 'x-length-unit' = 'UNICODE_CODE_POINT' 'x-well-formed-unicode' = $true 'x-boundary-whitespace' = 'REJECT' 'x-forbidden-code-point-classes' = @('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') }) } { param($document) $document.components.schemas.GenerationPoemText.pattern = '^.*$' } 'GenerationPoemText must be NFC' Assert-MutationRejected 'int64 generation number' { param($document) Set-Property $document.components.schemas 'GenerationPoemGenerationNo' ([pscustomobject]@{ type = 'integer'; format = 'int32'; minimum = 1; maximum = 2147483647; nullable = $false }) } { param($document) $document.components.schemas.GenerationPoemGenerationNo.format = 'int64' } 'GenerationPoemGenerationNo must be a non-null safe int32-range' Assert-LocalBaselineAccepted 'legal int32 generation number' { param($document) Set-Property $document.components.schemas 'GenerationPoemGenerationNo' ([pscustomobject]@{ type = 'integer'; format = 'int32'; minimum = 1; maximum = 2147483647; nullable = $false }) } 'GenerationPoemGenerationNo must be a non-null safe int32-range|GenerationPoemGenerationNo contains' Assert-LocalBaselineAccepted 'legal component requestBody ref' { param($document) Set-Property $document.components.requestBodies 'GenerationPoemUpdateRequest' ([pscustomobject]@{ required = $true content = [pscustomobject]@{ 'application/json' = [pscustomobject]@{ schema = [pscustomobject]@{ '$ref' = '#/components/schemas/AppGenerationPoemSetUpdateBody' } } } }) $document.paths.'/genealogy/app/genealogies/{genealogyId}/generation-poems'.put.requestBody = [pscustomobject]@{ '$ref' = '#/components/requestBodies/GenerationPoemUpdateRequest' } } 'PUT generation-poem request body (must be required|must expose only|OpenAPI 3.0.1 Reference Object)' Assert-LocalBaselineAccepted 'lineage generation projection is not a G12 owner' { param($document) Set-Property $document.components.schemas 'LineagePersonView' ([pscustomobject]@{ type = 'object' description = 'generation poem display text is a consumer annotation only' properties = [pscustomobject]@{ personId = [pscustomobject]@{ type = 'string' } generationNo = [pscustomobject]@{ type = 'integer' } generationName = [pscustomobject]@{ type = 'string'; description = 'generation poem label' } } }) Set-Property $document.components.schemas 'LineagePersonBody' ([pscustomobject]@{ type = 'object' properties = [pscustomobject]@{ generationNo = [pscustomobject]@{ type = 'integer' } generationName = [pscustomobject]@{ type = 'string'; description = 'generation poem label' } } }) $get = New-MinimalOperation 'getLineagePersonShadow' 'lineage consumer' Set-Property $get.responses '200' (New-ApiResponse '#/components/schemas/LineagePersonView' $false) $put = New-MinimalOperation 'updateLineagePersonShadow' 'lineage consumer' Set-Property $put 'requestBody' ([pscustomobject]@{ required = $true content = [pscustomobject]@{ 'application/json' = [pscustomobject]@{ schema = (New-Ref '#/components/schemas/LineagePersonBody') } } }) Set-Property $document.paths '/genealogy/app/genealogies/{genealogyId}/lineage/person-shadow' ([pscustomobject]@{ get = $get; put = $put }) } 'generation-poem APP (mutation|canonical read).*lineage/person-shadow' Assert-LocalBaselineAccepted 'schema annotations are not G12 owners' { param($document) Set-Property $document.components.schemas 'UnrelatedAnnotatedBody' ([pscustomobject]@{ type = 'object' description = 'unrelated write schema' example = [pscustomobject]@{ generationNo = 1; generationText = '示例' } default = [pscustomobject]@{ generationNo = 2; generationText = '默认' } properties = [pscustomobject]@{ unrelatedValue = [pscustomobject]@{ type = 'string' } } }) $operation = New-MinimalOperation 'updateUnrelatedAnnotatedBody' 'unrelated write' Set-Property $operation 'parameters' @([pscustomobject]@{ name = 'sample'; in = 'query'; required = $false schema = [pscustomobject]@{ type = 'string' } example = [pscustomobject]@{ generationNo = 3; generationText = '参数示例' } }) Set-Property $operation 'requestBody' ([pscustomobject]@{ required = $true content = [pscustomobject]@{ 'application/json' = [pscustomobject]@{ schema = (New-Ref '#/components/schemas/UnrelatedAnnotatedBody') examples = [pscustomobject]@{ sample = [pscustomobject]@{ value = [pscustomobject]@{ generationNo = 4; generationText = '媒体示例' } } } } } }) Set-Property $document.paths '/genealogy/app/unrelated-annotated-write' ([pscustomobject]@{ put = $operation }) } 'generation-poem APP mutation owners.*unrelated-annotated-write' $unicodePattern = '^(?!.*[\u0000-\u001F\u007F-\u009F\u061C\u200B-\u200F\u2028-\u202E\u2060\u2066-\u2069\uFEFF])\S(?:[\s\S]*\S)?$' $hanDe = [string][char]0x5FB7 $hanCheng = [string][char]0x627F $validPair = "$hanDe$hanCheng" foreach ($sample in @(" $validPair", "$validPair ", "$hanDe`t$hanCheng", "$hanDe`r$hanCheng", "$hanDe`n$hanCheng", "$hanDe$([char]0x0085)$hanCheng", "$hanDe$([char]0x061C)$hanCheng", "$hanDe$([char]0x200B)$hanCheng", "$hanDe$([char]0x200E)$hanCheng", "$hanDe$([char]0x2028)$hanCheng", "$hanDe$([char]0x202E)$hanCheng", "$hanDe$([char]0x2060)$hanCheng", "$hanDe$([char]0x2066)$hanCheng", "$hanDe$([char]0xFEFF)$hanCheng")) { if ([regex]::IsMatch($sample, $unicodePattern)) { throw "Unicode regex accepted a forbidden sample: $([BitConverter]::ToString([Text.Encoding]::Unicode.GetBytes($sample)))" } } $supplementary = [char]::ConvertFromUtf32(0x20000) foreach ($sample in @($validPair, "$hanDe$hanDe", "$supplementary$hanCheng")) { if (-not [regex]::IsMatch($sample, $unicodePattern)) { throw "Unicode regex rejected a valid sample: $sample" } } $unicodeRuntimePath = Join-Path $PSScriptRoot 'g12-generation-poem-unicode-contract-runtime-smoke.js' $unicodeRuntimeOutput = @(& node $unicodeRuntimePath 2>&1) if ($LASTEXITCODE -ne 0 -or 'G12-GENERATION-POEM-UNICODE-RUNTIME-SMOKE PASS' -notin $unicodeRuntimeOutput) { throw "G12 ECMAScript Unicode contract smoke failed: $($unicodeRuntimeOutput -join ' | ')" } Write-Output 'G12-GENERATION-POEM-OPENAPI-ADVERSARIAL-CONTRACT PASS'