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.
- TableMetadata struct + tableview_get_metadata: lee la metadata de un
nodo Table (path, table, row_type, columns, label_column, expanded...).
- tableview_set_expanded: persiste el flag expanded usando json_set.
- tableview_set_columns: sobrescribe metadata.columns.
- tableview_promote_row: idempotente — si ya existe entidad con
metadata.source.row_id == row_id la devuelve; si no, lee fila completa
desde DuckDB e inserta entity con id 'prom_<type>_<row_id>' y metadata
incluyendo source + columnas.
- tableview_demote_row: DELETE FROM entities (la fila DuckDB no se toca).
- tableview_ingest_file: CREATE TABLE AS SELECT * FROM read_csv_auto/
read_parquet/read_json_auto segun extension del input.
- tableview_list_columns: SELECT * FROM tabla LIMIT 0 -> nombres.
0010 cambia de modelo SQLite CONTAINS_ROW a tier DuckDB:
operations.db sigue con grafo + filas promovidas, tablas grandes
viven en projects/<proj>/apps/graph_explorer/tables/<slug>.duckdb.
0011 separa la fase 2 (UI expandida + promote/demote + ingesta CSV).
- entity_ops: entity_list_rows (bulk pull id/name/type_ref/status/updated_at).
- AppState::TableRow + cache + filtros (search substring + show_all toggle).
- views_table: tabs por type_ref (alfabetico) o tabla unica con todos los
tipos. ImGui::BeginTable con sort + clipper para >10k filas. Click en
Selectable selecciona el nodo en el viewport (clear + add via
graph_viewport_*).
- views_table_refresh_indices: degree + node_idx por user_data hash.
- main.cpp: panel "Table" en g_panels; cache build tras load_input y
reload_after_mutation.
- views_type_editor: panel "Types" con tabs Entities/Relations.
Entities: name, color picker, shape combo, icon (ti-* + cp preview),
principal_field combo, tabla de Fields (string/int/float/bool/date/url/enum)
con required y enum values CSV; up/down/X por fila.
Relations: name, color, style.
Footer Save / Reload from disk + indicador dirty + error inline.
- views_type_editor_delete_modal: confirm con conteo de entidades en uso.
- types_registry: shape_name() + shape: emit en types_save_yaml para
round-trip estable de la cosmetica editada en UI.
- main.cpp: panel "Types" en g_panels; init types_draft tras load_input;
want_types_save -> save + apply_types_yaml + rebuild atlas + bind +
refresh inspector caches; want_types_reload simetrico; conteo de
uso desde operations.db cuando se abre el modal de delete.
Editar name/type/description/status, fields tipados (string/int/float/
bool/date/url/enum) renderizados desde el schema del tipo, extras
key-value libres, tags como chips con autocomplete por la BD.
Save persiste con un solo UPDATE y dispara reload del grafo.
Cierra issue 0008.
Issue 0008:
- types.yaml: stash ParsedTypes en g_app.parsed_types tras parsear (lo
consume el Inspector para resolver schemas por type_ref).
- load_input: tras load, llama views_inspector_clear_draft + _refresh_caches.
- switch_to_project: limpia draft y parsed_types antes de cargar nuevo.
- render(): seleccion → load_draft (si single sel y no dirty), Save →
entity_update + reload_graph + relocaliza node_idx por sql_id +
re-load draft + reselecciona en viewport, Discard → re-load draft.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issue 0008 — refactor del panel Inspector de read-only a editable.
views.h:
- AppState gana ParsedTypes parsed_types (schema vivo del proyecto), draft
del Inspector (insp_*: name/type/desc/status buffers, field_keys/values
paralelas, is_extra mask, tags vector, dirty flag), y dos triggers
(want_inspector_save, want_inspector_discard).
- Helpers expuestos: views_inspector_clear_draft, _refresh_caches,
_load_draft, _build_record.
views.cpp:
- views_inspector_load_draft: entity_load_full → buffers; campos del
schema primero (orden del EntitySpec), extras detras.
- views_inspector_build_record: reconstruye EntityRecord respetando el
schema para decidir is_string de cada campo (FK_BOOL → 'true'/'false',
FK_INT/FLOAT → literal, resto → string). Extras siempre string.
- views_inspector: render por bloques:
* Identity: name, type combo (lista del proyecto + tipos del grafo),
status combo, description multiline.
* Fields del schema: render por kind (string→InputText con hint,
int→InputInt, float→InputDouble, bool→Checkbox, date→InputText
con hint YYYY-MM-DD, url→InputText + boton Open en navegador,
enum→Combo con values). Required marcado con '*'.
* Extras: lista key-value con boton trash por fila + 'Add' al final.
* Tags: chips clickables (click = quitar) + input con autocomplete
(lista compacta de tags distintas en BD).
* Footer: Save/Discard/Open notes + label '(modified)' si dirty.
* Neighbors read-only (igual que antes).
- Si el draft no esta sincronizado con la seleccion actual y NO hay
cambios pendientes, el inspector muestra 'Cargando...' (main.cpp
carga). Si hay dirty, banner 'Save/Discard primero' bloqueando.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issue 0008 — capa de datos para el Inspector editable:
- struct MetadataField {key, value_str, is_string} — pares de la
columna metadata. is_string distingue '"foo"' de literal (number,
bool). EntityRecord agrupa los campos editables (id, name, type_ref,
description, status, tags[], metadata[]).
- entity_load_full: SELECT name/type/desc/status/tags/metadata, parsea
JSON plano con un parser propio (evita arrastrar libs). Soporta
escapes basicos (\n \t \" \\\\ etc.; \uXXXX → '?').
- entity_update: un solo UPDATE con tags+metadata serializados a JSON.
Toca updated_at.
- entity_list_distinct_tags: usa json_each (SQLITE_ENABLE_JSON1) para
enumerar tags distintas — autocomplete del Inspector.
- Parser JSON plano: parse_string_array, parse_flat_object. Solo
objetos planos (sin nested objects/arrays excepto consumirlos como
literal). Suficiente para el caso del Inspector.
- Writer JSON: build_string_array, build_flat_object con escape
apropiado. Si is_string=false pero el valor no es literal valido,
se re-emite como string para no producir JSON invalido.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Parser+writer para fields (string/int/float/bool/date/url/enum),
principal_field y round-trip estable. Semilla con 44 fields en
11 tipos. CLI --test-types-yaml para verificar round-trip.
Cierra issue 0005.
- examples/types.yaml: principal_field + fields para Person, Email,
Domain, Phone, Org, IBAN, Account, Document, Address, Url, Table.
44 fields totales. Documentacion del formato en cabecera.
- project_manager.cpp: seed con fields para los tipos basicos (fallback
cuando no se encuentra examples/types.yaml).
- main.cpp:
- Log de carga incluye conteo de schemas y total de fields.
- --test-types-yaml <path>: smoke test que carga, serializa a temp y
recarga. Compara entidades/relaciones/fields field-a-field. Salida
PASS/FAIL con exit code 0/1. Permite verificar round-trip sin
framework de tests.
Verificado: examples/types.yaml round-trip estable (11 entities, 44
fields, 6 relations).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issue 0005:
- types_registry.h: enum FieldKind {string,int,float,bool,date,url,enum},
struct FieldSpec {name,kind,required,enum_values}. EntitySpec gana
principal_field, fields[], icon_name (para round-trip exacto).
- Parser: tolerante con yaml antiguo (sin fields). Soporta sub-key
'fields:' como lista multilinea de inline-maps con bracket-aware
split (respeta [a,b,c] sin partir). Soporta 'principal_field:'.
Tipos desconocidos → FK_STRING con warning.
- Writer types_save_yaml: emite formato compacto (un dict por entity,
fields como inline-maps, color en #RRGGBB[AA], icon por nombre).
Round-trip estable: load→save→load produce ParsedTypes identico.
apply_types_yaml sigue ignorando fields (eso lo consumen 0007/0008).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- types_registry.cpp::apply_types_yaml: tras aplicar el yaml, sobreescribe
shape de cada tipo: 'Table' → SHAPE_SQUARE, todo lo demas → SHAPE_CIRCLE.
Convencion fija — ediciones futuras del Type Editor (issue 0007) o del
yaml no rompen la regla.
- examples/types.yaml + project_manager.cpp seed: quitar campo `shape`,
añadir tipo Table (cuadrado) y relacion CONTAINS_ROW (preview de 0010).
- main.cpp run_force_step: damping=0.7, max_velocity=8 explicitos para
evitar que el grafo "explote" al cargar grafos pequenos.
- AppState repulsion: 1500 → 800 (lo mismo, aplicado al force layout).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cada proyecto es una subcarpeta junto al exe con su operations.db,
types.yaml y graph_explorer.db. Switcher en la toolbar con New / Open /
Recent / Reveal. Migracion automatica del layout legacy a projects/default/.
Cierra issue 0006.
main.cpp:
- Forward decl + switch_to_project: cierra layout_store, libera grafo,
aplica nuevos paths, vuelve a cargar.
- apply_project_paths: deriva operations.db/types.yaml/graph_explorer.db
del slug y los expone a g_app.active_project.
- main: arg --project <slug>; modo legacy si --input/positional dado;
modo proyecto si no — migra layout legacy, decide target via
arg/last_active/'default', crea si no existe, abre BDs y carga.
- render(): handler want_switch_project + monta views_new_project_modal.
views.h: AppState gana active_project, want_switch_project,
switch_project_target, show_new_project_modal, new_project_buf,
new_project_error, project_list_cache, project_recent_cache.
views.cpp:
- Toolbar: boton 'Project: <slug>' con popup (New/Recent/Open/Reveal).
Refresca caches al abrir el menu.
- views_new_project_modal: input slug + validacion + creacion + switch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Modulo nuevo que gestiona el sistema de proyectos del issue 0006.
Cada proyecto vive como subcarpeta junto al exe con su operations.db,
types.yaml y graph_explorer.db propios. Helpers:
- project_validate_slug / project_paths / project_list / project_exists
- project_create — bootstrap operations.db con DDL completo (entities,
relations, fts5, triggers, assertions, executions, logs) + types.yaml
semilla (copia de examples/types.yaml o embed si no existe).
- projects_migrate_legacy_layout — mueve operations.db / graph_explorer.db
del cwd a projects/default/ si el directorio projects/ no existe.
- project_settings_load/save/touch — graph_explorer.ini con last_active
y recent (max 5).
- project_reveal_in_explorer — Windows ShellExecute / Linux xdg-open.
CMakeLists registra project_manager.cpp en add_imgui_app.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug fixes
- ImGui ID conflict en menu Change type: dedup tipos del grafo +
defaults; PushID/PopID por entrada.
- Dockspace ya no tapa la toolbar: se posiciona 44 px por debajo, asi
las ventanas dockeadas al borde superior quedan bajo la barra de
filtros, no detras.
- Hover radius proporcional al tamaño visual del nodo: query espacial
amplio (24/zoom) + filtro fino por (radio_visual + 2 px) / zoom. El
tooltip solo se dispara si el raton esta efectivamente sobre el nodo.
Layout
- Default layout = grid (en vez de force) para que los grafos cargados
se distribuyan ordenadamente al abrir.
- Boton "Reset layout" en la toolbar: limpia NF_PINNED en todos los
nodos, resetea velocidades y reaplica el layout activo.
- Nodos recien creados (add_node, duplicate) caen en un anillo poisson
alrededor del centro de la vista, no en el origen. Posicion
determinista por user_data para que el mismo nodo no salte entre
reloads.
Notes (markdown)
- Panel "Note" (dockeable) abierto con doble click sobre un nodo.
- entity_get_notes / entity_set_notes en entity_ops sobre la columna
`notes` de operations.db (ya existente en el schema).
- Ctrl+S guarda. Cabecera muestra entity, type, id.
- Dockspace host (PassthruCentralNode) bajo la toolbar para que las
ventanas Viewport/Legend/Inspector/Stats puedan dockearse dentro de la
app principal.
- Toolbar: input "Add node" con auto-deteccion de tipo (text/email/
ip_address/url/domain/phone). Insert en operations.db + reload.
- Context menu (right-click sobre nodo): Change type, Duplicate, Delete,
submenu "Run enricher" (placeholder hasta issues 0001-0003).
- Inspector: vecinos ahora muestran etiqueta de relacion ("-> employs",
"<- owns") usando rel_types[].name como label de arista.
- Default relation label k_default_relation_name="RELATED_TO" para
relaciones creadas sin nombre semantico explicito.
- Indice EntityIndex (FNV1a hash -> sql id) reconstruido tras cada load
para resolver mutaciones desde el grafo en memoria.
Issues planteadas para iteraciones siguientes:
- 0001: chat con Claude sobre el grafo (HTTP + tool-use)
- 0002: enricher GLiNER+GLiREL desde nodo texto
- 0003: enricher web (fetch URL/dominio + extract text)
- 0004: vista tabla por tipo de entidad