> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
fuentes: tapar el agujero de libtool y que el vigía distinga noticia de emergencia
El vigía decía 5 fuentes muertas y sólo UNA estaba en riesgo de verdad: busybox y freetype
tenían el tarball en la caché local Y en el mirror, y firefox-pgo-profile apunta a .invalid a
propósito. La que no tenía copia en ningún lado era libtool (incoming-kde), con ftpmirror.gnu.org
devolviendo 502 — o sea una receta que ya no se podía reconstruir y nadie lo sabía.
Arreglado: el tarball se bajó de ftp.gnu.org, se verificó contra el sha256 PINEADO (coincide
byte a byte), y está en la caché local y en el mirror. La URL de la receta pasa al espejo que
responde, y se midió que eso no re-hashea nada — ANTES y DESPUÉS dan el mismo
b3:5c09bff5..., que es el ADR 0013 comprobado en vez de citado.
Y el vigía ahora cruza cada fuente muerta con la caché local y el mirror, porque mezclar
'upstream caído con el blob guardado' (noticia) con 'caído y sin copia' (emergencia) hace que
el número suba, nadie lo mire, y el día que uno importe esté enterrado entre cuatro que no. A
un guardián lo mata el ruido. Ahora informa EN RIESGO aparte:
== vivas 1175 / 1179 muertas arriba 4 EN RIESGO 0
⚠ Y si el mirror no se puede consultar —el worker no tiene la clave del Storage Box— dice
'indeterminado', NUNCA 'EN RIESGO': un instrumento que no pudo correr no puede parecer un
hallazgo.
Probado con rotura a propósito y con los dos controles, que es lo que separa un guardián que
sirve de uno que nunca saltó:
sha inexistente + upstream muerto → EN RIESGO ✓ salta
blob en caché + upstream muerto → cache-local ✓ no salta
mirror inalcanzable → indeterminado ✓ no alarma en falso
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
diff --git a/docs/state/fuentes-vigia.json b/docs/state/fuentes-vigia.json
index fcea5860..e06467bc 100644
--- a/docs/state/fuentes-vigia.json
+++ b/docs/state/fuentes-vigia.json
@@ -1,48 +1,50 @@
{
"schema": "hammer-fuentes-vigia/1",
"total": 1179,
- "vivas": 1174,
- "muertas": 5,
+ "vivas": 1175,
+ "muertas": 4,
"detalle_muertas": [
{
"receta": "busybox",
"fichero": "recipes/busybox.toml",
"tipo": "tarball",
"url": "https://busybox.net/downloads/busybox-1.36.1.tar.bz2",
+ "sha256": "b8cc24c9574d809e7279c3be349795c5d5ceb6fdf19ca709f80cde50e47de314",
"codigo": "000",
- "viva": false
+ "viva": false,
+ "respaldo": "cache-local"
},
{
"receta": "firefox-pgo-profile",
"fichero": "recipes/firefox-pgo-profile.toml",
"tipo": "tarball",
"url": "https://no-hay-upstream.invalid/hammer/firefox-pgo-2026-09-08.tar.xz",
+ "sha256": "95472411ba252e14c4d57de7193d53e4ba35fbccf4819ba95ceff1ca843e4a7a",
"codigo": "000",
- "viva": false
+ "viva": false,
+ "respaldo": "cache-local"
},
{
"receta": "freetype",
"fichero": "recipes/freetype.toml",
"tipo": "tarball",
"url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.14.3.tar.xz",
+ "sha256": "36bc4f1cc413335368ee656c42afca65c5a3987e8768cc28cf11ba775e785a5f",
"codigo": "502",
- "viva": false
+ "viva": false,
+ "respaldo": "cache-local"
},
{
"receta": "freetype-shared",
"fichero": "recipes/freetype-shared.toml",
"tipo": "tarball",
"url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.14.3.tar.xz",
- "codigo": "000",
- "viva": false
- },
- {
- "receta": "libtool",
- "fichero": "recipes/incoming-kde/libtool.toml",
- "tipo": "tarball",
- "url": "https://ftpmirror.gnu.org/libtool/libtool-2.5.4.tar.xz",
+ "sha256": "36bc4f1cc413335368ee656c42afca65c5a3987e8768cc28cf11ba775e785a5f",
"codigo": "502",
- "viva": false
+ "viva": false,
+ "respaldo": "cache-local"
}
- ]
+ ],
+ "en_riesgo": 0,
+ "mirror_consultado": true
}
diff --git a/recipes/incoming-kde/libtool.toml b/recipes/incoming-kde/libtool.toml
index 41b53ab6..9cbcf49a 100644
--- a/recipes/incoming-kde/libtool.toml
+++ b/recipes/incoming-kde/libtool.toml
@@ -4,7 +4,7 @@ name = "libtool"
version = "2.5.4"
license = "GPL-2.0-or-later"
[source]
-tarball = "https://ftpmirror.gnu.org/libtool/libtool-2.5.4.tar.xz"
+tarball = "https://ftp.gnu.org/gnu/libtool/libtool-2.5.4.tar.xz"
sha256 = "f81f5860666b0bc7d84baddefa60d1cb9fa6fceb2398cc3baca6afaa60266675"
[build]
compiler = "zig-cc"
diff --git a/scripts/fuentes/fuentes-vigia.py b/scripts/fuentes/fuentes-vigia.py
index fd312d59..be11a21c 100755
--- a/scripts/fuentes/fuentes-vigia.py
+++ b/scripts/fuentes/fuentes-vigia.py
@@ -24,7 +24,9 @@ def fuentes():
nombre = (re.search(r'^\s*name\s*=\s*"([^"]+)"', s, re.M) or [None, os.path.basename(f)[:-5]])[1]
t = re.search(r'^\s*tarball\s*=\s*"([^"]+)"', s, re.M)
g = re.search(r'^\s*repo\s*=\s*"([^"]+)"', s, re.M)
- if t: out.append({"receta": nombre, "fichero": f, "tipo": "tarball", "url": t.group(1)})
+ sh = re.search(r'^\s*sha256\s*=\s*"([0-9a-f]{64})"', s, re.M)
+ if t: out.append({"receta": nombre, "fichero": f, "tipo": "tarball", "url": t.group(1),
+ "sha256": sh.group(1) if sh else None})
elif g: out.append({"receta": nombre, "fichero": f, "tipo": "git", "url": g.group(1)})
return out
@@ -52,6 +54,51 @@ def revisa(e):
except Exception as ex:
return {**e, "codigo": f"error:{type(ex).__name__}", "viva": False}
+def respaldo(muertas):
+ """¿El blob de cada fuente muerta está a salvo en algún sitio?
+
+ POR QUÉ (2026-09-08). El vigía decía «5 muertas» y sólo UNA estaba en riesgo de verdad: las
+ otras cuatro tenían el tarball en la caché local y en el mirror, o eran `.invalid` a propósito.
+ Un upstream caído con el blob guardado es una NOTICIA; uno sin copia es una EMERGENCIA — la
+ receta deja de poder reconstruirse. Mezclarlos hace que el número suba, nadie lo mire, y el día
+ que uno importe esté enterrado entre cuatro que no. A un guardián lo mata el ruido.
+
+ ⚠ Si el mirror NO se puede consultar (el worker no tiene la clave del Storage Box), esto dice
+ `indeterminado`, NUNCA `en_riesgo`. Un instrumento que no pudo correr y un hallazgo real se
+ parecen demasiado como para dejar que el informe los confunda.
+ """
+ if not muertas:
+ return True, set()
+ en_mirror, consultado = set(), True
+ try:
+ r = subprocess.run(
+ ["ssh", "-4", "-p", os.environ.get("SB_PORT", "23"),
+ "-i", os.path.expanduser(os.environ.get("SB_KEY", "~/.ssh/github5")),
+ "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new",
+ "-o", "ConnectTimeout=15",
+ f"{os.environ.get('SB_USER','u647150')}@{os.environ.get('SB_HOST','u647150.your-storagebox.de')}",
+ "ls hammer/fuentes/"], capture_output=True, text=True, timeout=90)
+ if r.returncode == 0:
+ en_mirror = {l.strip() for l in r.stdout.splitlines() if l.strip().endswith(".tar")}
+ else:
+ consultado = False
+ except Exception:
+ consultado = False
+ for m in muertas:
+ sha = m.get("sha256")
+ blob = f"{sha}.tar" if sha else None
+ if blob and os.path.exists(os.path.join("work/tarballs", blob)):
+ m["respaldo"] = "cache-local"
+ elif blob and blob in en_mirror:
+ m["respaldo"] = "mirror"
+ elif not consultado:
+ m["respaldo"] = "indeterminado" # NO es un hallazgo: es que no pude mirar
+ elif not sha:
+ m["respaldo"] = "sin-sha" # git, o receta sin pin: no aplica el mirror
+ else:
+ m["respaldo"] = "EN RIESGO"
+ return consultado, en_mirror
+
def main():
fs = fuentes()
if LIMITE:
@@ -61,6 +108,8 @@ def main():
with ThreadPoolExecutor(max_workers=JOBS) as ex:
res = list(ex.map(revisa, fs))
muertas = [r for r in res if not r["viva"]]
+ consultado, _ = respaldo(muertas)
+ en_riesgo = [m for m in muertas if m.get("respaldo") == "EN RIESGO"]
# El informe se ordena por receta para que el diff de git sea legible entre corridas.
inf = {
"schema": "hammer-fuentes-vigia/1",
@@ -68,14 +117,22 @@ def main():
"vivas": len(res) - len(muertas),
"muertas": len(muertas),
"detalle_muertas": sorted(muertas, key=lambda r: r["receta"]),
+ # La cifra que de verdad importa: muerto arriba Y sin copia en ningún lado.
+ "en_riesgo": len(en_riesgo),
+ "mirror_consultado": consultado,
}
os.makedirs("docs/state", exist_ok=True)
with open("docs/state/fuentes-vigia.json", "w") as fh:
json.dump(inf, fh, indent=1, ensure_ascii=False)
fh.write("\n")
- print(f"== vivas {inf['vivas']} / {inf['total']} MUERTAS {inf['muertas']}")
+ print(f"== vivas {inf['vivas']} / {inf['total']} muertas arriba {inf['muertas']} "
+ f"EN RIESGO {inf['en_riesgo']}" + ("" if consultado else " (mirror NO consultado)"))
for m in inf["detalle_muertas"][:40]:
- print(f" {m['codigo']:>12} {m['receta']:<24} {m['url'][:70]}")
+ print(f" {m['codigo']:>12} {m.get('respaldo','?'):<14} {m['receta']:<24} {m['url'][:60]}")
+ if inf["en_riesgo"]:
+ print(" ⚠ EN RIESGO = upstream caído Y sin blob en caché ni mirror ⇒ receta irreconstruible.")
+ print(" Bajar el tarball de otro espejo, verificar el sha256 pineado y "
+ "`scripts/fuentes/mirror-poblar.sh`.")
# Exit 0 SIEMPRE: informar, no abortar el latido. Ver la docstring.
return 0