Files
fn_registry/functions/infra/audit_services_spec.go
egutierrez b9716a7cd6 chore: snapshot WIP previo + flow 0008 + 7 sub-issues (0112-0119)
Snapshot de WIP acumulado de sesiones previas antes de merge wave 1
del flow 0008 (kanban_cpp + agent_runner_api + DoD schema).

Incluye:
- dev/flows/0008-kanban-cpp-and-agent-workflows.md
- dev/issues/0112-0119*.md (7 sub-issues)
- WIP previo en cmd/fn/doctor.go, registry/*, modules/, cpp/, etc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:17:08 +02:00

132 lines
4.0 KiB
Go

package infra
import (
"database/sql"
"fmt"
"strings"
_ "github.com/mattn/go-sqlite3"
)
// ServiceSpecAudit reports drift between an app tagged `service` and the
// `service:` frontmatter block populated by the indexer (issue 0105).
type ServiceSpecAudit struct {
AppID string `json:"app_id"`
Name string `json:"name"`
HasBlock bool `json:"has_block"`
Runtime string `json:"runtime"`
Port int `json:"port"`
HealthPath string `json:"health_endpoint"`
SystemdUnit string `json:"systemd_unit"`
PCTargets []string `json:"pc_targets"`
IsLocalOnly bool `json:"is_local_only"`
RestartPolicy string `json:"restart_policy"`
Issues []string `json:"issues"`
OK bool `json:"ok"`
}
// AuditServicesSpec lists every app with tag `service` and reports whether its
// `service:` frontmatter is complete enough for downstream monitoring
// (services_monitor app, issue 0106).
//
// Rules:
// - block must exist (otherwise IsLocalOnly/runtime are all defaults).
// - runtime is required (one of: systemd-user, systemd-system, docker-compose, stdio, manual).
// - pc_targets must declare >= 1 pc_id.
// - if runtime starts with `systemd-`, systemd_unit is required.
// - if runtime in {systemd-*, docker-compose} and port > 0, health_endpoint is recommended (warning, not failure).
func AuditServicesSpec(registryRoot string) ([]ServiceSpecAudit, error) {
dbPath := registryRoot + "/registry.db"
db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&mode=ro")
if err != nil {
return nil, fmt.Errorf("audit_services_spec: open db: %w", err)
}
defer db.Close()
rows, err := db.Query(`
SELECT id, name,
COALESCE(service_runtime,''),
COALESCE(service_port,0),
COALESCE(service_health_endpoint,''),
COALESCE(service_systemd_unit,''),
COALESCE(service_restart_policy,''),
COALESCE(service_is_local_only,0)
FROM apps
WHERE tags LIKE '%service%'
ORDER BY id
`)
if err != nil {
return nil, fmt.Errorf("audit_services_spec: query: %w", err)
}
defer rows.Close()
var out []ServiceSpecAudit
for rows.Next() {
var a ServiceSpecAudit
var localOnly int
if err := rows.Scan(
&a.AppID, &a.Name,
&a.Runtime, &a.Port, &a.HealthPath, &a.SystemdUnit, &a.RestartPolicy, &localOnly,
); err != nil {
return nil, fmt.Errorf("audit_services_spec: scan: %w", err)
}
a.IsLocalOnly = localOnly != 0
a.HasBlock = a.Runtime != "" || a.SystemdUnit != "" || a.Port != 0 || a.HealthPath != ""
// pc_targets from service_targets table.
tRows, err := db.Query(
"SELECT pc_id FROM service_targets WHERE app_id = ? ORDER BY pc_id",
a.AppID,
)
if err != nil {
return nil, fmt.Errorf("audit_services_spec: service_targets query: %w", err)
}
for tRows.Next() {
var pc string
if err := tRows.Scan(&pc); err != nil {
tRows.Close()
return nil, err
}
a.PCTargets = append(a.PCTargets, pc)
}
tRows.Close()
// Validate.
if !a.HasBlock {
a.Issues = append(a.Issues, "missing service: block in app.md")
}
if a.Runtime == "" {
a.Issues = append(a.Issues, "missing service.runtime")
} else if !validRuntimes[a.Runtime] {
a.Issues = append(a.Issues, "invalid service.runtime: "+a.Runtime)
}
if len(a.PCTargets) == 0 {
a.Issues = append(a.Issues, "missing service.pc_targets (>= 1 required)")
}
if strings.HasPrefix(a.Runtime, "systemd-") && a.SystemdUnit == "" {
a.Issues = append(a.Issues, "runtime systemd-* requires service.systemd_unit")
}
if a.RestartPolicy != "" && !validRestart[a.RestartPolicy] {
a.Issues = append(a.Issues, "invalid service.restart_policy: "+a.RestartPolicy)
}
a.OK = len(a.Issues) == 0
out = append(out, a)
}
return out, rows.Err()
}
var validRuntimes = map[string]bool{
"systemd-user": true,
"systemd-system": true,
"docker-compose": true,
"stdio": true,
"manual": true,
}
var validRestart = map[string]bool{
"always": true,
"on-failure": true,
"none": true,
}