Commit Graph

141 Commits

Author SHA1 Message Date
egutierrez 3c98fee443 Merge quick/browser-cdp-issues — issues 0038, 0039, 0040 (browser externo + CDP + profiles) 2026-05-04 22:16:46 +02:00
egutierrez 8733b7d175 docs(issues): browser externo + CDP + multi-profile (0038, 0039, 0040)
- 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.
2026-05-04 22:16:42 +02:00
egutierrez 2a49c2b3fa Merge issue 0013 — Paste & Extract panel
- 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>
2026-05-04 14:31:54 +02:00
egutierrez f614a51c58 Merge issue 0035e — polish del Group + tests cross-platform
- Iconografia heredada del tipo mayoritario (homogeneo) o slate generico
- Threshold via manifest auto_group_threshold propagado a Python
- 11 tests pytest nuevos (visual inheritance, threshold override, migration)
- gx-cli group visual <id> mirror del SQL
- conftest.py endurecido: marker estricto + FN_REGISTRY_ROOT override

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 14:31:45 +02:00
egutierrez 65a4e7f4a8 docs: cerrar issue 0035e 2026-05-04 14:25:40 +02:00
egutierrez deb86b24ec test(0035e): cobertura del visual heredado, threshold override y migracion idempotente
- 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.
2026-05-04 14:25:03 +02:00
egutierrez 5417834950 feat(0035e): gx-cli group visual <id> espejea visual heredado del Group
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.
2026-05-04 14:24:54 +02:00
egutierrez 5056b5e0d8 docs(0013): cerrar issue 0013 — paste & extract panel
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 14:24:47 +02:00
egutierrez 2233280302 test(0013): pytest suite for paste_extract
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>
2026-05-04 14:24:44 +02:00
egutierrez 992e8fa06c feat(0013): wire extract_panel into main + CMakeLists + app.md
- 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>
2026-05-04 14:24:32 +02:00
egutierrez fdc6b91f4d feat(0013): add extract_panel — UI + subprocess + apply (dedupe)
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>
2026-05-04 14:24:26 +02:00
egutierrez 009d387d9a feat(0013): add paste_extract enricher (preview-only)
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>
2026-05-04 14:24:16 +02:00
egutierrez c27d8e7ffc feat(0035e): Group hereda iconografia de hijos homogeneos
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.
2026-05-04 14:21:01 +02:00
egutierrez 52495af779 feat(0035e): manifest auto_group_threshold override + propagacion a Python
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.
2026-05-04 14:20:52 +02:00
egutierrez 65a14749f3 test(0035e): conftest resolver tolerante a worktrees fuera de fn_registry/
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.
2026-05-04 14:20:44 +02:00
egutierrez c6d17998e7 docs(app.md): document additional uses_functions deps
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 11:46:48 +02:00
egutierrez 27849200ce Merge issue/0037b-fix-promote-button-and-tighter-placement 2026-05-04 01:36:54 +02:00
egutierrez 616c46297b fix: promote button funciona + placement direccional mas cercano
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.
2026-05-04 01:36:54 +02:00
egutierrez 28548e053d Merge issue/0037-directional-orphan-placement 2026-05-04 01:27:13 +02:00
egutierrez fdd169bc35 feat(0037): placement direccional 45 grados de orphans (away from centroide)
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
2026-05-04 01:27:11 +02:00
egutierrez 502ce80b9f Merge issue/0036f-view-menu-open-nodegroups 2026-05-04 01:13:03 +02:00
egutierrez f6f53b60c3 feat(0036f): view menu accion 'Open NodeGroups for selected'
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
2026-05-04 01:12:58 +02:00
egutierrez 3e71fcc4ca docs(0036e): mark issue as done 2026-05-04 01:06:41 +02:00
egutierrez 436c23d155 Merge issue/0036e-row-click-focus 2026-05-04 01:06:34 +02:00
egutierrez f4e4dd5a0b feat(0036e): row click en NodeGroups enfoca la entidad (kind-aware)
- 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
2026-05-04 01:06:30 +02:00
egutierrez 8bfe0b841c Merge issue/0036d-promote-kind-aware 2026-05-04 01:03:14 +02:00
egutierrez f0d8a5ad04 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
2026-05-04 01:03:11 +02:00
egutierrez 98e744ea4e docs(0036c): mark issue as done 2026-05-04 00:56:50 +02:00
egutierrez 176d9b232d Merge issue/0036c-doubleclick-group-opens-nodegroups 2026-05-04 00:56:47 +02:00
egutierrez 8f91b4ed23 feat(0036c): doble click en Group abre NodeGroups; cleanup Table panel
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
2026-05-04 00:56:44 +02:00
egutierrez 7277f63985 docs(0036b): mark issue as done 2026-05-04 00:52:36 +02:00
egutierrez b27578f093 Merge issue/0036b-kind-and-group-loader 2026-05-04 00:52:36 +02:00
egutierrez d6e13fddc3 feat(0036b): NodeGroups admite kind=Group + loader entities
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
2026-05-04 00:52:25 +02:00
egutierrez 2a783187a3 Merge issue/0036a-rename-nodegroups 2026-05-04 00:43:25 +02:00
egutierrez f8b16b2a5a docs(0036a): mark issue as done 2026-05-04 00:43:22 +02:00
egutierrez 810b564127 refactor(0036a): rename Table-expanded -> NodeGroups (paperwork)
Rename masivo sin cambio de comportamiento. Habilita 0036b-f que ya
asumen la nueva convencion.

Archivos:
- tableview.{cpp,h} -> node_groups.{cpp,h} (git mv para preservar history)
- CMakeLists.txt: tableview.cpp -> node_groups.cpp

Tipos:
- TableWindowState  -> NodeGroupsWindowState  (views.h)
- TableMetadata     -> NodeGroupsMeta         (node_groups.h)
- TablePageRow      -> NodeGroupsRow          (node_groups.h)

Campos AppState:
- table_windows         -> node_groups_windows
- table_node_counts     -> node_groups_counts
- toggle_expanded_id    -> toggle_nodegroups_id
- want_toggle_expanded  -> want_toggle_nodegroups

Funciones (window por contenedor — NO el panel generico Table):
- tableview_create / count / page / smoke_test / resolve_path /
  refresh_counts / list_columns / get_metadata / set_expanded /
  set_columns / promote_row / demote_row / ingest_file
  -> prefijo node_groups_*
- views_table_window         -> views_node_groups_window
- views_table_windows_sync   -> views_node_groups_windows_sync
- views_table_overlay        -> views_node_groups_overlay

Strings de UI:
- "Expand table" / "Collapse table" -> "Open NodeGroups" / "Close NodeGroups"
- Window title "<icon> <name>" -> "<icon> NodeGroups: <name>"
- Tooltip "(no expanded tables)" -> "(no open NodeGroups)"
- Logs [tableview_*] -> [node_groups_*]

Preservados intencionalmente (no son cambio de identificadores C++):
- CLI flag --test-tableview (cambiarlo seria cambio de behavior publico)
- Valor 'tableview' en columna entities.source (cambiarlo afectaria
  datos persistidos en BD)

NO tocado:
- Panel generico Table (views_table, panel_table, table_rows,
  table_show_all, table_search_buf, table_filter_*, table_col_filters,
  table_active_tab, TableRow, table_filter_group_*, etc.)
- issues/completed/* (historia)

Verificacion:
- Build C++ Linux + Windows: green sin warnings nuevos.
- pytest WSL: 89 passed.
- pytest Windows: 78 passed + 11 skipped.
- git grep audit: solo residuos en issues/ (historia) + CLI flag y
  source DB value preservados.

Refs: issues/0036a-rename-nodegroups.md
2026-05-04 00:43:16 +02:00
egutierrez 441a697abf Merge issue/split-words-enricher 2026-05-04 00:14:57 +02:00
egutierrez 352b27d488 feat: enricher split_words para probar grouping con volumen alto
split_sentences a menudo no llega al umbral de 50 (un texto medio
tiene 5-15 frases). split_words tokeniza el mismo notes en palabras
y trivialmente lo supera con cualquier parrafo decente -> Group
visible y testeable end-to-end sin necesidad de pegar megabytes.

Diferencias respecto a split_sentences:

* Splits por regex de letras (incluye acentos espanyoles + apostrofo
  interno como "don't"). Numeros y puntuacion ignorados.
* Lowercase + filtro por min_length (default 3, filtra a/el/de/y/o).
* Param `dedupe` (default true): vocabulario unico vs cada ocurrencia.
  Con dedupe=false sirve como stress test de volumen.
* Tipo `Word` en types.yaml: amarillo, ti-letter-w, principal_field=word.
* Relacion `WORD_OF` desde cada Word al source.
* Mismo patron de grouping que split_sentences (threshold 50, K=10
  preview, batch_id en metadata, Group con count + enricher).

Tests:

* below threshold no crea Group.
* >=50 tokens unicos -> Group + 10 sueltos + resto agrupados.
* dedupe=true (default) colapsa repeticiones; dedupe=false las
  conserva como nodos separados.
* min_length filtra correctamente.
* notes prioriza sobre node_name.
* texto vacio -> exit 2.
* max_words trunca.

WSL 89 / Windows 78 + 11 skipped.
2026-05-04 00:14:57 +02:00
egutierrez 87b9a4dc02 Merge issue/agent-jobs-path-and-logs 2026-05-03 16:32:22 +02:00
egutierrez 652ff6f02c fix(agent_jobs): queue dir desde GX_APP_DB, no GX_APP_DIR + logs verbosos
Bug derivado del fix anterior: gx-cli escribia ficheros JSON en
`$GX_APP_DIR/agent_jobs_queue/` (apuntando al repo fuente) mientras
main.cpp escaneaba `parent(g_layout_db_path)/agent_jobs_queue/`
(install Windows). Dos directorios distintos -> jobs huerfanos.

Echo reportaba "encolado" pero el worker nunca veia los ficheros.
La causa: chat.cpp setea GX_APP_DIR=<registry>/projects/osint_graph/
apps/graph_explorer y GX_APP_DB=<install>/local_files/projects/<slug>/
graph_explorer.db. Dos sitios. Solo APP_DB coincide con donde
graph_explorer.exe escanea (parent del .db).

Fix:

* gx-cli cmd_enricher_run: queue_dir = parent(GX_APP_DB) /
  agent_jobs_queue. Alineado con main.cpp.
* gx-cli: nuevo helper `_log(tag, msg)` que escribe a stderr Y a
  `<parent(app_db)>/gx-cli.log` para auditoria persistente. Cubre
  node_create, node_update, node_delete, rel_create, enricher_run.
* gx-cli mcp _mcp_log tambien persiste a gx-cli.log.
* main.cpp: log el queue scan dir una vez por sesion para detectar
  mismatches a futuro.
* .gitignore: agent_jobs_queue/ y gx-cli.log son runtime, no se
  commitean.

Tests:

* test_enricher_run_queue_dir_derives_from_app_db (regresion)
  configura GX_APP_DB en un dir distinto de GX_APP_DIR y verifica
  que el JSON aterriza junto a APP_DB.
* test_enricher_run_writes_log_to_gx_cli_log valida la auditoria.

WSL 81 / Windows 70 + 11 skipped.
2026-05-03 16:32:22 +02:00
egutierrez b67c44e8f9 Merge issue/agent-jobs-file-queue 2026-05-03 16:23:18 +02:00
egutierrez 3e7b3adc16 fix(agent_jobs): mover cola de SQLite a ficheros JSON (cross-9p safe)
Bug: Echo (gx-cli en WSL) recibia "disk I/O error" al INSERT en la
tabla `agent_jobs` de graph_explorer.db. Causa: graph_explorer.exe
mantiene esa BD abierta con journal_mode=WAL desde Windows, y SQLite
WAL exige mmap del .shm compartido entre procesos. Cuando un escritor
accede via /mnt/c (9p) y el otro nativo NTFS, ese mmap falla.

El proyecto ya habia resuelto este patron antes: el contador de
mutaciones (.mutations.marker) usa fichero plano en vez de SQL por
exactamente la misma razon. agent_jobs era la unica cola que se
quedo en SQLite — momento de aplicar el mismo fix.

Cambios:

* gx-cli cmd_enricher_run: en lugar de INSERT, escribe
  `<app_dir>/agent_jobs_queue/<req_id>.json` con el payload del job.
  Atomic write (tmp + rename, atomico tanto en NTFS como en 9p).
* main.cpp polling: en lugar de SELECT/DELETE sobre agent_jobs,
  escanea ese directorio cada frame, lee cada JSON via json_extract
  (sqlite3 in-memory, sin tocar archivos en disco), llama jobs_submit,
  y borra el fichero. Throttle a 8 jobs por frame igual que antes.
* main.cpp: anyade <filesystem> y <fstream>.
* tests/test_gx_cli.py: 5 tests nuevos en TestCliEnricherRun:
  - escribe fichero JSON con req_id como nombre
  - NO crea tabla agent_jobs en graph_explorer.db (regresion)
  - errores claros si enricher o nodo no existen
  - no quedan .tmp tras encolado exitoso

WSL 79 / Windows 68 + 11 skipped.
2026-05-03 16:23:18 +02:00
egutierrez 82a576b844 Merge issue/mcp-notes-bugfix 2026-05-03 16:09:47 +02:00
egutierrez e35c30cdf7 fix(gx-cli mcp): expone notes/append_notes en MCP y bloquea regresion
Bug encontrado por el agente Echo: el MCP server gx-cli (subcomando
`mcp-server`) llamaba a cmd_node_create / cmd_node_update con un
SimpleNamespace que NO incluia `notes`, asi que `args.notes` lanzaba
AttributeError. Causa raiz: MCP_DISPATCH no defaulteaba `notes` ni
`append_notes`, y el inputSchema de las tools tampoco los anunciaba.

Cambios:

* MCP_TOOLS["node_create"].inputSchema.properties anyade `notes`.
* MCP_TOOLS["node_update"].inputSchema.properties anyade `notes`
  + `append_notes` (boolean, default false).
* MCP_DISPATCH["node_create"] defaultea `notes: None`.
* MCP_DISPATCH["node_update"] defaultea `notes: None`,
  `append_notes: False`.

Tests nuevos en tests/test_gx_cli.py (30 tests):

* CLI: node create/update/delete con notes (replace + append),
  list/show/search, rel create/list/delete con cascada, query
  read-only que rechaza writes, autodetect de tipos.
* MCP dispatcher: cada cmd_* tolera args opcionales omitidos,
  notes y append_notes funcionan via dispatch, MCP_TOOLS y
  MCP_DISPATCH coinciden 1:1 (sanity contractual).
* Regresion 0035d: tests dedicados que congelan el contrato
  notes/append_notes en defaults e inputSchema — si alguien
  vuelve a quitarlos el test se queja inmediatamente.

WSL 74 / Windows 63 + 11 skipped.
2026-05-03 16:09:47 +02:00
egutierrez a0921d8a2c Merge issue/gx-cli-notes-support 2026-05-03 15:40:59 +02:00
egutierrez 9b9df496da feat(gx-cli): expone columna notes en node create/update
El agente Echo (chat panel + claude -p) usa gx-cli para todas las
mutaciones del grafo. Antes solo podia setear name/type/status/
description/tags — pero los enrichers nuevos split_sentences y
extract_iocs_text leen entities.notes (lo que se escribe en el panel
Note del Inspector). Sin este flag, el agente no podia darle texto
largo a un nodo para luego enricharlo.

Cambios:
- node create: --notes "..." (vacio por default).
- node update: --notes "..." (replace) + --append-notes (concat con
  doble newline como separador, util para acumular contexto sin
  pisar lo previo).

Smoke test: replace y append funcionan, default vacio sigue ok.
2026-05-03 15:40:58 +02:00
egutierrez e4524684d7 Merge issue/text-enrichers-read-notes 2026-05-03 15:36:23 +02:00
egutierrez 2a5127fcaf fix(enrichers): split_sentences y extract_iocs_text leen entities.notes
El campo `notes` es lo que el usuario escribe en el panel Note del
Inspector (doble click sobre el nodo) — sitio canonico para texto
largo. Antes los enrichers leian metadata.text/description/query como
prioridad, dejando notes ignorado y forzando al usuario a inyectar
texto via la UI metadata-extra (poco descubrible).

Cambios:
- Ambos run.py abren la BD y leen `entities.notes` por SQL antes de
  fallback a node_name. metadata.text/description/query ya no se
  consultan (KISS — solo notes y name).
- conftest.make_node admite kwarg `notes` para inyectar contenido
  en la columna notes desde tests.
- Tests actualizados: SAMPLE_TEXT y los IoC dumps van por `notes=`
  en lugar de `metadata={"text": ...}`.
- Renombrado el test que verificaba prioridad: ahora se llama
  `*_uses_notes_priority` y verifica notes > name.

Tests verdes WSL (44) y Windows (33 + 11 skipped).
2026-05-03 15:36:18 +02:00
egutierrez bcc86c43c1 Merge issue/offline-text-enrichers 2026-05-03 15:20:41 +02:00
egutierrez 0e435c2e21 feat: enrichers offline split_sentences + extract_iocs_text
Para probar la app sin depender de red (DDG bloquea con captcha desde
ciertas IPs). Ambos aplican grouping (umbral 50, preview K=10) replicando
el patron de web_search.

- split_sentences: parte texto en frases (regex), crea nodos Sentence
  conectados con SENTENCE_OF.
- extract_iocs_text: variante de extract_text_entities que lee directo
  metadata.text/description/name, sin requerir fetch previo. Vendoriza
  extract_iocs_py_cybersecurity. Multi-tipo, agrupado en un solo Group
  heterogeneo (decision 6 multi-grupo-por-tipo es fase 2).
- Tipo Sentence en types.yaml.

Tests pytest cubren below/above threshold para ambos.
2026-05-03 15:20:39 +02:00