完成全项目响应式审核与换机交接

This commit is contained in:
2026-07-22 07:41:27 +08:00
parent a127280ce6
commit eced3d1e6e
77 changed files with 2052 additions and 378 deletions
+2 -1
View File
@@ -48,7 +48,8 @@ foreach ($required in @(
'const prepareLogin = () => uni.redirectTo({ url:',
'/pages/auth/a01-entry',
'if (!/^1\d{10}$/.test(phone.value))',
'if (!password.value)',
'validatePassword(password.value)',
'PASSWORD_POLICY_MESSAGE',
'if (!confirmPassword.value)',
'if (password.value !== confirmPassword.value)'
)) {
+2 -1
View File
@@ -50,7 +50,8 @@ foreach ($required in @(
'/pages/auth/a01-entry',
'if (!/^1\d{10}$/.test(phone.value))',
'if (!/^\d{6}$/.test(verificationCode.value))',
'if (!password.value)',
'validatePassword(password.value)',
'PASSWORD_POLICY_MESSAGE',
'if (password.value !== confirmPassword.value)',
'adaptive.adaptive-feedback-toast;',
'top: calc(var(--status-bar-height, 0px) + 24rpx);',
@@ -8,7 +8,9 @@ foreach ($token in @(
':compact="compactActions && showCancel"',
'@use "../styles/adaptive-frame-profiles.scss" as adaptive;',
'@include adaptive.adaptive-auth-dialog;',
'max-height: calc(var(--app-viewport-height, 100vh) - 160rpx);',
'padding: calc(40rpx + env(safe-area-inset-top)) 40rpx calc(40rpx + env(safe-area-inset-bottom));',
'max-height: calc(var(--app-viewport-height, 100vh) - 80rpx - env(safe-area-inset-top) - env(safe-area-inset-bottom));',
'min-height: 0;',
'margin-top: 26rpx;'
)) {
if (-not $dialog.Contains($token)) { throw "AppDialog responsive contract missing: $token" }
+1
View File
@@ -37,6 +37,7 @@ foreach ($file in $files) {
$source = [System.IO.File]::ReadAllText($file.FullName, [System.Text.Encoding]::UTF8)
foreach ($styleMatch in [regex]::Matches($source, '(?s)<style\b[^>]*>(?<css>.*?)</style>')) {
$css = [regex]::Replace($styleMatch.Groups['css'].Value, '(?s)/\*.*?\*/', '')
$css = [regex]::Replace($css, '(?m)^\s*@(use|forward|import)\b[^;]+;\s*', '')
foreach ($rule in [regex]::Matches($css, '(?s)(?<selector>[^{}]+)\{(?<body>[^{}]*)\}')) {
$selector = (($rule.Groups['selector'].Value -replace '(?s)^.*@media[^\{]*', '') -replace '\s+', ' ').Trim()
$body = $rule.Groups['body'].Value
File diff suppressed because one or more lines are too long
+165 -155
View File
@@ -1,157 +1,167 @@
[
{
"file": "components/AppDialog.vue",
"selector": ".app-dialog-layer",
"reason": "全屏弹窗遮罩"
},
{
"file": "components/AppToast.vue",
"selector": ".app-toast",
"reason": "跨页面轻提示"
},
{
"file": "components/PageHeader.vue",
"selector": ".page-header",
"reason": "用户指定的固定顶部导航栏"
},
{
"file": "components/AppTabbar.vue",
"selector": ".app-tabbar",
"reason": "用户指定的固定底部导航栏"
},
{
"file": "components/PageHeader.vue",
"selector": ".header-texture",
"reason": "固定顶部栏内部纯装饰纹理层"
},
{
"file": "components/PageHeader.vue",
"selector": ".header-hall",
"reason": "固定顶部栏内部纯装饰祠堂层"
},
{
"file": "components/GenealogyPageBackground.vue",
"selector": ".genealogy-page-background",
"reason": "不参与排版且覆盖视口的独立家谱背景层"
},
{
"file": "components/GenealogyPageBackground.vue",
"selector": ".genealogy-page-background__art",
"reason": "独立家谱背景层内部的底部背景画面"
},
{
"file": "components/ModulePageBackground.vue",
"selector": ".module-page-background",
"reason": "不参与排版且覆盖视口的独立模块背景层"
},
{
"file": "components/ModulePageBackground.vue",
"selector": ".module-page-background__image",
"reason": "独立模块背景层内部的底部背景画面"
},
{
"file": "pages/family/f08-album-detail.vue",
"selector": ".album-photo-tile",
"reason": "仅作为照片裁切层与底部说明叠层的精确局部边界"
},
{
"file": "pages/family/f08-album-detail.vue",
"selector": ".album-hero-photo, .album-photo-tile__image",
"reason": "在固定比例照片格内执行裁切的媒体画面层"
},
{
"file": "pages/family/f08-album-detail.vue",
"selector": ".album-photo-tile__caption",
"reason": "依附照片格底部的局部说明叠层"
},
{
"file": "pages/family/f08-album-detail.vue",
"selector": ".album-preview",
"reason": "用户触发的全屏照片预览层"
},
{
"file": "pages/family/f09-media-upload.vue",
"selector": ".media-photo-tile",
"reason": "仅作为照片缩略图角标与删除操作的精确局部边界"
},
{
"file": "pages/family/f09-media-upload.vue",
"selector": ".media-photo-order, .media-photo-current, .media-photo-status",
"reason": "依附照片缩略图的顺序与状态角标"
},
{
"file": "pages/family/f09-media-upload.vue",
"selector": ".media-photo-remove",
"reason": "依附照片缩略图右上角的删除操作"
},
{
"file": "pages/tree/t01-tree-overview.vue",
"selector": ".member-sheet",
"reason": "用户选择节点后显示的固定底部详情抽屉"
},
{
"file": "pages/tree/t01-tree-overview.vue",
"selector": ".member-sheet__skin",
"reason": "固定底部详情抽屉内部的装饰框层"
},
{
"file": "pages/auth/a01-entry.vue",
"selector": ".feedback-toast",
"reason": "登录页面跨内容轻提示"
},
{
"file": "pages/auth/a01-entry.vue",
"selector": ".verification-layer",
"reason": "用户触发的验证码弹窗遮罩"
},
{
"file": "pages/auth/a04-register.vue",
"selector": ".feedback-toast",
"reason": "注册页面跨内容轻提示"
},
{
"file": "pages/auth/a05-reset-password.vue",
"selector": ".feedback-toast",
"reason": "重置密码页面跨内容轻提示"
},
{
"file": "pages/auth/a05-reset-password.vue",
"selector": ".success-layer",
"reason": "用户触发的重置成功弹窗遮罩"
},
{
"file": "pages/auth/a06-auth-status.vue",
"selector": ".recovery-layer",
"reason": "用户触发的账号恢复弹窗遮罩"
},
{
"file": "pages/genealogy/g01-my-genealogies.vue",
"selector": ".add-dialog-layer",
"reason": "用户触发的固定底部添加家谱弹层"
},
{
"file": "pages/genealogy/g01-my-genealogies.vue",
"selector": ".genealogy-switcher-layer",
"reason": "用户触发的固定家谱切换弹窗遮罩"
},
{
"file": "pages/genealogy/g10-application-review.vue",
"selector": ".review-feedback",
"reason": "审核操作后跨内容显示的固定轻提示"
},
{
"file": "pages/genealogy/g03-create-genealogy.vue",
"selector": ".duplicate-reminder-layer, .flow-success-layer",
"reason": "用户触发的重复提醒与成功弹窗遮罩"
},
{
"file": "pages/genealogy/g11-genealogy-settings.vue",
"selector": ".settings-feedback",
"reason": "保存设置后跨内容显示的固定轻提示"
},
{
"file": "pages/genealogy/g12-generation-poems.vue",
"selector": ".poem-feedback",
"reason": "保存字辈后跨内容显示的固定轻提示"
}
{
"file": "components/PageHeader.vue",
"selector": ".page-header",
"reason": "用户指定的固定顶部导航栏"
},
{
"file": "components/AppTabbar.vue",
"selector": ".app-tabbar",
"reason": "用户指定的固定底部导航栏"
},
{
"file": "components/PageHeader.vue",
"selector": ".header-texture",
"reason": "固定顶部栏内部纯装饰纹理层"
},
{
"file": "components/PageHeader.vue",
"selector": ".header-hall",
"reason": "固定顶部栏内部纯装饰祠堂层"
},
{
"file": "components/GenealogyPageBackground.vue",
"selector": ".genealogy-page-background",
"reason": "不参与排版且覆盖视口的独立家谱背景层"
},
{
"file": "components/GenealogyPageBackground.vue",
"selector": ".genealogy-page-background__art",
"reason": "独立家谱背景层内部的底部背景画面"
},
{
"file": "components/ModulePageBackground.vue",
"selector": ".module-page-background",
"reason": "不参与排版且覆盖视口的独立模块背景层"
},
{
"file": "components/ModulePageBackground.vue",
"selector": ".module-page-background__image",
"reason": "独立模块背景层内部的底部背景画面"
},
{
"file": "pages/family/f08-album-detail.vue",
"selector": ".album-photo-tile",
"reason": "仅作为照片裁切层与底部说明叠层的精确局部边界"
},
{
"file": "pages/family/f08-album-detail.vue",
"selector": ".album-hero-photo, .album-photo-tile__image",
"reason": "在固定比例照片格内执行裁切的媒体画面"
},
{
"file": "pages/family/f08-album-detail.vue",
"selector": ".album-photo-tile__caption",
"reason": "依附照片格底部的局部说明叠层"
},
{
"file": "pages/family/f08-album-detail.vue",
"selector": ".album-preview",
"reason": "用户触发的全屏照片预览层"
},
{
"file": "pages/family/f09-media-upload.vue",
"selector": ".media-photo-tile",
"reason": "仅作为照片缩略图角标与删除操作的精确局部边界"
},
{
"file": "pages/family/f09-media-upload.vue",
"selector": ".media-photo-order, .media-photo-current, .media-photo-status",
"reason": "依附照片缩略图的顺序与状态角标"
},
{
"file": "pages/family/f09-media-upload.vue",
"selector": ".media-photo-remove",
"reason": "依附照片缩略图右上角的删除操作"
},
{
"file": "pages/tree/t01-tree-overview.vue",
"selector": ".member-sheet",
"reason": "用户选择节点后显示的固定底部详情抽屉"
},
{
"file": "pages/tree/t01-tree-overview.vue",
"selector": ".member-sheet__skin",
"reason": "固定底部详情抽屉内部的装饰框层"
},
{
"file": "pages/auth/a01-entry.vue",
"selector": ".feedback-toast",
"reason": "登录页面跨内容轻提示"
},
{
"file": "pages/auth/a01-entry.vue",
"selector": ".verification-layer",
"reason": "用户触发的验证码弹窗遮罩"
},
{
"file": "pages/auth/a04-register.vue",
"selector": ".feedback-toast",
"reason": "注册页面跨内容轻提示"
},
{
"file": "pages/auth/a05-reset-password.vue",
"selector": ".feedback-toast",
"reason": "重置密码页面跨内容轻提示"
},
{
"file": "pages/auth/a05-reset-password.vue",
"selector": ".success-layer",
"reason": "用户触发的重置成功弹窗遮罩"
},
{
"file": "pages/auth/a06-auth-status.vue",
"selector": ".recovery-layer",
"reason": "用户触发的账号恢复弹窗遮罩"
},
{
"file": "pages/genealogy/g01-my-genealogies.vue",
"selector": ".add-dialog-layer",
"reason": "用户触发的固定底部添加家谱弹层"
},
{
"file": "pages/genealogy/g01-my-genealogies.vue",
"selector": ".genealogy-switcher-layer",
"reason": "用户触发的固定家谱切换弹窗遮罩"
},
{
"file": "pages/genealogy/g10-application-review.vue",
"selector": ".review-feedback",
"reason": "审核操作后跨内容显示的固定轻提示"
},
{
"file": "pages/genealogy/g03-create-genealogy.vue",
"selector": ".duplicate-reminder-layer, .flow-success-layer",
"reason": "用户触发的重复提醒与成功弹窗遮罩"
},
{
"file": "pages/genealogy/g11-genealogy-settings.vue",
"selector": ".settings-feedback",
"reason": "保存设置后跨内容显示的固定轻提示"
},
{
"file": "pages/genealogy/g12-generation-poems.vue",
"selector": ".poem-feedback",
"reason": "保存字辈后跨内容显示的固定轻提示"
},
{
"file": "components/AppDialog.vue",
"selector": ".app-dialog-layer",
"reason": "全屏弹窗遮罩层"
},
{
"file": "components/AppToast.vue",
"selector": ".app-toast",
"reason": "跨页面轻提示"
},
{
"file": "components/AuthPageShell.vue",
"selector": ".auth-shell__header",
"reason": "为顶部品牌装饰建立局部定位上下文"
},
{
"file": "components/AuthPageShell.vue",
"selector": ".auth-shell__seal",
"reason": "仅定位顶部品牌区内的固定比例印章装饰"
}
]
+1 -1
View File
@@ -14,7 +14,7 @@ foreach ($file in $files) {
$relative = $file.FullName.Substring($root.Length + 1).Replace('\', '/')
$source = [System.IO.File]::ReadAllText($file.FullName, [System.Text.Encoding]::UTF8)
foreach ($styleMatch in [regex]::Matches($source, '(?s)<style\b[^>]*>(?<css>.*?)</style>')) {
$css = $styleMatch.Groups['css'].Value
$css = [regex]::Replace($styleMatch.Groups['css'].Value, '(?m)^\s*@(use|forward|import)\b[^;]+;\s*', '')
foreach ($rule in [regex]::Matches($css, '(?s)(?<selector>[^{}]+)\{(?<body>[^{}]*)\}')) {
$body = [regex]::Replace($rule.Groups['body'].Value, '(?s)/\*.*?\*/', '')
$positionMatches = [regex]::Matches($body, '(?im)(?<![-\w])position\s*:\s*(?<value>[^;{}]+?)\s*;')
+12 -3
View File
@@ -1,16 +1,25 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent
$page = [System.IO.File]::ReadAllText((Join-Path $root 'pages/family/f01-family-feed.vue'), [System.Text.Encoding]::UTF8)
$profiles = [System.IO.File]::ReadAllText((Join-Path $root 'styles/adaptive-frame-profiles.scss'), [System.Text.Encoding]::UTF8)
foreach ($expected in @(
'box-sizing: border-box',
'width: 514rpx',
'background: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png") center / 100% 100% no-repeat',
'background: url("/static/assets/modules/family/transparent/f01-family-letter-card.png") center / 100% 100% no-repeat',
'background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png") center / 100% 100% no-repeat'
'@use "../../styles/adaptive-frame-profiles.scss" as adaptive;',
'@include adaptive.adaptive-scroll-button(secondary);',
'@include adaptive.adaptive-family-letter;',
'@include adaptive.adaptive-scroll-button(primary);'
)) {
if (-not $page.Contains($expected)) { throw "F01 document-flow contract missing: $expected" }
}
foreach ($expected in @(
'@mixin adaptive-family-letter',
'"/static/assets/modules/family/transparent/f01-family-letter-card.png"'
)) {
if (-not $profiles.Contains($expected)) { throw "Adaptive profile contract missing: $expected" }
}
if ($page -match '100%\s+100%\s+no-repeat|border-image-slice\s*:') { throw 'F01 must consume adaptive frame geometry without page-local stretching or slicing' }
if ($page -match '<image\s+(?:[^>]*\s)?class="(?:feed-shortcuts__skin|feed-card__skin)"|feed-state-card\s*>\s*<image|feed-action[^>]*>\s*<image') { throw 'F01 decorative skins must be container backgrounds' }
if ($page -match 'position\s*:') { throw 'F01 must keep ordinary content in document flow' }
+4 -2
View File
@@ -1,19 +1,21 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$page = Get-Content -LiteralPath (Join-Path $root 'pages/family/f01-family-feed.vue') -Raw -Encoding utf8
$profiles = Get-Content -LiteralPath (Join-Path $root 'styles/adaptive-frame-profiles.scss') -Raw -Encoding utf8
function Assert-Match([string]$Pattern, [string]$Message) {
if ($page -notmatch $Pattern) { throw $Message }
}
Assert-Match 'modules/family/transparent/f01-family-letter-card\.png' 'F01 must use its dedicated transparent family-letter card.'
Assert-Match '@include\s+adaptive\.adaptive-family-letter\s*;' 'F01 must consume its dedicated adaptive family-letter profile.'
if ($profiles -notmatch '(?s)@mixin\s+adaptive-family-letter\s*\{.*?modules/family/transparent/f01-family-letter-card\.png') { throw 'The adaptive profile owner must retain F01 dedicated family-letter artwork.' }
if ($page -match 'application-status-card\.png') { throw 'F01 must not reuse the application status card.' }
Assert-Match 'class="feed-card__title"' 'F01 must expose an explicit feed title hierarchy.'
Assert-Match 'class="feed-card__meta"' 'F01 must expose category and time as secondary metadata.'
Assert-Match 'class="feed-card__summary"' 'F01 must expose feed summary copy.'
Assert-Match 'class="feed-card__author"' 'F01 must expose the author as tertiary information.'
Assert-Match '(?s)\.feed-shortcut\s*\{[^}]*min-height:\s*44px;' 'F01 shortcuts must preserve a 44 CSS px touch height.'
Assert-Match 'a01-scroll-secondary-v3\.png' 'F01 shortcuts must share one visible navigation skin.'
Assert-Match '@include\s+adaptive\.adaptive-scroll-button\(secondary\)\s*;' 'F01 shortcuts must share the adaptive secondary navigation profile.'
Assert-Match '(?s)\.feed-shortcuts\s*\{[^}]*grid-template-columns:\s*repeat\(4,\s*minmax\(0,\s*1fr\)\);' 'F01 shortcuts must use one four-column equal-width navigation strip.'
if ($page -match 'position\s*:') { throw 'F01 must keep ordinary content in document flow.' }
+6 -3
View File
@@ -3,12 +3,15 @@ $root = Split-Path $PSScriptRoot -Parent
$page = [System.IO.File]::ReadAllText((Join-Path $root 'pages/family/f02-publish-feed.vue'), [System.Text.Encoding]::UTF8)
foreach ($expected in @(
'background: url("/static/assets/modules/family/transparent/module-content-frame.png") center / 100% 100% no-repeat',
'background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat',
'min-height: min(640px, calc((100vw - 16px) * 1.48))'
'@include adaptive.adaptive-family-content;',
'@include adaptive.adaptive-family-field;'
)) {
if (-not $page.Contains($expected)) { throw "F02 document-flow contract missing: $expected" }
}
if ($page -notmatch '(?s)<textarea[^>]*auto-height') { throw 'F02 publish textarea must grow from its content' }
$style = [regex]::Match($page, '(?s)<style[^>]*>(.*?)</style>').Groups[1].Value
$panelStyle = [regex]::Match($style, '(?ms)^\s*\.publish-panel\s*\{(?<body>.*?)\}').Groups['body'].Value
if ($panelStyle -match '\bmin-height\s*:') { throw 'F02 publish panel height must be driven by its current content' }
if ($page -match '<image\s+(?:[^>]*\s)?class="publish-panel__skin"|publish-field[^>]*>\s*<image') { throw 'F02 decorative frames must be container backgrounds' }
if ($page -match 'position\s*:') { throw 'F02 must keep panel, form, result, and field in document flow' }
+5 -4
View File
@@ -22,6 +22,7 @@ foreach ($expected in @(
'v-model="form.title"',
'v-model="form.category"',
'v-model="form.content"',
'auto-height',
'fieldErrors.title',
'fieldErrors.category',
'fieldErrors.content',
@@ -41,10 +42,8 @@ foreach ($expected in @(
'ModulePageBackground',
'AppLoading',
'AppButton',
'border-image-source: url("/static/assets/modules/family/transparent/module-content-frame.png");',
'border-image-slice: 72 fill;',
'border-image-width: 64rpx;',
'border-image-repeat: stretch;'
'@include adaptive.adaptive-family-panel;',
'@include adaptive.adaptive-family-field;'
)) {
Assert-Contains $page $expected "F06 dedicated editor contract missing: $expected"
}
@@ -53,6 +52,8 @@ foreach ($forbidden in @('<template><ModulePage', 'import ModulePage from', 'pag
if ($page.Contains($forbidden)) { throw "F06 retains forbidden dependency: $forbidden" }
}
if ($page -match 'border-image-slice\s*:') { throw 'F06 must consume border-image geometry from the adaptive profile owner' }
if ($page -match 'position\s*:') { throw 'F06 must keep editor content in document flow' }
if ($catalog -match '(?m)^\s*f06\s*:') { throw 'F06 obsolete page-catalog owner must be removed' }
+3
View File
@@ -26,6 +26,8 @@ foreach ($expected in @(
'class="media-photo-editor"',
'class="media-photo-editor__thumb"',
'class="media-photo-editor__position"',
'<textarea',
'auto-height',
'const MAX_PHOTOS = 9;',
'const selectMockPhotos = () =>',
'const selectPhoto = (index) =>',
@@ -71,6 +73,7 @@ foreach ($requiredPosition in @(
if ($page -notmatch $requiredPosition) { throw "F09 required thumbnail overlay boundary missing: $requiredPosition" }
}
if ([regex]::Matches($page, 'position\s*:').Count -ne 3) { throw 'F09 must keep only thumbnail badges and their local photo boundary positioned' }
if ([regex]::Matches($page, '<textarea(?=[^>]*\sauto-height(?:\s|>))').Count -ne 2) { throw 'F09 batch and per-photo descriptions must both grow with content' }
if ($catalog -match '(?m)^\s*f09\s*:') {
throw 'F09 obsolete page-catalog owner must be removed'
+5 -3
View File
@@ -19,12 +19,14 @@ foreach ($expected in @(
'/pages/family/f01-family-feed',
'ModulePageBackground',
'PageHeader',
'AppButton'
'AppButton',
'@use "../../styles/adaptive-frame-profiles.scss" as adaptive;',
'@include adaptive.adaptive-family-panel;'
)) {
Assert-Contains $page $expected "F10 contract missing: $expected"
}
foreach ($forbidden in @('<ModulePage page-id="f10"', 'import ModulePage from')) {
foreach ($forbidden in @('<ModulePage page-id="f10"', 'import ModulePage from', 'class="video-status-card__skin"', 'module-content-frame.png')) {
if ($page.Contains($forbidden)) { throw "F10 retains forbidden dependency: $forbidden" }
}
@@ -32,7 +34,7 @@ if ($page -match '(?m)^\s*position\s*:\s*relative\s*;') {
throw 'F10 ordinary page content must not use position: relative'
}
foreach ($selector in @('.video-status-lead text', '.video-status-card__body')) {
foreach ($selector in @('.video-status-lead text')) {
$escapedSelector = [regex]::Escape($selector)
if ($page -notmatch "(?s)$escapedSelector\s*\{[^}]*z-index\s*:\s*1\s*;") {
throw "F10 grid content must render above its decorative skin: $selector"
@@ -5,8 +5,11 @@ $editor = [System.IO.File]::ReadAllText((Join-Path $root 'pages/family/f06-artic
if ($publish -match 'position\s*:') { throw 'F02 must keep its editor in document flow' }
if ($editor -match 'position\s*:') { throw 'F06 must keep its editor in document flow' }
if (-not $publish.Contains('background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat')) { throw 'F02 field frame must be a container background' }
if (-not $editor.Contains('background: url("/static/assets/modules/family/transparent/module-field-frame.png") center / 100% 100% no-repeat')) { throw 'F06 field frame must be a container background' }
if (-not $publish.Contains('@include adaptive.adaptive-family-field;')) { throw 'F02 field frame must come from the adaptive profile owner' }
if (-not $editor.Contains('@include adaptive.adaptive-family-field;')) { throw 'F06 field frame must come from the adaptive profile owner' }
if ($publish -notmatch '(?s)<textarea[^>]*\sauto-height(?:\s|>)') { throw 'F02 long-form textarea must grow with its content' }
if ($editor -notmatch '(?s)<textarea[^>]*\sauto-height(?:\s|>)') { throw 'F06 long-form textarea must grow with its content' }
if ($publish -match '100%\s+100%\s+no-repeat' -or $editor -match '100%\s+100%\s+no-repeat') { throw 'Family editors must not stretch decorative backgrounds' }
if ($editor -match '<view[^>]+class="editor-control[^>]*>\s*<image') { throw 'F06 control must not retain a positioned decorative image' }
Write-Output 'FAMILY-EDITORS-DOCUMENT-FLOW-CONTRACT PASS'
@@ -2,9 +2,12 @@ $ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent
$header = [System.IO.File]::ReadAllText((Join-Path $root 'components/PageHeader.vue'), [System.Text.Encoding]::UTF8)
$tabbar = [System.IO.File]::ReadAllText((Join-Path $root 'components/AppTabbar.vue'), [System.Text.Encoding]::UTF8)
$global = [System.IO.File]::ReadAllText((Join-Path $root 'styles/global.scss'), [System.Text.Encoding]::UTF8)
if ($header -notmatch 'class="page-header-slot"') { throw 'PageHeader must reserve normal-flow space for the fixed header' }
if ($header -notmatch '(?s)\.page-header\s*\{[^}]*position\s*:\s*fixed\s*;[^}]*top\s*:\s*0\s*;[^}]*right\s*:\s*0\s*;[^}]*left\s*:\s*0\s*;') { throw 'PageHeader must be fixed to the top viewport edge' }
if ($header -notmatch '(?s)\.page-header\s*\{[^}]*transform\s*:\s*translateZ\(0\)\s*;') { throw 'PageHeader must own a stable compositor layer above native media while scrolling' }
if ($global -notmatch '(?s)view:not\(\.page-header-slot\):has\(> \.page-header-slot\)\s*\{[^}]*position\s*:\s*relative\s*;[^}]*z-index\s*:\s*31\s*;') { throw 'Every direct PageHeader host must outrank later page content stacking contexts' }
if ($tabbar -notmatch '(?s)\.app-tabbar\s*\{[^}]*position\s*:\s*fixed\s*;[^}]*right\s*:\s*0\s*;[^}]*bottom\s*:\s*0\s*;[^}]*left\s*:\s*0\s*;') { throw 'AppTabbar must remain fixed to the bottom viewport edge' }
Write-Output 'FIXED-NAVIGATION-POSITION-CONTRACT PASS'
+2 -1
View File
@@ -11,6 +11,7 @@ foreach ($sourceFile in $sourceFiles) {
foreach ($match in $matches) {
$assetUrl = $match.Value
if ($assetUrl.Contains('${')) { continue }
$assetPath = Join-Path $root $assetUrl.TrimStart('/')
$relativeSource = $sourceFile.FullName.Substring($root.Length + 1)
@@ -23,7 +24,7 @@ foreach ($sourceFile in $sourceFiles) {
$errors.Add("Legacy asset path $assetUrl referenced by $relativeSource")
}
if ($assetUrl -match '(-v[0-9]+|-source)\.') {
if ($assetUrl -match '-source\.') {
$errors.Add("Temporary asset name $assetUrl referenced by $relativeSource")
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ foreach ($entry in $pages.GetEnumerator()) {
Assert-Match -Content $pages.G05 -Pattern 'v-else-if="overviewState === ''loading''"\s+class="overview-state overview-state--loading"' -Message 'G05 must retain its transparent framed loading branch'
Assert-Match -Content $pages.G05 -Pattern '(?s)<AppLoading\s+[^>]*description="[^"]+"[^>]*/>' -Message 'G05 must render AppLoading with a loading description'
Assert-Match -Content $pages.G06 -Pattern '<AppLoading\s+v-if="searchState === ''loading''"\s+variant="section"[^>]+description="[^"]+"\s*/>' -Message 'G06 search results must render the section AppLoading variant'
Assert-Match -Content $pages.G06 -Pattern 'requestedState\s*===\s*["'']loading["'']' -Message 'G06 must expose its section loading state for visual review'
Assert-Match -Content $pages.G06 -Pattern 'query\?\.state\s*===\s*["'']loading["'']' -Message 'G06 must expose its native section loading state for visual review'
Assert-Match -Content $pages.G09 -Pattern '<AppLoading\s+v-else-if="applicationState === ''loading''"[^>]+description="[^"]+"\s*/>' -Message 'G09 must render a page AppLoading branch'
Assert-Match -Content $pages.G10 -Pattern '<AppLoading\s+v-else-if="reviewState === ''loading''"[^>]+description="[^"]+"\s*/>' -Message 'G10 must render a page AppLoading branch'
Assert-Match -Content $pages.G11 -Pattern '<AppLoading\s+v-if="settingsState === ''loading''"[^>]+description="[^"]+"\s*/>' -Message 'G11 must render a page AppLoading branch'
+5 -5
View File
@@ -30,12 +30,12 @@ if ($paths -contains 'pages/genealogy/g02-empty-genealogies') { throw 'G02 route
foreach ($required in @(
'forceEmptyState',
'query.get("state") || "default"',
'query?.state',
'empty-create-action',
'empty-search-action',
'empty-invite-action',
'empty-create-note',
'g01-empty-panel-frame.png',
'@include adaptive.adaptive-genealogy-state-panel;',
'const createGenealogy = () =>',
'/pages/genealogy/g03-create-genealogy',
'/pages/genealogy/g06-search-genealogies',
@@ -59,14 +59,14 @@ foreach ($required in @(
Assert-Match -Content $g01 -Pattern '(?s)<view class="genealogy-fixed-zone">.*class="current-slip".*class="shortcut-grid".*class="section-divider".*</view>\s*<scroll-view[^>]*class="genealogy-list-scroll"[^>]*>.*class="genealogy-lower".*</scroll-view>' -Message 'G01 fixed and scrolling regions are not separated correctly'
Assert-Match -Content $g01 -Pattern '(?s)\.genealogy-index--split\s*\{[^}]*height:\s*100vh;[^}]*padding-bottom:\s*calc\(112rpx\s*\+\s*env\(safe-area-inset-bottom\)\)' -Message 'G01 split layout must reserve the fixed tabbar and safe area'
Assert-Match -Content $g01 -Pattern '(?s)\.genealogy-list-scroll\s*\{[^}]*height:\s*0;[^}]*flex:\s*1;' -Message 'G01 list scroll region must consume the remaining computed height'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-panel\s*\{[^}]*background:\s*url\("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame\.png"\)\s*center\s*/\s*100%\s+100%\s+no-repeat;' -Message 'G01 empty state must render the exact extracted transparent frame as its container background'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-panel\s*\{[^}]*@include\s+adaptive\.adaptive-genealogy-state-panel;' -Message 'G01 empty state must consume the shared adaptive state-panel profile'
Assert-Match -Content $g01 -Pattern '(?s)\.genealogy-empty-state\s*\{[^}]*min-height:\s*1120rpx;' -Message 'G01 empty state does not reserve the approved visibly fuller height'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-panel\s*\{[^}]*height:\s*1120rpx;' -Message 'G01 empty panel does not use the approved visibly fuller height'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-panel\s*\{[^}]*min-height:\s*1120rpx;' -Message 'G01 empty panel does not retain the approved minimum height'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-panel__content\s*\{[^}]*justify-content:\s*center;[^}]*padding:\s*50rpx 28rpx 46rpx;' -Message 'G01 empty panel content is not centered with the approved responsive padding'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-seal\s*\{[^}]*width:\s*120rpx;[^}]*height:\s*184rpx;' -Message 'G01 empty seal does not use the approved prominent size'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-title\s*\{[^}]*margin-top:\s*30rpx;[^}]*font-size:\s*48rpx;' -Message 'G01 empty title does not use the approved prominent size and spacing'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-copy\s*\{[^}]*min-height:\s*84rpx;[^}]*margin-top:\s*18rpx;[^}]*font-size:\s*28rpx;' -Message 'G01 empty guidance does not use the approved prominent size and spacing'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-search-action,\s*\.empty-invite-action\s*\{[^}]*width:\s*560rpx;[^}]*height:\s*124rpx;' -Message 'G01 empty actions do not use the approved prominent size'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-search-action,\s*\.empty-invite-action\s*\{[^}]*width:\s*560rpx;[^}]*min-height:\s*124rpx;' -Message 'G01 empty actions do not retain the approved prominent minimum size'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-search-action__copy,\s*\.empty-invite-action__copy\s*\{[^}]*font-size:\s*34rpx;' -Message 'G01 empty action labels do not use the approved prominent size'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-create-action\s*\{[^}]*min-height:\s*76rpx;[^}]*margin-top:\s*14rpx;' -Message 'G01 empty create action does not use the approved prominent hit area and spacing'
Assert-Match -Content $g01 -Pattern '(?s)\.empty-create-action__copy\s*\{[^}]*font-size:\s*31rpx;' -Message 'G01 empty create action does not use the approved prominent size'
+2 -2
View File
@@ -13,9 +13,9 @@ if (-not $errorBlock) { throw 'G01 error state block missing' }
foreach ($required in @('error-panel__seal', 'brand-seal.png', 'error-panel__divider', 'section-divider.png', 'state-retry', '@click="retryLoad"')) {
if ($errorBlock -notmatch [regex]::Escape($required)) { throw "G01 error state missing approved contract: $required" }
}
Assert-Match -Content $g01 -Pattern '(?s)\.state-panel--error\s*\{[^}]*background:\s*url\("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame\.png"\)\s*center\s*/\s*100%\s+100%\s+no-repeat;' -Message 'G01 error state must use the transparent frame as its container background'
Assert-Match -Content $g01 -Pattern '(?s)\.state-panel--error\s*\{[^}]*@include\s+adaptive\.adaptive-genealogy-state-panel;' -Message 'G01 error state must consume the shared adaptive state-panel profile'
Assert-Match -Content $g01 -Pattern '(?s)\.state-retry\s*\{[^}]*background:\s*url\("/static/assets/foundation/transparent/a01-scroll-primary-v3\.png"\)\s*center\s*/\s*contain\s+no-repeat;' -Message 'G01 retry must use the approved scroll skin as a container background'
Assert-Match -Content $g01 -Pattern '(?s)\.state-panel--error\s*\{[^}]*height:\s*1120rpx;' -Message 'G01 error state must use the approved full-height frame slot'
Assert-Match -Content $g01 -Pattern '(?s)\.state-panel--error\s*\{[^}]*min-height:\s*1120rpx;' -Message 'G01 error state must retain the approved minimum frame height'
Assert-Match -Content $g01 -Pattern '(?s)\.error-panel__seal\s*\{[^}]*width:\s*132rpx;[^}]*height:\s*136rpx;' -Message 'G01 error state seal must use the approved prominent size'
Assert-Match -Content $g01 -Pattern '(?s)\.state-panel--error\s+\.state-title\s*\{[^}]*font-size:\s*42rpx;' -Message 'G01 error title must use the approved hierarchy'
Assert-Match -Content $g01 -Pattern '(?s)\.state-panel--error\s+\.state-copy\s*\{[^}]*font-size:\s*27rpx;' -Message 'G01 error guidance must use the approved readable size'
+2 -2
View File
@@ -27,8 +27,8 @@ if (Test-Path -LiteralPath $g04) { throw 'G04 page file must be deleted; first-a
foreach ($required in @(
'const currentStep = ref("create");',
'query.get("step") === "ancestor"',
'window.addEventListener("hashchange", syncFlowFromRoute)',
'query?.step === "ancestor"',
'onLoad((query) =>',
'step=ancestor&genealogyId=',
'const createState = ref("form");',
'const ancestorState = ref("form");',
+1 -1
View File
@@ -7,7 +7,7 @@ if ($positions.Count -ne 1 -or $positions[0].Groups[1].Value.Trim() -ne 'fixed')
throw "G03 must retain only its combined fixed dialog layer rule; found $($positions.Count) position declarations"
}
foreach ($required in @(
'background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png")',
'@include adaptive.adaptive-genealogy-state-panel;',
'background: url("/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png")',
'background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")'
)) { if ($page -notmatch [regex]::Escape($required)) { throw "G03 is missing real asset background: $required" } }
+2 -2
View File
@@ -5,9 +5,9 @@ $g05 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g05-genealogy-
function Assert-Match { param([string]$Pattern,[string]$Message); if ($g05 -notmatch $Pattern) { throw $Message } }
Assert-Match 'overview-surface--state' 'G05 system states must own an independent surface'
Assert-Match '(?s)\.overview-state\s*\{[^}]*background:\s*url\("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame\.png"\)' 'G05 system states must reuse the transparent gold frame as its real background'
Assert-Match '(?s)\.overview-state\s*\{[^}]*@include\s+adaptive\.adaptive-genealogy-state-panel;' 'G05 system states must consume the shared adaptive state-panel profile'
Assert-Match '(?s)class="overview-state__seal".*?brand-seal\.png' 'G05 non-loading states must use the approved red-gold seal'
Assert-Match '(?s)\.overview-surface\s*\{[^}]*background:\s*url\("/static/assets/modules/genealogy/opaque/g05-overview-surface\.png"\)' 'G05 ready surface must use the approved overview asset as its real background'
Assert-Match '(?s)\.overview-surface\s*\{[^}]*@include\s+adaptive\.adaptive-g05-overview-surface;' 'G05 ready surface must consume the shared adaptive overview profile'
Assert-Match '(?s)\.overview-state__seal\s*\{[^}]*width:\s*132rpx;[^}]*height:\s*136rpx;' 'G05 state seal must use the approved size'
Assert-Match 'overview-action-lock' 'G05 member mode must explain owner-only actions instead of leaving blank cells'
Assert-Match '(?s)\.overview-actions\s*\{[^}]*min-height:\s*386rpx;' 'G05 actions must retain a minimum two-row surface while allowing data growth'
+2 -2
View File
@@ -13,8 +13,8 @@ if ($positions.Count -ne 0) {
}
foreach ($required in @(
'background: url("/static/assets/modules/genealogy/opaque/g05-overview-surface.png")',
'background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png")',
'@include adaptive.adaptive-g05-overview-surface;',
'@include adaptive.adaptive-genealogy-state-panel;',
'background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")'
)) {
if ($page -notmatch [regex]::Escape($required)) {
+3 -3
View File
@@ -13,9 +13,9 @@ if ($positions.Count -ne 0) {
}
foreach ($required in @(
'background: url("/static/assets/modules/genealogy/opaque/g06-search-input-wide.png")',
'background: url("/static/assets/modules/genealogy/opaque/g06-search-button.png")',
'background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png")',
'@include adaptive.adaptive-g06-search-field;',
'@include adaptive.adaptive-g06-search-action;',
'@include adaptive.adaptive-genealogy-list-card;',
'background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")'
)) {
if ($styleWithoutComments -notmatch [regex]::Escape($required)) {
+2 -2
View File
@@ -5,8 +5,8 @@ $style = [regex]::Match($page, '(?s)<style[^>]*>(.*?)</style>').Groups[1].Value
$positions = [regex]::Matches([regex]::Replace($style, '(?s)/\*.*?\*/', ''), '(?m)\bposition\s*:\s*([^;]+);')
if ($positions.Count -ne 0) { throw "G08 join flow must use document flow; found $($positions.Count) position declarations" }
foreach ($required in @(
'background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png")',
'background: url("/static/assets/modules/genealogy/transparent/g-form-field-frame.png")',
'@include adaptive.adaptive-genealogy-state-panel;',
'@include adaptive.adaptive-genealogy-form-field;',
'background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")'
)) { if ($page -notmatch [regex]::Escape($required)) { throw "G08 is missing real asset background: $required" } }
if ($page -match 'class="join-panel__skin"') { throw 'G08 must not retain the decorative panel image layer' }
+1 -1
View File
@@ -5,7 +5,7 @@ $style = [regex]::Match($page, '(?s)<style[^>]*>(.*?)</style>').Groups[1].Value
$positions = [regex]::Matches([regex]::Replace($style, '(?s)/\*.*?\*/', ''), '(?m)\bposition\s*:\s*([^;]+);')
if ($positions.Count -ne 0) { throw "G09 application list must use document flow; found $($positions.Count) position declarations" }
foreach ($required in @(
'background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png")',
'@include adaptive.adaptive-genealogy-list-card;',
'background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")'
)) { if ($page -notmatch [regex]::Escape($required)) { throw "G09 is missing real asset background: $required" } }
if ($page -match 'class="application-card__skin"') { throw 'G09 must not retain decorative card image layers' }
+13 -1
View File
@@ -5,7 +5,19 @@ $review = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g10-applica
foreach ($token in @('query.status', 'WITHDRAWN:', 'previous=withdrawn')) {
if (-not $mine.Contains($token)) { throw "G09 business state missing: $token" }
}
foreach ($token in @('rejectionReason', 'rejectionError', 'class="rejection-field"', 'current.item.rejectionReason')) {
foreach ($token in @(
'rejectionReason',
'rejectionError',
'class="rejection-field"',
'current.item.rejectionReason',
'id="g10-rejection-reason"',
':aria-invalid="!!rejectionError"',
'aria-describedby="g10-rejection-error"',
'id="g10-rejection-error"',
'role="alert"',
':focus="rejectionFocused"',
'await nextTick();'
)) {
if (-not $review.Contains($token)) { throw "G10 rejection contract missing: $token" }
}
Write-Output 'G09-G10-BUSINESS-STATE-CONTRACT PASS'
+7 -3
View File
@@ -7,10 +7,14 @@ if ($positions.Count -ne 1 -or $positions[0].Groups[1].Value.Trim() -ne 'fixed')
throw "G11 must retain only its fixed feedback layer; found $($positions.Count) position declarations"
}
foreach ($required in @(
'background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png")',
'background: url("/static/assets/modules/genealogy/transparent/g-form-field-frame.png")',
'@include adaptive.adaptive-genealogy-state-panel;',
'@include adaptive.adaptive-genealogy-form-field;',
'background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")',
'background: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png")'
)) { if ($page -notmatch [regex]::Escape($required)) { throw "G11 is missing real asset background: $required" } }
)) { if ($page -notmatch [regex]::Escape($required)) { throw "G11 is missing adaptive asset surface: $required" } }
if ($page -notmatch '(?s)<textarea[^>]*auto-height') { throw 'G11 access note must grow from its text data' }
$settingsPanelStyle = [regex]::Match($style, '(?s)\.settings-panel\s*\{(?<body>.*?)\}').Groups['body'].Value
if ($settingsPanelStyle -match '\bmin-height\s*:') {
throw 'G11 settings panel height must be driven by its current content'
}
Write-Output 'G11-DOCUMENT-FLOW-CONTRACT PASS'
+7 -3
View File
@@ -7,9 +7,13 @@ if ($positions.Count -ne 1 -or $positions[0].Groups[1].Value.Trim() -ne 'fixed')
throw "G12 must retain only its fixed feedback layer; found $($positions.Count) position declarations"
}
foreach ($required in @(
'background: url("/static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png")',
'background: url("/static/assets/modules/genealogy/transparent/g-form-field-frame.png")',
'@include adaptive.adaptive-genealogy-state-panel;',
'@include adaptive.adaptive-genealogy-form-field;',
'background: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png")'
)) { if ($page -notmatch [regex]::Escape($required)) { throw "G12 is missing real asset background: $required" } }
)) { if ($page -notmatch [regex]::Escape($required)) { throw "G12 is missing adaptive asset surface: $required" } }
if ($page -notmatch '(?s)<textarea[^>]*auto-height') { throw 'G12 generation poem field must grow from its text data' }
$poemPanelStyle = [regex]::Match($style, '(?s)\.poem-panel\s*\{(?<body>.*?)\}').Groups['body'].Value
if ($poemPanelStyle -match '\bmin-height\s*:') {
throw 'G12 poem panel height must be driven by its current content'
}
Write-Output 'G12-DOCUMENT-FLOW-CONTRACT PASS'
+14 -5
View File
@@ -1,15 +1,16 @@
$ErrorActionPreference = 'Stop'
$source = Get-Content 'pages/profile/m01-profile-home.vue' -Raw -Encoding UTF8
$profiles = Get-Content 'styles/adaptive-frame-profiles.scss' -Raw -Encoding UTF8
$required = @(
'profile-state--ready',
'profile-state--error',
'm01-profile-summary-card.png',
'@include adaptive.adaptive-profile-summary;',
'@include adaptive.adaptive-scroll-button(primary);',
'brand-seal.png',
'auth-divider-knot.png',
'chevron-right.png',
'root-header-hall.png',
'a01-scroll-primary-v3.png',
'a01-icon-lock-v1.png',
'notice.png',
'AppButton',
@@ -38,11 +39,19 @@ if ($source -match '<image\s+(?:[^>]*\s)?class="(?:profile-hero__frame|profile-s
if ($source -match 'position\s*:') { throw 'M01 must keep content and local decoration in flex/grid document flow' }
foreach ($token in @(
'class="profile-hero__content"',
'grid-area: 1 / 1',
'background: url("/static/assets/modules/profile/transparent/m01-profile-summary-card.png") center / 100% 100% no-repeat',
'background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png") center / 100% 100% no-repeat'
'grid-area: 1 / 1'
)) {
if (-not $source.Contains($token)) { throw "M01 missing document-flow token: $token" }
}
if ($profiles -notmatch '(?s)@mixin\s+adaptive-profile-summary\s*\{.*?m01-profile-summary-card\.png') {
throw 'The adaptive profile owner must retain the M01 summary-card artwork'
}
if ($profiles -notmatch '(?s)@mixin\s+adaptive-scroll-button\s*\(\$type\).*?a01-scroll-primary-v3\.png') {
throw 'The adaptive profile owner must retain the primary scroll-button artwork'
}
if ($source -match '100%\s+100%\s+no-repeat|border-image-slice\s*:') {
throw 'M01 must consume adaptive frame geometry without page-local stretching or slicing'
}
Write-Output 'M01-MODULE-BASELINE-CONTRACT PASS'
+1 -1
View File
@@ -57,7 +57,7 @@ if ($component -match '(?im)(?<![-\w])position\s*:') { throw 'ModulePage ordinar
foreach ($rule in @(
'(?s)\.module-page\s*\{[^}]*display:\s*flex;[^}]*flex-direction:\s*column;',
'(?s)\.module-page__header,\s*\.module-page__content,\s*\.module-page__loading\s*\{[^}]*z-index:\s*1;',
'(?s)\.module-lead\s*\{[^}]*background:\s*url\("/static/assets/modules/genealogy/transparent/section-divider\.png"\) center / 100% 100% no-repeat;',
'(?s)\.module-lead\s*\{[^}]*background:\s*url\("/static/assets/modules/genealogy/transparent/section-divider\.png"\) center / 100% auto no-repeat;',
'(?s)\.module-lead\s*\{[^}]*display:\s*flex;[^}]*align-items:\s*center;[^}]*justify-content:\s*center;',
'(?s)\.form-row,\s*\.settings-row\s*\{[^}]*@include\s+adaptive\.adaptive-module-field;',
'(?s)\.list-card,\s*\.detail-card,\s*\.timeline-row,\s*\.status-card\s*\{[^}]*@include\s+adaptive\.adaptive-module-content;'
+7 -6
View File
@@ -1,12 +1,13 @@
$ErrorActionPreference = 'Stop'
$source = Get-Content 'pages/notification/n01-message-center.vue' -Raw -Encoding UTF8
$profiles = Get-Content 'styles/adaptive-frame-profiles.scss' -Raw -Encoding UTF8
$unread = [string]([char]0x672A) + [char]0x8BFB
$read = [string]([char]0x5DF2) + [char]0x8BFB
$markAllRead = [string]([char]0x5168) + [char]0x90E8 + $read
$goToReview = [string]([char]0x524D) + [char]0x5F80 + [char]0x5165 + [char]0x8C31 + [char]0x5BA1 + [char]0x6838
$required = @(
'n01-notice-card.png',
'@include adaptive.adaptive-notification-content;',
'AppLoading',
'AppToast',
'notice-state--loading',
@@ -35,11 +36,11 @@ if ($source -match '<image\s+(?:[^>]*\s)?class="notice-(?:card|state-card)__skin
if ($source -match 'position\s*:') {
throw 'N01 must keep ordinary content in document flow'
}
foreach ($background in @(
'background: url("/static/assets/modules/notification/transparent/n01-notice-card.png") center / 100% 100% no-repeat',
'background: url("/static/assets/modules/notification/transparent/n01-notice-card.png") top center / 100% 220rpx no-repeat'
)) {
if (-not $source.Contains($background)) { throw "N01 missing document-flow background: $background" }
if ($profiles -notmatch '(?s)@mixin\s+adaptive-notification-content\s*\{.*?n01-notice-card\.png') {
throw 'The adaptive profile owner must retain the N01 notice-card artwork'
}
if ($source -match '100%\s+100%\s+no-repeat|border-image-slice\s*:') {
throw 'N01 must consume adaptive frame geometry without page-local stretching or slicing'
}
Write-Output 'N01-MODULE-BASELINE-CONTRACT PASS'
+29
View File
@@ -0,0 +1,29 @@
$ErrorActionPreference = 'Stop'
$activeFiles = @(
'pages/genealogy/g01-my-genealogies.vue',
'pages/genealogy/g03-create-genealogy.vue',
'pages/genealogy/g06-search-genealogies.vue'
)
foreach ($file in $activeFiles) {
$source = Get-Content $file -Raw -Encoding UTF8
foreach ($forbidden in @('location.hash', 'new URLSearchParams', 'hashchange')) {
if ($source.Contains($forbidden)) {
throw "$file must not read native route state through $forbidden"
}
}
if ($source -notmatch 'onLoad\s*\(\s*\(?\s*query\b') {
throw "$file must consume its native query through onLoad(query)"
}
}
$g01 = Get-Content 'pages/genealogy/g01-my-genealogies.vue' -Raw -Encoding UTF8
$g03 = Get-Content 'pages/genealogy/g03-create-genealogy.vue' -Raw -Encoding UTF8
$g06 = Get-Content 'pages/genealogy/g06-search-genealogies.vue' -Raw -Encoding UTF8
if ($g01 -notmatch 'query\?\.state') { throw 'G01 must normalize presentation state from onLoad query' }
if ($g03 -notmatch 'query\?\.step\s*===\s*"ancestor"') { throw 'G03 must whitelist the ancestor step from onLoad query' }
if ($g06 -notmatch 'query\?\.mode\s*===\s*"invite"') { throw 'G06 must whitelist invite mode from onLoad query' }
Write-Output 'NATIVE-ROUTE-QUERY-CONTRACT PASS'
+24
View File
@@ -0,0 +1,24 @@
$ErrorActionPreference = 'Stop'
$owner = 'utils/validation.js'
if (-not (Test-Path $owner)) { throw 'Missing shared password policy owner' }
$validation = Get-Content $owner -Raw -Encoding UTF8
foreach ($token in @('export const PASSWORD_POLICY_MESSAGE', 'export const validatePassword')) {
if (-not $validation.Contains($token)) { throw "Password policy owner missing: $token" }
}
foreach ($page in @(
'pages/auth/a04-register.vue',
'pages/auth/a05-reset-password.vue',
'pages/profile/m04-change-password.vue'
)) {
$source = Get-Content $page -Raw -Encoding UTF8
foreach ($token in @('validatePassword', 'PASSWORD_POLICY_MESSAGE', '@/utils/validation.js')) {
if (-not $source.Contains($token)) { throw "$page does not consume shared password policy: $token" }
}
if ($source -match '\(\?=\.\*\[A-Za-z\]\)|\{8,32\}\$') {
throw "$page must not own a local password policy regex"
}
}
Write-Output 'PASSWORD-POLICY-CONTRACT PASS'
+19
View File
@@ -0,0 +1,19 @@
const assert = require("assert");
const fs = require("fs");
const vm = require("vm");
const filename = "utils/validation.js";
const source = `${fs.readFileSync(filename, "utf8")
.replace(/export const /g, "const ")}\nmodule.exports = { PASSWORD_POLICY_MESSAGE, validatePassword };`;
const sandbox = { module: { exports: {} } };
vm.runInNewContext(source, sandbox, { filename });
const { validatePassword } = sandbox.module.exports;
for (const value of ["", "abc1234", "abcdefgh", "12345678", "a".repeat(33) + "1"]) {
assert.equal(validatePassword(value).valid, false, `${value || "empty"} must fail`);
}
for (const value of ["abc12345", "Family2026!"]) {
assert.equal(validatePassword(value).valid, true, `${value} must pass`);
}
console.log("PASSWORD-POLICY-RUNTIME-SMOKE PASS");
+16 -2
View File
@@ -1,16 +1,30 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot -Parent
$page = [System.IO.File]::ReadAllText((Join-Path $root 'pages/records/r01-people-list.vue'), [System.Text.Encoding]::UTF8)
$profiles = [System.IO.File]::ReadAllText((Join-Path $root 'styles/adaptive-frame-profiles.scss'), [System.Text.Encoding]::UTF8)
foreach ($expected in @(
'grid-template-columns: minmax(0, 1fr) 126rpx',
'class="people-state-card__copy"',
'background: url("/static/assets/modules/records/transparent/r01-search-input-frame.png") center / 100% 100% no-repeat',
'background: url("/static/assets/modules/records/transparent/r01-person-name-card.png") center / 100% 100% no-repeat'
'@include adaptive.adaptive-records-field;',
'@include adaptive.adaptive-records-person;',
'min-height: clamp(92px, 190rpx, 108px)',
'import { onLoad } from "@dcloudio/uni-app";',
'onLoad((query) => {'
)) {
if (-not $page.Contains($expected)) { throw "R01 document-flow contract missing: $expected" }
}
if ($profiles -notmatch '(?s)@mixin\s+adaptive-records-person\s*\{.*?r01-person-name-card\.png.*?padding:\s*28rpx\s+64rpx;') {
throw 'The adaptive profile owner must retain the R01 artwork and content-safe padding'
}
if ($page -match '(?s)\.person-card\s*\{[^}]*(?<!min-)height\s*:|(?s)\.person-card\s*\{[^}]*padding\s*:\s*\d+%') {
throw 'R01 person cards must grow with content and consume profile-owned frame padding'
}
if ($page -match 'location\.hash|new URLSearchParams') {
throw 'R01 review states must use the UniApp onLoad route contract on Android'
}
if ($page -match '<image\s+(?:[^>]*\s)?class="person-card__skin"|people-search\s*>\s*<image|people-result-empty\s*>\s*<image|people-state-card\s*>\s*<image') {
throw 'R01 decorative frames must be container backgrounds instead of positioned image layers'
}
+4 -2
View File
@@ -1,6 +1,7 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$page = Get-Content -LiteralPath (Join-Path $root 'pages/records/r01-people-list.vue') -Raw -Encoding utf8
$profiles = Get-Content -LiteralPath (Join-Path $root 'styles/adaptive-frame-profiles.scss') -Raw -Encoding utf8
function Assert-Match([string]$Pattern, [string]$Message) {
if ($page -notmatch $Pattern) { throw $Message }
@@ -9,8 +10,9 @@ function Assert-Match([string]$Pattern, [string]$Message) {
if ($page -match "import\s+ModulePage\s+from|<ModulePage(?:\s|/|>)") { throw 'R01 must own its page instead of delegating to ModulePage.' }
if ($page -match 'application-status-card\.png') { throw 'R01 must not reuse the application status card.' }
if ($page -match 'people-intro|brand-seal\.png') { throw 'R01 must not repeat a numbered intro block below the page header.' }
Assert-Match 'modules/records/transparent/r01-person-name-card\.png' 'R01 must use its dedicated transparent person name card.'
Assert-Match 'modules/records/transparent/r01-search-input-frame\.png' 'R01 must use its dedicated search frame.'
Assert-Match '@include\s+adaptive\.adaptive-records-person\s*;' 'R01 must consume its dedicated adaptive person-card profile.'
Assert-Match '@include\s+adaptive\.adaptive-records-field\s*;' 'R01 must consume the shared adaptive records field profile.'
if ($profiles -notmatch '(?s)@mixin\s+adaptive-records-person\s*\{.*?modules/records/transparent/r01-person-name-card\.png') { throw 'The adaptive profile owner must retain R01 dedicated person-card artwork.' }
Assert-Match 'class="people-search"' 'R01 must expose its search region.'
Assert-Match 'people-state--empty' 'R01 must expose an empty review state.'
Assert-Match 'people-state--error' 'R01 must expose an error review state.'
+10 -5
View File
@@ -2,12 +2,13 @@ $ErrorActionPreference = 'Stop'
function Read-Utf8([string]$Path) { [System.IO.File]::ReadAllText((Join-Path (Join-Path $PSScriptRoot '..') $Path), [System.Text.Encoding]::UTF8) }
function Assert-Contains([string]$Content,[string]$Expected) { if (-not $Content.Contains($Expected)) { throw "R02 contract missing: $Expected" } }
$page = Read-Utf8 'pages/records/r02-person-detail.vue'
$profiles = Read-Utf8 'styles/adaptive-frame-profiles.scss'
$catalog = Read-Utf8 'data/page-catalog.js'
foreach ($expected in @(
'class="person-detail-page"','person-detail-state--','class="person-identity-card"',
'class="person-edit-action"','class="person-save-action"','class="person-cancel-action"',
'shortFields','longFields','person-field__label','person-long-field__label','const enterEdit = () =>','const savePerson = () =>',
'const returnToPeople = () =>','r01-person-name-card.png','module-content-frame.png','module-field-frame.png',
'const returnToPeople = () =>','@include adaptive.adaptive-records-person;','@include adaptive.adaptive-records-content;','@include adaptive.adaptive-records-field;',
'ModulePageBackground','PageHeader','AppButton','AppLoading','AppToast'
)) { Assert-Contains $page $expected }
foreach ($forbidden in @('<ModulePage page-id="r02"','import ModulePage from')) { if ($page.Contains($forbidden)) { throw "R02 retains forbidden dependency: $forbidden" } }
@@ -21,10 +22,14 @@ if ($page -match '(?im)(?<![-\w])position\s*:') {
foreach ($rule in @(
'(?s)\.person-detail-page\s*\{[^}]*display:\s*flex;[^}]*flex-direction:\s*column;',
'(?s)\.person-detail-header,\.person-detail-loading,\.person-detail-content\s*\{[^}]*z-index:\s*1;',
'(?s)\.person-identity-card\s*\{[^}]*background:\s*url\("/static/assets/modules/records/transparent/r01-person-name-card\.png"\) center / 100% 100% no-repeat;',
'(?s)\.person-archive-card,\.person-long-field,\.person-state-card\s*\{[^}]*background:\s*url\("/static/assets/modules/records/transparent/module-content-frame\.png"\) center / 100% 100% no-repeat;',
'(?s)\.person-field\s*\{[^}]*background:\s*url\("/static/assets/modules/records/transparent/module-field-frame\.png"\) center / 100% 100% no-repeat;'
'(?s)\.person-identity-card\s*\{[^}]*@include\s+adaptive\.adaptive-records-person\s*;',
'(?s)\.person-archive-card,\.person-long-field,\.person-state-card\s*\{[^}]*@include\s+adaptive\.adaptive-records-content\s*;',
'(?s)\.person-field\s*\{[^}]*@include\s+adaptive\.adaptive-records-field\s*;'
)) {
if ($page -notmatch $rule) { throw "R02 document-flow background rule missing: $rule" }
if ($page -notmatch $rule) { throw "R02 adaptive document-flow rule missing: $rule" }
}
foreach ($owner in @('r01-person-name-card.png','module-content-frame.png','module-field-frame.png')) {
if (-not $profiles.Contains($owner)) { throw "R02 adaptive profile owner missing: $owner" }
}
if ($page -match '100%\s+100%\s+no-repeat|border-image-slice\s*:') { throw 'R02 must consume adaptive frame geometry without page-local stretching or slicing' }
Write-Output 'R02-PERSON-DETAIL-CONTRACT PASS'
+4 -4
View File
@@ -4,10 +4,10 @@ $root=Split-Path -Parent $PSScriptRoot
$files=@('pages/family/f01-family-feed.vue','pages/family/f02-publish-feed.vue','pages/notification/n01-message-center.vue','pages/profile/m01-profile-home.vue')
$pages=@{}
foreach($file in $files){$content=Get-Content -LiteralPath(Join-Path $root $file)-Raw -Encoding utf8;if($content-match"@/utils/api\.js|\bappApi\b"){ throw "$file must not connect the API layer" };Assert-Contains $content 'ModulePageBackground' "$file must use its module background";$pages[$file]=$content}
foreach($required in @('feed-state--list','feed-state--empty','feed-state--error','f01-family-letter-card.png','/pages/family/f02-publish-feed')){Assert-Contains $pages['pages/family/f01-family-feed.vue'] $required "Missing F01 contract: $required"}
foreach($required in @('publish-state--form','publish-state--success','publish-state--error','module-content-frame.png','module-field-frame.png','AppButton','AppToast')){Assert-Contains $pages['pages/family/f02-publish-feed.vue'] $required "Missing F02 contract: $required"}
foreach($required in @('notice-state--loading','notice-state--list','notice-state--empty','notice-state--error','n01-notice-card.png','AppButton','AppLoading','AppToast','/pages/genealogy/g10-application-review')){Assert-Contains $pages['pages/notification/n01-message-center.vue'] $required "Missing N01 contract: $required"}
foreach($required in @('feed-state--list','feed-state--empty','feed-state--error','@include adaptive.adaptive-family-letter;','/pages/family/f02-publish-feed')){Assert-Contains $pages['pages/family/f01-family-feed.vue'] $required "Missing F01 contract: $required"}
foreach($required in @('publish-state--form','publish-state--success','publish-state--error','@include adaptive.adaptive-family-content;','@include adaptive.adaptive-family-field;','AppButton','AppToast')){Assert-Contains $pages['pages/family/f02-publish-feed.vue'] $required "Missing F02 contract: $required"}
foreach($required in @('notice-state--loading','notice-state--list','notice-state--empty','notice-state--error','@include adaptive.adaptive-notification-content;','AppButton','AppLoading','AppToast','/pages/genealogy/g10-application-review')){Assert-Contains $pages['pages/notification/n01-message-center.vue'] $required "Missing N01 contract: $required"}
if($pages['pages/notification/n01-message-center.vue']-match'application-status-card\.png'){throw 'N01 must not reuse the genealogy application card'}
foreach($required in @('profile-state--ready','profile-state--error','m01-profile-summary-card.png','brand-seal.png','auth-divider-knot.png','chevron-right.png','AppButton','/pages/profile/m02-edit-profile')){Assert-Contains $pages['pages/profile/m01-profile-home.vue'] $required "Missing M01 contract: $required"}
foreach($required in @('profile-state--ready','profile-state--error','@include adaptive.adaptive-profile-summary;','brand-seal.png','auth-divider-knot.png','chevron-right.png','AppButton','/pages/profile/m02-edit-profile')){Assert-Contains $pages['pages/profile/m01-profile-home.vue'] $required "Missing M01 contract: $required"}
foreach($forbidden in @('m01-profile-notice-card.png','m01-profile-menu-frame.png','g03-create-flow-panel.png','g06-search-input-wide.png','application-status-card.png')){if($pages['pages/profile/m01-profile-home.vue']-match[regex]::Escape($forbidden)){throw "M01 must not reuse repeated or opaque business asset: $forbidden"}}
Write-Output 'ROOT-PAGES-VISUAL-CONTRACT PASS'
@@ -29,7 +29,7 @@ foreach ($token in @(
}
$dialog = Read-Utf8 'components/AppDialog.vue'
foreach ($token in @('role="dialog"', 'aria-modal="true"', 'tabindex="-1"', '@keydown.esc.stop="cancel"', 'max-height: calc(var(--app-viewport-height, 100vh) - 160rpx)', 'overflow-y: auto')) {
foreach ($token in @('role="dialog"', 'aria-modal="true"', 'tabindex="-1"', '@keydown.esc.stop="cancel"', 'max-height: calc(var(--app-viewport-height, 100vh) - 80rpx - env(safe-area-inset-top) - env(safe-area-inset-bottom))', 'overflow-y: auto')) {
if (-not $dialog.Contains($token)) { throw "AppDialog accessibility contract missing: $token" }
}
@@ -43,4 +43,14 @@ foreach ($token in @('role="status"', 'aria-live="polite"', 'aria-busy="true"',
if (-not $loading.Contains($token)) { throw "AppLoading accessibility contract missing: $token" }
}
$tabbar = Read-Utf8 'components/AppTabbar.vue'
foreach ($token in @('role="tablist"', 'role="tab"', ':aria-selected="active === item.key"', 'hover-class="tab-item--pressed"')) {
if (-not $tabbar.Contains($token)) { throw "AppTabbar interaction contract missing: $token" }
}
$card = Read-Utf8 'components/GenealogyCard.vue'
foreach ($token in @('role="button"', ':aria-label="accessibleLabel"', 'hover-class="genealogy-card--pressed"', 'const accessibleLabel = computed(')) {
if (-not $card.Contains($token)) { throw "GenealogyCard interaction contract missing: $token" }
}
Write-Output 'SHARED-INTERACTION-ACCESSIBILITY-CONTRACT PASS'
+10 -3
View File
@@ -14,7 +14,9 @@ foreach ($required in @(
't01-member-drawer.png',
'v-for="connector in lineageConnectors"',
'lineage-pan-cue',
'const initialScrollLeft = ref(90)'
'const treeScrollLeft = ref(90)',
':scroll-left="treeState === ''tree'' ? treeScrollLeft : 0"',
':style="treeState === ''tree'' ? treeMetricsStyle : undefined"'
)) {
if ($page -notmatch [regex]::Escape($required)) { throw "T01 member action asset missing: $required" }
}
@@ -36,17 +38,22 @@ foreach ($rule in @(
'(?s)\.sheet-meta\s*\{[^}]*font-size:\s*22rpx;',
'(?s)\.tree-stage--lineage\s*\{[^}]*display:\s*grid;[^}]*grid-template-columns:\s*190rpx minmax\(0, 1fr\);',
'(?s)\.generation-rail\s*\{[^}]*display:\s*grid;[^}]*width:\s*190rpx;',
'(?s)\.tree-scroll--lineage\s*\{[^}]*min-width:\s*0;',
'(?s)\.tree-scroll--lineage\s*\{[^}]*grid-area:\s*1 / 2;[^}]*min-width:\s*0;',
'(?s)\.lineage-pan-cue\s*\{[^}]*grid-area:\s*1 / 2;',
'const treeMetricsStyle = computed',
'(?s)\.generation-band\s*\{[^}]*display:\s*grid;',
'(?s)\.generation-band image,\s*\.generation-band__copy\s*\{[^}]*grid-area:\s*1 / 1;',
'(?s)\.member-node\s*\{[^}]*display:\s*grid;',
'(?s)\.member-node__skin,\s*\.member-node__copy\s*\{[^}]*grid-area:\s*1 / 1;',
'(?s)\.member-sheet\s*\{[^}]*height:\s*240rpx;',
'(?s)\.member-sheet\s*\{[^}]*min-height:\s*240rpx;',
'(?s)\.sheet-actions\s*\{[^}]*justify-content:\s*center;[^}]*margin-top:\s*auto;',
'(?s)\.sheet-action\s*\{[^}]*width:\s*250rpx;[^}]*min-height:\s*88rpx;'
)) {
if ($page -notmatch $rule) { throw "T01 readable visual rule missing: $rule" }
}
if ($page -match '(?s)<view\s+class="tree-canvas".*?<view class="lineage-pan-cue">') {
throw 'T01 horizontal reading cue must stay in the visible lineage viewport, not inside the wide canvas'
}
Write-Output 'T01-ALL-STATES-VISUAL-CONTRACT PASS'
+11 -2
View File
@@ -60,7 +60,16 @@ foreach ($required in @(
'/pages/tree/t04-add-relative',
'/pages/tree/t06-edit-relationship',
'/pages/tree/t07-member-directory',
'query.genealogyId'
'query.genealogyId',
':id="`tree-member-${member.id}`"',
':scroll-left="treeState === ''tree'' ? treeScrollLeft : 0"',
'@scroll="handleTreeScroll"',
'const currentTreeScrollLeft = ref(90)',
'uni.createSelectorQuery()',
'nodeRect.left + nodeRect.width / 2 - (scrollRect.left + scrollRect.width / 2)',
'const treeHasDrifted = ref(false)',
'const recenterSelectedMember = async () =>',
'class="tree-recenter"'
)) { Assert-Contains $page $required "Missing T01 contract: $required" }
foreach ($forbidden in @(
@@ -95,7 +104,7 @@ foreach ($obsolete in @('const nodeStyle = (member)', 'const generationRowStyle
foreach ($required in @(
'const treeMetrics = computed(',
':style="treeMetricsStyle"',
':style="treeState === ''tree'' ? treeMetricsStyle : undefined"',
':style="generationBandStyle(row)"',
'overflow-y: auto;'
)) { Assert-Contains $page $required "T01 data-driven tree boundary missing: $required" }
+10 -5
View File
@@ -4,9 +4,14 @@ $page = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t03-member-profile
$style = [regex]::Match($page, '(?s)<style[^>]*>(.*?)</style>').Groups[1].Value
if ([regex]::IsMatch($style, '(?m)\bposition\s*:')) { throw 'T03 member profile must use document flow throughout' }
foreach ($required in @(
'background: url("/static/assets/modules/tree/transparent/t01-state-panel.png")',
'background: url("/static/assets/modules/genealogy/transparent/current-seal-frame.png")',
'background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png")',
'background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")'
)) { if ($page -notmatch [regex]::Escape($required)) { throw "T03 is missing real asset background: $required" } }
'@include adaptive.adaptive-tree-panel;',
'@include adaptive.adaptive-tree-field;',
'@include adaptive.adaptive-genealogy-list-card;',
'@include adaptive.adaptive-scroll-button(primary);',
'background: url("/static/assets/modules/genealogy/transparent/current-seal-frame.png")'
)) { if ($page -notmatch [regex]::Escape($required)) { throw "T03 is missing adaptive asset surface: $required" } }
$memberPanelStyle = [regex]::Match($style, '(?ms)^\s*\.member-panel\s*\{(?<body>.*?)\}').Groups['body'].Value
if ($memberPanelStyle -match '\bmin-height\s*:') {
throw 'T03 member panel height must be driven by its current content'
}
Write-Output 'T03-DOCUMENT-FLOW-CONTRACT PASS'
@@ -47,6 +47,11 @@ foreach ($required in @(
'<picker',
'<AppDialog'
)) { Assert-Contains $t04 $required "T04 missing independent add-relative contract: $required" }
$t04Style = [regex]::Match($t04, '(?s)<style[^>]*>(.*?)</style>').Groups[1].Value
$t04PanelStyle = [regex]::Match($t04Style, '(?ms)^\s*\.add-relative-panel\s*\{(?<body>.*?)\}').Groups['body'].Value
if ($t04PanelStyle -match '\bmin-height\s*:') {
throw 'T04 add-relative panel height must be driven by its current content'
}
foreach ($required in @(
'const editState = ref("form")',
@@ -63,6 +68,11 @@ foreach ($required in @(
'<AppDialog',
'onBackPress('
)) { Assert-Contains $t05 $required "T05 missing independent member-edit contract: $required" }
$t05Style = [regex]::Match($t05, '(?s)<style[^>]*>(.*?)</style>').Groups[1].Value
$t05PanelStyle = [regex]::Match($t05Style, '(?ms)^\s*\.edit-member-panel\s*\{(?<body>.*?)\}').Groups['body'].Value
if ($t05PanelStyle -match '\bmin-height\s*:') {
throw 'T05 edit-member panel height must be driven by its current content'
}
foreach ($required in @(
'const relationshipState = ref("form")',
@@ -80,6 +90,11 @@ foreach ($required in @(
'<picker',
'<AppDialog'
)) { Assert-Contains $t06 $required "T06 missing independent relationship-edit contract: $required" }
$t06Style = [regex]::Match($t06, '(?s)<style[^>]*>(.*?)</style>').Groups[1].Value
$t06PanelStyle = [regex]::Match($t06Style, '(?ms)^\s*\.relationship-panel\s*\{(?<body>.*?)\}').Groups['body'].Value
if ($t06PanelStyle -match '\bmin-height\s*:') {
throw 'T06 relationship panel height must be driven by its current content'
}
foreach ($required in @(
'const genealogyId = ref("")',
+6 -2
View File
@@ -1,6 +1,7 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$page = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t07-member-directory.vue') -Raw -Encoding utf8
$profiles = Get-Content -LiteralPath (Join-Path $root 'styles/adaptive-frame-profiles.scss') -Raw -Encoding utf8
function Assert-Match([string]$Pattern, [string]$Message) {
if ($page -notmatch $Pattern) { throw $Message }
@@ -17,8 +18,11 @@ Assert-Match '(?s)\.directory-page\s*\{[^}]*overflow-y:\s*auto;' 'T07 must allow
if ($page -match '(?s)\.directory-page\s*\{[^}]*overflow-x:\s*hidden;') { throw 'T07 must not hide horizontal layout failures at the page root.' }
Assert-Match '(?s)\.directory-search\s*\{[^}]*min-height:\s*44px;' 'T07 search field must preserve a 44 CSS px minimum touch height at every viewport.'
Assert-Match '(?s)\.directory-search__action\s*\{[^}]*min-width:\s*88rpx;[^}]*min-height:\s*44px;' 'T07 search action must preserve a 44 CSS px touch target.'
Assert-Match 'modules/tree/transparent/t07-search-input-frame\.png' 'T07 search must use its dedicated transparent input frame.'
Assert-Match 'modules/genealogy/transparent/list-slip-frame\.png' 'T07 member cards must use the approved transparent list frame.'
Assert-Match '@include adaptive\.adaptive-tree-field;' 'T07 search must consume the shared adaptive tree field profile.'
Assert-Match '@include adaptive\.adaptive-genealogy-list-card;' 'T07 member cards must consume the shared adaptive genealogy list profile.'
foreach ($asset in @('modules/tree/transparent/t07-search-input-frame.png', 'modules/genealogy/transparent/list-slip-frame.png')) {
if ($profiles -notmatch [regex]::Escape($asset)) { throw "Adaptive profiles must own the T07 surface: $asset" }
}
if ($page -match 'g06-search-input-wide\.png|application-status-card\.png') {
throw 'T07 must not reuse opaque search or application-status skins.'
}