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>
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
- framework_cpp — shell ImGui obligatorio
- data_table_cpp — tabla TQL completa
- Capability matrix consolidada
- Ciclo de vida de un modulo
- 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.icoexiste). - Layouts persistentes (save/load/list/delete/reset) con restore-on-open + save-on-close.
- About / Settings / Logs windows + menubar.
imgui.ini+app_settings.inibajo<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=alwaysen systemd unit (NOon-failure) —SIGTERMlimpio = exit success.- App write-paths SIEMPRE via
fn::local_path("X"). NOfopen("X.db", ...)con path relativo. init_gl_loader = trueSOLO 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
app.mdfrontmatter:uses_modules: - name: data_table_cpp min_version: "1.4" # o "2.0" post 0107cCMakeLists.txt:target_link_libraries(<app> PRIVATE fn_module_data_table)- Header en el
.cpp:#include "data_table/data_table.h" #include "core/data_table_types.h" - Reservar
data_table::Statepersistente 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.
labelvacio = 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
chipspero configurado en runtime por usuario). - NumericRange: gradient sobre rango numerico (igual que
ColorScalepero 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.
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:
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
Statedebe persistir entre frames:staticper panel. Si la pones en stack, pierdes filter chips/sort/etc cada frame. Cada panel necesita SUState(no compartir entre tablas distintas).cellsrow-major:cells[row*cols + col]. Apps que pasan column-major rompen sin warning.TableInput.nameno vacio si hay joins: si declaras varias tablas, cada una connameunico para que el join UI las identifique.column_specs.size() == cols: si no resizeas, el modulo asumeCellRenderer::Textpara columnas faltantes.ColumnSpec.iddebe ser estable entre frames. Si cambia, las preferencias de sort/filter del usuario sobre esa col se pierden.button_actiondebe ser unico dentro del scope semantic de la app. Apps mismas que reusan"retry"en multiples columnas/contextos deberian diferenciar ("retry_run","retry_test").badgesvschips: Badge = fondo coloreado completo + label; CategoricalChip = solo dot + texto plano. Eligir segun densidad visual deseada.Dotsreusabadgesmap pero IGNORABadgeRule.label. Solocolor_hexpor token.ColorScalerequiere ColumnType numerico (Float,Int,Date). String no calcula gradient.tooltip = "auto"= string magico literal. NOtooltip.empty().- events_out se LIMPIA cada frame — la app debe leerlo INMEDIATAMENTE tras
render(). No acumular entre frames. events.rowNO indexa al output del stage — siempre apunta aTableInput.cellsoriginal. Si tu app hizo TQL pipeline, mapea manualmente viastate.row_mappingsi necesitas el row del output.- Multi-instancia: cada
data_table::renderdebe usaridunico ("##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):
-
mkdir modules/chat_ia/con:module.md(frontmatter: name, version, description, members, dir_path).CMakeLists.txtdefiniendoadd_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).
-
Anadir
add_subdirectoryencpp/CMakeLists.txt. -
Crear seccion en este doc (
docs/MODULES_API.md) siguiendo el template dedata_table_cpp. -
Anadir fila en
modules/README.md. -
fn indexregistra el modulo enregistry.db::modules. -
Cada bump de version:
/version modules/chat_ia <major|minor|patch> "<reason>". Editamodule.md::version+## Capability growth log. -
Apps consumidoras declaran:
uses_modules: - name: chat_ia_cpp min_version: "0.1"target_link_libraries(<app> PRIVATE fn_module_chat_ia).
-
fn doctor modulesaudita 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-testflag 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 enregistry.db::modulestiene seccion aqui. Si no, WARN.- Generado parcialmente desde
module.mdvia codegen futuro (issue postponed). Hoy mantenido a mano.