$ErrorActionPreference = 'Stop' $root = Split-Path -Parent $PSScriptRoot $openApiPath = Join-Path $root 'genealogy-app-openapi.yaml' if (-not (Test-Path -LiteralPath $openApiPath)) { throw 'Current backend OpenAPI export genealogy-app-openapi.yaml is missing at the repository root' } $document = Get-Content -Raw -Encoding UTF8 -LiteralPath $openApiPath $operationCount = [regex]::Matches($document, '(?m)^ (?:get|post|put|delete|patch):\s*$').Count if ($operationCount -ne 137) { throw "Current backend OpenAPI operation count drifted: $operationCount" } function Get-PathBlock { param([string]$Path) $escapedPath = [regex]::Escape($Path) $match = [regex]::Match($document, "(?ms)^ ${escapedPath}:`r?`n(.*?)(?=^ /|\z)") if (-not $match.Success) { throw "Missing path: $Path" } return $match.Groups[1].Value } function Assert-Contains { param([string]$Content, [string]$Expected, [string]$Message) if (-not $Content.Contains($Expected)) { throw $Message } } foreach ($path in @( '/genealogy/app/auth/sms/{operationCode}/code', '/genealogy/app/files/resumable/init', '/genealogy/app/files/resumable/chunk', '/genealogy/app/files/resumable/complete', '/genealogy/app/region/children', '/genealogy/app/region/path/{regionCode}', '/genealogy/app/region/search', '/genealogy/app/region/{regionCode}' )) { [void](Get-PathBlock $path) } foreach ($retiredPath in @( '/captcha/require', '/captcha/challenge', '/captcha/verify', '/auth/code', '/genealogy/app/auth/sms/code', '/genealogy/app/files/upload', '/genealogy/app/files/reference', '/genealogy/region/children', '/genealogy/region/path/{regionCode}', '/genealogy/region/search', '/genealogy/region/{regionCode}' )) { if ($document -match "(?m)^ $([regex]::Escape($retiredPath)):") { throw "Retired path remains in current backend OpenAPI: $retiredPath" } } $videoPath = Get-PathBlock '/genealogy/app/genealogies/{genealogyId}/videos' Assert-Contains $videoPath "`$ref: '#/components/requestBodies/Video'" 'Video create must consume VideoBody' Assert-Contains $videoPath "`$ref: '#/components/responses/ListResult'" 'Video list remains a generic ListResult until the backend publishes a VideoView DTO' $videoSchema = [regex]::Match($document, "(?ms)^ VideoBody:`r?`n(.*?)(?=^ [A-Za-z][A-Za-z0-9_-]*:|\z)").Value Assert-Contains $videoSchema 'required: [videoTitle, videoOssId]' 'VideoBody required fields drifted' foreach ($path in @('/genealogy/app/site/articles', '/genealogy/app/site/pages/{pageKey}')) { $sitePath = Get-PathBlock $path if ($sitePath -match 'VideoView|SiteArticleView|SitePageView') { throw "Site content response DTO changed for $path; review M10 integration" } } $phoneChangePath = Get-PathBlock '/genealogy/app/auth/phone' Assert-Contains $phoneChangePath "`$ref: '#/components/requestBodies/PhoneChange'" 'Phone change must consume PhoneChangeBody' $phoneChangeSchema = [regex]::Match($document, "(?ms)^ PhoneChangeBody:`r?`n(.*?)(?=^ [A-Za-z][A-Za-z0-9_-]*:|\z)").Value Assert-Contains $phoneChangeSchema 'additionalProperties: false' 'PhoneChangeBody must reject legacy auth payload fields' Assert-Contains $phoneChangeSchema 'required: [phone, smsCode]' 'PhoneChangeBody required fields drifted' $apiSource = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $root 'utils/api.js') foreach ($expected in @( '/genealogy/app/auth/sms/${encodeURIComponent(assertAuthVerificationOperation(operationCode))}/code', '/genealogy/app/region/children', '/genealogy/app/region/path/${encodeURIComponent(normalizeRegionCode(regionCode))}', '/genealogy/app/region/search', '/genealogy/app/region/${encodeURIComponent(normalizeRegionCode(regionCode))}' )) { Assert-Contains $apiSource $expected "Current API adapter missing: $expected" } foreach ($retiredPath in @('/genealogy/app/auth/sms/code', '/genealogy/app/files/upload', '/genealogy/app/files/reference', '/genealogy/region/')) { if ($apiSource.Contains($retiredPath)) { throw "API adapter retains retired backend path: $retiredPath" } } $phonePayload = [regex]::Match($apiSource, "(?ms)const normalizePhoneChangePayload = \(payload\) => \{(.*?)^\}").Value Assert-Contains $phonePayload 'return { phone: payload.phone.trim(), smsCode: assertSmsCode(payload.smsCode) }' 'Phone change payload must only contain the documented fields' if ($phonePayload.Contains('authPayload(')) { throw 'Phone change payload must not add clientId or tenantId to PhoneChangeBody' } $videoPage = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $root 'pages/family/f10-video-list.vue') foreach ($token in @('pickAndUploadVideo', 'appApi.createVideo', 'videoTitle', 'videoOssId: receipt.value.ossId')) { Assert-Contains $videoPage $token "F10 video publish contract missing: $token" } foreach ($forbidden in @('v-model="form.videoOssId"', 'v-model="form.status"', 'v-model="form.durationSeconds"')) { if ($videoPage.Contains($forbidden)) { throw "F10 must not expose auto or management field: $forbidden" } } Write-Output 'CURRENT-OPENAPI-INVENTORY-CONTRACT PASS'