Status: accepted.
Follows the ADR-026 precedent for converging divergent naming: policy + mechanical enforcement + direct renames for the existing population (no backward-compat alias shims), each rename in its own task.
The PyO3 layer (the degenbot_rs cdylib, Python module degenbot._ffi)
has two coexisting conventions for registered #[pyclass] types:
- Clean Python-facing names:
ArbitrageEngine,AlloyProvider,Contract,AsyncContract,AnvilFork,CancelHandle,BlockStream,PathIterator,LogFilter,AlloySubscription,Erc20TokenRow, the*Row/*RowInputtypes, and the typed exceptions. Py-prefixed visible names: the grandfather list in the Decision below (27 names at census time).
The consumer layer already splits policy on this: degenbot/dispatch/__init__.py
strips the prefix via alias re-exports (stable companion names for driver
code), while degenbot/bot/__init__.py recommends the prefixed names
(from degenbot.bot import Bot, PyBot, PyBotIo). New types have been picking
a convention by drift, not by policy.
- New
#[pyclass]types MUST use clean Python-facing names: leading capital, noPyprefix. (Py-prefixed Rust type names remain an acceptable Rust-internal convention; only the Python-facingname=registration is constrained here. The*_pypyfunction suffix stays where it marks the FFI seam — the companion packages strip it for driver code.) - A
Py-prefixed Python-visible name is a Rust-internal seam type and may exist only on the grandfather list. The prefix is never extended. - Grandfather list (runtime census 2026-08-17, 27 names):
PyAavePriceOracle,PyBalanceVectorView,PyBot,PyBotIo,PyChainlinkPriceFeed,PyCollateralPositionData,PyConcentratedLiquidityView,PyDatabasePositionQuery,PyDatabaseSnapshot,PyDebtPositionData,PyDexIdentity,PyDispatchCandidate,PyDispatchOutcome,PyDispatcher,PyDivergentPool,PyErc20Token,PyLiquidityPool,PyPayloadComposer,PyPool,PyReservePairView,PySimulateContext,PySolveResult,PySubscription,PySubmitCandidate,PyTxParams,PyTxSigner,PyUserPositionSummary. - Retirement: task
VD5MD5(epicC7D2CH) renames the grandfather population to clean names per the ADR-026 precedent (direct rename, all import sites updated in-repo, no backward-compat aliases). Each rename removes the name from this list and from the gate test's grandfather set in the same commit. - Enforcement:
tests/rust/test_pyclass_naming_gate.pywalks the runtimedegenbot._ffimodule tree and asserts, in both directions, that the set of registeredPy-prefixed class names equals the grandfather list. A newPy-prefixed registration — or a dead list entry — fails the test.
- The
VD5MD5rename is mechanical and auditable: the list IS the scope. - Companion packages (
degenbot.dispatch,degenbot.bot) keep working without alias churn until the rename lands; after it, prefix-stripping aliases become the vestige to remove. - The gate is a runtime test over the built extension: it runs in the same
tests/rustsuite as the registration↔stub drift gate (taskDSWX6Z), so a renamed class that skips its stub update fails both gates.
The grandfather list is empty: all 27 census names were renamed
(task VD5MD5, direct rename, no backward-compat aliases, per the ADR-026
precedent). Mapping:
| Python name before | Python name after | Note |
|---|---|---|
PyBot |
RustBot |
the Python session class degenbot.bot.Bot keeps the clean name; the Rust engine handle is origin-descriptive |
PyBotIo |
RustBotIo |
same collision reasoning |
PyErc20Token |
RustErc20Token |
the Python model class degenbot.erc20.Erc20Token keeps the clean name |
PySubscription |
PoolStateSubscription |
distinct from the Python provider.Subscription wrapper; role-specific name (pool-state-change subscription handle) |
PyDatabaseSnapshot |
RustDatabaseSnapshot |
the Python uniswap.{v3,v4}_snapshot.DatabaseSnapshot sources keep the clean name |
PyDatabasePositionQuery |
RustDatabasePositionQuery |
the Python aave.analysis.orchestrator.DatabasePositionQuery shell keeps the clean name |
PyLiquidityPool |
LiquidityPool |
mechanical |
PyPool |
Pool |
mechanical (structural handle mirroring degenbot_pools::Pool) |
PyDexIdentity |
DexIdentity |
mechanical; degenbot.types already exported this alias — now a direct import |
PyDispatcher, PyTxSigner, PyDivergentPool, PySubmitCandidate, PyTxParams |
Dispatcher, TxSigner, DivergentPool, SubmitCandidate, TxParams |
mechanical; companion aliases in degenbot.dispatch became direct imports |
PySimulateContext, PyDispatchCandidate, PyDispatchOutcome, PySolveResult, PyPayloadComposer |
SimulateContext, DispatchCandidate, DispatchOutcome, SolveResult, PayloadComposer |
mechanical |
PyChainlinkPriceFeed, PyAavePriceOracle |
ChainlinkPriceFeed, AavePriceOracle |
mechanical; degenbot.chainlink / degenbot.aave alias imports became direct |
PyUserPositionSummary, PyCollateralPositionData, PyDebtPositionData |
UserPositionSummary, CollateralPositionData, DebtPositionData |
mechanical (db analysis trio) |
PyReservePairView, PyConcentratedLiquidityView, PyBalanceVectorView |
ReservePairView, ConcentratedLiquidityView, BalanceVectorView |
mechanical (state views) |
Mechanically: the 15 pyclasses with explicit name = "PyX" attributes took
the new value; the 12 struct-default pyclasses gained an explicit
name = "X" (Rust struct names keep the Py qualifier — Rust-internal,
per D1). All Python import sites, companion alias imports (collapsed to
direct imports), .pyi stubs, tests, and examples were updated in the same
change; Python-visible repr/error-message strings in Rust were aligned.
Polars names its raw PyO3 layer PySeries / PyDataFrame, and that
pattern was the original precedent for this repo's PyBot. It fits
Polars because those names are internal: polars.polars is not part
of the documented API, no example imports it, and the public surface is
the clean Python class (polars.Series). There, Py works as a
keep-out marker for internals, and "wrapping mechanism" is a fair label
for things nobody is meant to touch directly.
degenbot inverts that premise: degenbot._ffi is a documented,
first-class seam — .pyi-stubbed, drift-gated against its
registrations (task DSWX6Z), exercised by the README code-block (sybil)
gate, and imported directly by companion modules and shipped examples.
The seam's names are public surface, so:
Pyis zero-information there: every name in the module is PyO3-managed, so the prefix disambiguates nothing and merely repeats the module path's fact.- the colliding names (
PyBotvs.degenbot.bot.Bot,PyErc20Tokenvs.degenbot.erc20.Erc20Token, ...) collide on layer, not mechanism — Python driver class vs. Rust engine handle — so the informative qualifier is the layer origin:RustBotreads as "the Rust engine handle" at exactly the point where the reader crosses the boundary. - Rust is the sole core implementation by architectural commitment
(Rust is the engine; Python is a driver shell), so
Rustencodes a design promise rather than an implementation detail.
Rejected: keep PyBot etc. (Polars precedent). The precedent
depends on the raw layer staying hidden; degenbot's seam is a public,
gated, documented surface, and the prefix there would be noise.
Rejected: uniform Rust* prefix on every handle. Over-prefixing —
clean names remain the default (D1); the prefix is reserved for names
where the clean form is already occupied by a Python type. The Mapping
table above lists each such case and why.
The five collision names kept their origin prefix under the D3 escape hatch. The fork decision retires the prefix entirely: the disambiguator is the module path, not the prefix.
| Python name before | after | collision resolved by |
|---|---|---|
RustBot |
Bot (on degenbot._ffi) |
module path vs. degenbot.bot.Bot |
RustBotIo |
BotIo (on degenbot._ffi) |
no driver-side homonym |
RustErc20Token |
Erc20Token (on degenbot._ffi) |
module path vs. degenbot.erc20.Erc20Token |
RustDatabaseSnapshot |
DatabaseSnapshot (on degenbot._ffi.db) |
module path vs. the uniswap.{v3,v4}_snapshot shells |
RustDatabasePositionQuery |
DatabasePositionQuery (on degenbot._ffi.db) |
module path vs. aave.analysis.orchestrator |
D3's collision escape (origin prefix on collision) is superseded. First-party code imports the handles directly from _ffi (the ADR-013 amendment); files that also bind a same-named Python class alias the import locally (e.g. _TokenHandle, _EngineSnapshot). The naming gate now asserts both the Py* and the Rust* registered-name sets are empty: no origin prefix of any kind, full stop.