scripts/internal/bootstrap/ValidationEngine.ps1
|
<#
.SYNOPSIS Validate the session anchor against current project state and clear it when stale. .DESCRIPTION Engine (IDesign). Per the co-designed engine call-rule, ValidationEngine MAY call accessors directly (the reads are predictable and the underlying data - git history, the project tree - is large), so it composes SessionStateAccessor + ProjectMetadataAccessor. It returns a verdict with an explicit cleared_reason and human-readable findings so the cleared/full path is observable (security d2, ui-ux d3). An anchor is valid ONLY when active, portable, project-local, and not merged. Feature 174 (FR-013, FR-015, FR-017, SC-004). Depends on SessionStateAccessor.ps1 + ProjectMetadataAccessor.ps1 (co-loaded by the module). .OUTPUTS [pscustomobject] { valid, cleared_reason, findings, anchor } cleared_reason: $null | 'non-portable' | 'missing' | 'merged' #> function Test-SpecrewAnchorValidity { [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)][string] $StatePath, [Parameter(Mandatory)][string] $ProjectRoot, [Parameter()][string] $BaseBranch = 'main' ) $anchor = Get-SpecrewSessionAnchor -StatePath $StatePath if ($null -eq $anchor) { return [pscustomobject]@{ valid = $false; cleared_reason = $null; findings = @('no active session anchor'); anchor = $null } } if (-not $anchor.active) { return [pscustomobject]@{ valid = $false; cleared_reason = $null; findings = @('anchor is not active'); anchor = $anchor } } # FR-015: a non-portable absolute path (different worktree) is never trusted. if (-not (Test-SpecrewAnchorPortable -Anchor $anchor -ProjectRoot $ProjectRoot)) { return [pscustomobject]@{ valid = $false; cleared_reason = 'non-portable' findings = @("anchor path is non-portable (different worktree): $($anchor.feature_path)"); anchor = $anchor } } # FR-013: re-resolve project-local + git merged-status. $res = Get-SpecrewFeatureResumable -ProjectRoot $ProjectRoot -FeatureRef $anchor.feature_ref -BaseBranch $BaseBranch if (-not $res.present) { return [pscustomobject]@{ valid = $false; cleared_reason = 'missing' findings = @("feature not present in this project: $($anchor.feature_ref)"); anchor = $anchor } } if ($res.merged) { return [pscustomobject]@{ valid = $false; cleared_reason = 'merged' findings = @("feature is already merged: $($anchor.feature_ref)"); anchor = $anchor } } return [pscustomobject]@{ valid = $true; cleared_reason = $null; findings = @(); anchor = $anchor } } function Test-SpecrewHandoverValidity { # Handover recency is necessary but NOT sufficient (architecture-core d2): a fresh handover # must still validate against current project state before it is treated as resume truth. # Composes ProjectMetadataAccessor (Get-SpecrewFeatureResumable). Feature 174 (FR-010, FR-017). [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter()][AllowNull()]$Handover, # from Get-SpecrewRollingHandover, or $null [Parameter(Mandatory)][string] $ProjectRoot, [Parameter()][string] $BaseBranch = 'main' ) if ($null -eq $Handover) { return [pscustomobject]@{ valid = $false; reason = $null; findings = @('no handover present') } } if (-not [bool]$Handover.fresh) { return [pscustomobject]@{ valid = $false; reason = 'stale'; findings = @("handover older than the freshness window: $($Handover.recorded_at)") } } $feature = $Handover.active_feature if ([string]::IsNullOrWhiteSpace($feature)) { return [pscustomobject]@{ valid = $false; reason = 'no-feature'; findings = @('handover names no active feature') } } $res = Get-SpecrewFeatureResumable -ProjectRoot $ProjectRoot -FeatureRef $feature -BaseBranch $BaseBranch if (-not $res.present) { return [pscustomobject]@{ valid = $false; reason = 'missing'; findings = @("handover feature not present: $feature") } } if ($res.merged) { return [pscustomobject]@{ valid = $false; reason = 'merged'; findings = @("handover feature already merged: $feature") } } return [pscustomobject]@{ valid = $true; reason = $null; findings = @() } } |