b9716a7cd6
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>
132 lines
4.0 KiB
Go
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,
|
|
}
|