This document provides comprehensive guidance for AI assistants (like Claude) working with the Krep codebase. It explains the architecture, conventions, workflows, and critical context needed to make effective contributions.
Krep is a Rust-based cardio microdose prescription system that intelligently prescribes 30-second to 5-minute high-intensity cardio sessions throughout the day. It tracks progression, integrates with external strength training signals, and uses a robust WAL-based persistence system.
Current Status: v0.1 (stable core, tray app WIP)
krep/
├── cardio_core/ # Core business logic library
│ ├── src/
│ │ ├── lib.rs # Public API exports
│ │ ├── types.rs # Domain types (movements, sessions, metrics)
│ │ ├── engine.rs # Prescription logic (v1.1 rules)
│ │ ├── progression.rs # Intensity upgrade algorithms
│ │ ├── catalog.rs # Movement/workout definitions
│ │ ├── wal.rs # Write-ahead logging
│ │ ├── csv_rollup.rs # CSV archival and deduplication
│ │ ├── state.rs # User progression state management
│ │ ├── history.rs # Session history loading
│ │ ├── strength.rs # External strength signal integration
│ │ ├── config.rs # Configuration management (TOML)
│ │ ├── error.rs # Error types (thiserror)
│ │ └── logging.rs # Logging setup (tracing)
│ └── Cargo.toml
├── cardio_cli/ # Command-line interface binary
│ ├── src/
│ │ └── main.rs # CLI entry point (clap)
│ ├── tests/ # Integration tests
│ │ ├── integration_tests.rs
│ │ ├── concurrency_tests.rs
│ │ └── corruption_recovery_tests.rs
│ └── Cargo.toml
├── cardio_tray/ # GNOME tray app (WIP)
│ ├── src/
│ │ └── main.rs # GTK4/libadwaita UI
│ └── Cargo.toml
├── Cargo.toml # Workspace configuration
├── Cargo.lock # Locked dependencies
└── README.md # User-facing documentation
The codebase uses Rust's type system to enforce correctness at compile time:
- Enum-based metrics:
MetricSpecuses tagged enums (Reps,Band) instead of stringly-typed maps - Session type safety:
SessionKindenum distinguishes betweenRealsessions (persisted) andShownButSkipped(in-memory only) to prevent accidental persistence of skipped prescriptions - Movement styles:
MovementStyleenum wrapsBurpeeStyleandBandSpecfor type-safe progression - No unsafe code:
#![forbid(unsafe_code)]incardio_core
Data flow: WAL → CSV → Analysis
- Write-Ahead Log (WAL): Append-only JSONL file (
microdose_sessions.wal) with fs2 file locking - Atomic operations: All file writes use atomic operations (write-to-temp + rename)
- CSV rollup: Background archival process with deduplication via UUID tracking
- Zero data loss: WAL never deleted until successfully rolled up and archived to
.processed
File locations (XDG compliant):
- WAL:
~/.local/share/krep/wal/microdose_sessions.wal - State:
~/.local/share/krep/wal/state.json - CSV:
~/.local/share/krep/sessions.csv - Strength signal:
~/.local/share/krep/strength/signal.json
The engine implements intelligent workout selection (see cardio_core/src/engine.rs):
Decision flow:
- Strength-based override: If lower-body strength session within 24h → prefer GTG or Mobility
- VO2 timing: If last VO2 session > 4h ago → prioritize VO2
- Round-robin: Cycle through categories and definitions to avoid repetition
- User-forced category:
--category vo2|gtg|mobilityoverrides all rules
Key functions:
prescribe_next(): Main entry pointdetermine_category(): Category selection logicselect_definition_from_category(): Round-robin within categorycompute_intensity(): Apply progression state to definition
Each microdose definition has independent progression state (see cardio_core/src/progression.rs):
Burpees (upgrade_burpee):
- Increment reps to ceiling (default: 10)
- Then upgrade style: 4-count → 6-count → 6-count-2-pump → seal
- Reset reps after style upgrade
KB Swings (upgrade_kb_swing):
- Linear:
base_reps + level, capped atmax_reps(default: 15)
Pullups (upgrade_pullup):
- Rep progression (band selection is manual via state file)
State persisted in state.json as:
{
"progressions": {
"burpee_emom_5": {
"reps": 5,
"style": { "burpee": "four_count" },
"level": 3,
"last_upgraded": "2025-11-17T10:00:00Z"
}
},
"last_mobility_def_id": "hip_car"
}The catalog is the source of truth for workouts (see cardio_core/src/catalog.rs):
build_default_catalog(): Returns hardcoded catalog (no database)Catalog::validate(): Runs integrity checks (movement references, metric specs)- Extensible via config TOML for custom mobility drills
Adding new movements: Edit catalog.rs directly (no runtime registration yet)
- Modules: One concept per file (e.g.,
engine.rsonly contains prescription logic) - Comments: Doc comments (
///) on all public items, module-level (//!) docs - Imports: Grouped (stdlib → external crates → internal modules)
- Naming:
- Functions:
verb_noun(e.g.,load_recent_sessions) - Types:
PascalCase(e.g.,MicrodoseDefinition) - Modules:
snake_case(e.g.,csv_rollup)
- Functions:
All errors use thiserror (see cardio_core/src/error.rs):
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("Prescription error: {0}")]
Prescription(String),
// ...
}Usage:
- Propagate with
?operator - Library functions return
Result<T> - CLI
main()returnsResult<()>for clean error display
Uses tracing crate (see cardio_core/src/logging.rs):
tracing::info!("Prescribing microdose from category: {:?}", category);
tracing::warn!("Category not found, using fallback");
tracing::debug!("Burpee progression: increased reps to {}", state.reps);Log levels:
info: User-relevant actions (prescription, session logging)warn: Non-fatal issues (fallback logic, missing files)debug: Internal state changes (progression upgrades)error: Failures (file I/O errors, validation failures)
Environment variable: RUST_LOG=debug cargo run
Unit tests (in cardio_core/src/*.rs):
- Test pure functions (progression logic, catalog validation)
- Use
#[cfg(test)]modules at file bottom - Current coverage: 42 tests passing
Integration tests (in cardio_cli/tests/*.rs):
- Test CLI end-to-end (session logging, rollup, concurrency)
- Use
assert_cmdandtempfilecrates - Key tests:
test_session_logged_to_wal: Verify WAL appendtest_dry_run_does_not_log: Ensure dry-run flag workstest_concurrent_writes: fs2 locking correctnesstest_corruption_recovery: WAL line-skipping resilience
Running tests:
# All tests
cargo test
# Core library only
cargo test -p cardio_core
# With output
cargo test -- --nocapture
# Specific test
cargo test test_burpee_progression-
Edit
cardio_core/src/catalog.rs:movements.insert( "jump_rope".into(), Movement { id: "jump_rope".into(), name: "Jump Rope".into(), kind: MovementKind::Cardio, default_style: MovementStyle::None, tags: vec!["vo2".into()], reference_url: Some("https://...".into()), }, );
-
Create microdose definition (in same file):
microdoses.insert( "jump_rope_3min".into(), MicrodoseDefinition { id: "jump_rope_3min".into(), name: "3-Min Jump Rope".into(), category: MicrodoseCategory::Vo2, suggested_duration_seconds: 180, gtg_friendly: false, blocks: vec![MicrodoseBlock { movement_id: "jump_rope".into(), movement_style: MovementStyle::None, duration_hint_seconds: 180, metrics: vec![], }], reference_url: None, }, );
-
Add validation test:
#[test] fn test_jump_rope_in_catalog() { let catalog = build_default_catalog(); assert!(catalog.movements.contains_key("jump_rope")); assert!(catalog.microdoses.contains_key("jump_rope_3min")); }
-
Run tests:
cargo test -p cardio_core
-
Locate rule in
cardio_core/src/engine.rs:- Category selection:
determine_category() - Definition selection:
select_definition_from_category() - Intensity:
compute_intensity()
- Category selection:
-
Update logic (example: add time-of-day rule):
fn determine_category(ctx: &UserContext) -> Result<MicrodoseCategory> { // New rule: No VO2 after 8pm if ctx.now.hour() >= 20 { return Ok(MicrodoseCategory::Mobility); } // Existing rules... }
-
Add test:
#[test] fn test_late_evening_prescribes_mobility() { let ctx = UserContext { now: Utc.ymd(2025, 11, 17).and_hms(21, 0, 0), // ... }; let category = determine_category(&ctx).unwrap(); assert_eq!(category, MicrodoseCategory::Mobility); }
-
Document in docstring (v1.2 prescription logic)
-
Update
Configstruct incardio_core/src/config.rs:#[derive(Deserialize)] pub struct ProgressionConfig { pub burpee_rep_ceiling: i32, pub kb_swing_max_reps: i32, pub pullup_gtg_increment: i32, // NEW }
-
Update default values (in
impl Default):impl Default for ProgressionConfig { fn default() -> Self { Self { // ... pullup_gtg_increment: 1, } } }
-
Use in progression logic (
cardio_core/src/progression.rs):pub fn upgrade_pullup(state: &mut ProgressionState, increment: i32) { state.reps += increment; // ... }
-
Document in README (Configuration section)
Critical invariants (violations = data loss):
- WAL must be append-only: Never truncate or overwrite
- Atomic writes: Always write to
.tmp+ rename - File locking: Use
fs2::FileExt::try_lock_exclusive()before WAL append - UUID uniqueness: Generate with
uuid::Uuid::new_v4() - CSV deduplication: Track seen UUIDs in rollup to prevent duplicates
Common bugs:
- Missing
.flush()after WAL write → partial records - Forgot to unlock file → deadlock on next write
- Parsing error → entire WAL unreadable (use line-by-line parsing with
serde_json::from_str)
Debugging: Check ~/.local/share/krep/wal/ for lock files, tmp files
-
Update
Commandsenum incardio_cli/src/main.rs:#[derive(Subcommand)] enum Commands { // ... /// Show workout history History { #[arg(long, default_value = "7")] days: u32, }, }
-
Implement handler:
fn cmd_history(data_dir: PathBuf, days: u32) -> Result<()> { let sessions = load_recent_sessions(&wal_path, &csv_path, days)?; for session in sessions { println!("{:?}", session); } Ok(()) }
-
Wire up in
main():match cli.command { // ... Some(Commands::History { days }) => cmd_history(data_dir, days), }
-
Add integration test:
#[test] fn test_history_command() { cli().arg("history").arg("--days").arg("3") .assert().success(); }
- WAL format: Changing schema breaks existing user data (needs migration)
- State.json structure: Same issue (add new fields as
Option<T>) - Catalog IDs: Changing
definition_idbreaks progression history - File locking strategy: fs2 is required for correctness (don't replace)
- Type safety patterns:
SessionKindenum prevents bugs (don't flatten to single type)
- Breaking changes: Schema migrations, catalog ID changes
- New dependencies: Especially platform-specific (GTK, etc.)
- Prescription logic changes: May affect user experience
- Data directory location: XDG compliance is deliberate
- Forgot to run tests:
cargo testbefore committing - Hardcoded paths: Use
dirs::data_dir()for XDG compliance - Unwrapping Results: Use
?operator (see error.rs) - Missing validation: Run
catalog.validate()after catalog changes - Platform assumptions: Code runs on Linux primarily (GTK4 tray app)
# Development build
cargo build
# Release build (optimized)
cargo build --release
# CLI only (no GTK dependencies)
cargo build --release -p cardio_cli
# Check without building
cargo checkBinaries output to target/release/:
cardio_cli(orkrepsymlink)cardio_tray(requires GTK4)
# All tests
cargo test
# Core library only (unit tests)
cargo test -p cardio_core
# Integration tests only
cargo test -p cardio_cli --test integration_tests
# With logging
RUST_LOG=debug cargo test -- --nocaptureCore (always required):
serde,serde_json: Serializationchrono: Date/time handlinguuid: Session IDsthiserror: Error typestoml: Config parsingtracing: Loggingfs2: File lockingcsv: CSV exportdirs: XDG directories
CLI (for binary):
clap: Argument parsing
Tray app (optional, Linux only):
gtk4,libadwaita: UIgio,glib: GObject bindingslibayatana-appindicator: System tray
Testing:
tempfile: Temp directoriesassert_cmd: CLI testingpredicates: Assertions
- Core prescription engine
- WAL persistence
- CSV rollup
- Progression algorithms
- CLI interface
- Integration tests
- GNOME tray application (Milestone 5)
- GitHub Actions CI (Milestone 6)
- Installation script
- Time-based prescriptions (cron integration)
- HR zone tracking
- Multi-user support
- Web dashboard for analytics
- User documentation: See
README.md - Domain spec: v1.1 prescription logic (documented in
engine.rs) - API docs:
cargo doc --open(generates from///comments) - Tracing:
RUST_LOG=debugfor verbose logs
If you're an AI assistant uncertain about:
- Architecture decisions: Check this doc's principles section
- Code patterns: Look at existing modules (e.g.,
progression.rsfor upgrade logic) - Tests: See
cardio_cli/tests/integration_tests.rsfor patterns - Breaking changes: Ask the user before modifying WAL/state format
Remember: This codebase prioritizes type safety, data integrity, and zero data loss. When in doubt, prefer compile-time checks over runtime validation, and WAL-first persistence over in-memory state.