diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b135b2..258a95f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Notable changes to JevGate. Versions follow [Semantic Versioning](https://semver ## [Unreleased] +- `--dry-run` plans the files whose purpose the cache already answers, as a run does, so a warm cache's estimate matches the run: on a Rails project it counted 106 of 1,532 requests as new while the run sent none. + ## [0.19.0] - 2026-09-25 - Inline suppressions: a comment `jevgate: allow(RULE) reason` on a finding's line, or in the comments and attributes directly above it, accepts that finding as the baseline does. RULE is a rule ID, key or group, and the reason is required; the report keeps the finding with its reason (`suppressed`, and `gate.suppressed_findings`), and `jevgate baseline` leaves it out. diff --git a/src/evaluate.rs b/src/evaluate.rs index ed9b24d..c9d7c6e 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -123,9 +123,10 @@ fn empty_report(args: &CheckArgs, current: &SnapshotContext<'_>, files: Vec {} - Ok(Scheduled::Purpose(request)) => planned.push(request), + Ok(Scheduled::Purpose(request)) => { + if let Some(body) = crate::requests::answered(root, args, &request) { + let file = &mut report.files[owner]; + let view = crate::file_kind::record_purpose(file, &request, &body) + .and_then(|()| crate::file_kind::decide_after_purpose(input, args, file)); + match view { + Ok(Some(view)) => { + views.insert(owner, view); + } + Ok(None) => {} + Err(error) => report.errors.push(error.to_string()), + } + } + planned.push(request); + } Ok(Scheduled::Ready(view)) => { views.insert(owner, *view); } @@ -155,7 +170,7 @@ fn preview(inputs: &[Input], args: &CheckArgs, root: &std::path::Path, report: & .or_default(); stage.planned_requests += 1; stage.planned_evidence_bytes += crate::requests::evidence_bytes(&request); - if crate::requests::answered(root, args, &request) { + if crate::requests::answered(root, args, &request).is_some() { stage.planned_cached += 1; } else { stage.planned_tokens += budget.request_tokens(&request) as u64; diff --git a/src/file_kind.rs b/src/file_kind.rs index 650aff7..e2fae04 100644 --- a/src/file_kind.rs +++ b/src/file_kind.rs @@ -896,6 +896,24 @@ mod tests { } } + #[test] + fn a_dry_run_plans_the_units_of_a_file_whose_purpose_the_cache_answers() { + let project = Project::new(); + project.write("tests/support.rs", SUPPORT); + let mut options = args(); + options.rules = vec![crate::catalog::FUNCTION_SIMPLIFICATION.into()]; + run(&project, &options, &mut PurposeEval::new("mixed")); + options.dry_run = true; + let stages = crate::tests::snapshot(&project, &options).1.stages; + let planned = |stage: &str| (stages[stage].planned_requests, stages[stage].planned_cached); + assert_eq!(planned("file-purpose"), (1, 1)); + assert_eq!( + planned("functions"), + (1, 1), + "the units a run sends are planned" + ); + } + struct PurposeEval { mode: &'static str, calls: usize, diff --git a/src/requests.rs b/src/requests.rs index 099d901..3d6342a 100644 --- a/src/requests.rs +++ b/src/requests.rs @@ -92,17 +92,17 @@ fn cached_answer( .filter(|(b, _)| response::validate(b, request).is_ok()) } -/// Whether a dry run's planned request already has a cached answer, read -/// without opening the store. +/// A dry run's cached answer to a planned request, read without opening the +/// store. pub(super) fn answered( root: &std::path::Path, args: &crate::options::CheckArgs, request: &Value, -) -> bool { +) -> Option { cached_answer(args, request, |key, ttl| { crate::storage::peek(root, key, ttl) }) - .is_some() + .map(|(body, _)| body) } impl Session<'_> { diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 3dd85b1..3f738d4 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -167,7 +167,10 @@ pub(super) fn session<'a>( } /// The selected inputs and the first snapshot of a check, before evaluation. -fn snapshot(project: &Project, options: &CheckArgs) -> (Vec, schema::Report) { +pub(super) fn snapshot( + project: &Project, + options: &CheckArgs, +) -> (Vec, schema::Report) { let context = project.context(); let scope = inventory::scope(options, &context).unwrap(); let inputs = inventory::collect(options, &context, &scope).unwrap();