Initial commit

This commit is contained in:
2026-05-17 17:24:03 +08:00
commit 9204ae36fd
6 changed files with 802 additions and 0 deletions

View File

@@ -0,0 +1,257 @@
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 ', ')"
}
}