?# kb_bug_intake_gate_v1.6.ps1 # - Fix: ?vite la collision $Tags/$tags (case-insensitive) -> utilise $tagsOut # - Absorbe les tokens r?siduels (Remainder) dans Tags et d?duplique # - Staging robuste + IO atomiques + sanitation ASCII # PS 5.1-safe, TXT-only. [CmdletBinding(PositionalBinding=$false)] param( [switch]$Preview, [switch]$Execute, [string]$Root = "\\DS-918\chatgpt\ChatGPT-Gouvernance-Projets\_registry", [string]$StageRoot = "C:\Temp_Gouvernance\stage_io", [Parameter(Mandatory=$true)] [string]$Title, [switch]$Blocking, [string]$Workaround = "", [string]$Note = "", [string]$Fix = "", # Recommand?: -Tags "a,b,c" (CSV) [string[]]$Tags = @(), # Optionnel: -SeenIn "url1,url2" [string[]]$SeenIn = @(), [switch]$NoRunPipeline, # Avale les tokens non nomm?s (ex: ps51 parser) -> ajout? ? Tags [Parameter(ValueFromRemainingArguments=$true)] [string[]]$Remainder = @() ) $ErrorActionPreference = 'Stop' 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){ $p = Split-Path -Parent $Target; if($p){ Ensure-Dir $p } } function NowIso(){ (Get-Date).ToString("yyyy-MM-ddTHH:mm:ssK") } function AsciiFold([string]$s){ if($null -eq $s){ return "" } $s = $s -replace "`u2018","'" -replace "`u2019","'" -replace "`u201A","'" -replace "`u201B","'" $s = $s -replace "`u201C",'"' -replace "`u201D",'"' -replace "`u201E",'"' $s = $s -replace "`u2013","-" -replace "`u2014","-" $s = $s -replace "`u00A0"," " $norm = $s.Normalize([Text.NormalizationForm]::FormD) $sb = New-Object Text.StringBuilder foreach($ch in $norm.ToCharArray()){ $cat = [Globalization.CharUnicodeInfo]::GetUnicodeCategory($ch) if($cat -ne [Globalization.UnicodeCategory]::NonSpacingMark){ [void]$sb.Append($ch) } } $s2 = $sb.ToString() $s2 = [regex]::Replace($s2,"[^\x09\x0A\x0D\x20-\x7E]"," ") $s2 = $s2 -replace "@\{","(" $s2 = [regex]::Replace($s2,"\s{2,}"," ") $s2.Trim() } function Trunc([string]$s,[int]$n){ if($null -eq $s){ return "" } if($s.Length -le $n){ return $s } return $s.Substring(0,$n) } function Normalize-List([string[]]$arr){ $out=@() if($null -eq $arr){ return $out } foreach($item in $arr){ if($null -eq $item){ continue } $parts = ([string]$item) -split "[,;]" foreach($p in $parts){ $t = $p.Trim() if(-not [string]::IsNullOrWhiteSpace($t)){ $out += $t } } } $out } function Slugify([string]$s){ $t = AsciiFold $s $t = $t.ToLower() $t = [regex]::Replace($t,"[^a-z0-9]+","-") $t = [regex]::Replace($t,"-+","-").Trim("-") if([string]::IsNullOrEmpty($t)){ $t = "bug" } if($t.Length -gt 60){ $t = $t.Substring(0,60).Trim("-") } $t } function Get-StageBase([string]$requested){ $def = "C:\Temp_Gouvernance\stage_io" if([string]::IsNullOrWhiteSpace($requested)){ return $def } $t = $requested if([IO.Path]::GetExtension($t)){ return $def } if(-not [IO.Path]::IsPathRooted($t)){ $t = Join-Path (Get-Location).Path $t } if([IO.Path]::GetExtension($t)){ return $def } return $t } function Write-SafeText([string]$Target,[string]$Content,[string]$StageDir){ Ensure-Dir $StageDir Ensure-Parent $Target $tmpName = "write_" + ([IO.Path]::GetRandomFileName()).Replace('.','_') $tmp = Join-Path $StageDir $tmpName $utf8 = New-Object Text.UTF8Encoding($true) [IO.File]::WriteAllText($tmp,$Content,$utf8) $tmpR = "$Target.tmp" Copy-Item -LiteralPath $tmp -Destination $tmpR -Force Move-Item -LiteralPath $tmpR -Destination $Target -Force Remove-Item -LiteralPath $tmp -Force } # Absorption tokens r?siduels -> Tags if($Remainder.Count -gt 0){ $rem2=@() foreach($tok in $Remainder){ if($null -ne $tok){ $t=[string]$tok; if(-not $t.StartsWith('-')){ $rem2 += $t } } } if($rem2.Count -gt 0){ Write-Host ("[WARN] Tokens r?siduels absorb?s en Tags: {0}" -f ($rem2 -join ", ")) -ForegroundColor Yellow $Tags = @($Tags) + $rem2 } } # Build entry $Inbox = Join-Path $Root "updates\inbox\kb" Ensure-Dir $Inbox $title = Trunc (AsciiFold $Title) 200 $work = Trunc (AsciiFold $Workaround) 4000 $note = Trunc (AsciiFold $Note) 8000 $fix = Trunc (AsciiFold $Fix) 4000 # Normalize + dedupe tags $tagsOut = @() $tagSeen = @{} foreach($t in (Normalize-List $Tags)){ $u = Trunc (AsciiFold $t) 100 if([string]::IsNullOrWhiteSpace($u)){ continue } if(-not $tagSeen.ContainsKey($u)){ $tagSeen[$u] = $true $tagsOut += $u } } # Normalize SeenIn (dedupe simple) $seenOut = @() $seenSeen = @{} foreach($s in (Normalize-List $SeenIn)){ $u = Trunc (AsciiFold $s) 200 if([string]::IsNullOrWhiteSpace($u)){ continue } if(-not $seenSeen.ContainsKey($u)){ $seenSeen[$u] = $true $seenOut += $u } } $slug = Slugify $title $tsId = (Get-Date -f yyyyMMddHHmmss) $id = ("bug-" + $slug + "-" + $tsId) $entry = [ordered]@{ id=$id title=$title blocking=[bool]$Blocking workaround=$work note=$note fix=$fix tags=@($tagsOut) seen_in_threads=@($seenOut) last_seen=NowIso } $json = ($entry | ConvertTo-Json -Depth 5 -Compress) $autoName = ("AUTO_{0}_{1}.txt" -f (Get-Date -f yyyyMMdd_HHmmss), $slug) $target = Join-Path $Inbox $autoName # PREVIEW if($Preview -or (-not $Execute)){ Write-Host "== PREVIEW :: BUG INTAKE GATE v1.6 ==" Write-Host ("StageBase -> {0}" -f (Get-StageBase $StageRoot)) Write-Host ("Will write -> {0}" -f $target) Write-Host "Content(JSON entry):" Write-Host $json Write-Host "No write performed (Preview)." exit 0 } # EXECUTE try{ $stageBase = Get-StageBase $StageRoot Write-SafeText -Target $target -Content $json -StageDir $stageBase Write-Host ("[OK] AUTO written -> {0}" -f $target) } catch { Write-Host ("[ERR] Failed to write AUTO: {0}" -f $_.Exception.Message) -ForegroundColor Red exit 1 } if(-not $NoRunPipeline){ $wrapNas = Join-Path (Join-Path $Root "scripts\wrappers") "gov_execute_nas.cmd" if(Test-Path -LiteralPath $wrapNas){ Write-Host ("[RUN] Pipeline via NAS wrapper: {0}" -f $wrapNas) & $wrapNas } else { $wrapLoc = "C:\Temp_Gouvernance\gov_execute.cmd" if(Test-Path -LiteralPath $wrapLoc){ Write-Host ("[RUN] Pipeline via local wrapper: {0}" -f $wrapLoc) & $wrapLoc } else { Write-Host "[WARN] Aucun wrapper trouv?. AUTO d?pos?, mais pipeline non lanc?e." } } }