$ErrorActionPreference = 'Stop' $root = Split-Path -Parent $PSScriptRoot $manifestRelativePath = 'design-pipeline/manifests/a01.json' $requiredFiles = @( 'design-pipeline/package.json', $manifestRelativePath, 'design-pipeline/scripts/build-a01.mjs' ) foreach ($relativePath in $requiredFiles) { $absolutePath = Join-Path $root $relativePath if (-not (Test-Path -LiteralPath $absolutePath -PathType Leaf)) { throw "Missing A01 code pipeline file: $relativePath" } } $manifest = Get-Content -LiteralPath (Join-Path $root $manifestRelativePath) -Raw -Encoding UTF8 | ConvertFrom-Json if ($manifest.schemaVersion -ne 1) { throw "Expected A01 pipeline schemaVersion 1, found $($manifest.schemaVersion)." } if ($manifest.canvas.width -ne 412 -or $manifest.canvas.height -ne 915) { throw 'A01 logical canvas must be 412x915.' } $requiredStates = @('password-hidden', 'password-visible', 'sms-default', 'sms-countdown') foreach ($stateName in $requiredStates) { if ($manifest.states.name -notcontains $stateName) { throw "Missing A01 preview state: $stateName" } } $workspacePrefix = [IO.Path]::GetFullPath($root).TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar $runtimePrefix = 'static/assets/modules/auth/' $assetIds = @{} foreach ($asset in $manifest.assets) { if (-not $asset.id -or $assetIds.ContainsKey($asset.id)) { throw "A01 asset id must be present and unique: $($asset.id)" } $assetIds[$asset.id] = $true foreach ($propertyName in @('source', 'output', 'width', 'height', 'alpha', 'maxBytes')) { if ($null -eq $asset.$propertyName -or "$($asset.$propertyName)" -eq '') { throw "A01 asset '$($asset.id)' is missing property: $propertyName" } } if (-not $asset.output.StartsWith($runtimePrefix, [StringComparison]::OrdinalIgnoreCase)) { throw "A01 runtime asset must stay in ${runtimePrefix}: $($asset.output)" } if ($asset.source -ne $asset.output) { throw "A01 portable pipeline must use the committed runtime asset as its stable source: $($asset.id)" } if ($asset.source -match '(?i)candidates|review|generated|node_modules|\.psd$') { throw "A01 portable pipeline source must not depend on local intermediate files: $($asset.source)" } if ($asset.output -match '(?i)review|page-v2|\.psd$') { throw "A01 runtime asset must not use a preview, full-page image, or PSD: $($asset.output)" } $sourcePath = [IO.Path]::GetFullPath((Join-Path $root $asset.source)) $outputPath = [IO.Path]::GetFullPath((Join-Path $root $asset.output)) foreach ($path in @($sourcePath, $outputPath)) { if (-not $path.StartsWith($workspacePrefix, [StringComparison]::OrdinalIgnoreCase)) { throw "A01 pipeline path escaped the workspace: $path" } } if (-not (Test-Path -LiteralPath $sourcePath -PathType Leaf)) { throw "Missing A01 source asset: $($asset.source)" } if (-not (Test-Path -LiteralPath $outputPath -PathType Leaf)) { throw "Missing generated A01 runtime asset: $($asset.output)" } $bytes = [IO.File]::ReadAllBytes($outputPath) if ($bytes.Length -lt 24 -or $bytes[0] -ne 0x89 -or $bytes[1] -ne 0x50 -or $bytes[2] -ne 0x4E -or $bytes[3] -ne 0x47) { throw "Generated A01 asset is not a PNG: $($asset.output)" } $width = [Net.IPAddress]::NetworkToHostOrder([BitConverter]::ToInt32($bytes, 16)) $height = [Net.IPAddress]::NetworkToHostOrder([BitConverter]::ToInt32($bytes, 20)) if ($width -ne $asset.width -or $height -ne $asset.height) { throw "Generated A01 asset has wrong dimensions: $($asset.output) expected $($asset.width)x$($asset.height), found ${width}x${height}." } if ($bytes.Length -gt $asset.maxBytes) { throw "Generated A01 asset exceeds maxBytes: $($asset.output) has $($bytes.Length), limit $($asset.maxBytes)." } $pngColorType = $bytes[25] $hasAlpha = $pngColorType -in @(4, 6) if ([bool]$asset.alpha -ne $hasAlpha) { throw "Generated A01 asset Alpha contract mismatch: $($asset.output)" } } foreach ($preview in $manifest.previews) { $previewPath = Join-Path $root $preview.output if (-not (Test-Path -LiteralPath $previewPath -PathType Leaf)) { throw "Missing generated A01 preview: $($preview.output)" } } $buildReport = Join-Path $root 'design-pipeline/generated/a01/build-report.json' if (-not (Test-Path -LiteralPath $buildReport -PathType Leaf)) { throw 'Missing A01 code pipeline build report.' } $builder = Get-Content -LiteralPath (Join-Path $root 'design-pipeline/scripts/build-a01.mjs') -Raw -Encoding UTF8 foreach ($forbidden in @('Photoshop.Application', '.psd', 'scripts/photoshop')) { if ($builder -match [regex]::Escape($forbidden)) { throw "A01 code pipeline must not depend on Photoshop or PSD: $forbidden" } } Write-Output 'A01-CODE-PIPELINE-CONTRACT PASS'