---
name: vault_doctor
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func VaultDoctor(repoRoot string) ([]VaultDoctorEntry, error)"
description: "Audita la salud de todos los vaults declarados en projects/*/vaults/vault.yaml. Comprueba existencia del directorio, layout estándar, presencia del índice, staleness y drift entre disco e índice. Read-only."
tags: [vault, doctor, health, audit]
uses_functions:
  - "vault_manifest_read_go_infra"
  - "vault_index_open_go_infra"
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports:
  - "fmt"
  - "os"
  - "path/filepath"
  - "strings"
  - "time"
tested: true
tests:
  - "TestVaultDoctor_OK"
  - "TestVaultDoctor_MissingDir"
  - "TestVaultDoctor_NoIndex"
  - "TestVaultDoctor_LayoutDrift"
  - "TestVaultDoctor_EmptyVault"
test_file_path: "functions/infra/vault_doctor_test.go"
file_path: "functions/infra/vault_doctor.go"
params:
  - name: repoRoot
    desc: "Ruta absoluta a la raiz del fn_registry (donde están projects/ y registry.db)."
output: "Slice de VaultDoctorEntry con Status (ok/warning/error), Issues, DiskFiles, IndexedFiles y LastIndexedAt por vault. Error fatal solo si los manifests no se pueden leer."
---

## Checks aplicados

| Check | Condición | Severidad |
|---|---|---|
| `directory_missing` | `e.Path` no existe en disco | error |
| `layout_missing` | no hay `data/` ni `knowledge/` en la raíz del vault | warning |
| `non_standard_layout` | no hay `data/`/`knowledge/` pero sí otros subdirectorios (ej. imagegen_models) | warning |
| `index_missing` | no existe `vault_index.db` | warning |
| `index_stale` | algún archivo en disco tiene mtime > MAX(indexed_at) | warning |
| `index_drift` | count disco != count en tabla `files` | warning |
| `empty_vault` | DiskFiles == 0 | warning |

## Ejemplo

```go
entries, err := infra.VaultDoctor("$HOME/fn_registry")
for _, e := range entries {
    fmt.Printf("%-30s  %-8s  files=%d  issues=%v\n",
        e.VaultName, e.Status, e.DiskFiles, e.Issues)
}
```

## Notas

- Función read-only: nunca escribe en disco ni en ninguna base de datos.
- `countDiskFiles` usa `filepath.WalkDir` sin hash (cheap) — excluye `vault_index.db*`, `.git/` y ficheros ocultos.
- `isIndexStale` también usa WalkDir; compara mtime de archivos con MAX(indexed_at) de la BD.
- El VaultIndexOpen de sólo lectura no crea el DB (si no existe, retorna error y se reporta `index_missing`).
