368 lines
29 KiB
Markdown
368 lines
29 KiB
Markdown
# Federation is optional at the dependency level, not just in code paths
|
|
|
|
The `federation` feature has existed since ADR-0002 and has been treated by every ADR since as the
|
|
switch that decides whether this instance speaks ActivityPub. It did not do that. It gated code
|
|
paths, HTTP routes and struct fields; it removed nothing from the build. Measured at this branch's
|
|
merge-base `f6ff7dd` with `cargo tree -p server -e normal --prefix none`, deduplicated by
|
|
`sed 's/ (\*)$//' | awk '{print $1" "$2}' | sort -u | wc -l`:
|
|
|
|
| Feature set at `f6ff7dd` | unique crates | `activitypub_federation` |
|
|
| --- | --- | --- |
|
|
| `sqlite` | 357 | present |
|
|
| `sqlite,sqlite-federation` | 357 | present |
|
|
| `postgres` | 368 | present |
|
|
| `postgres,postgres-federation` | 368 | present |
|
|
|
|
Identical, to the crate. The cause was one unconditional manifest edge per backend:
|
|
`crates/adapters/sqlite/Cargo.toml:16` read `sqlite-federation = { workspace = true }`, and
|
|
`crates/adapters/postgres/Cargo.toml:16` the same for `postgres-federation`. Those crates depend on
|
|
`k-ap`, which depends on `activitypub_federation`. Anything that wanted a database therefore got the
|
|
whole ActivityPub stack, and `--no-default-features --features sqlite` compiled, linked and shipped
|
|
it. The feature was a runtime posture, not a build-time decision.
|
|
|
|
Two consequences followed from that, and only the first was known. The first is size: an operator who
|
|
does not want federation still pays for it in compile time, binary size and dependency surface. The
|
|
second needs more care than an earlier draft of this document gave it: it is a read-path bug, not the
|
|
write-path bug it first looked like.
|
|
|
|
Because the whole social stack lived on one struct inside the federation crate, a federation-off
|
|
`crates/server` had nothing to wire `SocialCommand`/`FollowGraphQuery`/`BlockQuery` to, so it wired
|
|
`NoopSocialCommand`/`NoopSocialQuery` — `follow` returned `Ok(())` and wrote nothing, every query
|
|
returned `0` or `vec![]`. It is tempting to read that as "following a user on your own instance
|
|
silently did nothing," directly contradicting ADR-0003. **That reading is wrong: nothing federation-off
|
|
could ever reach `NoopSocialCommand::follow` in the first place.** Every social write route —
|
|
`/social/follow`, `/social/unfollow`, and everything else `SocialCommand` exposes — lives in
|
|
`federation_api_routes()` (`crates/presentation/src/routes.rs:485`) and `federation_html_routes()`
|
|
(`routes.rs:194`), both `#[cfg(feature = "federation")]`, merged into the router only under that gate
|
|
(`routes.rs:459`, `:188`). `crates/server/Cargo.toml` takes `presentation` with `default-features =
|
|
false`, so on a federation-off build `POST /api/v1/social/follow` is a 404 — the handler was never
|
|
compiled in. Nobody could follow anybody, successfully or silently, because there was no route to call.
|
|
`NoopSocialCommand::follow`'s silent `Ok(())` was a latent trap, genuinely worth removing, but it was
|
|
never a shipped bug: correct-looking code sitting behind a door that no federation-off request could
|
|
open.
|
|
|
|
`NoopSocialQuery` sat behind no such door. Three consumers call `FollowGraphQuery` through routes that
|
|
carry **no federation gate at all**: `get_local_profile` (follower/following counts on `/users/{id}`,
|
|
which is an ungated route at `routes.rs:76`), `get_page_viewer` (the pending-follower badge), and
|
|
`get_activity_feed` (the followed-user set behind the activity feed's `filter_following`). All three
|
|
read `deps.social_query`, which `crates/composition` wires from `repos.follow_graph` — before this
|
|
branch, `NoopSocialQuery`, federation-off. **A federation-off instance did not fail to follow; it lied
|
|
about its own follow graph on every profile view and every activity-feed request** — reporting zero
|
|
followers, zero following, no pending requests, no followed users, regardless of what
|
|
`ap_followers`/`ap_following` actually held. That is the live bug this branch fixes, and it is the read
|
|
half, not the write half: the write half was unreachable over HTTP, so its noop cost nothing; the read
|
|
half was reachable everywhere, so its noop cost the truth.
|
|
|
|
## Decision
|
|
|
|
**A `<backend>-social` crate speaks only `domain::ports`; a `<backend>-federation` crate speaks
|
|
ActivityPub; the former must never depend on the latter.** `crates/adapters/sqlite-social` and
|
|
`crates/adapters/postgres-social` are new and hold the implementations of the **seven** *domain* ports
|
|
that the federation crates used to carry, across three types. `SqliteSocialRepository` /
|
|
`PostgresSocialRepository` carry five — `FollowCommand`, `FollowQuery`, `FederatedProfileQuery`,
|
|
`RemoteWatchlistRepository`, `FederationAdminQuery` — and two more sit on their own single-purpose
|
|
types: `SqliteApContentQuery` implements `LocalApContentQuery` (`sqlite-social/src/ap_content.rs:9`),
|
|
and `SqliteRemoteGoalRepository` implements `RemoteGoalRepository`
|
|
(`sqlite-social/src/remote_goals.rs:18`). This is not cosmetic: those last two are exactly what the base
|
|
`sqlite` crate re-exported from the federation crate before this split
|
|
(`crates/adapters/sqlite/src/lib.rs:35`, `:121`) — they are the answer to "why did `sqlite` depend on
|
|
`sqlite-federation` at all." `sqlite-federation` and `postgres-federation` keep the twelve `k_ap` trait
|
|
impls — `ActivityRepository`, `KeypairRepository`, `RemoteActorCache`,
|
|
`AnnounceRepository`, `RemoteReviewRepository`, `DomainBlocklist`, `ActorBlocklist`, and the five
|
|
`follow/` traits. The base backend crates now depend on `-social` unconditionally and on `-federation`
|
|
not at all; the federation crates enter the graph only through the `sqlite-federation` /
|
|
`postgres-federation` features. That inversion is the whole mechanism: the feature is real because the
|
|
edge it gates is the only edge.
|
|
|
|
Measured on this branch with the same command and the same dedup pipeline:
|
|
|
|
| Feature set | unique crates | `activitypub_federation` |
|
|
| --- | --- | --- |
|
|
| `sqlite` | **282** | **absent** |
|
|
| `sqlite,sqlite-federation` | 358 | present |
|
|
| `postgres` | **293** | **absent** |
|
|
| `postgres,postgres-federation` | 369 | present |
|
|
|
|
Both backends drop exactly **76** crates when federation is off, and `grep -c activitypub_federation`
|
|
over the federation-off tree returns `0` for each. The on-counts are 358 and 369 rather than the old
|
|
357 and 368 because `sqlite-social` and `postgres-social` are themselves new crates in the graph;
|
|
against the old build, a federation-off deployment resolves 75 fewer crates.
|
|
|
|
**The 76 crates are visible in the shipped image, not only in `cargo tree`.** Both container images
|
|
were built and compared:
|
|
|
|
```
|
|
docker build --build-arg FEATURES=sqlite . -> 171MB
|
|
docker build --build-arg FEATURES=sqlite,sqlite-federation . -> 190MB
|
|
```
|
|
|
|
**19MB smaller.** This is the measurement that matters to an operator: a crate count is an argument
|
|
about the build graph, whereas the image size is the consequence someone actually deploys. It is also
|
|
the evidence that the decoupling reaches all the way through — a change that only moved `impl` blocks
|
|
around would leave both images the same size.
|
|
|
|
Note the second invocation is what plain `docker build .` does: **the Dockerfile's default is
|
|
`ARG FEATURES=sqlite,sqlite-federation`, i.e. federation-on.** The federation-off image — the entire
|
|
point of this ADR — is only built when asked for explicitly, so `docker build .` on its own does not
|
|
exercise it. Both commands above are the reproduction, and both should be run before a release that
|
|
claims the federation-off deployment works.
|
|
|
|
**The orphan rule forced the struct split, and this is the third consecutive plan in which it has
|
|
dictated a design.** `SqliteFederationRepository` was a two-field struct — `pool` and `instance` —
|
|
carrying **seventeen** trait impls: five owned by `domain`, twelve owned by `k_ap`. Moving five impls
|
|
to a new crate is not a matter of moving five `impl` blocks, because from inside `sqlite-social` both
|
|
`domain`'s traits and a type defined in `sqlite-federation` are foreign, which is E0117. The five
|
|
impls needed a type defined where they live, so `SqliteSocialRepository { pool, instance }` exists,
|
|
and `PostgresSocialRepository` likewise. ADR-0008 records the first two occasions
|
|
(`ApServiceAdapter`, `DomainUserRepoAdapter`); the pattern is now settled enough to state as a rule
|
|
of this codebase: **"move these impls to another crate" always implies "and define a type there to
|
|
carry them."** Plan for the type, not just the impls.
|
|
|
|
The two structs are deliberately *not* symmetric. `SqliteFederationRepository` lost its `instance`
|
|
field entirely — after the five domain impls left, nothing read it, and the alternative on the table
|
|
was an `#[allow(dead_code)]`, which was reversed during review in favour of deleting the field and
|
|
updating its seven crate-internal call sites. `SqliteSocialRepository` keeps `instance`, because
|
|
`follow_repository.rs` reads it in eight places to build the actor URLs ADR-0003 specifies. The
|
|
asymmetry was measured before the postgres half was written, so `postgres-social` was built to the
|
|
corrected shape rather than copied from its sibling and then fixed.
|
|
|
|
**A non-recursive glob misclassified an entire subdirectory, and the correction is worth recording
|
|
because it nearly moved the wrong code.** The plan's first pass classified which files were
|
|
ActivityPub-free using a `src/*.rs` glob. That glob never descends into `src/follow/`, so `follow/`
|
|
was scored as clean and slated to move into the social crate. It is the opposite of clean: all five
|
|
of its traits — `FollowerWriter`, `FollowerReader`, `FollowingWriter`, `FollowingReader`,
|
|
`FollowMigration` — are `k_ap` traits, and its files import `k_ap::{Follower, FollowerStatus,
|
|
RemoteActor, ...}` directly. A recursive rescan before any code moved corrected the classification and
|
|
moved the split from a claimed 1333/359 line division to an actual 923/769. `follow/` stayed put.
|
|
|
|
The near-miss is easy to make because the *local* follow path really is ActivityPub-free — it is just
|
|
a different file. `crates/adapters/sqlite-social/src/follow_repository.rs` imports only `domain`,
|
|
`sqlx`, `chrono`, `async_trait` and `adapter_common`, names no `k_ap` type, and carries the
|
|
`FollowCommand`/`FollowQuery` impls. Two similarly-named things sit side by side: the AP follower
|
|
protocol in `follow/`, and ADR-0003's direct-SQL local follow in `follow_repository.rs`. That the file
|
|
layout already separated them cleanly is ADR-0003's design showing through — the split was legible
|
|
because that decision had been made years earlier. The lesson stands regardless: **scan recursively**.
|
|
This was the second wrong classification produced by a single grep pattern in the same session.
|
|
|
|
**The wiring is correct now; the read path is what a federation-off deployment actually gets today.**
|
|
`application::social::LocalSocialService` implements `SocialCommand`, `FollowGraphQuery` and
|
|
`BlockQuery` against the domain ports only, and `crates/server/src/main.rs` wires one instance of it
|
|
across all three when `federation` is off, replacing three separate noops. The bodies were ported from
|
|
`CompositeSocialAdapter`'s local branches rather than rewritten, and were compared token-by-token
|
|
against the originals during review — the two copies coexisted and agreed for three commits, which is
|
|
what made that comparison possible. `CompositeSocialAdapter` then delegates its local branches to
|
|
`Arc<dyn LocalSocial>` and shrank from 299 to 207 lines; every surviving `SocialCommand` line in it is
|
|
either a delegation or genuinely ActivityPub-specific.
|
|
|
|
What this buys a federation-off build, concretely, is the read half described above:
|
|
`get_local_profile`, `get_page_viewer` and `get_activity_feed` now read real
|
|
`ap_followers`/`ap_following` rows through `LocalSocialService` instead of `NoopSocialQuery`'s
|
|
always-empty answer. **An instance rebuilt federation-off stops lying about its own follow graph** —
|
|
that is the user-visible improvement this branch ships, and the ADR previously did not name it.
|
|
|
|
The write half — `SocialCommand::follow`/`unfollow`/`accept_follow`/`reject_follow` — is now wired
|
|
correctly and errors honestly for a remote target (below), but it has **no HTTP surface** federation-off:
|
|
the social routes that would call it are federation-gated in `presentation`, as established above, and
|
|
this branch does not touch that gating. The wiring exists so it is correct on the day those routes are
|
|
ungated, not because anything reaches it today. Ungating the local-only social routes — splitting
|
|
`federation_api_routes`/`federation_html_routes` into a local-only half (follow, unfollow, accept/reject,
|
|
the counts and lists) and an AP-only half (blocks, remote admin queries, the AP follower/following
|
|
collections) — was raised and considered during review. **Decision: do not ungate them here.** It is a
|
|
deliberate non-goal of this branch and a candidate follow-up, not a gap in it: the split is a
|
|
presentation design task nobody has done or reviewed, since the two halves are currently interleaved
|
|
one function each, and getting it right means deciding case-by-case which of
|
|
`get_followers`/`get_following`/`get_relationship`/etc. are actually AP-agnostic versus AP-flavored in
|
|
their response shape. Recorded in the follow-ups below.
|
|
|
|
**A remote target on a federation-off build now errors, and the error names the actor:** `cannot
|
|
reach remote actor {actor}: this instance was built without the federation feature`
|
|
(`crates/application/src/social/local_service.rs:74`). This is the branch's one intentional behaviour
|
|
change and lands in its own commit (`b8f74eb`). The verb is "reach", not "follow", because the same
|
|
constructor serves five call sites including unfollow and unblock, and "cannot follow" would
|
|
misdescribe four of them. At the four non-follow sites `{actor}` is whatever `actor_url_of` returns,
|
|
which for a persisted row is an AP URL rather than an `@user@host` handle; the message says "remote
|
|
actor", not "handle", so both read correctly.
|
|
|
|
**The wiring fix is guarded by a Makefile grep rather than a test, and that is a limitation, not a
|
|
preference.** The decision lives in `wire_dependencies()` — a private `async fn` in
|
|
`crates/server/src/main.rs`, a *binary* crate. Binary crates expose no library API, so nothing under
|
|
`crates/server/tests/` can call it; `server/tests/api_test.rs` builds `AppState` and `AppContext` by
|
|
hand with its own fakes and never exercises the real wiring either. Making it testable means splitting
|
|
`server` into lib+bin, a structural change well outside this work. `LocalSocialService`'s *behaviour*
|
|
has five unit tests from earlier in the branch; what has no test is the *wiring*, and this repo
|
|
already expresses that class of rule as a Makefile grep guard — four existed, and ADR-0006/0007
|
|
record the convention. `check-federation-off-social` is therefore the idiomatic instrument here rather
|
|
than a consolation prize. It was observed red (three hits at `main.rs:161,164,167`) before the fix and
|
|
green after, and is wired into `make check` (`Makefile:4`), `.PHONY`, and line 47 of both
|
|
`.github/workflows/ci.yml` and `.gitea/workflows/ci.yml`. `make check` now runs **six** guards: the
|
|
five above, plus `check-social-crates-are-ap-free`, added during this review to enforce the invariant
|
|
the whole federation-optional property rests on — a `<backend>-social` crate must never depend on
|
|
`k-ap`, `activitypub`, or anything matching `*-federation`. A single manifest line
|
|
(`k-ap = { version = "0.5.0", registry = "gitea" }` in `sqlite-social/Cargo.toml`) would silently
|
|
restore all 76 crates and 19MB while `make check`, `cargo test` and CI stayed green without it.
|
|
`check-federation-off-social` was also strengthened in the same review: it previously only asserted the
|
|
noops were *absent* from `main.rs`, which passes even if `LocalSocialService` were deleted from the same
|
|
file and nothing were wired at all. It now also asserts `LocalSocialService` is *present*.
|
|
|
|
Its scope is a single file, which is narrower than its siblings and is now documented in the recipe:
|
|
`crates/server/src/main.rs` is the sole wiring site, and `worker` and `tui` do not name
|
|
`SocialCommand`, `FollowGraphQuery` or `BlockQuery` at all — verified by recursive grep over both
|
|
crates' `src/`, zero hits. Relocating the wiring out of `main.rs` would move it out of the guard's
|
|
scope; the comment says to widen the guard to `crates/server/src` if that ever happens.
|
|
|
|
**Both halves share the `ap_followers` and `ap_following` tables, and neither owns the migrations.**
|
|
Those tables are created by `crates/adapters/sqlite/migrations/0003_activitypub.sql` and
|
|
`crates/adapters/postgres/migrations/0001_initial.sql` — the *base* backend crates, which both halves
|
|
already depend on. This is ADR-0003's arrangement unchanged: a local follow writes the same rows in
|
|
the same format as a remote one, which is what makes local relationships visible to the fediverse
|
|
through the AP collection endpoints without any duplication. The consequence for maintenance is that
|
|
**a schema change to either table touches both crates**, and neither crate's directory is where you
|
|
would look for the migration. This is a real coupling, accepted deliberately, because the alternative
|
|
— a separate `local_follows` table — was rejected in ADR-0003 as two sources of truth for one concept.
|
|
|
|
**A latent bug in `worker` was exposed, not caused, by cutting the dependency edge.**
|
|
`crates/worker/src/main.rs` calls `tokio::signal::ctrl_c()` with no feature gate, but the workspace
|
|
`tokio` (`Cargo.toml:44`) declares only `macros, net, rt, rt-multi-thread, sync, time` — no `signal`.
|
|
It had been arriving transitively, through `sqlite → sqlite-federation → activitypub_federation`.
|
|
Removing that edge broke the worker build, which looked like collateral damage and was not: the worker
|
|
had a hard requirement satisfied by a dependency four hops away that it did not declare and had no
|
|
reason to expect. It would have broken the moment anything else in the graph shifted. Diagnosed by
|
|
stashing back to the pristine tree to confirm the transitive source, and fixed at the place the
|
|
requirement actually lives — `crates/worker/Cargo.toml:31` now reads
|
|
`tokio = { workspace = true, features = ["signal"] }`. This is the kind of thing a decoupling finds
|
|
that no amount of reading finds.
|
|
|
|
### What this ADR does not claim
|
|
|
|
**Blocks remain federation-only, and `LocalSocialService::block`/`unblock` always error.** There is no
|
|
domain port for block storage — blocking is `k_ap::ActorBlocklist` and `k_ap::DomainBlocklist`, both
|
|
`k_ap` traits on the federation struct — so making blocks work federation-off is a new port and new
|
|
tables, not a wiring change. It was not attempted.
|
|
|
|
**`LocalSocialService::get_blocked` returns `Ok(vec![])` federation-off — the same stale-row shape this
|
|
section already discusses at length for `FederationAdminQuery`, and worth naming rather than leaving
|
|
implicit.** `block`/`unblock` always error, so no block can be *recorded* through this path either way,
|
|
but an instance that accumulated blocks while federation-ON and was then rebuilt federation-OFF would
|
|
report none. Operationally harmless: block enforcement itself lives in `k_ap`, absent federation-off,
|
|
and the route that exposes this list (`/social/blocked`) is federation-gated, so nothing acts on the
|
|
stale answer. But it is the same class of claim as the paragraph below makes for
|
|
`FederationAdminQuery`, and this section should say so plainly rather than naming `block`/`unblock` and
|
|
skipping the query.
|
|
|
|
**`FederationAdminQuery` is still noop'd federation-off, returning an empty list, and the empty list
|
|
is an invariant rather than an assumption.** The weaker argument — "you cannot follow a remote actor
|
|
federation-off, so empty is honest" — is only plausible. The stronger one is provable from the wiring
|
|
this change installs: `LocalSocialService::follow_resolved` hard-errors on any non-`Local` target
|
|
(`local_service.rs:253-256`) and `block`/`unblock` always error, so a federation-off build **cannot
|
|
persist a remote follow through this path at all**. The empty list is enforced, not estimated. A
|
|
comment at the wiring site now records this so a future reader does not re-derive it.
|
|
|
|
There is one exception, and it is a display artifact rather than a functional regression: **an
|
|
instance that ran federation-ON, accumulated remote follow rows, and was then rebuilt federation-OFF
|
|
would report zero remote follows while the rows still exist.** Those follows cannot be acted on
|
|
without federation anyway, so nothing is lost operationally, but the admin view understates reality.
|
|
Wiring `FederationAdminQuery` for real is viable whenever that is judged worth surfacing — both
|
|
`-social` crates implement it, and both are present federation-off. It was left noop'd because doing
|
|
otherwise would surface rows the build cannot act on.
|
|
|
|
**"The federation-off builds compile" is not "they are clippy-clean."**
|
|
`cargo clippy -p worker --no-default-features --features sqlite -- -D warnings` still fails, with two
|
|
errors: unused `remote_goal` and `FollowBackfillHandler is never constructed`. ADR-0008 recorded
|
|
*three* here; the third, unused `app_config`, is incidentally gone because this branch hoists
|
|
`let instance` above `build_database_adapters` in both binaries and derives it from `app_config`
|
|
unconditionally. The remaining two predate this work and no gate runs this combination. Likewise
|
|
`cargo build -p server --no-default-features --features sqlite` emits one pre-existing warning,
|
|
unused `ap_content_repo` at `main.rs:65` (introduced at `ac7edd6`, long before this branch), and the
|
|
single-backend postgres builds emit unreachable-pattern warnings on `DbPool` matches that are
|
|
inherent to compiling a two-variant enum with one variant enabled. All warnings, none errors, none
|
|
new.
|
|
|
|
**The known-broken feature combinations ADR-0008 recorded are unchanged.**
|
|
`--features sqlite,federation` on `server` still fails — `federation` is a bare meta-feature that does
|
|
not imply `dep:activitypub` — and `cargo check -p presentation --no-default-features --all-targets`
|
|
still fails with the same two pre-existing errors. This branch neither fixed nor worsened either.
|
|
|
|
## Considered Options
|
|
|
|
- **Gate the existing `sqlite-federation` dependency behind the feature instead of splitting the
|
|
crate** — the obvious one-line fix, and the reason it does not work is the whole point. The five
|
|
domain-port impls the rest of the system needs unconditionally (`FollowCommand`, `FollowQuery`,
|
|
`FederatedProfileQuery`, `RemoteWatchlistRepository`, `FederationAdminQuery`) lived on the same
|
|
struct in the same crate as the twelve `k_ap` impls. Gating the edge would have removed those five
|
|
too, and the `federation`-off build would have had no follow storage at all — which is precisely the
|
|
state that produced the silent-noop bug. The dependency could not be made conditional until the code
|
|
behind it was separated by what it speaks.
|
|
- **`#[cfg(feature = "federation")]` inside the existing crates rather than two new crates** —
|
|
cheaper in file moves, and rejected because a cfg inside a crate does not remove the crate's
|
|
manifest dependency on `k-ap`. `cargo tree` would have been unchanged, `activitypub_federation`
|
|
would still resolve, and the headline measurement would still read 357/357. Conditional compilation
|
|
cannot subtract a dependency edge; only the manifest can.
|
|
- **Keep the noops and document that federation-off means no social features** — internally
|
|
consistent, and rejected because it contradicts ADR-0003 on the merits, not merely on the docs. That
|
|
ADR's entire argument is that a local follow is two SQL writes and does not need ActivityPub. A build
|
|
that omitted ActivityPub and therefore omitted local-follow *storage and querying* would prove
|
|
ADR-0003 wrong; the split proves it right at the storage and service level — the HTTP surface for the
|
|
write path remains gated regardless of which option was chosen here, see the follow-up below.
|
|
- **Put `follow_resolved` on `SocialCommand` instead of on a new sibling trait** — the smaller-looking
|
|
option, rejected by enumerating implementors rather than by taste. Only `LocalSocialService` and one
|
|
domain test stub satisfy all four original `LocalSocial` bounds; `CompositeSocialAdapter` and
|
|
`InMemorySocialRepository` are real `SocialCommand` implementors that are *not* `LocalSocial` and
|
|
could not meaningfully implement `follow_resolved`. Extending `SocialCommand` would have stranded
|
|
two types. `LocalSocial` is now a marker supertrait over five traits with an unconstrained blanket
|
|
impl, matching `k_ap::FollowRepository`'s precedent over its own five.
|
|
- **Resolve the follow target twice on the local path** — what the plan originally prescribed, and
|
|
what the first implementation did: the composite called `resolve_target` to choose a branch, then
|
|
handed the raw `FollowTarget` to `local.follow`, which resolved it again. Cost was a redundant
|
|
`find_by_username` per handle-based local follow plus a race in which a user deleted between the two
|
|
resolutions makes a *federation-ON* deployment emit "this instance was built without the federation
|
|
feature", which is nonsense in that configuration. Replaced by `ResolvedFollow::follow_resolved`, so
|
|
the composite passes the identity it already computed. The five `LocalSocialService` tests passed
|
|
byte-unmodified across that change, which is the evidence that plumbing moved and behaviour did not.
|
|
|
|
## Known follow-ups, out of scope here
|
|
|
|
- **Ungating the local-only social routes** (`/social/follow`, `/social/unfollow`, accept/reject, the
|
|
counts and lists in `federation_api_routes`/`federation_html_routes`) so a federation-off build gets an
|
|
HTTP surface for the write path described above. Considered and deliberately deferred during review,
|
|
not because it is undesirable but because it is unreviewed design work: it needs splitting those two
|
|
functions into a local-only half and an AP-only half, deciding case-by-case which handlers are
|
|
AP-agnostic versus AP-flavored, and nobody has done or reviewed that split. Until it happens,
|
|
`LocalSocialService`'s write path is correctly wired and unreachable over HTTP federation-off.
|
|
- **The Dockerfile hand-enumerates every workspace member's `Cargo.toml`, and this branch broke it.**
|
|
`docker build .` failed at `RUN cargo fetch` with `failed to read
|
|
/build/crates/adapters/sqlite-social/Cargo.toml`, because the dependency-cache layer copies manifests
|
|
one `COPY` line at a time and the two new crates had no lines. Fixed here by adding them and by
|
|
verifying the list now matches `find crates -name Cargo.toml` exactly (37 against 37), with a
|
|
comment saying that adding a crate means adding a line.
|
|
|
|
**This is a standing trap, not a one-off, and it is still unguarded.** Adding any workspace crate
|
|
silently breaks the container image, and the breakage is invisible to `make check`, to `cargo build`
|
|
in every feature combination, and to CI as currently configured — only a full `docker build` finds
|
|
it. This plan tripped it, and the project's history already records a Docker build broken across
|
|
three merges for the same reason: nobody ran it. **Recommendation, not implemented here** (it is a
|
|
separate decision): a `make` target that diffs the Dockerfile's `COPY` list against
|
|
`find crates -name Cargo.toml` and fails on any difference would close it in a few lines, and is
|
|
exactly the check that was run by hand to confirm this fix. A seventh guard is cheap; a fourth broken
|
|
release is not.
|
|
- **The error message names the actor and the build-time cause, but not which mutation was attempted.**
|
|
A user who unfollows and sees "cannot reach remote actor X: built without the federation feature"
|
|
learns why but not what. Closing this needs either per-site verbs — which reintroduces five message
|
|
strings to keep in sync — or route-level logging. Left open deliberately.
|
|
- **`FederationAdminQuery`'s stale-row case**, described above. Both `-social` crates already implement
|
|
the port federation-off, so this is wiring, not new code, if the staleness is judged worth surfacing.
|
|
- **`crates/infra-wiring` (87 lines across two files) still exists to break a real cycle** — `DbPool`,
|
|
`EventBusBackend` and `AppConfig` need to be visible to both `composition` and the binaries without
|
|
`composition` depending on `application::config`. It looks like a candidate for dissolution and is
|
|
not one; the cycle is genuine. Recorded so the next reader does not re-investigate.
|
|
- **`build_database_adapters`' two backend arms are near-duplicates** (`crates/composition/src/factory.rs:49`).
|
|
Each now constructs a `*SocialRepository` alongside everything else, so the duplication grew slightly
|
|
in this branch. Collapsing them needs a trait over the two backends' `wire()` outputs — real design
|
|
work, not a tidy-up, and not attempted during a refactor whose contract was behaviour preservation.
|
|
- **`tests/local_service.rs`'s `service()` helper takes `user_repo` by value and `follow_store` by
|
|
reference.** Pure polish, explicitly left alone.
|
|
- **`crates/adapters/` now holds 28 directories, up from the 26 ADR-0008 counted.**
|
|
`check-presentation-adapters` iterates those directory names and still passes, so neither new crate
|
|
leaked into `presentation`. ADR-0008's caveat carries over unchanged: the guard assumes directory
|
|
name equals package name, true for all 28 today but not enforced.
|
|
- The deferred items in ADR-0005/0006/0007/0008 are otherwise unaffected by this plan.
|