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>
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>