site: el post y el deck del expediente 8/4, y las puertas que no miraban
Proyecta el caso `la-disciplina-sin-disparador` (8/4) a dos superficies
públicas, en es/en, y arregla cinco mecanismos que pasaban en verde sin
medir lo que decían medir.
Contenido
· blog/{es,en}/ontoref/a-rule-without-a-trigger-is-a-sign — «Una regla sin
disparador es un cartel». Anclado en la tensión enforcement-vs-emergence
(212 Hard declaradas, 95 violadas, nada bloquea); no colapsa el Spiral:
ni «te obliga a cumplir» ni «emergencia gratis».
· deck disciplina-sin-disparador, 12 slides, fuente Slidev por idioma
→ /recursos/presentaciones/una-regla-sin-disparador
→ /resources/decks/a-rule-without-a-trigger
Puertas
· decks-served derivaba de dos URLs escritas a mano, ambas del mismo deck.
Había cuatro: salía exit 0 sin cargar tres. Ahora deriva del manifiesto
(8 páginas, 8 verificadas) y aborta si el manifiesto declara cero, que es
un hallazgo y no un pase. Es coverage-superset-of-extent (adr-072) roto
en la propia cadena que lo debía aplicar.
· decks.ncl declara `kind = { es = "recursos", en = "resources" }`: vivía
solo en un comentario, y por eso la URL no se podía calcular.
Reproducibilidad
· Cuatro sidecars llevaban image_url/read_time/sort_order que su .md no
tiene; regenerar los perdía, y dos posts se quedaban sin imagen. La cura
va en el .md —la fuente— no en el .ncl: ahora se reproducen solos.
· og cards de los posts y del deck, compuestas por gen-og-cards (script
huérfano: ninguna receta lo llama).
Verificado: posts-check, decks-check (A-D), og-cards-check y server-check
pasan. decks-served pasa las 8 puertas en las 2 páginas nuevas — y deja en
rojo rustikon[es], gate J: slides #27 y #31 recortadas en los 4 viewports,
un defecto preexistente que la puerta no veía porque no la miraba. Ese rojo
se queda: es la puerta haciendo su trabajo.
|
|
@ -1,6 +1,6 @@
|
|||
# AUTO-GENERATED — do not edit manually
|
||||
# Scope: blog/en
|
||||
# Updated: 2026-06-24T02:32:42Z
|
||||
# Updated: 2026-07-17T05:56:25Z
|
||||
# Regen: nu scripts/content/generate-ncl-index.nu
|
||||
|
||||
std.array.flatten [
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# AUTO-GENERATED — do not edit manually
|
||||
# Scope: blog/en/engineering
|
||||
# Updated: 2026-06-24T02:32:42Z
|
||||
# Updated: 2026-07-17T05:56:25Z
|
||||
# Regen: nu scripts/content/generate-ncl-index.nu
|
||||
|
||||
[
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ schema.make_post {
|
|||
tags = ["release-engineering", "ci", "oci", "nickel", "supply-chain", "rust"],
|
||||
read_time = "8 min read",
|
||||
sort_order = 1,
|
||||
image_url = "/images/ontoref-release-one-model-post.webp",
|
||||
graph = {
|
||||
implements = ["oci-distribution", "adr-038", "adr-029", "adr-017", "protocol-not-runtime", "formalization-vs-adoption"],
|
||||
related_to = [],
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
# AUTO-GENERATED — do not edit manually
|
||||
# Scope: blog/en/ontoref
|
||||
# Updated: 2026-06-24T02:32:42Z
|
||||
# Updated: 2026-07-17T05:56:25Z
|
||||
# Regen: nu scripts/content/generate-ncl-index.nu
|
||||
|
||||
[
|
||||
import "./a-rule-without-a-trigger-is-a-sign.ncl",
|
||||
import "./ai-knowledge-tool-who-keeps-it-alive.ncl",
|
||||
import "./context-declared-not-filled.ncl",
|
||||
import "./dags-everywhere-none-know-what-they-are.ncl",
|
||||
import "./one-protocol-multiple-subjects.ncl",
|
||||
import "./the-seven-sins-of-ai-agents.ncl",
|
||||
import "./trust-is-an-output-not-an-input.ncl",
|
||||
import "./your-ontology-should-live-with-your-code.ncl",
|
||||
import "./context-declared-not-filled.ncl",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
---
|
||||
id: a-rule-without-a-trigger-is-a-sign
|
||||
title: "A rule without a trigger is a sign"
|
||||
slug: a-rule-without-a-trigger-is-a-sign
|
||||
subtitle: "Eight false assertions in four hours. Four mechanisms that already forbade them, in writing. No alarm."
|
||||
excerpt: "No rules were missing. There were four — written, canonical, consultable — and not one of them ran. The distance between a rule that is written and a rule that applies has a name, and it is the only thing separating discipline from documentation."
|
||||
author: Jesús Pérez
|
||||
date: 2026-07-17
|
||||
published: true
|
||||
featured: false
|
||||
category: ontoref
|
||||
tags:
|
||||
- developer
|
||||
- agents
|
||||
- verification
|
||||
- ontoref
|
||||
image_url: "/images/ontoref-a-rule-without-a-trigger-is-a-sign.webp"
|
||||
og_image: "/images/og/ontoref-a-rule-without-a-trigger-is-a-sign.png"
|
||||
sort_order: 8
|
||||
---
|
||||
|
||||
<picture>
|
||||
<source srcset="/images/ontoref-a-rule-without-a-trigger-is-a-sign.avif" type="image/avif">
|
||||
<source srcset="/images/ontoref-a-rule-without-a-trigger-is-a-sign.webp" type="image/webp">
|
||||
<img src="/images/ontoref-a-rule-without-a-trigger-is-a-sign.webp" alt="A rule without a trigger is a sign" width="1536" height="1024" loading="lazy">
|
||||
</picture>
|
||||
|
||||
A four-hour-and-seven-minute agent session said eight false things with complete composure. None of them reached disk. They all died in the conversation, and seven of them had to be killed by the human, pointing.
|
||||
|
||||
The interesting part is not that it said them. It is that **four written mechanisms already forbade them** — all four older than the first sentence, all four correct, all four consultable in a second. Not one fired.
|
||||
|
||||
This is the case, and why the lesson is not "be more careful".
|
||||
|
||||
## The weapon: the negative assertion
|
||||
|
||||
The eight sentences, verbatim and timestamped, from the session's log:
|
||||
|
||||
```
|
||||
14:06 «the English glosses are not written» → there were 20; a generator comment was read
|
||||
14:06 «the schema has nowhere to hang a gloss» → term.ncl: definition | i18n_str, bilingual
|
||||
14:16 «no such command, and there never was» → describe term existed; the grep read only crates/*.rs
|
||||
14:39 a server started by hand, never verified → 3 false diagnoses, chained
|
||||
14:52 «the language switcher sends you to a 404» → it translated fine; the URL was invented
|
||||
15:23 «the lexicon's 26 are left with no surface» → gen-nota-lenguaje.nu → /sobre-el-idioma
|
||||
15:32 «the tooltip is blocked by the contract» → reason_type carried 'SourceLanguage for that
|
||||
17:25 «It is this session» → the id was sitting in the scratchpad path
|
||||
```
|
||||
|
||||
The weapon is none of the eight sentences: it is **the grammatical form they share**. Seven are negative — "it is not written", "no such command", "left with no surface" — and a negative assertion delivered with confidence is the perfect lie, because the one saying it only knows that **he** did not find it, and it sounds exactly like someone who actually looked.
|
||||
|
||||
The eighth is the same error with the sign flipped: "It is this session", said because the date matched, with the true identifier sitting in the scratchpad path since the first message. It was said **inside the very step that exists to prevent it**.
|
||||
|
||||
Not one of the eight ever produced an error message. The channel where they were said has no gates.
|
||||
|
||||
## The four that did not fire
|
||||
|
||||
```
|
||||
ondaod in the CLAUDE.md, with a consultable procedure
|
||||
assertion ⇒ witness ADR-072, accepted, with the anti-pattern named
|
||||
qa:expediente-howto 267 lines; it warned about this session's exact traps
|
||||
mode:generate-expediente 11 steps with their actor; prints itself with --dry-run
|
||||
```
|
||||
|
||||
This is where the case stops being an anecdote about a careless agent. The three explanations anyone would offer each have an alibi, and all three are verified against the log:
|
||||
|
||||
**"It did not know the rules."** False: it knew all of them. The startup hook enumerated the 29 modes — `generate-expediente` among them — and the named tensions; the project's `CLAUDE.md` carries the entry protocol with `describe capabilities` as step 1. All of it was in context at 13:19:10, before the first tool call. The first CLI invocation came at 14:14:48 — fifty-five minutes later, and because the human asked. It was not ignorance: **knowing fires nothing**.
|
||||
|
||||
**"The mechanisms did not exist yet."** All four predate the first assertion. They existed, they were correct, they were consultable. And this alibi is precisely what turns an oversight into a case.
|
||||
|
||||
**"The rules were badly written."** When they were finally read, they worked on the first try. The howto warned about the exact traps the session fell into: count the card and not the chain, verify the page and never the file, start by the declared path or every measurement is false. They did not need interpreting or correcting. They needed reading.
|
||||
|
||||
That leaves the fourth, and it is nobody's slip: **"a written rule applies itself"**. It is the default belief of anyone who has drafted a rule and watched it come out well written. You publish it, and you file away the feeling that it is now handled.
|
||||
|
||||
The mode went past the screen as a filename in an `ls` at 14:28. Nobody opened it until 17:16 — three hours and fifty-seven minutes in, pointed at by the human.
|
||||
|
||||
A mechanism like that is not a gate. It is a sign by the road, and signs do not brake for anyone.
|
||||
|
||||
## This already had a name, and a number
|
||||
|
||||
The `enforcement-vs-emergence` tension in <a href="/about" class="brand-site">ontoref</a>'s own ontology says it without anaesthetic, and says it about the project itself:
|
||||
|
||||
> A declared-but-unenforced constraint is not a floor, it is a description of one. […] Enforcement that cannot be trusted to fail is indistinguishable from no enforcement, and it is more dangerous, because it is believed.
|
||||
|
||||
And it carries the number: of **212 Hard constraints declared** across the ADR set, **95 report as violated and nothing blocks on them**. `adr validate` is run, read, and ignored.
|
||||
|
||||
The four-hour case is that number's anecdote. It is not an agent defect: it is the shape a floor takes when it is only described.
|
||||
|
||||
## The fix was not a new rule
|
||||
|
||||
None of the four rules changed. What they got was a **trigger** — and each one needed a different kind:
|
||||
|
||||
```sh
|
||||
# 1. A hook — the rule travels toward the agent, not the other way round.
|
||||
# On every prompt, if the task is already governed, the QA that governs it is served whole.
|
||||
"UserPromptSubmit": nu ~/.claude/hooks/onre-prompt-orient.nu
|
||||
|
||||
# 2. A gate — the degraded server dies in one second, not in three diagnoses.
|
||||
just server-check
|
||||
|
||||
# 3. A contract — one mode id resolves to ONE DAG (two were answering for one).
|
||||
# shared.nu#all-mode-files — dedupe by id: 29 ids → 29 DAGs
|
||||
|
||||
# 4. A schema — a case that stays silent about its lesson does not export.
|
||||
# LessonAccounted: verified pointers to whatever executes the lesson, or the debt, named.
|
||||
```
|
||||
|
||||
A hook, a gate, a contract, a schema. Not one of them asks anybody to remember anything — which is the only requirement that matters, because remembering is exactly what failed eight times.
|
||||
|
||||
The first already fired in production: the session that wrote this case's file received the howto governing its task in its first prompt, instead of discovering it four hours later.
|
||||
|
||||
## What this does not fix
|
||||
|
||||
`--artifacts` remains unreachable from the CLI wrapper, and the historical runs carry that column empty because of it. `verify` exists in zero of the mode's eleven steps: with no oracle per step, a step's status is whatever the agent says it is. The howto's step list is not checked against the DAG, and it has already lied once.
|
||||
|
||||
And the deep one, the one with no possible gate: **a sentence is not an artifact**. This project's files have oracles to spare — contracts that reject, gates that fail, schemas that refuse to export. The conversation has none. That is why in four hours not one lie reached disk and eight were spoken: what gets written passes through gates, what gets said passes through nothing.
|
||||
|
||||
The only oracle of the human↔agent channel is the human. That is not an excuse. It is the thesis.
|
||||
|
||||
## The question
|
||||
|
||||
The honest answer to "with <a href="/about" class="brand-site">ontoref</a>, wouldn't this have been avoided?" is uncomfortable, and it is the only one that holds: **<a href="/about" class="brand-site">ontoref</a>, declared, was already there — and it was not enough**. It was there four times. Until each rule got its trigger.
|
||||
|
||||
Neither comfortable position is true. The friction is not the product: selling "this makes you comply" mistakes the scaffolding for the building, and the friction is real but transitional. And the opposite is no better: coherence does not become gravity on its own. With no floor beneath it, emergence is not *wu wei* — it is drift. Both at once, or neither is honest.
|
||||
|
||||
So the question the case leaves is not "is it written?". That one was answered yes, four times over.
|
||||
|
||||
It is **"what fires it?"**.
|
||||
|
||||
If the answer is "somebody will remember", you already know what you have. And the day it matters, a human will point at it, with exactly the patience required.
|
||||
|
||||
In this case, eight times.
|
||||
|
||||
---
|
||||
|
||||
*This article projects from case file 8/4, "the discipline without a trigger" — a typed case whose every figure comes from a command run against the session's log, never from anyone's memory. The full case file, with the lineup and the alibis, is in the [case series](/expedientes).*
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# Generated from a-rule-without-a-trigger-is-a-sign.md
|
||||
# Schema: ../../../../../../../../../../jesusperezlorenzo/.local/share/rustelo/nickel/content/metadata/post_metadata.ncl
|
||||
# Regenerate: nu scripts/content/md-to-ncl.nu --force
|
||||
|
||||
let schema = import "../../../../../../../../../../jesusperezlorenzo/.local/share/rustelo/nickel/content/metadata/post_metadata.ncl" in
|
||||
|
||||
schema.make_post {
|
||||
id = "a-rule-without-a-trigger-is-a-sign",
|
||||
title = "A rule without a trigger is a sign",
|
||||
slug = "a-rule-without-a-trigger-is-a-sign",
|
||||
subtitle = "Eight false assertions in four hours. Four mechanisms that already forbade them, in writing. No alarm.",
|
||||
excerpt = "No rules were missing. There were four — written, canonical, consultable — and not one of them ran. The distance between a rule that is written and a rule that applies has a name, and it is the only thing separating discipline from documentation.",
|
||||
date = "2026-07-17",
|
||||
category = "ontoref",
|
||||
tags = ["developer", "agents", "verification", "ontoref"],
|
||||
sort_order = 8,
|
||||
image_url = "/images/ontoref-a-rule-without-a-trigger-is-a-sign.webp",
|
||||
graph = {
|
||||
implements = ["enforcement-vs-emergence", "internal-coherence-enforced", "sufficient-verification", "governed-delivery", "witness-as-axis-seam", "audit-harness"],
|
||||
related_to = ["the-seven-sins-of-ai-agents", "trust-is-an-output-not-an-input", "context-declared-not-filled", "dags-everywhere-none-know-what-they-are"],
|
||||
},
|
||||
}
|
||||
|
|
@ -16,8 +16,13 @@ featured: true
|
|||
category: "ontoref"
|
||||
tags: ["ontoref", "context-engineering", "library-science", "warrant", "knowledge-graphs", "developer", "knowledge-works"]
|
||||
|
||||
# Display
|
||||
read_time: "6 min read"
|
||||
sort_order: 7
|
||||
|
||||
# Image (card thumbnail + in-post featured)
|
||||
thumbnail: "/images/ontoref-context-declared-not-filled.webp"
|
||||
image_url: "/images/ontoref-context-declared-not-filled.webp"
|
||||
og_image: "/images/og/ontoref-context-declared-not-filled.png"
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -22,4 +22,4 @@ schema.make_post {
|
|||
implements = ["witness-as-axis-seam", "adr-050", "adr-057", "adr-063", "adr-066"],
|
||||
related_to = ["trust-is-an-output-not-an-input", "ai-knowledge-tool-who-keeps-it-alive"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ category_published: true
|
|||
|
||||
# Image (card thumbnail + in-post featured)
|
||||
thumbnail: "/images/ontoref-one-protocol-multiple-subjects.webp"
|
||||
image_url: "/images/ontoref-one-protocol-multiple-subjects.webp"
|
||||
og_image: "/images/og/ontoref-one-protocol-multiple-subjects.png"
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -22,4 +22,4 @@ schema.make_post {
|
|||
implements = ["ontology-vs-reflection", "ontology-axis-substance", "reflection-axis-act", "personal-ontology-schemas", "level-hierarchy-resolution", "domain-extension-system", "content-vehicle-convergence", "adr-001"],
|
||||
related_to = ["dags-everywhere-none-know-what-they-are", "your-ontology-should-live-with-your-code", "ai-knowledge-tool-who-keeps-it-alive"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ schema.make_post {
|
|||
subtitle = "Why almost every framework answers agent failure with more human process — and why ontoref graduates decisions through gates that verify themselves",
|
||||
excerpt = "Agents don't fail at random: they fail with seven systematic vices that all survive the 'looks correct' test. The instinct is to add process — a PEP, a KEP, a committee — but every graduation stage rests on a human who approves it, and the agent's speed outruns the human you put at the gate. This is the honest comparison: what ontoref's ADRs inherit from PEP and KEP, and where they surpass both with witnessed, decidable, bounded-slice graduation criteria.",
|
||||
date = "2026-06-30",
|
||||
featured = false,
|
||||
category = "ontoref",
|
||||
tags = ["ontoref", "ai-agents", "verification", "adr", "governance", "pep-kep"],
|
||||
read_time = "6 min read",
|
||||
|
|
@ -22,4 +21,4 @@ schema.make_post {
|
|||
implements = ["enforcement-vs-emergence", "adr-lifecycle", "governed-delivery", "sufficient-verification", "witness-as-axis-seam", "adr-069"],
|
||||
related_to = ["trust-is-an-output-not-an-input", "context-declared-not-filled", "one-protocol-multiple-subjects"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# AUTO-GENERATED — do not edit manually
|
||||
# Scope: blog/es
|
||||
# Updated: 2026-06-24T02:32:42Z
|
||||
# Updated: 2026-07-17T05:56:25Z
|
||||
# Regen: nu scripts/content/generate-ncl-index.nu
|
||||
|
||||
std.array.flatten [
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# AUTO-GENERATED — do not edit manually
|
||||
# Scope: blog/es/engineering
|
||||
# Updated: 2026-06-24T02:32:42Z
|
||||
# Updated: 2026-07-17T05:56:25Z
|
||||
# Regen: nu scripts/content/generate-ncl-index.nu
|
||||
|
||||
[
|
||||
|
|
|
|||
|
|
@ -16,4 +16,5 @@ schema.make_post {
|
|||
tags = ["release-engineering", "ci", "oci", "nickel", "supply-chain", "rust"],
|
||||
read_time = "8 min de lectura",
|
||||
sort_order = 1,
|
||||
image_url = "/images/ontoref-release-one-model-post.webp",
|
||||
}
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
# AUTO-GENERATED — do not edit manually
|
||||
# Scope: blog/es/ontoref
|
||||
# Updated: 2026-06-24T02:32:42Z
|
||||
# Updated: 2026-07-17T05:56:25Z
|
||||
# Regen: nu scripts/content/generate-ncl-index.nu
|
||||
|
||||
[
|
||||
import "./a-rule-without-a-trigger-is-a-sign.ncl",
|
||||
import "./ai-knowledge-tool-who-keeps-it-alive.ncl",
|
||||
import "./context-declared-not-filled.ncl",
|
||||
import "./dags-everywhere-none-know-what-they-are.ncl",
|
||||
import "./one-protocol-multiple-subjects.ncl",
|
||||
import "./the-seven-sins-of-ai-agents.ncl",
|
||||
import "./trust-is-an-output-not-an-input.ncl",
|
||||
import "./your-ontology-should-live-with-your-code.ncl",
|
||||
import "./context-declared-not-filled.ncl",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
---
|
||||
id: a-rule-without-a-trigger-is-a-sign
|
||||
title: "Una regla sin disparador es un cartel"
|
||||
slug: "una-regla-sin-disparador-es-un-cartel"
|
||||
subtitle: "Ocho afirmaciones falsas en cuatro horas. Cuatro mecanismos que ya las prohibían por escrito. Ninguna alarma."
|
||||
excerpt: "No faltaban reglas. Había cuatro, escritas, canónicas y consultables — y ninguna se ejecutó. La distancia entre una regla escrita y una regla que se aplica tiene nombre, y es lo único que separa la disciplina de la documentación."
|
||||
author: Jesús Pérez
|
||||
date: 2026-07-17
|
||||
published: true
|
||||
featured: false
|
||||
category: ontoref
|
||||
tags:
|
||||
- developer
|
||||
- agentes
|
||||
- verificacion
|
||||
- ontoref
|
||||
image_url: "/images/ontoref-a-rule-without-a-trigger-is-a-sign-es.webp"
|
||||
og_image: "/images/og/ontoref-a-rule-without-a-trigger-is-a-sign-es.png"
|
||||
sort_order: 8
|
||||
---
|
||||
|
||||
<picture>
|
||||
<source srcset="/images/ontoref-a-rule-without-a-trigger-is-a-sign-es.avif" type="image/avif">
|
||||
<source srcset="/images/ontoref-a-rule-without-a-trigger-is-a-sign-es.webp" type="image/webp">
|
||||
<img src="/images/ontoref-a-rule-without-a-trigger-is-a-sign-es.webp" alt="Una regla sin disparador es un cartel" width="1536" height="1024" loading="lazy">
|
||||
</picture>
|
||||
|
||||
Una sesión de agente de cuatro horas y siete minutos dijo ocho cosas falsas con total aplomo. Ninguna llegó a disco. Todas murieron en la conversación, y siete de ellas las tuvo que matar el humano señalándolas con el dedo.
|
||||
|
||||
Lo interesante no es que las dijera. Es que había **cuatro mecanismos escritos que las prohibían**, los cuatro anteriores a la primera frase, los cuatro correctos, los cuatro consultables en un segundo. Ninguno disparó.
|
||||
|
||||
Este es el caso, y por qué la lección no es «hay que tener más cuidado».
|
||||
|
||||
## El arma: la afirmación negativa
|
||||
|
||||
Las ocho frases, literales y con hora, sacadas del log de la sesión:
|
||||
|
||||
```
|
||||
14:06 «las glosas inglesas no están escritas» → había 20; se leyó un comentario del generador
|
||||
14:06 «el esquema no tiene dónde colgar una glosa» → term.ncl: definition | i18n_str, bilingüe
|
||||
14:16 «no hay tal comando, y no lo hubo» → describe term existía; el grep miró solo crates/*.rs
|
||||
14:39 un servidor arrancado a mano, sin verificar → 3 diagnósticos falsos encadenados
|
||||
14:52 «el conmutador de idioma manda a 404» → traducía bien; la URL estaba inventada
|
||||
15:23 «las 26 del léxico se quedan sin superficie» → gen-nota-lenguaje.nu → /sobre-el-idioma
|
||||
15:32 «el tooltip está bloqueado por contrato» → reason_type traía 'SourceLanguage para eso
|
||||
17:25 «Es esta sesión» → el id estaba en la ruta del scratchpad
|
||||
```
|
||||
|
||||
El arma no es ninguna de las ocho frases: es **la forma gramatical que comparten**. Siete son negativas — «no está escrito», «no hay tal comando», «se quedan sin superficie» — y una afirmación negativa dicha con seguridad es la mentira perfecta, porque quien la dice solo sabe que **él** no lo ha encontrado, y suena exactamente igual que quien sí buscó.
|
||||
|
||||
La octava es el mismo error con el signo cambiado: «Es esta sesión», dicho porque la fecha cuadraba, con el identificador verdadero escrito en la ruta del scratchpad desde el primer mensaje. Se dijo **dentro del paso que existe para impedirlo**.
|
||||
|
||||
Ninguna de las ocho produjo jamás un mensaje de error. El canal donde se dijeron no tiene puertas.
|
||||
|
||||
## Los cuatro que no dispararon
|
||||
|
||||
```
|
||||
ondaod en el CLAUDE.md, con procedimiento consultable
|
||||
afirmación ⇒ testigo ADR-072, aceptado, con el anti-patrón nombrado
|
||||
qa:expediente-howto 267 líneas; advertía las trampas exactas de esta sesión
|
||||
mode:generate-expediente 11 pasos con actor; se imprime con --dry-run
|
||||
```
|
||||
|
||||
Aquí es donde el caso deja de ser una anécdota sobre un agente descuidado. Las tres explicaciones que cualquiera daría tienen coartada, y las tres están verificadas contra el log:
|
||||
|
||||
**«No conocía las reglas.»** Falso: las conocía todas. El hook de arranque enumeró los 29 modos —`generate-expediente` entre ellos— y las tensiones con nombre; el `CLAUDE.md` del proyecto trae el protocolo de entrada con `describe capabilities` como paso 1. Todo eso estaba en contexto a las 13:19:10, antes de la primera herramienta. La primera invocación del CLI llegó a las 14:14:48 — cincuenta y cinco minutos después, y porque el humano preguntó. No fue ignorancia: **saber no dispara nada**.
|
||||
|
||||
**«Los mecanismos no existían todavía.»** Los cuatro son anteriores a la primera afirmación. Existían, eran correctos, eran consultables. Y esta coartada es precisamente lo que convierte un descuido en un caso.
|
||||
|
||||
**«Las reglas estaban mal escritas.»** Cuando por fin se leyeron, funcionaron a la primera. El howto advertía las trampas exactas en las que la sesión cayó: cuenta la tarjeta y no la cadena, verifica la página y nunca el fichero, arranca por la vía declarada o toda medición es falsa. No hizo falta interpretarlas ni corregirlas. Hizo falta leerlas.
|
||||
|
||||
Queda la cuarta, y no es el despiste de nadie: **«una regla escrita se aplica sola»**. Es la creencia por defecto de cualquiera que haya redactado una regla y la haya visto quedar bien escrita. Se publica, y se archiva la sensación de que ya está resuelto.
|
||||
|
||||
El modo pasó por la pantalla como nombre de fichero en un `ls` a las 14:28. Nadie lo abrió hasta las 17:16 — tres horas y cincuenta y siete minutos dentro, señalado por el humano.
|
||||
|
||||
Un mecanismo así no es una puerta. Es un cartel junto a la carretera, y los carteles no frenan a nadie.
|
||||
|
||||
## Esto ya tenía nombre, y tenía cifra
|
||||
|
||||
La tensión `enforcement-vs-emergence` de la ontología de <a href="/about" class="brand-site">ontoref</a> lo dice sin anestesia, y lo dice del propio proyecto (traduzco del nodo, que está en inglés):
|
||||
|
||||
> Una restricción declarada y no exigida no es un suelo: es la descripción de un suelo. […] Una exigencia de la que no cabe fiarse de que falle es indistinguible de no exigir nada, y es más peligrosa, porque se la cree.
|
||||
|
||||
Y trae el número: de **212 restricciones Hard declaradas** en el conjunto de ADRs, **95 constan como violadas y nada bloquea por ellas**. `adr validate` se ejecuta, se lee, y se ignora.
|
||||
|
||||
El caso de las cuatro horas es la anécdota de ese número. No es un fallo del agente: es la forma que toma un suelo que solo está descrito.
|
||||
|
||||
## El arreglo no fue una regla nueva
|
||||
|
||||
Ninguna de las cuatro reglas cambió. Lo que se les puso fue **disparador**, y cada una necesitó uno distinto:
|
||||
|
||||
```sh
|
||||
# 1. Un hook — la regla viaja hacia el agente, no al revés.
|
||||
# En cada prompt, si la tarea ya está gobernada, el QA que la gobierna se sirve entero.
|
||||
"UserPromptSubmit": nu ~/.claude/hooks/onre-prompt-orient.nu
|
||||
|
||||
# 2. Una puerta — el servidor degradado muere en un segundo, no en tres diagnósticos.
|
||||
just server-check
|
||||
|
||||
# 3. Un contrato — un id de modo resuelve a UN solo DAG (había dos contestando por uno).
|
||||
# shared.nu#all-mode-files — dedupe por id: 29 ids → 29 DAGs
|
||||
|
||||
# 4. Un esquema — un caso que calla su lección no exporta.
|
||||
# LessonAccounted: punteros verificados a lo que ejecuta la lección, o la deuda nombrada.
|
||||
```
|
||||
|
||||
Un hook, una puerta, un contrato, un esquema. Ninguno pide que nadie se acuerde de nada — que es el único requisito que importa, porque acordarse es exactamente lo que falló ocho veces.
|
||||
|
||||
El primero ya disparó en producción: la sesión que escribió el expediente de este caso recibió el howto que gobernaba su tarea en su primer prompt, en vez de descubrirlo cuatro horas después.
|
||||
|
||||
## Lo que esto no arregla
|
||||
|
||||
`--artifacts` sigue inalcanzable desde el envoltorio del CLI, y los runs históricos llevan esa columna vacía por eso. `verify` existe en cero de los once pasos del modo: sin oráculo por paso, el estado de un paso es lo que el agente diga. La lista de pasos del howto no se coteja contra el DAG, y ya mintió una vez.
|
||||
|
||||
Y la de fondo, la que no tiene puerta posible: **una frase no es un artefacto**. Los ficheros de este proyecto tienen oráculos de sobra — contratos que rechazan, puertas que fallan, esquemas que no exportan. La conversación no tiene ninguno. Por eso en cuatro horas no llegó ni una mentira a disco y se dijeron ocho: lo que se escribe pasa por puertas, lo que se dice no pasa por ninguna.
|
||||
|
||||
El único oráculo del canal humano↔agente es el humano. Eso no es una excusa. Es la tesis.
|
||||
|
||||
## La pregunta
|
||||
|
||||
La respuesta honesta a «¿con <a href="/about" class="brand-site">ontoref</a> no habría pasado?» es incómoda, y es la única que se sostiene: **<a href="/about" class="brand-site">ontoref</a>, declarado, ya estaba — y no bastó**. Estaba cuatro veces. Hasta que cada regla consiguió su disparador.
|
||||
|
||||
Ninguna de las dos posturas cómodas es cierta. La fricción no es el producto: vender «esto te obliga a cumplir» es confundir el andamio con el edificio, y la fricción es real pero transitoria. Y la contraria tampoco: la coherencia no se vuelve gravedad sola. Sin suelo debajo, la emergencia no es *wu wei* — es deriva. Las dos cosas a la vez, o ninguna es honesta.
|
||||
|
||||
Así que la pregunta que deja el caso no es «¿está escrito?». Eso se contestaba cuatro veces que sí.
|
||||
|
||||
Es **«¿qué lo dispara?»**.
|
||||
|
||||
Si la respuesta es «alguien se acordará», ya sabes lo que tienes. Y el día que haga falta, lo señalará un humano con la paciencia justa.
|
||||
|
||||
En este caso, ocho veces.
|
||||
|
||||
---
|
||||
|
||||
*Este artículo se proyecta del expediente 8/4, «la disciplina sin disparador» — un caso tipado cuyas cifras salen todas de comandos ejecutados contra el log de la sesión, nunca de la memoria de nadie. El expediente entero, con la rueda de reconocimiento y las coartadas, está en la [serie de expedientes](/expedientes).*
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# Generated from a-rule-without-a-trigger-is-a-sign.md
|
||||
# Schema: ../../../../../../../../../../jesusperezlorenzo/.local/share/rustelo/nickel/content/metadata/post_metadata.ncl
|
||||
# Regenerate: nu scripts/content/md-to-ncl.nu --force
|
||||
|
||||
let schema = import "../../../../../../../../../../jesusperezlorenzo/.local/share/rustelo/nickel/content/metadata/post_metadata.ncl" in
|
||||
|
||||
schema.make_post {
|
||||
id = "a-rule-without-a-trigger-is-a-sign",
|
||||
title = "Una regla sin disparador es un cartel",
|
||||
slug = "una-regla-sin-disparador-es-un-cartel",
|
||||
subtitle = "Ocho afirmaciones falsas en cuatro horas. Cuatro mecanismos que ya las prohibían por escrito. Ninguna alarma.",
|
||||
excerpt = "No faltaban reglas. Había cuatro, escritas, canónicas y consultables — y ninguna se ejecutó. La distancia entre una regla escrita y una regla que se aplica tiene nombre, y es lo único que separa la disciplina de la documentación.",
|
||||
date = "2026-07-17",
|
||||
category = "ontoref",
|
||||
tags = ["developer", "agentes", "verificacion", "ontoref"],
|
||||
sort_order = 8,
|
||||
image_url = "/images/ontoref-a-rule-without-a-trigger-is-a-sign-es.webp",
|
||||
graph = {
|
||||
implements = ["enforcement-vs-emergence", "internal-coherence-enforced", "sufficient-verification", "governed-delivery", "witness-as-axis-seam", "audit-harness"],
|
||||
related_to = ["the-seven-sins-of-ai-agents", "trust-is-an-output-not-an-input", "context-declared-not-filled", "dags-everywhere-none-know-what-they-are"],
|
||||
},
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
# Post metadata
|
||||
id: "context-declared-not-filled"
|
||||
title: "Declarado, no rellenado: un contexto que puedes auditar"
|
||||
slug: "context-declared-not-filled"
|
||||
slug: "declarado-no-rellenado-contexto-que-puedes-auditar"
|
||||
subtitle: "La ventana de la IA rellena un contexto y olvida por qué sostuvo esos tokens. La biblioteconomía declara el contexto como una propiedad tipada y con warrant desde 1911 — y ontoref extiende el mismo movimiento del término catalogado al acto gobernado."
|
||||
excerpt: "El ensayo de Jessica Talisman parte en dos la palabra de moda del año: el contexto como contenedor que rellenas, o como propiedad que declaras. ANSI/NISO Z39.19 sostiene la vista-propiedad desde hace un siglo — calificadores, notas de alcance, warrant. ontoref llegó a la misma forma de registro sin citar nunca el estándar, y lo empujó un paso más allá: de declarar contexto sobre términos a declararlo sobre actos, donde un testigo firmado responde '¿con qué autoridad existe este resultado?' con un exit code en lugar de con fluidez."
|
||||
|
||||
|
|
@ -16,8 +16,13 @@ featured: true
|
|||
category: "ontoref"
|
||||
tags: ["ontoref", "context-engineering", "library-science", "warrant", "knowledge-graphs", "developer", "knowledge-works"]
|
||||
|
||||
# Display
|
||||
read_time: "6 min read"
|
||||
sort_order: 7
|
||||
|
||||
# Image (card thumbnail + in-post featured)
|
||||
thumbnail: "/images/ontoref-context-declared-not-filled-es.webp"
|
||||
image_url: "/images/ontoref-context-declared-not-filled-es.webp"
|
||||
og_image: "/images/og/ontoref-context-declared-not-filled-es.png"
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ let schema = import "../../../../../../../../../../jesusperezlorenzo/.local/shar
|
|||
schema.make_post {
|
||||
id = "context-declared-not-filled",
|
||||
title = "Declarado, no rellenado: un contexto que puedes auditar",
|
||||
slug = "context-declared-not-filled",
|
||||
slug = "declarado-no-rellenado-contexto-que-puedes-auditar",
|
||||
subtitle = "La ventana de la IA rellena un contexto y olvida por qué sostuvo esos tokens. La biblioteconomía declara el contexto como una propiedad tipada y con warrant desde 1911 — y ontoref extiende el mismo movimiento del término catalogado al acto gobernado.",
|
||||
excerpt = "El ensayo de Jessica Talisman parte en dos la palabra de moda del año: el contexto como contenedor que rellenas, o como propiedad que declaras. ANSI/NISO Z39.19 sostiene la vista-propiedad desde hace un siglo — calificadores, notas de alcance, warrant. ontoref llegó a la misma forma de registro sin citar nunca el estándar, y lo empujó un paso más allá: de declarar contexto sobre términos a declararlo sobre actos, donde un testigo firmado responde '¿con qué autoridad existe este resultado?' con un exit code en lugar de con fluidez.",
|
||||
date = "2026-07-09",
|
||||
|
|
@ -22,4 +22,4 @@ schema.make_post {
|
|||
implements = ["witness-as-axis-seam", "adr-050", "adr-057", "adr-063", "adr-066"],
|
||||
related_to = ["trust-is-an-output-not-an-input", "ai-knowledge-tool-who-keeps-it-alive"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ category_published: true
|
|||
|
||||
# Image (card thumbnail + in-post featured)
|
||||
thumbnail: "/images/ontoref-one-protocol-multiple-subjects-es.webp"
|
||||
image_url: "/images/ontoref-one-protocol-multiple-subjects-es.webp"
|
||||
og_image: "/images/og/ontoref-one-protocol-multiple-subjects-es.png"
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -22,4 +22,4 @@ schema.make_post {
|
|||
implements = ["ontology-vs-reflection", "ontology-axis-substance", "reflection-axis-act", "personal-ontology-schemas", "level-hierarchy-resolution", "domain-extension-system", "content-vehicle-convergence", "adr-001"],
|
||||
related_to = ["dags-everywhere-none-know-what-they-are", "your-ontology-should-live-with-your-code", "ai-knowledge-tool-who-keeps-it-alive"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ schema.make_post {
|
|||
subtitle = "Por qué casi todo framework responde a los fallos de los agentes con más proceso humano — y por qué ontoref los gradúa con gates que se verifican solos",
|
||||
excerpt = "Los agentes no fallan al azar: fallan con siete vicios sistemáticos que sobreviven a la prueba de 'parece correcto'. El instinto es añadir proceso —un PEP, un KEP, un comité—, pero toda fase de graduación termina apoyada en un humano que la aprueba, y la velocidad del agente supera al humano que pones en la puerta. Esta es la comparación honesta: qué heredan los ADR de ontoref de PEP y KEP, y dónde los rebasan con criterios de graduación witnessed, decidibles y de slice acotado.",
|
||||
date = "2026-06-30",
|
||||
featured = false,
|
||||
category = "ontoref",
|
||||
tags = ["ontoref", "agentes-ia", "verificacion", "adr", "gobernanza", "pep-kep"],
|
||||
read_time = "6 min de lectura",
|
||||
|
|
@ -22,4 +21,4 @@ schema.make_post {
|
|||
implements = ["enforcement-vs-emergence", "adr-lifecycle", "governed-delivery", "sufficient-verification", "witness-as-axis-seam", "adr-069"],
|
||||
related_to = ["trust-is-an-output-not-an-input", "context-declared-not-filled", "one-protocol-multiple-subjects"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -22,4 +22,4 @@ schema.make_post {
|
|||
implements = ["witness-as-axis-seam", "openness-vs-sustainability", "adr-050", "adr-052", "adr-056"],
|
||||
related_to = ["your-ontology-should-live-with-your-code", "ai-knowledge-tool-who-keeps-it-alive", "one-protocol-multiple-subjects"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 247.73 83.02">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #f1f2f2;
|
||||
font-family: SignPainter-HouseScript, SignPainter;
|
||||
font-size: 62px;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
letter-spacing: .03em;
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Layer_1-2" data-name="Layer 1">
|
||||
<text class="cls-1" transform="translate(.38 52.7)"><tspan class="cls-5" x="0" y="0">Je</tspan><tspan class="cls-2" x="49.41" y="0">s</tspan><tspan class="cls-4" x="69.69" y="0">ú</tspan><tspan class="cls-2" x="96.04" y="0">s </tspan><tspan class="cls-6" x="131.5" y="0">Pé</tspan><tspan class="cls-3" x="179.55" y="0">rez</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 987 B |
1453
site/site/public/images/decks/disciplina-sin-disparador-en/deck.css
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
|
||||
(function () {
|
||||
var deck = document.getElementById('deck-view');
|
||||
if (!deck) return;
|
||||
var W = 980, H = 552;
|
||||
// Strings come from the markup, not from the script: one deck.js serves every language and
|
||||
// stays cacheable, while the chrome around the deck speaks the page's language.
|
||||
var T_SLIDE = deck.dataset.tSlide || 'Slide';
|
||||
var T_ZOOM = deck.dataset.tZoom || 'Zoom';
|
||||
var n = +deck.dataset.count;
|
||||
var slides = [].slice.call(deck.querySelectorAll('.deck-slide'));
|
||||
var count = deck.querySelector('.deck-count');
|
||||
var navs = deck.querySelector('.deck-navs');
|
||||
var list = deck.querySelector('.deck-toc-list');
|
||||
var i = 0;
|
||||
|
||||
// The index is DERIVED from each slide's own H1, never authored — a hand-written TOC
|
||||
// is one more surface to drift out of sync with the deck, which is the exact disease
|
||||
// this projection exists to cure.
|
||||
slides.forEach(function (s, k) {
|
||||
s.setAttribute('data-slide-label', (k + 1) + ' / ' + n);
|
||||
// El id ya viene EN EL MARCADO (build-body). Asignarlo aquí es asignarlo tarde: el
|
||||
// navegador ya resolvió el hash y no encontró nada.
|
||||
var h = s.querySelector('h1, h2, h3');
|
||||
var title = (h && h.textContent.trim()) || (T_SLIDE + ' ' + (k + 1));
|
||||
|
||||
var z = document.createElement('button');
|
||||
z.type = 'button'; z.className = 'deck-zoom'; z.dataset.zoomSlide = k;
|
||||
z.setAttribute('aria-label', T_ZOOM + ' ' + (k + 1));
|
||||
z.textContent = '\u2921';
|
||||
s.appendChild(z);
|
||||
|
||||
var li = document.createElement('li');
|
||||
var b = document.createElement('button');
|
||||
b.type = 'button'; b.className = 'deck-toc-link'; b.dataset.goto = k;
|
||||
b.innerHTML = '<span class="deck-toc-num"></span><span class="deck-toc-text"></span>';
|
||||
b.querySelector('.deck-toc-num').textContent = (k + 1) + '.';
|
||||
b.querySelector('.deck-toc-text').textContent = title;
|
||||
li.appendChild(b); list.appendChild(li);
|
||||
});
|
||||
var links = [].slice.call(list.querySelectorAll('.deck-toc-link'));
|
||||
|
||||
// Each slide's REAL content height. Slidev's frame is 552px tall and 12 of this deck's 16
|
||||
// slides hold more than that — up to 672px — so a rigid frame silently cuts three quarters
|
||||
// of the deck. Slidev clips them too; nobody noticed, because until now the slides were
|
||||
// PNGs of themselves. A reading surface may not lose content.
|
||||
function contentHeight(slide) {
|
||||
var page = slide.querySelector('.slidev-page') || slide.querySelector('.print-slide-container');
|
||||
if (!page) return H;
|
||||
// The container is already scaled by --deck-k, and getBoundingClientRect reports
|
||||
// POST-transform coordinates. Divide it back out, or --deck-h lands in scaled pixels and
|
||||
// the CSS scales it a second time.
|
||||
var k = parseFloat(getComputedStyle(slide).getPropertyValue('--deck-k')) || 1;
|
||||
var top = page.getBoundingClientRect().top;
|
||||
var max = H, all = page.querySelectorAll('*');
|
||||
// An image reaching past 1.5x the frame is a decorative BLEED, not content: Slidev renders it
|
||||
// oversized and clips it at the 552 frame ON PURPOSE (rustikon 17/23: a Ferris at ~1170px).
|
||||
// Growing the card to contain it turns the slide into a giant portrait. A small image that
|
||||
// overflows alongside the text (ontology's svg at ~660px) is NOT a bleed — it grows with the
|
||||
// text. So: a bleeding image pins the slide to the native frame, and the card's overflow:hidden
|
||||
// clips it exactly where Slidev does. Only TEXT overflow earns a taller card.
|
||||
var BLEED = H * 1.5;
|
||||
for (var i = 0; i < all.length; i++) {
|
||||
var el = all[i];
|
||||
var bottom = (el.getBoundingClientRect().bottom - top) / k;
|
||||
if (bottom > max) max = bottom;
|
||||
if (/^(img|svg|picture)$/i.test(el.tagName) && bottom > BLEED) return H;
|
||||
}
|
||||
return Math.ceil(max);
|
||||
}
|
||||
// Measure in whatever mode we are in. A display:none slide has no geometry, so force the
|
||||
// stacked layout, measure, and restore — synchronously, so the browser never paints the
|
||||
// intermediate state. Doing this by actually SWITCHING mode created a race: the markup
|
||||
// ships stacked (the right no-JS fallback), the switch to slider waited on fonts, and a
|
||||
// reader who pressed "Read all" inside that window was silently thrown back to slider.
|
||||
function measureAll() {
|
||||
var prev = deck.dataset.mode;
|
||||
deck.dataset.mode = 'read';
|
||||
slides.forEach(function (s) { s.style.setProperty('--deck-h', contentHeight(s)); });
|
||||
deck.dataset.mode = prev;
|
||||
}
|
||||
|
||||
// The scale CSS cannot express: box width / native width, as a plain number.
|
||||
var ro = new ResizeObserver(function (entries) {
|
||||
entries.forEach(function (e) {
|
||||
// A display:none slide reports 0x0. Believing it writes --deck-k: 0 onto the fifteen
|
||||
// hidden slides, and when the reader switches back to stacked their cards are zero-height
|
||||
// until the observer fires again — asynchronously, so anything that measures in between
|
||||
// sees an empty deck. An observer must not trust a measurement of an element that is not
|
||||
// in the layout: keep the last good scale.
|
||||
if (e.contentRect.width > 0) e.target.style.setProperty('--deck-k', e.contentRect.width / W);
|
||||
});
|
||||
});
|
||||
slides.forEach(function (s) { ro.observe(s); });
|
||||
deck.setAttribute('data-js', '');
|
||||
|
||||
// In slider mode the current slide is the one shown. Stacked, it is the one you are
|
||||
// LOOKING at — which is not the same as the one you last clicked, so the rail follows
|
||||
// the scroll rather than the click.
|
||||
var visible = 0;
|
||||
function mark() {
|
||||
var cur = deck.dataset.mode === 'slider' ? i : visible;
|
||||
links.forEach(function (l, k) { l.setAttribute('aria-current', String(k === cur)); });
|
||||
}
|
||||
var io = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (e) {
|
||||
if (e.isIntersecting) {
|
||||
var k = slides.indexOf(e.target);
|
||||
if (k >= 0) { visible = k; if (deck.dataset.mode === 'read') mark(); }
|
||||
}
|
||||
});
|
||||
}, { rootMargin: '-45% 0px -45% 0px' }); // whichever slide crosses the viewport's middle
|
||||
slides.forEach(function (s) { io.observe(s); });
|
||||
function show(k) {
|
||||
i = (k + n) % n;
|
||||
slides.forEach(function (s, j) { s.toggleAttribute('data-active', j === i); });
|
||||
count.textContent = (i + 1) + ' / ' + n;
|
||||
mark();
|
||||
}
|
||||
function setMode(m) {
|
||||
deck.dataset.mode = m;
|
||||
deck.querySelectorAll('[data-set-mode]').forEach(function (b) {
|
||||
b.setAttribute('aria-pressed', String(b.dataset.setMode === m));
|
||||
});
|
||||
var slider = m === 'slider';
|
||||
count.hidden = !slider; navs.hidden = !slider;
|
||||
// Stacked, the rail is the navigation — a collapsed <details> above 16 slides is off
|
||||
// screen by slide 5. Open it. In slider mode leave it as the reader left it.
|
||||
if (slider) show(i); else mark();
|
||||
}
|
||||
function goto(k) {
|
||||
if (deck.dataset.mode === 'slider') { show(k); deck.focus(); }
|
||||
else { i = k; slides[k].scrollIntoView({ behavior: 'smooth', block: 'start' }); }
|
||||
// Mobile only (< 60rem, where the index and the slides cannot sit side by side): after a jump,
|
||||
// close the index so the target slide is not left buried beneath it. On desktop the rail is a
|
||||
// permanent sidebar — leave it open.
|
||||
if (!window.matchMedia('(min-width: 60rem)').matches) setToc(false);
|
||||
}
|
||||
|
||||
// The rail shows by default where there is width for a column; narrow, it starts hidden
|
||||
// so it does not push the deck below the fold. Either way the reader can toggle it.
|
||||
var wide = window.matchMedia('(min-width: 60rem)');
|
||||
var toggle = deck.querySelector('.deck-toc-toggle');
|
||||
function setToc(on) {
|
||||
deck.dataset.toc = on ? 'on' : 'off';
|
||||
toggle.setAttribute('aria-expanded', String(on));
|
||||
}
|
||||
setToc(wide.matches);
|
||||
toggle.addEventListener('click', function () { setToc(deck.dataset.toc !== 'on'); });
|
||||
|
||||
// The sticky bar has to sit on the page's own background, whatever it is. Measure it
|
||||
// rather than guess it, and re-measure when the SITE switches theme — it has its own
|
||||
// toggle, and a bar frozen to the old palette is worse than no bar.
|
||||
function pageBg() {
|
||||
var e = deck.parentElement;
|
||||
while (e) {
|
||||
var bg = getComputedStyle(e).backgroundColor;
|
||||
// No regex here on purpose. Inside a template literal every backslash is eaten once
|
||||
// before the regex engine sees it, and /^rgba(0,0,0,0)$/ silently became a capture
|
||||
// group that matched nothing — syntactically valid, semantically wrong, invisible to
|
||||
// a syntax check. String comparison needs no escapes and cannot be mis-escaped.
|
||||
if (bg && bg !== 'transparent' && bg !== 'rgba(0, 0, 0, 0)') return bg;
|
||||
e = e.parentElement;
|
||||
}
|
||||
return getComputedStyle(document.body).backgroundColor;
|
||||
}
|
||||
function syncPageBg() { deck.style.setProperty('--deck-page-bg', pageBg()); }
|
||||
syncPageBg();
|
||||
new MutationObserver(syncPageBg).observe(document.documentElement, {
|
||||
attributes: true, attributeFilter: ['class', 'data-theme'],
|
||||
});
|
||||
|
||||
// Help panel: a plain show/hide, and its own close button. Its content is server-rendered per
|
||||
// language (from decks.ncl help_items); JS only toggles visibility.
|
||||
var help = deck.querySelector('.deck-help');
|
||||
var helpBtn = deck.querySelector('.deck-help-toggle');
|
||||
function setHelp(on) {
|
||||
help.hidden = !on;
|
||||
helpBtn.setAttribute('aria-expanded', String(on));
|
||||
}
|
||||
helpBtn.addEventListener('click', function () { setHelp(help.hidden); });
|
||||
deck.querySelector('.deck-help-close').addEventListener('click', function () { setHelp(false); });
|
||||
|
||||
var lastFocus = null;
|
||||
function zoomOpen(k) {
|
||||
lastFocus = document.activeElement;
|
||||
show(k);
|
||||
deck.setAttribute('data-zoom', '');
|
||||
document.body.style.overflow = 'hidden';
|
||||
deck.focus();
|
||||
}
|
||||
function zoomClose() {
|
||||
if (!deck.hasAttribute('data-zoom')) return;
|
||||
deck.removeAttribute('data-zoom');
|
||||
document.body.style.overflow = '';
|
||||
if (lastFocus && lastFocus.isConnected) lastFocus.focus();
|
||||
}
|
||||
|
||||
// Slidev's copy buttons are in the captured DOM but inert — their handler was Vue.
|
||||
// Rewire them. innerText, not textContent: it is the RENDERED code, so Shiki's per-line
|
||||
// spans come back as actual newlines instead of one run-on string.
|
||||
function copyCode(btn) {
|
||||
var wrap = btn.closest('.slidev-code-wrapper');
|
||||
var pre = wrap && wrap.querySelector('pre');
|
||||
if (!pre) return;
|
||||
var text = pre.innerText.replace(/\n$/, '');
|
||||
var done = function () {
|
||||
wrap.classList.add('deck-copied');
|
||||
setTimeout(function () { wrap.classList.remove('deck-copied'); }, 1200);
|
||||
};
|
||||
function legacy() {
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';
|
||||
document.body.appendChild(ta); ta.select();
|
||||
try { document.execCommand('copy'); done(); } finally { ta.remove(); }
|
||||
}
|
||||
// The Clipboard API REJECTS when the document is not focused — a real case, not a
|
||||
// theoretical one. Without the catch, the copy fails in complete silence.
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(text).then(done, legacy);
|
||||
} else {
|
||||
legacy();
|
||||
}
|
||||
}
|
||||
|
||||
deck.addEventListener('click', function (e) {
|
||||
var cp = e.target.closest('.slidev-code-copy');
|
||||
if (cp) { e.preventDefault(); return copyCode(cp); }
|
||||
if (e.target.closest('.deck-backdrop')) return zoomClose();
|
||||
// data-zoom-slide (the button) is deliberately NOT data-zoom (the deck's open state).
|
||||
var z = e.target.closest('[data-zoom-slide]');
|
||||
if (z) return deck.hasAttribute('data-zoom') ? zoomClose() : zoomOpen(+z.dataset.zoomSlide);
|
||||
var m = e.target.closest('[data-set-mode]');
|
||||
if (m) return setMode(m.dataset.setMode);
|
||||
var t = e.target.closest('[data-goto]');
|
||||
if (t) return goto(+t.dataset.goto);
|
||||
var nav = e.target.closest('.deck-nav');
|
||||
if (nav) show(i + (+nav.dataset.dir));
|
||||
});
|
||||
|
||||
// Keys are scoped to the deck holding focus. A document-wide Space handler would steal
|
||||
// page scrolling from every reader who never asked the deck for anything.
|
||||
deck.addEventListener('keydown', function (e) {
|
||||
var zoomed = deck.hasAttribute('data-zoom');
|
||||
if (zoomed && e.key === 'Escape') { e.preventDefault(); return zoomClose(); }
|
||||
// Zoomed, the keys must drive even stacked: a lightbox you can leave but not walk is
|
||||
// a dead end.
|
||||
if (!zoomed && deck.dataset.mode !== 'slider') return;
|
||||
if (e.target.closest('.deck-toc')) return;
|
||||
|
||||
var fwd = e.key === 'ArrowRight' || e.key === 'ArrowDown' || (e.key === ' ' && !e.shiftKey);
|
||||
var back = e.key === 'ArrowLeft' || e.key === 'ArrowUp' || (e.key === ' ' && e.shiftKey);
|
||||
if (fwd) { e.preventDefault(); show(i + 1); }
|
||||
else if (back) { e.preventDefault(); show(i - 1); }
|
||||
else if (e.key === 'Home') { e.preventDefault(); show(0); }
|
||||
else if (e.key === 'End') { e.preventDefault(); show(n - 1); }
|
||||
});
|
||||
|
||||
// Printing in slider mode would emit exactly one slide.
|
||||
window.addEventListener('beforeprint', function () { setMode('read'); });
|
||||
|
||||
// ── DÓNDE ATERRIZA UN ENLACE ────────────────────────────────────────────────────────
|
||||
//
|
||||
// Dos formas, y el lector no tiene por qué saber cuál le tocó:
|
||||
// #slide-7 posicional — toda slide la tiene, es la URL de «lo que estoy viendo».
|
||||
// #ncl nombrada — declarada en el frontmatter de la slide; es la que se escribe
|
||||
// en el léxico, porque sobrevive a que el deck se reordene.
|
||||
// La nombrada se resuelve por el DOM (getElementById → la .deck-slide que la contiene) en
|
||||
// vez de por una tabla en este script: el ancla ya está en el marcado, y una segunda copia
|
||||
// aquí es una segunda cosa que se puede desincronizar de la primera.
|
||||
//
|
||||
// El navegador NO puede hacer este salto solo. En modo slider la slide destino está en
|
||||
// display:none y un ancla dentro de ella no tiene geometría: el scroll nativo no va a
|
||||
// ninguna parte. Por eso el destino se ACTIVA antes de mirarlo, y por eso el id en el
|
||||
// marcado no basta por sí solo.
|
||||
function slideFromHash() {
|
||||
var h = location.hash;
|
||||
if (!h || h === '#') return -1;
|
||||
var num = h.match(/^#slide-(\d+)$/);
|
||||
if (num) {
|
||||
var k = +num[1] - 1;
|
||||
return (k >= 0 && k < n) ? k : -1;
|
||||
}
|
||||
// Un id cualquiera del deck: puede ser el ancla nombrada o algo dentro de una slide.
|
||||
var el = null;
|
||||
try { el = document.getElementById(decodeURIComponent(h.slice(1))); } catch (e) { el = null; }
|
||||
var fig = el && el.closest ? el.closest('.deck-slide') : null;
|
||||
return fig ? slides.indexOf(fig) : -1;
|
||||
}
|
||||
// LA BARRA SE MIDE; NO SE ADIVINA. Es sticky en top:0, así que tapa la cabecera de la slide a
|
||||
// la que acabas de saltar — el título cortado justo debajo. El CSS deja 4rem de suelo, pero la
|
||||
// barra crece con el tipo de letra del sitio y con el ancho (los botones se envuelven), y un
|
||||
// número escrito a mano está bien el día que se escribe. scroll-margin es lo único que
|
||||
// scrollIntoView respeta, así que la medida va ahí.
|
||||
function syncScrollMargin() {
|
||||
var bar = deck.querySelector('.deck-bar');
|
||||
var g = (bar ? Math.ceil(bar.getBoundingClientRect().height) : 48) + 12;
|
||||
slides.forEach(function (s) { s.style.scrollMarginTop = g + 'px'; });
|
||||
}
|
||||
syncScrollMargin();
|
||||
window.addEventListener('resize', syncScrollMargin);
|
||||
|
||||
function gotoHash(smooth) {
|
||||
var k = slideFromHash();
|
||||
if (k < 0) return false;
|
||||
// No es goto(k): aquí no queremos cerrar el índice ni robar el foco de la página a la
|
||||
// que el lector acaba de llegar. Solo poner delante la slide que pidió la URL.
|
||||
if (deck.dataset.mode === 'slider') show(k);
|
||||
else i = k;
|
||||
// Y SE SCROLLEA EN LOS DOS MODOS. En slider esto faltaba: show(k) solo activa la slide, así
|
||||
// que lo único que movía la página era el scroll NATIVO del navegador al ancla — hecho antes
|
||||
// de que este script corra y con el deck todavía apilado. Al cambiar a slider el documento
|
||||
// encoge de 16 tarjetas a una, ese scroll queda apuntando a un layout que ya no existe, y el
|
||||
// lector aterriza con el título medio tapado por la barra. La slide correcta, mal puesta.
|
||||
slides[k].scrollIntoView({ behavior: smooth ? 'smooth' : 'auto', block: 'start' });
|
||||
return true;
|
||||
}
|
||||
|
||||
// Measure AFTER the webfonts land. With the fallback font the text is shorter and every
|
||||
// slide "fits"; once Nunito Sans arrives it grows and spills. Measured before fonts, every
|
||||
// --deck-h came back 552 and nine slides were still cut.
|
||||
// And measure while still stacked: a display:none slide has no geometry at all. So the
|
||||
// switch to slider waits for the measurement, not the other way round.
|
||||
|
||||
// Take the mode immediately — no waiting, no race with the reader.
|
||||
measureAll();
|
||||
setMode('slider');
|
||||
gotoHash(false);
|
||||
|
||||
// Un enlace al MISMO deck con otro fragmento no recarga la página: sin esto, pulsar NCL y
|
||||
// luego DAG desde otro tooltip cambia la URL y deja la slide donde estaba. La URL diría una
|
||||
// cosa y la pantalla otra — que es la deriva de siempre, servida a 200.
|
||||
window.addEventListener('hashchange', function () { gotoHash(true); });
|
||||
|
||||
// Then measure again once the webfonts land: with the fallback face the text is shorter
|
||||
// and every slide "fits", so a pre-font measurement returns 552 for all of them and nine
|
||||
// slides stay cut. This pass changes no mode and interrupts nothing.
|
||||
if (document.fonts && document.fonts.ready) document.fonts.ready.then(measureAll);
|
||||
window.addEventListener('resize', measureAll);
|
||||
})();
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 247.73 83.02">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #f1f2f2;
|
||||
font-family: SignPainter-HouseScript, SignPainter;
|
||||
font-size: 62px;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
letter-spacing: .03em;
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Layer_1-2" data-name="Layer 1">
|
||||
<text class="cls-1" transform="translate(.38 52.7)"><tspan class="cls-5" x="0" y="0">Je</tspan><tspan class="cls-2" x="49.41" y="0">s</tspan><tspan class="cls-4" x="69.69" y="0">ú</tspan><tspan class="cls-2" x="96.04" y="0">s </tspan><tspan class="cls-6" x="131.5" y="0">Pé</tspan><tspan class="cls-3" x="179.55" y="0">rez</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 987 B |
1453
site/site/public/images/decks/disciplina-sin-disparador-es/deck.css
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
|
||||
(function () {
|
||||
var deck = document.getElementById('deck-view');
|
||||
if (!deck) return;
|
||||
var W = 980, H = 552;
|
||||
// Strings come from the markup, not from the script: one deck.js serves every language and
|
||||
// stays cacheable, while the chrome around the deck speaks the page's language.
|
||||
var T_SLIDE = deck.dataset.tSlide || 'Slide';
|
||||
var T_ZOOM = deck.dataset.tZoom || 'Zoom';
|
||||
var n = +deck.dataset.count;
|
||||
var slides = [].slice.call(deck.querySelectorAll('.deck-slide'));
|
||||
var count = deck.querySelector('.deck-count');
|
||||
var navs = deck.querySelector('.deck-navs');
|
||||
var list = deck.querySelector('.deck-toc-list');
|
||||
var i = 0;
|
||||
|
||||
// The index is DERIVED from each slide's own H1, never authored — a hand-written TOC
|
||||
// is one more surface to drift out of sync with the deck, which is the exact disease
|
||||
// this projection exists to cure.
|
||||
slides.forEach(function (s, k) {
|
||||
s.setAttribute('data-slide-label', (k + 1) + ' / ' + n);
|
||||
// El id ya viene EN EL MARCADO (build-body). Asignarlo aquí es asignarlo tarde: el
|
||||
// navegador ya resolvió el hash y no encontró nada.
|
||||
var h = s.querySelector('h1, h2, h3');
|
||||
var title = (h && h.textContent.trim()) || (T_SLIDE + ' ' + (k + 1));
|
||||
|
||||
var z = document.createElement('button');
|
||||
z.type = 'button'; z.className = 'deck-zoom'; z.dataset.zoomSlide = k;
|
||||
z.setAttribute('aria-label', T_ZOOM + ' ' + (k + 1));
|
||||
z.textContent = '\u2921';
|
||||
s.appendChild(z);
|
||||
|
||||
var li = document.createElement('li');
|
||||
var b = document.createElement('button');
|
||||
b.type = 'button'; b.className = 'deck-toc-link'; b.dataset.goto = k;
|
||||
b.innerHTML = '<span class="deck-toc-num"></span><span class="deck-toc-text"></span>';
|
||||
b.querySelector('.deck-toc-num').textContent = (k + 1) + '.';
|
||||
b.querySelector('.deck-toc-text').textContent = title;
|
||||
li.appendChild(b); list.appendChild(li);
|
||||
});
|
||||
var links = [].slice.call(list.querySelectorAll('.deck-toc-link'));
|
||||
|
||||
// Each slide's REAL content height. Slidev's frame is 552px tall and 12 of this deck's 16
|
||||
// slides hold more than that — up to 672px — so a rigid frame silently cuts three quarters
|
||||
// of the deck. Slidev clips them too; nobody noticed, because until now the slides were
|
||||
// PNGs of themselves. A reading surface may not lose content.
|
||||
function contentHeight(slide) {
|
||||
var page = slide.querySelector('.slidev-page') || slide.querySelector('.print-slide-container');
|
||||
if (!page) return H;
|
||||
// The container is already scaled by --deck-k, and getBoundingClientRect reports
|
||||
// POST-transform coordinates. Divide it back out, or --deck-h lands in scaled pixels and
|
||||
// the CSS scales it a second time.
|
||||
var k = parseFloat(getComputedStyle(slide).getPropertyValue('--deck-k')) || 1;
|
||||
var top = page.getBoundingClientRect().top;
|
||||
var max = H, all = page.querySelectorAll('*');
|
||||
// An image reaching past 1.5x the frame is a decorative BLEED, not content: Slidev renders it
|
||||
// oversized and clips it at the 552 frame ON PURPOSE (rustikon 17/23: a Ferris at ~1170px).
|
||||
// Growing the card to contain it turns the slide into a giant portrait. A small image that
|
||||
// overflows alongside the text (ontology's svg at ~660px) is NOT a bleed — it grows with the
|
||||
// text. So: a bleeding image pins the slide to the native frame, and the card's overflow:hidden
|
||||
// clips it exactly where Slidev does. Only TEXT overflow earns a taller card.
|
||||
var BLEED = H * 1.5;
|
||||
for (var i = 0; i < all.length; i++) {
|
||||
var el = all[i];
|
||||
var bottom = (el.getBoundingClientRect().bottom - top) / k;
|
||||
if (bottom > max) max = bottom;
|
||||
if (/^(img|svg|picture)$/i.test(el.tagName) && bottom > BLEED) return H;
|
||||
}
|
||||
return Math.ceil(max);
|
||||
}
|
||||
// Measure in whatever mode we are in. A display:none slide has no geometry, so force the
|
||||
// stacked layout, measure, and restore — synchronously, so the browser never paints the
|
||||
// intermediate state. Doing this by actually SWITCHING mode created a race: the markup
|
||||
// ships stacked (the right no-JS fallback), the switch to slider waited on fonts, and a
|
||||
// reader who pressed "Read all" inside that window was silently thrown back to slider.
|
||||
function measureAll() {
|
||||
var prev = deck.dataset.mode;
|
||||
deck.dataset.mode = 'read';
|
||||
slides.forEach(function (s) { s.style.setProperty('--deck-h', contentHeight(s)); });
|
||||
deck.dataset.mode = prev;
|
||||
}
|
||||
|
||||
// The scale CSS cannot express: box width / native width, as a plain number.
|
||||
var ro = new ResizeObserver(function (entries) {
|
||||
entries.forEach(function (e) {
|
||||
// A display:none slide reports 0x0. Believing it writes --deck-k: 0 onto the fifteen
|
||||
// hidden slides, and when the reader switches back to stacked their cards are zero-height
|
||||
// until the observer fires again — asynchronously, so anything that measures in between
|
||||
// sees an empty deck. An observer must not trust a measurement of an element that is not
|
||||
// in the layout: keep the last good scale.
|
||||
if (e.contentRect.width > 0) e.target.style.setProperty('--deck-k', e.contentRect.width / W);
|
||||
});
|
||||
});
|
||||
slides.forEach(function (s) { ro.observe(s); });
|
||||
deck.setAttribute('data-js', '');
|
||||
|
||||
// In slider mode the current slide is the one shown. Stacked, it is the one you are
|
||||
// LOOKING at — which is not the same as the one you last clicked, so the rail follows
|
||||
// the scroll rather than the click.
|
||||
var visible = 0;
|
||||
function mark() {
|
||||
var cur = deck.dataset.mode === 'slider' ? i : visible;
|
||||
links.forEach(function (l, k) { l.setAttribute('aria-current', String(k === cur)); });
|
||||
}
|
||||
var io = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (e) {
|
||||
if (e.isIntersecting) {
|
||||
var k = slides.indexOf(e.target);
|
||||
if (k >= 0) { visible = k; if (deck.dataset.mode === 'read') mark(); }
|
||||
}
|
||||
});
|
||||
}, { rootMargin: '-45% 0px -45% 0px' }); // whichever slide crosses the viewport's middle
|
||||
slides.forEach(function (s) { io.observe(s); });
|
||||
function show(k) {
|
||||
i = (k + n) % n;
|
||||
slides.forEach(function (s, j) { s.toggleAttribute('data-active', j === i); });
|
||||
count.textContent = (i + 1) + ' / ' + n;
|
||||
mark();
|
||||
}
|
||||
function setMode(m) {
|
||||
deck.dataset.mode = m;
|
||||
deck.querySelectorAll('[data-set-mode]').forEach(function (b) {
|
||||
b.setAttribute('aria-pressed', String(b.dataset.setMode === m));
|
||||
});
|
||||
var slider = m === 'slider';
|
||||
count.hidden = !slider; navs.hidden = !slider;
|
||||
// Stacked, the rail is the navigation — a collapsed <details> above 16 slides is off
|
||||
// screen by slide 5. Open it. In slider mode leave it as the reader left it.
|
||||
if (slider) show(i); else mark();
|
||||
}
|
||||
function goto(k) {
|
||||
if (deck.dataset.mode === 'slider') { show(k); deck.focus(); }
|
||||
else { i = k; slides[k].scrollIntoView({ behavior: 'smooth', block: 'start' }); }
|
||||
// Mobile only (< 60rem, where the index and the slides cannot sit side by side): after a jump,
|
||||
// close the index so the target slide is not left buried beneath it. On desktop the rail is a
|
||||
// permanent sidebar — leave it open.
|
||||
if (!window.matchMedia('(min-width: 60rem)').matches) setToc(false);
|
||||
}
|
||||
|
||||
// The rail shows by default where there is width for a column; narrow, it starts hidden
|
||||
// so it does not push the deck below the fold. Either way the reader can toggle it.
|
||||
var wide = window.matchMedia('(min-width: 60rem)');
|
||||
var toggle = deck.querySelector('.deck-toc-toggle');
|
||||
function setToc(on) {
|
||||
deck.dataset.toc = on ? 'on' : 'off';
|
||||
toggle.setAttribute('aria-expanded', String(on));
|
||||
}
|
||||
setToc(wide.matches);
|
||||
toggle.addEventListener('click', function () { setToc(deck.dataset.toc !== 'on'); });
|
||||
|
||||
// The sticky bar has to sit on the page's own background, whatever it is. Measure it
|
||||
// rather than guess it, and re-measure when the SITE switches theme — it has its own
|
||||
// toggle, and a bar frozen to the old palette is worse than no bar.
|
||||
function pageBg() {
|
||||
var e = deck.parentElement;
|
||||
while (e) {
|
||||
var bg = getComputedStyle(e).backgroundColor;
|
||||
// No regex here on purpose. Inside a template literal every backslash is eaten once
|
||||
// before the regex engine sees it, and /^rgba(0,0,0,0)$/ silently became a capture
|
||||
// group that matched nothing — syntactically valid, semantically wrong, invisible to
|
||||
// a syntax check. String comparison needs no escapes and cannot be mis-escaped.
|
||||
if (bg && bg !== 'transparent' && bg !== 'rgba(0, 0, 0, 0)') return bg;
|
||||
e = e.parentElement;
|
||||
}
|
||||
return getComputedStyle(document.body).backgroundColor;
|
||||
}
|
||||
function syncPageBg() { deck.style.setProperty('--deck-page-bg', pageBg()); }
|
||||
syncPageBg();
|
||||
new MutationObserver(syncPageBg).observe(document.documentElement, {
|
||||
attributes: true, attributeFilter: ['class', 'data-theme'],
|
||||
});
|
||||
|
||||
// Help panel: a plain show/hide, and its own close button. Its content is server-rendered per
|
||||
// language (from decks.ncl help_items); JS only toggles visibility.
|
||||
var help = deck.querySelector('.deck-help');
|
||||
var helpBtn = deck.querySelector('.deck-help-toggle');
|
||||
function setHelp(on) {
|
||||
help.hidden = !on;
|
||||
helpBtn.setAttribute('aria-expanded', String(on));
|
||||
}
|
||||
helpBtn.addEventListener('click', function () { setHelp(help.hidden); });
|
||||
deck.querySelector('.deck-help-close').addEventListener('click', function () { setHelp(false); });
|
||||
|
||||
var lastFocus = null;
|
||||
function zoomOpen(k) {
|
||||
lastFocus = document.activeElement;
|
||||
show(k);
|
||||
deck.setAttribute('data-zoom', '');
|
||||
document.body.style.overflow = 'hidden';
|
||||
deck.focus();
|
||||
}
|
||||
function zoomClose() {
|
||||
if (!deck.hasAttribute('data-zoom')) return;
|
||||
deck.removeAttribute('data-zoom');
|
||||
document.body.style.overflow = '';
|
||||
if (lastFocus && lastFocus.isConnected) lastFocus.focus();
|
||||
}
|
||||
|
||||
// Slidev's copy buttons are in the captured DOM but inert — their handler was Vue.
|
||||
// Rewire them. innerText, not textContent: it is the RENDERED code, so Shiki's per-line
|
||||
// spans come back as actual newlines instead of one run-on string.
|
||||
function copyCode(btn) {
|
||||
var wrap = btn.closest('.slidev-code-wrapper');
|
||||
var pre = wrap && wrap.querySelector('pre');
|
||||
if (!pre) return;
|
||||
var text = pre.innerText.replace(/\n$/, '');
|
||||
var done = function () {
|
||||
wrap.classList.add('deck-copied');
|
||||
setTimeout(function () { wrap.classList.remove('deck-copied'); }, 1200);
|
||||
};
|
||||
function legacy() {
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';
|
||||
document.body.appendChild(ta); ta.select();
|
||||
try { document.execCommand('copy'); done(); } finally { ta.remove(); }
|
||||
}
|
||||
// The Clipboard API REJECTS when the document is not focused — a real case, not a
|
||||
// theoretical one. Without the catch, the copy fails in complete silence.
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(text).then(done, legacy);
|
||||
} else {
|
||||
legacy();
|
||||
}
|
||||
}
|
||||
|
||||
deck.addEventListener('click', function (e) {
|
||||
var cp = e.target.closest('.slidev-code-copy');
|
||||
if (cp) { e.preventDefault(); return copyCode(cp); }
|
||||
if (e.target.closest('.deck-backdrop')) return zoomClose();
|
||||
// data-zoom-slide (the button) is deliberately NOT data-zoom (the deck's open state).
|
||||
var z = e.target.closest('[data-zoom-slide]');
|
||||
if (z) return deck.hasAttribute('data-zoom') ? zoomClose() : zoomOpen(+z.dataset.zoomSlide);
|
||||
var m = e.target.closest('[data-set-mode]');
|
||||
if (m) return setMode(m.dataset.setMode);
|
||||
var t = e.target.closest('[data-goto]');
|
||||
if (t) return goto(+t.dataset.goto);
|
||||
var nav = e.target.closest('.deck-nav');
|
||||
if (nav) show(i + (+nav.dataset.dir));
|
||||
});
|
||||
|
||||
// Keys are scoped to the deck holding focus. A document-wide Space handler would steal
|
||||
// page scrolling from every reader who never asked the deck for anything.
|
||||
deck.addEventListener('keydown', function (e) {
|
||||
var zoomed = deck.hasAttribute('data-zoom');
|
||||
if (zoomed && e.key === 'Escape') { e.preventDefault(); return zoomClose(); }
|
||||
// Zoomed, the keys must drive even stacked: a lightbox you can leave but not walk is
|
||||
// a dead end.
|
||||
if (!zoomed && deck.dataset.mode !== 'slider') return;
|
||||
if (e.target.closest('.deck-toc')) return;
|
||||
|
||||
var fwd = e.key === 'ArrowRight' || e.key === 'ArrowDown' || (e.key === ' ' && !e.shiftKey);
|
||||
var back = e.key === 'ArrowLeft' || e.key === 'ArrowUp' || (e.key === ' ' && e.shiftKey);
|
||||
if (fwd) { e.preventDefault(); show(i + 1); }
|
||||
else if (back) { e.preventDefault(); show(i - 1); }
|
||||
else if (e.key === 'Home') { e.preventDefault(); show(0); }
|
||||
else if (e.key === 'End') { e.preventDefault(); show(n - 1); }
|
||||
});
|
||||
|
||||
// Printing in slider mode would emit exactly one slide.
|
||||
window.addEventListener('beforeprint', function () { setMode('read'); });
|
||||
|
||||
// ── DÓNDE ATERRIZA UN ENLACE ────────────────────────────────────────────────────────
|
||||
//
|
||||
// Dos formas, y el lector no tiene por qué saber cuál le tocó:
|
||||
// #slide-7 posicional — toda slide la tiene, es la URL de «lo que estoy viendo».
|
||||
// #ncl nombrada — declarada en el frontmatter de la slide; es la que se escribe
|
||||
// en el léxico, porque sobrevive a que el deck se reordene.
|
||||
// La nombrada se resuelve por el DOM (getElementById → la .deck-slide que la contiene) en
|
||||
// vez de por una tabla en este script: el ancla ya está en el marcado, y una segunda copia
|
||||
// aquí es una segunda cosa que se puede desincronizar de la primera.
|
||||
//
|
||||
// El navegador NO puede hacer este salto solo. En modo slider la slide destino está en
|
||||
// display:none y un ancla dentro de ella no tiene geometría: el scroll nativo no va a
|
||||
// ninguna parte. Por eso el destino se ACTIVA antes de mirarlo, y por eso el id en el
|
||||
// marcado no basta por sí solo.
|
||||
function slideFromHash() {
|
||||
var h = location.hash;
|
||||
if (!h || h === '#') return -1;
|
||||
var num = h.match(/^#slide-(\d+)$/);
|
||||
if (num) {
|
||||
var k = +num[1] - 1;
|
||||
return (k >= 0 && k < n) ? k : -1;
|
||||
}
|
||||
// Un id cualquiera del deck: puede ser el ancla nombrada o algo dentro de una slide.
|
||||
var el = null;
|
||||
try { el = document.getElementById(decodeURIComponent(h.slice(1))); } catch (e) { el = null; }
|
||||
var fig = el && el.closest ? el.closest('.deck-slide') : null;
|
||||
return fig ? slides.indexOf(fig) : -1;
|
||||
}
|
||||
// LA BARRA SE MIDE; NO SE ADIVINA. Es sticky en top:0, así que tapa la cabecera de la slide a
|
||||
// la que acabas de saltar — el título cortado justo debajo. El CSS deja 4rem de suelo, pero la
|
||||
// barra crece con el tipo de letra del sitio y con el ancho (los botones se envuelven), y un
|
||||
// número escrito a mano está bien el día que se escribe. scroll-margin es lo único que
|
||||
// scrollIntoView respeta, así que la medida va ahí.
|
||||
function syncScrollMargin() {
|
||||
var bar = deck.querySelector('.deck-bar');
|
||||
var g = (bar ? Math.ceil(bar.getBoundingClientRect().height) : 48) + 12;
|
||||
slides.forEach(function (s) { s.style.scrollMarginTop = g + 'px'; });
|
||||
}
|
||||
syncScrollMargin();
|
||||
window.addEventListener('resize', syncScrollMargin);
|
||||
|
||||
function gotoHash(smooth) {
|
||||
var k = slideFromHash();
|
||||
if (k < 0) return false;
|
||||
// No es goto(k): aquí no queremos cerrar el índice ni robar el foco de la página a la
|
||||
// que el lector acaba de llegar. Solo poner delante la slide que pidió la URL.
|
||||
if (deck.dataset.mode === 'slider') show(k);
|
||||
else i = k;
|
||||
// Y SE SCROLLEA EN LOS DOS MODOS. En slider esto faltaba: show(k) solo activa la slide, así
|
||||
// que lo único que movía la página era el scroll NATIVO del navegador al ancla — hecho antes
|
||||
// de que este script corra y con el deck todavía apilado. Al cambiar a slider el documento
|
||||
// encoge de 16 tarjetas a una, ese scroll queda apuntando a un layout que ya no existe, y el
|
||||
// lector aterriza con el título medio tapado por la barra. La slide correcta, mal puesta.
|
||||
slides[k].scrollIntoView({ behavior: smooth ? 'smooth' : 'auto', block: 'start' });
|
||||
return true;
|
||||
}
|
||||
|
||||
// Measure AFTER the webfonts land. With the fallback font the text is shorter and every
|
||||
// slide "fits"; once Nunito Sans arrives it grows and spills. Measured before fonts, every
|
||||
// --deck-h came back 552 and nine slides were still cut.
|
||||
// And measure while still stacked: a display:none slide has no geometry at all. So the
|
||||
// switch to slider waits for the measurement, not the other way round.
|
||||
|
||||
// Take the mode immediately — no waiting, no race with the reader.
|
||||
measureAll();
|
||||
setMode('slider');
|
||||
gotoHash(false);
|
||||
|
||||
// Un enlace al MISMO deck con otro fragmento no recarga la página: sin esto, pulsar NCL y
|
||||
// luego DAG desde otro tooltip cambia la URL y deja la slide donde estaba. La URL diría una
|
||||
// cosa y la pantalla otra — que es la deriva de siempre, servida a 200.
|
||||
window.addEventListener('hashchange', function () { gotoHash(true); });
|
||||
|
||||
// Then measure again once the webfonts land: with the fallback face the text is shorter
|
||||
// and every slide "fits", so a pre-font measurement returns 552 for all of them and nine
|
||||
// slides stay cut. This pass changes no mode and interrupts nothing.
|
||||
if (document.fonts && document.fonts.ready) document.fonts.ready.then(measureAll);
|
||||
window.addEventListener('resize', measureAll);
|
||||
})();
|
||||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 782 KiB |
|
After Width: | Height: | Size: 840 KiB |
|
After Width: | Height: | Size: 219 KiB |
|
After Width: | Height: | Size: 234 KiB |
|
After Width: | Height: | Size: 244 KiB |
|
After Width: | Height: | Size: 267 KiB |