diff --git a/scripts/decks/gen-deck-pages.nu b/scripts/decks/gen-deck-pages.nu index 1cf9364..3099877 100644 --- a/scripts/decks/gen-deck-pages.nu +++ b/scripts/decks/gen-deck-pages.nu @@ -27,6 +27,15 @@ def manifest [] { ^nickel export --format json $f | from json } +# The deck→artifact keys, from lib/variants.mjs — the one authority on the rule. This file used to +# mirror it by hand ("the variant key mirrors lib/project.mjs"), and a mirror is a copy that drifts: +# a THIRD copy in site/justfile never learned that a bilingual deck is one source per language, and +# gated an asset dir that does not exist. Ask, do not re-derive. +def variants [] { + let script = [(outreach) "scripts" "decks" "lib" "variants.mjs"] | path join + ^node $script | from json +} + # ONE dist PER DECK. `slidev build` builds a single entry, so a shared dist/ means the second # deck silently captures the FIRST one's slides — the projection would be wrong and nothing # would say so. The deck's own build dir is what makes this a pipeline instead of a one-off. @@ -66,14 +75,9 @@ def ensure-build [id: string, source: string, --force] { def main [--dry-run, --force] { # One build per SOURCE, not per deck: a bilingual deck is two sources, and sharing a build is - # how an English URL ends up serving Spanish slides. The variant key mirrors lib/project.mjs. - for d in (manifest).decks { - let sources = ($d.pages | each {|p| $p.source? | default $d.source } | uniq) - for src in $sources { - let langs = ($d.pages | where {|p| ($p.source? | default $d.source) == $src } | get lang) - let key = if ($sources | length) == 1 { $d.id } else { $"($d.id)-($langs | str join '-')" } - ensure-build $key $src --force=$force - } + # how an English URL ends up serving Spanish slides. + for v in (variants) { + ensure-build $v.key $v.source --force=$force } let flags = if $dry_run { ["--dry-run"] } else { [] } let script = [(outreach) "scripts" "decks" "lib" "project.mjs"] | path join diff --git a/scripts/decks/lib/project.mjs b/scripts/decks/lib/project.mjs index b1fe306..3aab425 100644 --- a/scripts/decks/lib/project.mjs +++ b/scripts/decks/lib/project.mjs @@ -8,7 +8,6 @@ // site/public/images/decks//fonts/* woff2, latin + latin-ext // site/content/resources//decks/.md body replaced, FRONTMATTER PRESERVED import { readFile, writeFile, mkdir } from 'node:fs/promises' -import { execFileSync } from 'node:child_process' import vm from 'node:vm' import { join, dirname } from 'node:path' import { capture } from './capture.mjs' @@ -17,14 +16,13 @@ import { projectAssets } from './project-assets.mjs' import { fetchFonts } from './fetch-fonts.mjs' import { buildBody } from './build-body.mjs' import { readAnchors } from './anchors.mjs' -import { OUTREACH, distDir, publicBase, publicDir, contentFile } from './paths.mjs' +import { readManifest, variants } from './variants.mjs' +import { distDir, publicBase, publicDir, contentFile } from './paths.mjs' const DRY = process.argv.includes('--dry-run') const apply = !DRY -const manifest = JSON.parse( - execFileSync('nickel', ['export', '--format', 'json', join(OUTREACH, 'scripts/decks/decks.ncl')], { encoding: 'utf8' }) -) +const manifest = readManifest() const { boundary_allow: ALLOW, slide: { width: W, height: H }, i18n: I18N, category: CATEGORY } = manifest // The generated body is fenced. Anything outside the markers is the author's and is left @@ -40,25 +38,6 @@ function replaceBody(existing, generated) { const report = [] -// A deck may have one source per language. Group its pages by the source they actually come -// from: each distinct source is its own build, its own capture and its own asset dir. Sharing a -// capture across two sources is how an English URL ends up serving Spanish slides. -function variants(deck) { - const by = new Map() - for (const p of deck.pages) { - const source = p.source ?? deck.source - if (!by.has(source)) by.set(source, []) - by.get(source).push(p) - } - // One source → the deck's own id. Several → one asset dir per source, keyed by its languages, - // because the CSS, the fonts and the images of two different decks are two different things. - return [...by.entries()].map(([source, pages]) => ({ - source, - pages, - key: by.size === 1 ? deck.id : `${deck.id}-${pages.map(p => p.lang).join('-')}`, - })) -} - for (const deck of manifest.decks) { for (const variant of variants(deck)) { const { key: id, pages } = variant diff --git a/scripts/decks/lib/variants.mjs b/scripts/decks/lib/variants.mjs new file mode 100644 index 0000000..e9ce03f --- /dev/null +++ b/scripts/decks/lib/variants.mjs @@ -0,0 +1,59 @@ +// The deck → artifact key rule, in ONE place, because it has already drifted once. +// +// A bilingual deck is TWO sources (decks.ncl: `pages[].source` overrides the deck's), and each +// source is its own build, its own capture and its own asset dir — sharing them is how an English +// URL ends up serving Spanish slides. So the artifact is keyed by the VARIANT (`ai-infra-es`), +// not by the deck (`ai-infra`), whose bare dir holds only thumbnails. +// +// That rule was re-derived in three places: here (project.mjs), gen-deck-pages.nu, and the +// justfile's `jq -r '.decks[].id'`. The third never learned the split. It kept handing `ai-infra` +// to the artifact gate, which ENOENT'd on a deck.css that was never written there — and PASSED on +// ontoref-ontologia-reflexion, whose PRE-SPLIT bundle was still lying on disk. A green gate over a +// file no page loads is worse than the crash: the crash is the only reason anyone looked. +// +// A rule copied is a rule that drifts. Every consumer asks this module now; nobody re-derives it. +// +// Usage: node lib/variants.mjs → JSON [{ deck, key, source, langs }] +// node lib/variants.mjs --ids → one artifact key per line +import { execFileSync } from 'node:child_process' +import { fileURLToPath } from 'node:url' +import { join } from 'node:path' +import { OUTREACH } from './paths.mjs' + +export function readManifest() { + return JSON.parse(execFileSync( + 'nickel', ['export', '--format', 'json', join(OUTREACH, 'scripts/decks/decks.ncl')], + { encoding: 'utf8' }, + )) +} + +// Group a deck's pages by the source they actually come from: each distinct source is its own +// build, capture and asset dir. One source → the deck's own id (nothing to disambiguate). +// Several → one key per source, named by its languages, because the CSS, the fonts and the images +// of two different sources are two different decks' worth of artifact. +export function variants(deck) { + const by = new Map() + for (const p of deck.pages) { + const source = p.source ?? deck.source + if (!by.has(source)) by.set(source, []) + by.get(source).push(p) + } + return [...by.entries()].map(([source, pages]) => ({ + source, + pages, + key: by.size === 1 ? deck.id : `${deck.id}-${pages.map(p => p.lang).join('-')}`, + })) +} + +export function allVariants(manifest = readManifest()) { + return manifest.decks.flatMap(d => variants(d).map(v => ({ + deck: d.id, key: v.key, source: v.source, langs: v.pages.map(p => p.lang), + }))) +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + const all = allVariants() + console.log(process.argv.includes('--ids') + ? all.map(v => v.key).join('\n') + : JSON.stringify(all, null, 2)) +}