$ErrorActionPreference = 'Stop' function Assert-Contains { param([string]$Content, [string]$Expected, [string]$Message) if ($Content -notmatch [regex]::Escape($Expected)) { throw $Message } } function Assert-NoCssSurface { param([string]$Content, [string]$ClassName) foreach ($property in @('border', 'border-radius')) { $pattern = "(?s)\\.$ClassName\\s*\\{[^}]*\\b$property\\s*:" if ($Content -match $pattern) { throw "G06 must not construct .$ClassName with CSS $property" } } } $root = Split-Path -Parent $PSScriptRoot $pages = Get-Content -LiteralPath (Join-Path $root 'pages.json') -Raw -Encoding utf8 | ConvertFrom-Json $paths = @($pages.pages.path) $g06 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g06-search-genealogies.vue') -Raw -Encoding utf8 $mock = Get-Content -LiteralPath (Join-Path $root 'data/mock.js') -Raw -Encoding utf8 $profiles = Get-Content -LiteralPath (Join-Path $root 'styles/adaptive-frame-profiles.scss') -Raw -Encoding utf8 $g07 = Join-Path $root 'pages/genealogy/g07-search-result.vue' $inputSkinPath = Join-Path $root 'static/assets/modules/genealogy/opaque/g06-search-input-wide.png' $buttonSkinPath = Join-Path $root 'static/assets/modules/genealogy/opaque/g06-search-button.png' if (-not ($paths -contains 'pages/genealogy/g06-search-genealogies')) { throw 'G06 route missing' } if ($paths -contains 'pages/genealogy/g07-search-result') { throw 'G07 route must be removed; search results are a G06 state' } if (Test-Path -LiteralPath $g07) { throw 'G07 page file must be deleted; search results are a G06 state' } foreach ($required in @( 'const mode = ref("search")', 'const searchState = ref("initial")', 'const inviteState = ref("initial")', 'getGenealogyFixtureAccess,', 'isGenealogySearchVisible,', 'publicGenealogies,', 'const resultActionLabel = (item) =>', 'const resultRelationLabel = (item) =>', 'getGenealogyFixtureAccess(String(item.id)).canApply', 'isGenealogySearchVisible(String(item.id))', 'search-page__header', 'adaptive.adaptive-g06-search-field', 'adaptive.adaptive-g06-search-action', 'section-divider.png', 'root-header-hall.png', 'search-controls', 'search-initial', 'search-results', 'search-empty', 'search-action', 'const invalidateSearch = () =>', 'if (searchTimer !== timer || mode.value !== "search") return;', '@input="resetInvite"', 'if (inviteState.value !== "valid") return false;' )) { Assert-Contains -Content $g06 -Expected $required -Message "Missing G06 flow contract: $required" } foreach ($required in @( "relation: 'available'", "relation: 'joined'", "relation: 'pending'", "relation: 'rejected'", "relation: 'removed'", "relation: 'owned'", 'parentName:', 'branchName:', 'manager:', 'certification:' )) { Assert-Contains -Content $mock -Expected $required -Message "Missing shared G06 fixture contract: $required" } foreach ($assetName in @('g06-search-input-wide.png', 'g06-search-button.png')) { Assert-Contains $profiles $assetName "Adaptive G06 profile must own: $assetName" } foreach ($forbidden in @("from '@/utils/api.js'", 'appApi.', 'uni.showToast', 'uni.showModal', 'uni.showLoading', 'uni.showActionSheet', 'search-title-strip', 'g06-search-title-strip.png')) { if ($g06 -match [regex]::Escape($forbidden)) { throw "G06 retains forbidden implementation: $forbidden" } } if ($g06 -match [regex]::Escape('appApi.applyToJoin')) { throw 'G06 must navigate to G08 instead of submitting an application directly' } if ($g06 -match [regex]::Escape('g06-search-panel-v2.png')) { throw 'G06 must not embed the search controls in a large panel image' } if ($g06 -match [regex]::Escape('search-hero')) { throw 'G06 must replace the floating hero with the selected title-strip hierarchy' } if ($g06 -match [regex]::Escape('g06-search-input.png')) { throw 'G06 must use the wide selected-design input skin instead of the tall legacy skin' } foreach ($className in @('search-field', 'search-action')) { Assert-NoCssSurface -Content $g06 -ClassName $className } if ($g06 -notmatch '(?s)\.search-controls\s*\{[^}]*display:\s*flex[^}]*align-items:\s*center[^}]*gap:\s*20rpx') { throw 'G06 search input and action must form one compact control row' } Assert-Contains -Content $g06 -Expected 'class="status-retry"' -Message 'G06 error retry action is missing' Assert-Contains -Content $g06 -Expected 'background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")' -Message 'G06 error retry must use the approved A-series primary scroll asset as its real background' if ($g06 -notmatch '(?s)\.status-retry\s*\{[^}]*width:\s*300rpx;[^}]*height:\s*76rpx;') { throw 'G06 error retry must use the approved compact action size' } Add-Type -AssemblyName System.Drawing foreach ($asset in @( @{ Path = $inputSkinPath; Width = 1120; Height = 248; Label = 'search-input' }, @{ Path = $buttonSkinPath; Width = 300; Height = 132; Label = 'search-button' } )) { if (-not (Test-Path -LiteralPath $asset.Path)) { throw "G06 requires a final opaque $($asset.Label) bitmap" } $bitmap = [System.Drawing.Bitmap]::FromFile($asset.Path) try { if ($bitmap.Width -ne $asset.Width -or $bitmap.Height -ne $asset.Height) { throw "G06 $($asset.Label) asset must be $($asset.Width) x $($asset.Height)px" } $corners = @( $bitmap.GetPixel(0, 0).A, $bitmap.GetPixel(($bitmap.Width - 1), 0).A, $bitmap.GetPixel(0, ($bitmap.Height - 1)).A, $bitmap.GetPixel(($bitmap.Width - 1), ($bitmap.Height - 1)).A ) if (($corners | Where-Object { $_ -ne 255 }).Count -ne 0) { throw "G06 $($asset.Label) outer corners must be opaque" } } finally { $bitmap.Dispose() } } Write-Output 'G06-SEARCH-FLOW-CONTRACT PASS'