package main import ( "encoding/json" "fmt" "os" "strings" "text/tabwriter" "fn-registry/functions/infra" ) func cmdDoctor(args []string) { jsonOut := false emitClaudeMd := false sub := "" for _, a := range args { switch a { case "--json": jsonOut = true case "--emit-claude-md": emitClaudeMd = 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 "services-spec": doctorServicesSpec(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) case "copied-code": doctorCopiedCode(r, jsonOut) case "capabilities": if emitClaudeMd { doctorCapabilitiesEmitMd(r) } else { doctorCapabilities(r, jsonOut) } case "app-location": doctorAppLocation(r, jsonOut) case "modules": doctorModules(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) services-spec Audit del bloque service: en app.md de apps tag 'service' (issue 0105) 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 copied-code Detecta cuerpos de funcion del registry copiados en apps sin import (issue 0085k) capabilities Drift entre docs/capabilities/INDEX.md, tags de funciones, y paginas .md (issue 0086) app-location Detecta artefactos (apps/analysis) en carpetas de lenguaje (cpp/apps/, etc.) - issue 0096 modules Drift entre uses_modules (app.md) y fn_module_ link calls (CMakeLists.txt) - issue 0097 Flags: --json Salida JSON (para scripting/agentes) --emit-claude-md (solo capabilities) Genera bloque markdown para CLAUDE.md`) } 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.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 { 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() } if v, err := infra.AuditCapabilityGroups(root); err == nil { all["capabilities"] = v } else { all["capabilities_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) fmt.Println("\n=== Capability groups ===") doctorCapabilities(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) } tableAudits, err2 := infra.AuditCppTableMigration(root) if err2 != nil { fmt.Fprintf(os.Stderr, "warning: table migration audit failed: %v\n", err2) tableAudits = nil } if jsonOut { emit(map[string]any{ "conformance": audits, "table_migration": tableAudits, }) return } // Conformance section. 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)) // 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) { 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 doctorServicesSpec(root string, jsonOut bool) { audits, err := infra.AuditServicesSpec(root) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } if jsonOut { emit(audits) return } if len(audits) == 0 { fmt.Println("No services declared (no apps with tag 'service').") return } bad := 0 w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "STATUS\tAPP\tRUNTIME\tPORT\tHEALTH\tUNIT\tTARGETS\tISSUES") for _, a := range audits { status := "OK" issues := "-" if !a.OK { status = "FAIL" issues = strings.Join(a.Issues, "; ") bad++ } port := "-" if a.Port > 0 { port = fmt.Sprintf("%d", a.Port) } health := a.HealthPath if health == "" { health = "-" } unit := a.SystemdUnit if unit == "" { unit = "-" } targets := strings.Join(a.PCTargets, ",") if targets == "" { targets = "-" } runtime := a.Runtime if runtime == "" { runtime = "-" } fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", status, a.Name, runtime, port, health, unit, targets, issues) } w.Flush() fmt.Printf("\n%d/%d services with complete service: block.\n", len(audits)-bad, len(audits)) } 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)) } func doctorCapabilities(root string, jsonOut bool) { audits, err := infra.AuditCapabilityGroups(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\tGROUP\tIN_INDEX\tDOC\tFN_COUNT\tISSUES") for _, a := range audits { status := "OK" issues := "-" if !a.OK { status = "FAIL" issues = strings.Join(a.Issues, "; ") bad++ } doc := "no" if a.DocExists { doc = "yes" } inIdx := "no" if a.DeclaredInIndex { inIdx = "yes" } fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\t%s\n", status, a.Group, inIdx, doc, a.FunctionCount, issues) } w.Flush() fmt.Printf("\n%d/%d capability groups healthy.\n", len(audits)-bad, len(audits)) } func doctorCapabilitiesEmitMd(root string) { result, err := infra.EmitCapabilitiesMd(root) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } fmt.Print(infra.RenderCapabilitiesMd(result)) } func doctorCopiedCode(root string, jsonOut bool) { entries, err := infra.AuditCopiedCode(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 copied-code detected (exact_copy match).") return } w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "KIND\tSIMILARITY\tAPP_FILE\tAPP_FUNCTION\tREGISTRY_ID") for _, e := range entries { fmt.Fprintf(w, "%s\t%.2f\t%s\t%s\t%s\n", e.Kind, e.Similarity, e.AppFile, e.AppFunction, e.RegistryID) } w.Flush() fmt.Printf("\n%d suspected copy match(es).\n", len(entries)) } func doctorAppLocation(root string, jsonOut bool) { violations, err := infra.AuditAppLocation(root) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } if jsonOut { emit(violations) return } if len(violations) == 0 { fmt.Println("OK: no artefacts under language-named folders.") return } w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "KIND\tLANG\tPATH") for _, v := range violations { fmt.Fprintf(w, "%s\t%s\t%s\n", v.Kind, v.Lang, v.Path) } w.Flush() fmt.Printf("\n%d violation(s): move artefact to apps// or projects/

/apps// (issue 0096).\n", len(violations)) } func doctorModules(root string, jsonOut bool) { checks, err := infra.AuditModulesDrift(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\tAPP\tDECLARED\tLINKED\tMISSING\tEXTRA") for _, c := range checks { status := "OK" if !c.OK { status = "DRIFT" bad++ } decl := strings.Join(c.Declared, ",") if decl == "" { decl = "-" } link := strings.Join(c.Linked, ",") if link == "" { link = "-" } missing := strings.Join(c.MissingLinks, ",") if missing == "" { missing = "-" } extra := strings.Join(c.ExtraLinks, ",") if extra == "" { extra = "-" } fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", status, c.AppID, decl, link, missing, extra) } w.Flush() fmt.Printf("\n%d/%d apps with module drift.\n", bad, len(checks)) if bad > 0 { fmt.Println("Fix: align uses_modules in app.md with target_link_libraries(fn_module_*) in CMakeLists.txt.") } }