param( [string]$OutputPath = "PROJECT_MD5_REFERENCE.md", [switch]$Verify ) $ErrorActionPreference = "Stop" $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $fallbackRoot = Resolve-Path -LiteralPath (Join-Path $scriptDir "..") try { $gitRoot = git -C $fallbackRoot rev-parse --show-toplevel 2>$null if ([string]::IsNullOrWhiteSpace($gitRoot)) { $repoRoot = $fallbackRoot.Path } else { $repoRoot = (Resolve-Path -LiteralPath $gitRoot.Trim()).Path } } catch { $repoRoot = $fallbackRoot.Path } Push-Location $repoRoot try { $normalizedOutputPath = $OutputPath.Replace("\", "/") $files = @() try { $files = git ls-files --cached --modified --others --exclude-standard | ForEach-Object { $_.Replace("\", "/") } | Where-Object { $_ -and $_ -ne $normalizedOutputPath } } catch { $ignoredDirectories = @(".git", ".vs", "bin", "obj", "publish", "Migrations") $ignoredExtensions = @(".user", ".suo", ".userosscache", ".sln.docstates", ".log", ".zip") $files = Get-ChildItem -LiteralPath $repoRoot -Recurse -File | Where-Object { $relative = [System.IO.Path]::GetRelativePath($repoRoot, $_.FullName).Replace("\", "/") $parts = $relative -split "/" $extension = [System.IO.Path]::GetExtension($_.Name) ($ignoredDirectories | Where-Object { $parts -contains $_ }).Count -eq 0 -and ($ignoredExtensions -notcontains $extension) -and $relative -ne $normalizedOutputPath } | ForEach-Object { [System.IO.Path]::GetRelativePath($repoRoot, $_.FullName).Replace("\", "/") } } $files = $files | Sort-Object -Unique $hashRows = foreach ($file in $files) { $fullPath = Join-Path $repoRoot $file if (Test-Path -LiteralPath $fullPath -PathType Leaf) { $item = Get-Item -LiteralPath $fullPath $hash = Get-FileHash -LiteralPath $fullPath -Algorithm MD5 [PSCustomObject]@{ Path = $file Size = $item.Length MD5 = $hash.Hash.ToLowerInvariant() } } } $statusLines = @() try { $statusLines = git status --short | Where-Object { $statusPath = ($_ -replace "^[ MADRCU?!]{2}\s+", "").Replace("\", "/") $statusPath -ne $normalizedOutputPath } } catch { $statusLines = @() } if ($Verify) { if (-not (Test-Path -LiteralPath $OutputPath -PathType Leaf)) { throw "Cannot verify because $OutputPath does not exist." } $expectedRows = Get-Content -LiteralPath $OutputPath | ForEach-Object { if ($_ -match "^\| (?.+?) \| (?\d+) \| (?[a-f0-9]{32}) \|$") { [PSCustomObject]@{ Path = $Matches.Path Size = [int64]$Matches.Size MD5 = $Matches.MD5 } } } $expectedByPath = @{} foreach ($row in $expectedRows) { $expectedByPath[$row.Path] = $row } $currentByPath = @{} foreach ($row in $hashRows) { $currentByPath[$row.Path] = $row } $added = @($currentByPath.Keys | Where-Object { -not $expectedByPath.ContainsKey($_) } | Sort-Object) $removed = @($expectedByPath.Keys | Where-Object { -not $currentByPath.ContainsKey($_) } | Sort-Object) $changed = @( foreach ($path in $expectedByPath.Keys) { if ($currentByPath.ContainsKey($path) -and $currentByPath[$path].MD5 -ne $expectedByPath[$path].MD5) { "$path ($($expectedByPath[$path].MD5) -> $($currentByPath[$path].MD5))" } } ) | Sort-Object if ($added.Count -eq 0 -and $removed.Count -eq 0 -and $changed.Count -eq 0) { Write-Host "MD5 reference verified. No changes detected across $($hashRows.Count) files." return } Write-Host "MD5 reference verification found differences." if ($added.Count -gt 0) { Write-Host "" Write-Host "Added files:" $added | ForEach-Object { Write-Host " $_" } } if ($removed.Count -gt 0) { Write-Host "" Write-Host "Removed files:" $removed | ForEach-Object { Write-Host " $_" } } if ($changed.Count -gt 0) { Write-Host "" Write-Host "Changed files:" $changed | ForEach-Object { Write-Host " $_" } } exit 1 } $projectName = Split-Path -Leaf $repoRoot $generatedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss zzz" $targetFramework = "" if (Test-Path -LiteralPath "Fitness_core_BE.csproj") { $projectXml = [xml](Get-Content -Raw -LiteralPath "Fitness_core_BE.csproj") $targetFramework = $projectXml.Project.PropertyGroup.TargetFramework } $topLevelCounts = $hashRows | Group-Object { if ($_.Path.Contains("/")) { ($_.Path -split "/")[0] } else { "." } } | Sort-Object Name $lines = [System.Collections.Generic.List[string]]::new() $lines.Add("# Project MD5 Reference") $lines.Add("") $lines.Add("Generated: $generatedAt") $lines.Add("") $lines.Add("Project: $projectName") if (-not [string]::IsNullOrWhiteSpace($targetFramework)) { $lines.Add("") $lines.Add("Target framework: $targetFramework") } $lines.Add("") $lines.Add("Purpose: baseline MD5 hashes for future implementation checks. MD5 is used here for change detection, not for security-sensitive integrity verification.") $lines.Add("") $lines.Add('Manifest scope: Git tracked files plus untracked files not ignored by `.gitignore`; build outputs, IDE files, ignored migrations, and this manifest are excluded.') $lines.Add("") $lines.Add("Regenerate command:") $lines.Add("") $lines.Add('```powershell') $lines.Add('.\tools\Update-Md5Reference.ps1') $lines.Add('```') $lines.Add("") $lines.Add("Verify command:") $lines.Add("") $lines.Add('```powershell') $lines.Add('.\tools\Update-Md5Reference.ps1 -Verify') $lines.Add('```') $lines.Add("") $lines.Add("## Project Shape") $lines.Add("") $lines.Add("| Area | Files |") $lines.Add("| --- | ---: |") foreach ($group in $topLevelCounts) { $lines.Add("| $($group.Name) | $($group.Count) |") } $lines.Add("") $lines.Add("## Git Status At Baseline") $lines.Add("") if ($statusLines.Count -gt 0) { $lines.Add('```text') foreach ($status in $statusLines) { $lines.Add($status) } $lines.Add('```') } else { $lines.Add("Clean working tree or Git status unavailable.") } $lines.Add("") $lines.Add("## MD5 Hashes") $lines.Add("") $lines.Add("| Path | Size Bytes | MD5 |") $lines.Add("| --- | ---: | --- |") foreach ($row in $hashRows) { $lines.Add("| $($row.Path) | $($row.Size) | $($row.MD5) |") } $lines.Add("") Set-Content -LiteralPath $OutputPath -Value $lines -Encoding UTF8 Write-Host "Wrote $OutputPath with $($hashRows.Count) MD5 hashes." } finally { Pop-Location }