$ErrorActionPreference = 'Stop' Add-Type -AssemblyName System.Drawing $root = Split-Path -Parent $PSScriptRoot $requiredAssets = @( 'static/assets/foundation/transparent/brand-seal.png', 'static/assets/foundation/transparent/notice.png', 'static/assets/foundation/transparent/chevron-right.png', 'static/assets/foundation/transparent/meta-location.png', 'static/assets/foundation/transparent/meta-member.png', 'static/assets/foundation/transparent/meta-admin.png', 'static/assets/modules/genealogy/transparent/shortcut-tree.png', 'static/assets/modules/genealogy/transparent/shortcut-members.png', 'static/assets/modules/genealogy/transparent/shortcut-generation-poem.png', 'static/assets/modules/genealogy/transparent/shortcut-application.png', 'static/assets/modules/genealogy/transparent/current-seal-frame.png', 'static/assets/modules/genealogy/transparent/row-seal-frame.png', 'static/assets/modules/genealogy/transparent/add.png', 'static/assets/modules/genealogy/transparent/create-cloud.png', 'static/assets/foundation/transparent/tab-genealogy.png', 'static/assets/foundation/transparent/tab-genealogy-active.png', 'static/assets/foundation/transparent/tab-family.png', 'static/assets/foundation/transparent/tab-family-active.png', 'static/assets/foundation/transparent/tab-profile.png', 'static/assets/foundation/transparent/tab-profile-active.png', 'static/assets/modules/genealogy/transparent/current-slip-frame.png', 'static/assets/modules/genealogy/transparent/list-slip-frame.png', 'static/assets/foundation/opaque/root-header-cinnabar.jpg', 'static/assets/modules/genealogy/opaque/genealogy-page-background-long.png', 'static/assets/modules/genealogy/transparent/section-divider.png' ) foreach ($asset in $requiredAssets) { if (-not (Test-Path -LiteralPath (Join-Path $root $asset))) { throw "Missing G-01 visual asset: $asset" } } $header = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'components/PageHeader.vue') $tabbar = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'components/AppTabbar.vue') if ($header -notmatch 'brand-seal\.png') { throw 'Header does not use the approved tight brand PNG.' } if ($header -notmatch 'brand-seal\.png') { throw 'Header does not use the tight header Logo asset.' } if ($header -notmatch 'notice\.png') { throw 'Header does not use the approved notice PNG.' } if ($header -notmatch 'unreadCount\s*>\s*0') { throw 'Header badge is not conditional on unread count.' } if ($header -notmatch 'class="header-hall"') { throw 'Header does not isolate the hall line-art as a low-contrast background layer.' } if ($header -match 'background-image: url') { throw 'Header still applies hall line-art directly to the red header surface.' } if ($header -notmatch 'opacity:\s*\.29') { throw 'Header hall line-art is not visible enough for the approved eave layer.' } if ($header -notmatch 'root-header-cinnabar\.jpg') { throw 'Header does not use the approved cinnabar texture asset.' } if ($header -notmatch 'height:\s*calc\(124rpx\s*\+\s*var\(--status-bar-height,\s*0px\)\)') { throw 'Header does not preserve the approved visual height above the system status bar.' } if ($header -notmatch 'padding-top:\s*var\(--status-bar-height,\s*0px\)') { throw 'Header does not reserve the real system status-bar height.' } if ($tabbar -match '/static/icons/tab-') { throw 'Tabbar still references rejected legacy tab assets.' } if ($tabbar -notmatch 'safe-area-inset-bottom') { throw 'Tabbar has no Android safe-area padding.' } if ($tabbar -notmatch 'font-size:\s*30rpx') { throw 'Tabbar label size does not match the approved visual weight.' } if ($tabbar -notmatch 'width:\s*64rpx') { throw 'Tabbar icon size does not match the approved visual weight.' } $page = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages/genealogy/g01-my-genealogies.vue') if ($page -match "from '@/utils/api\.js'") { throw 'G-01 visual phase must not call the remote API.' } foreach ($state in @('isLoading', 'hasGenealogies', 'createdGenealogies', 'joinedGenealogies')) { if ($page -notmatch $state) { throw "G-01 is missing presentation state: $state" } } foreach ($state in @('hasError', 'applicationRecords', 'addDialogVisible', 'switcherVisible')) { if ($page -notmatch $state) { throw "G-01 is missing current acceptance state: $state" } } foreach ($required in @('class="add-dialog-layer"', 'class="genealogy-switcher-layer"', 'application-section')) { if ($page -notmatch [regex]::Escape($required)) { throw "G-01 is missing current acceptance structure: $required" } } foreach ($nativeUi in @('uni.showToast', 'uni.showModal', 'uni.showLoading', 'uni.showActionSheet')) { if ($page -match [regex]::Escape($nativeUi)) { throw "G-01 must not use native UniApp UI: $nativeUi" } } foreach ($asset in @('shortcut-tree.png', 'shortcut-members.png', 'shortcut-generation-poem.png', 'shortcut-application.png', 'add.png')) { if ($page -notmatch [regex]::Escape($asset)) { throw "G-01 does not consume $asset" } } if ($page -notmatch 'current-slip-frame\.png') { throw 'G-01 does not use the approved current genealogy frame asset.' } foreach ($token in @('current-summary', 'current-meta-item', 'current-seal-frame', 'meta-location.png', 'meta-member.png', 'meta-admin.png', 'create-cloud.png')) { if ($page -notmatch $token) { throw "G-01 current panel is missing $token" } } if ($page -notmatch 'width:\s*82rpx') { throw 'G-01 shortcut icons remain below the reference visual size.' } if ($page -notmatch '') { throw 'G-01 does not consume the shared genealogy background component.' } if ($page -notmatch 'section-divider\.png') { throw 'G-01 does not consume the approved section divider asset.' } foreach ($legacyBackground in @('page-paper.jpg', 'footer-mountain-bamboo.png', 'page-paper-texture', 'page-footer-landscape')) { if ($page -match [regex]::Escape($legacyBackground)) { throw "G-01 must replace the legacy layered background: $legacyBackground" } } $backgroundComponent = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'components/GenealogyPageBackground.vue') if ($backgroundComponent -notmatch 'mode="widthFix"') { throw 'G shared background must preserve the complete C artwork without horizontal cropping.' } if ($backgroundComponent -match 'aspectFill|scaleToFill') { throw 'G shared background must not crop or stretch the selected C artwork.' } if ($backgroundComponent -notmatch '(?s)\.genealogy-page-background\s*\{.*?position:\s*fixed;.*?inset:\s*0;.*?background:\s*#e7ded1;') { throw 'G shared background must extend the selected C artwork with the approved paper color.' } $backgroundPath = Join-Path $root 'static/assets/modules/genealogy/opaque/genealogy-page-background-long.png' $backgroundImage = [System.Drawing.Image]::FromFile($backgroundPath) try { if ($backgroundImage.Width -ne 1440 -or $backgroundImage.Height -ne 3600) { throw "G-01 approved long runtime background must be 1440x3600, got $($backgroundImage.Width)x$($backgroundImage.Height)." } } finally { $backgroundImage.Dispose() } $backgroundHash = (Get-FileHash -LiteralPath $backgroundPath -Algorithm SHA256).Hash.ToLowerInvariant() if ($backgroundHash -ne '9aa9ebdd231eddbc151e238ba340a00f8603f23143117ad8c93009947dcca2e2') { throw 'G-01 runtime background must be the user-selected flagship long output.' } if ($page -match 'background:\s*rgba\(255,\s*249,\s*238,\s*\.88\)') { throw 'G-01 current genealogy card retains a rectangular pale backing behind its transparent corners.' } $card = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'components/GenealogyCard.vue') if ($card -notmatch 'list-slip-frame\.png') { throw 'G-01 list rows do not use the approved slip frame asset.' } if ($card -match 'background:\s*rgba\(255,\s*249,\s*238,\s*\.(9|96)\)') { throw 'G-01 list cards retain a rectangular pale backing behind their transparent corners.' } foreach ($token in @('row-seal-frame.png', 'card-main', 'card-title-row', 'card-detail-row', 'card-updated')) { if ($card -notmatch $token) { throw "G-01 list card is missing $token" } } if ($card -match 'background:\s*#b93b2e') { throw 'G-01 list seal is still a plain CSS red block.' } foreach ($asset in @('meta-location.png', 'meta-member.png', 'meta-admin.png', 'current-seal-frame.png', 'row-seal-frame.png', 'create-cloud.png')) { if ($page -notmatch [regex]::Escape($asset) -and $card -notmatch [regex]::Escape($asset)) { throw "G-01 does not consume $asset" } } $mock = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'data/mock.js') $fixtures = @( [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('bmFtZTogJ+axpOawj+WutuiwsSc=')), [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('bG9jYXRpb246ICfmsrPljZfCt+a0m+mYsyc=')), 'memberCount: 158', [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('bmFtZTogJ+axpOawj+Wul+iwsSc=')), [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('bG9jYXRpb246ICflsbHkuJzCt+a1juWugSc=')), 'memberCount: 286' ) foreach ($fixture in $fixtures) { if (-not $mock.Contains($fixture)) { throw "G-01 visual fixture is missing $fixture" } } if ($page -notmatch 'current-info-divider') { throw 'G-01 current panel is missing the reference title-to-meta divider.' } if ($card -match 'calendar-v1\.png') { throw 'G-01 list shows a calendar icon that does not exist in the reference.' } if ($card -match 'genealogy\.surname') { throw 'G-01 list seal still shows a surname instead of the fixed 家谱 seal.' } if ($card -match 'card-copy') { throw 'G-01 list still retains the superseded split content column.' } if ($card -notmatch '(?s)\.card-title-row\s*\{.*?justify-content:\s*space-between') { throw 'G-01 list title, role, and chevron do not share the required first row.' } if ($card -notmatch '(?s)\.card-meta\s*\{.*?font-size:\s*26rpx') { throw 'G-01 list metadata is below the approved readable size.' } if ($card -notmatch '(?s)\.card-updated\s*\{.*?font-size:\s*22rpx') { throw 'G-01 list update date is below the approved readable size.' } if ($page -notmatch '(?s)\.create-action\s*>\s*text\s*\{.*?white-space:\s*nowrap') { throw 'G-01 create action text can wrap onto multiple lines.' } if ($page -notmatch '(?s)\.application-record\s*\+\s*\.application-record\s*\{[^}]*margin-top:\s*12rpx') { throw 'G-01 adjacent application cards must keep the approved 12rpx separation.' } if ($page -notmatch '(?s)\.application-record__copy\s*\{[^}]*color:\s*#62584c;[^}]*font-size:\s*28rpx;[^}]*font-weight:\s*500;[^}]*line-height:\s*1\.4;') { throw 'G-01 application descriptions do not use the approved readable style.' } if ($page -notmatch '(?s)\.application-record__status\s*\{[^}]*color:\s*#7f4f16;[^}]*font-size:\s*27rpx;[^}]*font-weight:\s*600;') { throw 'G-01 application statuses do not use the approved readable base style.' } if ($page -notmatch '(?s)\.application-record__status--rejected\s*\{[^}]*color:\s*#a7160c;') { throw 'G-01 rejected status must preserve the approved semantic red.' } if ($page -notmatch '(?s)\.application-record__status--muted\s*\{[^}]*color:\s*#62584c;') { throw 'G-01 muted status does not use the approved readable color.' } $manifest = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'manifest.json') if ($manifest -notmatch '"statusbar"\s*:\s*\{\s*"immersed"\s*:\s*"supportedDevice"\s*,\s*"style"\s*:\s*"light"\s*,\s*"background"\s*:\s*"#B52E22"') { throw 'Android status bar is not configured for the cinnabar root header.' } Write-Output 'PASS G-01 visual contract'