# kb_emit_final_report_v1.0.ps1 # Purpose: Generate a plain-text final report with metrics and integrity checks. # PS 5.1-safe, no ternary, UTF-8 BOM, TXT-only, SAFE-WRITE (staging + .tmp + .bak). [CmdletBinding()] param( [switch]$Preview, [switch]$Execute, [string]$Root = "\\DS-918\chatgpt\ChatGPT-Gouvernance-Projets\_registry", [string]$StageRoot = "C:\Temp_Gouvernance" ) function Ensure-Dir([string]$Path){ if(-not (Test-Path -LiteralPath $Path)){ New-Item -ItemType Directory -Force -Path $Path | Out-Null } } function Ensure-Parent([string]$Target){ $p=Split-Path -Parent $Target; if($p){ Ensure-Dir $p } } function Get-NowIso(){ (Get-Date).ToString("yyyy-MM-ddTHH:mm:ssK") } function Read-JsonSansFooter([string]$Path){ $sr = New-Object IO.StreamReader($Path) try{ $L = New-Object 'System.Collections.Generic.List[string]' while(-not $sr.EndOfStream){ $line = $sr.ReadLine() if($line -match '^\s*---\s*DOC-VERSION-FOOTER'){ break } $L.Add($line) | Out-Null } ($L -join "`n") } finally { $sr.Dispose() } } function Get-FileSHA256([string]$Path){ if(-not(Test-Path -LiteralPath $Path)){ return "" } $sha=[Security.Cryptography.SHA256]::Create() $fs=[IO.File]::OpenRead($Path) try{ -join ($sha.ComputeHash($fs) | ForEach-Object { $_.ToString("x2") }) } finally { $fs.Dispose(); $sha.Dispose() } } function Write-SafeText([string]$Target,[string]$Content,[string]$StageRoot){ Ensure-Dir $StageRoot; Ensure-Parent $Target $tmp=Join-Path $StageRoot ("write_" + [IO.Path]::GetRandomFileName()) $utf8=New-Object Text.UTF8Encoding($true) # UTF-8 with BOM [IO.File]::WriteAllText($tmp,$Content,$utf8) $bak=$null if(Test-Path -LiteralPath $Target){ $bak=$Target+"."+(Get-Date -f yyyyMMdd_HHmmss)+".bak" Copy-Item -LiteralPath $Target -Destination $bak -Force } $tmpR="$Target.tmp" Copy-Item -LiteralPath $tmp -Destination $tmpR -Force Move-Item -LiteralPath $tmpR -Destination $Target -Force Remove-Item -LiteralPath $tmp -Force return $bak } # Paths $BugKbDir = Join-Path $Root "bug_kb" $BootDir = Join-Path $Root "bootpack" $LogsDir = Join-Path $Root "logs" Ensure-Dir $BugKbDir; Ensure-Dir $BootDir; Ensure-Dir $LogsDir $KbPath = Join-Path $BugKbDir "BUG_KB.json.txt" $BootPaste = Join-Path $BootDir "bootpack_paste.txt" $BootTxt = Join-Path $BootDir "bootpack.txt" $ReportOut = Join-Path $LogsDir ("final_report_" + (Get-Date -f yyyyMMdd_HHmmss) + ".txt") # Load KB (no footer) + metrics $kbSize=0; $kbSha=""; $kbEntries=-1; $kbUpdated=""; $kbParseOk=$false; $kbMojibake=-1 $kbRawHead = "" if(Test-Path -LiteralPath $KbPath){ $kbSize = (Get-Item -LiteralPath $KbPath).Length $kbSha = Get-FileSHA256 $KbPath $kbRawHead = Read-JsonSansFooter $KbPath try { $kbObj = $kbRawHead | ConvertFrom-Json -ErrorAction Stop $kbParseOk = $true if($kbObj -and ($kbObj.PSObject.Properties.Name -contains 'entries') -and $kbObj.entries){ try { $kbEntries = $kbObj.entries.Count } catch { $kbEntries = -1 } } if($kbObj -and ($kbObj.PSObject.Properties.Name -contains 'updated')){ $kbUpdated = [string]$kbObj.updated } } catch { $kbParseOk = $false } $kbMojibake = ([regex]::Matches($kbRawHead,'�|�|'|"|�|?|@\{')).Count } # Load PASTE + metrics $pasteSize=0; $pasteSha=""; $pasteEntries=-1; $pasteParseOk=$false; $pasteMojibake=-1; $pasteRaw="" if(Test-Path -LiteralPath $BootPaste){ $pasteSize = (Get-Item -LiteralPath $BootPaste).Length $pasteSha = Get-FileSHA256 $BootPaste $pasteRaw = Get-Content -LiteralPath $BootPaste -Raw try { $pasteObj = $pasteRaw | ConvertFrom-Json -ErrorAction Stop $pasteParseOk=$true if($pasteObj -and ($pasteObj.PSObject.Properties.Name -contains 'entries') -and $pasteObj.entries){ try { $pasteEntries = $pasteObj.entries.Count } catch { $pasteEntries = -1 } } } catch { $pasteParseOk=$false } $pasteMojibake = ([regex]::Matches($pasteRaw,'�|�|'|"|�|?|@\{')).Count } # Load BOOTPACK pointer section + checks $bootSize=0; $bootSha=""; $bootRaw="" $ptrPath=""; $ptrSha=""; $ptrUpdated=""; $ptrEntries=-1; $ptrSize=-1 $chkPath=$false; $chkSha=$false; $chkEntries=$false; $chkSize=$false; $hasJsonMassive=$false if(Test-Path -LiteralPath $BootTxt){ $bootSize = (Get-Item -LiteralPath $BootTxt).Length $bootSha = Get-FileSHA256 $BootTxt $bootRaw = Get-Content -LiteralPath $BootTxt -Raw $mPath = [regex]::Match($bootRaw,'(?m)^Path=(.+)$') $mSha = [regex]::Match($bootRaw,'(?m)^SHA256=([0-9A-Fa-f]{64})$') $mUpd = [regex]::Match($bootRaw,'(?m)^Updated=(.+)$') $mEnt = [regex]::Match($bootRaw,'(?m)^Entries=(\d+)$') $mSize = [regex]::Match($bootRaw,'(?m)^SizeBytes=(\d+)$') if($mPath.Success){ $ptrPath = $mPath.Groups[1].Value.Trim() } if($mSha.Success){ $ptrSha = $mSha.Groups[1].Value.Trim().ToLower() } if($mUpd.Success){ $ptrUpdated = $mUpd.Groups[1].Value.Trim() } if($mEnt.Success){ $ptrEntries = [int]$mEnt.Groups[1].Value } if($mSize.Success){ $ptrSize = [int]$mSize.Groups[1].Value } # Checks vs actual paste if(($ptrPath.ToLower()) -eq ($BootPaste.ToLower())){ $chkPath = $true } if(($ptrSha) -eq ($pasteSha.ToLower())){ $chkSha = $true } if(($ptrEntries -ge 0) -and ($pasteEntries -ge 0) -and ($ptrEntries -eq $pasteEntries)){ $chkEntries = $true } if(($ptrSize -ge 0) -and ($ptrSize -eq $pasteSize)){ $chkSize = $true } $hasJsonMassive = ($bootRaw -match '"entries"\s*:') } # List recent backups (KB and boot files) function List-RecentBaks([string]$folder,[int]$n){ if(-not (Test-Path -LiteralPath $folder)){ return @() } Get-ChildItem -LiteralPath $folder -Filter "*.bak" -File | Sort-Object LastWriteTime -Descending | Select-Object -First $n | ForEach-Object { [pscustomobject]@{ Name=$_.Name; Size=$_.Length; When=$_.LastWriteTime } } } $kbBaks = List-RecentBaks $BugKbDir 3 $bootBaks = List-RecentBaks $BootDir 3 # Build report text $ts = Get-NowIso $lines = New-Object System.Collections.Generic.List[string] $lines.Add("===== FINAL REPORT :: ChatGPT-Gouvernance-Projets =====") $lines.Add("Generated: " + $ts) $lines.Add("Root: " + $Root) $lines.Add("Policy: TXT-ONLY; SAFE-WRITE v1.1; GOV_SCRIPT_GATE v1.3") $lines.Add("") $lines.Add("[KB]") $lines.Add("Path=" + $KbPath) $lines.Add(("SizeBytes={0} SHA256={1} ParseOK={2}" -f $kbSize,$kbSha,$kbParseOk)) $lines.Add(("Entries={0} Updated={1} MojibakeHits={2}" -f $kbEntries,$kbUpdated,$kbMojibake)) $lines.Add("") $lines.Add("[PASTE]") $lines.Add("Path=" + $BootPaste) $lines.Add(("SizeBytes={0} SHA256={1} ParseOK={2}" -f $pasteSize,$pasteSha,$pasteParseOk)) $lines.Add(("Entries={0} MojibakeHits={1}" -f $pasteEntries,$pasteMojibake)) $lines.Add("") $lines.Add("[BOOTPACK POINTER]") $lines.Add("Path=" + $BootTxt) $lines.Add(("SizeBytes={0} SHA256={1}" -f $bootSize,$bootSha)) $lines.Add(("Pointer.Path={0}" -f $ptrPath)) $lines.Add(("Pointer.SHA256={0}" -f $ptrSha)) $lines.Add(("Pointer.Updated={0}" -f $ptrUpdated)) $lines.Add(("Pointer.Entries={0}" -f $ptrEntries)) $lines.Add(("Pointer.SizeBytes={0}" -f $ptrSize)) $lines.Add(("JSON_massive_in_bootpack=" + $hasJsonMassive)) $lines.Add(("CHK.Path==Paste : " + ($(if($chkPath){"OK"}else{"FAIL"})))) $lines.Add(("CHK.SHA==Paste : " + ($(if($chkSha){"OK"}else{"FAIL"})))) $lines.Add(("CHK.Entries match : " + ($(if($chkEntries){"OK"}else{"FAIL"})))) $lines.Add(("CHK.Size match : " + ($(if($chkSize){"OK"}else{"FAIL"})))) $lines.Add("") $lines.Add("[Recent backups]") if($kbBaks.Count -gt 0){ $lines.Add("KB backups (top 3):") foreach($b in $kbBaks){ $lines.Add((" - {0} size={1} when={2:yyyy-MM-dd HH:mm:ss}" -f $b.Name,$b.Size,$b.When)) } } else { $lines.Add("KB backups: ") } if($bootBaks.Count -gt 0){ $lines.Add("BOOT/PASTE backups (top 3):") foreach($b in $bootBaks){ $lines.Add((" - {0} size={1} when={2:yyyy-MM-dd HH:mm:ss}" -f $b.Name,$b.Size,$b.When)) } } else { $lines.Add("BOOT/PASTE backups: ") } $lines.Add("") # Tail of activity log (last 5) $logFile = Join-Path $LogsDir "registry_activity.log" $lines.Add("[Log tail]") if(Test-Path -LiteralPath $logFile){ $tail = Get-Content -LiteralPath $logFile -Tail 5 foreach($t in $tail){ $lines.Add(" " + $t) } } else { $lines.Add(" ") } $report = ($lines -join "`r`n") # PREVIEW if($Preview -or (-not $Execute)){ Write-Host "== PREVIEW :: FINAL REPORT v1.0 ==" Write-Host ("Will write -> {0}" -f $ReportOut) $previewLines = $report.Split("`n") $max=40; if($previewLines.Count -lt $max){ $max = $previewLines.Count } for($i=0;$i -lt $max;$i++){ Write-Host $previewLines[$i] } if($previewLines.Count -gt $max){ Write-Host "... (truncated preview) ..." } Write-Host "No write performed (Preview)." exit 0 } # EXECUTE $bak = Write-SafeText -Target $ReportOut -Content $report -StageRoot $StageRoot $bakMsg = $bak; if(-not $bakMsg){ $bakMsg = "" } Write-Host ("[OK] Final report written -> {0}" -f $ReportOut) Write-Host ("Backup: {0}" -f $bakMsg)