param( [string]$Root = (Get-Location).Path, [string]$OutputPath, [switch]$Quiet ) $ErrorActionPreference = "Stop" if (-not (Test-Path -LiteralPath $Root)) { throw "Root path does not exist: $Root" } $rootPath = (Resolve-Path -LiteralPath $Root).Path.TrimEnd([char[]]"\/") if ([string]::IsNullOrWhiteSpace($OutputPath)) { $OutputPath = Join-Path (Join-Path $rootPath "_indexes") "requirements-index.json" } function Get-RelativePath { param([string]$Path) $fullPath = (Resolve-Path -LiteralPath $Path).Path if ($fullPath.StartsWith($rootPath, [System.StringComparison]::OrdinalIgnoreCase)) { return ($fullPath.Substring($rootPath.Length).TrimStart([char[]]"\/") -replace "\\", "/") } return ($fullPath -replace "\\", "/") } function Get-FrontMatter { param([string]$Content) $match = [regex]::Match($Content, "(?s)\A---\r?\n(?.*?)\r?\n---") if ($match.Success) { return $match.Groups["body"].Value } return "" } function Get-MetadataValue { param( [string]$FrontMatter, [string]$Key ) $match = [regex]::Match($FrontMatter, "(?m)^$([regex]::Escape($Key))\s*:\s*(?.*)\s*$") if ($match.Success) { return $match.Groups["value"].Value.Trim().Trim('"').Trim("'") } return "" } function Get-MetadataList { param( [string]$FrontMatter, [string]$Key ) $lines = $FrontMatter -split "`r?`n" $values = New-Object System.Collections.Generic.List[string] for ($i = 0; $i -lt $lines.Count; $i++) { if ($lines[$i] -match "^$([regex]::Escape($Key))\s*:\s*(?.*)\s*$") { $inline = $Matches["inline"].Trim() if ($inline.StartsWith("[") -and $inline.EndsWith("]")) { $inline.Trim("[]").Split(",") | ForEach-Object { $_.Trim().Trim('"').Trim("'") } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { [void]$values.Add($_) } } for ($j = $i + 1; $j -lt $lines.Count; $j++) { if ($lines[$j] -match "^\s*-\s*(?.+?)\s*$") { [void]$values.Add($Matches["value"].Trim().Trim('"').Trim("'")) continue } if ($lines[$j] -match "^\S[^:]*\s*:") { break } } break } } return @($values) } function Get-MarkdownSectionBody { param( [string]$Content, [string]$Section ) $match = [regex]::Match($Content, "(?ms)^##\s+$([regex]::Escape($Section))[ `t]*\r?\n(?.*?)(?=^##\s+|\z)") if ($match.Success) { return $match.Groups["body"].Value } return "" } function ConvertTo-PlainText { param([string]$Value) if ([string]::IsNullOrWhiteSpace($Value)) { return "" } return (($Value -replace "`r?`n", " ") -replace "\s+", " ").Trim() } function Get-PackageInfo { param([string]$RelativePath) $parts = $RelativePath -split "/" $domain = "" $packageType = "" $slug = "" if ($parts.Count -ge 4) { $domain = $parts[0] $packageType = $parts[1] $slug = $parts[2] } return [pscustomobject]@{ domain = $domain packageType = $packageType slug = $slug } } $excludedTopDirs = @(".git", "templates", "scripts") $requirementFiles = Get-ChildItem -Path $rootPath -Recurse -File -Filter "requirement.md" -Force | Where-Object { $relativePath = Get-RelativePath $_.FullName $topDir = ($relativePath -split "/")[0] $excludedTopDirs -notcontains $topDir } $items = New-Object System.Collections.Generic.List[object] foreach ($file in $requirementFiles) { $relativePath = Get-RelativePath $file.FullName $content = Get-Content -Raw -Encoding UTF8 -LiteralPath $file.FullName if ($content -notmatch "(?m)^status:\s*requirement-draft\s*$") { continue } $frontMatter = Get-FrontMatter -Content $content $packageInfo = Get-PackageInfo -RelativePath $relativePath $tags = @(Get-MetadataList -FrontMatter $frontMatter -Key "tags") [void]$items.Add([pscustomobject][ordered]@{ schemaVersion = 1 type = "requirement-package" title = Get-MetadataValue -FrontMatter $frontMatter -Key "title" category = Get-MetadataValue -FrontMatter $frontMatter -Key "category" tags = $tags status = Get-MetadataValue -FrontMatter $frontMatter -Key "status" updated = Get-MetadataValue -FrontMatter $frontMatter -Key "updated" source = Get-MetadataValue -FrontMatter $frontMatter -Key "source" domain = $packageInfo.domain packageType = $packageInfo.packageType slug = $packageInfo.slug relativePath = $relativePath fullPath = $file.FullName feature = ConvertTo-PlainText (Get-MarkdownSectionBody -Content $content -Section "功能") flow = ConvertTo-PlainText (Get-MarkdownSectionBody -Content $content -Section "流程") dataTables = ConvertTo-PlainText (Get-MarkdownSectionBody -Content $content -Section "数据表") dictionaries = ConvertTo-PlainText (Get-MarkdownSectionBody -Content $content -Section "字典") businessRules = ConvertTo-PlainText (Get-MarkdownSectionBody -Content $content -Section "业务规则") acceptance = ConvertTo-PlainText (Get-MarkdownSectionBody -Content $content -Section "验收") portability = ConvertTo-PlainText (Get-MarkdownSectionBody -Content $content -Section "移植说明") sourceEvidence = ConvertTo-PlainText (Get-MarkdownSectionBody -Content $content -Section "来源依据") pending = ConvertTo-PlainText (Get-MarkdownSectionBody -Content $content -Section "待确认") related = ConvertTo-PlainText (Get-MarkdownSectionBody -Content $content -Section "Related") }) } $outputDirectory = Split-Path -Parent $OutputPath if (-not [string]::IsNullOrWhiteSpace($outputDirectory) -and -not (Test-Path -LiteralPath $outputDirectory)) { New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null } $itemArray = @($items.ToArray()) $json = ConvertTo-Json -InputObject $itemArray -Depth 8 Set-Content -LiteralPath $OutputPath -Value $json -Encoding UTF8 if (-not $Quiet) { Write-Host "Requirement index built. Indexed $($items.Count) package(s): $OutputPath" }