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.
199 lines
8.3 KiB
Text
199 lines
8.3 KiB
Text
#!/usr/bin/env nu
|
|
|
|
# Project the REPLAYS of the typed expediente CASEs (.ncl) → the session-protocol page,
|
|
# a STANDALONE document per locale (like the informe), served at the path each language
|
|
# declares in site/config/expedientes-surfaces.ncl (`protocol`).
|
|
#
|
|
# It is NOT a card in the grid: the grid indexes cases, and a projection filed among the
|
|
# instances it is projected from is the same category error as a schema filed among them.
|
|
# It lived one commit as a content card — the card read as an eleventh case, and the page
|
|
# wore the article chrome with none of the series' dress. Now it wears the series' own
|
|
# paper (scripts/build/protocolo-template.html owns the chrome; this script owns the data).
|
|
#
|
|
# The autopsies say what died and what now guards the grave; this page says how the
|
|
# OPERATOR conducts the next session. It is not written — it is projected, and `--check`
|
|
# fails if the output was edited by hand: a hand-kept checklist is the next drift.
|
|
#
|
|
# Usage:
|
|
# nu scripts/build/gen-protocolo.nu # every locale
|
|
# nu scripts/build/gen-protocolo.nu --check # assert reproducible, write nothing
|
|
|
|
use ./expediente-vocab.nu [VOCAB]
|
|
|
|
def esc []: string -> string {
|
|
$in | str replace --all '&' '&' | str replace --all '<' '<' | str replace --all '>' '>'
|
|
}
|
|
|
|
# Prose that lands inside an HTML element: escaped, with the two authorial marks
|
|
# converted (`code` → mono span, **bold** → <b>) — the template page is HTML, not
|
|
# markdown, so nothing else survives on purpose.
|
|
def inline [] : string -> string {
|
|
$in
|
|
| str replace --all '&' '&'
|
|
| str replace --all '<' '<'
|
|
| str replace --all '>' '>'
|
|
| str replace --all --regex '`([^`]+)`' '<span class="mono">${1}</span>'
|
|
| str replace --all --regex '\*\*([^*]+)\*\*' '<b>${1}</b>'
|
|
}
|
|
|
|
# The reading order is the one the articles declare in their frontmatter — the same rule
|
|
# gen-informe.nu applies, because a second ordering rule would be a second source of truth.
|
|
def sort-order [ncl: string]: nothing -> int {
|
|
let md = ($ncl | str replace ".ncl" ".md")
|
|
if not ($md | path exists) { return 999 }
|
|
open --raw $md
|
|
| lines
|
|
| where {|l| $l | str starts-with "sort_order:" }
|
|
| get -o 0
|
|
| default "sort_order: 999"
|
|
| str replace --regex '^sort_order:\s*' ''
|
|
| str trim
|
|
| into int
|
|
}
|
|
|
|
def render-body [cases: list, lang: string, grid: string]: nothing -> string {
|
|
let v = ($VOCAB | get $lang)
|
|
let p = $v.protocolo
|
|
let with_replay = ($cases | where {|c| ($c.case.replay? | default null) != null })
|
|
let n_rep = ($with_replay | length)
|
|
|
|
mut out = []
|
|
|
|
# ── header + stamp
|
|
$out = ($out | append ([
|
|
$"<div class='stamp' aria-hidden='true'><b>($p.stamp_lead)</b><span>($p.stamp_sub) · ($n_rep) REPLAYS</span></div>"
|
|
"<header>"
|
|
$" <div class='kicker'>($p.kicker)</div>"
|
|
$" <h1>($p.h1_lead)<span class='red'>($p.h1_red)</span></h1>"
|
|
$" <p class='logline'>($p.intro | inline)</p>"
|
|
$" <div class='meta'><span class='tag'>($p.replays_word) <b>($n_rep)</b></span><span class='tag'>($p.cases_word) <b>($cases | length)</b></span></div>"
|
|
"</header>"
|
|
] | str join "\n"))
|
|
|
|
# ── the four rules, as the series' lineup cards
|
|
let rules = ($p.rules | each {|r|
|
|
$" <div class='rule'><h3>($r.0 | inline)</h3><p>($r.1 | inline)</p></div>"
|
|
} | str join "\n")
|
|
$out = ($out | append ([
|
|
"<section>"
|
|
$" <div class='exhibit'>($p.rules_title)</div>"
|
|
$" <div class='rules'>\n($rules)\n </div>"
|
|
"</section>"
|
|
] | str join "\n"))
|
|
|
|
# ── the replays, case by case
|
|
$out = ($out | append $"<section><div class='exhibit'>($p.cases_title)</div></section>")
|
|
for r in $with_replay {
|
|
let c = $r.case
|
|
let href = $"($grid)/($c.slug)"
|
|
let rep = $c.replay
|
|
let steps = ($rep.steps | each {|s|
|
|
$"<tr><td>($s.task | inline)</td><td>($s.verifiable | inline)</td></tr>"
|
|
} | str join "\n")
|
|
let adr = (
|
|
if ($rep.adr_trigger | is-empty) { "" } else {
|
|
$"\n <p><b>($v.md.replay_adr):</b> ($rep.adr_trigger | inline)</p>"
|
|
}
|
|
)
|
|
$out = ($out | append ([
|
|
"<section>"
|
|
$" <div class='exhibit'>($c.case_no | esc)</div>"
|
|
$" <h2><a href='($href)'>($c.title | esc)</a></h2>"
|
|
$" <span class='caplabel'>($v.md.replay_after)</span>"
|
|
$"<pre>($rep.prompt_after | str trim | esc)</pre>"
|
|
$" <table class='cont'><tbody>\n<tr class='head'><td>($v.md.replay_task)</td><td>($v.md.replay_verif)</td></tr>\n($steps)\n </tbody></table>"
|
|
$" <p><b>($v.md.replay_gate):</b> ($rep.gate_before_delegation | inline)</p>($adr)"
|
|
$" <a class='caselink' href='($href)'>($p.open_case)</a>"
|
|
"</section>"
|
|
] | str join "\n"))
|
|
}
|
|
|
|
# ── the living debt: every ⊘ across the whole series. When a debt is settled in its
|
|
# case, it leaves this box on the next projection, on its own.
|
|
let indebted = ($cases | where {|c| ($c.case.lesson_debt | default "") != "" })
|
|
if ($indebted | is-not-empty) {
|
|
let rows = ($indebted | each {|r|
|
|
$"<tr><td><a href='($grid)/($r.case.slug)'>($r.case.case_no | esc)</a></td><td>⊘ ($r.case.lesson_debt | inline)</td></tr>"
|
|
} | str join "\n")
|
|
$out = ($out | append ([
|
|
"<section class='debts'>"
|
|
$" <h2>($p.debts_title)</h2>"
|
|
$" <p>($p.debts_intro | inline)</p>"
|
|
$" <table class='cont'><tbody>\n($rows)\n </tbody></table>"
|
|
"</section>"
|
|
] | str join "\n"))
|
|
}
|
|
|
|
$out = ($out | append $"<footer><span>($p.foot_left)</span><span><a href='https://ontoref.dev' target='_blank' rel='noopener'>ontoref.dev</a></span></footer>")
|
|
$out | str join "\n\n"
|
|
}
|
|
|
|
def main [
|
|
--root: string = ".",
|
|
--check,
|
|
] {
|
|
let base = $"($root)/site/content/expedientes"
|
|
let tpl = (open --raw $"($root)/scripts/build/protocolo-template.html")
|
|
|
|
let surf_file = ([$root, "site", "config", "expedientes-surfaces.ncl"] | path join)
|
|
if not ($surf_file | path exists) {
|
|
error make { msg: $"no surface declaration at ($surf_file) — I cannot know where to publish" }
|
|
}
|
|
let surfaces = (^nickel export $surf_file | from json | get surfaces)
|
|
|
|
mut drift = []
|
|
for lang in ($surfaces | columns | sort) {
|
|
let s = ($surfaces | get $lang)
|
|
# A language whose surface does not declare its protocol path is an UNANSWERED extent,
|
|
# not a language without one — the difference between a gap and a shrug.
|
|
let served = ($s.protocol? | default "")
|
|
if ($served | is-empty) {
|
|
error make { msg: $"($lang): expedientes-surfaces.ncl declares no `protocol` path — a language must answer for its protocol page, loudly" }
|
|
}
|
|
let cases = (
|
|
glob $"($base)/($lang)/($s.dir)/*.ncl"
|
|
| sort
|
|
| each {|f| { order: (sort-order $f), case: (^nickel export $f | from json) } }
|
|
| sort-by order
|
|
)
|
|
let n_replay = ($cases | where {|c| ($c.case.replay? | default null) != null } | length)
|
|
# A protocol page projected from zero replays is a page with nothing to say, and an
|
|
# empty page that answers 200 is this series' oldest pathology.
|
|
if $n_replay == 0 {
|
|
error make { msg: $"($lang): no case carries a replay — nothing to project. The page is a projection of replays, not a blank to fill by hand." }
|
|
}
|
|
|
|
let v = ($VOCAB | get $lang)
|
|
let body = (render-body $cases $lang $s.grid)
|
|
let html = (
|
|
$tpl
|
|
| str replace --all "{{LANG}}" $lang
|
|
| str replace --all "{{TITLE}}" $v.protocolo.page_title
|
|
| str replace "{{VIEWER_HREF}}" $s.viewer
|
|
| str replace "{{VIEWER_LABEL}}" $v.protocolo.viewer_label
|
|
| str replace "{{BACK_HREF}}" $v.ui.back_href
|
|
| str replace "{{BACK_LABEL}}" $v.ui.back
|
|
| str replace "{{BODY}}" $body
|
|
)
|
|
|
|
let out = ([$root, "site", "public", ($served | str trim --left --char "/")] | path join)
|
|
if $check {
|
|
let cur = (if ($out | path exists) { open --raw $out } else { "" })
|
|
if $cur != $html { $drift = ($drift | append $"($lang) → ($served)") }
|
|
} else {
|
|
mkdir ($out | path dirname)
|
|
$html | save --force $out
|
|
print $"protocolo → ($served) [($n_replay) replays de ($cases | length) casos]"
|
|
}
|
|
}
|
|
|
|
if $check {
|
|
if ($drift | is-empty) {
|
|
print "protocolo-check: the session-protocol page reproduces from the cases' replays — no hand-edited output"
|
|
} else {
|
|
print $"protocolo-check: DRIFT — ($drift | str join ', ') differ from what the replays produce."
|
|
print " the page is a projection; edit the case .ncl, never the served HTML."
|
|
exit 1
|
|
}
|
|
}
|
|
}
|