201 lines
7.9 KiB
PowerShell
201 lines
7.9 KiB
PowerShell
[CmdletBinding()]
|
||
param(
|
||
[string]$Root = (Join-Path $PSScriptRoot '..'),
|
||
[switch]$FailOnWarning,
|
||
[string[]]$LongPageAllowlist = @(
|
||
'Glossary.md',
|
||
'INDEX.md',
|
||
'concepts/Harness-Engineering-Complete-Guide.md',
|
||
'concepts/Meta-Harness.md',
|
||
'practices/OpenAI-Codex-Harness-Engineering.md',
|
||
'queries/What-is-Harness-Engineering-in-Simple-Terms.md'
|
||
)
|
||
)
|
||
|
||
$ErrorActionPreference = 'Stop'
|
||
$Root = [IO.Path]::GetFullPath($Root)
|
||
$Wiki = Join-Path $Root 'wiki'
|
||
$Index = Join-Path $Wiki 'INDEX.md'
|
||
$State = Join-Path $Wiki 'compile-results.tsv'
|
||
|
||
$errors = [Collections.Generic.List[string]]::new()
|
||
$warnings = [Collections.Generic.List[string]]::new()
|
||
$infos = [Collections.Generic.List[string]]::new()
|
||
$longPageAllowlistSet = @{}
|
||
foreach ($relativePath in $LongPageAllowlist) {
|
||
$normalizedPath = $relativePath.Replace('\', '/')
|
||
$longPageAllowlistSet[$normalizedPath] = $true
|
||
}
|
||
|
||
function Add-Error([string]$Message) { $script:errors.Add("ERROR $Message") }
|
||
function Add-Warning([string]$Message) { $script:warnings.Add("WARNING $Message") }
|
||
function Add-Info([string]$Message) { $script:infos.Add("INFO $Message") }
|
||
|
||
if (-not (Test-Path -LiteralPath $Wiki -PathType Container)) {
|
||
Add-Error "wiki directory does not exist: $Wiki"
|
||
}
|
||
if (-not (Test-Path -LiteralPath $Index -PathType Leaf)) {
|
||
Add-Error "wiki index does not exist: $Index"
|
||
}
|
||
|
||
$pages = @()
|
||
if (Test-Path -LiteralPath $Wiki -PathType Container) {
|
||
$pages = @(Get-ChildItem -LiteralPath $Wiki -Recurse -File -Filter '*.md' |
|
||
Where-Object { $_.DirectoryName -ne (Join-Path $Wiki '_meta') })
|
||
}
|
||
|
||
$byBaseName = @{}
|
||
foreach ($page in $pages) {
|
||
if ($byBaseName.ContainsKey($page.BaseName)) {
|
||
Add-Error "duplicate Wiki basename: $($page.BaseName)"
|
||
} else {
|
||
$byBaseName[$page.BaseName] = $page.FullName
|
||
}
|
||
|
||
}
|
||
|
||
foreach ($page in $pages) {
|
||
$firstLine = Get-Content -LiteralPath $page.FullName -TotalCount 1
|
||
if ($firstLine -ne '---') {
|
||
Add-Error "missing frontmatter: $($page.FullName)"
|
||
}
|
||
|
||
$lineCount = @(Get-Content -LiteralPath $page.FullName).Count
|
||
if ($lineCount -gt 200) {
|
||
$relativeWikiPath = [IO.Path]::GetRelativePath($Wiki, $page.FullName).Replace('\', '/')
|
||
if (-not $longPageAllowlistSet.ContainsKey($relativeWikiPath)) {
|
||
Add-Warning "long page ($lineCount lines): $($page.FullName)"
|
||
}
|
||
}
|
||
|
||
$text = [IO.File]::ReadAllText($page.FullName)
|
||
|
||
# frontmatter 必填字段(schema 第 3 节:title/type/last_updated/tags/confidence;
|
||
# 导航页整体豁免(走导航 frontmatter 约定),raw_sources 另对 query 页豁免)
|
||
$fmMatch = [regex]::Match($text, '(?s)^---\r?\n(.*?)\r?\n---')
|
||
if ($fmMatch.Success) {
|
||
$fmText = $fmMatch.Groups[1].Value
|
||
$isNavPage = $page.Name -in @('INDEX.md', 'Glossary.md', 'sources.md', '期货研究资料目录.md', '期货资料来源与证据边界.md')
|
||
if (-not $isNavPage) {
|
||
foreach ($field in @('title', 'type', 'last_updated', 'tags', 'confidence')) {
|
||
if (-not [regex]::IsMatch($fmText, "(?m)^$field\s*:")) {
|
||
Add-Warning "missing frontmatter field '$field' in $($page.FullName)"
|
||
}
|
||
}
|
||
}
|
||
$isQueryPage = [regex]::IsMatch($fmText, '(?m)^type:\s*query')
|
||
if (-not $isQueryPage -and -not $isNavPage) {
|
||
if (-not [regex]::IsMatch($fmText, '(?m)^raw_sources\s*:')) {
|
||
Add-Warning "missing raw_sources field in $($page.FullName)"
|
||
}
|
||
}
|
||
}
|
||
|
||
foreach ($match in [regex]::Matches($text, '\[\[([^\]|#]+)(?:#[^\]|]+)?(?:\|[^\]]+)?\]\]')) {
|
||
$target = $match.Groups[1].Value.Trim()
|
||
if (-not $byBaseName.ContainsKey($target)) {
|
||
Add-Error "broken Wiki link in $($page.FullName): $target"
|
||
}
|
||
}
|
||
|
||
foreach ($match in [regex]::Matches($text, '!\[[^\]]*\]\(([^)]+)\)')) {
|
||
$reference = $match.Groups[1].Value.Trim()
|
||
if ($reference -match '^https?://' -or $reference -match '^data:') {
|
||
continue
|
||
}
|
||
$candidate = [IO.Path]::GetFullPath([IO.Path]::Combine($page.DirectoryName, $reference))
|
||
if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) {
|
||
Add-Error "missing image in $($page.FullName): $reference"
|
||
}
|
||
}
|
||
|
||
foreach ($match in [regex]::Matches($text, '(?m)^\s*- path:\s*["'']?([^"''\r\n]+)')) {
|
||
$sourcePath = $match.Groups[1].Value.Trim()
|
||
if ($sourcePath -match '^https?://') {
|
||
continue
|
||
}
|
||
$candidate = Join-Path $Root $sourcePath
|
||
if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) {
|
||
Add-Warning "missing raw_sources path in $($page.FullName): $sourcePath"
|
||
}
|
||
}
|
||
}
|
||
|
||
if (Test-Path -LiteralPath $Index -PathType Leaf) {
|
||
$indexText = [IO.File]::ReadAllText($Index)
|
||
foreach ($page in ($pages | Where-Object { $_.Name -notin @('INDEX.md', 'Glossary.md', 'sources.md') })) {
|
||
if ($indexText -notmatch [regex]::Escape($page.BaseName)) {
|
||
Add-Error "page is not indexed by INDEX.md: $($page.FullName)"
|
||
}
|
||
}
|
||
}
|
||
|
||
if (Test-Path -LiteralPath $State -PathType Leaf) {
|
||
try {
|
||
$rows = @(Import-Csv -LiteralPath $State -Delimiter ([char]9))
|
||
$seenPaths = @{}
|
||
foreach ($row in $rows) {
|
||
if ([string]::IsNullOrWhiteSpace($row.raw_path) -or [string]::IsNullOrWhiteSpace($row.hash)) {
|
||
Add-Error "compile-results.tsv contains an incomplete row"
|
||
continue
|
||
}
|
||
if ($seenPaths.ContainsKey($row.raw_path)) {
|
||
Add-Error "duplicate compile-results raw_path: $($row.raw_path)"
|
||
}
|
||
$seenPaths[$row.raw_path] = $true
|
||
if ($row.hash -match 'initial|pending|to-be-computed|placeholder') {
|
||
Add-Error "placeholder hash in compile-results.tsv: $($row.raw_path)"
|
||
}
|
||
$candidate = Join-Path $Root $row.raw_path
|
||
if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) {
|
||
Add-Warning "compile row points to missing raw file: $($row.raw_path)"
|
||
}
|
||
}
|
||
Add-Info "compile rows: $($rows.Count)"
|
||
} catch {
|
||
Add-Error "compile-results.tsv cannot be parsed: $($_.Exception.Message)"
|
||
}
|
||
} else {
|
||
Add-Error "compile-results.tsv does not exist: $State"
|
||
}
|
||
|
||
# 月度回顾页积累提醒(schema 第 7 节时序内容约定:满 3 页应归档最早为历史层)
|
||
$reviewPages = @($pages | Where-Object { $_.BaseName -match '20\d{2}年\d{1,2}月市场回顾$' })
|
||
if ($reviewPages.Count -ge 3) {
|
||
$reviewNames = ($reviewPages | Sort-Object Name | ForEach-Object { $_.BaseName }) -join '、'
|
||
Add-Warning "月度回顾页已积累 $($reviewPages.Count) 页($reviewNames);按 schema 时序内容约定应归档最早页面为历史层(status: historical)并由季度/年度综述承接"
|
||
}
|
||
|
||
$allTextFiles = @(Get-ChildItem -LiteralPath $Root -Recurse -File -Include '*.md', '*.tsv', '*.log' -ErrorAction SilentlyContinue)
|
||
$deletedSourceRefs = @()
|
||
foreach ($file in $allTextFiles) {
|
||
$text = [IO.File]::ReadAllText($file.FullName)
|
||
if ($text -match 'MiniMax-M27-Self-Evolution|raw/MiniMax M2\.7|MiniMax M2\.7 开启模型的自我进化') {
|
||
if ($text -notmatch '用户已删除|同步移除|delete \|') {
|
||
$deletedSourceRefs += $file.FullName
|
||
}
|
||
}
|
||
}
|
||
foreach ($file in ($deletedSourceRefs | Sort-Object -Unique)) {
|
||
Add-Warning "possible stale MiniMax source/page reference: $file"
|
||
}
|
||
|
||
$linkCount = 0
|
||
foreach ($page in $pages) {
|
||
$linkCount += ([regex]::Matches([IO.File]::ReadAllText($page.FullName), '\[\[')).Count
|
||
}
|
||
Add-Info "Wiki Markdown pages: $($pages.Count)"
|
||
Add-Info "Wiki links scanned: $linkCount"
|
||
Add-Info "long-page allowlist entries: $($longPageAllowlistSet.Count)"
|
||
Add-Info "errors: $($errors.Count)"
|
||
Add-Info "warnings: $($warnings.Count)"
|
||
|
||
$errors
|
||
$warnings
|
||
$infos
|
||
|
||
if ($errors.Count -gt 0 -or ($FailOnWarning -and $warnings.Count -gt 0)) {
|
||
exit 1
|
||
}
|
||
exit 0
|