# 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 render_fn)` **State:** no requerido (singleton interno via `fn::run_app` lifecycle) **Linkage en apps:** transitivo via macro `add_imgui_app( ...)` — 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( ...)`. 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` | `{}` | 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` | `{}` | 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 `/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 `/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(); // /local_files/ const char* fn::local_path(const char*); // / const char* fn::asset_dir(); // /assets/ const char* fn::asset_path(const char*); // / 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& tables, State& state, std::vector* 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( 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 headers; // names de columna std::vector types; // String/Int/Float/Bool/Date/Json/Auto const char* const* cells; // row-major, cells[r*cols + c] int rows; int cols; std::vector 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 badges; std::vector chips; std::vector 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 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 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 `.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 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 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 ""`. 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( 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//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.