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) <noreply@anthropic.com>
This commit is contained in:
@@ -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`.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 <type> <name> [--metadata k=v ...]` — crea entidad.
|
||||
- `gx rel <from_id> <kind> <to_id>` — crea relacion.
|
||||
- `gx ingest <file>` — manda archivo al endpoint, abre preview en TUI.
|
||||
- `gx from-url <url>` — fetch + extract.
|
||||
- `gx search "query"` — devuelve hits del grafo activo (json o tabla).
|
||||
- `gx neighbors <id> [--depth N]`.
|
||||
- `gx open <id>` — 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 <id> --depth 2 --format json | jq` funciona en pipeline.
|
||||
- Errores de conexion al endpoint se reportan claros (no stack traces).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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/<bot_name>/` 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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/<name>/`. 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).
|
||||
@@ -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**: `<app_dir>/cache/<sha256[0:2]>/<sha256>.{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<JobId>` + 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<JobRow>* out);`
|
||||
- Worker hace:
|
||||
1. Pop de cola → mark running, started_at = now, pid = subprocess pid.
|
||||
2. Spawn `python/.venv/bin/python3 enrichers/<id>/run.py` con stdin = JSON.
|
||||
3. Lee stderr line-by-line buscando `PROGRESS:<float> <stage>` 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<std::string> applies_to; // tipos validos
|
||||
std::vector<EnricherParam> params;
|
||||
std::string run_path; // enrichers/<id>/run.py absoluto
|
||||
```
|
||||
- API:
|
||||
- `void enrichers_load(const char* enrichers_dir);`
|
||||
- `std::vector<EnricherSpec> enrichers_for_type(const char* type_ref);`
|
||||
|
||||
## Contrato enricher (wire protocol)
|
||||
|
||||
Cada enricher vive en `apps/graph_explorer/enrichers/<id>/`:
|
||||
|
||||
```
|
||||
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/<id>/`.
|
||||
- **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.
|
||||
@@ -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/<sha256[0:2]>/<sha256>.html
|
||||
- { name: markdown_path, type: string } # cache/<sha256[0:2]>/<sha256>.md
|
||||
- { name: screenshot_path,type: string } # cache/<sha256[0:2]>/<sha256>.png
|
||||
- { name: text_length, type: int }
|
||||
- { name: lang, type: string }
|
||||
```
|
||||
|
||||
## Layout del cache
|
||||
|
||||
```
|
||||
<app_dir>/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 `<app_dir>` 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.
|
||||
@@ -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/<sha[0:2]>/<sha>.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: <dominio>, metadata: {}}`.
|
||||
- `relations`: `from_id: <node_id>, to_name: <dominio>, 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/<sha>.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.
|
||||
@@ -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).
|
||||
@@ -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/<sha>.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.
|
||||
@@ -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://<domain>/` y `http://<domain>/` (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).
|
||||
Reference in New Issue
Block a user