Los casos diagnosticaban y no exigían. Cada uno cerraba con un veredicto —«un contrato que no
se aplica no es un contrato: es un comentario»— y esa lección no llegaba a ninguna parte: los
nueve apuntaban `related_modes` al modo que ESCRIBE expedientes. Un bucle circular. La serie
denunciaba el contrato desconectado y ella misma lo era.
El esquema ahora lo impide:
lesson_codified_in pares what/where con gramática de punteros verificados
(adr:NNN/id · qa:id · mode:id/paso · gate:just X · test:ruta · contract:ruta#campo)
lesson_debt lo que nada exige todavía, con la dirección de quien lo saldaría
LessonAccounted contrato que RECHAZA el caso que no llena ninguno de los dos.
Visto rechazar antes de rellenar nada. Confesar cuesta (adr-071).
replay la sesión repetida con protocolo: el prompt literal recibido (con su
procedencia declarada — una reconstrucción sin fuente es ficción), el
prompt corregido, las microtareas con su oráculo NOMBRADO, la puerta
antes de delegar y el disparador de adr?.
Contabilizados los diez casos × 2 idiomas. Cuatro llevan replay. El reparto es el hallazgo:
las lecciones que dolieron en MECANISMO acabaron en constraints de ADR; las que dolieron en
CONDUCTA DEL OPERADOR quedaron en deuda. Esa mitad era justo la que faltaba.
Nuevo: /docs/expedientes/protocolo.html y /docs/case-files/protocol.html — el protocolo de
sesión, PROYECTADO de los replays (gen-protocolo.nu, con --check en la cadena). No es una
tarjeta del grid: el grid indexa CASOS, y una proyección archivada entre las instancias de las
que se proyecta es el mismo error de categoría que un esquema archivado entre ellas. Lleva las
cuatro caras de una sola regla —el agente nunca es su propio oráculo— y la deuda viva de la
serie, que se recalcula sola: cuando una ⊘ se salda en su caso, desaparece de la página.
Nuevo expediente 8/0 — la cura que no volvió a casa (ES+EN, 'clinical). Las ocho líneas que
cerraron el caso fundacional existen, probadas y sirviendo, y no habían llegado a ninguna
parte: ni al commit de su instancia (donde alguien siguió añadiendo kinds A MANO hasta siete),
ni a las dos plantillas de las que nace cada sitio. 23 constraints en 8 ADRs, 0 ejecutadas.
Y su punto de abandono: el arquitecto recomendó borrar el atajo con «prueba empírica» construida
sobre dos árboles distintos — habría roto 6 de 8 rutas respondiendo 200. Lo paró el testigo que
el propio encargo exigía. La puerta cazó al arquitecto.
Deuda corregida en 404-PAP y 404-PAP-bis: decían que faltaban tres cosas cuando faltaba una.
El backport está hecho y la constraint escrita; sólo falta que algo la ejecute. Un howto no
expira solo — éste caducó en dos horas.
Y el mensaje de la puerta de ilustración daba rutas que no resuelven desde donde la puerta
corre. Corregido: las rutas son de la raíz de la constelación, y el conversor NO reescala.
Verificado: 22 casos reproducen · completos en en, es · servidos · informe reproduce ·
parity 11×2 · protocolo-check reproduce · oráculo de cobertura ALL PASS.
649 lines
36 KiB
Makefile
649 lines
36 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.
|
|
# `glosario-*.json` joins the exclude list for the same reason the other four are on it: the
|
|
# nu generators own it in site/public/r, content_processor knows nothing about it, and an
|
|
# unexcluded --delete PRUNES IT. Measured, not feared: `just content` deleted it and the
|
|
# tooltip layer 404'd on its first publish. That is expediente 69/58 — the mirror that reverts
|
|
# its own work — on a path its cure never covered.
|
|
rsync -a --delete \
|
|
--exclude about.json --exclude adr-map.json \
|
|
--exclude taglines.json --exclude content_graph.json \
|
|
--exclude 'glosario-*.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 glosses were written in the registry "for a reader who does not know the term" and no
|
|
# surface ever showed them: /acerca-de said NCL and DAG to people who had met neither. This is the
|
|
# projection that ends that — and it is IN THE CHAIN, because a generator nobody calls is a
|
|
# generator that does not run (see adr-pages, eleven ADRs, four weeks).
|
|
# Project the term registry → site/public/r/glosario-<lang>.json (the tooltip's only source).
|
|
[no-cd]
|
|
glossary:
|
|
nu "{{ project_root }}/scripts/build/gen-glossary-json.nu" --root "{{ project_root }}/../.."
|
|
|
|
# Project the term registry → la página /conceptos (servida, no pintada con JS).
|
|
[no-cd]
|
|
conceptos:
|
|
nu "{{ project_root }}/scripts/build/gen-conceptos.nu" --root "{{ project_root }}/../.."
|
|
|
|
# Project the Slidev decks → real HTML on the deck pages. The words of a published slide used
|
|
# to be an IMAGE of words: nothing indexed them, no glossary tooltip could reach them, no screen
|
|
# reader read them, and the page drifted from the source the moment a slide changed. The deck is
|
|
# NOT re-parsed — Slidev renders it (its own /print route) and we capture what it produced.
|
|
# Lives in outreach/scripts/decks because it reads presentation/ and writes site/: it is the
|
|
# bridge, not the site's.
|
|
[no-cd]
|
|
decks *args:
|
|
nu "{{ project_root }}/../scripts/decks/gen-deck-pages.nu" {{ args }}
|
|
|
|
# Register a NEW deck: scaffold the content pages (frontmatter only — the body is the
|
|
# projection's) and print the decks.ncl entry to add. The manifest is not written by a script:
|
|
# which deck ships, under which slug, in which language, is a decision.
|
|
# monolingüe: just deck-new intro_slides.md intro --title-es "…" --slug-es "…"
|
|
# bilingüe (traduc): just deck-new ontology_slides.md ontology --title-es … --title-en … --slug-…
|
|
# bilingüe nativo: just deck-new rustikon_es.md rustikon --title-es … --slug-es … \
|
|
# --title-en … --slug-en … --source-en rustikon.md
|
|
[no-cd]
|
|
deck-new source id title_es="" slug_es="" title_en="" slug_en="" source_en="":
|
|
#!/usr/bin/env bash
|
|
set -e
|
|
# just binds positionals LITERALLY: `title_es="X"` arrives as the string `title_es=X`.
|
|
# Same `key=` strip the `content` recipe does, so both forms work. Omit a language's
|
|
# title/slug and that language gets NO page — a deck that exists only in Spanish must not
|
|
# have an English URL serving Spanish slides.
|
|
TE="{{ title_es }}"; TE="${TE#title_es=}"
|
|
SE="{{ slug_es }}"; SE="${SE#slug_es=}"
|
|
TN="{{ title_en }}"; TN="${TN#title_en=}"
|
|
SN="{{ slug_en }}"; SN="${SN#slug_en=}"
|
|
XN="{{ source_en }}"; XN="${XN#source_en=}"
|
|
nu "{{ project_root }}/../scripts/decks/new-deck.nu" "{{ source }}" "{{ id }}" \
|
|
--title-es "$TE" --slug-es "$SE" --title-en "$TN" --slug-en "$SN" --source-en "$XN"
|
|
|
|
# Gate (on disk): the deck's CSS bundle cannot reach out of its wrapper, the site's CSS cannot
|
|
# reach in — except the glossary, which is entitled to and which is the whole point.
|
|
[no-cd]
|
|
decks-check:
|
|
#!/usr/bin/env bash
|
|
set -e
|
|
cd "{{ project_root }}/../presentation"
|
|
for id in $(nickel export --format json ../scripts/decks/decks.ncl | jq -r '.decks[].id'); do
|
|
node ../scripts/decks/gates/artifact.mjs "$id"
|
|
done
|
|
|
|
# Gate (served): the PAGE, not the file. 16/16 slides rendered, nothing clipped, the keyboard and
|
|
# the index drive it, the zoom fits, the copy buttons copy, no third-party origin added. Verifying
|
|
# the file would prove nothing — the site's content pipeline sits between generator and reader,
|
|
# and it is the pipeline that once ate an inline <script> whole.
|
|
[no-cd]
|
|
decks-served base_url="http://localhost:3030":
|
|
#!/usr/bin/env bash
|
|
set -e
|
|
BASE="{{ base_url }}"; BASE="${BASE#*=}"
|
|
cd "{{ project_root }}/../presentation"
|
|
node ../scripts/decks/gates/served.mjs "$BASE/recursos/decks/ontoref-ontologia-reflexion"
|
|
node ../scripts/decks/gates/served.mjs "$BASE/resources/decks/ontoref-ontology-and-reflection"
|
|
|
|
# Gate: la página de conceptos reproduce desde el registro — nadie edita un glosario a mano.
|
|
[no-cd]
|
|
conceptos-check:
|
|
nu "{{ project_root }}/scripts/build/gen-conceptos.nu" --root "{{ project_root }}/../.." --check
|
|
|
|
# Gate: the served glossary reproduces from the registry — nobody hand-edits a tooltip.
|
|
[no-cd]
|
|
glossary-check:
|
|
nu "{{ project_root }}/scripts/build/gen-glossary-json.nu" --root "{{ project_root }}/../.." --check
|
|
|
|
# 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"
|
|
nu "{{ project_root }}/../../scripts/gen-architecture.nu" --root "{{ project_root }}/../.." --only outreach/site
|
|
|
|
# The home architecture SVG was a HAND COPY of scripts/templates/architecture.svg.tmpl with every
|
|
# {{{{TOKEN}}}} frozen: it shipped v0.1.7 / 41 practices / 163 edges / 56 nodes against a spine at
|
|
# 45 / 171 / 60, under a footer claiming to be a live projection. The generator already existed —
|
|
# it simply did not know about this target, and nothing gated it. --only keeps the blast radius
|
|
# here: a stale brand asset in assets/ is not this site's gate to fail.
|
|
# Gate: assert the home architecture SVG matches the spine it is generated from.
|
|
[no-cd]
|
|
architecture-check:
|
|
nu "{{ project_root }}/../../scripts/gen-architecture.nu" --root "{{ project_root }}/../.." --only outreach/site --check
|
|
|
|
# Gate: the About's static hook reproduces from core.ncl — nobody draws the ontology by hand.
|
|
#
|
|
# `architecture-check` existed and these two did not, so `diagram` shipped THREE projections and
|
|
# only one was gated. The other two rode outside the chain: `diagram` was not in `sync-full`
|
|
# either, so the generator ran by hand, once, and nothing compared it to the ontology afterwards
|
|
# — `generator-outside-the-chain`, verbatim. Measured 2026-07-16: no drift, but core.ncl was
|
|
# NEWER than both outputs (12-jul 00:59 vs 11-jul 23:47). They agreed by luck, not by floor.
|
|
[no-cd]
|
|
diagram-check:
|
|
nu "{{ project_root }}/scripts/build/gen-diagram.nu" --root "{{ project_root }}/../.." --out "{{ project_root }}/site/public/images/ontoref-diagram.svg" --check
|
|
|
|
# Gate: the About's LIVE PRIZE reproduces from core.ncl (ADR-057 hook-static / prize-live).
|
|
# A prize that silently stops tracking the ontology is worse than no prize: the reveal still
|
|
# promises it, and the visitor who follows the route is the one who finds out.
|
|
[no-cd]
|
|
graph-page-check:
|
|
nu "{{ project_root }}/scripts/build/gen-graph-page.nu" --root "{{ project_root }}/../.." --out "{{ project_root }}/site/public/images/ontoref-graph.html" --check
|
|
|
|
# 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 }}"
|
|
nu "{{ project_root }}/scripts/build/gen-protocolo.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.
|
|
#
|
|
# The gate runs FIRST, on the real tree, so a real defect is reported as itself. THEN its ORACLE
|
|
# runs — because a gate nobody has seen REFUSE is not a gate (adr-072,
|
|
# falsify-against-the-new-capability). The oracle carries one negative case per axis the gate
|
|
# covers: the extent grows, a language goes missing, the served tree falls behind, the extent
|
|
# becomes unreadable. Each was falsified by hand once and the falsification thrown away; this is
|
|
# the residue that refuses the same failure tomorrow.
|
|
[no-cd]
|
|
expedientes-check:
|
|
nu "{{ project_root }}/scripts/build/gen-expediente.nu" --root "{{ project_root }}" --check
|
|
nu "{{ project_root }}/scripts/build/gen-protocolo.nu" --root "{{ project_root }}" --check
|
|
nu "{{ project_root }}/../../.ontoref/reflection/tests/test_expediente_coverage.nu"
|
|
|
|
# The standalone informe (site/public/images/expedientes/<lang>/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 & htmx runtime (the override chain) ───────────────────────────
|
|
#
|
|
# ONE rule governs both trees below, and it is the rule the implementation already
|
|
# applies to the framework: a file present in a higher layer WINS, a file absent from
|
|
# it is INHERITED from the layer below. The chain has three levels, not two:
|
|
#
|
|
# rustelo (framework) → website-htmx-rustelo (implementation) → outreach/site (this)
|
|
#
|
|
# The consumer's layer is site/_htmx/ — `_` prefixed like site/_public, and for the same
|
|
# reason: underscore is the tree you EDIT, its unprefixed twin is the tree that gets
|
|
# GENERATED. htmx-templates/ and htmx-assets/ are outputs. They are wiped and rebuilt on
|
|
# every run, so a file that lives only there lives nowhere — that is how the nav submenus
|
|
# were lost once already. Both targets keep their current paths (HTMX_TEMPLATE_PATH /
|
|
# HTMX_ASSETS_PATH, the PV deliverable); only the sources moved.
|
|
#
|
|
# site/_htmx/templates/** → htmx-templates/**
|
|
# site/_htmx/assets/** → htmx-assets/**
|
|
|
|
framework_templates := rustelo_root + "/crates/foundation/crates/rustelo_pages_htmx/templates"
|
|
impl_templates := source_project + "/crates/pages_htmx/templates"
|
|
site_templates := project_root + "/site/_htmx/templates"
|
|
|
|
framework_htmx := rustelo_root + "/templates/shared/htmx"
|
|
impl_htmx := source_project + "/templates/shared/htmx"
|
|
site_htmx := project_root + "/site/_htmx/assets"
|
|
|
|
# htmx.lock.toml is the framework's provenance record for the vendored runtime, not part
|
|
# of it; .gitkeep only exists to declare an empty consumer layer. Neither ships.
|
|
htmx_assets_exclude := "htmx.lock.toml,.gitkeep"
|
|
|
|
# Assemble htmx-templates: framework → implementation → site/_htmx/templates.
|
|
[no-cd]
|
|
templates:
|
|
nu "{{ project_root }}/scripts/build/assemble-layers.nu" \
|
|
"{{ framework_templates }}" "{{ impl_templates }}" "{{ site_templates }}" \
|
|
--out "{{ project_root }}/htmx-templates" --label "htmx templates"
|
|
|
|
# Materialize the htmx runtime (htmx.min.js + ext/*) into htmx-assets/, sibling of
|
|
# htmx-templates/. Shipped to the PV by content.nu (distro.ncl deliverable) and served at
|
|
# /assets/htmx/* via HTMX_ASSETS_PATH — it lives OUTSIDE site/public (the router mounts
|
|
# /assets/htmx as its own ServeDir). Dev masks this via the local-install wrapper's
|
|
# HTMX_ASSETS_PATH; prod has no such mount.
|
|
#
|
|
# It gets the SAME three layers as the templates: until now it had one (the framework),
|
|
# so an ext/ override shipped by the implementation could never reach the site, and the
|
|
# consumer had nowhere to put one at all.
|
|
# Assemble htmx-assets: framework → implementation → site/_htmx/assets.
|
|
[no-cd]
|
|
htmx-assets:
|
|
nu "{{ project_root }}/scripts/build/assemble-layers.nu" \
|
|
"{{ framework_htmx }}" "{{ impl_htmx }}" "{{ site_htmx }}" \
|
|
--out "{{ project_root }}/htmx-assets" --exclude "{{ htmx_assets_exclude }}" \
|
|
--label "htmx runtime"
|
|
|
|
# Proves the assembly is lossless: reassemble into a temp tree and diff it against the
|
|
# live one. Any difference means the target holds work that exists in no layer, and the
|
|
# next run (or `just sync`, which calls both) will delete it — the fix is to move that
|
|
# work into site/_htmx/, never to skip the run.
|
|
# Gate: assert htmx-templates/ is exactly reproducible from its declared layers.
|
|
[no-cd]
|
|
templates-check:
|
|
#!/usr/bin/env bash
|
|
set -e
|
|
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
|
|
nu "{{ project_root }}/scripts/build/assemble-layers.nu" \
|
|
"{{ framework_templates }}" "{{ impl_templates }}" "{{ site_templates }}" \
|
|
--out "$tmp" >/dev/null
|
|
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 site/_htmx/templates/:"
|
|
diff -rq "{{ project_root }}/htmx-templates" "$tmp" | sed "s|$tmp|<regenerated>|g"
|
|
exit 1
|
|
fi
|
|
|
|
# Gate: assert htmx-assets/ is exactly reproducible from its declared layers.
|
|
[no-cd]
|
|
assets-check:
|
|
#!/usr/bin/env bash
|
|
set -e
|
|
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
|
|
nu "{{ project_root }}/scripts/build/assemble-layers.nu" \
|
|
"{{ framework_htmx }}" "{{ impl_htmx }}" "{{ site_htmx }}" \
|
|
--out "$tmp" --exclude "{{ htmx_assets_exclude }}" >/dev/null
|
|
if diff -r "{{ project_root }}/htmx-assets" "$tmp" >/dev/null 2>&1; then
|
|
echo "assets-check: htmx-assets/ is reproducible — no unwitnessed work"
|
|
else
|
|
echo "assets-check: DRIFT — htmx-assets/ holds work absent from site/_htmx/assets/:"
|
|
diff -rq "{{ project_root }}/htmx-assets" "$tmp" | sed "s|$tmp|<regenerated>|g"
|
|
exit 1
|
|
fi
|
|
|
|
# ── 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 }}"
|
|
|
|
# Every gate this repo owns, in one run. It does NOT stop at the first failure: a build
|
|
# that publishes nothing tells you everything that is wrong, not the first thing.
|
|
#
|
|
# Until now each of these was an orphan recipe — written by the expediente that motivated
|
|
# it and then invoked by nobody. A gate outside the chain is a private ritual, not a gate;
|
|
# it is the exact shape of the bug the 69/58 case is about (a generator existed, no recipe
|
|
# called it, eleven ADRs went unpublished for four weeks). So: `sync-full` ends here, and
|
|
# `just install-hooks` puts it in front of every commit that touches site/.
|
|
[no-cd]
|
|
check:
|
|
#!/usr/bin/env bash
|
|
# The gates resolve some paths RELATIVE to the working directory (the nu generators default
|
|
# to --out-dir "site/public/r"). `check` is [no-cd], and a git hook runs from the REPO ROOT —
|
|
# where those paths point at nothing, and the gate reads "no file" as DRIFT. It reported
|
|
# `glossary-check: DRIFT` over a glossary that reproduces perfectly. There was no drift;
|
|
# there was a missing cd. Anchor it, so `check` means the same thing from anywhere.
|
|
cd "{{ project_root }}"
|
|
fail=()
|
|
gates=(adr-check architecture-check diagram-check graph-page-check glossary-check conceptos-check templates-check assets-check expedientes-check informe-check expedientes-parity posts-check decks-check)
|
|
for gate in "${gates[@]}"; do
|
|
# --justfile, explicitly. The outer call inherits nothing: `check` is [no-cd], and a git
|
|
# hook runs from the REPO ROOT, where there is no justfile — so a bare `just "$gate"`
|
|
# resolved against nothing and every gate came back "does not contain recipe", including
|
|
# the ones that had been green for months. The hook could never have passed; nobody had
|
|
# tried to commit a change under site/ since it was installed.
|
|
just --justfile "{{ justfile() }}" "$gate" > /tmp/gate-$$.log 2>&1 || fail+=("$gate")
|
|
sed 's/^/ /' /tmp/gate-$$.log | grep -v '^\s*nu \|^\s*#!' || true
|
|
rm -f /tmp/gate-$$.log
|
|
done
|
|
if [ ${#fail[@]} -eq 0 ]; then
|
|
echo "check: ${#gates[@]}/${#gates[@]} gates green — ON DISK. This is NOT proof of publication:"
|
|
echo " the server reads its content index INTO MEMORY at startup, so a tree it has"
|
|
echo " never re-read is a tree that, to every reader, does not exist."
|
|
echo " The page is the publication: just check-live <base-url>"
|
|
else
|
|
echo "check: FAILED — ${fail[*]}"
|
|
exit 1
|
|
fi
|
|
|
|
# THE OTHER HALF OF `check`, and the half that a reader would recognise.
|
|
#
|
|
# `check` asks whether the DISK is right. It cannot ask whether the SITE is right, and the
|
|
# difference is not academic: both trees said five cases, the grid rendered four, and only a
|
|
# restart fixed it — because the server had cached its index at boot. A gate that stops at the
|
|
# filesystem certifies a publication that nobody can see.
|
|
#
|
|
# So this is the witness at the end of the chain, and it asks the only question a reader can:
|
|
# IS THE CASE ON THE PAGE? It needs a running site, and if it cannot reach one it FAILS — it does
|
|
# not skip. "I could not look" is a finding, never a pass (adr-072).
|
|
[no-cd]
|
|
check-live base_url="http://localhost:3030":
|
|
#!/usr/bin/env bash
|
|
set -e
|
|
BASE="{{ base_url }}"; BASE="${BASE#*=}"
|
|
just expedientes-served "$BASE"
|
|
just decks-served "$BASE"
|
|
echo "check-live: the served surface renders what the tree says is published"
|
|
|
|
# Gate: every published case is RENDERED AS A CARD on the live grid — counting the card, never
|
|
# the string. The ids also live in the nav, the meta tags and two JSON blobs, so a page-wide grep
|
|
# reports a case as present while its card is missing. That grep is the check that once reported
|
|
# "5 cards" over a grid that was rendering four.
|
|
# ¿El servidor que responde en ese puerto es ESTE sitio? Pide las rutas que site/config/routes/
|
|
# declara y exige el texto que su title_key promete. Un servidor arrancado sin el entorno de
|
|
# run.sh responde 200 a lo que la imagen compartida trae compilado y 404 a lo que aporta el sitio:
|
|
# medir contra él da diagnósticos falsos con toda la confianza del mundo. Corre sola, y de
|
|
# preflight dentro de expedientes-served.
|
|
[no-cd]
|
|
server-check base_url="http://localhost:3030":
|
|
#!/usr/bin/env bash
|
|
set -e
|
|
BASE="{{ base_url }}"; BASE="${BASE#*=}"
|
|
nu "{{ project_root }}/scripts/build/check-live-server.nu" --base-url "$BASE" --root "{{ project_root }}"
|
|
|
|
[no-cd]
|
|
expedientes-served base_url="http://localhost:3030":
|
|
#!/usr/bin/env bash
|
|
set -e
|
|
BASE="{{ base_url }}"; BASE="${BASE#*=}"
|
|
nu "{{ project_root }}/scripts/build/check-served-live.nu" --base-url "$BASE" --root "{{ project_root }}"
|
|
|
|
# Install the pre-commit gate into this repo (sets core.hooksPath). Undo with:
|
|
# git config --unset core.hooksPath
|
|
[no-cd]
|
|
install-hooks:
|
|
#!/usr/bin/env bash
|
|
set -e
|
|
# Absolute: the repo root is outreach/, the justfile lives in outreach/site/, and a
|
|
# relative hooksPath would resolve against the wrong one of the two.
|
|
git -C "{{ project_root }}" config core.hooksPath "{{ project_root }}/scripts/hooks"
|
|
chmod +x "{{ project_root }}/scripts/hooks/pre-commit"
|
|
echo "install-hooks: pre-commit → just check (only when the commit touches site/)"
|
|
|
|
# ── 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.
|
|
# check LAST — a build that cannot assert what it just produced is a reporter, and in
|
|
# this repo the check decides, never the reporter (ADR-066).
|
|
# Requires ONTOREF_NICKEL_IMPORT_PATH in .env.
|
|
# Full resync: projections → content indexes → derived artifacts → serve tree.
|
|
# `diagram` sits before `about-json` because the About graph block references its output — the
|
|
# recipe's own comment says so, and for that same reason it was left OUT of this chain entirely:
|
|
# it ran by hand. `graph` here is content_graph.json, a different artifact; the name collision is
|
|
# exactly what let the omission read as covered.
|
|
[no-cd]
|
|
sync-full: templates htmx-assets rotator-assets adr-pages expedientes informe catalog-content decks content glossary conceptos graph adr-map diagram about-json sync-artifacts check
|
|
|
|
# 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
|