????param( [string]$Source="", # .bak ou KB par d?faut [switch]$Execute, [int]$MaxFieldLen = 8000 ) $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 "" } $s = [regex]::Replace($s,'[\x00-\x08\x0B\x0C\x0E-\x1F]',' ') return $s } function Truncate-LongFields([string]$json,[int]$max){ if($max -le 0){ return $json } $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){ 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 Collect-Objects([string]$Path,[switch]$WithinEntries){ $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 $inStr=$false; $esc=$false; $brace=0 $buf = New-Object System.Text.StringBuilder $started=$false $stopAtFooter=$true while(-not $sr.EndOfStream){ $line = $sr.ReadLine() if($stopAtFooter -and $line -match '--- DOC-VERSION-FOOTER ---'){ break } if($WithinEntries){ if(-not $started){ # Tol?rant aux guillemets typographiques : on matche entries quel que soit le type de quotes if($line -match 'entries\s*:\s*\['){ $started=$true } continue } if($started -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){ $obj = $buf.ToString() try{ $o=$obj | ConvertFrom-Json # garde si "id" et "title" existent (s?curit?) $hasId = $false; $hasTitle=$false if($o -ne $null){ $hasId = ($o.PSObject.Properties.Name -contains 'id') $hasTitle = ($o.PSObject.Properties.Name -contains 'title') } if($hasId -and $hasTitle){ [void]$objs.Add($obj) } } catch { } $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 Rescue-Entries([string]$Path){ # 1) Essaye "dans entries[...]" $objs = Collect-Objects -Path $Path -WithinEntries if(($objs | Measure-Object).Count -gt 0){ return $objs } # 2) Fallback: scan global (avant footer) return (Collect-Objects -Path $Path) } 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.1`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 $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){ $label = if($parseOK){"OK"}else{"FAIL"} "{0,-20} {1}" -f "source:", $src | Write-Host "{0,-20} {1}" -f "valid objects:", $okCount | Write-Host "{0,-20} {1}" -f "json parse:", $label | 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.1" Write-Host "[OK] KB reconstruite (entries=$okCount) -> $kb"