---
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 <dir> 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/<id>`. 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).
