Why domain crates depend on traits, never sqlx
The port rule (INV-5): tests swap mocks, business logic never touches a pool.

The moment a domain crate imports a concrete database client, the test for that crate needs a database. That is the moment the architecture dies — not all at once, but one use sqlx::PgPool at a time. The port rule is the one that prevents it.
INV-5 is blunt: AppState repositories are Arc<dyn Trait>, so tests swap mocks. The domain crates — deino-loom, deino-oracle, deino-sisters, deino-scry and the rest — depend on repository traits defined in deino-ledger, never on the Pg*Repository adapters that implement them. The trait is the port; the adapter is the thing that talks to Postgres. The domain never sees the adapter; it sees the interface. That is the whole rule, and it is the one that keeps the test suite fast and the architecture honest.
The trait is the contract
deino-ledger holds repository traits — the abstract operations the domain needs (insert a simulation, read an actor, fetch a sentiment series). The Pg*Repository structs live in the same crate, but the domain crates import only the trait. When deino-loom persists a simulation, it calls a method on a LoomStore trait it holds as Arc<dyn LoomStore>. It does not know whether the thing behind the Arc is a PgLoomPersistence backed by Postgres or a mock built in a test harness. That is the point: it does not need to know.
Why the test does not need a database
A test for deino-loom constructs a mock LoomStore, hands it to the orchestrator, and asserts on what the mock recorded. No Postgres, no migration, no Docker. The test runs in milliseconds, not seconds; it runs in CI without a live database; it runs on a laptop offline. The cost of that speed is the trait boundary — every persistence operation is a method on a trait, which is one layer of indirection. The payoff is that the domain logic is tested in isolation, which is the only way to test domain logic honestly.

A test that needs a database tests the database as much as the domain. The port is what lets you test the domain alone.
- deino-ledger holds repository traits + Pg* adapters — the domain imports only the trait.
- Arc<dyn Trait> at AppState so the composition root swaps — mock in tests, Pg* in prod.
- A domain crate that imports sqlx is a domain crate that needs a live database to test. That is the failure the port rule prevents.
The slice-owned exception
Not every table belongs in deino-ledger. A use-case slice that owns a private table — one no other slice reads — defines a slice-owned port: a trait plus its colocated Pg* adapter, in the slice own crate. deino-scry is the reference: SentimentReader is a trait in deino-scry, PgSentimentReader is the adapter in the same crate, and the worker holds Arc<dyn SentimentReader>. The rule is the same — depend on the trait, never the pool — the only difference is where the trait lives. A cross-cutting table goes in deino-ledger; a private table stays in its slice. Both are behind a port.
What this buys the manifest
The manifest records which adapter ran, but the domain behavior is defined by the trait, not the adapter. That means the provenance record is complete without coupling the domain to the storage. An auditor reading the manifest sees the trait method names and the adapter identity; they do not need to see sqlx inside the domain to understand what the domain did. We are pre-revenue, building the POC; the port rule is what keeps the POC testable as it grows, and what keeps the domain readable when the adapter changes underneath it.
Depend on the trait, never the pool. The day a domain crate imports sqlx is the day the architecture stops being hexagonal and starts being a database app with opinions.
— port rule note