Payload/scripts/kaden/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'
)