feat(0036d): promote kind-aware (Group → clear group_id)
NodeGroups window kind=Group ahora expone un boton SmallButton(TI_ARROW_UP) por fila que saca la entidad del grupo (group_id = NULL) y dispara reload del grafo. kind=Table mantiene el comportamiento de issue 0011. - entity_ops: nueva op `entity_clear_group_id(db, id)` idempotente. Si la columna group_id no existe (BD pre-0035a) retorna true como no-op. Falla solo si la entidad no existe o SQLite revienta. - views.cpp: extra columna "promote" en kind=Group, tooltip header diferenciado por kind, boton conectado a app.want_clear_group_id_entity. - main.cpp: handler que ejecuta entity_clear_group_id, marca windows como dirty, llama reload_after_mutation y loguea `[node_groups] promoted X out of group`. - gx-cli: flag `node update --clear-group-id` (booleano) y exposicion MCP en inputSchema + MCP_DISPATCH defaults para que el agente Echo pueda promover via tool calls. - tests: 3 nuevos CLI (clear, idempotente, combinable con --name) y 4 MCP (defaults, schema, dispatch end-to-end, idempotente). WSL: 102 passed (95 base + 7). Windows: 91 passed, 11 skipped (84 base + 7). Refs: issues/0036d-promote-kind-aware.md
This commit is contained in:
@@ -199,6 +199,66 @@ bool entity_update_type(const char* db_path, const char* id, const char* new_typ
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool entity_clear_group_id(const char* db_path, const char* entity_id) {
|
||||
if (!db_path || !entity_id || !*entity_id) return false;
|
||||
sqlite3* db = nullptr;
|
||||
if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK) {
|
||||
if (db) sqlite3_close(db);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Detectar si la columna `group_id` existe en `entities`. En BDs viejas
|
||||
// (pre-issue 0035a) la columna no esta — el clear es no-op coherente:
|
||||
// la entidad no puede pertenecer a un grupo si la columna no existe.
|
||||
bool has_group_id = false;
|
||||
{
|
||||
sqlite3_stmt* pst = nullptr;
|
||||
if (sqlite3_prepare_v2(db, "PRAGMA table_info(entities)", -1, &pst, nullptr) == SQLITE_OK) {
|
||||
while (sqlite3_step(pst) == SQLITE_ROW) {
|
||||
const unsigned char* col = sqlite3_column_text(pst, 1);
|
||||
if (col && std::strcmp((const char*)col, "group_id") == 0) {
|
||||
has_group_id = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(pst);
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar que la entidad existe (independiente del path con/sin
|
||||
// columna). Si no existe, devolvemos false (mismo contrato que
|
||||
// entity_update_type).
|
||||
{
|
||||
sqlite3_stmt* est = nullptr;
|
||||
if (sqlite3_prepare_v2(db, "SELECT 1 FROM entities WHERE id = ?", -1,
|
||||
&est, nullptr) != SQLITE_OK) {
|
||||
sqlite3_close(db);
|
||||
return false;
|
||||
}
|
||||
sqlite3_bind_text(est, 1, entity_id, -1, SQLITE_TRANSIENT);
|
||||
bool exists = (sqlite3_step(est) == SQLITE_ROW);
|
||||
sqlite3_finalize(est);
|
||||
if (!exists) {
|
||||
sqlite3_close(db);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_group_id) {
|
||||
// No-op valido: la entidad existe pero no puede tener group_id.
|
||||
sqlite3_close(db);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string ts = now_iso();
|
||||
const char* p[2] = { ts.c_str(), entity_id };
|
||||
bool ok = exec_one(db,
|
||||
"UPDATE entities SET group_id = NULL, updated_at = ? WHERE id = ?",
|
||||
p, 2);
|
||||
sqlite3_close(db);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool entity_duplicate(const char* db_path, const char* id,
|
||||
char* out_id, size_t out_id_n)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user