Slice-owned ports: when a table belongs to one use-case
The deino-scry pattern — a private table gets a trait + adapter in its own crate.

Not every table is cross-cutting. Some belong to one slice and one slice only, and forcing them into the shared ledger couples slices that have no business knowing each other. The slice-owned port is the answer: a trait and its adapter, colocated in the slice own crate, behind the same port rule.
INV-6 says persistence is always through a port, never a concrete Pg* or PgPool. Cross-cutting repositories live in deino-ledger. But a use-case slice that owns a private table — one no other slice reads — would, if it put that table repository in deino-ledger, be forcing every other slice to depend on a repository it does not use. That is coupling dressed as sharing. The slice-owned port is the pattern that keeps the port rule without creating false coupling: the trait and its Pg* adapter live in the slice own crate, and the slice coordinator or worker holds an Arc<dyn Store>.
deino-scry is the reference
deino-scry is the sentiment slice. It owns a private table for sentiment series, and it defines SentimentReader — a trait — plus PgSentimentReader, the adapter that talks to Postgres, both in the deino-scry crate. The worker holds Arc<dyn SentimentReader>. The composition root wires the Pg adapter into that Arc; the test wires a mock. No other crate imports SentimentReader, because no other crate needs to read the sentiment table. The port rule holds — the slice never touches sqlx for behavior, only its adapter module does — and the coupling stays inside the slice.

The other two: deino-loom and deino-whisper
The same pattern appears in deino-loom — LoomStore is the port, LoomPersistence is the adapter, both in the deino-loom crate — and in deino-whisper — WhisperStore is the port, PgWhisperStore is the adapter, both in deino-whisper. Each slice owns a table that only it reads, defines its own port, and holds the Arc<dyn Store> at the coordinator or worker. The rule is identical across all three: depend on the trait, never the pool. The only thing that varies is where the trait lives.
A cross-cutting table goes in the shared ledger. A private table stays in its slice. Both are behind a port — the rule does not move, only the address does.
- deino-scry: SentimentReader + PgSentimentReader — the reference pattern.
- deino-loom: LoomStore + LoomPersistence — orchestration owns its persistence.
- deino-whisper: WhisperStore + PgWhisperStore — the webhook delivery worker port.
Why not put everything in deino-ledger
The temptation is to put every repository in deino-ledger for uniformity. The cost is that every slice that imports deino-ledger for a cross-cutting repo — say, the actor repository — also transitively sees the sentiment repository, the whisper queue, and the loom persistence, tables it has no reason to know exist. That is not a performance problem; it is a readability and coupling problem. A slice that does not read the sentiment table should not be able to import its repository. The slice-owned port keeps the dependency graph honest: you can only depend on what you actually use.
What the port buys the slice
A slice-owned port is not a loophole in the port rule; it is the port rule applied to a table only one slice owns. The slice gets the same benefit as a cross-cutting repo: tests swap the adapter for a mock, the domain never touches the pool, and the manifest records the adapter that ran without coupling the domain to storage. The only difference is locality — the trait and adapter live next to the code that uses them, not in a shared crate. That locality is the point: a private table is a private concern, and the architecture lets it stay one. We are building the POC; the three slice-owned ports are built, and the pattern is the reference for any new slice that owns a private table.
The port rule does not say where the trait lives. It says the domain depends on the trait, never the pool. A slice-owned port is the rule applied to a table only one slice owns.
— slice-owned port note