split_sentences a menudo no llega al umbral de 50 (un texto medio
tiene 5-15 frases). split_words tokeniza el mismo notes en palabras
y trivialmente lo supera con cualquier parrafo decente -> Group
visible y testeable end-to-end sin necesidad de pegar megabytes.
Diferencias respecto a split_sentences:
* Splits por regex de letras (incluye acentos espanyoles + apostrofo
interno como "don't"). Numeros y puntuacion ignorados.
* Lowercase + filtro por min_length (default 3, filtra a/el/de/y/o).
* Param `dedupe` (default true): vocabulario unico vs cada ocurrencia.
Con dedupe=false sirve como stress test de volumen.
* Tipo `Word` en types.yaml: amarillo, ti-letter-w, principal_field=word.
* Relacion `WORD_OF` desde cada Word al source.
* Mismo patron de grouping que split_sentences (threshold 50, K=10
preview, batch_id en metadata, Group con count + enricher).
Tests:
* below threshold no crea Group.
* >=50 tokens unicos -> Group + 10 sueltos + resto agrupados.
* dedupe=true (default) colapsa repeticiones; dedupe=false las
conserva como nodos separados.
* min_length filtra correctamente.
* notes prioriza sobre node_name.
* texto vacio -> exit 2.
* max_words trunca.
WSL 89 / Windows 78 + 11 skipped.
Bug derivado del fix anterior: gx-cli escribia ficheros JSON en
`$GX_APP_DIR/agent_jobs_queue/` (apuntando al repo fuente) mientras
main.cpp escaneaba `parent(g_layout_db_path)/agent_jobs_queue/`
(install Windows). Dos directorios distintos -> jobs huerfanos.
Echo reportaba "encolado" pero el worker nunca veia los ficheros.
La causa: chat.cpp setea GX_APP_DIR=<registry>/projects/osint_graph/
apps/graph_explorer y GX_APP_DB=<install>/local_files/projects/<slug>/
graph_explorer.db. Dos sitios. Solo APP_DB coincide con donde
graph_explorer.exe escanea (parent del .db).
Fix:
* gx-cli cmd_enricher_run: queue_dir = parent(GX_APP_DB) /
agent_jobs_queue. Alineado con main.cpp.
* gx-cli: nuevo helper `_log(tag, msg)` que escribe a stderr Y a
`<parent(app_db)>/gx-cli.log` para auditoria persistente. Cubre
node_create, node_update, node_delete, rel_create, enricher_run.
* gx-cli mcp _mcp_log tambien persiste a gx-cli.log.
* main.cpp: log el queue scan dir una vez por sesion para detectar
mismatches a futuro.
* .gitignore: agent_jobs_queue/ y gx-cli.log son runtime, no se
commitean.
Tests:
* test_enricher_run_queue_dir_derives_from_app_db (regresion)
configura GX_APP_DB en un dir distinto de GX_APP_DIR y verifica
que el JSON aterriza junto a APP_DB.
* test_enricher_run_writes_log_to_gx_cli_log valida la auditoria.
WSL 81 / Windows 70 + 11 skipped.
Bug: Echo (gx-cli en WSL) recibia "disk I/O error" al INSERT en la
tabla `agent_jobs` de graph_explorer.db. Causa: graph_explorer.exe
mantiene esa BD abierta con journal_mode=WAL desde Windows, y SQLite
WAL exige mmap del .shm compartido entre procesos. Cuando un escritor
accede via /mnt/c (9p) y el otro nativo NTFS, ese mmap falla.
El proyecto ya habia resuelto este patron antes: el contador de
mutaciones (.mutations.marker) usa fichero plano en vez de SQL por
exactamente la misma razon. agent_jobs era la unica cola que se
quedo en SQLite — momento de aplicar el mismo fix.
Cambios:
* gx-cli cmd_enricher_run: en lugar de INSERT, escribe
`<app_dir>/agent_jobs_queue/<req_id>.json` con el payload del job.
Atomic write (tmp + rename, atomico tanto en NTFS como en 9p).
* main.cpp polling: en lugar de SELECT/DELETE sobre agent_jobs,
escanea ese directorio cada frame, lee cada JSON via json_extract
(sqlite3 in-memory, sin tocar archivos en disco), llama jobs_submit,
y borra el fichero. Throttle a 8 jobs por frame igual que antes.
* main.cpp: anyade <filesystem> y <fstream>.
* tests/test_gx_cli.py: 5 tests nuevos en TestCliEnricherRun:
- escribe fichero JSON con req_id como nombre
- NO crea tabla agent_jobs en graph_explorer.db (regresion)
- errores claros si enricher o nodo no existen
- no quedan .tmp tras encolado exitoso
WSL 79 / Windows 68 + 11 skipped.
Bug encontrado por el agente Echo: el MCP server gx-cli (subcomando
`mcp-server`) llamaba a cmd_node_create / cmd_node_update con un
SimpleNamespace que NO incluia `notes`, asi que `args.notes` lanzaba
AttributeError. Causa raiz: MCP_DISPATCH no defaulteaba `notes` ni
`append_notes`, y el inputSchema de las tools tampoco los anunciaba.
Cambios:
* MCP_TOOLS["node_create"].inputSchema.properties anyade `notes`.
* MCP_TOOLS["node_update"].inputSchema.properties anyade `notes`
+ `append_notes` (boolean, default false).
* MCP_DISPATCH["node_create"] defaultea `notes: None`.
* MCP_DISPATCH["node_update"] defaultea `notes: None`,
`append_notes: False`.
Tests nuevos en tests/test_gx_cli.py (30 tests):
* CLI: node create/update/delete con notes (replace + append),
list/show/search, rel create/list/delete con cascada, query
read-only que rechaza writes, autodetect de tipos.
* MCP dispatcher: cada cmd_* tolera args opcionales omitidos,
notes y append_notes funcionan via dispatch, MCP_TOOLS y
MCP_DISPATCH coinciden 1:1 (sanity contractual).
* Regresion 0035d: tests dedicados que congelan el contrato
notes/append_notes en defaults e inputSchema — si alguien
vuelve a quitarlos el test se queja inmediatamente.
WSL 74 / Windows 63 + 11 skipped.
El agente Echo (chat panel + claude -p) usa gx-cli para todas las
mutaciones del grafo. Antes solo podia setear name/type/status/
description/tags — pero los enrichers nuevos split_sentences y
extract_iocs_text leen entities.notes (lo que se escribe en el panel
Note del Inspector). Sin este flag, el agente no podia darle texto
largo a un nodo para luego enricharlo.
Cambios:
- node create: --notes "..." (vacio por default).
- node update: --notes "..." (replace) + --append-notes (concat con
doble newline como separador, util para acumular contexto sin
pisar lo previo).
Smoke test: replace y append funcionan, default vacio sigue ok.
El campo `notes` es lo que el usuario escribe en el panel Note del
Inspector (doble click sobre el nodo) — sitio canonico para texto
largo. Antes los enrichers leian metadata.text/description/query como
prioridad, dejando notes ignorado y forzando al usuario a inyectar
texto via la UI metadata-extra (poco descubrible).
Cambios:
- Ambos run.py abren la BD y leen `entities.notes` por SQL antes de
fallback a node_name. metadata.text/description/query ya no se
consultan (KISS — solo notes y name).
- conftest.make_node admite kwarg `notes` para inyectar contenido
en la columna notes desde tests.
- Tests actualizados: SAMPLE_TEXT y los IoC dumps van por `notes=`
en lugar de `metadata={"text": ...}`.
- Renombrado el test que verificaba prioridad: ahora se llama
`*_uses_notes_priority` y verifica notes > name.
Tests verdes WSL (44) y Windows (33 + 11 skipped).
Para probar la app sin depender de red (DDG bloquea con captcha desde
ciertas IPs). Ambos aplican grouping (umbral 50, preview K=10) replicando
el patron de web_search.
- split_sentences: parte texto en frases (regex), crea nodos Sentence
conectados con SENTENCE_OF.
- extract_iocs_text: variante de extract_text_entities que lee directo
metadata.text/description/name, sin requerir fetch previo. Vendoriza
extract_iocs_py_cybersecurity. Multi-tipo, agrupado en un solo Group
heterogeneo (decision 6 multi-grupo-por-tipo es fase 2).
- Tipo Sentence en types.yaml.
Tests pytest cubren below/above threshold para ambos.
- entity_ops: EntityRowSnapshot.group_id + SQL con COALESCE(group_id,'')
+ deteccion via PRAGMA para BDs viejas sin la columna.
- views.h: TableRow.group_id + AppState.table_filter_group_id /
table_filter_group_name (RAM-only).
- main.cpp: dispatch en want_open_note — si type_ref == "Group", setea
filtro de grupo + abre panel Table en vez de Note. Reset de search
buf y col_filters al entrar al drill-in para que el usuario vea todo
el contenido del grupo.
- views.cpp: build_visible compone group_id con search/tabs/col_filters
(AND). types_present se reduce a tipos presentes en el grupo cuando
hay drill-in activo. Header pintado en amarillo con TI_FOLDER +
contador + boton "Clear group filter". Al cerrarse el panel se
limpia el filtro automaticamente.
Tests: pytest 35 passed (WSL) / 24 passed + 11 skipped (Windows).
Refs: issues/0035d-tableview-drill-in.md
Cuando un enricher web_search produce >= 50 resultados, los primeros 10
quedan sueltos colgando del source (preview Twitter/Reddit) y los
restantes entran como hijos de un nuevo nodo Group cuadrado.
Cambios:
- enrichers/web_search/run.py:
- DEFAULT_GROUP_THRESHOLD=50, GROUP_PREVIEW_K=10 (constantes globales).
- has_group_id_column(): detecta si el schema soporta agrupacion.
- insert_group_entity(): crea nodo Group con metadata
{enricher, query, count, batch_id}.
- insert_url_entity() acepta batch_id y group_id; los inyecta en
metadata/columna respectivamente. Nodos existentes mantienen su
group_id actual (no se machaca).
- Generacion de batch_id (UUID4 hex) por ejecucion, compartido por
todos los nodos creados (group + sueltos + agrupados).
- Cada hijo del grupo conserva su relacion individual SEARCH_RESULT_OF
al source original — la procedencia es la relacion real, no el
contenedor.
- El JSON de salida añade batch_id, group_id, grouped.
- tests/conftest.py: añade columna entities.group_id al SCHEMA_SQL y
expone group_id en list_entities() para que los tests lo verifiquen.
- tests/test_web_search.py: 3 tests nuevos
- below_threshold_no_group: 5 resultados → 0 Groups, comportamiento clasico.
- above_threshold_creates_group_and_preview: 100 resultados → 1 Group +
10 sueltos + 90 con group_id, todos con SEARCH_RESULT_OF al source.
- batch_id_shared_across_outputs: group + preview + hijos comparten
batch_id.
- _build_lite_html() genera HTML sintetico con N resultados sin
necesidad de fixture estatico grande.
Tests: 35 passed (32 previos + 3 nuevos) en WSL.
24 passed + 11 skipped en Windows.
Refs: issues/0035c-web-search-creates-groups.md
- AppState anade `group_expanded` (unordered_map<string,bool>) en RAM,
default vacio = todos los grupos colapsados al arranque. Sin
persistencia entre sesiones (fase 1).
- `apply_group_filter(GraphData*, db_path, expanded)` consulta
entities (id, group_id, type_ref) de operations.db, marca como
ocultos los nodos cuyo group_id apunta a un grupo no expandido,
compacta `g->nodes` y re-mapea indices de aristas.
- Aristas:
* Cross-edge (un extremo oculto, otro fuera): se redirige el
extremo oculto al nodo del grupo. Sin dedup (issue 0035 dec. 5).
* Internas (ambos extremos en el mismo grupo colapsado): se ocultan.
* Inter-grupo (ambos en grupos colapsados distintos): dedup por
par no ordenado (group_a, group_b) + rel_type, una linea por par.
* Orfanas (group_id apunta a un grupo no presente en grafo): el
nodo se oculta y sus aristas se descartan.
- Centralizado: el filtro corre en `reload_graph()` cuando se le
pasa `group_expanded`, y en `load_input()` tras el load inicial.
Cubre las 4 rutas de carga del app (toolbar reload, mutaciones,
inspector save, primera carga / switch project).
- Idempotente sobre un grafo ya filtrado y robusto frente a BDs sin
columna `group_id` (schema antiguo) — no toca el grafo.
Smoke test manual con 3 BDs sintéticas:
- Grupo + 2 children + edges cruzadas/internas: nodes 5→3, edges
4→3 (internal hidden, cross redirected).
- 2 grupos con 4 cross-edges entre ellos: edges 4→1 (dedup).
- group_id huerfano: nodo oculto + arista descartada.
Build clean en Windows. Tests verdes:
- WSL pytest: 32 passed.
- Windows pytest: 21 passed + 11 skipped.
Refs: issues/0035b-renderer-hides-grouped-children.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bloque de cambios revisados y validados con el usuario en sesiones
previas que no habian aterrizado en commits propios. Lista por tema:
* enrichers: web_search ahora usa lite.duckduckgo.com como endpoint
primario (mas tolerante con bot detection desde IP residencial),
con fallback al endpoint html. Detecta pagina captcha y emite
error claro si ambos fallan. Anyade _DDGLiteParser para el formato
lite + auto-pick de parser por contenido.
* enrichers: tipo Webpage unificado en Url (campos de cuerpo
cacheado viven en metadata del Url). Manifests actualizados
(applies_to: [Url]). fetch_webpage ya no convierte Url->Webpage.
* enrichers/manifest: campo `params` parseado a EnricherSpec.params
(name, type, default_value, description). UI puede renderizar
dialog de configuracion.
* jobs: fix de path conversion para Python embebido nativo Windows
(no convertir a /mnt/c/... cuando el subproceso es Windows-native;
solo cuando es bash o python via WSL).
* main.cpp: ventana ImGui (no modal) "Run enricher" con layout
2-col (label izq, input der). Inserta job con JSON tipado. Layout
clustering apretado: hijos del mismo anchor en un solo anillo
alrededor del padre, sin desperdigar por anillos crecientes.
* views: inspector con layout 2-col via BeginTable (Identity,
Schema fields, Extras). Description full-width debajo de su label.
* tests: portable conftest (auto-detecta REGISTRY_ROOT, PYTHON_BIN,
ENRICHERS_DIR para WSL y Windows portable). _runner.py trampoline
inyecta stub via sys.path porque embedded Python ignora PYTHONPATH.
Tests bash-only (vendor_script, freeze, dispatcher bash, resolver
Linux-binary) skipean en Windows. Tests existentes adaptados a
Webpage->Url.
Resultado actual: 32 passed WSL, 21 passed + 11 skipped Windows.
Plumbing para issue 0035 — agrupacion de resultados de enrichers
cuando exceden umbral. Sin cambios visibles para el usuario todavia.
- Migracion idempotente: ALTER TABLE entities ADD COLUMN group_id si
no existe (detectado via PRAGMA table_info). Se ejecuta al abrir
el proyecto en switch_to_project y en el bootstrap inicial.
- Tipo Group en examples/types.yaml (template) y en el types.yaml
del proyecto default activo en Windows.
- shape=square (regla en types_registry.cpp extendida a Group),
color=#94A3B8, icon=ti-stack-2.
- Fields: name (req), count (int), enricher (string), batch_id (string).
Refs: issues/0035a-group-type-and-schema.md
Junto con el cambio del framework (commit 81d8a7c9), graph_explorer
ahora resuelve enrichers/, runtime Python y gx-cli desde
<exe_dir>/assets/ con fallback a las rutas dev legacy.
- main.cpp: enrichers_dir busca primero <exe_dir>/assets/enrichers/
(deploy con /compile). Fallback a <app_dir>/enrichers/ del repo
cuando se ejecuta desde build/ (modo dev).
- jobs.cpp::resolve_python_runtime: incluye
<exe_dir>/assets/runtime/python/{python.exe|bin/python3} como
primera opcion de la cadena de fallback. La opcion legacy sin
assets/ queda como segundo intento.
- chat.cpp: gxcli_path busca <exe_dir>/assets/gx-cli{.exe} con
fallback a <app_dir>/gx-cli para modo dev.
Tests: 32/32 verde. Build Linux + Windows OK. Deploy fresco a
Desktop con todas las 6 apps confirma layout limpio:
<app>.exe + (duckdb.dll si aplica) + assets/ + local_files/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adopta la convencion local_files/ del framework para separar
distribuibles (.exe, dlls, enrichers/, runtime/) de estado del
usuario (settings, DBs, proyectos). Con esto + el runtime Python
embebido (Windows) ya copiado al Desktop, la app es completamente
portable a otra maquina Windows sin WSL ni fn_registry montado.
Sustituye paths hardcodeados (graph_explorer.db, graph_explorer.ini,
projects/) por resolutores que apuntan a <exe_dir>/local_files/.
- project_manager: k_projects_dir y k_settings_file pasan a ser
helpers projects_root() / settings_path() que llaman a
fn::local_path internamente. Layout en disco documentado en el
comentario de cabecera del .h.
- main.cpp: el modo legacy y el fallback de jobs_init usan
fn::local_path('graph_explorer.db') en lugar de relativo al cwd.
Junto al cambio del framework (commit f102aba9), graph_explorer
se distribuye con su carpeta limpia: solo .exe + duckdb.dll +
TTFs + enrichers/ + runtime/. Todo el estado del usuario vive
en local_files/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Integra el sistema de vendoring para enrichers Python. Cada
enricher empaqueta las funciones del registry que declara en
uses_functions a `_vendored/` durante el build, lo que cierra la
ultima dependencia hacia el fn_registry montado.
Tests: 32/32 verde (6 nuevos del script vendor).
Estado del 0033 main:
- Fase A (dispatcher multi-lang): hecha.
- Fase B (runtime Python embebido): hecha.
- Fase C (UI badges): pendiente.
Estado de los sub-issues:
- 0033b vendoring: hecho.
- 0033c fn check vendored: pendiente.
- 0033d indexer python_runtime: pendiente.
- 0033e /compile orquestador: pendiente.
Cada enricher con `lang: python` y `uses_functions` no vacio ahora
puede empaquetar las funciones del registry que necesita en
`<enricher>/_vendored/`. El run.py importa de ahi en lugar de
`<registry_root>/python/functions/`, lo que hace al binario
distribuible sin dependencia de un fn_registry montado.
Cambios:
1. tools/vendor_enricher_python.sh
- Lee `uses_functions` del manifest (filtrando IDs `*_py_*`).
- Resuelve `file_path` desde registry.db.
- Copia recursivamente con expansion transitiva: si un fichero
vendorizado importa siblings del mismo dominio, los siblings
tambien se copian (resuelve el caso `extract_iocs.py` que
importa 7 modulos hermanos).
- Genera `.vendor.lock` con `<id> <sha256> <src_path>` por
funcion declarada para auditoria.
- Idempotente — si todos los hashes coinciden, no rehace nada.
2. Manifests actualizados con `uses_functions`:
- fetch_webpage: normalize_url + html_to_markdown
- extract_links: extract_urls
- extract_text_entities: extract_iocs
3. run.py de los 3 enrichers afectados: importan de `_vendored/`
si existe, fallback a `<registry_root>/python/functions/` en
modo dev (mantiene los tests pytest funcionando).
4. app.md: anade `cryptography` a python_runtime_deps porque el
blob `cybersecurity.cybersecurity` lo importa al top.
5. Tests:
- test_vendor_script.py — 6 tests del script: layout correcto,
transitive siblings, lock con SHA256, idempotencia, modulos
importables en aislamiento.
- 16 tests de enrichers existentes pasan via vendoring (no usan
registry_root porque _vendored/ tiene prioridad).
6. Issue 0033b movido a issues/completed/.
Tests: 32/32 verde (16 enrichers + 6 dispatcher + 4 runtime + 6
vendor).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Status sincronizado con master:
- 0001 chat con Claude -> shipped como panel Echo
- 0003 enricher web -> shipped (0028 + 0028b)
- 0026 sistema de jobs -> shipped
- 0027 tipo Webpage -> shipped
- 0028 fetch_webpage -> shipped
- 0028b extract trio -> shipped
- 0031 layout estable -> shipped
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Integra fase B del issue 0033: graph_explorer puede distribuirse
con runtime Python autocontenido en <exe_dir>/runtime/python/.
Cero dependencia de WSL en Windows cuando hay embed; el legacy
registry_venv sigue como fallback transparente.
Pendientes del 0033:
- Fase C: badges [Go]/[Py]/[Sh] en la UI (cosmetica).
- Sub-issues 0033b-e (vendoring, fn check, indexer, /compile).
Tests: 26/26 verde.
Permite distribuir graph_explorer.exe Windows sin dependencia de WSL
ni del .venv del registry. Tambien funciona en Linux como bundle
autocontenido portable.
Cambios:
1. tools/freeze_python_runtime.sh
- Linux: copia python-build-standalone (uv) ~87 MB,
elimina marker EXTERNALLY-MANAGED, instala wheels.
- Windows: descarga python-3.12.7-embed-amd64.zip oficial
(~12 MB), habilita site-packages, instala wheels via
pip install --target --platform win_amd64.
- Idempotente via runtime/.lock con SHA256 del estado.
- Lee python_runtime_deps del frontmatter de app.md.
2. jobs.cpp::cached_python_runtime() — resolver con cadena:
1. <exe_dir>/runtime/python/{python.exe|bin/python3} (embedded)
2. $FN_PYTHON (env)
3. <registry_root>/python/.venv/bin/python3 (registry_venv)
4. python3 del PATH (system)
Loggea procedencia al iniciar jobs_init.
3. POSIX run_subprocess: usa el runtime resuelto en lugar del
path hardcodeado.
4. Windows run_subprocess: ramifica por needs_wsl. Si embedded
o env, lanza Python Windows nativo via CreateProcessW
directamente (run_path tambien Windows nativo). Solo el
legacy registry_venv sigue por wsl.exe.
5. app.md: nuevos campos python_runtime: true y
python_runtime_deps: [requests, certifi, urllib3].
6. .gitignore extendido con runtime/, projects/, _vendored/,
.vendor.lock, binarios Go de enrichers.
Tests: 26/26 verde — 16 originales + 6 dispatcher fase A + 4
nuevos del resolver fase B (con/sin embed, FN_PYTHON, idempotencia
del freeze script).
Smoke E2E manual: runtime/python/bin/python3 ejecuta web_search
con cwd /tmp y registry_root pasado en ctx, sin tocar el .venv del
registry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Integra la fase A del issue 0033: el sistema de enrichers ahora
acepta `lang: go|python|bash` en el manifest y `jobs.cpp` ramifica
el spawn segun lang. Retrocompatible al 100% — los 5 enrichers
existentes funcionan sin cambios.
Pendientes del issue 0033:
- Fase B: runtime Python embebido (<app>/runtime/python/).
- Fase C: badges [Go]/[Py]/[Sh] en la UI.
Tests: 22/22 verde (16 regresion + 6 dispatcher nuevos).
Extiende el sistema de enrichers para soportar varios lenguajes en el
mismo registro. El manifest gana dos campos opcionales:
lang: python|go|bash (default: python — retrocompat con los 5
enrichers existentes que no lo declaran)
exec: run (basename del script o binario; default "run")
EnricherSpec ahora lleva `lang`, `exec_basename`, `disabled` y
`disabled_reason`. parse_manifest lee los nuevos campos y aplica
defaults; resolve_run_path busca <dir>/<exec>{.py|.sh|.exe|<vacio>}
segun lang + plataforma. Si el ejecutable no existe (binario Go sin
compilar, script ausente), el spec queda en el registro pero
disabled — enrichers_for_type lo oculta del menu y jobs.cpp aborta
con mensaje claro si llega un job para uno disabled.
run_subprocess (POSIX y Windows) ramifica argv segun lang:
- go -> execv del binario directamente, sin python ni wsl.exe
- bash -> /bin/bash <run_path> (en Windows: wsl.exe -- bash ...)
- python -> python3 <run_path> (default)
El call site en jobs.cpp resuelve run_path y lang via
ge::enricher_by_id() en lugar del hardcode "run.py". Los 5 enrichers
existentes siguen funcionando sin cambios — heredan lang: python por
default.
Tests pytest (22/22 verde):
- 16 regresion: los 5 enrichers actuales siguen pasando.
- 6 nuevos en test_dispatcher_lang.py: parser default a python,
parser lee lang: bash, wire protocol identico para python y
bash, enricher Go sin binario queda disabled, enricher real
sigue funcionando tras el cambio.
NO incluye: runtime Python embebido (fase B) ni badges de lang en
la UI (fase C). El issue 0033 sigue abierto hasta cerrar las dos
fases restantes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Anade siete issues que definen el camino para hacer graph_explorer
distribuible como binario Windows autocontenido (sin WSL):
- 0032 — browser_session enrichers via Playwright (login interactivo,
cookies persistentes, fetch_webpage_browser, web_search_browser).
- 0033 — dispatcher multi-lenguaje (lang: go|python|bash en manifest)
+ runtime Python embebido en <app>/runtime/. 3 fases (A=dispatcher,
B=runtime, C=UI badges).
- 0033b — vendoring de funciones Python por enricher (_vendored/ +
.vendor.lock) para que los enrichers no dependan de registry_root
en runtime.
- 0033c — fn check vendored: drift detection con --fix.
- 0033d — fn index lee python_runtime / python_runtime_deps de app.md.
- 0033e — /compile orquesta freeze + vendor + go builds.
- 0034 — port de los 5 enrichers de sistema a Go. Reusa funciones
Go del registry directamente (no copias). Tests pytest existentes
pasan sin cambios.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
build_stdin_json enviaba ops_db_path tal cual al subprocess Python
(tipicamente "projects/<slug>/operations.db", relativo). Si el cwd
del proceso padre no era el dir del proyecto, sqlite3.connect
creaba un fichero vacio en otra ruta y el primer SELECT fallaba con
"no such table: entities".
Anade lambda absify que normaliza separadores (\\ -> /) antes de
std::filesystem::absolute (en Linux \\ es char literal del nombre,
no separador) y absolutiza ops_db, app_dir y registry_root antes
del to_wsl_path. Cubre los 5 enrichers de una sola vez.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Anade enricher web_search aplicable a nodos text/Concept/Topic. Hace
POST a html.duckduckgo.com con la query del nodo, parsea resultados
con HTMLParser stdlib, decodifica el redirect uddg= y crea N nodos
Url con relacion SEARCH_RESULT_OF apuntando al nodo origen.
Encadenable: tras web_search, fetch_webpage sobre cada Url completa
el pipeline search -> fetch -> extract.
Defensa contra ops_db_path mal resuelto: normaliza backslashes,
resuelve relativo contra app_dir, valida que la tabla entities
exista antes de tocar nada (exit codes 7/8/9 con JSON resumen).
Tests pytest (16/16 verde): conftest con operations.db temp +
schema minimo, stub de requests via PYTHONPATH para mockear red.
Cubre los 5 enrichers (extract_domain, fetch_webpage, extract_links,
extract_text_entities, web_search) + sanity check de manifests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Anade panel "Echo" — copiloto OSINT que invoca claude -p con un MCP
server propio (gx-cli) exponiendo el grafo como tools tipadas:
info, node_*, rel_*, table_*, enricher_*, query.
Cambios:
- chat.cpp/h: panel UI dockeable con history, raw stream-json toggle,
spawn de claude -p con system prompt OSINT, ChatMessage con USER/
ASSISTANT/TOOL_USE/TOOL_RESULT/SYSTEM/ERROR_MSG, escritura de
mcp.json con paths Linux para WSL en Windows.
- gx-cli: binario MCP standalone que valida cada tool, abre
operations.db en RW, escribe agent_mutations counter para que el
viewport detecte cambios en vivo.
- CMakeLists.txt: anade chat.cpp al target.
- views.h: panel_chat boolean en AppState.
- main.cpp: integracion del panel Chat (rename a Echo en menubar +
init), refresh de contexto al cambiar operations.db, drain de cola
agent_jobs tras enricher_run.
Mensajes del panel renderizan con fn_ui::selectable_text_wrapped_force
(wrap forzado + seleccion) para que URLs/JSON largos no se clippeen
y permitan copy/paste.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
El usuario reportaba "no enrichers for url" en Windows. Tres bugs:
1. resolve_registry_root tenia el fallback hardcoded a "Ubuntu" pero la
distro real era "Ubuntu-22.04". Reemplazado por detect_wsl_distro()
que sondea las distros comunes (Ubuntu, Ubuntu-24.04, Ubuntu-22.04,
Ubuntu-20.04, Debian, kali-linux, Fedora, openSUSE-Tumbleweed) y se
queda con la primera cuyo UNC tenga registry.db.
2. enrichers_load construia paths con mixed separators
("\\\\wsl.localhost\\Ubuntu-22.04\\...\\enrichers/foo/manifest.yaml")
que confunden a opendir de MinGW. Ahora normaliza todo a backslashes
en Windows antes de opendir + concatena con el separador nativo.
3. El menu "Run enricher" decia simplemente "(no enrichers para tipo X)"
sin distinguir si era 0/N (no se carga ninguno) o N>0/M (existen pero
ninguno aplica). Ahora muestra "(no enrichers cargados — revisa
FN_REGISTRY_ROOT)" vs "(0/4 enrichers para tipo 'url')".
Si el usuario tiene una distro con nombre raro, sigue pudiendo setear
FN_REGISTRY_ROOT explicitamente.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sustituye el stub Windows por la implementacion real:
C++:
- Bloque #ifdef _WIN32 con CreateProcessW + 3 anonymous pipes
(CreatePipe + SetHandleInformation), STARTF_USESTDHANDLES,
CREATE_NO_WINDOW, ReadFile/WriteFile, WaitForSingleObject con polling
para soportar cancelacion via TerminateProcess.
- Helper to_wsl_path: convierte paths Windows a WSL antes de mandarlos
al subprocess. Soporta:
* "C:\\..." -> "/mnt/c/..."
* "\\\\wsl.localhost\\<distro>\\..." -> "/..."
* "\\\\wsl$\\<distro>\\..." -> "/..."
* "/..." -> tal cual
En POSIX la funcion es no-op.
- build_stdin_json siempre normaliza ops_db_path/app_dir/cache_dir/
registry_root a paths WSL — el run.py corre dentro de WSL y solo
entiende paths /home, /mnt, etc.
- Subprocess invocacion: `wsl.exe --cd <root_wsl> -- <python_wsl> <run_wsl>`.
Asume que el usuario tiene WSL instalado y la distro Ubuntu (o ajusta
FN_REGISTRY_ROOT al UNC adecuado).
- kill_proc unificado: TerminateProcess en Win32, kill(SIGTERM) en POSIX.
- JobControl con HANDLE+pid en Win32, pid_t en POSIX.
main.cpp:
- resolve_registry_root con fallback Windows: si FN_REGISTRY_ROOT env
no esta y getcwd no encuentra registry.db (caso del .exe en Desktop),
usa "\\\\wsl.localhost\\Ubuntu\\home\\lucas\\fn_registry". El usuario
cambia el UNC via env var si su distro tiene otro nombre.
Build:
- cpp/build/windows/apps/graph_explorer/graph_explorer.exe linkea limpio
contra MinGW; solo dependencias windows.h estandar (kernel32, etc.).
- Linux smoke test sigue detectando los 4 enrichers tras la
refactorizacion compartida.
Notas operativas para el usuario Windows:
- Ejecutar el .exe desde C:\\Users\\lucas\\Desktop\\apps\\graph_explorer\\
(doble clic). El primer arranque tarda ~1 s mas por cold-start de wsl.exe.
- Si la distro no es Ubuntu, setear FN_REGISTRY_ROOT con el UNC correcto
(ej. "\\\\wsl.localhost\\Debian\\home\\lucas\\fn_registry").
- Cancelar un job en Windows usa TerminateProcess (mas brutal que SIGTERM
pero los run.py no tienen estado critico — sqlite3 rollback automatico
por la transaccion implicita).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
El sistema de jobs usa fork+exec+pipes POSIX que no existen en MinGW.
Anade un stub _WIN32 que devuelve false en jobs_init y no-op en el resto,
de forma que la app compila para Windows pero los enrichers quedan
desactivados ahi. La build Linux/WSL conserva la implementacion completa.
TODO futuro: implementacion Windows con CreateProcess + anonymous pipes
+ TerminateProcess. No urgente — el desarrollo principal es WSL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Antes: cada reload disparado por enrichers (dirty_counter) ejecutaba
graph_viewport_fit (recentraba camara), recargaba desde SQL con todos
los nodos en (0,0), aplicaba layout_circular si todo estaba en cero, y
los huerfanos quedaban apilados sobre el origen. Si physics estaba ON,
las fuerzas dispersaban todo el grafo violentamente.
Ahora:
- Auto-save de posiciones antes de cada reload — preserva lo que el
usuario ve en pantalla sin pulsar "Save layout".
- No graph_viewport_fit en reloads (solo en primera carga via
load_input(first_load=true)). La camara permanece donde estaba.
- No layout_circular en reloads (mismo guard via first_load).
- Halo placement: nodos huerfanos (en (0,0) tras layout_store_load)
se colocan junto a su primer vecino con coordenadas conocidas,
buscando slot angular libre en radios crecientes (80,140,200,280,400)
con jitter deterministico por user_data. Si no hay vecinos
colocados, se aparcan en columna lateral fuera del bbox.
- Anti-overlap garantizado a min_dist=60 px entre centros.
- Physics siempre OFF tras reload — el usuario las activa
explicitamente.
- Auto-save tambien al inicio de reload_after_mutation (mutaciones
manuales add/delete/duplicate/change_type) por consistencia.
- Refresca entity_index tras reload (los nuevos nodos creados por
enrichers tienen user_data nuevos que el indice anterior no conoce).
Tests visuales: compila limpio, jobs_init continua detectando
enrichers, smoke test del binario OK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>