186 lines
9.8 KiB
PowerShell
186 lines
9.8 KiB
PowerShell
$ErrorActionPreference = 'Stop'
|
|
|
|
$root = Split-Path -Parent $PSScriptRoot
|
|
$contracts = [ordered]@{
|
|
'pages/notification/n01-message-center.vue' = @('listNotificationFixtures', 'notices', 'markAllRead', 'openNotice', 'openPage(', '"N02",', '"G10",')
|
|
'pages/notification/n02-message-detail.vue' = @('findNotificationFixture', 'noticeDetail', 'markAsRead', 'openNoticeTarget', 'targetType', 'targetParams', 'notice-state--expired', 'notice-state--ready')
|
|
'pages/profile/m01-profile-home.vue' = @('menuItems', 'routeKey', 'openPage(', '"M02"', '"N01"')
|
|
'pages/profile/m02-edit-profile.vue' = @('profileForm', 'chooseAvatar', 'validateProfile', 'saveProfile', 'profile-state--saving')
|
|
'pages/profile/m03-security-settings.vue' = @('securityItems', 'openSecurityItem', 'device-state--limited', 'checkSecurity', 'routeKey: "M04"', 'routeKey: "M05"')
|
|
'pages/profile/m04-change-password.vue' = @('passwordForm', 'validatePassword', 'togglePassword', 'savePassword', 'password-state--saving')
|
|
'pages/profile/m05-change-phone.vue' = @('phoneForm', 'sendCode', 'savePhone', 'phone-state--saving')
|
|
'pages/profile/m06-help-center.vue' = @('helpCategories', 'filteredQuestions', 'toggleQuestion', 'contactSupport', 'help-source-note', 'openPage("M07", {}, "M06")')
|
|
'pages/profile/m07-feedback.vue' = @('feedbackForm', 'feedbackTypes', 'validateFeedback', 'submitFeedback', 'feedback-state--submitting')
|
|
'pages/profile/m08-promotion.vue' = @('inviteState', 'openInviteExplanation', 'share-state--unavailable', 'explanationVisible')
|
|
'pages/profile/m09-vip-orders.vue' = @('serviceBenefits', 'orderState', 'order-state--unavailable', 'openServiceNotice')
|
|
'pages/profile/m10-about-settings.vue' = @('agreementItems', 'openAgreement', 'confirmLogout', 'appVersion', 'logoutVisible')
|
|
}
|
|
|
|
foreach ($entry in $contracts.GetEnumerator()) {
|
|
$source = Get-Content -LiteralPath (Join-Path $root $entry.Key) -Raw -Encoding UTF8
|
|
if ($source -match '<ModulePage(?:\s|/|>)|import\s+ModulePage\s+from') { throw "$($entry.Key) must not consume ModulePage" }
|
|
foreach ($forbidden in @('uni.showToast', 'uni.showModal', 'uni.showLoading', 'uni.showActionSheet', 'uni.navigateTo', 'uni.navigateBack', 'uni.redirectTo', 'uni.reLaunch', 'getCurrentPages(', '/pages/')) {
|
|
if ($source.Contains($forbidden)) { throw "$($entry.Key) contains forbidden token: $forbidden" }
|
|
}
|
|
if ($entry.Key -notin @('pages/profile/m04-change-password.vue', 'pages/profile/m07-feedback.vue', 'pages/profile/m10-about-settings.vue') -and $source.Contains('@/utils/api.js')) {
|
|
throw "$($entry.Key) must not consume the API before its independent interface batch"
|
|
}
|
|
foreach ($required in @('ModulePageBackground', 'PageHeader', 'AppButton') + $entry.Value) {
|
|
if (-not $source.Contains($required)) { throw "$($entry.Key) missing business token: $required" }
|
|
}
|
|
if ($source -notmatch 'min-height:\s*100vh') { throw "$($entry.Key) must use natural page height" }
|
|
}
|
|
|
|
$sources = @{}
|
|
foreach ($path in $contracts.Keys) {
|
|
$sources[$path] = Get-Content -LiteralPath (Join-Path $root $path) -Raw -Encoding UTF8
|
|
}
|
|
$n01 = $sources['pages/notification/n01-message-center.vue']
|
|
$n02 = $sources['pages/notification/n02-message-detail.vue']
|
|
$m02 = $sources['pages/profile/m02-edit-profile.vue']
|
|
$m03 = $sources['pages/profile/m03-security-settings.vue']
|
|
$m04 = $sources['pages/profile/m04-change-password.vue']
|
|
$m08 = $sources['pages/profile/m08-promotion.vue']
|
|
$m10 = $sources['pages/profile/m10-about-settings.vue']
|
|
$m10 = $sources['pages/profile/m10-about-settings.vue']
|
|
|
|
if ($n01 -notmatch '(?s)openPage\(\s*"N02",\s*\{ id: String\(item\.id\) \},\s*"N01",?\s*\)') {
|
|
throw 'N01 must open N02 with only its registered lexical notice identity'
|
|
}
|
|
if ($n01 -notmatch '(?s)genealogyId\.value\s*\?\s*openPage\(\s*"G10",\s*\{ genealogyId: genealogyId\.value \},\s*"N01",?\s*\)') {
|
|
throw 'N01 review entry must fail closed without genealogyId and use the gateway'
|
|
}
|
|
foreach ($source in @($n01, $n02)) {
|
|
foreach ($forbiddenTarget in @('detailId', 'target: "/pages/', 'targetUrl', 'returnUrl', 'const noticeDetails')) {
|
|
if ($source.Contains($forbiddenTarget)) { throw "Notification pages retain a duplicate identity, owner, or raw target: $forbiddenTarget" }
|
|
}
|
|
}
|
|
if ($n02 -match 'noticeDetail\.value\.target(?![A-Za-z0-9_$])') {
|
|
throw 'N02 retains an executable target field'
|
|
}
|
|
if ($n02.Contains('noticeDetail.read') -or -not $n02.Contains('noticeDetail.unread')) {
|
|
throw 'N02 must consume the notification owner unread field instead of creating a duplicate read field'
|
|
}
|
|
if ($n02 -notmatch '(?s)const selectedNotice = findNotificationFixture\(noticeId\.value\);.*?if \(.*?!selectedNotice.*?\) \{.*?noticeState\.value = "expired";.*?return;') {
|
|
throw 'N02 unknown notice identity must fail closed instead of reusing a default notice'
|
|
}
|
|
foreach ($required in @(
|
|
'getGenealogyFixtureAccess',
|
|
'targetError',
|
|
'navigateNoticeTarget(',
|
|
'noticeDetail.value.targetType',
|
|
'noticeDetail.value.targetParams'
|
|
)) {
|
|
if (-not $n02.Contains($required)) { throw "N02 safe target flow missing: $required" }
|
|
}
|
|
|
|
foreach ($formPath in @(
|
|
'pages/profile/m02-edit-profile.vue',
|
|
'pages/profile/m04-change-password.vue',
|
|
'pages/profile/m05-change-phone.vue',
|
|
'pages/profile/m07-feedback.vue'
|
|
)) {
|
|
$source = $sources[$formPath]
|
|
foreach ($required in @(
|
|
'custom-back',
|
|
'@back="requestBack"',
|
|
'createDiscardConfirmation',
|
|
'const isDirty = computed(',
|
|
'runBackGuard({',
|
|
'dirty: isDirty.value',
|
|
'submitting:',
|
|
'onBackPress((event) => handleBackPress(event, requestBack));',
|
|
'discardConfirmation.dispose();',
|
|
':close-on-mask="false"'
|
|
)) {
|
|
if (-not $source.Contains($required)) { throw "$formPath back-guard contract missing: $required" }
|
|
}
|
|
}
|
|
|
|
foreach ($previewContract in @(
|
|
@{ Path = 'pages/profile/m02-edit-profile.vue'; Retired = @('个人资料已保存') },
|
|
@{ Path = 'pages/profile/m05-change-phone.vue'; Retired = @('演示验证码已发送', '绑定手机号已更新', 'phoneForm.currentPhone =') }
|
|
)) {
|
|
$source = $sources[$previewContract.Path]
|
|
if (-not $source.Contains('尚未提交服务器')) {
|
|
throw "$($previewContract.Path) must identify local validation as unsaved"
|
|
}
|
|
foreach ($retired in $previewContract.Retired) {
|
|
if ($source.Contains($retired)) { throw "$($previewContract.Path) retains fake server success: $retired" }
|
|
}
|
|
}
|
|
|
|
foreach ($required in @(
|
|
'appApi.changePassword',
|
|
'calcMD5(passwordForm.current)',
|
|
'calcMD5(passwordForm.next)',
|
|
'createRequestController',
|
|
'passwordRequestController.abort()',
|
|
'isRequestCancelled(error)',
|
|
'确认修改密码'
|
|
)) {
|
|
if (-not $m04.Contains($required)) { throw "M04 declared password change contract missing: $required" }
|
|
}
|
|
foreach ($forbidden in @('校验新密码(不提交)', '本地校验通过,尚未提交服务器')) {
|
|
if ($m04.Contains($forbidden)) { throw "M04 must not retain local-only password change copy: $forbidden" }
|
|
}
|
|
|
|
foreach ($required in @('appApi.logout', 'logoutRequestController', 'session.clear();', 'return goRoot("A01");')) {
|
|
if (-not $m10.Contains($required)) { throw "M10 declared logout contract missing: $required" }
|
|
}
|
|
|
|
foreach ($dialogPath in @(
|
|
'pages/profile/m08-promotion.vue',
|
|
'pages/profile/m09-vip-orders.vue',
|
|
'pages/profile/m10-about-settings.vue'
|
|
)) {
|
|
$source = $sources[$dialogPath]
|
|
foreach ($required in @(
|
|
'custom-back',
|
|
'@back="requestBack"',
|
|
'runBackGuard({',
|
|
'transientOpen:',
|
|
'onBackPress((event) => handleBackPress(event, requestBack));',
|
|
':close-on-mask="false"'
|
|
)) {
|
|
if (-not $source.Contains($required)) { throw "$dialogPath transient back-guard contract missing: $required" }
|
|
}
|
|
}
|
|
|
|
foreach ($retiredInviteCopy in @('仍需管理员审核才能加入', '家人提交加入申请', '管理员核实并审核', 'inviteCode', 'setClipboardData', 'posterVisible', 'generatePoster')) {
|
|
if ($m08.Contains($retiredInviteCopy)) { throw "M08 retains retired invitation review semantics: $retiredInviteCopy" }
|
|
}
|
|
foreach ($directInviteCopy in @('邀请码校验成功后直接加入', '不会生成入谱审核记录')) {
|
|
if (-not $m08.Contains($directInviteCopy)) { throw "M08 direct-join contract missing: $directInviteCopy" }
|
|
}
|
|
foreach ($unavailableInviteCopy in @('当前没有可用邀请码', '尚未接入邀请码校验')) {
|
|
if (-not $m08.Contains($unavailableInviteCopy)) { throw "M08 unavailable boundary missing: $unavailableInviteCopy" }
|
|
}
|
|
|
|
if (-not $m03.Contains('currentUser.phone') -or $m03.Contains('139****6421')) {
|
|
throw 'M03 must consume the unique current-user phone instead of a second hard-coded identity'
|
|
}
|
|
if (-not $m02.Contains('currentUser.name') -or $m02.Contains('nickName: "汤文远"')) {
|
|
throw 'M02 must initialize editable identity from the unique current-user owner'
|
|
}
|
|
|
|
$m09 = $sources['pages/profile/m09-vip-orders.vue']
|
|
foreach ($forbiddenOrderPreview in @('query.state', '演示订单', '¥0.00', 'order-state--ready')) {
|
|
if ($m09.Contains($forbiddenOrderPreview)) { throw "M09 must not fabricate an order before API integration: $forbiddenOrderPreview" }
|
|
}
|
|
|
|
if ($m10 -notmatch 'import \{ session \} from "@/utils/session\.js";') {
|
|
throw 'M10 must consume the unique session owner'
|
|
}
|
|
if ($m10 -notmatch '(?s)const confirmLogout = async \(\) => \{.*?await appApi\.logout\(\{ requestController: logoutRequestController \}\);.*?finally \{\s*session\.clear\(\);\s*logoutVisible\.value = false;.*?\}\s*return goRoot\("A01"\);\s*\};') {
|
|
throw 'M10 logout must attempt the declared remote owner, then clear the local session and enter A01 in every result'
|
|
}
|
|
if (([regex]::Matches($m10, 'session\.clear\(\)')).Count -ne 1) {
|
|
throw 'M10 cancellation or back paths must never clear the session'
|
|
}
|
|
if ($m10 -notmatch 'import manifest from "@/manifest\.json";' -or $m10.Contains('const appVersion = "1.0.0"')) {
|
|
throw 'M10 must display the version from manifest.json instead of duplicating release metadata'
|
|
}
|
|
|
|
Write-Output 'NM-PAGE-BUSINESS-CONTRACT PASS'
|