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 = "" $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)") }