CaiWuXiTong/scripts/codex-task-preflight.ps1

275 lines
9.4 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$Task,
[Parameter(Mandatory = $true)]
[ValidatePattern('^[A-Za-z0-9_-]+$')]
[string]$Developer,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string[]]$PlannedPath,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[Alias('WriteContract')]
[string[]]$Contract,
[string[]]$ReuseContract = @(),
[string]$Remote = 'origin',
[string]$BaseRef = 'origin/main'
)
$ErrorActionPreference = 'Stop'
function Invoke-Git {
param([Parameter(ValueFromRemainingArguments = $true)][string[]]$Arguments)
$output = & git @Arguments 2>&1
if ($LASTEXITCODE -ne 0) {
throw "Git 命令执行失败:git $($Arguments -join ' ')`n$($output -join "`n")"
}
return @($output)
}
function Normalize-RepoPath {
param([string]$Path)
return ($Path.Trim() -replace '\\', '/').TrimStart('.', '/').TrimEnd('/')
}
function Test-PathOverlap {
param(
[string]$Planned,
[string]$Changed
)
return $Planned -eq $Changed `
-or $Changed.StartsWith($Planned + '/', [StringComparison]::OrdinalIgnoreCase) `
-or $Planned.StartsWith($Changed + '/', [StringComparison]::OrdinalIgnoreCase)
}
$repoRoot = (Invoke-Git rev-parse --show-toplevel | Select-Object -First 1).Trim()
$repoName = Split-Path -Leaf $repoRoot
$originUrl = (Invoke-Git remote get-url $Remote | Select-Object -First 1).Trim()
$gitUser = (Invoke-Git config user.name | Select-Object -First 1).Trim()
if ($repoName -ne 'CaiWuXiTong') {
throw "当前目录不是 CaiWuXiTong:$repoRoot"
}
if ($originUrl -notmatch 'WuYongJie/CaiWuXiTong(?:\.git)?$') {
throw "远程仓库不是 WuYongJie/CaiWuXiTong:$originUrl"
}
if ([string]::IsNullOrWhiteSpace($gitUser)) {
throw 'Git user.name 未配置。'
}
$plannedPaths = @($PlannedPath | ForEach-Object { Normalize-RepoPath $_ } | Where-Object { $_ } | Select-Object -Unique)
$writeContracts = @($Contract | ForEach-Object { $_.Trim() } | Where-Object { $_ } | Select-Object -Unique)
$reuseContracts = @($ReuseContract | ForEach-Object { $_.Trim() } | Where-Object { $_ } | Select-Object -Unique)
if ($plannedPaths.Count -eq 0 -or $writeContracts.Count -eq 0) {
throw '必须同时提供预计修改路径和将写入的公共契约关键词,不能在范围不明确时开始开发。'
}
$broadPathRoots = @('app', 'bootstrap', 'config', 'database', 'deploy', 'docs', 'modules', 'public', 'resources', 'routes', 'scripts', 'tests')
$broadPlannedPaths = @($plannedPaths | Where-Object { $broadPathRoots -contains $_ })
if ($broadPlannedPaths.Count -gt 0) {
throw "预计修改路径不能使用宽泛根目录:$($broadPlannedPaths -join ', ')。请填写精确文件或最小子目录,例如 tests/Feature/Business/OrderDataNavigationTest.php。"
}
Invoke-Git fetch --prune $Remote | Out-Null
Invoke-Git rev-parse --verify $BaseRef | Out-Null
$status = @(Invoke-Git status --porcelain=v1)
$trackedChanges = @($status | Where-Object { $_ -and -not $_.StartsWith('?? ') })
$untrackedChanges = @($status | Where-Object { $_ -and $_.StartsWith('?? ') })
if ($trackedChanges.Count -gt 0) {
Write-Host '阻断:工作区已有受控文件改动,不能开始新任务。' -ForegroundColor Red
$trackedChanges | ForEach-Object { Write-Host " $_" }
exit 2
}
$currentBranch = (Invoke-Git branch --show-current | Select-Object -First 1).Trim()
$remoteBranches = @(
Invoke-Git for-each-ref '--format=%(refname:short)' "refs/remotes/$Remote" |
Where-Object {
$_ -and
$_ -ne $Remote -and
$_ -ne "$Remote/HEAD" -and
$_ -ne $BaseRef -and
$_ -ne "$Remote/$currentBranch"
}
)
$branchFindings = @()
$reuseFindings = @()
foreach ($branch in $remoteBranches) {
$counts = ((Invoke-Git rev-list --left-right --count "$BaseRef...$branch") | Select-Object -First 1).Trim() -split '\s+'
$branchOnly = [int]$counts[1]
if ($branchOnly -eq 0) {
continue
}
& git merge-base --is-ancestor $branch $BaseRef
if ($LASTEXITCODE -eq 0) {
continue
}
$changedFiles = @(Invoke-Git diff --name-only "$BaseRef...$branch" | ForEach-Object { Normalize-RepoPath $_ })
$pathHits = @()
foreach ($planned in $plannedPaths) {
foreach ($changed in $changedFiles) {
if (Test-PathOverlap -Planned $planned -Changed $changed) {
$pathHits += $changed
}
}
}
$subject = (Invoke-Git log -1 '--format=%s' $branch | Select-Object -First 1).Trim()
$diffText = (Invoke-Git diff --unified=0 "$BaseRef...$branch") -join "`n"
$writeContractHits = @(
$writeContracts | Where-Object {
$branch -match [Regex]::Escape($_) `
-or $subject -match [Regex]::Escape($_) `
-or $diffText -match [Regex]::Escape($_)
}
)
$reuseContractHits = @(
$reuseContracts | Where-Object {
$branch -match [Regex]::Escape($_) `
-or $subject -match [Regex]::Escape($_) `
-or $diffText -match [Regex]::Escape($_)
}
)
if ($pathHits.Count -gt 0 -or $writeContractHits.Count -gt 0) {
$branchFindings += [pscustomobject]@{
Branch = $branch
Subject = $subject
PathHits = @($pathHits | Select-Object -Unique)
WriteContractHits = @($writeContractHits | Select-Object -Unique)
ReuseContractHits = @($reuseContractHits | Select-Object -Unique)
}
} elseif ($reuseContractHits.Count -gt 0) {
$reuseFindings += [pscustomobject]@{
Branch = $branch
Subject = $subject
ReuseContractHits = @($reuseContractHits | Select-Object -Unique)
}
}
}
$existingFindings = @()
$existingReuseFindings = @()
$searchRoots = @('app', 'config', 'database', 'modules', 'resources', 'routes', 'tests')
foreach ($keyword in $writeContracts) {
$matches = @(& git grep -n -I -F -e $keyword $BaseRef -- @searchRoots 2>$null)
if ($LASTEXITCODE -notin @(0, 1)) {
throw "检查主线公共契约失败:$keyword"
}
if ($matches.Count -gt 0) {
$existingFindings += [pscustomobject]@{
Keyword = $keyword
Matches = @($matches | Select-Object -First 8)
}
}
}
foreach ($keyword in $reuseContracts) {
$matches = @(& git grep -n -I -F -e $keyword $BaseRef -- @searchRoots 2>$null)
if ($LASTEXITCODE -notin @(0, 1)) {
throw "检查主线仅复用公共契约失败:$keyword"
}
if ($matches.Count -gt 0) {
$existingReuseFindings += [pscustomobject]@{
Keyword = $keyword
Matches = @($matches | Select-Object -First 8)
}
}
}
Write-Host "Codex 任务开工检查:$Task" -ForegroundColor Cyan
Write-Host "开发者:$Developer;Git 身份:$gitUser;基线:$BaseRef"
Write-Host "预计路径:$($plannedPaths -join ', ')"
Write-Host "将写入的公共契约:$($writeContracts -join ', ')"
if ($reuseContracts.Count -gt 0) {
Write-Host "仅复用、不修改的公共契约:$($reuseContracts -join ', ')"
}
if ($untrackedChanges.Count -gt 0) {
Write-Host '提醒:存在未跟踪文件,Codex 必须确认归属且不得顺带提交:' -ForegroundColor Yellow
$untrackedChanges | ForEach-Object { Write-Host " $_" }
}
if ($existingFindings.Count -gt 0) {
Write-Host '主线已存在相同关键词,请先复用或说明差异:' -ForegroundColor Yellow
foreach ($finding in $existingFindings) {
Write-Host " [$($finding.Keyword)]"
$finding.Matches | ForEach-Object { Write-Host " $_" }
}
}
if ($existingReuseFindings.Count -gt 0) {
Write-Host '主线已存在下列仅复用契约;这属于预期复用,不单独阻断:' -ForegroundColor Cyan
foreach ($finding in $existingReuseFindings) {
Write-Host " [$($finding.Keyword)]"
$finding.Matches | ForEach-Object { Write-Host " $_" }
}
}
if ($reuseFindings.Count -gt 0) {
Write-Host '提示:以下远程任务分支也在使用仅复用契约;当前任务已声明不修改它们,因此不单独阻断。若实施中需要改该契约实现,必须停止、更新 WIP PR 并重新预检:' -ForegroundColor Yellow
foreach ($finding in $reuseFindings) {
Write-Host " $($finding.Branch) — $($finding.Subject)"
Write-Host " 仅复用契约:$($finding.ReuseContractHits -join ', ')"
}
}
if ($branchFindings.Count -gt 0) {
Write-Host '阻断:以下远程任务分支与当前计划存在实际写入重叠。停止修改,由相关开发者先完成协调:' -ForegroundColor Red
foreach ($finding in $branchFindings) {
Write-Host " $($finding.Branch) — $($finding.Subject)"
if ($finding.PathHits.Count -gt 0) {
Write-Host " 重叠路径:$($finding.PathHits -join ', ')"
}
if ($finding.WriteContractHits.Count -gt 0) {
Write-Host " 重叠写入契约:$($finding.WriteContractHits -join ', ')"
}
if ($finding.ReuseContractHits.Count -gt 0) {
Write-Host " 同时仅复用:$($finding.ReuseContractHits -join ', ')"
}
}
exit 2
}
Write-Host '通过:未发现远程任务分支与当前计划的实际写入重叠。可以创建或继续独立任务分支。' -ForegroundColor Green
exit 0