This file provides guidance to AI coding assistants like Claude Code and Cursor when working with code in this repository.
pks (packs) is a high-performance Rust implementation of Ruby's packwerk tool for gradual modularization. It's 10-20x faster than the Ruby version while maintaining full compatibility.
# Build the project
cargo build
# Run all tests
cargo test
# Run a specific test
cargo test test_name
# Format code (required before committing)
cargo fmt
# Lint code
cargo clippy
# Check code without building
cargo check# Core commands
pks check # Look for violations
pks validate # Look for validation errors
pks update # Update package_todo.yml files
pks create # Create new packsThe codebase is organized around a modular checker system that validates various aspects of package boundaries:
-
Entry Points:
src/main.rs(CLI) andsrc/lib.rs(library) -
Core Module:
src/packs/contains all business logic -
Error Handling: Uses
anyhowfor simplifiedResulthandling throughout the codebase -
Checker System: Located in
src/packs/checker/, implements different violation types:dependency.rs- Package dependency violationsprivacy.rs- Public API violationsvisibility.rs- Package visibility violationslayer.rs- Architecture layer violationsfolder_privacy.rs- Folder-level privacy violations
-
Parsing:
src/packs/parsing/handles Ruby and ERB code parsing usinglib-ruby-parser -
Performance: Uses
rayonfor parallel processing and implements caching insrc/packs/caching/ -
Configuration: Managed through
src/packs/configuration.rs, readspackwerk.ymlandpackage.ymlfiles
Tests are fixture-based and located in tests/. Key test fixtures in tests/fixtures/ represent different application scenarios. The test suite uses assert_cmd for CLI testing.
To run tests for a specific checker or feature, use:
cargo test checker_name- Rust version: specified in
rust-toolchain.toml - Parallel Processing: Heavily uses
rayonfor performance - Serialization: Uses
serde_yamlfor YAML files, with deterministic serialization forpackage_todo.yml - Ruby Parsing: Uses
lib-ruby-parserto generate ASTs for Ruby code analysis - Graph Operations: Uses
petgraphfor dependency cycle detection
src/packs/pack.rs- Core Pack struct and package representationsrc/packs/package_todo.rs- Manages TODO violations inpackage_todo.ymlfilessrc/packs/checker.rs- Main violation checking orchestrationsrc/packs/cli.rs- CLI command definitions and argument parsing
This codebase follows idiomatic Rust patterns. Here are key conventions:
This codebase follows a principled approach to error handling: panic for programming bugs, Result for recoverable errors.
Use panics for programming bugs - situations that should never happen if the code is written correctly:
// GOOD: Panic with descriptive message for programming bugs
fn defining_pack_name(&self) -> &str {
self.defining_pack
.as_ref()
.map(|pack| pack.name.as_str())
.unwrap_or_else(|| {
panic!(
"defining_pack_name() called for {:?} checker when defining_pack is None. \
This indicates checkable() was not called or returned true incorrectly.",
self.checker_type
)
})
}
// GOOD: Debug assertions catch bugs during development
debug_assert!(
checker.defining_pack.is_some() || !checker.needs_defining_pack(),
"PackChecker created without defining_pack when one is required for {:?} checker",
checker.checker_type
);Use Result for recoverable errors - things that can go wrong due to external factors:
// GOOD: Use ? operator with anyhow::Result for external errors
fn process_pack(path: &Path) -> anyhow::Result<Pack> {
let content = std::fs::read_to_string(path)?; // File might not exist
let pack = serde_yaml::from_str(&content)?; // File might be malformed
Ok(pack)
}
// GOOD: Handle potentially corrupted data gracefully
fn interpolate_violation_message(&self) -> anyhow::Result<String> {
let defining_pack_name = self.defining_pack
.as_ref()
.map(|pack| pack.name.clone())
.ok_or_else(|| {
anyhow::anyhow!(
"Cannot create violation message: defining_pack is None. \
This may indicate corrupted reference data."
)
})?;
// ... rest of function
}Ask yourself: "If this fails, is it because:"
- My code has a bug? → Use
.expect()with descriptive message (crash and fix the bug) - External factors beyond my control? → Use
Resultand?(handle gracefully)
// BAD: Generic unwrap() with no context
fn process_pack(path: &Path) -> Pack {
let content = std::fs::read_to_string(path).unwrap(); // Could be external error!
serde_yaml::from_str(&content).unwrap() // No debugging info!
}// GOOD: Use iterator chains for transformations
let violations: Vec<_> = packs
.iter()
.filter(|p| p.enforce_dependencies)
.flat_map(|p| check_violations(p))
.collect();
// BAD: Manual loops with push
let mut violations = Vec::new();
for pack in &packs {
if pack.enforce_dependencies {
for violation in check_violations(pack) {
violations.push(violation);
}
}
}// GOOD: Use match for exhaustive handling
match checker_type {
CheckerType::Dependency => check_dependencies(&pack),
CheckerType::Privacy => check_privacy(&pack),
CheckerType::Layer => check_layers(&pack),
}
// BAD: Chain of if-else (when all variants should be handled)
if checker_type == CheckerType::Dependency {
check_dependencies(&pack)
} else if checker_type == CheckerType::Privacy {
check_privacy(&pack)
} else {
check_layers(&pack)
}// GOOD: Borrow when you don't need ownership
fn validate_pack(pack: &Pack) -> bool {
pack.dependencies.iter().all(|d| is_valid(d))
}
// BAD: Taking ownership unnecessarily
fn validate_pack(pack: Pack) -> bool {
pack.dependencies.iter().all(|d| is_valid(d))
}// GOOD: Use if let for single pattern matching
if let Some(config) = load_config()? {
process_with_config(&config);
}
// GOOD: Use .ok() to convert Result to Option when errors can be ignored
let cache = load_cache().ok();
// BAD: Verbose match when if let would suffice
match load_config()? {
Some(config) => process_with_config(&config),
None => {},
}// GOOD: Use rayon's parallel iterators
use rayon::prelude::*;
let results: Vec<_> = files
.par_iter()
.map(|f| process_file(f))
.collect();
// BAD: Sequential processing when parallel would be better
let results: Vec<_> = files
.iter()
.map(|f| process_file(f))
.collect();- Make changes to the relevant module
- Run
cargo fmtto format code - Run
cargo clippyto check for lints - Run
cargo testto ensure tests pass - For CLI changes, test with
cargo run -- [command]
The project maintains compatibility with Ruby's packwerk, so any changes should preserve existing YAML file formats and CLI behavior.