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
+704
View File
@@ -0,0 +1,704 @@
# Modules API — contrato publico de modulos C++
Contrato canonico de cada modulo C++ del registry. Una app DEBE poder integrar un modulo leyendo SOLO esta pagina, sin abrir el `.cpp`. Si una app inventa un patron distinto al documentado aqui, es bug — abrir issue o anadir la capacidad al modulo, NO improvisar.
Issue 0107 (modules-standardization). Mantenido en sync con `modules/*/module.md::version` por el slash command `/version`.
---
## Indice
1. [framework_cpp](#framework_cpp) — shell ImGui obligatorio
2. [data_table_cpp](#data_table_cpp) — tabla TQL completa
3. [Capability matrix consolidada](#capability-matrix-consolidada)
4. [Ciclo de vida de un modulo](#ciclo-de-vida-de-un-modulo)
5. [Como NO conectarse a un modulo (anti-patrones)](#como-no-conectarse-a-un-modulo)
---
## framework_cpp
**Static lib target:** `fn_framework`
**Header path:** `cpp/framework/app_base.h`
**Namespace:** `fn` + `fn_ui`
**Entry function:** `int fn::run_app(AppConfig cfg, std::function<void()> render_fn)`
**State:** no requerido (singleton interno via `fn::run_app` lifecycle)
**Linkage en apps:** transitivo via macro `add_imgui_app(<target> ...)` — NO se declara en `uses_modules`.
**Version actual:** `1.1.0`
### Opt-in
Apps cpp del registry obtienen `fn_framework` automaticamente via `add_imgui_app(<target> ...)`. NO listar miembros del framework en `uses_functions` ni en `uses_modules`.
### Ejemplo minimo
```cpp
#include "framework/app_base.h"
int main() {
fn::AppConfig cfg;
cfg.title = "My App";
cfg.width = 1280;
cfg.height = 720;
cfg.about = { "my_app", "1.0.0", "Lo que hace mi app." };
cfg.log = { "my_app.log", 1 }; // file relativo a local_files/, level Info
return fn::run_app(cfg, []() {
ImGui::Begin("Main");
ImGui::Text("hello");
ImGui::End();
});
}
```
### API completa
| Campo `AppConfig` | Tipo | Default | Para que |
|---|---|---|---|
| `title` | `const char*` | `"fn_registry"` | Titulo de la ventana |
| `width` / `height` | `int` | 1280 / 720 | Tamano inicial |
| `vsync` | `bool` | `true` | VSync GL |
| `viewports` | `bool` | `true` | Multi-viewport (paneles dragged-out fuera del main) |
| `theme` | `ThemeMode` | `FnDark` | `FnDark` / `ImGuiDark` / `ImGuiLight` / `None` |
| `bg_r/g/b` | `float` | `fn_tokens::colors::bg` | Color background main |
| `about` | `AppAboutInfo` | `{}` | `name`, `version`, `description` para About window |
| `panels` + `panel_count` | `PanelToggle*` | `nullptr` | Toggles del menubar (paneles abre/cierra) |
| `layouts_cb` | `LayoutCallbacks*` | `nullptr` | Override del menu Layouts (default: auto-storage SQLite) |
| `auto_layouts` | `bool` | `true` | Si crear `LayoutStorage` por defecto |
| `auto_layouts_db` | `const char*` | `"layouts.db"` | Archivo SQLite relativo a `local_files/` |
| `view_extras` | `std::function<bool()>` | `{}` | Items extra en menu View |
| `init_gl_loader` | `bool` | `false` | Solo si la app llama `gl*` directo |
| `auto_dockspace` | `bool` | `true` | DockSpaceOverViewport antes de `render_fn` cada frame |
| `log` | `AppLogConfig` | `{}` | `file_path`, `level` 0-3 |
| `pre_frame` | `std::function<void()>` | `{}` | Hook antes del menubar (LayoutStorage custom apply) |
### Capacidades automaticas (zero opt-in)
- Window dark titlebar (Windows DWMWA_USE_IMMERSIVE_DARK_MODE).
- AltSnap-safe sizemove + Alt+RMB resize + Alt+LMB move (via Win32 WndProc subclass per HWND).
- App icon attach a HWND main + viewports secundarios (si `<app_dir>/appicon.ico` existe).
- Layouts persistentes (save/load/list/delete/reset) con restore-on-open + save-on-close.
- About / Settings / Logs windows + menubar.
- `imgui.ini` + `app_settings.ini` bajo `<exe_dir>/local_files/`.
- Migration desde cwd/exe_dir a `local_files/` para instalaciones viejas.
### Helpers publicos
```cpp
const char* fn::exe_dir(); // dir del exe
const char* fn::local_dir(); // <exe_dir>/local_files/
const char* fn::local_path(const char*); // <local_dir>/<name>
const char* fn::asset_dir(); // <exe_dir>/assets/
const char* fn::asset_path(const char*); // <asset_dir>/<name>
void fn::migrate_to_local_files(const char* const* names, size_t n);
const char* fn::framework_version();
const char* fn::framework_description();
```
### Gotchas
- `Restart=always` en systemd unit (NO `on-failure`) — `SIGTERM` limpio = exit success.
- App write-paths SIEMPRE via `fn::local_path("X")`. NO `fopen("X.db", ...)` con path relativo.
- `init_gl_loader = true` SOLO si la app llama GL directo (`gl_loader_init`). ImGui/ImPlot puro NO lo necesita.
- Anti-jitter pausa render durante `WM_ENTERSIZEMOVE` — apps con telemetria live deben tolerar pausa transitoria.
---
## data_table_cpp
**Static lib target:** `fn_module_data_table`
**Header path:** `data_table/data_table.h`
**Namespace:** `data_table`
**Entry function:** `void data_table::render(const char* id, const std::vector<TableInput>& tables, State& state, std::vector<TableEvent>* events_out, bool show_chrome)`
**State:** `data_table::State` opaco. Debe persistir entre frames (`static` per panel, NO stack-local).
**Linkage en apps:** explicito.
**Version actual:** `1.4.0` (sera `2.0.0` post 0107c).
### Opt-in en una app
1. `app.md` frontmatter:
```yaml
uses_modules:
- name: data_table_cpp
min_version: "1.4" # o "2.0" post 0107c
```
2. `CMakeLists.txt`:
```cmake
target_link_libraries(<app> PRIVATE fn_module_data_table)
```
3. Header en el `.cpp`:
```cpp
#include "data_table/data_table.h"
#include "core/data_table_types.h"
```
4. Reservar `data_table::State` persistente entre frames. NO ponerla en stack.
### Tipos publicos
```cpp
struct TableInput {
std::string name; // identificador estable (joins)
std::vector<std::string> headers; // names de columna
std::vector<ColumnType> types; // String/Int/Float/Bool/Date/Json/Auto
const char* const* cells; // row-major, cells[r*cols + c]
int rows;
int cols;
std::vector<ColumnSpec> column_specs; // renderer config per col
};
enum class ColumnType : uint8_t {
Auto = 0, String, Int, Float, Bool, Date, Json,
};
struct ColumnSpec {
std::string id; // estable; usado en TQL
CellRenderer renderer = CellRenderer::Text;
// Renderer-specific (ver tabla mas abajo)
std::vector<BadgeRule> badges;
std::vector<ChipRule> chips;
std::vector<IconMapEntry> icon_map;
bool progress_scale_100 = false;
std::string progress_color_hex;
float duration_warn_ms = 1000.0f;
float duration_error_ms = 5000.0f;
std::string button_action;
std::string button_label;
std::string button_color_hex;
char dots_separator = ',';
float dots_glyph_size = 0.0f;
int dots_max = 0;
bool dots_show_count = false;
double range_min = 0.0;
double range_max = 1.0;
float range_alpha = 0.25f;
std::vector<ColorStop> color_stops;
std::string tooltip; // "auto" -> show cell value
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,
};
struct BadgeRule { std::string value, color_hex, label; };
struct ChipRule { std::string match, color; };
struct IconMapEntry { std::string value, icon_name, color_hex; };
struct ColorStop { float position; std::string color; };
enum class TableEventKind : uint8_t {
ButtonClick=1, RowDoubleClick=2, RowRightClick=3, CellEdit=4,
};
struct TableEvent {
TableEventKind kind;
int row; // index en TableInput, NO en stage output
int col;
std::string column_id; // ColumnSpec.id de la columna clicada
std::string action_id; // para ButtonClick: ColumnSpec.button_action
std::string value; // valor de la celda
};
enum class JoinStrategy : uint8_t { Left, Inner, Right, Full };
```
### Capacidad 1: render basico (Text)
Caso minimo — todas las columnas como texto plano.
```cpp
static data_table::State st;
data_table::TableInput tbl;
tbl.name = "items";
tbl.headers = {"id", "name", "price"};
tbl.types = {data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::Float};
tbl.cells = &cells_flat[0]; // row-major
tbl.rows = N;
tbl.cols = 3;
data_table::render("##items_tbl", {tbl}, st);
```
NO `column_specs` necesario — `CellRenderer::Text` es default. Tabla aparece con sort, filter chips, freeze cols, todo gratis.
### Capacidad 2: Badge — colored badge per value
Para columnas con valores categoricos (status, version, env).
```cpp
tbl.column_specs.resize(tbl.cols);
for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i];
data_table::ColumnSpec& cs = tbl.column_specs[1]; // col "status"
cs.renderer = data_table::CellRenderer::Badge;
cs.badges = {
data_table::BadgeRule{"ok", "#22c55e", ""}, // verde
data_table::BadgeRule{"error", "#ef4444", "ERR"}, // rojo + label custom
data_table::BadgeRule{"running", "#f59e0b", ""}, // amber
};
```
- Match exacto, case-sensitive.
- `label` vacio = usa el valor de la celda como texto.
- Si ningun rule matchea, se renderiza como Text (sin badge).
### Capacidad 3: CategoricalChip — dot + texto
Igual que Badge pero solo dot de color a la izquierda del texto. Menos visual que Badge.
```cpp
cs.renderer = data_table::CellRenderer::CategoricalChip;
cs.chips = {
{"go", "#3b82f6"},
{"py", "#22c55e"},
{"cpp", "#a855f7"},
};
```
Pattern canonico apps:
- `app_gestion`: `status_col` + `enabled_col`.
- `data_factory`: kind columns + status columns.
- `registry_dashboard`: language column.
### Capacidad 4: ColorScale — gradient en background de celda
Numerico continuo. Mapea `range_min..range_max` a gradient (default green→amber→red).
```cpp
cs.renderer = data_table::CellRenderer::ColorScale;
cs.range_min = 0.0;
cs.range_max = 5000.0; // ms
cs.range_alpha = 0.30f; // bajo para legibilidad
// Opcional: custom stops
cs.color_stops = {
{0.0f, "#22c55e"},
{0.5f, "#f59e0b"},
{1.0f, "#ef4444"},
};
```
Pattern canonico: `duration_ms`, `latency_ms`, `bytes_transferred`. Apps: `data_factory` (node_runs), `services_monitor` (health latency).
### Capacidad 5: Duration — milisegundos con thresholds
Especifico para tiempos. Renderiza valor formateado (`123ms`, `1.2s`) + color por threshold.
```cpp
cs.renderer = data_table::CellRenderer::Duration;
cs.duration_warn_ms = 1000.0f;
cs.duration_error_ms = 5000.0f;
```
< warn = verde, warn..error = amber, > error = rojo.
### Capacidad 6: Progress — barra de progreso
```cpp
cs.renderer = data_table::CellRenderer::Progress;
cs.progress_scale_100 = false; // true -> valor en 0..100, false -> 0..1
cs.progress_color_hex = "#3b82f6"; // override; "" -> ImPlot auto
```
### Capacidad 7: Icon — Tabler icon lookup
```cpp
cs.renderer = data_table::CellRenderer::Icon;
cs.icon_map = {
{"ok", "TI_CHECK", "#22c55e"},
{"error", "TI_X", "#ef4444"},
{"pending", "TI_CIRCLE", "#a3a3a3"},
};
```
Iconos disponibles: `cpp/functions/core/icons_tabler.h` (Tabler v3.41, 1500+ glyphs).
### Capacidad 8: Button — clickable + event sink
Genera click events que la app procesa.
```cpp
cs.renderer = data_table::CellRenderer::Button;
cs.button_action = "retry_run"; // semantic id que la app reconoce
cs.button_label = "Retry"; // "" -> usa valor de la celda como label
cs.button_color_hex = "#3b82f6"; // "" -> color default
```
Consumir events:
```cpp
std::vector<data_table::TableEvent> events;
data_table::render("##runs", {tbl}, st, &events);
for (const auto& ev : events) {
if (ev.kind == data_table::TableEventKind::ButtonClick) {
if (ev.action_id == "retry_run") {
retry_job(ev.value); // ev.value = celda donde se clico
}
else if (ev.action_id == "cancel_job") {
cancel_job(ev.value);
}
}
}
```
`events_out` puede ser `nullptr` si no quieres consumir eventos (back-compat).
### Capacidad 9: Dots — inline status sparkline
Celda contiene tokens separados (`"ok,error,ok,ok"`). Renderiza dots de color por token.
```cpp
cs.renderer = data_table::CellRenderer::Dots;
cs.badges = { // reutiliza BadgeRule.color_hex (NO label)
{"ok", "#22c55e", ""},
{"error", "#ef4444", ""},
};
cs.dots_separator = ',';
cs.dots_glyph_size = 0.0f; // 0 = default font size
cs.dots_max = 10; // hard limit; 0 = unlimited
cs.dots_show_count = true; // append " (N)" tras los dots
```
Pattern canonico: timeline de status de runs recientes.
### Capacidad 10: Tooltip por celda
```cpp
cs.tooltip = "auto"; // muestra el valor de la celda en hover
// o:
cs.tooltip = "Click to retry";
cs.tooltip_on_hover = true;
```
### Capacidad 11: RowDoubleClick — drill / open detail
Sin setup especial. Solo consumir el event.
```cpp
for (const auto& ev : events) {
if (ev.kind == data_table::TableEventKind::RowDoubleClick) {
open_detail_modal(ev.row);
}
}
```
`ev.row` es el indice en `TableInput.cells`, NO en el output del stage activo si hay TQL aplicado.
### Capacidad 12: RowRightClick — context menu app-side
Igual que RowDoubleClick. App abre su propio popup ImGui cuando recibe el event.
```cpp
if (ev.kind == data_table::TableEventKind::RowRightClick) {
g_ctx_menu_row = ev.row;
ImGui::OpenPopup("##row_ctx");
}
// En el siguiente frame:
if (ImGui::BeginPopup("##row_ctx")) {
if (ImGui::MenuItem("Inspect")) inspect(g_ctx_menu_row);
if (ImGui::MenuItem("Delete")) delete_row(g_ctx_menu_row);
ImGui::EndPopup();
}
```
### Capacidad 13: Joins entre tablas
Pasar multiples `TableInput`. El usuario configura join via UI de chips (drag table → drop). El primer `tables[0]` es main, los demas son joinables.
```cpp
data_table::TableInput main_tbl, lookup_tbl;
// ... fill ambas ...
data_table::render("##joined", {main_tbl, lookup_tbl}, st, &events);
```
Strategies: `JoinStrategy::{Left, Inner, Right, Full}`. Default Left.
### Capacidad 14: TQL pipeline (filter / breakout / agg / sort)
Pipeline declarativo construido por usuario via chips bar. Estado en `State.stages[]`. App no toca esto — el modulo lo gestiona.
Stages adicionales (`State.stages.resize(N)`) si la app quiere PRE-cargar un pipeline:
```cpp
data_table::Stage s;
s.filters.push_back({0, data_table::Op::Eq, "go"}); // col 0 == "go"
s.breakouts.push_back("lang"); // group by lang
s.aggregations.push_back({data_table::AggFn::Count}); // count(*)
st.stages.push_back(s);
st.active_stage = (int)st.stages.size() - 1;
```
Agg fns: `Count`, `Sum`, `Avg`, `Min`, `Max`, `Median`, `P95`, `Distinct`.
### Capacidad 15: Ask AI panel
Usuario abre via boton "Ask AI" en chips bar. Modal con prompt natural language → llama `llm_anthropic::ask` (si `FN_LLM_ANTHROPIC` definido) → devuelve TQL o SQL ejecutable → Apply mutates `State.stages`.
App NO necesita setup. Solo `ANTHROPIC_API_KEY` en env.
### Capacidad 16: Color rules condicionales
Reglas de color que el usuario configura via header menu → "Conditional color". Tres tipos:
- **CellBg**: color de fondo fijo si valor matchea.
- **CategoricalDot**: dot de color izquierdo (igual que `chips` pero configurado en runtime por usuario).
- **NumericRange**: gradient sobre rango numerico (igual que `ColorScale` pero por regla, no por spec).
Persistencia en `State.stages[k].color_rules` — la app puede preset reglas en su setup si quiere, pero normal es dejar al usuario.
### Capacidad 17: Drill-down between stages
Usuario click derecho en celda de stage > 0 → "Drill into N=valor" → anade filter al stage previo, activa ese stage. Stack de back/forward en `State.drill_back/forward`.
App no toca. Solo `data_table::render` lo provee.
### Capacidad 18: Save/Load TQL desde disco
Modal "Save TQL" / "Load TQL" — escribe/lee `<state>.tql` en `local_files/`. App no toca.
### Capacidad 19: Export CSV
Boton "Export CSV" en chips bar. Escribe el output del stage activo a CSV. App no toca.
### Capacidad 20: `show_chrome` toggle
Si `show_chrome=false`, oculta chips bar + breadcrumb. Util para dashboards densos donde la tabla es read-only. Usuario puede reactivar via boton "Show UI".
```cpp
data_table::render("##compact", {tbl}, st, &events, /*show_chrome=*/false);
```
### Capacidad 21: Module version metadata (post 0107c)
```cpp
const char* data_table::module_version(); // "2.0.0"
const char* data_table::module_description();
```
Para que About panel de la app muestre version del modulo, no solo del app.
### Cuando usar `data_table::render` vs `ImGui::BeginTable` directo
| Caso | API correcta | Razon |
|---|---|---|
| **Tabla de datos** con N filas dinamicas, sort/filter/freeze/drill | `data_table::render` | Es lo que el modulo aporta |
| **KPI grid** (4-8 cards en grid de columnas) | `ImGui::BeginTable` directo | No es tabla de datos — layout helper |
| **Form key/value** (inspector, settings, schema metadata) | `ImGui::BeginTable` directo | Form fijo, columnas estaticas |
| **2-col layout** (tree izda / detail dcha) | `ImGui::BeginTable` directo | Splitter pattern, no data table |
| **Chart grid** (4 plots lado a lado) | `ImGui::BeginTable` directo | Layout multi-panel, no datos |
| **Schema editor row-form** (1 fila por field, con inputs editables por columna) | discutible — `ImGui::BeginTable` si rows < 20, `data_table::render` con `CellRenderer::Button`/edit si rows >> 20 | Depende del scale |
| **Recipes / Flows / Issues list** (data dinamica con sort/filter) | `data_table::render` (migrar si esta inline) | Es tabla de datos canonica |
Regla heuristica: si la tabla necesita SORT, FILTER, FREEZE o RENDERERS DECLARATIVOS → usar `data_table::render`. Si es solo layout → BeginTable directo OK.
Audit automatico: `mcp__registry__fn_run audit_data_table_usage` detecta apps con `inline_begintable` warnings + verifica `state_not_persistent`, `no_child_host`, `no_event_sink`, `cmake_missing_link`. Output formato `dev/data_table_integration_audit.md`.
### Host container — BeginChild recommended
`data_table::render` NO crea su propio `BeginChild` para el contenido principal (si para sub-paneles de viz). Si la app no envuelve la llamada en un host con bounds definidos:
- La tabla se renderiza inline con `ImGui::GetContentRegionAvail()` actual.
- Si el contenedor padre es scrollable, la tabla NO scrollea independientemente — comparte el scroll del padre.
- Si hay widgets DESPUES del `render()` en el mismo Begin/BeginChild, compiten por espacio.
Pattern recomendado:
```cpp
ImGui::BeginChild("##tbl_host", ImVec2(-1, -1)); // o tamano fijo
data_table::render("##my_tbl", {tbl}, st, &events);
ImGui::EndChild();
```
Excepcion: si la tabla es lo UNICO que renderiza dentro de su `ImGui::Begin(...)` window, BeginChild es opcional. El audit lo marca como `no_child_host [warn]`, no error.
### Gotchas
- **`State` debe persistir entre frames**: `static` per panel. Si la pones en stack, pierdes filter chips/sort/etc cada frame. Cada panel necesita SU `State` (no compartir entre tablas distintas).
- **`cells` row-major**: `cells[row*cols + col]`. Apps que pasan column-major rompen sin warning.
- **`TableInput.name` no vacio si hay joins**: si declaras varias tablas, cada una con `name` unico para que el join UI las identifique.
- **`column_specs.size() == cols`**: si no resizeas, el modulo asume `CellRenderer::Text` para columnas faltantes.
- **`ColumnSpec.id` debe ser estable** entre frames. Si cambia, las preferencias de sort/filter del usuario sobre esa col se pierden.
- **`button_action` debe ser unico** dentro del scope semantic de la app. Apps mismas que reusan `"retry"` en multiples columnas/contextos deberian diferenciar (`"retry_run"`, `"retry_test"`).
- **`badges` vs `chips`**: Badge = fondo coloreado completo + label; CategoricalChip = solo dot + texto plano. Eligir segun densidad visual deseada.
- **`Dots` reusa `badges`** map pero IGNORA `BadgeRule.label`. Solo `color_hex` por token.
- **`ColorScale` requiere ColumnType numerico** (`Float`, `Int`, `Date`). String no calcula gradient.
- **`tooltip = "auto"`** = string magico literal. NO `tooltip.empty()`.
- **events_out se LIMPIA cada frame** — la app debe leerlo INMEDIATAMENTE tras `render()`. No acumular entre frames.
- **`events.row` NO indexa al output del stage** — siempre apunta a `TableInput.cells` original. Si tu app hizo TQL pipeline, mapea manualmente via `state.row_mapping` si necesitas el row del output.
- **Multi-instancia**: cada `data_table::render` debe usar `id` unico (`"##tbl_a"`, `"##tbl_b"`). Si dos paneles comparten id, ImGui se confunde.
### Pattern canonico end-to-end (replicable)
```cpp
// Setup BadgeRule sets reusables a nivel de app
static std::vector<data_table::BadgeRule> run_status_badges() {
return {
{"ok", "#22c55e", ""},
{"error", "#ef4444", "ERR"},
{"running", "#f59e0b", ""},
{"pending", "#a3a3a3", ""},
};
}
// Inside panel render
static data_table::State st;
data_table::TableInput tbl;
tbl.name = "runs";
tbl.headers = {"id", "status", "duration_ms", "started_at", ""};
tbl.types = {
data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::Float,
data_table::ColumnType::Date,
data_table::ColumnType::String, // actions col (vacio, solo button)
};
tbl.cells = &flat_cells[0];
tbl.rows = N;
tbl.cols = 5;
tbl.column_specs.resize(tbl.cols);
for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i].empty()
? std::string("actions") : tbl.headers[i];
// Status → CategoricalChip
tbl.column_specs[1].renderer = data_table::CellRenderer::CategoricalChip;
tbl.column_specs[1].chips = {
{"ok", "#22c55e"}, {"error", "#ef4444"}, {"running", "#f59e0b"},
};
// duration_ms → ColorScale + Duration thresholds combinados
tbl.column_specs[2].renderer = data_table::CellRenderer::Duration;
tbl.column_specs[2].duration_warn_ms = 1000.0f;
tbl.column_specs[2].duration_error_ms = 5000.0f;
// Actions col → Button retry
tbl.column_specs[4].renderer = data_table::CellRenderer::Button;
tbl.column_specs[4].button_action = "retry_run";
tbl.column_specs[4].button_label = "Retry";
tbl.column_specs[4].tooltip = "Re-run this job";
tbl.column_specs[4].tooltip_on_hover = true;
// Render
std::vector<data_table::TableEvent> events;
data_table::render("##runs_tbl", {tbl}, st, &events);
// Consume events
for (const auto& ev : events) {
if (ev.kind == data_table::TableEventKind::ButtonClick && ev.action_id == "retry_run") {
retry_run_by_id(ev.value); // ev.value = id (col 0 de la fila clicada... NO)
// ^ CORRECCION: ev.value = valor de la celda del boton
// = "Retry" si button_label fijo. Para obtener el ID
// de la fila: mapear via ev.row y leer tbl.cells[ev.row*cols + 0]
}
if (ev.kind == data_table::TableEventKind::RowDoubleClick) {
open_run_detail_modal(/*run_id=*/ tbl.cells[ev.row * tbl.cols + 0]);
}
}
```
---
## Capability matrix consolidada
| Capacidad | Renderer / Event | ColumnSpec fields | Apps que la usan hoy |
|---|---|---|---|
| Plain text | `Text` (default) | — | todas |
| Colored badge | `Badge` | `badges` | registry_dashboard, app_gestion (version pinning) |
| Categorical dot | `CategoricalChip` | `chips` | services_monitor, dag_engine_ui, data_factory |
| Numeric heatmap | `ColorScale` | `range_min/max/alpha`, `color_stops` | data_factory (duration), services_monitor (latency) |
| Duration with thresholds | `Duration` | `duration_warn_ms`, `duration_error_ms` | data_factory (node_runs) |
| Progress bar | `Progress` | `progress_scale_100`, `progress_color_hex` | (raro — candidato para nuevos consumers) |
| Tabler icon | `Icon` | `icon_map` | (raro — candidato) |
| Click button | `Button` + ButtonClick | `button_action`, `button_label`, `button_color_hex` | graph_explorer (cancel/delete_job), data_factory |
| Status timeline | `Dots` | `dots_separator`, `dots_max`, `badges` map | registry_dashboard (recent runs) |
| Hover tooltip | (cualquier renderer) | `tooltip`, `tooltip_on_hover` | varios |
| Open detail | RowDoubleClick | — | app_gestion, data_factory, services_monitor |
| Context menu | RowRightClick | — | (raro) |
| Joins | (UI-driven) | usar varios `TableInput` | (raro — UI feature) |
| TQL pipeline | (UI-driven) | preset via `State.stages` | data_factory (preload filters) |
| Ask AI | (UI-driven) | `FN_LLM_ANTHROPIC` env | usuario lo dispara |
| Color rules runtime | (UI-driven) | `State.stages[k].color_rules` | usuario lo configura |
| Drill-down | (UI-driven) | `State.drill_back/forward` | usuario lo dispara |
| Save/Load TQL | (UI-driven) | — | usuario lo dispara |
| Export CSV | (UI-driven) | — | usuario lo dispara |
| Hide chrome | `show_chrome=false` | parametro de render() | dashboards densos |
---
## Como NO conectarse a un modulo
Anti-patrones que generan "conexiones raras entre apps":
| Anti-patron | Consecuencia | Sustituir por |
|---|---|---|
| Llamar funciones internas del modulo (helpers `static` en `data_table.cpp`) via include de path interno | Acoplamiento fuerte; rompe en cualquier refactor | API publica `data_table::*` solo |
| Reescribir logica que el modulo ya hace (parser TQL propio, color picker propio) | Duplicacion + drift | Usar API; si falta capacidad, abrir issue |
| Acceder a `State` fields directamente sin pasar por la API | State es opaco — campos pueden cambiar versiones | Usar la UI (chips bar) o setear `State.stages` segun esta doc |
| Inventar nuevos `action_id` cross-app esperando que otro modulo lo entienda | events son scoped a la app que los emite | Cada app maneja sus propios action_ids |
| Asumir `events_out` acumula entre frames | Se limpia cada frame | Procesar inmediatamente despues de `render()` |
| Pasar `State` por valor / hacer copia | Pierde sort/filter/drill cada frame | `static data_table::State st;` referencia |
| Compartir `State` entre dos `render()` con distintos `id` | UI dual-rendering corrupta | UN State per panel |
| Reuso de `column_specs` con `id` que cambia entre frames | Sort/filter preferences perdidas | `id` estable como `TableInput.headers[i]` |
| Saltarse `column_specs.resize(cols)` y solo settear algunas | Modulo asume `Text` para faltantes — comportamiento confuso | resize SIEMPRE a `cols` |
| Pasar TableInput de UNA tabla con `name=""` y esperar joins | Joins necesitan name unico | Setear `name` siempre |
| Mezclar Badge + CategoricalChip + Dots en misma columna (no compatible) | Solo uno por columna | Eligir basado en densidad visual |
| Acoplar app a header interno (`modules/data_table/data_table_internal.h`) | header NO PUBLICO — solo para .cpp del modulo | API publica `data_table/data_table.h` solo |
---
## Ciclo de vida de un modulo
Crear modulo nuevo (ej. `chat_ia_cpp` tras 0107 cerrar):
1. `mkdir modules/chat_ia/` con:
- `module.md` (frontmatter: name, version, description, members, dir_path).
- `CMakeLists.txt` definiendo `add_library(fn_module_chat_ia STATIC ...)`.
- `chat_ia.cpp` + `chat_ia.h` (entry function).
- `chat_ia_internal.h` (compartido entre sub-cpp del modulo si tiene >1 archivo).
2. Anadir `add_subdirectory` en `cpp/CMakeLists.txt`.
3. Crear seccion en este doc (`docs/MODULES_API.md`) siguiendo el template de `data_table_cpp`.
4. Anadir fila en `modules/README.md`.
5. `fn index` registra el modulo en `registry.db::modules`.
6. Cada bump de version: `/version modules/chat_ia <major|minor|patch> "<reason>"`. Edita `module.md::version` + `## Capability growth log`.
7. Apps consumidoras declaran:
```yaml
uses_modules:
- name: chat_ia_cpp
min_version: "0.1"
```
+ `target_link_libraries(<app> PRIVATE fn_module_chat_ia)`.
8. `fn doctor modules` audita drift uses_modules vs uses_functions (0107a).
---
## E2E testing por modulo
Cada modulo declara su test en `modules/<name>/e2e_run.sh` (o equivalente). Ver issue 0108 para el testbed agresivo de `data_table_cpp` que combina:
- Catch2 unit (`cpp/tests/`) para sub-funciones puras.
- Golden PNG diff (`primitives_gallery --capture`).
- Auto-driving worker (modelo `altsnap_jitter_test`).
- Dear ImGui Test Engine (`FN_BUILD_TESTS=ON`).
- `--self-test` flag headless.
- Cross-platform run (`e2e_run_cpp_windows.sh`).
Modulos sin testbed dedicado → al menos build smoke + linkage de >=2 apps consumidoras debe verificarse en `e2e_run.sh`.
---
## Mantenimiento de este doc
- Actualizar tras cada bump de version de un modulo (referenciado por `/version`).
- Si una app introduce un patron nuevo no listado en capability matrix → anadirlo aqui ANTES de commit (o la fila queda undocumentada).
- `fn doctor modules` (0107a) verifica que cada modulo en `registry.db::modules` tiene seccion aqui. Si no, WARN.
- Generado parcialmente desde `module.md` via codegen futuro (issue postponed). Hoy mantenido a mano.