ontoref-outreach/site/scripts/build/check-served-live.nu
2026-07-17 01:01:30 +01:00

265 lines
14 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env nu
# THE LIVE SURFACE IS THE ONLY PUBLICATION THERE IS.
#
# `gen-expediente.nu --check` closes the gap between the canonical tree (site/r/) and the served
# tree (site/public/r/) — a real gap, and the one that made a case answer 200, sit in the index,
# and be absent from the grid. But it compares two trees ON DISK, and the disk is itself a PROXY:
# THE SERVER READS ITS INDEX INTO MEMORY AT STARTUP. Both trees said five, the grid showed four,
# and only a restart fixed it. A check that stops at the filesystem cannot see that.
#
# So this is the witness at the end of the chain, and it asks the only question a reader can:
#
# IS THE CASE ON THE PAGE?
#
# Two rules govern how it asks, and both were learned by getting them wrong:
#
# · IT COUNTS THE CARD, NOT THE STRING. The ids of these cases also live in the page's nav, its
# meta tags and two JSON blobs — `testigo-ciego` occurs FOUR TIMES before the first card ever
# opens. A page-wide grep therefore reports a case as present while its card is missing, and
# that is not hypothetical: it is precisely the verification that reported "5 cards" over a
# grid that was rendering four (adr-072, a-check-measures-the-capability-not-a-proxy).
#
# · ZERO CARDS IS A FINDING, NEVER AN ANSWER. If the page yields no card at all, this check
# cannot tell "the grid is empty" from "I no longer know how to read a card" — the selector
# changed, the framework re-skinned, the page 500'd into a friendly shell. Both are findings.
# An empty list is what a blind check returns (adr-072, a-check-can-say-i-did-not-look).
#
# Usage:
# nu scripts/build/check-served-live.nu --base-url http://localhost:3030 --root .
# ── The parser. Pure, and separated from the fetch SO THAT IT CAN BE FALSIFIED OFFLINE ────────
#
# A gate nobody has seen refuse is not a gate. Keeping this pure is what lets the oracle feed it
# a page whose fifth case appears only in a JSON blob and assert that it is NOT counted.
#
# TWO INDEPENDENT READINGS OF THE SAME PAGE, because one reading cannot check itself.
#
# READING B (the marker) knows the framework's card class. It is precise, and it is COUPLED: a
# re-skin renames the class and the reading silently returns nothing.
#
# READING A (the structure) knows no class at all. It deletes the regions that are not content —
# <script>, <style>, <template>, <nav>, <header>, <footer> — and asks what case links survive.
# It cannot tell a card from a sidebar link, so it is not the assertion. But it CANNOT GO BLIND
# TO A RE-SKIN, because it never knew the skin.
#
# So the readings check each other. If the marker finds nothing and the structure finds cases, the
# page is fine and THE WITNESS IS STALE — and it can say so, by name. That sentence is the whole
# reason for the second reading: "I found no cards" is a mystery, and a mystery reads as an empty
# grid. "My card marker `ds-card` no longer matches a page that renders five cases" is a defect,
# and it names its own repair.
# The case links that survive once everything that is not page CONTENT is deleted.
# Class-free, skin-free, framework-free — this is the reading that cannot be re-skinned into
# silence, and it is why the marker's staleness is detectable at all.
export def content-links [html: string, prefix: string]: nothing -> list<string> {
let body = (
$html
| str replace --all --regex '(?is)<script[^>]*>.*?</script>' ''
| str replace --all --regex '(?is)<style[^>]*>.*?</style>' ''
| str replace --all --regex '(?is)<template[^>]*>.*?</template>' ''
| str replace --all --regex '(?is)<nav[^>]*>.*?</nav>' ''
| str replace --all --regex '(?is)<header[^>]*>.*?</header>' ''
| str replace --all --regex '(?is)<footer[^>]*>.*?</footer>' ''
)
$body
| parse --regex ('href="' + $prefix + '(?P<slug>[^"/#?]+)"')
| get slug
| uniq
}
# The slugs RENDERED AS CARDS in a grid page, and nothing else. `marker` is DECLARED, never
# guessed: site/config/expedientes-surfaces.ncl owns it, so a re-skin is repaired in data.
export def cards-in [html: string, prefix: string, marker: string]: nothing -> list<string> {
let blocks = ($html | split row ('class="' + $marker + ' '))
if ($blocks | length) <= 1 { return [] }
$blocks
| skip 1
| each { |b|
let hit = ($b | parse --regex ('href="' + $prefix + '(?P<slug>[^"/#?]+)"'))
if ($hit | is-empty) { null } else { ($hit | first | get slug) }
}
| compact
| uniq
}
# How many cards the page rendered. Distinguishes "the grid is empty" from "I could not read it".
export def card-count [html: string, marker: string]: nothing -> int {
(($html | split row ('class="' + $marker + ' ')) | length) - 1
}
def main [
--base-url: string = "http://localhost:3030",
--root: string = ".",
] {
let root_abs = ($root | path expand)
# ── ANTES DE MEDIR: ¿ESTE SERVIDOR ES ESTE SITIO? ───────────────────────────────────────────
#
# Esta comprobación existe porque «la superficie viva es la única publicación que hay» — y luego
# aceptaba un `--base-url` y se creía lo que le daban. Es la premisa que nunca verificaba: que al
# otro lado del puerto está el sitio. Un servidor arrancado sin el entorno de run.sh no fusiona
# site/config/routes/ y responde 404 a todo lo que este sitio aporta, mientras contesta 200 a lo
# que la imagen compartida trae compilado. Contra ese servidor, todo lo que esta puerta diga
# sobre esta web no es sobre esta web — y lo diría con la misma confianza.
#
# Va PRIMERO y aborta: publicar un veredicto medido contra un servidor que no es el sitio es peor
# que no medir. El de abajo es el mismo criterio que el resto del fichero — un hallazgo que no se
# puede alcanzar no es un hallazgo (adr-072).
let preflight = ([$root_abs, "scripts", "build", "check-live-server.nu"] | path join)
let pf = (do { ^nu $preflight --base-url $base_url --root $root_abs } | complete)
if $pf.exit_code != 0 {
print $pf.stdout
print "served-live: NO MIDO NADA CONTRA ESTE SERVIDOR."
exit 1
}
# ── The extent: languages (protocol) × surfaces (site) ──────────────────────────────────────
let lex = ([$root_abs, "..", "..", ".ontoref", "ontology", "lexicon.ncl"] | path join | path expand)
let spine = ([$root_abs, "..", "..", ".ontoref"] | path join | path expand)
if not ($lex | path exists) {
print $"served-live: I DID NOT LOOK — the declared extent does not exist: ($lex)"
exit 1
}
let lr = (do { ^nickel export --import-path $spine $lex } | complete)
if $lr.exit_code != 0 {
print $"served-live: I DID NOT LOOK — the declared extent failed to export: ($lex)"
exit 1
}
let langs = ($lr.stdout | from json).languages
let surf_file = ([$root_abs, "site", "config", "expedientes-surfaces.ncl"] | path join)
if not ($surf_file | path exists) {
print $"served-live: I DID NOT LOOK — no surface declaration at ($surf_file)"
exit 1
}
let sr = (do { ^nickel export $surf_file } | complete)
if $sr.exit_code != 0 {
print $"served-live: I DID NOT LOOK — the surface declaration failed to export: ($surf_file)"
exit 1
}
let surf = ($sr.stdout | from json)
let surfaces = $surf.surfaces
# The card marker is DECLARED. If the declaration has none, the witness does not fall back to a
# literal it happens to remember — a default here would be the folklore this file exists to end.
let marker = ($surf | get -o card | default "")
if ($marker | is-empty) {
print $"served-live: I DID NOT LOOK — ($surf_file) declares no `card` marker."
print " Without it I cannot tell a rendered card from a mention of its id in a JSON blob,"
print " and guessing the marker is exactly the folklore this declaration exists to end."
exit 1
}
mut findings = []
# COVERAGE ⊇ EXTENT, on the surface axis. Declare a language and give it no surface and the
# publisher would file its cases under `cases/` and serve them nowhere — silently, because the
# rule used to be `if es { casos } else { cases }`, which says every language that is not
# Spanish is English.
for l in $langs {
if ($surfaces | get -o $l | is-empty) {
$findings = ($findings | append
$"[($l)] the extent declares this language and site/config/expedientes-surfaces.ncl does not answer for it")
}
}
# The surfaces file REPEATS a path that routes.ncl owns. A repetition with no gate is a mirror,
# and a mirror drifts (adr-070). So the repetition is gated: rename the route and this breaks.
let routes = ([$root_abs, "site", "config", "routes.ncl"] | path join)
if ($routes | path exists) {
let rtxt = (open --raw $routes)
for l in ($surfaces | columns) {
let g = ($surfaces | get $l | get grid)
if not ($rtxt | str contains $'($l) = "($g)"') {
$findings = ($findings | append
$"[($l)] surface declares grid ($g), and routes.ncl does not — the mirror has drifted")
}
}
}
if ($findings | is-not-empty) {
print "served-live: EXTENT UNANSWERED — the surface declaration does not cover what is declared:"
for f in $findings { print $" ✗ ($f)" }
exit 1
}
# ── The live surface, per language ──────────────────────────────────────────────────────────
mut stale = []
mut looked = 0
for l in $langs {
let s = ($surfaces | get $l)
let idx = ([$root_abs, "site", "public", "r", "expedientes", $l, $s.dir, "index.json"] | path join)
if not ($idx | path exists) {
$stale = ($stale | append $"[($l)] no served index at ($idx) — nothing is published in this language")
continue
}
let expected = (open $idx | get posts? | default [] | where published? != false)
if ($expected | is-empty) { continue }
let url = $"($base_url)($s.grid)"
let r = (do { ^curl -s -f --max-time 15 $url } | complete)
if $r.exit_code != 0 {
# An unreachable page is not "no cases". It is "I did not look" — and this is the one place
# a live check is tempted to shrug and pass, which is how a deploy goes green over a dead site.
print $"served-live: I DID NOT LOOK — ($url) did not answer \(curl exit ($r.exit_code))."
print " A page I could not fetch is a FINDING. It is not an empty grid, and it is not green."
exit 1
}
let html = $r.stdout
let prefix = $"($s.grid)/($s.dir)/"
let shown = (cards-in $html $prefix $marker)
let structural = (content-links $html $prefix)
# THE TWO READINGS ADJUDICATE EACH OTHER, and this is the whole point of having two.
#
# The marker sees nothing and the structure sees cases: the page is FINE and the WITNESS is
# stale. Before there were two readings, this was indistinguishable from an empty grid — the
# check could only say "I found no cards", which is a mystery, and a mystery gets read as
# health. Now it is a defect with a name and an address.
if ($shown | is-empty) and ($structural | is-not-empty) {
print $"served-live: MY CARD MARKER IS STALE — not the page's fault, MINE."
print $" ($url) renders ($structural | length) case link\(s), and `class=\"($marker) \"` matches nothing in it."
print " The grid was re-skinned and this witness was not. It is not an empty grid, and it is"
print " NOT GREEN: a witness that cannot recognise what it is watching has stopped witnessing."
print " Repair the marker in site/config/expedientes-surfaces.ncl (`card`), then re-falsify."
exit 1
}
# Neither reading sees anything, and the index says cases are published: the page is broken,
# or it is not the page I think it is. Either way it is a finding, never an empty list.
if ($shown | is-empty) and ($structural | is-empty) {
print $"served-live: I DID NOT LOOK — ($url) answered, and NEITHER reading found a case in it."
print $" ($expected | length) case\(s) are published in ($l). The grid is broken, or this is"
print " not the page I think it is. Both are findings; neither is an empty result."
exit 1
}
# The structure sees a case the marker does not: a case link escaped the card element. The
# readings disagree, one of them is wrong, and the check does not get to pick. It says so.
let escaped = ($structural | where {|c| not ($c in $shown) })
if ($escaped | is-not-empty) {
$stale = ($stale | append
$"[($l)] my two readings of ($url) DISAGREE — ($escaped | str join ', ') appear as case links outside any `($marker)` card. One reading is wrong and I cannot tell which.")
}
for p in $expected {
if not ($p.slug in $shown) {
$stale = ($stale | append
$"[($l)] ($p.slug) — in the served index, NOT rendered as a card at ($url)")
}
}
$looked += 1
}
if ($stale | is-not-empty) {
print "served-live: STALE — the server is not serving what the tree says is published:"
for s in $stale { print $" ✗ ($s)" }
print " The disk is not the publication. The server reads its index INTO MEMORY AT STARTUP,"
print " so a mirrored tree it has never re-read is a tree that, to every reader, does not exist."
print " Reload the content index (or restart the server) — then this check is the proof."
exit 1
}
print $"served-live: the grid RENDERS every published case · ($langs | str join ', ') · ($looked) surface\(s) read live"
}