???param([switch]$Execute,[ValidateSet("Move","Delete","None")][string]$After="Move") $ErrorActionPreference='Stop' $root="\\DS-918\chatgpt\ChatGPT-Gouvernance-Projets\_registry" $inbox = Join-Path $root "updates\inbox\kb" $proc = Join-Path $root "updates\processed\kb" $logs = Join-Path $root "logs" $kb = Join-Path $root "bug_kb\BUG_KB.json.txt" $boot = Join-Path $root "bootpack\bootpack.txt" $thr = Join-Path $root "threads_archive" $stage = "C:\Temp_Gouvernance" foreach($d in @($inbox,$proc,$logs,$thr,$stage)){ if(!(Test-Path $d)){ New-Item -ItemType Directory -Force -Path $d|Out-Null } } $Utf8NoBom = New-Object Text.UTF8Encoding($false) function Read-KbObj([string]$Path){ if(!(Test-Path $Path)){ return [pscustomobject]@{ entries=@(); updated=(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK') } } $t = Get-Content -LiteralPath $Path -Raw $m='--- DOC-VERSION-FOOTER ---' $i=$t.IndexOf($m); if($i -ge 0){ $t=$t.Substring(0,$i).Trim() } try { $o = $t | ConvertFrom-Json } catch { return [pscustomobject]@{ entries=@(); updated=(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK') } } if($o -and $o.entries){ if($o.entries -is [array]){ return $o } else { $o.entries=@($o.entries); return $o } } return [pscustomobject]@{ entries=@(); updated=(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK') } } function Write-SafeTxt([string]$Dest,[string]$Body,[string]$SourceTag){ $iso=(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK') $policy='TXT-ONLY v1.0; SAFE-WRITE v1.1; GOV_SCRIPT_GATE v1.3' if(!(Test-Path (Split-Path $Dest -Parent))){ New-Item -ItemType Directory -Force -Path (Split-Path $Dest -Parent)|Out-Null } if(!(Test-Path $stage)){ New-Item -ItemType Directory -Force -Path $stage|Out-Null } $tmpLocal = Join-Path $stage ([IO.Path]::GetFileName($Dest)) [IO.File]::WriteAllText($tmpLocal,$Body,$Utf8NoBom) $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_BULK_v1.2`r`n",$Utf8NoBom) $tmp="$Dest.tmp"; Copy-Item $tmpLocal $tmp -Force if(Test-Path $Dest){ Copy-Item $Dest ($Dest+".bak_"+(Get-Date -Format 'yyyyMMdd_HHmmss')) -Force } Move-Item $tmp $Dest -Force } function Update-Bootpack-BugJsonStream([string]$BootPath,[string]$JsonString){ if(!(Test-Path $BootPath)){ return } $tmp="$BootPath.tmp" $sr = New-Object IO.StreamReader($BootPath,(New-Object Text.UTF8Encoding($false)),$true,4096) $sw = New-Object IO.StreamWriter($tmp,(New-Object Text.UTF8Encoding($false))) try{ $inKb=$false; $wrote=$false while(-not $sr.EndOfStream){ $line=$sr.ReadLine() if(-not $inKb){ if($line -match '^\[BUG_KB_JSON\]\s*$'){ $inKb=$true; $sw.WriteLine($line); $sw.WriteLine($JsonString); $wrote=$true while(-not $sr.EndOfStream){ $posLine=$sr.ReadLine() if($posLine -match '^\['){ $sw.WriteLine($posLine); $inKb=$false; break } } } else { $sw.WriteLine($line) } } else { if($line -match '^\['){ $sw.WriteLine($line); $inKb=$false } } } if(-not $wrote){ $sw.WriteLine('[BUG_KB_JSON]') $sw.WriteLine($JsonString) } } finally{ $sr.Close(); $sw.Close() } if(Test-Path $BootPath){ Copy-Item $BootPath ($BootPath+'.bak_'+(Get-Date -Format 'yyyyMMdd_HHmmss')) -Force } Move-Item $tmp $BootPath -Force } function Slugify([string]$s){ return (($s.ToLower() -replace '[^a-z0-9]+','-').Trim('-')) } function Parse-AutoFile([string]$Path){ $map=@{} $lines = Get-Content -LiteralPath $Path -ErrorAction Stop foreach($ln in $lines){ if($ln -match '^\s*([^:]+)\s*:\s*(.*)$'){ $map[$matches[1].Trim()]=$matches[2] } } $title = if([string]::IsNullOrWhiteSpace($map['Title'])){ "Untitled KB entry" } else { $map['Title'] } $blocking = ($map['Blocking'] -match 'true|1|yes') $rootcause = $map['RootCause']; $workaround=$map['Workaround']; $fix=$map['Fix'] $tags = @(); if($map['Tags']){ $tmp=$map['Tags'].Split(','); foreach($t in $tmp){ $t=$t.Trim(); if($t){ $tags += $t } } } $seen = @(); if($map['SeenInThreads']){ $tmp=$map['SeenInThreads'].Split(','); foreach($s in $tmp){ $s=$s.Trim(); if($s){ $seen += $s } } } return [pscustomobject]@{ title=$title; blocking=$blocking; note=$rootcause; workaround=$workaround; fix=$fix; tags=$tags; seen_in_threads=$seen } } # Collecter tous les AUTO_*.txt une seule fois $files = Get-ChildItem -LiteralPath $inbox -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '^AUTO_.*\.txt$' } if(($files | Measure-Object).Count -eq 0){ Write-Host "[BULK] inbox vide." exit 0 } $kbObj = Read-KbObj $kb $before = ($kbObj | ConvertTo-Json -Depth 20) $now = (Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK') # Fusion idempotente par slug de Title $map=@{}; foreach($e in $kbObj.entries){ if($e.title){ $map[(Slugify $e.title)]=$e } } $ingested=0 foreach($f in $files){ $x = Parse-AutoFile $f.FullName $slug = Slugify $x.title if($map.ContainsKey($slug)){ $e=$map[$slug] if($x.blocking){ $e.blocking=$true } if($x.note){ $e.note=$x.note } if($x.workaround){ $e.workaround=$x.workaround } if($x.fix){ $e.fix=$x.fix } if($x.tags){ $e.tags = @([string[]]($e.tags + $x.tags | Select-Object -Unique)) } if($x.seen_in_threads){ $e.seen_in_threads = @([string[]]($e.seen_in_threads + $x.seen_in_threads | Select-Object -Unique)) } $e.last_seen=$now } else { $new=[pscustomobject]@{ id="KB-AUTO-"+$slug+"-"+(Get-Date -Format 'yyyyMMdd_HHmmss') title=$x.title; blocking=$x.blocking; note=$x.note; workaround=$x.workaround; fix=$x.fix tags=$x.tags; seen_in_threads=$x.seen_in_threads; last_seen=$now } $kbObj.entries += ,$new $map[$slug]=$new } $ingested++ } $kbObj.updated=$now $kbJson = ($kbObj | ConvertTo-Json -Depth 20) if($before -eq $kbJson){ Write-Host ("[BULK] no-op (unchanged) ; files={0}" -f $ingested) if($After -eq 'Move'){ $dst = Join-Path $proc (Get-Date -Format 'yyyyMMdd'); if(!(Test-Path $dst)){ New-Item -ItemType Directory -Force -Path $dst|Out-Null }; foreach($f in $files){ Move-Item $f.FullName (Join-Path $dst $f.Name) -Force } } elseif($After -eq 'Delete'){ foreach($f in $files){ Remove-Item $f.FullName -Force } } exit 0 } if(-not $Execute){ Write-Host ("[PREVIEW] KB changerait ; {0} fichiers. Utilise -Execute pour appliquer." -f $ingested) exit 0 } Write-SafeTxt -Dest $kb -Body $kbJson -SourceTag "KB_BULK_v1.2" # Un seul write streaming du bootpack $srch='--- DOC-VERSION-FOOTER ---'; $idx = $kbJson.IndexOf($srch); if($idx -ge 0){ $json = $kbJson.Substring(0,$idx).Trim() } else { $json = $kbJson } Update-Bootpack-BugJsonStream -BootPath $boot -JsonString $json # Archiver + log + post-traitement $day=(Get-Date -Format 'yyyy-MM-dd'); $t=(Get-Date -Format 'HHmmss') $archDir = Join-Path (Join-Path $thr $day) $t; if(!(Test-Path $archDir)){ New-Item -ItemType Directory -Force -Path $archDir | Out-Null } [IO.File]::WriteAllText((Join-Path $archDir 'kb_bulk_snapshot.txt'),("Files="+$ingested), (New-Object Text.UTF8Encoding($false))) $log = Join-Path $logs 'registry_activity.log' $old=@(); if(Test-Path $log){ $old += Get-Content -LiteralPath $log -ErrorAction SilentlyContinue } $old += ("[{0}] kb_bulk_v1.2: files={1}" -f (Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK'),$ingested) [IO.File]::WriteAllText($log, ($old -join "`r`n"), (New-Object Text.UTF8Encoding($false))) if($After -eq 'Move'){ $dst = Join-Path $proc (Get-Date -Format 'yyyyMMdd'); if(!(Test-Path $dst)){ New-Item -ItemType Directory -Force -Path $dst|Out-Null }; foreach($f in $files){ Move-Item $f.FullName (Join-Path $dst $f.Name) -Force } } elseif($After -eq 'Delete'){ foreach($f in $files){ Remove-Item $f.FullName -Force } } Write-Host "[BULK] KB+bootpack mis ? jour."