set shell := ["bash", "-c"] set dotenv-load := true project_url := "https://ontoref.dev" project_root := justfile_directory() source_project := justfile_directory() + "/../../../website-htmx-rustelo/code" rustelo_root := justfile_directory() + "/../../../rustelo/code" # `authoring` (not `content`) to avoid colliding with the flat content recipe below. # Content-authoring toolkit via rustelo-content CLI: images | sync | validate | new mod authoring ".just/content.just" default: @just --list @echo -e "\nproject_url = {{ project_url }}" # ── Server ────────────────────────────────────────────────────────────────── # Start the HTMX-SSR dev server (auto-detects site/ in PWD) [no-cd] dev: lsof -ti:3030 | xargs kill -9 2>/dev/null || true rustelo-htmx-server # ── Content ───────────────────────────────────────────────────────────────── # Positional filters: `just content projects` or `just content projects es`. # A `type=`/`lang=` prefix is tolerated, so `just content type=projects lang=es` works too. # The targeted (--content-type/--language) path is non-incremental — it always rewrites. # # The two trees own DIFFERENT files, so the mirror runs in both directions: # site/r ← content_processor writes the content indexes here (canonical); # the runtime server reads it (SITE_SERVER_ROOT_CONTENT=r). # site/public/r ← the nu generators write about/adr-map/taglines/content_graph here, # and this is the tree the deploy pack ships (site/public/**). # So: content indexes go r → public/r (or nothing new ever reaches production), and # the four nu artifacts come back public/r → r (or dev serves June's about/adr-map). # A whole-tree rsync in ONE direction cannot be right for both, and the old one # (public/r → r) clobbered each fresh index with the stale deploy-tree copy. # Regenerate content indexes from markdown in site/content/, then reconcile both trees. [no-cd] content type="" lang="": #!/usr/bin/env bash set -e # Brand-linkify blog "ontoref" mentions → /about (idempotent, source-level). nu "{{ project_root }}/scripts/build/linkify-site.nu" "{{ project_root }}/site/content/blog/**/*.md" # just args are POSITIONAL: `just content type=projects` binds type="type=projects". # Strip an optional `key=` prefix so both positional and key=value forms work. TYPE="{{ type }}"; TYPE="${TYPE#*=}" LANG_ARG="{{ lang }}"; LANG_ARG="${LANG_ARG#*=}" ARGS=() [ -n "$TYPE" ] && ARGS+=(--content-type "$TYPE") [ -n "$LANG_ARG" ] && ARGS+=(--language "$LANG_ARG") content_processor "${ARGS[@]}" # Content indexes → deploy tree. --delete is what keeps a renamed slug from living # on as a live URL: content_processor prunes site/r, and without --delete the dead # page would survive in public/r and ship to production. The four nu artifacts are # excluded (site/r holds only stale copies that would overwrite the fresh originals # gen-adr-map / gen-about-pages / gen-taglines just wrote) — and --exclude also # protects them from --delete, since they legitimately exist only on this side. rsync -a --delete \ --exclude about.json --exclude adr-map.json \ --exclude taglines.json --exclude content_graph.json \ "{{ project_root }}/site/r/" "{{ project_root }}/site/public/r/" # nu artifacts → serve tree, so `./run.sh` renders the current About/ADR map. for f in about.json adr-map.json taglines.json content_graph.json; do src="{{ project_root }}/site/public/r/$f" if [ -f "$src" ]; then cp -f "$src" "{{ project_root }}/site/r/$f"; fi done # Watch mode: auto-regenerate content on file changes [no-cd] content-watch type="" lang="": #!/usr/bin/env bash set -e TYPE="{{ type }}"; TYPE="${TYPE#*=}" LANG_ARG="{{ lang }}"; LANG_ARG="${LANG_ARG#*=}" ARGS=(--watch) [ -n "$TYPE" ] && ARGS+=(--content-type "$TYPE") [ -n "$LANG_ARG" ] && ARGS+=(--language "$LANG_ARG") content_processor "${ARGS[@]}" # Regenerate content_graph.json (mini-graphs + ONTOLOGY CONTEXT sidebar) [no-cd] graph daemon=project_url: RUSTELO_ROOT="{{ rustelo_root }}" \ ONTOREF_NICKEL_IMPORT_PATH="{{ project_root }}/../../code/ontology" \ CONTENT_GRAPH_IMPL_ONTOLOGY="{{ project_root }}/../../.ontoref/ontology/core.ncl" \ CONTENT_GRAPH_IMPL_ADRS_GLOB="{{ project_root }}/../../.ontoref/adrs/adr-[0-9]*.ncl" \ CONTENT_GRAPH_FRAMEWORK_ONTOLOGY="{{ rustelo_root }}/../.ontoref/ontology/core.ncl" \ CONTENT_GRAPH_FRAMEWORK_ADRS_GLOB="{{ rustelo_root }}/../.ontoref/adrs/adr-[0-9]*.ncl" \ CONTENT_GRAPH_DAEMON_BASE="{{ daemon }}" \ gen-content-graph # Regenerate ADR reference map from .ontoref/adrs → site/public/r/adr-map.json [no-cd] adr-map daemon=project_url: nu "{{ project_root }}/scripts/build/gen-adr-map.nu" --root "{{ project_root }}/../.." --daemon "{{ daemon }}" # The web-usable form of `onre diagram` (served at /images/, the static asset root). # Referenced by the About graph block, so must run before about-json. # Project the ontology (core.ncl) → site/public/images/ontoref-diagram.svg. [no-cd] diagram: nu "{{ project_root }}/scripts/build/gen-diagram.nu" --root "{{ project_root }}/../.." --out "{{ project_root }}/site/public/images/ontoref-diagram.svg" nu "{{ project_root }}/scripts/build/gen-graph-page.nu" --root "{{ project_root }}/../.." --out "{{ project_root }}/site/public/images/ontoref-graph.html" # The ADR set moves with the spine, not with this repo, so the projection has to be in # the build chain or it silently stops tracking it: ADR-059..069 sat unpublished for # months because this generator existed but no recipe called it. A generator outside the # dependency chain is a generator that does not run. Regenerating is safe and idempotent — # it rewrites the whole adr/ kind from .ontoref/adrs, md + .ncl sidecar per ADR. # Project .ontoref/adrs → adr content-kind markdown (site/content/adr/en/{accepted,proposed}). [no-cd] adr-pages: #!/usr/bin/env bash set -e # The status dirs are cleared first because status is part of the PATH # (accepted/adr-070.md vs proposed/adr-070.md): accepting an ADR moves its page, # and the generator only writes — it never removes the page at the old status. Without # this, `ontoref adr accept` leaves the ADR published twice and adr-check fails on the # count. The wipe is safe precisely because this projection is fully reproducible. rm -f "{{ project_root }}"/site/content/adr/en/accepted/adr-[0-9]*.{md,ncl} \ "{{ project_root }}"/site/content/adr/en/proposed/adr-[0-9]*.{md,ncl} ONTOREF_NICKEL_IMPORT_PATH="{{ project_root }}/../../code/ontology" \ nu "{{ project_root }}/scripts/build/gen-adr-pages.nu" \ --root "{{ project_root }}/../.." \ --mode content \ --content-out "{{ project_root }}/site/content/adr/en" # Fails if the spine holds an ADR the site never published — the exact drift that hid # eleven ADRs. Non-mutating: compares .ontoref/adrs against the adr content tree. # Gate: assert every accepted/proposed ADR in the spine has a page. [no-cd] adr-check: #!/usr/bin/env bash set -e spine=$(ls "{{ project_root }}/../../.ontoref/adrs"/adr-[0-9]*.ncl | wc -l | tr -d ' ') pages=$(ls "{{ project_root }}"/site/content/adr/en/*/adr-[0-9]*.md | wc -l | tr -d ' ') if [ "$spine" -eq "$pages" ]; then echo "adr-check: $pages/$spine ADRs projected — spine and site agree" else echo "adr-check: DRIFT — spine has $spine ADRs, site publishes $pages. Run: just adr-pages" exit 1 fi # The howto calls the CASE object the single source of truth; until now it was not — # the .md body was hand-rendered, and 356 of its 500 lines were the shared glossary # widget pasted in verbatim. The body is now a projection of the .ncl; the frontmatter # (title, excerpt, image, sort_order) stays authored. Edit the .ncl, never the body. # Project the typed expediente CASEs (.ncl) → the body of their .md. [no-cd] expedientes: nu "{{ project_root }}/scripts/build/gen-expediente.nu" --root "{{ project_root }}" # Fails when a .md body differs from what its .ncl produces — i.e. when someone edited # the render instead of the source, which is how the prose the contract did not model # came to live only in the generated file. # Gate: assert every expediente .md body is reproducible from its typed CASE. [no-cd] expedientes-check: nu "{{ project_root }}/scripts/build/gen-expediente.nu" --root "{{ project_root }}" --check # The standalone informe (site/public/images/expedientes//expedientes.html) was # hand-kept HTML with its cases inlined as a JS array, while the ARTICLES were already # projected from the same .ncl. Two hands on one dataset drift, and they did: deriva's # ledger carried 6 rows in the article and 8 in the viewer, and `espejo` was never added # at all — so its own article linked to an anchor that did not exist. # Project the typed CASEs → the standalone informe, one per locale. [no-cd] informe: nu "{{ project_root }}/scripts/build/gen-informe.nu" --root "{{ project_root }}" # Gate: assert every informe is reproducible from its typed CASEs. [no-cd] informe-check: nu "{{ project_root }}/scripts/build/gen-informe.nu" --root "{{ project_root }}" --check # Gate: every case must exist in every locale. The English tree carried two cases by hand # and never got the third — an absence nothing could report, because nothing compared the # two trees. A locale that is deliberately behind should say so, not simply be missing. [no-cd] expedientes-parity: #!/usr/bin/env nu let base = "{{ project_root }}/site/content/expedientes" let langs = (ls $base | where type == dir | get name | each {|d| $d | path basename } | where {|d| not ($d | str starts-with "_") }) let by_lang = ($langs | each {|l| { lang: $l, ids: (glob $"($base)/($l)/*/*.ncl" | each {|f| $f | path basename | str replace ".ncl" "" } | sort) } }) let all = ($by_lang | get ids | flatten | uniq | sort) let gaps = ($by_lang | each {|e| { lang: $e.lang, missing: ($all | where {|id| $id not-in $e.ids }) } } | where {|e| ($e.missing | length) > 0 }) if ($gaps | is-empty) { print $"expedientes-parity: ($all | length) case\(s\) present in all ($langs | length) locales" } else { for g in $gaps { print $"expedientes-parity: ($g.lang) is missing ($g.missing | str join ', ')" } exit 1 } # Makes /catalog (browse) + /catalog/{slug} serve as SSR content (runtime kind, no # rebuild — registered in site/content/content-kinds.toml). # Project .ontoref/catalog → catalog markdown, then build the content index. [no-cd] catalog-content: nu "{{ project_root }}/scripts/build/gen-catalog-pages.nu" --root "{{ project_root }}/../.." --mode content --content-out "{{ project_root }}/site/content/catalog/en" content_processor --content-type catalog # Hot-reloaded by the SSR /about handler (via pages/about_project.j2 when kind=project). # daemon="" → the "see it live" graph link is emitted host-relative (/graph?…), # because on the SSR site /graph is served by this same origin. The static # outreach/web pages pass an absolute --daemon since they may be served elsewhere. # Emit the level-aware About view-model → site/public/r/about.json (needs adr-map fresh). [no-cd] about-json daemon="": diagram nu "{{ project_root }}/scripts/build/gen-about-pages.nu" \ --root "{{ project_root }}/../.." \ --emit-json "{{ project_root }}/site/public/r/about.json" \ --daemon "{{ daemon }}" # ── Spine (bl-035) ──────────────────────────────────────────────────────────── # Generate spine landing pages from .ontoref/positioning/spine.ncl [no-cd] spine daemon=project_url: nu "{{ project_root }}/scripts/build/gen-spine-pages.nu" \ --root "{{ project_root }}/../.." \ --content "{{ project_root }}/site/content" \ --daemon "{{ daemon }}" # ── Taglines (rotating hero phrases) ────────────────────────────────────────── # Drift-checked at export (taglines-schema.ncl); the hero rotator fetches /r/taglines.json. # Project .ontoref/positioning/taglines.ncl → site/public/r/taglines.json. [no-cd] taglines: nu "{{ project_root }}/scripts/build/gen-taglines.nu" \ --root "{{ project_root }}/../.." \ --out "{{ project_root }}/site/public/r/taglines.json" # Projects code/Cargo.toml [workspace.package].version into the home hero badge — the # one hand-written version literal on the site. Run after a version bump so the badge # never drifts. Scoped to the home-hero-badge line, so other version strings are untouched. # Stamp the canonical workspace version into the home hero badge (en + es). [no-cd] stamp-version: #!/usr/bin/env nu let ver = (open "{{ project_root }}/../../code/Cargo.toml" | get workspace.package.version) let ftls = [ "{{ project_root }}/site/i18n/locales/en/pages/home.ftl" "{{ project_root }}/site/i18n/locales/es/pages/home.ftl" ] for f in $ftls { let out = (open --raw $f | lines | each {|l| if ($l | str starts-with "home-hero-badge") { $l | str replace --all --regex 'v\d+\.\d+\.\d+' $"v($ver)" } else { $l } } | str join "\n") $"($out)\n" | save --force $f } print $"stamped v($ver) into home-hero-badge \(en + es\)" # From the constellation canonical source (assets/vendor) into the site asset trees # (_public source + public served mirror). # Materialize the shared hero rotator + typed.js vendor into the site asset trees. [no-cd] rotator-assets: cp -f "{{ project_root }}/../../assets/vendor/tagline-rotator.js" "{{ project_root }}/site/_public/js/tagline-rotator.js" cp -f "{{ project_root }}/../../assets/vendor/typed.umd.js" "{{ project_root }}/site/_public/js/typed.umd.js" cp -f "{{ project_root }}/../../assets/vendor/tagline-rotator.js" "{{ project_root }}/site/public/js/tagline-rotator.js" cp -f "{{ project_root }}/../../assets/vendor/typed.umd.js" "{{ project_root }}/site/public/js/typed.umd.js" # assets/site/ is the source of truth; only the web formats are mirrored — PNG/JPG # originals stay out of the deploy. Safe to re-run. # Mirror web-optimised images (AVIF + WebP) from assets/site/ → site/public/images/. [no-cd] sync-site-images: #!/usr/bin/env nu let src = "{{ project_root }}/../../assets/site" let dest = "{{ project_root }}/site/public/images" mkdir $dest let files = (glob $"($src)/*.{avif,webp}") if ($files | is-empty) { error make { msg: $"no AVIF/WebP found in ($src)" } } $files | each {|f| cp $f $dest } print $"synced ($files | length) image\(s\) from ($src) → ($dest)" # ── Templates ─────────────────────────────────────────────────────────────── # Safe to re-run; overwrites htmx-templates/ from scratch — which is exactly why the # site's own templates cannot live in the output. The assembler has two layers # (framework defaults, then source-project templates) but the level chain has three: # rustelo → website-htmx-rustelo → outreach/site. site/templates-overlay/ is the # missing third slot: the only declared home for template work owned by THIS level. # Anything edited straight into htmx-templates/ is destroyed on the next run — that is # how the nav submenus were lost once already. # Assemble htmx-templates: framework defaults → source-project templates → site overlay. [no-cd] templates: nu "{{ source_project }}/scripts/build/assemble-htmx-templates.nu" \ --project-templates "{{ source_project }}/crates/pages_htmx/templates" \ --out "{{ project_root }}/htmx-templates" cp -R "{{ project_root }}/site/templates-overlay/." "{{ project_root }}/htmx-templates/" # Proves `just templates` is lossless: reassemble into a temp tree and diff it against # the live one. Any difference means htmx-templates/ holds work that exists nowhere # else and the next `just templates` (or `just sync`, which calls it) will delete it — # the fix is to move that work into site/templates-overlay/, never to skip the run. # Gate: assert htmx-templates/ is exactly reproducible from its declared sources. [no-cd] templates-check: #!/usr/bin/env bash set -e tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT nu "{{ source_project }}/scripts/build/assemble-htmx-templates.nu" \ --project-templates "{{ source_project }}/crates/pages_htmx/templates" \ --out "$tmp" >/dev/null cp -R "{{ project_root }}/site/templates-overlay/." "$tmp/" if diff -r "{{ project_root }}/htmx-templates" "$tmp" >/dev/null 2>&1; then echo "templates-check: htmx-templates/ is reproducible — no unwitnessed work" else echo "templates-check: DRIFT — htmx-templates/ holds work absent from its sources:" diff -rq "{{ project_root }}/htmx-templates" "$tmp" | sed "s|$tmp||g" exit 1 fi # Safe to re-run; overwrites htmx-assets/ from scratch. # Materialize the framework htmx runtime (htmx.min.js + ext/*) into a top-level # htmx-assets/ tree (sibling of htmx-templates/). Shipped to the PV by content.nu # (distro.ncl deliverable) and served at /assets/htmx/* via HTMX_ASSETS_PATH — the # runtime lives OUTSIDE site/public (router mounts /assets/htmx as its own ServeDir, # not from the public tree). Dev masks this via the local-install wrapper's # HTMX_ASSETS_PATH; prod has no such mount. Source of truth: templates/shared/htmx. [no-cd] htmx-assets: rm -rf "{{ project_root }}/htmx-assets" mkdir -p "{{ project_root }}/htmx-assets/ext" cp -f "{{ rustelo_root }}/templates/shared/htmx/htmx.min.js" "{{ project_root }}/htmx-assets/htmx.min.js" cp -R "{{ rustelo_root }}/templates/shared/htmx/ext/." "{{ project_root }}/htmx-assets/ext/" # ── Posts ───────────────────────────────────────────────────────────────────── # Brand-linkify mentions → /about, regenerate the content indexes, and rebuild the # content graph (mini-graph + sidecar relations). Run after editing a post + .ncl sidecar. # Build a post for publication: linkify + content indexes + content graph. [no-cd] post: content graph # 1. every brand mention is already linkified (no `just content` pending) # 2. every post graph sidecar resolves into content_graph.json (no stale graph) # Verify ALL posts are publication-ready without mutating anything (CI gate). [no-cd] posts-check: #!/usr/bin/env bash set -e nu "{{ project_root }}/scripts/build/linkify-site.nu" --check "{{ project_root }}/site/content/blog/**/*.md" nu "{{ project_root }}/scripts/build/check-graph-coverage.nu" "{{ project_root }}" # ── Sync ──────────────────────────────────────────────────────────────────── # Full resync: templates + htmx runtime + vendor assets + content indexes # (no graph — that needs ONTOREF env) [no-cd] sync: templates htmx-assets rotator-assets content # Order is load-bearing, not cosmetic: # adr-pages / catalog-content BEFORE content — they emit the markdown content indexes. # graph / adr-map / about-json AFTER content — they read the freshly built indexes. # sync-artifacts LAST — the nu generators write to site/public/r, and only `content` # copies them back to the serve tree; without this final leg a sync-full leaves # site/r rendering the previous run's About and ADR map. # Requires ONTOREF_NICKEL_IMPORT_PATH in .env. # Full resync: projections → content indexes → derived artifacts → serve tree. [no-cd] sync-full: templates htmx-assets rotator-assets adr-pages expedientes informe catalog-content content graph adr-map about-json sync-artifacts # The nu generators own these four files in site/public/r (the deploy tree); the serve # tree needs the same copies. `content` does this too, so a standalone `just content` # stays correct — this recipe exists for the chains that regenerate them AFTER content. # Copy the four nu artifacts public/r → site/r so the dev server serves what was just built. [no-cd] sync-artifacts: #!/usr/bin/env bash set -e for f in about.json adr-map.json taglines.json content_graph.json; do src="{{ project_root }}/site/public/r/$f" if [ -f "$src" ]; then cp -f "$src" "{{ project_root }}/site/r/$f"; fi done