# gov_pipeline_doctor_v1.0.ps1 # Purpose: pre-flight checks before interactive write. # - Validate encodings (no BOM) and CRLF endings for *.ps1 under scripts\ # - Flag fragile patterns (here-strings, inline if inside -f, Split-Path -LiteralPath -Parent, triple dots ...) # - Validate bootpack pointer block vs bootpack_paste.txt (SHA, entries, size, path) # - Optional acceptance run and optional pointer fix. param( [string]$Root = '\\DS-918\\chatgpt\\ChatGPT-Gouvernance-Projets\\_registry', [switch]$RunAcceptance, [switch]$FixPointer, [switch]$Utf8Console ) if($Utf8Console){ try{ [Console]::OutputEncoding = [Text.Encoding]::UTF8 }catch{} } function Exists([string]$p){ return (-not [string]::IsNullOrWhiteSpace($p)) -and (Test-Path -LiteralPath $p) } function Read-Bytes([string]$p){ try{ return [IO.File]::ReadAllBytes($p) }catch{ return $null } } function Read-Text([string]$p){ if(-not (Exists $p)){ return '' } try{ return Get-Content -LiteralPath $p -Raw -Encoding UTF8 }catch{ return (Get-Content -LiteralPath $p -Raw) } } function Has-BOM([byte[]]$b){ if($b -eq $null){ return $false } if($b.Length -ge 3){ return ($b[0] -eq 0xEF -and $b[1] -eq 0xBB -and $b[2] -eq 0xBF) } return $false } function LineEnding-Counts([string]$t){ $crlf = ([regex]::Matches($t, "`r`n")).Count $tmp = [regex]::Replace($t,"`r`n","") $lf = ([regex]::Matches($tmp, "`n")).Count $cr = ([regex]::Matches($tmp, "`r")).Count return @{ CRLF=$crlf; LF=$lf; CR=$cr } } function Count-EntriesFromJson([string]$json){ if([string]::IsNullOrWhiteSpace($json)){ return -1 } if($json.Length -gt 0 -and ([int]$json[0] -eq 0xFEFF)){ $json = $json.Substring(1) } try{ $o = $json | ConvertFrom-Json; if($o -and $o.PSObject.Properties.Name -contains 'entries'){ return @($o.entries).Count } }catch{} try{ $count=0; $i=0; while($true){ $j = $json.IndexOf('"id"',$i); if($j -lt 0){ break } $count++; $i=$j+4 } return $count }catch{} return -1 } $scriptsDir = Join-Path $Root 'scripts' $bpFile = Join-Path $Root 'bootpack\bootpack.txt' $pasteFile = Join-Path $Root 'bootpack\bootpack_paste.txt' if(-not (Exists $scriptsDir)){ Write-Host ("[FATAL] scripts dir missing: {0}" -f $scriptsDir); exit 3 } Write-Host "== GOV PIPELINE :: DOCTOR v1.0 ==" Write-Host ("Root : {0}" -f $Root) # 1) Scan scripts encodings and fragile patterns $warn = @() $ps1 = Get-ChildItem -LiteralPath $scriptsDir -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue foreach($f in $ps1){ $b = Read-Bytes $f.FullName $t = Read-Text $f.FullName $hasBom = Has-BOM $b $le = LineEnding-Counts $t if($hasBom){ $warn += ("[BOM] {0}" -f $f.FullName) } if(($le.LF -gt 0) -or ($le.CR -gt 0)){ $warn += ("[LINE-ENDINGS] {0} -> CRLF={1} LF={2} CR={3}" -f $f.FullName,$le.CRLF,$le.LF,$le.CR) } # here-string openers: *@"* et *@'* (deux tests simples, sans regex complexes) $hasHS = $false if($t -like '*@"*'){ $hasHS = $true } if($t -like '*@'*'){ $hasHS = $true } if($hasHS){ $warn += ("[HERE-STRING] {0}" -f $f.FullName) } # inline if proche d'un -f (heuristique simple) if( ($t -like '*-f*') -and ($t -like '*if(*') ){ $warn += ("[INLINE-IF-FMT] {0}" -f $f.FullName) } # Split-Path -LiteralPath -Parent (paramset clash potentiel en PS 5.1) if( ($t -like '*Split-Path*') -and ($t -like '*-LiteralPath*') -and ($t -like '*-Parent*') ){ $warn += ("[SPLIT-PATH-PARAMSET] {0}" -f $f.FullName) } # triple dots (copier/coller tronques) if($t.Contains('...')){ $warn += ("[TRIPLE-DOTS] {0}" -f $f.FullName) } } Write-Host ("Scripts scanned : {0}" -f (@($ps1).Count)) if($warn.Count -gt 0){ Write-Host "-- WARNINGS --"; $warn | ForEach-Object { Write-Host $_ } } else { Write-Host "[OK] encodings and patterns look fine." } # 2) Bootpack pointer sanity (ligne par ligne, tolere espaces) if(-not (Exists $bpFile)){ Write-Host ("[FATAL] missing bootpack.txt: {0}" -f $bpFile); exit 3 } if(-not (Exists $pasteFile)){ Write-Host ("[FATAL] missing bootpack_paste.txt: {0}" -f $pasteFile); exit 3 } $rawBP = Read-Text $bpFile $rawPP = Read-Text $pasteFile try{ $psSha = (Get-FileHash -Algorithm SHA256 -LiteralPath $pasteFile).Hash.ToUpperInvariant() }catch{ $psSha=' } try{ $psSize = (Get-Item -LiteralPath $pasteFile).Length }catch{ $psSize=-1 } $psEnt = Count-EntriesFromJson $rawPP $P_path = ''; $P_sha=''; $P_ent=-1; $P_sz=-1; $lines = $rawBP -split "(`r?`n)" $inBlk = $false foreach($ln in $lines){ $trim = $ln.Trim() if($trim -ceq '[BUG_KB_JSON_POINTER]'){ $inBlk = $true; continue } if($inBlk){ if($trim -match '^\['){ $inBlk = $false; break } if($trim -match '^Path\s*:\s*(.+)$'){ $P_path = $matches[1].Trim() } elseif($trim -match '^SHA256\s*:\s*([A-Fa-f0-9]+)$'){ $P_sha = $matches[1].ToUpperInvariant() } elseif($trim -match '^Entries\s*:\s*(\d+)$'){ $P_ent = [int]$matches[1] } elseif($trim -match '^SizeBytes\s*:\s*(\d+)$'){ $P_sz = [int]$matches[1] } } } $eqPath = ($P_path -ceq $pasteFile) $eqSha = ($P_sha -eq $psSha) $eqEnt = ($P_ent -eq $psEnt) $eqSize = ($P_sz -eq [int]$psSize) Write-Host ("PTR : path==paste={0} sha==paste={1} entries==paste={2} size==paste={3}" -f $eqPath,$eqSha,$eqEnt,$eqSize) if(-not ($eqPath -and $eqSha -and $eqEnt -and $eqSize)){ Write-Host "Pointer mismatch detected." if($FixPointer){ $post = Join-Path $scriptsDir 'post_bootpack_pointer_refresh_v1.0.4.ps1' if(Exists $post){ Write-Host "[FIX] running pointer refresh + acceptance..." $null = & powershell -NoProfile -ExecutionPolicy Bypass -File $post -Root $Root -NoPrompt -Verify } else { Write-Host ("[WARN] missing post-hook: {0}" -f $post) } } } else { Write-Host "[OK] pointer block matches paste." } if($RunAcceptance){ $acc = Join-Path $scriptsDir 'kb_acceptance_tests_v1.0.1.ps1' if(Exists $acc){ $null = & powershell -NoProfile -ExecutionPolicy Bypass -File $acc $Root } else { Write-Host ("[WARN] acceptance script not found: {0}" -f $acc) } } # Exit code (WARN n empeche pas le run, on log seulement) if( ($eqPath -and $eqSha -and $eqEnt -and $eqSize) -and ($warn.Count -eq 0) ){ Write-Host "Result: PASS"; exit 0 } else { Write-Host "Result: WARN"; exit 0 }