157 lines
8.3 KiB
PowerShell
157 lines
8.3 KiB
PowerShell
[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
|