merge issue/0109a-skill-tree-shell: scan dev/issues+flows + parser frontmatter

Fase A del epic 0109 completa. App skill_tree compila, parsea 166 nodos (159 issues + 7 flows) sin errores, renderiza conteos en panel Tree + detalle en Inspector. Reload manual F5. Smoke --self-test exit 0.

Funcion nueva: parse_md_frontmatter_cpp_core (pure, subset YAML, 10/10 tests).

Sigue: 0109b layout anillos + render estatico.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 19:46:49 +02:00
3 changed files with 242 additions and 10 deletions
+1
View File
@@ -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})
+2 -1
View File
@@ -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"
+239 -9
View File
@@ -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 <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::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 },