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 }