Payload/scripts/harness/harness-persist-quality.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Persist quality-gate reports under docs/harness/features/<slug>/quality/. .DESCRIPTION Writes security / consistency / documentation report files from content or paths. Rejects stub reports (under MinBytes 2000, or missing a findings/score section) so agents cannot satisfy gates with empty shells. Persist specialist markdown verbatim. If extraction finds ids, the specialist findings sibling must already exist and not be empty; persist registers that file and does not backfill from markdown. Tests may pass -AllowStub. .EXAMPLE pwsh -File scripts/harness-invoke.ps1 harness-persist-quality.ps1 -Feature saved-search-queries -Slice mo-save-list -SecurityPath .\tmp\sec.md -ConsistencyPath .\tmp\cons.md -DocumentationPath .\tmp\docs.md #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Feature, [string]$Slice = '', [string]$FeatureRoot = '', [string]$RepoRoot = '', [string]$SecurityPath = '', [string]$ConsistencyPath = '', [string]$DocumentationPath = '', [string]$SmellPath = '', [string]$TestDesignPath = '', [string]$SecurityContent = '', [string]$ConsistencyContent = '', [string]$DocumentationContent = '', [string]$SmellContent = '', [string]$TestDesignContent = '', [int]$MinBytes = 2000, # Immutable host dispatch id when the host exposes one (Cursor Task). Copilot does not expose it. [string]$SourceInvocationId = '', [ValidateSet('host-id', 'artifact-attested')] [string]$SourceProvenance = 'host-id', # When omitted, inherit from the open-turn ActivityStarted model. [ValidateSet('cursor', 'copilot', 'other', '')] [string]$ModelProvider = '', [string]$ModelSlug = '', [ValidateSet('primary', 'subagent')] [string]$ModelRole = 'primary', [switch]$AllowStub, # Tests only: allow ModelProvider other without an open-turn inherit. [switch]$AllowUnknownActor, [switch]$AllowArbitraryFeatureRoot ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' Import-Module (Join-Path (Join-Path $PSScriptRoot 'lib') 'HarnessContract.psm1') -Force Import-Module (Join-Path (Join-Path $PSScriptRoot 'lib') 'HarnessJournal.psm1') -Force Test-HarnessContractModuleReady if ([string]::IsNullOrWhiteSpace($RepoRoot)) { $RepoRoot = Get-HarnessRepoRoot -StartPath $PSScriptRoot } if ([string]::IsNullOrWhiteSpace($Slice)) { throw 'harness-persist-quality.ps1 requires -Slice for feature-scoped quality reports.' } $featureRootPath = Get-HarnessFeatureRoot -RepoRoot $RepoRoot -Feature $Feature ` -FeatureRootOverride $FeatureRoot -AllowArbitraryFeatureRoot:$AllowArbitraryFeatureRoot $hasAnyReport = @( @( $SecurityPath, $ConsistencyPath, $DocumentationPath, $SmellPath, $TestDesignPath, $SecurityContent, $ConsistencyContent, $DocumentationContent, $SmellContent, $TestDesignContent ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } ) if ($hasAnyReport.Count -gt 0 -and -not $AllowStub) { if ($SourceProvenance -eq 'host-id' -and [string]::IsNullOrWhiteSpace($SourceInvocationId)) { throw 'persist-quality with SourceProvenance host-id requires -SourceInvocationId.' } if ($SourceProvenance -eq 'artifact-attested' -and ($ModelProvider -ne 'copilot' -or -not [string]::IsNullOrWhiteSpace($SourceInvocationId))) { throw 'SourceProvenance artifact-attested is only valid for Copilot subagents without a host invocation id.' } } $resolvedProvider = $ModelProvider $resolvedSlug = $ModelSlug if ($ModelProvider -eq 'other' -and -not $AllowUnknownActor -and -not $AllowStub) { throw 'persist-quality refuses ModelProvider other without -AllowUnknownActor; omit -ModelProvider to inherit the open-turn model.' } if ([string]::IsNullOrWhiteSpace($resolvedProvider)) { $inherited = Get-HarnessOpenTurnModel -FeatureRoot $featureRootPath -SliceId $Slice if ($inherited -and -not [string]::IsNullOrWhiteSpace([string]$inherited.provider)) { $resolvedProvider = [string]$inherited.provider if ([string]::IsNullOrWhiteSpace($resolvedSlug) -or $resolvedSlug -eq 'unknown') { $resolvedSlug = [string]$inherited.slug } } } if ([string]::IsNullOrWhiteSpace($resolvedProvider)) { $resolvedProvider = 'other' } if ([string]::IsNullOrWhiteSpace($resolvedSlug)) { $resolvedSlug = 'unknown' } if ($resolvedProvider -eq 'other' -and -not $AllowUnknownActor -and -not $AllowStub) { throw 'persist-quality has no open-turn model to inherit; pass -ModelProvider cursor|copilot or -AllowUnknownActor (tests).' } $qualityDir = Join-Path $featureRootPath 'quality' New-Item -ItemType Directory -Path $qualityDir -Force | Out-Null $canonical = Get-HarnessCanonicalQualityArtifactNames -SliceId $Slice function Get-HarnessQualityBody { param([string]$Path, [string]$Content, [string]$Label, [switch]$AllowStubBody) $body = '' if (-not [string]::IsNullOrWhiteSpace($Content)) { $body = $Content } elseif (-not [string]::IsNullOrWhiteSpace($Path)) { Assert-HarnessPipelineSourceNotScratch -Path $Path -RepoRoot $RepoRoot if (-not (Test-Path -LiteralPath $Path)) { throw "Quality report path not found ($Label): $Path" } $body = Get-Content -LiteralPath $Path -Raw } else { throw ("Provide -{0}Path or -{1}Content" -f $Label, $Label) } $bytes = [System.Text.Encoding]::UTF8.GetByteCount($body) if (-not $AllowStubBody -and $bytes -lt $MinBytes) { throw "Quality report '$Label' is only $bytes bytes (min $MinBytes). Persist the full subagent report, not a stub." } if ($body -notmatch '(?i)verdict|PASS|FAIL|APPROVE|NEEDS_REVISION') { throw "Quality report '$Label' must include a verdict keyword (PASS/FAIL/APPROVE/...)." } if (-not $AllowStubBody -and -not (Test-HarnessQualityReportHasSpecialistStructure -Body $body)) { throw "Quality report '$Label' must include a findings or score section. Persist the specialist report verbatim; do not hand-summarize." } return $body } function Remove-HarnessQualityStagingIncomingFile { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Non-interactive harness cleanup - agents must not prompt.')] param([string]$SourcePath, [string]$FeatureRootPath) if ([string]::IsNullOrWhiteSpace($SourcePath)) { return } $full = [System.IO.Path]::GetFullPath($SourcePath) $qualityFull = [System.IO.Path]::GetFullPath((Join-Path $FeatureRootPath 'quality')) if (-not (Test-HarnessPathContainedUnder -Root $qualityFull -CandidatePath $full)) { return } $leaf = Split-Path $full -Leaf if ($leaf -like '_incoming*') { Remove-Item -LiteralPath $full -Force -ErrorAction SilentlyContinue } } $written = [ordered]@{} $pairs = @( @{ Label = 'Security'; Rel = $canonical.Security; Path = $SecurityPath; Content = $SecurityContent; SourcePath = $SecurityPath; Agent = 'security-assessor' } @{ Label = 'Consistency'; Rel = $canonical.Consistency; Path = $ConsistencyPath; Content = $ConsistencyContent; SourcePath = $ConsistencyPath; Agent = 'consistency-checker' } @{ Label = 'Documentation'; Rel = $canonical.Documentation; Path = $DocumentationPath; Content = $DocumentationContent; SourcePath = $DocumentationPath; Agent = 'documentation-reviewer' } @{ Label = 'Smell'; Rel = $canonical.Smell; Path = $SmellPath; Content = $SmellContent; SourcePath = $SmellPath; Agent = 'code-smell-detector' } @{ Label = 'TestDesign'; Rel = $canonical.TestDesign; Path = $TestDesignPath; Content = $TestDesignContent; SourcePath = $TestDesignPath; Agent = 'test-design-reviewer' } ) foreach ($p in $pairs) { if ([string]::IsNullOrWhiteSpace($p.Path) -and [string]::IsNullOrWhiteSpace($p.Content)) { continue } if (-not [string]::IsNullOrWhiteSpace($p.Path)) { Assert-HarnessPipelineSourceNotScratch -Path $p.Path -RepoRoot $RepoRoot } $destFull = Join-HarnessPath -Root $featureRootPath -Child $p.Rel $contentOnly = (-not [string]::IsNullOrWhiteSpace($p.Content)) -and [string]::IsNullOrWhiteSpace($p.Path) if ($contentOnly) { Assert-HarnessCanonicalQualityReportOnDisk -FeatureRoot $featureRootPath -RelativePath $p.Rel -AllowStub:$AllowStub } if (-not $AllowStub -and $contentOnly -and (Test-Path -LiteralPath $destFull)) { $body = Get-Content -LiteralPath $destFull -Raw $normIn = ($p.Content -replace '\r\n', "`n").Trim() $normDisk = ($body -replace '\r\n', "`n").Trim() if ($normIn -ne $normDisk) { throw ("Quality report '{0}' Content does not match on-disk allowlist file {1}" -f $p.Label, $p.Rel) } $bytes = [System.Text.Encoding]::UTF8.GetByteCount($body) if ($bytes -lt $MinBytes) { throw "Quality report '$($p.Label)' is only $bytes bytes (min $MinBytes). Persist the full subagent report, not a stub." } if ($body -notmatch '(?i)verdict|PASS|FAIL|APPROVE|NEEDS_REVISION') { throw "Quality report '$($p.Label)' must include a verdict keyword (PASS/FAIL/APPROVE/...)." } if (-not (Test-HarnessQualityReportHasSpecialistStructure -Body $body)) { throw "Quality report '$($p.Label)' must include a findings or score section. Persist the specialist report verbatim; do not hand-summarize." } } elseif (-not $AllowStub -and -not $contentOnly -and (Test-Path -LiteralPath $destFull) -and -not [string]::IsNullOrWhiteSpace($p.Path)) { $pathFull = [System.IO.Path]::GetFullPath($p.Path) if ([string]::Equals($pathFull, [System.IO.Path]::GetFullPath($destFull), [StringComparison]::OrdinalIgnoreCase)) { $body = Get-Content -LiteralPath $destFull -Raw $bytes = [System.Text.Encoding]::UTF8.GetByteCount($body) if ($bytes -lt $MinBytes) { throw "Quality report '$($p.Label)' is only $bytes bytes (min $MinBytes). Persist the full subagent report, not a stub." } if ($body -notmatch '(?i)verdict|PASS|FAIL|APPROVE|NEEDS_REVISION') { throw "Quality report '$($p.Label)' must include a verdict keyword (PASS/FAIL/APPROVE/...)." } if (-not (Test-HarnessQualityReportHasSpecialistStructure -Body $body)) { throw "Quality report '$($p.Label)' must include a findings or score section. Persist the specialist report verbatim; do not hand-summarize." } } else { $body = Get-HarnessQualityBody -Path $p.Path -Content $p.Content -Label $p.Label -AllowStubBody:$AllowStub } } else { $body = Get-HarnessQualityBody -Path $p.Path -Content $p.Content -Label $p.Label -AllowStubBody:$AllowStub } if ($p.Label -eq 'Security' -and -not (Test-HarnessSecurityReportScannerHonesty -ReportBody $body -RepoRoot $RepoRoot)) { throw "Security report appears score-100 clean while scanners were skipped on changed path classes. Add informational findings with disposition (skipped-not-in-scope) before persist." } $extracted = @(Get-HarnessQualityMarkdownFinding -Body $body) if (-not $AllowStub) { if ((Test-HarnessQualityMarkdownFindingsTableHasCandidateRows -Body $body) -and $extracted.Count -eq 0) { throw ("Quality report '{0}' has a Findings table with data rows but no findings were extracted. Use parseable ID and Severity columns or ATX headings; do not hand-summarize." -f $p.Label) } } $full = $destFull New-Item -ItemType Directory -Path (Split-Path $full -Parent) -Force | Out-Null $alreadyCanonical = (-not $AllowStub) -and (Test-Path -LiteralPath $full) -and ( $contentOnly -or ( -not [string]::IsNullOrWhiteSpace($p.Path) -and [string]::Equals([System.IO.Path]::GetFullPath($p.Path), [System.IO.Path]::GetFullPath($full), [StringComparison]::OrdinalIgnoreCase) ) ) if (-not $alreadyCanonical) { [System.IO.File]::WriteAllText($full, $body, [System.Text.UTF8Encoding]::new($false)) } $written[$p.Label] = $p.Rel $stem = [System.IO.Path]::GetFileNameWithoutExtension((Split-Path -Path ($p.Rel -replace '\\', '/') -Leaf)) $dataRel = 'quality/' + $stem + '-data.json' $dataFull = Join-HarnessPath -Root $featureRootPath -Child $dataRel $specialistFindings = @() if (Test-Path -LiteralPath $dataFull) { $dataExisting = ConvertFrom-HarnessJson -Json (Get-Content -LiteralPath $dataFull -Raw) $specialistFindings = @((Get-HarnessProperty -Object $dataExisting -Name 'findings' -Default @()) | Where-Object { $null -ne $_ }) } if (-not $AllowStub -and $extracted.Count -gt 0 -and $specialistFindings.Count -eq 0) { throw ("Quality report '{0}' names findings but the findings file is missing or findings is empty. Specialist must write {1}; persist does not backfill." -f $p.Label, $dataRel) } if ($specialistFindings.Count -eq 0) { # Empty-report path, or -AllowStub. Persist does not backfill a named-findings report. $findingRows = [System.Collections.ArrayList]@() foreach ($f in $extracted) { [void]$findingRows.Add([ordered]@{ id = [string]$f.id severity = [string]$f.severity location = [string]$f.location message = [string]$f.message suggested_fix = [string]$f.suggested_fix }) } $dataDoc = [ordered]@{ agent = [string]$p.Agent findings = @($findingRows.ToArray()) } [System.IO.File]::WriteAllText($dataFull, ($dataDoc | ConvertTo-Json -Depth 6), [System.Text.UTF8Encoding]::new($false)) } Remove-HarnessQualityStagingIncomingFile -SourcePath $p.SourcePath -FeatureRootPath $featureRootPath Remove-HarnessStagedPayloadIfEligible -CandidatePath $p.SourcePath -RepoRoot $RepoRoot -FeatureRoot $featureRootPath $actor = New-HarnessJournalActor -Agent 'harness-persist-quality.ps1' -ParentAgent $null ` -Tool 'harness-script' -Provider $resolvedProvider -Slug $resolvedSlug -Role $ModelRole $activity = New-HarnessJournalActivity -Id (New-HarnessJournalActivityId) -Type 'artifact' -Name 'persist-quality' -Role 'instant' Add-HarnessJournalEvent -FeatureRoot $featureRootPath -Feature $Feature -Slice $(if ($Slice) { $Slice } else { $null }) ` -Kind 'ArtifactRegistered' -Activity $activity -Actor $actor -Outcome 'PASS' ` -Summary "quality report landed $($p.Rel)" ` -Payload ([pscustomobject]@{ gate = $p.Label.ToLowerInvariant() artifact = $p.Rel bytes = [System.Text.Encoding]::UTF8.GetByteCount($body) source_provenance = $SourceProvenance source_invocation_id = $(if ($SourceInvocationId) { $SourceInvocationId } else { $null }) }) ` -Refs ([pscustomobject]@{ amends_event_id = $null pair_event_id = $null artifact = $p.Rel command = '.\scripts\harness-persist-quality.ps1' }) | Out-Null } $canonicalPaths = [ordered]@{} foreach ($label in @($written.Keys)) { $rel = [string]$written[$label] $canonicalPaths[$label] = Join-HarnessPath -Root $featureRootPath -Child $rel } [pscustomobject]@{ feature = $Feature slice = $Slice written = $written canonical_paths = $canonicalPaths minBytes = $MinBytes } | ConvertTo-Json -Depth 5 |