$ErrorActionPreference = 'Stop' $root = Split-Path $PSScriptRoot -Parent $allowlistPath = Join-Path $PSScriptRoot 'data-driven-layout-risk-allowlist.json' $allowlist = Get-Content -LiteralPath $allowlistPath -Raw -Encoding UTF8 | ConvertFrom-Json $allowed = @{} $seenAllowed = @{} foreach ($entry in $allowlist) { foreach ($required in @('file', 'selector', 'risk', 'reason')) { if (-not $entry.$required) { throw "DATA-DRIVEN-LAYOUT-CONTRACT allowlist entry missing $required" } } $allowed["$($entry.file)::$($entry.selector)::$($entry.risk)"] = $entry.reason } $violations = New-Object System.Collections.Generic.List[string] $files = Get-ChildItem -LiteralPath (Join-Path $root 'pages'), (Join-Path $root 'components') -Recurse -File -Filter '*.vue' function Add-Risk { param( [string]$File, [string]$Selector, [string]$Risk, [string]$Evidence ) $key = "$File::$Selector::$Risk" if ($allowed.ContainsKey($key)) { $seenAllowed[$key] = $true } else { $violations.Add("$File :: $Selector :: $Risk :: $Evidence") } } 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)]*>(?.*?)')) { $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 = (($rule.Groups['selector'].Value -replace '(?s)^.*@media[^\{]*', '') -replace '\s+', ' ').Trim() $body = $rule.Groups['body'].Value if (-not $selector) { continue } $height = [regex]::Match($body, '(?im)(?\d+(?:\.\d+)?(?:rpx|px))\s*;') if ($height.Success) { Add-Risk -File $relative -Selector $selector -Risk 'fixed-content-height' -Evidence "height:$($height.Groups['value'].Value)" } $overflow = [regex]::Match($body, '(?im)(?[^;{}]+)\s*;') if ($gridRows.Success -and $gridRows.Groups['value'].Value -match '(\d+(?:\.\d+)?%|\d+(?:\.\d+)?(?:rpx|px))') { Add-Risk -File $relative -Selector $selector -Risk 'fixed-grid-track' -Evidence "grid-template-rows:$((($gridRows.Groups['value'].Value -replace '\s+', ' ').Trim()))" } $repeat = [regex]::Match($body, '(?im)grid-template-(?:columns|rows)\s*:\s*repeat\(\s*(?\d+)\s*,') if ($repeat.Success -and [int]$repeat.Groups['count'].Value -ge 12) { Add-Risk -File $relative -Selector $selector -Risk 'fixed-capacity-canvas' -Evidence (($repeat.Value -replace '\s+', ' ').Trim()) } } } } $stale = @($allowed.Keys | Where-Object { -not $seenAllowed.ContainsKey($_) } | Sort-Object) if ($stale.Count -gt 0) { $stale | ForEach-Object { Write-Output "STALE ALLOWLIST :: $_" } throw "DATA-DRIVEN-LAYOUT-CONTRACT found $($stale.Count) stale allowlist entries." } if ($violations.Count -gt 0) { $violations | Sort-Object | ForEach-Object { Write-Output $_ } throw "DATA-DRIVEN-LAYOUT-CONTRACT found $($violations.Count) non-allowlisted layout capacity risks." } Write-Output 'DATA-DRIVEN-LAYOUT-CONTRACT PASS'