use mimalloc::MiMalloc;
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use anyhow::Result;
use clap::Parser;
use tracing_subscriber::EnvFilter;
use _diffctx::config::limits::{
DEFAULT_PIPELINE_TIMEOUT_SECONDS, DEFAULT_PPR_ALPHA, DEFAULT_SCORING,
DEFAULT_STOPPING_THRESHOLD,
};
use _diffctx::mode::ScoringMode;
use _diffctx::pipeline::build_diff_context;
use _diffctx::render::DiffContextOutput;
const UNLIMITED_BUDGET_TOKENS: u32 = 10_000_000;
const EXIT_EMPTY_DIFF: i32 = 4;
#[derive(Parser)]
#[command(
name = "diffctx",
version,
about = "Semantic diff context selector",
disable_version_flag = true
)]
struct Cli {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(short = 'v', short_alias = 'V', long, action = clap::ArgAction::Version)]
version: Option<bool>,
#[arg(long, allow_negative_numbers = true)]
budget: Option<i64>,
#[arg(short = 'f', long, default_value = "yaml", value_parser = ["yaml", "json"])]
format: String,
#[arg(long = "diff", num_args = 0..=1, default_missing_value = "HEAD")]
diff_ref: Option<String>,
#[arg(long, default_value_t = DEFAULT_PPR_ALPHA)]
alpha: f64,
#[arg(long, default_value_t = DEFAULT_STOPPING_THRESHOLD)]
tau: f64,
#[arg(long)]
no_content: bool,
#[arg(long)]
full: bool,
#[arg(long, default_value = DEFAULT_SCORING, value_parser = _diffctx::mode::SCORING_MODE_NAMES.to_vec())]
scoring: String,
#[arg(long, default_value = "pack", value_parser = ["pack", "locate"])]
mode: String,
#[arg(long, default_value_t = DEFAULT_PIPELINE_TIMEOUT_SECONDS)]
timeout: u64,
#[arg(short = 'q', long)]
quiet: bool,
}
fn resolve_budget(budget: Option<i64>) -> Option<u32> {
match budget {
None => None,
Some(n) if n < -1 => {
eprintln!(
"error: --budget must be >= -1 (-1 = unlimited, 0 = strict-zero floor; use --full \
for changed files only), got {n}"
);
std::process::exit(2);
}
Some(n) if n < 0 => Some(UNLIMITED_BUDGET_TOKENS),
Some(n) => Some(u32::try_from(n).unwrap_or(UNLIMITED_BUDGET_TOKENS)),
}
}
fn group_thousands(n: u32) -> String {
let digits = n.to_string();
let mut grouped = String::with_capacity(digits.len() + digits.len() / 3);
for (i, ch) in digits.chars().enumerate() {
if i > 0 && (digits.len() - i) % 3 == 0 {
grouped.push(',');
}
grouped.push(ch);
}
grouped
}
fn format_size(byte_size: usize) -> String {
const KB: f64 = 1024.0;
const MB: f64 = 1024.0 * 1024.0;
if byte_size < 1024 {
format!("{byte_size} B")
} else if byte_size < 1024 * 1024 {
format!("{:.1} KB", byte_size as f64 / KB)
} else {
format!("{:.1} MB", byte_size as f64 / MB)
}
}
fn print_token_summary(rendered: &str) {
eprintln!(
"{} tokens (o200k_base), {}",
group_thousands(_diffctx::tokenizer::count_tokens(rendered)),
format_size(rendered.len())
);
}
fn diff_result_is_empty(output: &DiffContextOutput) -> bool {
output.deleted_files.is_empty()
&& output.renamed_files.is_empty()
&& output.lockfile_changes.is_empty()
&& output.ignored_changes.is_empty()
&& output.policy_excluded_count == 0
&& output.fragment_count == 0
}
fn empty_diff_hint(root: &Path, budget: Option<i64>, diff_ref: &str) -> String {
match budget {
Some(0) => "--budget 0 selects only the changed code itself; omit --budget for auto sizing"
.to_string(),
Some(n) if n > 0 => {
format!(
"--budget {n} may be too small to fit any fragment; raise it or omit for auto sizing"
)
}
_ if diff_ref == "HEAD" => {
"the working tree matches HEAD; try --diff HEAD~1 for the last commit".to_string()
}
_ if is_duration_window(root, diff_ref) => {
format!("nothing changed in the last {diff_ref}; widen the window (e.g. --diff 7d)")
}
_ => format!("check the range with: git diff --stat {diff_ref}"),
}
}
fn is_duration_window(root: &Path, diff_ref: &str) -> bool {
_diffctx::git::resolve_duration_range(root, Some(diff_ref))
.map(|resolved| resolved.from_duration)
.unwrap_or(false)
}
#[allow(clippy::too_many_arguments)]
fn run_locate(
cli: &Cli,
path: PathBuf,
diff_ref: Option<String>,
budget: Option<u32>,
alpha: f64,
tau: f64,
scoring_mode: ScoringMode,
timeout: u64,
) -> Result<()> {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let result = _diffctx::pipeline::build_diff_context_locate(
&path,
diff_ref.as_deref(),
budget,
alpha,
tau,
scoring_mode,
timeout,
);
let _ = tx.send(result);
});
let output = match rx.recv_timeout(Duration::from_secs(timeout)) {
Ok(result) => result?,
Err(mpsc::RecvTimeoutError::Timeout) => {
eprintln!(
"diffctx: pipeline exceeded {timeout}s wall-clock deadline; aborting before \
OOM/SIGKILL. Narrow the review with an explicit '--diff <from>..<to>' range or \
run on a smaller subtree, or raise '--timeout'."
);
std::process::exit(124);
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
anyhow::bail!("diffctx: pipeline worker terminated unexpectedly");
}
};
let rendered = format!("{}\n", serde_json::to_string(&output)?);
let is_empty = output.item_count == 0
&& output.deleted_files.is_empty()
&& output.renamed_files.is_empty()
&& output.lockfile_changes.is_empty()
&& output.ignored_changes.is_empty()
&& output.policy_excluded_count == 0;
if is_empty {
eprintln!(
"diffctx: diff produced no semantic context (clean working tree, binary-only, or \
files over the size cap); {}",
empty_diff_hint(
&cli.path,
cli.budget,
cli.diff_ref.as_deref().unwrap_or("HEAD")
)
);
}
if !cli.quiet {
print_token_summary(&rendered);
}
print!("{rendered}");
io::stdout().flush()?;
if is_empty {
std::process::exit(EXIT_EMPTY_DIFF);
}
Ok(())
}
fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
let cli = Cli::parse();
let scoring_mode = ScoringMode::from_str(&cli.scoring).unwrap_or_else(|e| {
eprintln!("error: {e}");
std::process::exit(2);
});
let timeout = cli.timeout;
let path = cli.path.clone();
let diff_ref = cli.diff_ref.clone();
let budget = resolve_budget(cli.budget);
let alpha = cli.alpha;
let tau = cli.tau;
let no_content = cli.no_content;
let full = cli.full;
if cli.mode == "locate" {
if full {
eprintln!(
"error: --mode locate is incompatible with --full (locate ranks the selection; --full bypasses it)"
);
std::process::exit(2);
}
return run_locate(
&cli,
path,
diff_ref,
budget,
alpha,
tau,
scoring_mode,
timeout,
);
}
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let result = build_diff_context(
&path,
diff_ref.as_deref(),
budget,
alpha,
tau,
no_content,
full,
scoring_mode,
timeout,
);
let _ = tx.send(result);
});
let output = match rx.recv_timeout(Duration::from_secs(timeout)) {
Ok(result) => result?,
Err(mpsc::RecvTimeoutError::Timeout) => {
eprintln!(
"diffctx: pipeline exceeded {timeout}s wall-clock deadline; aborting before \
OOM/SIGKILL. Narrow the review with an explicit '--diff <from>..<to>' range or \
run on a smaller subtree, or raise '--timeout'."
);
std::process::exit(124);
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
anyhow::bail!("diffctx: pipeline worker terminated unexpectedly");
}
};
let rendered = match cli.format.as_str() {
"json" => format!("{}\n", serde_json::to_string_pretty(&output)?),
"yaml" => serde_yaml::to_string(&output)?,
other => {
anyhow::bail!("diffctx: unsupported --format '{other}' (native binary: yaml, json)")
}
};
if diff_result_is_empty(&output) {
eprintln!(
"diffctx: diff produced no semantic context (clean working tree, binary-only, or \
files over the size cap); {}",
empty_diff_hint(
&cli.path,
cli.budget,
cli.diff_ref.as_deref().unwrap_or("HEAD")
)
);
}
if !cli.quiet {
print_token_summary(&rendered);
}
print!("{rendered}");
io::stdout().flush()?;
if diff_result_is_empty(&output) {
std::process::exit(EXIT_EMPTY_DIFF);
}
Ok(())
}