tools/Audit-PublicJargon.ps1
|
<#
.SYNOPSIS Scan the GitEasy public surface for git-jargon leakage. .DESCRIPTION Audit-PublicJargon.ps1 walks every Public\*.ps1 file, extracts user-facing strings (Write-Host / Write-Warning / Write-Error / Write-Information / Write-Output / Write-Verbose call arguments and throw expressions) plus parameter names, and flags occurrences of git terminology categorized as HARD (almost certainly should be translated) or SOFT (sticky words like "branch" or "push" that often have no good plain-English replacement and should be reviewed in context). The script does not modify any files; it produces a report. .PARAMETER ProjectRoot Absolute path to the GitEasy source repository. Defaults to C:\Sysadmin\Scripts\GitEasy. .EXAMPLE .\tools\Audit-PublicJargon.ps1 .NOTES Companion to the GitEasy "no jargon for users" rule. #> [CmdletBinding()] param( [string]$ProjectRoot = 'C:\Sysadmin\Scripts\GitEasy' ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' # STATE CHECK Write-Host '' Write-Host 'STATE CHECK: GitEasy public-surface jargon audit' -ForegroundColor Cyan if (-not (Test-Path -LiteralPath $ProjectRoot -PathType Container)) { throw "Missing project folder: $ProjectRoot" } $PublicPath = Join-Path $ProjectRoot 'Public' if (-not (Test-Path -LiteralPath $PublicPath -PathType Container)) { throw "Missing Public folder: $PublicPath" } $PublicFiles = @(Get-ChildItem -LiteralPath $PublicPath -Filter '*.ps1' -File | Sort-Object Name) if ($PublicFiles.Count -eq 0) { throw "No public command files found in $PublicPath." } Write-Host "Public files found: $($PublicFiles.Count)" Write-Host '' # Jargon vocabulary # HARD = almost certainly bad in user-facing text. Translate. # SOFT = sticky git words; flag for review, not auto-fail. May be acceptable in context. $HardJargon = @( 'upstream','downstream','HEAD','refspec','reflog', 'rebase','rebased','rebasing', 'fast-forward','fastforward', 'staged','unstaged','staging', 'working tree','working directory','working copy', 'fetch','fetched','fetching', 'SHA','blob','hunk','detached', 'tracking branch', 'cherry-pick','cherrypick', 'stash','stashed', 'porcelain' ) $SoftJargon = @( 'commit','commits','committed','committing', 'push','pushed','pushing', 'pull','pulled','pulling', 'branch','branches', 'merge','merged','merging','conflict','conflicts', 'master','main','origin','remote','remotes', 'diff','revert','reset','checkout', 'ahead','behind','diverged' ) $UserFacingCommands = @( 'Write-Host','Write-Warning','Write-Error', 'Write-Information','Write-Output','Write-Verbose' ) function Test-JargonInText { <# .DESCRIPTION Internal. Returns the subset of jargon terms that appear as whole words in the given text. Steps: 1. Return an empty array for empty or null input. 2. For each term, test for a whole-word match in the text. 3. Return the list of matching terms. #> param( [string]$Text, [string[]]$Terms ) $hits = @() if ([string]::IsNullOrWhiteSpace($Text)) { return $hits } foreach ($term in $Terms) { $pattern = '(?i)\b' + [regex]::Escape($term) + '\b' if ($Text -match $pattern) { $hits += $term } } return $hits } function Test-StringIsUserFacing { <# .DESCRIPTION Internal. Returns true if the given AST node is a direct argument to a user-facing output command or a throw statement. Steps: 1. Walk up the parent chain from the given node. 2. Return true when a user-facing command or throw statement is found first. 3. Return false when a non-user-facing command is found first, or no command is found. #> param([System.Management.Automation.Language.Ast]$Node) $parent = $Node.Parent while ($parent) { if ($parent -is [System.Management.Automation.Language.CommandAst]) { $cmdName = $parent.GetCommandName() if ($cmdName -and ($script:UserFacingCommands -contains $cmdName)) { return $true } return $false } if ($parent -is [System.Management.Automation.Language.ThrowStatementAst]) { return $true } $parent = $parent.Parent } return $false } $script:UserFacingCommands = $UserFacingCommands $Findings = New-Object System.Collections.Generic.List[object] foreach ($File in $PublicFiles) { $Tokens = $null $ParseErrors = $null $Ast = [System.Management.Automation.Language.Parser]::ParseFile( $File.FullName, [ref]$Tokens, [ref]$ParseErrors ) if ($ParseErrors -and $ParseErrors.Count -gt 0) { $Findings.Add([PSCustomObject]@{ File = $File.Name Severity = 'PARSE' Location = '-' Term = '-' Text = "$($ParseErrors.Count) parse error(s)" }) continue } # 1) Parameter names $params = @($Ast.FindAll( { param($n) $n -is [System.Management.Automation.Language.ParameterAst] }, $true )) foreach ($p in $params) { $name = $p.Name.VariablePath.UserPath $hardHits = Test-JargonInText -Text $name -Terms $HardJargon foreach ($h in $hardHits) { $Findings.Add([PSCustomObject]@{ File = $File.Name Severity = 'HARD' Location = "param -$name" Term = $h Text = "-$name" }) } $softHits = Test-JargonInText -Text $name -Terms $SoftJargon foreach ($h in $softHits) { $Findings.Add([PSCustomObject]@{ File = $File.Name Severity = 'SOFT' Location = "param -$name" Term = $h Text = "-$name" }) } } # 2) User-facing string literals $stringNodes = @($Ast.FindAll( { param($n) $n -is [System.Management.Automation.Language.StringConstantExpressionAst] }, $true )) foreach ($sNode in $stringNodes) { $text = $sNode.Value if ([string]::IsNullOrWhiteSpace($text)) { continue } if (-not (Test-StringIsUserFacing -Node $sNode)) { continue } $line = $sNode.Extent.StartLineNumber $hardHits = Test-JargonInText -Text $text -Terms $HardJargon foreach ($h in $hardHits) { $Findings.Add([PSCustomObject]@{ File = $File.Name Severity = 'HARD' Location = "line $line" Term = $h Text = $text }) } $softHits = Test-JargonInText -Text $text -Terms $SoftJargon foreach ($h in $softHits) { $Findings.Add([PSCustomObject]@{ File = $File.Name Severity = 'SOFT' Location = "line $line" Term = $h Text = $text }) } } # 3) ExpandableString literals (double-quoted strings with interpolation) — match against the raw text $expandableNodes = @($Ast.FindAll( { param($n) $n -is [System.Management.Automation.Language.ExpandableStringExpressionAst] }, $true )) foreach ($eNode in $expandableNodes) { $text = $eNode.Value if ([string]::IsNullOrWhiteSpace($text)) { continue } if (-not (Test-StringIsUserFacing -Node $eNode)) { continue } $line = $eNode.Extent.StartLineNumber $hardHits = Test-JargonInText -Text $text -Terms $HardJargon foreach ($h in $hardHits) { $Findings.Add([PSCustomObject]@{ File = $File.Name Severity = 'HARD' Location = "line $line" Term = $h Text = $text }) } $softHits = Test-JargonInText -Text $text -Terms $SoftJargon foreach ($h in $softHits) { $Findings.Add([PSCustomObject]@{ File = $File.Name Severity = 'SOFT' Location = "line $line" Term = $h Text = $text }) } } } if ($Findings.Count -eq 0) { Write-Host 'No jargon hits found in public surface.' -ForegroundColor Green return } $Hard = @($Findings | Where-Object { $_.Severity -eq 'HARD' }) $Soft = @($Findings | Where-Object { $_.Severity -eq 'SOFT' }) $Parse = @($Findings | Where-Object { $_.Severity -eq 'PARSE' }) if ($Hard.Count -gt 0) { Write-Host '--- HARD jargon (translate) ---' -ForegroundColor Red $Hard | Sort-Object File, Location | Format-Table -Property File, Location, Term, Text -AutoSize -Wrap | Out-Host } if ($Soft.Count -gt 0) { Write-Host '--- SOFT jargon (review) ---' -ForegroundColor Yellow $Soft | Sort-Object File, Location | Format-Table -Property File, Location, Term, Text -AutoSize -Wrap | Out-Host } if ($Parse.Count -gt 0) { Write-Host '--- PARSE errors ---' -ForegroundColor Magenta $Parse | Format-Table -AutoSize -Wrap | Out-Host } Write-Host '' Write-Host 'Audit summary:' -ForegroundColor Cyan Write-Host " Public files audited: $($PublicFiles.Count)" Write-Host " HARD jargon hits: $($Hard.Count)" Write-Host " SOFT jargon hits: $($Soft.Count)" if ($Parse.Count -gt 0) { Write-Host " Files with parse errors: $($Parse.Count)" -ForegroundColor Magenta } if ($Hard.Count -gt 0) { Write-Warning 'HARD-jargon terms appear in user-facing strings or parameter names. Translate before shipping.' } |