$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" } } } function Assert-NonEmptyString { param([object]$Schema, [string]$SchemaName, [string]$Field) if (-not $Schema) { return } $property = $Schema.properties.PSObject.Properties[$Field] if (-not $property) { Add-Issue "JSON $SchemaName missing property: $Field" return } if ($property.Value.type -ne 'string') { Add-Issue "JSON $SchemaName.$Field must be string" } if ([int]$property.Value.minLength -lt 1) { Add-Issue "JSON $SchemaName.$Field must declare minLength >= 1" } } $path = '/genealogy/app/help-articles' $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/RListHelpArticleVo') { Add-Issue "JSON GET $path must return RListHelpArticleVo; actual: $responseRef" } } $envelope = Get-Schema 'RListHelpArticleVo' $article = Get-Schema 'HelpArticleVo' Assert-Required $envelope 'RListHelpArticleVo' @('code', 'data') Assert-Required $article 'HelpArticleVo' @('helpCategory', 'helpTitle', 'helpContent') if ($envelope) { if ($envelope.properties.code.type -ne 'integer') { Add-Issue 'JSON RListHelpArticleVo.code must be integer' } $data = $envelope.properties.data if ($data.type -ne 'array' -or $data.items.'$ref' -ne '#/components/schemas/HelpArticleVo') { Add-Issue 'JSON RListHelpArticleVo.data must be HelpArticleVo[]' } } foreach ($field in @('helpCategory', 'helpTitle', 'helpContent')) { Assert-NonEmptyString $article 'HelpArticleVo' $field } if ($article) { $contentDescription = [string]$article.properties.helpContent.description if ($contentDescription -notmatch '(?i)plain[ -]?text') { Add-Issue 'JSON HelpArticleVo.helpContent must declare plain-text semantics' } } foreach ($yamlFact in @( ' /genealogy/app/help-articles:', '#/components/schemas/RListHelpArticleVo', ' RListHelpArticleVo:', ' HelpArticleVo:', ' - code', ' - data', ' - helpCategory', ' - helpTitle', ' - helpContent' )) { 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('HELP-CENTER-OPENAPI-CONTRACT BLOCKED') foreach ($issue in $issues) { $lines.Add("- $issue") } $lines.Add('- M06 will use the complete list response as its only remote owner; the detail endpoint and helpId are not consumed.') $lines.Add('- Authenticated release tests must still cover published-only ordering, 401, malformed data, empty 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 'HELP-CENTER-OPENAPI-CONTRACT PASS'