951a77ec7f
- 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>
277 lines
8.9 KiB
Go
277 lines
8.9 KiB
Go
package infra
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
// CppAppAudit holds the standard-conformance report for a single C++ app.
|
|
type CppAppAudit struct {
|
|
AppID string `json:"app_id"`
|
|
DirPath string `json:"dir_path"`
|
|
OK bool `json:"ok"`
|
|
Issues []string `json:"issues"`
|
|
}
|
|
|
|
// AuditCppApps walks every app registered with lang='cpp' in registry.db and
|
|
// checks conformance with cpp/PATTERNS.md and .claude/rules/cpp_apps.md:
|
|
// - main.cpp exists at the root of the app dir
|
|
// - main.cpp uses fn::run_app(...) (not glfwInit directly)
|
|
// - cfg.about is declared (or .about = passed in run_app literal)
|
|
// - cfg.log is declared
|
|
// - main.cpp does NOT call fn_ui::app_menubar(nullptr,...) manually
|
|
// - main.cpp does NOT call ImGui::DockSpaceOverViewport(...) unless
|
|
// cfg.auto_dockspace = false is also set
|
|
// - app.md frontmatter declares framework: imgui and entry_point
|
|
//
|
|
// Apps whose dir does not exist on disk are skipped with a placeholder issue.
|
|
func AuditCppApps(registryRoot string) ([]CppAppAudit, 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_apps: open db: %w", err)
|
|
}
|
|
defer db.Close()
|
|
if err := db.Ping(); err != nil {
|
|
return nil, fmt.Errorf("audit_cpp_apps: ping db: %w", err)
|
|
}
|
|
|
|
rows, err := db.Query(`SELECT id, dir_path, COALESCE(framework, '') FROM apps WHERE lang = 'cpp' ORDER BY id`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("audit_cpp_apps: query apps: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var results []CppAppAudit
|
|
for rows.Next() {
|
|
var id, dir, framework string
|
|
if err := rows.Scan(&id, &dir, &framework); err != nil {
|
|
continue
|
|
}
|
|
// Solo auditar apps imgui — apps CLI/test no necesitan cfg.about/log/menubar.
|
|
if framework != "imgui" {
|
|
continue
|
|
}
|
|
audit := CppAppAudit{AppID: id, DirPath: dir}
|
|
|
|
absDir := dir
|
|
if !filepath.IsAbs(absDir) {
|
|
absDir = filepath.Join(registryRoot, dir)
|
|
}
|
|
if _, err := os.Stat(absDir); os.IsNotExist(err) {
|
|
audit.Issues = append(audit.Issues, "directory_missing")
|
|
results = append(results, audit)
|
|
continue
|
|
}
|
|
|
|
// main.cpp checks
|
|
mainPath := filepath.Join(absDir, "main.cpp")
|
|
mainBytes, err := os.ReadFile(mainPath)
|
|
if err != nil {
|
|
audit.Issues = append(audit.Issues, "main.cpp_missing_or_unreadable")
|
|
} else {
|
|
src := string(mainBytes)
|
|
if !strings.Contains(src, "fn::run_app") {
|
|
audit.Issues = append(audit.Issues, "no_fn_run_app_call")
|
|
}
|
|
if strings.Contains(src, "glfwInit(") {
|
|
audit.Issues = append(audit.Issues, "manual_glfwInit_call")
|
|
}
|
|
if !cppHasAbout(src) {
|
|
audit.Issues = append(audit.Issues, "missing_cfg_about")
|
|
}
|
|
if !cppHasLog(src) {
|
|
audit.Issues = append(audit.Issues, "missing_cfg_log")
|
|
}
|
|
if cppHasManualMenubar(src) {
|
|
audit.Issues = append(audit.Issues, "manual_app_menubar_call")
|
|
}
|
|
if cppHasManualDockSpace(src) && !cppHasAutoDockspaceFalse(src) {
|
|
audit.Issues = append(audit.Issues, "manual_DockSpaceOverViewport_without_auto_dockspace_false")
|
|
}
|
|
}
|
|
|
|
// app.md checks
|
|
appMdPath := filepath.Join(absDir, "app.md")
|
|
mdBytes, err := os.ReadFile(appMdPath)
|
|
if err != nil {
|
|
audit.Issues = append(audit.Issues, "app.md_missing")
|
|
} else {
|
|
md := string(mdBytes)
|
|
if !strings.Contains(md, "framework:") || !strings.Contains(md, "imgui") {
|
|
audit.Issues = append(audit.Issues, "app.md_missing_framework_imgui")
|
|
}
|
|
if !strings.Contains(md, "entry_point:") {
|
|
audit.Issues = append(audit.Issues, "app.md_missing_entry_point")
|
|
}
|
|
}
|
|
|
|
audit.OK = len(audit.Issues) == 0
|
|
results = append(results, audit)
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// cppHasAbout returns true if main.cpp sets cfg.about or passes .about = in a
|
|
// run_app literal initializer.
|
|
func cppHasAbout(src string) bool {
|
|
return strings.Contains(src, "cfg.about") ||
|
|
strings.Contains(src, ".about ") ||
|
|
strings.Contains(src, ".about=") ||
|
|
strings.Contains(src, "about_window_set_info")
|
|
}
|
|
|
|
// cppHasLog returns true if main.cpp sets cfg.log or .log = in a run_app
|
|
// literal initializer.
|
|
func cppHasLog(src string) bool {
|
|
return strings.Contains(src, "cfg.log") ||
|
|
strings.Contains(src, ".log ") ||
|
|
strings.Contains(src, ".log=")
|
|
}
|
|
|
|
// cppHasManualMenubar returns true if main.cpp invokes
|
|
// fn_ui::app_menubar(...) directly. The framework already calls it once per
|
|
// frame, so any manual call is a duplicate.
|
|
func cppHasManualMenubar(src string) bool {
|
|
return strings.Contains(src, "fn_ui::app_menubar(") ||
|
|
strings.Contains(src, "app_menubar(nullptr")
|
|
}
|
|
|
|
// cppHasManualDockSpace returns true if main.cpp invokes
|
|
// ImGui::DockSpaceOverViewport(...) inside its render(). Coexists with the
|
|
// framework's auto dockspace only if cfg.auto_dockspace = false is set.
|
|
func cppHasManualDockSpace(src string) bool {
|
|
return strings.Contains(src, "DockSpaceOverViewport(")
|
|
}
|
|
|
|
// cppHasAutoDockspaceFalse returns true if main.cpp opts out of the
|
|
// framework auto-dockspace via cfg.auto_dockspace = false or
|
|
// .auto_dockspace = false.
|
|
func cppHasAutoDockspaceFalse(src string) bool {
|
|
return 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
|
|
}
|