Coding Guidelines
Architecture · Runtime behaviour
Ax3l should be easy to navigate, understand, and maintain. Each component should have a clear responsibility, an obvious home, and a defined interface. These guidelines describe the design goals; existing modules may still need to be brought into this structure.
Develop for the current platform
Develop for Ax3l, the locally hosted LLM, SnakeLab, MariaDB, and systemd. Do not add third-party API or vendor integrations, portability layers, or abstractions for hypothetical platforms. This does not exclude the libraries used to build the application.
Run one LLM model at a time. Model evaluation and generic harness support are established; do not design for concurrent models. Work in short iterations with thin, working slices, implementing only the data and behavior needed for the current slice.
Keep shared features, such as event logging and reporting, separate from domain-specific processes and prompts. Introduce shared abstractions for actual reuse. Separate stored event names and data from presentation labels and descriptions; reports should make the conversation and process readable as a story.
Classify classes by responsibility
Ax3l uses a custom Common Unified Development Process as a development style. Categorize each class by its responsibility:
| Category | Responsibility | Examples |
|---|---|---|
| Interface | Bridges a technology, API, or protocol and handles the mechanics of communicating with it. | DbMgr for MariaDB, SnakeLab for simulations, ZeroMQ transport classes |
| Activity | Transforms data without storing state: the T in ETL. | Score distributions and report transformations |
| Entity | Stores data, whether persistent or held only in memory. | A prompt, simulation configuration, or in-memory result |
Control flow sequences the work and makes decisions.
Keep these responsibilities separate to prevent monolithic classes. An activity works on entities and uses interface classes when it needs to read, write, or communicate with an external system. An entity representing persistent data does not also own the database connection or persistence mechanics.
When a class combines technology access, data transformation, and data storage, split those responsibilities into focused collaborators. Use composition to connect them rather than growing one class to do everything. Classification describes a class’s role; it does not require a shared base class or an inheritance hierarchy.
Here, interface class means a technology bridge. Elsewhere in this document, an interface can also mean a component’s public contract; having public methods alone does not make a class an interface class.
Organize code by responsibility
Group related components under ax3l/:
| Package | Responsibility |
|---|---|
app/ |
Application logic and workflows |
activity/ |
Stateless transformations, calculations, and report generation |
entities/ |
Data and prompts |
server/ |
Server entry points and orchestration |
interface/ |
Interfaces to external services and application components |
constants/ |
Shared application constants |
zmq/ |
ZeroMQ transport components |
Add packages when a responsibility needs its own home. Keep the structure focused on the functionality Ax3l actually uses.
Give each major class its own module, named after the class. Small supporting types and private helpers may stay with the component they serve.
Prefer explicit imports from the owning module, for example:
from ax3l.constants.DAx3l import DAx3l
Keep presentation code and its assets together, separate from application
logic. Keep package initializers small. Use __main__.py when a package needs
to be executable, and keep documented entry points working during refactors.
Separate application meaning from implementation mechanics
Application code should call an interface that expresses its intent. That interface translates the request into operations on a lower-level helper. Callers should not bypass the interface or reach into the helper’s resources.
Keep generic helpers independent of Ax3l rules and schema details. Keep application decisions in the layer that understands Ax3l. Use composition to make that relationship explicit.
Trust internal contracts
Ax3l’s internal modules are developed and maintained together. Use clear interfaces, type annotations, and tests to establish their contracts. Do not add runtime type checks, attribute-existence checks, repeated validation, or fallback paths merely to defend against another internal module being used incorrectly.
Validate external inputs at their entry points, such as configuration files, incoming messages, and decoded database results. Once data has been validated and converted into application objects, internal callers should use those objects directly without rechecking the same contract at every layer. Do not add fallback parsing, repair prompts, or retries for contract violations.
Let internal programming errors surface clearly, even if they stop the application. Fix the root cause and add a regression test. Do not hide bugs with broad exception handlers, silent defaults, coercions, or speculative recovery code intended to keep execution going.
Keep resource cleanup and transaction rollback reliable while allowing the failure to propagate. Handle expected external failures at the responsible boundary, preserving their cause. These responsibilities do not justify defensive scaffolding around trusted internal calls.
Use a shared data access layer
Application components that access MariaDB must use the shared data access
layer. DbMgr alone connects to MariaDB and owns connections, cursors, SQL
execution, and transactions. Database modules own their queries and call its
generic SQL methods; application code calls named operations on those modules.
Separate application-specific operations from database mechanics.
The application interface owns:
- Ax3l table names, column mappings, and application queries.
- Conversion between application values and persisted representations.
- Decisions about which operations must succeed together in one transaction.
The database helper owns:
- MariaDB connection creation, credentials, timeouts, and connection cleanup.
- Cursor creation, use, and cleanup. Cursors never escape this layer.
- Generic SQL execution, identifier handling, and bound values; application queries remain in database modules.
- Transaction execution: begin, commit, rollback, and read-only transactions.
- Consistent result handling and database errors that preserve their causes.
Let database errors leave the transaction block so the database helper can roll back and report them. Handle them outside the block; do not swallow an error and continue issuing statements inside the same transaction. Rely on MariaDB’s row locks and constraints rather than adding duplicate consistency checks.
Keep workflow decisions out of the database helper. Application components must not create separate connection or SQL execution mechanisms.
Keep application side effects explicit. Ax3l’s DbMgr initialization creates
the shared logging tables; preserve that established contract. Opening a
connection must not trigger unrelated workflows.
Lean on MariaDB for application data and Ax3l’s own execution state. Persist enough state to delete partial data on restart and continue LLM workflows.
Keep configuration validation behind one interface
Resolve external configuration through one application interface before passing it to internal components. That interface owns application rules and returns a complete configuration or raises a clear error. Internal components use the resolved configuration without validating it again.
If schema validation needs a generic helper, keep it independent of Ax3l rules and behind the configuration interface.
Resolving a configuration must not mutate the caller’s data or share mutable defaults between resolutions. Keep validation errors specific enough to identify the field that failed.
Refactor in reviewable steps
Make one coherent structural change at a time. Preserve behavior when moving or renaming components, and treat behavior changes as separate work.
For each move, update imports, relevant test references, resource paths, and deployment file lists and copying rules. A module that works in the source checkout must also be included in the installed application.
Run the tests relevant to the change. Use the real, disposable DEV MariaDB database for database development and integration checks. For changes to layer boundaries, verify contracts such as transaction atomicity, rollback, resource cleanup, error translation, and use of shared interfaces. Check entry points and asset loading when converting a module into a package.
Distinguish new failures from existing failures. Report verification limits clearly, including when live database integration has not been tested.
Update the CHANGELOG.md
Update CHANGELOG.md when making changes. If there are many low-level changes,
include a ### Summary section immediately below ## [Unreleased] with a short
summary of the changes.