Status: accepted. Decision recorded 2026-08-17 (architecture review, encode-seam candidate). Implementation: ergo task NSOFR2 under epic 4GHYBP (follow-up SMOZG3 for the ERC6909 capture it keeps first-class).
The composer funnel encode_cmd_stream (degenbot-executor) was the single public entry
the whole repo encodes through, and its interface carried the grammar's internal
vocabulary at the seam:
- 8 positional args — path infos, three parallel amount arrays
(
optimal_input/hop_outputs/consumed_inputs), three session-scoped deployment addresses, and an options bundle. - A solver-internal invariant re-asked of every caller —
consumed_inputs[i]must be the executable input to hop i; for an over-fed CL hop the solver's clamp sets it toinput_consumed − 1so the on-chain exact-in loop terminates atamountRemaining == 0(UO3JM4 / path-5000 EMPTY-HALT). The invariant is documented in ≥4 files, yet the declarative harness — the one tool meant to exercise encode behaviour on the realcmd_executor— synthesizesconsumed_inputsasonce(optimal_input).chain(hop_outputs), so the clamp path was never tested at runtime. - A legacy precedence rule at the seam —
erc6909_profit: boolvs thecaptureaxis, collapsed byresolve_axes. - Facade rot — a 43-line one-statement
grammar.rspass-through; a crate header still describing the crate as a warmup-slot math leaf; a doc/code split oncheck_mode(doc: 0 for non-ERC-6909; code emits 1, plus an undocumentedSweepToAddress → 3); an ignored_pool_managerparameter; deadfits_int128tombstones.
EncodeRequest(new,degenbot-executor::composers) is the funnel's intake: it owns thePathInfoplus the amount triple plus the axis options, checked at construction (array-length alignment; a mismatch panics loudly naming the array — a programmer error, not aResult). A request without its path is the shape that let amounts be synthesized blind to what the path constrains; path-and-amounts are one unit.EncodeContext(new, same module) carries the session-scoped deployment addresses (executor / PoolManager / WETH); one per session, never per-path.- The funnel becomes
encode_cmd_stream(&EncodeContext, &EncodeRequest) -> Option<Vec<u8>>— name and decline-Nonesemantics unchanged (ADR-030's public-seam shape stands), 8-arg form deleted in the same commit. Hard cutover: every in-tree caller migrates in one pass; there is no parallel implementation (AGENTS.md: no back-compat layer; byte-identity is already gated by the golden corpus + revm matrix). erc6909_profitstays a first-class option. It is the operator's toggle for Uniswap-V4 ERC6909-vault profit capture — modeled end to end (axis,check_mode = 2on-chain assert, warm slots) but not yet production-wired. It is a live feature knob, not dead weight; wiring it correctly is follow-up taskSMOZG3, not this ADR's scope.- The sweep deletes the pass-through, fixes the facade and the
check_modedoc to the code's reality, drops the ignored parameter (and its_ffi.executorstub ripple), and removes the dead tombstones. - The harness earns the depth: a new declarative entry
(
run_chain_with_consumed) accepting caller-supplied per-hop amounts — the shape the production solver commits afterclamp_cl_hop_capacityre-alignshop_outputs[i]/consumed_inputs[i+1](path-73385) — so the productionencode_cmd_streamintake is first exercised with non-synthesized amounts against the realcmd_executor, with a clamped aligned-shape fixture. The over-fed CL pair (clamped profitable / unclamped EMPTY-HALT) is parked with a note: the stubPoolV3is closed-form (single active range, no tick bitmap), so its capacity bound sits at the extremeMIN/MAXprice limits — u128-amount-infeasible for any loopback path at those prices. The profitable clamped-execution / EMPTY-HALT verdict proof lives in the real-PoolManagertier-3 regression (tier3_path5000_v4_clamp.rs); the pure clamp rule (V3SwapOutcome::exact_input_clamp_bound) is unit-green indegenbot-pools. - Internal encoders stay (
encode_cmd_3_hop's 8 pinned call sites included) — the deepening is the interface move; the walker/encoder internals are the grammar-walker continuation's territory.
- Path stays a funnel argument (
(ctx, &PathInfo, &EncodeRequest)): rejected — the request without its path is exactly the drift shape; the funnel's whole interface should be “session context + per-path request.” - Addresses folded into the request: rejected — session scope re-stated per path, and it would make the harness (which has no deployment addresses) build fakes.
- Kill the legacy
erc6909_profitbool at the seam (collapse into thecaptureaxis, deprecate the Python kwarg): rejected on operator decision — the bool is the user-facing toggle for V4 ERC6909-vault capture, which is an intended feature awaiting correct implementation, not a legacy relic. - Additive cutover (new entry alongside the 8-arg funnel, later deletion):
rejected — no external surface depends on the arg shape (the encode funnel is
not exposed to Python at all;
_ffiexposes only warmup-slot/config helpers), and the byte-gates make a hard cutover low-risk. - Result at the value level for length misalignment: rejected — a funnel
argument-shape error is a programmer error; the fixture-driver convention in
this repo is a loud, named failure, and
Optionat the seam stays ADR-030's decline semantics.
- The interface a caller (or test) must learn is two small values; the amount-triple invariants become a property of one constructor instead of prose repeated at every crossing.
- The clamp invariant's runtime home is split by pool fidelity: the
declarative matrix exercises the intake with the clamp's re-aligned amount
shape (clamped commit + aligned
hop_outputs[0], executed against the realcmd_executor), while the real-PoolManagertier-3 regression owns the profitable clamped-execution / EMPTY-HALT verdict (Decision 6, parked rationale). - No new crate edge was added: the harness consumes the engine's
already-committed amounts (the seam's contract), so the pure clamp rule
stays in
degenbot-poolsand the application policy (margin, which hops, forward-alignment) stays in the engine'sclamp_cl_hop_capacity. - The
degenbot-executionseam's decoupled amount bundle (ADR-025 D5) is deliberately untouched; unifying it, if the default adapter deepening proceeds, is a separate decision. - Semver: the public funnel signature changes within 0.6.x — acceptable per
the repo's migration posture; standalone Rust consumers of
encode_cmd_stream(examples,standalone_consumer.rs) migrate in-tree.