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
+563
View File
@@ -0,0 +1,563 @@
# data_table integration audit — 8 apps
Canonical pattern (from modules/data_table/data_table.md):
```cpp
static data_table::State g_table_state; // PERSISTENT entre frames
data_table::TableInput tbl;
tbl.name = "my_table";
tbl.headers = {"col1","col2"};
tbl.types = {data_table::ColumnType::String, data_table::ColumnType::Int};
tbl.cells = cells_ptr; // row-major flat array
tbl.rows = N;
tbl.cols = 2;
std::vector<data_table::TableEvent> events;
ImGui::BeginChild("##tbl_host");
data_table::render("##my_tbl_id", { tbl }, g_table_state, &events);
ImGui::EndChild();
for (const auto& ev : events) {
if (ev.kind == data_table::TableEventKind::RowDoubleClick) { ... }
}
```
Anti-patterns the audit flags:
- inline_begintable [warn]: app calls ImGui::BeginTable directly instead of data_table::render. Migrar la tabla a data_table::render con un TableInput.
- state_not_persistent [error]: data_table::State declarado dentro de funcion sin `static`. Mover a `static` local o miembro global; sino se pierde drill/sort/filtros cada frame.
- no_child_host [warn]: data_table::render se llama sin BeginChild/Begin en las ~30 lineas previas. Envolver con `ImGui::BeginChild("##host");``ImGui::EndChild();`.
- no_event_sink [info]: app declara uses_modules data_table_cpp pero no captura events_out. Pasar `&events` para reaccionar a double-click / right-click / ButtonClick.
- cmake_missing_link [error]: app.md declara `uses_modules: [data_table_cpp]` pero CMakeLists.txt no enlaza `fn_module_data_table`.
---
## app_gestion [warn] (apps/app_gestion)
### inline_begintable [warn] apps/app_gestion/main.cpp:722
snippet: if (ImGui::BeginTable("##linked_tbl", 4,
```
717 ? "?" : a->linked_build.c_str());
718 if (a->linked_modules.empty()) {
719 ImGui::TextDisabled("(sin info — la app no se ha buildeado todavia,"
720 " o no genera _modules_generated.cpp)");
721 } else {
>> 722 if (ImGui::BeginTable("##linked_tbl", 4,
723 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
724 ImGui::TableSetupColumn("module");
725 ImGui::TableSetupColumn("linked");
726 ImGui::TableSetupColumn("registry");
727 ImGui::TableSetupColumn("status");
```
## dag_engine_ui [warn] (apps/dag_engine_ui)
### inline_begintable [warn] apps/dag_engine_ui/tabs.cpp:382
snippet: if (ImGui::BeginTable("##dt_run_steps", 6, steps_flags)) {
```
377 ImGui::BeginChild("##run_steps_wrap", ImVec2(-1, ImGui::GetContentRegionAvail().y * 0.5f));
378 const ImGuiTableFlags steps_flags =
379 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
380 ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp |
381 ImGuiTableFlags_ScrollY;
>> 382 if (ImGui::BeginTable("##dt_run_steps", 6, steps_flags)) {
383 ImGui::TableSetupScrollFreeze(0, 1);
384 ImGui::TableSetupColumn("Step", ImGuiTableColumnFlags_WidthStretch, 1.6f);
385 ImGui::TableSetupColumn("Function", ImGuiTableColumnFlags_WidthStretch, 2.2f);
386 ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 0.8f);
387 ImGui::TableSetupColumn("Exit", ImGuiTableColumnFlags_WidthStretch, 0.4f);
```
### inline_begintable [warn] apps/dag_engine_ui/tabs.cpp:731
snippet: if (ImGui::BeginTable("##health_kpis", 4,
```
726 "Trigger a DAG to populate health metrics.");
727 ImGui::End();
728 return;
729 }
730
>> 731 if (ImGui::BeginTable("##health_kpis", 4,
732 ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchSame))
733 {
734 ImGui::TableNextRow();
735
736 ImGui::TableNextColumn();
```
## data_factory [warn] (apps/data_factory)
### no_child_host [warn] apps/data_factory/tabs.cpp:291
snippet: data_table::render(dt_id, {tbl}, *st, &events);
```
286 }
287 cells_to_ptrs(*backing, *ptrs);
288 tbl.cells = ptrs->data();
289
290 std::vector<data_table::TableEvent> events;
>> 291 data_table::render(dt_id, {tbl}, *st, &events);
292
293 for (auto& ev : events) {
294 if (ev.kind == data_table::TableEventKind::RowDoubleClick &&
295 ev.row >= 0 && ev.row < static_cast<int>(filtered.size()))
296 {
```
### no_child_host [warn] apps/data_factory/tabs.cpp:454
snippet: data_table::render("##dt_tables", {tbl}, g_st_tables, &tbl_events);
```
449 }
450 cells_to_ptrs(g_back_tables, g_ptrs_tables);
451 tbl.cells = g_ptrs_tables.data();
452
453 std::vector<data_table::TableEvent> tbl_events;
>> 454 data_table::render("##dt_tables", {tbl}, g_st_tables, &tbl_events);
455 for (auto& ev : tbl_events) {
456 if (ev.kind == data_table::TableEventKind::RowDoubleClick &&
457 ev.row >= 0 && ev.row < static_cast<int>(tables.size()))
458 {
459 const auto& t = tables[ev.row];
```
### no_child_host [warn] apps/data_factory/tabs.cpp:531
snippet: data_table::render("##dt_databases", {tbl}, g_st_databases);
```
526 g_back_databases.push_back(d.last_seen_at.empty() ? "-" : d.last_seen_at);
527 }
528 cells_to_ptrs(g_back_databases, g_ptrs_databases);
529 tbl.cells = g_ptrs_databases.data();
530
>> 531 data_table::render("##dt_databases", {tbl}, g_st_databases);
532 }
533 ImGui::End();
534 }
535
536 // ---------------------------------------------------------------------------
```
### no_child_host [warn] apps/data_factory/tabs.cpp:619
snippet: data_table::render("##dt_kpis", {tbl}, g_st_kpis);
```
614 std::snprintf(buf, sizeof(buf), "%lld KB", kb_24h); g_back_kpis.push_back(buf);
615
616 cells_to_ptrs(g_back_kpis, g_ptrs_kpis);
617 tbl.cells = g_ptrs_kpis.data();
618
>> 619 data_table::render("##dt_kpis", {tbl}, g_st_kpis);
620 }
621
622 ImGui::Separator();
623 ImGui::TextDisabled("Computed client-side from %zu runs in cache.", runs_all.size());
624 ImGui::End();
```
### no_child_host [warn] apps/data_factory/tabs.cpp:883
snippet: data_table::render("##dt_node_runs", {tbl}, g_st_node_runs, &events);
```
878 }
879 cells_to_ptrs(g_back_node_runs, g_ptrs_node_runs);
880 tbl.cells = g_ptrs_node_runs.data();
881
882 std::vector<data_table::TableEvent> events;
>> 883 data_table::render("##dt_node_runs", {tbl}, g_st_node_runs, &events);
884
885 for (auto& ev : events) {
886 if (ev.kind == data_table::TableEventKind::RowDoubleClick &&
887 ev.row >= 0 && ev.row < static_cast<int>(shown_runs.size()))
888 {
```
### no_child_host [warn] apps/data_factory/tabs.cpp:992
snippet: data_table::render("##dt_preview", {tbl}, g_st_preview, nullptr, true);
```
987 }
988 }
989 cells_to_ptrs(g_back_preview, g_ptrs_preview);
990 tbl.cells = g_ptrs_preview.data();
991
>> 992 data_table::render("##dt_preview", {tbl}, g_st_preview, nullptr, true);
993 }
994
995 // Pagination controls.
996 ImGui::Separator();
997 {
```
## graph_explorer [warn] (projects/osint_graph/apps/graph_explorer)
### inline_begintable [warn] projects/osint_graph/apps/graph_explorer/extract_panel.cpp:981
snippet: if (ImGui::BeginTable("##ents", 5,
```
976 }
977
978 // Tabla de entidades.
979 if (!res->entities.empty() &&
980 ImGui::CollapsingHeader("Entities", ImGuiTreeNodeFlags_DefaultOpen)) {
>> 981 if (ImGui::BeginTable("##ents", 5,
982 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
983 ImGuiTableFlags_ScrollY,
984 ImVec2(0.0f, 200.0f))) {
985 ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 28.0f);
986 ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 120.0f);
```
### inline_begintable [warn] projects/osint_graph/apps/graph_explorer/extract_panel.cpp:1027
snippet: if (ImGui::BeginTable("##rels", 5,
```
1022 }
1023
1024 // Tabla de relaciones.
1025 if (!res->relations.empty() &&
1026 ImGui::CollapsingHeader("Relations", ImGuiTreeNodeFlags_DefaultOpen)) {
>> 1027 if (ImGui::BeginTable("##rels", 5,
1028 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
1029 ImGuiTableFlags_ScrollY,
1030 ImVec2(0.0f, 160.0f))) {
1031 ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 28.0f);
1032 ImGui::TableSetupColumn("From", ImGuiTableColumnFlags_WidthFixed, 100.0f);
```
### inline_begintable [warn] projects/osint_graph/apps/graph_explorer/main.cpp:1127
snippet: if (ImGui::BeginTable("##enr_params", 2,
```
1122 // con mas params de los que llenamos al abrir la ventana.
1123 if (g_app.enr_modal_param_bufs.size() < spec->params.size()) {
1124 g_app.enr_modal_param_bufs.resize(spec->params.size());
1125 }
1126
>> 1127 if (ImGui::BeginTable("##enr_params", 2,
1128 ImGuiTableFlags_SizingStretchProp |
1129 ImGuiTableFlags_NoBordersInBody)) {
1130 ImGui::TableSetupColumn("name", ImGuiTableColumnFlags_WidthFixed, 110.0f);
1131 ImGui::TableSetupColumn("value", ImGuiTableColumnFlags_WidthStretch);
1132
```
### inline_begintable [warn] projects/osint_graph/apps/graph_explorer/views.cpp:885
snippet: if (ImGui::BeginTable("##insp_id", 2,
```
880 // Layout label-izquierda / input-derecha via 2-col table. El label
881 // alineado al frame del input y el input estirado al ancho restante.
882 ImGui::TextUnformatted("Identity");
883 ImGui::Separator();
884
>> 885 if (ImGui::BeginTable("##insp_id", 2,
886 ImGuiTableFlags_SizingStretchProp |
887 ImGuiTableFlags_NoBordersInBody)) {
888 ImGui::TableSetupColumn("k", ImGuiTableColumnFlags_WidthFixed, 90.0f);
889 ImGui::TableSetupColumn("v", ImGuiTableColumnFlags_WidthStretch);
890
```
### inline_begintable [warn] projects/osint_graph/apps/graph_explorer/views.cpp:958
snippet: if (ImGui::BeginTable("##insp_fields", 2,
```
953 ImGui::TextUnformatted("Fields");
954 ImGui::Separator();
955 const EntitySpec* spec = find_entity_spec(app.parsed_types,
956 app.insp_type_buf);
957
>> 958 if (ImGui::BeginTable("##insp_fields", 2,
959 ImGuiTableFlags_SizingStretchProp |
960 ImGuiTableFlags_NoBordersInBody)) {
961 ImGui::TableSetupColumn("k", ImGuiTableColumnFlags_WidthFixed, 90.0f);
962 ImGui::TableSetupColumn("v", ImGuiTableColumnFlags_WidthStretch);
963
```
### inline_begintable [warn] projects/osint_graph/apps/graph_explorer/views.cpp:1546
snippet: // OLD: ImGui::BeginTable("##tablev", 6, ...) with manual sort/filter/clipper.
```
1541 }
1542
1543 // ----------------------------------------------------------------------------
1544 // Table view (issue 0004) — migrated to data_table::render (issue 0081-J).
1545 //
>> 1546 // OLD: ImGui::BeginTable("##tablev", 6, ...) with manual sort/filter/clipper.
1547 // Per-column filter popups + chips toolbar + TabBar per type.
1548 // Click on row selected node in graph viewport.
1549 //
1550 // NEW: data_table::render() provides sort + filter + viz + stages.
1551 // AppState::table_dt_state persists the UI state between frames.
```
### inline_begintable [warn] projects/osint_graph/apps/graph_explorer/views.cpp:1854
snippet: if (col_count > 0 && ImGui::BeginTable("##te_rows", col_count, tflags,
```
1849 : (int)m.columns.size() + 2;
1850 ImGuiTableFlags tflags =
1851 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
1852 ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable |
1853 ImGuiTableFlags_SizingStretchProp;
>> 1854 if (col_count > 0 && ImGui::BeginTable("##te_rows", col_count, tflags,
1855 ImVec2(0, -ImGui::GetFrameHeightWithSpacing()))) {
1856 ImGui::TableSetupScrollFreeze(0, 1);
1857 if (is_group) {
1858 for (size_t i = 0; i < m.columns.size(); ++i) {
1859 bool is_id = (i == 0);
```
### inline_begintable [warn] projects/osint_graph/apps/graph_explorer/views.cpp:2292
snippet: if (ImGui::BeginTable("##te_fields", 5,
```
2287 // Fields table
2288 ImGui::Spacing();
2289 ImGui::TextUnformatted("Fields");
2290 ImGui::Separator();
2291
>> 2292 if (ImGui::BeginTable("##te_fields", 5,
2293 ImGuiTableFlags_BordersInnerV
2294 | ImGuiTableFlags_RowBg
2295 | ImGuiTableFlags_SizingStretchProp)) {
2296 ImGui::TableSetupColumn("name");
2297 ImGui::TableSetupColumn("type", ImGuiTableColumnFlags_WidthFixed, 90.0f);
```
### no_child_host [warn] projects/osint_graph/apps/graph_explorer/views.cpp:1632
snippet: data_table::render("##tablev_dt", {tbl}, app.table_dt_state);
```
1627 tbl.rows = s_rows_cached;
1628 tbl.cols = k_ncols;
1629
1630 // Render con chrome completo (barra de chips + breadcrumb).
1631 // app.table_dt_state persiste entre frames.
>> 1632 data_table::render("##tablev_dt", {tbl}, app.table_dt_state);
1633
1634 ImGui::End();
1635 }
1636
1637 // ----------------------------------------------------------------------------
```
## navegator_dashboard [warn] (projects/navegator/apps/navegator_dashboard)
### inline_begintable [warn] projects/navegator/apps/navegator_dashboard/autoextract_panel.cpp:528
snippet: if (ImGui::BeginTable("##ax_schema", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
```
523 {
524 std::lock_guard<std::mutex> lk(g_ax.mu);
525 sc_copy = g_ax.schema;
526 }
527
>> 528 if (ImGui::BeginTable("##ax_schema", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
529 ImGui::TableSetupColumn("field");
530 ImGui::TableSetupColumn("selector");
531 ImGui::TableSetupColumn("sample");
532 ImGui::TableSetupColumn("type");
533 ImGui::TableSetupColumn("keep");
```
### no_child_host [warn] projects/navegator/apps/navegator_dashboard/panels.cpp:234
snippet: data_table::render("##dt_browsers", {tbl}, g_browsers.dt_state, &dt_events, /*show_chrome=*/false);
```
229 data_table::BadgeRule{"visible", "#22c55e", "visible"},
230 };
231 }
232
233 std::vector<data_table::TableEvent> dt_events;
>> 234 data_table::render("##dt_browsers", {tbl}, g_browsers.dt_state, &dt_events, /*show_chrome=*/false);
235 for (auto& ev : dt_events) {
236 if (ev.kind == data_table::TableEventKind::RowDoubleClick &&
237 ev.row >= 0 && ev.row < static_cast<int>(g_browsers.instances.size())) {
238 g_session().select_browser(g_browsers.instances[ev.row].port);
239 }
```
### no_child_host [warn] projects/navegator/apps/navegator_dashboard/panels.cpp:456
snippet: data_table::render("##dt_tabs", {tbl}, g_tabs_ui.dt_state, &dt_events, /*show_chrome=*/false);
```
451 data_table::BadgeRule{"no", "#6b7280", "no"},
452 };
453 }
454
455 std::vector<data_table::TableEvent> dt_events;
>> 456 data_table::render("##dt_tabs", {tbl}, g_tabs_ui.dt_state, &dt_events, /*show_chrome=*/false);
457 for (auto& ev : dt_events) {
458 if (ev.kind == data_table::TableEventKind::RowDoubleClick &&
459 ev.row >= 0 && ev.row < static_cast<int>(visible_tabs.size())) {
460 const CdpTab* tp = visible_tabs[ev.row];
461 if (tp && !tp->ws_url.empty()) {
```
### no_child_host [warn] projects/navegator/apps/navegator_dashboard/panels.cpp:1016
snippet: data_table::render("##dt_wsframes", {ws_tbl}, g_dt_wsframes, false);
```
1011 data_table::BadgeRule{"ping", "#6b7280", "ping"},
1012 data_table::BadgeRule{"pong", "#6b7280", "pong"},
1013 };
1014 }
1015
>> 1016 data_table::render("##dt_wsframes", {ws_tbl}, g_dt_wsframes, false);
1017 ImGui::EndTabItem();
1018 }
1019 ImGui::EndTabBar();
1020 }
1021 }
```
### no_child_host [warn] projects/navegator/apps/navegator_dashboard/panels.cpp:1322
snippet: data_table::render("##dt_requests", {req_tbl}, g_net_ui.dt_state, &dt_events, /*show_chrome=*/true);
```
1317 cs.duration_warn_ms = 1000.0f;
1318 cs.duration_error_ms = 5000.0f;
1319 }
1320
1321 std::vector<data_table::TableEvent> dt_events;
>> 1322 data_table::render("##dt_requests", {req_tbl}, g_net_ui.dt_state, &dt_events, /*show_chrome=*/true);
1323 for (auto& ev : dt_events) {
1324 if (ev.kind == data_table::TableEventKind::RowDoubleClick &&
1325 ev.row >= 0 && ev.row < static_cast<int>(filtered.size())) {
1326 g_net_ui.selected_id = filtered[ev.row]->id;
1327 g_net_ui.selected_index = ev.row;
```
### inline_begintable [warn] projects/navegator/apps/navegator_dashboard/recipes_panel.cpp:238
snippet: } else if (ImGui::BeginTable("##recipes_tbl", 6,
```
233 }
234
235 if (rows_copy.empty()) {
236 ImGui::TextDisabled("No recipes in projects/navegator/profiles/default/recipes/.");
237 ImGui::TextDisabled("Use AutoExtract panel to create one.");
>> 238 } else if (ImGui::BeginTable("##recipes_tbl", 6,
239 ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
240 ImGui::TableSetupColumn("name");
241 ImGui::TableSetupColumn("url_pattern");
242 ImGui::TableSetupColumn("last_status");
243 ImGui::TableSetupColumn("last_at");
```
## odr_console [ok] (projects/online_data_recopilation/apps/odr_console)
### no_event_sink [info] projects/online_data_recopilation/apps/odr_console:0
snippet: no TableEvent / events_out found in any source file
## registry_dashboard [warn] (projects/fn_monitoring/apps/registry_dashboard)
### inline_begintable [warn] projects/fn_monitoring/apps/registry_dashboard/views.cpp:380
snippet: if (ImGui::BeginTable("##kpi_grid", 4, flags)) {
```
375 // del registry). Si no hay datos cargados, queda vacio y el card mostrara
376 // solo valor + delta placeholder.
377 const float* spark_data = data.date_values.empty() ? nullptr : data.date_values.data();
378 const int spark_count = static_cast<int>(data.date_values.size());
379
>> 380 if (ImGui::BeginTable("##kpi_grid", 4, flags)) {
381 struct KPI { const char* label; float value; const char* fmt; const char* icon; };
382 const KPI cards[8] = {
383 {"Functions", static_cast<float>(stats.total_functions), "%.0f", TI_FUNCTION},
384 {"Types", static_cast<float>(stats.total_types), "%.0f", TI_HEXAGON},
385 {"Apps", static_cast<float>(stats.total_apps), "%.0f", TI_APPS},
```
### inline_begintable [warn] projects/fn_monitoring/apps/registry_dashboard/views.cpp:436
snippet: if (ImGui::BeginTable("##chart_grid", 4, flags)) {
```
431 void draw_charts(RegistryData& data, float height) {
432 const ImGuiTableFlags flags = ImGuiTableFlags_SizingStretchSame
433 | ImGuiTableFlags_NoPadOuterX;
434 const float plot_h = height - 48.0f;
435
>> 436 if (ImGui::BeginTable("##chart_grid", 4, flags)) {
437 ImGui::TableNextRow();
438
439 ImGui::TableSetColumnIndex(0);
440 {
441 ImVec2 sz(ImGui::GetContentRegionAvail().x, height);
```
### inline_begintable [warn] projects/fn_monitoring/apps/registry_dashboard/views.cpp:648
snippet: if (ImGui::BeginTable("##monitor_kpi", 7, flags)) {
```
643
644 // 7 KPI cards: Calls / MCP / Reg% / Errors / Violations / Copies / Versions
645 // "MCP" = calls Claude lanza via tools registry-aware (mcp / fn_cli_run /
646 // heredoc). "Reg %" = porcentaje del total con function_id no vacio.
647 const ImGuiTableFlags flags = ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX;
>> 648 if (ImGui::BeginTable("##monitor_kpi", 7, flags)) {
649 struct KPI { const char* label; float value; const char* icon; const char* fmt; };
650 const KPI cards[7] = {
651 {"Calls", static_cast<float>(cu.total_calls), TI_ACTIVITY, "%.0f"},
652 {"MCP", static_cast<float>(cu.total_mcp), TI_PLUG_CONNECTED, "%.0f"},
653 {"Reg %", static_cast<float>(cu.registry_pct), TI_PERCENTAGE, "%.1f%%"},
```
### inline_begintable [warn] projects/fn_monitoring/apps/registry_dashboard/views.cpp:1110
snippet: if (!ImGui::BeginTable("##proj_layout", 2, flags)) return;
```
1105 }
1106
1107 // Dos columnas: izquierda arbol, derecha detalle.
1108 const ImGuiTableFlags flags = ImGuiTableFlags_Resizable
1109 | ImGuiTableFlags_SizingStretchProp;
>> 1110 if (!ImGui::BeginTable("##proj_layout", 2, flags)) return;
1111
1112 ImGui::TableSetupColumn("tree", ImGuiTableColumnFlags_WidthStretch, 1.0f);
1113 ImGui::TableSetupColumn("detail", ImGuiTableColumnFlags_WidthStretch, 2.5f);
1114 ImGui::TableNextRow();
1115
```
### inline_begintable [warn] projects/fn_monitoring/apps/registry_dashboard/views.cpp:1448
snippet: if (!ImGui::BeginTable("##explorer_layout", 2, flags)) return;
```
1443 return;
1444 }
1445
1446 const ImGuiTableFlags flags = ImGuiTableFlags_Resizable
1447 | ImGuiTableFlags_SizingStretchProp;
>> 1448 if (!ImGui::BeginTable("##explorer_layout", 2, flags)) return;
1449
1450 ImGui::TableSetupColumn("list", ImGuiTableColumnFlags_WidthStretch, 1.0f);
1451 ImGui::TableSetupColumn("detail", ImGuiTableColumnFlags_WidthStretch, 2.4f);
1452 ImGui::TableNextRow();
1453
```
### inline_begintable [warn] projects/fn_monitoring/apps/registry_dashboard/work_tab.cpp:239
snippet: if (ImGui::BeginTable("##flows_work", 8,
```
234 // Flows table
235 ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
236 ImGui::TextUnformatted("Flows");
237 ImGui::PopStyleColor();
238
>> 239 if (ImGui::BeginTable("##flows_work", 8,
240 ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders |
241 ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_Resizable)) {
242 ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 50.0f);
243 ImGui::TableSetupColumn("Name");
244 ImGui::TableSetupColumn("Pattern", ImGuiTableColumnFlags_WidthFixed, 110.0f);
```
### inline_begintable [warn] projects/fn_monitoring/apps/registry_dashboard/work_tab.cpp:272
snippet: if (ImGui::BeginTable("##top_issues_work", 7,
```
267 ImGui::Spacing();
268 ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
269 ImGui::TextUnformatted("Top issues (priority alta, not done)");
270 ImGui::PopStyleColor();
271
>> 272 if (ImGui::BeginTable("##top_issues_work", 7,
273 ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders |
274 ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_Resizable)) {
275 ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 70.0f);
276 ImGui::TableSetupColumn("Title");
277 ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 80.0f);
```
### no_event_sink [info] projects/fn_monitoring/apps/registry_dashboard:0
snippet: no TableEvent / events_out found in any source file
## services_monitor [ok] (apps/services_monitor)
### no_event_sink [info] apps/services_monitor:0
snippet: no TableEvent / events_out found in any source file
+7
View File
@@ -1,4 +1,11 @@
{
"modules-v2": {
"enabled": true,
"issue": "0107",
"description": "Sistema de modulos C++ estandarizado: 0/17 apps con drift (fn doctor modules), data_table.cpp partido en 6 sub-funciones (4777->1818 LOC, -62%), members vs uses_functions separados (tiers), docs API publica completa (modules/README.md + docs/MODULES_API.md con 21 capacidades). 0107e (min_version + codegen fail-loud) y 0107g (8 inline_begintable migrations) quedan como follow-ups bajo el mismo flag — son evolutivos, no bloqueantes.",
"added": "2026-05-17",
"enabled_at": "2026-05-17"
},
"dag-engine": {
"enabled": true,
"issue": "0007",
@@ -0,0 +1,157 @@
---
name: kanban-cpp-and-agent-workflows
id: 0008
status: pending
created: 2026-05-18
updated: 2026-05-18
priority: high
risk: medium
related_issues: [0109, 0112, 0113, 0114, 0115, 0116, 0117, 0118, 0119]
apps:
- kanban_cpp
- kanban
- skill_tree
- agent_runner_api
trigger: ui-button
schedule: ""
expected_runtime_s: 0
tags: [agents, workflows, dod, worktrees, cpp, kanban, llm]
pattern: realtime-loop
---
## Goal
Montar el stack para que agentes LLM trabajen issues/cards **en paralelo, en worktrees aislados, con DoD obligatorio y evidencia adjunta**. Tres apps colaboran:
- **kanban_cpp** (nueva, C++ ImGui) — clon visual de `kanban_web` con su propio backend Go (copia + operations.db independiente). Tablero pensado para conducir agentes, no humanos.
- **skill_tree v2** (existente) — reemplaza el boton `Claude fix` (que abria terminal `wt.exe`) por `Launch workflow` que dispara un run remoto. Anade panel DoD evidence + timeline cross-app.
- **agent_runner_api** (nueva, service Go) — daemon que persiste workflows, crea worktrees, lanza `claude --headless`, valida DoD, expone HTTP + SSE. Source of truth de `agent_runs.db`.
El bucle final: humano clica card en kanban_cpp (o issue en skill_tree) -> agent_runner_api crea worktree `auto/<id>-<slug>` -> spawn agente -> agente trabaja + adjunta evidencia (screenshot, log, url) por cada item DoD -> humano valida en el panel DoD -> merge TBD a master.
## Pre-requisitos
- Backend Go de `apps/kanban/` funcional (HTTP API + WS + auth + migrations). YA.
- `claude --dangerously-skip-permissions` disponible en PATH del runner (host WSL).
- `parallel-fix-issues` skill como base para worktree spawning. YA.
- Frontmatter canonico de issues/flows (issue 0100). YA.
- Capability group `agents` definido (o crear).
## Arquitectura
```
┌─────────────────┐ ┌─────────────────┐
│ kanban_cpp │ │ skill_tree v2 │
│ (C++ ImGui) │ │ (C++ ImGui) │
│ Board/Cal/Dash │ │ Tree + DoD pan │
│ Agent runs │ │ Timeline pan │
│ Worktree mgr │ │ DoD inspector │
│ DoD inspector │ │ │
└────────┬────────┘ └────────┬────────┘
│ HTTP+SSE │ HTTP+SSE
├──────────────┬──────────┘
│ │
▼ ▼
┌───────────────┐ ┌───────────────────────┐
│ kanban_cpp │ │ agent_runner_api │
│ backend Go │ │ (service Go, :8486) │
│ (copia, :8401)│ │ ┌───────────────────┐ │
│ operations.db │ │ │ agent_runs.db │ │
└───────────────┘ │ │ - workflows │ │
│ │ - runs │ │
│ │ - worktrees │ │
│ │ - dod_items │ │
│ │ - dod_evidence │ │
│ └───────────────────┘ │
│ spawn claude headless │
│ git worktree add/rm │
│ TBD merge --no-ff │
└───────────────────────┘
```
Decisiones cerradas con el usuario:
- kanban_cpp y kanban_web tienen backends Go **independientes** (copia identica, datos separados). NO sync.
- `agent_runs.db` vive en service nuevo `agent_runner_api`. Port dedicado (default :8486).
- Workflows = `git worktree` paralelos. Trunk-based development obligatorio (rama `auto/<issue>` -> PR -> merge master).
- DoD: declarado en frontmatter (`dod_user:` + nuevo `dod_evidence_schema:`), persistido en BD con tabla `dod_evidence` (un row por evidencia adjunta).
## Flow
### Trigger 1: humano dispara workflow desde UI
1. En `skill_tree` clica issue -> boton `Launch workflow` (sustituye `Claude fix`).
- POST `http://localhost:8486/api/runs` con `{issue_id, mode: "fix-issue", dod_items: [...]}`.
2. O en `kanban_cpp` arrastra card a columna `Doing (agent)` -> mismo endpoint con `{card_id, kanban_app: "kanban_cpp"}`.
3. `agent_runner_api`:
- Crea row en `agent_runs` (status=`pending`).
- `git worktree add ../wt-<run_id> -b auto/<issue>-<slug>`.
- Lanza subprocess `claude --dangerously-skip-permissions -p "<prompt con DoD items + worktree path>"` en background.
- Devuelve `run_id` + URL SSE para stream de progreso.
### Trigger 2: agente trabaja en worktree
1. Agente lee DoD items del prompt + `dev/issues/<id>.md`.
2. Por cada item DoD: implementa -> ejecuta -> captura evidencia:
- Tipo `screenshot`: `wt.exe ...` o `import` -> path PNG en `agent_runs/<run_id>/evidence/<item_id>.png`.
- Tipo `log`: `tee` output a `agent_runs/<run_id>/evidence/<item_id>.log`.
- Tipo `url`: ruta de service deployado (ej. `http://localhost:8401/board`).
- Tipo `cmd`: comando + stdout esperado, ej. `fn doctor cpp-apps -> 0 errors`.
3. POST `http://localhost:8486/api/runs/<run_id>/evidence` por cada item -> persiste en `dod_evidence`.
4. SSE empuja `progress` a UIs conectadas (kanban_cpp + skill_tree refrescan timeline).
### Trigger 3: humano valida DoD
1. Panel DoD inspector (kanban_cpp + skill_tree): muestra checklist DoD + evidencia por item.
2. Humano aprueba item por item (`POST /api/runs/<run_id>/evidence/<id>/validate`).
3. Cuando todos validados:
- `agent_runner_api` ejecuta `cd <main_repo> && git merge --no-ff auto/<issue>` (o abre PR Gitea si configurado).
- Limpia worktree.
- Status -> `done`.
### Trigger 4: rollback / abort
1. Humano clica `Abort` en cualquier UI.
2. `agent_runner_api`: kill subprocess + `git worktree remove --force ../wt-<run_id>` + `git branch -D auto/<issue>`.
3. Run status -> `aborted`.
## Sub-issues que abre
| # | Titulo | Tipo | Notas |
|---|--------|------|------|
| 0112 | kanban_cpp app + backend copia | app | Scaffolder `/new-cpp-app kanban_cpp` + `cp -r apps/kanban/backend apps/kanban_cpp/backend` + 6 panels (Board/Cal/Dash/Runs/Worktrees/DoD). Trio app.md (`columns-3` accent `#a855f7`) |
| 0113 | agent_runner_api service + agent_runs.db | app | Service Go :8486, tag `service`, migrations en `apps/agent_runner_api/migrations/` (001 workflows + 002 runs + 003 worktrees + 004 dod_items + 005 dod_evidence) |
| 0114 | DoD evidence schema canonico | feature | Anade `dod_evidence_schema:` al frontmatter de issues/flows. Cada item: `{id, kind: screenshot\|log\|url\|cmd, expected, required: bool}`. Validator en `audit_dod_schema_go_infra` |
| 0115 | Worktree launcher con DoD | feature | Funcion `agent_launch_worktree_go_infra` (impure): `git worktree add + spawn claude + tail stderr`. Reemplaza logica inline de `parallel-fix-issues` |
| 0116 | skill_tree: replace Claude-fix por Launch workflow | feature | Modificar `apps/skill_tree/main.cpp`. Remove `spawn_claude_terminal`. Anade POST a `:8486/api/runs`. Toggle setting `legacy_claude_fix` para fallback temporal |
| 0117 | DoD evidence panel (componente C++ reutilizable) | feature | Nueva funcion `dod_evidence_panel_cpp_viz` en `cpp/functions/viz/`. Render screenshots (stb_image), logs (selectable_text), urls (ShellExecute), cmds (output expected vs actual). Reusable kanban_cpp + skill_tree |
| 0118 | Agent runs timeline panel | feature | Nueva funcion `agent_runs_timeline_cpp_viz`. Lee SSE de agent_runner_api, lista runs ordenados por started_at, color por status. Reusable cross-app |
| 0119 | kanban_cpp sync issues + flows como cards | feature | Backend lee `dev/issues/*.md` + `dev/flows/*.md`, expone 2 boards (Issues + Flows). PATCH frontmatter writes back. Watcher fsnotify. Click `Launch` invoca agent_runner_api |
Issue 0109 (skill_tree roadmap) ya existe -> se actualiza con dependencia hacia 0116/0117/0118.
DoD tecnico (Acceptance: corre + Tecnico: vive solo) se redacta dentro de **cada sub-issue**. Aqui solo vive el DoD user-facing del flow como capacidad.
## Definition of Done (user-facing, 4 surfaces obligatorios)
Flow = capacidad de usuario. DoD tecnico (migrations, e2e_checks, uses_functions, build cross-compile, capability groups) vive en los sub-issues 0110-0116, NO aqui.
- [ ] **Surface 1 (kanban_cpp board)**: usuario abre `kanban_cpp.exe` desde el App Hub, ve board con columnas + cards de su backend independiente. Drag card a columna `Doing (agent)` arranca un agente; ve barra de progreso en panel `Agent runs` sin tocar terminal.
- [ ] **Surface 2 (skill_tree Launch workflow)**: usuario clica `Launch workflow` sobre un nodo issue, ve toast `run_id=...` + nueva entrada en panel `Timeline`. El boton viejo `Claude fix` (terminal externa) ya no aparece (o queda detras de feature flag OFF). Cero `wt.exe` abierto.
- [ ] **Surface 3 (DoD inspector)**: usuario abre cualquier run desde kanban_cpp o skill_tree, ve N items DoD con: estado (pending/done/validated/failed), thumbnail screenshot, snippet log, link clickable url, output cmd vs expected. Boton `Validate` por item; al validarlos todos, el run pasa a `merged` y el worktree desaparece.
- [ ] **Surface 4 (Timeline cross-app)**: panel `Timeline` en skill_tree muestra runs de skill_tree + kanban_cpp + futuras apps en una sola lista. Filtros por status / app / fecha. Click en run abre el detalle con los items DoD y sus evidencias.
## Gotchas
- Backend copia kanban: `operations.db` independiente. Auth tokens (`users` table) NO se replican entre apps por defecto — cada app tiene sus usuarios. Decidir si compartir tabla auth o duplicar (decision: duplicar, evita coupling).
- Worktree + pre-commit hooks: comparten `.git/hooks/` con main. Si un hook llama scripts via path absoluto, ejecuta version main. Mismo gotcha que `autonomous_loop.md` -> aplicar fixes EN el worktree, nunca en main.
- claude headless en WSL: `--dangerously-skip-permissions` necesario; salida a stderr/stdout debe quedar en `agent_runs/<run_id>/agent.log` para auditoria. NO subir log a repo.
- Screenshots desde WSL apuntan a `/mnt/c/...` o nombre Windows -> el panel DoD lee paths con `wslpath` cuando hace falta.
- Trio app.md obligatorio en `kanban_cpp/app.md`: ver `feedback_app_trio_required.md`. Sin description + icon.phosphor + icon.accent, hub queda gris.
- `dod_user:` ya existe en frontmatter (issue 0102) — NO renombrar. Anadimos `dod_evidence_schema:` como bloque hermano que declara la forma de cada evidencia.
- skill_tree mantiene `Claude fix` legacy detras de feature flag `legacy_claude_fix` durante 1-2 semanas para rollback rapido (ver `feature_flags.md`).
## Telemetria esperada
- `agent_runner_api` registra cada run, cada evidence, cada validate en su propia `operations.db` (entities + executions). Cross-cut con `call_monitor` via tag `agent_run`.
- Metrica `Reg %` debe SUBIR: agentes en worktree usan funciones del registry para todo (screenshots, log capture, validators). Inline -> proposal automatica.
- Metrica nueva `agent_runs.dod_pass_rate` = `validated_items / total_dod_items` por run. Threshold inicial 0.8.
+1
View File
@@ -11,6 +11,7 @@ Tabla de casos de uso multi-app. Mantenida por `/flow create` y `/flow done`.
| [0005](0005-osint-person-lookup.md) | osint-person-lookup | manual-deep | navegator_dashboard, odr_console, graph_explorer | pending | medium | 0% | 2026-05-16 |
| [0006](0006-metabase-versioning.md) | metabase-versioning | gitops | auto_metabase, dag_engine | pending | medium | 0% | 2026-05-16 |
| [0007](0007-matrix-telemetry-bot.md) | matrix-telemetry-bot | event-driven | data_factory, dag_engine, call_monitor, agents_and_robots | pending | low | 0% | 2026-05-16 |
| [0008](0008-kanban-cpp-and-agent-workflows.md) | kanban-cpp-and-agent-workflows | realtime-loop | kanban_cpp, kanban, skill_tree, agent_runner_api | pending | medium | 0% | 2026-05-18 |
## Leyenda
@@ -0,0 +1,113 @@
---
id: "0105"
title: "Estandarizar bloque service: en app.md + indexer + fn doctor services-spec"
status: in-progress
type: feature
domain:
- meta
- apps-infra
- deploy
- telemetry
scope: multi-app
priority: alta
depends: []
blocks:
- "0106"
related:
- "0085"
- "0086"
created: 2026-05-17
updated: 2026-05-17
tags: [services, monitoring, frontmatter, indexer, fn-doctor, pc-locations]
---
# 0105 — Estandarizar `service:` en app.md
## Problema
Diagnostico (2026-05-17): `sqlite_api` cayo 20h sin alerta. Causa: nadie monitoriza. Causa-de-causa: no hay forma uniforme de saber "esta app DEBE estar corriendo en este PC con este puerto y este health endpoint".
Hoy:
- 10 apps con `tag: service` en `registry.db`.
- 8/10 con `systemctl active=inactive` segun `fn doctor services` (algunas porque viven solo en remoto, otras porque genuinamente murieron).
- `port` se descubre por `--port` en `ExecStart` de un unit file que puede o no existir local.
- `health_endpoint` solo declarado en `deploy_server/operations.db` para 1 target (registry_api).
- `systemd_unit` se asume = `<name>.service`, no documentado.
- `pc_targets` (en que PCs DEBE correr) no existe en ninguna parte.
Consecuencia: imposible escribir un monitor que reconcilie "esperado vs real" sin hardcodear cada app.
## Decision
Anadir bloque `service:` opcional al frontmatter de `app.md`. Obligatorio para apps con `tag: service`. Indexer parsea y persiste. `fn doctor services-spec` audita.
## Schema del bloque
```yaml
service:
# Endpoints HTTP (opcional — apps stdio/daemon dejan null o omiten)
port: 8484
health_endpoint: /api/health # ruta GET, 200 == sano
health_timeout_s: 3
# Identidad systemd (cuando aplica)
systemd_unit: sqlite_api.service # nombre exacto
systemd_scope: user # user|system|null (docker-compose)
restart_policy: always # always|on-failure|none
# Estrategia de runtime (extiende systemd_scope para casos no-systemd)
runtime: systemd-user # systemd-user|systemd-system|docker-compose|stdio|manual
# Donde DEBE correr — referencia pc_locations.pc_id
pc_targets:
- aurgi-pc
- home-wsl
# Banderas
is_local_only: false # true => no se monitoriza por SSH; siempre via mecanismo local
```
Reglas:
- `port` null si la app no expone HTTP (stdio MCP, daemons sin API).
- `health_endpoint` null si no hay http; monitor cae a check de proceso (systemd active + port listening).
- `pc_targets` LISTA de `pc_id` de `pc_locations`. Vacia => no se monitoriza.
- `runtime: docker-compose` => monitor chequea contenedores via `docker compose ps` por SSH al PC target.
- `is_local_only: true` => monitor solo se ejecuta en el PC donde corre el daemon (no se intenta SSH al propio host).
## Tareas
- [x] Auditar 10 services existentes (port real, unit name, descripcion)
- [ ] Editar 10 app.md con bloque `service:` realista
- [ ] Migration: anadir columnas a tabla `apps` (`port INTEGER`, `health_endpoint TEXT`, `health_timeout_s INTEGER`, `systemd_unit TEXT`, `systemd_scope TEXT`, `restart_policy TEXT`, `runtime TEXT`, `is_local_only INTEGER`)
- [ ] Migration: nueva tabla `service_targets (app_id TEXT, pc_id TEXT, role TEXT DEFAULT 'primary', PRIMARY KEY(app_id, pc_id))`
- [ ] Indexer: parsear bloque `service:` desde frontmatter y rellenar columnas + `service_targets`
- [ ] `fn doctor services-spec` (Go func + subcommand): lista apps con `tag: service` y bloque incompleto. Salida tabwriter + `--json`
- [ ] Test: `fn index` sobre fixture con bloque service produce filas correctas
- [ ] Fix retroactivo: `~/.config/systemd/user/sqlite_api.service` con `Restart=always` (no `on-failure` — TERM no es failure)
## Materia: 10 apps actuales
| App | dir | port | health | unit | scope | pc_targets | runtime |
|---|---|---|---|---|---|---|---|
| sqlite_api | projects/fn_monitoring/apps/sqlite_api | 8484 | /api/status | sqlite_api.service | user | aurgi-pc, home-wsl | systemd-user |
| dag_engine | apps/dag_engine | 8090 | /api/dags | dag_engine.service | user | aurgi-pc, home-wsl | systemd-user |
| call_monitor | projects/fn_monitoring/apps/call_monitor | null | null | call_monitor.service | user | aurgi-pc, home-wsl | systemd-user |
| kanban | apps/kanban | 8095 | /api/board | kanban.service | user | aurgi-pc | systemd-user |
| deploy_server | apps/deploy_server | 9090 | /api/health | deploy_server.service | user | aurgi-pc | systemd-user |
| registry_mcp | apps/registry_mcp | null | null | registry_mcp.service | user | aurgi-pc | stdio (manual) |
| registry_api | apps/registry_api | 8420 | /api/status | null | null | organic-machine.com | docker-compose |
| footprint_geo_stack | apps/footprint_geo_stack | 3000 | null | null | null | aurgi-pc | docker-compose |
| element_matrix_chat | projects/element_agents/apps/element_matrix_chat | null | null | null | null | organic-machine.com | docker-compose |
| agents_and_robots | projects/element_agents/apps/agents_and_robots | null | null | agents_and_robots.service | system | organic-machine.com | systemd-remote |
## DoD
- 10 app.md con bloque `service:` valido (parseable, valores reales).
- `fn index` puebla `apps.port/...` y `service_targets`.
- `fn doctor services-spec` reporta `OK` para los 10.
- Migration aplica idempotente en `registry.db` de aurgi-pc + home-wsl.
- `services_status_go_infra` extendida para leer datos del nuevo schema (no hardcoded port discovery).
## Bloquea
- 0106: app `services_monitor` (UI + backend `services_api`). Necesita `service_targets` + `apps.port`/`health_endpoint` poblados.
+92
View File
@@ -0,0 +1,92 @@
---
id: "0106"
title: "App services_monitor: dashboard cross-PC de services activos"
status: pendiente
type: app
domain:
- apps-infra
- cpp-stack
- telemetry
- deploy
scope: multi-app
priority: alta
depends:
- "0105"
blocks: []
related:
- "0085"
created: 2026-05-17
updated: 2026-05-17
tags: [services, monitoring, cross-pc, ssh, systemd, healthcheck, dashboard]
---
# 0106 — App `services_monitor`
## Problema
`fn doctor services` da snapshot puntual del PC local. Falta vista en vivo cross-PC:
- ¿Cuales de mis 10 services estan vivos en aurgi-pc?
- ¿Cuales en organic-machine.com?
- ¿Cuales murieron sin que me entere (caso sqlite_api 2026-05-17)?
## Decision
App ImGui `services_monitor` consumiendo backend Go `services_api` (port 8485). Reconcilia esperado (`service_targets` + `apps.*` del registry) vs real (systemd state + port listening + HTTP health) en cada PC target. Persistencia historica = transiciones + agregado horario.
## Componentes
### Backend `apps/services_api/` (Go, tag: service, port 8485)
Endpoints:
- `GET /api/services` lista plana `(app_id, pc_id, expected, actual, port, last_check_ts, last_healthy_ts, transitions_24h)`
- `GET /api/services/:app/:pc` detalle + ultimas N transiciones + journalctl tail
- `POST /api/services/:app/:pc/check` fuerza check inmediato
- `POST /api/services/:app/:pc/action` (action=start|stop|restart) feature-flag OFF en v1
- `GET /api/pcs` estado por PC (reachable, lag_ms, version_uname)
- `GET /api/ws/services` WS push de delta cada check
Worker pool: ciclo 10s por PC, paralelo.
Checker local (is_local_only=true o PC = self): exec `systemctl --user is-active <unit>` + `ss -tln | grep :<port>` + `curl -m <timeout> <health_endpoint>`.
Checker remoto: `ssh_exec_go_infra` con los mismos comandos + parseo de output.
BD: `services_api.db`:
- `service_check` append-only (ts, app_id, pc_id, systemd_state, port_listening, http_status, latency_ms)
- `service_transition` (ts, app_id, pc_id, from, to)
- `service_state_hourly` (hour_bucket, app_id, pc_id, healthy_ratio, transitions)
### Frontend `apps/services_monitor/` (C++ ImGui)
Patron `data_factory`. Paneles:
1. **Overview** Grid `pcs x apps`. Celda = semaforo. Click => Detail.
2. **PC Detail** apps esperadas en el PC, drift expected vs actual, accion restart (disabled v1).
3. **App Detail** por app: estado en cada PC, transitions ultimas 7d, mini chart healthy_ratio horario.
4. **Live (WS)** stream transitions.
5. **Alerts** apps expected=running AND actual=inactive > 5min. (v1 solo lista; notifs separadas).
UI: `data_table_cpp_viz`, `badge_cpp_core`, `empty_state_cpp_core`.
## Decisiones cerradas (2026-05-17)
1. **Local especial**: PC local NO se chequea via SSH. Flag `pc_is_self` por PC. Checker selecciona path: local exec vs ssh exec.
2. **Persistencia**: transitions + hourly aggregate. Append-only `service_check` con TTL 7d (vacuum job nocturno).
3. **Auto-start**: NO en v1. Solo alerta. Feature flag `services_monitor.auto_fix` OFF.
## Tareas (orden)
- [ ] Migration `services_api.db`: tabla `service_check`, `service_transition`, `service_state_hourly`
- [ ] Funciones registry: `port_listening_check_go_infra`, `http_health_probe_go_infra` (si no existen) via fn-constructor paralelo
- [ ] `services_api` MVP: worker loop + `/api/services` + WS
- [ ] systemd unit + Restart=always + actualizar issue 0105 con 11mo service
- [ ] App C++ `services_monitor` scaffold via `fn run init_cpp_app services_monitor`
- [ ] Panel Overview + WS client
- [ ] PC Detail + App Detail
- [ ] Alerts panel
## DoD
- 10 services visibles en Overview con semaforo correcto contra ground truth.
- Caida simulada (kill -9 sqlite_api) detectada en <15s.
- Recovery (auto-restart via Restart=always) detectada y reflejada en transitions.
- App lanzable en aurgi-pc + home-wsl (sin SSH a self).
- Backend `services_api` corriendo como `tag: service` (dogfooding completo).
@@ -0,0 +1,96 @@
---
id: "0107e"
title: "uses_modules con min_version + codegen fail-loud"
status: pendiente
type: feature
domain:
- meta
- cpp-stack
- tooling
scope: registry
priority: media
depends:
- "0107a"
blocks: []
related:
- "0107"
created: 2026-05-17
updated: 2026-05-17
tags: [modules, versioning, codegen, fail-loud]
---
# 0107e — Version pinning + codegen fail-loud
Parte del issue principal [0107](0107-modules-standardization.md).
## Objetivo
Permitir que `app.md` declare version minima del modulo y que el codegen falle ruidosamente si:
1. Modulo declarado en `uses_modules` tiene `version < min_version`.
2. Python3 no disponible y `uses_modules` no vacio.
3. Codegen produce `count = 0` cuando `uses_modules` declara N>0 modulos.
## Schema extendido `app.md::uses_modules`
```yaml
# Forma corta (back-compat, sin pinning):
uses_modules: [data_table_cpp]
# Forma larga (con pinning):
uses_modules:
- name: data_table_cpp
min_version: "1.4" # SemVer minor — acepta 1.4.x, 1.5.x, ..., NO 2.0.0
- name: chat_ia_cpp
min_version: "0.1"
# Mixed permitido:
uses_modules:
- data_table_cpp # sin pin
- name: framework_cpp # con pin
min_version: "1.1"
```
Reglas:
- `min_version` formato `MAJOR.MINOR` (no patch — patch siempre bug fix compatible).
- Comparacion: `module.md::version >= min_version` AND `module.md::version.major == min_version.major`. Esto previene saltos de major silenciosos.
## Tareas
- [ ] **5.1** `python/functions/infra/codegen_app_modules.py`: parser acepta string corto Y dict largo.
- [ ] **5.2** Codegen lee `module.md::version` y compara contra `min_version` declarado en `uses_modules`. Si falla → exit 3 con mensaje claro.
- [ ] **5.3** `cpp/CMakeLists.txt::add_imgui_app`: si codegen exit != 0 AND != 2 → `message(FATAL_ERROR ...)` (no WARNING como hoy).
- [ ] **5.4** Si Python3 NO encontrado AND `app.md` tiene `uses_modules` no vacio → `message(FATAL_ERROR "Python3 required to parse uses_modules; install python3.")`.
- [ ] **5.5** Si codegen devuelve count=0 cuando `uses_modules` declara N>0 → `message(FATAL_ERROR ...)` con apunta a la app.md.
- [ ] **5.6** Indexer `registry/parser.go`: parsea ambas formas y persiste en `apps.uses_modules_json` con shape canonico `[{name, min_version}]` (min_version null si no declarado).
- [ ] **5.7** Test: app sintetica con dict largo + modulo sintetico con version 1.3 y min_version 1.4 → cmake configure falla con mensaje esperado.
- [ ] **5.8** Migrar 7 apps consumidoras de data_table a usar dict largo con `min_version: "1.4"` (o lo que sea v actual cuando 0107e cierre).
## Mensajes de error esperados
```
CMake Error at cpp/CMakeLists.txt:289 (message):
codegen_app_modules failed for services_monitor: module 'data_table_cpp'
version 1.3.0 does not satisfy min_version 1.4 declared in
apps/services_monitor/app.md::uses_modules.
Either bump module version (via /version) or relax pin in app.md.
```
```
CMake Error at cpp/CMakeLists.txt:289 (message):
codegen_app_modules: app 'dag_engine_ui' declares uses_modules: [data_table_cpp]
but generated module manifest has count=0.
Likely cause: Python parser failed silently. Re-run with verbose:
python3 python/functions/infra/codegen_app_modules.py \
--app-md apps/dag_engine_ui/app.md \
--modules-root modules/ \
--app-name dag_engine_ui \
--out /tmp/test.cpp
```
## Riesgos
- **Apps existentes sin pinning rompen?**: NO. La forma corta sigue valida y trata `min_version` como null = sin chequeo. Solo apps que opten explicitamente reciben gate.
- **Build se vuelve ruidoso si modulo no se actualizo**: deliberado. Es el sentido del fail-loud.
+160
View File
@@ -0,0 +1,160 @@
---
id: "0109"
title: "App skill_tree: mapa interactivo de issues+flows en anillos concentricos por estado (roadmap)"
status: in-progress
type: epic
domain:
- meta
- cpp-stack
scope: cross-stack
priority: media
depends: []
blocks: []
related:
- "0069"
- "0085"
- "0086"
- "0087"
- "0100"
created: 2026-05-17
updated: 2026-05-17
tags:
- skill-tree
- roadmap
- meta
- cpp
- imgui
- gamification
---
# 0109 — skill_tree app (roadmap)
## Vision
App C++ ImGui que muestra los **79 issues + 7 flows** del registry como un mapa de capacidades en **anillos concentricos por estado** (centro = `completado`, exterior = `locked`/`deferred`), con **sectores radiales por dominio**. Click en nodo → panel `Inspector` con DoD + funciones del registry asociadas + 2 botones:
- **Generate ideas (`claude -p`)** → escribe a tabla `idea_drafts` para revision manual.
- **Run autonomous-task (`fn-orquestador`)** → spawn subagente en sandbox `auto/<issue>` con tail de logs.
Centro NO es nodo — es **HUD overlay** con LV, XP, conteos por dominio. Animacion lerp 1s cuando un nodo migra de anillo (cambio de status). Sin fisicas — layout estatico y determinista.
## Por que
- Discovery visual: lo que hoy es `ls dev/issues/` + lectura individual de 79 .md, se reduce a panoramica de 5s.
- Dispatcher unificado: lanzar `claude -p` o `/autonomous-task` desde 1 click contextual (hoy: copiar ID + tipear comando).
- Gamificacion derivada (XP/nivel) sin inflar nada artificial — todo deriva de trabajo real (status frontmatter + telemetria).
## No-goals (fuera de scope)
- NO editor de issues. Solo lectura + dispatch. Editar issue = boton "Open in editor" → `code <path>`.
- NO orquestador propio. Reusa `fn-orquestador` + `/autonomous-task` (issue 0069).
- NO base de datos paralela de issues. `registry.db` y `dev/issues/*.md` siguen siendo fuente unica.
## Modelo
### Tipos de nodo
| Tipo | Origen | Ring | Render |
|---|---|---|---|
| Issue (epic) | `dev/issues/NNNN-*.md` (`type=epic`) | segun status | nodo grande, color accent del dominio |
| Issue (feature/bugfix/refactor/...) | mismo, otros types | segun status | nodo medio, tono mas claro |
| Flow | `dev/flows/NNNN-*.md` | segun status (flow.status) | nodo distinto shape (rombo) |
### Aristas
- `issue.depends` / `issue.blocks` → DAG (linea solida).
- `flow.related_issues` → linea punteada flow → issue.
- On-hover: aristas al cinturon perimetral de funciones del registry (`uses_functions` cruzado con tags del issue).
### Estado lock/unlock (DERIVADO — nunca manual)
| Status visual | Regla |
|---|---|
| `done` | `status=completado` (issues) o `status=completed` (flows) |
| `in-progress` | `status=in-progress` |
| `unlocked` | `status=pendiente` Y todos `depends[]` resueltos |
| `locked` | `status=pendiente` Y algun `depends[]` no resuelto |
| `bloqueado` | `status=bloqueado` |
| `deferred` | `status=deferred` |
Flows sin `depends` usan `related_issues` — flow unlocked si todos los related estan done.
### XP / nivel
- `xp_value` por tipo: `epic=10, feature=3, bugfix=1, refactor=2, chore=1, docs=1, flow=5`.
- `xp_total = SUM(xp_value WHERE status in {completado, completed})`.
- `level = floor(sqrt(xp_total))`.
- HUD muestra: `LV X · N done · M open · K in-progress · domains mastered: ...`.
## Layout: anillos concentricos
| Ring | Radio (px) | Que contiene | Notas |
|---|---|---|---|
| 0 | 0 - 150 | `completado` | Si > 30 nodos, mostrar top-N por recencia + bucket "+N mas" |
| 1 | 150 - 280 | `in-progress` | Pulse animation suave |
| 2 | 280 - 450 | `unlocked` pendiente | Color pleno, clickable |
| 3 | 450 - 650 | `locked` (depends sin cumplir) | Gris, aristas hacia bloqueantes |
| 4 | 650+ | `deferred` + `bloqueado` | Semi-transparente |
Cada ring subdividido en **18 sectores radiales = 1 dominio** (allowlist de `dev/TAXONOMY.md`). Aristas curvas hacia el centro siguiendo el sector. Cuando un issue cambia status → lerp 1s entre posicion vieja y nueva.
## Stack tecnico
- **App C++ ImGui** via `fn::run_app` (scaffolder `init_cpp_app_bash_pipelines`).
- **Viz**: `graph_renderer_cpp_viz` (solo draw — sin force layout) + `graph_viewport_cpp_viz` + `graph_labels_cpp_viz` + `graph_spatial_hash_cpp_core` (picking O(1)).
- **Layout**: nueva fn `compute_ring_layout_cpp_core` (pure) — input `[{node_id, status, domain, recency}]`, output `[{node_id, x, y, ring, sector}]`.
- **Parser**: nueva fn `parse_md_frontmatter_cpp_core` (pure) — extrae bloque `---...---` + parse YAML simple (subset: key:value, key:list).
- **BD propia**: `apps/skill_tree/local_files/skill_tree.db` con tablas:
- `node_state_cache` (denormalizacion para render rapido)
- `agent_jobs` (claude -p + fn-orquestador spawns)
- `xp_events` (append-only para timeline)
- `idea_drafts` (drafts de `claude -p` antes de promover a proposal)
- **Registry.db**: read-only (`?mode=ro`), solo para enriquecer Inspector con info de `functions/types`.
## Sub-issues (rompimiento)
| ID | Titulo | DoD resumido |
|---|---|---|
| 0109a | App shell + parsers | Scaffolder corre. App abre. Lee 79 issues + 7 flows. Log conteos en stdout. e2e_checks build OK. |
| 0109b | Layout anillos + render estatico | Nodos pintados en su ring+sector. Aristas depends/related visibles. Sin interaccion clic. |
| 0109c | Panel Inspector + estado derivado | Click nodo → panel derecho con DoD + uses_functions. Lock/unlock derivado de depends. Reload manual F5. |
| 0109d | HUD XP + animacion migracion | Overlay HUD arriba-izq. Lerp 1s en cambio de status. xp_events append-only. |
| 0109e | Boton Ideas (claude -p → drafts) | Spawn `claude -p` con prompt contextual. Persiste en `idea_drafts`. UI aprobar/rechazar → proposals. |
| 0109f | Boton Auto-run (fn-orquestador) | Spawn `Agent(fn-orquestador)` background. Panel inferior con tail logs. Barra progreso vive (`task_runs.checks_pass/total`). |
## Riesgos / mitigaciones
| Riesgo | Mitigacion |
|---|---|
| Ring 0 con ~72 done satura visualmente | Top-N por recencia + bucket "+N mas..." expandible |
| 18 sectores * 158 nodos = legibilidad pobre | Zoom semantico: zoom-out muestra solo epics + flows |
| `claude -p` puede consumir tokens en bucle | Rate-limit UI: 1 invocacion por nodo por hora (`idea_drafts.last_request_at`) |
| Drift status del .md vs cache | F5 manual fuerza re-scan. Sin file watcher en fase A. |
| Parser YAML C++ frágil con campos exoticos | Test golden sobre los 79 issues actuales antes de mergear 0109a |
## DoD del epic
- [ ] App `skill_tree` indexada en `registry.db` con `framework=imgui`, trio icon completo, `e2e_checks` declarados, `uses_functions` no vacio.
- [ ] Compila en Linux + Windows. Desplegada via `redeploy_cpp_app_windows`.
- [ ] Tarjeta visible en `app_hub_launcher`.
- [ ] Los 6 sub-issues (a-f) mergeados a master de su sub-repo Gitea.
- [ ] Pipeline `fn doctor cpp-apps` limpio para `skill_tree`.
- [ ] `fn doctor uses-functions` sin drift para `skill_tree`.
## Funciones nuevas previstas (delegar a fn-constructor)
1. `parse_md_frontmatter_cpp_core` (pure) — parser frontmatter YAML simple en C++.
2. `compute_ring_layout_cpp_core` (pure) — posiciones determinsticas por anillo+sector.
3. `spawn_claude_p_bash_infra` o `_go_infra` — lanza `claude -p "<prompt>"` con timeout + captura stdout JSON. Reusable.
## Decisiones
| Tema | Decision | Razon |
|---|---|---|
| Centro del mapa | HUD overlay, NO nodo | Centro = "estado actual del usuario" — no es un item, es un agregado. |
| Layout | Estatico anillos+sectores | Sin fisicas. Usuario dijo "no quiero que se muevan, nos volveran locos". |
| Animacion | Lerp 1s entre rings | Da sensacion de progreso sin caos visual. |
| Boton Ideas dest | `idea_drafts` (revision manual) | Evita ruido en `proposals`. |
| Refresh | F5 manual | Simplicidad MVP. File watcher en fase posterior. |
| Service tag | NO | App interactiva, no daemon. |
| Ubicacion | `apps/skill_tree/` | Meta-tool del registry entero, no de proyecto. |
@@ -0,0 +1,78 @@
---
id: "0109g"
title: "skill_tree: panel terminal embebida (claude TUI dentro de la app)"
status: pendiente
type: feature
domain:
- meta
- cpp-stack
scope: app-scoped
priority: baja
depends:
- "0109b"
blocks: []
related:
- "0109"
created: 2026-05-17
updated: 2026-05-17
tags:
- skill-tree
- cpp
- imgui
- terminal
- pty
---
# 0109g — Terminal embebida en skill_tree
Hoy el boton `Claude fix` lanza terminal externa (Windows Terminal). Funciona pero saca al usuario fuera de la app. Issue: integrar terminal completa dentro de skill_tree como panel.
## Opciones a evaluar
### Opcion 1: lib externa ImGui terminal
- **ImTerm** (github.com/Optiroc/ImTerm): minimalista, sin PTY. NO sirve para TUI.
- **imgui-console**: similar, comandos hardcoded. NO sirve.
- **imgui-vt100** o forks: emuladores ANSI dentro de ImGui. Algunos con PTY. Investigar:
- github.com/Magenta-Inc/MagentaImGuiTerminal
- github.com/microsoft/terminal — ConPTY API + custom UI
- github.com/jbrd/imterm — soporta escape sequences basicas
- Riesgo: claude usa **alt screen + raw mode + bracketed paste + colores 24-bit + cursor moves**. La mayoria de libs no cubren todo.
### Opcion 2: PTY + parser ANSI propio
- Linux: `openpty()` + fork + exec claude. Buffer texto. Parser ANSI minimo (CSI, SGR, cursor).
- Windows: ConPTY (`CreatePseudoConsole`). Similar.
- Render ImGui con `AddText` + colores por celda. Soportar redimension via `TIOCSWINSZ`.
- Mucho trabajo (~1500 LOC). Resultado limitado (no soporta TUIs complejas tipo less, vim).
### Opcion 3: Pipe simple (no TUI)
- spawn `claude --print "..."` con un prompt y leer stdout en panel scrollable.
- NO interactivo. Solo one-shot.
- Util para `Generate ideas` (0109h), NO para `Claude fix` interactivo.
## Recomendacion
- **Corto plazo**: dejar terminal externa (esta hecha en 0109b3).
- **Medio plazo**: hacer Opcion 3 (pipe one-shot) para 0109h.
- **Largo plazo**: Opcion 2 (ConPTY/openpty propio) solo si el usuario lo pide explicitamente. Es trabajo de semana y limitara la app a un emulador de calidad mediocre.
## Investigacion previa (sub-issue 0109g1)
Antes de implementar, abrir sub-issue 0109g1 que evalua:
- ¿Hay alguna lib **imgui-pty** madura en 2026?
- ¿La calidad de `imgui-vt100` cubre claude TUI?
- ¿Cuanto cuesta ConPTY/openpty propio para soportar al menos: scrollback, colores 24-bit, cursor moves, alt screen, bracketed paste?
Si la respuesta a (1) o (2) es "si", saltar a implementar. Si la respuesta es "no", el esfuerzo de (3) probablemente no compensa vs la terminal externa.
## DoD (cuando se implemente)
- [ ] Panel "Terminal" toggable desde menu View (Ctrl+3).
- [ ] Spawn `claude --dangerously-skip-permissions` en cwd `~/fn_registry`.
- [ ] Input box manda chars al PTY.
- [ ] Output renderizado con colores ANSI minimo.
- [ ] Cierre limpio del proceso al cerrar el panel o salir de la app.
- [ ] Funciona en Windows nativo (no requiere WSL terminal externa).
## Anti-DoD
- NO soporta TUIs muy complejas (less con keyboard, vim) hasta que se justifique con un user need real.
@@ -0,0 +1,124 @@
---
id: "0109h"
title: "skill_tree: generar ideas con LLM y promover a issue/flow desde la interfaz"
status: in-progress
type: feature
domain:
- meta
- cpp-stack
scope: app-scoped
priority: alta
depends:
- "0109b"
blocks: []
related:
- "0109"
- "0085"
- "0086"
created: 2026-05-17
updated: 2026-05-17
tags:
- skill-tree
- cpp
- imgui
- llm
- claude-cli
- ideas
- ghost-nodes
---
# 0109h — Generate ideas + promote to issue/flow
## UX que el usuario quiere
Click en cualquier nodo del Tree → Inspector aparece con boton `[ ⨁ Generate ideas ]`. Click ese boton →
1. Skill_tree lanza `claude -p "<prompt contextual>"` en background.
2. LLM devuelve N (3-5) ideas JSON con `{title, description, type: issue|flow, domain}`.
3. Cada idea aparece como un **ghost-node** (nodo semi-transparente con outline animado) que **emerge del nodo source** y se anima hacia su target ring/sector segun su domain.
4. Click sobre el ghost → Inspector pivota al draft. Muestra title + description + buttons:
- `[ + Generate issue ]` (technical — escribe `dev/issues/NNNN-<slug>.md`)
- `[ + Generate flow ]` (use-case — escribe `dev/flows/NNNN-<slug>.md`)
- `[ × Discard ]` (elimina del buffer)
5. Si el usuario promueve → archivo creado, draft eliminado, F5 (auto-trigger) recarga y el nodo aparece como nodo real con ID nuevo.
## Modelo
### Ghost-node
```cpp
struct DraftNode {
std::string id; // tmp_<uuid>
std::string source_id; // ID del nodo que la genero
std::string title;
std::string description;
std::string proposed_type; // "issue" or "flow"
std::string proposed_domain;
std::string proposed_priority;
std::vector<std::string> proposed_depends;
std::vector<std::string> proposed_dod_items;
float x = 0, y = 0; // posicion actual (lerp)
float target_x = 0, target_y = 0;
double spawn_t = 0; // animacion 1s emerge
};
```
Buffer global `g_drafts` (en memoria, NO persistido — drafts viven hasta promote o discard, sin persistencia entre runs).
### Spawn LLM
`spawn_claude_p_for_ideas(node)`:
- Construye prompt:
```
Eres un asistente que propone sub-tareas para el sistema fn_registry.
Contexto del nodo origen:
ID: 0109b
Title: skill_tree layout anillos
Status: completado
Domain: meta, cpp-stack
Type: feature
Body:
<pegado del .md sin frontmatter>
Proponme 3-5 ideas de sub-tareas (issues tecnicas o flows de uso) que extiendan o validen lo logrado por este nodo. Para cada idea devuelve JSON:
{ "title": "...", "description": "...", "type": "issue|flow", "domain": "<uno de meta/cpp-stack/...>", "priority": "alta|media|baja", "depends": [], "dod": ["..."] }
Devuelve SOLO un array JSON, sin texto extra.
```
- `claude --print "<prompt>"` con timeout 60s. stdout capturado en pipe.
- Parse JSON array → cada elemento se convierte en `DraftNode`.
### Promote
`promote_draft_to_issue(draft, output_dir)`:
- Encuentra siguiente NNNN libre escaneando `dev/issues/`.
- Renderiza frontmatter YAML desde template + body.
- Escribe `dev/issues/NNNN-<slug>.md`.
- Quita `draft` de `g_drafts`.
- Trigger `reload_scan()`.
Similar para flows en `dev/flows/`.
## Sub-issues
- **0109h1** — Framework ghost-nodes (sin LLM, mock con datos hardcoded): Render + animacion + Inspector + promote.
- **0109h2** — Integracion claude -p real: spawn async, parse JSON, fill drafts.
- **0109h3** — Loading indicator + cancel.
- **0109h4** — Rate-limit: 1 invocacion por nodo por hora (`last_request_t` en draft buffer).
## Riesgos
- **claude -p timeout**: respuestas >60s posibles. Mitigacion: async + ImGui spinner.
- **JSON malformado**: el modelo a veces devuelve markdown alrededor del JSON. Mitigacion: extraer entre primeras `[` y ultimas `]`, retry parsing.
- **Costo**: cada invocacion consume tokens. Mostrar contador de invocaciones en sesion (`g_llm_calls_count`).
- **Promote a archivo escribe en `dev/issues/` que NO es sub-repo**: ese write toca fn_registry main repo. OK porque dev/issues/ es parte del registry. Pero requiere que skill_tree.exe tenga permiso de escritura — en Windows con paths WSL `\\wsl$\Ubuntu\home\lucas\fn_registry\dev\issues\` debe funcionar.
## DoD
- [ ] Boton Generate ideas en Inspector lanza spawn LLM.
- [ ] Drafts aparecen como ghost-nodes en el canvas con animacion emerge.
- [ ] Click ghost → Inspector pivota a draft.
- [ ] Buttons Generate issue / Generate flow escriben .md a disco.
- [ ] Reload F5 (o auto-trigger tras escribir) muestra el nuevo nodo en el canvas.
- [ ] Discard elimina draft del buffer.
- [ ] Rate-limit suave (1 invocacion / nodo / hora).
@@ -0,0 +1,68 @@
---
id: "0109k"
title: "skill_tree: panel Dashboard con stats por dominio + XP + level"
status: in-progress
type: feature
domain:
- meta
- cpp-stack
scope: app-scoped
priority: media
depends:
- "0109b"
blocks: []
related:
- "0109"
created: 2026-05-17
updated: 2026-05-17
tags:
- skill-tree
- cpp
- imgui
- dashboard
- gamification
---
# 0109k — Dashboard panel
Tercer panel de skill_tree (Tree + Inspector + **Dashboard**). Vista cuantitativa del arbol de habilidades, complementaria al canvas visual.
## Contenido
- **HUD top**: LV global + XP total + total nodes done/planned/todo.
- **Tabla por dominio** (18 filas, una por dominio canonico):
- Domain
- Done / Planned / Todo / Total
- % completado (barra de progreso)
- XP acumulado en ese dominio
- Level por dominio (sqrt(xp_domain))
- **Top dominios masterizados** (top 3 por % completado).
- **Dominios mas lock-loaded** (los que tienen mas locked vs unlocked — proximos en desbloquearse).
- **Distribucion XP por type** (epic vs feature vs bugfix...): mini-barras.
## XP scheme
Por type del issue (al completarse):
- `epic` → 10 XP
- `feature` → 3 XP
- `infra` → 4 XP
- `refactor` → 2 XP
- `bugfix` → 1 XP
- `chore` → 1 XP
- `docs` → 1 XP
- `spike` → 2 XP
- `planning` → 2 XP
Flows completados → 5 XP cada uno.
`xp_total = sum(xp_per_done_node)`. `level = floor(sqrt(xp_total))`.
Per-domain: igual pero filtrado por domain match (un nodo cuenta en cada uno de sus domain tags).
## DoD
- [x] Tercer panel toggable desde menu View (Ctrl+3).
- [x] HUD con LV global + XP + counts.
- [x] Tabla por dominio con barras de progreso.
- [ ] Distribucion XP por type (mini-bars).
- [ ] Top mastered / next-to-unlock.
- [ ] Refresh sincronizado con F5 del Tree.
+169
View File
@@ -0,0 +1,169 @@
---
id: "0109m"
title: "issues_api service: HTTP backend para issues+flows (skill_tree, kanban, dashboards)"
status: pendiente
type: feature
domain:
- meta
- apps-infra
scope: app-scoped
priority: media
depends: []
blocks:
- "0109h2"
related:
- "0109"
- "0106"
created: 2026-05-18
updated: 2026-05-18
tags:
- service
- go
- http
- issues
- flows
- api
---
# 0109m — issues_api service
## Por que
`skill_tree.exe` corre en Windows nativo. Necesita leer `dev/issues/*.md` + `dev/flows/*.md` que viven en WSL `~/fn_registry/`. Opciones:
| Solucion | Pro | Contra |
|---|---|---|
| UNC `\\wsl.localhost\Ubuntu\home\lucas\fn_registry` | Cero infra extra | Hardcodea distro/user. No funciona si WSL caido. |
| Service HTTP (este issue) | Robusto, reusable por otras apps (kanban, web dashboard) | Mas piezas que mantener |
| Embeber YAML parser + nested fields | Self-contained | Mucha logica duplicada |
Fix UNC ya esta hecho (0109l). Pero el patron canonico del registry son services (services_api, sqlite_api, registry_api). Este issue formaliza `issues_api`.
## Spec
Service Go HTTP en puerto **8486**. Patron identico a `services_api` (issue 0106).
### Endpoints
- `GET /api/health``{"status":"ok"}`
- `GET /api/issues` → array JSON con TODOS los issues (open + completed)
- `GET /api/issues/:id` → un issue concreto con body markdown completo
- `GET /api/flows` → array JSON con todos los flows
- `GET /api/flows/:id` → un flow concreto
- `GET /api/stats` → counts por status/domain/type (agregados para Dashboard)
- `POST /api/issues` → crea nuevo issue (escribe `.md`). Body: `{id, title, type, domain, depends, body, dod}`. Devuelve `{path}`.
- `POST /api/flows` → crea nuevo flow. Mismo shape adaptado a frontmatter de flows.
### Schema JSON issue
```json
{
"id": "0109",
"title": "...",
"status": "pendiente",
"status_eff": "pendiente_unlocked",
"type": "epic",
"domain": ["meta", "cpp-stack"],
"priority": "media",
"depends": [],
"blocks": [],
"related": ["0085"],
"tags": ["skill-tree"],
"created": "2026-05-17",
"updated": "2026-05-18",
"file_path": "dev/issues/0109-skill-tree-app-roadmap.md",
"body_md": "(opcional, solo en /api/issues/:id)",
"dod": [{"text":"App existe","done":true}, ...]
}
```
### Implementacion
```go
// apps/issues_api/main.go
package main
import (
"encoding/json"
"net/http"
"log"
"os"
// reusa funciones del registry:
// - extract_frontmatter (Go port; existe extract_frontmatter_py_core, pero
// en C++ existe parse_md_frontmatter_cpp_core — necesitamos Go port o
// reusar yaml.v3 directo aqui).
)
func main() {
root := os.Getenv("FN_REGISTRY_ROOT")
if root == "" { log.Fatal("FN_REGISTRY_ROOT not set") }
mux := http.NewServeMux()
mux.HandleFunc("/api/health", health)
mux.HandleFunc("/api/issues", listIssues(root))
mux.HandleFunc("/api/issues/", showIssue(root))
mux.HandleFunc("/api/flows", listFlows(root))
mux.HandleFunc("/api/flows/", showFlow(root))
mux.HandleFunc("/api/stats", stats(root))
log.Fatal(http.ListenAndServe(":8486", mux))
}
```
Frontmatter: existe `extract_frontmatter_py_core` (Python). Hace falta:
- **Opcion A**: crear `parse_md_frontmatter_go_core` (port del C++). Reusable por otros services Go.
- **Opcion B**: usar `gopkg.in/yaml.v3` directo dentro de `issues_api`.
Recomiendo **A** — Go port reusable. Pattern: header `parse_md_frontmatter.go` en `functions/core/`.
### Frontmatter service: block en app.md
```yaml
service:
port: 8486
health_endpoint: /api/health
health_timeout_s: 3
systemd_unit: issues_api.service
systemd_scope: user
restart_policy: always
runtime: systemd-user
pc_targets:
- aurgi-pc
- home-wsl
is_local_only: false
```
### Skill_tree consume
Cambio en `skill_tree/main.cpp`:
- Si `discover_registry_root()` falla, intentar HTTP `http://localhost:8486/api/issues`.
- Si responde 200 → parsear JSON y poblar `g_scan.nodes`.
- Fallback a UNC + file scan si no.
Cliente HTTP en C++:
- Linux: `libcurl` (ya disponible) o `popen("curl ...")`.
- Windows: WinHTTP nativo o curl.exe (Windows 10+ trae curl).
- Mas simple: spawn `curl.exe -s http://...` y parsear stdout (nlohmann json — habria que vendor).
## Sub-issues
- **0109m1** — crear `parse_md_frontmatter_go_core` port del C++ (mismas semanticas, mismos tests).
- **0109m2** — scaffolder `issues_api` app + service block + systemd unit.
- **0109m3** — implementar endpoints GET (list, show, stats).
- **0109m4** — implementar POST issues + POST flows (escribir .md valido).
- **0109m5** — cliente HTTP en skill_tree + fallback chain (env > walk > UNC > HTTP).
## Beneficios cross-app
Una vez issues_api existe, reutilizan:
- `kanban` (issue 0058 sync) podria leer/sync issues.
- Frontend web dashboard ya tendria backend.
- CI/automation puede crear issues via POST en lugar de editar .md a mano.
- `services_monitor` mostrara `issues_api` como service mas (auto via `tag: service`).
## DoD
- [ ] `apps/issues_api/` scaffoldada via `/cpp-app` ?? no — Go service via `/app`.
- [ ] Endpoints listed devuelven JSON valido.
- [ ] systemd unit instalado (`issues_api.service`).
- [ ] Visible en `services_monitor` como service activo.
- [ ] skill_tree consume HTTP cuando esta disponible, cae a UNC/file si no.
- [ ] Tests Go basicos (list, show, post).
+134
View File
@@ -0,0 +1,134 @@
---
id: "0110"
title: "Gap registry: helper HTTP cliente C++ (curl/popen) reutilizable"
status: pendiente
type: feature
domain:
- cpp-stack
- registry-quality
scope: registry
priority: media
depends: []
blocks:
- "0111"
related:
- "0106"
created: 2026-05-18
updated: 2026-05-18
tags: [http, cpp, registry-gap, curl, helper]
---
# 0110 — Helper HTTP cliente C++ en el registry
## Problema
Hoy no existe funcion HTTP cliente reutilizable en `cpp/functions/`. Cada app C++ que necesita
golpear un endpoint reinventa la capa:
| App | Fichero | Tecnica | LOC aprox |
|---|---|---|---|
| `apps/services_monitor/` | `http_client.cpp` | cURL popen/WinHTTP segun plataforma | ~150 |
| `apps/dag_engine_ui/` | inline en `main.cpp` | curl CLI via popen + parse | ~80 |
| `apps/data_factory/` | inline | popen curl | ~60 |
| `cpp/functions/core/llm_anthropic.cpp` | propio | cURL popen — solo Anthropic | — |
| `apps/process_explorer/` (issue 0111) | `http_client.cpp` local | pendiente — clonara services_monitor | ~150 esperados |
El patron ya supera el umbral `>2x` que dispara la regla de promocion (CLAUDE.md
"Si patron se repite >2x → propose nueva funcion via fn-constructor"). Cada app duplica:
- Detection de plataforma (Linux: `popen("curl -s ...")`, Win: `WinHTTPOpen`/popen)
- Manejo de basicAuth / Bearer tokens
- Timeouts
- Captura de body + status code
- Manejo de errores transitorios (DNS, conexion rechazada)
## Decision
Anadir al registry dos funciones C++ en dominio `core` (o `infra`):
### `http_request_cpp_core` (impure)
```cpp
namespace fn_http {
struct Request {
std::string method; // "GET", "POST", "PUT", "DELETE"
std::string url;
std::vector<std::pair<std::string,std::string>> headers;
std::string body; // raw bytes (JSON, etc.)
int timeout_ms = 5000;
std::string bearer_token; // shortcut: anade Authorization: Bearer <token>
std::string basic_user; // shortcut: anade Authorization: Basic base64(user:pass)
std::string basic_pass;
};
struct Response {
int status = 0; // 0 = error de transporte
std::string body;
std::vector<std::pair<std::string,std::string>> headers;
std::string error; // vacio si OK
int64_t duration_ms = 0;
};
Response request(const Request& req);
}
```
Implementacion: cURL via popen (portable WSL+Win+Linux, igual que `llm_anthropic`).
Si en el futuro queremos rendimiento real, swap a libcurl linkado estaticamente
o WinHTTP via `#ifdef _WIN32` — interfaz Request/Response no cambia.
### `http_get_json_cpp_core` (impure, pure wrapper)
Helper que envuelve `http_request` + parse JSON (via `nlohmann::json` o similar
ya disponible en el repo) para los casos comunes:
```cpp
namespace fn_http {
// Devuelve parsed JSON o lanza si status != 2xx
nlohmann::json get_json(const std::string& url,
const std::string& bearer_token = "",
int timeout_ms = 5000);
}
```
## Plan de migracion
Tras crear las funciones, abrir issue separado por cada consumer para migrar:
1. `apps/services_monitor/http_client.cpp` -> usar `fn_http::request`
2. `apps/dag_engine_ui/main.cpp` (inline)
3. `apps/data_factory/` (inline)
4. `cpp/functions/core/llm_anthropic.cpp` — refactor para usar `fn_http::request` por
debajo (mantiene API publica)
5. `apps/process_explorer/` (issue 0111) — nace ya usando el helper
## Criterios de aceptacion
- [ ] `cpp/functions/core/http_request.{cpp,h,md}` registrado en `registry.db`
- [ ] `cpp/functions/core/http_get_json.{cpp,h,md}` idem
- [ ] Tests visuales o de integracion contra `httpbin.org` (200/404/timeout/auth)
- [ ] Frontmatter completo (`params`/`output`/`tags`/`example`)
- [ ] `.md` cumple contrato self-doc (`## Ejemplo`, `## Cuando usarla`, `## Gotchas`)
- [ ] Al menos 1 consumer migrado para validar API (recomendado `services_monitor`)
- [ ] `fn doctor uses-functions` limpio
## Gotchas conocidos
- cURL popen en Windows necesita `curl.exe` en PATH — todos los WSL/Win lo tienen,
pero documentar en `## Gotchas`.
- Bodies binarios: popen complica el escape; primera version solo string bodies.
- TLS verify: por defecto on; permitir `req.insecure = true` solo para testing.
- Timeouts: cURL `--max-time` cubre handshake+transfer; documentar diferencia con
read-timeout puro.
## Por que no usar libcurl linkado
- `popen("curl ...")` no requiere anadir libcurl al toolchain MinGW cross-compile
(que ya costo configurar). `llm_anthropic` lleva meses funcionando asi.
- Cuando aparezca un caso real de latencia (>10 req/s sostenido), abrimos issue
separado para swap a libcurl.
## Out of scope (no en este issue)
- WebSocket / SSE — cliente WS C++ es otro gap; abrir issue propio cuando aplique.
- Cliente gRPC.
- Streaming responses (SSE chunk-by-chunk) — usar caso de `dag_engine_ui` para
decidir cuando.
+112
View File
@@ -0,0 +1,112 @@
---
id: "0112"
title: "App kanban_cpp: clon C++ ImGui de kanban_web con backend Go propio"
status: pendiente
type: app
domain:
- cpp-stack
- kanban
- agents
scope: app
priority: alta
depends:
- "0110"
blocks:
- "0116"
related:
- "0008"
- "0113"
- "0117"
- "0118"
created: 2026-05-18
updated: 2026-05-18
tags: [kanban, cpp, imgui, agents, backend-copy]
flow: "0008"
---
# 0112 — App `kanban_cpp`
## Problema
`kanban_web` (React + Mantine) funciona bien para humanos. Falta un kanban dedicado a **conducir agentes LLM**: arrastrar card a `Doing (agent)` -> arranca workflow, sin abrir browser ni terminal.
Flow 0008 lo requiere como surface 1 user-facing.
## Decision
Nueva app `apps/kanban_cpp/` (C++ ImGui via `fn::run_app`). Backend Go **copia identica** de `apps/kanban/backend/` con su propia `operations.db`. Frontend ImGui consume HTTP + SSE/WS del backend local.
NO sync con kanban_web — datos independientes a proposito. Auth/users duplicados (cada app sus propios usuarios).
### Estructura
```
apps/kanban_cpp/
CMakeLists.txt # add_imgui_app + linkea http_request_cpp_core
app.md # trio: description + icon.phosphor=columns-3 + icon.accent=#a855f7
appicon.ico # generado via generate_app_icon_py_infra
main.cpp # fn::run_app + 6 panels
data.{h,cpp} # HTTP client wrapper (usa http_request_cpp_core)
panel_board.{h,cpp} # columnas + cards drag-and-drop
panel_calendar.{h,cpp} # vista calendar (port de CalendarView.tsx)
panel_dashboard.{h,cpp} # KPIs (port de Dashboard.tsx)
panel_agent_runs.{h,cpp} # lista runs (usa agent_runs_timeline_cpp_viz)
panel_worktrees.{h,cpp} # git worktree manager
panel_dod.{h,cpp} # DoD inspector (usa dod_evidence_panel_cpp_viz)
backend/ # COPIA de apps/kanban/backend
main.go # port distinto: 8401 (web) -> 8403 (cpp)
db.go, handlers.go, ... # idem
migrations/ # mismas migrations
operations.db # SU PROPIA DB
```
## Panels (6 + Board)
Mapping con `apps/kanban/frontend/src/components/`:
| Mantine component | Panel C++ | Funcion registry |
|---|---|---|
| `KanbanColumn` + `KanbanCard` | `panel_board` | inline (logica especifica) |
| `CalendarView.tsx` | `panel_calendar` | inline |
| `Dashboard.tsx` | `panel_dashboard` | `kpi_card_cpp_viz` + `sparkline_cpp_viz` |
| nuevo | `panel_agent_runs` | `agent_runs_timeline_cpp_viz` (0118) |
| nuevo | `panel_worktrees` | inline (calls `agent_runner_api`) |
| nuevo | `panel_dod` | `dod_evidence_panel_cpp_viz` (0117) |
## Criterios de aceptacion
- [ ] `apps/kanban_cpp/` creado via `./fn run init_cpp_app kanban_cpp` (scaffolder canonico).
- [ ] `backend/` copiado de `apps/kanban/backend` con port distinto (8403) y migrations propias.
- [ ] `app.md` con trio completo: description 1 linea + `icon.phosphor: columns-3` + `icon.accent: "#a855f7"`.
- [ ] `appicon.ico` generado via `./fn run generate_app_icon "columns-3" "#a855f7" apps/kanban_cpp/appicon.ico`.
- [ ] App registrada en `cpp/CMakeLists.txt` (bloque `add_subdirectory`).
- [ ] Build Windows cross-compile OK: `cmake --build cpp/build/windows --target kanban_cpp -j`.
- [ ] Deploy a `/mnt/c/Users/lucas/Desktop/apps/kanban_cpp/` via `./fn run redeploy_cpp_app_windows kanban_cpp`.
- [ ] `--self-test` arranca + verifica GL loader + SQLite local + conexion al backend `:8403`.
- [ ] Trio aparece en App Hub tras `./fn run refresh_app_hub`.
- [ ] Backend `:8403` arranca via systemd unit (tag `service`).
- [ ] `uses_functions` declarado en `app.md` cubre: `http_request_cpp_core`, `kpi_card_cpp_viz`, `sparkline_cpp_viz`, `data_table_cpp_viz`, `dod_evidence_panel_cpp_viz` (0117), `agent_runs_timeline_cpp_viz` (0118).
- [ ] `fn doctor uses-functions kanban_cpp` limpio.
- [ ] `e2e_checks` declarados en `app.md`: build, --self-test, backend health, smoke board panel.
## Gotchas
- 2 services + 2 sqlite locks: nunca compartir `operations.db` entre `kanban` y `kanban_cpp`. Ports + DB independientes.
- Auth: backend copia trae `users.go`. Decision: usuarios independientes por app. Documentar en `app.md`.
- Mantine es modal-heavy (`Modal`, `Drawer`). En ImGui usar `ImGui::OpenPopup` + `BeginPopupModal`. NO replicar look pixel-perfect — adaptar a fn_tokens.
- BD drift cross-PC: `operations.db` por PC, no se sincroniza. Si el usuario quiere portabilidad de cards -> issue separado.
## Out of scope
- Sync de cards entre `kanban_web` y `kanban_cpp` (otro issue, decision deferred).
- Multi-user concurrent edit en kanban_cpp (single-user por ahora).
- Tab `Chat` (`CardChatPanel.tsx`) — postergar a v2.
## Plan implementacion
1. `./fn run init_cpp_app kanban_cpp` (no flag --project: app suelta).
2. `cp -r apps/kanban/backend apps/kanban_cpp/backend` + sed port 8401 -> 8403 + clear `operations.db`.
3. Build cliente data.{h,cpp} con `fn_http::request`.
4. Implement `panel_board` primero (MVP). Resto en orden.
5. Trio + icon + refresh_app_hub.
6. e2e_checks + smoke + deploy Windows.
+113
View File
@@ -0,0 +1,113 @@
---
id: "0113"
title: "Service agent_runner_api: orquestador de workflows con worktrees + DoD"
status: pendiente
type: app
domain:
- agents
- workflows
- apps-infra
scope: app
priority: alta
depends: []
blocks:
- "0115"
- "0116"
- "0117"
- "0118"
related:
- "0008"
- "0069"
created: 2026-05-18
updated: 2026-05-18
tags: [agents, service, worktrees, dod, claude-headless]
flow: "0008"
---
# 0113 — Service `agent_runner_api`
## Problema
Hoy hay tres puntos donde se lanza Claude:
1. `apps/skill_tree/main.cpp::spawn_claude_terminal` — abre `wt.exe` con `claude --dangerously-skip-permissions`. Termina sin trazabilidad.
2. `parallel-fix-issues` skill — worktrees paralelos pero stateless.
3. `fn-orquestador` (issue 0069) — autonomous loop dentro de Claude Code.
Ninguno persiste runs, evidencias o DoD. No hay manera de saber que workflows estan vivos cross-app.
## Decision
Service Go nuevo `apps/agent_runner_api/` puerto `:8486`, tag `service`. Single source of truth de:
- workflows declarados (templates de prompt + DoD schema)
- runs activos (worktree + subprocess Claude + status)
- evidencias DoD (path/url/log/cmd output + validated_by)
Endpoints minimos:
- `POST /api/runs` — crea worktree + lanza claude headless. Body: `{issue_id|card_id, mode, kanban_app}`.
- `GET /api/runs` — lista runs (filtros status/app/since).
- `GET /api/runs/:id` — detalle run.
- `GET /api/runs/:id/sse` — stream progreso.
- `POST /api/runs/:id/evidence` — agente adjunta evidencia.
- `POST /api/runs/:id/evidence/:eid/validate` — humano aprueba.
- `POST /api/runs/:id/merge` — TBD merge (todos items validated).
- `POST /api/runs/:id/abort` — kill subprocess + worktree remove.
- `GET /api/health` — 200 OK.
## Schema `agent_runs.db`
Migrations en `apps/agent_runner_api/migrations/`:
- `001_workflows.sql` — templates: `id, name, prompt_template, dod_schema_json, created_at`.
- `002_runs.sql``id, workflow_id, issue_id, card_id, kanban_app, branch, worktree_path, status, started_at, finished_at, agent_pid, agent_log_path`.
- `003_worktrees.sql``id, run_id, path, branch, created_at, removed_at`.
- `004_dod_items.sql` — un row por item declarado: `id, run_id, item_key, kind, expected, required, status (pending|done|validated|failed)`.
- `005_dod_evidence.sql` — un row por evidencia adjunta: `id, dod_item_id, kind, payload_path, payload_url, payload_text, attached_at, validated_at, validated_by`.
Aplicadas via `embed.FS + applyMigrations()` al arrancar.
## Frontmatter `app.md` (service)
```yaml
tags: [service, agents, go]
service:
port: 8486
health_endpoint: /api/health
health_timeout_s: 3
systemd_unit: agent_runner_api.service
systemd_scope: user
restart_policy: always
runtime: systemd-user
pc_targets:
- aurgi-pc
- home-wsl
is_local_only: true
```
## Criterios de aceptacion
- [ ] `apps/agent_runner_api/` scaffold Go (main.go, db.go, handlers.go, sse.go, agent_spawn.go).
- [ ] Migrations 001-005 versionadas + aplicadas al arrancar (idempotente).
- [ ] Endpoints arriba implementados con tests `*_test.go`.
- [ ] systemd unit `agent_runner_api.service` con `Restart=always`.
- [ ] `app.md` con trio + bloque `service:` completo (issue 0105).
- [ ] `fn doctor services-spec` valida bloque.
- [ ] Smoke test: POST /api/runs con issue dummy crea worktree real en `/tmp/wt-test-<id>`, persiste row en `agent_runs`, lanza echo subprocess (no claude real en test).
- [ ] Cleanup en abort: subprocess killed + `git worktree remove --force` + row marcada `aborted`.
- [ ] e2e_checks: build, migration apply, health, smoke run dummy, cleanup.
- [ ] Documentado en `docs/capabilities/agents.md` (capability group `agents`, ver 0114).
## Gotchas
- `git worktree add` falla si el branch ya existe. Reset hard antes (mismo patron que `autonomous_loop.md`).
- Worktree y main repo comparten `.git/hooks/`. Pre-commit puede bloquear. Permitir `--no-verify` SOLO si `events_json[].decision="skip_hook"` documentado.
- `claude --headless` necesita PATH correcto (`~/.local/bin`). Service systemd corre con user env: verificar `Environment=PATH=...`.
- Subprocess Claude puede correr horas. NO bloquear handler HTTP: spawn async, devolver `run_id` inmediato, monitorear PID en goroutine.
- SSE: clientes ImGui (kanban_cpp, skill_tree) deben reconectar con `Last-Event-ID`.
- Paths protegidos (`dev/autonomous_protected_paths.json`) aplican igual aqui. Reusar logica de fn-orquestador.
## Out of scope
- UI propio del service (es backend puro; UIs son kanban_cpp + skill_tree).
- Auth/auth tokens (local-only por ahora; agregar en issue separado si se expone fuera de localhost).
- Webhook Gitea para auto-trigger desde commits.
- Schedule cron para workflows recurrentes.
+106
View File
@@ -0,0 +1,106 @@
---
id: "0114"
title: "DoD evidence schema canonico: frontmatter + BD + validator"
status: pendiente
type: feature
domain:
- taxonomy
- dev-loop
- registry-quality
scope: registry
priority: alta
depends: []
blocks:
- "0115"
- "0117"
related:
- "0008"
- "0100"
- "0102"
created: 2026-05-18
updated: 2026-05-18
tags: [dod, evidence, frontmatter, taxonomy, validator]
flow: "0008"
---
# 0114 — DoD evidence schema canonico
## Problema
Hoy `## Definition of Done` es una lista markdown libre. `dod_user:` existe en frontmatter (issue 0102) como ratio. Falta una forma **estructurada** de declarar QUE evidencia tiene que aportar el agente por cada item DoD (screenshot, log, url, output cmd).
Sin schema, el agente no sabe que adjuntar y el validador no puede checkear automaticamente.
## Decision
Anadir bloque `dod_evidence_schema:` al frontmatter de **issues** y **flows**. Lista de items con shape canonico:
```yaml
dod_evidence_schema:
- id: surface_1_board_drag
kind: screenshot
expected: "kanban_cpp.exe board con card en columna Doing (agent), barra progreso visible"
required: true
- id: backend_health
kind: cmd
expected: "curl -fsS http://localhost:8403/api/health == 200"
required: true
- id: timeline_entry
kind: url
expected: "http://localhost:8486/api/runs?app=kanban_cpp devuelve >=1 run"
required: false
- id: agent_log
kind: log
expected: "agent_runs/<run_id>/agent.log contiene 'workflow done'"
required: true
```
### Kinds
| `kind` | Que adjunta el agente | Como valida |
|---|---|---|
| `screenshot` | path PNG en `agent_runs/<run_id>/evidence/<item_id>.png` | check existe + tamaño > 0 + dimensions sensatas |
| `log` | path file (txt/log) | check existe + grep pattern de `expected` (opcional) |
| `url` | URL string | HEAD request (2xx/3xx) o GET + match pattern |
| `cmd` | comando + stdout esperado | exec + compare exit code + grep stdout |
### Persistencia
Frontmatter declara el SCHEMA (lo que se espera). `agent_runner_api` (0113) crea un row en `dod_items` por cada entrada al iniciar run. Agente luego adjunta `dod_evidence` rows.
## Validator: `audit_dod_schema_go_infra`
Funcion Go nueva en `functions/infra/`. Lee `.md` de `dev/issues/` + `dev/flows/`, parsea frontmatter, valida:
- `id` unico por archivo.
- `kind` in [`screenshot`, `log`, `url`, `cmd`].
- `expected` no vacio.
- `required` bool (default true).
Output: tabla caveman con drift / errores.
Wrapper CLI: `fn doctor dod`.
## Criterios de aceptacion
- [ ] Plantilla `docs/templates/issue.md` + `docs/templates/flow.md` actualizadas con bloque opcional `dod_evidence_schema:` y ejemplo.
- [ ] `audit_dod_schema_go_infra` registrado (`functions/infra/audit_dod_schema.{go,md}`).
- [ ] `fn doctor dod` muestra: items por archivo + drift + errores.
- [ ] Indexer (`registry/parser.go`) lee `dod_evidence_schema:` y lo persiste si afecta a la tabla `issues`/`flows` (en `apps/issues_api/`).
- [ ] Migracion `apps/agent_runner_api/migrations/004_dod_items.sql` referencia este schema (issue 0113).
- [ ] Doc en `dev/issues/README.md` + `dev/flows/README.md`: cuando declarar evidence schema, ejemplos por kind.
- [ ] Al menos 2 issues piloto con bloque rellenado (recomendado: 0112 + 0116).
- [ ] Tests Go: `audit_dod_schema_test.go` cubre kinds validos/invalidos + frontmatter malformed.
## Gotchas
- `dod_user:` (0102) es METRICA (ratio). `dod_evidence_schema:` es DECLARACION. NO renombrar ni fusionar — son cosas distintas.
- Frontmatter YAML con array de objects: parser actual debe soportarlo. Verificar con `registry/parser.go` antes.
- Schema retroactivo: issues viejos sin bloque siguen validos (`dod_evidence_schema: []` o ausente -> sin validacion automatica).
- `cmd` con secretos/credenciales: NUNCA en el `expected`. Si el comando los necesita, env var.
## Out of scope
- UI para editar schema (eso vive en kanban_cpp/skill_tree v2).
- Validacion en CI / pre-commit (futuro: hook que rechaza issue sin schema si type=feature).
- Schema versioning — por ahora v1 implicito.
+118
View File
@@ -0,0 +1,118 @@
---
id: "0115"
title: "Funcion agent_launch_worktree: crear worktree + spawn claude headless"
status: pendiente
type: feature
domain:
- agents
- workflows
- registry-quality
scope: registry
priority: alta
depends:
- "0113"
blocks: []
related:
- "0008"
- "0069"
created: 2026-05-18
updated: 2026-05-18
tags: [agents, worktree, claude, registry-gap, go]
flow: "0008"
---
# 0115 — Funcion `agent_launch_worktree`
## Problema
`agent_runner_api` (0113) tiene que: (1) crear worktree, (2) spawn `claude --headless`, (3) tail stderr para capturar status, (4) cleanup en abort. Esa logica ya esta inline en:
- `.claude/skills/parallel-fix-issues/` (bash)
- `fn-orquestador` agent (issue 0069)
- futura `agent_runner_api`
>2x repeticion -> promover a funcion del registry (regla `delegation.md`).
## Decision
Funcion Go nueva `agent_launch_worktree_go_infra` (impure) en `functions/infra/`. API:
```go
package infra
type WorktreeLaunchConfig struct {
RepoRoot string // path al main repo
Branch string // "auto/<issue>-<slug>"
WorktreePath string // "../wt-<run_id>" o absoluto
Prompt string // prompt para claude -p
LogPath string // donde escribir stderr+stdout
Env map[string]string // PATH, HOME, etc.
SkipPerms bool // pasa --dangerously-skip-permissions
ResetIfExists bool // si branch ya existe, reset --hard a master
}
type WorktreeLaunchResult struct {
PID int
Branch string
WorktreePath string
LogPath string
StartedAt int64
Error string // vacio si OK
}
func AgentLaunchWorktree(cfg WorktreeLaunchConfig) WorktreeLaunchResult
```
Comportamiento:
1. `cd RepoRoot`.
2. Si `ResetIfExists` y branch existe: `git branch -D Branch` + `git worktree remove --force WorktreePath` (best-effort).
3. `git worktree add WorktreePath -b Branch master`.
4. `cmd := exec.Command("claude", args...)` con stdin del Prompt, stderr+stdout a `LogPath`.
5. `cmd.Start()` (no Wait — async).
6. Devuelve PID + path log.
Funcion hermana `agent_cleanup_worktree_go_infra`:
```go
func AgentCleanupWorktree(repoRoot, branch, worktreePath string, pid int) error
```
Kill PID + remove worktree + delete branch.
## Capability group `agents`
Crear `docs/capabilities/agents.md` listando:
- `agent_launch_worktree_go_infra`
- `agent_cleanup_worktree_go_infra`
- (futuro 0117) `dod_evidence_panel_cpp_viz`
- (futuro 0118) `agent_runs_timeline_cpp_viz`
- (futuro) `dod_validate_evidence_go_infra`
Tag `agents` aplicado a las funciones. `fn doctor capabilities` valida.
## Criterios de aceptacion
- [ ] `functions/infra/agent_launch_worktree.{go,md}` registrado.
- [ ] `functions/infra/agent_cleanup_worktree.{go,md}` idem.
- [ ] `params_schema` y `output` completos en frontmatter.
- [ ] `.md` cumple contrato self-doc (`## Ejemplo` con args reales, `## Cuando usarla`, `## Gotchas`).
- [ ] Tests `*_test.go` con repo temp + branch dummy + comando echo (sin claude real en CI).
- [ ] `agent_runner_api` (0113) usa estas funciones — `uses_functions` declarado en `app.md`.
- [ ] `parallel-fix-issues` skill actualizada para invocar la funcion (no reescribir inline). O issue separado si scope.
- [ ] Tag `agents` aplicado a ambas funciones.
- [ ] `docs/capabilities/agents.md` creado con tabla + ejemplo canonico end-to-end.
- [ ] `fn doctor capabilities` lista grupo `agents` sin drift.
- [ ] `fn doctor uses-functions` limpio en consumers.
## Gotchas
- `git worktree add` con branch existente FALLA. `ResetIfExists=true` cubre el caso reanudar de `fn-orquestador`.
- Worktrees comparten `.git/hooks/`. Documentar en `## Gotchas` del `.md`.
- `claude --headless` no acepta TTY. `cmd.Stdin = strings.NewReader(prompt)`. NO usar `cmd.StdinPipe()` sin cerrar.
- Subprocess vive horas. NO esperar Wait sincrono — devolver PID y dejar al caller monitorear.
- Path absoluto vs relativo: si `WorktreePath` es relativo, se resuelve respecto a `RepoRoot`. Documentar.
- `Env` debe incluir `PATH` que contenga `claude` binary. Service systemd-user no hereda PATH interactivo por defecto.
## Out of scope
- Logica TBD merge (vive en `agent_runner_api/handlers.go`).
- Watchdog/timeout (vive en `agent_runner_api`).
- Telemetria de run (vive en `agent_runs` table).
@@ -0,0 +1,95 @@
---
id: "0116"
title: "skill_tree v2: reemplazar boton Claude fix por Launch workflow"
status: pendiente
type: feature
domain:
- cpp-stack
- agents
- dev-loop
scope: app
priority: alta
depends:
- "0113"
- "0114"
blocks: []
related:
- "0008"
- "0109"
- "0117"
- "0118"
created: 2026-05-18
updated: 2026-05-18
tags: [skill-tree, agents, ui, feature-flag]
flow: "0008"
---
# 0116 — skill_tree v2: Launch workflow
## Problema
`apps/skill_tree/main.cpp::spawn_claude_terminal` abre `wt.exe new-tab wsl.exe -- bash -ic "claude --dangerously-skip-permissions"`. Side effects:
- Terminal externa fuera de la app (mala UX para agentes paralelos).
- Sin trazabilidad (run no persistido).
- Sin DoD ni evidencias.
- Imposible cancelar desde la UI.
Flow 0008 surface 2: usuario clica `Launch workflow` -> run trackeado en `agent_runner_api`, NO se abre terminal.
## Decision
Modificar `apps/skill_tree/main.cpp`:
1. **Eliminar** (o esconder detras de feature flag `legacy_claude_fix`) el boton `TI_TERMINAL_2 " Claude fix"`.
2. **Anadir** boton `TI_PLAY " Launch workflow"`:
- POST `http://localhost:8486/api/runs` con `{issue_id, mode: "fix-issue"}`.
- Captura `run_id` del response.
- Toast `run_id=...` visible 3s.
- Refresh panel `Timeline` (issue 0118).
3. Pasar al panel `DoD inspector` (issue 0117) si el run tiene `dod_evidence_schema` declarado.
### Feature flag `legacy_claude_fix`
`dev/feature_flags.json`:
```json
"legacy_claude_fix": {
"enabled": false,
"issue": "0116",
"description": "Mostrar boton Claude fix viejo (terminal externa). Default OFF tras 0116",
"added": "2026-05-18",
"enabled_at": null
}
```
Logica: si flag ON, muestra ambos botones (Launch workflow + Claude fix legacy) durante rollback window. Si OFF (default), solo Launch workflow.
Eliminar flag + codigo legacy en issue separado tras 2 semanas estables.
## Criterios de aceptacion
- [ ] `apps/skill_tree/main.cpp` modificado: boton `Launch workflow` operativo.
- [ ] POST a `:8486/api/runs` usa `http_request_cpp_core` (0110) — NO popen inline.
- [ ] Feature flag `legacy_claude_fix` registrado en `dev/feature_flags.json` con `enabled: false`.
- [ ] Cuando flag OFF: `Claude fix` no se renderiza.
- [ ] Cuando flag ON: ambos botones aparecen para rollback.
- [ ] Toast `run_id=...` visible al lanzar.
- [ ] Si `agent_runner_api` no responde (`:8486` down): toast error + sugerencia `systemctl --user start agent_runner_api`.
- [ ] `uses_functions` actualizado en `apps/skill_tree/app.md`: anadir `http_request_cpp_core`.
- [ ] `e2e_checks` actualizados: smoke launch workflow con backend mock.
- [ ] Test manual: clic en nodo issue -> ve run en kanban_cpp `panel_agent_runs` y en skill_tree `Timeline` (0118).
- [ ] Version bump `apps/skill_tree/app.md::version` minor (`/version apps/skill_tree minor "..."`).
## Gotchas
- `spawn_claude_terminal` esta en main.cpp inline. NO eliminar la funcion en este issue — solo no llamarla cuando flag OFF. Eliminar en issue separado tras 2 semanas.
- HTTP POST sincrono bloquearia frame ImGui. Usar `std::async` o spawn thread + flag `pending_launch`. Renderizar spinner.
- Sin `agent_runner_api` corriendo, el boton es decorativo. Doctor check: `fn doctor services` debe mostrar `agent_runner_api active`.
- Si el issue NO tiene `dod_evidence_schema` declarado: run igualmente arranca, pero sin items DoD precreados.
## Out of scope
- Panel DoD inspector (0117).
- Panel Timeline (0118).
- Tab Calendar/Dashboard de skill_tree (otros sub-issues 0109*).
- Eliminacion definitiva de `spawn_claude_terminal` (issue separado tras rollback window).
+100
View File
@@ -0,0 +1,100 @@
---
id: "0117"
title: "Funcion cpp dod_evidence_panel: render screenshots/logs/urls/cmd-output"
status: pendiente
type: feature
domain:
- cpp-stack
- agents
- registry-quality
scope: registry
priority: alta
depends:
- "0110"
- "0114"
blocks: []
related:
- "0008"
- "0112"
- "0116"
created: 2026-05-18
updated: 2026-05-18
tags: [dod, evidence, cpp, imgui, viz, registry-gap]
flow: "0008"
---
# 0117 — Funcion `dod_evidence_panel_cpp_viz`
## Problema
kanban_cpp (0112) y skill_tree v2 (0116) necesitan ambos un panel que muestre items DoD + evidencias adjuntas. Sin funcion compartida, se duplica logica (regla `delegation.md`).
## Decision
Funcion C++ en `cpp/functions/viz/dod_evidence_panel.{cpp,h,md}`. API:
```cpp
namespace fn_viz {
struct DodItem {
std::string id;
std::string kind; // "screenshot" | "log" | "url" | "cmd"
std::string expected;
bool required;
std::string status; // "pending" | "done" | "validated" | "failed"
};
struct DodEvidence {
std::string item_id;
std::string kind;
std::string payload_path; // para screenshot/log
std::string payload_url; // para url
std::string payload_text; // para cmd output
int64_t attached_at;
bool validated;
std::string validated_by;
};
struct DodPanelState {
std::vector<DodItem> items;
std::vector<DodEvidence> evidences;
std::string run_id;
std::function<void(const std::string&)> on_validate; // callback(evidence_id)
std::function<void(const std::string&)> on_reject; // idem
};
void render_dod_evidence_panel(DodPanelState& state);
}
```
Renderiza:
- Lista de items con icono por status (`TI_CIRCLE_DASHED`/`TI_CIRCLE_CHECK`/`TI_CIRCLE_DOT`/`TI_CIRCLE_X`).
- Por item: evidencia adjunta segun kind:
- `screenshot`: thumbnail via `stb_image_load` + `ImGui::Image`. Click -> open full-size en popup.
- `log`: `selectable_text_cpp_core` (registry) con scroll + grep pattern de `expected` highlighted.
- `url`: `TI_EXTERNAL_LINK` clickable -> `ShellExecuteW` (Win) / `xdg-open` (Linux).
- `cmd`: dos columnas (expected vs actual) usando `data_table_cpp_viz`. Diff highlight rojo si mismatch.
- Botones por evidencia: `Validate` (verde, callback `on_validate`) / `Reject` (rojo, callback `on_reject`).
## Criterios de aceptacion
- [ ] `cpp/functions/viz/dod_evidence_panel.{cpp,h,md}` registrado.
- [ ] `params_schema` y `output` completos en frontmatter.
- [ ] Tag `agents` aplicado.
- [ ] `.md` cumple contrato self-doc (`## Ejemplo` con DodPanelState concreto, `## Cuando usarla`, `## Gotchas`).
- [ ] Demo en `cpp/apps/primitives_gallery/demos_viz.cpp` con DodPanelState dummy (4 kinds + 2 validados + 2 pending).
- [ ] `kanban_cpp::panel_dod` usa la funcion (declarado en `uses_functions`).
- [ ] `skill_tree` (panel inferior cuando hay run activo) usa la funcion.
- [ ] Tests visuales: golden image en `primitives_gallery --capture`.
- [ ] `fn doctor capabilities` muestra grupo `agents` con esta funcion listada.
- [ ] `uses_functions` declara: `selectable_text_cpp_core`, `data_table_cpp_viz`, `icons_tabler_cpp_core`.
## Gotchas
- `stb_image_load` para PNGs grandes (screenshots full HD): clamp dimensions del thumbnail a 320x180. Liberar texturas con `glDeleteTextures` al cerrar panel.
- WSL paths vs Windows: si `payload_path` viene como `/mnt/c/...`, convertir a `C:\...` solo para `ShellExecuteW`. Para `stb_image` da igual.
- Cache de texturas: re-cargar PNG cada frame es caro. Map `path -> GLuint` con LRU. Invalidar si mtime cambia.
- Callbacks `on_validate`/`on_reject`: invocan POST HTTP al `agent_runner_api`. NO bloquear frame; spawn thread.
- `cmd` evidence con stdout largo: clip a 50 lineas + boton `Show full`.
## Out of scope
- Editor de schema DoD (vive en kanban_cpp o futura UI).
- Comparacion AI de screenshots (futuro: visual diff).
- Persistencia local de validaciones offline.
+100
View File
@@ -0,0 +1,100 @@
---
id: "0118"
title: "Funcion cpp agent_runs_timeline: panel SSE de runs cross-app"
status: pendiente
type: feature
domain:
- cpp-stack
- agents
- registry-quality
scope: registry
priority: alta
depends:
- "0110"
- "0113"
blocks: []
related:
- "0008"
- "0112"
- "0116"
- "0117"
created: 2026-05-18
updated: 2026-05-18
tags: [agents, timeline, sse, cpp, imgui, viz, registry-gap]
flow: "0008"
---
# 0118 — Funcion `agent_runs_timeline_cpp_viz`
## Problema
Panel de timeline de runs es identico en kanban_cpp (0112) y skill_tree v2 (0116). Sin funcion compartida = duplicacion.
## Decision
Funcion C++ en `cpp/functions/viz/agent_runs_timeline.{cpp,h,md}`. API:
```cpp
namespace fn_viz {
struct AgentRun {
std::string id;
std::string app; // "kanban_cpp" | "skill_tree" | ...
std::string issue_id;
std::string card_id;
std::string branch;
std::string status; // "pending" | "running" | "done" | "validated" | "merged" | "aborted" | "failed"
int64_t started_at;
int64_t finished_at;
int dod_total;
int dod_done;
int dod_validated;
};
struct TimelineFilter {
std::vector<std::string> apps; // vacio = todos
std::vector<std::string> statuses; // vacio = todos
int64_t since_ts; // 0 = sin filtro
};
struct TimelineState {
std::string sse_url; // ej "http://localhost:8486/api/runs/sse"
std::vector<AgentRun> runs;
TimelineFilter filter;
std::function<void(const std::string&)> on_select; // callback(run_id)
};
void render_agent_runs_timeline(TimelineState& state);
void poll_sse_runs(TimelineState& state); // llamar 1x/frame, no bloquea
}
```
Renderiza tabla con `data_table_cpp_viz`:
- Cols: status (icon), app (chip color), issue/card, branch, dod_done/total, dod_validated/total, duration, started_at.
- Sort por started_at desc por defecto.
- Click row -> `on_select(run_id)`.
- Filtros arriba: combo apps multi-select + combo statuses + date picker `since`.
- SSE en background thread: append runs nuevos, update statuses en vivo.
## Criterios de aceptacion
- [ ] `cpp/functions/viz/agent_runs_timeline.{cpp,h,md}` registrado.
- [ ] `params_schema` y `output` completos.
- [ ] Tag `agents` aplicado.
- [ ] `.md` cumple contrato self-doc (`## Ejemplo`, `## Cuando usarla`, `## Gotchas`).
- [ ] SSE client en background thread con reconnect + `Last-Event-ID` resume.
- [ ] Demo en `cpp/apps/primitives_gallery/demos_viz.cpp` con TimelineState mock (5 runs varied status).
- [ ] `kanban_cpp::panel_agent_runs` usa la funcion.
- [ ] `skill_tree` `panel_timeline` usa la funcion.
- [ ] `fn doctor capabilities` muestra grupo `agents` con esta funcion.
- [ ] `uses_functions` declara: `http_request_cpp_core`, `data_table_cpp_viz`, `icons_tabler_cpp_core`, `tokens_cpp_core`.
## Gotchas
- SSE reconnect: si `agent_runner_api` cae, reintenta cada 5s con backoff. Estado `disconnected` visible.
- Thread-safety: SSE thread escribe `state.runs`; UI thread lee. Mutex o cola SPSC.
- Memory: si runs > 1000 historicos, paginar (LRU 200 visibles). Antiguos se descargan on-demand via `GET /api/runs?since=...`.
- App `chip color`: mapear app_name -> accent del trio (kanban_cpp `#a855f7`, skill_tree color actual). Cache.
- Click row durante streaming SSE: no resetear seleccion al refrescar lista; preservar por `run_id`.
## Out of scope
- Editor inline de DoD desde timeline (eso es panel DoD 0117).
- Export CSV/JSON.
- Notificaciones desktop al cambiar status.
@@ -0,0 +1,102 @@
---
id: "0119"
title: "kanban_cpp: leer dev/issues + dev/flows como cards (sync layer)"
status: pendiente
type: feature
domain:
- cpp-stack
- kanban
- dev-loop
scope: app
priority: alta
depends:
- "0112"
blocks: []
related:
- "0008"
- "0109m"
- "0114"
created: 2026-05-18
updated: 2026-05-18
tags: [kanban, issues, flows, sync, frontmatter]
flow: "0008"
---
# 0119 — kanban_cpp sync layer: issues + flows como cards
## Problema
kanban_cpp (0112) clona el backend de kanban_web con `operations.db` vacia. Pero la intencion del flow 0008 es **gestionar issues y flows directamente desde kanban_cpp**. Sin sync con `dev/issues/*.md` + `dev/flows/*.md`, el board nace vacio y el usuario tendria que recrear cards a mano.
## Decision
Anadir endpoint al backend de kanban_cpp que lee frontmatter de los `.md` y los expone como cards/columnas virtuales. Lectura directa de filesystem (no via `issues_api` 0109m — el API service aun no existe; cuando exista, swap).
### Mapping `.md` -> card
| Frontmatter | Campo card |
|---|---|
| `id` | `card.external_id` |
| `title` | `card.title` |
| `status` (`pendiente`/`en-curso`/`done`/`deferred`) | `column` (mapping) |
| `priority` (alta/media/baja) | `card.priority` |
| `type` (feature/bug/chore/app) | `card.tag` |
| `tags` | `card.tags` |
| `flow` | `card.flow_id` |
| `dod_evidence_schema` (0114) | `card.dod_items[]` |
| body `## Problema` + `## Decision` | `card.description` |
### Mapping status -> columna kanban
```
pendiente -> Backlog
en-curso -> Doing
en-revisión -> Review
done -> Done
deferred -> Deferred
```
### Tableros separados
- Board `issues`: source = `dev/issues/*.md`.
- Board `flows`: source = `dev/flows/*.md`, columnas distintas (Pending / Running / Done / Deferred). flow body trae `## Definition of Done (user-facing, 4 surfaces obligatorios)`.
### Endpoints nuevos backend kanban_cpp
- `GET /api/boards/issues/cards` — lee `dev/issues/*.md`, cachea 30s.
- `GET /api/boards/flows/cards` — lee `dev/flows/*.md`, cachea 30s.
- `PATCH /api/boards/<board>/cards/<id>` — escribe vuelta al `.md` (cambia `status` en frontmatter). Usa `edit_yaml_frontmatter_go_core` del registry (verificar existe; si no, crear via fn-constructor).
- `POST /api/boards/<board>/cards/<id>/launch` — proxy a `agent_runner_api:8486/api/runs`.
### Watcher de cambios
`fsnotify` sobre `dev/issues/` + `dev/flows/`. Cuando hay write -> invalida cache + push SSE a UI -> kanban_cpp refresca board.
## Criterios de aceptacion
- [ ] `apps/kanban_cpp/backend/issues_source.go` parsea frontmatter via funcion del registry.
- [ ] `apps/kanban_cpp/backend/flows_source.go` idem para flows.
- [ ] Endpoints arriba implementados con tests.
- [ ] `fsnotify` watcher activo; cambio en `.md` propaga a UI en < 2s.
- [ ] PATCH actualiza solo el campo `status` del frontmatter, preserva resto.
- [ ] Round-trip: PATCH `/cards/0113` status=`en-curso` -> file mtime cambia -> `dev/issues/0113-*.md` tiene `status: en-curso`.
- [ ] Click `Launch` en card invoca `agent_runner_api` con `issue_id` + DoD items extraidos del frontmatter.
- [ ] Tag taxonomy issue 0103 respetada — solo statuses canonicos.
- [ ] kanban_cpp UI muestra ambos boards (tabs `Issues` / `Flows`) con cards reales del repo al arrancar.
- [ ] e2e_check: crear `.md` dummy -> aparece en board en <5s.
## Gotchas
- Frontmatter mal formado (yaml invalid): card aparece con badge `parse-error` + tooltip detalle. NO crashea backend.
- Cambios concurrentes: humano edita `.md` con vim mientras agente lo PATCHea via API. Usar lock file `.md.lock` corto + retry.
- `fsnotify` no funciona bien en WSL para `/mnt/c/` paths. Backend corre en WSL, lee paths nativos (`/home/lucas/fn_registry/dev/...`). Verificar.
- Issue / flow grandes (>500 lineas body): NO mandar full body en `/cards` (lento). Devolver solo frontmatter + primeras 5 lineas. Body completo via `/cards/<id>` on demand.
- Cards sin `status` (frontmatter incompleto): default `pendiente`.
- Reordenamiento manual: drag-and-drop en UI cambia status (cruzar columna) PERO no orden vertical persistente — no hay campo `order` en frontmatter. Decision: orden por `updated` desc dentro de cada columna.
## Out of scope
- Sync bidireccional con `issues_api` (0109m) — cuando exista, swap filesystem por API call.
- Conflict resolution manual (3-way merge UI).
- Edicion inline del body markdown desde la card (solo PATCH de `status` + `priority` por ahora).
- Bulk operations (mover N cards a la vez).
+11
View File
@@ -115,3 +115,14 @@
| [0083](0083-imagegen-spike02-cross-validation.md) | imagegen — notebook 02 validacion cruzada diffusers vs sdcpp_python | pendiente | alta | feature | — |
| [0084](0084-imagegen-studio-go-app.md) | imagegen_studio — app Go binario producto (Fase 3 plan stack) | pendiente | media | feature | 0082 |
| [0099](0099-datahub-app-launcher.md) | datahub — launcher central para arrancar todas las apps del registry | pendiente | alta | feature | — |
| [0105](0105-service-frontmatter-standardization.md) | Estandarizar bloque `service:` en app.md + indexer + `fn doctor services-spec` | in-progress | alta | feature | — |
| [0106](0106-services-monitor-app.md) | App `services_monitor`: dashboard cross-PC de services activos | in-progress | alta | app | 0105 |
| [0107](completed/0107-modules-standardization.md) | Estandarizar sistema de modulos C++: drift + split data_table + tiers + version pinning + docs API | completado | alta | refactor | flag `modules-v2` activo |
| [0107a](completed/0107a-fn-doctor-modules.md) | `fn doctor modules` — detectar drift uses_modules vs uses_functions | completado | alta | feature | parte de 0107 |
| [0107b](completed/0107b-clean-data-table-consumers.md) | Limpiar uses_functions de 8 apps consumidoras de data_table (67 entradas) | completado | alta | refactor | parte de 0107 |
| [0107c](completed/0107c-split-data-table.md) | Partir `modules/data_table/data_table.cpp` (4777 LOC) en sub-funciones del registry | completado | alta | refactor | parte de 0107 |
| [0107d](completed/0107d-module-tiers-policy.md) | Tiers — members vs uses_functions en module.md | completado | alta | refactor | parte de 0107 |
| [0107e](0107e-version-pinning-codegen.md) | uses_modules con min_version + codegen fail-loud | pendiente | media | feature | follow-up de 0107 |
| [0107f](completed/0107f-modules-api-docs.md) | `modules/README.md` + `docs/MODULES_API.md` — contrato publico de modulos | completado | alta | docs | parte de 0107 |
| [0107g](completed/0107g-migrate-inline-begintable.md) | Migrar inline `ImGui::BeginTable` a `data_table::render` (4/8 migradas + 4 abortadas con razon tecnica + 9 LAYOUT-TABLE comentados) | completado | media | refactor | parte de 0107 |
| [0108](completed/0108-tables-playground-aggressive-testbed.md) | App `tables_qa` — testbed agresivo data_table: 10 tabs + panel QA flotante + perf tests (1K..10M filas) + Run Tests + apphub + tables_playground deprecado | completado | alta | app | depende 0107 |
@@ -0,0 +1,260 @@
---
id: "0107"
title: "Estandarizar sistema de modulos C++: limpiar drift data_table + politica API + version pinning + /version command"
status: pendiente
type: refactor
domain:
- meta
- cpp-stack
- tooling
scope: multi-app
priority: alta
depends: []
blocks:
- "0108"
related:
- "0097"
- "0081"
- "0086"
created: 2026-05-17
updated: 2026-05-17
tags: [modules, cpp, data-table, framework, refactor, fn-doctor, versioning]
---
# 0107 — Estandarizar sistema de modulos C++
## Problema
Auditoria 2026-05-17 sobre `modules/` (framework + data_table) revela que el sistema de modulos C++ esta a medio camino: la idea (modulos opt-in versionados con manifest auditable) es solida, pero su implementacion tiene fugas que invalidan el contrato.
### Drift uses_modules ↔ uses_functions (7/7 apps consumidoras)
`module.md` dice "cuando declaras `uses_modules`, NO repetir los miembros en `uses_functions`". Realidad medida hoy:
| App | uses_functions total | miembros data_table duplicados |
|---|---|---|
| services_monitor | 12 | 12 |
| dag_engine_ui | 13 | 12 |
| odr_console | 5 | 5 |
| navegator_dashboard | 20 | 12 |
| graph_explorer | 42 | 12 |
| registry_dashboard | 37 | 11 |
| app_gestion | 12 | 12 |
7 de 7 apps violan la regla clave que justifica el sistema. `fn doctor cpp-apps` no detecta el drift.
### `data_table.cpp` = 4777 LOC
El "modulo" es un god-file con UI entera (chips, viz, grid, drill, joins, AI, button, color rules) dentro de un `.cpp`. Imposible auditar consumidores parciales, imposible registrar miembros como funciones reales del registry (cada uno con su `.md`).
### Boundary modulo vs funcion borrosa
`lua_engine`, `llm_anthropic`, `join_tables` son members de `data_table`. Pero lua/llm/join son utiles fuera de tablas. Forzar membership infla el surface del modulo y obliga a las apps a tragarse lua+llm+anthropic+join aunque solo quieran render simple. No hay tier "data_table_core" vs "data_table_full".
### Versionado declarado, no enforced
`module.md` tiene `version: 1.4.0`. `app.md` dice `uses_modules: [data_table_cpp]` sin version. Bump breaking de modulo → todas las apps rompen sin warning hasta compile error.
### Codegen silencioso
`execute_process(... codegen_app_modules)` emite WARNING solo si rc != 0 y != 2. Si Python falta → stub vacio sin error. About panel muestra "0 modules" en apps que SI deberian tener 1.
### Hard dep oculto
`fn_module_data_table` linkea `fn_framework` PRIVATE para `fn::local_path()`. `module.md` no lo documenta como precondicion publica. Si alguien intenta usar el modulo en una app no-framework, falla en link sin mensaje claro.
### Sin doc API de modulos
No hay un sitio canonico que diga "para usar el modulo X, incluye Y.h, llama X::render(...), pasa State Z". Cada modulo lo improvisa en su `module.md`.
### Sin /version command
No hay flujo estandar para bumpear semver de un modulo o framework. Cada PR lo hace a ojo, sin coherencia entre `module.md::version` y `## Capability growth log`.
## Decision
Issue desglosado en 6 sub-issues independientes detras de feature flag `modules-v2`:
1. **0107a**`fn doctor modules` que detecta drift uses_modules vs uses_functions.
2. **0107b** — Limpiar `uses_functions` de las 7 apps consumidoras de data_table (eliminar miembros duplicados).
3. **0107c** — Partir `modules/data_table/data_table.cpp` (4777 LOC) en sub-funciones del registry (`data_table_chips`, `data_table_grid`, `data_table_viz_panels`, `data_table_drill`, `data_table_ai_panel`, `data_table_color_rules`). Cada una con `.md` propio. Entrypoint queda thin.
4. **0107d** — Mover members generales (`lua_engine`, `llm_anthropic`, `join_tables`, `auto_detect_type`) fuera de `data_table` module. Quedan funciones sueltas que el modulo USA pero no posee. Crear tiers explicitos.
5. **0107e** — Version pinning en `uses_modules` (`uses_modules: [{name: data_table_cpp, min_version: "1.4"}]`) + codegen fail-loud (error si Python falta o count=0 cuando deberia ser >0).
6. **0107f**`modules/README.md` (catalogo) + `docs/MODULES_API.md` (contrato publico por modulo: header path, namespace, entry function, State struct, lifecycle).
Ademas:
- `/version` slash command para bumpear semver de modulo/framework consistentemente (`module.md::version` + `## Capability growth log` + git commit).
- `/fix-issue` referenciara `/version` cuando el cambio toque framework/modules.
Feature flag `modules-v2` activado solo cuando 0107a-f cierran. Antes de cerrar, recompilar TODAS las apps cpp para verificar que el refactor no rompe linkage. Aceptamos coste de recompilacion total.
## Restriccion explicita
Prohibido empezar `chat_ia` (proximo modulo planeado) hasta que 0107 cierre. Razon: si arrancamos otro modulo sin estandar estable, replicamos los mismos bugs en el doble de superficie.
## Tareas (resumen — detalle en sub-issues)
- [ ] **1** Auditoria automatizada → `fn doctor modules` (0107a)
- [ ] **2** Limpiar drift en 7 apps consumidoras (0107b)
- [ ] **3** Partir `data_table.cpp` en sub-funciones del registry (0107c)
- [ ] **4** Politica members generales + tiers (0107d)
- [ ] **5** Version pinning + codegen fail-loud (0107e)
- [ ] **6** Docs API publica modulos (0107f)
- [ ] **7** Recompilar todas las apps cpp + verificar smoke (al cerrar)
- [ ] **8** Activar feature flag `modules-v2`
- [ ] **9** `/version` + `/fix-issue` (no son sub-issues; tareas inline en este issue principal)
## Desglose multi-issue
| Sub-issue | Rama | Alcance | Estado |
|-----------|------|---------|--------|
| 0107a-fn-doctor-modules | issue/0107a-fn-doctor-modules | Check drift uses_modules vs uses_functions + version skew | pendiente |
| 0107b-clean-data-table-consumers | issue/0107b-clean-data-table-consumers | Eliminar miembros duplicados en 7 app.md | pendiente |
| 0107c-split-data-table | issue/0107c-split-data-table | Partir data_table.cpp 4777 LOC en sub-funciones del registry | pendiente |
| 0107d-module-tiers-policy | issue/0107d-module-tiers-policy | Sacar lua/llm/join del modulo data_table; tiers + politica | pendiente |
| 0107e-version-pinning-codegen | issue/0107e-version-pinning-codegen | min_version en uses_modules + codegen fail-loud | pendiente |
| 0107f-modules-api-docs | issue/0107f-modules-api-docs | modules/README.md + docs/MODULES_API.md | pendiente |
### Feature flag
Nombre: `modules-v2`
Se activa cuando 0107a-f cierran + recompilacion total verificada + `fn doctor modules` reporta 0 drift.
### Progreso por tarea
- [ ] **1.1** Implementar check drift en `fn doctor cpp-apps` o sub-comando nuevo — 0107a
- [ ] **1.2** Output JSON con apps que violan regla — 0107a
- [ ] **1.3** Tests sobre fixture sintetica (1 modulo, 3 apps simuladas) — 0107a
- [ ] **2.1** Editar 7 `app.md` removiendo miembros data_table — 0107b
- [ ] **2.2** Verificar build pasa post-clean (no rompe nada — solo metadata) — 0107b
- [ ] **3.1** Identificar fronteras funcionales en data_table.cpp 4777 LOC — 0107c
- [ ] **3.2** Crear `cpp/functions/viz/data_table_chips.cpp` + .h + .md — 0107c
- [ ] **3.3** Crear `cpp/functions/viz/data_table_grid.cpp` + .h + .md — 0107c
- [ ] **3.4** Crear `cpp/functions/viz/data_table_viz_panels.cpp` + .h + .md — 0107c
- [ ] **3.5** Crear `cpp/functions/viz/data_table_drill.cpp` + .h + .md — 0107c
- [ ] **3.6** Crear `cpp/functions/viz/data_table_ai_panel.cpp` + .h + .md — 0107c
- [ ] **3.7** Crear `cpp/functions/viz/data_table_color_rules.cpp` + .h + .md — 0107c
- [ ] **3.8** `data_table.cpp` queda como entrypoint thin que compone las sub-funciones — 0107c
- [ ] **3.9** Bump `module.md::version` a 2.0.0 (breaking interno, API publica `data_table::render` intacta) — 0107c
- [ ] **4.1** Crear `cpp/functions/core/lua_engine.cpp` (ya existe) como funcion suelta; quitar de `module.md::members` — 0107d
- [ ] **4.2** Idem `llm_anthropic`, `join_tables`, `auto_detect_type` — 0107d
- [ ] **4.3** Actualizar `modules/data_table/CMakeLists.txt`: estos `.cpp` ya no se enlazan dentro del modulo; apps que los necesiten los anaden a su CMakeLists — 0107d
- [ ] **4.4** Definir tiers en `module.md`: `core_members` (esenciales) vs `optional_members` (deps externas pesadas) — 0107d
- [ ] **5.1** Parser `app.md::uses_modules` acepta string corto y dict largo — 0107e
- [ ] **5.2** Codegen comprueba `min_version` vs `module.md::version` — error si no cumple — 0107e
- [ ] **5.3** Codegen: si `Python3 NOT FOUND` y app tiene `uses_modules` → CMake FATAL_ERROR — 0107e
- [ ] **5.4** Codegen: si parser devuelve count=0 pero app.md declara `uses_modules` no-vacio → FATAL_ERROR — 0107e
- [ ] **6.1** `modules/README.md` con tabla modulos + version + descripcion + link a contrato — 0107f
- [ ] **6.2** `docs/MODULES_API.md` con contrato canonico (template + ejemplos data_table + framework) — 0107f
- [ ] **6.3** Actualizar `.claude/rules/cpp_apps.md` referenciando `docs/MODULES_API.md` — 0107f
- [ ] **7.1** `redeploy_all_cpp_apps_bash_pipelines` + verificar 0 errores de link — issue principal, al cerrar
- [ ] **7.2** Smoke manual de cada app con `data_table::render` — issue principal, al cerrar
- [ ] **8** Flip `modules-v2: enabled: true` en `dev/feature_flags.json` — issue principal
- [ ] **9.1** Crear `.claude/commands/version.md` (slash command bump semver) — issue principal, ya en este turno
- [ ] **9.2** Crear `.claude/commands/fix-issue.md` que referencie `/version` — issue principal, ya en este turno
## Arquitectura
### Archivos afectados
**0107a** (`fn doctor modules`):
- `functions/infra/audit_modules_drift.go` (NEW) — funcion del registry
- `functions/infra/audit_modules_drift.md` (NEW)
- `cmd/fn/doctor.go` — subcomando `modules`
- `apps/registry_mcp/...` — exponer via `mcp__registry__fn_doctor subcommand="modules"`
**0107b**:
- 7 `app.md` editados: services_monitor, dag_engine_ui, odr_console, navegator_dashboard, graph_explorer, registry_dashboard, app_gestion.
**0107c**:
- `modules/data_table/data_table.cpp` — pasa de 4777 LOC a ~400 (entrypoint que compone).
- `cpp/functions/viz/data_table_chips.cpp/.h/.md` (NEW) — ~600 LOC
- `cpp/functions/viz/data_table_grid.cpp/.h/.md` (NEW) — ~1200 LOC
- `cpp/functions/viz/data_table_viz_panels.cpp/.h/.md` (NEW) — ~800 LOC
- `cpp/functions/viz/data_table_drill.cpp/.h/.md` (NEW) — ~300 LOC
- `cpp/functions/viz/data_table_ai_panel.cpp/.h/.md` (NEW) — ~500 LOC
- `cpp/functions/viz/data_table_color_rules.cpp/.h/.md` (NEW) — ~400 LOC
- `modules/data_table/module.md` — bump version + actualizar members.
- `modules/data_table/CMakeLists.txt` — anadir las sub-funciones a la static lib.
**0107d**:
- `modules/data_table/module.md` — quitar `lua_engine`, `llm_anthropic`, `join_tables`, `auto_detect_type` de members.
- `modules/data_table/CMakeLists.txt` — estos `.cpp` salen.
- Apps consumidoras que necesiten lua/llm/join → declarar en `uses_functions` + anadir el `.cpp` a su CMake.
**0107e**:
- `python/functions/infra/codegen_app_modules.py` — soporte dict largo `{name, min_version}`.
- `cpp/CMakeLists.txt::add_imgui_app` — fail-loud en codegen errors.
- `registry/parser.go` — indexer entiende dict largo.
**0107f**:
- `modules/README.md` (NEW)
- `docs/MODULES_API.md` (NEW)
- `.claude/rules/cpp_apps.md` — link nuevo doc.
**Slash commands** (este issue):
- `.claude/commands/version.md` (NEW)
- `.claude/commands/fix-issue.md` (NEW si no existe)
### pkg/ puro vs shell/ impuro
`audit_modules_drift_go_infra` (0107a) es **impuro** — lee `registry.db` + filesystem (`app.md`, `module.md`). Vive en `functions/infra/`. Core logico (comparar listas de IDs, detectar miembros duplicados) es **puro** y vive como sub-funcion interna del paquete `infra`.
## Ejemplo de uso
```bash
# 1. Detectar drift
fn doctor modules
# Output:
# Module drift report
# ===================
# data_table_cpp (v1.4.0): 7/7 consumers list members in uses_functions
# services_monitor — duplicates: data_table_cpp_viz, viz_render_cpp_viz, ...
# dag_engine_ui — duplicates: ...
# Total apps with drift: 7
# Total modules: 2
# Exit: 1
# 2. Bump version de un modulo (slash command)
/version modules/data_table minor "split data_table.cpp into 6 sub-functions; API publica intacta"
# - Detecta version actual en module.md (1.4.0)
# - Calcula proxima (1.5.0 si minor, 2.0.0 si major)
# - Anade entrada a ## Capability growth log con fecha de hoy
# - Stage en git pero NO commit
# 3. Pinning version en una app
# app.md:
# uses_modules:
# - name: data_table_cpp
# min_version: "1.4"
# Codegen falla en cmake si module.md::version < 1.4
# 4. fix-issue referencia /version
/fix-issue 0107c
# Flow normal del fix-issue + "¿este cambio bumpea version de modulo/framework? si si → /version"
```
## Decisiones de diseno
1. **No tocar API publica `data_table::render(...)`**. Refactor interno. Apps existentes no cambian su llamada.
2. **Aceptamos recompilacion total**. El usuario lo dijo explicito. Coste de tiempo razonable a cambio de limpieza.
3. **Feature flag `modules-v2` no protege codigo runtime** — protege la "promesa" del sistema. Cuando flag flip, `fn doctor modules` debe pasar verde.
4. **`/version` NO hace commit**. Solo edita archivos + stage. El commit final lo hace el flujo normal del fix-issue.
5. **Bloqueamos `chat_ia`**. Si saltamos a otro modulo sin estandar, el caos se duplica.
## Prerequisitos
- Issue 0097 (modules infra inicial) — completado, esto es la evolucion.
- `fn doctor cpp-apps` existe (0081) — reutilizamos paths.
## Riesgos
- **Refactor 4777 LOC**: alto riesgo de regresion visual/funcional. Mitigacion: smoke manual app-por-app + `primitives_gallery --capture` golden images antes/despues.
- **Apps que dependen indirectamente de lua/llm/join** sin declararlo: post-0107d podrian fallar en link. Mitigacion: `fn doctor uses-functions` + recompilacion total como gate.
- **Codegen fail-loud** rompe builds que hoy pasan con stub: deliberado, parte de la idea — pero hay que arreglar todos los app.md antes de mergear 0107e.
## Notas
- `/version` se referenciara desde `/fix-issue` con prompt: "este cambio toca `modules/` o `cpp/framework/`? si si → run `/version <path> [major|minor|patch] [reason]` antes de commit".
- Politica: bump de modulo SIN bump de version = bug. `fn doctor modules` lo detectara via diff hash de `members` + `description` vs ultima version registrada.
@@ -0,0 +1,82 @@
---
id: "0107a"
title: "fn doctor modules — detectar drift uses_modules vs uses_functions y version skew"
status: pendiente
type: feature
domain:
- meta
- cpp-stack
- tooling
scope: registry
priority: alta
depends: []
blocks:
- "0107b"
related:
- "0107"
created: 2026-05-17
updated: 2026-05-17
tags: [modules, fn-doctor, drift, audit, cpp]
---
# 0107a — `fn doctor modules`
Parte del issue principal [0107](0107-modules-standardization.md). Feature flag `modules-v2`.
## Objetivo
Subcomando `fn doctor modules` + funcion del registry `audit_modules_drift_go_infra` que detecta:
1. App declara `uses_modules: [X]` Y un miembro de X aparece en `uses_functions` → drift.
2. App declara `uses_modules: [X]` pero su `CMakeLists.txt` NO linkea `fn_module_X` → mismatch.
3. App linkea `fn_module_X` pero NO declara `uses_modules: [X]` → mismatch inverso.
4. App declara `uses_modules: [{name: X, min_version: "1.4"}]` y `module.md::version` < 1.4 → version skew (post 0107e).
## Tareas
- [ ] **1.1** `functions/infra/audit_modules_drift.go` con firma:
```go
type ModuleDriftReport struct {
ModuleID string
ModuleVersion string
ConsumersTotal int
ConsumersWithDrift int
Violations []DriftViolation
}
type DriftViolation struct {
AppID string
Kind string // "duplicate_members" | "uses_modules_no_link" | "link_no_uses_modules" | "version_skew"
DuplicatedIDs []string
Message string
}
func AuditModulesDrift(registryDB string, cppRoot string) ([]ModuleDriftReport, error)
```
- [ ] **1.2** `.md` correspondiente con frontmatter completo + ejemplo lanzable.
- [ ] **1.3** Subcomando en `cmd/fn/doctor.go`: `fn doctor modules` + `fn doctor modules --json`.
- [ ] **1.4** Exponer via MCP: `mcp__registry__fn_doctor subcommand="modules"`.
- [ ] **1.5** Test sintetico: fixture con 1 modulo + 3 apps (1 limpia, 1 con drift de duplicados, 1 con version skew).
- [ ] **1.6** Anadir entrada a `.claude/rules/fn_doctor.md` mapeando subcomando.
## Output esperado (texto)
```
fn doctor modules
=================
data_table_cpp v1.4.0 — 7 consumers
services_monitor DRIFT 12 duplicated members in uses_functions
dag_engine_ui DRIFT 12 duplicated members in uses_functions
odr_console DRIFT 5 duplicated members in uses_functions
navegator_dashboard DRIFT 12 duplicated members in uses_functions
graph_explorer DRIFT 12 duplicated members in uses_functions
registry_dashboard DRIFT 11 duplicated members in uses_functions
app_gestion DRIFT 12 duplicated members in uses_functions
framework_cpp v1.1.0 — 0 explicit consumers (transitive via add_imgui_app)
Summary: 2 modules, 7 apps with drift, 0 version skews.
Exit: 1 (drift detected)
```
## Riesgos
- Falso positivo si un app legitimamente necesita un miembro fuera del scope del modulo (ej. usar `lua_engine` standalone). Mitigacion: post-0107d (members generales fuera del modulo), este caso desaparece. Mientras tanto: flag `--ignore-known` con allowlist temporal.
@@ -0,0 +1,73 @@
---
id: "0107b"
title: "Limpiar uses_functions de 7 apps consumidoras de data_table (eliminar miembros duplicados)"
status: pendiente
type: refactor
domain:
- meta
- cpp-stack
scope: multi-app
priority: alta
depends:
- "0107a"
blocks: []
related:
- "0107"
created: 2026-05-17
updated: 2026-05-17
tags: [modules, drift, app-md, cleanup]
---
# 0107b — Limpiar drift en 7 apps consumidoras
Parte del issue principal [0107](0107-modules-standardization.md).
## Objetivo
Eliminar de `uses_functions` en 7 `app.md` los IDs que ya son miembros de `data_table` module (declarado en `uses_modules`).
## Apps afectadas
| App | Path | Drift count |
|---|---|---|
| services_monitor | apps/services_monitor/app.md | 12 |
| dag_engine_ui | apps/dag_engine_ui/app.md | 12 |
| odr_console | projects/online_data_recopilation/apps/odr_console/app.md | 5 |
| navegator_dashboard | projects/navegator/apps/navegator_dashboard/app.md | 12 |
| graph_explorer | projects/osint_graph/apps/graph_explorer/app.md | 12 |
| registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/app.md | 11 |
| app_gestion | apps/app_gestion/app.md | 12 |
## Miembros a quitar (segun module.md de data_table v1.4)
- `data_table_cpp_viz`
- `compute_stage_cpp_core`
- `compute_pipeline_cpp_core`
- `compute_column_stats_cpp_core`
- `tql_emit_cpp_core`
- `tql_helpers_cpp_core`
- `tql_apply_cpp_core`
- `tql_to_sql_cpp_core`
- `lua_engine_cpp_core` (hasta 0107d que lo saca del modulo)
- `join_tables_cpp_core` (idem)
- `auto_detect_type_cpp_core` (idem)
- `llm_anthropic_cpp_core` (idem)
- `viz_render_cpp_viz`
NOTA: 0107d sacara lua/join/auto_detect/llm del modulo. Cuando eso pase, esas apps DEBEN volver a anadirlos a `uses_functions` (si los usan directamente). 0107b limpia el estado actual contra `module.md` v1.4; despues de 0107d se ejecuta `fn doctor modules` otra vez y se ajusta.
## Tareas
- [ ] **2.1** Para cada app.md, eliminar las lineas listadas en "Miembros a quitar" del bloque `uses_functions`.
- [ ] **2.2** `fn index` despues.
- [ ] **2.3** Verificar con `fn doctor modules` que `services_monitor` etc. reportan 0 drift.
- [ ] **2.4** Build completo de las 7 apps. Linkage NO debe cambiar (los .cpp seguian viniendo via `fn_module_data_table` enlazado en su CMake).
- [ ] **2.5** Smoke manual rapido (lanzar y cerrar) de cada app.
## Riesgos
- Si `fn doctor uses-functions` se ejecuta antes de que `uses_modules` se entienda como cobertura, marcara las apps como "missing imports". Mitigacion: arreglar primero `audit_uses_functions_go_infra` para que considere `uses_modules` como cobertura transitiva. Tarea inline 2.0 antes de 2.1.
## Notas
- Es solo metadata. No toca codigo, no rompe build. Coste = editar 7 archivos + fn index.
@@ -0,0 +1,74 @@
---
id: "0107c"
title: "Partir modules/data_table/data_table.cpp (4777 LOC) en sub-funciones del registry"
status: pendiente
type: refactor
domain:
- cpp-stack
- meta
scope: module
priority: alta
depends: []
blocks:
- "0107d"
related:
- "0107"
- "0081"
created: 2026-05-17
updated: 2026-05-17
tags: [modules, data-table, refactor, registry, viz]
---
# 0107c — Partir `data_table.cpp` 4777 LOC
Parte del issue principal [0107](0107-modules-standardization.md).
## Problema
`modules/data_table/data_table.cpp` es un god-file de 4777 LOC con UI entera dentro: barra de chips, tabla grid, paneles de viz, drill-down, joins, panel Ask AI, button event sink, color rules, breadcrumb, tooltips. Imposible auditar consumidores parciales. Cada bloque deberia ser una funcion del registry con su `.md` propio (Ejemplo + Cuando usarla + Gotchas).
## Decision
Partir en **6 sub-funciones** dentro de `cpp/functions/viz/` (no en `modules/data_table/` — son funciones del registry reutilizables). El modulo bundla las 6 + el entrypoint thin.
| Sub-funcion | LOC objetivo | Responsabilidad |
|---|---|---|
| `data_table_chips_cpp_viz` | ~600 | Barra de chips superior: filtros activos, TQL preview, save/load query |
| `data_table_grid_cpp_viz` | ~1200 | Render del grid: cells, sorting, freeze cols, declarative renderers (Badge/Progress/Duration/Icon/Button/Dots/CategoricalChip/ColorScale) |
| `data_table_viz_panels_cpp_viz` | ~800 | Paneles de viz lateral: histograms, line, scatter, value-counts |
| `data_table_drill_cpp_viz` | ~300 | Drill-down stack + breadcrumb |
| `data_table_ai_panel_cpp_viz` | ~500 | Panel Ask AI: prompt, llamada a `llm_anthropic`, render respuesta, export |
| `data_table_color_rules_cpp_viz` | ~400 | Editor de reglas de color por columna + aplicacion |
Entrypoint `data_table.cpp` queda ~400 LOC: compone las 6 sub-funciones, gestiona `State`, dispatcher de eventos.
## Tareas
- [ ] **3.1** Leer `data_table.cpp` end-to-end e identificar fronteras funcionales (comentario header de cada bloque sirve).
- [ ] **3.2** `cpp/functions/viz/data_table_chips.cpp` + `.h` + `.md` (NEW).
- [ ] **3.3** `cpp/functions/viz/data_table_grid.cpp` + `.h` + `.md` (NEW).
- [ ] **3.4** `cpp/functions/viz/data_table_viz_panels.cpp` + `.h` + `.md` (NEW).
- [ ] **3.5** `cpp/functions/viz/data_table_drill.cpp` + `.h` + `.md` (NEW).
- [ ] **3.6** `cpp/functions/viz/data_table_ai_panel.cpp` + `.h` + `.md` (NEW).
- [ ] **3.7** `cpp/functions/viz/data_table_color_rules.cpp` + `.h` + `.md` (NEW).
- [ ] **3.8** Reducir `data_table.cpp` a entrypoint thin que llama las 6.
- [ ] **3.9** `modules/data_table/CMakeLists.txt`: anadir las 6 sub-funciones a la static lib.
- [ ] **3.10** `modules/data_table/module.md`: bump `version: 2.0.0` (breaking interno, API publica intacta) + extender `members:` con las 6 nuevas + entrada en `## Capability growth log`.
- [ ] **3.11** Recompilar TODAS las apps que linkean `fn_module_data_table` (7 apps).
- [ ] **3.12** Smoke manual de cada app: tabla renderiza, chips funcionan, viz panels OK, AI panel OK, drill OK, color rules OK.
- [ ] **3.13** `primitives_gallery --capture` golden image antes vs despues — diff visual cero.
## Riesgos
- **State struct**: hoy `data_table::State` es opaco interno del .cpp. Tras split, las 6 sub-funciones necesitan acceder a partes de el. Opciones:
- (a) Mover `State` a `data_table_types.h` publico (mas exposicion pero claro).
- (b) Definir `State` en `data_table.h` con sub-structs por sub-funcion (`State::chips_state`, `State::grid_state`, etc.) y cada sub-funcion recibe su sub-state.
- **Recomendacion**: (b). Mantiene encapsulacion y cada sub-funcion tiene firma clara.
- **API publica `data_table::render(...)` no cambia**. Es la regla dura. Si la firma debe cambiar, ya no es 0107c sino issue nuevo con migration plan.
- **Tiempo de refactor**: 4777 LOC → 6 archivos requiere cuidado quirurgico. Lanzamos `fn-constructor` en paralelo.
## Notas
- Las 6 sub-funciones son `purity: impure` (manipulan ImGui state global).
- Cada `.md` con `tags: [viz, table, imgui, ui]` + `framework: imgui`.
- El refactor lo hara un sub-agente fn-constructor lanzado en paralelo desde el flujo principal del issue 0107.
@@ -0,0 +1,91 @@
---
id: "0107d"
title: "Sacar lua_engine/llm_anthropic/join_tables/auto_detect_type del modulo data_table — politica de tiers"
status: pendiente
type: refactor
domain:
- cpp-stack
- meta
scope: module
priority: alta
depends:
- "0107c"
blocks: []
related:
- "0107"
created: 2026-05-17
updated: 2026-05-17
tags: [modules, data-table, policy, tiers, lua, llm]
---
# 0107d — Politica members generales + tiers
Parte del issue principal [0107](0107-modules-standardization.md).
## Problema
`data_table` module hoy lleva como miembros `lua_engine`, `llm_anthropic`, `join_tables`, `auto_detect_type`. Esos 4 son **utiles fuera de tablas**:
- `lua_engine`: scripting general.
- `llm_anthropic`: LLM wrapper, util en chat_ia (proximo modulo) y otros.
- `join_tables`: util en cualquier app que combine tablas (no solo data_table::render).
- `auto_detect_type`: util en data import generico.
Forzar membership infla el modulo y obliga a las apps a tragarse 4 deps pesadas (lua + curl + libllm + http) aunque solo quieran render basico.
## Decision
Politica de tiers:
```yaml
# module.md (post-0107d)
members:
# core_members: esenciales, sin ellos no hay funcionalidad
- data_table_chips_cpp_viz
- data_table_grid_cpp_viz
- data_table_drill_cpp_viz
- data_table_color_rules_cpp_viz
- data_table_viz_panels_cpp_viz
- compute_stage_cpp_core
- compute_pipeline_cpp_core
- compute_column_stats_cpp_core
- tql_emit_cpp_core
- tql_helpers_cpp_core
- tql_apply_cpp_core
- tql_to_sql_cpp_core
- viz_render_cpp_viz
uses_functions:
# Deps externas usadas por el modulo (no son miembros del modulo)
- lua_engine_cpp_core # TQL scripting (opt-in via feature flag interno)
- llm_anthropic_cpp_core # Ask AI panel (opt-in via FN_LLM_ANTHROPIC)
- join_tables_cpp_core # Joins
- auto_detect_type_cpp_core # Detect tipos al cargar nueva tabla
```
Distincion:
- **`members`**: funciones que el modulo POSEE — viven en `cpp/functions/viz/` y nadie mas las usa directamente (renderizan dentro del modulo).
- **`uses_functions`**: funciones que el modulo CONSUME — viven en `cpp/functions/core/`, son utiles fuera del modulo, otras apps pueden importarlas directamente.
Apps consumidoras de `data_table`:
- Si solo llaman `data_table::render(...)` → solo `uses_modules: [data_table_cpp]`, nada mas.
- Si ademas usan `lua_engine` directamente para sus propios scripts → anaden `lua_engine_cpp_core` a `uses_functions` (no es duplicado, es uso directo independiente).
## Tareas
- [ ] **4.1** Editar `modules/data_table/module.md`: separar `members` core de `uses_functions`. Bump version 2.1.0.
- [ ] **4.2** Editar `modules/data_table/CMakeLists.txt`: `lua_engine.cpp`, `llm_anthropic.cpp`, `join_tables.cpp`, `auto_detect_type.cpp` quedan dentro de la static lib (el modulo los usa internamente), pero el linkage transitivo se controla via PUBLIC vs PRIVATE. Si una app NO usa directamente lua/llm fuera del modulo, igual los recibe pero solo como impl detail del modulo. Decidir: SI mantenemos lua/llm/join dentro de la static lib del modulo (PUBLIC link) o sacamos al app para que cada una decida (PRIVATE en modulo, app linkea por su lado).
- [ ] **4.3** Documentar tier en `docs/MODULES_API.md` (0107f).
- [ ] **4.4** `fn doctor modules` (0107a) entiende la distincion members vs uses_functions y NO marca drift cuando una app lista en `uses_functions` algo que el modulo declara en su `uses_functions` (no es miembro).
- [ ] **4.5** Actualizar 7 app.md: si un app necesita lua/llm/join standalone, declararlo. Si no, no.
## Decisiones de diseno
**Decision 4.2 detallada:** mantener lua/llm/join dentro de `fn_module_data_table` static lib (PUBLIC), porque:
- 100% de las apps que linkean `fn_module_data_table` hoy lo usan para tablas, que usan internamente lua/llm/join.
- Cero apps quieren un "data_table ligero sin lua". Si llegara ese caso → split modulo en `data_table_core` + `data_table_full`. Mientras tanto, KISS.
- La distincion `members` vs `uses_functions` queda solo en `module.md` (metadata) — el CMakeLists agrupa todo bajo la static lib.
## Riesgos
- Ambiguedad "¿el modulo posee X o lo usa?": resolver via 1 regla simple — si `X` aparece como funcion suelta consumida por otras apps fuera del modulo, va a `uses_functions`. Si nadie mas la usa, es `member`.
@@ -0,0 +1,131 @@
---
id: "0107f"
title: "modules/README.md (catalogo) + docs/MODULES_API.md (contrato publico por modulo)"
status: pendiente
type: docs
domain:
- meta
- cpp-stack
- docs
scope: registry
priority: alta
depends: []
blocks: []
related:
- "0107"
created: 2026-05-17
updated: 2026-05-17
tags: [modules, docs, api, contract]
---
# 0107f — Docs API publica de modulos
Parte del issue principal [0107](0107-modules-standardization.md).
## Objetivo
Resolver "no hay un sitio canonico que diga como usar un modulo". Dos archivos:
1. `modules/README.md` — catalogo (tabla con nombre, version, descripcion 1-linea, link a contrato).
2. `docs/MODULES_API.md` — contrato canonico publico de cada modulo (template + ejemplos).
## Estructura `modules/README.md`
```markdown
# Modulos C++ del registry
Bundles versionados de funciones del registry expuestos como static libs.
Apps opt-in via `app.md::uses_modules` + `target_link_libraries(... fn_module_<X>)`.
| Modulo | Version | Static lib | Header | Entry | Descripcion | Contrato |
|---|---|---|---|---|---|---|
| framework_cpp | 1.1.0 | fn_framework | cpp/framework/app_base.h | fn::run_app(cfg, render) | Shell ImGui (GLFW+OpenGL+ImGui+ImPlot+themas+layouts+logger) | [framework_cpp](../docs/MODULES_API.md#framework_cpp) |
| data_table_cpp | 2.0.0 | fn_module_data_table | modules/data_table/data_table.h | data_table::render(...) | Tabla completa TQL+viz+drill+AI+button events | [data_table_cpp](../docs/MODULES_API.md#data_table_cpp) |
## Como anadir un modulo
Ver [docs/MODULES_API.md#cycle](../docs/MODULES_API.md#ciclo-de-vida-de-un-modulo) y `.claude/rules/cpp_apps.md`.
```
## Estructura `docs/MODULES_API.md`
```markdown
# Modules API
Contrato publico canonico de cada modulo C++ del registry. Una app DEBE
poder integrar un modulo leyendo solo esta pagina (sin abrir el .cpp).
## Template por modulo
### <module_name>
**Static lib target:** `<cmake_target>`
**Header path:** `<include>`
**Namespace:** `<namespace>`
**Entry function:** `<signature>`
**State struct:** `<type>` (lifecycle: <lifecycle notes>)
**Public types:** lista de tipos publicos consumidos (TableInput, TableEvent, etc.)
**Dependencias transitivas:** lista de libs externas (lua54, imgui, etc.)
**Min ImGui version:** <X.Y>
**Thread safety:** <main thread only | safe to call from any thread>
#### Opt-in en una app
1. `app.md`: anadir `uses_modules: [{name: <id>, min_version: "X.Y"}]`.
2. `CMakeLists.txt`: `target_link_libraries(<app> PRIVATE <cmake_target>)`.
3. Header: `#include "<header_path>"`.
4. Reservar `<state>` persistente entre frames.
5. Llamar `<entry>` cada frame.
#### Ejemplo minimo
```cpp
[bloque de codigo lanzable]
```
#### Eventos / callbacks
[Tabla de eventos publicos si aplica]
#### Gotchas
[Lista de gotchas conocidos]
#### Version policy
Semver. Major = breaking ABI/API de la entry function o State publico.
Minor = additive (nuevo evento, nuevo renderer opcional). Patch = bugfix.
---
## framework_cpp
[Aplicar template]
## data_table_cpp
[Aplicar template]
## Ciclo de vida de un modulo
1. Crear `modules/<name>/` con `module.md`, `CMakeLists.txt`, `<name>.cpp`, `<name>.h`.
2. `module.md::members` lista funciones del registry que el modulo bundla.
3. `module.md::version` SemVer estricto.
4. `CMakeLists.txt` define static lib `fn_module_<name>` con sus deps.
5. Anadir entrada en `modules/README.md`.
6. Anadir seccion en `docs/MODULES_API.md` siguiendo el template.
7. `fn index` registra el modulo.
8. Cada bump de version: `/version modules/<name> <major|minor|patch> "<reason>"` (ver `.claude/commands/version.md`).
```
## Tareas
- [ ] **6.1** Crear `modules/README.md` con tabla canonica.
- [ ] **6.2** Crear `docs/MODULES_API.md` con template + secciones para los 2 modulos actuales.
- [ ] **6.3** Anadir referencia desde `.claude/rules/cpp_apps.md` a `docs/MODULES_API.md`.
- [ ] **6.4** Anadir referencia desde `cpp/PATTERNS.md` a `docs/MODULES_API.md`.
- [ ] **6.5** Anadir referencia desde `.claude/CLAUDE.md` en la seccion "Estructura" listando `modules/` y enlazando docs.
## Riesgos
- Doc drift: facil que `module.md` y `MODULES_API.md` se desincronicen. Mitigacion: `fn doctor modules` (0107a) verifica que cada modulo registrado tiene seccion en `MODULES_API.md`. Si no, warning.
@@ -0,0 +1,209 @@
---
id: "0107g"
title: "Migrar inline ImGui::BeginTable a data_table::render en apps con tablas de datos reales"
status: en-progreso
type: refactor
domain:
- cpp-stack
- meta
scope: multi-app
priority: media
depends:
- "0107c"
blocks: []
related:
- "0107"
created: 2026-05-17
updated: 2026-05-17
tags: [modules, data-table, drift, audit, inline-begintable]
---
# 0107g — Migrar inline BeginTable a `data_table::render` (data tables reales)
Parte del issue principal [0107](0107-modules-standardization.md). Detectado por `audit_data_table_usage_go_infra` (output en `dev/data_table_integration_audit.md`).
## Problema
Audit automatico identifica ~12 hits de `ImGui::BeginTable` inline en apps que YA declaran `uses_modules: [data_table_cpp]`. Mezcla legitimos + bugs:
- **Legitimos** (NO migrar): KPI grids, schema forms k/v, layout 2-col splitters, chart grids. NO son tablas de datos.
- **Bugs reales**: tablas de datos con filas dinamicas + sort/filter potencial que reinventan logica que el modulo provee.
Resultado: codigo duplicado, comportamiento inconsistente, color/badge/sort/filter "casi-pero-no" igual entre apps. Conexiones raras: cada app personaliza su tabla a mano.
## Decision
Migrar los hits identificados como bugs reales a `data_table::render`. Dejar los legitimos como excepciones documentadas en `docs/MODULES_API.md::Cuando usar data_table::render vs BeginTable directo`.
## Tabla de migracion
| App | Path | Linea | Es bug? | Accion |
|---|---|---|---|---|
| dag_engine_ui | apps/dag_engine_ui/tabs.cpp | 382 | **BUG** (`##dt_run_steps`, 6 cols, scroll Y, runs dinamicas) | Migrar |
| dag_engine_ui | apps/dag_engine_ui/tabs.cpp | 731 | LEGITIMO (`##health_kpis`, 4 cols stretch same, KPI grid) | Dejar + comentar |
| navegator_dashboard | projects/navegator/apps/navegator_dashboard/autoextract_panel.cpp | 528 | **BUG** (`##ax_schema`, 5 cols, filas dinamicas schema) | Migrar |
| navegator_dashboard | projects/navegator/apps/navegator_dashboard/recipes_panel.cpp | 238 | **BUG** (`##recipes_tbl`, 6 cols, filas dinamicas recipes) | Migrar |
| graph_explorer | projects/osint_graph/apps/graph_explorer/extract_panel.cpp | 981 | **BUG** (`##ents`, 5 cols, filas dinamicas entities) | Migrar |
| graph_explorer | projects/osint_graph/apps/graph_explorer/extract_panel.cpp | 1027 | **BUG** (`##rels`, 5 cols, filas dinamicas relations) | Migrar |
| graph_explorer | projects/osint_graph/apps/graph_explorer/main.cpp | 1127 | LEGITIMO (`##enr_params`, form k/v) | Dejar + comentar |
| graph_explorer | projects/osint_graph/apps/graph_explorer/views.cpp | 885 | LEGITIMO (`##insp_id`, inspector form k/v) | Dejar + comentar |
| graph_explorer | projects/osint_graph/apps/graph_explorer/views.cpp | 958 | LEGITIMO (`##insp_fields`, inspector form) | Dejar + comentar |
| graph_explorer | projects/osint_graph/apps/graph_explorer/views.cpp | 1546 | INFO comment, ya migrado a data_table::render | Ignorar (es comentario) |
| graph_explorer | projects/osint_graph/apps/graph_explorer/views.cpp | 1854 | **BUG** (`##te_rows`, col_count dinamico, data table type explorer) | Migrar (segunda fase de la migration ya empezada) |
| graph_explorer | projects/osint_graph/apps/graph_explorer/views.cpp | 2292 | DISCUTIBLE (`##te_fields`, 5 cols, fields de un tipo — semi-dinamico) | Evaluar; si rows >20 migrar, sino dejar |
| registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/views.cpp | 380 | LEGITIMO (`##kpi_grid`, KPI cards) | Dejar + comentar |
| registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/views.cpp | 436 | LEGITIMO (`##chart_grid`, plots grid) | Dejar + comentar |
| registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/views.cpp | 648 | LEGITIMO (`##monitor_kpi`, KPI cards) | Dejar + comentar |
| registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/views.cpp | 1110 | LEGITIMO (`##proj_layout`, 2-col splitter) | Dejar + comentar |
| registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/views.cpp | 1448 | LEGITIMO (`##explorer_layout`, 2-col splitter) | Dejar + comentar |
| registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/work_tab.cpp | 239 | **BUG** (`##flows_work`, 8 cols, filas dinamicas flows) | Migrar |
| registry_dashboard | projects/fn_monitoring/apps/registry_dashboard/work_tab.cpp | 272 | **BUG** (`##top_issues_work`, 7 cols, filas dinamicas issues) | Migrar |
| app_gestion | apps/app_gestion/main.cpp | 722 | DISCUTIBLE (`##linked_tbl`, 4 cols, lista de modulos linked — semi-dinamico, rows <20) | Evaluar; bias a dejar como esta |
**Total a migrar (BUGs): 8 tablas en 4 apps.**
**Total LEGITIMOS (dejar + comentar): 9.**
**Total DISCUTIBLES: 2 — decision contextual.**
**Total comentarios/already-migrated: 1.**
## Tareas
- [x] **1.1** Migrar `dag_engine_ui/tabs.cpp:382` (`##dt_run_steps`) → `data_table::render`. HECHO. Function col → Button action="open_fn"; Status → CategoricalChip; Duration → Duration renderer.
- [~] **1.2** Migrar `navegator_dashboard/autoextract_panel.cpp:528` (`##ax_schema`). ABORTADO: form editor con InputText/Checkbox editables inline en cada fila (field, selector, type, keep). data_table::render no soporta CellEdit como InputText inline. Comentado con LAYOUT-TABLE.
- [x] **1.3** Migrar `navegator_dashboard/recipes_panel.cpp:238` (`##recipes_tbl`). HECHO. Patron B: 4 columnas Button (run/edit/delete/open_df) + ev.row para indexar yaml_path. last_status → CategoricalChip.
- [~] **1.4** Migrar `graph_explorer/extract_panel.cpp:981` (`##ents`). ABORTADO: form editor con InputText editables por fila (type_buf, name_buf) + Checkbox "sel". Mutacion directa de structs entities[i]. No mapeable a data_table. Comentado con LAYOUT-TABLE.
- [~] **1.5** Migrar `graph_explorer/extract_panel.cpp:1027` (`##rels`). ABORTADO: form editor con Checkbox "sel" + inmutabilidad necesaria (relations[i].selected se muta inline). Comentado con LAYOUT-TABLE.
- [~] **1.6** Migrar `graph_explorer/views.cpp:1854` (`##te_rows`). ABORTADO: interactividad app-específica no mapeable — Selectable + single-click ramificado por estado de promocion (promoted/unpromoted), dblclick promote-flow, PopupContextItem con promote/demote/focus condicionales, SmallButton Promote-out-of-group, paginacion manual. Equivalente exact en data_table events no existe. Comentado explicando razon.
- [x] **1.7** Migrar `registry_dashboard/work_tab.cpp:239` (`##flows_work`). HECHO. 8 cols. Status + Risk → CategoricalChip. BeginChild host 220px.
- [x] **1.8** Migrar `registry_dashboard/work_tab.cpp:272` (`##top_issues_work`). HECHO. 7 cols. Status + Deps + Prio → CategoricalChip. Deps string "-"/"OK"/"blocked" preserva logica de color original. BeginChild host -1.
- [x] **2.1** Anadir comentario `// LAYOUT-TABLE — KPI/form/splitter, no data; keep BeginTable inline.` encima de los 9 hits LEGITIMOS para que `audit_data_table_usage` los excluya en proximas pasadas. HECHO en: ##health_kpis, ##enr_params, ##insp_id, ##insp_fields, ##kpi_grid, ##chart_grid, ##monitor_kpi, ##proj_layout, ##explorer_layout. Los 3 ABORTADOS tambien comentados con razon tecnica.
- [ ] **2.2** Actualizar `audit_data_table_usage_go_infra` para leer ese comentario y filtrar `[warn] -> [ignored:declared_layout_table]`.
- [x] **3.1** Decidir los 2 DISCUTIBLES (`te_fields`, `linked_tbl`) con criterio "si rows pueden crecer > 50, migrar". Decision: DEJAR. `te_fields` max ~20 fields por tipo; `linked_tbl` max ~10 modules linked. Rows no escalan.
- [x] **4.1** Envolver TODAS las llamadas a `data_table::render` en `ImGui::BeginChild` host. HECHO en las 3 tablas migradas: flows_work (220px), top_issues_work (-1), recipes_tbl (300px), dt_run_steps (usa el BeginChild preexistente ##run_steps_wrap).
- [ ] **5.1** Re-ejecutar audit:
```
./fn run audit_data_table_usage
```
Verificar: 0 BUG hits, 9 LEGITIMOS comentados, 11 `no_child_host` resueltos o documentados como excepcion.
- [x] **5.2** Build de las 4 apps modificadas. HECHO: dag_engine_ui, registry_dashboard, graph_explorer compilan OK (Linux). navegator_dashboard es Windows-only (CMakeLists.txt retorna en non-WIN32); sintaxis verificada via g++ -fsyntax-only sin errores.
## Patrones de migracion canonicos
### Patron A: ImGui::BeginTable inline → data_table::render basico
```cpp
// ANTES
if (ImGui::BeginTable("##recipes_tbl", 6, flags)) {
ImGui::TableSetupColumn("name");
ImGui::TableSetupColumn("url_pattern");
// ...
for (const auto& r : recipes) {
ImGui::TableNextRow();
ImGui::TableNextColumn(); ImGui::TextUnformatted(r.name.c_str());
ImGui::TableNextColumn(); ImGui::TextUnformatted(r.url_pattern.c_str());
// ...
}
ImGui::EndTable();
}
// DESPUES
static data_table::State g_st_recipes;
static std::vector<std::string> g_back_recipes; // backing
static std::vector<const char*> g_ptrs_recipes; // ptrs row-major
g_back_recipes.clear();
for (const auto& r : recipes) {
g_back_recipes.push_back(r.name);
g_back_recipes.push_back(r.url_pattern);
// ... resto cols
}
g_ptrs_recipes.clear();
for (auto& s : g_back_recipes) g_ptrs_recipes.push_back(s.c_str());
data_table::TableInput tbl;
tbl.name = "recipes";
tbl.headers = {"name", "url_pattern", "last_status", "last_at", "tries", "ok"};
tbl.types = {data_table::ColumnType::String, data_table::ColumnType::String,
data_table::ColumnType::String, data_table::ColumnType::Date,
data_table::ColumnType::Int, data_table::ColumnType::Bool};
tbl.cells = g_ptrs_recipes.data();
tbl.rows = (int)recipes.size();
tbl.cols = 6;
// Status como CategoricalChip (ganancia inmediata sobre BeginTable)
tbl.column_specs.resize(tbl.cols);
for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i];
tbl.column_specs[2].renderer = data_table::CellRenderer::CategoricalChip;
tbl.column_specs[2].chips = {{"ok","#22c55e"},{"error","#ef4444"},{"pending","#a3a3a3"}};
std::vector<data_table::TableEvent> events;
ImGui::BeginChild("##recipes_host", ImVec2(-1, -1));
data_table::render("##recipes_dt", {tbl}, g_st_recipes, &events);
ImGui::EndChild();
for (const auto& ev : events) {
if (ev.kind == data_table::TableEventKind::RowDoubleClick) {
open_recipe_detail(ev.row);
}
}
```
### Patron B: BeginTable inline con interactividad (boton por fila)
Si la BeginTable inline tiene un boton "Delete" / "Edit" por fila → migrar usando `CellRenderer::Button` + `action_id`:
```cpp
// ANTES
ImGui::TableNextColumn();
if (ImGui::SmallButton(("Delete##" + r.id).c_str())) { delete_recipe(r.id); }
// DESPUES — anadir columna actions con button renderer
tbl.headers.push_back("actions");
tbl.types.push_back(data_table::ColumnType::String);
data_table::ColumnSpec actions_spec;
actions_spec.id = "actions";
actions_spec.renderer = data_table::CellRenderer::Button;
actions_spec.button_action = "delete_recipe";
actions_spec.button_label = "Delete";
actions_spec.button_color_hex = "#ef4444";
tbl.column_specs.push_back(actions_spec);
tbl.cols++;
// Backing extra
for (const auto& r : recipes) {
g_back_recipes.push_back(r.id); // celda actions = el id (consumido en ev.value)
}
// Handler
for (const auto& ev : events) {
if (ev.kind == data_table::TableEventKind::ButtonClick && ev.action_id == "delete_recipe") {
delete_recipe(ev.value); // ev.value == r.id de la fila clicada
}
}
```
## Riesgos
- **Backing storage**: las apps deben mantener `std::vector<std::string>` (estable) + `std::vector<const char*>` (ptrs row-major). Helper `cells_to_ptrs()` ya esta usado en data_factory — generalizar como `cpp/functions/core/cells_to_ptrs.cpp` si patron se repite >2 veces (ya pasa).
- **State persistente**: cada migracion requiere `static data_table::State g_st_<name>;`. Si la app tiene N tablas, N states.
- **Comportamiento sutil**: filter/sort/freeze ahora son user-toggle, no controlados por la app. La app pierde control fino, pero gana consistencia.
## Bonus: nueva funcion del registry `cells_to_ptrs_cpp_core`
Patron `g_back + g_ptrs` aparece en data_factory + (post 0107g) en 4 apps mas. Promover a funcion del registry:
```cpp
// cpp/functions/core/cells_to_ptrs.h
namespace fn {
// Converts a row-major flat vector<string> to a row-major vector<const char*>
// pointing into the backing storage. Stable pointers — backing must not be
// resized while ptrs are in use.
void cells_to_ptrs(const std::vector<std::string>& back,
std::vector<const char*>& ptrs);
}
```
Issue separado o sub-task de 0107g segun apetito.
## Notas
- El audit `audit_data_table_usage_go_infra` ya existe (FRESH 7d). Se referencia desde `fn doctor modules` (0107a) para mostrar drift en CI/dashboard.
@@ -0,0 +1,356 @@
---
id: "0108"
title: "App tables_qa — testbed agresivo del modulo data_table + deprecar tables_playground"
status: in-progress
type: app
domain:
- cpp-stack
- apps-infra
- meta
scope: app
priority: alta
depends:
- "0107"
blocks: []
related:
- "0081"
- "0097"
created: 2026-05-17
updated: 2026-05-17
tags: [data-table, modules, testbed, qa, regression, version-selector, apphub]
---
# 0108 — tables_playground como testbed agresivo del modulo data_table
## Problema
`apps/primitives_gallery/playground/tables/` hoy es el playground original de tablas — fue el origen de `modules/data_table`. Pero esta DESACOPLADO del modulo: linkea sus PROPIAS copias de `data_table.cpp`, `data_table_logic.cpp`, `tql.cpp`, `tql_to_sql.cpp`, `lua_engine.cpp`, `llm_anthropic.cpp`, `viz.cpp` (versiones legacy). El `self_test.cpp` (430 checks) prueba la logica legacy, no el modulo.
Consecuencias:
- Bug en el modulo no se detecta en el playground.
- Cualquier mejora del modulo (post-0107) NO se valida contra el playground hasta que algun app lo encuentre en runtime.
- El playground es deuda — codigo duplicado que nadie mantiene a la par.
Ademas, las apps consumidoras (`app_gestion`, `data_factory`, `registry_dashboard`, `services_monitor`, etc.) usan el modulo con patrones repetidos pero no documentados como "casos de uso canonicos":
- `CellRenderer::CategoricalChip` con `chips` map (status, kind, enabled).
- `CellRenderer::ColorScale` con `range_min`/`range_max`/`range_alpha` (duracion en ms).
- `CellRenderer::Badge` con `badges` map (version pinning, status, env).
- `CellRenderer::Duration` con `duration_warn_ms` / `duration_error_ms`.
- `CellRenderer::Button` con `button_action` → consumido en `events_out` (`TableEventKind::ButtonClick`).
- `TableEventKind::RowDoubleClick` → abrir detalle / drilldown.
- Joins entre `TableInput[0]` (main) y `TableInput[i]` (joinables) con `JoinStrategy`.
- Color rules condicionales (numericas + categoricas) — declaradas en `State.stages[k].color_rules`.
NO HAY un sitio donde un agente o humano pueda ver TODOS estos patrones funcionando lado-a-lado para verificar visualmente que el modulo se comporta bien. El primitives_gallery hace eso para componentes basicos, pero no para tablas avanzadas.
## Decision (actualizada 2026-05-17)
Crear **app NUEVA `tables_qa`** (no evolucionar `tables_playground` in-place). Razon: playground tiene 8 .cpp legacy + self_test 2921 LOC mezclado. Mas limpio crear desde 0 con `init_cpp_app` + `uses_modules: [data_table_cpp]` desde el principio.
`tables_playground` se **deprecara** tras `tables_qa` verde:
- `apps/primitives_gallery/playground/tables/` → mover a `apps/primitives_gallery/playground/tables_legacy_archive/` (gitignored o conservado por historia).
- Entry en `cpp/CMakeLists.txt` que registra `tables_playground` se elimina.
- `tables_playground_self_test` deja de buildear.
### Identidad de la app
- `name`: `tables_qa`
- `description`: "Testbed agresivo del modulo data_table — multi-tabla, menu QA toggleable, inyector de eventos, counters live, version selector + downgrade side-by-side."
- `icon.phosphor`: `"test-tube"` (Phosphor — claro QA, sin ambiguedad)
- `icon.accent`: `"#f59e0b"` (amber — testing zone, distinto de los colores existentes de otras apps)
- `dir_path`: `apps/tables_qa`
- `repo_url`: `https://gitea.organic-machine.com/dataforge/tables_qa`
- `tags`: `[imgui, dashboard, qa, testing, data-table, regression]`
### Registro en App Hub
Tras build verde:
1. `./fn run generate_app_icon "test-tube" "#f59e0b" "apps/tables_qa/appicon.ico"` — genera .ico multi-res.
2. Anadir trio obligatorio (`description` + `icon.phosphor` + `icon.accent`) al `app.md` (ya cubierto en frontmatter del scaffolder).
3. `./fn run refresh_app_hub` — regenera icons PNG + manifest TSV del hub + reinicia `app_hub_launcher` si esta corriendo.
4. Verificar tarjeta visible en hub con icono amber + descripcion.
## Decision (original — refundar a aplicacion completa)
Refundar `tables_playground` como **testbed agresivo del modulo `fn_module_data_table`**:
1. **Migrar al modulo**: el playground PASA a depender de `fn_module_data_table` static lib. Eliminar las copias locales (`data_table.cpp`, `data_table_logic.cpp`, `tql.cpp`, `lua_engine.cpp`, `viz.cpp`, `tql_to_sql.cpp`, `llm_anthropic.cpp`). El playground se convierte en cliente del modulo igual que las apps reales.
2. **Multi-tabla**: 6-8 tablas demo cubriendo cada `CellRenderer` + cada `TableEventKind` + joins + color rules. Cada una con su `State` persistente. Tab navegable (`ImGui::BeginTabBar`).
3. **Menu de acciones replicando apps**: barra superior con toggles que activan/desactivan features tipicas usadas por apps:
- "Buttons en celdas (Cancel/Retry/Inspect)" → emite ButtonClick.
- "Color condicional numerico (ColorScale en duraciones)".
- "Color condicional categorico (CategoricalChip por status)".
- "Badge version pinning".
- "Duration con thresholds (warn=1000ms / error=5000ms)".
- "Dots sparkline (status timeline)".
- "Icon map (TI_CHECK/TI_X/TI_CIRCLE)".
- "Join Left/Inner/Right/Full entre tablas".
- "Row double-click → modal de detalle".
- "Row right-click → context menu".
- "TQL pipeline (filter/breakout/agg/sort) con chips".
- "Ask AI panel (TQL natural language)".
- "Save/Load TQL desde disco".
- "Export CSV".
- "Drill-down entre stages".
4. **Version selector + downgrade**:
- Mostrar version actual del modulo en header (`fn::framework_version()` / `data_table::module_version()` — anadir API).
- Selector de "modo compatibilidad" para simular comportamiento de versiones anteriores (`v1.4`, `v1.3`, etc.). Implementacion: feature flags por version dentro del modulo o branch-by-config al construir `TableInput`. Util para auditar regresiones cuando bumpemos versiones.
- Side-by-side: renderizar la misma `TableInput` con la version actual a la izquierda y con el modo compatibilidad a la derecha. Diff visual inmediato.
5. **Capture & compare visual**: integrar con el sistema `primitives_gallery --capture` existente (golden images) para que cada commit corra screenshots del testbed y compare contra master. Si pixel diff > threshold → CI rojo.
6. **Self-test del modulo**: nuevo `tables_playground_module_test.cpp` que ejerza la API publica `data_table::render()` con casos sinteticos. Headless (sin GLFW window) usando `imgui_test_engine` (si FN_BUILD_TESTS=ON). Cubre:
- Cada `CellRenderer` enum se renderiza sin crash.
- `events_out` recibe el evento correcto al simular click.
- State se mantiene entre frames.
- TQL pipeline produce output esperado.
- Joins respetan strategy.
## Arquitectura
### Estructura final del playground
```
apps/primitives_gallery/playground/tables/
CMakeLists.txt # linka fn_module_data_table; ELIMINA sources legacy
main.cpp # entry: fn::run_app + render_playground()
playground.cpp # render_playground(): tab bar + acciones menu
tables/ # NEW — 1 archivo por tabla demo
tbl_basic.cpp # Text + numerico, base case
tbl_renderers.cpp # 1 columna por cada CellRenderer (visual review)
tbl_buttons.cpp # Cancel/Retry/Inspect — emite ButtonClick
tbl_color_rules.cpp # ColorScale numerico + CategoricalChip
tbl_durations.cpp # Duration renderer + thresholds
tbl_dots.cpp # Dots sparkline (status timelines)
tbl_joins.cpp # 2 tablas + Left/Inner/Right/Full
tbl_tql.cpp # Pipeline TQL completo + Ask AI
tbl_drill.cpp # Drill-down stages
version_compat.cpp # version selector + side-by-side downgrade
test_module.cpp # NEW: self_test contra API publica del modulo
e2e_run.sh # actualizar: build + test_module + capture
(ELIMINAR: data_table.cpp, data_table_logic.cpp, tql.cpp, tql_to_sql.cpp,
lua_engine.cpp, llm_anthropic.cpp, viz.cpp, tql_duckdb.cpp, self_test.cpp)
README.md # NEW: documenta cada tabla demo + checklist e2e
```
### API publica del modulo expuesta al playground (documentar)
```cpp
// Entry function
namespace data_table {
void render(const char* id,
const std::vector<TableInput>& tables,
State& state,
std::vector<TableEvent>* events_out = nullptr,
bool show_chrome = true);
}
// Tipos publicos consumidos por las apps
struct TableInput {
std::string name;
std::vector<std::string> headers;
std::vector<ColumnType> types;
const char* const* cells; // row-major, rows*cols
int rows;
int cols;
std::vector<ColumnSpec> column_specs; // renderer config per col
};
struct ColumnSpec {
std::string id;
CellRenderer renderer = CellRenderer::Text;
// Badge / CategoricalChip / Dots
std::vector<BadgeRule> badges;
std::vector<ChipRule> chips;
// Progress
bool progress_scale_100 = false;
std::string progress_color_hex;
// Duration
float duration_warn_ms = 1000.0f;
float duration_error_ms = 5000.0f;
// Icon
std::vector<IconMapEntry> icon_map;
// Button
std::string button_action;
std::string button_label;
std::string button_color_hex;
// ColorScale
double range_min = 0.0;
double range_max = 1.0;
float range_alpha = 0.25f;
std::vector<ColorStop> color_stops;
// Tooltip
std::string tooltip;
bool tooltip_on_hover = false;
};
enum class CellRenderer : uint8_t {
Text=0, Badge=1, Progress=2, Duration=3, Icon=4, Button=5,
Dots=8, CategoricalChip=9, ColorScale=10,
};
enum class TableEventKind : uint8_t {
ButtonClick=1, RowDoubleClick=2, RowRightClick=3, CellEdit=4,
};
struct TableEvent {
TableEventKind kind;
int row;
int col;
std::string column_id;
std::string action_id; // ColumnSpec::button_action on ButtonClick
std::string value;
};
struct State {
// (opaca al consumidor — gestionada internamente; debe persistir entre frames)
};
// Module metadata (NEW en este issue)
namespace data_table {
const char* module_version(); // ej. "2.0.0"
const char* module_description();
}
```
Esta especificacion va a `docs/MODULES_API.md` (issue 0107f) como el contrato canonico de `data_table_cpp`.
### Patron de uso canonico (apps)
```cpp
static data_table::State st;
data_table::TableInput tbl;
tbl.name = "runs";
tbl.headers = {"id", "status", "duration_ms", "started_at"};
tbl.types = {ColumnType::String, ColumnType::String, ColumnType::Float, ColumnType::Date};
tbl.cells = &cells_flat[0];
tbl.rows = N;
tbl.cols = 4;
// Configure renderers
tbl.column_specs.resize(tbl.cols);
for (int i = 0; i < tbl.cols; i++) tbl.column_specs[i].id = tbl.headers[i];
tbl.column_specs[1].renderer = data_table::CellRenderer::CategoricalChip;
tbl.column_specs[1].chips = { {"ok", "#22c55e"}, {"error", "#ef4444"}, {"running", "#f59e0b"} };
tbl.column_specs[2].renderer = data_table::CellRenderer::ColorScale;
tbl.column_specs[2].range_min = 0.0;
tbl.column_specs[2].range_max = 5000.0;
tbl.column_specs[2].range_alpha = 0.30f;
// Button column for retry action
data_table::ColumnSpec retry_col;
retry_col.id = "actions";
retry_col.renderer = data_table::CellRenderer::Button;
retry_col.button_action = "retry_run";
retry_col.button_label = "Retry";
tbl.headers.push_back("actions");
tbl.column_specs.push_back(retry_col);
// Render + consume events
std::vector<data_table::TableEvent> events;
data_table::render("##runs_tbl", {tbl}, st, &events);
for (auto& ev : events) {
if (ev.kind == data_table::TableEventKind::ButtonClick && ev.action_id == "retry_run") {
// app handler
}
if (ev.kind == data_table::TableEventKind::RowDoubleClick) {
// open detail modal
}
}
```
## Tareas
- [ ] **1.1** Investigar TODOS los patrones de uso del modulo en apps (script audit):
```bash
grep -rhnE "data_table::(render|TableInput|State|ColumnSpec|TableEvent|CellRenderer::|TableEventKind::|JoinStrategy)" \
apps/*/main.cpp apps/*/*.cpp projects/*/apps/*/*.cpp 2>/dev/null | sort -u > /tmp/data_table_api_usage.txt
```
Producir tabla de patrones canonicos en `docs/MODULES_API.md`.
- [ ] **2.1** Backup `apps/primitives_gallery/playground/tables/{data_table.cpp,data_table_logic.cpp,tql.cpp,tql_to_sql.cpp,lua_engine.cpp,llm_anthropic.cpp,viz.cpp,tql_duckdb.cpp,self_test.cpp}` a `apps/primitives_gallery/playground/tables/legacy_archive/` (gitignored o conservado por historia).
- [ ] **2.2** Editar `apps/primitives_gallery/playground/tables/CMakeLists.txt`:
- Quitar sources legacy.
- `target_link_libraries(tables_playground PRIVATE fn_module_data_table)`.
- El target `tables_playground_self_test` se renombra a `tables_playground_module_test` con sources nuevos.
- [ ] **3.1** Crear `playground.cpp` con tab bar + menu acciones.
- [ ] **3.2** Crear 9 archivos `tables/tbl_*.cpp` con cada caso demo.
- [ ] **3.3** Crear `version_compat.cpp` con selector + side-by-side.
- [ ] **3.4** Anadir API `data_table::module_version()` + `module_description()` a `modules/data_table/data_table.h`. Implementacion lee `version_generated.h` (post 0107c bump a 2.0.0).
- [ ] **4.1** Crear `test_module.cpp` headless con `imgui_test_engine` (gated por `FN_BUILD_TESTS=ON`):
- 1 test por `CellRenderer` enum.
- 1 test por `TableEventKind`.
- 1 test de joins.
- 1 test de TQL pipeline.
- Compara cells output via golden TSV.
- [ ] **4.2** Migrar lo aplicable del `self_test.cpp` legacy (430 checks) a tests del modulo. Lo que prueba logica pura ya extraida al registry (parse_number, compare, tql parsing, lua) va a `cpp/functions/core/*_test.cpp`. Lo que prueba render() va a `test_module.cpp`.
- [ ] **5.1** Actualizar `e2e_run.sh`: build + module_test + capture screenshot.
- [ ] **5.2** Integrar con `primitives_gallery --capture` (golden images de cada tab del testbed).
- [ ] **5.3** README.md con checklist de QA por tab.
- [ ] **6.1** Verificar que las apps consumidoras (7) siguen compilando y comportandose igual.
- [ ] **6.2** `fn index` para que el playground actualizado quede registrado (aunque sea playground, su `app.md` puede declarar `uses_modules: [data_table_cpp]` con min_version 2.0.0).
## Decisiones de diseño
1. **Eliminar sources legacy**: opcion alternativa era mantenerlos como golden-reference. Decision: eliminarlos. Razon: si quedan vivos, la tentacion de "arreglar el playground" sin tocar el modulo permanece. Forzar a usar el modulo cierra el loop.
2. **Side-by-side downgrade**: opcion alternativa era branchear el modulo en `data_table_v1` static lib paralelo. Decision: feature flags por version dentro del modulo actual (`v_compat_mode`). Menos duplicacion, mas pragmatico.
3. **`module_version()` como API publica**: alternativa era leer `version_generated.h` desde la app. Decision: API publica + estable. Las apps que quieran mostrar la version del modulo en su About panel lo hacen via la funcion, no via include privado.
## Prerequisitos
- Issue 0107 (estandarizacion modulos) cerrado y `modules-v2` activo. **Bloqueo duro**: sin 0107, el sistema sigue con drift y refactorizar el playground encima de codigo inestable es desperdicio.
## Riesgos
- **Headless test con imgui_test_engine**: requiere `FN_BUILD_TESTS=ON`. Si no esta disponible en CI, los tests no corren. Mitigacion: documentar requirement en CI config + fallback a tests de logica pura sin UI.
- **Version compat mode** es facil de hacer mal: el codigo del modulo se llena de `if (v_compat < X)`. Mitigacion: limite estricto — solo se mantiene compat para las 2 versiones previas (`v_current - 1`, `v_current - 2`). Mas alla, se elimina y se documenta breaking change.
- **Capture and compare** depende de fonts/DPI/driver GL → false positives en CI. Mitigacion: capture solo en Linux x86_64 con driver mesa fijado.
## Infra de testing reutilizable (existente en el repo)
El testbed se construye combinando estos mecanismos ya disponibles. Ningun nuevo motor.
| Mecanismo | Ubicacion | Que aporta al testbed |
|---|---|---|
| Catch2 unit | `cpp/tests/` (50+ tests, macro `add_fn_test`) | Tests de logica pura de sub-funciones del registry usadas por el modulo (`compute_column_stats`, `tql_emit`, `lua_engine`, `join_tables`, `tql_to_sql`). Ya existen — los tomamos como dado. |
| Golden PNG diff | `cpp/tests/golden/` (43 PNGs) + `png_diff.cpp` | Cada tab del testbed exporta golden via `primitives_gallery --capture`. Diff en CI bloquea drift visual. |
| Auto-driving worker | `apps/altsnap_jitter_test/main.cpp` (modelo de referencia) | Pattern: worker thread fakea eventos + counters monotonicos en `fn::internal::*` + `set_force_alt_for_test()` bypass. Replicar para `data_table`: counters `fn::internal::data_table::{button_click_count, row_double_click_count, color_rule_applied_count, ...}` + worker que inyecta clicks. |
| Dear ImGui Test Engine | `cpp/framework/app_base.h:205-225` (`fn::run_app_test`, gated por `FN_BUILD_TESTS=ON`) | UI-level tests: `IM_REGISTER_TEST` lambdas que llaman `ctx->ItemClick`, `ctx->ItemDoubleClick`, verifican events emitidos. **CERO consumidores actuales** — el testbed sera el primer cliente. |
| `--self-test` flag | `.claude/rules/e2e_validation.md` (patron canonico) | `tables_playground --self-test` corre toda la suite headless, exit 0/1. Compatible con CI sin display. |
| Cross-platform e2e | `bash/functions/infra/e2e_run_cpp_windows.sh` | Funcion bash: cross-compile mingw + deploy Desktop + taskkill + run native + capturar stdout/exit. Linux CI valida Windows. |
### Panel de control QA agresivo (UI del testbed)
Barra superior con:
1. **Toggles por feature** (15+ checkboxes). Cada toggle muta `ColumnSpec` o `State` en runtime, mismo patron que apps reales hacen en codigo de setup. El usuario QA puede activar/desactivar y ver cambio visual inmediato sin recompilar.
2. **Inyector de eventos**: 4 botones — "Simulate ButtonClick", "Simulate RowDoubleClick", "Simulate RowRightClick", "Simulate Drill". Cada uno llama el worker thread pattern de altsnap → verifica que `TableEvent` apropiado se emite.
3. **Counters live**: contador por evento, contador por renderer aplicado, latency p50/p95 por frame (medir desde `ImGui::GetIO().Framerate`). Visible siempre, util para detectar regresiones de performance.
4. **Version selector**: dropdown con versiones del modulo (`2.0.0` actual, `1.4.0` compat, `1.3.0` compat...). Cambio re-renderiza la misma data side-by-side: actual vs version seleccionada. Diff visual inmediato + diff de events emitidos.
5. **Boton "Run --self-test"**: ejecuta toda la suite headless dentro de la misma ventana. Output en log panel. Util para iteracion rapida sin relanzar.
6. **Boton "Export golden"**: corre `--capture` sobre cada tab, escribe PNGs a `golden/data_table_testbed/`. Para regenerar baseline tras cambio visual intencional.
### Counters internos a anadir al modulo
Issue 0108 anade en `modules/data_table/data_table.cpp`:
```cpp
namespace data_table::internal {
int button_click_count();
int row_double_click_count();
int row_right_click_count();
int color_rule_applied_count();
int tql_stages_executed();
int last_render_duration_us();
void reset_counters();
}
```
Mismo modelo que `fn::internal::*` para altsnap. Test-only observability, cost cero en prod (counters atomicos triviales).
## Notas
- Issue 0108 NO empieza hasta 0107 cerrar. Bloqueado duro.
- Se referencia desde `docs/MODULES_API.md` (0107f) como el ejemplo canonico de "como usar el modulo data_table".
- Cuando 0108 cierre, abrir issue 0109 paralelo para `chat_ia` (que era el siguiente modulo planeado al inicio de 0107).
@@ -0,0 +1,60 @@
---
id: "0109a"
title: "skill_tree app shell + parsers issues/flows"
status: completado
type: feature
domain:
- meta
- cpp-stack
scope: app-scoped
priority: media
depends: []
blocks:
- "0109b"
related:
- "0109"
created: 2026-05-17
updated: 2026-05-17
tags:
- skill-tree
- cpp
- imgui
- parsers
---
# 0109a — skill_tree shell + parsers
Primer slice del epic 0109. Foco: app C++ scaffoldada, compilando, leyendo los 79 issues + 7 flows y reportando conteos en stdout. Sin render del grafo todavia — solo plumbing.
## Tareas
1. Scaffolder `./fn run init_cpp_app skill_tree --domain tools --desc "..." --tags "dashboard,meta"`.
2. Editar `app.md` generado: trio icon (`tree-structure`, `#c026d3`), `e2e_checks`, `uses_functions` iniciales.
3. Generar `appicon.ico` via `generate_app_icon_py_infra`.
4. Crear funcion `parse_md_frontmatter_cpp_core` (delegar a fn-constructor):
- Input: `std::string content` (texto del .md completo).
- Output: `MdFrontmatter` struct con `std::unordered_map<std::string, YamlValue>` y `std::string body`.
- `YamlValue` = variant `{string, list<string>, null}`. Subset YAML suficiente para issues actuales.
- Pure. Test golden: parsea los 79 issues + 7 flows sin error.
5. En `main.cpp` (scaffold inicial): al arrancar, scan `dev/issues/*.md` + `dev/flows/*.md`, parse cada uno, contar por status/domain. Log a stdout:
```
skill_tree v0.1.0
issues: 79 (28 pendiente, 3 in-progress, 72 completado, ...)
flows: 7 (5 pending, 2 completed)
parse errors: 0
```
6. `e2e_checks` build + self-test warning.
7. Compilar + deploy Windows via `redeploy_cpp_app_windows`.
8. Refrescar hub via `refresh_app_hub`.
## DoD
- [ ] App existe en `apps/skill_tree/` con `.git/` apuntando a `dataforge/skill_tree`.
- [ ] `app.md` con trio completo + `e2e_checks` + `uses_functions` declarados.
- [ ] `appicon.ico` generado.
- [ ] `fn index` exitoso, `mcp__registry__fn_show id="skill_tree"` muestra metadata.
- [ ] `parse_md_frontmatter_cpp_core` indexado en registry.
- [ ] `cmake --build cpp/build --target skill_tree` exitoso.
- [ ] `./skill_tree` (Linux) o `skill_tree.exe` (Windows) imprime conteos correctos.
- [ ] Tarjeta visible en `app_hub_launcher`.
- [ ] `fn doctor cpp-apps` limpio.
@@ -0,0 +1,69 @@
---
id: "0109b"
title: "skill_tree layout anillos + render canvas ImDrawList con cards"
status: completado
type: feature
domain:
- meta
- cpp-stack
scope: app-scoped
priority: media
depends:
- "0109a"
blocks:
- "0109c"
related:
- "0109"
created: 2026-05-17
updated: 2026-05-17
tags:
- skill-tree
- cpp
- imgui
- layout
- canvas
---
# 0109b — skill_tree layout anillos + render canvas
Segundo slice del epic 0109. Reemplaza la lista textual del Tree por un canvas interactivo basado en `ImDrawList`. Pivote desde `graph_renderer_cpp_viz` (GPU) → `ImDrawList` (CPU) para mantener simplicidad: 166 nodos no justifican el pipeline GPU.
## Decisiones tomadas durante la implementacion
- **Stack: `ImGui::ImDrawList`**, NO `graph_renderer_cpp_viz`. Razon: 166 nodos cabian de sobra en CPU; `graph_renderer` exige `init_gl_loader=true`, build de `GraphData` con tipos OSINT, shaders, FBO + texture flip-Y. Diferencia ~120 LOC + un monton de rebuilds para cero beneficio observable.
- **Sin fisicas** (el usuario lo pidio explicito). Layout deterministico via `compute_ring_layout_cpp_core`.
- **5 rings**: done (0), in-progress (1), unlocked (2), locked (3), deferred/bloqueado (4).
- **18 sectores radiales** = 18 dominios canonicos (`dev/TAXONOMY.md`). Labels en el aro exterior.
- **Lock derivation**: `pendiente` se subdivide en `pendiente_unlocked` (todos los `depends[]` en done) vs `pendiente_locked` (algun depends sin completar). Set de `done` IDs se computa al cargar y se cruza con cada `depends[]`.
- **Animacion lerp 1s** entre prev y current position cuando un nodo cambia de `status_eff` entre dos `reload_scan()`s. Ease-in-out cuadratica.
- **Cards con texto**: cada nodo muestra su ID en blanco con sombra negra para legibilidad sobre cualquier color de ring.
- **Diferencial visual flows vs issues**: issues = circulos, flows = rombos.
- **Pan**: drag con boton derecho o medio.
- **Zoom**: rueda del raton, centrado en cursor (re-anchora coordenadas mundo bajo el puntero).
- **Picking**: O(N) radius check (166 nodos = trivial; spatial hash innecesario).
## Tareas
1. Crear funcion del registry `compute_ring_layout_cpp_core` (pure, 10 tests Catch2, FNV-1a determinista para sub-jitter angular).
2. Reescribir `main.cpp::draw_tree()` como canvas con `ImGui::InvisibleButton` + `ImDrawList`.
3. Implementar `derive_status_eff()` para lock/unlock.
4. Implementar `apply_layout()` con preservacion de prev_x/prev_y para animacion.
5. Render: aristas curvas Bezier (depends + related) + nodos con outline + label ID + tooltip on hover.
6. HUD strip con LV/XP/contadores.
7. Self-test 0-exit cuando `parse_errors == 0 && unmapped == 0`.
8. Build Linux + deploy Windows.
## DoD
- [x] `compute_ring_layout_cpp_core` indexada (10/10 tests, 142 assertions).
- [x] `apps/skill_tree/main.cpp` usa `parse_md_frontmatter_cpp_core` + `compute_ring_layout_cpp_core`.
- [x] `app.md uses_functions` actualizado con ambas.
- [x] Self-test imprime breakdown por ring: `done=77 in-progress=2 unlocked=64 locked=22 deferred=1 unmapped=0`.
- [x] Linux build OK.
- [x] Windows deploy OK (PID corriendo).
- [x] Tarjeta visible en `app_hub_launcher`.
- [x] `fn doctor cpp-apps` limpio.
## Sigue
0109c: Inspector evolucionado con DoD parseado de la seccion `## DoD` del .md (checkboxes interactivos read-only) + lista de `uses_functions` del registry para esa issue.