Files
fn_registry/docs/MODULES_API.md
T
egutierrez 7eb7b3d0c8 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>
2026-05-18 18:17:08 +02:00

29 KiB

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 — shell ImGui obligatorio
  2. data_table_cpp — tabla TQL completa
  3. Capability matrix consolidada
  4. Ciclo de vida de un modulo
  5. Como NO conectarse a un modulo (anti-patrones)

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

#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

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:
    uses_modules:
      - name: data_table_cpp
        min_version: "1.4"   # o "2.0" post 0107c
    
  2. CMakeLists.txt:
    target_link_libraries(<app> PRIVATE fn_module_data_table)
    
  3. Header en el .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

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.

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

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.

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

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.

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

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

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.

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:

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.

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

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.

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.

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.

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:

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

data_table::render("##compact", {tbl}, st, &events, /*show_chrome=*/false);

Capacidad 21: Module version metadata (post 0107c)

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.

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:

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)

// 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:

    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.