From 516db8efc0dab86f1ba09e34d9cf29582abe4f56 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Wed, 3 Jun 2026 16:56:53 +0200 Subject: [PATCH] feat(infra): auto-commit con 10 cambios Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/CLAUDE.md | 2 +- .claude/commands/e2e-cpp.md | 28 ++++++++++++++---- .claude/rules/INDEX.md | 2 +- .claude/rules/apps_subrepo.md | 30 ++++++++++++++++++++ .claude/rules/cpp_apps.md | 2 +- .gitignore | 4 +-- bash/functions/infra/discover_git_repos.sh | 2 +- cpp/PATTERNS.md | 33 ++++++++++++++++++++++ cpp/framework/app_base.cpp | 32 +++++++++++++++++++++ cpp/framework/app_base.h | 15 ++++++++++ 10 files changed, 138 insertions(+), 12 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index abeb7fff..539b8a7c 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -21,7 +21,7 @@ Cualquier decision tecnica que choque con estos objetivos esta mal priorizada. E **Sync entre PCs:** `fn sync` sincroniza datos no regenerables (proposals, apps, projects, analysis, vaults, pc_locations) contra `registry_api` en `https://registry.organic-machine.com`. Config: `~/.fn_pc` (identidad del PC), `FN_REGISTRY_API` (URL con basicAuth), `REGISTRY_API_TOKEN` (token). -**Sub-repos:** cada app y cada analysis es su propio repo Gitea en `dataforge/` con branch `master` (ver ADR 0002). `apps/*` y `analysis/*` estan en el `.gitignore` del repo padre — el codigo de cada app vive en `apps//.git/`. Los slash commands `/full-git-push` y `/full-git-pull` orquestan push/pull/clone de fn_registry + todos los sub-repos + `fn sync`. `/full-git-push` auto-inicializa apps/analyses sin `.git` via `ensure_repo_synced_bash_infra`. Los `vaults/` y `subrepos/` NO entran en este flujo. **Gotcha worktrees**: si creas una app nueva dentro de un git worktree del repo padre, haz `git init` dentro de `apps//` ANTES de limpiar el worktree, sino el codigo se pierde (apps/* gitignored). Ver `.claude/rules/apps_subrepo.md`. +**Sub-repos:** cada app y cada analysis es su propio repo Gitea en `dataforge/` con branch `master` (ver ADR 0002). `apps/*` y `analysis/*` estan en el `.gitignore` del repo padre — el codigo de cada app vive en `apps//.git/`. Los slash commands `/full-git-push` y `/full-git-pull` orquestan push/pull/clone de fn_registry + todos los sub-repos + `fn sync`. `/full-git-push` auto-inicializa apps/analyses sin `.git` via `ensure_repo_synced_bash_infra`. Los `vaults/` y `subrepos/` NO entran en este flujo. **Gotcha worktrees**: si creas una app nueva dentro de un git worktree del repo padre, haz `git init` dentro de `apps//` ANTES de limpiar el worktree, sino el codigo se pierde (apps/* gitignored). **REGLA DURA**: el repo padre NUNCA trackea contenido de artefactos hijos (apps/analysis/projects) — solo `.gitkeep`. Nada de `git add -f` sobre esos paths: deja el padre permanentemente dirty (doble-tracking). Auditoria + fix en `.claude/rules/apps_subrepo.md`. Ver `.claude/rules/apps_subrepo.md`. **Artefactos:** termino paraguas para apps, analysis, vaults, projects y playgrounds — todo lo que NO es codigo reutilizable. Usa "artefacto" cuando una afirmacion aplica a varios tipos a la vez para no repetir la lista. Ver `.claude/rules/artefactos.md` y `.claude/rules/playgrounds.md`. diff --git a/.claude/commands/e2e-cpp.md b/.claude/commands/e2e-cpp.md index db7c21e7..db308710 100644 --- a/.claude/commands/e2e-cpp.md +++ b/.claude/commands/e2e-cpp.md @@ -173,23 +173,39 @@ Si el build falla: - "undefined reference to render" → falta quitar `static` o falta el `#ifndef FN_TEST_BUILD` en main.cpp. - "multiple definition of main" → falta el `target_compile_definitions(... FN_TEST_BUILD)` en CMakeLists. -### 8. Ejecutar (headless en WSL) +### 8. Ejecutar (headless preferente — sin parpadeo) -WSL no tiene GLX 4.3 nativo — los tests corren bajo `xvfb` con software renderer Mesa. Wrapper canonico: +`fn::run_app_test` crea la ventana GLFW **oculta por defecto** (`GLFW_VISIBLE=FALSE`, ver `cpp/framework/app_base.cpp`). El contexto GL real se crea igual, así que el render que ejercita el Test Engine es fiel, pero la ventana nunca se mapea en pantalla: cero parpadeo, no roba foco. Por eso los tests de frontend C++ corren headless por defecto, sin tocar el código de cada app. + +Dos formas de lanzar, según el entorno: ```bash cd "$ROOT/cpp/build/linux_tests" TEST_BIN="$(find . -name "${APP_ARG}_tests" -type f -executable | head -1)" [ -z "$TEST_BIN" ] && { echo "no encuentro el binario de tests"; exit 1; } -timeout 90 xvfb-run -a -s "-screen 0 1280x800x24" \ - env LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe \ - "$TEST_BIN" 2>&1 +if [ -n "$DISPLAY" ] && command -v glxinfo >/dev/null 2>&1 \ + && glxinfo 2>/dev/null | grep -q "OpenGL core profile version"; then + # Host con GL nativo (PC enmanuel, X11 + GPU): binario directo. + # La ventana ya nace oculta -> sin parpadeo, y usa la GPU real (rapido). + timeout 90 "$TEST_BIN" 2>&1 +else + # CI / WSL sin GLX 4.3 nativo: display virtual en RAM + software Mesa. + timeout 90 xvfb-run -a -s "-screen 0 1280x800x24" \ + env LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe \ + "$TEST_BIN" 2>&1 +fi EXIT=$? echo "EXIT: $EXIT" ``` -Si en el host el usuario tiene GL nativo y `DISPLAY` funciona, el wrapper xvfb-run sigue siendo seguro (ejecuta dentro de su propio display). +Ambas vías son headless. `xvfb-run` sigue siendo seguro en host con display (corre en su propio display virtual), así que si el sniff de GL falla puedes usar siempre la rama xvfb. + +**Para depurar un test a ojo** (ver la UI mientras el engine la maneja), desactiva el headless con `FN_HEADLESS=0`: + +```bash +FN_HEADLESS=0 timeout 90 "$TEST_BIN" 2>&1 +``` ### 9. Reportar diff --git a/.claude/rules/INDEX.md b/.claude/rules/INDEX.md index 45363773..e3c5144a 100644 --- a/.claude/rules/INDEX.md +++ b/.claude/rules/INDEX.md @@ -21,7 +21,7 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente. | 15 | [projects.md](projects.md) | Projects: agrupar apps, analysis y vaults bajo un tema | | 16 | [kiss.md](kiss.md) | KISS en proyectos y apps: cuestionar herramientas externas, sin abstracciones especulativas | | 17 | [apps_tbd.md](apps_tbd.md) | Trunk-based development obligatorio en apps generadas con `fn` (registry exento) | -| 17b | [apps_subrepo.md](apps_subrepo.md) | Apps son sub-repos Gitea (apps/* gitignored). `git init` dentro de cada app nueva ANTES de limpiar worktree, sino se pierde el codigo | +| 17b | [apps_subrepo.md](apps_subrepo.md) | Apps son sub-repos Gitea (apps/* gitignored). El padre NUNCA trackea contenido de artefactos hijos (solo `.gitkeep`); nada de `git add -f` sobre apps/analysis/projects o deja el padre dirty. `git init` dentro de cada app nueva ANTES de limpiar worktree, sino se pierde el codigo | | 18 | [uses_functions.md](uses_functions.md) | Convencion de uses_functions para C++: el .md del consumidor declara las dependencias | | 19 | [cpp_apps.md](cpp_apps.md) | Estandarizacion de apps C++: estructura, CMake, app.md, sub-repo, runtime — apunta a cpp/PATTERNS.md y cpp/DESIGN_SYSTEM.md como autoritativas | | 20 | [artefactos.md](artefactos.md) | Termino paraguas para apps, analysis, vaults, projects y playgrounds (todo lo que no es codigo reutilizable) | diff --git a/.claude/rules/apps_subrepo.md b/.claude/rules/apps_subrepo.md index 481a1277..adca2d06 100644 --- a/.claude/rules/apps_subrepo.md +++ b/.claude/rules/apps_subrepo.md @@ -45,6 +45,36 @@ Cuando el humano corre `/full-git-push` despues del merge, el script `ensure_rep Todo lo demas (codigo de la app + app.md + appicon + service unit + tests propios de la app) vive en `apps//.git` independiente. +### REGLA DURA: el repo padre NUNCA trackea contenido de artefactos hijos + +El repo padre `fn_registry` solo versiona codigo del registry (`functions/`, `types/`, `registry/`, `cmd/`, `docs/`, `.claude/`, `dev/`, `migrations/`, y el framework/functions/vendor de `cpp/`). NUNCA debe trackear el contenido de un artefacto hijo: + +- apps: `apps/*`, `cpp/apps/*`, `projects/*/apps/*` +- analyses: `analysis/*`, `projects/*/analysis/*` +- projects: `projects/*` + +Cada artefacto es un sub-repo Gitea independiente con su propio `.git`; su contenido completo (codigo, `app.md`, `analysis.md`, `appicon.*`, binarios, frontend, `local_files/`, tests propios) vive SOLO en ese sub-repo. `fn index` lee los `.md` de registro directamente del disco — no necesitan estar en el git del padre. Lo unico que el padre versiona dentro de esos arboles son los marcadores `.gitkeep` (mantienen `apps/` y `analysis/` presentes cuando estan vacios) y, en `projects/`, los `project.md` template si los hubiera. + +**Como se rompe (sintoma = repo padre permanentemente dirty):** un `git add -f apps//...` (forzado, saltandose el `.gitignore`) o un commit que mete contenido del hijo al padre. Como el archivo ya queda en el indice, el `.gitignore` NO lo vuelve a ignorar y aparece para siempre en `git status` del padre como modificado cada vez que el sub-repo cambia (doble-tracking). Caso real (2026-06-03): `apps/dag_engine/` (31 archivos: Go + frontend + app.md) y `apps/shaders_lab/` (app.md + un binario `.exe` de 23 MB) quedaron forzados al indice del padre y lo dejaban dirty en cada cambio del sub-repo. + +**Auditoria (cero salida = sano):** + +```bash +git ls-files 'apps/*' 'analysis/*' 'projects/*/apps/*' 'projects/*/analysis/*' 'cpp/apps/*' \ + | grep -vE '(^|/)\.gitkeep$' +``` + +**Fix si aparece contenido trackeado:** + +```bash +# --cached SIEMPRE: saca del indice del padre sin borrar el working tree. +# El codigo sigue a salvo en el .git del sub-repo. +git rm -r --cached apps/ +git commit -m "chore: untrack contenido del artefacto (es sub-repo Gitea)" +``` + +NUNCA `git rm` sin `--cached` (borraria el working tree del sub-repo). **Prevencion:** jamas usar `git add -f` sobre paths de artefactos; las reglas `apps/*/`, `analysis/*/`, `projects/*/` del `.gitignore` ya cubren el caso por defecto y solo un force las salta. + ### Sintomas de la perdida Si limpias el worktree y luego corres `ls apps//`, devuelve "No such file or directory" pese a que el issue aparece cerrado en `dev/issues/completed/`. **Patron** = scaffold sin sub-repo init = trabajo perdido. diff --git a/.claude/rules/cpp_apps.md b/.claude/rules/cpp_apps.md index 88075fcd..db63ac4d 100644 --- a/.claude/rules/cpp_apps.md +++ b/.claude/rules/cpp_apps.md @@ -131,7 +131,7 @@ El `if(EXISTS ...)` hace el registro tolerante a apps no clonadas (cada app es s ### 6. Sub-repo Gitea (TBD obligatorio) Cada app C++ es su propio repo en `dataforge/` con branch `master`. Esto significa: -- El directorio `/` esta en el `.gitignore` de `fn_registry` (excepto `app.md`). +- TODO el directorio `/` (incluido `app.md`, `appicon.*`, binarios y `local_files/`) esta en el `.gitignore` de `fn_registry`: el repo padre NUNCA versiona contenido del artefacto. `fn index` lee `app.md` directo del disco, no del git. NO forzar con `git add -f` — deja el padre dirty. Ver la regla dura en `apps_subrepo.md`. - El propio directorio tiene `.git/` apuntando al sub-repo. - TBD obligatorio mientras se desarrolla la app: ver `apps_tbd.md`. Trabajar en `issue/-` o `quick/`, mergear a `master` con `--no-ff`. - Sync entre PCs y push/pull se gestionan con `/full-git-push` y `/full-git-pull`. diff --git a/.gitignore b/.gitignore index 20cbe1c9..3d2f737a 100644 --- a/.gitignore +++ b/.gitignore @@ -67,8 +67,8 @@ worktrees/ # Temp — workspace efimero para pruebas rapidas (APIs, scripts, analisis) temp/ -# C++ build artifacts -cpp/build/ +# C++ build artifacts (build/, build-tests/, build-windows/, etc.) +cpp/build*/ /build/ # OS diff --git a/bash/functions/infra/discover_git_repos.sh b/bash/functions/infra/discover_git_repos.sh index 7bfefc8f..0f7e4915 100644 --- a/bash/functions/infra/discover_git_repos.sh +++ b/bash/functions/infra/discover_git_repos.sh @@ -32,7 +32,7 @@ discover_git_repos() { -not -path "*/node_modules/*" \ -not -path "*/.venv/*" \ -not -path "*/cpp/vendor/*" \ - -not -path "*/cpp/build/*" \ + -not -path "*/cpp/build*/*" \ -not -path "*/sources/*" \ -not -path "*/temp/*" \ -not -path "*/subrepos/*" \ diff --git a/cpp/PATTERNS.md b/cpp/PATTERNS.md index cd0cd501..3532a8cf 100644 --- a/cpp/PATTERNS.md +++ b/cpp/PATTERNS.md @@ -169,6 +169,39 @@ Para diagnosticar un diff: revisar el PNG actual en `cpp/build/tests/visual_actual/.png` vs el golden en `cpp/tests/golden/.png`. +### Tests de UI headless (Dear ImGui Test Engine) + +`fn::run_app_test` (el harness del Test Engine usado por `/e2e-cpp`) crea la +ventana GLFW **oculta por defecto** (`GLFW_VISIBLE=FALSE`). El contexto OpenGL +real se crea igual, así que el render que el Test Engine ejercita sigue siendo +fiel, pero la ventana nunca se mapea en pantalla: cero parpadeo y no roba foco +mientras corre la suite. Es el comportamiento preferente para tests de +frontend en C++. + +Control del modo (en orden de prioridad): + +| Mecanismo | Efecto | +|---|---| +| `FN_HEADLESS=0` (env) | Fuerza ventana **visible** — para depurar un test a ojo. | +| `FN_HEADLESS=1` (env) | Fuerza oculta (es el default del path de test). | +| `cfg.headless = true` | Oculta también `fn::run_app` (apps reales, p.ej. smoke/capture). | +| sin nada | `run_app_test` → oculta; `run_app` → visible. | + +Cómo correr la suite sin parpadeo: + +```bash +# Host con GL nativo (GPU real): binario directo, ventana oculta, sin parpadeo. +./build/linux_tests/apps//_tests + +# CI / WSL sin display: display virtual en RAM (también headless). +xvfb-run -a -s "-screen 0 1280x800x24" \ + env LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe \ + ./build/linux_tests/apps//_tests + +# Ver un test a ojo (desactiva headless): +FN_HEADLESS=0 ./build/linux_tests/apps//_tests +``` + ### CI gate `check_tested.sh` `cpp/scripts/check_tested.sh [days]` (default `30`) consulta `registry.db` y diff --git a/cpp/framework/app_base.cpp b/cpp/framework/app_base.cpp index 13cae0f8..4a33dcfe 100644 --- a/cpp/framework/app_base.cpp +++ b/cpp/framework/app_base.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -647,6 +648,25 @@ static void draw_header_badge_on_floating_panels(const AppConfig& cfg) { } } +// Resuelve si la ventana GLFW debe crearse oculta (GLFW_VISIBLE=FALSE). +// default_hidden : politica base del path de entrada (apps reales = false, +// tests de UI = true). +// config_headless: AppConfig.headless explicito de la app. +// El entorno FN_HEADLESS gana sobre ambos: "0"/"false" fuerza visible, +// cualquier otro valor no vacio fuerza oculta. Sin la variable, se respeta +// default_hidden || config_headless. +static bool resolve_headless(bool default_hidden, bool config_headless) { + bool hidden = default_hidden || config_headless; + if (const char* e = std::getenv("FN_HEADLESS")) { + if (std::strcmp(e, "0") == 0 || std::strcmp(e, "false") == 0) { + hidden = false; + } else if (e[0] != '\0') { + hidden = true; + } + } + return hidden; +} + int run_app(AppConfig config, std::function render_fn) { // Logger primero para capturar fallos del propio init (GLFW, ventana, GL). if (config.log.file_path != nullptr) { @@ -672,6 +692,11 @@ int run_app(AppConfig config, std::function render_fn) { glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); + // Apps reales: ventana visible por defecto. Solo se oculta si la app pide + // headless o el entorno FN_HEADLESS lo fuerza (smoke/capture sin parpadeo). + const bool hidden = resolve_headless(/*default_hidden=*/false, config.headless); + glfwWindowHint(GLFW_VISIBLE, hidden ? GLFW_FALSE : GLFW_TRUE); + GLFWwindow* window = glfwCreateWindow(config.width, config.height, config.title, nullptr, nullptr); if (!window) { fprintf(stderr, "Failed to create GLFW window\n"); @@ -1178,6 +1203,13 @@ int run_app_test(AppConfig config, glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); + // Tests de frontend: ventana OCULTA por defecto (headless) para no parpadear + // en la pantalla del desarrollador ni robar foco mientras el Test Engine + // ejercita la UI. El contexto GL real se crea igual, asi que el render sigue + // siendo fiel. Opt-out para depurar visualmente: FN_HEADLESS=0. + const bool hidden = resolve_headless(/*default_hidden=*/true, config.headless); + glfwWindowHint(GLFW_VISIBLE, hidden ? GLFW_FALSE : GLFW_TRUE); + GLFWwindow* window = glfwCreateWindow( config.width, config.height, config.title ? config.title : "fn_test", nullptr, nullptr); diff --git a/cpp/framework/app_base.h b/cpp/framework/app_base.h index 5f966de1..4c6daf4b 100644 --- a/cpp/framework/app_base.h +++ b/cpp/framework/app_base.h @@ -101,6 +101,21 @@ struct AppConfig { int height = 720; bool vsync = true; bool viewports = true; // Multi-viewport ON por defecto: ventanas ImGui arrastrables fuera del main window + + // Headless: si true, la ventana GLFW se crea oculta (GLFW_VISIBLE=FALSE). + // El contexto OpenGL real se sigue creando y el render ocurre offscreen, + // por lo que las pruebas visuales y de UI siguen siendo fieles, pero la + // ventana nunca se mapea en pantalla (cero parpadeo, no roba foco). + // + // Politica por path: + // - run_app (apps reales): default visible (headless = false). + // - run_app_test (Dear ImGui Test Engine): default OCULTA. Los tests de + // frontend corren headless salvo opt-out explicito para debug visual. + // + // Override por entorno (gana sobre el default del path y sobre este flag): + // FN_HEADLESS=1 / true -> fuerza ventana oculta. + // FN_HEADLESS=0 / false -> fuerza ventana visible (ej. ver un test). + bool headless = false; ThemeMode theme = ThemeMode::FnDark; // Identidad visual unificada por defecto float bg_r = 0.102f; // fn_tokens::colors::bg (dark.7 #1A1B1E) float bg_g = 0.106f;