[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