$ErrorActionPreference = 'Stop' $root = Split-Path -Parent $PSScriptRoot $currentDocuments = @( 'docs/项目当前总览.md', 'docs/家谱项目全量治理设计.md', 'docs/家谱项目全量治理实施计划.md', 'docs/视觉资产与构建基线.md', 'docs/接口与页面映射总表.md' ) $combined = '' $documentContents = @{} foreach ($relativePath in $currentDocuments) { $absolutePath = Join-Path $root $relativePath if (-not (Test-Path -LiteralPath $absolutePath -PathType Leaf)) { throw "缺少当前权威文档:$relativePath" } $content = Get-Content -Raw -Encoding UTF8 -LiteralPath $absolutePath $documentContents[$relativePath] = $content $combined += "`n" + $content } foreach ($token in @('TO' + 'DO', 'TB' + 'D', 'FIX' + 'ME', 'PLACE' + 'HOLDER')) { if ($combined -match "(?i)\b$([regex]::Escape($token))\b") { throw "当前权威文档仍含未完成占位标记:$token" } } # 当前进度只由项目总览表述;其余权威文档不得把已完成的阶段 0 或已收口的书面设计写成进行中。 $designDocument = $documentContents['docs/家谱项目全量治理设计.md'] $planDocument = $documentContents['docs/家谱项目全量治理实施计划.md'] $mappingDocument = $documentContents['docs/接口与页面映射总表.md'] $overviewDocument = $documentContents['docs/项目当前总览.md'] if ($designDocument -match '阶段 0 正在执行') { throw '治理设计仍把已完成的阶段 0 写成进行中' } if ($mappingDocument -match '当前阶段:阶段 0——迁移现行页面与业务事实') { throw '接口与页面映射总表仍把阶段 0 迁移写成当前任务' } if ($mappingDocument -notmatch '家谱工作区、G03 原子创建、M06 帮助、个人资料读写、通知读写、M10 服务端退出、M04 密码凭证与 M05 手机号换绑三人审查及 OpenAPI 红灯已完成' -or $mappingDocument -notmatch 'T01、TAC、工作区、G03、帮助、profile、通知、logout、password 与 phone-change 后端接口门禁当前红灯') { throw '接口与页面映射总表没有登记导航、TAC 客户端、后端红灯与 MuMu 边界' } if ($designDocument -notmatch '导航任务 1—10 的静态实施和零债务门禁已经完成' -or $designDocument -notmatch '当前 SFC 分段词法扫描的迁移债务已经清零' -or $designDocument -notmatch 'MIGRATION-DEBT=0') { throw '治理设计没有同步导航静态零债务结果' } if ($mappingDocument -notmatch '实际 `64/64` 个 Vue 文件') { throw '接口与页面映射总表没有同步退役通用页面后的响应式覆盖实数' } if ($overviewDocument -notmatch 'G01/G05、G03、M06、M01/M02/M03 个人资料读写、N01/N02/M01/G01 通知域、M10 退出域、M04 密码凭证域与 M05 换绑域已完成三人接口审查和失败门禁' -or $overviewDocument -notmatch '继续审查下一个不依赖现有红灯的业务域') { throw '项目总览没有写明领域上下文与 M07 完成后的当前精确实施入口' } if ($overviewDocument -notmatch 'API-T01-001') { throw '项目总览没有登记 T01 后端接口门禁' } if ($overviewDocument -match '当前 MuMu[^\r\n]*在线') { throw '项目总览不得把易漂移的 MuMu 在线状态写成长期当前事实' } if ($overviewDocument -notmatch '阶段 0 验收时的 MuMu 证据') { throw '项目总览缺少带时间边界的 MuMu 验收证据' } $visualDocument = $documentContents['docs/视觉资产与构建基线.md'] if ($visualDocument -notmatch '`static/tac/` 当前共 5 个文件' -or $visualDocument -notmatch '4 个后端提供的供应商文件' -or $visualDocument -notmatch '`static/tac/js/jiapu-tac-adapter.js`' -or $visualDocument -notmatch 'tests/auth-tac-integration-contract.ps1') { throw '视觉资产基线没有登记 TAC 供应商资产、项目适配器及唯一校验入口' } if ($mappingDocument -notmatch 'A01、A04、A05 已接入同一个 `TacVerification`' -or $mappingDocument -notmatch '不能把本地滑动成功冒充服务端验证' -or $mappingDocument -notmatch '密码登录入口保持不可用') { throw '接口页面映射没有准确登记 A01/A04/A05 的 TAC 客户端状态与密码登录硬关闭' } if ($mappingDocument -notmatch '任务 5 实施前' -or $mappingDocument -notmatch '这只是历史视觉基线' -or $mappingDocument -notmatch '任务 4—6 的当前代码仍须.*MuMu 流程矩阵') { throw '接口页面映射把历史 MuMu 证据越界成了当前代码验收' } # 导航设计已经三人终审,文档合同必须保护其所有权、根语义、公开方法和阶段边界,不能只检查“有一份计划”。 foreach ($owner in @('utils/navigation-routes.js', 'utils/navigation.js')) { if ($designDocument -notmatch [regex]::Escape($owner) -or $planDocument -notmatch [regex]::Escape($owner)) { throw "导航设计或实施计划缺少唯一所有者:$owner" } if (-not (Test-Path -LiteralPath (Join-Path $root $owner) -PathType Leaf)) { throw "导航唯一所有者尚未真实落地:$owner" } } foreach ($semanticMethod in @('openPage', 'goBack', 'returnTo', 'finishPage', 'goRoot')) { if ($designDocument -notmatch "(?m)^$([regex]::Escape($semanticMethod))\(") { throw "导航设计缺少公开语义方法:$semanticMethod" } } if ($planDocument -notmatch 'ROOT_ROUTE_KEYS = Object\.freeze\(\["A01", "G01", "F01", "M01"\]\)') { throw '实施计划没有把四个根语义精确锁定为 A01/G01/F01/M01' } if ($planDocument -match 'ROOT_ROUTE_KEYS[^\r\n]*N01') { throw 'N01 不是根页,不得进入 ROOT_ROUTE_KEYS' } if ($designDocument -notmatch 'G01、F01、M01.*自定义 `AppTabbar`' -or $designDocument -notmatch 'A01 是认证根页') { throw '治理设计没有区分三个业务根页与 A01 认证根页' } if ($designDocument -notmatch '字段键精确为 `operation、entityId(可选)、refresh`' -or $designDocument -notmatch '不允许加入 `treeVersion、genealogyId、focusId、payload`') { throw '一次性导航结果没有保持 operation/entityId/refresh 的唯一形状' } if ($designDocument -notmatch 'T03 原生页面实例只保留一个' -or $designDocument -notmatch '轨迹初始化为恰好 `\[initialPersonId\]`') { throw '治理设计缺少 T03 单实例与初始成员轨迹合同' } if ($planDocument -notmatch '导航阶段允许只迁移 T01 的现有导航调用' -or $planDocument -notmatch '导航门禁通过前,不得实施 T01 新图合同') { throw '导航与 T01 的阶段隔离边界不准确' } if ($planDocument -notmatch 'T03_CONTEXT_CONFLICT') { throw '实施计划缺少 T03 跨家谱上下文冲突规则' } if ($planDocument -notmatch 'T03_STACK_CONFLICT') { throw '实施计划缺少 T03 历史重复实例冲突规则' } if ($planDocument -notmatch 'returnTo\(routeKey, targetParams = \{\}\)' -or $planDocument -notmatch 'finishPage\(routeKey, targetParams, result\)') { throw '实施计划没有锁定普通返回与唯一完成结果入口' } if ($planDocument -match 'returnTo\([^\)\r\n]*,\s*result\s*\)') { throw '实施计划重新引入了 returnTo 携带流程结果的旧入口' } if ($planDocument -notmatch 'own enumerable data properties' -or $planDocument -notmatch 'WeakMap') { throw '实施计划缺少严格数据属性快照或页面实例弱身份令牌合同' } $navigationFoundation = [regex]::Match( $planDocument, '(?s)### 任务 1:建立路由语义注册表.*?(?=### 任务 3:迁移共享页头、底栏并退役通用母版)' ).Value if ([string]::IsNullOrWhiteSpace($navigationFoundation) -or $navigationFoundation -match '- \[ \]') { throw '实施计划没有把已经验证的导航任务 1/2 精确标为完成' } if ($planDocument -notmatch 'M04 密码凭证任务 33、M05 手机号换绑任务 34 和 G03 原子创建任务 35 已完成三人审查及 OpenAPI 红灯' -or $planDocument -notmatch '等待后端期间转向 G/F/R 下一域') { throw '实施计划没有登记领域上下文与 M07 完成后的独立实施入口' } $taskThree = [regex]::Match( $planDocument, '(?s)### 任务 3:迁移共享页头、底栏并退役通用母版.*?(?=### 任务 4:迁移认证导航)' ).Value if ([string]::IsNullOrWhiteSpace($taskThree) -or $taskThree -match '- \[ \] \*\*步骤 [123]:') { throw '实施计划没有把任务 3 的静态实施步骤精确标为完成' } if ($taskThree -notmatch 'MuMu 复核待执行' -or $taskThree -notmatch '39') { throw '实施计划没有明确任务 3 的 MuMu 边界或剩余迁移债务' } $taskFour = [regex]::Match( $planDocument, '(?s)### 任务 4:迁移认证导航.*?(?=### 任务 5:迁移 G 系列导航)' ).Value if ([string]::IsNullOrWhiteSpace($taskFour) -or $taskFour -match '- \[ \] \*\*步骤 [123]:') { throw '实施计划没有把任务 4 的静态实施步骤精确标为完成' } if ($taskFour -notmatch '- \[ \] \*\*步骤 4:在 MuMu 复核认证流程' -or $taskFour -notmatch '35') { throw '实施计划没有明确任务 4 的 MuMu 边界或剩余迁移债务' } if ($taskFour -notmatch 'tests/navigation-flow-contract.ps1' -or $taskFour -match 'navigation-auth-flow-contract') { throw '认证导航没有消费跨批次唯一流程合同' } $taskFive = [regex]::Match( $planDocument, '(?s)### 任务 5:迁移 G 系列导航.*?(?=### 任务 6:迁移 T 系列并实现 T03 单实例轨迹)' ).Value if ([string]::IsNullOrWhiteSpace($taskFive) -or $taskFive -match '- \[ \] \*\*步骤 [1234]:') { throw '实施计划没有把任务 5 的静态实施步骤精确标为完成' } if ($taskFive -notmatch '- \[ \] \*\*步骤 5:完成 G 系列 MuMu 矩阵' -or $taskFive -notmatch '29 个文件') { throw '实施计划没有明确任务 5 的 MuMu 边界或剩余迁移债务' } if ($taskFive -match 'finishPage\("G05"' -or $taskFive -match 'finishPage\("G09"') { throw '实施计划重新引入了 G 系列本地预览的伪后端完成结果' } if ($taskFive -notmatch '未知值失败关闭' -or $taskFive -notmatch '每批 50 行渲染') { throw '实施计划没有登记 G11/G12 的最终静态合同' } $taskSix = [regex]::Match( $planDocument, '(?s)### 任务 6:迁移 T 系列并实现 T03 单实例轨迹.*?(?=### 任务 7:迁移 F 系列导航)' ).Value if ([string]::IsNullOrWhiteSpace($taskSix) -or $taskSix -match '- \[ \] \*\*步骤 [123]:') { throw '实施计划没有把任务 6 的静态实施步骤精确标为完成' } if ($taskSix -notmatch '- \[ \] \*\*步骤 4:完成 T 系列 MuMu 矩阵' -or $taskSix -notmatch '22 个页面/表单组件文件') { throw '实施计划没有明确任务 6 的 MuMu 边界或剩余迁移债务' } if ($taskSix -match 'finishPage\(' -or $taskSix -match 'relative-created|member-updated|relationship-updated') { throw '实施计划在 T 系列本地预览阶段伪造了服务端写成功结果' } foreach ($taskSixContract in @( '本地预览', '尚未提交服务器', '`goBack()`', '宿主页路由身份', '页内活动成员', 'listTreeMemberFixtures(genealogyId)', 'findTreeMemberFixture(genealogyId, personId)', '深拷贝快照', '跨谱隔离', '失败关闭', 'tests/tree-member-fixture-runtime-smoke.js', 'PowerShell `122/122`', 'Node 语法 `35/35`', '纯 Node `7/7`' )) { if ($taskSix -notmatch [regex]::Escape($taskSixContract)) { throw "实施计划缺少任务 6 最终合同:$taskSixContract" } } $taskSeven = [regex]::Match( $planDocument, '(?s)### 任务 7:迁移 F 系列导航.*?(?=### 任务 8:迁移 R 系列导航)' ).Value if ($planDocument -match 'assert\.deepEqual\(consumeNavigationResult\("F04"\),\s*\{\s*operation:\s*"article-created"') { throw '实施计划仍保留已经废止的 F04 伪写结果示例' } if ([string]::IsNullOrWhiteSpace($taskSeven) -or $taskSeven -match '- \[ \] \*\*步骤 [12345]:') { throw '实施计划没有把任务 7 的静态实施步骤精确标为完成' } if ($taskSeven -notmatch '- \[ \] \*\*步骤 6:完成 F 系列 MuMu 矩阵' -or $taskSeven -notmatch '13 个页面/表单组件文件') { throw '实施计划没有明确任务 7 的 MuMu 边界或剩余迁移债务' } foreach ($taskSevenContract in @( 'F01—F10', '跨谱', 'WRITE_UNAVAILABLE', 'feed-created/article-created/article-updated/media-uploaded', 'PowerShell `122/122`', 'Node 语法 `36/36`', '纯 Node `8/8`', 'MIGRATION-DEBT=13' )) { if ($taskSeven -notmatch [regex]::Escape($taskSevenContract)) { throw "实施计划缺少任务 7 最终合同:$taskSevenContract" } } $taskEight = [regex]::Match( $planDocument, '(?s)### 任务 8:迁移 R 系列导航.*?(?=### 任务 9:)' ).Value foreach ($taskEightContract in @( 'R03/R04 归属 `relative-records`', '`giftId` 全量退役为 `relativeId`', '`ritualId` 全量退役为 `ceremonyId`', 'R09 必须保持硬关闭', 'resultOperations', '本地预览,尚未提交服务器', '只读复合身份 owner', 'MuMu' )) { if ($taskEight -notmatch [regex]::Escape($taskEightContract)) { throw "实施计划缺少任务 8 三方审查结论:$taskEightContract" } } if ([string]::IsNullOrWhiteSpace($taskEight) -or $taskEight -match '- \[ \] \*\*步骤 [123456]:') { throw '实施计划没有把任务 8 的静态实施步骤精确标为完成' } if ($taskEight -notmatch '- \[ \] \*\*步骤 7:完成 R 系列 MuMu 矩阵' -or $taskEight -notmatch 'MIGRATION-DEBT=6') { throw '实施计划没有明确任务 8 的 MuMu 边界或剩余迁移债务' } foreach ($taskEightVerification in @('PowerShell `122/122`', 'Node 语法 `37/37`', '纯 Node `9/9`')) { if ($taskEight -notmatch [regex]::Escape($taskEightVerification)) { throw "实施计划缺少任务 8 最新验证:$taskEightVerification" } } $taskNine = [regex]::Match( $planDocument, '(?s)### 任务 9:迁移 N/M 系列与安全通知目标.*?(?=### 任务 10:关闭导航阶段)' ).Value if ([string]::IsNullOrWhiteSpace($taskNine) -or $taskNine -match '- \[ \] \*\*步骤 [1234]:') { throw '实施计划没有把任务 9 静态实施步骤精确标为完成' } if ($taskNine -notmatch '- \[ \] \*\*步骤 5:完成 N/M 系列 MuMu 矩阵' -or $taskNine -notmatch 'MIGRATION-DEBT=1') { throw '实施计划没有明确任务 9 的 MuMu 边界或最后一项导航债务' } $taskTen = [regex]::Match( $planDocument, '(?s)### 任务 10:关闭导航阶段.*?(?=---\s*## 第二阶段:T01 大规模世系树)' ).Value if ([string]::IsNullOrWhiteSpace($taskTen) -or $taskTen -match '- \[ \] \*\*步骤 [12]:') { throw '实施计划没有把任务 10 的静态门禁步骤精确标为完成' } if ($taskTen -notmatch '- \[ \] \*\*步骤 3:执行 NAV-MUMU-01 至 NAV-MUMU-08' -or $taskTen -notmatch 'MIGRATION-DEBT=0' -or $taskTen -notmatch 'TreeMemberForm\.vue') { throw '实施计划没有记录导航零债务、孤儿删除或 MuMu 待验边界' } $taskEleven = [regex]::Match( $planDocument, '(?s)### 任务 11:向后端提交规范图合同并建立接口门禁.*?(?=### 任务 12:实现新图的规范化与严格校验)' ).Value if ([string]::IsNullOrWhiteSpace($taskEleven) -or $taskEleven -match '- \[ \] \*\*步骤 [12]:' -or $taskEleven -notmatch '- \[ \] \*\*步骤 [34]:') { throw '实施计划没有精确登记 T01 门禁已落地、后端双导出仍待提供' } foreach ($taskElevenFact in @('LINEAGE-OPENAPI-CONTRACT BLOCKED', 'JSON/YAML 各缺四条操作和三个固定根模型', '平铺 query parameters 无法由 OpenAPI 静态证明')) { if ($taskEleven -notmatch [regex]::Escape($taskElevenFact)) { throw "任务 11 缺少门禁分层事实:$taskElevenFact" } } if ($mappingDocument -notmatch '### 2\.12 R 系列线上接口与静态迁移边界') { throw '接口与页面映射总表缺少 R 系列三方接口审查账本' } $taskEighteen = [regex]::Match( $planDocument, '(?s)### 任务 18:接入新接口、mock 和跨页变更版本.*?(?=### 任务 19:完成 T01 MuMu、性能与三人终审)' ).Value foreach ($taskEighteenContract in @( 'pages/tree/t05-edit-member.vue', 'appApi.updateLineagePerson(genealogyId, memberId, payload, treeVersion)', 'T04/T05/T06 完成回流', 'genealogyId/personId/memberId', 'personId=A', 'memberId=B', 'hostPersonId', 'editedMemberId', '禁止 `memberId || personId` 双读', 'parentParamMap` 仍只负责无历史时构造父页', '同步收紧 `tests/navigation-flow-contract.ps1`' )) { if ($taskEighteen -notmatch [regex]::Escape($taskEighteenContract)) { throw "任务 18 缺少 T05 双身份或原子合同迁移约束:$taskEighteenContract" } } foreach ($retiredOwner in @('components/ModulePage.vue', 'data/page-catalog.js')) { if ($combined -match [regex]::Escape($retiredOwner)) { throw "当前权威文档不得把退役通用页面重新登记为 owner:$retiredOwner" } } if ($combined -notmatch 'tests/retired-module-page-contract.ps1') { throw '当前权威文档缺少通用母版退役防回归合同' } $navigationSource = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $root 'utils/navigation.js') $actualNavigationExports = @( [regex]::Matches($navigationSource, '(?m)^export const ([A-Za-z][A-Za-z0-9]*)') | ForEach-Object { $_.Groups[1].Value } | Sort-Object ) $expectedNavigationExports = @( 'buildRouteUrl', 'consumeNavigationResult', 'finishPage', 'goBack', 'goRoot', 'handleBackPress', 'openNoticeTarget', 'openPage', 'resolveBackAction', 'returnTo', 'runBackGuard' ) | Sort-Object if (($actualNavigationExports -join ',') -ne ($expectedNavigationExports -join ',')) { throw "导航网关公开导出面漂移:$($actualNavigationExports -join ', ')" } if ($navigationSource -match '(?m)^export\s+(const|function)\s+replaceStep\b' -or $designDocument -match '(?m)^replaceStep\(') { throw '当前没有替换边,不得恢复公开 replaceStep 入口' } if ($mappingDocument -match '将在阶段 1.*确定' -or $mappingDocument -match '阶段 1 依赖') { throw '接口映射总表仍把已收口导航或未来业务错误绑定为阶段 1 待定项' } if ($overviewDocument -notmatch '当前物理库存为 `140` 个 PowerShell 合同、`47` 个 Node 文件' -or $overviewDocument -notmatch 'PowerShell `126/140` 通过' -or $overviewDocument -notmatch 'Node 语法 `47/47` 通过' -or $overviewDocument -notmatch '纯 Node 冒烟 `19/19` 通过' -or $overviewDocument -notmatch 'Vue 脚本模块语法 `64/64` 通过') { throw '项目总览没有登记当前测试库存与最新全量验证证据' } foreach ($completedDomainFact in @( 'appApi.submitFeedback', '/genealogy/app/feedback', 'WRITE_UNAVAILABLE', 'feedbackContent', '撤权', '禁止静默', 'uncertain' )) { if ($overviewDocument -notmatch [regex]::Escape($completedDomainFact) -and $mappingDocument -notmatch [regex]::Escape($completedDomainFact) -and $planDocument -notmatch [regex]::Escape($completedDomainFact)) { throw "当前文档没有登记领域上下文或 M07 完成事实:$completedDomainFact" } } foreach ($workspaceGateFact in @( 'API-GENEALOGY-WORKSPACE-001', 'genealogy-workspace-openapi-contract.ps1', 'RListAppGenealogyVo', 'RAppGenealogyVo', 'canView', 'GENEALOGY-WORKSPACE-OPENAPI-CONTRACT BLOCKED' )) { if ($overviewDocument -notmatch [regex]::Escape($workspaceGateFact) -and $mappingDocument -notmatch [regex]::Escape($workspaceGateFact) -and $planDocument -notmatch [regex]::Escape($workspaceGateFact)) { throw "当前文档没有登记家谱工作区门禁事实:$workspaceGateFact" } } foreach ($helpGateFact in @( 'API-M06-001', 'help-center-openapi-contract.ps1', 'RListHelpArticleVo', 'HelpArticleVo', 'list-only', 'HELP-CENTER-OPENAPI-CONTRACT BLOCKED' )) { if ($overviewDocument -notmatch [regex]::Escape($helpGateFact) -and $mappingDocument -notmatch [regex]::Escape($helpGateFact) -and $planDocument -notmatch [regex]::Escape($helpGateFact)) { throw "当前文档没有登记 M06 帮助门禁事实:$helpGateFact" } } foreach ($profileGateFact in @( 'API-PROFILE-READ-001', 'profile-openapi-contract.ps1', 'RAppProfileVo', 'AppProfileVo', 'maskedPhone', 'PROFILE-OPENAPI-CONTRACT BLOCKED' )) { if ($overviewDocument -notmatch [regex]::Escape($profileGateFact) -and $mappingDocument -notmatch [regex]::Escape($profileGateFact) -and $planDocument -notmatch [regex]::Escape($profileGateFact)) { throw "当前文档没有登记个人资料读取门禁事实:$profileGateFact" } } foreach ($profileUpdateGateFact in @( 'API-PROFILE-UPDATE-001', 'profile-update-openapi-contract.ps1', 'AppProfileMergeUpdateBody', 'profileVersion', 'If-Match', 'PROFILE_VERSION_CHANGED', 'PROFILE-UPDATE-OPENAPI-CONTRACT BLOCKED' )) { if ($overviewDocument -notmatch [regex]::Escape($profileUpdateGateFact) -and $mappingDocument -notmatch [regex]::Escape($profileUpdateGateFact) -and $planDocument -notmatch [regex]::Escape($profileUpdateGateFact)) { throw "当前文档没有登记个人资料写入门禁事实:$profileUpdateGateFact" } } foreach ($notificationGateFact in @( 'API-NOTIFICATION-READ-001', 'API-NOTIFICATION-STATE-001', 'notification-read-openapi-contract.ps1', 'notification-read-state-openapi-contract.ps1', 'RListNotificationVo', 'RNotificationUnreadCount', 'generation+ordinal', 'NOTIFICATION-READ-OPENAPI-CONTRACT BLOCKED', 'NOTIFICATION-READ-STATE-OPENAPI-CONTRACT BLOCKED' )) { if ($overviewDocument -notmatch [regex]::Escape($notificationGateFact) -and $mappingDocument -notmatch [regex]::Escape($notificationGateFact) -and $planDocument -notmatch [regex]::Escape($notificationGateFact)) { throw "当前文档没有登记通知读取或已读写入门禁事实:$notificationGateFact" } } foreach ($logoutGateFact in @( 'API-LOGOUT-001', 'logout-openapi-contract.ps1', 'RLogoutRejected', 'TOKEN_CLIENT_MISMATCH', 'LOGOUT-OPENAPI-CONTRACT BLOCKED', 'logoutCoordinator' )) { if ($overviewDocument -notmatch [regex]::Escape($logoutGateFact) -and $mappingDocument -notmatch [regex]::Escape($logoutGateFact) -and $planDocument -notmatch [regex]::Escape($logoutGateFact)) { throw "当前文档没有登记 M10 服务端退出门禁事实:$logoutGateFact" } } foreach ($passwordChangeGateFact in @( 'API-PASSWORD-001', 'API-PASSWORD-005', 'password-change-openapi-contract.ps1', 'CurrentPasswordSecret', 'NewPasswordSecret', 'RPasswordChangeRejected', 'CREDENTIAL_VERSION_CONFLICT', 'credentialChangeInFlight', 'PASSWORD-CHANGE-OPENAPI-CONTRACT BLOCKED' )) { if ($overviewDocument -notmatch [regex]::Escape($passwordChangeGateFact) -and $mappingDocument -notmatch [regex]::Escape($passwordChangeGateFact) -and $planDocument -notmatch [regex]::Escape($passwordChangeGateFact)) { throw "当前文档没有登记 M04 密码凭证门禁事实:$passwordChangeGateFact" } } foreach ($phoneChangeGateFact in @( 'API-PHONE-001', 'API-PHONE-005', 'phone-change-openapi-contract.ps1', 'PhoneChangeSmsCodeBody', 'NewBoundPhone', 'SmsCodeSecret', 'RPhoneChangeRejected', 'STEP_UP_UNAVAILABLE', 'PHONE-CHANGE-OPENAPI-CONTRACT BLOCKED' )) { if ($overviewDocument -notmatch [regex]::Escape($phoneChangeGateFact) -and $mappingDocument -notmatch [regex]::Escape($phoneChangeGateFact) -and $planDocument -notmatch [regex]::Escape($phoneChangeGateFact)) { throw "当前文档没有登记 M05 手机号换绑门禁事实:$phoneChangeGateFact" } } foreach ($g03BootstrapGateFact in @( 'API-G03-001', 'API-G03-005', 'g03-bootstrap-openapi-contract.ps1', 'AppGenealogyBootstrapBody', 'GenealogyBootstrapOperationKey', 'GenealogyAccessPreset', 'GenealogyRegionCode', 'FAILED_NO_COMMIT', 'x-bootstrap-root-editable-fields', 'x-state-transitions', 'x-domain-effects=NONE', '任意精度', 'G03-BOOTSTRAP-OPENAPI-CONTRACT BLOCKED', 'g03-bootstrap-client-release-gate.ps1', 'G03-BOOTSTRAP-CLIENT-RELEASE BLOCKED', 'openapi-yaml-json-parity-runtime-smoke.js', '/genealogy/app/region/search', 'GET 必须纯读', 'fatal/quarantined', '家谱工作区读取门禁', 'If-Match、版本/CAS', 'receipt→`/mine` cache→context→G05' )) { if ($overviewDocument -notmatch [regex]::Escape($g03BootstrapGateFact) -and $designDocument -notmatch [regex]::Escape($g03BootstrapGateFact) -and $mappingDocument -notmatch [regex]::Escape($g03BootstrapGateFact) -and $planDocument -notmatch [regex]::Escape($g03BootstrapGateFact)) { throw "当前文档没有登记 G03 原子创建门禁事实:$g03BootstrapGateFact" } } foreach ($liveOpenApiFact in @('https://backend-api.ddxcjp.cn/', '3.1.0', '722', '858', '507', '/captcha/challenge', 'validToken')) { if ($overviewDocument -notmatch [regex]::Escape($liveOpenApiFact) -or $mappingDocument -notmatch [regex]::Escape($liveOpenApiFact)) { throw "项目总览或接口映射缺少新线上 OpenAPI 事实:$liveOpenApiFact" } } if ($mappingDocument -notmatch '密码登录体尚无票据字段' -or $mappingDocument -notmatch '没有任何 `/genealogy/app/v2/') { throw '接口映射没有锁定 TAC 密码登录或 T01 v2 的线上硬缺口' } foreach ($lineageGateFact in @('LINEAGE-OPENAPI-CONTRACT BLOCKED', 'LineageGraphWindow/LineageOverview/LineageLocator')) { if ($overviewDocument -notmatch [regex]::Escape($lineageGateFact) -and $mappingDocument -notmatch [regex]::Escape($lineageGateFact) -and $planDocument -notmatch [regex]::Escape($lineageGateFact)) { throw "当前文档没有登记 T01 门禁事实:$lineageGateFact" } } foreach ($authGateFact in @( 'AUTH-TAC-OPENAPI-CONTRACT BLOCKED', 'ANDROID-AUTH-ACCESSIBILITY-RELEASE BLOCKED', 'API-AUTH-TAC-001', 'API-AUTH-TAC-002', 'API-AUTH-TAC-003', 'API-AUTH-TAC-004', 'runtimeConfig.mode', 'mock', '15 秒', 'APP_SMS_LOGIN', 'APP_REGISTER', 'APP_FORGOT_PASSWORD' )) { if ($overviewDocument -notmatch [regex]::Escape($authGateFact) -and $mappingDocument -notmatch [regex]::Escape($authGateFact) -and $planDocument -notmatch [regex]::Escape($authGateFact)) { throw "当前文档没有登记认证门禁事实:$authGateFact" } } if ($combined -notmatch '同一验证中心' -or $combined -notmatch '不得.*无障碍.*绕过' -or $combined -notmatch '非交互风控' -or $combined -notmatch '已安全绑定设备' -or $combined -notmatch '人工.*兜底' -or $combined -notmatch 'POC') { throw '当前文档没有锁定 TAC 可访问替代路径的统一票据、安全边界和待验证状态' } # A04 线上响应已经迁移,注册成功仍必须建立会话并进入 G01,不能保留旧令牌字段或“注册后再登录”终点。 if ($mappingDocument -notmatch '成功建立登录态并进入 G01' -or $mappingDocument -notmatch 'RAppLoginVo' -or $mappingDocument -notmatch 'AppLoginVo' -or $mappingDocument -notmatch 'access_token') { throw 'A04 文档没有同时锁定线上登录响应与建立登录态后进入 G01' } if ($combined -match 'token/accessToken/tokenValue') { throw '当前权威文档仍把旧登录令牌字段写成目标合同' } if ($designDocument -notmatch '正式终点不是“注册后再登录”' -or $planDocument -notmatch '不能先回 A01 再让用户重复登录') { throw '设计或计划没有明确排除注册后再次登录的并行终点' } # T01 后端合同、固定 MuMu 配置和人工验收规模已经收口;旧字段只能出现在“必须拒绝”的文字中,不能再出现在目标 JSON 形状。 foreach ($canonicalField in @('schemaVersion', 'treeVersion', 'state', 'entryPersonIds', 'genealogyRootPersonIds', 'redactedGenealogyRootCount', 'rootVisibility', 'pathCompleteness', 'ancestorPathSegments', 'returnedNodeCount', 'partnerRelationship', 'relationshipKind', 'parentRole', 'LINEAGE_QUERY_INVALID', 'LINEAGE_FOCUS_NOT_AVAILABLE', 'RELATIONSHIP_PATCH_EMPTY', 'projectLineageGraph', 'sceneVersion')) { if ($designDocument -notmatch [regex]::Escape($canonicalField) -and $planDocument -notmatch [regex]::Escape($canonicalField)) { throw "T01 设计与计划缺少规范字段或阶段:$canonicalField" } } if ($planDocument -match '"partnerIds"\s*:' -or $planDocument -match '"rootPersonIds"\s*:' -or $planDocument -match '"rootReason"\s*:' -or $planDocument -match '"schema"\s*:' -or $planDocument -match '"tree"\s*:') { throw 'T01 目标 JSON 示例仍在定义已经废弃的旧字段' } foreach ($emptyContract in @('"state": "EMPTY"', '"focusPersonId": null', '"entryPersonIds": []', '"generationRange": null', '合法的 `state=EMPTY` 空谱', '`state=POPULATED`')) { if (-not $planDocument.Contains($emptyContract)) { throw "T01 计划缺少 EMPTY/POPULATED 判别合同:$emptyContract" } } if ($planDocument -notmatch 'POPULATED 分支要求 nodes 非空、`focusPersonId` 引用其中一个 VISIBLE 节点') { throw 'T01 POPULATED 仍允许把 REDACTED 节点作为焦点' } if ($designDocument -match '无主根时显示合同异常' -or $designDocument -notmatch '只有 `state=POPULATED` 缺少有效焦点或窗口入口时才显示合同异常') { throw 'T01 仍把合法 EMPTY 误判为无主根合同异常' } foreach ($entryInvariant in @('没有 primary 入边的节点集合', '每个入口在当前窗口恰有零条 primary 入边', '每个非入口恰有一条 primary 入边', '`entryPersonIds` 引用不存在人物', '列表内人物 `entryReason=null`', '列表外人物 `entryReason!=null`', '入口仍有 primary 入边')) { if (-not $planDocument.Contains($entryInvariant) -and -not $designDocument.Contains($entryInvariant)) { throw "T01 窗口入口与 primary 森林不变量缺失:$entryInvariant" } } if ($planDocument -notmatch 'VisibleLineagePerson.*精确包含' -or $planDocument -notmatch 'RedactedLineagePerson.*只允许' -or $planDocument -notmatch '不得用于 FOCUS、locator、搜索或写接口') { throw 'T01 人物模型没有按 visibility 严格区分可见字段与匿名字段' } if ($planDocument -notmatch 'REDACTED 节点只显示通用隐私说明' -or $planDocument -notmatch 'focusPersonId.*targetPersonId.*稳定 VISIBLE 人物 ID' -or $planDocument -notmatch 'focusable=false、targetPersonId=null') { throw 'T01 仍可能把 REDACTED opaque ID 用作焦点或聚合跳转目标' } if ($planDocument -notmatch 'redacted:\{treeVersion\}:\{token\}' -or $planDocument -notmatch '不得以 `redacted:` 开头的稳定人物 ID') { throw 'T01 没有提供可执行的稳定人物 ID 与匿名 ID 边界' } if ($planDocument -notmatch 'protectedPathPersonIds/protectedPathFamilyUnitIds' -or $planDocument -notmatch '任意时刻每个家庭的真实子女上限始终为 5') { throw 'T01 投影没有同时保护焦点祖先路径和固定五人渲染上限' } if ($planDocument -notmatch 'relationshipKind.*discriminator.*oneOf' -or $planDocument -notmatch 'PARTNER 分支只允许 `relationType/status`') { throw 'T01 关系修改没有形成按不可变 relationshipKind 区分的唯一请求合同' } if ($planDocument -notmatch '至少提交一个可修改字段' -or $planDocument -notmatch '未提交的可修改字段保持原值' -or $planDocument -notmatch '422 RELATIONSHIP_PATCH_EMPTY') { throw 'T01 关系 PATCH 没有拒绝空更新或没有定义省略字段语义' } foreach ($enumContract in @('EMPTY/POPULATED', 'MALE/FEMALE/UNKNOWN', 'VISIBLE/REDACTED', 'GENEALOGY_ROOT/WINDOW_CUT/DISCONNECTED_COMPONENT', 'ANCHOR/PARTNER', 'MARRIAGE/PARTNERSHIP/UNKNOWN', 'ACTIVE/ENDED/UNKNOWN')) { if ($planDocument -notmatch [regex]::Escape($enumContract)) { throw "T01 OpenAPI 计划缺少完整枚举:$enumContract" } } if ($planDocument -notmatch '规范化最坏复杂度明确为 `O\(N log N \+ E\)`' -or $planDocument -notmatch '投影只接受已经规范化且校验通过的图' -or $planDocument -notmatch '投影阶段保持 `O\(N\+E\)`') { throw 'T01 规范化排序与线性投影的复杂度边界不清晰' } if (-not $planDocument.Contains('Scene 根对象精确为 `{ sceneVersion, treeVersion, focusPersonId, bounds, items }`') -or $planDocument -notmatch '相同 `sceneVersion` 对应不同序列化 payload.*失败' -or $planDocument -notmatch 'scene\.sceneVersion') { throw 'T01 Scene 根模型、版本所有权或版本碰撞规则不完整' } if ($planDocument -notmatch '瞬时 `selectedId` 不属于 Scene payload' -or $planDocument -notmatch '只改变 `selectedId` 时 Scene payload 与 `scene\.sceneVersion` 不变') { throw 'T01 瞬时选中态仍可能制造同 sceneVersion 的不同 Scene payload' } foreach ($overviewInvariant in @('overview` 只接受必填查询参数 `treeVersion`', 'genealogyPersonCount=0、genealogyRootPersonIds=[]、redactedGenealogyRootCount=0、generationRange=null、buckets=[]', 'genealogyRootPersonIds.length + redactedGenealogyRootCount >= 1', '三类计数总和等于 `genealogyPersonCount`', '隐私根只计入 `redactedGenealogyRootCount`', 'getLineageOverview(genealogyId, treeVersion)')) { if (-not $planDocument.Contains($overviewInvariant) -and -not $designDocument.Contains($overviewInvariant)) { throw "T01 全谱概览的版本、空谱、计数或隐私根不变量缺失:$overviewInvariant" } } foreach ($locatorInvariant in @('根可见性与路径完整性独立校验', 'pathCompleteness=REDACTED_GAPS', 'ancestorPathSegments', '可见根+REDACTED_GAPS', '隐私根+REDACTED_GAPS', '可见根路径不从 `rootPersonId` 开始', 'REDACTED 段含人物 ID', 'validateLineageLocator')) { if (-not $planDocument.Contains($locatorInvariant) -and -not $designDocument.Contains($locatorInvariant)) { throw "T01 locator 的隐私根或路径完整性分支缺失:$locatorInvariant" } } foreach ($runtimeSemantic in @('tests/lineage-overview-locator-runtime-smoke.js', 'validateLineageOverview', 'bucket 三类计数与 `genealogyPersonCount` 不等', '隐私根 POPULATED', '响应 `treeVersion` 与请求不一致')) { if (-not $planDocument.Contains($runtimeSemantic)) { throw "T01 overview 跨字段语义缺少运行时负例门禁:$runtimeSemantic" } } if ($designDocument -match '焦点、版本和展开状态' -or $planDocument -match '共享焦点、版本、展开状态') { throw 'T01 文档仍残留已删除的页内聚合展开状态' } if ($planDocument -notmatch '/genealogy/app/v2/genealogies/' -or $planDocument -match '若旧 `/lineage/tree`') { throw 'T01 后端路径没有固定为明确 v2 合同' } if ($planDocument -notmatch 'emulator-5554 / 720×1280 / 320dpi' -or $designDocument -notmatch '10、50、100、300 代') { throw 'T01 文档没有锁定当前 MuMu 配置和 10/50/100/300 代人工定位矩阵' } $retiredEntrypoints = @( 'design-pipeline/manifests/a01.json', 'design-pipeline/manifests/a01-scroll-skins-v3.json', 'design-pipeline/scripts/build-a01.mjs', 'tests/a01-code-pipeline-contract.ps1', 'docs/design/assets/a01-vnext/source/A01-layered-source-v1.psd', 'build:a01', 'validate:auth-runtime-assets', 'design-pipeline/manifests/g01-background-candidates.json', 'design-pipeline/manifests/module-page-backgrounds.json', 'design-pipeline/scripts/build-module-page-backgrounds.mjs', 'design-pipeline/scripts/build_module_page_backgrounds.py', 'design-pipeline/scripts/build-g01-empty-frame.mjs', 'design-pipeline/tests/test_build_g01_backgrounds.py', 'build:g01-background-candidates', 'build:module-page-backgrounds', 'build:g01-empty-frame' ) foreach ($entrypoint in $retiredEntrypoints) { if ($combined.Contains($entrypoint)) { throw "当前权威文档仍引用已退役入口:$entrypoint" } } foreach ($owner in @( 'design-pipeline/manifests/runtime-assets.json', 'design-pipeline/manifests/application-runtime-assets.json', 'design-pipeline/manifests/shared-scroll-skins-v3.json', 'design-pipeline/manifests/page-backgrounds-v3.json', 'design-pipeline/manifests/g01-state-frame-v3.json', 'tests/runtime-assets-contract.ps1', 'tests/retired-asset-removal-contract.ps1', 'tests/mumu-visual-acceptance-boundary-contract.ps1', 'npm.cmd run validate:runtime-assets', 'npm.cmd run build:page-backgrounds', 'npm.cmd run build:g01-state-frame', 'docs/接口与页面映射总表.md' )) { if (-not $combined.Contains($owner)) { throw "当前权威文档缺少现行所有者:$owner" } } # 当前文档写出的正式 owner 与验证入口必须真实存在;文档不得指向已删除脚本制造“可恢复”的假象。 foreach ($relativePath in @( 'components/GenealogyPageBackground.vue', 'components/ModulePageBackground.vue', 'design-pipeline/manifests/application-runtime-assets.json', 'design-pipeline/manifests/page-backgrounds-v3.json', 'design-pipeline/manifests/g01-state-frame-v3.json', 'design-pipeline/scripts/build-raster-assets.mjs', 'design-pipeline/scripts/build_raster_assets.py', 'tests/runtime-assets-contract.ps1', 'tests/retired-asset-removal-contract.ps1', 'tests/global-heritage-visual-system-contract.ps1' )) { if (-not (Test-Path -LiteralPath (Join-Path $root $relativePath) -PathType Leaf)) { throw "当前正式 owner 或验证入口不存在:$relativePath" } } foreach ($referenceOnly in @( 'docs/design/assets/a01-vnext/A01-shared-scroll-skins-with-dialog-approved.png', 'docs/design/assets/a01-vnext/A01-shared-skin-family-option-2-selected.png' )) { if (-not (Test-Path -LiteralPath (Join-Path $root $referenceOnly) -PathType Leaf)) { throw "缺少用户批准的视觉锚点:$referenceOnly" } if (-not $combined.Contains($referenceOnly)) { throw "当前文档没有登记 reference-only 视觉锚点:$referenceOnly" } } $pipelinePackage = Get-Content -Raw -Encoding UTF8 -LiteralPath (Join-Path $root 'design-pipeline/package.json') | ConvertFrom-Json if ($pipelinePackage.scripts.'build:page-backgrounds' -ne 'node scripts/build-raster-assets.mjs design-pipeline/manifests/page-backgrounds-v3.json') { throw 'design-pipeline 缺少长页面背景 schema v3 重建命令' } if ($pipelinePackage.scripts.'build:g01-state-frame' -ne 'node scripts/build-raster-assets.mjs design-pipeline/manifests/g01-state-frame-v3.json') { throw 'design-pipeline 缺少 G01 状态边框 schema v3 重建命令' } if ($pipelinePackage.dependencies -and $pipelinePackage.dependencies.sharp) { throw '通用 Pillow 构建器生效后不得重新引入 Sharp 单用途依赖' } # 阶段 0 已结束,文档体系只允许五个长期入口;临时清单、旧规格、旧计划、交接和设计记录不得重新出现。 $temporaryChecklist = Join-Path $root 'docs/项目基线清理清单.md' if (Test-Path -LiteralPath $temporaryChecklist -PathType Leaf) { throw '阶段 0 已完成,临时清理清单必须删除' } $allowedMarkdown = @($currentDocuments) $actualMarkdown = @( Get-ChildItem -LiteralPath (Join-Path $root 'docs') -Recurse -File -Filter '*.md' | ForEach-Object { $_.FullName.Substring($root.Length + 1).Replace('\', '/') } ) $unexpectedMarkdown = @($actualMarkdown | Where-Object { $_ -notin $allowedMarkdown }) if ($unexpectedMarkdown.Count -gt 0) { throw "仍存在当前体系之外的旧 Markdown:$($unexpectedMarkdown -join ', ')" } foreach ($retiredRootDocument in @('design-qa.md', '记录.md')) { if (Test-Path -LiteralPath (Join-Path $root $retiredRootDocument) -PathType Leaf) { throw "仍存在已退役的根目录文档:$retiredRootDocument" } } # 旧 H5/静态设计图不能再次成为长期视觉证据;目录在 fresh checkout 中可以不存在。 $screenDirectory = Join-Path $root 'docs/design/screens' if (Test-Path -LiteralPath $screenDirectory -PathType Container) { $retiredScreens = @(Get-ChildItem -LiteralPath $screenDirectory -Recurse -File) if ($retiredScreens.Count -gt 0) { throw "仍存在已退役的长期截图:$($retiredScreens.Count) 张" } } foreach ($retiredTest in @( 'tests/a01-design-record-audit.ps1', 'tests/g06-screenshot-consistency.ps1', 'tests/handoff-documentation-contract.ps1', 'tests/repository-handoff-size-contract.ps1' )) { if (Test-Path -LiteralPath (Join-Path $root $retiredTest) -PathType Leaf) { throw "仍存在只保护旧文档或截图的测试:$retiredTest" } } $sddDirectory = Join-Path $root '.superpowers/sdd' if (Test-Path -LiteralPath $sddDirectory -PathType Container) { $sddFiles = @(Get-ChildItem -LiteralPath $sddDirectory -Recurse -File) if ($sddFiles.Count -gt 0) { throw "仍存在已完成任务的内部 SDD 过程文件:$($sddFiles.Count) 个" } } Write-Output 'CURRENT-DOCUMENTATION-CONTRACT PASS'