$ErrorActionPreference = 'Stop' $root = Split-Path -Parent $PSScriptRoot $pagesPath = Join-Path $root 'pages.json' $mappingPath = Join-Path $root 'docs/接口与页面映射总表.md' $planPath = Join-Path $root 'docs/家谱项目全量治理实施计划.md' if (-not (Test-Path -LiteralPath $mappingPath -PathType Leaf)) { throw '缺少页面与接口关系的唯一总表:docs/接口与页面映射总表.md' } $pagesDocument = Get-Content -Raw -Encoding UTF8 -LiteralPath $pagesPath | ConvertFrom-Json $registeredRoutes = @($pagesDocument.pages | ForEach-Object { $_.path }) $mappingLines = @(Get-Content -Encoding UTF8 -LiteralPath $mappingPath) $mappingText = $mappingLines -join "`n" $planLines = @(Get-Content -Encoding UTF8 -LiteralPath $planPath) $planText = $planLines -join "`n" # 第一步:只解析以正式页面编号开头的表格行,避免把正文中的示例路由误算成页面映射。 $routeRows = @( foreach ($line in $mappingLines) { if ($line -match '^\|\s*(?[AGTFRNM]\d{2})\s*\|\s*[^|]+\|\s*`(?pages/[^`]+)`\s*\|') { $columns = @($line.Trim('|').Split('|') | ForEach-Object { $_.Trim() }) [pscustomobject]@{ Code = $Matches.code Route = $Matches.route Line = $line Columns = $columns } } } ) if ($routeRows.Count -ne $registeredRoutes.Count) { throw "页面映射行数与 pages.json 不一致:映射=$($routeRows.Count),注册=$($registeredRoutes.Count)" } # 第二步:每个活动页面必须完整填写业务目标、入口、流程结果、状态和接口业务域;只列编号和路由不算完成迁移。 foreach ($row in $routeRows) { if ($row.Columns.Count -ne 9) { throw "页面映射列数错误:$($row.Code) 应为 9 列,实际为 $($row.Columns.Count)" } foreach ($columnIndex in 1..7) { if ([string]::IsNullOrWhiteSpace($row.Columns[$columnIndex])) { throw "页面映射存在空业务列:$($row.Code) 第 $($columnIndex + 1) 列" } } } # 第三步:页面编号和路由都必须唯一;重复行会制造两个互相冲突的当前合同。 $duplicateCodes = @($routeRows | Group-Object Code | Where-Object Count -gt 1) if ($duplicateCodes.Count -gt 0) { throw "页面映射存在重复编号:$($duplicateCodes.Name -join ', ')" } $duplicateRoutes = @($routeRows | Group-Object Route | Where-Object Count -gt 1) if ($duplicateRoutes.Count -gt 0) { throw "页面映射存在重复路由:$($duplicateRoutes.Name -join ', ')" } # 第四步:总表顺序和路由集合必须与 pages.json 精确一致,新增或删除页面时必须同轮更新唯一总表。 $mappedRoutes = @($routeRows | ForEach-Object Route) $missingRoutes = @($registeredRoutes | Where-Object { $_ -notin $mappedRoutes }) $staleRoutes = @($mappedRoutes | Where-Object { $_ -notin $registeredRoutes }) if ($missingRoutes.Count -gt 0 -or $staleRoutes.Count -gt 0) { throw "页面映射与 pages.json 不相等:缺失=[$($missingRoutes -join ', ')];陈旧=[$($staleRoutes -join ', ')]" } for ($index = 0; $index -lt $registeredRoutes.Count; $index += 1) { if ($mappedRoutes[$index] -ne $registeredRoutes[$index]) { throw "页面映射顺序错误:第 $($index + 1) 行应为 $($registeredRoutes[$index]),实际为 $($mappedRoutes[$index])" } } # 第五步:只允许 A04 和 T01 使用已经形成证据的专项结论;其余页面不能提前冒充完成 OpenAPI 审查。 $prematureRows = @( $routeRows | Where-Object { if ($_.Code -eq 'A04') { return $_.Line -notmatch '已核对注册成功响应为 `LoginResult`' -or $_.Line -notmatch '成功建立登录态并进入 G01' } if ($_.Code -eq 'T01') { return $_.Line -notmatch '专项已核对:现有递归 `LineagePersonTreeView` 不满足' } return $_.Line -notmatch '待对应业务阶段 OpenAPI 审查' } ) if ($prematureRows.Count -gt 0) { throw "存在没有准确表达接口审查状态的活动页面:$($prematureRows.Code -join ', ')" } foreach ($heading in @( '## 一、权威边界', '## 二、全局业务与交互合同', '## 三、52 个活动页面映射', '## 四、封存与已移除页面', '## 五、后续接口审查填充规则' )) { if (-not $mappingText.Contains($heading)) { throw "接口与页面映射总表缺少章节:$heading" } } # 第六步:锁定旧资料中仍然有效的关键业务事实,避免未来把表格保留却悄悄清空真正的产品合同。 foreach ($requiredFact in @( '8–32', 'A01 在本地校验通过后、登录请求前触发', 'A05 在请求发送短信验证码前触发', 'A04 当前临时在完整注册表单提交后触发', '聚焦密码框', '审核中记录只展示进度和“撤回申请”', '被拒绝记录展示原因和“修改后重新提交”', '仅成员可见', '公开可申请', 'LineageGraphWindow', 'EMPTY/POPULATED', 'entryPersonIds', 'genealogyRootPersonIds', 'redactedGenealogyRootCount', 'rootVisibility', 'pathCompleteness', 'ancestorPathSegments', 'anchorPersonId', 'schemaVersion', 'returnedNodeCount', 'partnerRelationship', 'relationshipKind', 'parentRole', 'LINEAGE_QUERY_INVALID', 'LINEAGE_FOCUS_NOT_AVAILABLE', 'RELATIONSHIP_PATCH_EMPTY', 'sceneVersion', '/genealogy/app/v2/', 'API-T01-001', '单个原生页面实例', '注册成功响应为 `LoginResult`', 'aria-describedby', '邀请码直接加入且不生成审核记录', 'appApi', '500', '20', '100ms', '44dp', '导航栈语义统一', '跨页面领域数据持久化', '全局文字层级和无障碍第二轮', 'A02 账号登录', 'A06 登录状态', 'G02', 'G04', 'G07', 'T02' )) { if (-not $mappingText.Contains($requiredFact)) { throw "接口与页面映射总表缺少关键业务事实:$requiredFact" } } # 第七步:在可执行注册表落地前解析实施计划中的 52 行路由表,防止计划与页面总表各自正确却互相漂移。 $plannedRouteRows = @( foreach ($line in $planLines) { if ($line -match '^\|\s*(?[AGTFRNM]\d{2})\s*\|\s*(?认证根页|根页|普通页|流程页|单实例页)\s*\|') { $columns = @($line.Trim('|').Split('|') | ForEach-Object { $_.Trim() }) [pscustomobject]@{ Code = $Matches.code Kind = $Matches.kind Columns = $columns Line = $line } } } ) if ($plannedRouteRows.Count -ne 52) { throw "实施计划路由表不是 52 行:实际=$($plannedRouteRows.Count)" } $duplicatePlannedCodes = @($plannedRouteRows | Group-Object Code | Where-Object Count -gt 1) if ($duplicatePlannedCodes.Count -gt 0) { throw "实施计划路由表存在重复编号:$($duplicatePlannedCodes.Name -join ', ')" } $mappedCodes = @($routeRows | ForEach-Object Code) $plannedCodes = @($plannedRouteRows | ForEach-Object Code) $missingPlannedCodes = @($mappedCodes | Where-Object { $_ -notin $plannedCodes }) $stalePlannedCodes = @($plannedCodes | Where-Object { $_ -notin $mappedCodes }) if ($missingPlannedCodes.Count -gt 0 -or $stalePlannedCodes.Count -gt 0) { throw "实施计划路由表与页面映射编号不相等:缺失=[$($missingPlannedCodes -join ', ')];陈旧=[$($stalePlannedCodes -join ', ')]" } $plannedRootCodes = @($plannedRouteRows | Where-Object { $_.Kind -in @('认证根页', '根页') } | ForEach-Object Code) if (($plannedRootCodes -join ',') -ne 'A01,G01,F01,M01') { throw "实施计划四个根语义错误:$($plannedRootCodes -join ',')" } $plannedBusinessRoots = @($plannedRouteRows | Where-Object Kind -eq '根页' | ForEach-Object Code) if (($plannedBusinessRoots -join ',') -ne 'G01,F01,M01') { throw "实施计划业务根页必须仅为 G01/F01/M01:$($plannedBusinessRoots -join ',')" } if ($planText -notmatch 'T03: defineRoute\([^\r\n]*parentParamMap: \{ selectedId: "personId" \}') { throw '实施计划缺少 T03 直接进入时 personId 到 selectedId 的父页参数映射' } if ($planText -notmatch 'openPage\("N02", \{ id \}, "N01"\)') { throw '实施计划没有把 N01 打开 N02 的真实来源固定为 N01' } $plannedTableText = ($plannedRouteRows | ForEach-Object Line) -join "`n" foreach ($forbiddenPlannedParam in @('genealogyId,step', 'genealogyName')) { if ($plannedTableText.Contains($forbiddenPlannedParam)) { throw "实施计划路由表仍含已删除参数:$forbiddenPlannedParam" } } if ($mappingText -match '汤正华堂侄') { throw '当前产品文档不得使用具体姓名示例,应改为“某某某堂侄”等通用占位表达' } Write-Output "INTERFACE-PAGE-MAPPING-CONTRACT PASS ROUTES=$($routeRows.Count)"