param( [int]$BackendPort = 8003, [int]$FrontendPort = 3006, [switch]$SkipBaseline ) $ErrorActionPreference = "Stop" $repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") $frontendDir = Join-Path $repoRoot "frontend" $backendDir = Join-Path $repoRoot "backend" $backendPython = Join-Path $backendDir ".venv\Scripts\python.exe" $timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss" $artifactDir = Join-Path $repoRoot "project-docs\qa-artifacts\phase3-closed-loop\$timestamp" $runtimeDir = Join-Path $artifactDir "runtime" $frontendLog = Join-Path $artifactDir "frontend-dev-server.log" $backendLog = Join-Path $artifactDir "backend-server.log" $sharedAiLog = Join-Path $artifactDir "shared-ai-stub.log" $npmAuditLog = Join-Path $artifactDir "npm-audit-high.json" $pipCheckLog = Join-Path $artifactDir "pip-check.txt" $backendUrl = "http://127.0.0.1:$BackendPort" $webUrl = "http://127.0.0.1:$FrontendPort" $sqliteDbPath = Join-Path $runtimeDir "phase3-contract-platform.db" $sqliteDbUrl = ("sqlite+pysqlite:///" + (($sqliteDbPath -replace "\\", "/"))) $localStorageDir = Join-Path $runtimeDir "storage" $sharedAiPort = 18011 $sharedAiBaseUrl = "http://127.0.0.1:$sharedAiPort" $sharedAiApiPrefix = "/api/v0.1" $sharedAiAppId = "ai-contract-platform" $sharedAiApiKey = "shared-ai-platform-admin-dev-token" New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null New-Item -ItemType Directory -Force -Path $runtimeDir | Out-Null New-Item -ItemType Directory -Force -Path $localStorageDir | Out-Null if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) { throw "gstack is required for all AI-assisted work in this repo." } function Wait-HttpReady { param( [string]$Url, [int]$TimeoutSeconds = 90 ) $deadline = (Get-Date).AddSeconds($TimeoutSeconds) do { try { $response = Invoke-WebRequest -Uri $Url -TimeoutSec 3 if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 500) { return } } catch { Start-Sleep -Seconds 2 } } while ((Get-Date) -lt $deadline) throw "Timed out waiting for $Url" } function Stop-Listener { param([int]$Port) $listener = Get-NetTCPConnection -LocalPort $Port -ErrorAction SilentlyContinue | Where-Object { $_.State -eq "Listen" } | Select-Object -First 1 if ($listener) { Stop-Process -Id $listener.OwningProcess -Force -ErrorAction SilentlyContinue } } function Stop-ExistingNextDevProcess { param([string]$TargetDirectory) $normalizedTarget = $TargetDirectory.ToLowerInvariant() $processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq "node.exe" -and $_.CommandLine -and $_.CommandLine.ToLowerInvariant().Contains($normalizedTarget) -and ( $_.CommandLine -match "next\\dist\\bin\\next" -or $_.CommandLine -match "start-server\.js" ) } foreach ($process in $processes) { Stop-Process -Id $process.ProcessId -Force -ErrorAction SilentlyContinue } } $frontendProcess = $null $backendProcess = $null $sharedAiProcess = $null try { if (-not (Test-Path (Join-Path $frontendDir "node_modules"))) { Push-Location $frontendDir npm.cmd install --no-package-lock if ($LASTEXITCODE -ne 0) { throw "npm install failed" } Pop-Location } if (-not (Test-Path (Join-Path $frontendDir "node_modules\playwright"))) { Push-Location $frontendDir npm.cmd install --no-save --no-package-lock playwright if ($LASTEXITCODE -ne 0) { throw "playwright install failed" } Pop-Location } if (-not $SkipBaseline) { Push-Location $frontendDir npm.cmd run lint if ($LASTEXITCODE -ne 0) { throw "frontend lint failed" } npm.cmd run build if ($LASTEXITCODE -ne 0) { throw "frontend build failed" } npm.cmd audit --audit-level=high --json > $npmAuditLog if ($LASTEXITCODE -ne 0) { throw "frontend npm audit high threshold failed" } Pop-Location Push-Location $repoRoot .\backend\.venv\Scripts\python.exe -m pytest -q backend/tests if ($LASTEXITCODE -ne 0) { throw "backend pytest failed" } .\backend\.venv\Scripts\python.exe -m pip check > $pipCheckLog if ($LASTEXITCODE -ne 0) { throw "backend pip check failed" } Pop-Location } Stop-Listener -Port $BackendPort Stop-ExistingNextDevProcess -TargetDirectory $frontendDir Stop-Listener -Port $FrontendPort Stop-Listener -Port $sharedAiPort $env:PYTHONPATH = $backendDir $env:DATABASE_URL = $sqliteDbUrl $env:FILE_STORAGE_BACKEND = "local" $env:LOCAL_STORAGE_DIR = $localStorageDir $env:SHARED_AI_PLATFORM_BASE_URL = $sharedAiBaseUrl $env:SHARED_AI_PLATFORM_API_PREFIX = $sharedAiApiPrefix $env:SHARED_AI_PLATFORM_APP_ID = $sharedAiAppId $env:SHARED_AI_PLATFORM_API_KEY = $sharedAiApiKey $env:SHARED_AI_PLATFORM_TIMEOUT_SECONDS = "180" $env:SHARED_AI_PLATFORM_LEGAL_REVIEW_ENABLED = "true" $env:SHARED_AI_PLATFORM_LEGAL_REVIEW_FALLBACK_TO_LOCAL = "false" $sharedAiCommand = "`$env:SLICE4_SHARED_AI_STUB_PORT='$sharedAiPort'; node .\\frontend\\scripts\\slice4-shared-ai-stub.mjs *> '$sharedAiLog'" $sharedAiProcess = Start-Process -FilePath "powershell.exe" ` -ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", $sharedAiCommand) ` -WorkingDirectory $repoRoot ` -WindowStyle Hidden ` -PassThru Wait-HttpReady -Url "$sharedAiBaseUrl/health" -TimeoutSeconds 60 @' from app.db import Base, engine import app.models # noqa: F401 Base.metadata.create_all(bind=engine) print("PHASE3_SQLITE_SCHEMA_READY") '@ | & $backendPython - if ($LASTEXITCODE -ne 0) { throw "phase3 sqlite schema bootstrap failed" } $backendCommand = @" `$env:PYTHONPATH='$backendDir' `$env:DATABASE_URL='$sqliteDbUrl' `$env:FILE_STORAGE_BACKEND='local' `$env:LOCAL_STORAGE_DIR='$localStorageDir' `$env:SHARED_AI_PLATFORM_BASE_URL='$sharedAiBaseUrl' `$env:SHARED_AI_PLATFORM_API_PREFIX='$sharedAiApiPrefix' `$env:SHARED_AI_PLATFORM_APP_ID='$sharedAiAppId' `$env:SHARED_AI_PLATFORM_API_KEY='$sharedAiApiKey' `$env:SHARED_AI_PLATFORM_TIMEOUT_SECONDS='180' `$env:SHARED_AI_PLATFORM_LEGAL_REVIEW_ENABLED='true' `$env:SHARED_AI_PLATFORM_LEGAL_REVIEW_FALLBACK_TO_LOCAL='false' & '$backendPython' -m uvicorn app.main:app --host 127.0.0.1 --port $BackendPort *> '$backendLog' "@ $backendProcess = Start-Process -FilePath "powershell.exe" ` -ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", $backendCommand) ` -WorkingDirectory $backendDir ` -WindowStyle Hidden ` -PassThru Wait-HttpReady -Url "$backendUrl/health" -TimeoutSeconds 90 Wait-HttpReady -Url "$backendUrl/api/v1/system/readiness" -TimeoutSeconds 90 $frontendCommand = "`$env:NEXT_PUBLIC_API_BASE_URL='/api/v1'; `$env:BACKEND_BASE_URL='$backendUrl'; npm.cmd run dev -- --hostname 127.0.0.1 --port $FrontendPort *> '$frontendLog'" $frontendProcess = Start-Process -FilePath "powershell.exe" ` -ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", $frontendCommand) ` -WorkingDirectory $frontendDir ` -WindowStyle Hidden ` -PassThru Wait-HttpReady -Url "$webUrl/login" -TimeoutSeconds 120 Push-Location $frontendDir $env:PHASE3_WEB_URL = $webUrl $env:PHASE3_API_URL = "$webUrl/api/v1" $env:PHASE3_HEALTH_URL = "$backendUrl/health" $env:PHASE3_READINESS_URL = "$backendUrl/api/v1/system/readiness" $env:PHASE3_ARTIFACT_DIR = $artifactDir $env:AI_CONTRACT_ADMIN_PASSWORD = "123456" node .\scripts\phase3-closed-loop-e2e.mjs if ($LASTEXITCODE -ne 0) { throw "phase3 closed-loop e2e failed" } Pop-Location Write-Host "PHASE3_CLOSED_LOOP_PASS $artifactDir" } finally { if ($frontendProcess -and -not $frontendProcess.HasExited) { Stop-Process -Id $frontendProcess.Id -Force -ErrorAction SilentlyContinue } if ($backendProcess -and -not $backendProcess.HasExited) { Stop-Process -Id $backendProcess.Id -Force -ErrorAction SilentlyContinue } if ($sharedAiProcess -and -not $sharedAiProcess.HasExited) { Stop-Process -Id $sharedAiProcess.Id -Force -ErrorAction SilentlyContinue } Stop-Listener -Port $FrontendPort Stop-Listener -Port $BackendPort Stop-Listener -Port $sharedAiPort }