From a49481e1d48ce0182848ff05db8d53a294a16299 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sun, 17 May 2026 19:44:10 +0200 Subject: [PATCH] feat(skill_tree): scan dev/issues+flows, parse frontmatter, render counts - main.cpp scan dev/issues + dev/flows con std::filesystem - parsea cada .md con parse_md_frontmatter_cpp_core (nueva fn pure) - cuenta por status/domain/kind, lista nodos en panel Tree - panel Inspector muestra detalle del nodo seleccionado (DoD, depends, related) - --self-test imprime conteos a stdout (exit 0 si parse_errors=0) - Reload manual via boton o tecla F5 - discover_registry_root: FN_REGISTRY_ROOT env o walk-up desde cwd - uses_functions actualizado con parse_md_frontmatter_cpp_core - CMakeLists.txt incluye el .cpp del parser Smoke test 166 nodos parseados (159 issues + 7 flows), 0 parse errors. Fase A del epic 0109. Sigue: 0109b layout anillos + render estatico. Co-Authored-By: Claude Opus 4.7 (1M context) --- CMakeLists.txt | 1 + app.md | 3 +- main.cpp | 248 +++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 242 insertions(+), 10 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 98361d3..03c4b5f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,6 @@ add_imgui_app(skill_tree main.cpp + ${CMAKE_SOURCE_DIR}/functions/core/parse_md_frontmatter.cpp ) target_include_directories(skill_tree PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/app.md b/app.md index f1141fe..dad94ee 100644 --- a/app.md +++ b/app.md @@ -7,7 +7,8 @@ tags: [dashboard, meta, imgui] icon: phosphor: "tree-structure" accent: "#c026d3" -uses_functions: [] +uses_functions: + - parse_md_frontmatter_cpp_core uses_types: [] framework: "imgui" entry_point: "main.cpp" diff --git a/main.cpp b/main.cpp index 35f5d39..c4652de 100644 --- a/main.cpp +++ b/main.cpp @@ -3,20 +3,191 @@ #include "core/panel_menu.h" #include "core/icons_tabler.h" #include "core/logger.h" -// #include "viz/data_table.h" // uncomment to enable data_table::render() panel +#include "core/parse_md_frontmatter.h" -static bool g_show_tree = true; -static bool g_show_inspector = true; +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +// ---- Node model --------------------------------------------------------- + +enum class NodeKind { Issue, Flow }; + +struct Node { + NodeKind kind; + std::string id; // "0109" / "0109a" / "0001" (flow) + std::string title; + std::string status; // pendiente|in-progress|completado|bloqueado|deferred|completed|pending + std::string type; // epic|feature|bugfix|... (issues only) + std::string priority; + std::vector domain; + std::vector depends; + std::vector related; + std::string file_path; +}; + +struct ScanResult { + std::vector nodes; + std::map count_by_status; + std::map count_by_domain; + std::map count_by_kind; + int parse_errors = 0; +}; + +// ---- Registry root discovery ------------------------------------------- + +static fs::path discover_registry_root() { + if (const char* env = std::getenv("FN_REGISTRY_ROOT")) { + fs::path p(env); + if (fs::exists(p / "registry.db")) return p; + } + // walk up from cwd + for (auto p = fs::current_path(); !p.empty() && p != p.root_path(); p = p.parent_path()) { + if (fs::exists(p / "registry.db") && fs::exists(p / "dev" / "issues")) return p; + } + return {}; +} + +// ---- Helpers ------------------------------------------------------------ + +static std::string read_file(const fs::path& p) { + std::ifstream f(p); + if (!f) return {}; + std::stringstream ss; ss << f.rdbuf(); + return ss.str(); +} + +static std::string get_str(const fn_md::Frontmatter& fm, const char* key) { + auto it = fm.fields.find(key); + if (it == fm.fields.end()) return {}; + if (auto* s = std::get_if(&it->second)) return *s; + return {}; +} + +static std::vector get_list(const fn_md::Frontmatter& fm, const char* key) { + auto it = fm.fields.find(key); + if (it == fm.fields.end()) return {}; + if (auto* v = std::get_if>(&it->second)) return *v; + if (auto* s = std::get_if(&it->second)) { + if (!s->empty()) return { *s }; + } + return {}; +} + +// ---- Scanner ------------------------------------------------------------ + +static void scan_dir(const fs::path& dir, NodeKind kind, ScanResult& out) { + if (!fs::exists(dir) || !fs::is_directory(dir)) return; + for (const auto& entry : fs::directory_iterator(dir)) { + if (!entry.is_regular_file()) continue; + const auto& p = entry.path(); + if (p.extension() != ".md") continue; + // skip template.md / INDEX.md / README.md / AGENT_GUIDE.md + auto stem = p.stem().string(); + if (stem == "template" || stem == "INDEX" || stem == "README" || stem == "AGENT_GUIDE") continue; + + auto content = read_file(p); + auto fm = fn_md::parse_md_frontmatter(content); + if (!fm.has_frontmatter) { ++out.parse_errors; continue; } + + Node n; + n.kind = kind; + n.id = get_str(fm, kind == NodeKind::Issue ? "id" : "id"); + if (n.id.empty()) n.id = get_str(fm, "name"); + n.title = get_str(fm, kind == NodeKind::Issue ? "title" : "name"); + n.status = get_str(fm, "status"); + n.type = get_str(fm, "type"); + n.priority = get_str(fm, "priority"); + n.domain = get_list(fm, "domain"); + n.depends = get_list(fm, kind == NodeKind::Issue ? "depends" : "related_issues"); + n.related = get_list(fm, "related"); + n.file_path = p.string(); + + if (n.status.empty()) n.status = "(unknown)"; + if (n.domain.empty()) n.domain.push_back("(unknown)"); + + ++out.count_by_status[n.status]; + ++out.count_by_kind[kind == NodeKind::Issue ? "issue" : "flow"]; + for (const auto& d : n.domain) ++out.count_by_domain[d]; + + out.nodes.push_back(std::move(n)); + } +} + +static ScanResult scan_registry(const fs::path& root) { + ScanResult r; + scan_dir(root / "dev" / "issues", NodeKind::Issue, r); + scan_dir(root / "dev" / "issues" / "completed", NodeKind::Issue, r); + scan_dir(root / "dev" / "flows", NodeKind::Flow, r); + scan_dir(root / "dev" / "flows" / "completed", NodeKind::Flow, r); + return r; +} + +// ---- UI state ----------------------------------------------------------- + +static fs::path g_root; +static ScanResult g_scan; +static bool g_show_tree = true; +static bool g_show_inspector = true; +static int g_selected = -1; + +static void reload_scan() { + g_scan = scan_registry(g_root); + g_selected = -1; + fn_log::log_info("skill_tree: reloaded %d nodes, %d parse errors", + (int)g_scan.nodes.size(), g_scan.parse_errors); +} + +// ---- Panels ------------------------------------------------------------- static void draw_tree() { if (!ImGui::Begin(TI_GRAPH " Tree", &g_show_tree)) { ImGui::End(); return; } - ImGui::TextUnformatted("skill_tree v0.1.0 — fase A (shell)"); - ImGui::TextUnformatted(""); - ImGui::TextUnformatted("Parsers de dev/issues + dev/flows pendientes (0109a)."); - ImGui::TextUnformatted("Render anillos pendiente (0109b)."); + + ImGui::Text("skill_tree v0.1.0 — fase A (shell)"); + ImGui::Text("Root: %s", g_root.string().c_str()); + ImGui::Separator(); + + ImGui::Text("Total: %d nodos (%d issues + %d flows). Parse errors: %d", + (int)g_scan.nodes.size(), + g_scan.count_by_kind["issue"], + g_scan.count_by_kind["flow"], + g_scan.parse_errors); + + if (ImGui::Button(TI_REFRESH " Reload (F5)")) reload_scan(); + if (ImGui::IsKeyPressed(ImGuiKey_F5, false)) reload_scan(); + + ImGui::SeparatorText("Por status"); + for (auto& [k, v] : g_scan.count_by_status) { + ImGui::Text(" %-16s %d", k.c_str(), v); + } + + ImGui::SeparatorText("Por dominio"); + for (auto& [k, v] : g_scan.count_by_domain) { + ImGui::Text(" %-22s %d", k.c_str(), v); + } + + ImGui::SeparatorText("Nodos (lista provisional — render anillos en 0109b)"); + ImGui::BeginChild("nodes_list", ImVec2(0, 0), ImGuiChildFlags_Borders); + for (int i = 0; i < (int)g_scan.nodes.size(); ++i) { + const auto& n = g_scan.nodes[i]; + char label[512]; + std::snprintf(label, sizeof(label), "%s %s · %s · %s##%d", + n.kind == NodeKind::Issue ? "[ISSUE]" : "[FLOW] ", + n.id.c_str(), n.status.c_str(), n.title.c_str(), i); + if (ImGui::Selectable(label, g_selected == i)) g_selected = i; + } + ImGui::EndChild(); ImGui::End(); } @@ -25,7 +196,34 @@ static void draw_inspector() { ImGui::End(); return; } - ImGui::TextDisabled("Click en un nodo del Tree para inspeccionar."); + if (g_selected < 0 || g_selected >= (int)g_scan.nodes.size()) { + ImGui::TextDisabled("Click en un nodo del Tree para inspeccionar."); + ImGui::End(); + return; + } + const auto& n = g_scan.nodes[g_selected]; + ImGui::Text("%s %s", n.kind == NodeKind::Issue ? "[ISSUE]" : "[FLOW]", n.id.c_str()); + ImGui::TextWrapped("%s", n.title.c_str()); + ImGui::Separator(); + ImGui::Text("status: %s", n.status.c_str()); + if (!n.type.empty()) ImGui::Text("type: %s", n.type.c_str()); + if (!n.priority.empty()) ImGui::Text("priority: %s", n.priority.c_str()); + ImGui::Text("file: %s", n.file_path.c_str()); + + ImGui::SeparatorText("domain"); + for (const auto& d : n.domain) ImGui::BulletText("%s", d.c_str()); + + if (!n.depends.empty()) { + ImGui::SeparatorText(n.kind == NodeKind::Issue ? "depends" : "related_issues"); + for (const auto& d : n.depends) ImGui::BulletText("%s", d.c_str()); + } + if (!n.related.empty()) { + ImGui::SeparatorText("related"); + for (const auto& r : n.related) ImGui::BulletText("%s", r.c_str()); + } + + ImGui::Separator(); + ImGui::TextDisabled("Botones [Generate ideas] y [Run autonomous-task] llegan en 0109e/f."); ImGui::End(); } @@ -34,7 +232,39 @@ static void render() { if (g_show_inspector) draw_inspector(); } -int main(int /*argc*/, char** /*argv*/) { +// ---- Self-test ---------------------------------------------------------- + +static int run_self_test() { + g_root = discover_registry_root(); + if (g_root.empty()) { + std::fprintf(stderr, "skill_tree --self-test: FN_REGISTRY_ROOT not found\n"); + return 1; + } + g_scan = scan_registry(g_root); + std::printf("skill_tree v0.1.0\n"); + std::printf("root: %s\n", g_root.string().c_str()); + std::printf("nodes: %zu (%d issues + %d flows)\n", + g_scan.nodes.size(), + g_scan.count_by_kind["issue"], + g_scan.count_by_kind["flow"]); + std::printf("parse_errors: %d\n", g_scan.parse_errors); + std::printf("by_status:\n"); + for (auto& [k, v] : g_scan.count_by_status) std::printf(" %-16s %d\n", k.c_str(), v); + std::printf("by_domain:\n"); + for (auto& [k, v] : g_scan.count_by_domain) std::printf(" %-22s %d\n", k.c_str(), v); + return g_scan.parse_errors == 0 ? 0 : 2; +} + +// ---- Entry -------------------------------------------------------------- + +int main(int argc, char** argv) { + for (int i = 1; i < argc; ++i) { + if (std::strcmp(argv[i], "--self-test") == 0) return run_self_test(); + } + + g_root = discover_registry_root(); + g_scan = scan_registry(g_root); + static fn_ui::PanelToggle panels[] = { { "Tree", nullptr, &g_show_tree }, { "Inspector", nullptr, &g_show_inspector },