ontoref-outreach/site/justfile
Jesús Pérez 56d3c10d9e site: put the ADR projection in the build chain, and a contract outside the data tree
Two failures, one shape: something living in a tree whose consumers assume it holds
only something else.

adr-pages: gen-adr-pages.nu existed but no recipe called it, so the ADR projection
silently stopped tracking the spine — ADR-059..069 were written months ago and never
reached the site. A generator outside the dependency chain is a generator that does
not run. Wired into sync-full ahead of `content` (it emits the markdown that content
indexes), and it is idempotent: this commit carries no ADR diff because regenerating
reproduces the tree byte for byte.

graph: gen-content-graph nickel-exports every .ncl under site/content, and
expedientes/_schema/expediente.ncl was a CONTRACT, not an instance — export demanded
`id` and the recipe aborted. `just graph` had been dead since expedientes landed. The
generator was right and the file was in the wrong tree: site/content/ holds instances,
so a schema there is a category error. Moved to site/schemas/expediente.ncl; the one
sidecar that imports it still type-checks against the contract. graph now builds
(mini:154 related:222 ontology:15).

sync-full ordering: graph / adr-map / about-json ran AFTER content, but the leg that
copies the four nu artifacts into the serve tree lived INSIDE content — so a full sync
left site/r rendering the previous run's About and ADR map. Added sync-artifacts as the
final link. Both trees now agree on all four.

Gates
-----
adr-check  spine vs site: fails if the spine holds an ADR the site never published.
           Verified by deleting adr-069 — the gate caught it (69 vs 68, exit 1) and
           sync-full rebuilt it unattended.

This is the third gate (with templates-check and posts-check) and, like the others, it
runs before the damage, not after. None of them run on deploy yet: `just authoring
publish` still ships without asking. That is what keeps these gates a habit rather
than a law — and is the subject of the governed-delivery ADR still to be written.

Refs: ADR-066 (the check decides, never the reporter).

Versión corta, si la prefieres:

site: wire the ADR projection into the build chain; move a contract out of the data tree

gen-adr-pages existed but no recipe called it — eleven ADRs never reached the site. Now in
sync-full, with an adr-check gate that fails when the spine holds an ADR the site doesn't.
`just graph` had been dead since expedientes landed: a schema sat inside site/content/, which
gen-content-graph nickel-exports as if everything there were an instance. Moved to
site/schemas/. Also fixed sync-full ordering so the serve tree stops rendering the previous
run's About and ADR map.
2026-07-12 00:02:01 +01:00

327 lines
17 KiB
Makefile

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:
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
# 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|<regenerated>|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 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