diff --git a/site/.gitignore b/site/.gitignore
index 5edfe54..fbca28c 100644
--- a/site/.gitignore
+++ b/site/.gitignore
@@ -17,10 +17,11 @@ site/public/r/
provisioning/.content-packs/
# ── Generated: runtime + build outputs ───────────────────────────────────────
-# htmx-templates/ and htmx-assets/ are ASSEMBLED (`just templates` rm -rf's them).
-# They stay tracked until templates-overlay/ is the declared source for every
-# hand-authored template and `just templates-check` proves regeneration is a
-# no-op — ignoring a generated tree that still holds unique work loses it.
+# htmx-templates/ and htmx-assets/ are ASSEMBLED (`just templates` / `just htmx-assets`
+# rm -rf them). They stay tracked until site/_htmx/ is the declared source for every
+# hand-authored template and asset, and `just templates-check` + `just assets-check`
+# prove regeneration is a no-op — ignoring a generated tree that still holds unique
+# work loses it.
cache/
data/
logs_*.out
diff --git a/site/justfile b/site/justfile
index 01dac09..d66c2cc 100644
--- a/site/justfile
+++ b/site/justfile
@@ -152,12 +152,18 @@ deck-new source id title_es="" slug_es="" title_en="" slug_en="" source_en="":
# 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.
+#
+# The ids come from lib/variants.mjs, the one authority on the deck→artifact key. This recipe used
+# to re-derive them with `jq -r '.decks[].id'`, from before a bilingual deck became one source (and
+# one asset dir) PER LANGUAGE: it asked for `ai-infra`, which holds thumbnails and no bundle, while
+# the pages load `ai-infra-es`. It crashed on the deck built after the split and passed on the one
+# whose pre-split bundle still sat on disk — gating a file no page loads. Ask the authority.
[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
+ for id in $(node ../scripts/decks/lib/variants.mjs --ids); do
node ../scripts/decks/gates/artifact.mjs "$id"
done
diff --git a/site/site/public/docs/case-files/protocol.html b/site/site/public/docs/case-files/protocol.html
index 2c239cb..27e571c 100644
--- a/site/site/public/docs/case-files/protocol.html
+++ b/site/site/public/docs/case-files/protocol.html
@@ -132,7 +132,7 @@
Session protocol · Case-file series
The session protocol
This protocol was not written: it is projected from the case files' replays — the session replayed with protocol that every case leaves written and typed. Change a case and this page changes with it. Editing it by hand is impossible by construction, because a hand-kept checklist is the next drift.
- Replays 4Cases 11
+ Replays 4Cases 12
@@ -262,6 +262,7 @@ the one you deduce by reading the file.
| 404-PAP-bis | ⊘ The relapse had a cause, and it was not memory: the first fix had never gone back to the template, so it was not a relapse but reinfection from the source. It has gone back now — the backport is in both templates and the hand-written list is forbidden by a typed constraint. What remains is the usual: nothing runs it. Rustelo's justfile calls no validator, so the gate that would prevent the next reinfection exists and is silent. A just recipe in its chain would settle it. Case 8/0 tells why the fix took months to come home. |
| 1/1852 | ⊘ The frontier stays open where data becomes text: whatever is written past it is published on someone's word. A contract over the EMITTED artifact — a schema for the generated resource — would settle it, not one over its source alone. |
| 8/0 | ⊘ Nothing runs the new constraint: the recipe that runs all 23 and reports per constraint is unwritten — and without it, this rots again exactly the same way. A just recipe in rustelo's chain would settle it, kept out of check-strict until the 11 real hits are triaged. And four remain ownerless: the third list in htmx_pages.rs (3 hits), the 11 real hits of no-hardcoded-route-paths, templates/ unversioned — the gravest, and no ADR says whether it is policy or oversight — and an instance ↔ template drift-check, which has neither mechanism nor owner. |
+| 8/4 | ⊘ «--artifacts» declared list<string> and unreachable from the CLI wrapper — the five historical runs carry an empty column because of it; settled by accepting a comma-separated string or a repeatable option. «verify» in zero of the mode's eleven steps; adding it to the three with an oracle is approved (assemble_case: the .ncl exports; render: the .md reproduces; publish: expedientes-check). The howto's step list is not checked against the DAG; settled by a gate comparing its ids against mode show --fmt json. And the underlying one, with no gate possible: a sentence is not an artifact — the only oracle of the human↔agent channel is the human. |
diff --git a/site/site/public/docs/expedientes/protocolo.html b/site/site/public/docs/expedientes/protocolo.html
index b396625..6c52763 100644
--- a/site/site/public/docs/expedientes/protocolo.html
+++ b/site/site/public/docs/expedientes/protocolo.html
@@ -132,7 +132,7 @@
Protocolo de sesión · Serie Expedientes
El protocolo de sesión
Este protocolo no se redactó: se proyecta de los replays de los expedientes — la sesión repetida con protocolo que cada caso deja escrita y tipada. Cambia un caso y esta página cambia con él. Editarla a mano es imposible por construcción, porque una checklist mantenida a mano es la siguiente deriva.
- Replays 4Casos 11
+ Replays 4Casos 12
@@ -262,6 +262,7 @@ deduce leyendo el fichero.
| 404-PAP-bis | ⊘ La recaída tenía causa, y no era la memoria: el primer arreglo nunca había vuelto a la plantilla, así que no era recaída sino reinfección desde la fuente. Ya volvió — el backport está en las dos plantillas y la lista a mano está prohibida por una restricción tipada. Lo que queda es lo de siempre: nada la ejecuta. El justfile de rustelo no llama a ningún validador, así que la puerta que impediría la próxima reinfección existe y está callada. La saldaría una receta just en su cadena. El expediente 8/0 cuenta por qué el arreglo tardó meses en volver a casa. |
| 1/1852 | ⊘ La frontera sigue abierta donde el dato se vuelve texto: lo que se escribe tras ella se publica bajo palabra. Lo saldaría un contrato sobre el artefacto EMITIDO —un esquema del recurso generado—, no sólo sobre su fuente. |
| 8/0 | ⊘ Nada ejecuta la restricción nueva: la receta que corra las 23 y reporte por restricción está sin escribir — y sin ella, esto se vuelve a pudrir exactamente igual. La saldaría una receta just en la cadena de rustelo, fuera de check-strict hasta que los 11 hits reales estén triados. Y quedan cuatro sin dueño: la tercera lista de htmx_pages.rs (3 hits), los 11 hits reales de no-hardcoded-route-paths, templates/ sin versionar —la más grave, y ningún ADR dice si es política o descuido—, y un drift-check instancia ↔ plantillas, que no tiene ni mecanismo ni dueño. |
+| 8/4 | ⊘ «--artifacts» declarado list<string> e inalcanzable desde el envoltorio del CLI — los cinco runs históricos llevan la columna vacía por eso; lo salda aceptar cadena separada por comas o una opción repetible. «verify» en cero de los once pasos del modo; aprobado añadirlo a los tres con oráculo (assemble_case: el .ncl exporta; render: el .md reproduce; publish: expedientes-check). La lista de pasos del howto no se coteja contra el DAG; lo salda una puerta que compare sus ids contra mode show --fmt json. Y la de fondo, sin puerta posible: una frase no es un artefacto — el único oráculo del canal humano↔agente es el humano. |
diff --git a/site/site/rbac.ncl b/site/site/rbac.ncl
index 316d1dd..1384a2d 100644
--- a/site/site/rbac.ncl
+++ b/site/site/rbac.ncl
@@ -257,7 +257,18 @@ let E = {
# Per-post engagement (views/ratings/messages) — public, anonymous by design.
{ resource = "endpoint", pattern = "/api/htmx/post/*", outcome = "allow", methods = ["GET", "POST"], effects = [] },
# Contact form — public submission, no auth required.
+ # Superseded by the declarative endpoint below; kept while the old handler still exists.
{ resource = "endpoint", pattern = "/api/htmx/contact", outcome = "allow", methods = ["POST"], effects = [] },
+ # Declarative forms — one endpoint, one policy. A form is DECLARED in
+ # site/config/forms/.ncl and served by POST /api/htmx/form/: no Rust, no rebuild.
+ # The handler refuses an id that is not [a-z0-9-] and refuses a posted field the spec does
+ # not declare, so opening the pattern does not open a hole — the spec is the boundary.
+ { resource = "endpoint", pattern = "/api/htmx/form/*", outcome = "allow", methods = ["POST"], effects = [] },
+ # Newsletter subscription — public, anonymous by design. The handler RELAYS to the provider in
+ # newsletter.ncl and answers with a fragment, so the reader stays on the site and learns
+ # whether it worked. The list is read from that config, never from the request: opening this
+ # pattern lets a visitor subscribe, not choose which list they land on.
+ { resource = "endpoint", pattern = "/api/htmx/newsletter", outcome = "allow", methods = ["POST"], effects = [] },
# Leptos server functions for OTP auth flow — hash suffix varies per build,
# glob * matches the numeric hash without requiring a path separator.
{ resource = "endpoint", pattern = "/api/server_request_otp*", outcome = "allow", methods = ["POST"], effects = [] },