From 9042110ea272ac0672a89de2d4778e79b7bc2530 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Fri, 1 May 2026 18:24:13 +0200 Subject: [PATCH] docs(issues): plan enrichers asincronos + recoleccion web (0026-0030) Cinco issues que componen el plan: - 0026: sistema de jobs (infra, contrato wire) - 0027: tipo Webpage + cache de documentos - 0028: enricher fetch_webpage (MVP end-to-end) - 0028b: enrichers extract_domain / extract_links / extract_text_entities - 0029: variantes CDP (Chrome headless, screenshot) - 0030: macro "Deep enrich" + expand_domain Tambien anade los issues previos 0012-0025 que estaban untracked. Co-Authored-By: Claude Opus 4.7 (1M context) --- issues/0012-http-ingest-endpoint.md | 54 ++++++ issues/0013-paste-extract-panel.md | 45 +++++ issues/0014-browser-extension.md | 49 +++++ issues/0015-drag-drop-ingest.md | 47 +++++ issues/0016-clipboard-watcher.md | 44 +++++ issues/0017-gx-cli.md | 38 ++++ issues/0018-headless-browser-transforms.md | 54 ++++++ issues/0019-ocr-ingest.md | 39 ++++ issues/0020-email-bot-ingest.md | 46 +++++ issues/0021-command-palette.md | 42 +++++ issues/0022-nl-graph-query.md | 41 ++++ issues/0023-saved-views.md | 39 ++++ issues/0024-subgraph-export.md | 45 +++++ issues/0025-vault-sync.md | 44 +++++ issues/0026-jobs-system.md | 209 +++++++++++++++++++++ issues/0027-webpage-type-cache.md | 82 ++++++++ issues/0028-enricher-fetch-webpage.md | 78 ++++++++ issues/0028b-enrichers-extract-trio.md | 72 +++++++ issues/0029-enrichers-cdp.md | 63 +++++++ issues/0030-deep-enrich-macro.md | 62 ++++++ 20 files changed, 1193 insertions(+) create mode 100644 issues/0012-http-ingest-endpoint.md create mode 100644 issues/0013-paste-extract-panel.md create mode 100644 issues/0014-browser-extension.md create mode 100644 issues/0015-drag-drop-ingest.md create mode 100644 issues/0016-clipboard-watcher.md create mode 100644 issues/0017-gx-cli.md create mode 100644 issues/0018-headless-browser-transforms.md create mode 100644 issues/0019-ocr-ingest.md create mode 100644 issues/0020-email-bot-ingest.md create mode 100644 issues/0021-command-palette.md create mode 100644 issues/0022-nl-graph-query.md create mode 100644 issues/0023-saved-views.md create mode 100644 issues/0024-subgraph-export.md create mode 100644 issues/0025-vault-sync.md create mode 100644 issues/0026-jobs-system.md create mode 100644 issues/0027-webpage-type-cache.md create mode 100644 issues/0028-enricher-fetch-webpage.md create mode 100644 issues/0028b-enrichers-extract-trio.md create mode 100644 issues/0029-enrichers-cdp.md create mode 100644 issues/0030-deep-enrich-macro.md diff --git a/issues/0012-http-ingest-endpoint.md b/issues/0012-http-ingest-endpoint.md new file mode 100644 index 0000000..4ca0ddf --- /dev/null +++ b/issues/0012-http-ingest-endpoint.md @@ -0,0 +1,54 @@ +--- +id: 0012 +title: Endpoint HTTP local de ingesta y consulta +status: pending +priority: medium +created: 2026-05-01 +--- + +## Objetivo + +Exponer un servidor HTTP local en `graph_explorer` (o como `ingest_server` +hermano) que sea el punto de entrada unico para todo flujo externo: +extension de navegador, CLI `gx`, watcher de portapapeles, bots, OCR, etc. + +Sin este endpoint cada cliente externo tendria que abrir `operations.db` +directamente — colisiona con el lock del proceso vivo y duplica logica de +extraccion. + +## Endpoints minimos + +- `POST /entity` — crea entidad. Body: `{type, name, metadata}`. +- `POST /relation` — crea relacion. Body: `{from_id, to_id, kind, metadata}`. +- `POST /ingest/text` — texto libre -> `extract_graph_hybrid` -> preview o auto-commit. +- `POST /ingest/url` — URL -> fetch + extract -> preview o auto-commit. +- `POST /ingest/file` — multipart upload (PDF/CSV/JSON/.eml/imagen) -> router por mime -> extract. +- `GET /search?q=` — fuzzy / FTS sobre entidades. +- `GET /entity/:id`, `GET /entity/:id/neighbors`. + +## Decisiones + +- Bind a `127.0.0.1` por defecto, puerto fijo (ej. 7878) o aleatorio + escrito a `~/.fn_graph_port`. +- Auth: token compartido en `~/.fn_graph_token` (header `X-Token`). + Generado al primer arranque. +- Modo "preview": las rutas de ingesta aceptan `?commit=false` y + devuelven entities/relations propuestas para que el cliente las muestre + antes de persistir. Cuando es `true`, escribe directo. +- Implementacion: httplib o Mongoose embebido en C++ (sin nuevas deps + pesadas). Alternativa: lanzar el servidor en Go/Python aparte si la + integracion C++ se complica. + +## Bloquea + +Issues 0014, 0017, 0018, 0019, 0020 dependen de este. + +## Definicion de hecho + +- `curl -H "X-Token: ..." -d '{"type":"person","name":"X"}' localhost:7878/entity` + crea entidad. +- `POST /ingest/text` con texto en castellano devuelve entities/relations + detectadas por el pipeline hibrido. +- El endpoint corre en background mientras la UI sigue interactiva. +- Si `graph_explorer` no esta abierto, un binario `ingest_server` + standalone ofrece el mismo API contra la misma `operations.db`. diff --git a/issues/0013-paste-extract-panel.md b/issues/0013-paste-extract-panel.md new file mode 100644 index 0000000..44bf622 --- /dev/null +++ b/issues/0013-paste-extract-panel.md @@ -0,0 +1,45 @@ +--- +id: 0013 +title: Panel "Paste & Extract" — texto libre a entidades con extract_graph_hybrid +status: pending +priority: high +created: 2026-05-01 +--- + +## Objetivo + +Panel dockeable dentro de `graph_explorer` con un textarea grande. Pegas +texto (articulo, mensaje, transcripcion, documento), pulsas Extract, corre +el pipeline `extract_graph_hybrid` (regex + GLiNER + GLiREL + LLM fallback) +y muestra preview de entidades y relaciones detectadas. El usuario marca +cuales aceptar antes de commit a `operations.db`. + +Es el quick-win de mas alto valor: aprovecha el pipeline ya mergeado +(commit 1a353878) y elimina la friccion de tipear datos a mano. + +## Alcance + +- Panel "Extract" con textarea, combo de proyecto/tipos esperados, boton + "Extract". +- Lanza el pipeline en hilo aparte (es Python — invocar via subprocess + o el endpoint HTTP de 0012 con `commit=false`). +- Tabla de entidades propuestas: checkbox, type, name, source span. Tabla + de relaciones propuestas: from, kind, to, checkbox. +- Edicion inline de tipo/nombre antes de commit. +- "Apply selected" -> escribe a operations.db, refresca grafo, posiciona + los nuevos nodos cerca del centro o vinculados al ultimo seleccionado. +- Dedupe: si una entidad propuesta ya existe (mismo type+name) reusar el + id en lugar de duplicar. + +## Decisiones + +- Invocacion del pipeline: via 0012 si esta disponible, o subprocess + directo como fallback (para que el panel funcione sin levantar HTTP). +- Resaltado de spans en el textarea (v2 — primera version solo lista). + +## Definicion de hecho + +- Pego un parrafo en castellano sobre una empresa y un directivo, pulso + Extract, veo entidades correctas tipadas y la relacion entre ambas. +- Apply crea los nodos en el grafo en menos de 1 s tras click. +- Re-extraer el mismo texto no duplica entidades (dedupe funciona). diff --git a/issues/0014-browser-extension.md b/issues/0014-browser-extension.md new file mode 100644 index 0000000..d085473 --- /dev/null +++ b/issues/0014-browser-extension.md @@ -0,0 +1,49 @@ +--- +id: 0014 +title: Extension de navegador "Add to graph" +status: pending +priority: high +created: 2026-05-01 +depends_on: [0012] +--- + +## Objetivo + +Extension Firefox/Chrome que añade items al grafo desde el navegador con +un click. Cubre el flujo Maltego "estoy leyendo algo en web -> nodo en +mi grafo" sin abandonar el navegador. + +## Casos + +- Click derecho sobre seleccion de texto -> "Add to graph" (manda texto + via `/ingest/text`). +- Click derecho sobre link -> "Add link" (crea entidad URL + metadata + del href, opcionalmente trigger fetch). +- Boton de toolbar -> "Add this page" (URL + titulo + meta description + + texto principal extraido con Readability). +- Modo "select & relate": dos selecciones consecutivas -> crea relacion + entre las entidades resultantes. + +## Alcance + +- WebExtension API (compatible Firefox/Chrome, Manifest v3). +- Settings: URL del endpoint (default `http://localhost:7878`), token, + proyecto destino. +- Preview popup tras extraccion: muestra entities propuestas, el usuario + acepta o edita antes de commit (reusa `?commit=false` de 0012). +- Atajo configurable (ej. `Ctrl+Shift+G`) para "add page". + +## Decisiones + +- Sin auth OAuth — token local compartido es suficiente para localhost. +- Empaquetar en `apps/graph_explorer/extension/` o como sub-repo propio + bajo `dataforge/graph_explorer_extension`. +- Si `graph_explorer` no esta corriendo: la extension muestra error + claro y guarda la accion en cola para reintentar. + +## Definicion de hecho + +- Selecciono un parrafo en una pagina, click derecho -> Add, en menos de + 2 s veo los nodos en `graph_explorer`. +- Funciona en Firefox y Chrome con la misma build. +- Reintento automatico de la cola cuando vuelve a haber endpoint vivo. diff --git a/issues/0015-drag-drop-ingest.md b/issues/0015-drag-drop-ingest.md new file mode 100644 index 0000000..ca21be3 --- /dev/null +++ b/issues/0015-drag-drop-ingest.md @@ -0,0 +1,47 @@ +--- +id: 0015 +title: Drag & drop de archivos sobre el viewport para ingesta +status: pending +priority: medium +created: 2026-05-01 +--- + +## Objetivo + +Soltar archivos sobre la ventana de `graph_explorer` lanza el extractor +adecuado segun extension y mete las entidades en el grafo, sin abrir +modales ni navegar menus. + +## Tipos soportados + +- `.pdf` -> texto + `extract_graph_hybrid`. +- `.eml` / `.msg` -> headers (from/to/cc) como entidades persona/email + + cuerpo via extract. +- `.csv` / `.parquet` -> ingesta como tabla DuckDB (encadena con 0011). +- `.json` / `.jsonl` -> si tiene shape entity/relation, importar; si no, + extract sobre stringify. +- `.png` / `.jpg` -> OCR (issue 0019) y luego extract. +- `.txt` / `.md` -> extract directo. + +## Alcance + +- Hook de drop de ImGui -> dispatcher por mime/extension -> pipeline + correspondiente -> preview con seleccion antes de commit (igual UX que + 0013). +- Indicador visual de zona drop activa cuando hay drag sobre la ventana. +- Multiples archivos en un drop: procesar en cola, mostrar progreso. + +## Decisiones + +- Dispatcher reutiliza `/ingest/file` del endpoint 0012 si esta vivo, o + resuelve localmente como fallback. +- Limite de tamaño por archivo configurable (default 50 MB) para evitar + bloqueos en PDFs gigantes. + +## Definicion de hecho + +- Suelto un PDF en castellano sobre el canvas, en menos de 30 s veo + preview con entidades correctas. +- Suelto un .eml y aparecen `from`/`to` como nodos persona conectados + por una relacion `mailed`. +- Cancelar durante el preview no toca operations.db. diff --git a/issues/0016-clipboard-watcher.md b/issues/0016-clipboard-watcher.md new file mode 100644 index 0000000..3db6c5b --- /dev/null +++ b/issues/0016-clipboard-watcher.md @@ -0,0 +1,44 @@ +--- +id: 0016 +title: Watcher de portapapeles con deteccion de patrones +status: pending +priority: low +created: 2026-05-01 +--- + +## Objetivo + +Servicio (toggle desde la toolbar) que escucha el portapapeles y, cuando +detecta patrones de interes, ofrece añadir como entidad sin abandonar el +flujo en otra app. Pensado para sesiones de OSINT manual donde el coste +de "abrir la app y tipear" rompe el ritmo. + +## Patrones detectados + +- URL -> entidad URL (con fetch + extract opcional). +- Email, telefono, IBAN, DNI/NIE/CIF, BIC -> entidad tipada con regex. +- Coordenadas (lat,lon), hash (sha1/sha256), wallet crypto (BTC/ETH). + +## Alcance + +- Polling del clipboard (ImGui `GetClipboardText` + diff) o API nativa + (X11 selection / Win32 clipboard listener). +- Toast / notificacion no intrusiva con boton "Add". El usuario decide + por defecto. +- Modo "auto-add" para tipos seguros (IBAN/DNI raras veces son ruido). +- Lista de patrones configurable en `graph_explorer.db`. + +## Decisiones + +- Por defecto OFF — opt-in desde settings, para evitar leer todo lo que + el usuario copia. +- Anonimizar logs: nunca persistir el contenido del clipboard si el + usuario no lo añade. +- Deduplicar: copiar la misma cadena dos veces seguidas no notifica. + +## Definicion de hecho + +- Activo el watcher, copio un IBAN, recibo notificacion, click en Add y + el nodo aparece en el grafo. +- Apagar el watcher detiene la escucha en menos de 1 s. +- Patrones configurados como lista de regex editable desde settings. diff --git a/issues/0017-gx-cli.md b/issues/0017-gx-cli.md new file mode 100644 index 0000000..2dffa46 --- /dev/null +++ b/issues/0017-gx-cli.md @@ -0,0 +1,38 @@ +--- +id: 0017 +title: CLI `gx` para hablar con el endpoint local +status: pending +priority: medium +created: 2026-05-01 +depends_on: [0012] +--- + +## Objetivo + +Cliente CLI fino, instalable en `~/.local/bin/gx`, que habla con el +endpoint HTTP local de `graph_explorer` (issue 0012). Permite ingesta y +consulta desde terminal o scripts sin abrir la app. + +## Comandos + +- `gx add [--metadata k=v ...]` — crea entidad. +- `gx rel ` — crea relacion. +- `gx ingest ` — manda archivo al endpoint, abre preview en TUI. +- `gx from-url ` — fetch + extract. +- `gx search "query"` — devuelve hits del grafo activo (json o tabla). +- `gx neighbors [--depth N]`. +- `gx open ` — abre el grafo y enfoca el nodo en `graph_explorer`. + +## Decisiones + +- Implementar como sub-comando del `fn` CLI existente (`fn gx ...`) o + binario aparte? Probablemente sub-comando para reusar config y auth. +- Output JSON por defecto si stdout no es TTY (componible con jq). +- Tabla legible si stdout es TTY. + +## Definicion de hecho + +- `gx add person "Juan Perez"` añade el nodo en el grafo en vivo. +- `gx ingest articulo.pdf` lanza preview interactivo en terminal y commit. +- `gx neighbors --depth 2 --format json | jq` funciona en pipeline. +- Errores de conexion al endpoint se reportan claros (no stack traces). diff --git a/issues/0018-headless-browser-transforms.md b/issues/0018-headless-browser-transforms.md new file mode 100644 index 0000000..4fbff7a --- /dev/null +++ b/issues/0018-headless-browser-transforms.md @@ -0,0 +1,54 @@ +--- +id: 0018 +title: Transforms automatizadas tipo Maltego con browser headless +status: pending +priority: low +created: 2026-05-01 +depends_on: [0012] +--- + +## Objetivo + +Dada una entidad seleccionada, ejecutar un script (Playwright/Puppeteer) +que enriquece el grafo con datos derivados — equivalente a las +"transforms" de Maltego. Es la pieza que diferencia frente a alternativas +mas estaticas. + +## Ejemplos para banking/OSINT espanol + +- Persona/empresa -> consulta BORME, registro mercantil, axesor. +- Dominio -> whois, DNS records, certificados (crt.sh). +- Email -> haveibeenpwned, hunter.io. +- Telefono -> truecaller-like. +- Empresa -> LinkedIn search publico, opencorporates. + +## Alcance + +- Cada transform es un pipeline del registry con tag `transform` y un + contrato fijo: input = `{id, type, metadata}`, output = `{entities, + relations}`. +- Registro de transforms aplicables por entity_type. +- UI: context menu sobre nodo -> "Run transform..." -> lista filtrada + por type aplicable -> ejecuta async -> notifica al terminar -> preview + antes de commit. +- Sandbox: cada transform en proceso aparte, timeout configurable. + +## Riesgos y mitigaciones + +- Los scrapers se rompen cuando los sitios cambian -> mantener una suite + de "transform health checks" automaticos (cron ligero) que avisa de + fallos antes de que el usuario los descubra en vivo. +- Cumplimiento legal y robots.txt -> documentar en cada transform su + fuente, politica y ToS. +- Rate limiting -> cooldown por host configurable. + +## Definicion de hecho + +- Selecciono un dominio en el grafo, lanzo "whois", aparecen registrant, + registrar y nameservers como nodos vinculados con relaciones tipadas. +- Un transform que falla loguea el error y no afecta a otros que + esten corriendo en paralelo. +- La lista de transforms aplicables a una entidad se computa segun su + type (no se ofrecen los inaplicables). +- Health check cron escribe a `proposals` cuando un transform empieza a + fallar repetidamente. diff --git a/issues/0019-ocr-ingest.md b/issues/0019-ocr-ingest.md new file mode 100644 index 0000000..b3ad76e --- /dev/null +++ b/issues/0019-ocr-ingest.md @@ -0,0 +1,39 @@ +--- +id: 0019 +title: OCR de region de pantalla y archivos imagen +status: pending +priority: low +created: 2026-05-01 +depends_on: [0012] +--- + +## Objetivo + +Capturar una region de pantalla (atajo global) o soltar imagen sobre la +app (issue 0015) -> Tesseract / PaddleOCR -> texto -> `extract_graph_hybrid`. + +Util cuando la fuente solo esta como captura, PDF escaneado, o pantalla +de un sistema sin copy/paste. + +## Alcance + +- Captura: usar herramienta del SO (gnome-screenshot, flameshot, snipping + tool) con flag de region. Linux primero, Windows con Snip & Sketch. +- OCR: Tesseract con datos de espanol (`spa.traineddata`). PaddleOCR + como alternativa para texto manuscrito o calidades bajas. +- Pipeline: imagen -> OCR -> texto -> panel preview de 0013. + +## Decisiones + +- Atajo global configurable (default `Ctrl+Alt+G`). +- Idiomas OCR como lista en settings (default `[spa, eng]`). +- Persistir la imagen original como `metadata.source_image_path` en la + entidad creada para trazabilidad. + +## Definicion de hecho + +- Atajo abre selector de region, capturo un parrafo en pantalla, en + menos de 5 s veo entidades extraidas. +- Suelto un PNG con texto sobre el canvas, mismo flujo (encadena con 0015). +- Calidad de OCR para espanol > 90% en capturas estandar 1080p de texto + impreso. diff --git a/issues/0020-email-bot-ingest.md b/issues/0020-email-bot-ingest.md new file mode 100644 index 0000000..ffb6e73 --- /dev/null +++ b/issues/0020-email-bot-ingest.md @@ -0,0 +1,46 @@ +--- +id: 0020 +title: Ingesta via email forwarding y bot Telegram/Signal +status: pending +priority: low +created: 2026-05-01 +depends_on: [0012] +--- + +## Objetivo + +Ingerir entidades sin estar delante del PC. Util para capturar cosas +sobre la marcha (movil, lectura en otra pantalla, conversaciones). + +## Canales + +- **Email**: direccion dedicada (mailbox o alias) que se chequea via + IMAP cada N minutos. Adjuntos -> ingesta como en 0015. Cuerpo -> + extract. +- **Bot Telegram/Signal**: forwardear un mensaje, una imagen, o escribir + comandos (`/add empresa Acme`, `/relate Acme owns Bravo`). El bot + habla con el endpoint 0012. + +## Alcance + +- Cliente IMAP minimo o uso de un MTA local (postfix+dovecot) que + redirija a un script. +- Bot Telegram: BotFather + python-telegram-bot o equivalente Go (vive + en `apps//` con tag `service`). +- Auth: solo procesar mensajes de chat IDs / direcciones whitelisted en + config. +- Confirmacion: el bot responde con preview ("¿añado estas 3 entidades? + responde 'si' o 'no'") antes de commit. + +## Decisiones + +- Bot self-hosted (no SaaS) — corre como service en VPS o en el PC. +- Multiples grafos: el bot puede targetear distintos `operations.db` + segun el chat de origen (mapping en config). + +## Definicion de hecho + +- Reenvio un PDF a la direccion dedicada y, en menos de 2 minutos, veo + las entidades en el grafo con notificacion del bot. +- El bot rechaza mensajes de chat IDs no autorizados sin responder. +- Comando `/search Acme` desde el bot devuelve hits del grafo. diff --git a/issues/0021-command-palette.md b/issues/0021-command-palette.md new file mode 100644 index 0000000..534d5b2 --- /dev/null +++ b/issues/0021-command-palette.md @@ -0,0 +1,42 @@ +--- +id: 0021 +title: Command palette Ctrl+K — busqueda y acciones globales +status: pending +priority: high +created: 2026-05-01 +--- + +## Objetivo + +Atajo `Ctrl+K` (configurable) abre overlay flotante con input de busqueda +fuzzy global. Lo que mas acelera el dia a dia: cero navegacion por menus +para encontrar un nodo o disparar una accion. + +## Alcance + +Indexa y matchea sobre: + +- Entidades del grafo (por name, type, metadata). +- Acciones de la app ("Toggle inspector", "Save layout", "Run transform", + "Export subgraph", "Switch project", "Open settings"). +- Comandos recientes (MRU al tope sin escribir). + +Selecciono con flechas + Enter -> ejecuta accion o enfoca nodo en +el viewport. + +## Implementacion + +- Overlay modal centrado, input de texto + lista virtualizada + (`ImGuiListClipper`). +- Indexador en memoria sobre entidades; refresh al cambiar grafo. +- Fuzzy matcher (fzf-like, p.ej. `fts_fuzzy_match` de Forrest the woods, + o algo equivalente). +- Acciones registrables desde cualquier panel — registro central tipo + `cmd_palette_register("name", lambda)`. + +## Definicion de hecho + +- Ctrl+K, escribo 3 letras del nombre de un nodo, lo enfoca en el grafo. +- Ctrl+K, "exp", veo accion "Export subgraph as Markdown" disponible. +- Latencia de matching imperceptible con 50k entidades. +- MRU pone arriba lo usado recientemente. diff --git a/issues/0022-nl-graph-query.md b/issues/0022-nl-graph-query.md new file mode 100644 index 0000000..f1d0a86 --- /dev/null +++ b/issues/0022-nl-graph-query.md @@ -0,0 +1,41 @@ +--- +id: 0022 +title: Consulta del grafo en lenguaje natural via LLM +status: pending +priority: medium +created: 2026-05-01 +depends_on: [0001] +--- + +## Objetivo + +Input de texto ("personas relacionadas con BancoX que aparecen en mas de +3 documentos") -> LLM traduce a SQL sobre `operations.db` o a un set de +filtros sobre el grafo en memoria -> resalta el subgrafo resultante. + +Complementa el chat de 0001 con un modo "consulta puntual" sin +conversacion: input -> resultado destacado, sin chat history. + +## Alcance + +- Tool-use ya disponible en 0001 (`query_entities`, `query_relations`). +- Modo "highlight": en lugar de devolver texto, el LLM emite un set de + ids -> la UI dibuja el subgrafo con resaltado y oscurece el resto. +- Boton "save as filter" -> persiste como vista nombrada (issue 0023). +- Historial de queries recientes en un dropdown. +- Indicador de query en curso (puede tardar varios segundos). + +## Decisiones + +- ¿Mismo cliente HTTP/Anthropic que 0001 o duplicado? Reusar. +- Modelo por defecto el mismo que 0001 (`claude-sonnet-4-6`). +- Query schema (que campos ve el LLM) dado por `types_registry` para + que aprenda los nombres de campos del proyecto. + +## Definicion de hecho + +- "personas con mas de 5 conexiones" devuelve subgrafo correcto. +- "documentos publicados en 2025" funciona si la metadata tiene fechas. +- Fallo silencioso (LLM mal interpreta) -> mensaje claro y opcion de + reintentar refinando. +- Query guardada como filtro reutilizable. diff --git a/issues/0023-saved-views.md b/issues/0023-saved-views.md new file mode 100644 index 0000000..80e29d7 --- /dev/null +++ b/issues/0023-saved-views.md @@ -0,0 +1,39 @@ +--- +id: 0023 +title: Vistas guardadas y filtros nombrados +status: pending +priority: medium +created: 2026-05-01 +--- + +## Objetivo + +Guardar combinaciones de filtros (tipo, tag, FTS, layout, zoom, nodos +fijados) bajo un nombre y reaplicarlas con un click o atajo. + +Util para volver siempre al "mapa de la red de empresa X" o "vista de +emails sospechosos" sin reconfigurar todo cada vez. + +## Alcance + +- Tabla `saved_views(graph_hash, name, payload_json, hotkey, created_at)` + en `graph_explorer.db`. +- Panel/menu "Views" con lista, atajos asignables (Ctrl+1..9). +- Payload incluye: filtros activos, expanded nodes, viewport rect, layout + mode, theme overrides, nodos pinned. +- Boton "Save current as view..." en toolbar. +- Boton "Update view" cuando una view esta activa y el usuario cambia algo. + +## Decisiones + +- Las views son por `graph_hash` (no globales) — cada `operations.db` + tiene su set propio. +- Compartir view entre PCs: export/import JSON manual (v2 podria sync via + `fn sync`). + +## Definicion de hecho + +- Configuro filtros, "Save view as 'Banca'", la veo en el menu. +- Reload de la app -> "Banca" aplica todo lo guardado. +- Un atajo (Ctrl+1..9) salta a la vista correspondiente al instante. +- "Update view" persiste cambios sin crear duplicados. diff --git a/issues/0024-subgraph-export.md b/issues/0024-subgraph-export.md new file mode 100644 index 0000000..5dda857 --- /dev/null +++ b/issues/0024-subgraph-export.md @@ -0,0 +1,45 @@ +--- +id: 0024 +title: Exportar subgrafo seleccionado a Markdown / Mermaid / CSV / PNG +status: pending +priority: medium +created: 2026-05-01 +--- + +## Objetivo + +Seleccion de nodos (rect drag o filtro activo) -> menu "Export as..." con +varios formatos de salida segun el destino. + +## Formatos + +- **Markdown**: una pagina por entidad con sus campos y links a vecinos. + Encaja con 0025 (sync con vault). +- **Mermaid `graph TD`**: para pegar en notas o issues. +- **CSV**: dos archivos `nodes.csv` + `edges.csv` para Gephi/Cytoscape. +- **PNG / SVG**: render del subgrafo con layout actual. +- **JSON**: shape `{nodes:[], edges:[]}` para reimportar o procesar. + +## Alcance + +- Menu "Export selected" en context menu del canvas y en menu superior. +- Cada exportador es una funcion del registry reutilizable + (`export_subgraph_md_cpp_viz`, `export_subgraph_mermaid_cpp_viz`, etc). +- Para PNG/SVG: reusar el render actual a un framebuffer offscreen, con + factor de escalado configurable (1x / 2x / 4x). +- Diccionario de plantillas configurable para Markdown (por entity_type). + +## Decisiones + +- Mermaid copiado al portapapeles automaticamente; otros formatos + abren dialogo de guardado. +- Limite suave a 500 nodos para Mermaid (ilegible mas alla). + +## Definicion de hecho + +- Selecciono 20 nodos, exporto Markdown -> directorio con 20 .md y + enlaces cruzados validos. +- Exporto Mermaid -> string copiado al portapapeles, valido en + mermaid.live. +- Exporto PNG con layout fijo, calidad 2x, fidelidad pixel a la vista. +- CSV importable directo en Gephi sin transformaciones. diff --git a/issues/0025-vault-sync.md b/issues/0025-vault-sync.md new file mode 100644 index 0000000..de36450 --- /dev/null +++ b/issues/0025-vault-sync.md @@ -0,0 +1,44 @@ +--- +id: 0025 +title: Sync bidireccional con vault Obsidian / markdown +status: pending +priority: low +created: 2026-05-01 +depends_on: [0024] +--- + +## Objetivo + +Espejar el grafo activo a un vault de markdown (estilo Obsidian) en +`projects/osint_graph/vaults//`. Cada entidad = una nota; cada +relacion = un wikilink. El usuario puede navegar el grafo desde Obsidian +y editar campos alli; los cambios vuelven al grafo. + +Encaja con `vaults/` ya conceptualizados en el registry. + +## Alcance + +- Watcher de filesystem + serializer/parser de notas con frontmatter + YAML para los campos del entity_type. +- Plantilla por entity_type configurable (apoyandose en el exporter + Markdown de 0024). +- Resolucion de conflictos: timestamp + merge campo a campo; preferencia + configurable (vault wins / db wins / prompt). +- Modo unidireccional inicial (graph -> vault) si la ida y vuelta es + mucho trabajo. v2 anade sync de vuelta. + +## Decisiones + +- Sync continuo o on-demand (boton "Sync now")? Empezar on-demand. El + watcher se anade en una segunda fase. +- Detectar cambios externos via `mtime` + checksum. +- Wikilinks usan ids del registry, no nombres (estables ante renames). + +## Definicion de hecho + +- Boton "Sync to vault" genera N notas con frontmatter correcto y + wikilinks navegables en Obsidian. +- Editar un campo en la nota y "Sync from vault" actualiza la entidad + en operations.db. +- No se pierden datos cuando hay edicion concurrente en ambos lados + (resolucion de conflicto explicita). diff --git a/issues/0026-jobs-system.md b/issues/0026-jobs-system.md new file mode 100644 index 0000000..3e45aa9 --- /dev/null +++ b/issues/0026-jobs-system.md @@ -0,0 +1,209 @@ +--- +id: 0026 +title: Sistema de jobs — enrichers asincronos en background +status: in_progress +priority: high +created: 2026-05-01 +blocks: [0027, 0028, 0029, 0030] +supersedes: [0001, 0002, 0003] +--- + +## Objetivo + +Convertir el menu "Run enricher" (hoy placeholder en main.cpp:485) en un +sistema real de jobs asincronos: el usuario lanza un enricher sobre un +nodo, vuelve a la app y sigue trabajando mientras el enricher procesa +en background. Al terminar, el grafo se recarga automaticamente con las +entidades/relaciones nuevas. + +Este issue solo cubre la **infra**. Los enrichers concretos se escriben +en 0028, 0029, 0030. + +## Decisiones tomadas + +1. **Workers concurrentes**: 2 por defecto, configurable via Settings. +2. **Cache de documentos**: `/cache//.{html,md,png}`. Carpeta gitignored en el sub-repo. +3. **Webpage vs Url**: tipos separados (issue 0027). Url = link suelto, Webpage = documento descargado con cuerpo. +4. **Subprocess Python por job** (no daemon residente): cold start ~200 ms aceptable. Si molesta, issue futura. +5. **Estado en `graph_explorer.db`** (NO en `operations.db`): jobs son especificos de la app, no del grafo. + +## Tabla `jobs` (graph_explorer.db) + +```sql +CREATE TABLE IF NOT EXISTS jobs ( + id TEXT PRIMARY KEY, -- ULID + enricher_id TEXT NOT NULL, -- ej: "fetch_webpage" + node_id TEXT, -- nodo objetivo (NULL si batch) + node_name TEXT DEFAULT '', -- snapshot para mostrar en UI + params_json TEXT NOT NULL DEFAULT '{}', + status TEXT NOT NULL, -- queued|running|done|error|cancelled + progress REAL NOT NULL DEFAULT 0, -- 0..1 + stage TEXT NOT NULL DEFAULT '', -- mensaje corto: "fetching", "extracting" + result_json TEXT, -- {entities_added: N, relations_added: M, ...} + error TEXT, + pid INTEGER, -- subprocess pid para cancelar + created_at INTEGER NOT NULL, + started_at INTEGER, + finished_at INTEGER +); +CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status, created_at); +``` + +## Runtime C++ + +### `jobs.{h,cpp}` (nuevo) + +- `JobRunner`: pool de N std::thread workers (default 2). +- Cola en memoria `std::queue` + persistencia en BD. +- API publico: + - `bool jobs_init(const char* db_path, int n_workers);` + - `bool jobs_submit(const char* enricher_id, const char* node_id, const char* params_json, char* out_id);` + - `bool jobs_cancel(const char* job_id);` + - `void jobs_shutdown();` + - `int jobs_dirty_counter();` // incrementa al completar un job; render lo lee + - `bool jobs_list(std::vector* out);` +- Worker hace: + 1. Pop de cola → mark running, started_at = now, pid = subprocess pid. + 2. Spawn `python/.venv/bin/python3 enrichers//run.py` con stdin = JSON. + 3. Lee stderr line-by-line buscando `PROGRESS: ` para actualizar fila. + 4. Lee stdout completo al cerrar — JSON final con entities/relations/node_updates. + 5. Aplica al `operations.db` desde el worker (entity_insert/relation_insert/entity_update). + 6. Marca done o error con result_json/error, finished_at, increment dirty_counter. +- Al arrancar: marca jobs `running` huerfanos como `error: "process died"`. + +### `enrichers.{h,cpp}` (nuevo) + +- Escanea `enrichers/*/manifest.yaml` al arrancar. +- Estructura `EnricherSpec`: + ``` + std::string id, name, description; + std::vector applies_to; // tipos validos + std::vector params; + std::string run_path; // enrichers//run.py absoluto + ``` +- API: + - `void enrichers_load(const char* enrichers_dir);` + - `std::vector enrichers_for_type(const char* type_ref);` + +## Contrato enricher (wire protocol) + +Cada enricher vive en `apps/graph_explorer/enrichers//`: + +``` +enrichers/ + fetch_webpage/ + manifest.yaml + run.py +``` + +### `manifest.yaml` + +```yaml +id: fetch_webpage +name: "Fetch web page" +description: "Descarga HTML, extrae markdown y guarda en cache." +applies_to: [Webpage, Url] +params: + - { name: timeout_s, type: int, default: 15 } + - { name: use_browser, type: bool, default: false } +``` + +### `run.py` — stdin/stdout/stderr + +**stdin** (una linea JSON): +```json +{"node_id":"webpage_123","node_type":"Webpage","node_name":"...", + "metadata":{"url":"https://..."},"params":{"timeout_s":15}, + "db_path":"/path/operations.db","cache_dir":"/path/cache", + "registry_root":"/home/lucas/fn_registry"} +``` + +**stderr** (lineas de progreso, opcional): +``` +PROGRESS:0.10 connecting +PROGRESS:0.50 parsing +PROGRESS:0.90 writing +``` + +**stdout** (una linea JSON al final): +```json +{"node_updates":[ + {"id":"webpage_123","metadata_patch":{"title":"...","status_code":200}} + ], + "entities":[ + {"type":"Domain","name":"example.com","metadata":{}} + ], + "relations":[ + {"from_id":"webpage_123","to_name":"example.com","to_type":"Domain","kind":"BELONGS_TO"} + ], + "notes":""} +``` + +Resolucion de relaciones: +- Si `to_id` esta presente, se usa directamente. +- Si no, se busca por `(to_name, to_type)` en operations.db; si no existe, se crea primero. + +## UI + +### Panel "Jobs" + +- Nuevo panel dockeable (entry en `g_panels[]`). +- Tabla con columnas: enricher, target node (clicable → centra viewport), status (badge), progress bar, stage, duracion, error tooltip. +- Botones inline por fila: cancelar (running), reintentar (error/cancelled), borrar (terminal). +- Filtro: all | active | done | error. + +### Toolbar + +- Badge en la toolbar superior con contador de `running + queued`. Click abre el panel Jobs. + +### Context menu (main.cpp:485) + +Reemplazar el `TextDisabled("coming soon")` por: + +```cpp +auto specs = ge::enrichers_for_type(node->type_ref); +if (specs.empty()) { + ImGui::TextDisabled("(no hay enrichers para este tipo)"); +} else { + for (const auto& s : specs) { + if (ImGui::MenuItem(s.name.c_str())) { + char job_id[64]; + ge::jobs_submit(s.id.c_str(), node->id.c_str(), "{}", job_id); + } + } +} +``` + +## Fases del bucle reactivo en BD + +- **CONSTRUIR**: enricher escrito en `enrichers//`. +- **EJECUTAR**: subprocess corre, escribe progress en `jobs`. +- **RECOPILAR**: stdout JSON parseado, entities/relations aplicadas a operations.db. +- **ANALIZAR**: jobs.result_json contiene metricas (entities_added, duration_ms). +- **MEJORAR**: si un enricher falla repetidamente, futura issue de health checks. + +## Cancelacion + +- Boton "Cancel" en panel Jobs: + - Lee `pid` de la fila. + - `kill(pid, SIGTERM)` (Linux/WSL) o `TerminateProcess` (Windows). + - El worker captura el exit code y marca `cancelled`. + - Si el job aun no salio de la cola (status = queued), el worker simplemente no lo coge — al pop chequea status y skipea cancelled. + +## Definicion de hecho + +- Tabla `jobs` se crea al arrancar la app. +- `JobRunner` con 2 workers acepta `jobs_submit` y procesa. +- Panel Jobs muestra estado en tiempo real (progress bar avanza visiblemente). +- Cancelar mata subprocess y marca `cancelled`. +- Al completar, el grafo se recarga (dirty_counter detectado en render). +- Subir el contador de jobs en la toolbar. +- Test manual: enricher dummy `noop` (incluido en este issue) que duerme 3 s emitiendo PROGRESS y termina sin entidades. Lanzarlo y comprobar UI. + +## Trabajo posterior + +- 0027: tipo Webpage + cache. +- 0028: primer enricher real (`fetch_webpage`) end-to-end. +- 0028b: enrichers extract_domain, extract_links, extract_text_entities. +- 0029: enrichers via CDP (browser headless). +- 0030: macro "Deep enrich" + expand_domain. diff --git a/issues/0027-webpage-type-cache.md b/issues/0027-webpage-type-cache.md new file mode 100644 index 0000000..1591252 --- /dev/null +++ b/issues/0027-webpage-type-cache.md @@ -0,0 +1,82 @@ +--- +id: 0027 +title: Tipo Webpage + cache de documentos descargados +status: pending +priority: high +created: 2026-05-01 +depends_on: [0026] +blocks: [0028, 0029, 0030] +--- + +## Objetivo + +Anadir un tipo `Webpage` al `examples/types.yaml` y un layout +estandarizado de cache donde los enrichers guardan HTML, markdown y +screenshots descargados. El tipo `Url` existente queda como link suelto; +`Webpage` es un documento descargado con cuerpo. + +## Cambios en `examples/types.yaml` + +Anadir tras el bloque `Url`: + +```yaml +- name: Webpage + color: "#89E0FC" + icon: ti-file-text + principal_field: url + fields: + - { name: url, type: url, required: true } + - { name: title, type: string } + - { name: status_code, type: int } + - { name: content_type, type: string } + - { name: fetched_at, type: date } + - { name: html_path, type: string } # cache//.html + - { name: markdown_path, type: string } # cache//.md + - { name: screenshot_path,type: string } # cache//.png + - { name: text_length, type: int } + - { name: lang, type: string } +``` + +## Layout del cache + +``` +/cache/ + ab/ + abcd1234...ef.html + abcd1234...ef.md + abcd1234...ef.png + cd/ + cdef5678...01.html + ... +``` + +- `sha256[0:2]` para evitar miles de archivos en un solo dir. +- Path absoluto desde `` para que sea portable entre PCs (paths relativos en metadata). +- `cache/` se anade al `.gitignore` del sub-repo. + +## Helper C++ + +Funcion en `data.{h,cpp}` (o nuevo `cache_paths.{h,cpp}`): + +```cpp +namespace ge { +// Resuelve el path absoluto donde un enricher debe escribir el blob. +// Crea el dir si no existe. ext sin punto: "html", "md", "png". +// hash_input: tipicamente la URL canonica (normalizada). +std::string cache_path(const char* app_dir, + const char* hash_input, + const char* ext); +} +``` + +- SHA256 calculado en C++ (usar implementacion existente en cpp/functions/core/ si la hay; si no, vendor/sqlite3 trae uno o se anade simple). +- O: el enricher Python calcula el sha256 (mas simple) y lo devuelve como parte de `node_updates`. Decidido: Python calcula el sha256, C++ solo expone `app_dir/cache/` como path absoluto al enricher. + +## Definicion de hecho + +- `Webpage` aparece en types.yaml con icono `ti-file-text`. +- El icono se renderiza correctamente (existe en tabler_codepoint_by_name). +- `cache/` esta en `.gitignore` del sub-repo del app. +- C++ pasa `cache_dir` al enricher en el JSON de stdin. +- Test manual: crear nodo `Webpage` desde el inspector, comprobar que + aparece con el color/icono correctos. diff --git a/issues/0028-enricher-fetch-webpage.md b/issues/0028-enricher-fetch-webpage.md new file mode 100644 index 0000000..53ec317 --- /dev/null +++ b/issues/0028-enricher-fetch-webpage.md @@ -0,0 +1,78 @@ +--- +id: 0028 +title: Enricher fetch_webpage (MVP end-to-end) +status: pending +priority: high +created: 2026-05-01 +depends_on: [0026, 0027] +--- + +## Objetivo + +Primer enricher real sobre el sistema de jobs (0026). Right-click sobre +un nodo `Url` o `Webpage` → "Fetch web page". Descarga el HTML, lo +convierte a markdown, guarda los blobs en cache, actualiza el nodo +(o lo convierte a Webpage si era Url) y crea el nodo `Domain` con +relacion `BELONGS_TO`. + +Este enricher valida el contrato entero. Los siguientes (0028b) reusan +exactamente el mismo wire protocol. + +## Archivos + +``` +apps/graph_explorer/enrichers/fetch_webpage/ + manifest.yaml + run.py +``` + +## `manifest.yaml` + +```yaml +id: fetch_webpage +name: "Fetch web page" +description: "Descarga HTML, extrae markdown limpio y guarda en cache." +applies_to: [Webpage, Url] +emits: [Domain] +relations: [BELONGS_TO] +params: + - { name: timeout_s, type: int, default: 15 } +``` + +## `run.py` + +Logica: +1. Lee JSON de stdin. +2. Saca `url` de `metadata.url` (o `metadata.address` si es Url legacy). +3. `PROGRESS:0.05 normalize` — `normalize_url_py_cybersecurity`. +4. `PROGRESS:0.20 fetching` — descarga via `requests.get(url, timeout=N)`. +5. `PROGRESS:0.60 parsing` — `html_to_markdown_py_core` con readabilipy. +6. `PROGRESS:0.85 writing` — calcula sha256(url), escribe `cache//.html` y `.md`. +7. Emite stdout JSON: + - `node_updates`: cambia type a Webpage si era Url, anade title/status_code/content_type/fetched_at/html_path/markdown_path/text_length. + - `entities`: `{type: Domain, name: , metadata: {}}`. + - `relations`: `from_id: , to_name: , to_type: Domain, kind: BELONGS_TO`. + +## Funciones del registry usadas + +- `normalize_url_py_cybersecurity` — limpia tracking params. +- `html_to_markdown_py_core` — readabilipy + markdownify. +- `extract_domain` se hace inline en el enricher (regex trivial sobre la URL parseada). + +## Manejo de errores + +- HTTP error (4xx/5xx) → escribe status_code en metadata pero NO marca el job como error (el nodo guarda evidencia del fallo). +- Timeout / DNS error / etc → exit con error JSON en stdout: `{"error": "...", "node_updates": [], "entities": [], "relations": []}`. +- Si el enricher levanta excepcion, sale con codigo != 0 y stderr capturado va a `jobs.error`. + +## Definicion de hecho + +- Crear nodo Url con `https://example.com` → click derecho → "Fetch web page". +- En segundos aparece en panel Jobs como `running` con progress. +- Al terminar: + - El nodo cambia a tipo `Webpage` con icono `ti-file-text`. + - El inspector muestra title, status_code, html_path, markdown_path. + - Aparece nodo `Domain` "example.com" conectado por `BELONGS_TO`. + - El archivo `cache/.md` existe en disco. +- El job aparece en panel Jobs como `done` con `entities_added=1, relations_added=1`. +- Tirar la red (sin internet) → el job acaba en `error` con mensaje claro. diff --git a/issues/0028b-enrichers-extract-trio.md b/issues/0028b-enrichers-extract-trio.md new file mode 100644 index 0000000..8fe18df --- /dev/null +++ b/issues/0028b-enrichers-extract-trio.md @@ -0,0 +1,72 @@ +--- +id: 0028b +title: Enrichers extract_domain, extract_links, extract_text_entities +status: pending +priority: high +created: 2026-05-01 +depends_on: [0028] +--- + +## Objetivo + +Tres enrichers Python adicionales que reusan el contrato validado por +`fetch_webpage`. Cada uno cubre un eje de extraccion distinto. + +## 1. `extract_domain` + +``` +applies_to: [Url, Webpage, Email] +emits: [Domain] +relations: [BELONGS_TO] +``` + +- Saca el dominio de `metadata.url` o `metadata.address`. +- Crea nodo `Domain` si no existe + relacion `BELONGS_TO`. +- Util cuando el usuario tiene un Url/Email que aun no ha sido fetched + pero quiere ver el dominio en el grafo. + +## 2. `extract_links` + +``` +applies_to: [Webpage] +emits: [Url] +relations: [LINKS_TO] +``` + +- Lee `metadata.markdown_path`. Si vacio → exit con error "run fetch_webpage first". +- `extract_urls_py_cybersecurity` sobre el contenido. +- Para cada URL distinta encontrada: + - Crea nodo `Url` con `metadata.url` (si no existe). + - Relacion `LINKS_TO` desde la Webpage origen. +- Param: `max_links` (default 50) para no saturar el grafo. + +## 3. `extract_text_entities` + +``` +applies_to: [Webpage] +emits: [Person, Org, Email, Phone, Domain, Location, IPAddress, CVE, ...] +relations: [EXTRACTED_FROM, ...relaciones que GLiREL detecte] +``` + +- Lee `metadata.markdown_path`. +- Llama `extract_graph_hybrid_py_pipelines` (regex IoCs + GLiNER + GLiREL + LLM fallback). +- Para cada entidad detectada: + - Resuelve por `(name, type)` en operations.db. Si no existe la crea. + - Relacion `EXTRACTED_FROM` desde la entidad nueva al nodo Webpage. +- Para cada relacion detectada por GLiREL: + - Relacion entre las dos entidades con el `kind` predicho. +- Params: + - `chunk_size` (default 2000) + - `use_llm_fallback` (default false — evitar coste; el usuario lo activa en jobs concretos) + +## Definicion de hecho + +- Los tres enrichers aparecen en el menu "Run enricher" segun el tipo + del nodo right-clickado. +- En un nodo Webpage el menu muestra los 3 + fetch_webpage. +- Test integracion: + - Crear Url → fetch_webpage → run extract_links sobre el resultado + → run extract_text_entities → grafo se llena con persons/orgs/etc. + - Cada paso es un job independiente visible en panel Jobs. +- `extract_text_entities` con LLM off termina sin coste y produce + entidades de IoC + entidades GLiNER (gratis). diff --git a/issues/0029-enrichers-cdp.md b/issues/0029-enrichers-cdp.md new file mode 100644 index 0000000..37b9e34 --- /dev/null +++ b/issues/0029-enrichers-cdp.md @@ -0,0 +1,63 @@ +--- +id: 0029 +title: Enrichers via Chrome headless (CDP) — fetch_webpage_browser, fetch_screenshot +status: pending +priority: medium +created: 2026-05-01 +depends_on: [0028] +--- + +## Objetivo + +Variantes de los enrichers basicos que usan Chrome headless via CDP, +para sitios con contenido renderizado por JavaScript (SPA, paginas con +auth visual, etc.) o cuando se quiere capturar evidencia visual. + +## 1. `fetch_webpage_browser` + +``` +applies_to: [Url, Webpage] +emits: [Domain] +relations: [BELONGS_TO] +params: + - { name: chrome_port, type: int, default: 9222 } + - { name: wait_after_load_ms, type: int, default: 1500 } +``` + +- Usa funciones del registry: + - `chrome_launch_go_browser` — lanza Chrome en port (reusa si ya esta). + - `cdp_connect_go_browser` + - `cdp_navigate_go_browser` + - `cdp_wait_load_go_browser` + - `cdp_get_html_go_browser` — DOM post-JS. +- El run.py shell-out a un binario Go pequeno o llama estas funciones via + un wrapper Python que invoca el Go function como subprocess. +- Decision pendiente: empaquetar las funciones Go en un binario CLI + `cdp-fetcher` que el run.py invoque, o reescribir la logica en Python + con `pychrome` / `playwright`. Preferencia: binario Go para reusar las + funciones del registry. + +## 2. `fetch_screenshot` + +``` +applies_to: [Webpage, Url] +params: + - { name: full_page, type: bool, default: true } +``` + +- `cdp_screenshot_go_browser` → guarda `cache/.png`. +- `node_updates`: anade `screenshot_path` a metadata del Webpage. +- No emite entidades nuevas. + +## Definicion de hecho + +- `fetch_webpage_browser` extrae correctamente DOM de una SPA (test: + twitter.com, linkedin.com publico). +- `fetch_screenshot` produce PNG legible en el cache. +- Inspector del nodo Webpage muestra una preview del screenshot + cuando `screenshot_path` existe (mejora UI opcional). + +## Out of scope + +- Login flows / auth via CDP — fuera de v1. +- Adblock / fingerprint evasion — el user-agent default es suficiente. diff --git a/issues/0030-deep-enrich-macro.md b/issues/0030-deep-enrich-macro.md new file mode 100644 index 0000000..1a4119c --- /dev/null +++ b/issues/0030-deep-enrich-macro.md @@ -0,0 +1,62 @@ +--- +id: 0030 +title: Macro "Deep enrich" + enricher expand_domain +status: pending +priority: medium +created: 2026-05-01 +depends_on: [0028, 0028b] +--- + +## Objetivo + +Encadenar varios enrichers con un solo click. Cubre dos flujos: + +1. **Deep enrich Webpage**: sobre un nodo Webpage, ejecuta en orden + `fetch_webpage` (si no fetched aun) → `extract_domain` → `extract_links` + → `extract_text_entities`. Cuatro jobs separados, en cadena. +2. **Expand domain**: sobre un nodo Domain, fetch homepage + 1 nivel de + links + extraccion de entidades sobre cada pagina. Util para "dame + todo lo que sepas de este dominio en un click". + +## Implementacion + +### Macro Deep enrich (no es un enricher Python — es UI + orquestacion en C++) + +- Boton/menu item "Deep enrich" en el context menu del nodo Webpage. +- Encolar 4 jobs con dependencias: cada job tiene `depends_on_job_id`. +- Worker pool respeta dependencias: si el job tiene depends_on y el + predecesor no esta `done`, lo deja en cola. +- Anadir columna a tabla `jobs`: `depends_on_job_id TEXT`. + +### Enricher `expand_domain` + +``` +applies_to: [Domain] +params: + - { name: max_pages, type: int, default: 5 } + - { name: deep, type: bool, default: false } # si true, deep enrich cada pagina +``` + +- run.py: + 1. Fetch `https:///` y `http:///` (probando ambos esquemas). + 2. Crea Webpage homepage + relacion `HOMEPAGE_OF` desde Domain. + 3. Si `deep`, encola un job `extract_text_entities` por pagina via + un endpoint local de control (out of scope v1) o emite un campo + especial `chained_jobs: [...]` que el worker C++ encola. + 4. Decision: v1 solo crea las paginas. La cadena con extract_* + se puede hacer manualmente desde la UI o esperar a un sistema + de chained jobs decente. + +## Definicion de hecho + +- Click derecho en Webpage → "Deep enrich" → 4 jobs en cadena visibles + en panel Jobs. Al terminar el ultimo, el grafo tiene domain + links + + persons/orgs/etc. +- Click derecho en Domain → "Expand domain" → Webpage homepage aparece + conectada al Domain. +- Cancelar el job intermedio cancela en cascada los que dependen. + +## Out of scope v1 + +- Cron / repeat schedule de enrichers. +- Progress agregado de la cadena (cada job mantiene su progress propio).