# `AppContext` carries no `Repositories`; `composition` is a dev-only dependency of `presentation` ADR-0006 closed sixteen call sites where a presentation handler called a repository method directly, but explicitly left `AppContext.repos` in place: ~40 sites still read `state.app_ctx.repos..clone()` to pass an `Arc` positionally into a use case that took individual params rather than a deps struct, and migrating all of them at once would have made ADR-0006's handler-extraction change harder to review and revert independently. `check-handler-repos` was written narrowly on purpose, forbidding only the direct-call bypass ADR-0006 closed, not the clone-and-pass pattern this plan (Plan C2) exists to remove. Plan C2 did that migration in five tasks. Tasks 1-3 converted the 23 use cases the plan's initial audit found reading an `Arc` cloned from `Repositories` — across `integrations`/`webhook`, `wrapup`, `watchlist`, `import`, `users`, `movies`, `goals`, `auth`, and `search` — from individual `Arc` parameters to a single `&FooDeps` struct each, registered on `application::Deps` and constructed by `composition::build_deps`, following the convention ~30 other use cases already used. Task 4 replaced the last two direct repository calls in the codebase — `extractors.rs`'s `AdminApiUser` and `AdminUser` both called `repos.user.find_by_id(...)` inline to check admin status — with a new `users::authorize_admin` use case, closing the gap ADR-0006 honestly scoped as handlers-only (these were two axum extractors, not handler functions). Task 5 widened `check-handler-repos` from `handlers/` to all of `crates/presentation/src`, and reported zero offenders. That measurement was wrong. Its pattern was `repos\.[a-z_]+\.clone\(\)` — built to catch the clone-and-pass shape the plan had inventoried — and it could not see two other shapes present in the same tree: a bare reference passed by argument (`&state.app_ctx.repos.diary`, no `.clone()` anywhere) and a `let`-bound access (`if let Some(ref fed_query) = ...repos.federated_profile`, followed by a method call on the *binding* rather than on `repos` itself). `diary::get_diary`, `diary::get_review_history`, and `diary::export_diary` all took their repository argument **by reference** rather than owned, so their three call sites (`handlers/diary.rs`, `handlers/rss.rs`, `handlers/movies.rs`, `handlers/helpers.rs`) were structurally invisible to a regex anchored on `.clone()`. `handlers/users.rs`'s federated-profile lookup bound `repos.federated_profile` with `if let` and called `.get_federated_profile(...)` on the bound variable, which no `repos..(` pattern can match across a `let`. The first attempt at this task found the five live sites by inspection when it went to delete the field the guard was meant to make safe to delete, and correctly returned blocked rather than deleting `AppContext.repos` out from under them; a new Task 7 then converted all five (plus a sixth, non-`repos` internal caller broken purely by `get_diary`'s signature change) before this attempt at Task 6 ran. ## Decision **`AppContext` no longer has a `repos` field, and `presentation` no longer depends on `composition` in production.** `pub repos: Repositories` and the `pub use composition::Repositories;` re-export are both deleted from `crates/presentation/src/context.rs`. There is no route by which any code in `crates/presentation/src` (outside `src/tests/`, which builds its own `Repositories` purely to drive `composition::build_deps` for test fixtures) can reach a repository directly — not a stored field, not an import, not a re-export. Every dependency a handler or extractor needs now comes from `state.app_ctx.deps`, a prebuilt `Arc` assembled once at the composition root. `crates/presentation/Cargo.toml` moves `composition` from `[dependencies]` to `[dev-dependencies]`. After the field deletion, `composition` is referenced only from `src/tests/extractors.rs`, which calls `composition::build_deps` to produce a real `Deps` for test fixtures — a legitimate use of the composition root's public API, just not one production code needs. Confirmed both ways: `cargo tree -p presentation -e normal --depth 1` shows no `composition` anywhere in the production dependency tree; `cargo tree -p presentation -e dev --depth 1` shows it as a direct dev-dependency. `crates/server` (ADR-0006's home for the binary and every backend-selection feature) already depended on `composition` directly in `[dependencies]` — that entry is untouched by this task — because it is the actual composition root, not `presentation`. It still builds a `Repositories` and still needs `AppContext { deps, .. }` without a `repos` field, so this task's field deletion required two small production-adjacent fixes outside `crates/presentation` that no earlier task in this plan touched: `crates/server/src/main.rs::wire_dependencies` (production) and `crates/server/tests/api_test.rs` (its integration-test harness) both used to write `presentation::context::{AppContext, Repositories}` and construct `AppContext { repos, deps, .. }`; both now import `Repositories` from `composition` directly and drop `repos` from the struct literal, keeping the local `repos` binding alive only long enough to pass `&repos` into `composition::build_deps`, the same pattern `presentation`'s own test helper uses. **All use cases that used to read an `Arc` out of `AppContext.repos` — 23 converted in Tasks 1-3, three more (`get_diary`, `get_review_history`, `export_diary`) converted in Task 7 once the guard's blind spot surfaced them, plus two newly-created use cases (`users::authorize_admin` in Task 4, `users::get_federated_profile` in Task 7) — now take a single deps struct.** The two `extractors.rs` admin checks (`AdminApiUser`, `AdminUser`) both now call `users::authorize_admin::execute`, returning `Result, DomainError>` so "user not found" and "repository call failed" stay distinguishable by construction rather than by pattern-matching a `DomainError` variant an adapter happens not to raise today. `handlers/users.rs`'s federated-profile branch now calls `users::get_federated_profile::execute`, which mirrors `Repositories::federated_profile`'s own `Option`-typed optionality (federation can be configured off) rather than inventing a new way to say "this binary doesn't need it." Both migrations preserve their call sites' exact rejection behavior on every path (missing port, repository error, empty result, found result) — verified path-by-path against the pre-migration code in Task 4's and Task 7's own reports, not asserted here. **Two use cases remain on individual params, correctly, because they were never reachable through `AppContext.repos` in the first place:** `movies::request_enrichment::fetch_if_stale` and `movies::enrich_movie::execute`, both called only from `MovieEnrichmentHandler` (`crates/application/src/movies/event_handler.rs`), a `domain::ports::EventHandler` implementor that the composition root constructs directly with individual `Arc` fields, the same way any adapter is wired — never through `state.app_ctx.deps`. `fetch_if_stale` takes fully individual parameters (`&dyn MovieEnrichmentClient`, `&Arc`, plus two value params). `enrich_movie::execute` actually does take a struct, `EnrichMovieDeps` — but one built locally inside `MovieEnrichmentHandler::handle` from its own already-held `Arc` fields, not a struct registered on `application::Deps` or constructed by `composition::build_deps`. Neither shape is what this ADR means by "use cases now take deps structs"; both are correctly out of scope for the same reason `Services`- sourced use cases are (see below), and the plan's original note that they're "adapter-called, don't migrate them" holds up. **This ADR's "use cases take deps structs" claim is scoped to what Plan C2 touched: use cases that used to read `AppContext.repos`.** It is not a claim that every `execute` function in the codebase takes an `application::Deps`-registered struct. Two examples outside that scope, found while fact-checking this ADR, are worth naming so nobody mistakes their shape for a regression: `diary::log_review::execute` is called directly by two presentation handlers (`handlers/diary.rs::post_review` and `::create_review`) with an individual `&state.app_ctx.services.review_logger` argument — legitimate, because `review_logger` comes from `AppContext.services`, a field this plan never touched, not from `repos`. `wrapup::compute::execute`, `import::cleanup::execute`, and `integrations::cleanup::execute` also take individual params, but none of the three is ever called from a presentation handler — `compute` is called internally by `wrapup::handle_requested`, and the two `cleanup` use cases are called only from `crates/application/src/jobs/`, the background-job runner. All four were outside Plan C2's stated scope before this task started and remain untouched by it. **`check-handler-repos` is now a single whole-crate rule: any mention of `app_ctx.repos` outside `src/tests/` is an offender.** The two-pass shape-matching regex (`repos..(`, `repos..clone().(`) is gone. It is gone because it is no longer needed, not because it was simplified for its own sake: with `AppContext.repos` deleted, there is no shape of legitimate production access left to distinguish from an illegal one — no clone-as-argument to permit, no direct call to forbid separately. A single substring match is sufficient and, unlike the old guard, unevadeable: it cannot be blind to a reference pass or a `let`-binding the way the shape-matching guard demonstrably was (see above), because it does not attempt to parse the shape of the access at all — it only asks whether the now-nonexistent field is mentioned. Verified by injecting `app_ctx.repos.user` into `extractors.rs` (a non-test production file) and confirming `make check-handler-repos` fails, naming the exact file and line, before reverting. ## Considered Options - **Leaving the shape-matching two-pass guard in place, just widened to the whole crate** — this is what Task 5 actually did, and it is what let three reference-pass sites and one `let`-binding site through with a clean ✅. Once `repos` no longer exists, keeping the old guard's shape-matching would mean maintaining two regex passes to catch a category of mistake (any mention at all) that a single `grep` already catches completely. Rejected as strictly worse than the simple rule, not merely equivalent to it. - **Deleting `check-handler-repos` entirely, since the compiler now enforces the invariant** — a production file referencing `app_ctx.repos` simply fails to build once the field is gone, so the guard is redundant with `cargo build` in the narrow sense of "will this compile." Rejected because the guard is cheaper to run (`grep` vs. a full workspace build) for `make check`'s fast-path checks, and because it fails with a direct "here's the offending line" message rather than a compiler error several frames removed from the actual mistake (e.g. a struct-literal error at the `AppContext` construction site rather than at the stray reference itself). ## Known follow-ups, out of scope here - Plan D inverts `ActivityPubPort` so `presentation` drops its one remaining adapter dependency, `activitypub` — untouched by this plan, exactly as ADR-0006 recorded. - The pre-existing `follow_backfill_handler.rs` federation-gate bug and the isolated `cargo test -p composition` `worker_deps_wiring` failure (both recorded in ADR-0006) are unaffected. - `diary::log_review`, `wrapup::compute`, `import::cleanup`, and `integrations::cleanup` remain on individual params, correctly — none of them ever read from `AppContext.repos`, so none was ever in this plan's scope. Whether they *should* eventually move to deps structs for consistency with the rest of the codebase is a separate, unstarted question this ADR takes no position on. - `CONTRIBUTING.md`'s architecture rules were updated in this same commit to stop describing use cases as taking `&AppContext` (a claim already false before this plan) and instead describe the `&FooDeps`-struct convention this ADR documents.