#!/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** → ) — 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 '`([^`]+)`' '${1}' | str replace --all --regex '\*\*([^*]+)\*\*' '${1}' } # 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 ([ $"" "
" $"
($p.kicker)
" $"

($p.h1_lead)($p.h1_red)

" $"

($p.intro | inline)

" $"
($p.replays_word) ($n_rep)($p.cases_word) ($cases | length)
" "
" ] | str join "\n")) # ── the four rules, as the series' lineup cards let rules = ($p.rules | each {|r| $"

($r.0 | inline)

($r.1 | inline)

" } | str join "\n") $out = ($out | append ([ "
" $"
($p.rules_title)
" $"
\n($rules)\n
" "
" ] | str join "\n")) # ── the replays, case by case $out = ($out | append $"
($p.cases_title)
") for r in $with_replay { let c = $r.case let href = $"($grid)/($c.slug)" let rep = $c.replay let steps = ($rep.steps | each {|s| $"($s.task | inline)($s.verifiable | inline)" } | str join "\n") let adr = ( if ($rep.adr_trigger | is-empty) { "" } else { $"\n

($v.md.replay_adr): ($rep.adr_trigger | inline)

" } ) $out = ($out | append ([ "
" $"
($c.case_no | esc)
" $"

($c.title | esc)

" $" ($v.md.replay_after)" $"
($rep.prompt_after | str trim | esc)
" $" \n\n($steps)\n
($v.md.replay_task)($v.md.replay_verif)
" $"

($v.md.replay_gate): ($rep.gate_before_delegation | inline)

($adr)" $" ($p.open_case)" "
" ] | 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| $"($r.case.case_no | esc)⊘ ($r.case.lesson_debt | inline)" } | str join "\n") $out = ($out | append ([ "
" $"

($p.debts_title)

" $"

($p.debts_intro | inline)

" $" \n($rows)\n
" "
" ] | str join "\n")) } $out = ($out | append $"") $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 } } }