---
name: audit_services_spec
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func AuditServicesSpec(registryRoot string) ([]ServiceSpecAudit, error)"
description: "Audita apps con tag 'service': reporta drift entre el bloque service: del app.md y los datos requeridos por el monitor (port, health_endpoint, systemd_unit, pc_targets). Lee registry.db read-only via sql.Open. Issue 0105."
tags: [audit, services, doctor, registry, sqlite, issue-0105]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: error_go_core
imports:
  - database/sql
  - github.com/mattn/go-sqlite3
tested: false
file_path: functions/infra/audit_services_spec.go
params:
  - name: registryRoot
    desc: "Ruta absoluta a la raiz del fn_registry (donde vive registry.db)."
output: "Slice de ServiceSpecAudit (uno por app con tag service). OK=false si Issues no esta vacio."
---

# audit_services_spec

Reporta apps con tag `service` cuya `service:` block esta incompleta.

## Cuando usarla

- Subcomando `fn doctor services-spec` (este es su unico consumer hoy).
- Antes de desplegar `services_monitor` (issue 0106) — si esta funcion devuelve `OK=false` para alguna app, el monitor no puede reconciliar estado.
- En CI/cron para detectar regresiones cuando alguien crea app `tag: service` sin bloque.

## Ejemplo

```go
audits, err := infra.AuditServicesSpec("$HOME/fn_registry")
for _, a := range audits {
    if !a.OK {
        fmt.Println(a.AppID, "issues:", a.Issues)
    }
}
```

## Reglas que valida

- bloque presente (alguno de runtime/systemd_unit/port/health_endpoint != default).
- `runtime` declarado y en allowlist (`systemd-user`, `systemd-system`, `docker-compose`, `stdio`, `manual`).
- `pc_targets` con al menos 1 pc_id (cruzado contra tabla `service_targets`).
- `runtime` empieza con `systemd-` ⇒ `systemd_unit` obligatorio.
- `restart_policy` (si declarada) en `always`, `on-failure`, `none`.

## Gotchas

- Lee `registry.db` en modo `?mode=ro`; si la base no existe o esta locked retorna error.
- `service:` bloque parcial pasa el check `HasBlock=true` pero falla validaciones especificas — ver `Issues[]` para detalles.
- No valida que el `port` este libre o el `systemd_unit` exista en disco; eso lo hace `services_status_go_infra` (runtime check).
