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

218 lines
13 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") }
# 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"
# Keep confirmed relationship/resource facts separate from the proposed invention.
$continuityChecks = @(
@{ Path = '03_故事大纲/魔族落脚与魔王结缘_V03补充.md'; Terms = @('托克', '约六周', '874年晚秋', '875年秋', '唯一的救赎之光') },
@{ Path = '02_世界设定/女神庇护与寻伴仪.md'; Terms = @('魔族高阶种族', '魔王维奥拉', '不必托克替她鉴定') },
@{ Path = '02_世界设定/魔族食物与粮食来源.md'; Terms = @('购买的粮食', '魔兽肉', '雄厚矿脉', '人类大陆稀有', '仍要购买粮食') },
@{ Path = '02_世界设定/防御矿石与隔魔农田_讨论方案.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 sources and proposal status: $($continuityChecks.Count) documents 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)"
# 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"
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