The Garment Sustainability Bot is a Rust application (ported from a now-deleted Python prototype, behaviour preserved). It is built with a modular architecture that separates Discord wiring, the command surface, a service/persistence layer, and a pure correctness-critical scoring kernel.
Stack: poise 0.6 over serenity 0.12 (Discord), sqlx 0.8 + SQLite
(persistence), tokio (async), tracing (logging), dotenvy (config),
anyhow/thiserror (errors).
┌─────────────────────────────────────────────────────────────┐
│ Discord Platform │
└─────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Bot Layer (poise 0.6 / serenity 0.12) │
│ src/bot.rs (intents, presence, on_error mapping) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │sustain- │ │materials │ │ brands │ │ user_ │ │
│ │ability.rs│ │ .rs │ │ .rs │ │commands │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ ┌──────────┐ commands/ (one module per cog) │
│ │ admin.rs │ + mod.rs (registry, is_admin) │
│ └──────────┘ │
└─────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ domain.rs — PURE scoring kernel (the SPARK seam) │
│ no I/O · total · stable C ABI in `mod ffi` │
└─────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Service Layer (src/services.rs, sustainability.rs) │
│ query/service logic over sqlx · analyzer helpers │
└─────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Data Layer (sqlx 0.8, src/models.rs, src/db.rs) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │materials │ │ garments │ │ brands │ │ users │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ migrations/0001_init.sql applied via sqlx::migrate! │
└─────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ SQLite Database (only) │
└─────────────────────────────────────────────────────────────┘
Location: src/bot.rs, src/commands/
Handles Discord interactions via poise/serenity:
bot.rs: builds thepoise::Framework, sets gateway intents (GUILDS, GUILD_MESSAGES, MESSAGE_CONTENT, GUILD_MEMBERS), presence, and maps framework errors to user-facing messages (on_error).commands/: one module per former discord.py cog —sustainability.rs,materials.rs,brands.rs,user_commands.rs,admin.rs.commands/mod.rsholds the command registry (all()), embed colour helpers, and theis_admingate.
Key features:
async/.await(tokio) for Discord interactions- Error handling and
tracinglogging - Admin permission checks (
is_admin: ID inDISCORD_ADMIN_IDSor guild Administrator permission) - Message formatting with serenity embeds
Location: src/domain.rs
A pure, total, correctness-critical scoring kernel: no I/O, no
allocation in the numeric core, total over its documented domain. It holds
all the scoring formulas (material_overall_score, grade,
lifespan_multiplier, garment_sustainability_score,
environmental_impact, add_points, rank, brand_rating_summary,
impact_category, material_recommendation).
mod ffi exposes the numeric core under a stable C ABI:
gsbot_material_overall_score, gsbot_lifespan_multiplier,
gsbot_level_for_points (#[no_mangle] extern "C"). This is the
architecture's verification seam: a formally-verified SPARK/Ada module
can export the same symbols and be linked in place of the Rust bodies via
the hyperpolymath Zig-FFI / Idris2-ABI pattern, with no caller changes —
callers go through the safe Rust wrappers, so substitution is transparent.
Location: src/services.rs, src/sustainability.rs
Business logic and data access:
services.rs: typedsqlxqueries — get-by-name, search, alternatives, top-rated, leaderboard, user get-or-create/update.sustainability.rs: analyzer helpers (tips, impact category text).
Location: src/models.rs, src/db.rs, migrations/
models.rs: row structs for materials, garments, brands, users.db.rs: opens the SQLite pool (SqlitePoolOptions, max 5 connections) and applies migrations viasqlx::migrate!.migrations/0001_init.sql: schema, embedded at compile time and applied automatically at startup. SQLite only — no Postgres.
Location: src/config.rs
.envloaded viadotenvy- Environment variable loading with defaults and validation
- Feature flags (
ENABLE_CACHING,ENABLE_ANALYTICS) validate()fails fast ifDISCORD_TOKENis missing
src/logging.rs:tracingconsole output + optional file logging (tracing-appender)src/cache.rs: in-process cache for performancesrc/fixtures.rs: sample-data loadersrc/bin/:gsbot-load-fixtures,gsbot-export-data,gsbot-backup-db
1. User sends Discord command
↓
2. serenity gateway receives message
↓
3. poise routes to the matching command (src/commands/*.rs)
↓
4. Command parses/validates arguments
↓
5. Service layer (services.rs) queries via sqlx
↓
6. domain.rs computes scores (pure kernel)
↓
7. Results formatted into a serenity embed
↓
8. User points updated (add_points kernel + users table)
↓
9. Response sent via poise reply
Command → services.rs → sqlx → SQLite
↓
cache.rs (if ENABLE_CACHING)
↓
Result
- Presentation: serenity embeds and formatting (
commands/) - Correctness core: pure kernel (
domain.rs) - Business/data access:
services.rs,models.rs,db.rs
A Data { db: SqlitePool, config: Config } is constructed once in
Framework::setup and handed to every command via Context:
let db = &ctx.data().db;
let prefix = &ctx.data().config.discord_prefix;Data access is centralised in service types:
MaterialService::get_by_name(db, "cotton").await?;
GarmentService::get_alternatives(db, &garment).await?;Commands and their aliases are declared with the poise attribute macro:
#[poise::command(prefix_command, aliases("sus", "score"))]
pub async fn sustainability(ctx: Context<'_>, garment: String)
-> Result<(), Error> { /* ... */ }materials ◄────────┐
│ │ Many-to-Many (garment_materials)
└────────► garments
brands (standalone)
users (standalone — tracks Discord users)
materials: material definitions and metricsgarments: garment types and propertiesgarment_materials: association table (garment_id, material_id, percentage)brands: brand information and ratingsusers: user profiles and gamification
(See migrations/0001_init.sql for the exact columns and indexes.)
- SQLite database (the only supported backend)
- In-process caching
- Single bot instance
- Redis caching
- Multiple bot instances with sharding
- Web dashboard with read API
- Metrics and monitoring
- Formal verification of the
domain.rskernel in SPARK/Ada, linked through the existing C-ABI seam
In-crate #[cfg(test)] tests in isolation — notably the domain.rs
kernel tests that pin the scoring formulas (SPARK-ready).
cargo test --all-targets exercises binaries and library; tests use
tempfile / in-memory SQLite for fast, isolated runs.
DISCORD_TOKEN (required), DISCORD_PREFIX, DISCORD_ADMIN_IDS,
DATABASE_URL (SQLite only), DATABASE_ECHO, CACHE_TTL,
CACHE_MAXSIZE, LOG_LEVEL, LOG_FILE, API_TIMEOUT,
API_RETRY_COUNT, ENABLE_CACHING, ENABLE_ANALYTICS. See
src/config.rs.
Config::validate() runs on startup to fail fast (missing token, data
directories).
- Command level: user-friendly messages (the
on_errormapping inbot.rs) - Application level:
anyhow::Error(aliased asError); typed errors viathiserror - Database level:
sqlxresult propagation
tracingconsole output- Optional file output via
tracing-appender(LOG_FILE) - Level via
LOG_LEVEL
- User inputs validated by command argument parsing
- SQL injection prevented via
sqlxbind parameters - Admin command permission checks (
commands::is_admin)
- Secrets via environment variables /
.env(git-excluded) - No hardcoded credentials
- In-process cache (
src/cache.rs), TTL/size configurable
- Indexes on frequently queried fields (see migration)
sqlxconnection pool (max 5 connections)
- Add a
#[poise::command]fn insrc/commands/ - Register it in
commands::all()(src/commands/mod.rs) - Add service methods if needed; update docs and tests
- Add a migration under
migrations/ - Define the row struct in
src/models.rs - Add service methods in
src/services.rs - Add fixtures in
src/fixtures.rs; add tests
- Add a service in
src/services.rs - Add configuration settings in
src/config.rs - Implement caching and rate limiting
- Multi-stage
Containerfile(rust builder → debian-bookworm-slim runtime), non-rootgsbotuser,ENTRYPOINT ["/usr/local/bin/gsbot"] docker-compose.ymlbuilds withdockerfile: Containerfile; SQLite only
- Fleet-level GitHub Actions, including the Hypatia security scan that self-scans this repository
cargo test --all-targets,cargo clippy --all-targets -- -D warnings,cargo fmt --all -- --check
- Structured
tracinglogging, level perLOG_LEVEL, optional file rotation viatracing-appender
- Command usage statistics, response times, error rates
- Rustdoc comments, this architecture doc, the API reference, deployment
guide, and
CLAUDE.mdfor AI agents
- Formal verification: prove the
domain.rsnumeric core in SPARK/Ada and link it through the existing C-ABI seam (no caller changes) - Sharding: scale across multiple gateway shards
- Read API: optional web/JSON read surface
- Richer analytics: opt-in usage metrics