Files
the-silent-gods-hearthfire/tools/validate-project.ps1
T

135 lines
8.3 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
param()
$ErrorActionPreference = 'Stop'
$projectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$failures = [System.Collections.Generic.List[string]]::new()
function Read-ProjectText([string]$relativePath) {
Get-Content -LiteralPath (Join-Path $projectRoot $relativePath) -Raw -Encoding UTF8
}
# Validate file destinations, not external URLs or Markdown heading anchors.
$linkCount = 0
$markdownFiles = Get-ChildItem -LiteralPath $projectRoot -Filter '*.md' -File -Recurse |
Where-Object { $_.FullName -notmatch '[\\/]\.git[\\/]' }
foreach ($file in $markdownFiles) {
$source = Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8
foreach ($match in [regex]::Matches($source, '\[[^\]]*\]\(([^)]+)\)')) {
$target = $match.Groups[1].Value.Trim().Trim('<', '>')
if ($target -match '^(https?://|mailto:|codex:|#)' -or $target -match '[<>]') { continue }
$target = [uri]::UnescapeDataString(($target -split '#', 2)[0])
if (-not $target) { continue }
$linkCount++
if (-not (Test-Path -LiteralPath (Join-Path $file.DirectoryName $target))) {
$failures.Add("Missing link: $($file.FullName) -> $target")
}
}
}
$timelineRows = 0
foreach ($line in ((Read-ProjectText '03_故事大纲/主线总年表.md') -split '\r?\n')) {
if ($line -match '^\|\s*([+-]?\d+)\s*\|\s*(\d+)\s*\|[^|]+\|\s*(\d+)\s*\|\s*(\d+|未出生)\s*\|') {
$timelineRows++
$year = [int]$Matches[2]
if ([int]$Matches[1] -ne ($year - 874) -or [int]$Matches[3] -ne ($year - 855)) {
$failures.Add("Timeline mismatch: $line")
}
if ($Matches[4] -ne '未出生' -and [int]$Matches[4] -ne ($year - 856)) {
$failures.Add("Hero age mismatch: $line")
}
}
}
if ($timelineRows -ne 22) { $failures.Add("Expected 22 timeline rows, got $timelineRows") }
$scriptText = Read-ProjectText '06_AI动漫制作/01_改编开发/03_动画剧本/S01E01_停不下来的夜_V01.txt'
$sceneIds = @([regex]::Matches($scriptText, '(?m)^(01-\d{2})\s') | ForEach-Object { $_.Groups[1].Value })
$expectedSceneIds = @(1..16 | ForEach-Object { '01-{0:D2}' -f $_ })
if (($sceneIds -join ',') -ne ($expectedSceneIds -join ',')) { $failures.Add('Script scenes must be ordered 01-01 through 01-16') }
$outline = Read-ProjectText '06_AI动漫制作/01_改编开发/02_分集大纲/S01E01_分场大纲.md'
$sceneRows = @([regex]::Matches($outline, '(?m)^\| (01-\d{2}) \|[^|]+\| (\d+) \|'))
$sceneTotal = ($sceneRows | ForEach-Object { [int]$_.Groups[2].Value } | Measure-Object -Sum).Sum
if ($sceneTotal -ne 1290 -or (($sceneRows | ForEach-Object { $_.Groups[1].Value }) -join ',') -ne ($expectedSceneIds -join ',')) {
$failures.Add('Scene outline IDs or 1290-second budget mismatch')
}
$shotTable = Read-ProjectText '06_AI动漫制作/03_分镜与镜头/01_镜头表/S01E01_SC12_75秒试片镜头表.md'
$motionTable = Read-ProjectText '06_AI动漫制作/04_生成方案/02_视频提示词/S01E01_SC12_运动提示词.md'
$shots = @([regex]::Matches($shotTable, '(?m)^\| (SH\d{3}) \| (\d+)/(\d+) \|'))
$motion = @([regex]::Matches($motionTable, '(?m)^\| (SH\d{3}) \| (\d+) \|'))
$cumulative = 0
if ($shots.Count -ne 12 -or $motion.Count -ne 12) { $failures.Add('Expected 12 shot and motion rows') }
for ($index = 0; $index -lt $shots.Count; $index++) {
$shot = $shots[$index]
$cumulative += [int]$shot.Groups[2].Value
if ($shot.Groups[1].Value -ne ('SH{0:D3}' -f ($index + 1)) -or [int]$shot.Groups[3].Value -ne $cumulative) {
$failures.Add("Shot sequence or cumulative duration mismatch: $($shot.Value)")
}
if ($index -lt $motion.Count -and ($motion[$index].Groups[1].Value -ne $shot.Groups[1].Value -or $motion[$index].Groups[2].Value -ne $shot.Groups[2].Value)) {
$failures.Add("Motion mismatch at shot $($index + 1)")
}
}
if ($cumulative -ne 75) { $failures.Add("Expected 75 seconds, got $cumulative") }
$bodyStart = $scriptText.IndexOf('01-01 ')
$scriptChars = if ($bodyStart -ge 0) { [regex]::Replace($scriptText.Substring($bodyStart), '\s', '').Length } else { 0 }
$novelFiles = Get-ChildItem -LiteralPath (Join-Path $projectRoot '05_小说创作/03_正文/第一卷') -Filter '*.md' -File | Where-Object { $_.Name -match '^\d{2}_' }
$novelChars = ($novelFiles | ForEach-Object { [regex]::Replace((Get-Content -LiteralPath $_.FullName -Raw -Encoding UTF8), '\s', '').Length } | Measure-Object -Sum).Sum
$pngCount = @(Get-ChildItem -LiteralPath (Join-Path $projectRoot '06_AI动漫制作') -Filter '*.png' -File -Recurse).Count
# Species tables are author-defined fiction; check internal ranges and coverage only.
$speciesText = Read-ProjectText '02_世界设定/种族年龄与生命周期.md'
$speciesRows = @([regex]::Matches($speciesText, '(?m)^\| ([^|]+) \| (\d+)—(\d+)岁 \| (\d+)岁 \| (\d+)—(\d+)年 \| (\d+)岁 \| (\d+)岁 \|\r?$'))
$speciesAdults = @{}
if ($speciesRows.Count -ne 13) { $failures.Add('Expected 13 humanoid species age rows') }
foreach ($row in $speciesRows) {
$name = $row.Groups[1].Value
$matureMin = [int]$row.Groups[2].Value
$matureMax = [int]$row.Groups[3].Value
$adult = [int]$row.Groups[4].Value
$lifeMin = [int]$row.Groups[5].Value
$lifeMax = [int]$row.Groups[6].Value
$aging = [int]$row.Groups[7].Value
$rare = [int]$row.Groups[8].Value
if ($speciesAdults.ContainsKey($name)) { $failures.Add("Duplicate species: $name") }
$speciesAdults[$name] = $adult
if ($matureMin -gt $matureMax -or $adult -lt $matureMin -or $aging -le $adult -or $aging -gt $lifeMin -or $lifeMin -gt $lifeMax -or $lifeMax -gt $rare) {
$failures.Add("Invalid species age ranges: $name")
}
}
$lifeStages = @([regex]::Matches($speciesText, '(?m)^\| ([^|]+) \| 0—(\d+) \| (\d+)—(\d+) \| (\d+)—(\d+) \| (\d+)—(\d+) \| (\d+)起 \|'))
if ($lifeStages.Count -ne 13) { $failures.Add('Expected 13 humanoid life-stage rows') }
foreach ($row in $lifeStages) {
$name = $row.Groups[1].Value
$values = @(2..9 | ForEach-Object { [int]$row.Groups[$_].Value })
if (-not $speciesAdults.ContainsKey($name) -or $values[1] -ne $speciesAdults[$name] -or $values[0] -ge $values[1] -or $values[3] -ne ($values[2] + 1) -or $values[5] -ne ($values[4] + 1) -or $values[7] -ne ($values[6] + 1)) {
$failures.Add("Non-contiguous adult life stages: $name")
}
}
$familyRows = @([regex]::Matches($speciesText, '(?m)^\| ([^|]+) \| (\d+)—(\d+) \| (\d+)—(\d+)年 \| (\d+)—(\d+)年 \|'))
if ($familyRows.Count -ne 13) { $failures.Add('Expected 13 family-generation rows') }
foreach ($row in $familyRows) {
$name = $row.Groups[1].Value
if (-not $speciesAdults.ContainsKey($name) -or [int]$row.Groups[2].Value -lt $speciesAdults[$name] -or [int]$row.Groups[2].Value -gt [int]$row.Groups[3].Value -or [int]$row.Groups[4].Value -gt [int]$row.Groups[5].Value -or [int]$row.Groups[6].Value -gt [int]$row.Groups[7].Value) {
$failures.Add("Invalid family age ranges: $name")
}
}
$casteRows = @([regex]::Matches($speciesText, '(?m)^\| ([^|]+) \| (\d+)—(\d+)岁 \| (\d+)岁 \| (\d+)—(\d+)年 \| (\d+)岁 \| (\d+)岁 \| [^|]+ \|'))
if ($casteRows.Count -ne 6) { $failures.Add('Expected 6 insect functional-age rows') }
foreach ($row in $casteRows) {
if ([int]$row.Groups[2].Value -gt [int]$row.Groups[3].Value -or [int]$row.Groups[4].Value -lt [int]$row.Groups[2].Value -or [int]$row.Groups[7].Value -le [int]$row.Groups[4].Value -or [int]$row.Groups[7].Value -gt [int]$row.Groups[5].Value -or [int]$row.Groups[5].Value -gt [int]$row.Groups[6].Value -or [int]$row.Groups[6].Value -gt [int]$row.Groups[8].Value) {
$failures.Add("Invalid insect age ranges: $($row.Groups[1].Value)")
}
}
Write-Output "Species: $($speciesRows.Count); life stages: $($lifeStages.Count); family rows: $($familyRows.Count); insect castes: $($casteRows.Count)"
Write-Output "Links checked: $linkCount; timeline rows: $timelineRows; scenes: $($sceneIds.Count); scene budget: $sceneTotal sec"
Write-Output "Shots: $($shots.Count); shot budget: $cumulative sec; script body non-whitespace chars: $scriptChars"
Write-Output "Novel draft files: $($novelFiles.Count); draft non-whitespace chars including headings: $novelChars; PNG references: $pngCount"
if ($failures.Count -gt 0) {
$failures | ForEach-Object { Write-Output "FAIL: $_" }
exit 1
}
Write-Output 'PASS. This checks document structure, not artistic quality, medical facts, actual runtime or production readiness.'
exit 0