# kb_guard_validator_v1.2.ps1 - PS 5.1-safe, TXT-ONLY # Valide la couverture des "blocking=true" par [KB_GUARD_ASSERTS] et, sauf -AssertsOnly, par SELFTEST (logs de preview). # Options: # -Export -> export TXT du rapport # -Profile -> limite la recherche SELFTEST aux logs du profil (HUB, SEEDBOX, .) # -SuggestAsserts -> g?n?re un bloc "[KB_GUARD_ASSERTS]" pour chaque blocker non couvert # -AssertsOnly -> n'utilise PAS les selftests (strict) param( [switch]$Export, [string]$Profile, [switch]$SuggestAsserts, [switch]$AssertsOnly ) 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 { '' } } $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 $kbBlk = [regex]::Match($raw,'(?ms)^\[BUG_KB_JSON_POINTER\]\s*(.*?)(?=^\[|\Z)').Groups[1].Value $kbPath = [regex]::$(if(Match($kbBlk,'(){ mi)^\s*Path\s* } else { \s*(.+)$').Groups[1].Value.Trim() }) $kbShaIn= [regex]::$(if(Match($kbBlk,'(){ mi)^\s*SHA256\s* } else { \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 } $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 } } $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]::$(if(Match($t,'^\s*(.+){ )\s*=\s*covered_by } else { \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 } } } $selftestHits=@{} if(-not $AssertsOnly){ $logs = Join-Path $registry 'logs' if(Test-Path $logs){ $pattern = 'run_*preview*.txt' if(-not [string]::IsNullOrWhiteSpace($Profile)){ $pattern=('run_'+$Profile.ToUpper()+'_preview_*.txt') } $files = Get-ChildItem -LiteralPath $logs -Filter $pattern -File | Sort-Object LastWriteTime -Descending | Select -First 50 foreach($f in $files){ $txt = Read-Text $f.FullName foreach($b in $blockers){ if($selftestHits.ContainsKey($b)){ continue } if($txt -match ('SELFTEST[_\- ]OK.*'+[regex]::Escape($b)) -or $txt -match ('PRE-SELFTEST.*PASS.*'+[regex]::Escape($b)) ){ $selftestHits[$b] = ('SELFTEST in '+$f.Name) } } } } } $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(-not $AssertsOnly -and $selftestHits.ContainsKey($b)){ $mode='SELFTEST'; $detail=$selftestHits[$b]; $coveredSelf++ } else{ $uncovered += $b } $lines += ("- {0} => {1}{2}" -f $b,$mode,($(if($detail -ne ''){' ['+$detail+']'}else{''}))) } if($SuggestAsserts -and $uncovered.Count -gt 0){ Write-Host "=== SUGGESTED [KB_GUARD_ASSERTS] BLOCK ===" foreach($u in $uncovered){ Write-Host ("{0} = covered_by: RULE_TBD vX.Y" -f $u) } Write-Host "=== /SUGGESTED ===" } 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) } } } 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) } if($uncovered.Count -gt 0){ [void]$buf.AppendLine(""); [void]$buf.AppendLine("=== SUGGESTED [KB_GUARD_ASSERTS] ===") foreach($u in $uncovered){ [void]$buf.AppendLine(("{0} = covered_by: RULE_TBD vX.Y" -f $u)) } } } Write-NoBOM $out $buf.ToString() 'Export -> '+$out | Write-Host } if($uncovered.Count -gt 0){ exit 2 } else { exit 0 }