?# SALVAGE v1.0c (PS5.1-safe, ASCII-only) - reconstruit BUG_KB.json.txt compact, entree par entree param( [string]$Source="", [switch]$Execute, [int]$MaxBakMB = 50, [switch]$NoBootpack ) $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 } function Choose-Source([string]$kb,[string]$Source,[int]$MaxBakMB){ if($Source -and (Test-Path $Source)){ return (Get-Item $Source).FullName } $dir = Split-Path $kb -Parent $baks = Get-ChildItem -LiteralPath $dir -Filter "BUG_KB.json.txt.bak_*" -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending if($baks){ $cand = $baks | Where-Object { $_.Length -lt ($MaxBakMB*1MB) } | Select-Object -First 1 if(-not $cand){ $cand = $baks | Select-Object -First 1 } return $cand.FullName } return $kb } function Read-EntriesStream([string]$Path){ # Retourne le texte de l'array entries[...] (sans footer), sinon $null $fs=[IO.File]::Open($Path,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::ReadWrite) $sr=New-Object IO.StreamReader($fs,(New-Object Text.UTF8Encoding($false)),$true,65536) try{ $found=$false; $buf = New-Object System.Text.StringBuilder $inStr=$false; $esc=$false; $depth=0 while(-not $sr.EndOfStream){ $line = $sr.ReadLine() if(-not $found){ $m = [regex]::Match($line,'\"entries\"\s*:\s*\[') if($m.Success){ $found=$true $after = $line.Substring($m.Index + $m.Length) [void]$buf.Append('['); $depth=1 $chunk = $after } else { continue } } else { $chunk = $line } for($i=0;$i -lt $chunk.Length;$i++){ $ch = $chunk[$i] if($inStr){ [void]$buf.Append($ch) if($esc){ $esc=$false; continue } if($ch -eq '\'){ $esc=$true; continue } if($ch -eq '"'){ $inStr=$false;continue } continue } else { if($ch -eq '"'){ $inStr=$true; [void]$buf.Append($ch); continue } if($ch -eq '['){ $depth++; [void]$buf.Append($ch); continue } if($ch -eq ']'){ $depth--; [void]$buf.Append($ch) if($depth -eq 0){ return $buf.ToString() } continue } [void]$buf.Append($ch) } } [void]$buf.Append("`n") } return $null } finally { $sr.Close(); $fs.Close() } } function Clean-Recode1252ToUtf8([string]$s){ if(-not $s){ return $s } # Detection ASCII-only: motifs Unicode via echappements \u (pas de caracteres exotiques dans ce script) if($s -notmatch '\u00C3|\u00C2|\u00E2'){ return $s } try{ $enc1252 = [Text.Encoding]::GetEncoding(1252) $bytes = $enc1252.GetBytes($s) return [Text.Encoding]::UTF8.GetString($bytes) } catch { return $s } } function Clean-Text([string]$s,[int]$maxLen=4000){ if(-not $s){ return $s } $s = Clean-Recode1252ToUtf8 $s $s = $s.Replace([char]0xA0,' ') # nbsp -> espace $s = [regex]::Replace($s,'[\x00-\x08\x0B\x0C\x0E-\x1F]',' ') # compresse repets grotesques: meme char (ou 2e groupe) repete >=1024 $s = [regex]::Replace($s,'((.)\2{1023,})','...') if($s.Length -gt $maxLen){ $s = $s.Substring(0,$maxLen) + '...' } return $s } function Enumerate-EntryObjects([string]$arrText){ # Renvoie un tableau de strings, chaque string = un objet JSON { ... } de entries[] $out = New-Object System.Collections.ArrayList $inStr=$false; $esc=$false; $depth=0; $buf=$null for($i=0;$i -lt $arrText.Length;$i++){ $ch = $arrText[$i] if(-not $buf){ if($ch -eq '{'){ $buf = New-Object System.Text.StringBuilder; [void]$buf.Append('{'); $depth=1 } continue } if($inStr){ [void]$buf.Append($ch) if($esc){ $esc=$false; continue } if($ch -eq '\'){ $esc=$true; continue } if($ch -eq '"'){ $inStr=$false; continue } continue } else { if($ch -eq '"'){ $inStr=$true; [void]$buf.Append($ch); continue } if($ch -eq '{'){ $depth++; [void]$buf.Append($ch); continue } if($ch -eq '}'){ $depth--; [void]$buf.Append($ch) if($depth -eq 0){ [void]$out.Add($buf.ToString()); $buf=$null } continue } [void]$buf.Append($ch) } } return ,$out.ToArray() } function Keep-Whitelisted([object]$o){ $id = Clean-Text ([string]$o.id) $tit = Clean-Text ([string]$o.title) $blk = [bool]$o.blocking $wkr = Clean-Text ([string]$o.workaround) $note = Clean-Text ([string]$o.note) $fix = Clean-Text ([string]$o.fix) $tags = @() if($o.tags){ foreach($t in $o.tags){ $tt=Clean-Text ([string]$t) 200; if($tt){ $tags += $tt } } $tags = $tags | Select-Object -Unique } $seen = @() if($o.seen_in_threads){ foreach($s in $o.seen_in_threads){ $ss=Clean-Text ([string]$s) 50; if($ss){ $seen += $ss } } $seen = $seen | Select-Object -Unique } $last = Clean-Text ([string]$o.last_seen) 64 return [pscustomobject]@{ id=$id; title=$tit; blocking=$blk; workaround=$wkr; note=$note; fix=$fix; tags=$tags; seen_in_threads=$seen; last_seen=$last } } 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_SALVAGE_v1.0c`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 -MaxBakMB $MaxBakMB $entriesText = Read-EntriesStream -Path $src if(-not $entriesText){ Write-Host "[SALVAGE] echec: entries[] introuvable dans $src"; exit 1 } $objsText = Enumerate-EntryObjects $entriesText $total = ($objsText | Measure-Object).Count $okCnt=0; $badCnt=0; $acc=@() foreach($ot in $objsText){ $txt = $ot if($txt -match '\u00C3|\u00C2|\u00E2'){ $txt = Clean-Recode1252ToUtf8 $txt } $parsed=$null try { $parsed = $txt | ConvertFrom-Json } catch { $txt2 = [regex]::Replace($txt,'[\x00-\x08\x0B\x0C\x0E-\x1F]',' ') try { $parsed = $txt2 | ConvertFrom-Json } catch { $parsed=$null } } if($parsed){ $acc += (Keep-Whitelisted $parsed); $okCnt++ } else { $badCnt++ } } $now = Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK' $body = @{ entries = $acc; updated = $now } | ConvertTo-Json -Depth 8 if(-not $Execute){ $reb = [Text.Encoding]::UTF8.GetByteCount($body) Write-Host "[PREVIEW] source=$src" ("found objects:".PadRight(20) + $total) | Write-Host ("parsed ok:".PadRight(20) + $okCnt) | Write-Host ("parse failed:".PadRight(20) + $badCnt) | Write-Host ("rebuilt size:".PadRight(20) + ("{0:N1} MB" -f ($reb/1MB))) | Write-Host Write-Host "Utilise -Execute pour ecrire BUG_KB.json.txt (SAFE-WRITE)." exit 0 } Write-SafeTxt -Dest $kb -Body $body -Tag "KB_SALVAGE_v1.0c" Write-Host "[SALVAGE] KB reordonee -> $kb ; entries=" + (($acc|Measure-Object).Count)