|
| 1 | +# Architecture |
| 2 | + |
| 3 | +## The Big Picture |
| 4 | + |
| 5 | +``` |
| 6 | +┌─────────────────────────────────────────────────────────────────────────────┐ |
| 7 | +│ cargo rail <cmd> │ |
| 8 | +└─────────────────────────────────────────────────────────────────────────────┘ |
| 9 | + │ |
| 10 | + ▼ |
| 11 | +┌─────────────────────────────────────────────────────────────────────────────┐ |
| 12 | +│ main.rs │ |
| 13 | +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ |
| 14 | +│ │ Parse CLI │ → │ Init output │ → │ Build ctx │ → │ dispatch(cmd) │ │ |
| 15 | +│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────────┘ │ |
| 16 | +└─────────────────────────────────────────────────────────────────────────────┘ |
| 17 | + │ |
| 18 | + ┌──────────────────────────┼──────────────────────────┐ |
| 19 | + ▼ ▼ ▼ |
| 20 | + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ |
| 21 | + │ affected │ │ unify │ │ release │ |
| 22 | + │ test │ │ split │ │ sync │ |
| 23 | + └─────────────┘ └─────────────┘ └─────────────┘ |
| 24 | + │ │ │ |
| 25 | + └──────────────────────────┼──────────────────────────┘ |
| 26 | + │ |
| 27 | + ▼ |
| 28 | +┌─────────────────────────────────────────────────────────────────────────────┐ |
| 29 | +│ WorkspaceContext (built once, passed everywhere) │ |
| 30 | +│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ |
| 31 | +│ │ GitState │ │ CargoState │ │WorkspaceGraph │ │ RailConfig │ │ |
| 32 | +│ │ (Arc) │ │ (Arc) │ │ (Arc) │ │ (Arc) │ │ |
| 33 | +│ └───────────────┘ └───────────────┘ └───────────────┘ └───────────────┘ │ |
| 34 | +└─────────────────────────────────────────────────────────────────────────────┘ |
| 35 | + │ │ │ |
| 36 | + ▼ ▼ ▼ |
| 37 | + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ |
| 38 | + │ src/git/ │ │ src/cargo/ │ │ src/graph/ │ |
| 39 | + │ system git │ │ metadata │ │ petgraph │ |
| 40 | + └─────────────┘ │ manifests │ └─────────────┘ |
| 41 | + │ unify │ |
| 42 | + └─────────────┘ |
| 43 | +``` |
| 44 | + |
| 45 | +**The one rule that really matters:** WorkspaceContext loads once, passes by reference everywhere else. No command re-loads metadata. |
| 46 | + |
| 47 | +--- |
| 48 | + |
| 49 | +## Module Map |
| 50 | + |
| 51 | +| Module | What it does | Key files | |
| 52 | +|--------|--------------|-----------| |
| 53 | +| `commands/` | CLI handlers (one file = one command) | `cli.rs`, `mod.rs` (dispatch) | |
| 54 | +| `workspace/` | WorkspaceContext + state management | `context.rs`, `view.rs`, `change_analyzer.rs` | |
| 55 | +| `graph/` | Dependency graph (petgraph) | `core.rs`, `query.rs` | |
| 56 | +| `cargo/` | Metadata loading, manifest ops, unify | `multi_target_metadata.rs`, `unify_analyzer.rs` | |
| 57 | +| `git/` | System git wrapper | `system.rs`, `ops.rs` | |
| 58 | +| `change_detection/` | File classification | `classify.rs`, `presentation.rs` | |
| 59 | +| `split/` | Crate extraction with history | `engine.rs` | |
| 60 | +| `sync/` | Bidirectional sync | `engine.rs`, `conflict.rs` | |
| 61 | +| `release/` | Version, changelog, publish | `planner.rs`, `publisher.rs`, `validator.rs` | |
| 62 | +| `config/` | rail.toml parsing | `mod.rs`, per-feature files | |
| 63 | +| `backup/` | Undo support | | |
| 64 | +| `toml/` | Lossless TOML editing | | |
| 65 | +| `output.rs` | Quiet/JSON mode control | | |
| 66 | +| `error.rs` | RailError + exit codes | | |
| 67 | + |
| 68 | +--- |
| 69 | + |
| 70 | +## Core Type: WorkspaceContext |
| 71 | + |
| 72 | +```rust |
| 73 | +pub struct WorkspaceContext { |
| 74 | + pub workspace_root: PathBuf, |
| 75 | + pub git: Arc<GitState>, // Git pps |
| 76 | + pub cargo: Arc<CargoState>, // Cargo metadata (O(1) package lookup) |
| 77 | + pub graph: Arc<WorkspaceGraph>, // Dep graph |
| 78 | + pub config: Option<Arc<RailConfig>>, |
| 79 | +} |
| 80 | +``` |
| 81 | + |
| 82 | +**Why Arc?** Cheap cloning across threads. Heavy state loaded once, shared freely. |
| 83 | + |
| 84 | +**Access Patterns:** |
| 85 | + |
| 86 | +```rust |
| 87 | +// Get changed files |
| 88 | +let files = ctx.git.git().changed_files_between(from, to)?; |
| 89 | + |
| 90 | +// Look up a package (O(1)) |
| 91 | +let pkg = ctx.cargo.get_package("my-crate")?; |
| 92 | + |
| 93 | +// Find transitive dependents |
| 94 | +let dependents = ctx.graph.transitive_dependents("my-crate")?; |
| 95 | + |
| 96 | +// Get config (errors if not present) |
| 97 | +let config = ctx.require_config()?; |
| 98 | +``` |
| 99 | + |
| 100 | +--- |
| 101 | + |
| 102 | +## Data Flow |
| 103 | + |
| 104 | +### Startup (main.rs) |
| 105 | + |
| 106 | +``` |
| 107 | +Parse CLI (clap) |
| 108 | + ↓ |
| 109 | +Init output mode (quiet/JSON) |
| 110 | + ↓ |
| 111 | +Handle early commands (init, undo, sync) ← These don't need full context |
| 112 | + ↓ |
| 113 | +Build WorkspaceContext (~100-300ms) |
| 114 | + ├─ GitState (~5ms) |
| 115 | + ├─ CargoState (50-200ms, cached) |
| 116 | + ├─ WorkspaceGraph (10-50ms) |
| 117 | + └─ RailConfig (<5ms) |
| 118 | + ↓ |
| 119 | +dispatch(cmd, &ctx) |
| 120 | +``` |
| 121 | + |
| 122 | +### Command Execution |
| 123 | + |
| 124 | +``` |
| 125 | +dispatch(cmd, &ctx) |
| 126 | + ↓ |
| 127 | +Match command → call handler |
| 128 | + ↓ |
| 129 | +Handler uses ctx.{git,cargo,graph,config} |
| 130 | + ↓ |
| 131 | +Return RailResult<()> |
| 132 | +``` |
| 133 | + |
| 134 | +Every command follows this pattern: |
| 135 | + |
| 136 | +```rust |
| 137 | +pub fn run_whatever(ctx: &WorkspaceContext, args: Args) -> RailResult<()> { |
| 138 | + let config = ctx.require_config()?; |
| 139 | + // Use ctx.git, ctx.cargo, ctx.graph as needed |
| 140 | + // ... |
| 141 | + Ok(()) |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | +--- |
| 146 | + |
| 147 | +## Change Detection (3 Layers) |
| 148 | + |
| 149 | +``` |
| 150 | +Layer 1: classify_file(path) → ChangeKind |
| 151 | + Pure function. No I/O. Just path inspection. |
| 152 | + ↓ |
| 153 | +Layer 2: ChangeImpact::analyze(from, to) → ImpactReport |
| 154 | + Uses git + graph + cargo metadata. |
| 155 | + ↓ |
| 156 | +Layer 3: ChangeClassifier::classify(files) → ChangeClassification |
| 157 | + Applies user config (docs-only, rebuild_all, custom categories). |
| 158 | +``` |
| 159 | + |
| 160 | +Why layered? Layer 1 is fast and testable. Layer 2 adds graph awareness. Layer 3 adds user preferences. |
| 161 | + |
| 162 | +--- |
| 163 | + |
| 164 | +## Unify Pipeline |
| 165 | + |
| 166 | +``` |
| 167 | +cargo metadata (per target, parallel) |
| 168 | + ↓ |
| 169 | +MultiTargetMetadata (merged view) |
| 170 | + ↓ |
| 171 | +ManifestAnalyzer (what's used where?) |
| 172 | + ↓ |
| 173 | +FeatureScanner (classify features) |
| 174 | + ↓ |
| 175 | +UnifyAnalyzer (generate plan) |
| 176 | + ↓ |
| 177 | +ManifestWriter (lossless TOML edits) |
| 178 | +``` |
| 179 | + |
| 180 | +Key insight: operates on **resolved** dependencies (what Cargo chose), not manifest syntax. |
| 181 | + |
| 182 | +--- |
| 183 | + |
| 184 | +## Key Design Decisions |
| 185 | + |
| 186 | +| Decision | Why | |
| 187 | +|----------|-----| |
| 188 | +| System git (no libgit2) | Deterministic SHAs, full git fidelity | |
| 189 | +| Resolution-based unification | Accurate to what Cargo actually resolves | |
| 190 | +| Lossless TOML (toml_edit) | Preserve comments and formatting | |
| 191 | +| Thin main.rs (<100 lines) | All logic testable in library | |
| 192 | +| O(1) lookups everywhere | HashMap indexes pre-built at load time | |
| 193 | +| Petgraph directly | Own the domain types, no guppy abstraction | |
| 194 | + |
| 195 | +--- |
| 196 | + |
| 197 | +## Where to Make Changes |
| 198 | + |
| 199 | +### Adding a new command |
| 200 | + |
| 201 | +1. Define CLI args in `src/commands/cli.rs` |
| 202 | +2. Create handler in `src/commands/your_command.rs` |
| 203 | +3. Add to dispatch in `src/commands/mod.rs` |
| 204 | +4. Handle in `main.rs` if it needs special pre-context handling |
| 205 | + |
| 206 | +### Changing workspace loading |
| 207 | + |
| 208 | +→ `src/workspace/context.rs` |
| 209 | + |
| 210 | +### Modifying dependency graph logic |
| 211 | + |
| 212 | +→ `src/graph/core.rs` (structure) |
| 213 | +→ `src/graph/query.rs` (algorithms) |
| 214 | + |
| 215 | +### Adjusting unification |
| 216 | + |
| 217 | +→ `src/cargo/unify_analyzer.rs` (plan generation) |
| 218 | +→ `src/cargo/manifest_writer.rs` (TOML output) |
| 219 | + |
| 220 | +### Changing change detection |
| 221 | + |
| 222 | +→ `src/change_detection/classify.rs` (file classification) |
| 223 | +→ `src/workspace/change_analyzer.rs` (impact analysis) |
| 224 | + |
| 225 | +### Modifying split/sync/release |
| 226 | + |
| 227 | +→ `src/split/engine.rs` |
| 228 | +→ `src/sync/engine.rs` |
| 229 | +→ `src/release/planner.rs`, `publisher.rs` |
| 230 | + |
| 231 | +--- |
| 232 | + |
| 233 | +## File → Module Quick Reference |
| 234 | + |
| 235 | +``` |
| 236 | +"Where do I find..." |
| 237 | +
|
| 238 | +affected crates logic → src/commands/affected.rs |
| 239 | + → src/workspace/change_analyzer.rs |
| 240 | +
|
| 241 | +test runner → src/commands/test.rs |
| 242 | + → src/test/ |
| 243 | +
|
| 244 | +dependency unification → src/cargo/unify_*.rs |
| 245 | + → src/commands/unify.rs |
| 246 | +
|
| 247 | +split operation → src/split/engine.rs |
| 248 | + → src/commands/split.rs |
| 249 | +
|
| 250 | +sync operation → src/sync/engine.rs |
| 251 | + → src/commands/sync.rs |
| 252 | +
|
| 253 | +release workflow → src/release/planner.rs |
| 254 | + → src/release/publisher.rs |
| 255 | + → src/commands/release.rs |
| 256 | +
|
| 257 | +config loading → src/config/mod.rs |
| 258 | +
|
| 259 | +git operations → src/git/system.rs |
| 260 | + → src/git/ops.rs |
| 261 | +
|
| 262 | +error handling → src/error.rs |
| 263 | +
|
| 264 | +output control → src/output.rs |
| 265 | +``` |
| 266 | + |
| 267 | +--- |
| 268 | + |
| 269 | +## Exit Codes |
| 270 | + |
| 271 | +| Code | Meaning | |
| 272 | +|------|---------| |
| 273 | +| 0 | Success | |
| 274 | +| 1 | Check mode found changes (actionable, not error) | |
| 275 | +| 2 | Error | |
| 276 | + |
| 277 | +Exit code 1 lets CI detect "changes needed" vs "something broke". This is honestly not needed and will likely be adjusted in the next major release. |
0 commit comments