$ErrorActionPreference = 'Stop' function ConvertFrom-Utf8Base64 { param([string]$Value) return [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Value)) } function Assert-Contains { param( [string]$Content, [string]$Expected, [string]$Message ) if ($Content -notmatch [regex]::Escape($Expected)) { throw $Message } } function Assert-NotContains { param( [string]$Content, [string]$Unexpected, [string]$Message ) if ($Content -match [regex]::Escape($Unexpected)) { throw $Message } } $root = Split-Path -Parent $PSScriptRoot $entryPath = Join-Path $root 'pages/auth/a01-entry.vue' $legacyLoginPath = Join-Path $root 'pages/auth/a02-login.vue' $pagesPath = Join-Path $root 'pages.json' $globalStylesPath = Join-Path $root 'styles/global.scss' if (-not (Test-Path -LiteralPath $entryPath)) { throw 'Missing the single A01 login page.' } if (Test-Path -LiteralPath $legacyLoginPath) { throw 'A02 was merged into A01, so the legacy page file must be removed.' } $pages = Get-Content -LiteralPath $pagesPath -Raw -Encoding utf8 | ConvertFrom-Json if ($pages.pages[0].path -ne 'pages/auth/a01-entry') { throw 'A01 must remain the first application route.' } if ($pages.pages.path -contains 'pages/auth/a02-login') { throw 'pages.json must not retain the legacy A02 route.' } if ($pages.pages.Count -ne 52) { throw "Expected 52 active routes after the A02 merge and A06 archive, found $($pages.pages.Count)." } $entry = Get-Content -LiteralPath $entryPath -Raw -Encoding utf8 $globalStyles = Get-Content -LiteralPath $globalStylesPath -Raw -Encoding utf8 # A01 owns content in normal document flow. Decorative artwork must not become # a second full-page coordinate system. foreach ($required in @( 'class="auth-header"', 'class="auth-header__image"', 'a01-vnext-header-v1.png', 'mode="widthFix"', 'class="auth-paper"', 'auth-page-paper.jpg' )) { Assert-Contains -Content $entry -Expected $required -Message "A01 adaptive structure is missing: $required" } foreach ($forbidden in @( 'class="page-canvas"', 'class="page-backdrop"', 'a01-red-hall-ink-backdrop-v1.png', 'mode="scaleToFill"', '--auth-paper-start', '1665rpx' )) { Assert-NotContains -Content $entry -Unexpected $forbidden -Message "A01 still uses rejected page coordinates: $forbidden" } $pageRule = [regex]::Match($entry, '(?ms)^\.auth-page\s*\{(?.*?)^\}') $paperRule = [regex]::Match($entry, '(?ms)^\.auth-paper\s*\{(?.*?)^\}') $contentRule = [regex]::Match($entry, '(?ms)^\.login-content\s*\{(?.*?)^\}') foreach ($rule in @($pageRule, $paperRule, $contentRule)) { if (-not $rule.Success) { throw 'A01 adaptive layout rule is missing.' } } Assert-Contains -Content $pageRule.Groups['Body'].Value -Expected 'display: flex;' -Message 'A01 root must use flex flow.' Assert-Contains -Content $pageRule.Groups['Body'].Value -Expected 'flex-direction: column;' -Message 'A01 root must stack header and paper.' Assert-Contains -Content $pageRule.Groups['Body'].Value -Expected 'min-height: var(--app-viewport-height);' -Message 'A01 root must fill without locking content height.' if ($pageRule.Groups['Body'].Value -match '(?m)^\s*height:\s*var\(--app-viewport-height\);') { throw 'A01 root must not lock content to the viewport.' } Assert-Contains -Content $paperRule.Groups['Body'].Value -Expected 'flex: 1;' -Message 'A01 paper must receive remaining viewport space.' Assert-Contains -Content $contentRule.Groups['Body'].Value -Expected 'justify-content: space-between;' -Message 'A01 must distribute spare height in normal flow.' Assert-NotContains -Content $contentRule.Groups['Body'].Value -Unexpected 'grid-area:' -Message 'A01 interactive content must not overlap page artwork.' # Typography has one owner, and the title decoration participates in normal flow. Assert-Contains -Content $globalStyles -Expected 'font-family: "STKaiti", "KaiTi", serif' -Message 'Global styles must own the STKaiti/KaiTi font contract.' Assert-NotContains -Content $entry -Unexpected 'font-family:' -Message 'A01 must inherit the global font contract instead of overriding it locally.' $headingRule = [regex]::Match($entry, '(?ms)^\.login-heading\s*\{(?.*?)^\}') if (-not $headingRule.Success) { throw 'A01 is missing the login heading style rule.' } Assert-Contains -Content $headingRule.Groups['Body'].Value -Expected 'flex-direction: column;' -Message 'A01 login heading must lay out its title and divider vertically in normal flow.' Assert-NotContains -Content $headingRule.Groups['Body'].Value -Unexpected 'position:' -Message 'A01 login heading must not use positioning for ordinary vertical layout.' $titleRule = [regex]::Match($entry, '(?ms)^\.login-title\s*\{(?.*?)^\}') if (-not $titleRule.Success) { throw 'A01 is missing the login title style rule.' } Assert-NotContains -Content $titleRule.Groups['Body'].Value -Unexpected 'position:' -Message 'A01 login title must remain in normal flow.' $dividerRule = [regex]::Match($entry, '(?ms)^\.title-divider\s*\{(?.*?)^\}') if (-not $dividerRule.Success) { throw 'A01 is missing the title divider style rule.' } foreach ($positioning in @('position: absolute', 'left: 50%', 'translateX(')) { Assert-NotContains -Content $dividerRule.Groups['Body'].Value -Unexpected $positioning -Message "A01 title divider must not use manual positioning: $positioning" } Assert-Contains -Content $dividerRule.Groups['Body'].Value -Expected 'width: 360rpx;' -Message 'A01 title divider must be wide enough to remain visible on paper.' Assert-Contains -Content $dividerRule.Groups['Body'].Value -Expected 'height: 60rpx;' -Message 'A01 title divider must retain a legible knot and line weight.' Assert-Contains -Content $dividerRule.Groups['Body'].Value -Expected 'filter: brightness(0.68) saturate(1.5) contrast(1.2);' -Message 'A01 title divider must keep sufficient contrast against the paper background.' $brandSealRule = [regex]::Match($entry, '(?ms)^\.brand-seal\s*\{(?.*?)^\}') if (-not $brandSealRule.Success) { throw 'A01 is missing the brand seal style rule.' } Assert-Contains -Content $brandSealRule.Groups['Body'].Value -Expected 'position: absolute;' -Message 'A01 brand seal must be scoped as decoration inside the header.' Assert-Contains -Content $brandSealRule.Groups['Body'].Value -Expected 'left: 50%;' -Message 'A01 brand seal must remain centered inside the header.' Assert-Contains -Content $brandSealRule.Groups['Body'].Value -Expected 'transform: translateX(-50%);' -Message 'A01 brand seal must use header-local centering.' # Final decorative surfaces must come from real bitmap assets. foreach ($asset in @( 'a01-vnext-header-v1.png', 'auth-page-paper.jpg', 'brand-seal.png', 'a01-vnext-divider-v1.png', 'a01-scroll-primary-v3.png', 'a01-scroll-secondary-v3.png', 'a01-scroll-toast-v3.png', 'a01-scroll-dialog-v3.png', 'auth-wechat.png', 'a01-icon-phone-v1.png', 'a01-icon-lock-v1.png', 'a01-icon-eye-open-v1.png', 'a01-icon-eye-closed-pupil-v2.png', 'a01-icon-sms-code-v2.png', 'a02-agreement-unchecked.png', 'a02-agreement-checked.png' )) { Assert-Contains -Content $entry -Expected $asset -Message "A01 is missing asset reference: $asset" } foreach ($obsoleteVisualAsset in @( 'auth-header.png', 'a01-paper-transition-v1.png', 'a01-login-header-v1.png', 'a01-login-header-exact-v2.png', 'a01-login-scroll-frame-v1.png', 'a01-vnext-scroll-v1.png', 'auth-ink-scenery.png' 'a01-title-divider-v2.png', 'a01-primary-button-v2.png', 'a01-secondary-button-v2.png', 'a02-login-panel.png' )) { Assert-NotContains -Content $entry -Unexpected $obsoleteVisualAsset -Message "A01 must not retain the rejected header, scenery, or scroll asset: $obsoleteVisualAsset" } $buttonSkinTags = [regex]::Matches($entry, ']+>') if ($buttonSkinTags.Count -ne 2) { throw "A01 must have exactly two visible login button skins, found $($buttonSkinTags.Count)." } foreach ($buttonSkinTag in $buttonSkinTags) { Assert-Contains -Content $buttonSkinTag.Value -Expected 'mode="aspectFit"' -Message 'A01 button skins must use uniform aspectFit rendering.' Assert-NotContains -Content $buttonSkinTag.Value -Unexpected '/opaque/' -Message 'A01 visible button skins must use cleaned transparent v2 assets.' Assert-NotContains -Content $buttonSkinTag.Value -Unexpected 'mode="scaleToFill"' -Message 'A01 button skins must not distort fixed artwork with scaleToFill.' } $requiredCopy = @( '55m75b2V5a626LCx', '5a+G56CB55m75b2V', '6aqM6K+B56CB55m75b2V', '5omL5py65Y+3', '5a+G56CB', '6aqM6K+B56CB', '5b+Y6K6w5a+G56CB', '6I635Y+W6aqM6K+B56CB', '5b6u5L+h55m75b2V', '6L+Y5rKh5pyJ6LSm5Y+377yf', '5rOo5YaM6LSm5Y+3', '44CK55So5oi35Y2P6K6u44CL', '44CK6ZqQ56eB5pS/562W44CL' ) | ForEach-Object { ConvertFrom-Utf8Base64 $_ } foreach ($copy in $requiredCopy) { Assert-Contains -Content $entry -Expected $copy -Message "A01 is missing required visible copy: $copy" } $smsCodeCopy = ConvertFrom-Utf8Base64 '55+t5L+h6aqM6K+B56CB' Assert-Contains -Content $entry -Expected $smsCodeCopy -Message 'A01 SMS state must use the full SMS code field label.' foreach ($contract in @( 'const activeLoginMethod = ref("password")', 'const passwordVisible = ref(false)', 'const LOGIN_METHOD_STORAGE_KEY = "a01:last-login-method"', 'uni.getStorageSync(LOGIN_METHOD_STORAGE_KEY)', 'uni.setStorageSync(LOGIN_METHOD_STORAGE_KEY, method)', 'const switchLoginMethod = (method) =>', 'const togglePasswordVisibility = () =>', '/pages/auth/a04-register', '/pages/auth/a05-reset-password', 'class="feedback-toast"', 'class="verification-layer"', 'class="login-submit"', 'class="agreement-error"', 'const agreementError = ref(false)' )) { Assert-Contains -Content $entry -Expected $contract -Message "A01 is missing state or interaction contract: $contract" } foreach ($nativeUi in @('uni.showToast', 'uni.showModal', 'uni.showLoading', 'uni.showActionSheet')) { Assert-NotContains -Content $entry -Unexpected $nativeUi -Message "A01 must not use native UniApp feedback: $nativeUi" } Assert-NotContains -Content $entry -Unexpected "url: '/pages/auth/a02-login" -Message 'A01 must not navigate to A02.' Assert-NotContains -Content $entry -Unexpected 'brand-intro' -Message 'A01 must remove the legacy oversized brand introduction.' Assert-NotContains -Content $entry -Unexpected 'min-height: 100vh' -Message 'A01 must not depend on 100vh for its page shell.' Assert-NotContains -Content $entry -Unexpected 'exact-chrome-overlay' -Message 'A01 must not stack a second full-screen scroll perimeter over the complete scroll frame.' Assert-NotContains -Content $entry -Unexpected 'exact-header-overlay' -Message 'A01 must not stack a second header bitmap over the selected backdrop.' Assert-NotContains -Content $entry -Unexpected 'auth-design-scenery-v1.png' -Message 'A01 must not use the scenery bitmap that contains stray scroll handles.' Assert-NotContains -Content $entry -Unexpected 'a01-icon-eye-closed-v1.png' -Message 'A01 hidden-password icon must retain the center pupil instead of using the hollow crossed eye.' Assert-NotContains -Content $entry -Unexpected 'a01-icon-verification-v1.png' -Message 'A01 SMS field must use a message-code icon instead of a success-state checkmark.' Assert-NotContains -Content $entry -Unexpected 'a01-icon-sms-code-v1.svg' -Message 'A01 SMS field must use the custom generated icon instead of the temporary third-party SVG.' Assert-NotContains -Content $entry -Unexpected '@media (max-height:' -Message 'A01 must scroll naturally on short screens instead of compressing the selected design with height breakpoints.' Assert-NotContains -Content $entry -Unexpected 'feedback-toast__skin' -Message 'A01 feedback Toast must use the dedicated nine-slice border asset instead of an absolutely stretched image.' $feedbackToastRule = [regex]::Match($entry, '(?ms)^\.feedback-toast\s*\{(?.*?)^\}') if (-not $feedbackToastRule.Success) { throw 'A01 is missing the custom feedback Toast style rule.' } Assert-Contains -Content $feedbackToastRule.Groups['Body'].Value -Expected '/static/assets/foundation/transparent/a01-scroll-toast-v3.png' -Message 'A01 feedback Toast must use the approved v3 nine-slice asset.' Assert-Contains -Content $feedbackToastRule.Groups['Body'].Value -Expected 'border-image-slice:' -Message 'A01 feedback Toast must declare fixed decorative slices.' Assert-Contains -Content $feedbackToastRule.Groups['Body'].Value -Expected 'fill' -Message 'A01 feedback Toast nine-slice must fill the paper center.' Write-Output 'A01-LOGIN-MERGE-CONTRACT PASS'