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>
This commit is contained in:
Executable
+114
@@ -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())
|
||||
Reference in New Issue
Block a user