Files
jiapuapp/tests/current-documentation-contract.ps1
T
2026-07-22 17:31:38 +08:00

318 lines
18 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
$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'