162 lines
9.9 KiB
PowerShell
162 lines
9.9 KiB
PowerShell
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") }
|
||
|
||
# V03 chronology must keep early achievement, recruitment, conflict and exile in order.
|
||
$v3Timeline = Read-ProjectText '03_故事大纲/主线总年表.md'
|
||
$v3Checks = @(
|
||
@{ Year = 869; Terms = @('多年', '尚无正式冲突') },
|
||
@{ Year = 870; Terms = @('13岁艾达', '受监督寻伴', '推迟接纳') },
|
||
@{ Year = 871; Terms = @('第二农事季', '深秋仓塌') },
|
||
@{ Year = 872; Terms = @('黑腹热', '方法收编') },
|
||
@{ Year = 873; Terms = @('神判', '担保撤销') },
|
||
@{ Year = 874; Terms = @('洛恩', '庇护挡击', '自愿越境') },
|
||
@{ 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"
|
||
|
||
$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
|