chore: snapshot WIP previo + flow 0008 + 7 sub-issues (0112-0119)

Snapshot de WIP acumulado de sesiones previas antes de merge wave 1
del flow 0008 (kanban_cpp + agent_runner_api + DoD schema).

Incluye:
- dev/flows/0008-kanban-cpp-and-agent-workflows.md
- dev/issues/0112-0119*.md (7 sub-issues)
- WIP previo en cmd/fn/doctor.go, registry/*, modules/, cpp/, etc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 18:17:08 +02:00
parent ddb5366884
commit b9716a7cd6
119 changed files with 14929 additions and 3084 deletions
@@ -0,0 +1,260 @@
---
id: "0107"
title: "Estandarizar sistema de modulos C++: limpiar drift data_table + politica API + version pinning + /version command"
status: pendiente
type: refactor
domain:
- meta
- cpp-stack
- tooling
scope: multi-app
priority: alta
depends: []
blocks:
- "0108"
related:
- "0097"
- "0081"
- "0086"
created: 2026-05-17
updated: 2026-05-17
tags: [modules, cpp, data-table, framework, refactor, fn-doctor, versioning]
---
# 0107 — Estandarizar sistema de modulos C++
## Problema
Auditoria 2026-05-17 sobre `modules/` (framework + data_table) revela que el sistema de modulos C++ esta a medio camino: la idea (modulos opt-in versionados con manifest auditable) es solida, pero su implementacion tiene fugas que invalidan el contrato.
### Drift uses_modules ↔ uses_functions (7/7 apps consumidoras)
`module.md` dice "cuando declaras `uses_modules`, NO repetir los miembros en `uses_functions`". Realidad medida hoy:
| App | uses_functions total | miembros data_table duplicados |
|---|---|---|
| services_monitor | 12 | 12 |
| dag_engine_ui | 13 | 12 |
| odr_console | 5 | 5 |
| navegator_dashboard | 20 | 12 |
| graph_explorer | 42 | 12 |
| registry_dashboard | 37 | 11 |
| app_gestion | 12 | 12 |
7 de 7 apps violan la regla clave que justifica el sistema. `fn doctor cpp-apps` no detecta el drift.
### `data_table.cpp` = 4777 LOC
El "modulo" es un god-file con UI entera (chips, viz, grid, drill, joins, AI, button, color rules) dentro de un `.cpp`. Imposible auditar consumidores parciales, imposible registrar miembros como funciones reales del registry (cada uno con su `.md`).
### Boundary modulo vs funcion borrosa
`lua_engine`, `llm_anthropic`, `join_tables` son members de `data_table`. Pero lua/llm/join son utiles fuera de tablas. Forzar membership infla el surface del modulo y obliga a las apps a tragarse lua+llm+anthropic+join aunque solo quieran render simple. No hay tier "data_table_core" vs "data_table_full".
### Versionado declarado, no enforced
`module.md` tiene `version: 1.4.0`. `app.md` dice `uses_modules: [data_table_cpp]` sin version. Bump breaking de modulo → todas las apps rompen sin warning hasta compile error.
### Codegen silencioso
`execute_process(... codegen_app_modules)` emite WARNING solo si rc != 0 y != 2. Si Python falta → stub vacio sin error. About panel muestra "0 modules" en apps que SI deberian tener 1.
### Hard dep oculto
`fn_module_data_table` linkea `fn_framework` PRIVATE para `fn::local_path()`. `module.md` no lo documenta como precondicion publica. Si alguien intenta usar el modulo en una app no-framework, falla en link sin mensaje claro.
### Sin doc API de modulos
No hay un sitio canonico que diga "para usar el modulo X, incluye Y.h, llama X::render(...), pasa State Z". Cada modulo lo improvisa en su `module.md`.
### Sin /version command
No hay flujo estandar para bumpear semver de un modulo o framework. Cada PR lo hace a ojo, sin coherencia entre `module.md::version` y `## Capability growth log`.
## Decision
Issue desglosado en 6 sub-issues independientes detras de feature flag `modules-v2`:
1. **0107a**`fn doctor modules` que detecta drift uses_modules vs uses_functions.
2. **0107b** — Limpiar `uses_functions` de las 7 apps consumidoras de data_table (eliminar miembros duplicados).
3. **0107c** — Partir `modules/data_table/data_table.cpp` (4777 LOC) en sub-funciones del registry (`data_table_chips`, `data_table_grid`, `data_table_viz_panels`, `data_table_drill`, `data_table_ai_panel`, `data_table_color_rules`). Cada una con `.md` propio. Entrypoint queda thin.
4. **0107d** — Mover members generales (`lua_engine`, `llm_anthropic`, `join_tables`, `auto_detect_type`) fuera de `data_table` module. Quedan funciones sueltas que el modulo USA pero no posee. Crear tiers explicitos.
5. **0107e** — Version pinning en `uses_modules` (`uses_modules: [{name: data_table_cpp, min_version: "1.4"}]`) + codegen fail-loud (error si Python falta o count=0 cuando deberia ser >0).
6. **0107f**`modules/README.md` (catalogo) + `docs/MODULES_API.md` (contrato publico por modulo: header path, namespace, entry function, State struct, lifecycle).
Ademas:
- `/version` slash command para bumpear semver de modulo/framework consistentemente (`module.md::version` + `## Capability growth log` + git commit).
- `/fix-issue` referenciara `/version` cuando el cambio toque framework/modules.
Feature flag `modules-v2` activado solo cuando 0107a-f cierran. Antes de cerrar, recompilar TODAS las apps cpp para verificar que el refactor no rompe linkage. Aceptamos coste de recompilacion total.
## Restriccion explicita
Prohibido empezar `chat_ia` (proximo modulo planeado) hasta que 0107 cierre. Razon: si arrancamos otro modulo sin estandar estable, replicamos los mismos bugs en el doble de superficie.
## Tareas (resumen — detalle en sub-issues)
- [ ] **1** Auditoria automatizada → `fn doctor modules` (0107a)
- [ ] **2** Limpiar drift en 7 apps consumidoras (0107b)
- [ ] **3** Partir `data_table.cpp` en sub-funciones del registry (0107c)
- [ ] **4** Politica members generales + tiers (0107d)
- [ ] **5** Version pinning + codegen fail-loud (0107e)
- [ ] **6** Docs API publica modulos (0107f)
- [ ] **7** Recompilar todas las apps cpp + verificar smoke (al cerrar)
- [ ] **8** Activar feature flag `modules-v2`
- [ ] **9** `/version` + `/fix-issue` (no son sub-issues; tareas inline en este issue principal)
## Desglose multi-issue
| Sub-issue | Rama | Alcance | Estado |
|-----------|------|---------|--------|
| 0107a-fn-doctor-modules | issue/0107a-fn-doctor-modules | Check drift uses_modules vs uses_functions + version skew | pendiente |
| 0107b-clean-data-table-consumers | issue/0107b-clean-data-table-consumers | Eliminar miembros duplicados en 7 app.md | pendiente |
| 0107c-split-data-table | issue/0107c-split-data-table | Partir data_table.cpp 4777 LOC en sub-funciones del registry | pendiente |
| 0107d-module-tiers-policy | issue/0107d-module-tiers-policy | Sacar lua/llm/join del modulo data_table; tiers + politica | pendiente |
| 0107e-version-pinning-codegen | issue/0107e-version-pinning-codegen | min_version en uses_modules + codegen fail-loud | pendiente |
| 0107f-modules-api-docs | issue/0107f-modules-api-docs | modules/README.md + docs/MODULES_API.md | pendiente |
### Feature flag
Nombre: `modules-v2`
Se activa cuando 0107a-f cierran + recompilacion total verificada + `fn doctor modules` reporta 0 drift.
### Progreso por tarea
- [ ] **1.1** Implementar check drift en `fn doctor cpp-apps` o sub-comando nuevo — 0107a
- [ ] **1.2** Output JSON con apps que violan regla — 0107a
- [ ] **1.3** Tests sobre fixture sintetica (1 modulo, 3 apps simuladas) — 0107a
- [ ] **2.1** Editar 7 `app.md` removiendo miembros data_table — 0107b
- [ ] **2.2** Verificar build pasa post-clean (no rompe nada — solo metadata) — 0107b
- [ ] **3.1** Identificar fronteras funcionales en data_table.cpp 4777 LOC — 0107c
- [ ] **3.2** Crear `cpp/functions/viz/data_table_chips.cpp` + .h + .md — 0107c
- [ ] **3.3** Crear `cpp/functions/viz/data_table_grid.cpp` + .h + .md — 0107c
- [ ] **3.4** Crear `cpp/functions/viz/data_table_viz_panels.cpp` + .h + .md — 0107c
- [ ] **3.5** Crear `cpp/functions/viz/data_table_drill.cpp` + .h + .md — 0107c
- [ ] **3.6** Crear `cpp/functions/viz/data_table_ai_panel.cpp` + .h + .md — 0107c
- [ ] **3.7** Crear `cpp/functions/viz/data_table_color_rules.cpp` + .h + .md — 0107c
- [ ] **3.8** `data_table.cpp` queda como entrypoint thin que compone las sub-funciones — 0107c
- [ ] **3.9** Bump `module.md::version` a 2.0.0 (breaking interno, API publica `data_table::render` intacta) — 0107c
- [ ] **4.1** Crear `cpp/functions/core/lua_engine.cpp` (ya existe) como funcion suelta; quitar de `module.md::members` — 0107d
- [ ] **4.2** Idem `llm_anthropic`, `join_tables`, `auto_detect_type` — 0107d
- [ ] **4.3** Actualizar `modules/data_table/CMakeLists.txt`: estos `.cpp` ya no se enlazan dentro del modulo; apps que los necesiten los anaden a su CMakeLists — 0107d
- [ ] **4.4** Definir tiers en `module.md`: `core_members` (esenciales) vs `optional_members` (deps externas pesadas) — 0107d
- [ ] **5.1** Parser `app.md::uses_modules` acepta string corto y dict largo — 0107e
- [ ] **5.2** Codegen comprueba `min_version` vs `module.md::version` — error si no cumple — 0107e
- [ ] **5.3** Codegen: si `Python3 NOT FOUND` y app tiene `uses_modules` → CMake FATAL_ERROR — 0107e
- [ ] **5.4** Codegen: si parser devuelve count=0 pero app.md declara `uses_modules` no-vacio → FATAL_ERROR — 0107e
- [ ] **6.1** `modules/README.md` con tabla modulos + version + descripcion + link a contrato — 0107f
- [ ] **6.2** `docs/MODULES_API.md` con contrato canonico (template + ejemplos data_table + framework) — 0107f
- [ ] **6.3** Actualizar `.claude/rules/cpp_apps.md` referenciando `docs/MODULES_API.md` — 0107f
- [ ] **7.1** `redeploy_all_cpp_apps_bash_pipelines` + verificar 0 errores de link — issue principal, al cerrar
- [ ] **7.2** Smoke manual de cada app con `data_table::render` — issue principal, al cerrar
- [ ] **8** Flip `modules-v2: enabled: true` en `dev/feature_flags.json` — issue principal
- [ ] **9.1** Crear `.claude/commands/version.md` (slash command bump semver) — issue principal, ya en este turno
- [ ] **9.2** Crear `.claude/commands/fix-issue.md` que referencie `/version` — issue principal, ya en este turno
## Arquitectura
### Archivos afectados
**0107a** (`fn doctor modules`):
- `functions/infra/audit_modules_drift.go` (NEW) — funcion del registry
- `functions/infra/audit_modules_drift.md` (NEW)
- `cmd/fn/doctor.go` — subcomando `modules`
- `apps/registry_mcp/...` — exponer via `mcp__registry__fn_doctor subcommand="modules"`
**0107b**:
- 7 `app.md` editados: services_monitor, dag_engine_ui, odr_console, navegator_dashboard, graph_explorer, registry_dashboard, app_gestion.
**0107c**:
- `modules/data_table/data_table.cpp` — pasa de 4777 LOC a ~400 (entrypoint que compone).
- `cpp/functions/viz/data_table_chips.cpp/.h/.md` (NEW) — ~600 LOC
- `cpp/functions/viz/data_table_grid.cpp/.h/.md` (NEW) — ~1200 LOC
- `cpp/functions/viz/data_table_viz_panels.cpp/.h/.md` (NEW) — ~800 LOC
- `cpp/functions/viz/data_table_drill.cpp/.h/.md` (NEW) — ~300 LOC
- `cpp/functions/viz/data_table_ai_panel.cpp/.h/.md` (NEW) — ~500 LOC
- `cpp/functions/viz/data_table_color_rules.cpp/.h/.md` (NEW) — ~400 LOC
- `modules/data_table/module.md` — bump version + actualizar members.
- `modules/data_table/CMakeLists.txt` — anadir las sub-funciones a la static lib.
**0107d**:
- `modules/data_table/module.md` — quitar `lua_engine`, `llm_anthropic`, `join_tables`, `auto_detect_type` de members.
- `modules/data_table/CMakeLists.txt` — estos `.cpp` salen.
- Apps consumidoras que necesiten lua/llm/join → declarar en `uses_functions` + anadir el `.cpp` a su CMake.
**0107e**:
- `python/functions/infra/codegen_app_modules.py` — soporte dict largo `{name, min_version}`.
- `cpp/CMakeLists.txt::add_imgui_app` — fail-loud en codegen errors.
- `registry/parser.go` — indexer entiende dict largo.
**0107f**:
- `modules/README.md` (NEW)
- `docs/MODULES_API.md` (NEW)
- `.claude/rules/cpp_apps.md` — link nuevo doc.
**Slash commands** (este issue):
- `.claude/commands/version.md` (NEW)
- `.claude/commands/fix-issue.md` (NEW si no existe)
### pkg/ puro vs shell/ impuro
`audit_modules_drift_go_infra` (0107a) es **impuro** — lee `registry.db` + filesystem (`app.md`, `module.md`). Vive en `functions/infra/`. Core logico (comparar listas de IDs, detectar miembros duplicados) es **puro** y vive como sub-funcion interna del paquete `infra`.
## Ejemplo de uso
```bash
# 1. Detectar drift
fn doctor modules
# Output:
# Module drift report
# ===================
# data_table_cpp (v1.4.0): 7/7 consumers list members in uses_functions
# services_monitor — duplicates: data_table_cpp_viz, viz_render_cpp_viz, ...
# dag_engine_ui — duplicates: ...
# Total apps with drift: 7
# Total modules: 2
# Exit: 1
# 2. Bump version de un modulo (slash command)
/version modules/data_table minor "split data_table.cpp into 6 sub-functions; API publica intacta"
# - Detecta version actual en module.md (1.4.0)
# - Calcula proxima (1.5.0 si minor, 2.0.0 si major)
# - Anade entrada a ## Capability growth log con fecha de hoy
# - Stage en git pero NO commit
# 3. Pinning version en una app
# app.md:
# uses_modules:
# - name: data_table_cpp
# min_version: "1.4"
# Codegen falla en cmake si module.md::version < 1.4
# 4. fix-issue referencia /version
/fix-issue 0107c
# Flow normal del fix-issue + "¿este cambio bumpea version de modulo/framework? si si → /version"
```
## Decisiones de diseno
1. **No tocar API publica `data_table::render(...)`**. Refactor interno. Apps existentes no cambian su llamada.
2. **Aceptamos recompilacion total**. El usuario lo dijo explicito. Coste de tiempo razonable a cambio de limpieza.
3. **Feature flag `modules-v2` no protege codigo runtime** — protege la "promesa" del sistema. Cuando flag flip, `fn doctor modules` debe pasar verde.
4. **`/version` NO hace commit**. Solo edita archivos + stage. El commit final lo hace el flujo normal del fix-issue.
5. **Bloqueamos `chat_ia`**. Si saltamos a otro modulo sin estandar, el caos se duplica.
## Prerequisitos
- Issue 0097 (modules infra inicial) — completado, esto es la evolucion.
- `fn doctor cpp-apps` existe (0081) — reutilizamos paths.
## Riesgos
- **Refactor 4777 LOC**: alto riesgo de regresion visual/funcional. Mitigacion: smoke manual app-por-app + `primitives_gallery --capture` golden images antes/despues.
- **Apps que dependen indirectamente de lua/llm/join** sin declararlo: post-0107d podrian fallar en link. Mitigacion: `fn doctor uses-functions` + recompilacion total como gate.
- **Codegen fail-loud** rompe builds que hoy pasan con stub: deliberado, parte de la idea — pero hay que arreglar todos los app.md antes de mergear 0107e.
## Notas
- `/version` se referenciara desde `/fix-issue` con prompt: "este cambio toca `modules/` o `cpp/framework/`? si si → run `/version <path> [major|minor|patch] [reason]` antes de commit".
- Politica: bump de modulo SIN bump de version = bug. `fn doctor modules` lo detectara via diff hash de `members` + `description` vs ultima version registrada.
@@ -0,0 +1,82 @@
---
id: "0107a"
title: "fn doctor modules — detectar drift uses_modules vs uses_functions y version skew"
status: pendiente
type: feature
domain:
- meta
- cpp-stack
- tooling
scope: registry
priority: alta
depends: []
blocks:
- "0107b"
related:
- "0107"
created: 2026-05-17
updated: 2026-05-17
tags: [modules, fn-doctor, drift, audit, cpp]
---
# 0107a — `fn doctor modules`
Parte del issue principal [0107](0107-modules-standardization.md). Feature flag `modules-v2`.
## Objetivo
Subcomando `fn doctor modules` + funcion del registry `audit_modules_drift_go_infra` que detecta:
1. App declara `uses_modules: [X]` Y un miembro de X aparece en `uses_functions` → drift.
2. App declara `uses_modules: [X]` pero su `CMakeLists.txt` NO linkea `fn_module_X` → mismatch.
3. App linkea `fn_module_X` pero NO declara `uses_modules: [X]` → mismatch inverso.
4. App declara `uses_modules: [{name: X, min_version: "1.4"}]` y `module.md::version` < 1.4 → version skew (post 0107e).
## Tareas
- [ ] **1.1** `functions/infra/audit_modules_drift.go` con firma:
```go
type ModuleDriftReport struct {
ModuleID string
ModuleVersion string
ConsumersTotal int
ConsumersWithDrift int
Violations []DriftViolation
}
type DriftViolation struct {
AppID string
Kind string // "duplicate_members" | "uses_modules_no_link" | "link_no_uses_modules" | "version_skew"
DuplicatedIDs []string
Message string
}
func AuditModulesDrift(registryDB string, cppRoot string) ([]ModuleDriftReport, error)
```
- [ ] **1.2** `.md` correspondiente con frontmatter completo + ejemplo lanzable.
- [ ] **1.3** Subcomando en `cmd/fn/doctor.go`: `fn doctor modules` + `fn doctor modules --json`.
- [ ] **1.4** Exponer via MCP: `mcp__registry__fn_doctor subcommand="modules"`.
- [ ] **1.5** Test sintetico: fixture con 1 modulo + 3 apps (1 limpia, 1 con drift de duplicados, 1 con version skew).
- [ ] **1.6** Anadir entrada a `.claude/rules/fn_doctor.md` mapeando subcomando.
## Output esperado (texto)
```
fn doctor modules
=================
data_table_cpp v1.4.0 — 7 consumers
services_monitor DRIFT 12 duplicated members in uses_functions
dag_engine_ui DRIFT 12 duplicated members in uses_functions
odr_console DRIFT 5 duplicated members in uses_functions
navegator_dashboard DRIFT 12 duplicated members in uses_functions
graph_explorer DRIFT 12 duplicated members in uses_functions
registry_dashboard DRIFT 11 duplicated members in uses_functions
app_gestion DRIFT 12 duplicated members in uses_functions
framework_cpp v1.1.0 — 0 explicit consumers (transitive via add_imgui_app)
Summary: 2 modules, 7 apps with drift, 0 version skews.
Exit: 1 (drift detected)
```
## Riesgos
- Falso positivo si un app legitimamente necesita un miembro fuera del scope del modulo (ej. usar `lua_engine` standalone). Mitigacion: post-0107d (members generales fuera del modulo), este caso desaparece. Mientras tanto: flag `--ignore-known` con allowlist temporal.
@@ -0,0 +1,73 @@
---
id: "0107b"
title: "Limpiar uses_functions de 7 apps consumidoras de data_table (eliminar miembros duplicados)"
status: pendiente
type: refactor
domain:
- meta
- cpp-stack
scope: multi-app
priority: alta
depends:
- "0107a"
blocks: []
related:
- "0107"
created: 2026-05-17
updated: 2026-05-17
tags: [modules, drift, app-md, cleanup]
---
# 0107b — Limpiar drift en 7 apps consumidoras
Parte del issue principal [0107](0107-modules-standardization.md).
## Objetivo
Eliminar de `uses_functions` en 7 `app.md` los IDs que ya son miembros de `data_table` module (declarado en `uses_modules`).
## Apps afectadas
| App | Path | Drift count |
|---|---|---|
| services_monitor | apps/services_monitor/app.md | 12 |
| dag_engine_ui | apps/dag_engine_ui/app.md | 12 |
| odr_console | projects/online_data_recopilation/apps/odr_console/app.md | 5 |
| navegator_dashboard | projects/navegator/apps/navegator_dashboard/app.md | 12 |
| graph_explorer | projects/osint_graph/apps/graph_explorer/app.md | 12 |
| registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/app.md | 11 |
| app_gestion | apps/app_gestion/app.md | 12 |
## Miembros a quitar (segun module.md de data_table v1.4)
- `data_table_cpp_viz`
- `compute_stage_cpp_core`
- `compute_pipeline_cpp_core`
- `compute_column_stats_cpp_core`
- `tql_emit_cpp_core`
- `tql_helpers_cpp_core`
- `tql_apply_cpp_core`
- `tql_to_sql_cpp_core`
- `lua_engine_cpp_core` (hasta 0107d que lo saca del modulo)
- `join_tables_cpp_core` (idem)
- `auto_detect_type_cpp_core` (idem)
- `llm_anthropic_cpp_core` (idem)
- `viz_render_cpp_viz`
NOTA: 0107d sacara lua/join/auto_detect/llm del modulo. Cuando eso pase, esas apps DEBEN volver a anadirlos a `uses_functions` (si los usan directamente). 0107b limpia el estado actual contra `module.md` v1.4; despues de 0107d se ejecuta `fn doctor modules` otra vez y se ajusta.
## Tareas
- [ ] **2.1** Para cada app.md, eliminar las lineas listadas en "Miembros a quitar" del bloque `uses_functions`.
- [ ] **2.2** `fn index` despues.
- [ ] **2.3** Verificar con `fn doctor modules` que `services_monitor` etc. reportan 0 drift.
- [ ] **2.4** Build completo de las 7 apps. Linkage NO debe cambiar (los .cpp seguian viniendo via `fn_module_data_table` enlazado en su CMake).
- [ ] **2.5** Smoke manual rapido (lanzar y cerrar) de cada app.
## Riesgos
- Si `fn doctor uses-functions` se ejecuta antes de que `uses_modules` se entienda como cobertura, marcara las apps como "missing imports". Mitigacion: arreglar primero `audit_uses_functions_go_infra` para que considere `uses_modules` como cobertura transitiva. Tarea inline 2.0 antes de 2.1.
## Notas
- Es solo metadata. No toca codigo, no rompe build. Coste = editar 7 archivos + fn index.
@@ -0,0 +1,74 @@
---
id: "0107c"
title: "Partir modules/data_table/data_table.cpp (4777 LOC) en sub-funciones del registry"
status: pendiente
type: refactor
domain:
- cpp-stack
- meta
scope: module
priority: alta
depends: []
blocks:
- "0107d"
related:
- "0107"
- "0081"
created: 2026-05-17
updated: 2026-05-17
tags: [modules, data-table, refactor, registry, viz]
---
# 0107c — Partir `data_table.cpp` 4777 LOC
Parte del issue principal [0107](0107-modules-standardization.md).
## Problema
`modules/data_table/data_table.cpp` es un god-file de 4777 LOC con UI entera dentro: barra de chips, tabla grid, paneles de viz, drill-down, joins, panel Ask AI, button event sink, color rules, breadcrumb, tooltips. Imposible auditar consumidores parciales. Cada bloque deberia ser una funcion del registry con su `.md` propio (Ejemplo + Cuando usarla + Gotchas).
## Decision
Partir en **6 sub-funciones** dentro de `cpp/functions/viz/` (no en `modules/data_table/` — son funciones del registry reutilizables). El modulo bundla las 6 + el entrypoint thin.
| Sub-funcion | LOC objetivo | Responsabilidad |
|---|---|---|
| `data_table_chips_cpp_viz` | ~600 | Barra de chips superior: filtros activos, TQL preview, save/load query |
| `data_table_grid_cpp_viz` | ~1200 | Render del grid: cells, sorting, freeze cols, declarative renderers (Badge/Progress/Duration/Icon/Button/Dots/CategoricalChip/ColorScale) |
| `data_table_viz_panels_cpp_viz` | ~800 | Paneles de viz lateral: histograms, line, scatter, value-counts |
| `data_table_drill_cpp_viz` | ~300 | Drill-down stack + breadcrumb |
| `data_table_ai_panel_cpp_viz` | ~500 | Panel Ask AI: prompt, llamada a `llm_anthropic`, render respuesta, export |
| `data_table_color_rules_cpp_viz` | ~400 | Editor de reglas de color por columna + aplicacion |
Entrypoint `data_table.cpp` queda ~400 LOC: compone las 6 sub-funciones, gestiona `State`, dispatcher de eventos.
## Tareas
- [ ] **3.1** Leer `data_table.cpp` end-to-end e identificar fronteras funcionales (comentario header de cada bloque sirve).
- [ ] **3.2** `cpp/functions/viz/data_table_chips.cpp` + `.h` + `.md` (NEW).
- [ ] **3.3** `cpp/functions/viz/data_table_grid.cpp` + `.h` + `.md` (NEW).
- [ ] **3.4** `cpp/functions/viz/data_table_viz_panels.cpp` + `.h` + `.md` (NEW).
- [ ] **3.5** `cpp/functions/viz/data_table_drill.cpp` + `.h` + `.md` (NEW).
- [ ] **3.6** `cpp/functions/viz/data_table_ai_panel.cpp` + `.h` + `.md` (NEW).
- [ ] **3.7** `cpp/functions/viz/data_table_color_rules.cpp` + `.h` + `.md` (NEW).
- [ ] **3.8** Reducir `data_table.cpp` a entrypoint thin que llama las 6.
- [ ] **3.9** `modules/data_table/CMakeLists.txt`: anadir las 6 sub-funciones a la static lib.
- [ ] **3.10** `modules/data_table/module.md`: bump `version: 2.0.0` (breaking interno, API publica intacta) + extender `members:` con las 6 nuevas + entrada en `## Capability growth log`.
- [ ] **3.11** Recompilar TODAS las apps que linkean `fn_module_data_table` (7 apps).
- [ ] **3.12** Smoke manual de cada app: tabla renderiza, chips funcionan, viz panels OK, AI panel OK, drill OK, color rules OK.
- [ ] **3.13** `primitives_gallery --capture` golden image antes vs despues — diff visual cero.
## Riesgos
- **State struct**: hoy `data_table::State` es opaco interno del .cpp. Tras split, las 6 sub-funciones necesitan acceder a partes de el. Opciones:
- (a) Mover `State` a `data_table_types.h` publico (mas exposicion pero claro).
- (b) Definir `State` en `data_table.h` con sub-structs por sub-funcion (`State::chips_state`, `State::grid_state`, etc.) y cada sub-funcion recibe su sub-state.
- **Recomendacion**: (b). Mantiene encapsulacion y cada sub-funcion tiene firma clara.
- **API publica `data_table::render(...)` no cambia**. Es la regla dura. Si la firma debe cambiar, ya no es 0107c sino issue nuevo con migration plan.
- **Tiempo de refactor**: 4777 LOC → 6 archivos requiere cuidado quirurgico. Lanzamos `fn-constructor` en paralelo.
## Notas
- Las 6 sub-funciones son `purity: impure` (manipulan ImGui state global).
- Cada `.md` con `tags: [viz, table, imgui, ui]` + `framework: imgui`.
- El refactor lo hara un sub-agente fn-constructor lanzado en paralelo desde el flujo principal del issue 0107.
@@ -0,0 +1,91 @@
---
id: "0107d"
title: "Sacar lua_engine/llm_anthropic/join_tables/auto_detect_type del modulo data_table — politica de tiers"
status: pendiente
type: refactor
domain:
- cpp-stack
- meta
scope: module
priority: alta
depends:
- "0107c"
blocks: []
related:
- "0107"
created: 2026-05-17
updated: 2026-05-17
tags: [modules, data-table, policy, tiers, lua, llm]
---
# 0107d — Politica members generales + tiers
Parte del issue principal [0107](0107-modules-standardization.md).
## Problema
`data_table` module hoy lleva como miembros `lua_engine`, `llm_anthropic`, `join_tables`, `auto_detect_type`. Esos 4 son **utiles fuera de tablas**:
- `lua_engine`: scripting general.
- `llm_anthropic`: LLM wrapper, util en chat_ia (proximo modulo) y otros.
- `join_tables`: util en cualquier app que combine tablas (no solo data_table::render).
- `auto_detect_type`: util en data import generico.
Forzar membership infla el modulo y obliga a las apps a tragarse 4 deps pesadas (lua + curl + libllm + http) aunque solo quieran render basico.
## Decision
Politica de tiers:
```yaml
# module.md (post-0107d)
members:
# core_members: esenciales, sin ellos no hay funcionalidad
- data_table_chips_cpp_viz
- data_table_grid_cpp_viz
- data_table_drill_cpp_viz
- data_table_color_rules_cpp_viz
- data_table_viz_panels_cpp_viz
- compute_stage_cpp_core
- compute_pipeline_cpp_core
- compute_column_stats_cpp_core
- tql_emit_cpp_core
- tql_helpers_cpp_core
- tql_apply_cpp_core
- tql_to_sql_cpp_core
- viz_render_cpp_viz
uses_functions:
# Deps externas usadas por el modulo (no son miembros del modulo)
- lua_engine_cpp_core # TQL scripting (opt-in via feature flag interno)
- llm_anthropic_cpp_core # Ask AI panel (opt-in via FN_LLM_ANTHROPIC)
- join_tables_cpp_core # Joins
- auto_detect_type_cpp_core # Detect tipos al cargar nueva tabla
```
Distincion:
- **`members`**: funciones que el modulo POSEE — viven en `cpp/functions/viz/` y nadie mas las usa directamente (renderizan dentro del modulo).
- **`uses_functions`**: funciones que el modulo CONSUME — viven en `cpp/functions/core/`, son utiles fuera del modulo, otras apps pueden importarlas directamente.
Apps consumidoras de `data_table`:
- Si solo llaman `data_table::render(...)` → solo `uses_modules: [data_table_cpp]`, nada mas.
- Si ademas usan `lua_engine` directamente para sus propios scripts → anaden `lua_engine_cpp_core` a `uses_functions` (no es duplicado, es uso directo independiente).
## Tareas
- [ ] **4.1** Editar `modules/data_table/module.md`: separar `members` core de `uses_functions`. Bump version 2.1.0.
- [ ] **4.2** Editar `modules/data_table/CMakeLists.txt`: `lua_engine.cpp`, `llm_anthropic.cpp`, `join_tables.cpp`, `auto_detect_type.cpp` quedan dentro de la static lib (el modulo los usa internamente), pero el linkage transitivo se controla via PUBLIC vs PRIVATE. Si una app NO usa directamente lua/llm fuera del modulo, igual los recibe pero solo como impl detail del modulo. Decidir: SI mantenemos lua/llm/join dentro de la static lib del modulo (PUBLIC link) o sacamos al app para que cada una decida (PRIVATE en modulo, app linkea por su lado).
- [ ] **4.3** Documentar tier en `docs/MODULES_API.md` (0107f).
- [ ] **4.4** `fn doctor modules` (0107a) entiende la distincion members vs uses_functions y NO marca drift cuando una app lista en `uses_functions` algo que el modulo declara en su `uses_functions` (no es miembro).
- [ ] **4.5** Actualizar 7 app.md: si un app necesita lua/llm/join standalone, declararlo. Si no, no.
## Decisiones de diseno
**Decision 4.2 detallada:** mantener lua/llm/join dentro de `fn_module_data_table` static lib (PUBLIC), porque:
- 100% de las apps que linkean `fn_module_data_table` hoy lo usan para tablas, que usan internamente lua/llm/join.
- Cero apps quieren un "data_table ligero sin lua". Si llegara ese caso → split modulo en `data_table_core` + `data_table_full`. Mientras tanto, KISS.
- La distincion `members` vs `uses_functions` queda solo en `module.md` (metadata) — el CMakeLists agrupa todo bajo la static lib.
## Riesgos
- Ambiguedad "¿el modulo posee X o lo usa?": resolver via 1 regla simple — si `X` aparece como funcion suelta consumida por otras apps fuera del modulo, va a `uses_functions`. Si nadie mas la usa, es `member`.
@@ -0,0 +1,131 @@
---
id: "0107f"
title: "modules/README.md (catalogo) + docs/MODULES_API.md (contrato publico por modulo)"
status: pendiente
type: docs
domain:
- meta
- cpp-stack
- docs
scope: registry
priority: alta
depends: []
blocks: []
related:
- "0107"
created: 2026-05-17
updated: 2026-05-17
tags: [modules, docs, api, contract]
---
# 0107f — Docs API publica de modulos
Parte del issue principal [0107](0107-modules-standardization.md).
## Objetivo
Resolver "no hay un sitio canonico que diga como usar un modulo". Dos archivos:
1. `modules/README.md` — catalogo (tabla con nombre, version, descripcion 1-linea, link a contrato).
2. `docs/MODULES_API.md` — contrato canonico publico de cada modulo (template + ejemplos).
## Estructura `modules/README.md`
```markdown
# Modulos C++ del registry
Bundles versionados de funciones del registry expuestos como static libs.
Apps opt-in via `app.md::uses_modules` + `target_link_libraries(... fn_module_<X>)`.
| Modulo | Version | Static lib | Header | Entry | Descripcion | Contrato |
|---|---|---|---|---|---|---|
| framework_cpp | 1.1.0 | fn_framework | cpp/framework/app_base.h | fn::run_app(cfg, render) | Shell ImGui (GLFW+OpenGL+ImGui+ImPlot+themas+layouts+logger) | [framework_cpp](../docs/MODULES_API.md#framework_cpp) |
| data_table_cpp | 2.0.0 | fn_module_data_table | modules/data_table/data_table.h | data_table::render(...) | Tabla completa TQL+viz+drill+AI+button events | [data_table_cpp](../docs/MODULES_API.md#data_table_cpp) |
## Como anadir un modulo
Ver [docs/MODULES_API.md#cycle](../docs/MODULES_API.md#ciclo-de-vida-de-un-modulo) y `.claude/rules/cpp_apps.md`.
```
## Estructura `docs/MODULES_API.md`
```markdown
# Modules API
Contrato publico canonico de cada modulo C++ del registry. Una app DEBE
poder integrar un modulo leyendo solo esta pagina (sin abrir el .cpp).
## Template por modulo
### <module_name>
**Static lib target:** `<cmake_target>`
**Header path:** `<include>`
**Namespace:** `<namespace>`
**Entry function:** `<signature>`
**State struct:** `<type>` (lifecycle: <lifecycle notes>)
**Public types:** lista de tipos publicos consumidos (TableInput, TableEvent, etc.)
**Dependencias transitivas:** lista de libs externas (lua54, imgui, etc.)
**Min ImGui version:** <X.Y>
**Thread safety:** <main thread only | safe to call from any thread>
#### Opt-in en una app
1. `app.md`: anadir `uses_modules: [{name: <id>, min_version: "X.Y"}]`.
2. `CMakeLists.txt`: `target_link_libraries(<app> PRIVATE <cmake_target>)`.
3. Header: `#include "<header_path>"`.
4. Reservar `<state>` persistente entre frames.
5. Llamar `<entry>` cada frame.
#### Ejemplo minimo
```cpp
[bloque de codigo lanzable]
```
#### Eventos / callbacks
[Tabla de eventos publicos si aplica]
#### Gotchas
[Lista de gotchas conocidos]
#### Version policy
Semver. Major = breaking ABI/API de la entry function o State publico.
Minor = additive (nuevo evento, nuevo renderer opcional). Patch = bugfix.
---
## framework_cpp
[Aplicar template]
## data_table_cpp
[Aplicar template]
## Ciclo de vida de un modulo
1. Crear `modules/<name>/` con `module.md`, `CMakeLists.txt`, `<name>.cpp`, `<name>.h`.
2. `module.md::members` lista funciones del registry que el modulo bundla.
3. `module.md::version` SemVer estricto.
4. `CMakeLists.txt` define static lib `fn_module_<name>` con sus deps.
5. Anadir entrada en `modules/README.md`.
6. Anadir seccion en `docs/MODULES_API.md` siguiendo el template.
7. `fn index` registra el modulo.
8. Cada bump de version: `/version modules/<name> <major|minor|patch> "<reason>"` (ver `.claude/commands/version.md`).
```
## Tareas
- [ ] **6.1** Crear `modules/README.md` con tabla canonica.
- [ ] **6.2** Crear `docs/MODULES_API.md` con template + secciones para los 2 modulos actuales.
- [ ] **6.3** Anadir referencia desde `.claude/rules/cpp_apps.md` a `docs/MODULES_API.md`.
- [ ] **6.4** Anadir referencia desde `cpp/PATTERNS.md` a `docs/MODULES_API.md`.
- [ ] **6.5** Anadir referencia desde `.claude/CLAUDE.md` en la seccion "Estructura" listando `modules/` y enlazando docs.
## Riesgos
- Doc drift: facil que `module.md` y `MODULES_API.md` se desincronicen. Mitigacion: `fn doctor modules` (0107a) verifica que cada modulo registrado tiene seccion en `MODULES_API.md`. Si no, warning.
@@ -0,0 +1,209 @@
---
id: "0107g"
title: "Migrar inline ImGui::BeginTable a data_table::render en apps con tablas de datos reales"
status: en-progreso
type: refactor
domain:
- cpp-stack
- meta
scope: multi-app
priority: media
depends:
- "0107c"
blocks: []
related:
- "0107"
created: 2026-05-17
updated: 2026-05-17
tags: [modules, data-table, drift, audit, inline-begintable]
---
# 0107g — Migrar inline BeginTable a `data_table::render` (data tables reales)
Parte del issue principal [0107](0107-modules-standardization.md). Detectado por `audit_data_table_usage_go_infra` (output en `dev/data_table_integration_audit.md`).
## Problema
Audit automatico identifica ~12 hits de `ImGui::BeginTable` inline en apps que YA declaran `uses_modules: [data_table_cpp]`. Mezcla legitimos + bugs:
- **Legitimos** (NO migrar): KPI grids, schema forms k/v, layout 2-col splitters, chart grids. NO son tablas de datos.
- **Bugs reales**: tablas de datos con filas dinamicas + sort/filter potencial que reinventan logica que el modulo provee.
Resultado: codigo duplicado, comportamiento inconsistente, color/badge/sort/filter "casi-pero-no" igual entre apps. Conexiones raras: cada app personaliza su tabla a mano.
## Decision
Migrar los hits identificados como bugs reales a `data_table::render`. Dejar los legitimos como excepciones documentadas en `docs/MODULES_API.md::Cuando usar data_table::render vs BeginTable directo`.
## Tabla de migracion
| App | Path | Linea | Es bug? | Accion |
|---|---|---|---|---|
| dag_engine_ui | apps/dag_engine_ui/tabs.cpp | 382 | **BUG** (`##dt_run_steps`, 6 cols, scroll Y, runs dinamicas) | Migrar |
| dag_engine_ui | apps/dag_engine_ui/tabs.cpp | 731 | LEGITIMO (`##health_kpis`, 4 cols stretch same, KPI grid) | Dejar + comentar |
| navegator_dashboard | projects/navegator/apps/navegator_dashboard/autoextract_panel.cpp | 528 | **BUG** (`##ax_schema`, 5 cols, filas dinamicas schema) | Migrar |
| navegator_dashboard | projects/navegator/apps/navegator_dashboard/recipes_panel.cpp | 238 | **BUG** (`##recipes_tbl`, 6 cols, filas dinamicas recipes) | Migrar |
| graph_explorer | projects/osint_graph/apps/graph_explorer/extract_panel.cpp | 981 | **BUG** (`##ents`, 5 cols, filas dinamicas entities) | Migrar |
| graph_explorer | projects/osint_graph/apps/graph_explorer/extract_panel.cpp | 1027 | **BUG** (`##rels`, 5 cols, filas dinamicas relations) | Migrar |
| graph_explorer | projects/osint_graph/apps/graph_explorer/main.cpp | 1127 | LEGITIMO (`##enr_params`, form k/v) | Dejar + comentar |
| graph_explorer | projects/osint_graph/apps/graph_explorer/views.cpp | 885 | LEGITIMO (`##insp_id`, inspector form k/v) | Dejar + comentar |
| graph_explorer | projects/osint_graph/apps/graph_explorer/views.cpp | 958 | LEGITIMO (`##insp_fields`, inspector form) | Dejar + comentar |
| graph_explorer | projects/osint_graph/apps/graph_explorer/views.cpp | 1546 | INFO comment, ya migrado a data_table::render | Ignorar (es comentario) |
| graph_explorer | projects/osint_graph/apps/graph_explorer/views.cpp | 1854 | **BUG** (`##te_rows`, col_count dinamico, data table type explorer) | Migrar (segunda fase de la migration ya empezada) |
| graph_explorer | projects/osint_graph/apps/graph_explorer/views.cpp | 2292 | DISCUTIBLE (`##te_fields`, 5 cols, fields de un tipo — semi-dinamico) | Evaluar; si rows >20 migrar, sino dejar |
| registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/views.cpp | 380 | LEGITIMO (`##kpi_grid`, KPI cards) | Dejar + comentar |
| registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/views.cpp | 436 | LEGITIMO (`##chart_grid`, plots grid) | Dejar + comentar |
| registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/views.cpp | 648 | LEGITIMO (`##monitor_kpi`, KPI cards) | Dejar + comentar |
| registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/views.cpp | 1110 | LEGITIMO (`##proj_layout`, 2-col splitter) | Dejar + comentar |
| registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/views.cpp | 1448 | LEGITIMO (`##explorer_layout`, 2-col splitter) | Dejar + comentar |
| registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/work_tab.cpp | 239 | **BUG** (`##flows_work`, 8 cols, filas dinamicas flows) | Migrar |
| registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/work_tab.cpp | 272 | **BUG** (`##top_issues_work`, 7 cols, filas dinamicas issues) | Migrar |
| app_gestion | apps/app_gestion/main.cpp | 722 | DISCUTIBLE (`##linked_tbl`, 4 cols, lista de modulos linked — semi-dinamico, rows <20) | Evaluar; bias a dejar como esta |
**Total a migrar (BUGs): 8 tablas en 4 apps.**
**Total LEGITIMOS (dejar + comentar): 9.**
**Total DISCUTIBLES: 2 — decision contextual.**
**Total comentarios/already-migrated: 1.**
## Tareas
- [x] **1.1** Migrar `dag_engine_ui/tabs.cpp:382` (`##dt_run_steps`) → `data_table::render`. HECHO. Function col → Button action="open_fn"; Status → CategoricalChip; Duration → Duration renderer.
- [~] **1.2** Migrar `navegator_dashboard/autoextract_panel.cpp:528` (`##ax_schema`). ABORTADO: form editor con InputText/Checkbox editables inline en cada fila (field, selector, type, keep). data_table::render no soporta CellEdit como InputText inline. Comentado con LAYOUT-TABLE.
- [x] **1.3** Migrar `navegator_dashboard/recipes_panel.cpp:238` (`##recipes_tbl`). HECHO. Patron B: 4 columnas Button (run/edit/delete/open_df) + ev.row para indexar yaml_path. last_status → CategoricalChip.
- [~] **1.4** Migrar `graph_explorer/extract_panel.cpp:981` (`##ents`). ABORTADO: form editor con InputText editables por fila (type_buf, name_buf) + Checkbox "sel". Mutacion directa de structs entities[i]. No mapeable a data_table. Comentado con LAYOUT-TABLE.
- [~] **1.5** Migrar `graph_explorer/extract_panel.cpp:1027` (`##rels`). ABORTADO: form editor con Checkbox "sel" + inmutabilidad necesaria (relations[i].selected se muta inline). Comentado con LAYOUT-TABLE.
- [~] **1.6** Migrar `graph_explorer/views.cpp:1854` (`##te_rows`). ABORTADO: interactividad app-específica no mapeable — Selectable + single-click ramificado por estado de promocion (promoted/unpromoted), dblclick promote-flow, PopupContextItem con promote/demote/focus condicionales, SmallButton Promote-out-of-group, paginacion manual. Equivalente exact en data_table events no existe. Comentado explicando razon.
- [x] **1.7** Migrar `registry_dashboard/work_tab.cpp:239` (`##flows_work`). HECHO. 8 cols. Status + Risk → CategoricalChip. BeginChild host 220px.
- [x] **1.8** Migrar `registry_dashboard/work_tab.cpp:272` (`##top_issues_work`). HECHO. 7 cols. Status + Deps + Prio → CategoricalChip. Deps string "-"/"OK"/"blocked" preserva logica de color original. BeginChild host -1.
- [x] **2.1** Anadir comentario `// LAYOUT-TABLE — KPI/form/splitter, no data; keep BeginTable inline.` encima de los 9 hits LEGITIMOS para que `audit_data_table_usage` los excluya en proximas pasadas. HECHO en: ##health_kpis, ##enr_params, ##insp_id, ##insp_fields, ##kpi_grid, ##chart_grid, ##monitor_kpi, ##proj_layout, ##explorer_layout. Los 3 ABORTADOS tambien comentados con razon tecnica.
- [ ] **2.2** Actualizar `audit_data_table_usage_go_infra` para leer ese comentario y filtrar `[warn] -> [ignored:declared_layout_table]`.
- [x] **3.1** Decidir los 2 DISCUTIBLES (`te_fields`, `linked_tbl`) con criterio "si rows pueden crecer > 50, migrar". Decision: DEJAR. `te_fields` max ~20 fields por tipo; `linked_tbl` max ~10 modules linked. Rows no escalan.
- [x] **4.1** Envolver TODAS las llamadas a `data_table::render` en `ImGui::BeginChild` host. HECHO en las 3 tablas migradas: flows_work (220px), top_issues_work (-1), recipes_tbl (300px), dt_run_steps (usa el BeginChild preexistente ##run_steps_wrap).
- [ ] **5.1** Re-ejecutar audit:
```
./fn run audit_data_table_usage
```
Verificar: 0 BUG hits, 9 LEGITIMOS comentados, 11 `no_child_host` resueltos o documentados como excepcion.
- [x] **5.2** Build de las 4 apps modificadas. HECHO: dag_engine_ui, registry_dashboard, graph_explorer compilan OK (Linux). navegator_dashboard es Windows-only (CMakeLists.txt retorna en non-WIN32); sintaxis verificada via g++ -fsyntax-only sin errores.
## Patrones de migracion canonicos
### Patron A: ImGui::BeginTable inline → data_table::render basico
```cpp
// ANTES
if (ImGui::BeginTable("##recipes_tbl", 6, flags)) {
ImGui::TableSetupColumn("name");
ImGui::TableSetupColumn("url_pattern");
// ...
for (const auto& r : recipes) {
ImGui::TableNextRow();
ImGui::TableNextColumn(); ImGui::TextUnformatted(r.name.c_str());
ImGui::TableNextColumn(); ImGui::TextUnformatted(r.url_pattern.c_str());
// ...
}
ImGui::EndTable();
}
// DESPUES
static data_table::State g_st_recipes;
static std::vector<std::string> g_back_recipes; // backing
static std::vector<const char*> g_ptrs_recipes; // ptrs row-major
g_back_recipes.clear();
for (const auto& r : recipes) {
g_back_recipes.push_back(r.name);
g_back_recipes.push_back(r.url_pattern);
// ... resto cols
}
g_ptrs_recipes.clear();
for (auto& s : g_back_recipes) g_ptrs_recipes.push_back(s.c_str());
data_table::TableInput tbl;
tbl.name = "recipes";
tbl.headers = {"name", "url_pattern", "last_status", "last_at", "tries", "ok"};
tbl.types = {data_table::ColumnType::String, data_table::ColumnType::String,
data_table::ColumnType::String, data_table::ColumnType::Date,
data_table::ColumnType::Int, data_table::ColumnType::Bool};
tbl.cells = g_ptrs_recipes.data();
tbl.rows = (int)recipes.size();
tbl.cols = 6;
// Status como CategoricalChip (ganancia inmediata sobre BeginTable)
tbl.column_specs.resize(tbl.cols);
for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i];
tbl.column_specs[2].renderer = data_table::CellRenderer::CategoricalChip;
tbl.column_specs[2].chips = {{"ok","#22c55e"},{"error","#ef4444"},{"pending","#a3a3a3"}};
std::vector<data_table::TableEvent> events;
ImGui::BeginChild("##recipes_host", ImVec2(-1, -1));
data_table::render("##recipes_dt", {tbl}, g_st_recipes, &events);
ImGui::EndChild();
for (const auto& ev : events) {
if (ev.kind == data_table::TableEventKind::RowDoubleClick) {
open_recipe_detail(ev.row);
}
}
```
### Patron B: BeginTable inline con interactividad (boton por fila)
Si la BeginTable inline tiene un boton "Delete" / "Edit" por fila → migrar usando `CellRenderer::Button` + `action_id`:
```cpp
// ANTES
ImGui::TableNextColumn();
if (ImGui::SmallButton(("Delete##" + r.id).c_str())) { delete_recipe(r.id); }
// DESPUES — anadir columna actions con button renderer
tbl.headers.push_back("actions");
tbl.types.push_back(data_table::ColumnType::String);
data_table::ColumnSpec actions_spec;
actions_spec.id = "actions";
actions_spec.renderer = data_table::CellRenderer::Button;
actions_spec.button_action = "delete_recipe";
actions_spec.button_label = "Delete";
actions_spec.button_color_hex = "#ef4444";
tbl.column_specs.push_back(actions_spec);
tbl.cols++;
// Backing extra
for (const auto& r : recipes) {
g_back_recipes.push_back(r.id); // celda actions = el id (consumido en ev.value)
}
// Handler
for (const auto& ev : events) {
if (ev.kind == data_table::TableEventKind::ButtonClick && ev.action_id == "delete_recipe") {
delete_recipe(ev.value); // ev.value == r.id de la fila clicada
}
}
```
## Riesgos
- **Backing storage**: las apps deben mantener `std::vector<std::string>` (estable) + `std::vector<const char*>` (ptrs row-major). Helper `cells_to_ptrs()` ya esta usado en data_factory — generalizar como `cpp/functions/core/cells_to_ptrs.cpp` si patron se repite >2 veces (ya pasa).
- **State persistente**: cada migracion requiere `static data_table::State g_st_<name>;`. Si la app tiene N tablas, N states.
- **Comportamiento sutil**: filter/sort/freeze ahora son user-toggle, no controlados por la app. La app pierde control fino, pero gana consistencia.
## Bonus: nueva funcion del registry `cells_to_ptrs_cpp_core`
Patron `g_back + g_ptrs` aparece en data_factory + (post 0107g) en 4 apps mas. Promover a funcion del registry:
```cpp
// cpp/functions/core/cells_to_ptrs.h
namespace fn {
// Converts a row-major flat vector<string> to a row-major vector<const char*>
// pointing into the backing storage. Stable pointers — backing must not be
// resized while ptrs are in use.
void cells_to_ptrs(const std::vector<std::string>& back,
std::vector<const char*>& ptrs);
}
```
Issue separado o sub-task de 0107g segun apetito.
## Notas
- El audit `audit_data_table_usage_go_infra` ya existe (FRESH 7d). Se referencia desde `fn doctor modules` (0107a) para mostrar drift en CI/dashboard.
@@ -0,0 +1,356 @@
---
id: "0108"
title: "App tables_qa — testbed agresivo del modulo data_table + deprecar tables_playground"
status: in-progress
type: app
domain:
- cpp-stack
- apps-infra
- meta
scope: app
priority: alta
depends:
- "0107"
blocks: []
related:
- "0081"
- "0097"
created: 2026-05-17
updated: 2026-05-17
tags: [data-table, modules, testbed, qa, regression, version-selector, apphub]
---
# 0108 — tables_playground como testbed agresivo del modulo data_table
## Problema
`apps/primitives_gallery/playground/tables/` hoy es el playground original de tablas — fue el origen de `modules/data_table`. Pero esta DESACOPLADO del modulo: linkea sus PROPIAS copias de `data_table.cpp`, `data_table_logic.cpp`, `tql.cpp`, `tql_to_sql.cpp`, `lua_engine.cpp`, `llm_anthropic.cpp`, `viz.cpp` (versiones legacy). El `self_test.cpp` (430 checks) prueba la logica legacy, no el modulo.
Consecuencias:
- Bug en el modulo no se detecta en el playground.
- Cualquier mejora del modulo (post-0107) NO se valida contra el playground hasta que algun app lo encuentre en runtime.
- El playground es deuda — codigo duplicado que nadie mantiene a la par.
Ademas, las apps consumidoras (`app_gestion`, `data_factory`, `registry_dashboard`, `services_monitor`, etc.) usan el modulo con patrones repetidos pero no documentados como "casos de uso canonicos":
- `CellRenderer::CategoricalChip` con `chips` map (status, kind, enabled).
- `CellRenderer::ColorScale` con `range_min`/`range_max`/`range_alpha` (duracion en ms).
- `CellRenderer::Badge` con `badges` map (version pinning, status, env).
- `CellRenderer::Duration` con `duration_warn_ms` / `duration_error_ms`.
- `CellRenderer::Button` con `button_action` → consumido en `events_out` (`TableEventKind::ButtonClick`).
- `TableEventKind::RowDoubleClick` → abrir detalle / drilldown.
- Joins entre `TableInput[0]` (main) y `TableInput[i]` (joinables) con `JoinStrategy`.
- Color rules condicionales (numericas + categoricas) — declaradas en `State.stages[k].color_rules`.
NO HAY un sitio donde un agente o humano pueda ver TODOS estos patrones funcionando lado-a-lado para verificar visualmente que el modulo se comporta bien. El primitives_gallery hace eso para componentes basicos, pero no para tablas avanzadas.
## Decision (actualizada 2026-05-17)
Crear **app NUEVA `tables_qa`** (no evolucionar `tables_playground` in-place). Razon: playground tiene 8 .cpp legacy + self_test 2921 LOC mezclado. Mas limpio crear desde 0 con `init_cpp_app` + `uses_modules: [data_table_cpp]` desde el principio.
`tables_playground` se **deprecara** tras `tables_qa` verde:
- `apps/primitives_gallery/playground/tables/` → mover a `apps/primitives_gallery/playground/tables_legacy_archive/` (gitignored o conservado por historia).
- Entry en `cpp/CMakeLists.txt` que registra `tables_playground` se elimina.
- `tables_playground_self_test` deja de buildear.
### Identidad de la app
- `name`: `tables_qa`
- `description`: "Testbed agresivo del modulo data_table — multi-tabla, menu QA toggleable, inyector de eventos, counters live, version selector + downgrade side-by-side."
- `icon.phosphor`: `"test-tube"` (Phosphor — claro QA, sin ambiguedad)
- `icon.accent`: `"#f59e0b"` (amber — testing zone, distinto de los colores existentes de otras apps)
- `dir_path`: `apps/tables_qa`
- `repo_url`: `https://gitea.organic-machine.com/dataforge/tables_qa`
- `tags`: `[imgui, dashboard, qa, testing, data-table, regression]`
### Registro en App Hub
Tras build verde:
1. `./fn run generate_app_icon "test-tube" "#f59e0b" "apps/tables_qa/appicon.ico"` — genera .ico multi-res.
2. Anadir trio obligatorio (`description` + `icon.phosphor` + `icon.accent`) al `app.md` (ya cubierto en frontmatter del scaffolder).
3. `./fn run refresh_app_hub` — regenera icons PNG + manifest TSV del hub + reinicia `app_hub_launcher` si esta corriendo.
4. Verificar tarjeta visible en hub con icono amber + descripcion.
## Decision (original — refundar a aplicacion completa)
Refundar `tables_playground` como **testbed agresivo del modulo `fn_module_data_table`**:
1. **Migrar al modulo**: el playground PASA a depender de `fn_module_data_table` static lib. Eliminar las copias locales (`data_table.cpp`, `data_table_logic.cpp`, `tql.cpp`, `lua_engine.cpp`, `viz.cpp`, `tql_to_sql.cpp`, `llm_anthropic.cpp`). El playground se convierte en cliente del modulo igual que las apps reales.
2. **Multi-tabla**: 6-8 tablas demo cubriendo cada `CellRenderer` + cada `TableEventKind` + joins + color rules. Cada una con su `State` persistente. Tab navegable (`ImGui::BeginTabBar`).
3. **Menu de acciones replicando apps**: barra superior con toggles que activan/desactivan features tipicas usadas por apps:
- "Buttons en celdas (Cancel/Retry/Inspect)" → emite ButtonClick.
- "Color condicional numerico (ColorScale en duraciones)".
- "Color condicional categorico (CategoricalChip por status)".
- "Badge version pinning".
- "Duration con thresholds (warn=1000ms / error=5000ms)".
- "Dots sparkline (status timeline)".
- "Icon map (TI_CHECK/TI_X/TI_CIRCLE)".
- "Join Left/Inner/Right/Full entre tablas".
- "Row double-click → modal de detalle".
- "Row right-click → context menu".
- "TQL pipeline (filter/breakout/agg/sort) con chips".
- "Ask AI panel (TQL natural language)".
- "Save/Load TQL desde disco".
- "Export CSV".
- "Drill-down entre stages".
4. **Version selector + downgrade**:
- Mostrar version actual del modulo en header (`fn::framework_version()` / `data_table::module_version()` — anadir API).
- Selector de "modo compatibilidad" para simular comportamiento de versiones anteriores (`v1.4`, `v1.3`, etc.). Implementacion: feature flags por version dentro del modulo o branch-by-config al construir `TableInput`. Util para auditar regresiones cuando bumpemos versiones.
- Side-by-side: renderizar la misma `TableInput` con la version actual a la izquierda y con el modo compatibilidad a la derecha. Diff visual inmediato.
5. **Capture & compare visual**: integrar con el sistema `primitives_gallery --capture` existente (golden images) para que cada commit corra screenshots del testbed y compare contra master. Si pixel diff > threshold → CI rojo.
6. **Self-test del modulo**: nuevo `tables_playground_module_test.cpp` que ejerza la API publica `data_table::render()` con casos sinteticos. Headless (sin GLFW window) usando `imgui_test_engine` (si FN_BUILD_TESTS=ON). Cubre:
- Cada `CellRenderer` enum se renderiza sin crash.
- `events_out` recibe el evento correcto al simular click.
- State se mantiene entre frames.
- TQL pipeline produce output esperado.
- Joins respetan strategy.
## Arquitectura
### Estructura final del playground
```
apps/primitives_gallery/playground/tables/
CMakeLists.txt # linka fn_module_data_table; ELIMINA sources legacy
main.cpp # entry: fn::run_app + render_playground()
playground.cpp # render_playground(): tab bar + acciones menu
tables/ # NEW — 1 archivo por tabla demo
tbl_basic.cpp # Text + numerico, base case
tbl_renderers.cpp # 1 columna por cada CellRenderer (visual review)
tbl_buttons.cpp # Cancel/Retry/Inspect — emite ButtonClick
tbl_color_rules.cpp # ColorScale numerico + CategoricalChip
tbl_durations.cpp # Duration renderer + thresholds
tbl_dots.cpp # Dots sparkline (status timelines)
tbl_joins.cpp # 2 tablas + Left/Inner/Right/Full
tbl_tql.cpp # Pipeline TQL completo + Ask AI
tbl_drill.cpp # Drill-down stages
version_compat.cpp # version selector + side-by-side downgrade
test_module.cpp # NEW: self_test contra API publica del modulo
e2e_run.sh # actualizar: build + test_module + capture
(ELIMINAR: data_table.cpp, data_table_logic.cpp, tql.cpp, tql_to_sql.cpp,
lua_engine.cpp, llm_anthropic.cpp, viz.cpp, tql_duckdb.cpp, self_test.cpp)
README.md # NEW: documenta cada tabla demo + checklist e2e
```
### API publica del modulo expuesta al playground (documentar)
```cpp
// Entry function
namespace data_table {
void render(const char* id,
const std::vector<TableInput>& tables,
State& state,
std::vector<TableEvent>* events_out = nullptr,
bool show_chrome = true);
}
// Tipos publicos consumidos por las apps
struct TableInput {
std::string name;
std::vector<std::string> headers;
std::vector<ColumnType> types;
const char* const* cells; // row-major, rows*cols
int rows;
int cols;
std::vector<ColumnSpec> column_specs; // renderer config per col
};
struct ColumnSpec {
std::string id;
CellRenderer renderer = CellRenderer::Text;
// Badge / CategoricalChip / Dots
std::vector<BadgeRule> badges;
std::vector<ChipRule> chips;
// Progress
bool progress_scale_100 = false;
std::string progress_color_hex;
// Duration
float duration_warn_ms = 1000.0f;
float duration_error_ms = 5000.0f;
// Icon
std::vector<IconMapEntry> icon_map;
// Button
std::string button_action;
std::string button_label;
std::string button_color_hex;
// ColorScale
double range_min = 0.0;
double range_max = 1.0;
float range_alpha = 0.25f;
std::vector<ColorStop> color_stops;
// Tooltip
std::string tooltip;
bool tooltip_on_hover = false;
};
enum class CellRenderer : uint8_t {
Text=0, Badge=1, Progress=2, Duration=3, Icon=4, Button=5,
Dots=8, CategoricalChip=9, ColorScale=10,
};
enum class TableEventKind : uint8_t {
ButtonClick=1, RowDoubleClick=2, RowRightClick=3, CellEdit=4,
};
struct TableEvent {
TableEventKind kind;
int row;
int col;
std::string column_id;
std::string action_id; // ColumnSpec::button_action on ButtonClick
std::string value;
};
struct State {
// (opaca al consumidor — gestionada internamente; debe persistir entre frames)
};
// Module metadata (NEW en este issue)
namespace data_table {
const char* module_version(); // ej. "2.0.0"
const char* module_description();
}
```
Esta especificacion va a `docs/MODULES_API.md` (issue 0107f) como el contrato canonico de `data_table_cpp`.
### Patron de uso canonico (apps)
```cpp
static data_table::State st;
data_table::TableInput tbl;
tbl.name = "runs";
tbl.headers = {"id", "status", "duration_ms", "started_at"};
tbl.types = {ColumnType::String, ColumnType::String, ColumnType::Float, ColumnType::Date};
tbl.cells = &cells_flat[0];
tbl.rows = N;
tbl.cols = 4;
// Configure renderers
tbl.column_specs.resize(tbl.cols);
for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i];
tbl.column_specs[1].renderer = data_table::CellRenderer::CategoricalChip;
tbl.column_specs[1].chips = { {"ok", "#22c55e"}, {"error", "#ef4444"}, {"running", "#f59e0b"} };
tbl.column_specs[2].renderer = data_table::CellRenderer::ColorScale;
tbl.column_specs[2].range_min = 0.0;
tbl.column_specs[2].range_max = 5000.0;
tbl.column_specs[2].range_alpha = 0.30f;
// Button column for retry action
data_table::ColumnSpec retry_col;
retry_col.id = "actions";
retry_col.renderer = data_table::CellRenderer::Button;
retry_col.button_action = "retry_run";
retry_col.button_label = "Retry";
tbl.headers.push_back("actions");
tbl.column_specs.push_back(retry_col);
// Render + consume events
std::vector<data_table::TableEvent> events;
data_table::render("##runs_tbl", {tbl}, st, &events);
for (auto& ev : events) {
if (ev.kind == data_table::TableEventKind::ButtonClick && ev.action_id == "retry_run") {
// app handler
}
if (ev.kind == data_table::TableEventKind::RowDoubleClick) {
// open detail modal
}
}
```
## Tareas
- [ ] **1.1** Investigar TODOS los patrones de uso del modulo en apps (script audit):
```bash
grep -rhnE "data_table::(render|TableInput|State|ColumnSpec|TableEvent|CellRenderer::|TableEventKind::|JoinStrategy)" \
apps/*/main.cpp apps/*/*.cpp projects/*/apps/*/*.cpp 2>/dev/null | sort -u > /tmp/data_table_api_usage.txt
```
Producir tabla de patrones canonicos en `docs/MODULES_API.md`.
- [ ] **2.1** Backup `apps/primitives_gallery/playground/tables/{data_table.cpp,data_table_logic.cpp,tql.cpp,tql_to_sql.cpp,lua_engine.cpp,llm_anthropic.cpp,viz.cpp,tql_duckdb.cpp,self_test.cpp}` a `apps/primitives_gallery/playground/tables/legacy_archive/` (gitignored o conservado por historia).
- [ ] **2.2** Editar `apps/primitives_gallery/playground/tables/CMakeLists.txt`:
- Quitar sources legacy.
- `target_link_libraries(tables_playground PRIVATE fn_module_data_table)`.
- El target `tables_playground_self_test` se renombra a `tables_playground_module_test` con sources nuevos.
- [ ] **3.1** Crear `playground.cpp` con tab bar + menu acciones.
- [ ] **3.2** Crear 9 archivos `tables/tbl_*.cpp` con cada caso demo.
- [ ] **3.3** Crear `version_compat.cpp` con selector + side-by-side.
- [ ] **3.4** Anadir API `data_table::module_version()` + `module_description()` a `modules/data_table/data_table.h`. Implementacion lee `version_generated.h` (post 0107c bump a 2.0.0).
- [ ] **4.1** Crear `test_module.cpp` headless con `imgui_test_engine` (gated por `FN_BUILD_TESTS=ON`):
- 1 test por `CellRenderer` enum.
- 1 test por `TableEventKind`.
- 1 test de joins.
- 1 test de TQL pipeline.
- Compara cells output via golden TSV.
- [ ] **4.2** Migrar lo aplicable del `self_test.cpp` legacy (430 checks) a tests del modulo. Lo que prueba logica pura ya extraida al registry (parse_number, compare, tql parsing, lua) va a `cpp/functions/core/*_test.cpp`. Lo que prueba render() va a `test_module.cpp`.
- [ ] **5.1** Actualizar `e2e_run.sh`: build + module_test + capture screenshot.
- [ ] **5.2** Integrar con `primitives_gallery --capture` (golden images de cada tab del testbed).
- [ ] **5.3** README.md con checklist de QA por tab.
- [ ] **6.1** Verificar que las apps consumidoras (7) siguen compilando y comportandose igual.
- [ ] **6.2** `fn index` para que el playground actualizado quede registrado (aunque sea playground, su `app.md` puede declarar `uses_modules: [data_table_cpp]` con min_version 2.0.0).
## Decisiones de diseño
1. **Eliminar sources legacy**: opcion alternativa era mantenerlos como golden-reference. Decision: eliminarlos. Razon: si quedan vivos, la tentacion de "arreglar el playground" sin tocar el modulo permanece. Forzar a usar el modulo cierra el loop.
2. **Side-by-side downgrade**: opcion alternativa era branchear el modulo en `data_table_v1` static lib paralelo. Decision: feature flags por version dentro del modulo actual (`v_compat_mode`). Menos duplicacion, mas pragmatico.
3. **`module_version()` como API publica**: alternativa era leer `version_generated.h` desde la app. Decision: API publica + estable. Las apps que quieran mostrar la version del modulo en su About panel lo hacen via la funcion, no via include privado.
## Prerequisitos
- Issue 0107 (estandarizacion modulos) cerrado y `modules-v2` activo. **Bloqueo duro**: sin 0107, el sistema sigue con drift y refactorizar el playground encima de codigo inestable es desperdicio.
## Riesgos
- **Headless test con imgui_test_engine**: requiere `FN_BUILD_TESTS=ON`. Si no esta disponible en CI, los tests no corren. Mitigacion: documentar requirement en CI config + fallback a tests de logica pura sin UI.
- **Version compat mode** es facil de hacer mal: el codigo del modulo se llena de `if (v_compat < X)`. Mitigacion: limite estricto — solo se mantiene compat para las 2 versiones previas (`v_current - 1`, `v_current - 2`). Mas alla, se elimina y se documenta breaking change.
- **Capture and compare** depende de fonts/DPI/driver GL → false positives en CI. Mitigacion: capture solo en Linux x86_64 con driver mesa fijado.
## Infra de testing reutilizable (existente en el repo)
El testbed se construye combinando estos mecanismos ya disponibles. Ningun nuevo motor.
| Mecanismo | Ubicacion | Que aporta al testbed |
|---|---|---|
| Catch2 unit | `cpp/tests/` (50+ tests, macro `add_fn_test`) | Tests de logica pura de sub-funciones del registry usadas por el modulo (`compute_column_stats`, `tql_emit`, `lua_engine`, `join_tables`, `tql_to_sql`). Ya existen — los tomamos como dado. |
| Golden PNG diff | `cpp/tests/golden/` (43 PNGs) + `png_diff.cpp` | Cada tab del testbed exporta golden via `primitives_gallery --capture`. Diff en CI bloquea drift visual. |
| Auto-driving worker | `apps/altsnap_jitter_test/main.cpp` (modelo de referencia) | Pattern: worker thread fakea eventos + counters monotonicos en `fn::internal::*` + `set_force_alt_for_test()` bypass. Replicar para `data_table`: counters `fn::internal::data_table::{button_click_count, row_double_click_count, color_rule_applied_count, ...}` + worker que inyecta clicks. |
| Dear ImGui Test Engine | `cpp/framework/app_base.h:205-225` (`fn::run_app_test`, gated por `FN_BUILD_TESTS=ON`) | UI-level tests: `IM_REGISTER_TEST` lambdas que llaman `ctx->ItemClick`, `ctx->ItemDoubleClick`, verifican events emitidos. **CERO consumidores actuales** — el testbed sera el primer cliente. |
| `--self-test` flag | `.claude/rules/e2e_validation.md` (patron canonico) | `tables_playground --self-test` corre toda la suite headless, exit 0/1. Compatible con CI sin display. |
| Cross-platform e2e | `bash/functions/infra/e2e_run_cpp_windows.sh` | Funcion bash: cross-compile mingw + deploy Desktop + taskkill + run native + capturar stdout/exit. Linux CI valida Windows. |
### Panel de control QA agresivo (UI del testbed)
Barra superior con:
1. **Toggles por feature** (15+ checkboxes). Cada toggle muta `ColumnSpec` o `State` en runtime, mismo patron que apps reales hacen en codigo de setup. El usuario QA puede activar/desactivar y ver cambio visual inmediato sin recompilar.
2. **Inyector de eventos**: 4 botones — "Simulate ButtonClick", "Simulate RowDoubleClick", "Simulate RowRightClick", "Simulate Drill". Cada uno llama el worker thread pattern de altsnap → verifica que `TableEvent` apropiado se emite.
3. **Counters live**: contador por evento, contador por renderer aplicado, latency p50/p95 por frame (medir desde `ImGui::GetIO().Framerate`). Visible siempre, util para detectar regresiones de performance.
4. **Version selector**: dropdown con versiones del modulo (`2.0.0` actual, `1.4.0` compat, `1.3.0` compat...). Cambio re-renderiza la misma data side-by-side: actual vs version seleccionada. Diff visual inmediato + diff de events emitidos.
5. **Boton "Run --self-test"**: ejecuta toda la suite headless dentro de la misma ventana. Output en log panel. Util para iteracion rapida sin relanzar.
6. **Boton "Export golden"**: corre `--capture` sobre cada tab, escribe PNGs a `golden/data_table_testbed/`. Para regenerar baseline tras cambio visual intencional.
### Counters internos a anadir al modulo
Issue 0108 anade en `modules/data_table/data_table.cpp`:
```cpp
namespace data_table::internal {
int button_click_count();
int row_double_click_count();
int row_right_click_count();
int color_rule_applied_count();
int tql_stages_executed();
int last_render_duration_us();
void reset_counters();
}
```
Mismo modelo que `fn::internal::*` para altsnap. Test-only observability, cost cero en prod (counters atomicos triviales).
## Notas
- Issue 0108 NO empieza hasta 0107 cerrar. Bloqueado duro.
- Se referencia desde `docs/MODULES_API.md` (0107f) como el ejemplo canonico de "como usar el modulo data_table".
- Cuando 0108 cierre, abrir issue 0109 paralelo para `chat_ia` (que era el siguiente modulo planeado al inicio de 0107).
@@ -0,0 +1,60 @@
---
id: "0109a"
title: "skill_tree app shell + parsers issues/flows"
status: completado
type: feature
domain:
- meta
- cpp-stack
scope: app-scoped
priority: media
depends: []
blocks:
- "0109b"
related:
- "0109"
created: 2026-05-17
updated: 2026-05-17
tags:
- skill-tree
- cpp
- imgui
- parsers
---
# 0109a — skill_tree shell + parsers
Primer slice del epic 0109. Foco: app C++ scaffoldada, compilando, leyendo los 79 issues + 7 flows y reportando conteos en stdout. Sin render del grafo todavia — solo plumbing.
## Tareas
1. Scaffolder `./fn run init_cpp_app skill_tree --domain tools --desc "..." --tags "dashboard,meta"`.
2. Editar `app.md` generado: trio icon (`tree-structure`, `#c026d3`), `e2e_checks`, `uses_functions` iniciales.
3. Generar `appicon.ico` via `generate_app_icon_py_infra`.
4. Crear funcion `parse_md_frontmatter_cpp_core` (delegar a fn-constructor):
- Input: `std::string content` (texto del .md completo).
- Output: `MdFrontmatter` struct con `std::unordered_map<std::string, YamlValue>` y `std::string body`.
- `YamlValue` = variant `{string, list<string>, null}`. Subset YAML suficiente para issues actuales.
- Pure. Test golden: parsea los 79 issues + 7 flows sin error.
5. En `main.cpp` (scaffold inicial): al arrancar, scan `dev/issues/*.md` + `dev/flows/*.md`, parse cada uno, contar por status/domain. Log a stdout:
```
skill_tree v0.1.0
issues: 79 (28 pendiente, 3 in-progress, 72 completado, ...)
flows: 7 (5 pending, 2 completed)
parse errors: 0
```
6. `e2e_checks` build + self-test warning.
7. Compilar + deploy Windows via `redeploy_cpp_app_windows`.
8. Refrescar hub via `refresh_app_hub`.
## DoD
- [ ] App existe en `apps/skill_tree/` con `.git/` apuntando a `dataforge/skill_tree`.
- [ ] `app.md` con trio completo + `e2e_checks` + `uses_functions` declarados.
- [ ] `appicon.ico` generado.
- [ ] `fn index` exitoso, `mcp__registry__fn_show id="skill_tree"` muestra metadata.
- [ ] `parse_md_frontmatter_cpp_core` indexado en registry.
- [ ] `cmake --build cpp/build --target skill_tree` exitoso.
- [ ] `./skill_tree` (Linux) o `skill_tree.exe` (Windows) imprime conteos correctos.
- [ ] Tarjeta visible en `app_hub_launcher`.
- [ ] `fn doctor cpp-apps` limpio.
@@ -0,0 +1,69 @@
---
id: "0109b"
title: "skill_tree layout anillos + render canvas ImDrawList con cards"
status: completado
type: feature
domain:
- meta
- cpp-stack
scope: app-scoped
priority: media
depends:
- "0109a"
blocks:
- "0109c"
related:
- "0109"
created: 2026-05-17
updated: 2026-05-17
tags:
- skill-tree
- cpp
- imgui
- layout
- canvas
---
# 0109b — skill_tree layout anillos + render canvas
Segundo slice del epic 0109. Reemplaza la lista textual del Tree por un canvas interactivo basado en `ImDrawList`. Pivote desde `graph_renderer_cpp_viz` (GPU) → `ImDrawList` (CPU) para mantener simplicidad: 166 nodos no justifican el pipeline GPU.
## Decisiones tomadas durante la implementacion
- **Stack: `ImGui::ImDrawList`**, NO `graph_renderer_cpp_viz`. Razon: 166 nodos cabian de sobra en CPU; `graph_renderer` exige `init_gl_loader=true`, build de `GraphData` con tipos OSINT, shaders, FBO + texture flip-Y. Diferencia ~120 LOC + un monton de rebuilds para cero beneficio observable.
- **Sin fisicas** (el usuario lo pidio explicito). Layout deterministico via `compute_ring_layout_cpp_core`.
- **5 rings**: done (0), in-progress (1), unlocked (2), locked (3), deferred/bloqueado (4).
- **18 sectores radiales** = 18 dominios canonicos (`dev/TAXONOMY.md`). Labels en el aro exterior.
- **Lock derivation**: `pendiente` se subdivide en `pendiente_unlocked` (todos los `depends[]` en done) vs `pendiente_locked` (algun depends sin completar). Set de `done` IDs se computa al cargar y se cruza con cada `depends[]`.
- **Animacion lerp 1s** entre prev y current position cuando un nodo cambia de `status_eff` entre dos `reload_scan()`s. Ease-in-out cuadratica.
- **Cards con texto**: cada nodo muestra su ID en blanco con sombra negra para legibilidad sobre cualquier color de ring.
- **Diferencial visual flows vs issues**: issues = circulos, flows = rombos.
- **Pan**: drag con boton derecho o medio.
- **Zoom**: rueda del raton, centrado en cursor (re-anchora coordenadas mundo bajo el puntero).
- **Picking**: O(N) radius check (166 nodos = trivial; spatial hash innecesario).
## Tareas
1. Crear funcion del registry `compute_ring_layout_cpp_core` (pure, 10 tests Catch2, FNV-1a determinista para sub-jitter angular).
2. Reescribir `main.cpp::draw_tree()` como canvas con `ImGui::InvisibleButton` + `ImDrawList`.
3. Implementar `derive_status_eff()` para lock/unlock.
4. Implementar `apply_layout()` con preservacion de prev_x/prev_y para animacion.
5. Render: aristas curvas Bezier (depends + related) + nodos con outline + label ID + tooltip on hover.
6. HUD strip con LV/XP/contadores.
7. Self-test 0-exit cuando `parse_errors == 0 && unmapped == 0`.
8. Build Linux + deploy Windows.
## DoD
- [x] `compute_ring_layout_cpp_core` indexada (10/10 tests, 142 assertions).
- [x] `apps/skill_tree/main.cpp` usa `parse_md_frontmatter_cpp_core` + `compute_ring_layout_cpp_core`.
- [x] `app.md uses_functions` actualizado con ambas.
- [x] Self-test imprime breakdown por ring: `done=77 in-progress=2 unlocked=64 locked=22 deferred=1 unmapped=0`.
- [x] Linux build OK.
- [x] Windows deploy OK (PID corriendo).
- [x] Tarjeta visible en `app_hub_launcher`.
- [x] `fn doctor cpp-apps` limpio.
## Sigue
0109c: Inspector evolucionado con DoD parseado de la seccion `## DoD` del .md (checkboxes interactivos read-only) + lista de `uses_functions` del registry para esa issue.