$ErrorActionPreference = 'Stop' $root = Split-Path -Parent $PSScriptRoot $document = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $root 'APP.openapi.json') | ConvertFrom-Json $yaml = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $root 'APP.openapi.yaml') $issues = New-Object System.Collections.Generic.List[string] function Add-Issue { param([string]$Message) $script:issues.Add($Message) } function Get-Schema { param([string]$Name) $property = $document.components.schemas.PSObject.Properties[$Name] if (-not $property) { Add-Issue "JSON missing schema owner: $Name" return $null } return $property.Value } function Assert-Required { param([object]$Schema, [string]$SchemaName, [string[]]$Fields) if (-not $Schema) { return } foreach ($field in $Fields) { if ($field -notin @($Schema.required)) { Add-Issue "JSON $SchemaName.required missing: $field" } } } $path = '/genealogy/app/auth/profile' $pathProperty = $document.paths.PSObject.Properties[$path] $operation = if ($pathProperty) { $pathProperty.Value.get } else { $null } if (-not $operation) { Add-Issue "JSON missing GET $path" } else { $hasSaToken = $false foreach ($securityRequirement in @($operation.security)) { if ($securityRequirement.PSObject.Properties.Name -contains 'SaToken') { $hasSaToken = $true } } if (-not $hasSaToken) { Add-Issue "JSON GET $path must require SaToken" } $response = $operation.responses.PSObject.Properties['200'].Value if ($response.'$ref') { $responseName = ([string]$response.'$ref').Split('/')[-1] $response = $document.components.responses.PSObject.Properties[$responseName].Value } $media = @($response.content.PSObject.Properties) $responseRef = if ($media.Count -gt 0) { [string]$media[0].Value.schema.'$ref' } else { '' } if ($responseRef -ne '#/components/schemas/RAppProfileVo') { Add-Issue "JSON GET $path must return RAppProfileVo; actual: $responseRef" } } $envelope = Get-Schema 'RAppProfileVo' $profile = Get-Schema 'AppProfileVo' Assert-Required $envelope 'RAppProfileVo' @('code', 'data') Assert-Required $profile 'AppProfileVo' @('phone') if ($envelope) { if ($envelope.properties.code.type -ne 'integer') { Add-Issue 'JSON RAppProfileVo.code must be integer' } if ($envelope.properties.data.'$ref' -ne '#/components/schemas/AppProfileVo') { Add-Issue 'JSON RAppProfileVo.data must reference AppProfileVo' } } if ($profile) { foreach ($field in @('phone', 'nickName', 'realName', 'email')) { $property = $profile.properties.PSObject.Properties[$field] if (-not $property) { Add-Issue "JSON AppProfileVo missing property: $field" } elseif ($property.Value.type -ne 'string') { Add-Issue "JSON AppProfileVo.$field must be string" } } $phonePattern = [string]$profile.properties.phone.pattern try { $phoneRegex = [regex]::new($phonePattern) if (-not $phonePattern -or -not $phoneRegex.IsMatch('13800138000') -or $phoneRegex.IsMatch('138****8000') -or $phoneRegex.IsMatch('+8613800138000')) { Add-Issue 'JSON AppProfileVo.phone must declare the canonical mainland mobile pattern' } } catch { Add-Issue 'JSON AppProfileVo.phone pattern must be a valid regular expression' } foreach ($field in @('nickName', 'realName')) { if ([int]$profile.properties.$field.minLength -lt 1) { Add-Issue "JSON AppProfileVo.$field must declare minLength >= 1 when present" } if ([int]$profile.properties.$field.maxLength -ne 30) { Add-Issue "JSON AppProfileVo.$field must declare maxLength 30" } } if ([int]$profile.properties.email.minLength -lt 1) { Add-Issue 'JSON AppProfileVo.email must declare minLength >= 1 when present' } if ([int]$profile.properties.email.maxLength -ne 100) { Add-Issue 'JSON AppProfileVo.email must declare maxLength 100' } if ($profile.properties.email.format -ne 'email') { Add-Issue 'JSON AppProfileVo.email must declare email format' } } foreach ($yamlFact in @( ' /genealogy/app/auth/profile:', '#/components/schemas/RAppProfileVo', ' RAppProfileVo:', ' AppProfileVo:', ' - code', ' - data', ' - phone', ' nickName:', ' realName:', ' email:' )) { if (-not $yaml.Contains($yamlFact)) { Add-Issue "YAML fact is missing: $yamlFact" } } if ($issues.Count -gt 0) { $lines = New-Object System.Collections.Generic.List[string] $lines.Add('PROFILE-OPENAPI-CONTRACT BLOCKED') foreach ($issue in $issues) { $lines.Add("- $issue") } $lines.Add('- Only phone is required on the wire. Missing optional profile fields mean unset; present empty or null values are invalid.') $lines.Add('- The first client adapter immediately replaces raw phone with maskedPhone and projects nickName, realName, and email; numeric userId/avatar are not consumed.') $lines.Add('- Authenticated release tests must still cover masked display, account switching, 401, malformed data, 5xx, timeout, and cancellation.') $lines.Add('- Replace both protected exports from one backend version; do not hand-edit APP.openapi.json or APP.openapi.yaml.') throw ($lines -join [Environment]::NewLine) } Write-Output 'PROFILE-OPENAPI-CONTRACT PASS'