$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' = @('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"', '"R07"') 'pages/records/r06-ritual-detail.vue' = @('appApi.getCeremonyDetail', 'ceremonyId') 'pages/records/r07-ritual-editor.vue' = @('appApi.createCeremony', 'ceremonyType', 'ceremonyTitle', 'locationAddress') '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 current record contract: $token" } } foreach ($shared in @('ModulePageBackground', 'PageHeader', 'AppButton')) { if (-not $source.Contains($shared)) { throw "$($entry.Key) missing shared primitive: $shared" } } 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 ')|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" } } $r04 = Read-Page 'pages/records/r04-gift-editor.vue' 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 ($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 invent a 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 closed without a backend contract: $forbidden" } } $r07 = Read-Page 'pages/records/r07-ritual-editor.vue' foreach ($forbidden in @('form.longitude', 'form.latitude')) { if ($r07.Contains($forbidden)) { throw "R07 must not expose raw map coordinates without a map picker: $forbidden" } } Write-Output 'R-BUSINESS-FLOW-CONTRACT PASS'