# ADR-018: Tracked Debt — `bot_core`↔`solvers` Fusion **Status: accepted (debt tracking).** This ADR records an acknowledged architectural debt and its extraction trigger. No code change ships with it; it exists so the fusion is **tracked** rather than silently baked in. Recorded for epic `2PTFMZ` (task `JVRGO4`). ## Context `degenbot-bot`'s `lib.rs` states that `bot_core` (the `BotState` single-owner state, decoders, reorg journal, verifier, pump) and `solvers` (the Möbius solvers + the `ArbitrageEngine` path/solver/dispatch layer) are a **mutually coupled pair** — ~30 cross-references each way: - `BotState` needs `IntHopState` / `IntV3TickRangeSequence` / decoders from `solvers` to re-derive solve-ready hop states at resolve time. - The `ArbitrageEngine` needs `BotState` / `V3PoolState` / `TickInfo` / `PoolStateSubscriber` from `bot_core` to read/mutate pool state through the shared `Arc>` (ADR-003's engine-then-core lock order). ADR-003 explicitly refuses to extract a `LiquidityMap` generic against this sample-of-one, so the two live in one crate (`degenbot-bot`) rather than behind an artificial shared-trait seam. ### The standalone-Rust-core cost The fusion entangles the engine with `BotState`, so a standalone Rust consumer (`cargo add degenbot`) that wants **only the V2/V3/V4 solve math** cannot get it without dragging in the full `BotState` state machine **plus** `degenbot-rpc` (the WS/IPC/HTTP provider plumbing), `degenbot-db` (the SQLite schema + snapshot readers), `tokio` (the pump's async runtime), `rayon` (the parallel solve fan-out), and `dashmap` (the concurrent registry). This violates the spirit of ADR-005's standalone-Rust-core constraint for the **solve** surface specifically — though the pure swap-math leaves (`degenbot-v2-math`, `degenbot-concentrated-liquidity-math`, `degenbot-balancer-math`, `degenbot-curve-math`, `degenbot-solidly-math`) **are** already standalone and reachable without `degenbot-bot`. The gap is the `ArbitrageEngine` composition layer that sits between the pure math leaves and a standalone consumer. ## Decision **Track the fusion as debt with an explicit extraction trigger.** Do not extract a `solvers-core` crate or a `LiquidityMap` trait today — the sample-of-one ruling still holds. Instead: 1. **Record the trigger.** Extraction happens when a **second engine family** joins — e.g. an `AaveLiquidationEngine`, or a split `SolidlyEngine` from a future Solidly rename/separation. At that point the "engine reads `BotState`" coupling is no longer sample-of-one (two engines need the same state-reading surface), and a shared trait becomes justified. Until then, the cross-references are the cost of one engine family and one state owner co-evolving. 2. **Record the sketch.** When the trigger fires, extraction would look like one of: - A `solvers-core` crate holding the `ArbitrageEngine` + the `IntHopState` / `IntV3TickRangeSequence` intake, depending on a `LiquidityMap` trait (the trait ADR-003 deferred) rather than on `BotState` concretely. `degenbot-bot` then implements `LiquidityMap` for its `V2/V3/V4` state maps; a standalone consumer can use `solvers-core` against its own `LiquidityMap` impl without `BotState`. - Or, if the trait proves premature, a `solvers-core` crate that takes `BotState`-shaped state **by generic parameter** (`ArbitrageEngine`) so `degenbot-bot` is one `S = BotState` instantiation and a standalone consumer another. 3. **Keep the `lib.rs` doc honest.** The `lib.rs` module doc now references this ADR (rather than just stating ADR-003's refusal), so a reader landing on the fusion knows it is **tracked debt with a trigger**, not an accidental coupling. 4. **Record the disposition in the rubric.** The `three-layer-transition.md` rubric (in `docs/migration-guides/`; removed in the stale-docs cleanup `71ec78b2`) gains an entry applying the triage rubric to `degenbot-bot::solvers`: disposition `partial` (the solve math is Rust-owned but not reachable standalone), trigger = second engine family. ## Consequences - **No code change today.** The fusion stays; the cross-references stay; the crate-split target stays deferred. This ADR is the tracking artifact, not the extraction. - **The standalone solve-surface gap is documented.** A standalone consumer wanting the `ArbitrageEngine` today must take `degenbot-bot` (and its transitive `degenbot-rpc` / `degenbot-db` / `tokio` / `rayon` / `dashmap` deps). The pure swap-math leaves **are** standalone; the engine layer is not. This is the known, tracked cost. - **The trigger is named, not vague.** "Second engine family" is the signal; until then, extraction would be premature per the sample-of-one ruling. - **No regression on the stateful topology.** ADR-003's engine-then-core lock order, ADR-004's typed TickMap seam, and ADR-006's per-chain `Bot` orchestrator are unaffected. This ADR is about the **crate boundary** between engine and state, not the runtime topology. ## References - ADR-003 — `Bot` as the single Rust state owner (the refusal to extract a `LiquidityMap` generic against a sample-of-one; engine-then-core lock order). - ADR-005 — Polars-inspired three-layer FFI (the standalone-Rust-core constraint the `solvers-core` extraction would restore for the solve surface). - `rust/crates/engine/degenbot-bot/src/lib.rs` — the module doc referencing this ADR. - the `three-layer-transition.md` rubric (removed in the stale-docs cleanup `71ec78b2`; recoverable from git history) — its "Dispositions" section carried the `partial` entry for `degenbot-bot::solvers` with the trigger noted. - ergo `2PTFMZ` (epic) / `JVRGO4` (this task).