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{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)" # 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') } # The writing now extends through chapter 68; allow subsequent chapters without # freezing progress at today's count, while rejecting gaps and duplicate IDs. $firstVolumeIds = @($novelFiles | Where-Object { $_.Directory.Name -eq '第一卷' } | ForEach-Object { [int]$_.Name.Substring(0, 2) } | 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.Substring(0, 2) } | 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.Substring(0, 2) } | 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.Substring(0, 2) } | 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') } 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