Payload/scripts/harness/quality-gate/AiEvidenceAdapter.psm1
|
#Requires -Version 5.1 Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' <# .SYNOPSIS Trusted adapter that derives AI check evidence for Invoke-RepositoryQualityDecision. .DESCRIPTION Builds agent-skill-behavior and ai-surface-sync evidence from a real changed-path set, an explicit path policy, a canonical/generated surface pair, and a deterministic eval runner result. The decision port does not attest these fields; this adapter does. It never invents AdapterAttested without deriving CurrentTargetSet from ChangedPaths. #> function Test-AiSurfacePath { param([string]$Path) if ([string]::IsNullOrWhiteSpace($Path)) { return $false } $normalized = $Path.Replace('\', '/') while ($normalized.StartsWith('./')) { $normalized = $normalized.Substring(2) } return ( $normalized.StartsWith('.github/', [System.StringComparison]::OrdinalIgnoreCase) -or $normalized.StartsWith('.cursor/', [System.StringComparison]::OrdinalIgnoreCase) ) } function Test-BehaviorBearingAiPath { param([string]$Path) if (-not (Test-AiSurfacePath -Path $Path)) { return $false } $normalized = $Path.Replace('\', '/').ToLowerInvariant() foreach ($token in @( '/agents/', '/skills/', '/prompts/', '/commands/', '/rules/', '.agent.md', 'skill.md', '.prompt.md' )) { if ($normalized.Contains($token)) { return $true } } return $false } function Get-AiChangedTargetSet { param([string[]]$ChangedPaths) $targets = [System.Collections.Generic.List[string]]::new() foreach ($path in @($ChangedPaths)) { $text = [string]$path if ([string]::IsNullOrWhiteSpace($text)) { continue } if (-not (Test-AiSurfacePath -Path $text)) { continue } $normalized = $text.Replace('\', '/') while ($normalized.StartsWith('./')) { $normalized = $normalized.Substring(2) } if (-not ($targets -contains $normalized)) { [void]$targets.Add($normalized) } } return @($targets.ToArray()) } function Get-AiBehaviorClassification { param([string[]]$TargetSet) $anyBehavior = $false $anySurface = $false foreach ($path in @($TargetSet)) { if (Test-BehaviorBearingAiPath -Path $path) { $anyBehavior = $true } if (Test-AiSurfacePath -Path $path) { $anySurface = $true } } if (-not $anySurface) { return $null } if ($anyBehavior) { return 'behavior-bearing' } return 'non-behavior-bearing' } function Test-AiSurfacesSynchronized { param( [string]$CanonicalContent, [string]$GeneratedContent ) if ($null -eq $CanonicalContent -or $null -eq $GeneratedContent) { return $false } return [string]::Equals( [string]$CanonicalContent, [string]$GeneratedContent, [System.StringComparison]::Ordinal ) } function New-RepositoryAiCheckEvidence { <# .SYNOPSIS Derive one AI check evidence hashtable from changed paths and runner/pair inputs. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds an in-memory evidence hashtable for the decision port; no durable side effects.' )] param( [Parameter(Mandatory = $true)] [ValidateSet('agent-skill-behavior', 'ai-surface-sync')] [string]$Check, [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [string[]]$ChangedPaths, [Parameter(Mandatory = $true)] [datetime]$RequestedAt, [Parameter(Mandatory = $true)] [string]$ProtectedRevision, [string]$RuleIdentity, [string]$CanonicalContent = '', [string]$GeneratedContent = '', $EvalRunnerResult = $null ) $targets = @(Get-AiChangedTargetSet -ChangedPaths $ChangedPaths) $classification = Get-AiBehaviorClassification -TargetSet $targets $synchronized = Test-AiSurfacesSynchronized -CanonicalContent $CanonicalContent -GeneratedContent $GeneratedContent $evidence = @{ PresentedAgain = $false TargetSet = @($targets) ScannedTargetSet = @($targets) CurrentTargetSet = @($targets) RuleIdentity = $RuleIdentity CurrentRuleIdentity = $RuleIdentity RequestedAt = $RequestedAt TargetCount = @($targets).Count FailedCount = 0 BlockingFindingCount = 0 ErrorFindingCount = 0 SecretFindingCount = 0 AnalyzerFindingCount = 0 EncodingFaultCount = 0 ChangedPaths = @($ChangedPaths) ProtectedRevision = $ProtectedRevision RecordedRevision = $ProtectedRevision AffectedTarget = $null Condition = $null FixtureContents = @() CapturedOutput = $null DeterministicScore = $null StaticContentCheckOnly = $false NewlyCaptured = $false BehaviorClassification = $classification SurfacesSynchronized = $synchronized AdapterAttested = $true ApplicabilitySource = 'changed-path-policy' EvidenceSource = $(if ($Check -eq 'agent-skill-behavior') { 'deterministic-eval-runner' } else { 'canonical-generated-pair' }) } if ($Check -eq 'agent-skill-behavior') { if ($null -eq $EvalRunnerResult) { throw 'agent-skill-behavior requires EvalRunnerResult from the deterministic eval runner.' } $evidence.FixtureContents = @($EvalRunnerResult.FixtureContents) $evidence.CapturedOutput = $EvalRunnerResult.CapturedOutput $evidence.DeterministicScore = [string]$EvalRunnerResult.DeterministicScore $evidence.NewlyCaptured = [bool]$EvalRunnerResult.NewlyCaptured $evidence.StaticContentCheckOnly = [bool]$EvalRunnerResult.StaticContentCheckOnly $evidence.CompletedAt = $EvalRunnerResult.CompletedAt $evidence.PresentedAgain = [bool]$EvalRunnerResult.PresentedAgain } else { if ($null -ne $EvalRunnerResult -and $null -ne $EvalRunnerResult.CompletedAt) { $evidence.CompletedAt = $EvalRunnerResult.CompletedAt $evidence.PresentedAgain = [bool]$EvalRunnerResult.PresentedAgain } else { $evidence.CompletedAt = $RequestedAt } } return $evidence } Export-ModuleMember -Function @( 'Test-AiSurfacePath', 'Test-BehaviorBearingAiPath', 'Get-AiChangedTargetSet', 'Get-AiBehaviorClassification', 'Test-AiSurfacesSynchronized', 'New-RepositoryAiCheckEvidence' ) |