Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .harness/scripts/check-gap-registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,49 @@ def estados_registro() -> dict[str, str]:
CERRADO_REGISTRO = {"RESOLVED"}


# La cuarta superficie, y la que llevaba cinco filas mintiendo. La cabecera del board
# declara un recuento; nada lo contrastaba contra las filas, así que cerrar un ítem y
# olvidar el encabezado no lo veía nadie. Ocurrió entre el 2026-08-01 y el 2026-08-04:
# `CP-02`, `CP-04`, `CP-06`, `CP-14` y `GAP-020` pasaron a DONE y la cabecera se quedó
# en 174/17 — un documento de gobernanza afirmando 18 pendientes sobre una tabla con 13.
PROGRESO = re.compile(
r"\*\*Progress:\*\* (\d+) / (\d+) done · (\d+) pending · (\d+) in progress · "
r"(\d+) blocked · (\d+) deferred · (\d+) superseded · (\d+) wontfix"
)


def progreso_declarado():
"""Los números de la cabecera, o None si la línea no es legible."""
m = PROGRESO.search(BOARD.read_text())
if not m:
return None
d, total, p, ip, bl, df, sp, wf = (int(x) for x in m.groups())
return (
{"DONE": d, "TOTAL": total, "PENDING": p, "IN-PROGRESS": ip,
"BLOCKED": bl, "DEFERRED": df, "SUPERSEDED": sp, "WONTFIX": wf},
m.group(0),
)


def problemas_de_recuento(board: dict[str, str]) -> list[str]:
declarado = progreso_declarado()
if declarado is None:
# Ilegible no es correcto: una cabecera que este barrido no encuentra es
# exactamente la que puede mentir sin que nadie la contradiga.
return ["la línea `**Progress:**` del board no existe o no es legible por este guard"]
nums, linea = declarado
real = Counter(board.values())
fallos = []
if nums["TOTAL"] != len(board):
fallos.append(f"la cabecera dice {nums['TOTAL']} filas y la tabla tiene {len(board)}")
for estado in ("DONE", "PENDING", "IN-PROGRESS", "BLOCKED", "DEFERRED", "SUPERSEDED", "WONTFIX"):
if nums[estado] != real.get(estado, 0):
fallos.append(
f"la cabecera dice {nums[estado]} {estado} y la tabla tiene {real.get(estado, 0)}"
)
return [f"recuento del board: {f}" for f in fallos]


def main() -> int:
board = estados_board()
catalogo, dobles, sangradas = estados_catalogo()
Expand All @@ -131,6 +174,8 @@ def main() -> int:

problemas = []

problemas.extend(problemas_de_recuento(board))

for gid in sorted(set(registro) & set(fichas)):
if registro[gid] != fichas[gid]:
problemas.append(
Expand Down
57 changes: 54 additions & 3 deletions .harness/scripts/check-gap-registry.test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
una contradicción y no debe reportarse como tal.
"""
import shutil
from collections import Counter
import subprocess
import sys
import tempfile
Expand All @@ -19,7 +20,7 @@
GUARD = Path(__file__).resolve().parent / "check-gap-registry.py"


def _repo(tmp: Path, filas, fichas=None):
def _repo(tmp: Path, filas, fichas=None, progreso=None):
"""Monta un repositorio de usar y tirar con las tres superficies coherentes salvo
en lo que cada prueba quiera romper. `filas` = [(id, estado_board, estado_registro)];
un estado a None omite la fila en esa superficie."""
Expand Down Expand Up @@ -48,15 +49,30 @@ def _repo(tmp: Path, filas, fichas=None):
estado = (fichas or {}).get(gid, er)
registro += ["", f"### Detail {gid}", "", f"- **Status:** {estado}", ""]

# Cabecera coherente con las filas recien escritas. El guard contrasta el recuento
# declarado contra la tabla, asi que un board de prueba SIN cabecera es un board que
# miente — y relajar el guard para que la acepte seria desarmar justo lo que vigila.
# `progreso` permite a una prueba declarar numeros falsos a proposito.
conteo = Counter(eb for _gid, eb, _er in filas if eb is not None)
decl = progreso or {}
def n(estado):
return decl.get(estado, conteo.get(estado, 0))
total = decl.get("TOTAL", sum(conteo.values()))
board.append("")
board.append(
f"**Progress:** {n('DONE')} / {total} done · {n('PENDING')} pending · "
f"{n('IN-PROGRESS')} in progress · {n('BLOCKED')} blocked · {n('DEFERRED')} deferred · "
f"{n('SUPERSEDED')} superseded · {n('WONTFIX')} wontfix"
)
(tmp / "docs/audit/tracker-gap-tracking.md").write_text("\n".join(board) + "\n")
(tmp / "docs/audit/tracker-gap-reference-catalog.md").write_text("\n".join(catalogo) + "\n")
(tmp / "docs/audit/tracker-gaps-opportunities-tracking.md").write_text("\n".join(registro) + "\n")
return tmp / ".harness/scripts" / GUARD.name


def correr(filas, fichas=None):
def correr(filas, fichas=None, progreso=None):
with tempfile.TemporaryDirectory() as d:
script = _repo(Path(d), filas, fichas)
script = _repo(Path(d), filas, fichas, progreso)
p = subprocess.run([sys.executable, str(script)], capture_output=True, text=True)
return p.returncode, p.stdout + p.stderr

Expand Down Expand Up @@ -123,6 +139,41 @@ def test_fila_sin_ficha_falla(self):
self.assertEqual(code, 1)
self.assertIn("sin ficha", salida)

def test_cabecera_que_declara_menos_cerrados_falla(self):
"""El defecto real: cinco filas cerraron y el encabezado se quedo atras. Entre el
2026-08-01 y el 2026-08-04 `CP-02`, `CP-04`, `CP-06`, `CP-14` y `GAP-020` pasaron a
DONE con la cabecera intacta, y este guard pasaba en verde porque solo contrastaba
board, catalogo y registro — nunca el recuento."""
code, salida = correr(
[("GAP-001", "DONE", "🟢 RESOLVED"), ("GAP-002", "DONE", "🟢 RESOLVED")],
progreso={"DONE": 1, "PENDING": 1},
)
self.assertEqual(code, 1)
self.assertIn("la cabecera dice 1 DONE y la tabla tiene 2", salida)

def test_cabecera_con_total_equivocado_falla(self):
"""El total es su propio contador y puede derivar solo: una fila anadida sin tocar
el encabezado no cambia ningun estado, solo el denominador."""
code, salida = correr(
[("GAP-001", "DONE", "🟢 RESOLVED")], progreso={"TOTAL": 7}
)
self.assertEqual(code, 1)
self.assertIn("7 filas y la tabla tiene 1", salida)

def test_board_sin_cabecera_falla(self):
"""Ilegible no es correcto. Una cabecera que este barrido no encuentra es
exactamente la que puede mentir sin que nadie la contradiga, asi que su ausencia
se reporta en vez de tolerarse."""
with tempfile.TemporaryDirectory() as d:
script = _repo(Path(d), [("GAP-001", "DONE", "🟢 RESOLVED")])
board = Path(d) / "docs/audit/tracker-gap-tracking.md"
board.write_text(
"\n".join(l for l in board.read_text().splitlines() if "**Progress:**" not in l) + "\n"
)
p = subprocess.run([sys.executable, str(script)], capture_output=True, text=True)
self.assertEqual(p.returncode, 1)
self.assertIn("no es legible", p.stdout + p.stderr)


if __name__ == "__main__":
unittest.main()
2 changes: 1 addition & 1 deletion docs/audit/tracker-gap-tracking.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ This board is the single source of truth for Tracker technical debt, gaps, oppor
| [`GT-480`](./tracker-gap-reference-catalog.md#gt-480) | El job de despliegue corría también para cambios de sólo documentación | ~14 min de CI para publicar dos ficheros markdown | Sale a su propio workflow con `paths-ignore`; lista negra y no blanca, porque la blanca se queda obsoleta en silencio | `Infra` | Cross | P3 | XS | `DONE` |
| [`GT-481`](./tracker-gap-reference-catalog.md#gt-481) | El despliegue se comprobaba dos veces sobre el mismo árbol | ~7 min de clúster Kubernetes repetidos sobre contenido idéntico | Deja de correr en push a `main`; y las esperas fijas pasan a sondeo por hecho observable | `Infra` | Cross | P3 | XS | `DONE` |

**Progress:** 174 / 204 done · 18 pending · 0 in progress · 3 blocked · 7 deferred · 1 superseded · 1 wontfix
**Progress:** 179 / 204 done · 13 pending · 0 in progress · 3 blocked · 7 deferred · 1 superseded · 1 wontfix
*(Conteos reconciliados contra `python3 .harness/scripts/check-gap-registry.py` el 2026-08-01 al cerrar `CP-01` y `CP-08`: 203 fichas / 203 filas; estados `{PENDING: 17, DONE: 174, DEFERRED: 7, BLOCKED: 3, SUPERSEDED: 1, WONTFIX: 1}`.)*

**Wave 2026-06-07 → 2026-06-14 (BMAD audit + coherence):** Items `GAP-*`, `COH-*`, `OPP-*` from the PROMPT MAESTRO functional/technical/documentary audit and the source-coherence analysis (106 items: 81 resolved, 24 open, 1 blocked, 1 deferred at import time).
Expand Down
Loading