Making Botface AI-Ready: Architecture Improvements
Based on Matt Pocock’s “Your codebase is NOT ready for AI” and software architecture best practices.
Core Thesis
“Your codebase, way more than the prompt that you used, way more than your agents.md file, is the biggest influence on AI’s output.”
AI imposes weird constraints on codebases. If the architecture is wrong:
- AI doesn’t receive feedback fast enough
- AI finds it hard to make sense of things and find files
- Leads to cognitive burnout as humans try to hold AI context + codebase together
The Solution: Deep Modules
Deep Module: A component with a simple interface that hides complex implementation.
Why This Matters for AI
AI struggles with:
- Scattered logic - Functions spread across files
- Wide interfaces - Too many public methods to understand
- Implicit dependencies - Hidden coupling between modules
- No fast feedback - Can’t validate changes quickly
Deep modules solve all of these.
Current State Analysis
✅ What’s Working
- Modular structure - Clear separation:
audio/,wakeword/,llm/, etc. - Trait abstractions -
Gpiotrait allows mock/real implementations - Configuration system - TOML-based config with defaults
- Async architecture - Non-blocking I/O with tokio
⚠️ What’s Not AI-Ready
- Too many public modules - Implementation details exposed
- No automated tests - AI can’t validate changes
- Scattered configuration - Multiple config structs
- Dead code - Unused modules (vision/, ui/) confuse AI
- Missing documentation - AI doesn’t understand “why” decisions
- No integration tests - Can’t test full pipeline
- Implicit state machine - Logic spread across match arms
Recommended Changes
1. Deep Module Interfaces (Critical)
Current:
#![allow(unused)]
fn main() {
pub mod detector;
pub mod buffer;
// AI sees all implementation details
}
Target:
#![allow(unused)]
fn main() {
// Single public struct, hidden implementation
pub struct WakeWordDetector { inner: Inner }
impl WakeWordDetector {
pub fn new(config: &Config) -> Result<Self>;
pub fn predict(&mut self, audio: &[i16]) -> Result<bool>;
pub fn reset(&mut self);
}
}
Action:
- Create narrow public interfaces for each module
- Make implementation modules private (
mod inner;notpub mod) - Document the “contract” in struct-level docs
2. Comprehensive Testing (Critical)
Current: No tests = AI operates blindly
Target:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_wake_word_detects_jarvis() {
let detector = WakeWordDetector::new(&test_config()).unwrap();
let audio = load_test_audio("hey_jarvis.wav");
assert!(detector.predict(&audio).unwrap());
}
}
}
Action:
- Add unit tests for each module
- Create test fixtures (sample audio files)
- Add
cargo testto CI/validation - Use mock implementations for tests
3. Single Configuration Entry Point
Current: Multiple config structs scattered
Target:
#![allow(unused)]
fn main() {
//! src/config/mod.rs
//! Single AI-friendly entry point for all configuration
pub struct Config {
pub audio: AudioConfig,
pub wakeword: WakewordConfig,
pub llm: LlmConfig,
pub tts: TtsConfig,
pub gpio: GpioConfig,
}
impl Config {
/// Load with validation
///
/// # Errors
/// Returns error if config is invalid or files missing
pub fn load() -> Result<Self>;
/// Validate all paths exist
pub fn validate(&self) -> Result<()>;
}
}
Action:
- Consolidate all config in
config/mod.rs - Add validation methods
- Fail fast on invalid config
4. Architecture Decision Records (ADRs)
Create: docs/architecture.md
# Botface Architecture
## Core Principles
1. **Deep Modules**: Each subsystem has a narrow public interface
2. **Platform Abstraction**: Works on Mac (dev) and Pi (prod)
3. **Fail Fast**: Validation at startup, not runtime
4. **Observable**: Structured logging at all transitions
## Module Hierarchy
src/ ├── audio/ # Hardware abstraction (arecord/aplay) ├── wakeword/ # ONNX inference (OpenWakeWord) ├── stt/ # Speech-to-text (whisper.cpp) ├── llm/ # Language model (Ollama HTTP) ├── tts/ # Text-to-speech (Piper) ├── gpio/ # Hardware control (AIY HAT) └── state_machine/ # Orchestration layer
## State Machine
Idle → Listening → Recording → Transcribing → Thinking → Speaking → Idle
## Testing Strategy
- Unit: `cargo test` (fast feedback)
- Integration: Requires Ollama + hardware
- Mock: All hardware calls simulated
Action:
- Write comprehensive architecture.md
- Document “why” for each major decision
- Include testing strategy
5. Feature-Gate Unused Code
Current: vision/, ui/ modules exist but unused
Target:
[features]
default = []
vision = ["opencv", "camera"] # Only compile when needed
faces = ["eframe", "gui"] # LCD face animations
advanced = ["vision", "faces"] # Everything
Action:
- Remove or feature-gate unused modules
- Document feature flags
- Keep core lean
6. Integration Tests
Create: tests/integration_test.rs
#![allow(unused)]
fn main() {
//! End-to-end test of voice assistant pipeline
//!
//! Run: cargo test --test integration_test
#[tokio::test]
async fn test_full_pipeline_wake_to_response() {
// Given: Assistant in listening mode
// When: Wake word detected
// Then: Recording starts → Transcribe → LLM → TTS → Response
}
}
Action:
- Create
tests/directory - Add integration test for full pipeline
- Test with mock implementations first
7. Observable State Machine
Current: State transitions logged ad-hoc
Target:
#![allow(unused)]
fn main() {
async fn transition_to(&mut self, new_state: State) {
tracing::info!(
state.from = %self.current_state,
state.to = %new_state,
activation = self.activation_count,
"State transition"
);
// ...
}
}
Action:
- Add structured logging to all transitions
- Include relevant context (activation count, etc.)
- Use
tracingfields for machine-readable logs
8. AI-Context Comments
Add to each module:
#![allow(unused)]
fn main() {
//! Audio capture from microphone
//!
//! ## AI Context
//! - Uses `arecord` subprocess for ALSA compatibility
//! - Handles 48kHz → 16kHz resampling internally
//! - Returns int16 PCM samples (not float32)
//!
//! ## Testing
//! - `check_audio_device()` validates hardware
//! - Mock mode available: `AudioCapture::new_mock()`
//!
//! ## Common Tasks
//! - Change sample rate: Edit `config.audio.sample_rate`
//! - Add resampling: Use `rubato` in `resample.rs`
}
Action:
- Add “AI Context” section to each module doc
- Document common modification tasks
- Include testing guidance
Enforcement Strategies
1. CI/CD Checks
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Check formatting
run: cargo fmt -- --check
- name: Run clippy (strict)
run: cargo clippy -- -D warnings
- name: Run tests
run: cargo test --all-features
- name: Check documentation
run: cargo doc --no-deps --document-private-items
2. Pre-commit Hooks
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: fmt
name: cargo fmt
entry: cargo fmt -- --check
language: system
pass_filenames: false
- id: clippy
name: cargo clippy
entry: cargo clippy -- -D warnings
language: system
pass_filenames: false
- id: test
name: cargo test
entry: cargo test
language: system
pass_filenames: false
3. Module Interface Validation
Add to justfile or Makefile:
# Check that modules follow deep interface pattern
check-interfaces:
@echo "Checking module interfaces..."
@# Count public items (should be small)
@find src -name '*.rs' -exec grep -c '^pub ' {} \; | \
awk '{sum+=$$1} END {print "Total pub items:", sum}'
@# Ensure no pub mod of implementation
@! grep -r "pub mod inner" src/ || \
(echo "ERROR: pub mod inner found"; exit 1)
@echo "✅ Interface check passed"
4. Documentation Requirements
Add to CONTRIBUTING.md:
## Code Requirements
Every module must have:
1. Module-level doc comment with "AI Context" section
2. All public items documented
3. At least one unit test
4. No `pub` on implementation details
## Checklist
- [ ] `cargo fmt` passes
- [ ] `cargo clippy -- -D warnings` passes
- [ ] `cargo test` passes
- [ ] Documentation builds without warnings
- [ ] Module interface is "deep" (few public items)
5. Architectural Fitness Functions
Add to tests/architecture_test.rs:
#![allow(unused)]
fn main() {
//! Tests to enforce architectural constraints
#[test]
fn test_no_wide_modules() {
// Ensure no module has >5 public items
// This enforces "deep modules" principle
}
#[test]
fn test_all_modules_documented() {
// Ensure every module has //! doc comment
}
#[test]
fn test_no_dead_code() {
// Ensure no #[allow(dead_code)] without justification
}
}
Implementation Roadmap
Phase 1: Foundation (Week 1)
- Write architecture.md
- Add comprehensive tests to one module (e.g.,
gpio) - Create
tests/integration_test.rsshell - Set up CI with strict checks
Phase 2: Deep Modules (Week 2)
- Audit all
pubdeclarations - Convert wide interfaces to deep modules
- Add module-level “AI Context” docs
- Feature-gate unused code
Phase 3: Testing (Week 3)
- Achieve >80% test coverage
- Add integration tests
- Add architecture fitness tests
- Create test fixtures (audio files, etc.)
Phase 4: Observability (Week 4)
- Structured logging throughout
- Add metrics (optional)
- Create debugging guide
- Document common AI tasks
Measuring Success
Metrics
- Test Coverage: Target 80%+
- Module Depth: Average <5 public items per module
- Documentation: 100% public API documented
- CI Pass Rate: 100% (zero tolerance)
- AI Success Rate: Can AI add a feature without breaking things?
Test: Can AI Work With This?
Ask AI to:
- Add a new sound effect (should be 1 file change, tests pass)
- Change wake word threshold (config change, no code)
- Add a new state (state_machine.rs only, tests guide)
- Swap TTS engine (tts/ module only, interface unchanged)
If AI can do these without breaking anything = Success!