| name | interface-design-review |
| description | Review Rust interfaces for ease of correct use and resistance to misuse, applying "make interfaces easy to use correctly and hard to use incorrectly" |
Interface Design Review
Review public APIs, traits, structs, and module boundaries against the guiding
principle: "Make interfaces easy to use correctly and hard to use incorrectly."
The correct path should be the path of least resistance. Incorrect usage should
be caught at compile time when possible, at construction time when not, and at
runtime only as a last resort.
When to Use
- After designing or modifying a public API (
pub fn, pub struct, pub trait)
- When adding a new module or crate boundary
- Before stabilizing an interface that other code will depend on
- When reviewing builder patterns, configuration types, or plugin interfaces
- After discovering a bug caused by caller misuse of an existing API
- When an API requires documentation to explain "don't do X" — that's a signal
the interface should prevent X structurally
The Hierarchy of Enforcement
Prefer enforcement higher in this list. Each level down is weaker:
| Level | Mechanism | Example | Strength |
|---|
| 1. Unrepresentable | Type system makes the wrong state impossible | Newtype, typestate, NonZeroU32 | Strongest |
| 2. Unconstructable | Invalid values can't be built | Private fields + validated new() | Strong |
| 3. Compile error | Misuse fails to compile | Ownership, lifetimes, trait bounds | Strong |
| 4. Visible at call site | Misuse is obvious when reading code | Enums over bools, named types over primitives | Moderate |
| 5. Runtime rejection | Validated at runtime with clear error | TryFrom, Result-returning methods | Weak |
| 6. Documentation | "Don't do this" in a doc comment | /// # Panics | Weakest |
Review Checklist
A. Make Invalid States Unrepresentable
B. Validate at the Boundary, Trust Internally
C. Guide the Caller Toward Correct Usage
D. Make Misuse Visible
E. Leverage Ownership and Lifetimes
F. Error Design
Anti-Patterns to Flag
| Anti-Pattern | Why It's Bad | Fix |
|---|
fn foo(verbose: bool, recursive: bool) | Callers write foo(true, false) — meaning is invisible | Use enums: Verbosity::Quiet, Traversal::Flat |
pub struct Range { pub start: u32, pub end: u32 } | Nothing enforces start <= end | Private fields + Range::new(start, end) -> Result<Range> |
fn process(id: u64, parent_id: u64) | Easy to swap arguments silently | fn process(id: RuleId, parent: ParentId) |
fn configure(opts: HashMap<String, String>) | Unbounded input, typos compile fine | Typed config struct or builder |
fn init() -> *mut State | Caller must remember to free, can use-after-free | Return Box<State> or Arc<State> |
Returning i32 error codes | Caller can ignore, misinterpret, or mix with valid values | Return Result<T, E> with typed error |
fn send(data: &[u8], compress: bool, encrypt: bool, sign: bool) | 3 bools = 8 combinations, many invalid | Options struct or builder with valid combinations |
| Silent default on invalid input | Caller doesn't know their input was wrong | Return Err or use a type that can't be invalid |
Project-Specific Patterns
This codebase already uses several "easy to use correctly" patterns. New code
should follow them:
- Sentinel values as constants:
NONE_U32 = u32::MAX — centralizes the
sentinel so callers don't invent their own.
- Const generics for granularity: Compile-time selection via
<const G: usize>
prevents runtime branching on a configuration value.
- Feature-gated test tooling:
#[cfg(feature = "sim-harness")] ensures test
infrastructure can't accidentally leak into production builds.
debug_assert! for internal invariants: Zero-cost in release, catches
contract violations during development.
When reviewing, check that new APIs are consistent with these existing patterns.
Output Format
## Interface Design Review: [module/type/function]
### Enforcement Level Assessment
| Aspect | Current Level | Target Level | Gap |
|--------|--------------|--------------|-----|
| ID parameters | 6 (docs say "pass rule ID") | 1 (newtype `RuleId`) | 5 levels |
| Construction validity | 5 (runtime check in `new()`) | 2 (`new()` → private fields) | 3 levels |
| Flag parameters | 6 (doc: "`true` = verbose") | 4 (enum `Verbosity`) | 2 levels |
### Findings
| Priority | Issue | Location | Enforcement Gap |
|----------|-------|----------|-----------------|
| HIGH | Raw `u64` used for rule ID and parent ID — easy to swap | `fn register(id: u64, parent: u64)` | Unrepresentable → Newtype |
| MEDIUM | `bool` parameter for mode selection | `fn scan(data: &[u8], lenient: bool)` | Visible → Enum |
| LOW | Missing `#[must_use]` on `Result`-returning fn | `fn validate()` | Already level 5, add attribute |
### Recommendations
1. **[Issue]**: [Fix with code showing before/after]
```rust
// Before: callers can silently swap IDs
pub fn register(id: u64, parent: u64) { ... }
// After: compiler rejects swapped arguments
pub fn register(id: RuleId, parent: ParentId) { ... }
Positive Patterns Observed
- [Note any good patterns already present — reinforces correct design]
## Related Skills
- `security-reviewer` - Complements this: security reviews focus on memory
safety; interface reviews focus on API misuse prevention
- `test-strategy` - Property-based tests are powerful for validating that an
interface's invariants hold across all inputs
- `doc-rigor` - If an interface passes this review, its docs should describe
*why* the design prevents misuse, not just *how* to use it