- Reusa la infra de focus existente (AppState::want_focus_entity /
focus_entity_id) ya cableada en main.cpp desde 0011.
- kind=Group: single click sobre la fila pone want_focus_entity con
row.id; tooltip "Click to focus entity in viewport" en hover.
El doble click sigue funcionando (mismo efecto). El menu contextual
y el boton Promote-out-of-group quedan intactos.
- kind=Table promovida (row.promoted_entity_id no vacio): single click
pone want_focus_entity con promoted_entity_id; tooltip de focus.
- kind=Table no promovida: single click es no-op visual; tooltip
"promote first to focus\n(double click or right click to promote)"
como hint sutil. El doble click sigue lanzando el flujo de promote
(legado de 0036c) y el menu contextual ofrece Promote.
- Sin cambios en el handler de main.cpp — la logica de pan/zoom + select
+ load inspector ya existe y se reutiliza tal cual.
- Sin tests Python nuevos: el comportamiento es UI ImGui (no testeable
desde pytest). 102 passed WSL / 91+11 skipped Windows sin regresion.
Refs: issues/0036e-row-click-focus-viewport.md
NodeGroups window kind=Group ahora expone un boton SmallButton(TI_ARROW_UP)
por fila que saca la entidad del grupo (group_id = NULL) y dispara
reload del grafo. kind=Table mantiene el comportamiento de issue 0011.
- entity_ops: nueva op `entity_clear_group_id(db, id)` idempotente. Si
la columna group_id no existe (BD pre-0035a) retorna true como no-op.
Falla solo si la entidad no existe o SQLite revienta.
- views.cpp: extra columna "promote" en kind=Group, tooltip header
diferenciado por kind, boton conectado a app.want_clear_group_id_entity.
- main.cpp: handler que ejecuta entity_clear_group_id, marca windows
como dirty, llama reload_after_mutation y loguea
`[node_groups] promoted X out of group`.
- gx-cli: flag `node update --clear-group-id` (booleano) y exposicion
MCP en inputSchema + MCP_DISPATCH defaults para que el agente Echo
pueda promover via tool calls.
- tests: 3 nuevos CLI (clear, idempotente, combinable con --name) y
4 MCP (defaults, schema, dispatch end-to-end, idempotente).
WSL: 102 passed (95 base + 7).
Windows: 91 passed, 11 skipped (84 base + 7).
Refs: issues/0036d-promote-kind-aware.md
Cambia el dispatch del doble click sobre nodos del viewport: si el tipo
es Group o Table, ahora abre/enfoca la NodeGroups window correspondiente
via views_node_groups_open(...). El branch de Group ya no carga el
panel Table generico con un filtro group_id (logica heredada de 0035d
que provocaba el bug de "tabla vacia").
Limpieza correlativa en views_table:
- Eliminado el breadcrumb "Group: <name> (N)" + boton Clear filter.
- Eliminado el filtro r.group_id != table_filter_group_id en
build_visible y la restriccion de types_present.
- Eliminado el reset on-close de los campos de filtro.
Eliminados los campos AppState::table_filter_group_id y
table_filter_group_name (audit: git grep table_filter_group_id devuelve
vacio fuera de issues/).
Render de NodeGroups ahora consume focus_request: llama
SetNextWindowFocus() antes de Begin y SetWindowFocus() dentro, asi la
window queda al frente tanto al crearse como al re-enfocarse.
El right-click "Open NodeGroups" del context menu sigue intacto
(want_toggle_nodegroups + node_groups_set_expanded). El doble click es
flujo paralelo nuevo.
Refs: issues/0036c-double-click-group-opens-nodegroups.md
NodeGroupsWindowState gana un discriminador `kind` (Table | Group) y
un flag `focus_request` (lo consumira 0036c). Por defecto Table, asi
que el flujo historico (DuckDB rows tras expand de un nodo Table) no
cambia.
kind=Group lee directamente operations.db consultando
`entities WHERE group_id = container_id` con columnas fijas
(id, name, type_ref, status, updated_at) ordenadas por updated_at DESC.
Los nuevos loaders viven en node_groups.cpp:
- node_groups_count_for_group -> SELECT count(*) ...
- node_groups_page_for_group -> SELECT id,name,type_ref,status,
updated_at ... LIMIT ? OFFSET ?
Para columnas, opcion (A) del issue: pre-popular meta.columns con la
lista fija al abrir kind=Group, asi el render se mantiene generico.
NodeGroupsRow.values guarda los 5 campos en ese orden y row.id es la
key natural (= entity_id de la fila — al ser ya entidad, no hace falta
promocionarla).
Render en views.cpp ramifica por kind:
- Table: layout original [id_col + columns + promoted] con doble
click -> promote/focus.
- Group: layout [columns fijas] sin promoted. Doble click sobre la
fila ya pone want_focus_entity = id (los flujos posteriores 0036c-e
afinan UX). Right click ofrece "Focus in Inspector".
main.cpp dispatcha por kind al refrescar paginas y, al cerrar via X,
solo llama a node_groups_set_expanded para kind=Table (Group no usa
ese flag).
views_node_groups_windows_sync se hace kind-aware: solo reconcilia
entries kind=Table contra el set de Tables expandidas; no toca las
entries kind=Group (las gestiona views_node_groups_open).
Nueva API publica:
views_node_groups_open(app, container_id, kind, ops_db)
Crea o reusa la entry, setea focus_request=true y para kind=Group
pre-popula meta.columns + intenta leer `name` del Group para el
titulo. Sin caller todavia — la consume 0036c.
Tests:
- tests/test_node_groups_loader.py (6 tests) verifica el contrato
SQL via gx-cli. Nuevo subcomando `gx-cli group page <id>` espejea
el loader C++ exactamente (mismo SQL); tambien expuesto como tool
MCP `group_page` para que Echo pueda inspeccionar Groups.
Resultado:
- WSL: 89 -> 95 passed
- Windows: 78+11 -> 84+11 passed
- Build C++ Windows limpio, sin warnings nuevos.
- Regresion kind=Table: comportamiento identico (mismo render,
mismo loader DuckDB).
Refs: issues/0036b-kind-discriminator-and-group-loader.md
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).