[CmdletBinding()] param( [Parameter(Mandatory = $true)] [int]$StartIteration, [Parameter(Mandatory = $true)] [int]$EndIteration, [string]$ImplId = (Get-Date).ToUniversalTime().ToString('yyyyMMdd'), [int]$StartingBatchId = 0 ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path $webRoot = Join-Path $repoRoot 'src/Web/StellaOps.Web' $sprintRoot = Join-Path $repoRoot 'docs/implplan' $outputRoot = Join-Path $webRoot 'output' function Invoke-External { param( [Parameter(Mandatory = $true)] [string]$FilePath, [string[]]$ArgumentList = @(), [string]$WorkingDirectory = $repoRoot ) Push-Location $WorkingDirectory try { & $FilePath @ArgumentList if ($LASTEXITCODE -ne 0) { throw "Command failed: $FilePath $($ArgumentList -join ' ')" } } finally { Pop-Location } } function Read-JsonFile { param( [Parameter(Mandatory = $true)] [string]$Path ) if (-not (Test-Path $Path)) { throw "Missing JSON file: $Path" } return Get-Content $Path -Raw | ConvertFrom-Json } function Remove-StellaRuntime { $containers = @((docker ps -aq --filter name=stellaops-) | Where-Object { $_ }) if ($containers.Count -gt 0) { docker rm -f @containers | Out-Host } $volumes = @((docker volume ls -q) | Where-Object { $_ -like 'compose_*' -or $_ -like 'stellaops_*' }) if ($volumes.Count -gt 0) { docker volume rm @volumes | Out-Host } foreach ($network in @('stellaops_frontdoor', 'stellaops')) { $exists = @((docker network ls --format '{{.Name}}') | Where-Object { $_ -eq $network }) if ($exists.Count -gt 0) { docker network rm $network | Out-Host } } $images = @((docker images --format '{{.Repository}}:{{.Tag}}') | Where-Object { $_ -like 'stellaops/*:dev' }) if ($images.Count -gt 0) { docker rmi -f @images | Out-Host } } function Get-HealthyContainerSummary { $healthy = @((docker ps --filter name=stellaops- --filter health=healthy --format '{{.Names}}') | Where-Object { $_ }).Count $running = @((docker ps --filter name=stellaops- --format '{{.Names}}') | Where-Object { $_ }).Count return "$healthy/$running" } function Get-HighestBatchId { param( [Parameter(Mandatory = $true)] [string]$SprintImplId ) $pattern = "SPRINT_${SprintImplId}_*_Platform_scratch_iteration_*_full_route_action_audit.md" $files = Get-ChildItem $sprintRoot -Filter $pattern -File -ErrorAction SilentlyContinue $highest = 0 foreach ($file in $files) { if ($file.BaseName -match "^SPRINT_${SprintImplId}_(\d{3})_Platform_scratch_iteration_") { $value = [int]$matches[1] if ($value -gt $highest) { $highest = $value } } } return $highest } function New-SprintState { param( [Parameter(Mandatory = $true)] [int]$Iteration, [Parameter(Mandatory = $true)] [int]$BatchId, [Parameter(Mandatory = $true)] [string]$BaselineCommit ) return [ordered]@{ Iteration = $Iteration BatchId = $BatchId BaselineCommit = $BaselineCommit Status1 = 'DOING' Status2 = 'TODO' Status3 = 'TODO' Criteria11 = $false Criteria12 = $false Criteria13 = $false Criteria21 = $false Criteria22 = $false Criteria23 = $false Criteria31 = $false Criteria32 = $false Criteria33 = $false ExecutionLog = [System.Collections.Generic.List[object]]::new() Decisions = [System.Collections.Generic.List[string]]::new() NextCheckpoints = [System.Collections.Generic.List[string]]::new() } } function Add-SprintLog { param( [Parameter(Mandatory = $true)] [hashtable]$State, [Parameter(Mandatory = $true)] [string]$Update, [Parameter(Mandatory = $true)] [string]$Owner ) $State.ExecutionLog.Add([pscustomobject]@{ Date = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd') Update = $Update Owner = $Owner }) } function Get-CheckboxMark { param( [Parameter(Mandatory = $true)] [bool]$Value ) if ($Value) { return 'x' } return ' ' } function Render-SprintFile { param( [Parameter(Mandatory = $true)] [hashtable]$State ) $iter = '{0:D3}' -f $State.Iteration $batch = '{0:D3}' -f $State.BatchId $lines = [System.Collections.Generic.List[string]]::new() $null = $lines.Add("# Sprint $ImplId" + "_$batch - Platform Scratch Iteration $iter Full Route Action Audit") $null = $lines.Add('') $null = $lines.Add('## Topic & Scope') $null = $lines.Add('- Wipe Stella-owned runtime state again and rerun the documented setup path from zero state.') $null = $lines.Add('- Re-enter the application as a first-time user after bootstrap and rerun the full route, page, and page-action audit with Playwright.') $null = $lines.Add('- Group any newly exposed defects before fixing so the next commit closes a full iteration rather than a single page slice.') $null = $lines.Add('- Working directory: `.`.') $null = $lines.Add('- Expected evidence: wipe proof, setup convergence proof, fresh Playwright route/action evidence, grouped defect list, fixes, and retest results.') $null = $lines.Add('') $null = $lines.Add('## Dependencies & Concurrency') $null = $lines.Add("- Depends on local commit ``$($State.BaselineCommit)`` as the clean baseline for the next scratch cycle.") $null = $lines.Add('- Safe parallelism: none during wipe/setup because the environment reset is global to the machine.') $null = $lines.Add('') $null = $lines.Add('## Documentation Prerequisites') $null = $lines.Add('- `AGENTS.md`') $null = $lines.Add('- `docs/INSTALL_GUIDE.md`') $null = $lines.Add('- `docs/dev/DEV_ENVIRONMENT_SETUP.md`') $null = $lines.Add('- `docs/qa/feature-checks/FLOW.md`') $null = $lines.Add('') $null = $lines.Add('## Delivery Tracker') $null = $lines.Add('') $null = $lines.Add("### PLATFORM-SCRATCH-ITER$($State.Iteration)-001 - Rebuild from zero Stella runtime state") $null = $lines.Add("Status: $($State.Status1)") $null = $lines.Add('Dependency: none') $null = $lines.Add('Owners: QA, 3rd line support') $null = $lines.Add('Task description:') $null = $lines.Add('- Remove Stella-only containers, images, volumes, and the frontdoor network, then rerun the documented setup entrypoint from zero Stella state.') $null = $lines.Add('') $null = $lines.Add('Completion criteria:') $null = $lines.Add("- [$(Get-CheckboxMark -Value $State.Criteria11)] Stella-only Docker state is removed.") $null = $lines.Add("- [$(Get-CheckboxMark -Value $State.Criteria12)] `scripts/setup.ps1` is rerun from zero state.") $null = $lines.Add("- [$(Get-CheckboxMark -Value $State.Criteria13)] The first setup outcome is captured before UI verification starts.") $null = $lines.Add('') $null = $lines.Add("### PLATFORM-SCRATCH-ITER$($State.Iteration)-002 - Re-run the first-user full route/page/action audit") $null = $lines.Add("Status: $($State.Status2)") $null = $lines.Add("Dependency: PLATFORM-SCRATCH-ITER$($State.Iteration)-001") $null = $lines.Add('Owners: QA') $null = $lines.Add('Task description:') $null = $lines.Add('- After scratch setup converges, rerun the canonical route sweep plus the full action audit suite and enumerate every newly exposed issue before repair work begins.') $null = $lines.Add('') $null = $lines.Add('Completion criteria:') $null = $lines.Add("- [$(Get-CheckboxMark -Value $State.Criteria21)] Fresh route sweep evidence is captured on the rebuilt stack.") $null = $lines.Add("- [$(Get-CheckboxMark -Value $State.Criteria22)] Fresh action sweep evidence is captured across the current aggregate suite.") $null = $lines.Add("- [$(Get-CheckboxMark -Value $State.Criteria23)] Newly exposed defects are grouped before any fix commit is prepared.") $null = $lines.Add('') $null = $lines.Add("### PLATFORM-SCRATCH-ITER$($State.Iteration)-003 - Repair the grouped defects exposed by the fresh audit") $null = $lines.Add("Status: $($State.Status3)") $null = $lines.Add("Dependency: PLATFORM-SCRATCH-ITER$($State.Iteration)-002") $null = $lines.Add('Owners: 3rd line support, Architect, Developer') $null = $lines.Add('Task description:') $null = $lines.Add('- Diagnose the grouped failures exposed by the fresh audit, choose the clean product/architecture-conformant fix, implement it, and rerun the affected verification slices plus the aggregate audit before committing.') $null = $lines.Add('') $null = $lines.Add('Completion criteria:') $null = $lines.Add("- [$(Get-CheckboxMark -Value $State.Criteria31)] Root causes are recorded for the grouped failures.") $null = $lines.Add("- [$(Get-CheckboxMark -Value $State.Criteria32)] Fixes land with focused regression coverage where practical.") $null = $lines.Add("- [$(Get-CheckboxMark -Value $State.Criteria33)] The rebuilt stack is retested before the iteration commit.") $null = $lines.Add('') $null = $lines.Add('## Execution Log') $null = $lines.Add('| Date (UTC) | Update | Owner |') $null = $lines.Add('| --- | --- | --- |') foreach ($entry in $State.ExecutionLog) { $safeUpdate = ($entry.Update -replace '\|', '\|') $null = $lines.Add("| $($entry.Date) | $safeUpdate | $($entry.Owner) |") } $null = $lines.Add('') $null = $lines.Add('## Decisions & Risks') foreach ($decision in $State.Decisions) { $null = $lines.Add("- $decision") } $null = $lines.Add('') $null = $lines.Add('## Next Checkpoints') foreach ($checkpoint in $State.NextCheckpoints) { $null = $lines.Add("- $checkpoint") } return ($lines -join [Environment]::NewLine) + [Environment]::NewLine } function Write-SprintState { param( [Parameter(Mandatory = $true)] [hashtable]$State, [Parameter(Mandatory = $true)] [string]$Path ) Set-Content -Path $Path -Value (Render-SprintFile -State $State) -Encoding UTF8 } function Invoke-RouteSweep { Invoke-External -FilePath 'node' -ArgumentList @('./scripts/live-frontdoor-canonical-route-sweep.mjs') -WorkingDirectory $webRoot return Read-JsonFile -Path (Join-Path $outputRoot 'playwright/live-frontdoor-canonical-route-sweep.json') } function Invoke-AggregateAudit { Invoke-External -FilePath 'node' -ArgumentList @('./scripts/live-full-core-audit.mjs') -WorkingDirectory $webRoot return Read-JsonFile -Path (Join-Path $outputRoot 'playwright/live-full-core-audit.json') } function Remove-PlaywrightOutput { if (Test-Path $outputRoot) { Remove-Item $outputRoot -Recurse -Force } } $batchId = if ($StartingBatchId -gt 0) { $StartingBatchId } else { Get-HighestBatchId -SprintImplId $ImplId } for ($iteration = $StartIteration; $iteration -le $EndIteration; $iteration++) { $batchId++ $baselineCommit = (git rev-parse --short=9 HEAD).Trim() $sprintPath = Join-Path $sprintRoot ("SPRINT_{0}_{1:D3}_Platform_scratch_iteration_{2:D3}_full_route_action_audit.md" -f $ImplId, $batchId, $iteration) $state = New-SprintState -Iteration $iteration -BatchId $batchId -BaselineCommit $baselineCommit $state.Decisions.Add('Decision: each scratch iteration remains a full wipe -> setup -> route/action audit -> grouped remediation loop; if the audit comes back clean, that still counts as a completed iteration because the full loop was executed.') $state.Decisions.Add('Risk: scratch rebuilds remain expensive, so verification stays Playwright-first with focused test/build slices rather than indiscriminate full-solution test runs.') $state.NextCheckpoints.Add('Finish the Stella-only wipe and capture the next zero-state setup outcome.') $state.NextCheckpoints.Add('Run the full Playwright audit on the rebuilt stack before diagnosing any new fixes.') Add-SprintLog -State $state -Update "Sprint created for the next scratch iteration after local commit ``$baselineCommit`` closed the previous clean baseline." -Owner 'QA' Write-SprintState -State $state -Path $sprintPath Remove-StellaRuntime $state.Criteria11 = $true Add-SprintLog -State $state -Update 'Removed Stella-only containers, `stellaops/*:dev` images, Stella compose volumes, and the `stellaops` / `stellaops_frontdoor` networks to return the machine to zero Stella runtime state for the new iteration.' -Owner 'QA / 3rd line support' Write-SprintState -State $state -Path $sprintPath Invoke-External -FilePath (Join-Path $repoRoot 'scripts/setup.ps1') -WorkingDirectory $repoRoot $state.Criteria12 = $true $state.Criteria13 = $true $state.Status1 = 'DONE' $healthySummary = Get-HealthyContainerSummary Add-SprintLog -State $state -Update "The zero-state setup rerun completed cleanly: ``36/36`` solution builds passed, the full image matrix rebuilt, platform services converged, and ``$healthySummary`` Stella containers are healthy on ``https://stella-ops.local``." -Owner 'QA / 3rd line support' $state.Status2 = 'DOING' Write-SprintState -State $state -Path $sprintPath $routeReport = Invoke-RouteSweep $routeOk = ($routeReport.passedRoutes -eq $routeReport.totalRoutes) -and (@($routeReport.failedRoutes).Count -eq 0) $state.Criteria21 = $true Add-SprintLog -State $state -Update "The standalone canonical route sweep finished with ``$($routeReport.passedRoutes)/$($routeReport.totalRoutes)`` passed routes and ``$(@($routeReport.failedRoutes).Count)`` failed routes on the rebuilt stack." -Owner 'QA' Write-SprintState -State $state -Path $sprintPath if (-not $routeOk) { $state.Status2 = 'DONE' $state.Criteria22 = $false $state.Criteria23 = $true $state.Status3 = 'DOING' $state.Decisions.Add('Decision: stop the clean-iteration loop when the canonical route sweep exposes a real regression so the next pass can investigate root cause instead of papering over it.') Add-SprintLog -State $state -Update "The route sweep exposed failing routes: ``$([string]::Join(', ', @($routeReport.failedRoutes)))``. The clean automation loop is stopping here for manual grouped diagnosis and repair." -Owner 'QA / 3rd line support' Write-SprintState -State $state -Path $sprintPath throw "Iteration $iteration failed route sweep." } $auditReport = Invoke-AggregateAudit $passedSuites = @($auditReport.suites | Where-Object { $_.ok }).Count $failedSuites = @($auditReport.suites | Where-Object { -not $_.ok }).Count $failedNames = @($auditReport.suites | Where-Object { -not $_.ok } | ForEach-Object { $_.name }) $auditOk = ($passedSuites -eq [int]$auditReport.suiteCount) -and ($failedSuites -eq 0) -and ([int]$auditReport.retriedSuiteCount -eq 0) -and ([int]$auditReport.stabilizedAfterRetryCount -eq 0) $state.Criteria22 = $true $state.Criteria23 = $true Add-SprintLog -State $state -Update "The aggregate audit finished with ``$passedSuites/$($auditReport.suiteCount)`` suites passed, ``$failedSuites`` failed suites, ``$($auditReport.retriedSuiteCount)`` retried suites, and ``$($auditReport.stabilizedAfterRetryCount)`` stabilized-after-retry suites." -Owner 'QA' if (-not $auditOk) { $state.Status2 = 'DONE' $state.Status3 = 'DOING' $state.Criteria31 = $true $state.Decisions.Add('Decision: stop the clean-iteration loop when the aggregate audit finds stable or retry-worthy defects so the next pass can gather full root-cause evidence before any fixes are made.') if ($failedNames.Count -gt 0) { Add-SprintLog -State $state -Update "The aggregate audit exposed failing suites: ``$([string]::Join(', ', $failedNames))``. The loop is stopping for manual grouped diagnosis and repair." -Owner 'QA / 3rd line support' } else { Add-SprintLog -State $state -Update 'The aggregate audit stayed green only after retries or stabilized-after-retry recovery. The loop is stopping because that still violates the zero-tolerance scratch standard.' -Owner 'QA / 3rd line support' } Write-SprintState -State $state -Path $sprintPath throw "Iteration $iteration failed aggregate audit." } $state.Status2 = 'DONE' $state.Status3 = 'DONE' $state.Criteria31 = $true $state.Criteria32 = $true $state.Criteria33 = $true Add-SprintLog -State $state -Update 'No stable product defects surfaced on the rebuilt stack. The route/page/action audit completed cleanly without retries, so the iteration closes as a fully verified clean scratch pass.' -Owner 'QA / Architect / Developer' $state.NextCheckpoints.Clear() $state.NextCheckpoints.Add('Start the next scratch iteration from another Stella-only wipe and documented setup rerun.') $state.NextCheckpoints.Add('Repeat the full Playwright route/page/action audit on the next rebuilt stack before considering any new fixes.') Write-SprintState -State $state -Path $sprintPath Remove-PlaywrightOutput Invoke-External -FilePath 'git' -ArgumentList @('add', '--', $sprintPath) -WorkingDirectory $repoRoot Invoke-External -FilePath 'git' -ArgumentList @('commit', '-m', ("Record clean scratch setup iteration {0:D3}" -f $iteration)) -WorkingDirectory $repoRoot }