# timebomb — CLAUDE.md
This file gives Claude (and any other AI coding assistant) the context needed to work effectively in this repository. Read it before making changes.
---
## What this project is
`timebomb` is a Rust CLI tool that scans source code for structured expiry annotations (`TODO[2026-06-01]: ...`) and fails when any deadline has passed. It is designed to enforce the social contract that temporary code actually gets removed.
---
## Build, test, and lint commands
```sh
cargo build # compile (dev profile)
cargo build --release # compile (optimised)
cargo test # run all unit + integration tests
cargo test -- --nocapture # show eprintln! output during tests
cargo clippy -- -D warnings # lint; CI fails on any warning
cargo fmt # format; CI checks with --check
make smoke # end-to-end smoke tests against the release binary
```
All five must pass cleanly before any PR is merged.
---
## Project layout
```
src/
main.rs CLI entrypoint; resolve_config(); resolve_fuse_arg(); file_matches();
status_order(); subcommand dispatch; exit codes
cli.rs clap derive structs: Cli, Command, SweepArgs, ManifestArgs, …,
FormatArg, SortBy, GroupBy, CompletionsArgs; re-exports clap_complete::Shell
config.rs .timebomb.toml loading, CLI overlay merging, glob exclusion, extension filter
scanner.rs scan(), scan_file(), scan_content(), build_regex(), is_binary()
annotation.rs Fuse struct, Status enum (Detonated/Ticking/Inert), compute_status()
output.rs Terminal / JSON / CSV / GitHub Actions formatters;
age_col() compact age column; print_csv_list(); write_json_report()
error.rs Error enum, Result alias, parse_duration_days()
blame.rs git blame integration for --blame enrichment
hook.rs Pre-commit tripwire install / uninstall
trend.rs Report snapshot comparison (fallout command)
report.rs Report JSON generation and writing
stats.rs Aggregate stats by owner / tag / month (intel command);
compute_stats(), print_stats(), print_stats_month()
init.rs timebomb init command
add.rs timebomb plant command (insert fuses)
snooze.rs timebomb delay command (bump deadlines in-place)
fix.rs timebomb defuse command (interactive detonated fuse resolution)
diff.rs Unified diff parsing for --since/--changed mode
baseline.rs Bunker save/show/ratchet enforcement
git.rs Git helpers (validate_git_ref, changed files, repo detection)
lib.rs Public re-exports (makes src/ importable from tests/)
tests/
scanner_tests.rs Integration tests against fixture files
config_tests.rs Integration tests for config loading and merging
fix_tests.rs Integration tests for the defuse command
diff_tests.rs Integration tests for diff parsing / --since mode
baseline_tests.rs Integration tests for bunker ratchet enforcement
fixtures/ One sample.* file per supported language extension
```
---
## Naming — the bomb theme
Everything in the codebase uses bomb/explosion terminology. Key mappings:
| Annotation / TODO comment with a date | **fuse** (`Fuse` struct) |
| Past-due fuse | **detonated** (`Status::Detonated`) |
| Fuse within the warning window | **ticking** (`Status::Ticking`) |
| Fuse safely in the future | **inert** (`Status::Inert`) |
| Number of files scanned | **swept_files** |
| Scan and fail in CI | **sweep** (subcommand) |
| List all fuses | **manifest** (subcommand) |
| Insert a fuse | **plant** (subcommand) |
| Bump a deadline | **delay** (subcommand) |
| Remove a fuse | **disarm** (subcommand) |
| Stats by owner/tag/month | **intel** (subcommand) |
| Pre-commit hook | **tripwire** (subcommand: `set` / `cut`) |
| Compare two snapshots | **fallout** (subcommand) |
| Interactive resolve detonated fuses | **defuse** (subcommand) |
| Baseline ratchet | **bunker** (subcommand: `save` / `show`) |
| Shell completion scripts | **completions** (subcommand) |
| Warning window (days) | **fuse_days** (config key) |
| Max detonated ceiling | **max_detonated** (config key) |
| Max ticking ceiling | **max_ticking** (config key) |
---
## Key architecture decisions
### `today` is injected, never fetched internally
`scan()`, `scan_content()`, and `Fuse::compute_status()` all accept `today: NaiveDate` as a parameter. "Today" is derived once in `main.rs` at startup and threaded through. This makes every test deterministic without mocks or time-travel hacks.
### Regex compiled once
`build_regex(config)` is called once in `scan()` before the walk loop. The resulting `Regex` is `Send + Sync` and is shared (by reference) across all rayon worker threads. Never compile the regex inside `scan_file` or `scan_content`.
Helper regexes used in other modules (`snooze.rs`, `diff.rs`) are cached as `std::sync::OnceLock<Regex>` statics so they are compiled at most once per process.
### Three-phase scan pipeline
`scan()` is structured in three explicit phases:
1. **Serial walk** — `WalkDir` collects candidate `(abs_path, rel_path)` pairs after applying exclude globs, extension filter, and binary detection.
2. **Parallel scan** — `candidates.par_iter().map(scan_file)` via rayon. Each worker reads one file and returns `Vec<Fuse>`. No shared mutable state.
3. **Serial flatten + sort** — flatten the per-file vecs, sort by `NaiveDate` ascending.
If you restructure `scan()`, preserve this boundary so the rayon step stays pure.
### Config merging order
`Config` is resolved in `main.rs` via `resolve_config()`:
1. Look for `--config <file>` (explicit override).
2. Look for `.timebomb.toml` in the scan directory.
3. Fall back to `.timebomb.toml` in CWD.
4. If no file found, use `Config::default()` silently.
CLI flags (e.g. `--fuse`, `--fail-on-ticking`) are applied on top as `CliOverrides` after file loading. CLI always wins over file.
### `--fuse` resolution and `TIMEBOMB_FUSE_DAYS`
All six call sites that construct `CliOverrides::new(fuse, ...)` go through `resolve_fuse_arg(cli_fuse)` first:
```rust
fn resolve_fuse_arg(cli_fuse: Option<String>) -> Option<String> {
cli_fuse.or_else(|| {
std::env::var("TIMEBOMB_FUSE_DAYS").ok().map(|v| {
if v.ends_with('d') { v } else { format!("{}d", v) }
})
})
}
```
Priority: `--fuse` CLI flag > `TIMEBOMB_FUSE_DAYS` env var > config file > default (0).
### `--file` filter — three-step path matching
`manifest --file` accepts multiple values. Each is matched via `file_matches(fuse_file, filter)` in `main.rs`:
1. Strip a leading `./` or `.\` (shell tab-completion compatibility).
2. If the filter contains glob metacharacters (`*`, `?`, `[`, `{`), compile and match with `globset`.
3. Otherwise fall back to a component-aware suffix match (`Path::ends_with`).
This means `src/auth.rs`, `./src/auth.rs`, and `src/auth/**` all work transparently.
### Exit codes
| 0 | No detonated fuses (or counts within bunker/ceilings) |
| 1 | Detonated fuses found, `--fail-on-ticking` triggered, or ratchet ceiling breached |
| 2 | Configuration or runtime error |
`manifest` and `defuse` **always** exit 0 — they are informational/interactive. Only `sweep` uses exit code 1.
### `defuse` command — two-pass interactive resolution
`defuse` collects all user decisions in a first pass (interactive prompts: extend / delete / skip), then applies them in a second pass bottom-up by descending line number per file. This avoids line-shift bugs. It reuses `snooze::snooze_line` for Extend and `remove::remove_line` for Delete.
### `--since` mode — diff-aware filtering
`sweep --since <ref>` runs the normal scan, then filters results to fuses whose file+line appear in the changed line ranges returned by `diff::git_changed_line_ranges`. The diff parser (`parse_unified_diff`) is a pure function that takes a `git diff --unified=0` string and returns `HashMap<PathBuf, Vec<RangeInclusive<usize>>>`. Both staged and unstaged diffs are merged.
### Bunker ratchet
`bunker save` writes `.timebomb-baseline.json` with the current detonated and ticking counts. `sweep` loads this file (if present) and calls `check_ratchet` — a pure function returning a `Vec<String>` of violation messages. Four independent checks: `max_detonated` ceiling, `max_ticking` ceiling, regression vs. baseline detonated, regression vs. baseline ticking. Any violation causes `sweep` to exit 1.
### `intel --by month`
`compute_stats` in `stats.rs` groups fuses by `fuse.date.format("%Y-%m")` into `MonthRow` entries stored in `by_month: Vec<MonthRow>` on `StatsResult`. Month rows are sorted chronologically (ascending YYYY-MM string sort). The `--by month` arm in main.rs dispatches to `print_stats_month(result, format)` instead of `print_stats`.
### Shell completions
`timebomb completions <shell>` uses `clap_complete::generate` with `Cli::command()` to print a completion script to stdout. `clap_complete::Shell` is re-exported from `cli.rs` as `pub use clap_complete::Shell` so the type is accessible to `main.rs` without an extra import.
---
## Fuse format
```
// TODO[2026-06-01]: message
# FIXME[2026-03-15][alice]: message with owner
```
The regex (built in `Config::fuse_regex_pattern()`):
```