Files
egutierrez 7b0b697b18 feat(0121b): audit_e2e_coverage_go_infra + fn doctor e2e-coverage subcmd
- Crea functions/infra/audit_e2e_coverage.go: AuditE2ECoverage(roots) escanea
  app.md recursivamente, detecta e2e_checks: en frontmatter, retorna
  E2ECoverageReport{total, with_checks, missing, coverage_pct}.
- Crea functions/infra/e2e_coverage_report.go: tipo E2ECoverageReport con
  JSON tags (total, with_checks, missing, coverage_pct).
- Crea types/infra/e2e_coverage_report.md: metadata del tipo para registry.
- Crea functions/infra/audit_e2e_coverage.md: documentacion self-contained
  con Ejemplo, Cuando usarla, Gotchas.
- Crea functions/infra/audit_e2e_coverage_test.go: 3 tests (empty, all-covered,
  partial) — todos pasan.
- Edita cmd/fn/doctor.go: agrega case "e2e-coverage" -> doctorE2ECoverage().
  Output text (tabla tabwriter + lista de apps missing) y --json (E2ECoverageReport).

Acceptance verificado:
  fn doctor e2e-coverage --json -> {total, with_checks, missing, coverage_pct} OK
  fn doctor e2e-coverage        -> tabla text OK
  go test ./functions/infra/... -> 3/3 PASS
  fn show audit_e2e_coverage_go_infra -> indexada OK

task_run: task_d285372493cce2e6 iter 1

Co-authored-by: fn-orquestador <noreply@fn-registry>
2026-05-19 01:45:54 +02:00

729 lines
18 KiB
Go

package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"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)
case "dod":
doctorDod(r, jsonOut)
case "e2e-coverage":
doctorE2ECoverage(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 <grupo>.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_<x> link calls (CMakeLists.txt) - issue 0097
dod Audita bloque dod_evidence_schema en dev/issues/ y dev/flows/ (issue 0114)
e2e-coverage Porcentaje de apps con e2e_checks declarado en su app.md (issue 0121b)
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/<name>/ or projects/<p>/apps/<name>/ (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.")
}
}
func doctorDod(root string, jsonOut bool) {
issuesDir := filepath.Join(root, "dev", "issues")
flowsDir := filepath.Join(root, "dev", "flows")
report, err := infra.AuditDodSchema(issuesDir, flowsDir)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if jsonOut {
emit(report)
return
}
fmt.Println("=== DoD Schema Audit ===")
fmt.Printf("files scanned: %d\n", report.TotalFiles)
fmt.Printf("with schema: %d\n", report.FilesWithItems)
fmt.Printf("total items: %d\n", report.TotalItems)
fmt.Printf("invalid items: %d\n", report.InvalidItems)
if report.InvalidItems == 0 {
fmt.Println("\nAll DoD schemas valid.")
return
}
fmt.Println()
rel := func(p string) string {
if r, err := filepath.Rel(root, p); err == nil {
return r
}
return p
}
for _, f := range report.Files {
if len(f.Errors) == 0 {
continue
}
for _, e := range f.Errors {
fmt.Printf("%s : %s\n", rel(f.Path), e)
}
}
}
func doctorE2ECoverage(root string, jsonOut bool) {
roots := []string{
filepath.Join(root, "apps"),
filepath.Join(root, "cpp", "apps"),
filepath.Join(root, "projects"),
}
report, err := infra.AuditE2ECoverage(roots)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if jsonOut {
emit(report)
return
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "METRIC\tVALUE")
fmt.Fprintf(w, "total\t%d\n", report.Total)
fmt.Fprintf(w, "with_checks\t%d\n", report.WithChecks)
fmt.Fprintf(w, "missing\t%d\n", len(report.Missing))
fmt.Fprintf(w, "coverage_pct\t%.2f%%\n", report.CoveragePct)
w.Flush()
if len(report.Missing) > 0 {
fmt.Println("\nApps without e2e_checks:")
rel := func(p string) string {
if r, err := filepath.Rel(root, p); err == nil {
return r
}
return p
}
for _, m := range report.Missing {
fmt.Printf(" %s\n", rel(m))
}
}
}