$ErrorActionPreference = 'Stop' $root = Split-Path -Parent $PSScriptRoot $requiredFiles = @('utils/config.js', 'utils/session.js', 'utils/genealogy-context.js', 'utils/api.js', 'pages/genealogy/g03-create-genealogy.vue', 'pages/family/f04-article-list.vue', 'pages/family/f02-publish-feed.vue') foreach ($file in $requiredFiles) { if (-not (Test-Path (Join-Path $root $file))) { throw "Missing core-flow file: $file" } } $config = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'utils/config.js') foreach ($token in @('mode:', 'isMockMode')) { if ($config -notmatch [regex]::Escape($token)) { throw "Missing runtime mode contract: $token" } } $context = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'utils/genealogy-context.js') foreach ($method in @('getCurrentGenealogyId', 'setCurrentGenealogyId', 'clearCurrentGenealogyId')) { if ($context -notmatch [regex]::Escape($method)) { throw "Missing genealogy context method: $method" } } $api = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'utils/api.js') foreach ($method in @('unwrapResponse', 'getCaptchaRequirement', 'sendSmsCode', 'loginWithPassword', 'loginWithSms', 'registerWithPassword', 'resetPassword')) { if ($api -notmatch [regex]::Escape($method)) { throw "Missing auth API method: $method" } } foreach ($method in @('createPerson', 'getPerson')) { if ($api -notmatch [regex]::Escape($method)) { throw "Missing lineage API method: $method" } } foreach ($method in @('getArticles', 'getAlbums', 'getCeremonies', 'getGrowthRecords', 'createFeed')) { if ($api -notmatch [regex]::Escape($method)) { throw "Missing family content API method: $method" } } if ($api -notmatch 'getProfile') { throw 'Missing current-user profile API method' } $pages = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages.json') if ($pages -notmatch 'pages/genealogy/g03-create-genealogy') { throw 'Missing G03 create-flow route' } if ($pages -match 'pages/genealogy/g04-first-ancestor') { throw 'G04 first-person route must be merged into G03' } $createFlow = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages/genealogy/g03-create-genealogy.vue') foreach ($token in @('currentStep.value = "ancestor"', 'submitAncestor', 'genealogyId')) { if ($createFlow -notmatch [regex]::Escape($token)) { throw "Missing G03 first-person flow: $token" } } if ($config -notmatch "mode:\s*'remote'" -or $config -notmatch "baseUrl:\s*'https://backend-api\.ddxcjp\.cn'") { throw 'Runtime config must enable the requested remote test backend over HTTPS' } if ($config -match '182\.61\.18\.23|http://backend-api\.ddxcjp\.cn|https://backend-api\.ddxcjp\.cn/') { throw 'Runtime config retains an obsolete, insecure or trailing-slash backend URL' } if (-not $api.Contains('loginResult?.access_token') -or $api -match 'loginResult\?\.(token|accessToken|tokenValue)') { throw 'Session adapter must consume only the current AppLoginVo.access_token contract' } if ($api -match 'mock-session-token|mockResult') { throw '认证链路不得生成本地伪会话或伪票据' } if (([regex]::Matches($api, "return saveLogin\(result\)")).Count -ne 3) { throw '密码登录、短信登录与注册必须共用唯一 AppLoginVo 会话适配器' } if ($api -notmatch 'import \{ hasRemoteConfig, resolveRuntimeMode, runtimeConfig \}' -or ([regex]::Matches($api, 'requireRemoteAuth\(\)')).Count -ne 6) { throw '六个认证传输必须通过共享运行模式解析器失败关闭' } if ($api -match 'if \(isMockMode\(\)\)') { throw 'Auth transports must not treat every non-mock mode as remote' } # 普通加入申请的旧数字审核体与宽松 adapter 已移交给专项 OpenAPI 门禁; # 页面在后端门禁通过前继续由 G08-G10 合同证明未接入这些遗留方法。 if ($createFlow -match 'step=ancestor|query\?\.step') { throw 'G03 first-person flow must not restore the retired route step contract' } if ($createFlow -match 'createPerson') { throw 'G03 must not retain the removed createPerson entrypoint' } foreach ($route in @('pages/family/f04-article-list', 'pages/family/f02-publish-feed')) { if ($pages -notmatch [regex]::Escape($route)) { throw "Missing content page route: $route" } } foreach ($pageFile in @('pages/genealogy/g05-genealogy-overview.vue', 'pages/tree/t01-tree-overview.vue', 'pages/tree/t03-member-profile.vue')) { $page = Get-Content -Raw -Encoding UTF8 (Join-Path $root $pageFile) if ($page -match "id=1001|id=3") { throw "Hard-coded route ID remains in $pageFile" } if ($pageFile -eq 'pages/genealogy/g05-genealogy-overview.vue') { if ($page -notmatch 'const\s+genealogyId\s*=\s*ref\(["'']["'']\)' -or $page -notmatch 'query\.genealogyId') { throw 'G05 must own its explicit genealogyId route context' } } elseif ($pageFile -eq 'pages/tree/t01-tree-overview.vue') { if ($page -notmatch 'genealogyContext') { throw 'T01 must retain the selected genealogy fallback until its domain-data phase' } } else { if ($page -notmatch 'const\s+genealogyId\s*=\s*ref\(["'']["'']\)' -or $page -notmatch 'const\s+personId\s*=\s*ref\(["'']["'']\)' -or $page -notmatch 'query\.genealogyId' -or $page -notmatch 'query\.personId' -or $page -match 'genealogyContext') { throw 'T03 must use its validated genealogyId/personId route identity without mutable global fallback' } } } foreach ($method in @('markNotificationRead', 'markAllNotificationsRead')) { if ($api -notmatch [regex]::Escape($method)) { throw "Missing notification API method: $method" } } foreach ($pageFile in @('pages/genealogy/g10-application-review.vue', 'pages/notification/n01-message-center.vue')) { $page = Get-Content -Raw -Encoding UTF8 (Join-Path $root $pageFile) if ($page -match '1001') { throw "Hard-coded genealogy ID remains in $pageFile" } } $applications = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages/genealogy/g10-application-review.vue') foreach ($token in @('const genealogyId = ref("")', 'genealogyId.value = String(query.genealogyId || "")', 'getGenealogyFixtureAccess(genealogyId.value)')) { if ($applications -notmatch [regex]::Escape($token)) { throw "Applications page must use its explicit fail-closed route context: $token" } } if ($applications -match 'genealogyContext') { throw 'Applications page must not bypass its explicit route context through global genealogy context' } $notifications = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages/notification/n01-message-center.vue') foreach ($method in @('openNotice', 'markAllRead', 'notice-state--list')) { if ($notifications -notmatch [regex]::Escape($method)) { throw "Notification page does not expose local design interaction: $method" } } if ($notifications -match "@/utils/api\.js|\bappApi\b") { throw 'Notification design page must not connect the API layer' } $family = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages/family/f01-family-feed.vue') foreach ($token in @('genealogyContext', 'openSection', 'articles: "F04"', 'albums: "F07"', 'rituals: "R05"', 'memos: "R10"', 'openPage("F02"')) { if ($family -notmatch [regex]::Escape($token)) { throw "Missing family content flow: $token" } } if ($family -match '/pages/') { throw 'F01 family content flow must use the unique navigation registry' } $profile = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages/profile/m01-profile-home.vue') foreach ($token in @('profile-state--ready', 'menuItems', 'toProfile')) { if ($profile -notmatch [regex]::Escape($token)) { throw "Missing local profile design behavior: $token" } } if ($profile -match "@/utils/api\.js|\bappApi\b") { throw 'Profile design page must not connect the API layer' } Write-Output 'PASS core-flow session and auth contract'