$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 '阶段 0 已完成;导航栈与 T01 专项设计已完成三人终审,业务代码尚未实施') { throw '接口与页面映射总表缺少阶段 0 完成、书面设计收口及业务代码未实施的准确边界' } if ($overviewDocument -notmatch '下一步从导航失败测试开始') { throw '项目总览没有写明当前精确实施入口' } if ($overviewDocument -notmatch 'API-T01-001') { throw '项目总览没有登记 T01 后端接口门禁' } if ($overviewDocument -match '当前 MuMu[^\r\n]*在线') { throw '项目总览不得把易漂移的 MuMu 在线状态写成长期当前事实' } if ($overviewDocument -notmatch '阶段 0 验收时的 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" } } foreach ($semanticMethod in @('openPage', 'replaceStep', '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 ($mappingDocument -match '将在阶段 1.*确定' -or $mappingDocument -match '阶段 1 依赖') { throw '接口映射总表仍把已收口导航或未来业务错误绑定为阶段 1 待定项' } # A04 已有精确 OpenAPI 证据:注册成功必须建立会话并进入 G01,不能退回并行的“注册后再登录”终点。 if ($mappingDocument -notmatch '成功建立登录态并进入 G01' -or $mappingDocument -notmatch '注册成功响应为 `LoginResult`') { throw 'A04 文档没有同时锁定 LoginResult 与建立登录态后进入 G01' } 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'