a49481e1d4
- 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) <noreply@anthropic.com>
283 lines
9.9 KiB
C++
283 lines
9.9 KiB
C++
#include <imgui.h>
|
|
#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 <algorithm>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <map>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
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<std::string> domain;
|
|
std::vector<std::string> depends;
|
|
std::vector<std::string> related;
|
|
std::string file_path;
|
|
};
|
|
|
|
struct ScanResult {
|
|
std::vector<Node> nodes;
|
|
std::map<std::string, int> count_by_status;
|
|
std::map<std::string, int> count_by_domain;
|
|
std::map<std::string, int> 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<std::string>(&it->second)) return *s;
|
|
return {};
|
|
}
|
|
|
|
static std::vector<std::string> 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<std::vector<std::string>>(&it->second)) return *v;
|
|
if (auto* s = std::get_if<std::string>(&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);
|
|
}
|