← Files Power BI DesktopARCHIVED FILE

scripts/New-PowerBIPBIPChangeImpactGate.ps1

5.67 KB · Oct 2, 2026 · 00:32 UTC

↓ Download file

param(
    [string]$Path = ".",
    [string]$BaseRef,
    [string]$HeadRef = "HEAD",
    [string]$ChangedFilesPath,
    [string]$OutputPath,
    [switch]$Json
)

$ErrorActionPreference = 'Stop'
$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
$root = (Resolve-Path -LiteralPath $Path).Path

function Invoke-JsonScript {
    param([string]$ScriptName, [hashtable]$Arguments)
    $scriptPath = Join-Path $scriptRoot $ScriptName
    $cmdArgs = @{}
    foreach ($key in $Arguments.Keys) {
        if ($null -ne $Arguments[$key] -and $Arguments[$key] -ne '') { $cmdArgs[$key] = $Arguments[$key] }
    }
    try { & $scriptPath @cmdArgs -Json | ConvertFrom-Json } catch { [pscustomobject]@{ error = $_.Exception.Message } }
}

function Get-ChangedFiles {
    if ($ChangedFilesPath -and (Test-Path -LiteralPath $ChangedFilesPath)) {
        $raw = Get-Content -LiteralPath $ChangedFilesPath
        return @($raw | Where-Object { $_ -and -not $_.StartsWith('#') })
    }
    if ($BaseRef) {
        try {
            $range = if ($HeadRef) { "$BaseRef..$HeadRef" } else { $BaseRef }
            return @(git -C $root diff --name-only $range 2>$null | Where-Object { $_ })
        }
        catch { }
    }
    try { return @(git -C $root status --short 2>$null | ForEach-Object { ($_ -replace '^.. ', '').Trim() } | Where-Object { $_ }) } catch { @() }
}

function Test-TextMentionsMetric {
    param([string]$Text, [object]$Metric)
    if (-not $Text) { return $false }
    if ($Metric.name -and $Text -match [regex]::Escape($Metric.name)) { return $true }
    if ($Metric.id -and $Text -match [regex]::Escape($Metric.id)) { return $true }
    if ($Metric.table -and $Text -match [regex]::Escape($Metric.table)) { return $true }
    $false
}

$changedFiles = @(Get-ChangedFiles)
$catalog = Invoke-JsonScript 'New-PowerBIMetricCatalog.ps1' @{ Path = $root }
$dependency = Invoke-JsonScript 'New-PowerBIDependencyGraph.ps1' @{ Path = $root }
$trust = Invoke-JsonScript 'New-PowerBIKpiTrustScore.ps1' @{ Path = $root }
$usage = Invoke-JsonScript 'New-PowerBIUsageTrustMatrix.ps1' @{ Path = $root }
$visualImpact = Invoke-JsonScript 'New-PowerBIVisualMeasureImpactMap.ps1' @{ Path = $root }

$changedTexts = foreach ($file in $changedFiles) {
    $full = Join-Path $root $file
    if (Test-Path -LiteralPath $full -PathType Leaf) {
        try { [pscustomobject]@{ path = $file; text = (Get-Content -Raw -LiteralPath $full -ErrorAction Stop) } }
        catch { [pscustomobject]@{ path = $file; text = '' } }
    }
    else { [pscustomobject]@{ path = $file; text = $file } }
}

$affectedMetrics = foreach ($metric in @($catalog.metrics)) {
    $hits = @($changedTexts | Where-Object { (Test-TextMentionsMetric -Text $_.text -Metric $metric) -or (Test-TextMentionsMetric -Text $_.path -Metric $metric) })
    if ($hits.Count -eq 0 -and $changedFiles.Count -gt 0) { continue }
    $trustItem = @($trust.metrics | Where-Object { $_.name -eq $metric.name } | Select-Object -First 1)
    $usageItem = @($usage.matrixItems | Where-Object { $_.metricName -eq $metric.name } | Select-Object -First 1)
    $visuals = @($visualImpact.impacts | Where-Object { $_.measure -eq $metric.name -or $_.measureName -eq $metric.name })
    $edges = @($dependency.edges | Where-Object { $_.from -eq $metric.name -or $_.to -eq $metric.name })
    [pscustomobject]@{
        metric = $metric.name
        table = $metric.table
        changedFiles = @($hits | Select-Object -ExpandProperty path -Unique)
        trustScore = if ($trustItem) { $trustItem.trustScore } else { $null }
        usagePriority = if ($usageItem) { $usageItem.priority } else { $null }
        dependencyEdgeCount = $edges.Count
        visualReferenceCount = $visuals.Count
        releaseImpact = if ($trustItem -and $trustItem.trustScore -lt 60) { 'BlockerCandidate' } elseif ($edges.Count -gt 2 -or $visuals.Count -gt 0) { 'ReviewRequired' } else { 'Low' }
    }
}

$blockers = @($affectedMetrics | Where-Object { $_.releaseImpact -eq 'BlockerCandidate' })
$review = @($affectedMetrics | Where-Object { $_.releaseImpact -eq 'ReviewRequired' })
$decision = if ($blockers.Count -gt 0) { 'No-Go' } elseif ($review.Count -gt 0 -or $changedFiles.Count -eq 0) { 'Warn' } else { 'Go' }
$result = [pscustomobject]@{
    schema = 'codex.powerbi.pbipChangeImpactGate.v1'
    generated = (Get-Date).ToString('s')
    source = $root
    baseRef = $BaseRef
    headRef = $HeadRef
    changeCount = $changedFiles.Count
    changedFiles = @($changedFiles)
    affectedMetricCount = @($affectedMetrics).Count
    decision = $decision
    blockingReasonCount = $blockers.Count
    affectedMetrics = @($affectedMetrics | Sort-Object releaseImpact, metric)
    prSummary = if ($decision -eq 'Go') { 'No KPI release blockers detected from changed files.' } elseif ($decision -eq 'Warn') { 'KPI impact requires reviewer attention before release.' } else { 'KPI trust blockers are affected by this change.' }
}

if ($Json) {
    $text = $result | ConvertTo-Json -Depth 10
    if ($OutputPath) { Set-Content -LiteralPath $OutputPath -Value $text -Encoding UTF8 }
    $text
    return
}

$lines = @('# Power BI PBIP Change Impact Gate', '', "Decision: **$decision**", "Changed files: $($changedFiles.Count)", "Affected metrics: $($result.affectedMetricCount)", '', '## Affected Metrics') +
    @($result.affectedMetrics | ForEach-Object { "- [$($_.releaseImpact)] $($_.metric): trust=$($_.trustScore), dependencies=$($_.dependencyEdgeCount), visuals=$($_.visualReferenceCount)" })
$content = ($lines -join [Environment]::NewLine) + [Environment]::NewLine
if ($OutputPath) { Set-Content -LiteralPath $OutputPath -Value $content -Encoding UTF8 }
$content

SHA-256: 0a91de51d4b9fb27197714aee2dc6cd45dff7a5eb98a61c9341e39d1e9563547