a802f59f55
- cmd/fn/doctor.go - cmd/fn/main.go - cpp/apps/primitives_gallery/playground/tables/CMakeLists.txt - cpp/apps/primitives_gallery/playground/tables/data_table.cpp - cpp/apps/primitives_gallery/playground/tables/data_table_logic.cpp - cpp/apps/primitives_gallery/playground/tables/data_table_logic.h - cpp/apps/primitives_gallery/playground/tables/self_test.cpp - cpp/apps/primitives_gallery/playground/tables/tql.cpp - cpp/apps/primitives_gallery/playground/tables/viz.cpp - cpp/apps/primitives_gallery/playground/tables/viz.h - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
386 lines
9.1 KiB
Go
386 lines
9.1 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"text/tabwriter"
|
|
|
|
"fn-registry/functions/infra"
|
|
)
|
|
|
|
func cmdDoctor(args []string) {
|
|
jsonOut := false
|
|
sub := ""
|
|
for _, a := range args {
|
|
switch a {
|
|
case "--json":
|
|
jsonOut = true
|
|
case "-h", "--help":
|
|
doctorUsage()
|
|
return
|
|
default:
|
|
if sub == "" {
|
|
sub = a
|
|
}
|
|
}
|
|
}
|
|
|
|
r := root()
|
|
|
|
switch sub {
|
|
case "", "all":
|
|
doctorAll(r, jsonOut)
|
|
case "artefacts":
|
|
doctorArtefacts(r, jsonOut)
|
|
case "services":
|
|
doctorServices(r, jsonOut)
|
|
case "sync":
|
|
doctorSync(r, jsonOut)
|
|
case "uses-functions":
|
|
doctorUsesFunctions(r, jsonOut)
|
|
case "unused":
|
|
doctorUnused(r, jsonOut)
|
|
case "cpp-apps":
|
|
doctorCppApps(r, jsonOut)
|
|
case "ml":
|
|
doctorML(r, jsonOut)
|
|
case "vaults":
|
|
doctorVaults(r, jsonOut)
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "unknown doctor subcommand: %s\n", sub)
|
|
doctorUsage()
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func doctorUsage() {
|
|
fmt.Println(`fn doctor — diagnostico read-only del registry y artefactos
|
|
|
|
Usage:
|
|
fn doctor [subcommand] [--json]
|
|
|
|
Subcommands:
|
|
(none)|all Corre todos los checks
|
|
artefacts Salud de apps y analyses (git, venv, app.md, upstream)
|
|
services Estado de apps con tag 'service' (systemd + puerto)
|
|
sync Drift entre pc_locations BD y disco
|
|
uses-functions Audit imports reales vs uses_functions del app.md
|
|
unused Funciones del registry sin consumidores
|
|
cpp-apps Conformidad de apps C++ con cpp/PATTERNS.md (cfg.about, dockspace, menubar)
|
|
ml Entorno ML: GPUs NVIDIA, CUDA toolkit, venv Python, paquetes torch/diffusers, CLIs y vault
|
|
vaults Salud de vaults: directorio, layout, índice, staleness, drift
|
|
|
|
Flags:
|
|
--json Salida JSON (para scripting/agentes)`)
|
|
}
|
|
|
|
func doctorAll(root string, jsonOut bool) {
|
|
if jsonOut {
|
|
all := map[string]any{}
|
|
if v, err := infra.ArtefactDoctor(root); err == nil {
|
|
all["artefacts"] = v
|
|
} else {
|
|
all["artefacts_error"] = err.Error()
|
|
}
|
|
if v, err := infra.ServicesStatus(root); err == nil {
|
|
all["services"] = v
|
|
} else {
|
|
all["services_error"] = err.Error()
|
|
}
|
|
if v, err := infra.PcLocationsDrift(root, ""); err == nil {
|
|
all["sync"] = v
|
|
} else {
|
|
all["sync_error"] = err.Error()
|
|
}
|
|
if v, err := infra.AuditUsesFunctions(root); err == nil {
|
|
all["uses_functions"] = v
|
|
} else {
|
|
all["uses_functions_error"] = err.Error()
|
|
}
|
|
if v, err := infra.FindUnusedFunctions(root); err == nil {
|
|
all["unused"] = v
|
|
} else {
|
|
all["unused_error"] = err.Error()
|
|
}
|
|
if v, err := infra.AuditCppApps(root); err == nil {
|
|
all["cpp_apps"] = v
|
|
} else {
|
|
all["cpp_apps_error"] = err.Error()
|
|
}
|
|
if v, err := infra.AuditMlEnv(root); err == nil {
|
|
all["ml"] = v
|
|
} else {
|
|
all["ml_error"] = err.Error()
|
|
}
|
|
if v, err := infra.VaultDoctor(root); err == nil {
|
|
all["vaults"] = v
|
|
} else {
|
|
all["vaults_error"] = err.Error()
|
|
}
|
|
emit(all)
|
|
return
|
|
}
|
|
|
|
fmt.Println("=== Artefacts ===")
|
|
doctorArtefacts(root, false)
|
|
fmt.Println("\n=== Services ===")
|
|
doctorServices(root, false)
|
|
fmt.Println("\n=== Sync (pc_locations drift) ===")
|
|
doctorSync(root, false)
|
|
fmt.Println("\n=== uses_functions audit ===")
|
|
doctorUsesFunctions(root, false)
|
|
fmt.Println("\n=== Unused functions ===")
|
|
doctorUnused(root, false)
|
|
fmt.Println("\n=== C++ apps standard conformance ===")
|
|
doctorCppApps(root, false)
|
|
fmt.Println("\n=== ML environment ===")
|
|
doctorML(root, false)
|
|
fmt.Println("\n=== Vaults ===")
|
|
doctorVaults(root, false)
|
|
}
|
|
|
|
func doctorCppApps(root string, jsonOut bool) {
|
|
audits, err := infra.AuditCppApps(root)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if jsonOut {
|
|
emit(audits)
|
|
return
|
|
}
|
|
bad := 0
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
|
fmt.Fprintln(w, "STATUS\tAPP\tISSUES")
|
|
for _, a := range audits {
|
|
status := "OK"
|
|
issues := "-"
|
|
if !a.OK {
|
|
status = "FAIL"
|
|
issues = strings.Join(a.Issues, "; ")
|
|
bad++
|
|
}
|
|
fmt.Fprintf(w, "%s\t%s\t%s\n", status, a.AppID, issues)
|
|
}
|
|
w.Flush()
|
|
fmt.Printf("\n%d/%d C++ apps conform.\n", len(audits)-bad, len(audits))
|
|
}
|
|
|
|
func doctorArtefacts(root string, jsonOut bool) {
|
|
checks, err := infra.ArtefactDoctor(root)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if jsonOut {
|
|
emit(checks)
|
|
return
|
|
}
|
|
bad := 0
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
|
fmt.Fprintln(w, "STATUS\tTYPE\tID\tISSUES")
|
|
for _, c := range checks {
|
|
status := "OK"
|
|
issues := "-"
|
|
if !c.OK {
|
|
status = "FAIL"
|
|
issues = strings.Join(c.Issues, "; ")
|
|
bad++
|
|
}
|
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", status, c.Type, c.ID, issues)
|
|
}
|
|
w.Flush()
|
|
fmt.Printf("\n%d/%d artefacts healthy.\n", len(checks)-bad, len(checks))
|
|
}
|
|
|
|
func doctorServices(root string, jsonOut bool) {
|
|
statuses, err := infra.ServicesStatus(root)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if jsonOut {
|
|
emit(statuses)
|
|
return
|
|
}
|
|
if len(statuses) == 0 {
|
|
fmt.Println("No services registered (no apps with tag 'service').")
|
|
return
|
|
}
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
|
fmt.Fprintln(w, "NAME\tUNIT\tACTIVE\tPORT\tLISTENING")
|
|
for _, s := range statuses {
|
|
port := "-"
|
|
listen := "-"
|
|
if s.Port > 0 {
|
|
port = fmt.Sprintf("%d", s.Port)
|
|
listen = fmt.Sprintf("%v", s.PortListening)
|
|
}
|
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", s.Name, s.UnitName, s.UnitActive, port, listen)
|
|
}
|
|
w.Flush()
|
|
}
|
|
|
|
func doctorSync(root string, jsonOut bool) {
|
|
drifts, err := infra.PcLocationsDrift(root, "")
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if jsonOut {
|
|
emit(drifts)
|
|
return
|
|
}
|
|
if len(drifts) == 0 {
|
|
fmt.Println("No drift detected: pc_locations matches disk.")
|
|
return
|
|
}
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
|
fmt.Fprintln(w, "TYPE\tID\tDIR\tSTATUS\tISSUE")
|
|
for _, d := range drifts {
|
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", d.EntityType, d.EntityID, d.DirPath, d.Status, d.Issue)
|
|
}
|
|
w.Flush()
|
|
fmt.Printf("\n%d drift(s) detected.\n", len(drifts))
|
|
}
|
|
|
|
func doctorUsesFunctions(root string, jsonOut bool) {
|
|
audits, err := infra.AuditUsesFunctions(root)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if jsonOut {
|
|
emit(audits)
|
|
return
|
|
}
|
|
bad := 0
|
|
for _, a := range audits {
|
|
if len(a.Missing) == 0 && len(a.Unused) == 0 {
|
|
continue
|
|
}
|
|
bad++
|
|
fmt.Printf("\n%s (%s)\n", a.AppID, a.Lang)
|
|
if len(a.Missing) > 0 {
|
|
fmt.Printf(" missing in app.md: %s\n", strings.Join(a.Missing, ", "))
|
|
}
|
|
if len(a.Unused) > 0 {
|
|
fmt.Printf(" declared but unused: %s\n", strings.Join(a.Unused, ", "))
|
|
}
|
|
}
|
|
if bad == 0 {
|
|
fmt.Println("All apps have matching uses_functions vs imports.")
|
|
} else {
|
|
fmt.Printf("\n%d/%d apps have drift.\n", bad, len(audits))
|
|
}
|
|
}
|
|
|
|
func doctorUnused(root string, jsonOut bool) {
|
|
unused, err := infra.FindUnusedFunctions(root)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if jsonOut {
|
|
emit(unused)
|
|
return
|
|
}
|
|
if len(unused) == 0 {
|
|
fmt.Println("No unused functions.")
|
|
return
|
|
}
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
|
fmt.Fprintln(w, "ID\tLANG\tDOMAIN\tAGE_DAYS")
|
|
for _, u := range unused {
|
|
fmt.Fprintf(w, "%s\t%s\t%s\t%d\n", u.ID, u.Lang, u.Domain, u.AgeDays)
|
|
}
|
|
w.Flush()
|
|
fmt.Printf("\n%d unused functions (candidates to remove).\n", len(unused))
|
|
}
|
|
|
|
func doctorVaults(root string, jsonOut bool) {
|
|
entries, err := infra.VaultDoctor(root)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if jsonOut {
|
|
emit(entries)
|
|
return
|
|
}
|
|
if len(entries) == 0 {
|
|
fmt.Println("No vaults declared (no projects/*/vaults/vault.yaml found).")
|
|
return
|
|
}
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
|
fmt.Fprintln(w, "NAME\tSTATUS\tFILES\tINDEXED\tISSUES")
|
|
ok := 0
|
|
for _, e := range entries {
|
|
issues := "-"
|
|
if len(e.Issues) > 0 {
|
|
issues = strings.Join(e.Issues, "; ")
|
|
}
|
|
fmt.Fprintf(w, "%s\t%s\t%d\t%d\t%s\n",
|
|
e.VaultName, e.Status, e.DiskFiles, e.IndexedFiles, issues)
|
|
if e.Status == "ok" {
|
|
ok++
|
|
}
|
|
}
|
|
w.Flush()
|
|
fmt.Printf("\n%d/%d vaults healthy.\n", ok, len(entries))
|
|
}
|
|
|
|
func doctorML(root string, jsonOut bool) {
|
|
report, err := infra.AuditMlEnv(root)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if jsonOut {
|
|
emit(report)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("GPUs detected: %d\n", len(report.Gpus))
|
|
for _, g := range report.Gpus {
|
|
fmt.Printf(" [%d] %s VRAM: %d/%d MiB Driver: %s CUDA: %s\n",
|
|
g.Index, g.Name, g.VramFreeMb, g.VramTotalMb, g.DriverVersion, g.CudaVersion)
|
|
}
|
|
fmt.Println()
|
|
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
|
fmt.Fprintln(w, "CHECK\tSTATUS\tVERSION\tDETAIL")
|
|
for _, c := range report.Checks {
|
|
version := c.Version
|
|
if version == "" {
|
|
version = "-"
|
|
}
|
|
detail := c.Detail
|
|
if len(detail) > 60 {
|
|
detail = detail[:60] + "..."
|
|
}
|
|
if detail == "" {
|
|
detail = "-"
|
|
}
|
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", c.Name, c.Status, version, detail)
|
|
}
|
|
w.Flush()
|
|
|
|
overall := "OK"
|
|
if !report.OverallOK {
|
|
overall = "INCOMPLETE"
|
|
}
|
|
fmt.Printf("\nOverall ML environment: %s\n", overall)
|
|
}
|
|
|
|
func emit(v any) {
|
|
b, err := json.MarshalIndent(v, "", " ")
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "json error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println(string(b))
|
|
}
|