refactor: Phase 3a — absorb checkdefaults into core/app as pure Derive

Problem: check derivation logic lived in internal/checkdefaults with
the pure decision logic (buildKind, address/user/port resolution)
interleaved with tx I/O (entity_status insert, graph host fallback,
check upserts) — and internal/db importing it was the plan's called-out
inverted dependency.

Change:
- internal/core/app/checkdefaults.go: Derive(tree, target, lookup) —
  the full derivation (monitoring overrides, host fallback via an
  injected HostLookup thunk, per-kind builders) with zero I/O imports.
  Types renamed for the app surface: CheckTarget, CheckDef,
  DeriveResult, Skip; LogDeriveResult.
- internal/adapters/postgres/checks.go absorbs the I/O half:
  EnsureChecks (entity_status row + upsert loop), writeCheck, and
  hostViaGraph. The db→checkdefaults edge is gone — adapters→core is
  the ADR 0016 direction (the Phase 7 SeedService note anticipated
  this; the inversion is fixed a phase early).
- seed.go pending-checks loop uses app.CheckTarget + EnsureChecks;
  mcp formatting/tests follow the renamed types; both test files moved
  to internal/core/app.
- Deliberate behavior note: a hostViaGraph read failure inside the
  thunk now logs a warning and degrades to 'skipped: no address'
  instead of aborting the whole entity-create tx — a monitoring
  derivation gap is visible (warn log + coverage sweep) and self-heals
  on the next mutation; failing the create over a graph-read blip was
  disproportionate.

Verification: go build/vet, full test suite green (app tests exercise
every buildKind branch at their new home).
This commit is contained in:
2026-08-15 23:13:45 +02:00
parent 23c8144436
commit d4f5084a6d
7 changed files with 301 additions and 255 deletions

View File

@@ -6,7 +6,7 @@ import (
"errors"
"fmt"
"github.com/dtoro/oikos/internal/checkdefaults"
"github.com/dtoro/oikos/internal/core/app"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)
@@ -105,7 +105,7 @@ func IngestInventorySeed(ctx context.Context, tx pgx.Tx, data map[string]any) (*
// Entities
entities, _ := data["entities"].([]any)
entityTypes := make(map[string]string) // slug -> type, for edge validation
var pendingChecks []checkdefaults.Target
var pendingChecks []app.CheckTarget
for _, raw := range entities {
eMap, ok := raw.(map[string]any)
if !ok {
@@ -157,8 +157,8 @@ func IngestInventorySeed(ctx context.Context, tx pgx.Tx, data map[string]any) (*
// Default checks are deferred until after relationships are ingested:
// a service has no address of its own and inherits its container's,
// which means the hosting edge has to exist first.
pendingChecks = append(pendingChecks, checkdefaults.Target{
ID: entityID, Slug: slug, Type: typeName, Name: name, Attrs: attrsBytes,
pendingChecks = append(pendingChecks, app.CheckTarget{
ID: entityID.String(), Slug: slug, Type: typeName, Name: name, Attrs: attrsBytes,
})
r.Entities++
@@ -228,11 +228,11 @@ func IngestInventorySeed(ctx context.Context, tx pgx.Tx, data map[string]any) (*
// transaction while surfacing as an unrelated failure several entities
// later.
for _, target := range pendingChecks {
res, err := checkdefaults.Ensure(ctx, tx, tree, target)
res, err := EnsureChecks(ctx, tx, tree, target)
if err != nil {
return nil, fmt.Errorf("default checks for %s: %w", target.Slug, err)
}
checkdefaults.LogResult(target.Slug, target.Type, res)
app.LogDeriveResult(target.Slug, target.Type, res)
r.Checks += res.Created
}