microsoft/hve-core
Publicmirrored fromhttps://github.com/microsoft/hve-coreAvailable
scripts/evals/Get-ChangedAIArtifact.ps1
185lines · modecode
| 1 | #!/usr/bin/env pwsh |
| 2 | # Copyright (c) Microsoft Corporation. |
| 3 | # SPDX-License-Identifier: MIT |
| 4 | #Requires -Version 7.0 |
| 5 | |
| 6 | <# |
| 7 | .SYNOPSIS |
| 8 | Emits a JSON manifest of AI customization artifacts changed between two git refs. |
| 9 | |
| 10 | .DESCRIPTION |
| 11 | Runs `git diff --name-status <BaseRef>...<HeadRef>` (three-dot diff to use the merge base) |
| 12 | and classifies each entry as an agent / prompt / instruction / skill artifact via the |
| 13 | ArtifactDetection module. Writes a manifest JSON array to `-OutFile` (default |
| 14 | `logs/changed-ai-artifacts.json`) where each entry has `kind`, `path`, `artifactId`, |
| 15 | `status`, and (for renames/copies) `previousPath`. Repo-root-only artifacts and nested |
| 16 | collection-scoped artifacts are both detected. |
| 17 | |
| 18 | Exit codes: |
| 19 | 0 = manifest written successfully (manifest may be empty). |
| 20 | 2 = git invocation failed. |
| 21 | |
| 22 | .PARAMETER BaseRef |
| 23 | Base git ref for the diff. Defaults to `origin/main`. |
| 24 | |
| 25 | .PARAMETER HeadRef |
| 26 | Head git ref for the diff. Defaults to `HEAD`. |
| 27 | |
| 28 | .PARAMETER OutFile |
| 29 | Output JSON path. Defaults to `logs/changed-ai-artifacts.json` (relative to RepoRoot). |
| 30 | |
| 31 | .PARAMETER RepoRoot |
| 32 | Repository root. Defaults to the git toplevel or this script's parent directory. |
| 33 | |
| 34 | .EXAMPLE |
| 35 | pwsh -File scripts/evals/Get-ChangedAIArtifact.ps1 |
| 36 | Diff origin/main...HEAD and emit logs/changed-ai-artifacts.json. |
| 37 | |
| 38 | .EXAMPLE |
| 39 | pwsh -File scripts/evals/Get-ChangedAIArtifact.ps1 -BaseRef origin/main -HeadRef feature/branch |
| 40 | Diff a specific branch pair. |
| 41 | |
| 42 | .NOTES |
| 43 | Used by the PR-time eval coverage workflow to feed Test-StimulusPresence.ps1. |
| 44 | #> |
| 45 | |
| 46 | [CmdletBinding()] |
| 47 | param( |
| 48 | [Parameter(Mandatory = $false)] |
| 49 | [string]$BaseRef = 'origin/main', |
| 50 | |
| 51 | [Parameter(Mandatory = $false)] |
| 52 | [string]$HeadRef = 'HEAD', |
| 53 | |
| 54 | [Parameter(Mandatory = $false)] |
| 55 | [string]$OutFile, |
| 56 | |
| 57 | [Parameter(Mandatory = $false)] |
| 58 | [string]$RepoRoot |
| 59 | ) |
| 60 | |
| 61 | Set-StrictMode -Version Latest |
| 62 | $ErrorActionPreference = 'Stop' |
| 63 | |
| 64 | Import-Module (Join-Path $PSScriptRoot 'Modules/ArtifactDetection.psm1') -Force |
| 65 | Import-Module (Join-Path $PSScriptRoot 'Modules/AffectedAgents.psm1') -Force |
| 66 | |
| 67 | function Resolve-RepoRoot { |
| 68 | [CmdletBinding()] |
| 69 | [OutputType([string])] |
| 70 | param([string]$Hint) |
| 71 | |
| 72 | if (-not [string]::IsNullOrWhiteSpace($Hint)) { |
| 73 | return (Resolve-Path -LiteralPath $Hint).ProviderPath |
| 74 | } |
| 75 | |
| 76 | try { |
| 77 | $gitRoot = git rev-parse --show-toplevel 2>$null |
| 78 | if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrWhiteSpace($gitRoot)) { |
| 79 | return (Resolve-Path -LiteralPath $gitRoot.Trim()).ProviderPath |
| 80 | } |
| 81 | } |
| 82 | catch { |
| 83 | $null = $_ |
| 84 | } |
| 85 | |
| 86 | return (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '../..')).ProviderPath |
| 87 | } |
| 88 | |
| 89 | function Invoke-ChangedArtifactScan { |
| 90 | <# |
| 91 | .SYNOPSIS |
| 92 | Runs git diff and classifies the results into an artifact manifest. |
| 93 | |
| 94 | .OUTPUTS |
| 95 | [hashtable] `@{ baseRef; headRef; artifacts = @(...) }`. |
| 96 | #> |
| 97 | [CmdletBinding()] |
| 98 | [OutputType([hashtable])] |
| 99 | param( |
| 100 | [Parameter(Mandatory = $true)] |
| 101 | [string]$BaseRef, |
| 102 | |
| 103 | [Parameter(Mandatory = $true)] |
| 104 | [string]$HeadRef, |
| 105 | |
| 106 | [Parameter(Mandatory = $true)] |
| 107 | [string]$RepoRoot |
| 108 | ) |
| 109 | |
| 110 | Push-Location -LiteralPath $RepoRoot |
| 111 | try { |
| 112 | $diffOutput = & git diff --name-status "$BaseRef...$HeadRef" 2>&1 |
| 113 | $exit = $LASTEXITCODE |
| 114 | } |
| 115 | finally { |
| 116 | Pop-Location |
| 117 | } |
| 118 | |
| 119 | if ($exit -ne 0) { |
| 120 | throw "git diff failed (exit $exit): $($diffOutput -join [Environment]::NewLine)" |
| 121 | } |
| 122 | |
| 123 | $lines = @($diffOutput | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) }) |
| 124 | $changes = ConvertFrom-GitDiffNameStatus -Lines $lines |
| 125 | |
| 126 | $artifacts = [System.Collections.Generic.List[hashtable]]::new() |
| 127 | $changedPaths = [System.Collections.Generic.List[string]]::new() |
| 128 | foreach ($change in $changes) { |
| 129 | $record = Get-ChangedArtifactRecord -Change $change |
| 130 | if ($null -ne $record) { |
| 131 | $artifacts.Add($record) |
| 132 | } |
| 133 | if ($change.path) { $changedPaths.Add([string]$change.path) } |
| 134 | if ($change.previousPath) { $changedPaths.Add([string]$change.previousPath) } |
| 135 | } |
| 136 | |
| 137 | $affectedAgents = [string[]]@() |
| 138 | if ($changedPaths.Count -gt 0) { |
| 139 | try { |
| 140 | $affectedAgents = Get-AffectedAgentSlugs -ChangedFiles $changedPaths.ToArray() -RepoRoot $RepoRoot |
| 141 | } |
| 142 | catch { |
| 143 | Write-Warning "Failed to resolve affected agents: $($_.Exception.Message)" |
| 144 | $affectedAgents = [string[]]@() |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | return @{ |
| 149 | baseRef = $BaseRef |
| 150 | headRef = $HeadRef |
| 151 | artifacts = $artifacts.ToArray() |
| 152 | affectedAgents = [string[]]$affectedAgents |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | if ($MyInvocation.InvocationName -ne '.') { |
| 157 | $resolvedRepoRoot = Resolve-RepoRoot -Hint $RepoRoot |
| 158 | |
| 159 | if ([string]::IsNullOrWhiteSpace($OutFile)) { |
| 160 | $OutFile = Join-Path -Path $resolvedRepoRoot -ChildPath 'logs/changed-ai-artifacts.json' |
| 161 | } |
| 162 | elseif (-not [System.IO.Path]::IsPathRooted($OutFile)) { |
| 163 | $OutFile = Join-Path -Path $resolvedRepoRoot -ChildPath $OutFile |
| 164 | } |
| 165 | |
| 166 | try { |
| 167 | $manifest = Invoke-ChangedArtifactScan -BaseRef $BaseRef -HeadRef $HeadRef -RepoRoot $resolvedRepoRoot |
| 168 | } |
| 169 | catch { |
| 170 | Write-Error $_.Exception.Message |
| 171 | exit 2 |
| 172 | } |
| 173 | |
| 174 | $outDir = Split-Path -Path $OutFile -Parent |
| 175 | if (-not [string]::IsNullOrWhiteSpace($outDir) -and -not (Test-Path -LiteralPath $outDir -PathType Container)) { |
| 176 | New-Item -ItemType Directory -Path $outDir -Force | Out-Null |
| 177 | } |
| 178 | |
| 179 | $manifest | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $OutFile -Encoding UTF8 |
| 180 | |
| 181 | Write-Host "Detected $($manifest.artifacts.Count) changed AI artifact(s) between $BaseRef and $HeadRef." |
| 182 | Write-Host "Affected agent slugs: $($manifest.affectedAgents.Count)" |
| 183 | Write-Host "Manifest: $OutFile" |
| 184 | exit 0 |
| 185 | } |
| 186 | |