- 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>
Infra para correr enrichers en background mientras la app sigue interactiva.
C++:
- jobs.{h,cpp}: tabla jobs en graph_explorer.db, JobRunner con N=2 std::thread
workers, fork+exec POSIX con pipes, parser de PROGRESS:<float> <stage> en
stderr, captura de stdout JSON, persistencia + dirty_counter.
- enrichers.{h,cpp}: scanner de enrichers/<id>/manifest.yaml, parser YAML
minimo (id/name/description/applies_to), filtro por tipo de nodo.
- views_jobs.cpp: panel "Jobs" dockeable con tabla (status/enricher/target/
progress/time), filtro all/active/done/errors, cancelar/borrar inline.
Wiring:
- main.cpp: resolve_registry_root() (FN_REGISTRY_ROOT env o subir desde cwd
buscando registry.db), jobs_init/enrichers_load antes de fn::run_app,
jobs_shutdown al cerrar, dirty_counter -> want_reload, jobs_set_ops_db al
cambiar de proyecto.
- main.cpp:render_context_menu: menu "Run enricher" sustituye placeholder
con submenu filtrado por type_ref via enrichers_for_type. Submit abre
panel Jobs auto.
- views.h: AppState::panel_jobs flag + decl views_jobs().
- CMakeLists.txt: anade jobs.cpp + enrichers.cpp + views_jobs.cpp y enlaza
Threads::Threads.
Wire protocol enricher (subprocess Python):
- stdin: JSON con node_id, metadata, ops_db_path, app_dir, cache_dir,
registry_root, params.
- stderr: PROGRESS:<float> <stage> + LOG lineas libres.
- stdout: JSON resumen al final.
- exit 0 = ok, !=0 = error con stderr capturado en panel Jobs.
El run.py escribe directamente al operations.db (sqlite3 stdlib) — C++ solo
orquesta, no parsea entities/relations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- examples/types.yaml: nuevo tipo Webpage (icono ti-file-text, fields
url/title/status_code/content_type/fetched_at/html_path/markdown_path/
screenshot_path/text_length/lang). Url queda como link suelto.
- types_registry.cpp: anade ti-file-text al mapa de codepoints Tabler.
- .gitignore: cache/, graph_explorer.db (jobs+layouts), build artifacts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tras feedback en uso, 32 px era excesivo y dominaba el viewport. 8 px
mantiene la diferencia visual frente a los nodos circulo (4 px) sin
pisar el grafo.
Hipotesis del bug 'tras promover, la tabla expandida queda a 0 filas':
en Windows std::filesystem::path::string() devuelve la ruta con
backslashes ('C:\\Users\\...\\operations.db'). Al embebirla en
'ATTACH ''<path>'' AS ops' DuckDB la interpretaba con quirks segun
version, fallaba el ATTACH (silent), pero ademas el siguiente
duckdb_open con paths mixtos podria no abrir el .duckdb correcto.
Cambios:
- tableview_resolve_path normaliza '\\' -> '/' (DuckDB acepta ambos
para duckdb_open, pero forzamos '/' para evitar ambiguedad en SQL).
- ATTACH normaliza ops_db tambien.
- TableWindowState.last_error: cuando count o page fallan, se setea
con el path/tabla involucrada y se muestra en rojo en la cabecera
de la ventana. Asi el bug es visible sin abrir consola.
- tableview_page log incluye la SQL completa cuando falla — facil
diagnosticar via stderr en linux.
Cambios de UX en la toolbar y arranque:
- Boton 'Layout: <name>' que abre popup con la lista de layouts (force,
grid, circular, radial, hierarchical, fixed) + 'Reset positions
(unpin + restart)' + 'Save current layout'. Reemplaza el combo
pequeno + los botones Save/Reset que estaban dispersos.
- Boton 'Physics: ON/OFF' (Player Play/Pause) toggle visible que
reemplaza el checkbox 'Run layout'. Variant Primary cuando ON,
Subtle cuando OFF.
- Default: layout_mode = 5 (fixed) y layout_running = false. Asi al
abrir un proyecto los nodos respetan posiciones guardadas y no se
mueven solos. El usuario activa fisicas con el boton Physics y/o
cambia el layout desde el dropdown si quiere.
Reset layout (boton dentro del popup Layout) sigue activando physics
para que el grafo se reasiente; es el flujo natural del 'Reset'.
reload_after_mutation reconstruye g_graph.types[] con defaults via
reload_graph, pero NO reaplica el types.yaml ni reconstruye el icon
atlas. Resultado: cualquier mutacion (add/delete/duplicate/change_type/
promote/demote/import) hacia que los tipos perdiesen shape/color/icon
y todos los nodos volvieran a renderizarse como circulos grises.
Caso reproducible: doble-click en fila de tabla expandida -> promote
-> reload -> el nodo Table dejaba de ser cuadrado y se renderizaba
como circulo.
Fix: tras reload_graph + entity_index_build, si parsed_types tiene
contenido, reaplicar types.yaml y reconstruir el atlas con un
graph_icons_destroy + build_icon_atlas + g_atlas_bound=false +
g_gpu_dirty=true para que el viewport rebincie en el siguiente frame.
Tres ajustes derivados de feedback en uso:
1. tableview_promote_row recibe ahora `table_entity_id` y, si no es
nulo, inserta una relacion 'CONTAINS_ROW' (id estable, INSERT OR
IGNORE) entre la tabla origen y la entidad promovida. El viewport
pinta la arista de pertenencia automaticamente sin codigo extra.
2. apply_types_yaml fija default_size = 32 px (world) para tipos
Table junto al SHAPE_SQUARE ya existente. La GPU pinta el cuadrado
real; antes era invisible bajo el overlay rectangular.
3. views_table_overlay adelgaza al rol que le toca: solo dibuja un
contador discreto "<N> rows" debajo del cuadrado (texto pequeno
con bg semitransparente). El cuadrado en si lo pinta el GPU.
Defensiva: views_table_windows_sync marca page_dirty=true en TODAS las
windows live tras cada sync para que el flag promoted se refresque
inmediatamente despues de promote/demote/import.
Tres cambios pequenos relacionados con la UX de las tablas:
1. fix views_table_window: la fila usaba TextUnformatted en col 0 que
no registra hover/double-click sobre toda la fila. Reemplazado por
ImGui::Selectable con SpanAllColumns + AllowDoubleClick — ahora el
doble-click sobre fila no promovida promueve, sobre promovida abre
Inspector. El popup right-click tambien funciona ahora.
2. Toolbar 'Tables (N)' dropdown que lista las Table windows abiertas
con checkbox. Desmarcar = colapsar (cerrar ventana + expanded=false).
Tambien tiene 'Collapse all' al final.
3. views_table (issue 0004) — filtros por columna:
- Right-click sobre header de columna abre popup con InputText.
- Apply / Clear / Enter aceptan y guardan en table_col_filters.
- Chips arriba de la tabla con cada filtro activo + X para quitar.
- Boton 'Clear all'.
- build_visible aplica los filtros con substring case-insensitive.
json_extract(metadata,'\$.expanded') devuelve INTEGER 1 cuando el valor JSON
es true; json('true') devuelve TEXT 'true', asi que la comparacion era
1 = 'true' = 0 (falso) y views_table_windows_sync nunca encontraba las
Tables expandidas.
El bug se manifestaba como context menu Expand table sin abrir la ventana,
aunque tableview_set_expanded persistia correctamente el flag en la BD.
Fix: comparar contra 1 directamente.