ValueOpportunity/Get-FeatureAdoption.ps1
|
<#
.SYNOPSIS Scores feature adoption from assessment signals and license data. .DESCRIPTION For each feature in sku-feature-map.json, determines adoption state by cross-referencing signals accumulated by Add-SecuritySetting against the feature's checkIds. Reads sibling License Utilization CSV for license gating. Zero new API calls. .PARAMETER ProjectRoot Path to the module root (contains controls/). .PARAMETER AssessmentFolder Path to the assessment output folder (contains sibling CSVs). #> [CmdletBinding()] param( [Parameter()] [string]$ProjectRoot, [Parameter()] [string]$AssessmentFolder ) function Get-FeatureAdoption { [CmdletBinding()] [OutputType([PSCustomObject[]])] param( [Parameter(Mandatory)] [hashtable]$AdoptionSignals, [Parameter(Mandatory)] [PSCustomObject[]]$LicenseUtilization, [Parameter(Mandatory)] $FeatureMap, [Parameter(Mandatory)] [string]$AssessmentFolder, [Parameter()] [string]$OutputPath ) $categories = @{} foreach ($cat in $FeatureMap.categories) { $categories[$cat.id] = $cat.name } $licenseLookup = @{} foreach ($lic in $LicenseUtilization) { $licenseLookup[$lic.FeatureId] = $lic.IsLicensed } $results = foreach ($feature in $FeatureMap.features) { $featureId = $feature.featureId $isLicensed = $false if ($licenseLookup.ContainsKey($featureId)) { $isLicensed = $licenseLookup[$featureId] } if (-not $isLicensed) { [PSCustomObject]@{ FeatureId = $featureId FeatureName = $feature.name Category = $categories[$feature.category] AdoptionState = 'NotLicensed' AdoptionScore = 0 PassedChecks = 0 TotalChecks = 0 DepthMetric = '' } continue } $passedCount = 0 $totalCount = 0 foreach ($baseId in $feature.checkIds) { $prefix = "$baseId." foreach ($signalKey in $AdoptionSignals.Keys) { if ($signalKey.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { $totalCount++ if ($AdoptionSignals[$signalKey].Status -eq 'Pass') { $passedCount++ } } } } if ($totalCount -eq 0) { $adoptionState = 'Unknown' $adoptionScore = 0 } elseif ($passedCount -eq $totalCount) { $adoptionState = 'Adopted' $adoptionScore = 100 } elseif ($passedCount -eq 0) { $adoptionState = 'NotAdopted' $adoptionScore = 0 } else { $adoptionState = 'Partial' $adoptionScore = [math]::Round(($passedCount / $totalCount) * 100) } $depthMetric = '' $csvSignals = $feature.csvSignals if ($null -ne $csvSignals -and @($csvSignals).Count -gt 0) { $depthParts = @() foreach ($csvDef in $csvSignals) { try { $csvFile = Join-Path -Path $AssessmentFolder -ChildPath $csvDef.file if (-not (Test-Path -Path $csvFile)) { continue } $csvData = Import-Csv -Path $csvFile -Encoding UTF8 if ($csvDef.metric -eq 'passRate') { $column = $csvDef.column $pattern = $csvDef.pattern $matching = $csvData | Where-Object { $_.$column -match $pattern } $matchTotal = @($matching).Count $matchPass = @($matching | Where-Object { $_.Status -eq 'Pass' }).Count if ($matchTotal -gt 0) { $rate = [math]::Round(($matchPass / $matchTotal) * 100) $depthParts += "$($csvDef.label): $rate% ($matchPass/$matchTotal)" } } elseif ($csvDef.metric -eq 'count') { $column = $csvDef.column $pattern = $csvDef.pattern $matching = $csvData | Where-Object { $_.$column -match $pattern } $depthParts += "$($csvDef.label): $(@($matching).Count)" } } catch { Write-Verbose "CSV signal parsing failed for $($csvDef.file): $_" } } $depthMetric = $depthParts -join '; ' } [PSCustomObject]@{ FeatureId = $featureId FeatureName = $feature.name Category = $categories[$feature.category] AdoptionState = $adoptionState AdoptionScore = $adoptionScore PassedChecks = $passedCount TotalChecks = $totalCount DepthMetric = $depthMetric } } if ($OutputPath) { $results | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8 Write-Output "Exported feature adoption ($($results.Count) features) to $OutputPath" } else { Write-Output $results } } # --- Script entry point (called by orchestrator with -ProjectRoot) --- if ($ProjectRoot -and $AssessmentFolder) { $featureMapPath = Join-Path -Path $ProjectRoot -ChildPath 'controls\sku-feature-map.json' if (-not (Test-Path -Path $featureMapPath)) { Write-Warning "sku-feature-map.json not found at $featureMapPath" return } $featureMap = Get-Content -Path $featureMapPath -Raw | ConvertFrom-Json $signals = @{} if (Get-Command -Name Get-AdoptionSignals -ErrorAction SilentlyContinue) { $signals = Get-AdoptionSignals } elseif ($global:AdoptionSignals) { $signals = $global:AdoptionSignals.Clone() } # Read sibling License Utilization CSV $licCsvPath = Join-Path -Path $AssessmentFolder -ChildPath '40-License-Utilization.csv' $licenseData = @() if (Test-Path -Path $licCsvPath) { $licenseData = @(Import-Csv -Path $licCsvPath -Encoding UTF8 | ForEach-Object { [PSCustomObject]@{ FeatureId = $_.FeatureId IsLicensed = ($_.IsLicensed -eq 'True') } }) } Get-FeatureAdoption -AdoptionSignals $signals -LicenseUtilization $licenseData -FeatureMap $featureMap -AssessmentFolder $AssessmentFolder } |