fix(granja): un fichero de estado NUEVO trababa el adelanto de la rama, para siempre
Bloqueo circular, destapado al añadir `docs/state/cierres.json` y medido en la caja: la comprobación «el árbol ya coincide con el destino» usaba `git diff <commit> -- <ruta>`, y **`git diff` ignora las rutas SIN SEGUIR**. Para un fichero de estado nuevo el commit lo tiene y el índice no, así que aparece eternamente como «difiere» ⇒ la rama no adelanta ⇒ el fichero nunca pasa a seguido ⇒ vuelve a diferir. El síntoma era «la rama local queda detrás» en CADA ciclo, por un fichero que en el disco era byte a byte el publicado. Ahora se compara el contenido del disco: `git hash-object` del fichero contra el blob del commit, que funciona igual para rutas seguidas y sin seguir. Una ruta que el commit tiene y el disco no cuenta como diferencia, que es lo correcto — ahí el ff sí traería algo nuevo y no debe saltarse. Control en repo de juguete con un fichero de estado nuevo apareciendo: ciclo 1 adelanta, ciclo 2 sigue limpio, `HEAD == origin/main` y cero sucios. Es la tercera vez en esta serie que el fallo no está en lo que el guion hace sino en lo que una herramienta NO ve: `git diff` y los untracked acá, `git merge --ff-only` mirando índice-contra-HEAD en vez de árbol-contra-destino antes, y `du` colgándose en el subárbol gordo el otro día. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Delta semántico
Ninguna definición cambió: el cambio es de texto, documentación o datos.
1 archivos · +23 −1
diff --git a/scripts/farm/publicar-estado.sh b/scripts/farm/publicar-estado.sh
index b8ee5145..a14acebd 100755
--- a/scripts/farm/publicar-estado.sh
+++ b/scripts/farm/publicar-estado.sh
@@ -78,6 +78,21 @@ done
# `push`. Eso no es un error: es la carrera normal de un repo compartido, y la respuesta correcta
# es reconstruir el commit sobre la cabeza nueva, no forzar. `--force` nunca aparece acá: pisaría
# el trabajo del otro, que es justo lo que el latido no debe poder hacer ni por accidente.
+# ¿El árbol de trabajo ya tiene, byte a byte, lo que el commit destino pone en cada ruta que el
+# fast-forward tocaría? Se compara `git hash-object` del fichero EN DISCO contra el blob del commit,
+# que funciona igual para rutas seguidas y sin seguir — y una ruta que el commit tiene y el disco no
+# cuenta como diferencia, que es lo correcto: ahí el ff sí traería algo nuevo.
+_arbol_coincide_con() {
+ local destino="$1" ruta blob disco
+ for ruta in $(git diff --name-only HEAD "$destino" 2>/dev/null); do
+ blob="$(git rev-parse "$destino:$ruta" 2>/dev/null)" || return 1
+ [ -f "$ruta" ] || return 1
+ disco="$(git hash-object -- "$ruta" 2>/dev/null)" || return 1
+ [ "$blob" = "$disco" ] || return 1
+ done
+ return 0
+}
+
intento=0
while :; do
intento=$((intento + 1))
@@ -177,8 +192,15 @@ while :; do
# intacto y reescribe el índice —y por eso va GUARDADO por «el índice no tiene nada»: el índice
# es estado COMPARTIDO (regla 2) y pisar lo que otro agente dejó en `git add` es exactamente el
# daño que este repo ya documentó. Si hay algo stageado, no se toca y se dice.
+ # ⚠ La comprobación «el árbol ya coincide con el destino» se hace COMPARANDO EL CONTENIDO DEL
+ # DISCO, no con `git diff <commit> -- <ruta>`. Y es un bloqueo circular medido (2026-10-10, al
+ # añadir `cierres.json`): `git diff` **ignora las rutas sin seguir**, así que un fichero de
+ # estado NUEVO aparece eternamente como «difiere» —el commit lo tiene, el índice no— y la rama
+ # no adelanta nunca; pero volverlo seguido es justamente lo que el adelanto haría. El síntoma
+ # era «la rama local queda detrás» en cada ciclo, para siempre, por un fichero que en el disco
+ # era byte a byte el publicado.
elif git diff --cached --quiet HEAD 2>/dev/null \
- && git diff --quiet "$commit" -- $(git diff --name-only HEAD "$commit") 2>/dev/null; then
+ && _arbol_coincide_con "$commit"; then
git reset --mixed -q "$commit" \
&& echo " rama local al día (el árbol ya tenía el contenido publicado)" \
|| echo " la rama local queda detrás — lo publicado NO depende de eso"