#include #include "app_base.h" #include "core/panel_menu.h" #include "core/icons_tabler.h" #include "core/logger.h" #include "core/parse_md_frontmatter.h" #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::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(); } static void draw_inspector() { if (!ImGui::Begin(TI_INFO_CIRCLE " Inspector", &g_show_inspector)) { ImGui::End(); return; } 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(); } static void render() { if (g_show_tree) draw_tree(); if (g_show_inspector) draw_inspector(); } // ---- 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 }, }; fn::AppConfig cfg; cfg.title = "skill_tree"; cfg.about = { "skill_tree", "0.1.0", "Mapa interactivo de issues+flows en anillos concentricos por estado." }; cfg.log = { "skill_tree.log", 1 }; cfg.panels = panels; cfg.panel_count = sizeof(panels) / sizeof(panels[0]); return fn::run_app(cfg, render); }