param( [string]$Source="", # .bak ou BUG_KB.json.txt par d?faut [switch]$Execute, [int]$MaxFieldLen = 8000 # tronque champs texte monstrueux (s?curit?) ) $ErrorActionPreference='Stop' $root="\\DS-918\\chatgpt\\ChatGPT-Gouvernance-Projets\\_registry" $kb = Join-Path $root "bug_kb\\BUG_KB.json.txt" $stage="C:\\Temp_Gouvernance" if(!(Test-Path $stage)){ New-Item -ItemType Directory -Force -Path $stage | Out-Null } $Utf8NoBom = New-Object Text.UTF8Encoding($false) function Choose-Source([string]$kb,[string]$Source){ if($Source -and (Test-Path $Source)){ return (Get-Item $Source).FullName } return $kb } function Sanitize-Line([string]$s){ if(-not $s){ return "" } # 1) remplacer contr?les (hors CR/LF/TAB) par espace $s = [regex]::Replace($s,'[\x00-\x08\x0B\x0C\x0E-\x1F]',' ') # 2) compresser s?quences d'octets "hauts" (souvent mojibake) tr?s longues $s = [regex]::Replace($s,'[\u0080-\uFFFF]{50,}','...') return $s } function Truncate-LongFields([string]$json,[int]$max){ if($max -le 0){ return $json } # Heuristique: si un champ texte d?passe $max, on coupe proprement (sans casser les quotes) # On ne fait pas une vraie r??criture JSON champ-par-champ pour rester rapide: # on rep?re les segments entre guillemets et coupe s'ils sont ?normes. $sb = New-Object System.Text.StringBuilder $inStr=$false; $esc=$false; $count=0 for($i=0;$i -lt $json.Length;$i++){ $ch=$json[$i] if($inStr){ if($esc){ $esc=$false; $sb.Append($ch)|Out-Null; continue } if($ch -eq '\'){ $esc=$true; $sb.Append($ch)|Out-Null; continue } if($ch -eq '"'){ $inStr=$false; $count=0; $sb.Append($ch)|Out-Null; continue } $count++ if($count -gt $max){ # sauter jusqu'? la fin de la string (en respectant l'?chappement) while($i -lt ($json.Length-1)){ $i++ $x=$json[$i] if($x -eq '\'){ if($i -lt ($json.Length-1)){ $i++ }; continue } if($x -eq '"'){ break } } $sb.Append('"...')|Out-Null continue } $sb.Append($ch)|Out-Null continue } else { if($ch -eq '"'){ $inStr=$true; $count=0; $sb.Append($ch)|Out-Null; continue } $sb.Append($ch)|Out-Null } } return $sb.ToString() } function Rescue-Entries([string]$Path){ $fs=[IO.File]::Open($Path,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::ReadWrite) $sr=New-Object IO.StreamReader($fs,[Text.UTF8Encoding]::new($false),$true,65536) try{ $objs = New-Object System.Collections.ArrayList $seenEntries=$false $inStr=$false; $esc=$false; $brace=0 $buf = New-Object System.Text.StringBuilder while(-not $sr.EndOfStream){ $line = $sr.ReadLine() if($line -match '--- DOC-VERSION-FOOTER ---'){ break } if(-not $seenEntries){ if($line -match '"entries"\s*:\s*\['){ $seenEntries=$true } continue } # on arr?te si on voit la fermeture globale de entries : ']' if($seenEntries -and ($line -match '^\s*\]')){ break } $line = Sanitize-Line $line for($i=0;$i -lt $line.Length;$i++){ $ch = $line[$i] if($inStr){ $buf.Append($ch)|Out-Null if($esc){ $esc=$false; continue } if($ch -eq '\'){ $esc=$true; continue } if($ch -eq '"'){ $inStr=$false; continue } continue } else { if($ch -eq '"'){ $inStr=$true; $buf.Append($ch)|Out-Null; continue } if($ch -eq '{'){ if($brace -eq 0){ $buf.Clear() | Out-Null; $buf.Append('{')|Out-Null; $brace=1; continue } $brace++; $buf.Append('{')|Out-Null; continue } if($ch -eq '}'){ if($brace -gt 0){ $brace--; $buf.Append('}')|Out-Null if($brace -eq 0){ # objet complet captur? $obj = $buf.ToString() # Validation rapide (on ignore les objets invalides) try{ $null = $obj | ConvertFrom-Json [void]$objs.Add($obj) } catch { # drop object } $buf.Clear()|Out-Null } continue } } if($brace -gt 0){ $buf.Append($ch)|Out-Null } } } if($brace -gt 0){ $buf.Append("`n")|Out-Null } } return ,$objs } finally { $sr.Close(); $fs.Close() } } function Write-SafeTxt([string]$Dest,[string]$Body,[string]$Tag){ $iso=(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK') $policy='TXT-ONLY v1.0; SAFE-WRITE v1.1; GOV_SCRIPT_GATE v1.3' $dir=Split-Path $Dest -Parent; if($dir -and -not (Test-Path $dir)){ New-Item -ItemType Directory -Force -Path $dir | Out-Null } $tmpLocal= Join-Path "C:\\Temp_Gouvernance" ([IO.Path]::GetFileName($Dest)) [IO.File]::WriteAllText($tmpLocal,$Body,(New-Object Text.UTF8Encoding($false))) $sha=[Security.Cryptography.SHA256]::Create() try{ $bytes=[IO.File]::ReadAllBytes($tmpLocal); $h=[BitConverter]::ToString($sha.ComputeHash($bytes)).Replace('-','').ToLower() } finally{ $sha.Dispose() } [IO.File]::AppendAllText($tmpLocal,"`r`n`r`n--- DOC-VERSION-FOOTER ---`r`nGenerated: $iso`r`nSHA-256: $h`r`nPolicy: $policy`r`nSource: KB_STRICT_RESCUE_v1.0`r`n",(New-Object Text.UTF8Encoding($false))) $tmp="$Dest.tmp"; if(Test-Path $Dest){ Copy-Item $Dest ($Dest+".bak_"+(Get-Date -Format 'yyyyMMdd_HHmmss')) -Force } Copy-Item $tmpLocal $tmp -Force; Move-Item $tmp $Dest -Force } # --- main --- $src = Choose-Source -kb $kb -Source $Source $objs = Rescue-Entries -Path $src $okCount = ($objs | Measure-Object).Count # Rebuild entries JSON $entriesJson = "[`r`n" + ($objs -join ",`r`n") + "`r`n]" $entriesJson = Truncate-LongFields -json $entriesJson -max $MaxFieldLen $now = Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK' $body = "{`"entries`": $entriesJson, `"updated`": `"$now`"}" $parseOK=$true try{ $null = $body | ConvertFrom-Json } catch { $parseOK=$false } if(-not $Execute){ "{0,-20} {1}" -f "source:", $src | Write-Host "{0,-20} {1}" -f "valid objects:", $okCount | Write-Host "{0,-20} {1}" -f "json parse:", ($(if($parseOK){"OK"}else{"FAIL"})) | Write-Host $sz = [Text.Encoding]::UTF8.GetByteCount($body) "{0,-20} {1:N1} MB" -f "rebuilt size:", ($sz/1MB) | Write-Host Write-Host "Preview OK. Utilise -Execute pour ?crire BUG_KB.json.txt." exit 0 } if(-not $parseOK){ Write-Host "[FAIL] Reconstruction JSON invalide (aucune ?criture)." exit 2 } Write-SafeTxt -Dest $kb -Body $body -Tag "KB_STRICT_RESCUE_v1.0" Write-Host "[OK] KB reconstruite (entries=$okCount) -> $kb"