Publish only the Kalsa reader projection

This commit is contained in:
Meta Vens 2026-08-22 18:45:55 +02:00
commit 4db77bf2d0
6 changed files with 177 additions and 13 deletions

View file

@ -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

View file

@ -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."

View 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."

View file

@ -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"
}

View file

@ -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)

View file

@ -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