[CmdletBinding()] param( [string]$Root = (Join-Path $PSScriptRoot '..'), [string]$Output = '', [switch]$FailOnFindings ) $ErrorActionPreference = 'Stop' $Root = [IO.Path]::GetFullPath($Root) $Wiki = Join-Path $Root 'wiki' if ([string]::IsNullOrWhiteSpace($Output)) { $Output = Join-Path $Wiki '_meta\wiki-review.md' } else { $Output = [IO.Path]::GetFullPath($Output) } $pages = @(Get-ChildItem -LiteralPath $Wiki -Recurse -File -Filter '*.md' | Where-Object { $_.DirectoryName -ne (Join-Path $Wiki '_meta') }) $byBaseName = @{} foreach ($page in $pages) { $byBaseName[$page.BaseName] = $page } $lowConfidence = [Collections.Generic.List[object]]::new() $contested = [Collections.Generic.List[object]]::new() $longPages = [Collections.Generic.List[object]]::new() $missingRole = [Collections.Generic.List[object]]::new() $missingSources = [Collections.Generic.List[object]]::new() $inbound = @{} foreach ($page in $pages) { $inbound[$page.BaseName] = 0 } foreach ($page in $pages) { $text = [IO.File]::ReadAllText($page.FullName) $lines = @($text -split "`r?`n").Count $relative = $page.FullName.Substring($Root.Length + 1).Replace('\', '/') $title = if ($text -match '(?m)^title:\s*["'']?([^"''\r\n]+)') { $Matches[1].Trim() } else { $page.BaseName } $type = if ($text -match '(?m)^type:\s*["'']?([^"''\r\n]+)') { $Matches[1].Trim() } else { '' } if ($text -match '(?m)^confidence:\s*(low)\s*$') { $lowConfidence.Add([pscustomobject]@{ Path = $relative; Title = $title; Type = $type }) } if ($text -match '(?m)^contested:\s*true\s*$') { $contested.Add([pscustomobject]@{ Path = $relative; Title = $title; Type = $type }) } if ($lines -gt 200) { $longPages.Add([pscustomobject]@{ Path = $relative; Title = $title; Lines = $lines }) } if ($page.Name -notin @('INDEX.md', 'Glossary.md', 'sources.md') -and $text -notmatch '(?m)^## 页面定位|^## 页面定位与证据边界|^## 页面职责') { $missingRole.Add([pscustomobject]@{ Path = $relative; Title = $title }) } if ($page.Name -notin @('INDEX.md', 'Glossary.md', 'sources.md') -and $text -notmatch '(?m)^\s*- path:\s*') { $missingSources.Add([pscustomobject]@{ Path = $relative; Title = $title }) } foreach ($match in [regex]::Matches($text, '\[\[([^\]|#]+)(?:#[^\]|]+)?(?:\|[^\]]+)?\]\]')) { $target = $match.Groups[1].Value.Trim() if ($inbound.ContainsKey($target)) { $inbound[$target]++ } } } $orphans = @($pages | Where-Object { $_.Name -notin @('INDEX.md', 'Glossary.md', 'sources.md') -and $inbound[$_.BaseName] -eq 0 } | ForEach-Object { [pscustomobject]@{ Path = $_.FullName.Substring($Root.Length + 1).Replace('\', '/'); Title = $_.BaseName } }) $glossary = Join-Path $Wiki 'Glossary.md' $duplicateTerms = [Collections.Generic.List[string]]::new() if (Test-Path -LiteralPath $glossary -PathType Leaf) { $termCounts = @{} foreach ($match in [regex]::Matches([IO.File]::ReadAllText($glossary), '(?m)^###\s+(.+)$')) { $term = $match.Groups[1].Value.Trim() if ($termCounts.ContainsKey($term)) { $termCounts[$term]++ } else { $termCounts[$term] = 1 } } foreach ($term in ($termCounts.Keys | Sort-Object)) { if ($termCounts[$term] -gt 1) { $duplicateTerms.Add("$term($($termCounts[$term]) 次)") } } } $indexDuplicates = [Collections.Generic.List[string]]::new() $indexPath = Join-Path $Wiki 'INDEX.md' if (Test-Path -LiteralPath $indexPath -PathType Leaf) { $indexCounts = @{} foreach ($match in [regex]::Matches([IO.File]::ReadAllText($indexPath), '\[\[([^\]|#]+)')) { $target = $match.Groups[1].Value.Trim() if ($indexCounts.ContainsKey($target)) { $indexCounts[$target]++ } else { $indexCounts[$target] = 1 } } foreach ($target in ($indexCounts.Keys | Sort-Object)) { if ($indexCounts[$target] -gt 1) { $indexDuplicates.Add("$target($($indexCounts[$target]) 次)") } } } $today = Get-Date -Format 'yyyy-MM-dd' $sb = [Text.StringBuilder]::new() [void]$sb.AppendLine('---') [void]$sb.AppendLine('title: "Wiki 自我反思报告"') [void]$sb.AppendLine("generated: $today") [void]$sb.AppendLine('type: review') [void]$sb.AppendLine('status: generated') [void]$sb.AppendLine('---') [void]$sb.AppendLine('') [void]$sb.AppendLine('# Wiki 自我反思报告') [void]$sb.AppendLine('') [void]$sb.AppendLine('本报告由 `tools/wiki-reflect.ps1` 生成,只读扫描 Wiki 并覆盖写入本文件。它提出审阅任务,不自动修改知识正文。') [void]$sb.AppendLine('') [void]$sb.AppendLine("生成日期:$today;页面数:$($pages.Count)") [void]$sb.AppendLine('') [void]$sb.AppendLine('## 反思规则') [void]$sb.AppendLine('') [void]$sb.AppendLine('1. **保留价值**:页面是否有独立检索价值,是否保留了公式、单位、机制、失败模式和证据边界。') [void]$sb.AppendLine('2. **职责清晰**:页面是否明确自己是总览、概念、实践、案例还是来源摘编,是否与相邻页面重复。') [void]$sb.AppendLine('3. **证据可靠**:低置信度、争议、历史规则和来源方主张是否明确标注,是否仍有可访问 raw 来源。') [void]$sb.AppendLine('4. **可发现性**:页面是否进入 INDEX,是否有入链,术语和导航是否重复。') [void]$sb.AppendLine('5. **可维护性**:超长页面是否真的需要拆分,还是应保留为完整案例并增加专题入口。') [void]$sb.AppendLine('') function Add-Section([string]$Heading, [string[]]$Rows, [string]$Empty = '无') { [void]$sb.AppendLine("## $Heading") [void]$sb.AppendLine('') if ($Rows.Count -eq 0) { [void]$sb.AppendLine($Empty) } else { foreach ($row in $Rows) { [void]$sb.AppendLine("- $row") } } [void]$sb.AppendLine('') } $lowRows = @($lowConfidence | ForEach-Object { ('`{0}`:{1};建议复核主张、样本和失效条件' -f $_.Path, $_.Title) }) $contestedRows = @($contested | ForEach-Object { ('`{0}`:{1};建议确认来源冲突、版本日期和当前适用性' -f $_.Path, $_.Title) }) $longRows = @($longPages | Sort-Object Lines -Descending | ForEach-Object { ('`{0}`:{1} 行;先判断是否应按职责拆分,不以行数为唯一删除依据' -f $_.Path, $_.Lines) }) $roleRows = @($missingRole | ForEach-Object { ('`{0}`:建议补充页面负责/不负责内容及深入阅读入口' -f $_.Path) }) $sourceRows = @($missingSources | ForEach-Object { ('`{0}`:确认它是否为无需原始来源的导航、Q&A 或方法页;否则补充来源' -f $_.Path) }) $orphanRows = @($orphans | ForEach-Object { ('`{0}`:没有被其他 Wiki 页面链接;确认是否应由 INDEX 或主题入口挂载' -f $_.Path) }) Add-Section '低置信度页面' $lowRows Add-Section '争议或历史边界页面' $contestedRows Add-Section '超长页面' $longRows Add-Section '缺少页面定位区块' $roleRows Add-Section '缺少 raw_sources' $sourceRows Add-Section '可能的 Wiki 孤岛' $orphanRows Add-Section '术语重复' @($duplicateTerms) Add-Section 'INDEX 重复入口' @($indexDuplicates) [void]$sb.AppendLine('## 建议执行顺序') [void]$sb.AppendLine('') [void]$sb.AppendLine('1. 先处理 ERROR 和缺失来源,不要先改写低风险页面。') [void]$sb.AppendLine('2. 对低置信度/争议页面逐页确认“保留、降级、补证据、归档”四选一。') [void]$sb.AppendLine('3. 对职责缺失页面补充定位,而不是直接合并页面。') [void]$sb.AppendLine('4. 只有重复定义和重复导航确认后才去重。') [void]$sb.AppendLine('5. 运行 `pwsh -File .\\tools\\wiki-audit.ps1` 验证,再在 `wiki/compile.log` 记录实际变更。') $parent = Split-Path -Parent $Output New-Item -ItemType Directory -Path $parent -Force | Out-Null [IO.File]::WriteAllText($Output, $sb.ToString(), [Text.UTF8Encoding]::new($false)) Write-Output "REPORT $Output" Write-Output "FINDINGS low_confidence=$($lowConfidence.Count) contested=$($contested.Count) long_pages=$($longPages.Count) missing_role=$($missingRole.Count) missing_sources=$($missingSources.Count) orphans=$($orphans.Count) duplicate_terms=$($duplicateTerms.Count) duplicate_index=$($indexDuplicates.Count)" $findingCount = $lowConfidence.Count + $contested.Count + $longPages.Count + $missingRole.Count + $missingSources.Count + $orphans.Count + $duplicateTerms.Count + $indexDuplicates.Count if ($FailOnFindings -and $findingCount -gt 0) { exit 1 } exit 0