--- name: audit_projects_coverage kind: function lang: go domain: infra version: "1.0.0" purity: impure signature: "func AuditProjectsCoverage(registryRoot string) ([]ProjectCoverage, error)" description: "Audita la cobertura de los projects del registry frente a sus sub-repos Gitea: comprueba si cada project tiene .git local, remote origin y repo_url declarado, y cuantos de sus hijos (apps + analyses) estan clonados en disco versus solo conocidos por la BD. Motor del subcomando fn doctor projects. Solo lee registry.db + filesystem + git local, nunca la red ni la API de Gitea. Incluye FindOrphanProjectRefs, el check inverso: detecta apps o analyses que declaran un project_id sin fila en la tabla projects (project paraguas huerfano, riesgo de perdida al sincronizar)." tags: [projects, gitea, subrepo, audit, infra, fn-doctor, doctor] uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "error_go_core" imports: ["database/sql", "os/exec", "path/filepath", "strings", "text/tabwriter", "github.com/mattn/go-sqlite3"] tested: true tests: ["healthy project con un hijo sin clonar marca children_missing", "project sin repo_url ni remote marca no_gitea_repo", "project sin directorio en disco marca dir_not_found", "error si registry.db no existe", "repo con origin devuelve true", "repo sin origin devuelve false", "sin issues lo deja claro", "con issues cuenta los afectados", "app con project_id huerfano lo detecta y agrupa ordenado", "app con project_id valido no aparece", "sin huerfanos devuelve slice vacio sin error", "sin huerfanos lo deja claro", "con huerfanos lista ids y cuenta"] test_file_path: "functions/infra/audit_projects_coverage_test.go" file_path: "functions/infra/audit_projects_coverage.go" params: - name: registryRoot desc: "Raiz del repositorio (el directorio que contiene registry.db). Los dir_path relativos de projects, apps y analysis se resuelven contra esta raiz." output: "Slice de ProjectCoverage, una entrada por fila de la tabla projects, con flags de git/remote/repo_url, conteos de hijos clonados vs declarados, y la lista de issues detectados. La funcion de formato FormatProjectsCoverage produce una tabla de texto humano." --- ## Ejemplo ```go package main import ( "fmt" "fn-registry/functions/infra" ) func main() { rows, err := infra.AuditProjectsCoverage("/home/enmanuel/fn_registry") if err != nil { panic(err) } fmt.Print(infra.FormatProjectsCoverage(rows)) } ``` Salida típica: ``` PROJECT GIT REMOTE REPO_URL CHILDREN ISSUES fleet_monitoring ✓ ✓ ✓ 2/2 - fn_monitoring ✓ ✓ ✓ 3/3 - message_bus ✓ ✓ ✓ 3/4 children_missing web_scraping ✗ ✗ ✗ 0/3 no_gitea_repo; children_missing 1/4 projects con problemas de cobertura. ``` ## Check inverso: FindOrphanProjectRefs Mientras `AuditProjectsCoverage` parte de la tabla `projects` y mira hacia abajo (¿están sus hijos clonados?), `FindOrphanProjectRefs` recorre el grafo en sentido contrario: parte de las apps y analyses y mira hacia arriba (¿existe el project paraguas que declaran?). Detecta el drift inverso, apps o analyses cuyo `project_id` no tiene ninguna fila en la tabla `projects`. Es un project huérfano: existe en otro PC y nunca se sincronizó a este, o nunca se creó aquí. Es un riesgo de pérdida silenciosa, porque el enlace del hijo apunta a un project que este registro no conoce. ```go package main import ( "fmt" "fn-registry/functions/infra" ) func main() { orphans, err := infra.FindOrphanProjectRefs("/home/enmanuel/fn_registry") if err != nil { panic(err) } fmt.Print(infra.FormatOrphanProjectRefs(orphans)) } ``` La firma es `func FindOrphanProjectRefs(registryRoot string) ([]OrphanProjectRef, error)`. Cada `OrphanProjectRef` agrupa, por `ProjectID` huérfano, los ids de las apps (`Apps`) y analyses (`Analyses`) que lo referencian, ambas listas ordenadas alfabéticamente y el slice resultante ordenado por `ProjectID`. Cuando todos los hijos apuntan a un project conocido devuelve un slice vacío (no nil) sin error. `FormatOrphanProjectRefs` produce una tabla de texto humano con el `project_id`, cuántas apps y analyses lo referencian y sus ids; si no hay huérfanos imprime una sola línea dejándolo claro. Caso real detectado en este registro: apps con `project_id` ∈ {`element_agents`, `imagegen`, `osint_graph`} sin fila correspondiente en `projects`. Salida típica con huérfanos: ``` PROJECT_ID APPS ANALYSES REFERENCED_BY element_agents 1 0 shell_agent imagegen 1 0 imagegen_ui osint_graph 2 1 graph_explorer, scraper, gliner_glirel_tuning 3 project_id huérfanos (referenciados por hijos pero sin fila en projects). ``` ## Cuando usarla Úsala antes de un `/full-git-pull` masivo o tras clonar el registry en un PC nuevo para saber qué projects están realmente respaldados por su sub-repo Gitea y cuántos de sus hijos (apps y analyses) quedarían sin clonar. También como motor del futuro subcomando `fn doctor projects`: el caller la enchufa desde `cmd/fn/doctor.go` igual que `AuditUsesFunctions` o `AuditServicesSpec`, formatea con `FormatProjectsCoverage` para texto humano y serializa el slice directamente para `--json`. ## Gotchas - Es **impura**: lee `registry.db` (abierto en modo read-only `?mode=ro`), recorre el filesystem y ejecuta `git -C remote get-url origin`. No toca la red ni la API de Gitea, así que no necesita token y es rápida. - `HasRemote` solo se evalúa cuando el project tiene `.git` local; si no hay `.git`, queda `false` sin intentar el comando git. - `gitHasRemoteOrigin` devuelve `false` ante cualquier error (no hay remote `origin`, no es un repo, git no instalado). No distingue "sin origin" de "git ausente"; si necesitas esa distinción, comprueba `git` por separado. - El issue `no_gitea_repo` se emite solo cuando faltan **ambos** indicadores (`!HasRemote && !RepoURLDeclared`). Un project con `repo_url` declarado pero sin clonar (`dir_not_found`) NO se marca `no_gitea_repo` — el repo existe en Gitea, simplemente no está en este disco. - `ChildrenMissing` cuenta los hijos (apps + analyses con ese `project_id`) cuya carpeta no tiene `.git` en disco: son los que se perderían o habría que reclonar. Cero hijos en la BD produce `0/0` y no genera issue. - Si `projects.dir_path` está vacío, se deriva `projects/`. Los `dir_path` ya absolutos se respetan tal cual. - Devuelve error únicamente si `registry.db` no puede abrirse o consultarse. Los projects cuyo directorio no existe SÍ aparecen en el resultado, marcados con `dir_not_found`, para que el caller los muestre en vez de descartarlos en silencio. - `FindOrphanProjectRefs` también es **impura**: lee `registry.db` en modo read-only (`?mode=ro`), pero no toca el filesystem ni git, solo cruza las tablas `projects`, `apps` y `analysis`. Ignora los hijos con `project_id` vacío (no son huérfanos, simplemente no pertenecen a ningún project). Devuelve un slice vacío no-nil cuando no hay huérfanos, así que el caller puede distinguir "sin huérfanos" (slice vacío, error nil) de "fallo al leer la BD" (error no nil).