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.
327 lines
17 KiB
Makefile
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
|