param( [switch]$DryRun ) $ErrorActionPreference = "Continue" $script:RepoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "..")) $script:LogPath = Join-Path $script:RepoRoot ".git/graphify-hook.log" function Write-HookLog { param([string]$Message) $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $line = "[$timestamp] $Message" Write-Host $line try { Add-Content -Path $script:LogPath -Value $line -Encoding UTF8 } catch { Write-Host "[graphify hook] Unable to write log: $($_.Exception.Message)" } } function Test-GraphifyPython { param([string]$PythonPath) if ([string]::IsNullOrWhiteSpace($PythonPath)) { return $false } try { & $PythonPath -c "import graphify" *> $null return ($LASTEXITCODE -eq 0) } catch { return $false } } function Get-GraphifyPython { $candidates = @() foreach ($relative in @("Unity/graphify-out/.graphify_python", "graphify-out/.graphify_python")) { $path = Join-Path $script:RepoRoot $relative if (Test-Path $path) { $value = (Get-Content -Raw -Path $path -Encoding UTF8).Trim() if ($value) { $candidates += $value } } } foreach ($command in @("python", "py")) { $cmd = Get-Command $command -ErrorAction SilentlyContinue if ($cmd) { $candidates += $cmd.Source } } foreach ($candidate in $candidates | Select-Object -Unique) { if (Test-GraphifyPython $candidate) { return $candidate } } return $null } function Get-ChangedFiles { $fromEnv = $env:GRAPHIFY_CHANGED if (-not [string]::IsNullOrWhiteSpace($fromEnv)) { return $fromEnv -split "`r?`n" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } } $git = Get-Command git -ErrorAction SilentlyContinue if (-not $git) { return @() } Push-Location $script:RepoRoot try { $files = & git diff --name-only HEAD~1 HEAD 2>$null if (-not $files) { $files = & git diff --name-only HEAD 2>$null } return @($files | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) } finally { Pop-Location } } function Test-GitPathLike { param( [string]$Path, [string[]]$Patterns ) foreach ($pattern in $Patterns) { if ($Path -like $pattern) { return $true } } return $false } function Get-MatchingGitPaths { param( [string[]]$Paths, [string[]]$Patterns ) return @($Paths | Where-Object { Test-GitPathLike -Path $_ -Patterns $Patterns }) } function Set-GraphifyNeedsUpdate { param( [string]$RelativePath, [string]$Reason ) $target = Join-Path $script:RepoRoot $RelativePath $out = Join-Path $target "graphify-out" if (-not (Test-Path $out)) { return } $flag = Join-Path $out "needs_update" $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Add-Content -Path $flag -Value "[$timestamp] $Reason" -Encoding UTF8 Write-HookLog "Marked $RelativePath graph for manual semantic update: $Reason" } function Invoke-GraphifyRebuild { param( [string]$Label, [string]$RelativePath, [string]$PythonPath ) $target = Join-Path $script:RepoRoot $RelativePath $graphPath = Join-Path $target "graphify-out/graph.json" if (-not (Test-Path $graphPath)) { Write-HookLog "Skipping ${Label}: no graphify-out/graph.json found." return } if ($DryRun) { Write-HookLog "Dry run: would rebuild $Label graph at $target." return } Write-HookLog "Rebuilding $Label graph at $target." Push-Location $target try { & $PythonPath -c "from pathlib import Path; from graphify.watch import _rebuild_code; _rebuild_code(Path('.'))" *> $null if ($LASTEXITCODE -eq 0) { Write-HookLog "Rebuilt $Label graph." } else { Write-HookLog "Rebuild failed for $Label with exit code $LASTEXITCODE." } } catch { Write-HookLog "Rebuild failed for ${Label}: $($_.Exception.Message)" } finally { Pop-Location } } function Invoke-HookMain { Set-Location $script:RepoRoot $changedFiles = @( Get-ChangedFiles | ForEach-Object { $_ -replace '\\', '/' } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) -and $_ -notlike "graphify-out/*" -and $_ -notlike "Unity/graphify-out/*" } ) if ($changedFiles.Count -eq 0) { Write-HookLog "No changed files detected; nothing to rebuild." return 0 } $codeExtensions = @( ".cs", ".asmdef", ".shader", ".hlsl", ".cginc", ".py", ".js", ".ts", ".ps1", ".json", ".toml", ".yaml", ".yml" ) $changedCode = @( $changedFiles | Where-Object { $ext = [System.IO.Path]::GetExtension($_).ToLowerInvariant() $codeExtensions -contains $ext } ) $rootCodePatterns = @( "Unity/Assets/Scripts/*", "Tools/*.ps1", "Tools/*.py", "Tools/*.js", "Tools/OSS/*", "Tools/Dashboard/*.py", "ExcelExport/*" ) $unityCodePatterns = @( "Unity/Assets/Scripts/*" ) $graphifyConfigPatterns = @( ".graphifyignore", ".gitignore", "AGENTS.md", "Unity/.graphifyignore", "Tools/GraphifyPostCommit.ps1", "Tools/InstallGraphifyHook.ps1" ) $semanticDocPatterns = @( "MD/GameMDFramework/*.md", "MD/GameMDFramework/*/*.md" ) $rootChangedCode = @(Get-MatchingGitPaths -Paths $changedCode -Patterns $rootCodePatterns) $unityChangedCode = @(Get-MatchingGitPaths -Paths $changedCode -Patterns $unityCodePatterns) $changedGraphifyConfig = @(Get-MatchingGitPaths -Paths $changedFiles -Patterns $graphifyConfigPatterns) $changedSemanticDocs = @(Get-MatchingGitPaths -Paths $changedFiles -Patterns $semanticDocPatterns) if ($changedSemanticDocs.Count -gt 0) { Set-GraphifyNeedsUpdate -RelativePath "." -Reason "Architecture docs changed; run graphify update manually for semantic edges." } $shouldRebuildRoot = ($rootChangedCode.Count -gt 0 -or $changedGraphifyConfig.Count -gt 0) $shouldRebuildUnity = ($unityChangedCode.Count -gt 0 -or ($changedGraphifyConfig | Where-Object { $_ -eq "Unity/.graphifyignore" }).Count -gt 0) if (-not $shouldRebuildRoot -and -not $shouldRebuildUnity) { Write-HookLog "$($changedFiles.Count) changed file(s), but none are in the narrowed graphify AST scopes." return 0 } $python = Get-GraphifyPython if (-not $python) { Write-HookLog "No Python interpreter with graphify installed was found." return 0 } Write-HookLog "$($changedFiles.Count) changed file(s); root scope: $($rootChangedCode.Count), Unity scope: $($unityChangedCode.Count), graphify config: $($changedGraphifyConfig.Count). Using Python: $python" if ($shouldRebuildRoot) { Invoke-GraphifyRebuild -Label "root" -RelativePath "." -PythonPath $python } else { Write-HookLog "Skipping root graph: no narrowed root-scope code/config changed." } if ($shouldRebuildUnity) { Invoke-GraphifyRebuild -Label "Unity" -RelativePath "Unity" -PythonPath $python } else { Write-HookLog "Skipping Unity graph: no narrowed Unity-scope code/config changed." } return 0 } $mutexName = "Local\TH1-GraphifyPostCommit" $mutex = $null $hasLock = $false try { $mutex = [System.Threading.Mutex]::new($false, $mutexName) $hasLock = $mutex.WaitOne(0) if (-not $hasLock) { Write-HookLog "Another graphify rebuild is already running; skipping this post-commit run." exit 0 } $exitCode = Invoke-HookMain exit $exitCode } catch { Write-HookLog "Graphify hook failed: $($_.Exception.Message)" exit 1 } finally { if ($hasLock -and $mutex) { try { $mutex.ReleaseMutex() | Out-Null } catch { } } if ($mutex) { $mutex.Dispose() } }