> 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…
cierres §4: CVE por grafo — frontera mínima de rebuild (dirty_cone + topo_order)
`scripts/affected.py <pkg> [--agenda] [--sellados]`. Un CVE no obliga a
reconstruir el catálogo: obliga a reconstruir el CONO de lo que depende del
paquete, y nada más. El grafo declarado ya lo sabe.
CVE en zlib → 65 de 761 recetas (8%) — el resto NO se toca
CVE en expat → 20 de 761 (2%), las 20 selladas = el trabajo real
agenda: fontconfig → fcft → cairo → pango → dbus → wayland…
Es el `dirty_cone` de `dataflow` (crate no_std de tawasuyu) + `topo_order` como
agenda (una dep antes que su consumidor), calculados sobre las deps declaradas.
`--sellados` separa lo que hay que reconstruir de verdad (tiene artefacto) de lo
que no existe aún.
CAE SOLO DEL INVARIANTE: las deps están declaradas por hash ⇒ el cono es
computable y EXACTO. Una distro sin clausura declarada tiene que adivinar o
reconstruir todo por las dudas. Ésa es la tesis del SDD 17.
Honestidad grabada en el propio output: la frontera es exacta respecto de lo
DECLARADO. Lo que una receta usa sin declarar queda FUERA del cono — y ese es el
agujero que harkaq cierra (SDD 16). La campaña de esta semana declaró ~84 deps
que el kernel midió y nadie había escrito: sin eso, este cono habría sido
optimista. **El cono es tan bueno como la clausura**, y por eso los dos frentes
se necesitan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
diff --git a/scripts/affected.py b/scripts/affected.py
new file mode 100755
index 00000000..04603d81
--- /dev/null
+++ b/scripts/affected.py
@@ -0,0 +1,114 @@
+#!/usr/bin/env python3
+"""affected — frontera MÍNIMA de rebuild ante un CVE (SDD 17 §4).
+
+ affected.py zlib # ¿qué hay que reconstruir si zlib tiene un CVE?
+ affected.py zlib --agenda # + el orden topológico (qué primero)
+ affected.py zlib --sellados # sólo lo que YA está en el store (el trabajo real)
+
+Un CVE en `zlib` no obliga a reconstruir el catálogo: obliga a reconstruir **el cono de lo que
+depende de zlib**, y nada más. El grafo de dependencias declarado ya lo sabe — es el `dirty_cone`
+de `dataflow` (crate no_std de tawasuyu), y el `topo_order` es la agenda.
+
+Que esto sea exacto (y no "reconstruí todo por las dudas") **cae solo del invariante**: las deps
+están declaradas por hash, así que el cono es computable. Un distro sin clausura declarada tiene
+que adivinar.
+
+Nota honesta: la frontera es exacta respecto de **lo declarado**. Si una receta usa algo sin
+declararlo, queda fuera del cono — y ese es justo el agujero que harkaq cierra (SDD 16): la
+campaña de esta semana declaró ~84 deps que el kernel midió y nadie había escrito. **El cono es
+tan bueno como la clausura**, y por eso los dos frentes se necesitan.
+"""
+import collections
+import glob
+import os
+import re
+import sys
+
+
+def grafo():
+ """(deps, rev): receta → sus deps declaradas, y su inverso."""
+ deps = collections.defaultdict(set)
+ for f in glob.glob("recipes/*.toml"):
+ n = os.path.basename(f)[:-5]
+ s = open(f, errors="ignore").read()
+ m = re.search(r"\[deps\](.*?)(\n\[|\Z)", s, re.S)
+ if m:
+ for d in re.findall(r'"([^"]+)"', m.group(1)):
+ deps[n].add(d)
+ rev = collections.defaultdict(set)
+ for n, ds in deps.items():
+ for d in ds:
+ rev[d].add(n)
+ return deps, rev
+
+
+def cono(rev, semilla):
+ """dirty_cone: todo lo que depende TRANSITIVAMENTE de `semilla`. La frontera de rebuild."""
+ vistos, frontera = set(), [semilla]
+ while frontera:
+ x = frontera.pop()
+ for c in rev.get(x, ()):
+ if c not in vistos:
+ vistos.add(c)
+ frontera.append(c)
+ return vistos
+
+
+def agenda(deps, afectadas):
+ """topo_order dentro del cono: qué reconstruir primero (una dep antes que su consumidor)."""
+ pend = {n: {d for d in deps.get(n, ()) if d in afectadas} for n in afectadas}
+ orden, listos = [], [n for n, p in pend.items() if not p]
+ while listos:
+ n = listos.pop()
+ orden.append(n)
+ for m in afectadas:
+ if n in pend[m]:
+ pend[m].discard(n)
+ if not pend[m]:
+ listos.append(m)
+ ciclo = [n for n in afectadas if n not in orden]
+ return orden, ciclo
+
+
+def main():
+ args = [a for a in sys.argv[1:] if not a.startswith("--")]
+ if not args:
+ print(__doc__.split("\n")[2].strip(), file=sys.stderr)
+ return 2
+ pkg = args[0]
+ if not os.path.exists(f"recipes/{pkg}.toml"):
+ print(f"no existe recipes/{pkg}.toml", file=sys.stderr)
+ return 2
+
+ deps, rev = grafo()
+ afectadas = cono(rev, pkg)
+ total = len(glob.glob("recipes/*.toml"))
+
+ print(f"══ CVE en «{pkg}» — frontera mínima de rebuild")
+ print(f" {len(afectadas)} de {total} recetas afectadas ({len(afectadas)*100//max(total,1)}%)"
+ f" — el resto NO se toca")
+
+ if "--sellados" in sys.argv or "--agenda" in sys.argv:
+ sell = {n for n in afectadas if glob.glob(f"store/*-{n}")}
+ print(f" de ésas, {len(sell)} ya están selladas en el store ⇒ ése es el trabajo real")
+
+ if "--agenda" in sys.argv:
+ orden, ciclo = agenda(deps, afectadas)
+ print("\n ── agenda (topo_order: una dep antes que su consumidor):")
+ for i, n in enumerate(orden, 1):
+ marca = "●" if glob.glob(f"store/*-{n}") else "○"
+ print(f" {i:3}. {marca} {n}")
+ if ciclo:
+ print(f" ⚠ {len(ciclo)} en ciclo (no ordenables): {', '.join(sorted(ciclo))}")
+ print(" ● = sellado (hay que reconstruir) ○ = sin artefacto")
+ else:
+ for n in sorted(afectadas):
+ print(f" {n}")
+
+ print("\n La frontera es exacta respecto de lo DECLARADO. Lo no declarado queda fuera del"
+ "\n cono — es el agujero que harkaq cierra (SDD 16).")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())