$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' $shellPath = Join-Path $root 'components/AuthPageShell.vue' $legacyLoginPath = Join-Path $root 'pages/auth/a02-login.vue' $pagesPath = Join-Path $root 'pages.json' $globalStylesPath = Join-Path $root 'styles/global.scss' $profilePath = Join-Path $root 'styles/adaptive-frame-profiles.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)." } # A01 是 A02 退役边界的唯一所有者;所有仍保留的认证源码都不得重新引用旧登录页。 foreach ($relativePath in @( 'pages/auth/a01-entry.vue', 'pages/auth/a04-register.vue', 'pages/auth/a05-reset-password.vue', 'pages/auth/a06-auth-status.vue' )) { $authSource = Get-Content -LiteralPath (Join-Path $root $relativePath) -Raw -Encoding utf8 Assert-NotContains -Content $authSource -Unexpected '/pages/auth/a02-login' -Message "Legacy A02 navigation remains in $relativePath." } $entry = Get-Content -LiteralPath $entryPath -Raw -Encoding utf8 $shell = Get-Content -LiteralPath $shellPath -Raw -Encoding utf8 $globalStyles = Get-Content -LiteralPath $globalStylesPath -Raw -Encoding utf8 $profiles = Get-Content -LiteralPath $profilePath -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-shell__header"', 'class="auth-shell__header-image"', 'a01-vnext-header-v1.png', 'mode="widthFix"', 'class="auth-shell__paper"', 'auth-page-paper.jpg' )) { Assert-Contains -Content $shell -Expected $required -Message "A01 adaptive shell is missing: $required" } Assert-Contains -Content $entry -Expected '.*?)^\}') $headerRule = [regex]::Match($shell, '(?ms)^\.auth-shell__header\s*\{(?.*?)^\}') $paperRule = [regex]::Match($shell, '(?ms)^\.auth-shell__paper\s*\{(?.*?)^\}') $contentRule = [regex]::Match($entry, '(?ms)^\.login-content\s*\{(?.*?)^\}') foreach ($rule in @($pageRule, $headerRule, $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 $headerRule.Groups['Body'].Value -Expected 'height: calc(var(--app-safe-top) + min(36.5vw, 175px));' -Message 'A01 header must use the approved compact visible slot without distorting its image.' Assert-Contains -Content $headerRule.Groups['Body'].Value -Expected 'overflow: hidden;' -Message 'A01 header must crop only the excess lower doorway inside its decorative slot.' 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: 280rpx;' -Message 'A01 compact title divider must remain visible on paper.' Assert-Contains -Content $dividerRule.Groups['Body'].Value -Expected 'height: 46rpx;' -Message 'A01 compact 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($shell, '(?ms)^\.auth-shell__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 + $shell + $profiles) -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 the current transparent v3 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("sms")', 'const passwordVisible = ref(false)', 'login-tab--unavailable', 'aria-disabled="true"', 'const switchLoginMethod = (method) =>', 'const togglePasswordVisibility = () =>', '', '