---
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).
