60%
This commit is contained in:
@@ -1,140 +1,54 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
function Read-Page([string]$path) { Get-Content -LiteralPath (Join-Path $root $path) -Raw -Encoding UTF8 }
|
||||
|
||||
$contracts = [ordered]@{
|
||||
'pages/records/r03-gift-list.vue' = @('listRelativeRecordFixtures','relativeRecords','openRelative','createRelativePreview','relative-state--empty','openPage(','"R04",','relativeId')
|
||||
'pages/records/r04-gift-editor.vue' = @('findRelativeRecordFixture','relativeForm','validateRelative','localRelativePreview','relativeId','relativeName','eventName','giftAmount','requestBack')
|
||||
'pages/records/r05-ritual-list.vue' = @('listCeremonyFixtures','ceremonies','openCeremony','createCeremonyPreview','ceremony-state--empty','openPage(','"R06",','"R07",')
|
||||
'pages/records/r06-ritual-detail.vue' = @('findCeremonyFixture','listTreeMemberPresentationFixtures','ceremonyDetail','invitees','inviteeUserId','memberOptions','editCeremony','ceremonyId','openPage(','"R07",','ceremony-state--expired')
|
||||
'pages/records/r07-ritual-editor.vue' = @('findCeremonyFixture','ceremonyForm','validateCeremony','localCeremonyPreview','ceremonyId','ceremonyType','ceremonyTitle','requestBack')
|
||||
'pages/records/r08-growth-journal.vue' = @('listGrowthRecordFixtures','findTreeMemberPresentationFixture','growthRecords','localGrowthPreview','recordGrowth','requestBack','timeline-state--empty')
|
||||
'pages/records/r09-life-events.vue' = @('findTreeMemberPresentationFixture','人生事件接口尚未开放','serviceState','requestBack')
|
||||
'pages/records/r10-memo-list.vue' = @('listMemoFixtures','memos','createMemoPreview','localMemoPreview','memoTitle','requestBack','memo-state--empty')
|
||||
'pages/records/r11-merit-records.vue' = @('listMeritRecordFixtures','meritRecords','createMeritPreview','localMeritPreview','donorName','meritTitle','totalContribution','requestBack')
|
||||
'pages/records/r03-gift-list.vue' = @('appApi.getRelativeRecords', 'relativeId', 'openPage("R04"', 'record-list')
|
||||
'pages/records/r04-gift-editor.vue' = @('appApi.createRelativeRecord', 'relativeName', 'mediaOssIds', 'pickAndUploadImage')
|
||||
'pages/records/r05-ritual-list.vue' = @('appApi.getCeremonies', 'ceremonyId', 'openPage("R06"', 'openPage("R07"')
|
||||
'pages/records/r06-ritual-detail.vue' = @('appApi.getCeremonyDetail', 'ceremonyId')
|
||||
'pages/records/r07-ritual-editor.vue' = @('appApi.createCeremony', 'ceremonyType', 'ceremonyTitle')
|
||||
'pages/records/r08-growth-journal.vue' = @('appApi.getGrowthRecords', 'appApi.createGrowthRecord', 'recordId', 'recordTitle', 'view.value = "list"')
|
||||
'pages/records/r09-life-events.vue' = @('hasValidContext', 'returnToRecords')
|
||||
'pages/records/r10-memo-list.vue' = @('appApi.getMemos', 'appApi.createMemo', 'memoId', 'memoTitle', 'view.value = "list"')
|
||||
'pages/records/r11-merit-records.vue' = @('appApi.getMeritRecords', 'appApi.createMeritRecord', 'meritId', 'meritTitle', 'totalAmount')
|
||||
}
|
||||
|
||||
foreach ($entry in $contracts.GetEnumerator()) {
|
||||
$source = Read-Page $entry.Key
|
||||
foreach ($token in $entry.Value) {
|
||||
if (-not $source.Contains($token)) { throw "$($entry.Key) missing R contract: $token" }
|
||||
if (-not $source.Contains($token)) { throw "$($entry.Key) missing current record contract: $token" }
|
||||
}
|
||||
foreach ($shared in @('ModulePageBackground','PageHeader','AppButton')) {
|
||||
foreach ($shared in @('ModulePageBackground', 'PageHeader', 'AppButton')) {
|
||||
if (-not $source.Contains($shared)) { throw "$($entry.Key) missing shared primitive: $shared" }
|
||||
}
|
||||
foreach ($forbidden in @(
|
||||
'uni.navigateTo', 'uni.navigateBack', 'uni.redirectTo', 'uni.reLaunch',
|
||||
'/pages/records/', 'saveResult', 'finishPage(', 'Date.now()', '.unshift(',
|
||||
'giftId', 'ritualId', 'personName=', '已保存', '已删除'
|
||||
)) {
|
||||
if ($source.Contains($forbidden)) { throw "$($entry.Key) retains forbidden fake-write or old-route contract: $forbidden" }
|
||||
foreach ($forbidden in @('Fixture', 'localGrowthPreview', 'localMemoPreview', 'localMeritPreview', 'Date.now()', 'uni.navigateTo', 'uni.navigateBack', 'uni.redirectTo', 'uni.reLaunch')) {
|
||||
if ($source.Contains($forbidden)) { throw "$($entry.Key) retains retired local or direct-navigation contract: $forbidden" }
|
||||
}
|
||||
if ($source -match '<ModulePage(?:\s|/|>)|import\s+ModulePage\s+from') { throw "$($entry.Key) must own its business page" }
|
||||
if ($source -match 'uni\.(showToast|showModal|showLoading|showActionSheet)') { throw "$($entry.Key) must use project feedback primitives" }
|
||||
if ($source -match '(?im)(?<![-\w])position\s*:|overflow\s*:\s*hidden') { throw "$($entry.Key) contains forbidden document-flow declaration" }
|
||||
}
|
||||
|
||||
foreach ($pageAndTarget in @(
|
||||
@{ Path = 'pages/records/r04-gift-editor.vue'; Target = 'R03' },
|
||||
@{ Path = 'pages/records/r06-ritual-detail.vue'; Target = 'R05' },
|
||||
@{ Path = 'pages/records/r07-ritual-editor.vue'; Target = 'R05' }
|
||||
)) {
|
||||
$source = Read-Page $pageAndTarget.Path
|
||||
$safeReturnPattern = '(?s)genealogyId\.value\s*\?\s*returnTo\("{0}".*?:\s*goBack\(\)' -f $pageAndTarget.Target
|
||||
if ($source -notmatch $safeReturnPattern) {
|
||||
throw "$($pageAndTarget.Path) invalid-entry CTA must fall back to goBack without genealogyId"
|
||||
}
|
||||
}
|
||||
|
||||
$r03 = Read-Page 'pages/records/r03-gift-list.vue'
|
||||
$r04 = Read-Page 'pages/records/r04-gift-editor.vue'
|
||||
$r06 = Read-Page 'pages/records/r06-ritual-detail.vue'
|
||||
foreach ($forbiddenCurrency in @('¥', '¥')) {
|
||||
if ($r03.Contains($forbiddenCurrency) -or $r04.Contains($forbiddenCurrency)) {
|
||||
throw "R03/R04 must not invent a currency symbol absent from the backend contract: $forbiddenCurrency"
|
||||
}
|
||||
foreach ($optionalRelativeField in @('relationName', 'eventName', 'eventTime', 'giftAmount', 'recordContent', 'mediaOssIds', 'sortOrder')) {
|
||||
if ($r04 -match "if\s*\(!form\.$optionalRelativeField") { throw "R04 must not make optional backend field required: $optionalRelativeField" }
|
||||
}
|
||||
foreach ($optionalRelativeField in @('relationName', 'eventName', 'eventTime')) {
|
||||
if ($r04 -match "relativeErrors\.$optionalRelativeField\s*=\s*relativeForm\.$optionalRelativeField\.trim\(\)") {
|
||||
throw "R04 must not make optional backend field required: $optionalRelativeField"
|
||||
}
|
||||
}
|
||||
foreach ($invalidAmountRule in @('amount >= 0', 'String(Number(relativeForm.giftAmount))')) {
|
||||
if ($r04.Contains($invalidAmountRule)) {
|
||||
throw "R04 must preserve optional numeric amount semantics without inventing a boundary or string body: $invalidAmountRule"
|
||||
}
|
||||
}
|
||||
foreach ($inventedInvitationField in @('inviteStatusLabel', 'userId:', 'name:', 'relation:')) {
|
||||
if ($r06.Contains($inventedInvitationField)) {
|
||||
throw "R06 must join inviteeUserId to scoped member options and must not retain invented invitation fields: $inventedInvitationField"
|
||||
}
|
||||
}
|
||||
foreach ($previewGuard in @(
|
||||
@{ Path = 'pages/records/r04-gift-editor.vue'; Pattern = 'editorState\.value\s*===\s*"preview"' },
|
||||
@{ Path = 'pages/records/r07-ritual-editor.vue'; Pattern = 'editorState\.value\s*===\s*"preview"' },
|
||||
@{ Path = 'pages/records/r08-growth-journal.vue'; Pattern = 'dirty:\s*Boolean\(localGrowthPreview\.value\)' },
|
||||
@{ Path = 'pages/records/r10-memo-list.vue'; Pattern = 'dirty:\s*Boolean\(localMemoPreview\.value\)' },
|
||||
@{ Path = 'pages/records/r11-merit-records.vue'; Pattern = 'dirty:\s*Boolean\(localMeritPreview\.value\)' }
|
||||
)) {
|
||||
$source = Read-Page $previewGuard.Path
|
||||
if ($source -notmatch $previewGuard.Pattern) {
|
||||
throw "$($previewGuard.Path) must guard page back while a local preview remains unsaved"
|
||||
}
|
||||
}
|
||||
foreach ($dialogPage in @(
|
||||
'pages/records/r04-gift-editor.vue',
|
||||
'pages/records/r07-ritual-editor.vue',
|
||||
'pages/records/r08-growth-journal.vue',
|
||||
'pages/records/r10-memo-list.vue',
|
||||
'pages/records/r11-merit-records.vue'
|
||||
)) {
|
||||
$source = Read-Page $dialogPage
|
||||
$dialogs = [regex]::Matches($source, '(?s)<AppDialog\b.*?>')
|
||||
if ($dialogs.Count -eq 0 -or ($dialogs | Where-Object { $_.Value -notmatch ':close-on-mask="false"' })) {
|
||||
throw "$dialogPage dialogs must ignore mask taps"
|
||||
}
|
||||
}
|
||||
$r11 = Read-Page 'pages/records/r11-merit-records.vue'
|
||||
foreach ($amountContract in @(
|
||||
'type="digit"',
|
||||
'Number.isFinite(Number(amountInput))',
|
||||
'amount: amountInput ? Number(amountInput) : null',
|
||||
'金额数值(单位待确认)',
|
||||
'localMeritPreview.meritType'
|
||||
)) {
|
||||
if (-not $r11.Contains($amountContract)) {
|
||||
throw "R11 optional numeric amount or preview visibility contract missing: $amountContract"
|
||||
}
|
||||
|
||||
foreach ($page in @('pages/records/r08-growth-journal.vue', 'pages/records/r10-memo-list.vue', 'pages/records/r11-merit-records.vue')) {
|
||||
$source = Read-Page $page
|
||||
if ($source -notmatch 'onShow\(') { throw "$page must reread the service when returning to its list" }
|
||||
if ($source -notmatch 'await\s+load') { throw "$page must read back after a successful write" }
|
||||
}
|
||||
|
||||
$r10 = Read-Page 'pages/records/r10-memo-list.vue'
|
||||
foreach ($forbidden in @('toggleMemo', 'memo.done =', '点击标记完成', '点击恢复待办')) {
|
||||
if ($r10.Contains($forbidden)) { throw "R10 must not fake completed-state mutation: $forbidden" }
|
||||
}
|
||||
$r09 = Read-Page 'pages/records/r09-life-events.vue'
|
||||
foreach ($forbidden in @('lifeEvents', 'createLifeEvent', 'saveLifeEvent', 'growth-records', 'LIFE_EVENT')) {
|
||||
if ($r09.Contains($forbidden)) { throw "R09 must remain hard closed without a backend contract: $forbidden" }
|
||||
foreach ($forbidden in @('toggleMemo', 'memo.done =')) {
|
||||
if ($r10.Contains($forbidden)) { throw "R10 must not invent a completed-state mutation: $forbidden" }
|
||||
}
|
||||
|
||||
$runtimeSources = @(
|
||||
'tests/r-business-flow-runtime-smoke.js',
|
||||
'tests/r02-person-detail-runtime-smoke.js',
|
||||
'tests/r02-background-runtime-smoke.js',
|
||||
'tests/module-page-runtime-smoke.js',
|
||||
'tests/module-series-responsive-runtime-smoke.js'
|
||||
) | ForEach-Object { Read-Page $_ }
|
||||
$runtimeText = $runtimeSources -join "`n"
|
||||
foreach ($retired in @('giftId', 'ritualId', 'count=50', 'r02-person-detail?personId=1', 'r08-growth-journal",".timeline-card')) {
|
||||
if ($runtimeText.Contains($retired)) { throw "R runtime smoke retains retired route or fake-volume contract: $retired" }
|
||||
}
|
||||
foreach ($requiredRuntimeFact in @(
|
||||
'genealogyId=1001&mode=view&relativeId=301&sourceKey=R03',
|
||||
'R08 local preview must not enter the official timeline',
|
||||
'R11 local preview must not change the official count',
|
||||
'personId=103',
|
||||
'preview back guard missing'
|
||||
)) {
|
||||
if (-not $runtimeText.Contains($requiredRuntimeFact)) { throw "R runtime smoke misses current contract: $requiredRuntimeFact" }
|
||||
$r09 = Read-Page 'pages/records/r09-life-events.vue'
|
||||
foreach ($forbidden in @('lifeEvents', 'createLifeEvent', 'saveLifeEvent', 'growth-records', 'LIFE_EVENT')) {
|
||||
if ($r09.Contains($forbidden)) { throw "R09 must remain closed without a backend contract: $forbidden" }
|
||||
}
|
||||
|
||||
Write-Output 'R-BUSINESS-FLOW-CONTRACT PASS'
|
||||
|
||||
Reference in New Issue
Block a user