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:
2026-05-04 01:03:11 +02:00
parent 98e744ea4e
commit f0d8a5ad04
8 changed files with 311 additions and 5 deletions
+84
View File
@@ -0,0 +1,84 @@
---
id: 0036d
title: Promote en NodeGroups ramificado por kind (Table row vs Group child)
status: done
priority: high
created: 2026-05-04
parent: 0036
depends_on: [0036b]
---
## Objetivo
Boton "Promote" por fila en la NodeGroups window. La accion se
ramifica por `kind`:
- **kind = Table** (DuckDB row): comportamiento actual del issue 0011
— INSERT entity nueva apuntando a la fila DuckDB. Idempotente: si
ya existe entidad para ese row_id, no duplica.
- **kind = Group** (entidad hija): UPDATE clear group_id. La entidad
sale del grupo y aparece como nodo libre en el canvas tras reload.
## Cambios
### Nueva op en `entity_ops` (Group promote)
```cpp
// Saca la entidad de su grupo (group_id = NULL). No-op si ya estaba
// fuera (group_id ya era NULL). Idempotente.
bool entity_clear_group_id(const char* db_path, const char* entity_id);
```
Tambien anyadir su pareja MCP en `gx-cli`:
```
gx-cli node update <id> --clear-group-id
```
(Para que el agente Echo pueda promover entidades del grupo via MCP.
Argumento booleano que dispara la op directa, sin pasar por el flujo
de --notes/--name.)
### Render del boton Promote
En la pintura de la window (en `views.cpp`), por cada fila visible
mostrar un `SmallButton(TI_ARROW_UP)` con tooltip:
- kind = Table: `"Promote row to entity"` — comportamiento existente.
- kind = Group: `"Promote out of group (move to canvas)"` — llama a
la nueva op.
Tras la accion, marcar `g_app.want_reload = true` para que el grafo
se refresque y la entidad reaparezca suelta.
### Tooltip del header
Cuando entras a la window, mostrar un texto suave indicando el
comportamiento del Promote segun kind. Una sola linea, color text
muted.
## Acceptance criteria
- Click promote en row de Group: la entidad pierde `group_id`
(verificable por SQL), reload del grafo la muestra suelta
colgando del source via la relacion existente.
- Click promote en row de Group ya promovida (group_id ya NULL):
no-op sin error.
- Click promote en row de Table: comportamiento como hoy. Sin
regresion en flow existente.
- Tooltip diferenciado entre kinds.
- Tests pytest:
- `test_entity_clear_group_id_removes_membership`
- `test_entity_clear_group_id_idempotent`
- `test_gx_cli_node_update_clear_group_id` (CLI)
- MCP regression: `clear_group_id` en defaults
## TBD
Branch `issue/0036d-promote-kind-aware`, merge `--no-ff` a master.
## Out of scope
- Promote masivo (multi-select) — fase 2.
- Re-agrupar por tipo (multi-select dentro de NodeGroups window) —
fase 2.