290 lines
9.5 KiB
PowerShell
290 lines
9.5 KiB
PowerShell
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$LibraryRoot,
|
|
[string]$TargetRoot = (Get-Location).Path,
|
|
[string]$OutputDirectory = ".ai-specs\imported-standards",
|
|
[string]$Query = "",
|
|
[string]$Type = "",
|
|
[string]$Domain = "",
|
|
[int]$Top = 10,
|
|
[switch]$Force,
|
|
[switch]$Json
|
|
)
|
|
|
|
$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
|
|
$selectScript = Join-Path $scriptDir "select-library-assets.ps1"
|
|
|
|
if (-not (Test-Path -LiteralPath $selectScript)) {
|
|
throw "Missing select-library-assets script: $selectScript"
|
|
}
|
|
|
|
function Resolve-TargetChildPath {
|
|
param(
|
|
[string]$Root,
|
|
[string]$ChildPath
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($ChildPath)) {
|
|
throw "Child path must not be empty."
|
|
}
|
|
|
|
if ($ChildPath -match "^(?:[A-Za-z]:[\\/]|\\\\)") {
|
|
$candidate = [System.IO.Path]::GetFullPath($ChildPath)
|
|
}
|
|
else {
|
|
$candidate = [System.IO.Path]::GetFullPath((Join-Path $Root $ChildPath))
|
|
}
|
|
|
|
$normalizedRoot = [System.IO.Path]::GetFullPath($Root).TrimEnd([char[]]"\/") + [System.IO.Path]::DirectorySeparatorChar
|
|
if (-not ($candidate + [System.IO.Path]::DirectorySeparatorChar).StartsWith($normalizedRoot, [System.StringComparison]::OrdinalIgnoreCase) -and
|
|
-not $candidate.StartsWith($normalizedRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
throw "Resolved output path is outside target project: $candidate"
|
|
}
|
|
|
|
return $candidate
|
|
}
|
|
|
|
function Get-RelativePath {
|
|
param(
|
|
[string]$Root,
|
|
[string]$Path
|
|
)
|
|
|
|
$rootFull = [System.IO.Path]::GetFullPath($Root).TrimEnd([char[]]"\/")
|
|
$pathFull = [System.IO.Path]::GetFullPath($Path)
|
|
|
|
if ($pathFull.StartsWith($rootFull, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
return ($pathFull.Substring($rootFull.Length).TrimStart([char[]]"\/") -replace "\\", "/")
|
|
}
|
|
|
|
return ($pathFull -replace "\\", "/")
|
|
}
|
|
|
|
function Remove-FrontMatter {
|
|
param([string]$Content)
|
|
|
|
return ([regex]::Replace($Content, "(?s)\A---\r?\n.*?\r?\n---\r?\n?", "")).Trim()
|
|
}
|
|
|
|
function U {
|
|
param([string]$Text)
|
|
return [regex]::Replace($Text, "\\u([0-9A-Fa-f]{4})", {
|
|
param($match)
|
|
return [string][char][Convert]::ToInt32($match.Groups[1].Value, 16)
|
|
})
|
|
}
|
|
|
|
function Test-GeneratedFile {
|
|
param([string]$Path)
|
|
|
|
if (-not (Test-Path -LiteralPath $Path)) {
|
|
return $true
|
|
}
|
|
|
|
$content = Get-Content -Raw -Encoding UTF8 -LiteralPath $Path
|
|
return $content -match "generated-by:\s*ks-zl-extract"
|
|
}
|
|
|
|
function Write-GeneratedFile {
|
|
param(
|
|
[string]$Path,
|
|
[string]$Content,
|
|
[switch]$Force
|
|
)
|
|
|
|
if ((Test-Path -LiteralPath $Path) -and -not $Force -and -not (Test-GeneratedFile -Path $Path)) {
|
|
throw "Refusing to overwrite non-generated file: $Path. Re-run with -Force only after reviewing the existing file."
|
|
}
|
|
|
|
$directory = Split-Path -Parent $Path
|
|
if (-not (Test-Path -LiteralPath $directory)) {
|
|
New-Item -ItemType Directory -Path $directory -Force | Out-Null
|
|
}
|
|
|
|
Set-Content -LiteralPath $Path -Encoding UTF8 -Value $Content
|
|
}
|
|
|
|
function ConvertTo-SafeFileName {
|
|
param([string]$Value)
|
|
|
|
$safe = ($Value.ToLowerInvariant() -replace "[^a-z0-9-]+", "-").Trim("-")
|
|
if ([string]::IsNullOrWhiteSpace($safe)) {
|
|
return "general"
|
|
}
|
|
|
|
return $safe
|
|
}
|
|
|
|
$selectionArgs = @{
|
|
LibraryRoot = $libraryPath
|
|
TargetRoot = $targetPath
|
|
Top = $Top
|
|
Json = $true
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($Query)) {
|
|
$selectionArgs["Query"] = $Query
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($Type)) {
|
|
$selectionArgs["Type"] = $Type
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($Domain)) {
|
|
$selectionArgs["Domain"] = $Domain
|
|
}
|
|
|
|
$selection = & $selectScript @selectionArgs | ConvertFrom-Json
|
|
$results = @()
|
|
if ($null -ne $selection -and $null -ne $selection.results) {
|
|
$results = @($selection.results)
|
|
}
|
|
|
|
$outputRoot = Resolve-TargetChildPath -Root $targetPath -ChildPath $OutputDirectory
|
|
$generatedMarker = "<!-- generated-by: ks-zl-extract -->"
|
|
$writtenFiles = New-Object System.Collections.Generic.List[object]
|
|
|
|
$groups = $results | Group-Object {
|
|
$domainValue = $_.domain
|
|
if ([string]::IsNullOrWhiteSpace($domainValue)) {
|
|
$domainValue = "general"
|
|
}
|
|
|
|
ConvertTo-SafeFileName $domainValue
|
|
}
|
|
|
|
foreach ($group in $groups) {
|
|
$domainFileName = "$($group.Name).md"
|
|
$outputPath = Join-Path $outputRoot $domainFileName
|
|
$relativeOutput = Get-RelativePath -Root $targetPath -Path $outputPath
|
|
$lines = New-Object System.Collections.Generic.List[string]
|
|
|
|
[void]$lines.Add($generatedMarker)
|
|
[void]$lines.Add((U "# $($group.Name) \u63d0\u53d6\u89c4\u8303"))
|
|
[void]$lines.Add("")
|
|
[void]$lines.Add((U "\u6765\u6e90\u84b8\u998f\u5e93\uff1a$libraryPath"))
|
|
[void]$lines.Add((U "\u84b8\u998f\u5e93\u7d22\u5f15\uff1a$($selection.indexPath)"))
|
|
[void]$lines.Add((U "\u67e5\u8be2\u6761\u4ef6\uff1a$Query"))
|
|
[void]$lines.Add("")
|
|
|
|
foreach ($asset in @($group.Group)) {
|
|
$sourcePath = $asset.fullPath
|
|
if (-not (Test-Path -LiteralPath $sourcePath)) {
|
|
throw "Selected asset file is missing: $sourcePath"
|
|
}
|
|
|
|
$sourceContent = Get-Content -Raw -Encoding UTF8 -LiteralPath $sourcePath
|
|
$body = Remove-FrontMatter -Content $sourceContent
|
|
|
|
[void]$lines.Add("## $($asset.title)")
|
|
[void]$lines.Add("")
|
|
[void]$lines.Add((U "- \u6765\u6e90\uff1a$($asset.relativePath)"))
|
|
[void]$lines.Add((U "- \u7c7b\u578b\uff1a$($asset.type)"))
|
|
if (@($asset.matchedSignals).Count -gt 0) {
|
|
[void]$lines.Add((U "- \u547d\u4e2d\u4fe1\u53f7\uff1a$(@($asset.matchedSignals) -join ', ')"))
|
|
}
|
|
if (@($asset.matchedTerms).Count -gt 0) {
|
|
[void]$lines.Add((U "- \u547d\u4e2d\u5173\u952e\u8bcd\uff1a$(@($asset.matchedTerms) -join ', ')"))
|
|
}
|
|
if (@($asset.targetOutputs).Count -gt 0) {
|
|
[void]$lines.Add((U "- \u5efa\u8bae\u5199\u5165\u76ee\u6807\uff1a$(@($asset.targetOutputs) -join ', ')"))
|
|
}
|
|
[void]$lines.Add("")
|
|
[void]$lines.Add($body)
|
|
[void]$lines.Add("")
|
|
}
|
|
|
|
Write-GeneratedFile -Path $outputPath -Content ($lines -join "`n") -Force:$Force
|
|
|
|
[void]$writtenFiles.Add([pscustomobject]@{
|
|
path = $relativeOutput
|
|
assetCount = @($group.Group).Count
|
|
})
|
|
}
|
|
|
|
$reportPath = Join-Path $outputRoot "extraction-report.md"
|
|
$reportLines = New-Object System.Collections.Generic.List[string]
|
|
[void]$reportLines.Add($generatedMarker)
|
|
[void]$reportLines.Add((U "# \u63d0\u53d6\u62a5\u544a"))
|
|
[void]$reportLines.Add("")
|
|
[void]$reportLines.Add((U "## \u76ee\u6807\u9879\u76ee\u4fe1\u53f7"))
|
|
|
|
if ($null -ne $selection.signals -and @($selection.signals).Count -gt 0) {
|
|
foreach ($signal in @($selection.signals)) {
|
|
[void]$reportLines.Add("- `$($signal.name)`: $($signal.confidence); $($signal.reason)")
|
|
foreach ($evidence in @($signal.evidence)) {
|
|
[void]$reportLines.Add(" - $evidence")
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
[void]$reportLines.Add((U "- \u672a\u68c0\u6d4b\u5230\u3002"))
|
|
}
|
|
|
|
[void]$reportLines.Add("")
|
|
[void]$reportLines.Add((U "## \u84b8\u998f\u5e93\u7d22\u5f15"))
|
|
[void]$reportLines.Add((U "- \u84b8\u998f\u5e93\uff1a$libraryPath"))
|
|
[void]$reportLines.Add((U "- \u7d22\u5f15\uff1a$($selection.indexPath)"))
|
|
[void]$reportLines.Add((U "- \u67e5\u8be2\uff1a$Query"))
|
|
|
|
[void]$reportLines.Add("")
|
|
[void]$reportLines.Add((U "## \u5df2\u63d0\u53d6\u8d44\u4ea7"))
|
|
if ($results.Count -gt 0) {
|
|
[void]$reportLines.Add((U "| \u6765\u6e90\u8d44\u4ea7 | \u5199\u5165\u6587\u4ef6 | \u547d\u4e2d\u539f\u56e0 |"))
|
|
[void]$reportLines.Add("| --- | --- | --- |")
|
|
foreach ($asset in $results) {
|
|
$outputFile = ($writtenFiles | Where-Object { $_.path -like "*$(ConvertTo-SafeFileName $asset.domain).md" } | Select-Object -First 1).path
|
|
$matchReason = @()
|
|
$matchReason += @($asset.matchedSignals)
|
|
$matchReason += @($asset.matchedTerms)
|
|
if ($matchReason.Count -eq 0) {
|
|
$matchReason = @("selected")
|
|
}
|
|
[void]$reportLines.Add("| $($asset.relativePath) | $outputFile | $($matchReason -join ', ') |")
|
|
}
|
|
}
|
|
else {
|
|
[void]$reportLines.Add((U "- \u65e0\u3002"))
|
|
}
|
|
|
|
[void]$reportLines.Add("")
|
|
[void]$reportLines.Add((U "## \u51b2\u7a81\u4e0e\u5f85\u786e\u8ba4"))
|
|
[void]$reportLines.Add((U "- \u811a\u672c\u672a\u68c0\u6d4b\u5230\u51b2\u7a81\u3002\u5408\u5e76\u5bfc\u5165\u89c4\u5219\u524d\uff0c\u4ecd\u9700\u590d\u6838\u76ee\u6807\u9879\u76ee\u5df2\u6709\u6587\u6863\u3002"))
|
|
|
|
Write-GeneratedFile -Path $reportPath -Content ($reportLines -join "`n") -Force:$Force
|
|
[void]$writtenFiles.Add([pscustomobject]@{
|
|
path = Get-RelativePath -Root $targetPath -Path $reportPath
|
|
assetCount = 0
|
|
})
|
|
|
|
$writtenFileArray = @($writtenFiles.ToArray())
|
|
$result = @{
|
|
libraryRoot = $libraryPath
|
|
targetRoot = $targetPath
|
|
outputDirectory = Get-RelativePath -Root $targetPath -Path $outputRoot
|
|
selectedAssetCount = $results.Count
|
|
writtenFiles = $writtenFileArray
|
|
}
|
|
|
|
if ($Json) {
|
|
ConvertTo-Json -InputObject $result -Depth 8
|
|
return
|
|
}
|
|
|
|
Write-Host (U "\u5df2\u4ece\u84b8\u998f\u5e93\u63d0\u53d6 $($results.Count) \u4e2a\u8d44\u4ea7\u3002")
|
|
foreach ($file in $writtenFiles) {
|
|
Write-Host (U " \u5199\u5165\uff1a$($file.path)")
|
|
}
|