??# kb_guard_validator_v1.0.ps1 - PS 5.1-safe, TXT-ONLY # Valide que tous les bugs "blocking=true" de la KB sont couverts par [KB_GUARD_ASSERTS] (mode A), # et signale les non-couverts. Export optionnel d'un rapport TXT. param([switch]$Export) function Read-Text([string]$p){ if(Test-Path $p){ [IO.File]::ReadAllText($p,[Text.UTF8Encoding]::new($true)) } else { '' } } function Write-NoBOM([string]$p,[string]$t){ $enc=[Text.UTF8Encoding]::new($false); [IO.File]::WriteAllText($p,$t,$enc) } function Ensure-Dir([string]$p){ if($p -and !(Test-Path $p)){ New-Item -ItemType Directory -Force -Path $p|Out-Null } } function FileSHA([string]$p){ if(Test-Path $p){ (Get-FileHash -Algorithm SHA256 -LiteralPath $p).Hash } else { '' } } # R?solution _registry ? partir de l'emplacement du script $me = $MyInvocation.MyCommand.Definition $scriptDir = Split-Path -Parent $me $registry = Split-Path -Parent $scriptDir $boot = Join-Path $registry 'bootpack\bootpack.txt' if(!(Test-Path $boot)){ Write-Host "[BLOCKING] bootpack.txt introuvable: $boot"; exit 2 } $raw = Read-Text $boot # === Pointer KB $kbBlk = [regex]::Match($raw,'(?ms)^\[BUG_KB_JSON_POINTER\]\s*(.*?)(?=^\[|\Z)').Groups[1].Value $kbPath = [regex]::Match($kbBlk,'(?mi)^\s*Path\s*:\s*(.+)$').Groups[1].Value.Trim() $kbShaIn= [regex]::Match($kbBlk,'(?mi)^\s*SHA256\s*:\s*([0-9A-Fa-f]+)$').Groups[1].Value.Trim().ToLower() if([string]::IsNullOrWhiteSpace($kbPath)){ Write-Host "[BLOCKING] KB pointer path absent dans bootpack."; exit 2 } $kbShaReal = (FileSHA $kbPath).ToLower() if($kbShaIn -ne $kbShaReal){ Write-Host "[DESYNC] KB pointer SHA mismatch: bootpack=$kbShaIn vs file=$kbShaReal"; exit 2 } # === Lire KB et extraire blockers $rawKB = Read-Text $kbPath $i = $rawKB.LastIndexOf(']}'); if($i -gt 0){ $rawKB = $rawKB.Substring(0,$i+2) } $entries = @() try { $entries = (ConvertFrom-Json $rawKB).entries } catch { $entries=@() } $blockers=@() foreach($e in $entries){ $isBlocking = $false if($e.PSObject.Properties.Name -contains 'blocking'){ $v=$e.blocking if($v -is [string]){ if($v.ToLower() -eq 'true'){ $isBlocking=$true } } elseif($v -is [bool]){ if($v){ $isBlocking=$true } } } if($isBlocking){ $id = '' if($e.PSObject.Properties.Name -contains 'id'){ $id = [string]$e.id } if([string]::IsNullOrWhiteSpace($id)){ $id = '' } $blockers += $id } } # === Lire assertions de couverture $assertBlk = [regex]::Match($raw,'(?ms)^\[KB_GUARD_ASSERTS\]\s*(.*?)(?=^\[|\Z)').Groups[1].Value $asserts=@{} if(-not [string]::IsNullOrWhiteSpace($assertBlk)){ foreach($ln in ($assertBlk -split "`r?`n")){ $t=$ln.Trim() if($t -eq '' -or $t -like ';*'){ continue } $m=[regex]::Match($t,'^\s*(.+?)\s*=\s*covered_by:\s*([A-Z0-9_]+)\s+v([0-9\.]+)\s*$') if($m.Success){ $bid=$m.Groups[1].Value.Trim() $rule=$m.Groups[2].Value.Trim() + ' v' + $m.Groups[3].Value.Trim() $asserts[$bid]=$rule } } } # === Couverture via ASSERTS et (optionnel) via SELFTEST dans les logs r?cents $logs = Join-Path $registry 'logs' $selftestHits=@{} if(Test-Path $logs){ $files = Get-ChildItem -LiteralPath $logs -Filter 'run_*preview*.txt' -File | Sort-Object LastWriteTime -Descending $files = $files | Select-Object -First 30 foreach($f in $files){ $txt = Read-Text $f.FullName foreach($b in $blockers){ if($selftestHits.ContainsKey($b)){ continue } # Heuristiques de selftest PASS if($txt -match ('SELFTEST[_\- ]OK.*'+[regex]::Escape($b)) -or $txt -match ('PRE-SELFTEST.*PASS.*'+[regex]::Escape($b)) ){ $selftestHits[$b] = ('SELFTEST in '+$f.Name) } } } } # === Bilan $total = $blockers.Count $coveredAssert=0; $coveredSelf=0; $uncovered=@() $lines=@() foreach($b in $blockers){ $mode='UNCOVERED'; $detail='' if($asserts.ContainsKey($b)){ $mode='ASSERT'; $detail=$asserts[$b]; $coveredAssert++ } elseif($selftestHits.ContainsKey($b)){ $mode='SELFTEST'; $detail=$selftestHits[$b]; $coveredSelf++ } else{ $uncovered += $b } $lines += ("- {0} => {1}{2}" -f $b,$mode,($(if($detail -ne ''){' ['+$detail+']'}else{''}))) } $ok = ($total -gt 0 -and $uncovered.Count -eq 0) if($total -eq 0){ Write-Host "[INFO] Aucun bug blocking=true dans la KB. Rien ? couvrir." } else { Write-Host "KB GUARDS - R?sum?" Write-Host ("Total blockers : {0}" -f $total) Write-Host ("Couvert ASSERT : {0}" -f $coveredAssert) Write-Host ("Couvert SELFTEST: {0}" -f $coveredSelf) Write-Host ("Non couverts : {0}" -f $uncovered.Count) Write-Host "" foreach($l in $lines){ Write-Host $l } if($uncovered.Count -gt 0){ Write-Host "" Write-Host "UNCOVERED IDs:" foreach($u in $uncovered){ Write-Host (" - "+$u) } } } # Export optionnel if($Export){ $rep = Join-Path $registry 'reports'; Ensure-Dir $rep $ts = Get-Date -Format 'yyyyMMdd_HHmmss' $out = Join-Path $rep ('kb_guard_report_'+$ts+'.txt') $buf = New-Object System.Text.StringBuilder [void]$buf.AppendLine("=== KB GUARD REPORT - "+(Get-Date -Format s)) if($total -eq 0){ [void]$buf.AppendLine("[INFO] Aucun bug blocking=true dans la KB.") } else { [void]$buf.AppendLine(("Total={0}; ASSERT={1}; SELFTEST={2}; UNCOVERED={3}" -f $total,$coveredAssert,$coveredSelf,$uncovered.Count)) foreach($l in $lines){ [void]$buf.AppendLine($l) } } Write-NoBOM $out $buf.ToString() 'Export -> '+$out | Write-Host } # Code retour : 0 si tout couvert (ou aucun blocker), 2 sinon if($uncovered.Count -gt 0){ exit 2 } else { exit 0 }