feat: Phase 0 of hexagonal refactor — ADR 0016, core scaffold, depguard rules
Problem: the hexagonal-architecture plan (plans/2026-08-15-hexagonal-
architecture.md) needs its foundation — an accepted ADR, the target
directory tree, and machine-checked dependency rules — before any
service extraction starts. Also folds the four outstanding review
findings (F3.1/F5/F6/F7) into the plan: ObservationService owns the
bounded probe-concurrency contract (scheduler.go:133), Phase 9 gates
ExecutionService+PolicyService ≥ 90% with a gating-matrix test,
per-phase abort criteria, and the §3.2 internal/config note.
Change:
- docs/adr/0016-hexagonal-ports-adapters.md records context, decision,
and consequences of the ports & adapters migration.
- internal/domain → internal/core/domain (mechanical import rewrite,
20 files), new internal/core/{ports,app}, internal/adapters trees
with package docs.
- .golangci.yml: depguard rules for §3.1 (core purity, no agent-client
tech in core, nomos isolation — the nomos rules self-activate when
internal/nomos exists in Phase 8). Config migrated to golangci-lint
v2 format so it loads at all (the v1 config errored under v2, masked
by CI's advisory continue-on-error). Verified depguard fires on a
planted openai-go import in internal/core/app.
- CONTRIBUTING.md layout section now shows the core/adapters tree.
Risk: import path churn is mechanical and tests pass unchanged; the
lint config migration surfaces the pre-existing 400-issue baseline
(advisory in CI, unchanged policy) — new/moved packages lint clean.
Verification: go vet ./..., make test (race, core/domain at 100%
coverage), make generate-check, golangci-lint on internal/core/... and
internal/adapters/... — 0 issues; depguard violation probe confirmed.
This commit is contained in:
69
docs/adr/0016-hexagonal-ports-adapters.md
Normal file
69
docs/adr/0016-hexagonal-ports-adapters.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# ADR 0016 — Hexagonal (ports & adapters) architecture for the oikos backend
|
||||
|
||||
Status: accepted (2026-08-15) · Plan: plans/2026-08-15-hexagonal-architecture.md
|
||||
|
||||
## Context
|
||||
The oikos backend grew as delivery-layer packages with business logic inside
|
||||
them. `internal/httpapi` (~3500 LOC) and `internal/mcp` (~2100 LOC, 67 tools)
|
||||
each embed raw SQL next to validation, policy, and audit writes — two parallel
|
||||
silos re-implementing the same use-cases. The scheduler is a ~1100-LOC
|
||||
monolith mixing probe dispatch, health aggregation, signal transitions, and
|
||||
metric writes. There is one accidental port/adapter pair
|
||||
(`secrets.Backend`) and one inverted dependency (`internal/db` imports
|
||||
`checkdefaults`). Use-cases have no home: every new consumer (REST handler,
|
||||
MCP tool, scheduler pass) copy-pastes query + policy + audit logic, and
|
||||
behavior drifts between paths that must agree — the run/approve/execute
|
||||
lifecycle exists in three variants.
|
||||
|
||||
A staff-level review of the refactor plan settled the open questions: one
|
||||
hexagon (not per-binary), nomos stays an external agent client over the wire,
|
||||
aggregate-scoped repository methods instead of a UnitOfWork port, reads bypass
|
||||
services via a shared `ReadModels` port, and the client (web SPA + desktop)
|
||||
moves to its own repository before the backend churn starts.
|
||||
|
||||
## Decision
|
||||
Adopt ports & adapters across the backend, delivered in ten shippable phases:
|
||||
|
||||
- `internal/core/domain` (pure model, stdlib only), `internal/core/app`
|
||||
(application services = use-cases), `internal/core/ports` (driven-port
|
||||
interfaces). Core imports nothing from adapters; adapters and `cmd/*`
|
||||
import core.
|
||||
- All I/O behind named ports implemented by adapters under
|
||||
`internal/adapters/`: postgres (repositories), ssh (CommandExecutor),
|
||||
probes/* (one Checker per check kind), remote (TargetResolver), secrets,
|
||||
events (EventPublisher/SSE), plus the driving adapters httpapi, mcpserver,
|
||||
scheduler, execworker, cli.
|
||||
- One repository method = one transaction = one aggregate's atomic boundary
|
||||
(`§3.6` of the plan). Inputs carry audit/event entries and derived checks;
|
||||
no `pgx.Tx` crosses into core; no UnitOfWork port.
|
||||
- Commands flow through `core/app` services; invariant-free reads flow
|
||||
adapter → `ReadModels` → presenter with no service hop (CQRS-lite).
|
||||
- REST and MCP keep their existing wire formats and become thin driving
|
||||
adapters over the same services; presenters stay per-adapter.
|
||||
- nomos never links core: it consumes oikos exclusively through MCP/REST.
|
||||
Its Phase 8 cleanup defines nomos-local ports (`LLMClient`, `HomelabClient`,
|
||||
`SessionStore`) outside `core/ports`.
|
||||
- The Gitea `webhook` receiver and the desktop shell are leaf utilities —
|
||||
documented, not restructured.
|
||||
- Dependency rules are enforced with `depguard` from Phase 0 (rule 3, the
|
||||
`internal/nomos` bans, activates when that package exists in Phase 8).
|
||||
|
||||
## Consequences
|
||||
- No API, MCP-tool, DB-schema, or wire-format changes ride along; behavior
|
||||
parity is guarded by the existing handler contract tests plus a repository
|
||||
conformance suite asserting atomicity and check-then-act per command method.
|
||||
- `internal/db` shrinks to connection/migrations/sqlcgen inside the postgres
|
||||
adapter; the `db → checkdefaults` inversion disappears with `SeedService`.
|
||||
- Each phase merges green (`make lint test generate-check`, `make test-db`
|
||||
where repositories change) with a patch version bump; per-phase abort
|
||||
criteria revert a merge that breaks transaction semantics. Minor bump at
|
||||
Phase 9 completion.
|
||||
- Cost accepted: ~10 phases of import churn, sqlc path updates under
|
||||
`make generate-check`, and a temporary period where old and new package
|
||||
locations coexist (depguard denies core→adapter imports from Phase 0 so
|
||||
the new tree can never grow the old inversions).
|
||||
- Risk trade-off recorded in the plan: a repository method whose tx span is
|
||||
too narrow is a bug class this design makes possible; the conformance
|
||||
suite is the mitigation, and the check-then-act sites enumerated in the
|
||||
plan (approval decide, entity transition, check derivation, execution
|
||||
claim) are its first assertions.
|
||||
Reference in New Issue
Block a user