close issue 0081: tables promoted to registry + fn doctor cpp-apps BeginTable check

- docs/TQL.md: añadidas secciones joins, views, main_source, 24 viz tokens completos
  (extraidos de tql_helpers.cpp), color_rules, fn.* builtins completos (20 funciones),
  funciones bloqueadas del sandbox, tabla de estado de implementacion actualizada.
  Nota al pie referencia los 129 checks roundtrip (41 emit + 88 apply).

- functions/infra/audit_cpp_apps.go: añadida AuditCppTableMigration() que escanea
  .cpp de cada app imgui buscando ImGui::BeginTable; status CANDIDATE/MIXED/clean
  segun si usa data_table_cpp_viz en uses_functions.

- cmd/fn/doctor.go: fn doctor cpp-apps ahora incluye seccion BeginTable migration
  con tabwriter CANDIDATE/MIXED; --json produce {conformance, table_migration}.
  doctorAll incluye cpp_table_migration en el mapa JSON.

- .claude/rules/fn_doctor.md: tabla de subcomandos y acciones complementarias
  actualizadas con el nuevo check.

- dev/issues/0081 movido a completed/ con status done y notas de deuda documentadas.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 14:49:56 +02:00
parent fad7b2fccc
commit acecbbc821
5 changed files with 470 additions and 26 deletions
+12 -1
View File
@@ -20,10 +20,18 @@ fn doctor sync # Solo drift pc_locations BD vs disco local
fn doctor uses-functions # Solo audit imports reales vs uses_functions fn doctor uses-functions # Solo audit imports reales vs uses_functions
fn doctor unused # Solo funciones huerfanas del registry fn doctor unused # Solo funciones huerfanas del registry
fn doctor cpp-apps # Conformidad C++ con cpp/PATTERNS.md (cfg.about/log, no app_menubar manual, no DockSpace duplicado) fn doctor cpp-apps # Conformidad C++ con cpp/PATTERNS.md (cfg.about/log, no app_menubar manual, no DockSpace duplicado)
# + check BeginTable inline: CANDIDATE (no migrado) / MIXED (parcial) / silencio (limpio)
fn doctor --json # Salida JSON (cualquier subcomando) — para agentes/scripts fn doctor --json # Salida JSON (cualquier subcomando) — para agentes/scripts
``` ```
`fn doctor cpp-apps` produce dos secciones:
1. Conformance (cfg.about/log, fn::run_app, menubar, DockSpace) — una fila por app imgui.
2. BeginTable migration (issue 0081) — solo apps con `ImGui::BeginTable` inline:
- `CANDIDATE`: N tablas inline sin `data_table_cpp_viz` en uses_functions. Considerar migracion.
- `MIXED`: N tablas inline con `data_table_cpp_viz` ya declarado. Migracion parcial OK.
- silencio: 0 BeginTable inline (limpio o completamente migrado).
### Mapeo subcomando → funcion del registry ### Mapeo subcomando → funcion del registry
| Subcomando | Funcion | | Subcomando | Funcion |
@@ -33,7 +41,8 @@ fn doctor --json # Salida JSON (cualquier subcomando) — para agentes
| `sync` | `pc_locations_drift_go_infra` | | `sync` | `pc_locations_drift_go_infra` |
| `uses-functions` | `audit_uses_functions_go_infra` | | `uses-functions` | `audit_uses_functions_go_infra` |
| `unused` | `find_unused_functions_go_infra` | | `unused` | `find_unused_functions_go_infra` |
| `cpp-apps` | `audit_cpp_apps_go_infra` | | `cpp-apps` (conformance) | `audit_cpp_apps_go_infra` |
| `cpp-apps` (table migration) | `audit_cpp_table_migration_go_infra` (inline en `audit_cpp_apps.go`) |
Cada subcomando es un wrapper fino. Toda la logica vive en la funcion. Si quieres usar la salida en otro programa Go, importa la funcion directamente. Cada subcomando es un wrapper fino. Toda la logica vive en la funcion. Si quieres usar la salida en otro programa Go, importa la funcion directamente.
@@ -64,6 +73,8 @@ Texto humano por defecto (tabwriter). `--json` produce array/objeto serializable
| `manual_DockSpaceOverViewport_*` | Borrar la llamada o setear `cfg.auto_dockspace = false` si la app gestiona docking propio | | `manual_DockSpaceOverViewport_*` | Borrar la llamada o setear `cfg.auto_dockspace = false` si la app gestiona docking propio |
| `missing_cfg_about` / `missing_cfg_log` | Anadir `cfg.about = {...}` / `cfg.log = {"<name>.log", 1}` antes de `fn::run_app` | | `missing_cfg_about` / `missing_cfg_log` | Anadir `cfg.about = {...}` / `cfg.log = {"<name>.log", 1}` antes de `fn::run_app` |
| `app.md_missing_*` | Regenerar via plantilla del scaffolder (`/new-cpp-app`) o anadir campos a mano | | `app.md_missing_*` | Regenerar via plantilla del scaffolder (`/new-cpp-app`) o anadir campos a mano |
| cpp-apps BeginTable `CANDIDATE` | App tiene N `ImGui::BeginTable` sin migrar. Abrir rama TBD, reemplazar tablas por `data_table::render()` via `fn_table_viz`, añadir `data_table_cpp_viz` a `uses_functions` en `app.md` |
| cpp-apps BeginTable `MIXED` | Migracion parcial en curso. Continuar wave por wave hasta que no queden BeginTable inline |
| Backup viejo | `backup_all_bash_pipelines ~/backups/fn_registry` | | Backup viejo | `backup_all_bash_pipelines ~/backups/fn_registry` |
### Para agentes ### Para agentes
+42 -1
View File
@@ -123,6 +123,11 @@ func doctorAll(root string, jsonOut bool) {
} else { } else {
all["cpp_apps_error"] = err.Error() all["cpp_apps_error"] = err.Error()
} }
if v, err := infra.AuditCppTableMigration(root); err == nil {
all["cpp_table_migration"] = v
} else {
all["cpp_table_migration_error"] = err.Error()
}
if v, err := infra.AuditMlEnv(root); err == nil { if v, err := infra.AuditMlEnv(root); err == nil {
all["ml"] = v all["ml"] = v
} else { } else {
@@ -168,10 +173,21 @@ func doctorCppApps(root string, jsonOut bool) {
fmt.Fprintf(os.Stderr, "error: %v\n", err) fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1) os.Exit(1)
} }
tableAudits, err2 := infra.AuditCppTableMigration(root)
if err2 != nil {
fmt.Fprintf(os.Stderr, "warning: table migration audit failed: %v\n", err2)
tableAudits = nil
}
if jsonOut { if jsonOut {
emit(audits) emit(map[string]any{
"conformance": audits,
"table_migration": tableAudits,
})
return return
} }
// Conformance section.
bad := 0 bad := 0
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "STATUS\tAPP\tISSUES") fmt.Fprintln(w, "STATUS\tAPP\tISSUES")
@@ -187,6 +203,31 @@ func doctorCppApps(root string, jsonOut bool) {
} }
w.Flush() w.Flush()
fmt.Printf("\n%d/%d C++ apps conform.\n", len(audits)-bad, len(audits)) fmt.Printf("\n%d/%d C++ apps conform.\n", len(audits)-bad, len(audits))
// BeginTable migration section.
if len(tableAudits) == 0 {
return
}
hasMigrationNotes := false
for _, t := range tableAudits {
if t.Status != "clean" {
hasMigrationNotes = true
break
}
}
if !hasMigrationNotes {
return
}
fmt.Println("\n--- BeginTable migration (issue 0081) ---")
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(tw, "STATUS\tAPP\tTABLES\tMESSAGE")
for _, t := range tableAudits {
if t.Status == "clean" {
continue
}
fmt.Fprintf(tw, "%s\t%s\t%d\t%s\n", strings.ToUpper(t.Status), t.AppID, t.BeginTableCount, t.Message)
}
tw.Flush()
} }
func doctorArtefacts(root string, jsonOut bool) { func doctorArtefacts(root string, jsonOut bool) {
@@ -1,16 +1,22 @@
--- ---
id: 0081 id: 0081
title: tables playground — promote a registry + migrar apps C++ (fase 12) title: tables playground — promote a registry + migrar apps C++ (fase 12)
status: partial status: done
priority: high priority: high
created: 2026-05-12 created: 2026-05-12
updated: 2026-05-13 updated: 2026-05-15
notes: | notes: |
0081-A DONE: 20 types extraidos a cpp/functions/core/data_table_types.h con .md por type 0081-A DONE: 20 types extraidos a cpp/functions/core/data_table_types.h con .md por type
(17 core + 3 viz). Playground includes via "core/data_table_types.h", no duplicacion. (17 core + 3 viz). Playground includes via "core/data_table_types.h", no duplicacion.
603 tests pass, e2e linux+windows OK. 603 tests pass, e2e linux+windows OK.
0081-B..L PENDING: extraer functions (compute_stage, tql_emit/apply, lua_engine, tql_to_sql, 0081-B..L DONE (2026-05-15): 10 funciones registry (8 core + 2 viz), 1 lib fn_table_viz,
join_tables, viz_render, data_table) + fn_table_viz lib + migrar 5 apps. 3 apps migradas (chart_demo no aplica, graph_explorer parcial 1/9, registry_dashboard parcial 8/12),
fn doctor cpp-apps check anadido (BeginTable inline detection: CANDIDATE/MIXED),
docs/TQL.md actualizado con joins, views, main_source, 24 viz tokens, color_rules,
derived columns, fn.* sandbox completo (20 builtins), funciones bloqueadas.
Deuda: sqlite_api + deploy_server NO migrados (Go apps, requieren TS table system aparte);
graph_explorer + registry_dashboard + otras apps C++ marcadas CANDIDATE por fn doctor
(migrar en waves futuras con rama TBD dedicada por app).
related_components: [cpp/apps/primitives_gallery/playground/tables, cpp/functions, fn_framework] related_components: [cpp/apps/primitives_gallery/playground/tables, cpp/functions, fn_framework]
--- ---
+289 -20
View File
@@ -58,7 +58,7 @@ Independiente de los datos. La misma query puede renderizarse de N formas con un
### Implicaciones para TQL ### Implicaciones para TQL
TQL adopta esa separacion: `stages` (data) + `display` + `columns` (viz). Mismo patron, sintaxis Lua. TQL adopta esa separacion: `stages` (data) + `display` + `columns` (viz) + `views` (paneles adicionales). Mismo patron, sintaxis Lua.
Cuando un boton futuro "Add visualization" se construya, anade un nuevo `display` + viz settings a una query existente sin tocar `stages`. Asi tendremos M visualizaciones (table, bar, line, scatter) sobre los mismos datos transformados. Cuando un boton futuro "Add visualization" se construya, anade un nuevo `display` + viz settings a una query existente sin tocar `stages`. Asi tendremos M visualizaciones (table, bar, line, scatter) sobre los mismos datos transformados.
@@ -103,6 +103,10 @@ input_cells (raw dataset)
return { return {
version = 1, version = 1,
display = "table", display = "table",
main_source = "functions", -- opcional: nombre de la fuente principal
-- JOINS: unir tablas adicionales antes de stage 0
joins = { ... }, -- opcional
-- DATA: pipeline de transformacion -- DATA: pipeline de transformacion
stages = { stages = {
@@ -129,9 +133,14 @@ return {
color_rules = { {equals = "0.0", color = "#e08060"} }}, color_rules = { {equals = "0.0", color = "#e08060"} }},
{name = "internal", type = "string", visible = false, order = 3}, {name = "internal", type = "string", visible = false, order = 3},
}, },
visualization_settings = {
-- Futuro: opciones especificas del display (chart axes, paleta, stack, etc.) -- VIEWS: paneles de visualizacion (index 1 = principal, resto = extras)
views = {
{display = "table"},
{display = "bar", x_col = "lang", y_cols = {"count"}},
}, },
visualization_settings = {},
} }
``` ```
@@ -139,6 +148,57 @@ return {
--- ---
## `main_source`
Campo de cadena opcional. Identifica el nombre de la tabla/fuente principal del dataset. Usado por `tql_to_sql` para generar el `FROM "main_source"` correcto en el SQL emitido. Si esta vacio, el motor usa la tabla por defecto del contexto.
```lua
main_source = "functions"
```
En el SQL emitido: `FROM "functions"`. Util cuando la app expone multiples tablas y el agente necesita especificar explicitamente cual es la base del query.
---
## `joins`
Lista de joins que se aplican antes de stage 0. Los campos de las tablas unidas se añaden como columnas adicionales accesibles en todos los stages.
```lua
joins = {
{
alias = "t", -- prefijo para sus columnas ("t.field")
source = "types", -- nombre de la tabla a unir
strategy = "left", -- "left" | "inner" | "right" | "full"
on = {{"id", "t.id"}}, -- pares {col_izq, col_der}
fields = {"t.algebraic", "t.description"}, -- cols a incluir (opcional)
},
{
alias = "u",
source = "unit_tests",
strategy = "inner",
on = {{"id", "u.function_id"}, {"lang", "u.lang"}}, -- multi-key
},
}
```
**Estrategias:**
| Token | Semantica SQL |
|---|---|
| `"left"` | `LEFT OUTER JOIN` — todas las filas de la izq, nulls donde no hay match |
| `"inner"` | `INNER JOIN` — solo filas con match en ambas tablas |
| `"right"` | `RIGHT OUTER JOIN` — todas las filas de la der |
| `"full"` | `FULL OUTER JOIN` — todas las filas de ambas tablas |
Default si `strategy` se omite: `"left"`.
**Campos tras el join:** accesibles como `"alias.field"` (ej. `"t.algebraic"`) en filters, breakouts, aggregations y expressions. Si `fields` se omite, se incluyen todas las columnas de la tabla unida con prefijo alias.
**Join multi-key:** `on` es lista de pares; se traduce a `ON l.k1 = r.k1 AND l.k2 = r.k2`.
---
## `filter` ## `filter`
Lista de predicados. Multiples filters se combinan con AND implicito. Lista de predicados. Multiples filters se combinan con AND implicito.
@@ -191,6 +251,15 @@ breakout = { "lang", "domain" }
Cada combinacion unica de valores `(lang, domain)` produce una fila en el output. Si `breakout` esta vacio pero hay `aggregation`, todo el dataset se reduce a UNA sola fila. Cada combinacion unica de valores `(lang, domain)` produce una fila en el output. Si `breakout` esta vacio pero hay `aggregation`, todo el dataset se reduce a UNA sola fila.
**Breakout con granularidad de fecha** — sufijo `:granularity` en el nombre de la col:
```lua
breakout = { "created_at:month", "lang" }
-- equivale a GROUP BY date_trunc('month', created_at), lang
```
Granularidades disponibles: `year`, `month`, `week`, `day`, `hour`.
**Disponible solo en stages >= 1.** **Disponible solo en stages >= 1.**
--- ---
@@ -274,13 +343,97 @@ columns = {
**Cols que no aparecen en `columns`**: mantienen su estado UI actual (visible, posicion natural). **Cols que no aparecen en `columns`**: mantienen su estado UI actual (visible, posicion natural).
### `color_rules`
Reglas de color condicional por valor exacto. Se aplican al renderizar cada celda de la columna: si el valor de la celda es igual a `equals`, la celda se colorea con `color`.
```lua
color_rules = {
{equals = "go", color = "#86b56b"}, -- verde para Go
{equals = "py", color = "#6b8eb5"}, -- azul para Python
{equals = "bash", color = "#b58f6b"}, -- naranja para Bash
}
```
- Solo soporta igualdad exacta (string match). Para rangos numericos, usa una expression que produzca una etiqueta ("high"/"low") y aplica color_rules sobre esa columna derivada.
- Multiples reglas se evaluan en orden; la primera que hace match gana.
- Si ningun match: color por defecto del tema.
---
## `display` ## `display`
Tipo de visualizacion. v1 solo `"table"`. Futuro: `"bar"`, `"line"`, `"scatter"`, `"pie"`, `"scalar"`, `"area"`, `"pivot"`. Default: `"table"`. Tipo de visualizacion del panel principal. Default: `"table"`.
**Tokens validos (extraidos de `tql_helpers.cpp`):**
| Token | Tipo de chart |
|---|---|
| `"table"` | Tabla de datos (default) |
| `"bar"` | Barras horizontales |
| `"column"` | Barras verticales |
| `"grouped_bar"` | Barras agrupadas por categoria |
| `"stacked_bar"` | Barras apiladas |
| `"line"` | Lineas |
| `"area"` | Area rellena |
| `"stairs"` | Escalera (step function) |
| `"scatter"` | Dispersion XY |
| `"bubble"` | Dispersion XY con tamano variable |
| `"histogram"` | Histograma 1D |
| `"hist2d"` | Histograma 2D |
| `"heatmap"` | Mapa de calor |
| `"boxplot"` | Caja y bigotes |
| `"stem"` | Stem plot |
| `"errorbars"` | Barras de error |
| `"pie"` | Sectores (pie chart) |
| `"donut"` | Donut |
| `"funnel"` | Embudo |
| `"waterfall"` | Cascada |
| `"kpi"` | Metrica KPI (numero grande) |
| `"kpi_grid"` | Grid de KPIs |
| `"candlestick"` | Velas (OHLC) |
| `"radar"` | Radar / spider |
Token invalido: `tql_apply` genera warning `"unknown display"` y cae a `"table"`.
---
## `views`
Array de paneles de visualizacion. El indice 1 es el panel principal (equivale al `display` + `viz_config` del State); el resto son paneles extra que se muestran junto a la tabla.
```lua
views = {
-- Panel 0 (principal)
{display = "bar", x_col = "lang", y_cols = {"count"}, color = "#86b56b"},
-- Panel 1 (extra)
{display = "pie", cat_col = "lang", y_cols = {"sum_size_kb"}, show_legend = true},
}
```
**Campos por panel:**
| Campo | Tipo | Para que |
|---|---|---|
| `display` | string | Token de tipo de chart (ver tabla `display`) |
| `x_col` | string | Columna para eje X (bar, column, line, area, scatter, bubble, etc.) |
| `y_cols` | `{string,...}` | Columnas para eje Y. Multiple = multiple series |
| `cat_col` | string | Columna de categorias (pie, donut, funnel, radar) |
| `size_col` | string | Columna para tamano del burbuja (bubble) |
| `color` | string | Color primario `"#rrggbb"`. Sirve para series unicas o acento |
| `hist_bins` | int | Numero de bins para histogram / hist2d |
| `pie_radius` | float | Radio del donut interior (donut, 0.0 = pie solido) |
| `show_legend` | bool | Mostrar leyenda. Default `true` |
| `show_markers` | bool | Puntos en lineas/area. Default `false` |
| `locked` | bool | Panel fijo — el usuario no puede cerrarlo ni cambiar tipo |
Si `views` se omite, el emit lo serializa con un panel minimo que replica `state.display`.
---
## `visualization_settings` ## `visualization_settings`
Reservado para configuracion especifica por tipo de display. v1 vacio. Futuro: Reservado para configuracion especifica por tipo de display. v1 siempre vacio (`{}`). Emitido por `tql_emit` para mantener el round-trip completo. Futuro:
```lua ```lua
visualization_settings = { visualization_settings = {
@@ -293,6 +446,8 @@ visualization_settings = {
Sintaxis Metabase: las keys con `.` van entre brackets `[]`. Sintaxis Metabase: las keys con `.` van entre brackets `[]`.
---
## `sort` ## `sort`
Lista de clauses. Multi-sort por orden de aparicion (primera = primaria). Lista de clauses. Multi-sort por orden de aparicion (primera = primaria).
@@ -318,6 +473,8 @@ Pregunta: "Para las funciones puras con cobertura >= 80%, agrupa por lenguaje y
```lua ```lua
return { return {
version = 1,
display = "table",
stages = { stages = {
-- Stage 0: Raw + filter -- Stage 0: Raw + filter
{ {
@@ -350,6 +507,44 @@ return {
--- ---
## Ejemplo con join + views
```lua
return {
version = 1,
display = "bar",
main_source = "functions",
joins = {
{
alias = "u",
source = "unit_tests",
strategy = "left",
on = {{"id", "u.function_id"}},
fields = {"u.name"},
},
},
stages = {
{ filter = {{"=", "lang", "go"}} },
{
breakout = {"domain"},
aggregation = {{"count"}, {"distinct", "id"}},
sort = {{"desc", "count"}},
},
},
columns = {
{name = "domain", type = "string", visible = true, order = 1},
{name = "count", type = "int", visible = true, order = 2},
},
views = {
{display = "bar", x_col = "domain", y_cols = {"count"}, show_legend = false},
{display = "donut", cat_col = "domain", y_cols = {"count"}, show_legend = true},
},
visualization_settings = {},
}
```
---
## Drill-down (semantica) ## Drill-down (semantica)
Si el usuario interactua con una celda agrupada del stage N, hace **drill-down**: Si el usuario interactua con una celda agrupada del stage N, hace **drill-down**:
@@ -387,8 +582,7 @@ Las strings dentro de `expressions` siguen el mini-DSL Lua de columnas custom. R
- Type-aware: cell de col Int/Float llega como number; Bool como boolean; resto como string. Vacia = nil. - Type-aware: cell de col Int/Float llega como number; Bool como boolean; resto como string. Vacia = nil.
- UTF-8 ok en nombres `[año]`. - UTF-8 ok en nombres `[año]`.
- Comentarios `--` y `--[[ ]]` respetados. - Comentarios `--` y `--[[ ]]` respetados.
- Builtins disponibles via `fn.*`: `upper, lower, length, substring, contains, starts_with, ends_with, replace, trim, concat, to_number, to_string, to_bool, is_null, is_empty, coalesce, parse_date, year, month, day`. - Nombres de cols con espacios y puntos soportados en brackets: `[col con espacio]`, `[alias.field]`.
- Sandbox: sin `io`, `require`, `dofile`, `loadfile`, `load`, `package`, `debug`. `os` recortado a `date/time/difftime/clock`.
Ejemplos: Ejemplos:
@@ -397,21 +591,73 @@ Ejemplos:
fn.concat([lang], ":", [domain]) -- string compose fn.concat([lang], ":", [domain]) -- string compose
if [coverage_pct] >= 90 then "well" else "low" end if [coverage_pct] >= 90 then "well" else "low" end
fn.year([updated_at]) -- date helper fn.year([updated_at]) -- date helper
fn.coalesce([error_type], "none") -- null handling
``` ```
--- ---
## Funciones Lua disponibles (`fn.*`)
El sandbox expone estas funciones via la tabla global `fn`. Registradas en `lua_engine.cpp::register_builtins`:
| Funcion | Firma | Que hace |
|---|---|---|
| `fn.upper(s)` | string -> string | Convierte a mayusculas (ASCII) |
| `fn.lower(s)` | string -> string | Convierte a minusculas (ASCII) |
| `fn.length(s)` | string -> int | Longitud en bytes (`strlen`); nil -> 0 |
| `fn.substring(s, start [, len])` | string, int[, int] -> string | Subcadena 1-based; len omitido = hasta el final |
| `fn.contains(haystack, needle)` | string, string -> bool | True si needle aparece en haystack |
| `fn.starts_with(s, prefix)` | string, string -> bool | True si s empieza por prefix |
| `fn.ends_with(s, suffix)` | string, string -> bool | True si s termina por suffix |
| `fn.replace(s, find, repl)` | string, string, string -> string | Reemplaza todas las ocurrencias de find por repl |
| `fn.trim(s)` | string -> string | Elimina espacios/tabs/newlines del inicio y fin |
| `fn.concat(...)` | vararg -> string | Concatena N argumentos como string |
| `fn.to_number(s)` | string -> number\|nil | Parsea a numero; nil si no parseable |
| `fn.to_string(x)` | any -> string | Convierte a string (usa `luaL_tolstring`) |
| `fn.to_bool(x)` | any -> bool | True si `"true"` o `"1"` |
| `fn.is_null(x)` | any -> bool | True si x es nil |
| `fn.is_empty(x)` | any -> bool | True si x es nil o string vacia |
| `fn.coalesce(...)` | vararg -> any | Devuelve el primer argumento no-nil |
| `fn.parse_date(s)` | string -> table\|nil | Parsea `"YYYY-MM-DD"` -> `{year, month, day}` |
| `fn.year(s)` | string -> int\|nil | Extrae el año de `"YYYY-..."` |
| `fn.month(s)` | string -> int\|nil | Extrae el mes de `"YYYY-MM-..."` |
| `fn.day(s)` | string -> int\|nil | Extrae el dia de `"YYYY-MM-DD"` |
Ademas, las librerias Lua estandar `string`, `table`, `math`, `os` (recortado) estan disponibles.
---
## Sandbox — funciones bloqueadas
El engine aplica el sandbox via `lua_engine.cpp::apply_sandbox`. Globals eliminados:
| Global | Por que bloqueado |
|---|---|
| `io` | I/O de archivos y stdin/stdout |
| `require` | Carga de modulos externos |
| `loadfile` | Ejecucion de archivos Lua arbitrarios |
| `dofile` | Idem |
| `load` | Compilacion y ejecucion de strings arbitrarias |
| `package` | Sistema de paquetes Lua |
| `debug` | Introspection de call stack / upvalues |
`os` se sustituye por una version recortada que solo expone: `os.date`, `os.time`, `os.difftime`, `os.clock`. El resto de `os` (ejecutar comandos, salir, setenv, etc.) se elimina.
Las formulas de expresiones se compilan con `luaL_loadbufferx(..., "t")` — el flag `"t"` rechaza bytecode precompilado (solo acepta texto source).
---
## Restricciones v1 ## Restricciones v1
| No soportado | Workaround | | No soportado | Workaround |
|---|---| |---|---|
| Joins entre tablas | Pre-procesar fuera del registry. |
| Subqueries SQL | Usar stages encadenados (modelo equivalente). |
| `HAVING` post-aggregation | Stage siguiente con `filter` sobre cols agregadas. | | `HAVING` post-aggregation | Stage siguiente con `filter` sobre cols agregadas. |
| `LIMIT` | TBD — añadir como `limit = N` en stage v2. | | `LIMIT` | TBD — añadir como `limit = N` en stage v2. |
| Window functions | TBD. | | Window functions | TBD. |
| Custom aggregation Lua | TBD — `{"lua", "col", "<body>"}`. | | Custom aggregation Lua | TBD — `{"lua", "col", "<body>"}`. |
| Alias custom en aggregation v1 | Crear expression post-grupo. | | Alias custom en aggregation v1 | Crear expression post-grupo. |
| color_rules con rangos numericos | Usar expression que emita etiquetas; aplicar color_rules sobre la etiqueta. |
| Multiples fuentes sin join | Declarar cada fuente adicional en `joins`. |
--- ---
@@ -423,11 +669,21 @@ Cuando expongas TQL a un LLM, dale este preambulo:
You output TQL — a Lua table that describes a table transformation. Format: You output TQL — a Lua table that describes a table transformation. Format:
return { return {
version = 1,
display = "table", -- table|bar|column|grouped_bar|stacked_bar|line|area|stairs|scatter|
-- bubble|histogram|hist2d|heatmap|boxplot|stem|errorbars|
-- pie|donut|funnel|waterfall|kpi|kpi_grid|candlestick|radar
main_source = "...", -- optional: name of main table/source
joins = { ... }, -- optional: join additional tables
stages = { stages = {
{ filter = {...}, expressions = {...}, sort = {...} }, -- Stage 0 (Raw) { filter = {...}, expressions = {...}, sort = {...} }, -- Stage 0 (Raw)
{ filter = {...}, breakout = {...}, aggregation = {...}, sort = {...} }, -- Stage 1+ { filter = {...}, breakout = {...}, aggregation = {...}, sort = {...} }, -- Stage 1+
... ...
} },
views = {
{display="...", x_col="...", y_cols={...}, cat_col="...", color="...", ...}, -- panel 0 = main
... -- extra panels
},
} }
Rules: Rules:
@@ -437,6 +693,9 @@ Rules:
Available fns: count, sum, avg, min, max, distinct, stddev, median, p25, p75, p90, p99, percentile. Available fns: count, sum, avg, min, max, distinct, stddev, median, p25, p75, p90, p99, percentile.
- Sort: {{"desc", "col"}, ...}. Multi-sort por orden de la lista. - Sort: {{"desc", "col"}, ...}. Multi-sort por orden de la lista.
- Expressions value es una expresion Lua. Acceso a cols via [col_name]. - Expressions value es una expresion Lua. Acceso a cols via [col_name].
- Joins: alias + source + strategy (left/inner/right/full) + on pairs + optional fields list.
- Views: array de paneles, index 1 = principal. display token from the list above.
- color_rules: [{equals="val", color="#rrggbb"}, ...] dentro de cada entry de columns.
The available columns of the current input table are: <inject runtime>. The available columns of the current input table are: <inject runtime>.
The available column types: <inject runtime>. The available column types: <inject runtime>.
@@ -483,19 +742,25 @@ StageOutput compute_stage(const char* const* in_cells, int in_rows, int in_cols,
| Feature | Status | | Feature | Status |
|---|---| |---|---|
| `Stage` + `Aggregation` types | done | | `Stage` + `Aggregation` types | done |
| `compute_stage` (filter + group + agg + sort) | done (Phase 1) | | `compute_stage` (filter + group + agg + sort) | done |
| Todas las aggregations (count..percentile) | done | | Todas las aggregations (count..percentile) | done |
| `aggregation_alias` / `aggregation_type` | done | | `aggregation_alias` / `aggregation_type` | done |
| Multi-sort por stage | done | | Multi-sort por stage | done |
| Tests E2E logica | done (37 checks) | | Tests E2E logica | done (129 checks en tql_emit_test + tql_apply_test) |
| `tql_emit` / `tql_apply` (Lua round-trip) | Phase 2 (pendiente) | | `tql_emit` / `tql_apply` (Lua round-trip) | done |
| State refactor a `vector<Stage>` | Phase 3 (pendiente) | | `views` (paneles de visualizacion) | done |
| UI breadcrumb stages + chips por stage | Phase 3 (pendiente) | | `main_source` | done |
| Drill-down interactivo | Phase 3 (pendiente) | | `joins` (left/inner/right/full, multi-key, fields) | done |
| Show TQL / Apply TQL modals | Phase 2 | | `color_rules` por columna | done |
| Multi-sort drag-reorder | Phase 4 | | `breakout` con granularidad de fecha | done |
| Lua sandbox (`fn.*` builtins, sin io/require/load) | done |
Ver `cpp/apps/primitives_gallery/playground/tables/` para la implementacion del playground. | 24 tipos de viz (table, bar, column, pie, donut...) | done |
| `tql_to_sql` (SQL DuckDB emit) | done (issue 0080) |
| State refactor a `vector<Stage>` | done |
| UI breadcrumb stages + chips por stage | done |
| Drill-down interactivo | done |
| Show TQL / Apply TQL modals | done |
| Multi-sort drag-reorder | done |
--- ---
@@ -580,3 +845,7 @@ SQL transpile error en derived col 'fullname':
- **Agente flow:** TQL default. SQL solo si app linko DuckDB. UI Ask AI muestra toggle SQL solo cuando disponible. - **Agente flow:** TQL default. SQL solo si app linko DuckDB. UI Ask AI muestra toggle SQL solo cuando disponible.
Ver issue 0080 + `tql_to_sql.{h,cpp}` para implementacion. Ver issue 0080 + `tql_to_sql.{h,cpp}` para implementacion.
---
*Generado a partir de los tests roundtrip en `cpp/functions/core/tql_emit_test.cpp` y `cpp/functions/core/tql_apply_test.cpp` — 129 checks (41 emit + 88 apply) en verde garantizan compatibilidad del round-trip State <-> Lua.*
+117
View File
@@ -157,3 +157,120 @@ func cppHasAutoDockspaceFalse(src string) bool {
return strings.Contains(src, "auto_dockspace = false") || return strings.Contains(src, "auto_dockspace = false") ||
strings.Contains(src, "auto_dockspace=false") strings.Contains(src, "auto_dockspace=false")
} }
// CppTableMigrationAudit holds the BeginTable migration status for a single C++ app.
type CppTableMigrationAudit struct {
AppID string `json:"app_id"`
DirPath string `json:"dir_path"`
BeginTableCount int `json:"begin_table_count"`
UsesDataTableViz bool `json:"uses_data_table_viz"`
Status string `json:"status"` // "clean", "candidate", "mixed"
Message string `json:"message"`
}
// AuditCppTableMigration scans C++ apps (imgui framework) for inline
// ImGui::BeginTable calls and checks whether the app already declares
// data_table_cpp_viz in its uses_functions.
//
// Status values:
// - "clean" — no ImGui::BeginTable found (fully migrated or never had tables)
// - "mixed" — has ImGui::BeginTable AND declares data_table_cpp_viz
// (partial migration OK, wave N in progress)
// - "candidate" — has ImGui::BeginTable but does NOT declare data_table_cpp_viz
// (migration not started; consider data_table_cpp_viz, issue 0081)
func AuditCppTableMigration(registryRoot string) ([]CppTableMigrationAudit, error) {
dbPath := filepath.Join(registryRoot, "registry.db")
dsn := fmt.Sprintf("file:%s?mode=ro&_foreign_keys=on", dbPath)
db, err := sql.Open("sqlite3", dsn)
if err != nil {
return nil, fmt.Errorf("audit_cpp_table_migration: open db: %w", err)
}
defer db.Close()
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("audit_cpp_table_migration: ping db: %w", err)
}
rows, err := db.Query(`SELECT id, dir_path, COALESCE(framework,''), COALESCE(uses_functions,'') FROM apps WHERE lang = 'cpp' ORDER BY id`)
if err != nil {
return nil, fmt.Errorf("audit_cpp_table_migration: query apps: %w", err)
}
defer rows.Close()
var results []CppTableMigrationAudit
for rows.Next() {
var id, dir, framework, usesFunctions string
if err := rows.Scan(&id, &dir, &framework, &usesFunctions); err != nil {
continue
}
if framework != "imgui" {
continue
}
absDir := dir
if !filepath.IsAbs(absDir) {
absDir = filepath.Join(registryRoot, dir)
}
if _, err := os.Stat(absDir); os.IsNotExist(err) {
continue // directory_missing already reported by AuditCppApps
}
// Count ImGui::BeginTable occurrences across all .cpp files (excluding tests/).
count := 0
err := filepath.WalkDir(absDir, func(path string, d os.DirEntry, walkErr error) error {
if walkErr != nil {
return nil
}
// Skip test directories.
if d.IsDir() && (d.Name() == "tests" || d.Name() == "test") {
return filepath.SkipDir
}
if d.IsDir() {
return nil
}
if !strings.HasSuffix(d.Name(), ".cpp") {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return nil
}
src := string(data)
for start := 0; ; {
idx := strings.Index(src[start:], "ImGui::BeginTable(")
if idx < 0 {
break
}
count++
start += idx + len("ImGui::BeginTable(")
}
return nil
})
if err != nil {
continue
}
usesViz := strings.Contains(usesFunctions, "data_table_cpp_viz")
audit := CppTableMigrationAudit{
AppID: id,
DirPath: dir,
BeginTableCount: count,
UsesDataTableViz: usesViz,
}
switch {
case count == 0:
audit.Status = "clean"
audit.Message = ""
case usesViz:
audit.Status = "mixed"
audit.Message = fmt.Sprintf("%d ImGui::BeginTable inline (partial migration OK — data_table_cpp_viz already declared)", count)
default:
audit.Status = "candidate"
audit.Message = fmt.Sprintf("candidate to migrate: %d ImGui::BeginTable inline detected, consider data_table_cpp_viz (issue 0081)", count)
}
results = append(results, audit)
}
return results, nil
}