Publish only the Kalsa reader projection
This commit is contained in:
parent
f6ec266d09
commit
4db77bf2d0
6 changed files with 177 additions and 13 deletions
2
.github/workflows/deploy-quartz.yml
vendored
2
.github/workflows/deploy-quartz.yml
vendored
|
|
@ -19,6 +19,6 @@ jobs:
|
|||
deploy:
|
||||
uses: GameCult/GameCult-Quartz/.github/workflows/quartz-pages.yml@main
|
||||
with:
|
||||
content-dir: Kalsa
|
||||
content-dir: Kalsa/Public
|
||||
overlay-dir: site
|
||||
output-dir: quartz-site/public
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ if (-not $RepositoryRoot) {
|
|||
}
|
||||
$repoFull = [System.IO.Path]::GetFullPath($RepositoryRoot).TrimEnd('\', '/')
|
||||
if (-not $ContentRoot) {
|
||||
$ContentRoot = Join-Path $repoFull "Kalsa"
|
||||
$ContentRoot = Join-Path $repoFull "Kalsa\Public"
|
||||
}
|
||||
$contentFull = [System.IO.Path]::GetFullPath($ContentRoot).TrimEnd('\', '/')
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ if (-not (Test-Path -LiteralPath $repoFull -PathType Container)) {
|
|||
throw "Repository root not found: $repoFull"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $contentFull -PathType Container)) {
|
||||
throw "Canonical publication root not found: $contentFull"
|
||||
throw "Reader publication root not found: $contentFull"
|
||||
}
|
||||
if (-not $contentFull.StartsWith($repoFull + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "Publication root must remain inside the repository root"
|
||||
|
|
@ -47,8 +47,8 @@ if (-not $SkipEntrypointCheck) {
|
|||
}
|
||||
else {
|
||||
$launcherText = Get-Content -Raw -LiteralPath $launcher -Encoding utf8
|
||||
if ($launcherText -notmatch '(?s)--contentDir.{0,80}["'']Kalsa["'']') {
|
||||
$errors += "Quartz launcher does not bind --contentDir to Kalsa"
|
||||
if ($launcherText -notmatch '(?s)--contentDir.{0,80}["'']Kalsa/Public["'']') {
|
||||
$errors += "Quartz launcher does not bind --contentDir to Kalsa/Public"
|
||||
}
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $workflow -PathType Leaf)) {
|
||||
|
|
@ -56,8 +56,8 @@ if (-not $SkipEntrypointCheck) {
|
|||
}
|
||||
else {
|
||||
$workflowText = Get-Content -Raw -LiteralPath $workflow -Encoding utf8
|
||||
if ($workflowText -notmatch '(?m)^\s*content-dir:\s*Kalsa\s*$') {
|
||||
$errors += "Quartz deployment workflow does not bind content-dir to Kalsa"
|
||||
if ($workflowText -notmatch '(?m)^\s*content-dir:\s*Kalsa/Public\s*$') {
|
||||
$errors += "Quartz deployment workflow does not bind content-dir to Kalsa/Public"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -66,4 +66,4 @@ if ($errors.Count -gt 0) {
|
|||
throw "Publication-boundary verification failed:`n- $($errors -join "`n- ")"
|
||||
}
|
||||
|
||||
Write-Output "Publication boundary passed: only $contentFull is admitted as canonical site input."
|
||||
Write-Output "Publication boundary passed: only $contentFull is admitted as reader-site input."
|
||||
|
|
|
|||
100
scripts/lore/check-publication-output.ps1
Normal file
100
scripts/lore/check-publication-output.ps1
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
param(
|
||||
[string]$RepositoryRoot,
|
||||
[string]$ContentRoot,
|
||||
[string]$OutputRoot
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if (-not $RepositoryRoot) {
|
||||
$RepositoryRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
|
||||
}
|
||||
$repoFull = [System.IO.Path]::GetFullPath($RepositoryRoot).TrimEnd('\', '/')
|
||||
if (-not $ContentRoot) {
|
||||
$ContentRoot = Join-Path $repoFull "Kalsa\Public"
|
||||
}
|
||||
if (-not $OutputRoot) {
|
||||
$OutputRoot = Join-Path $repoFull "quartz-site\public"
|
||||
}
|
||||
$contentFull = [System.IO.Path]::GetFullPath($ContentRoot).TrimEnd('\', '/')
|
||||
$outputFull = [System.IO.Path]::GetFullPath($OutputRoot).TrimEnd('\', '/')
|
||||
|
||||
foreach ($root in @($contentFull, $outputFull)) {
|
||||
if (-not (Test-Path -LiteralPath $root -PathType Container)) {
|
||||
throw "Required publication surface not found: $root"
|
||||
}
|
||||
if (-not $root.StartsWith($repoFull + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "Publication surfaces must remain inside the repository root"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-SourceSlug([string]$path) {
|
||||
$relative = $path.Substring($contentFull.Length).TrimStart('\', '/') -replace '\\', '/'
|
||||
$withoutExtension = [System.IO.Path]::ChangeExtension($relative, $null).TrimEnd('.')
|
||||
return $withoutExtension -replace ' ', '-'
|
||||
}
|
||||
|
||||
function Get-Route([string]$sourceSlug) {
|
||||
if ($sourceSlug -eq 'index') { return '' }
|
||||
if ($sourceSlug.EndsWith('/index')) { return $sourceSlug.Substring(0, $sourceSlug.Length - 6) }
|
||||
return $sourceSlug
|
||||
}
|
||||
|
||||
function Compare-ExactSet([string]$label, [string[]]$expected, [string[]]$actual) {
|
||||
$expectedSet = @($expected | Sort-Object -Unique)
|
||||
$actualSet = @($actual | Sort-Object -Unique)
|
||||
$missing = @($expectedSet | Where-Object { $_ -notin $actualSet })
|
||||
$extra = @($actualSet | Where-Object { $_ -notin $expectedSet })
|
||||
if ($missing.Count -gt 0 -or $extra.Count -gt 0) {
|
||||
$parts = @()
|
||||
if ($missing.Count -gt 0) { $parts += "missing: $($missing -join ', ')" }
|
||||
if ($extra.Count -gt 0) { $parts += "unexpected: $($extra -join ', ')" }
|
||||
throw "$label does not match the reader source ($($parts -join '; '))"
|
||||
}
|
||||
}
|
||||
|
||||
$sourceSlugs = @(Get-ChildItem -LiteralPath $contentFull -Recurse -File -Filter '*.md' | ForEach-Object {
|
||||
Get-SourceSlug $_.FullName
|
||||
})
|
||||
$routes = @($sourceSlugs | ForEach-Object { Get-Route $_ })
|
||||
|
||||
$contentIndexPath = Join-Path $outputFull 'static\contentIndex.json'
|
||||
$sitemapPath = Join-Path $outputFull 'sitemap.xml'
|
||||
$rssPath = Join-Path $outputFull 'index.xml'
|
||||
foreach ($path in @($contentIndexPath, $sitemapPath, $rssPath)) {
|
||||
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
|
||||
throw "Generated publication artifact missing: $path"
|
||||
}
|
||||
}
|
||||
|
||||
$contentIndex = Get-Content -Raw -LiteralPath $contentIndexPath -Encoding utf8 | ConvertFrom-Json -AsHashtable
|
||||
Compare-ExactSet 'Search index routes' $sourceSlugs @($contentIndex.Keys)
|
||||
|
||||
$sitemapText = Get-Content -Raw -LiteralPath $sitemapPath -Encoding utf8
|
||||
$sitemapRoutes = @([regex]::Matches($sitemapText, '<loc>(?<url>[^<]+)</loc>') | ForEach-Object {
|
||||
[uri]::UnescapeDataString(([uri]$_.Groups['url'].Value).AbsolutePath.Trim('/'))
|
||||
})
|
||||
Compare-ExactSet 'Sitemap routes' $routes $sitemapRoutes
|
||||
|
||||
$rssText = Get-Content -Raw -LiteralPath $rssPath -Encoding utf8
|
||||
$rssRoutes = @([regex]::Matches($rssText, '(?s)<item>.*?<link>(?<url>[^<]+)</link>.*?</item>') | ForEach-Object {
|
||||
[uri]::UnescapeDataString(([uri]$_.Groups['url'].Value).AbsolutePath.Trim('/'))
|
||||
})
|
||||
Compare-ExactSet 'RSS routes' $routes $rssRoutes
|
||||
|
||||
$expectedHtml = @('404.html')
|
||||
foreach ($route in $routes) {
|
||||
$expectedHtml += if ($route) { "$route.html" } else { 'index.html' }
|
||||
if ($route -and $route.Contains('/')) {
|
||||
$segments = $route -split '/'
|
||||
for ($i = 1; $i -lt $segments.Count; $i += 1) {
|
||||
$expectedHtml += (($segments[0..($i - 1)] -join '/') + '/index.html')
|
||||
}
|
||||
}
|
||||
}
|
||||
$actualHtml = @(Get-ChildItem -LiteralPath $outputFull -Recurse -File -Filter '*.html' | ForEach-Object {
|
||||
$_.FullName.Substring($outputFull.Length).TrimStart('\', '/') -replace '\\', '/'
|
||||
})
|
||||
Compare-ExactSet 'Generated HTML routes' $expectedHtml $actualHtml
|
||||
|
||||
Write-Output "Publication output passed: $($sourceSlugs.Count) reader notes own search, sitemap, RSS, and HTML routes."
|
||||
|
|
@ -67,11 +67,21 @@ foreach ($note in $notes) {
|
|||
}
|
||||
$relativeCandidate = [System.IO.Path]::GetFullPath((Join-Path $note.DirectoryName ($noteTarget + '.md')))
|
||||
if (Test-Path -LiteralPath $relativeCandidate -PathType Leaf) { $resolved += $relativeCandidate }
|
||||
$stem = [System.IO.Path]::GetFileName($noteTarget).ToLowerInvariant()
|
||||
if ($noteByStem.ContainsKey($stem)) { $resolved += $noteByStem[$stem] }
|
||||
if ($noteTarget -notmatch '[/\\]') {
|
||||
$stem = [System.IO.Path]::GetFileName($noteTarget).ToLowerInvariant()
|
||||
if ($noteByStem.ContainsKey($stem)) { $resolved += $noteByStem[$stem] }
|
||||
}
|
||||
}
|
||||
$resolved = @($resolved | Sort-Object -Unique)
|
||||
$sourceRelative = $note.FullName.Substring($contentFull.Length).TrimStart('\', '/')
|
||||
$escaped = @($resolved | Where-Object {
|
||||
$resolvedFull = [System.IO.Path]::GetFullPath($_)
|
||||
-not ($resolvedFull.StartsWith($contentFull + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase))
|
||||
})
|
||||
if ($escaped.Count -gt 0) {
|
||||
$errors += "$sourceRelative -> [[$($match.Groups['target'].Value)]] escapes the selected content root"
|
||||
continue
|
||||
}
|
||||
if ($resolved.Count -eq 0) {
|
||||
$errors += "$sourceRelative -> [[$($match.Groups['target'].Value)]] is unresolved"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,31 @@ try {
|
|||
& (Join-Path $PSScriptRoot "check-publication-boundary.ps1") -RepositoryRoot $fixtureFull -ContentRoot $content -SkipEntrypointCheck
|
||||
& (Join-Path $PSScriptRoot "measure-depth.ps1") -ContentRoot $content -CompareRoot (Join-Path $fixtureFull "missing-comparison") | Out-Null
|
||||
|
||||
$output = Join-Path $fixtureFull "quartz-site\public"
|
||||
$static = Join-Path $output "static"
|
||||
New-Item -ItemType Directory -Path $static -Force | Out-Null
|
||||
Set-Content -LiteralPath (Join-Path $output "404.html") -Encoding utf8 -Value "<!doctype html><title>Not found</title>"
|
||||
Set-Content -LiteralPath (Join-Path $output "index.html") -Encoding utf8 -Value "<!doctype html><title>Fixture</title>"
|
||||
Set-Content -LiteralPath (Join-Path $output "Institution.html") -Encoding utf8 -Value "<!doctype html><title>Institution</title>"
|
||||
Set-Content -LiteralPath (Join-Path $static "contentIndex.json") -Encoding utf8 -Value '{"index":{},"Institution":{}}'
|
||||
Set-Content -LiteralPath (Join-Path $output "sitemap.xml") -Encoding utf8 -Value '<urlset><url><loc>https://example.invalid/</loc></url><url><loc>https://example.invalid/Institution</loc></url></urlset>'
|
||||
Set-Content -LiteralPath (Join-Path $output "index.xml") -Encoding utf8 -Value '<rss><channel><item><link>https://example.invalid/</link></item><item><link>https://example.invalid/Institution</link></item></channel></rss>'
|
||||
& (Join-Path $PSScriptRoot "check-publication-output.ps1") -RepositoryRoot $fixtureFull -ContentRoot $content -OutputRoot $output
|
||||
|
||||
$staleOutput = Join-Path $output "Stale.html"
|
||||
Set-Content -LiteralPath $staleOutput -Encoding utf8 -Value "<!doctype html><title>Stale</title>"
|
||||
$caughtStaleOutput = $false
|
||||
try {
|
||||
& (Join-Path $PSScriptRoot "check-publication-output.ps1") -RepositoryRoot $fixtureFull -ContentRoot $content -OutputRoot $output | Out-Null
|
||||
}
|
||||
catch {
|
||||
$caughtStaleOutput = $true
|
||||
}
|
||||
if (-not $caughtStaleOutput) {
|
||||
throw "Publication-output checker failed to reject stale generated HTML"
|
||||
}
|
||||
Remove-Item -LiteralPath $staleOutput -Force
|
||||
|
||||
$broken = Join-Path $content "Broken.md"
|
||||
Set-Content -LiteralPath $broken -Encoding utf8 -Value "# Broken`n`nSee [[No Such Note]]."
|
||||
$caughtBrokenLink = $false
|
||||
|
|
@ -48,6 +73,22 @@ try {
|
|||
}
|
||||
Remove-Item -LiteralPath $brokenAnchor -Force
|
||||
|
||||
$outside = Join-Path $fixtureFull "Outside.md"
|
||||
Set-Content -LiteralPath $outside -Encoding utf8 -Value "# Outside`n`nThis note is outside the selected publication root."
|
||||
$brokenEscape = Join-Path $content "Broken Escape.md"
|
||||
Set-Content -LiteralPath $brokenEscape -Encoding utf8 -Value "# Broken Escape`n`nSee [[../Outside]]."
|
||||
$caughtEscapedLink = $false
|
||||
try {
|
||||
& (Join-Path $PSScriptRoot "check-wikilinks.ps1") -ContentRoot $content | Out-Null
|
||||
}
|
||||
catch {
|
||||
$caughtEscapedLink = $true
|
||||
}
|
||||
if (-not $caughtEscapedLink) {
|
||||
throw "Wikilink checker failed to reject a link outside the selected content root"
|
||||
}
|
||||
Remove-Item -LiteralPath $brokenEscape -Force
|
||||
|
||||
$forbidden = Join-Path $content "workshop"
|
||||
New-Item -ItemType Directory -Path $forbidden | Out-Null
|
||||
$caughtBoundary = $false
|
||||
|
|
@ -61,7 +102,7 @@ try {
|
|||
throw "Publication checker failed to reject a nested workshop directory"
|
||||
}
|
||||
|
||||
Write-Output "Lore tool tests passed, including broken-link, broken-anchor, and publication-boundary negative checks."
|
||||
Write-Output "Lore tool tests passed, including broken-link, broken-anchor, escaped-link, stale-output, and publication-boundary negative checks."
|
||||
}
|
||||
finally {
|
||||
$resolvedFixture = [System.IO.Path]::GetFullPath($fixtureFull)
|
||||
|
|
|
|||
|
|
@ -34,18 +34,31 @@ if (-not (Test-Path $sharedQuartzRoot)) {
|
|||
}
|
||||
|
||||
$buildScript = Join-Path $sharedQuartzRoot "scripts\build-site.mjs"
|
||||
$outputRelative = "quartz-site/public"
|
||||
$outputRoot = [System.IO.Path]::GetFullPath((Join-Path $repoRoot "quartz-site"))
|
||||
$outputFull = [System.IO.Path]::GetFullPath((Join-Path $repoRoot $outputRelative))
|
||||
|
||||
if (-not (Test-Path $buildScript)) {
|
||||
throw "GameCult-Quartz build script was not found at '$buildScript'."
|
||||
}
|
||||
|
||||
if (-not $outputFull.StartsWith($outputRoot + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "Quartz output must remain below '$outputRoot'."
|
||||
}
|
||||
|
||||
# Quartz emitters replace the reader projection. Old author-vault pages must not
|
||||
# survive merely because the next source tree is smaller.
|
||||
if (Test-Path -LiteralPath $outputFull) {
|
||||
Remove-Item -LiteralPath $outputFull -Recurse -Force
|
||||
}
|
||||
|
||||
$scriptArgs = @(
|
||||
$buildScript,
|
||||
$Command,
|
||||
"--siteRoot", $repoRoot,
|
||||
"--overlayDir", "site",
|
||||
"--contentDir", "Kalsa",
|
||||
"--outputDir", "quartz-site/public"
|
||||
"--contentDir", "Kalsa/Public",
|
||||
"--outputDir", $outputRelative
|
||||
)
|
||||
|
||||
& $node @scriptArgs
|
||||
|
|
|
|||
Loading…
Reference in a new issue