# kb_salvage_strict_rescue_v1.1.ps1 # Objet : repartir d�un JSON "propre" (salvage), valider sch�ma, assainir (ASCII-safe), # d�doublonner par slug(title), et (en EXECUTE) r��crire la KB canonique en SAFE-WRITE. # R�gles : TXT-ONLY ; PS 5.1 compatible ; pas de ternaires ; pas de Select-String -Recurse ; # �criture via .NET UTF-8 BOM ; staging local + .tmp + backup .bak dat�. [CmdletBinding()] param( [switch]$Preview, [switch]$Execute, [string]$Root = "\\DS-918\chatgpt\ChatGPT-Gouvernance-Projets\_registry", [string]$SalvagePath = "\\DS-918\chatgpt\ChatGPT-Gouvernance-Projets\_registry\bug_kb\BUG_KB.json_salvage-manuel.txt", [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){ $parent = Split-Path -Parent $Target; if($parent){ Ensure-Dir $parent } } function Get-NowIso(){ (Get-Date).ToString("yyyy-MM-ddTHH:mm:ssK") } function Get-FileSize([string]$Path){ if(Test-Path -LiteralPath $Path){ (Get-Item -LiteralPath $Path).Length } else { 0 } } function Get-FileSHA256([string]$Path){ if(-not (Test-Path -LiteralPath $Path)){ return "" } $sha = [System.Security.Cryptography.SHA256]::Create() $fs = [System.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 $tmpLocal = Join-Path $StageRoot ("write_" + [System.IO.Path]::GetRandomFileName()) $utf8bom = New-Object System.Text.UTF8Encoding($true) [System.IO.File]::WriteAllText($tmpLocal, $Content, $utf8bom) $bak = $null if(Test-Path -LiteralPath $Target){ $bak = $Target + "." + (Get-Date -Format "yyyyMMdd_HHmmss") + ".bak" Copy-Item -LiteralPath $Target -Destination $bak -Force } $tmpRemote = $Target + ".tmp" Copy-Item -LiteralPath $tmpLocal -Destination $tmpRemote -Force Move-Item -LiteralPath $tmpRemote -Destination $Target -Force Remove-Item -LiteralPath $tmpLocal -Force return $bak } function Read-JsonSansFooter([string]$Path){ $sr = New-Object System.IO.StreamReader($Path) try{ $buf = New-Object System.Collections.Generic.List[string] while(-not $sr.EndOfStream){ $line = $sr.ReadLine() if($line -match '^\s*---\s*DOC-VERSION-FOOTER'){ break } $buf.Add($line) | Out-Null } ($buf -join "`n") } finally { $sr.Dispose() } } function Normalize-Ascii([string]$s){ if($null -eq $s){ return "" } $map = @{ "`u2018"="'"; "`u2019"="'"; "`u201A"="'"; "`u201B"="'"; "`u201C"='"'; "`u201D"='"'; "`u201E"='"'; "`u2013"="-"; "`u2014"="-"; "`u00A0"=" " } foreach($k in $map.Keys){ $s = $s -replace $k, $map[$k] } $s = $s -replace "'","'" -replace """,'"' -replace "�\u009d",'"' -replace "Â","" -replace "�","" $s = $s -replace "?","?" $s = [regex]::Replace($s, "[^\P{C}\r\n\t]", " ") $s = [regex]::Replace($s, "\s{2,}", " ") $s.Trim() } function Slug([string]$t){ if([string]::IsNullOrWhiteSpace($t)){ return "" } $x = $t.ToLowerInvariant() [regex]::Replace($x, "[^a-z0-9]+", "-").Trim("-") } $BugKbDir = Join-Path $Root "bug_kb" $KbCanon = Join-Path $BugKbDir "BUG_KB.json.txt" Ensure-Dir $BugKbDir if(-not (Test-Path -LiteralPath $SalvagePath)){ Write-Host "[ERR] Introuvable: $SalvagePath"; exit 2 } $raw = Read-JsonSansFooter $SalvagePath if([string]::IsNullOrWhiteSpace($raw)){ Write-Host "[ERR] Fichier de salvage vide: $SalvagePath"; exit 3 } $mojipat = '�|�|'|"|�|?|@\{' $mojiHit = ([regex]::Matches($raw, $mojipat)).Count try { $obj = $raw | ConvertFrom-Json -ErrorAction Stop } catch { Write-Host "[ERR] JSON invalide (salvage): $($_.Exception.Message)"; exit 4 } if(-not $obj.entries){ $obj | Add-Member -Name entries -MemberType NoteProperty -Value @() } $seen = @{} $clean = New-Object System.Collections.ArrayList $maxLen = 4000 $srcCount = 0 foreach($e in $obj.entries){ $srcCount++ $title = Normalize-Ascii ([string]$e.title) $slug = Slug $title if([string]::IsNullOrWhiteSpace($slug)){ continue } if($seen.ContainsKey($slug)){ continue } $seen[$slug] = $true $id = Normalize-Ascii ([string]$e.id) $work = Normalize-Ascii ([string]$e.workaround); if($work.Length -gt $maxLen){ $work = $work.Substring(0,$maxLen) } $note = Normalize-Ascii ([string]$e.note); if($note.Length -gt $maxLen){ $note = $note.Substring(0,$maxLen) } $fix = Normalize-Ascii ([string]$e.fix); if($fix.Length -gt $maxLen){ $fix = $fix.Substring(0,$maxLen) } $tags = @(); if($e.tags){ foreach($t in $e.tags){ $tags += (Normalize-Ascii ([string]$t)) } } $seenIn = @() if($e.seen_in_threads){ if($e.seen_in_threads -is [string]){ $seenIn = @($e.seen_in_threads) } else { $seenIn = @($e.seen_in_threads | ForEach-Object { [string]$_ }) } } $null = $clean.Add([ordered]@{ id = $id; title = $title; blocking = [bool]$e.blocking; workaround = $work; note = $note; fix = $fix; tags = @($tags); seen_in_threads = @($seenIn); last_seen = [string]$e.last_seen }) } $updated = Get-NowIso $outObj = [ordered]@{ entries = @($clean); updated = $updated } $json = ($outObj | ConvertTo-Json -Depth 6 -Compress) $footer = "`r`n`r`n--- DOC-VERSION-FOOTER ---`r`nGenerated: $updated`r`nPolicy: TXT-ONLY v1.0; SAFE-WRITE v1.1; GOV_SCRIPT_GATE v1.3`r`nSource: KB_SALVAGE_v1.1`r`n" if($Preview -or (-not $Execute)){ Write-Host "== PREVIEW :: SALVAGE STRICT RESCUE ==" Write-Host ("Input : {0}`n size={1:n0} bytes sha256={2}" -f $SalvagePath, (Get-FileSize $SalvagePath), (Get-FileSHA256 $SalvagePath)) Write-Host ("Entries (src)={0} (clean)={1} mojibake_hits={2}" -f $srcCount, $clean.Count, $mojiHit) Write-Host ("Will write -> {0} out-json-bytes�{1:n0}" -f $KbCanon, ([Text.Encoding]::UTF8.GetByteCount($json))) Write-Host "No write performed (Preview)." exit 0 } $bak = Write-SafeText -Target $KbCanon -Content ($json + $footer) -StageRoot $StageRoot $sha = Get-FileSHA256 $KbCanon $bakMsg = $bak; if(-not $bakMsg){ $bakMsg = "" } Write-Host ("[OK] KB canonique reconstruite`n file={0}`n entries={1}`n sha256={2}" -f $KbCanon, $clean.Count, $sha) Write-Host ("Backup: {0}" -f $bakMsg) # --- DOC-VERSION-FOOTER --- # Generated: # Policy : TXT-ONLY v1.0; SAFE-WRITE v1.1; GOV_SCRIPT_GATE v1.3 # Source : kb_salvage_strict_rescue_v1.1.ps1