5c7ff8d761
- 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>
729 lines
18 KiB
Go
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))
|
|
}
|
|
}
|
|
}
|