feat(infra): auto-commit con 10 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 16:56:53 +02:00
parent fa09ff9866
commit 516db8efc0
10 changed files with 138 additions and 12 deletions
+1 -1
View File
@@ -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). **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/<basename>` con branch `master` (ver ADR 0002). `apps/*` y `analysis/*` estan en el `.gitignore` del repo padre — el codigo de cada app vive en `apps/<name>/.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/<name>/` 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/<basename>` con branch `master` (ver ADR 0002). `apps/*` y `analysis/*` estan en el `.gitignore` del repo padre — el codigo de cada app vive en `apps/<name>/.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/<name>/` 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`. **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`.
+20 -4
View File
@@ -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. - "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. - "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 ```bash
cd "$ROOT/cpp/build/linux_tests" cd "$ROOT/cpp/build/linux_tests"
TEST_BIN="$(find . -name "${APP_ARG}_tests" -type f -executable | head -1)" TEST_BIN="$(find . -name "${APP_ARG}_tests" -type f -executable | head -1)"
[ -z "$TEST_BIN" ] && { echo "no encuentro el binario de tests"; exit 1; } [ -z "$TEST_BIN" ] && { echo "no encuentro el binario de tests"; exit 1; }
timeout 90 xvfb-run -a -s "-screen 0 1280x800x24" \ 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 \ env LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe \
"$TEST_BIN" 2>&1 "$TEST_BIN" 2>&1
fi
EXIT=$? EXIT=$?
echo "EXIT: $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 ### 9. Reportar
+1 -1
View File
@@ -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 | | 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 | | 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) | | 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 | | 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 | | 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) | | 20 | [artefactos.md](artefactos.md) | Termino paraguas para apps, analysis, vaults, projects y playgrounds (todo lo que no es codigo reutilizable) |
+30
View File
@@ -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/<name>/.git` independiente. Todo lo demas (codigo de la app + app.md + appicon + service unit + tests propios de la app) vive en `apps/<name>/.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/<x>/...` (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/<x>
git commit -m "chore: untrack contenido del artefacto <x> (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 ### Sintomas de la perdida
Si limpias el worktree y luego corres `ls apps/<name>/`, 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. Si limpias el worktree y luego corres `ls apps/<name>/`, 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.
+1 -1
View File
@@ -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) ### 6. Sub-repo Gitea (TBD obligatorio)
Cada app C++ es su propio repo en `dataforge/<name>` con branch `master`. Esto significa: Cada app C++ es su propio repo en `dataforge/<name>` con branch `master`. Esto significa:
- El directorio `<app_dir>/` esta en el `.gitignore` de `fn_registry` (excepto `app.md`). - TODO el directorio `<app_dir>/` (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. - El propio directorio tiene `.git/` apuntando al sub-repo.
- TBD obligatorio mientras se desarrolla la app: ver `apps_tbd.md`. Trabajar en `issue/<NNNN>-<slug>` o `quick/<slug>`, mergear a `master` con `--no-ff`. - TBD obligatorio mientras se desarrolla la app: ver `apps_tbd.md`. Trabajar en `issue/<NNNN>-<slug>` o `quick/<slug>`, mergear a `master` con `--no-ff`.
- Sync entre PCs y push/pull se gestionan con `/full-git-push` y `/full-git-pull`. - Sync entre PCs y push/pull se gestionan con `/full-git-push` y `/full-git-pull`.
+2 -2
View File
@@ -67,8 +67,8 @@ worktrees/
# Temp — workspace efimero para pruebas rapidas (APIs, scripts, analisis) # Temp — workspace efimero para pruebas rapidas (APIs, scripts, analisis)
temp/ temp/
# C++ build artifacts # C++ build artifacts (build/, build-tests/, build-windows/, etc.)
cpp/build/ cpp/build*/
/build/ /build/
# OS # OS
+1 -1
View File
@@ -32,7 +32,7 @@ discover_git_repos() {
-not -path "*/node_modules/*" \ -not -path "*/node_modules/*" \
-not -path "*/.venv/*" \ -not -path "*/.venv/*" \
-not -path "*/cpp/vendor/*" \ -not -path "*/cpp/vendor/*" \
-not -path "*/cpp/build/*" \ -not -path "*/cpp/build*/*" \
-not -path "*/sources/*" \ -not -path "*/sources/*" \
-not -path "*/temp/*" \ -not -path "*/temp/*" \
-not -path "*/subrepos/*" \ -not -path "*/subrepos/*" \
+33
View File
@@ -169,6 +169,39 @@ Para diagnosticar un diff: revisar el PNG actual en
`cpp/build/tests/visual_actual/<demo>.png` vs el golden en `cpp/build/tests/visual_actual/<demo>.png` vs el golden en
`cpp/tests/golden/<demo>.png`. `cpp/tests/golden/<demo>.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/<app>/<app>_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/<app>/<app>_tests
# Ver un test a ojo (desactiva headless):
FN_HEADLESS=0 ./build/linux_tests/apps/<app>/<app>_tests
```
### CI gate `check_tested.sh` ### CI gate `check_tested.sh`
`cpp/scripts/check_tested.sh [days]` (default `30`) consulta `registry.db` y `cpp/scripts/check_tested.sh [days]` (default `30`) consulta `registry.db` y
+32
View File
@@ -23,6 +23,7 @@
#include <atomic> #include <atomic>
#include <cmath> #include <cmath>
#include <cstdio> #include <cstdio>
#include <cstdlib>
#include <cstring> #include <cstring>
#include <filesystem> #include <filesystem>
#include <string> #include <string>
@@ -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<void()> render_fn) { int run_app(AppConfig config, std::function<void()> render_fn) {
// Logger primero para capturar fallos del propio init (GLFW, ventana, GL). // Logger primero para capturar fallos del propio init (GLFW, ventana, GL).
if (config.log.file_path != nullptr) { if (config.log.file_path != nullptr) {
@@ -672,6 +692,11 @@ int run_app(AppConfig config, std::function<void()> render_fn) {
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); 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); GLFWwindow* window = glfwCreateWindow(config.width, config.height, config.title, nullptr, nullptr);
if (!window) { if (!window) {
fprintf(stderr, "Failed to create GLFW window\n"); 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_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); 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( GLFWwindow* window = glfwCreateWindow(
config.width, config.height, config.width, config.height,
config.title ? config.title : "fn_test", nullptr, nullptr); config.title ? config.title : "fn_test", nullptr, nullptr);
+15
View File
@@ -101,6 +101,21 @@ struct AppConfig {
int height = 720; int height = 720;
bool vsync = true; bool vsync = true;
bool viewports = true; // Multi-viewport ON por defecto: ventanas ImGui arrastrables fuera del main window 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 ThemeMode theme = ThemeMode::FnDark; // Identidad visual unificada por defecto
float bg_r = 0.102f; // fn_tokens::colors::bg (dark.7 #1A1B1E) float bg_r = 0.102f; // fn_tokens::colors::bg (dark.7 #1A1B1E)
float bg_g = 0.106f; float bg_g = 0.106f;