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

387 lines
24 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
if ([string]::IsNullOrEmpty($source)) { continue }
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 23) { $failures.Add("Expected 23 timeline rows, got $timelineRows") }
# V03 chronology must keep early achievement, recruitment, conflict and exile in order.
$v3Timeline = Read-ProjectText '03_故事大纲/主线总年表.md'
$v3Checks = @(
@{ Year = 868; Terms = @('幕纹石', '修具师', '尚未做隔魔农田') },
@{ Year = 869; Terms = @('多年', '尚无正式冲突') },
@{ Year = 870; Terms = @('13岁艾达', '受监督寻伴', '推迟接纳') },
@{ Year = 871; Terms = @('第二农事季', '深秋仓塌') },
@{ Year = 872; Terms = @('黑腹热', '方法收编') },
@{ Year = 873; Terms = @('神判', '担保撤销') },
@{ Year = 874; Terms = @('洛恩', '祝福挡击', '自愿越境') },
@{ Year = 875; Terms = @('12000平方米', '800千克', '购粮和魔兽肉') },
@{ Year = 880; Terms = @('第二次', '推迟接纳') }
)
foreach ($check in $v3Checks) {
$row = [regex]::Match($v3Timeline, "(?m)^\| [+-]?\d+ \| $($check.Year) \|.*$").Value
foreach ($term in $check.Terms) {
if (-not $row.Contains($term)) { $failures.Add("V03 timeline fact missing in $($check.Year): $term") }
}
}
$v3Arc = Read-ProjectText '03_故事大纲/前期因果链_V03_从受祝福者到越境者.md'
$v3Rule = Read-ProjectText '02_世界设定/女神祝福与寻伴仪.md'
foreach ($term in @('865—866', '867—869', '870春', '15岁', '卢西安14岁', '艾达13岁', '卡莎')) {
if (-not $v3Arc.Contains($term)) { $failures.Add("V03 early arc missing: $term") }
}
foreach ($term in @('贤者之席', '没有隐藏回路', '祝福不是常亮光环', '教会能保管')) {
if (-not $v3Rule.Contains($term)) { $failures.Add("V03 blessing rule missing: $term") }
}
Write-Output "V03 chronology: $($v3Checks.Count) anchors; early arc and blessing rules checked"
# Verify the formal material setting, chronology and protagonist's knowledge limits.
$continuityChecks = @(
@{ Path = '03_故事大纲/魔族落脚与魔王结缘_V03补充.md'; Terms = @('托克', '约六周', '874年晚秋', '875年秋', '唯一的救赎之光') },
@{ Path = '02_世界设定/女神祝福与寻伴仪.md'; Terms = @('魔族高阶种族', '魔王维奥拉', '不必托克替她鉴定') },
@{ Path = '02_世界设定/魔族食物与粮食来源.md'; Terms = @('经中转购入的粮食', '魔兽肉', '矿脉雄厚', '教会知道', '未使大陆自给') },
@{ Path = '02_世界设定/幕纹石与隔魔农业.md'; Terms = @('正式设定 V1.0', '868年', '适魔,不是彻底无魔', '本地专家', '不是吸能电池', '不能抵挡集中的战斗魔流', '黛拉', '瑟恩', '诺尔') },
@{ Path = '01_角色卡/男主.md'; Terms = @('社畜', '不愿往管理岗升', '训练人工智能', '不愿靠压迫别人', '广而不精', '无回路') }
)
foreach ($check in $continuityChecks) {
$document = Read-ProjectText $check.Path
foreach ($term in $check.Terms) {
if (-not $document.Contains($term)) { $failures.Add("Continuity fact missing in $($check.Path): $term") }
}
}
Write-Output "Landing, food, formal agriculture and protagonist: $($continuityChecks.Count) documents checked"
$agriculture = Read-ProjectText '02_世界设定/幕纹石与隔魔农业.md'
$foodLedger = [regex]::Match($agriculture, '首季总干粮(\d+)千克,其中清选及保管损耗(\d+)千克,可用(\d+)千克;留种(\d+)千克,实际可分配食用(\d+)千克')
if (-not $foodLedger.Success) {
$failures.Add('Formal first-harvest ledger missing')
} else {
$gross = [int]$foodLedger.Groups[1].Value
$loss = [int]$foodLedger.Groups[2].Value
$usable = [int]$foodLedger.Groups[3].Value
$seed = [int]$foodLedger.Groups[4].Value
$food = [int]$foodLedger.Groups[5].Value
if (($gross - $loss) -ne $usable -or ($usable - $seed) -ne $food) {
$failures.Add('First-harvest quantities do not balance')
}
}
$fieldArea = [regex]::Match($agriculture, '四块各(\d+)平方米的试田,合计(\d+)平方米')
if (-not $fieldArea.Success -or 4 * [int]$fieldArea.Groups[1].Value -ne [int]$fieldArea.Groups[2].Value) {
$failures.Add('Four-field area does not balance')
}
Write-Output 'First-harvest and four-field arithmetic checked (fictional setting, not real yield validation)'
$scriptText = Read-ProjectText '06_AI动漫制作/01_改编开发/03_动画剧本/S01E01_留给你的那一碗_V05.fountain'
$sceneIds = @([regex]::Matches($scriptText, '(?m)^(?:INT\.|EXT\.|INT\./EXT\.)[^\r\n]*#(01-\d{2})#\s*$') | ForEach-Object { $_.Groups[1].Value })
$expectedSceneIds = @(1..25 | ForEach-Object { '01-{0:D2}' -f $_ })
if (($sceneIds -join ',') -ne ($expectedSceneIds -join ',')) { $failures.Add('V05 script scenes must be ordered 01-01 through 01-25') }
$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('INT. ')
$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 -Recurse | Where-Object { $_.Name -match '^\d+_' })
$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)"
# The demon taxonomy distinguishes physiology, culture and political membership.
# Compare its age summary against the existing source table, not duplicated constants.
$taxonomyText = Read-ProjectText '02_世界设定/魔族种族分类与生活图谱.md'
$taxonRows = @([regex]::Matches($taxonomyText, '(?m)^\| (M\d{2}) \| ([^|]+) \| ([^|]+) \| (\d+)岁 \| (\d+)—(\d+)年 \|'))
$branchRows = @([regex]::Matches($taxonomyText, '(?m)^\| (M\d{2}-[A-Z]) \| ([^|]+) \| ([^|]+) \|'))
$expectedTaxa = @(1..10 | ForEach-Object { 'M{0:D2}' -f $_ })
if (($taxonRows | ForEach-Object { $_.Groups[1].Value }) -join ',' -ne ($expectedTaxa -join ',')) {
$failures.Add('Demon taxonomy must contain ordered M01 through M10')
}
if ($branchRows.Count -ne 33 -or @($branchRows | ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique).Count -ne 33) {
$failures.Add('Expected 33 unique demon branch/culture IDs')
}
$mappedSpecies = @()
foreach ($row in $taxonRows) {
$taxonId = $row.Groups[1].Value
$ageName = $row.Groups[3].Value
$mappedSpecies += $ageName
$sourceRows = @($speciesRows | Where-Object { $_.Groups[1].Value -eq $ageName })
if ($sourceRows.Count -ne 1) {
$failures.Add("Demon taxon lacks a unique age source: $taxonId / $ageName")
} else {
$ageSource = $sourceRows[0]
foreach ($group in 4..6) {
if ($row.Groups[$group].Value -ne $ageSource.Groups[$group].Value) {
$failures.Add("Demon taxon age mismatch: $taxonId / group $group")
}
}
}
$lastBranch = if ($taxonId -eq 'M03') { 5 } else { 2 }
$expectedBranches = @(0..$lastBranch | ForEach-Object { "$taxonId-$([char](65 + $_))" })
$actualBranches = @($branchRows | Where-Object { $_.Groups[1].Value.StartsWith("$taxonId-") } | ForEach-Object { $_.Groups[1].Value })
if (($actualBranches -join ',') -ne ($expectedBranches -join ',')) {
$failures.Add("Demon branch coverage mismatch: $taxonId")
}
}
$expectedAgeNames = @($speciesRows | Where-Object { $_.Groups[1].Value -notin @('人类', '矮人', '精灵') } | ForEach-Object { $_.Groups[1].Value })
if (($mappedSpecies | Sort-Object) -join ',' -ne (($expectedAgeNames | Sort-Object) -join ',')) {
$failures.Add('Demon taxonomy must map all ten existing age groups exactly once')
}
Write-Output "Demon taxonomy: $($taxonRows.Count) groups; $($branchRows.Count) branches/cultures; age-source mapping checked"
function Year-Index([string]$yearText) {
if ($yearText -match '^历前(\d+)$') { return 1 - [int]$Matches[1] }
return [int]$yearText
}
$ageText = Read-ProjectText '01_角色卡/主要角色年龄表.md'
$characterRows = @([regex]::Matches($ageText, '(?m)^\| ([^|]+) \| ([^|]+) \| (历前\d+|\d+) \| (\d+) \|\r?$'))
if ($characterRows.Count -ne 24) { $failures.Add('Expected 24 named humanoid age rows') }
foreach ($row in $characterRows) {
$name = $row.Groups[1].Value
$species = $row.Groups[2].Value
$age = [int]$row.Groups[4].Value
if ($age -ne (874 - (Year-Index $row.Groups[3].Value))) {
$failures.Add("Character birth/age mismatch: $name")
}
if (-not $speciesAdults.ContainsKey($species)) {
$failures.Add("Unknown age species for: $name")
} elseif ($name -ne '艾达·梅洛' -and $age -lt $speciesAdults[$species]) {
$failures.Add("Unexpected underage character: $name")
}
}
$biography = Read-ProjectText '01_角色卡/人物履历与代际.md'
$careerSection = ($biography -split '## 家庭锚点', 2)[0]
$careerRows = @([regex]::Matches($careerSection, '(?m)^\| ([^|]+) \| (历前\d+|\d+) \| (\d+) \| (历前\d+|\d+) \| ([^|]+) \| (\d+)[^|]+ \| (\d+) \|\r?$'))
if ($careerRows.Count -ne 11) { $failures.Add('Expected 11 career chronology rows') }
foreach ($row in $careerRows) {
$name = $row.Groups[1].Value
$birth = Year-Index $row.Groups[2].Value
$adultYear = Year-Index $row.Groups[4].Value
$careerStart = [regex]::Match($row.Groups[5].Value, '^(历前\d+|\d+)').Value
$roleStart = [int]$row.Groups[6].Value
if ((874 - $birth) -ne [int]$row.Groups[3].Value -or (874 - $roleStart) -ne [int]$row.Groups[7].Value -or (Year-Index $careerStart) -lt $adultYear -or $roleStart -lt $adultYear) {
$failures.Add("Career arithmetic mismatch: $name")
}
$match = @($characterRows | Where-Object { $_.Groups[1].Value.StartsWith($name) })
if ($match.Count -ne 1 -or ($adultYear - $birth) -ne $speciesAdults[$match[0].Groups[2].Value] -or $row.Groups[3].Value -ne $match[0].Groups[4].Value) {
$failures.Add("Career/adult source mismatch: $name")
}
}
$cycleRows = @([regex]::Matches($biography, '(?m)^\| (874|883|884|887|902) \| (\d+) \| (\d+) \| (\d+) \|\r?$'))
if ($cycleRows.Count -ne 5) { $failures.Add('Expected five Nefra cycle anchors') }
foreach ($row in $cycleRows) {
$age = [int]$row.Groups[2].Value
$cycle = [math]::Floor(($age - 8) / 9) + 1
$stage = [math]::Floor(($cycle - 1) / 3) + 1
if ($age -ne ([int]$row.Groups[1].Value - 788) -or $cycle -ne [int]$row.Groups[3].Value -or $stage -ne [int]$row.Groups[4].Value) {
$failures.Add("Nefra cycle mismatch: $($row.Value)")
}
}
$chapterOutline = Read-ProjectText '05_小说创作/02_章节大纲/第一卷_章节大纲.md'
$chapterIds = @([regex]::Matches($chapterOutline, '(?m)^\| (\d+) \|') | ForEach-Object { [int]$_.Groups[1].Value })
if (($chapterIds -join ',') -ne ((1..24) -join ',')) { $failures.Add('Expected 24 ordered V03 chapter rows') }
$episodeText = Read-ProjectText '06_AI动漫制作/01_改编开发/02_分集大纲/第一季_12集分集大纲.md'
$episodeIds = @([regex]::Matches($episodeText, '(?m)^\| (\d+) \|') | ForEach-Object { [int]$_.Groups[1].Value })
if (($episodeIds -join ',') -ne ((1..12) -join ',')) { $failures.Add('Expected 12 ordered V03 episode rows') }
# Parse complete numeric prefixes beyond chapter 99. Allow further chapters
# without freezing today's total, while rejecting gaps and duplicate IDs.
$firstVolumeIds = @($novelFiles | Where-Object { $_.Directory.Name -eq '第一卷' } | ForEach-Object { [int]($_.Name -split '_', 2)[0] } | Sort-Object)
if (($firstVolumeIds -join ',') -ne ((0..24) -join ',')) {
$failures.Add('Volume one must contain the prologue and chapters 1-24')
}
$draftIds = @($novelFiles | ForEach-Object { [int]($_.Name -split '_', 2)[0] } | Sort-Object)
if ($draftIds.Count -lt 69 -or ($draftIds -join ',') -ne ((0..($draftIds.Count - 1)) -join ',')) {
$failures.Add('Novel draft IDs must be unique, continuous from 00 and reach at least chapter 68')
}
$secondVolumeIds = @($novelFiles | Where-Object { $_.Directory.Name -eq '第二卷' } | ForEach-Object { [int]($_.Name -split '_', 2)[0] } | Sort-Object)
if (($secondVolumeIds -join ',') -ne ((25..48) -join ',')) {
$failures.Add('Volume two must contain chapters 25-48')
}
$thirdVolumeIds = @($novelFiles | Where-Object { $_.Directory.Name -eq '第三卷' } | ForEach-Object { [int]($_.Name -split '_', 2)[0] } | Sort-Object)
if (($thirdVolumeIds -join ',') -ne ((49..68) -join ',')) {
$failures.Add('Volume three must contain chapters 49-68')
}
$thirdOutline = Read-ProjectText '05_小说创作/02_章节大纲/第三卷_章节大纲.md'
$thirdOutlineIds = @([regex]::Matches($thirdOutline, '(?m)^\| (\d+) \|') | ForEach-Object { [int]$_.Groups[1].Value })
if (($thirdOutlineIds -join ',') -ne ($thirdVolumeIds -join ',')) {
$failures.Add('Volume three chapter outline and prose IDs differ')
}
$fourthVolumeIds = @($novelFiles | Where-Object { $_.Directory.Name -eq '第四卷' } | ForEach-Object { [int]($_.Name -split '_', 2)[0] } | Sort-Object)
if ($fourthVolumeIds.Count -gt 0) {
if (($fourthVolumeIds -join ',') -ne ((69..88) -join ',')) {
$failures.Add('Completed volume four must contain chapters 69-88')
}
$fourthOutline = Read-ProjectText '05_小说创作/02_章节大纲/第四卷_章节大纲.md'
$fourthOutlineIds = @([regex]::Matches($fourthOutline, '(?m)^\| (\d+) \|') | ForEach-Object { [int]$_.Groups[1].Value })
if (($fourthOutlineIds -join ',') -ne ($fourthVolumeIds -join ',')) {
$failures.Add('Volume four chapter outline and prose IDs differ')
}
}
$fifthVolumeIds = @($novelFiles | Where-Object { $_.Directory.Name -eq '第五卷' } | ForEach-Object { [int]($_.Name -split '_', 2)[0] } | Sort-Object)
if ($fifthVolumeIds.Count -gt 0) {
if (($fifthVolumeIds -join ',') -ne ((89..112) -join ',')) {
$failures.Add('Completed volume five must contain chapters 89-112')
}
$fifthOutline = Read-ProjectText '05_小说创作/02_章节大纲/第五卷_章节大纲.md'
$fifthOutlineIds = @([regex]::Matches($fifthOutline, '(?m)^\| (\d+) \|') | ForEach-Object { [int]$_.Groups[1].Value })
if (($fifthOutlineIds -join ',') -ne ($fifthVolumeIds -join ',')) {
$failures.Add('Volume five chapter outline and prose IDs differ')
}
}
$sixthVolumeIds = @($novelFiles | Where-Object { $_.Directory.Name -eq '第六卷' } | ForEach-Object { [int]($_.Name -split '_', 2)[0] } | Sort-Object)
if ($sixthVolumeIds.Count -gt 0) {
if (($sixthVolumeIds -join ',') -ne ((113..136) -join ',')) {
$failures.Add('Completed volume six must contain chapters 113-136')
}
$sixthOutline = Read-ProjectText '05_小说创作/02_章节大纲/第六卷_章节大纲.md'
$sixthOutlineIds = @([regex]::Matches($sixthOutline, '(?m)^\| (\d+) \|') | ForEach-Object { [int]$_.Groups[1].Value })
if (($sixthOutlineIds -join ',') -ne ($sixthVolumeIds -join ',')) {
$failures.Add('Volume six chapter outline and prose IDs differ')
}
}
$seventhVolumeIds = @($novelFiles | Where-Object { $_.Directory.Name -eq '第七卷' } | ForEach-Object { [int]($_.Name -split '_', 2)[0] } | Sort-Object)
if ($seventhVolumeIds.Count -gt 0) {
if (($seventhVolumeIds -join ',') -ne ((137..164) -join ',')) {
$failures.Add('Completed volume seven must contain chapters 137-164')
}
$seventhOutline = Read-ProjectText '05_小说创作/02_章节大纲/第七卷_章节大纲.md'
$seventhOutlineIds = @([regex]::Matches($seventhOutline, '(?m)^\| (\d+) \|') | ForEach-Object { [int]$_.Groups[1].Value })
if (($seventhOutlineIds -join ',') -ne ($seventhVolumeIds -join ',')) {
$failures.Add('Volume seven chapter outline and prose IDs differ')
}
}
$eighthVolumeIds = @($novelFiles | Where-Object { $_.Directory.Name -eq '第八卷' } | ForEach-Object { [int]($_.Name -split '_', 2)[0] } | Sort-Object)
if ($eighthVolumeIds.Count -gt 0) {
if (($eighthVolumeIds -join ',') -ne ((165..192) -join ',')) {
$failures.Add('Completed volume eight must contain chapters 165-192')
}
$eighthOutline = Read-ProjectText '05_小说创作/02_章节大纲/第八卷_章节大纲.md'
$eighthOutlineIds = @([regex]::Matches($eighthOutline, '(?m)^\| (\d+) \|') | ForEach-Object { [int]$_.Groups[1].Value })
if (($eighthOutlineIds -join ',') -ne ($eighthVolumeIds -join ',')) {
$failures.Add('Volume eight chapter outline and prose IDs differ')
}
}
foreach ($legacy in @(
'06_AI动漫制作/03_分镜与镜头/01_镜头表/S01E01_SC12_75秒试片镜头表.md',
'06_AI动漫制作/04_生成方案/01_图像提示词/S01E01_SC12_关键帧提示词.md',
'06_AI动漫制作/04_生成方案/02_视频提示词/S01E01_SC12_运动提示词.md'
)) {
if (-not (Read-ProjectText $legacy).Contains('历史V01技术包')) {
$failures.Add("Legacy storyboard lacks a history warning: $legacy")
}
}
Write-Output "Characters: $($characterRows.Count); careers: $($careerRows.Count); Nefra cycles: $($cycleRows.Count); chapters: $($chapterIds.Count); episodes: $($episodeIds.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