param( [string]$UnityProjectRoot = '', [string]$DocsRoot = '' ) $ErrorActionPreference = 'Stop' function Get-FullPath { param([string]$Path, [string]$BasePath) if ([string]::IsNullOrWhiteSpace($Path)) { return $null } if (-not [System.IO.Path]::IsPathRooted($Path)) { $Path = Join-Path $BasePath $Path } return [System.IO.Path]::GetFullPath($Path) } function Test-UnityProject { param([string]$Path) if ([string]::IsNullOrWhiteSpace($Path)) { return $false } return (Test-Path -LiteralPath (Join-Path $Path 'Assets')) -and (Test-Path -LiteralPath (Join-Path $Path 'ProjectSettings/ProjectVersion.txt')) } function Resolve-UnityProject { param([string]$ExplicitPath, [string]$DocumentationRoot) if (-not [string]::IsNullOrWhiteSpace($ExplicitPath)) { $resolved = Get-FullPath $ExplicitPath (Get-Location).Path if (-not (Test-UnityProject $resolved)) { throw "Unity project was not found: $resolved" } return $resolved } if (-not [string]::IsNullOrWhiteSpace($env:UNITY_PROJECT_ROOT)) { $resolved = Get-FullPath $env:UNITY_PROJECT_ROOT (Get-Location).Path if (-not (Test-UnityProject $resolved)) { throw "UNITY_PROJECT_ROOT is not a Unity project: $resolved" } return $resolved } $current = [System.IO.Path]::GetFullPath((Get-Location).Path) if (Test-UnityProject $current) { return $current } $parent = Split-Path -Parent $DocumentationRoot $candidates = @() if (Test-Path -LiteralPath $parent) { $directories = Get-ChildItem -LiteralPath $parent -Directory -ErrorAction SilentlyContinue foreach ($directory in $directories) { if (Test-UnityProject $directory.FullName) { $candidates += $directory.FullName } } } if ($candidates.Count -eq 1) { return [System.IO.Path]::GetFullPath($candidates[0]) } if ($candidates.Count -gt 1) { Write-Warning 'Multiple sibling Unity projects were found. Use -UnityProjectRoot to select one.' } return $null } function Get-GitRoot { param([string]$Path) $result = & git -C $Path rev-parse --show-toplevel 2>$null if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($result)) { return $null } return [System.IO.Path]::GetFullPath(($result | Select-Object -First 1)) } function Get-GitChanges { param([string]$RepositoryRoot, [string]$RepositoryName) $result = @() if ([string]::IsNullOrWhiteSpace($RepositoryRoot)) { return $result } $lines = & git -C $RepositoryRoot -c core.quotepath=false status --porcelain=v1 --untracked-files=all if ($LASTEXITCODE -ne 0) { throw "Could not read git status: $RepositoryRoot" } foreach ($line in $lines) { if ([string]::IsNullOrWhiteSpace($line) -or $line.Length -lt 4) { continue } $path = $line.Substring(3).Trim() if ($path.Contains(' -> ')) { $path = ($path -split ' -> ')[-1] } $result += [pscustomobject]@{ Repository = $RepositoryName Root = $RepositoryRoot Status = $line.Substring(0, 2) Path = $path.Replace('\', '/') } } return $result } function Show-Changes { param([string]$Title, [object[]]$Changes) Write-Output "" Write-Output "[$Title]" if ($Changes.Count -eq 0) { Write-Output ' Clean' return } foreach ($change in $Changes) { Write-Output (" {0} {1}" -f $change.Status, $change.Path) } } function Get-MarkdownLinkFailures { param([string]$DocumentationRoot, [object[]]$Changes) $failures = @() $markdownChanges = $Changes | Where-Object { $_.Path -like '*.md' -and $_.Status -notmatch 'D' } foreach ($change in $markdownChanges) { $fullPath = Join-Path $DocumentationRoot $change.Path if (-not (Test-Path -LiteralPath $fullPath)) { continue } $content = [System.IO.File]::ReadAllText($fullPath, [System.Text.Encoding]::UTF8) $matches = [regex]::Matches($content, '!?(?:\[[^\]]*\])\((?[^\)]+)\)') foreach ($match in $matches) { $link = $match.Groups['path'].Value.Trim() if ([string]::IsNullOrWhiteSpace($link) -or $link.StartsWith('#') -or $link -match '^[a-zA-Z][a-zA-Z0-9+.-]*://' -or $link -match '^mailto:') { continue } if ($link.StartsWith('<') -and $link.EndsWith('>')) { $link = $link.Substring(1, $link.Length - 2) } $pathOnly = ($link -split '#')[0] if ([string]::IsNullOrWhiteSpace($pathOnly)) { continue } $decoded = [System.Uri]::UnescapeDataString($pathOnly) $target = Get-FullPath $decoded (Split-Path -Parent $fullPath) if (-not (Test-Path -LiteralPath $target)) { $failures += [pscustomobject]@{ Source = $change.Path Link = $link } } } } return $failures } $scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path if ([string]::IsNullOrWhiteSpace($DocsRoot)) { $DocsRoot = Split-Path -Parent $scriptRoot } $DocsRoot = Get-FullPath $DocsRoot (Get-Location).Path $docsGitRoot = Get-GitRoot $DocsRoot if ([string]::IsNullOrWhiteSpace($docsGitRoot)) { throw "Documentation git repository was not found: $DocsRoot" } $DocsRoot = $docsGitRoot $resolvedUnityRoot = Resolve-UnityProject $UnityProjectRoot $DocsRoot $unityGitRoot = $null if (-not [string]::IsNullOrWhiteSpace($resolvedUnityRoot)) { $unityGitRoot = Get-GitRoot $resolvedUnityRoot if ([string]::IsNullOrWhiteSpace($unityGitRoot)) { throw "Unity project is not a git repository: $resolvedUnityRoot" } } $docsChanges = @(Get-GitChanges $DocsRoot 'UnityAI') $unityChanges = @() if (-not [string]::IsNullOrWhiteSpace($unityGitRoot)) { $unityChanges = @(Get-GitChanges $unityGitRoot (Split-Path -Leaf $resolvedUnityRoot)) } Write-Output '[workflow-check] Read-only StrayFog workflow check' Write-Output ("Documentation repository: {0}" -f $DocsRoot) if ($resolvedUnityRoot) { Write-Output ("Unity project: {0}" -f $resolvedUnityRoot) $versionFile = Join-Path $resolvedUnityRoot 'ProjectSettings/ProjectVersion.txt' $versionLine = Get-Content -LiteralPath $versionFile -Encoding UTF8 | Where-Object { $_ -match '^m_EditorVersion:' } | Select-Object -First 1 if ($versionLine) { Write-Output ("Unity version: {0}" -f (($versionLine -split ':', 2)[1].Trim())) } } else { Write-Warning 'Unity project was not found. Running documentation-only checks.' } Show-Changes 'UnityAI changes' $docsChanges Show-Changes 'Unity project changes' $unityChanges $unityPaths = @($unityChanges | ForEach-Object { $_.Path }) $copyRelationships = @( [pscustomobject]@{ Name = 'CopyToHF' SourcePattern = '^Assets/StrayFog/Editor/CopyToX/CopyToHF/' TargetPattern = '^Assets/Game/GameHFScripte/CopyToHF/' GeneratedName = 'HF' }, [pscustomobject]@{ Name = 'CopyToGeneral' SourcePattern = '^Assets/StrayFog/Editor/CopyToX/CopyToGeneral/' TargetPattern = '^Assets/Game/GameGeneralScripte/CopyToGeneral/' GeneratedName = 'General' } ) Write-Output '' Write-Output '[Source and generation boundary]' $relationshipDetected = $false foreach ($relationship in $copyRelationships) { $hasSource = @($unityPaths | Where-Object { $_ -match $relationship.SourcePattern }).Count -gt 0 $hasTarget = @($unityPaths | Where-Object { $_ -match $relationship.TargetPattern }).Count -gt 0 if (-not $hasSource -and -not $hasTarget) { continue } $relationshipDetected = $true if ($hasSource -and -not $hasTarget) { Write-Warning ("{0} source changed but generated target did not change. Run CopyToX and inspect the generated diff." -f $relationship.Name) } elseif ($hasTarget -and -not $hasSource) { Write-Warning ("Generated {0} files changed without {1} source changes. Confirm that generated files were not edited directly." -f $relationship.GeneratedName, $relationship.Name) } else { Write-Output (" Source and generated target changes were both detected for {0}." -f $relationship.Name) } } if (-not $relationshipDetected) { Write-Output ' No CopyToX source/target relationship was detected.' } $mappingPath = Join-Path $scriptRoot 'workflow-path-mappings.json' $mapping = Get-Content -LiteralPath $mappingPath -Raw -Encoding UTF8 | ConvertFrom-Json $documents = @{} $verifications = @{} foreach ($path in $unityPaths) { foreach ($entry in $mapping.mappings) { if ($path -match $entry.pattern) { $documents[$entry.document] = $true $verifications[$entry.verification] = $true } } } Write-Output '' Write-Output '[Suggested verification]' if ($verifications.Count -eq 0) { Write-Output ' No Unity verification suggestions for the current diff.' } else { foreach ($item in $verifications.Keys | Sort-Object) { Write-Output (" - {0}" -f $item) } } Write-Output '' Write-Output '[Documentation impact]' if ($documents.Count -eq 0) { Write-Output ' No module document mapping matched the Unity diff.' } else { foreach ($document in $documents.Keys | Sort-Object) { $changed = @($docsChanges | Where-Object { $_.Path -eq $document }).Count -gt 0 $marker = if ($changed) { 'updated' } else { 'review if long-term behavior changed' } Write-Output (" - {0} ({1})" -f $document, $marker) } } $linkFailures = @(Get-MarkdownLinkFailures $DocsRoot $docsChanges) Write-Output '' Write-Output '[Markdown links]' if ($linkFailures.Count -eq 0) { Write-Output ' Changed Markdown links are valid.' } else { foreach ($failure in $linkFailures) { Write-Error ("Broken link in {0}: {1}" -f $failure.Source, $failure.Link) } exit 1 } Write-Output '' Write-Output '[workflow-check] Completed without writing files.' exit 0