param( [Parameter(Mandatory = $true)] [string]$LibraryRoot, [string]$TargetRoot = (Get-Location).Path, [string]$Query = "", [string]$Type = "", [string]$Domain = "", [int]$Top = 10, [switch]$Json, [switch]$Rebuild ) $ErrorActionPreference = "Stop" if (-not (Test-Path -LiteralPath $LibraryRoot)) { throw "Library root does not exist: $LibraryRoot" } if (-not (Test-Path -LiteralPath $TargetRoot)) { throw "Target root does not exist: $TargetRoot" } $libraryPath = (Resolve-Path -LiteralPath $LibraryRoot).Path $targetPath = (Resolve-Path -LiteralPath $TargetRoot).Path $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $detectScript = Join-Path $scriptDir "detect-project-signals.ps1" $indexPath = Join-Path $libraryPath "_indexes\artifacts-index.json" $buildScript = Join-Path $libraryPath "scripts\build-artifact-index.ps1" if (-not (Test-Path -LiteralPath $detectScript)) { throw "Missing detect-project-signals script: $detectScript" } if ($Rebuild -or -not (Test-Path -LiteralPath $indexPath)) { if (-not (Test-Path -LiteralPath $buildScript)) { throw "Library artifact index is missing and no build script was found: $indexPath" } & $buildScript -Root $libraryPath -OutputPath $indexPath -Quiet } if (-not (Test-Path -LiteralPath $indexPath)) { throw "Library artifact index does not exist: $indexPath" } $signalInfo = & $detectScript -Root $targetPath -Json | ConvertFrom-Json $signalNames = @() if ($null -ne $signalInfo -and $null -ne $signalInfo.signals) { $signalNames = @($signalInfo.signals | ForEach-Object { $_.name } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) } $signalsValue = $signalNames -join "," if ($signalNames.Count -eq 0 -and [string]::IsNullOrWhiteSpace($Query) -and [string]::IsNullOrWhiteSpace($Type) -and [string]::IsNullOrWhiteSpace($Domain)) { throw "No target project signals or search criteria were found. Provide -Query, -Type, or -Domain." } function Get-Terms { param([string]$Value) $terms = New-Object System.Collections.Generic.List[string] foreach ($part in [regex]::Split($Value.ToLowerInvariant(), "[\s,;\uFF0C\uFF1B\u3001]+")) { $trimmed = $part.Trim() if (-not [string]::IsNullOrWhiteSpace($trimmed)) { [void]$terms.Add($trimmed) } } return @($terms) } function Get-TextValue { param($Value) if ($null -eq $Value) { return "" } if ($Value -is [array]) { return (@($Value) -join " ") } return [string]$Value } function Test-ContainsText { param( [string]$Haystack, [string]$Needle ) if ([string]::IsNullOrWhiteSpace($Haystack) -or [string]::IsNullOrWhiteSpace($Needle)) { return $false } return $Haystack.ToLowerInvariant().Contains($Needle.ToLowerInvariant()) } function Test-ExactText { param( [string[]]$Values, [string]$Needle ) foreach ($value in @($Values)) { if (-not [string]::IsNullOrWhiteSpace($value) -and $value.Trim().ToLowerInvariant() -eq $Needle.Trim().ToLowerInvariant()) { return $true } } return $false } function Add-Score { param( [int]$Current, [string]$FieldValue, [string]$Term, [int]$Weight, [string]$FieldName, [System.Collections.Generic.List[string]]$MatchedFields ) if (Test-ContainsText -Haystack $FieldValue -Needle $Term) { [void]$MatchedFields.Add($FieldName) return ($Current + $Weight) } return $Current } $terms = @(Get-Terms -Value $Query) $rawItems = Get-Content -Raw -Encoding UTF8 -LiteralPath $indexPath | ConvertFrom-Json $items = @() if ($null -ne $rawItems) { if ($rawItems -is [array]) { $items = @($rawItems) } else { $items = @($rawItems) } } $rankableResults = New-Object System.Collections.Generic.List[object] foreach ($item in $items) { if (-not [string]::IsNullOrWhiteSpace($Type) -and ((Get-TextValue $item.type).ToLowerInvariant() -ne $Type.ToLowerInvariant())) { continue } if (-not [string]::IsNullOrWhiteSpace($Domain) -and ((Get-TextValue $item.domain).ToLowerInvariant() -ne $Domain.ToLowerInvariant())) { continue } $score = 0 $matchedTerms = New-Object System.Collections.Generic.HashSet[string] $matchedSignals = New-Object System.Collections.Generic.List[string] $matchedFields = New-Object System.Collections.Generic.List[string] foreach ($signal in $signalNames) { if (Test-ExactText -Values @($item.appliesWhen) -Needle $signal) { [void]$matchedSignals.Add($signal) $score += 70 } } foreach ($term in $terms) { $termScore = 0 $termScore = Add-Score -Current $termScore -FieldValue (Get-TextValue $item.title) -Term $term -Weight 80 -FieldName "title" -MatchedFields $matchedFields $termScore = Add-Score -Current $termScore -FieldValue (Get-TextValue $item.tags) -Term $term -Weight 60 -FieldName "tags" -MatchedFields $matchedFields $termScore = Add-Score -Current $termScore -FieldValue (Get-TextValue $item.domain) -Term $term -Weight 35 -FieldName "domain" -MatchedFields $matchedFields $termScore = Add-Score -Current $termScore -FieldValue (Get-TextValue $item.category) -Term $term -Weight 25 -FieldName "category" -MatchedFields $matchedFields $termScore = Add-Score -Current $termScore -FieldValue (Get-TextValue $item.appliesWhen) -Term $term -Weight 35 -FieldName "appliesWhen" -MatchedFields $matchedFields $termScore = Add-Score -Current $termScore -FieldValue (Get-TextValue $item.targetOutputs) -Term $term -Weight 25 -FieldName "targetOutputs" -MatchedFields $matchedFields $termScore = Add-Score -Current $termScore -FieldValue (Get-TextValue $item.body) -Term $term -Weight 15 -FieldName "body" -MatchedFields $matchedFields if ($termScore -gt 0) { [void]$matchedTerms.Add($term) $score += $termScore } } if ($terms.Count -gt 0 -and $matchedTerms.Count -eq $terms.Count) { $score += 30 } if ($signalNames.Count -gt 0 -and $matchedSignals.Count -eq $signalNames.Count) { $score += 25 } if ($score -gt 0 -or ($terms.Count -eq 0 -and $signalNames.Count -eq 0)) { [void]$rankableResults.Add([pscustomobject]@{ score = $score type = $item.type title = $item.title domain = $item.domain category = $item.category relativePath = $item.relativePath fullPath = $item.fullPath tags = @($item.tags) appliesWhen = @($item.appliesWhen) targetOutputs = @($item.targetOutputs) updated = $item.updated matchedTerms = @($matchedTerms) matchedSignals = @($matchedSignals | Select-Object -Unique) matchedFields = @($matchedFields | Select-Object -Unique) }) } } $results = @($rankableResults.ToArray() | Sort-Object ` @{ Expression = "score"; Descending = $true }, @{ Expression = "updated"; Descending = $true }, @{ Expression = "relativePath"; Descending = $false } | Select-Object -First $Top) $selection = [pscustomobject]@{ libraryRoot = $libraryPath targetRoot = $targetPath signals = @($signalInfo.signals) indexPath = $indexPath query = $Query type = $Type domain = $Domain results = @($results) } if ($Json) { ConvertTo-Json -InputObject $selection -Depth 10 return } Write-Host "Library: $libraryPath" Write-Host "Target: $targetPath" if ($signalNames.Count -gt 0) { Write-Host "Signals: $($signalNames -join ', ')" } else { Write-Host "Signals: none" } if ($results.Count -eq 0) { Write-Host "No matching library assets selected." return } foreach ($result in $results) { Write-Host "[$($result.score)] $($result.type): $($result.title)" Write-Host " path: $($result.relativePath)" if (@($result.targetOutputs).Count -gt 0) { Write-Host " targetOutputs: $(@($result.targetOutputs) -join ', ')" } }