diff --git a/scripts/build-state.py b/scripts/build-state.py index f1f8084b..a7292c17 100755 --- a/scripts/build-state.py +++ b/scripts/build-state.py @@ -176,11 +176,28 @@ for _d in SELLADOS_REMOTOS: _REMOTOS_POR_INAME.setdefault(_d.split("-", 1)[1] if "-" in _d else _d, []).append(_d) REMOTO_ONLY = set() +def _artefacto_presente(ruta): + """¿Hay un ARTEFACTO en `ruta`, o sólo un nombre? + + ⚠ Esto era `os.path.isdir(ruta)` y por eso un directorio VACÍO se contaba como `sealed` + (2026-09-01). No es hipotético: una cosecha que murió a mitad dejó `…-mise` sin un solo fichero + y el grafo lo dio por sellado — mismo número, misma línea, imposible de distinguir de la + corrida buena. Es el eslabón que describe la regla 3 del CLAUDE.md: `Store::has` ya rechaza los + vacíos, pero cada consumidor que mire presencia de directorio los vuelve a leer como presencia. + **Un ausente falla ruidosamente; un vacío llega hasta el final diciendo que todo fue bien.** + """ + try: + with os.scandir(ruta) as it: + return next(it, None) is not None + except (NotADirectoryError, FileNotFoundError): + return False + + def state_of(iname, h): if h is None: return "unhashable" dirname = f"{h[3:]}-{iname}" - if os.path.isdir(os.path.join(STORE, dirname)): + if _artefacto_presente(os.path.join(STORE, dirname)): return "sealed" if dirname in SELLADOS_REMOTOS: REMOTO_ONLY.add(dirname)