265 lines
14 KiB
Text
265 lines
14 KiB
Text
#!/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"
|
||
}
|