- Replace inline ImGui::BeginTable (6 cols) with data_table::render for 5 data
columns: status (Badge), enricher (Text), target (Text), progress (Progress
0..1), time_ms (Duration warn=1000ms error=10000ms).
- Add AppState::jobs_dt_state (data_table::State) for persistent filter/sort state.
- Keep Cancel/Delete action buttons via separate small ImGui::BeginTable (option a
from 0081-J spec); data_table::State does not expose selected_row_idx yet.
TODO(Phase 2): migrate actions to selected-row toolbar when State exposes it.
- Pre-filter cells by g_jobs_cache.filter_idx before passing to data_table so the
combo filter and the declarative table filter are both respected.
- Linux build: OK. Tests: 125/125 pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cierra el ciclo del analysis gliner_glirel_tuning: documenta en app.md el
pipeline NER+RE disponible en el registry y abre los dos issues que faltan
para cablearlo en extract_graph_hybrid + panel paste_extract. Archiva el
0042 original (mREBEL) tras la decision a favor de GLiNER2 (Apache 2.0,
joint NER+RE, 20-30x mas rapido en CPU).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 0038: lanzar Chrome/Edge/Brave externo con --remote-debugging-port +
--user-data-dir por profile, control via CDP desde cdp-cli Go.
Decision Go vs C++ in-process documentada; deja la puerta abierta a
un cliente C++ minimo solo para streaming en el futuro. Supersedes 0032.
- 0039: gestor de cookies/sesiones por profile via CDP — list, export
EditThisCookie, import, clear selectivo, health checks con selectores,
locks cuando un enricher esta usando el profile.
- 0040: profiles como concepto de primera clase — metadata (color, icon,
browser_preference, UA, project, template), templates anon/auth/work/
investigation, ProfilePicker reusable, project default, tag en
executions.metrics. Actualiza 0038 para apuntar a 0040 como duenio
del UX de profiles.
- Panel ImGui dockable: textarea, Extract button, preview tables (entities + relations)
- Subprocess directo a enrichers/paste_extract/run.py (no usa jobs system; preview puro)
- Pipeline Python emite preview JSON; commit a operations.db lo hace C++ con dedupe (type_ref, name)
- 12 tests pytest nuevos (paste_extract enricher + extract_panel logic)
- GLiNER/GLiREL path cableado pero no ejercitado en tests (modelos pesados); validacion interactiva pendiente
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- test_group_visual_inheritance.py (4 tests): homogeneo->Url heredado,
heterogeneo->generico Group, vacio->generico, subgrupos anidados
ignorados.
- test_manifest_threshold_override.py (4 tests): override 100 con 80
unicas no agrupa; override bajo (20) si agrupa cuando se supera;
threshold=0 cae al default 50; mirror Python del parser de manifest
C++ confirma el campo se extrae como int.
- test_schema_migration_group_id.py (3 tests): mirror Python de
project_migrate_schema, verifica idempotencia (1a y 2a apertura
no duplican columna), no-op sobre BD ya migrada, datos previos
sobreviven la migracion.
Subcomando que ejecuta SELECT DISTINCT type_ref FROM entities WHERE
group_id = ? AND type_ref != 'Group' (mismo SQL que el lado C++ de
apply_group_inherited_visuals). Devuelve homogeneous bool, child_types
ordenado y inherited (tipo unico o 'Group' generico). Permite a los
tests pytest validar el contrato sin ejecutar el binario.
12 tests cubriendo:
- modo preview (no escribe a operations.db)
- dedupe dentro de un run (mismo (type_ref, name) una sola vez)
- texto vacio retorna error con exit 2
- max_entities trunca al limite
- types filtra por tipo IoC
- use_hybrid=false ⇒ stats.layers solo regex
- runs idempotentes producen mismo proposal
Apply-side (replica en Python del extract_panel_apply C++):
- inserta solo selected
- dedupe por (type_ref, name)
- inserta relaciones cuando endpoints resuelven
- skip relacion si endpoint unselected
- dedupe relacion (from, to, name) en repeticion
GLiNER/GLiREL no se ejercitan en pytest — los modelos pesan
cientos de MB. La logica de hybrid se valida con regex+regex
(mismo path de merge/dedup) y con tests unitarios separados si
se quisiera. Se documenta la decision en el docstring del modulo.
Helper real_registry_root resuelve fn_registry desde un worktree
(el conftest del repo asume ancestor que en worktrees no existe).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- main.cpp: registra panel_extract en g_panels[], llama
extract_panel_init tras jobs_init y extract_panel_shutdown en el
cleanup, anade extract_panel_render(g_app) al render.
- CMakeLists.txt: anade extract_panel.cpp al target.
- app.md: declara extract_iocs_py_cybersecurity y
extract_graph_hybrid_py_pipelines en uses_functions (regla
uses_functions.md).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
extract_panel.{h,cpp}: panel ImGui dockeable con textarea grande,
boton Extract que lanza enrichers/paste_extract/run.py en un
std::thread aparte (no bloquea UI), tablas editables de entidades y
relaciones propuestas con checkboxes, y boton Apply Selected que
persiste a operations.db con dedupe por (type_ref, name) y por
(from, to, name) en relaciones.
Parser JSON ad-hoc (suficiente para el contrato del enricher) para
no añadir dependencias. Apply usa SQLite directamente (mismo
patron que entity_ops/jobs.cpp).
Anade panel_extract a AppState. La logica apply esta separada de
ImGui para poder testarla en aislamiento desde pytest.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Modo preview puro — no escribe a operations.db. Recibe texto via
params.text y devuelve JSON con entidades y relaciones propuestas.
Cascada: extract_iocs (regex) siempre + extract_graph_hybrid
(GLiNER+GLiREL) opcional con use_hybrid=true. La aplicacion procesa
el JSON y persiste con dedupe via codigo C++ (extract_panel_apply).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
apply_group_inherited_visuals(GraphData*, db_path) recorre los nodos
Group del grafo y, para cada uno, consulta los type_ref distintos de
sus hijos (entities con group_id apuntando al Group). Si todos
comparten un solo tipo, reasigna el type_id del Group al type_id de
ese tipo y fija shape_override = SHAPE_SQUARE para preservar el
cuadrado distintivo. Heterogeneo o sin hijos: el Group conserva su
visual generico (slate + ti-stack-2). Se invoca desde main.cpp y
reload_graph antes de apply_group_filter para que la reasignacion
sobreviva al compactado del array.
Manifest YAML puede declarar 'auto_group_threshold: <int>' a nivel
top-level. enrichers.cpp lo parsea y lo guarda en EnricherSpec.
jobs.cpp lo inyecta como campo opcional 'auto_group_threshold' en el
JSON stdin del subprocess. Los enrichers Python que crean Groups
(web_search, split_words, split_sentences, extract_iocs_text) leen el
campo y, si viene > 0, lo usan en lugar de su DEFAULT_GROUP_THRESHOLD.
Helper _coerce_threshold tolera int / str / None / 0 cayendo al default.
El resolver buscaba un marker 'registry.db' que falla en /home/lucas
con un .db parasito (4KB, sin tabla functions). Endurecemos el marker
a cmd/fn/main.go (mas estricto), anadimos override via FN_REGISTRY_ROOT
y un fallback a ~/fn_registry. Sin esto los tests de vendor_script
fallan al ejecutarse desde un git worktree.
Dos bugs reportados tras 0036d/0037:
1. Promote button en NodeGroups window kind=Group no respondia al
click. Causa: el Selectable con SpanAllColumns + AllowDoubleClick
se tragaba el click destinado al SmallButton(TI_ARROW_UP) sobre
la misma fila. ImGui tiene un flag dedicado para esto:
AllowOverlap. Anyadido al Selectable y los botones recuperan los
clicks. Mismo fix beneficia al kind=Table porque los botones
"promote" y "expand" de DuckDB rows estaban en la misma situacion
silenciosa.
2. Placement direccional de 0037 enviaba hijos hasta r=400 cuando
habia colisiones, "muy lejos" segun el usuario. Ajustes:
- Anillos mas cercanos: {60,90,120,150,180,210,240} en vez de
{80,140,200,280,400}. Maximo 240px del source.
- Mas anillos disponibles (7 vs 5) — fan-out gradual sin saltar
bruscamente de 80 a 140.
- Espaciado entre hermanos en arco usa cluster_min_dist=35 en
vez de min_dist=60. Permite mas hijos por anillo dentro del
arco de 45 grados (cap @ r=240 = 5 vs 3 antes).
- Para 10-15 hijos tipicos los inner rings cubren todo dentro
de 200px del source.
Build limpio. Tests WSL 102 / Windows 91 + 11 skipped.
Bonus: borrado /home/lucas/fn_registry/cpp/registry.db (vacio, 0
bytes, creado por algun binario con flag O_CREAT) — violacion de
db_locations.md (registry.db solo en raiz del repo). Era el motivo
de un test flaky de python_runtime_resolver.
Antes los hijos del mismo anchor se distribuian en un anillo de 360
grados alrededor del padre. Cuando un enricher producia 10+ hijos,
se llenaban todas las direcciones y se pisaban nodos preexistentes.
Ahora los hijos se reparten en un abanico de 45 grados (pi/4) saliendo
del anchor en la direccion outward (vector anchor - centroide del resto
del grafo). Si solo hay 1 nodo placed o coincide con el anchor, default
a la derecha (0 rad). Capacidad por anillo restringida al arco
(arc_span * r / min_dist), con fallback de subida de radio en mismo
angulo si el slot ideal colisiona con un nodo no-orphan.
Solo afecta la pasada 2 (orphans con anchor). Pasadas 1 y 3 intactas.
build limpio, 102 pytest passed (WSL) + 91 passed/11 skipped (Windows).
Refs: issues/0037-directional-orphan-placement.md
Anyade un item al menu View del framework via el nuevo callback
AppConfig.view_extras. El item:
- Esta enabled solo si la seleccion del viewport (o, en su defecto,
el inspector) apunta a un nodo con type_ref Table o Group.
- Click → resuelve sql_id via entity_index_lookup, deriva
NodeGroupsKind del type_ref y llama
views_node_groups_open(g_app, sql_id, kind, ops_db). La API
marca focus_request=true (cubierto por 0036c), de modo que la
window emerge al frente si ya existia.
- Disabled → tooltip 'Select a Table or Group node first' (mostrado
con AllowWhenDisabled).
Sin atajo de teclado (descartado por el usuario).
Sin submenu de windows abiertas (fase 2).
Refs: issues/0036f-view-menu-open-nodegroups.md
- Reusa la infra de focus existente (AppState::want_focus_entity /
focus_entity_id) ya cableada en main.cpp desde 0011.
- kind=Group: single click sobre la fila pone want_focus_entity con
row.id; tooltip "Click to focus entity in viewport" en hover.
El doble click sigue funcionando (mismo efecto). El menu contextual
y el boton Promote-out-of-group quedan intactos.
- kind=Table promovida (row.promoted_entity_id no vacio): single click
pone want_focus_entity con promoted_entity_id; tooltip de focus.
- kind=Table no promovida: single click es no-op visual; tooltip
"promote first to focus\n(double click or right click to promote)"
como hint sutil. El doble click sigue lanzando el flujo de promote
(legado de 0036c) y el menu contextual ofrece Promote.
- Sin cambios en el handler de main.cpp — la logica de pan/zoom + select
+ load inspector ya existe y se reutiliza tal cual.
- Sin tests Python nuevos: el comportamiento es UI ImGui (no testeable
desde pytest). 102 passed WSL / 91+11 skipped Windows sin regresion.
Refs: issues/0036e-row-click-focus-viewport.md
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
Cambia el dispatch del doble click sobre nodos del viewport: si el tipo
es Group o Table, ahora abre/enfoca la NodeGroups window correspondiente
via views_node_groups_open(...). El branch de Group ya no carga el
panel Table generico con un filtro group_id (logica heredada de 0035d
que provocaba el bug de "tabla vacia").
Limpieza correlativa en views_table:
- Eliminado el breadcrumb "Group: <name> (N)" + boton Clear filter.
- Eliminado el filtro r.group_id != table_filter_group_id en
build_visible y la restriccion de types_present.
- Eliminado el reset on-close de los campos de filtro.
Eliminados los campos AppState::table_filter_group_id y
table_filter_group_name (audit: git grep table_filter_group_id devuelve
vacio fuera de issues/).
Render de NodeGroups ahora consume focus_request: llama
SetNextWindowFocus() antes de Begin y SetWindowFocus() dentro, asi la
window queda al frente tanto al crearse como al re-enfocarse.
El right-click "Open NodeGroups" del context menu sigue intacto
(want_toggle_nodegroups + node_groups_set_expanded). El doble click es
flujo paralelo nuevo.
Refs: issues/0036c-double-click-group-opens-nodegroups.md
NodeGroupsWindowState gana un discriminador `kind` (Table | Group) y
un flag `focus_request` (lo consumira 0036c). Por defecto Table, asi
que el flujo historico (DuckDB rows tras expand de un nodo Table) no
cambia.
kind=Group lee directamente operations.db consultando
`entities WHERE group_id = container_id` con columnas fijas
(id, name, type_ref, status, updated_at) ordenadas por updated_at DESC.
Los nuevos loaders viven en node_groups.cpp:
- node_groups_count_for_group -> SELECT count(*) ...
- node_groups_page_for_group -> SELECT id,name,type_ref,status,
updated_at ... LIMIT ? OFFSET ?
Para columnas, opcion (A) del issue: pre-popular meta.columns con la
lista fija al abrir kind=Group, asi el render se mantiene generico.
NodeGroupsRow.values guarda los 5 campos en ese orden y row.id es la
key natural (= entity_id de la fila — al ser ya entidad, no hace falta
promocionarla).
Render en views.cpp ramifica por kind:
- Table: layout original [id_col + columns + promoted] con doble
click -> promote/focus.
- Group: layout [columns fijas] sin promoted. Doble click sobre la
fila ya pone want_focus_entity = id (los flujos posteriores 0036c-e
afinan UX). Right click ofrece "Focus in Inspector".
main.cpp dispatcha por kind al refrescar paginas y, al cerrar via X,
solo llama a node_groups_set_expanded para kind=Table (Group no usa
ese flag).
views_node_groups_windows_sync se hace kind-aware: solo reconcilia
entries kind=Table contra el set de Tables expandidas; no toca las
entries kind=Group (las gestiona views_node_groups_open).
Nueva API publica:
views_node_groups_open(app, container_id, kind, ops_db)
Crea o reusa la entry, setea focus_request=true y para kind=Group
pre-popula meta.columns + intenta leer `name` del Group para el
titulo. Sin caller todavia — la consume 0036c.
Tests:
- tests/test_node_groups_loader.py (6 tests) verifica el contrato
SQL via gx-cli. Nuevo subcomando `gx-cli group page <id>` espejea
el loader C++ exactamente (mismo SQL); tambien expuesto como tool
MCP `group_page` para que Echo pueda inspeccionar Groups.
Resultado:
- WSL: 89 -> 95 passed
- Windows: 78+11 -> 84+11 passed
- Build C++ Windows limpio, sin warnings nuevos.
- Regresion kind=Table: comportamiento identico (mismo render,
mismo loader DuckDB).
Refs: issues/0036b-kind-discriminator-and-group-loader.md