- 0050: jupyter_exec reescrito sin Y.js (REST + KernelClient). Bug raíz adicional: HEAD /api/contents da 405 → cambiado a GET. 9 tests (5 unit + 4 e2e).
- 0052: footprint_aurgi cerrado. Bug fix en setup_geo_stack_docker_pipeline (verify aborta si compose up falla; nombre de contenedor incorrecto).
- Nueva primitiva docker_container_running_py_infra (7 tests).
- /full-git-push y /full-git-pull pasan a modo automático: auto-commit + push sin preguntar, aborta solo si detecta secrets.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tamano FIJO del popup (Always + SizeConstraints) y flags NoResize/NoMove
para evitar feedback loop entre auto-resize del popup y TextWrapped/SameLine
internos. Reemplaza GetWindowContentRegionMax() por offsets explicitos
calculados a partir del ancho fijo, ya que ese valor fluctua frame a frame
con padding/borders y provocaba el ensanche/encogido continuo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 helpers puros para construir KPIs con display=smartscalar y comparacion
vs n-1 sin que Metabase v0.59 pida breakout temporal. Replican el patron
del dashboard Informe Lean (UNION ALL de 2 filas periodo/valor) y rellenan
la firma exacta de template-tags que el frontend MBQL5 acepta.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
graph_explorer.exe ya escribe en local_files/ via fn::local_path() segun la convencion de cpp_apps.md §7, asi que los archivos /graph_explorer.{db,ini,...} ya no aparecen en la raiz del registry. Eliminados los 4 archivos remanentes del 1 de mayo y las 6 lineas correspondientes del .gitignore que ya no protegen contra nada.
GLiREL declara proxies/resume_download como required-keyword en
_from_pretrained, pero huggingface_hub 1.x dejo de pasarlos en su
from_pretrained. Aplicamos un classmethod monkey-patch idempotente
que inyecta valores neutros si faltan. Verificado contra glirel==1.2.1
y huggingface_hub==1.13.0 con jackboyla/glirel-large-v0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Permite -D_GE_DIR=<path> y -D_DASH_DIR=<path> via cmake para apuntar
estas apps externas a un worktree aislado. Sin override, comportamiento
identico al previo. Habilita parallel-fix-issues sobre apps C++ cuyo
binario sale del arbol cpp/ pero cuyo source vive en projects/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
parallel_for_cpp_core: ThreadPool reutilizable con parallel_for(begin, end, fn)
y parallel_for_chunks(begin, end, fn(tid, lo, hi)). Captura excepciones del
worker y las relanza en el caller. Pareja CPU del despacho GPU para Monte
Carlo multi-core cuando dispatch GPU no compensa.
slider_cpp_core: wrapper de ImGui::SliderFloat/Int/Double con label muted
arriba, tokens (primary grab), full-width. Variantes float, float_log
(logaritmico), int, double. Para los calculadores que tienen 15-30 sliders
cada uno y se beneficia del estilo consistente.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tres kernels Monte Carlo intensivos sobre las primitivas G1-G7 + las puras
CPU como oraculo de tests numericos. Apuntados a hyper-paralelizar los
calculadores de sources/calculadoras (vr_tiered_lab, mcmc-bayes / full / lab,
mcmc-visualizer) en RTX-class GPUs.
- mc_session_sim_gpu (K1): N sesiones independientes de K spins en paralelo
(1 thread = 1 sesion). Modelo variable-ratio escalonado con tiers (q, m),
modes Pure/Pity/Streak, miss_streak, drawdown. SSBOs summary[N*8] y
tier_counts[N*max_tiers]. Portea vr_tiered_lab.
- mc_metropolis_hastings_gpu (K2): M cadenas independientes 1D. Target
log-pdf inyectable como string GLSL (mismo patron de gl_shader). u_user[16]
para cambiar parametros desde sliders sin recompilar. Output compatible
con rhat_split / ess_basic.
- mc_random_walk_2d_gpu (K3): walkers 2D MH con trace_xy xy-interleaved en
SSBO; pasable directamente a gpu_histogram_2d sin readback intermedio.
Pipeline GPU-only para mcmc-visualizer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nuevo dominio cpp/functions/datascience con primitivas puras CPU para post-
proceso de samples Monte Carlo y diagnostico de cadenas MCMC. Diseñadas como
gemelas CPU de los kernels GPU (rng pareja con gpu_rng_glsl, MH 1D/ND con
mc_metropolis_hastings_gpu) para validar numericamente y para datasets
pequeños donde el dispatch GPU no compensa.
- rng: xoshiro256++ con uniform / normal (Box-Muller) / below (Lemire) /
categorical. Determinista bit-exacto dado seed.
- stats_summary: sum (Kahan), mean, var/std (Welford one-pass), min, max,
quantile / percentile (R type-7).
- autocorr: r(k), ACF, tau_int (Sokal) — diagnostico ACF y ESS.
- rhat_ess: Gelman-Rubin clasico y split + ESS basico (multi-chain).
- beta_dist: lgamma (Lanczos), beta_pdf, beta_cdf (continued fraction),
beta_quantile, mean/var/std — para inferencia Beta-Binomial.
- drawdown: max_dd absoluto/pct + underwater series para sesiones
simuladas y backtests.
- samples_to_grid_2d: binning 2D CPU para alimentar heatmap_cpp_viz /
contour_cpp_viz desde samples (x[], y[]).
- metropolis_hastings: MH 1D y ND con target log-pdf como std::function
(no normalizada).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stack base de compute shaders OpenGL 4.3 para cargas Monte Carlo intensivas
en GPU. Reutiliza el patron de graph_force_layout_gpu (SSBO + compute) y se
integra con el resto del registry sin nuevos simbolos en gl_loader (todo lo
que se necesita ya estaba expuesto).
- gpu_ssbo: lifecycle de Shader Storage Buffer Objects.
- gpu_compute_program: compila compute GLSL 4.3 con preamble inyectable
(mismo pattern de gl_shader::compile_fragment).
- gpu_dispatch: dispatch_1d/2d/3d con ceil(N/local) automatico + barrier
helpers (storage, uniform, image, buffer_update, all).
- gpu_rng_glsl: PCG32 GLSL (uniform/normal/below) + SplitMix64 seed walkers
para sembrar deterministicamente N walkers desde un master seed.
- gpu_histogram_1d: SSBO float[N] -> uint[nbins] via atomicAdd.
- gpu_histogram_2d: SSBO float[2N] xy-interleaved -> uint[nx*ny] +
to_density helper para alimentar heatmap_cpp_viz.
- gpu_reduce: workgroup-shared sum/min/max/mean (local 256, partials CPU).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
functions/core/html_to_markdown: convierte HTML a Markdown limpio (golang-only
sin dependencias externas). util como prep para LLMs y para indexar contenido
web.
functions/cybersecurity/extract_iocs + types/cybersecurity/ioc: extrae
indicators of compromise (IPs, domains, URLs, hashes, emails, CVEs,
crypto wallets) de texto libre. Devuelve []IOC tipado.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Necesario para que las funciones GPU compute (gpu_histogram_1d/2d, gpu_reduce,
mc_*_gpu) puedan setear uniforms uint en Windows. En Linux ya estaba
disponible via GL_GLEXT_PROTOTYPES.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
registry/indexer.go ahora escanea <lang>/apps/*/app.md ademas de apps/ y
projects/*/apps/. cpp/apps/chart_demo y cpp/apps/shaders_lab pasan a estar
en registry.db con sus manifests.
Infraestructura de tests e2e (opt-in con -DFN_BUILD_TESTS=ON):
- vendor de Dear ImGui Test Engine (personal/open-source license).
- chart_demo_tests target con tests/chart_demo_tests.cpp.
- /e2e-cpp slash command para crear y ejecutar tests e2e.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cuando una funcion del registry parte su .cpp en varios TUs por testabilidad
o separacion ImGui-vs-puro, cada TU adicional se registra como entrada propia
con su .md en lugar de extender file_path para listar varios archivos.
Aplicado a:
- graph_labels_select_cpp_viz: helpers puros (compute_degrees + labels_select).
- graph_viewport_selection_cpp_viz: clear/add/toggle/is_selected puros.
- graph_types_cpp_viz: TU de update_bounds + find_node_by_user_data.
graph_labels y graph_viewport actualizados para declarar las nuevas entradas
en uses_functions. Razon detallada en docs/adr/0003 + regla actualizada en
.claude/rules/uses_functions.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Logger global thread-safe con ring buffer in-memory de 2000 entradas + escritura
opcional a archivo. log_window flotante consume el ring buffer con filtros por
nivel, busqueda y autoscroll; se abre desde Settings -> Logs en la menubar.
selectable_text cubre el patron drag-to-select + Ctrl+C en cualquier ventana.
app_menubar y framework run_app integran log_window_render() en el frame loop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refina la convencion de layout: el top de cada app distribuible
solo lleva el .exe + DLLs nativas; todo lo demas (TTFs, enrichers,
runtime Python, MCP servers) vive en <exe_dir>/assets/.
Cambios:
- cpp/CMakeLists.txt::add_imgui_app — copia las 5 TTFs (Karla,
Roboto, DroidSans, Cousine, tabler-icons) a
$<TARGET_FILE_DIR>/assets/ en lugar de junto al exe.
- framework/app_base: nuevas funciones fn::asset_dir() y
fn::asset_path(name) que resuelven a <exe_dir>/assets/<name>.
- functions/core/icon_font.cpp::find_asset — anade
fn::asset_path(filename) como PRIMERA ruta de busqueda, antes
de las legacy ./<file> y ./assets/<file>. Mantiene los
fallbacks para dev (FN_ASSETS_DIR, FN_CPP_ROOT).
- .claude/commands/compile.md — el deploy a Desktop pone TTFs +
enrichers/ + runtime/ + gx-cli en <DEST>/assets/. Solo .exe y
DLLs nativas (duckdb.dll) quedan en el top. local_files/ se
preserva si existe.
Layout final:
Desktop/apps/<APP>/
├── <APP>.exe + *.dll (binario + DLLs Windows)
├── assets/ (read-only distribuible)
│ ├── *.ttf, enrichers/, runtime/, gx-cli, ...
└── local_files/ (per-PC, creado al primer arranque)
Esto cierra la separacion conceptual de la convencion: la carpeta
es trivial de zippear (solo .exe + assets/), el reset/sync es
trivial (local_files/), y todas las apps del registry adoptan el
mismo layout via fn_framework.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Actualiza /compile para que el deploy a Desktop/apps/<app>/ siga la
convencion local_files/ del framework:
- Copia .exe + ttfs + dlls junto al exe (read-only).
- Copia <app_dir>/enrichers/ si existe (excluyendo pycache).
- Copia <app_dir>/runtime/ si app.md declara python_runtime: true.
Regenera el runtime via tools/freeze_python_runtime.sh windows
cuando app.md es mas nuevo que runtime/.lock.
- NUNCA toca local_files/ del destino — contiene estado del
usuario (DBs, ini, proyectos) que NO se debe perder al
recompilar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Toda app C++ basada en fn::run_app coloca sus archivos escribibles
bajo <exe_dir>/local_files/. Los distribuibles (.exe, dlls, ttfs,
enrichers/, runtime/) siguen junto al .exe. Esto deja la carpeta
distribuible limpia para zippear y separa con claridad lo que
viaja con la app de lo que el PC genera.
API publica en fn:: (cpp/framework/app_base.h):
- exe_dir() directorio del ejecutable
- local_dir() <exe_dir>/local_files/, creado on-demand
- local_path(name) <local_dir>/<name>
- migrate_to_local_files(...) mueve archivos viejos desde cwd/exe_dir
Cambios:
- run_app configura io.IniFilename = local_path("imgui.ini") y
llama migrate_to_local_files(["imgui.ini","app_settings.ini"])
antes de settings_load(). Migracion idempotente para PCs con
instalacion previa.
- app_settings.cpp usa local_path("app_settings.ini") en lugar de
hardcoded "app_settings.ini" relativo al cwd.
- cpp_apps.md §7 documenta la convencion como obligatoria. Las
apps deben usar fn::local_path() para cualquier archivo
escribible nuevo.
Beneficios:
- zip distribuible no se "ensucia" con .ini/.db generados al usar.
- reset trivial: borrar local_files/.
- backup/sync per-PC: solo local_files/ es propio del PC.
- elimina la mezcla de paths Linux/Windows que generaba bugs como
"projects\\default\\operations.db" en builds cross-platform.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Actualiza /full-git-pull para reflejar la realidad operativa: cada PC
mantiene solo el subset de sub-repos que necesita, segun la memoria
"Gitea = fuente de verdad; PCs subset".
Cambios:
- Quita la segunda pasada que clonaba automaticamente todos los
dataforge/<name> registrados en apps/analysis. Generaba clones no
deseados en PCs que no usan esas apps.
- Anade nota explicita de que el comando solo actualiza repos con
.git/ ya presente y deja el clone manual como pull-on-demand.
- Documenta el snippet de clone manual con token via pass para
cuando si haga falta traer un sub-repo nuevo.
Impacto: el comando es idempotente y predecible — no toca lo que no
existe localmente. No afecta a fn sync ni a la regeneracion de
registry.db.
Pipeline en cascada que combina extract_iocs (regex, coste 0), GLiNER
(zero-shot NER), GLiREL (zero-shot RE) y un fallback LLM opcional para
chunks con baja confianza o pocas entidades. Devuelve listas concatenadas
listas para deduplicate_entities/deduplicate_relations.
Cierra 0040.
- Move dev/issues/0038-gliner-entity-extractor.md a completed/
- Update README link y estado a completado
Closes#0038
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 tests sin necesidad de descargar el modelo (200 MB):
- StubModel duck-typed que valida el contrato de predict_entities
- Threshold y flat_ner se propagan al modelo
- Schema vacio lanza ValueError; schema sin labels validos warning + []
- Excepcion del modelo se captura
- Label desconocido se descarta
- gliner_load_model: ImportError simulado, cache hit, _resolve_device
auto cae a cpu si torch no esta presente
Refs #0038
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Funciones nuevas en python/functions/datascience/:
- gliner_load_model: carga + cachea modelo GLiNER por (name, device).
device='auto' resuelve a cuda/cpu segun torch.cuda.is_available, sin
fallar si torch no esta instalado. ImportError claro si falta gliner.
- extract_entities_gliner: contrato drop-in de extract_entities_llm
(mismo entity_schema, mismo list[EntityCandidate]). El caller inyecta
el modelo (cargado UNA vez por proceso). Anota offsets start/end en
attributes para reconciliar con extract_iocs (issue 0040).
Diferencias vs LLM extractor:
- 50-200x mas rapido en GPU, 0 USD/token.
- Malo con IoCs tecnicos (lo cubre 0037).
- Threshold y flat_ner ajustables por dominio.
pyproject.toml: gliner como extra opcional `[nlp]` para no inflar el
.venv de quien no use NER. Instalacion: `uv pip install -e '.[nlp]'`.
Refs #0038 — Desbloquea 0039 (GLiREL) y 0040 (pipeline hibrido).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Move dev/issues/0037-ioc-regex-extractor.md a completed/
- Update README link y estado a completado
- Limpiar duplicado obsoleto de 0042 (ya estaba en completed/)
Closes#0037
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
30 tests cubriendo positivos y negativos por tipo:
- IPv4 valida/invalida + rangos limite
- IPv6 forma completa/comprimida
- Emails (caracteres validos en local part)
- Dominios con TLD valido vs desconocido
- Hashes MD5/SHA1/SHA256/SHA512 por longitud
- Wallets BTC legacy/bech32 y ETH
- CVEs 4 y 7 digitos
- MAC con `:` y `-` (separadores mezclados rechazados)
- Telefonos E.164 y ES local 9 digitos
- Pipeline filtrado por types y deduplicacion de spans contenidos
Refs #0037
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extractores nuevos en python/functions/cybersecurity/:
- extract_ip_addresses (IPv4 + IPv6 con validacion ipaddress)
- extract_emails (RFC 5322 simplificado)
- extract_domains (FQDNs con TLD valido, lista estatica)
- extract_file_hashes (MD5/SHA1/SHA256/SHA512, algoritmo por longitud)
- extract_crypto_wallets (BTC legacy + bech32, ETH 0x+40hex)
- extract_cve_ids (CVE-YYYY-NNNN+)
- extract_mac_addresses (xx:xx:xx + xx-xx-xx, separador uniforme)
- extract_phone_numbers (E.164 + ES local 9 digitos)
Pipeline:
- extract_iocs corre todos, deduplica spans contenidos. Mantiene
purity:pure (kind:function con uses_functions no vacio) porque la
regla del registry exige que los pipelines sean impuros.
Todas devuelven list[dict] con value/start/end/type para que el
caller (issues 0038-0040) pueda reconciliar offsets con spans NER
sin reparsing.
Refs #0037
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
17 casos: helpers de tokenizacion/mapeo, schema basico con head_pos/tail_pos,
fallback por head_text, threshold, max_pairs, self-loops, ImportError, cache,
device='auto'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- glirel_load_model: cache por (model_name, device); device='auto' resuelve via torch
- extract_relations_glirel: tokeniza por whitespace, mapea spans char->token,
llama predict_relations y devuelve RelationCandidate; fallback text.find si la
entidad llega sin offsets; max_pairs=N -> top-N por score
- pyproject.toml: glirel en extra nlp
Closes#0039
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Move dev/issues/0038-gliner-entity-extractor.md a completed/
- Update README link y estado a completado
Closes#0038
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 tests sin necesidad de descargar el modelo (200 MB):
- StubModel duck-typed que valida el contrato de predict_entities
- Threshold y flat_ner se propagan al modelo
- Schema vacio lanza ValueError; schema sin labels validos warning + []
- Excepcion del modelo se captura
- Label desconocido se descarta
- gliner_load_model: ImportError simulado, cache hit, _resolve_device
auto cae a cpu si torch no esta presente
Refs #0038
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Funciones nuevas en python/functions/datascience/:
- gliner_load_model: carga + cachea modelo GLiNER por (name, device).
device='auto' resuelve a cuda/cpu segun torch.cuda.is_available, sin
fallar si torch no esta instalado. ImportError claro si falta gliner.
- extract_entities_gliner: contrato drop-in de extract_entities_llm
(mismo entity_schema, mismo list[EntityCandidate]). El caller inyecta
el modelo (cargado UNA vez por proceso). Anota offsets start/end en
attributes para reconciliar con extract_iocs (issue 0040).
Diferencias vs LLM extractor:
- 50-200x mas rapido en GPU, 0 USD/token.
- Malo con IoCs tecnicos (lo cubre 0037).
- Threshold y flat_ner ajustables por dominio.
pyproject.toml: gliner como extra opcional `[nlp]` para no inflar el
.venv de quien no use NER. Instalacion: `uv pip install -e '.[nlp]'`.
Refs #0038 — Desbloquea 0039 (GLiREL) y 0040 (pipeline hibrido).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Move dev/issues/0037-ioc-regex-extractor.md a completed/
- Update README link y estado a completado
- Limpiar duplicado obsoleto de 0042 (ya estaba en completed/)
Closes#0037
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
30 tests cubriendo positivos y negativos por tipo:
- IPv4 valida/invalida + rangos limite
- IPv6 forma completa/comprimida
- Emails (caracteres validos en local part)
- Dominios con TLD valido vs desconocido
- Hashes MD5/SHA1/SHA256/SHA512 por longitud
- Wallets BTC legacy/bech32 y ETH
- CVEs 4 y 7 digitos
- MAC con `:` y `-` (separadores mezclados rechazados)
- Telefonos E.164 y ES local 9 digitos
- Pipeline filtrado por types y deduplicacion de spans contenidos
Refs #0037
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extractores nuevos en python/functions/cybersecurity/:
- extract_ip_addresses (IPv4 + IPv6 con validacion ipaddress)
- extract_emails (RFC 5322 simplificado)
- extract_domains (FQDNs con TLD valido, lista estatica)
- extract_file_hashes (MD5/SHA1/SHA256/SHA512, algoritmo por longitud)
- extract_crypto_wallets (BTC legacy + bech32, ETH 0x+40hex)
- extract_cve_ids (CVE-YYYY-NNNN+)
- extract_mac_addresses (xx:xx:xx + xx-xx-xx, separador uniforme)
- extract_phone_numbers (E.164 + ES local 9 digitos)
Pipeline:
- extract_iocs corre todos, deduplica spans contenidos. Mantiene
purity:pure (kind:function con uses_functions no vacio) porque la
regla del registry exige que los pipelines sean impuros.
Todas devuelven list[dict] con value/start/end/type para que el
caller (issues 0038-0040) pueda reconciliar offsets con spans NER
sin reparsing.
Refs #0037
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Compila la app actual (cpp/apps/<X>/ o projects/*/apps/<X>/) para Windows
via MinGW y la copia al escritorio: /mnt/c/Users/lucas/Desktop/apps/<app>/.
Detecta target Android si aparece (hoy ninguna app la tiene).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- cpp/CMakeLists.txt: register projects/osint_graph/apps/graph_explorer/
via add_subdirectory pattern (igual que registry_dashboard).
- dev/feature_flags.json: osint_graph_v1 = true (enabled_at 2026-04-30).
- dev/issues/{0049,0049k} → dev/issues/completed/. README index actualizado.
La app vive en su sub-repo dataforge/graph_explorer (push hecho al cerrar).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
graph_labels_draw pinta etiquetas de nodos sobre el FBO del graph_renderer
via ImDrawList. Politica configurable: always-on para selected/hovered/
pinned, top-N por size*(degree+1), culling por viewport AABB y
min_node_pixel_size. Cap duro = max_visible + |always_*|.
API:
- graph_labels_draw(graph, viewport_state, policy, cb, user)
- graph_labels_draw_at(...) — variante con rect explicito
- graph_labels_select(...) — helper puro testeable
- graph_compute_degrees(...) — O(E)
Splitting en dos TUs:
- graph_labels.cpp — funciones draw (depende de ImGui)
- graph_labels_select.cpp — helpers puros para tests sin ImGui
12 tests en test_graph_labels (culling, max_visible cap, min_pixel_size,
always_* gating por viewport, top-N por score, edge cases). Todos verdes.
Integrado en demos_graph con UI: toggle Labels, sliders Max visible /
Font / Min px, checkboxes Selected/Hovered/Pinned. Golden de
graph_viewport regenerado.
Cierra issue 0049j.