Commit Graph

107 Commits

Author SHA1 Message Date
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
egutierrez 092ad2801e docs(0035d): mark issue as done 2026-05-03 14:57:28 +02:00
egutierrez e71c31264a Merge issue/0035d-tableview-drill-in 2026-05-03 14:57:26 +02:00
egutierrez b67da92e18 feat(0035d): doble click en Group abre tableview filtrado por group_id
- entity_ops: EntityRowSnapshot.group_id + SQL con COALESCE(group_id,'')
  + deteccion via PRAGMA para BDs viejas sin la columna.
- views.h: TableRow.group_id + AppState.table_filter_group_id /
  table_filter_group_name (RAM-only).
- main.cpp: dispatch en want_open_note — si type_ref == "Group", setea
  filtro de grupo + abre panel Table en vez de Note. Reset de search
  buf y col_filters al entrar al drill-in para que el usuario vea todo
  el contenido del grupo.
- views.cpp: build_visible compone group_id con search/tabs/col_filters
  (AND). types_present se reduce a tipos presentes en el grupo cuando
  hay drill-in activo. Header pintado en amarillo con TI_FOLDER +
  contador + boton "Clear group filter". Al cerrarse el panel se
  limpia el filtro automaticamente.

Tests: pytest 35 passed (WSL) / 24 passed + 11 skipped (Windows).

Refs: issues/0035d-tableview-drill-in.md
2026-05-03 14:57:20 +02:00
egutierrez eff273a2d4 docs(0035c): mark issue as done 2026-05-03 14:52:35 +02:00
egutierrez f08cdb4db4 Merge issue/0035c-web-search-creates-groups 2026-05-03 14:52:31 +02:00
egutierrez 67f10a8afd feat(0035c): web_search crea Group cuando excede umbral
Cuando un enricher web_search produce >= 50 resultados, los primeros 10
quedan sueltos colgando del source (preview Twitter/Reddit) y los
restantes entran como hijos de un nuevo nodo Group cuadrado.

Cambios:
- enrichers/web_search/run.py:
  - DEFAULT_GROUP_THRESHOLD=50, GROUP_PREVIEW_K=10 (constantes globales).
  - has_group_id_column(): detecta si el schema soporta agrupacion.
  - insert_group_entity(): crea nodo Group con metadata
    {enricher, query, count, batch_id}.
  - insert_url_entity() acepta batch_id y group_id; los inyecta en
    metadata/columna respectivamente. Nodos existentes mantienen su
    group_id actual (no se machaca).
  - Generacion de batch_id (UUID4 hex) por ejecucion, compartido por
    todos los nodos creados (group + sueltos + agrupados).
  - Cada hijo del grupo conserva su relacion individual SEARCH_RESULT_OF
    al source original — la procedencia es la relacion real, no el
    contenedor.
  - El JSON de salida añade batch_id, group_id, grouped.

- tests/conftest.py: añade columna entities.group_id al SCHEMA_SQL y
  expone group_id en list_entities() para que los tests lo verifiquen.

- tests/test_web_search.py: 3 tests nuevos
  - below_threshold_no_group: 5 resultados → 0 Groups, comportamiento clasico.
  - above_threshold_creates_group_and_preview: 100 resultados → 1 Group +
    10 sueltos + 90 con group_id, todos con SEARCH_RESULT_OF al source.
  - batch_id_shared_across_outputs: group + preview + hijos comparten
    batch_id.
  - _build_lite_html() genera HTML sintetico con N resultados sin
    necesidad de fixture estatico grande.

Tests: 35 passed (32 previos + 3 nuevos) en WSL.
       24 passed + 11 skipped en Windows.

Refs: issues/0035c-web-search-creates-groups.md
2026-05-03 14:52:29 +02:00
egutierrez 784b56ba10 Merge issue/0035b-renderer-hides-grouped-children 2026-05-03 14:48:20 +02:00
egutierrez d3da1416f3 feat(0035b): renderer oculta hijos de grupos colapsados + dedup aristas
- AppState anade `group_expanded` (unordered_map<string,bool>) en RAM,
  default vacio = todos los grupos colapsados al arranque. Sin
  persistencia entre sesiones (fase 1).
- `apply_group_filter(GraphData*, db_path, expanded)` consulta
  entities (id, group_id, type_ref) de operations.db, marca como
  ocultos los nodos cuyo group_id apunta a un grupo no expandido,
  compacta `g->nodes` y re-mapea indices de aristas.
- Aristas:
  * Cross-edge (un extremo oculto, otro fuera): se redirige el
    extremo oculto al nodo del grupo. Sin dedup (issue 0035 dec. 5).
  * Internas (ambos extremos en el mismo grupo colapsado): se ocultan.
  * Inter-grupo (ambos en grupos colapsados distintos): dedup por
    par no ordenado (group_a, group_b) + rel_type, una linea por par.
  * Orfanas (group_id apunta a un grupo no presente en grafo): el
    nodo se oculta y sus aristas se descartan.
- Centralizado: el filtro corre en `reload_graph()` cuando se le
  pasa `group_expanded`, y en `load_input()` tras el load inicial.
  Cubre las 4 rutas de carga del app (toolbar reload, mutaciones,
  inspector save, primera carga / switch project).
- Idempotente sobre un grafo ya filtrado y robusto frente a BDs sin
  columna `group_id` (schema antiguo) — no toca el grafo.

Smoke test manual con 3 BDs sintéticas:
- Grupo + 2 children + edges cruzadas/internas: nodes 5→3, edges
  4→3 (internal hidden, cross redirected).
- 2 grupos con 4 cross-edges entre ellos: edges 4→1 (dedup).
- group_id huerfano: nodo oculto + arista descartada.

Build clean en Windows. Tests verdes:
- WSL pytest: 32 passed.
- Windows pytest: 21 passed + 11 skipped.

Refs: issues/0035b-renderer-hides-grouped-children.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 14:48:17 +02:00
egutierrez fbe408015d Merge issue/0034b-prior-decisions-catchup 2026-05-03 14:41:41 +02:00
egutierrez 7a94160fd2 feat: catch-up de decisiones previas (Webpage→Url, anti-bot, UI 2-col, tests cross-platform)
Bloque de cambios revisados y validados con el usuario en sesiones
previas que no habian aterrizado en commits propios. Lista por tema:

* enrichers: web_search ahora usa lite.duckduckgo.com como endpoint
  primario (mas tolerante con bot detection desde IP residencial),
  con fallback al endpoint html. Detecta pagina captcha y emite
  error claro si ambos fallan. Anyade _DDGLiteParser para el formato
  lite + auto-pick de parser por contenido.

* enrichers: tipo Webpage unificado en Url (campos de cuerpo
  cacheado viven en metadata del Url). Manifests actualizados
  (applies_to: [Url]). fetch_webpage ya no convierte Url->Webpage.

* enrichers/manifest: campo `params` parseado a EnricherSpec.params
  (name, type, default_value, description). UI puede renderizar
  dialog de configuracion.

* jobs: fix de path conversion para Python embebido nativo Windows
  (no convertir a /mnt/c/... cuando el subproceso es Windows-native;
  solo cuando es bash o python via WSL).

* main.cpp: ventana ImGui (no modal) "Run enricher" con layout
  2-col (label izq, input der). Inserta job con JSON tipado. Layout
  clustering apretado: hijos del mismo anchor en un solo anillo
  alrededor del padre, sin desperdigar por anillos crecientes.

* views: inspector con layout 2-col via BeginTable (Identity,
  Schema fields, Extras). Description full-width debajo de su label.

* tests: portable conftest (auto-detecta REGISTRY_ROOT, PYTHON_BIN,
  ENRICHERS_DIR para WSL y Windows portable). _runner.py trampoline
  inyecta stub via sys.path porque embedded Python ignora PYTHONPATH.
  Tests bash-only (vendor_script, freeze, dispatcher bash, resolver
  Linux-binary) skipean en Windows. Tests existentes adaptados a
  Webpage->Url.

Resultado actual: 32 passed WSL, 21 passed + 11 skipped Windows.
2026-05-03 14:41:28 +02:00
egutierrez 4be5734ce5 docs(0035a): mark issue as done 2026-05-03 14:23:33 +02:00
egutierrez a80834f2ac Merge issue/0035a-group-type-and-schema 2026-05-03 14:23:26 +02:00
egutierrez fc4f0824da feat(0035a): tipo Group + columna group_id en entities
Plumbing para issue 0035 — agrupacion de resultados de enrichers
cuando exceden umbral. Sin cambios visibles para el usuario todavia.

- Migracion idempotente: ALTER TABLE entities ADD COLUMN group_id si
  no existe (detectado via PRAGMA table_info). Se ejecuta al abrir
  el proyecto en switch_to_project y en el bootstrap inicial.
- Tipo Group en examples/types.yaml (template) y en el types.yaml
  del proyecto default activo en Windows.
- shape=square (regla en types_registry.cpp extendida a Group),
  color=#94A3B8, icon=ti-stack-2.
- Fields: name (req), count (int), enricher (string), batch_id (string).

Refs: issues/0035a-group-type-and-schema.md
2026-05-03 14:23:23 +02:00
egutierrez b0706b71c0 merge: issue/assets-subfolder — distribuibles en assets/ 2026-05-03 00:50:48 +02:00
egutierrez 8623732d6d feat(graph_explorer): adopta layout assets/ via fn::asset_path
Junto con el cambio del framework (commit 81d8a7c9), graph_explorer
ahora resuelve enrichers/, runtime Python y gx-cli desde
<exe_dir>/assets/ con fallback a las rutas dev legacy.

- main.cpp: enrichers_dir busca primero <exe_dir>/assets/enrichers/
  (deploy con /compile). Fallback a <app_dir>/enrichers/ del repo
  cuando se ejecuta desde build/ (modo dev).
- jobs.cpp::resolve_python_runtime: incluye
  <exe_dir>/assets/runtime/python/{python.exe|bin/python3} como
  primera opcion de la cadena de fallback. La opcion legacy sin
  assets/ queda como segundo intento.
- chat.cpp: gxcli_path busca <exe_dir>/assets/gx-cli{.exe} con
  fallback a <app_dir>/gx-cli para modo dev.

Tests: 32/32 verde. Build Linux + Windows OK. Deploy fresco a
Desktop con todas las 6 apps confirma layout limpio:
  <app>.exe + (duckdb.dll si aplica) + assets/ + local_files/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:50:44 +02:00
egutierrez 375573db38 merge: issue/local-files-and-windows-runtime — convencion local_files/
Adopta la convencion local_files/ del framework para separar
distribuibles (.exe, dlls, enrichers/, runtime/) de estado del
usuario (settings, DBs, proyectos). Con esto + el runtime Python
embebido (Windows) ya copiado al Desktop, la app es completamente
portable a otra maquina Windows sin WSL ni fn_registry montado.
2026-05-03 00:33:13 +02:00
egutierrez 7a055809c2 feat(graph_explorer): adopta convencion local_files/
Sustituye paths hardcodeados (graph_explorer.db, graph_explorer.ini,
projects/) por resolutores que apuntan a <exe_dir>/local_files/.

- project_manager: k_projects_dir y k_settings_file pasan a ser
  helpers projects_root() / settings_path() que llaman a
  fn::local_path internamente. Layout en disco documentado en el
  comentario de cabecera del .h.
- main.cpp: el modo legacy y el fallback de jobs_init usan
  fn::local_path('graph_explorer.db') en lugar de relativo al cwd.

Junto al cambio del framework (commit f102aba9), graph_explorer
se distribuye con su carpeta limpia: solo .exe + duckdb.dll +
TTFs + enrichers/ + runtime/. Todo el estado del usuario vive
en local_files/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:33:08 +02:00
egutierrez 2d8aafea34 merge: issue/0033b-vendor-python-functions — vendoring 0033b
Integra el sistema de vendoring para enrichers Python. Cada
enricher empaqueta las funciones del registry que declara en
uses_functions a `_vendored/` durante el build, lo que cierra la
ultima dependencia hacia el fn_registry montado.

Tests: 32/32 verde (6 nuevos del script vendor).

Estado del 0033 main:
  - Fase A (dispatcher multi-lang): hecha.
  - Fase B (runtime Python embebido): hecha.
  - Fase C (UI badges): pendiente.
Estado de los sub-issues:
  - 0033b vendoring: hecho.
  - 0033c fn check vendored: pendiente.
  - 0033d indexer python_runtime: pendiente.
  - 0033e /compile orquestador: pendiente.
2026-05-03 00:20:50 +02:00
egutierrez ee0d26ce2d feat(enrichers): vendoring de funciones Python por enricher (issue 0033b)
Cada enricher con `lang: python` y `uses_functions` no vacio ahora
puede empaquetar las funciones del registry que necesita en
`<enricher>/_vendored/`. El run.py importa de ahi en lugar de
`<registry_root>/python/functions/`, lo que hace al binario
distribuible sin dependencia de un fn_registry montado.

Cambios:

1. tools/vendor_enricher_python.sh
   - Lee `uses_functions` del manifest (filtrando IDs `*_py_*`).
   - Resuelve `file_path` desde registry.db.
   - Copia recursivamente con expansion transitiva: si un fichero
     vendorizado importa siblings del mismo dominio, los siblings
     tambien se copian (resuelve el caso `extract_iocs.py` que
     importa 7 modulos hermanos).
   - Genera `.vendor.lock` con `<id>  <sha256>  <src_path>` por
     funcion declarada para auditoria.
   - Idempotente — si todos los hashes coinciden, no rehace nada.

2. Manifests actualizados con `uses_functions`:
   - fetch_webpage:        normalize_url + html_to_markdown
   - extract_links:        extract_urls
   - extract_text_entities: extract_iocs

3. run.py de los 3 enrichers afectados: importan de `_vendored/`
   si existe, fallback a `<registry_root>/python/functions/` en
   modo dev (mantiene los tests pytest funcionando).

4. app.md: anade `cryptography` a python_runtime_deps porque el
   blob `cybersecurity.cybersecurity` lo importa al top.

5. Tests:
   - test_vendor_script.py — 6 tests del script: layout correcto,
     transitive siblings, lock con SHA256, idempotencia, modulos
     importables en aislamiento.
   - 16 tests de enrichers existentes pasan via vendoring (no usan
     registry_root porque _vendored/ tiene prioridad).

6. Issue 0033b movido a issues/completed/.

Tests: 32/32 verde (16 enrichers + 6 dispatcher + 4 runtime + 6
vendor).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:20:41 +02:00
egutierrez 4ef6a5f7db chore(issues): mover 7 issues completadas a issues/completed/
Status sincronizado con master:
  - 0001 chat con Claude     -> shipped como panel Echo
  - 0003 enricher web        -> shipped (0028 + 0028b)
  - 0026 sistema de jobs     -> shipped
  - 0027 tipo Webpage        -> shipped
  - 0028 fetch_webpage       -> shipped
  - 0028b extract trio       -> shipped
  - 0031 layout estable      -> shipped

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:14:58 +02:00
egutierrez 9ec832ea9a merge: issue/0033b-python-runtime-embed — runtime Python embebido fase B
Integra fase B del issue 0033: graph_explorer puede distribuirse
con runtime Python autocontenido en <exe_dir>/runtime/python/.
Cero dependencia de WSL en Windows cuando hay embed; el legacy
registry_venv sigue como fallback transparente.

Pendientes del 0033:
  - Fase C: badges [Go]/[Py]/[Sh] en la UI (cosmetica).
  - Sub-issues 0033b-e (vendoring, fn check, indexer, /compile).

Tests: 26/26 verde.
2026-05-02 16:51:10 +02:00
egutierrez 30f6f3758f feat(jobs): runtime Python embebido + cadena de fallback (issue 0033 fase B)
Permite distribuir graph_explorer.exe Windows sin dependencia de WSL
ni del .venv del registry. Tambien funciona en Linux como bundle
autocontenido portable.

Cambios:

1. tools/freeze_python_runtime.sh
   - Linux: copia python-build-standalone (uv) ~87 MB,
     elimina marker EXTERNALLY-MANAGED, instala wheels.
   - Windows: descarga python-3.12.7-embed-amd64.zip oficial
     (~12 MB), habilita site-packages, instala wheels via
     pip install --target --platform win_amd64.
   - Idempotente via runtime/.lock con SHA256 del estado.
   - Lee python_runtime_deps del frontmatter de app.md.

2. jobs.cpp::cached_python_runtime() — resolver con cadena:
     1. <exe_dir>/runtime/python/{python.exe|bin/python3}  (embedded)
     2. $FN_PYTHON                                         (env)
     3. <registry_root>/python/.venv/bin/python3           (registry_venv)
     4. python3 del PATH                                   (system)
   Loggea procedencia al iniciar jobs_init.

3. POSIX run_subprocess: usa el runtime resuelto en lugar del
   path hardcodeado.

4. Windows run_subprocess: ramifica por needs_wsl. Si embedded
   o env, lanza Python Windows nativo via CreateProcessW
   directamente (run_path tambien Windows nativo). Solo el
   legacy registry_venv sigue por wsl.exe.

5. app.md: nuevos campos python_runtime: true y
   python_runtime_deps: [requests, certifi, urllib3].

6. .gitignore extendido con runtime/, projects/, _vendored/,
   .vendor.lock, binarios Go de enrichers.

Tests: 26/26 verde — 16 originales + 6 dispatcher fase A + 4
nuevos del resolver fase B (con/sin embed, FN_PYTHON, idempotencia
del freeze script).

Smoke E2E manual: runtime/python/bin/python3 ejecuta web_search
con cwd /tmp y registry_root pasado en ctx, sin tocar el .venv del
registry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:51:02 +02:00
egutierrez 2238355f40 merge: issue/0033a-multilang-dispatcher — dispatcher multi-lang fase A
Integra la fase A del issue 0033: el sistema de enrichers ahora
acepta `lang: go|python|bash` en el manifest y `jobs.cpp` ramifica
el spawn segun lang. Retrocompatible al 100% — los 5 enrichers
existentes funcionan sin cambios.

Pendientes del issue 0033:
  - Fase B: runtime Python embebido (<app>/runtime/python/).
  - Fase C: badges [Go]/[Py]/[Sh] en la UI.

Tests: 22/22 verde (16 regresion + 6 dispatcher nuevos).
2026-05-02 16:15:35 +02:00
egutierrez 7e0b55d282 docs(issues): marcar 0033 fase A como completada
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:15:28 +02:00
egutierrez fce3f97d53 feat(enrichers): dispatcher multi-lang go|python|bash (issue 0033 fase A)
Extiende el sistema de enrichers para soportar varios lenguajes en el
mismo registro. El manifest gana dos campos opcionales:

  lang: python|go|bash    (default: python — retrocompat con los 5
                            enrichers existentes que no lo declaran)
  exec: run               (basename del script o binario; default "run")

EnricherSpec ahora lleva `lang`, `exec_basename`, `disabled` y
`disabled_reason`. parse_manifest lee los nuevos campos y aplica
defaults; resolve_run_path busca <dir>/<exec>{.py|.sh|.exe|<vacio>}
segun lang + plataforma. Si el ejecutable no existe (binario Go sin
compilar, script ausente), el spec queda en el registro pero
disabled — enrichers_for_type lo oculta del menu y jobs.cpp aborta
con mensaje claro si llega un job para uno disabled.

run_subprocess (POSIX y Windows) ramifica argv segun lang:
  - go    -> execv del binario directamente, sin python ni wsl.exe
  - bash  -> /bin/bash <run_path>  (en Windows: wsl.exe -- bash ...)
  - python -> python3 <run_path>   (default)

El call site en jobs.cpp resuelve run_path y lang via
ge::enricher_by_id() en lugar del hardcode "run.py". Los 5 enrichers
existentes siguen funcionando sin cambios — heredan lang: python por
default.

Tests pytest (22/22 verde):
  - 16 regresion: los 5 enrichers actuales siguen pasando.
  - 6 nuevos en test_dispatcher_lang.py: parser default a python,
    parser lee lang: bash, wire protocol identico para python y
    bash, enricher Go sin binario queda disabled, enricher real
    sigue funcionando tras el cambio.

NO incluye: runtime Python embebido (fase B) ni badges de lang en
la UI (fase C). El issue 0033 sigue abierto hasta cerrar las dos
fases restantes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:15:03 +02:00
egutierrez 35ace544d9 docs(issues): roadmap fase 2 navegador + ports Go + runtime embebido
Anade siete issues que definen el camino para hacer graph_explorer
distribuible como binario Windows autocontenido (sin WSL):

- 0032 — browser_session enrichers via Playwright (login interactivo,
  cookies persistentes, fetch_webpage_browser, web_search_browser).
- 0033 — dispatcher multi-lenguaje (lang: go|python|bash en manifest)
  + runtime Python embebido en <app>/runtime/. 3 fases (A=dispatcher,
  B=runtime, C=UI badges).
- 0033b — vendoring de funciones Python por enricher (_vendored/ +
  .vendor.lock) para que los enrichers no dependan de registry_root
  en runtime.
- 0033c — fn check vendored: drift detection con --fix.
- 0033d — fn index lee python_runtime / python_runtime_deps de app.md.
- 0033e — /compile orquesta freeze + vendor + go builds.
- 0034 — port de los 5 enrichers de sistema a Go. Reusa funciones
  Go del registry directamente (no copias). Tests pytest existentes
  pasan sin cambios.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:10:35 +02:00
egutierrez 5fe856b30e fix(jobs): resolver ops_db_path absoluto y normalizar backslashes
build_stdin_json enviaba ops_db_path tal cual al subprocess Python
(tipicamente "projects/<slug>/operations.db", relativo). Si el cwd
del proceso padre no era el dir del proyecto, sqlite3.connect
creaba un fichero vacio en otra ruta y el primer SELECT fallaba con
"no such table: entities".

Anade lambda absify que normaliza separadores (\\ -> /) antes de
std::filesystem::absolute (en Linux \\ es char literal del nombre,
no separador) y absolutiza ops_db, app_dir y registry_root antes
del to_wsl_path. Cubre los 5 enrichers de una sola vez.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:10:21 +02:00
egutierrez 6919ebfe9c feat(enrichers): web_search DuckDuckGo + tests pytest de los 5 enrichers
Anade enricher web_search aplicable a nodos text/Concept/Topic. Hace
POST a html.duckduckgo.com con la query del nodo, parsea resultados
con HTMLParser stdlib, decodifica el redirect uddg= y crea N nodos
Url con relacion SEARCH_RESULT_OF apuntando al nodo origen.

Encadenable: tras web_search, fetch_webpage sobre cada Url completa
el pipeline search -> fetch -> extract.

Defensa contra ops_db_path mal resuelto: normaliza backslashes,
resuelve relativo contra app_dir, valida que la tabla entities
exista antes de tocar nada (exit codes 7/8/9 con JSON resumen).

Tests pytest (16/16 verde): conftest con operations.db temp +
schema minimo, stub de requests via PYTHONPATH para mockear red.
Cubre los 5 enrichers (extract_domain, fetch_webpage, extract_links,
extract_text_entities, web_search) + sanity check de manifests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:10:13 +02:00
egutierrez 0d2450bac5 feat(chat): panel Echo + gx-cli MCP server con tools tipadas
Anade panel "Echo" — copiloto OSINT que invoca claude -p con un MCP
server propio (gx-cli) exponiendo el grafo como tools tipadas:
info, node_*, rel_*, table_*, enricher_*, query.

Cambios:
- chat.cpp/h: panel UI dockeable con history, raw stream-json toggle,
  spawn de claude -p con system prompt OSINT, ChatMessage con USER/
  ASSISTANT/TOOL_USE/TOOL_RESULT/SYSTEM/ERROR_MSG, escritura de
  mcp.json con paths Linux para WSL en Windows.
- gx-cli: binario MCP standalone que valida cada tool, abre
  operations.db en RW, escribe agent_mutations counter para que el
  viewport detecte cambios en vivo.
- CMakeLists.txt: anade chat.cpp al target.
- views.h: panel_chat boolean en AppState.
- main.cpp: integracion del panel Chat (rename a Echo en menubar +
  init), refresh de contexto al cambiar operations.db, drain de cola
  agent_jobs tras enricher_run.

Mensajes del panel renderizan con fn_ui::selectable_text_wrapped_force
(wrap forzado + seleccion) para que URLs/JSON largos no se clippeen
y permitan copy/paste.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:10:01 +02:00
egutierrez 4281f3ccb2 fix(jobs): autodetectar distro WSL + normalizar separadores UNC (issue 0026)
El usuario reportaba "no enrichers for url" en Windows. Tres bugs:

1. resolve_registry_root tenia el fallback hardcoded a "Ubuntu" pero la
   distro real era "Ubuntu-22.04". Reemplazado por detect_wsl_distro()
   que sondea las distros comunes (Ubuntu, Ubuntu-24.04, Ubuntu-22.04,
   Ubuntu-20.04, Debian, kali-linux, Fedora, openSUSE-Tumbleweed) y se
   queda con la primera cuyo UNC tenga registry.db.
2. enrichers_load construia paths con mixed separators
   ("\\\\wsl.localhost\\Ubuntu-22.04\\...\\enrichers/foo/manifest.yaml")
   que confunden a opendir de MinGW. Ahora normaliza todo a backslashes
   en Windows antes de opendir + concatena con el separador nativo.
3. El menu "Run enricher" decia simplemente "(no enrichers para tipo X)"
   sin distinguir si era 0/N (no se carga ninguno) o N>0/M (existen pero
   ninguno aplica). Ahora muestra "(no enrichers cargados — revisa
   FN_REGISTRY_ROOT)" vs "(0/4 enrichers para tipo 'url')".

Si el usuario tiene una distro con nombre raro, sigue pudiendo setear
FN_REGISTRY_ROOT explicitamente.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 19:27:28 +02:00
egutierrez 598e6fcdd4 merge: issue/0026-jobs-windows — implementacion Win32 con wsl.exe
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 18:49:39 +02:00
egutierrez c3ce9956f7 feat(jobs): implementacion Win32 — wsl.exe + path translation (issue 0026)
Sustituye el stub Windows por la implementacion real:

C++:
- Bloque #ifdef _WIN32 con CreateProcessW + 3 anonymous pipes
  (CreatePipe + SetHandleInformation), STARTF_USESTDHANDLES,
  CREATE_NO_WINDOW, ReadFile/WriteFile, WaitForSingleObject con polling
  para soportar cancelacion via TerminateProcess.
- Helper to_wsl_path: convierte paths Windows a WSL antes de mandarlos
  al subprocess. Soporta:
    * "C:\\..."                   -> "/mnt/c/..."
    * "\\\\wsl.localhost\\<distro>\\..." -> "/..."
    * "\\\\wsl$\\<distro>\\..."         -> "/..."
    * "/..."                      -> tal cual
  En POSIX la funcion es no-op.
- build_stdin_json siempre normaliza ops_db_path/app_dir/cache_dir/
  registry_root a paths WSL — el run.py corre dentro de WSL y solo
  entiende paths /home, /mnt, etc.
- Subprocess invocacion: `wsl.exe --cd <root_wsl> -- <python_wsl> <run_wsl>`.
  Asume que el usuario tiene WSL instalado y la distro Ubuntu (o ajusta
  FN_REGISTRY_ROOT al UNC adecuado).
- kill_proc unificado: TerminateProcess en Win32, kill(SIGTERM) en POSIX.
- JobControl con HANDLE+pid en Win32, pid_t en POSIX.

main.cpp:
- resolve_registry_root con fallback Windows: si FN_REGISTRY_ROOT env
  no esta y getcwd no encuentra registry.db (caso del .exe en Desktop),
  usa "\\\\wsl.localhost\\Ubuntu\\home\\lucas\\fn_registry". El usuario
  cambia el UNC via env var si su distro tiene otro nombre.

Build:
- cpp/build/windows/apps/graph_explorer/graph_explorer.exe linkea limpio
  contra MinGW; solo dependencias windows.h estandar (kernel32, etc.).
- Linux smoke test sigue detectando los 4 enrichers tras la
  refactorizacion compartida.

Notas operativas para el usuario Windows:
- Ejecutar el .exe desde C:\\Users\\lucas\\Desktop\\apps\\graph_explorer\\
  (doble clic). El primer arranque tarda ~1 s mas por cold-start de wsl.exe.
- Si la distro no es Ubuntu, setear FN_REGISTRY_ROOT con el UNC correcto
  (ej. "\\\\wsl.localhost\\Debian\\home\\lucas\\fn_registry").
- Cancelar un job en Windows usa TerminateProcess (mas brutal que SIGTERM
  pero los run.py no tienen estado critico — sqlite3 rollback automatico
  por la transaccion implicita).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 18:49:36 +02:00
egutierrez a7c227354b fix(jobs): stub Windows para que la build cross-compile (issue 0026)
El sistema de jobs usa fork+exec+pipes POSIX que no existen en MinGW.
Anade un stub _WIN32 que devuelve false en jobs_init y no-op en el resto,
de forma que la app compila para Windows pero los enrichers quedan
desactivados ahi. La build Linux/WSL conserva la implementacion completa.

TODO futuro: implementacion Windows con CreateProcess + anonymous pipes
+ TerminateProcess. No urgente — el desarrollo principal es WSL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 18:42:46 +02:00
egutierrez b329358efa merge: issue/0031-stable-layout-on-reload — layout estable + halo placement
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 18:40:03 +02:00