Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- **Bumped `rustls` 0.23.45 → 0.23.46 and `quinn-proto` 0.11.16 → 0.11.19** (RUSTSEC-2026-0338/0339/0340/0341/0342 and RUSTSEC-2026-0346/0347/0348/0349), plus the yanked `chacha20` 0.10.1 → 0.10.2. Lockfile-only patch bumps; this was failing the `Security Audit (CVE)` job on every PR.

- **A judge's findings are untrusted input, bounded by one gate (#644).** `judge::admit` clears `rule_id` (only deterministic scanners may carry one; it feeds suppression and the finding cap), truncates `title`/`body`/`suggested_fix`, caps the list at 200 findings (most severe kept), drops findings for files outside the diff or scan batch, and turns a line the diff/file does not contain into a file-level finding. `cora review` and `cora scan` both apply it; deterministic findings are merged afterwards, so a judge can add findings but never remove one. `cora scan` now wraps each batch in an untrusted-data fence like review does.

- **The review cache can no longer serve a stale or cross-mode result (#644).** The cache moved behind the LLM adapter and stores the model's raw answer keyed by the exact prompts (so focus, rules, profile, system prompt, sanitization, context and memory all invalidate it) plus model/provider/base URL/temperature. `ignore.rules`, `hook.min_severity` and deterministic rules are applied on every run, including cache hits. Existing cache entries are simply not hit again.

### Changed

- **Review is now prepare / judge / finalize with the LLM as the seam (#649).** `review::prepare` turns a diff into a `judge::Packet` (judge-visible text, assembled context, system prompt override, `valid_files`, diff chunks, deterministic findings); a `judge::Judge` returns findings (`HttpJudge` = today's LLM call, `NoJudge` = deterministic only); `review::review_with` merges, guards file paths, post-processes and decides `should_block`. Output of `cora review` is unchanged. The review pipeline now has an end-to-end test with a fake judge (no network), and the `llm` module no longer prints: spinners moved to `progress::Spinner` and `llm::review_diff` / `llm::scan_files` take an events observer like the stream variant.
Expand Down
6 changes: 5 additions & 1 deletion src/commands/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use tracing::debug;
use crate::config::schema::Config;
use crate::engine::ReviewIssue;
use crate::engine::finalize::{self, HistoryEntry, ShapeOptions};
use crate::engine::judge::admit;
use crate::engine::postprocess::{Source, dropped_warning, postprocess_report};
use crate::engine::scanner::{FileEntry, batch_files, format_batch_for_prompt, walk_project};
use crate::engine::types::TokenUsage;
Expand Down Expand Up @@ -194,7 +195,10 @@ pub async fn execute_scan(
spinner.finish();
match scanned {
Ok((issues, _summary, tokens)) => {
all_issues.extend(issues);
// The model's answer is untrusted: only files of this batch,
// bounded text, no deterministic markers (#644).
let batch_paths: Vec<String> = batch.iter().map(|f| f.path.clone()).collect();
all_issues.extend(admit(issues, &batch_paths, &Source::Files(batch)));
total_tokens = match (total_tokens, tokens) {
(Some(mut acc), Some(t)) => {
acc.input_tokens += t.input_tokens;
Expand Down
21 changes: 14 additions & 7 deletions src/engine/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,20 @@ fn migrate_legacy_cache_dir(new_dir: &std::path::Path) {
}
}

/// Compute SHA-256 hex digest of the diff content + config parameters.
/// Compute SHA-256 hex digest of the judge input (the exact prompts) + config
/// parameters.
/// Includes model, provider, base_url, and temperature so config changes
/// invalidate the cache (e.g., switching providers with the same model name).
#[allow(clippy::format_collect)]
fn cache_key(diff: &str, model: &str, temperature: f32, provider: &str, base_url: &str) -> String {
fn cache_key(
key_text: &str,
model: &str,
temperature: f32,
provider: &str,
base_url: &str,
) -> String {
let mut hasher = Sha256::new();
hasher.update(diff.as_bytes());
hasher.update(key_text.as_bytes());
hasher.update(model.as_bytes());
hasher.update(provider.as_bytes());
hasher.update(base_url.as_bytes());
Expand All @@ -93,14 +100,14 @@ fn cache_key(diff: &str, model: &str, temperature: f32, provider: &str, base_url
/// Returns `None` if no cache exists or if the cache entry has expired
/// (based on `ttl` in minutes).
pub fn get_cached_review(
diff: &str,
key_text: &str,
model: &str,
temperature: f32,
ttl: u64,
provider: &str,
base_url: &str,
) -> Option<ReviewResponse> {
let hash = cache_key(diff, model, temperature, provider, base_url);
let hash = cache_key(key_text, model, temperature, provider, base_url);
let dir = cache_dir().ok()?;
let path = dir.join(format!("{hash}.json"));

Expand Down Expand Up @@ -139,7 +146,7 @@ pub fn get_cached_review(

/// Save a review response to the cache.
pub fn save_cached_review(
diff: &str,
key_text: &str,
model: &str,
temperature: f32,
response: &ReviewResponse,
Expand All @@ -149,7 +156,7 @@ pub fn save_cached_review(
let dir = cache_dir()?;
std::fs::create_dir_all(&dir).map_err(CoraError::CacheIo)?;

let hash = cache_key(diff, model, temperature, provider, base_url);
let hash = cache_key(key_text, model, temperature, provider, base_url);
let path = dir.join(format!("{hash}.json"));

let now = SystemTime::now()
Expand Down
Loading
Loading