extensions/specrew-speckit/scripts/Test-CopilotInstructionsChangeType.ps1
|
[CmdletBinding()] param( [AllowNull()] [Alias('BeforePath')] [string]$ClassifierBeforePath, [AllowNull()] [Alias('AfterPath')] [string]$ClassifierAfterPath, [AllowNull()] [Alias('ProjectPath')] [string]$ClassifierProjectPath, [AllowNull()] [Alias('BaselineCommitHash')] [string]$ClassifierBaselineCommitHash, [Alias('TargetPath')] [string]$ClassifierTargetPath = '.github/copilot-instructions.md', [switch]$AsJson ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $sharedGovernancePath = Join-Path $PSScriptRoot 'shared-governance.ps1' if (-not (Test-Path -LiteralPath $sharedGovernancePath -PathType Leaf)) { throw "Missing shared governance helper '$sharedGovernancePath'." } . $sharedGovernancePath function Get-CopilotInstructionsDocumentParts { param( [AllowNull()] [string]$Content ) $normalizedContent = if ($null -eq $Content) { '' } else { $Content -replace "`r`n", "`n" } $lines = @($normalizedContent -split "`n") $lineCount = if ($lines.Count -eq 1 -and $lines[0] -eq '') { 0 } else { $lines.Count } $timestampLines = New-Object System.Collections.Generic.List[string] $preambleLines = New-Object System.Collections.Generic.List[string] $sections = [ordered]@{} $currentHeading = $null $currentLines = New-Object System.Collections.Generic.List[string] $inFrontmatter = $false $frontmatterClosed = $false for ($index = 0; $index -lt $lineCount; $index++) { $line = [string]$lines[$index] if ($index -eq 0 -and $line.Trim() -eq '---') { $inFrontmatter = $true continue } if ($inFrontmatter) { if ($line.Trim() -eq '---') { $inFrontmatter = $false $frontmatterClosed = $true continue } if ($line -match '^\s*last_updated\s*:') { $timestampLines.Add($line.Trim()) | Out-Null } else { $preambleLines.Add($line) | Out-Null } continue } if ($line -match '^##\s+') { if ($null -ne $currentHeading) { $sections[$currentHeading] = ($currentLines.ToArray() -join "`n").Trim() } $currentHeading = $line.Trim() $currentLines = New-Object System.Collections.Generic.List[string] continue } if ($null -eq $currentHeading) { if ($frontmatterClosed -or -not [string]::IsNullOrWhiteSpace($line)) { $preambleLines.Add($line) | Out-Null } continue } $currentLines.Add($line) | Out-Null } if ($null -ne $currentHeading) { $sections[$currentHeading] = ($currentLines.ToArray() -join "`n").Trim() } return [pscustomobject]@{ Timestamp = ($timestampLines.ToArray() -join "`n").Trim() Preamble = ($preambleLines.ToArray() -join "`n").Trim() Sections = [pscustomobject]$sections } } function Test-CopilotInstructionsChangeType { [CmdletBinding(DefaultParameterSetName = 'Content')] param( [Parameter(ParameterSetName = 'Content', Mandatory = $true)] [AllowNull()] [string]$BeforeContent, [Parameter(ParameterSetName = 'Content', Mandatory = $true)] [AllowNull()] [string]$AfterContent, [Parameter(ParameterSetName = 'Paths', Mandatory = $true)] [string]$BeforePath, [Parameter(ParameterSetName = 'Paths', Mandatory = $true)] [string]$AfterPath, [Parameter(ParameterSetName = 'Repository', Mandatory = $true)] [string]$ProjectPath, [Parameter(ParameterSetName = 'Repository')] [AllowNull()] [string]$BaselineCommitHash, [Parameter(ParameterSetName = 'Repository')] [string]$TargetPath = '.github/copilot-instructions.md' ) if ($PSCmdlet.ParameterSetName -eq 'Paths') { $BeforeContent = if (Test-Path -LiteralPath $BeforePath -PathType Leaf) { Get-Content -LiteralPath $BeforePath -Raw -Encoding UTF8 } else { '' } $AfterContent = if (Test-Path -LiteralPath $AfterPath -PathType Leaf) { Get-Content -LiteralPath $AfterPath -Raw -Encoding UTF8 } else { '' } } elseif ($PSCmdlet.ParameterSetName -eq 'Repository') { $resolvedProjectPath = Resolve-ProjectPath -Path $ProjectPath $resolvedTargetPath = $TargetPath -replace '/', '\' $gitTargetPath = $TargetPath -replace '\\', '/' $afterPath = Join-Path $resolvedProjectPath $resolvedTargetPath $AfterContent = if (Test-Path -LiteralPath $afterPath -PathType Leaf) { Get-Content -LiteralPath $afterPath -Raw -Encoding UTF8 } else { '' } if ([string]::IsNullOrWhiteSpace($BaselineCommitHash)) { $BeforeContent = $AfterContent } else { $beforeOutput = @(& git -C $resolvedProjectPath show "$BaselineCommitHash`:$gitTargetPath" 2>$null) $BeforeContent = if ($LASTEXITCODE -eq 0) { ($beforeOutput -join [Environment]::NewLine) } else { '' } } } $beforeParts = Get-CopilotInstructionsDocumentParts -Content $BeforeContent $afterParts = Get-CopilotInstructionsDocumentParts -Content $AfterContent $bookkeepingSections = @('timestamp', '## Active Technologies', '## Recent Changes') $changedSections = New-Object System.Collections.Generic.List[string] $behaviorSections = New-Object System.Collections.Generic.List[string] if ($beforeParts.Timestamp -cne $afterParts.Timestamp) { $changedSections.Add('timestamp') | Out-Null } if ($beforeParts.Preamble -cne $afterParts.Preamble) { $changedSections.Add('preamble') | Out-Null $behaviorSections.Add('preamble') | Out-Null } $beforeSectionNames = @($beforeParts.Sections.PSObject.Properties | ForEach-Object { $_.Name }) $afterSectionNames = @($afterParts.Sections.PSObject.Properties | ForEach-Object { $_.Name }) $sectionNames = @( $beforeSectionNames + $afterSectionNames ) | Sort-Object -Unique foreach ($sectionName in $sectionNames) { $beforeProperty = $beforeParts.Sections.PSObject.Properties[$sectionName] $afterProperty = $afterParts.Sections.PSObject.Properties[$sectionName] $beforeSection = if ($null -ne $beforeProperty) { [string]$beforeProperty.Value } else { '' } $afterSection = if ($null -ne $afterProperty) { [string]$afterProperty.Value } else { '' } if ($beforeSection -ceq $afterSection) { continue } $changedSections.Add($sectionName) | Out-Null if ($sectionName -notin @('## Active Technologies', '## Recent Changes')) { $behaviorSections.Add($sectionName) | Out-Null } } $classification = if ($behaviorSections.Count -gt 0) { 'behavior' } else { 'bookkeeping' } return [pscustomobject]@{ Classification = $classification ChangedSections = $changedSections.ToArray() BookkeepingSections = $bookkeepingSections BehaviorSections = $behaviorSections.ToArray() RequiresRestart = $classification -eq 'behavior' } } if ($MyInvocation.InvocationName -ne '.') { $result = if (-not [string]::IsNullOrWhiteSpace($ClassifierProjectPath)) { Test-CopilotInstructionsChangeType -ProjectPath $ClassifierProjectPath -BaselineCommitHash $ClassifierBaselineCommitHash -TargetPath $ClassifierTargetPath } elseif (-not [string]::IsNullOrWhiteSpace($ClassifierBeforePath) -or -not [string]::IsNullOrWhiteSpace($ClassifierAfterPath)) { if ([string]::IsNullOrWhiteSpace($ClassifierBeforePath) -or [string]::IsNullOrWhiteSpace($ClassifierAfterPath)) { throw 'BeforePath and AfterPath must both be provided when classifying explicit files.' } Test-CopilotInstructionsChangeType -BeforePath $ClassifierBeforePath -AfterPath $ClassifierAfterPath } else { throw 'Provide either ProjectPath or both BeforePath and AfterPath.' } if ($AsJson) { $result | ConvertTo-Json -Depth 5 } else { $result } } |