diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ee77870..d81b247 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -153,6 +153,7 @@ jobs: - name: Publish GitHub release uses: softprops/action-gh-release@v3 with: + name: CodeForge Engine ${{ env.RELEASE_TAG }} body_path: docs/releases/${{ env.RELEASE_TAG }}.md prerelease: ${{ contains(env.RELEASE_TAG, '-') }} make_latest: ${{ !contains(env.RELEASE_TAG, '-') }} @@ -174,4 +175,3 @@ jobs: mkdir -p published gh release download "$RELEASE_TAG" --dir published --clobber python3 scripts/checksum_release_assets.py verify artifacts/checksums.txt published - diff --git a/CHANGELOG.md b/CHANGELOG.md index 24d68f8..2743dcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,19 @@ # Changelog -## Unreleased +## v1.1.0 - 2026-10-20 ### Added - Review responses include additive coverage metadata: selected scope and languages, indexed versus selected files, relevant engine availability, external-analysis intent, and explicit coverage limitations. - The desktop review view shows the coverage strip before diagnostics so a small finding count is not mistaken for repository-wide assurance. +- Benchmark summaries now include P95 latency and record CodeForge/version, Git revision/worktree state, OS, architecture and local Rust tool versions when available. ### Fixed -- `baseline prune` now evaluates the current workspace, defaults to a dry run, requires `--apply` to write, reports removable entries, and refuses the entire write on ambiguous structural/context matches. +- Baseline pruning now evaluates the current workspace, defaults to a dry run, removes only resolved entries, retains stale entries for review, requires `--complete-scan --apply`, and refuses writes on ambiguous matches or incomplete scan evidence. +- Rule lifecycle checks now compare an explicit rule semantics version instead of the CodeForge package version. +- Python `None` comparisons now use AST operands, avoiding string literals and chained-comparison rewrites. +- Java empty-catch detection now uses the catch body AST, catches whitespace-empty handlers and skips documented or nonempty handlers. ### Compatibility diff --git a/Cargo.lock b/Cargo.lock index 32a9015..dbb18db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -500,7 +500,7 @@ checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" [[package]] name = "codeforge-baseline" -version = "1.0.0" +version = "1.1.0" dependencies = [ "chrono", "codeforge-protocol", @@ -514,7 +514,7 @@ dependencies = [ [[package]] name = "codeforge-benchmark" -version = "1.0.0" +version = "1.1.0" dependencies = [ "chrono", "codeforge-protocol", @@ -527,7 +527,7 @@ dependencies = [ [[package]] name = "codeforge-cli" -version = "1.0.0" +version = "1.1.0" dependencies = [ "anyhow", "clap", @@ -546,7 +546,7 @@ dependencies = [ [[package]] name = "codeforge-core" -version = "1.0.0" +version = "1.1.0" dependencies = [ "async-trait", "codeforge-benchmark", @@ -572,7 +572,7 @@ dependencies = [ [[package]] name = "codeforge-desktop" -version = "1.0.0" +version = "1.1.0" dependencies = [ "codeforge-baseline", "codeforge-core", @@ -591,7 +591,7 @@ dependencies = [ [[package]] name = "codeforge-diagnostics" -version = "1.0.0" +version = "1.1.0" dependencies = [ "codeforge-protocol", "serde", @@ -600,7 +600,7 @@ dependencies = [ [[package]] name = "codeforge-engines" -version = "1.0.0" +version = "1.1.0" dependencies = [ "async-trait", "codeforge-protocol", @@ -625,7 +625,7 @@ dependencies = [ [[package]] name = "codeforge-fleet" -version = "1.0.0" +version = "1.1.0" dependencies = [ "async-trait", "chrono", @@ -651,7 +651,7 @@ dependencies = [ [[package]] name = "codeforge-git" -version = "1.0.0" +version = "1.1.0" dependencies = [ "codeforge-protocol", "serde", @@ -663,7 +663,7 @@ dependencies = [ [[package]] name = "codeforge-plugin" -version = "1.0.0" +version = "1.1.0" dependencies = [ "codeforge-protocol", "semver", @@ -675,7 +675,7 @@ dependencies = [ [[package]] name = "codeforge-protocol" -version = "1.0.0" +version = "1.1.0" dependencies = [ "chrono", "serde", @@ -686,7 +686,7 @@ dependencies = [ [[package]] name = "codeforge-scheduler" -version = "1.0.0" +version = "1.1.0" dependencies = [ "tempfile", "thiserror 2.0.21", @@ -697,7 +697,7 @@ dependencies = [ [[package]] name = "codeforge-transform" -version = "1.0.0" +version = "1.1.0" dependencies = [ "chrono", "codeforge-protocol", @@ -713,7 +713,7 @@ dependencies = [ [[package]] name = "codeforge-verification" -version = "1.0.0" +version = "1.1.0" dependencies = [ "codeforge-protocol", "serde", @@ -725,7 +725,7 @@ dependencies = [ [[package]] name = "codeforge-workspace" -version = "1.0.0" +version = "1.1.0" dependencies = [ "codeforge-protocol", "ignore", diff --git a/Cargo.toml b/Cargo.toml index 9f1d5eb..00a5307 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/*", "apps/desktop/src-tauri"] [workspace.package] -version = "1.0.0" +version = "1.1.0" edition = "2024" rust-version = "1.90" license = "Apache-2.0" diff --git a/README.md b/README.md index c81cfbd..0c9e7c7 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ autonomous coding agent. [![CI](https://github.com/styayur/codeforge-engine/actions/workflows/ci.yml/badge.svg)](https://github.com/styayur/codeforge-engine/actions/workflows/ci.yml) [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE) -[Documentation](docs/README.md) · [Latest stable release: v1.0.0](https://github.com/styayur/codeforge-engine/releases/tag/v1.0.0) · [Changelog](CHANGELOG.md) +[Documentation](docs/README.md) · [Latest stable release: v1.1.0](https://github.com/styayur/codeforge-engine/releases/tag/v1.1.0) · [Changelog](CHANGELOG.md) ## Why CodeForge @@ -75,7 +75,7 @@ missing required tools do not count as a pass. ## Installation Download the CLI archive for your platform from the -[v1.0.0 release](https://github.com/styayur/codeforge-engine/releases/tag/v1.0.0), +[v1.1.0 release](https://github.com/styayur/codeforge-engine/releases/tag/v1.1.0), verify it against `checksums.txt`, and extract `codeforge` (`codeforge.exe` on Windows). Desktop packages are available on the same page. Windows requires WebView2; Linux desktop builds require WebKitGTK. Packages do not install optional @@ -84,7 +84,7 @@ language tools. To build the CLI from the tagged source instead: ```bash -git clone --branch v1.0.0 --depth 1 https://github.com/styayur/codeforge-engine.git +git clone --branch v1.1.0 --depth 1 https://github.com/styayur/codeforge-engine.git cd codeforge-engine cargo install --locked --path crates/codeforge-cli codeforge --version @@ -121,7 +121,7 @@ repository. See [Verification](docs/VERIFICATION.md) and [baseline setup](docs/baseline.md); a baseline must be reviewed, not generated merely to make CI green. -Experimental commands included in the v1.0.0 package: +Experimental commands included in the v1.1.0 package: ```bash codeforge context . --query verification --limit 10 --max-bytes 32768 diff --git a/SECURITY.md b/SECURITY.md index f431462..94d0273 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,7 +1,7 @@ # Security policy Read the [security model](docs/SECURITY.md) for process, source and evidence -boundaries. CodeForge Engine v1.0.0 is the latest stable release. There is no +boundaries. CodeForge Engine v1.1.0 is the latest stable release. There is no long-term-support or security-response-time guarantee. Use [GitHub private vulnerability reporting](https://github.com/styayur/codeforge-engine/security/advisories/new) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index f1c2081..199507e 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@codeforge/desktop", "private": true, - "version": "1.0.0", + "version": "1.1.0", "type": "module", "scripts": { "dev": "vite --host 127.0.0.1 --port 1420", diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 3d89b40..778aeec 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "CodeForge Engine", - "version": "1.0.0", + "version": "1.1.0", "identifier": "dev.codeforge.engine", "build": { "beforeDevCommand": "pnpm dev", diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 39452f8..bd23862 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -633,6 +633,11 @@ function BenchmarkSnapshotBlock({ snapshot }: { snapshot: ReturnType

{snapshot.metric}

{snapshot.command.join(" ")} +

+ {snapshot.warmup} warm-up · {snapshot.samples} measured sample(s) + {snapshot.environment.git_commit ? ` · commit ${snapshot.environment.git_commit.slice(0, 12)}` : ""} + {snapshot.environment.rustc ? ` · ${snapshot.environment.rustc}` : ""} +

Median{snapshot.summary.median.toFixed(3)} {snapshot.summary.unit}
Variance{snapshot.summary.variance.toFixed(6)}
diff --git a/apps/desktop/src/types.ts b/apps/desktop/src/types.ts index 34cfb1b..d098b96 100644 --- a/apps/desktop/src/types.ts +++ b/apps/desktop/src/types.ts @@ -203,6 +203,9 @@ export interface BenchmarkSnapshot { metric: string; command: string[]; summary: TimingSummary; + warmup: number; + samples: number; + environment: Record; } export interface OptimizationCandidate { diff --git a/crates/codeforge-baseline/src/lib.rs b/crates/codeforge-baseline/src/lib.rs index f5ace0d..033f41f 100644 --- a/crates/codeforge-baseline/src/lib.rs +++ b/crates/codeforge-baseline/src/lib.rs @@ -6,7 +6,8 @@ use std::path::{Path, PathBuf}; use chrono::Utc; use codeforge_protocol::{ BaselineComparison, BaselineComparisonEntry, BaselineEntry, BaselineFile, BaselineMatchKind, - Diagnostic, FindingDisposition, FindingLifecycle, FindingStatus, SourceContext, + Diagnostic, FindingDisposition, FindingLifecycle, FindingStatus, RULE_SEMANTICS_VERSION, + SourceContext, }; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -29,6 +30,8 @@ pub enum BaselineError { Invalid(String), #[error("baseline pruning blocked by {0} ambiguous structural/context match(es)")] AmbiguousPrune(usize), + #[error("baseline pruning requires complete, traceable scan evidence: {0}")] + IncompleteScanPrune(String), #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] @@ -94,6 +97,21 @@ pub struct PruneCandidate { pub lifecycle: FindingLifecycle, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ScanEvidence { + pub complete: bool, + pub reasons: Vec, +} + +impl Default for ScanEvidence { + fn default() -> Self { + Self { + complete: false, + reasons: vec!["complete scan was not evidenced".to_owned()], + } + } +} + #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct PruneReport { pub applied: bool, @@ -101,9 +119,13 @@ pub struct PruneReport { pub removable: usize, pub removed: usize, pub retained: usize, + pub stale: usize, pub ambiguous: usize, pub blocked_by_ambiguity: bool, - pub entries: Vec, + pub blocked_by_incomplete_scan: bool, + pub scan: ScanEvidence, + pub resolved_entries: Vec, + pub stale_entries: Vec, } #[derive(Debug, Clone, Default)] @@ -183,16 +205,13 @@ impl BaselineStore { &mut self, diagnostics: &[Diagnostic], repository_root: impl AsRef, + scan: ScanEvidence, apply: bool, ) -> Result { let comparison = self.compare(diagnostics, repository_root); - if apply && !comparison.ambiguous.is_empty() { - return Err(BaselineError::AmbiguousPrune(comparison.ambiguous.len())); - } - let mut candidates = comparison + let mut resolved_entries = comparison .resolved .into_iter() - .chain(comparison.stale) .map(|entry| PruneCandidate { fingerprint: entry.fingerprint, rule_id: entry.rule_id, @@ -201,11 +220,36 @@ impl BaselineStore { lifecycle: entry.lifecycle, }) .collect::>(); - candidates.sort_by(|left, right| left.fingerprint.cmp(&right.fingerprint)); + let mut stale_entries = comparison + .stale + .into_iter() + .map(|entry| PruneCandidate { + fingerprint: entry.fingerprint, + rule_id: entry.rule_id, + path: entry.path, + disposition: entry.disposition, + lifecycle: entry.lifecycle, + }) + .collect::>(); + resolved_entries.sort_by(|left, right| left.fingerprint.cmp(&right.fingerprint)); + stale_entries.sort_by(|left, right| left.fingerprint.cmp(&right.fingerprint)); + let ambiguous = comparison.ambiguous.len(); + if apply && ambiguous > 0 { + return Err(BaselineError::AmbiguousPrune(ambiguous)); + } + if apply && !scan.complete { + return Err(BaselineError::IncompleteScanPrune( + if scan.reasons.is_empty() { + "scan evidence did not establish completeness".to_owned() + } else { + scan.reasons.join("; ") + }, + )); + } let inspected = self.file.entries.len(); - let removable = candidates.len(); + let removable = resolved_entries.len(); let removed = if apply { - let fingerprints = candidates + let fingerprints = resolved_entries .iter() .map(|entry| entry.fingerprint.as_str()) .collect::>(); @@ -217,30 +261,22 @@ impl BaselineStore { } else { 0 }; - let ambiguous = comparison.ambiguous.len(); Ok(PruneReport { applied: apply, inspected, removable, removed, retained: self.file.entries.len(), + stale: stale_entries.len(), ambiguous, blocked_by_ambiguity: ambiguous > 0, - entries: candidates, + blocked_by_incomplete_scan: !scan.complete, + scan, + resolved_entries, + stale_entries, }) } - pub fn prune(&mut self) -> usize { - let before = self.file.entries.len(); - self.file.entries.retain(|entry| { - !matches!( - entry.lifecycle, - FindingLifecycle::Resolved | FindingLifecycle::Stale - ) - }); - before - self.file.entries.len() - } - pub fn set_disposition( &mut self, fingerprint: &str, @@ -640,7 +676,7 @@ fn is_rule_version_stale(entry: &BaselineEntry) -> bool { entry .rule_version .as_deref() - .is_some_and(|version| version != env!("CARGO_PKG_VERSION")) + .is_some_and(|version| version != RULE_SEMANTICS_VERSION) } pub fn fingerprint_diagnostic( @@ -972,8 +1008,12 @@ mod tests { ) .expect("accept"); + let complete = ScanEvidence { + complete: true, + reasons: Vec::new(), + }; let report = store - .prune_against(&[], temp.path(), false) + .prune_against(&[], temp.path(), complete.clone(), false) .expect("dry run"); assert!(!report.applied); assert_eq!(report.inspected, 1); @@ -983,7 +1023,9 @@ mod tests { assert!(!report.blocked_by_ambiguity); assert_eq!(store.entries().len(), 1); - let report = store.prune_against(&[], temp.path(), true).expect("apply"); + let report = store + .prune_against(&[], temp.path(), complete, true) + .expect("apply"); assert!(report.applied); assert_eq!(report.removed, 1); assert_eq!(report.retained, 0); @@ -1021,12 +1063,76 @@ mod tests { }) .expect("store"); let error = store - .prune_against(&[diagnosis], temp.path(), true) + .prune_against( + &[diagnosis], + temp.path(), + ScanEvidence { + complete: true, + reasons: Vec::new(), + }, + true, + ) .expect_err("ambiguous prune must fail"); assert!(matches!(error, BaselineError::AmbiguousPrune(1))); assert_eq!(store.entries().len(), 2); } + #[test] + fn prune_retains_stale_entries_and_requires_complete_scan() { + let temp = tempfile::tempdir().expect("tempdir"); + let stale_entry = BaselineEntry { + fingerprint: "stale".to_owned(), + structural_fingerprint: None, + context_fingerprint: None, + rule_id: "RS-TEST-001".to_owned(), + path: PathBuf::from("src/lib.rs"), + symbol: None, + context_hash: None, + source_context: SourceContext::Production, + disposition: FindingDisposition::Accepted, + reason: Some("reviewed".to_owned()), + first_seen: Utc::now(), + last_reviewed: Some(Utc::now()), + rule_version: Some("old-rule-version".to_owned()), + lifecycle: FindingLifecycle::Active, + }; + let mut store = BaselineStore::from_file(BaselineFile { + schema_version: 1, + codeforge_version: env!("CARGO_PKG_VERSION").to_owned(), + entries: vec![stale_entry], + }) + .expect("store"); + let report = store + .prune_against( + &[], + temp.path(), + ScanEvidence { + complete: true, + reasons: Vec::new(), + }, + true, + ) + .expect("stale prune"); + assert_eq!(report.removed, 0); + assert_eq!(report.stale, 1); + assert_eq!(report.stale_entries[0].lifecycle, FindingLifecycle::Stale); + assert_eq!(store.entries().len(), 1); + + let error = store + .prune_against( + &[], + temp.path(), + ScanEvidence { + complete: false, + reasons: vec!["scanner unavailable".to_owned()], + }, + true, + ) + .expect_err("incomplete scan must fail"); + assert!(matches!(error, BaselineError::IncompleteScanPrune(_))); + assert_eq!(store.entries().len(), 1); + } + #[test] fn changed_rule_version_marks_baseline_stale() { let temp = tempfile::tempdir().expect("tempdir"); diff --git a/crates/codeforge-benchmark/src/lib.rs b/crates/codeforge-benchmark/src/lib.rs index 9dfd00a..942db13 100644 --- a/crates/codeforge-benchmark/src/lib.rs +++ b/crates/codeforge-benchmark/src/lib.rs @@ -100,12 +100,43 @@ pub fn environment() -> BTreeMap { values.insert("os".to_owned(), std::env::consts::OS.to_owned()); values.insert("arch".to_owned(), std::env::consts::ARCH.to_owned()); values.insert("family".to_owned(), std::env::consts::FAMILY.to_owned()); + values.insert( + "codeforge_version".to_owned(), + env!("CARGO_PKG_VERSION").to_owned(), + ); if let Ok(cpus) = std::thread::available_parallelism() { values.insert("logical_cpus".to_owned(), cpus.get().to_string()); } + if let Some(commit) = command_output("git", &["rev-parse", "HEAD"]) { + values.insert("git_commit".to_owned(), commit); + } + if let Some(status) = command_output("git", &["status", "--porcelain"]) { + values.insert( + "git_worktree".to_owned(), + if status.is_empty() { "clean" } else { "dirty" }.to_owned(), + ); + } + if let Some(rustc) = command_output("rustc", &["--version"]) { + values.insert("rustc".to_owned(), rustc); + } + if let Some(cargo) = command_output("cargo", &["--version"]) { + values.insert("cargo".to_owned(), cargo); + } values } +fn command_output(program: &str, args: &[&str]) -> Option { + let output = std::process::Command::new(program) + .args(args) + .output() + .ok()?; + if !output.status.success() { + return None; + } + let text = String::from_utf8_lossy(&output.stdout).trim().to_owned(); + (!text.is_empty()).then_some(text) +} + #[cfg(test)] mod tests { use super::*; @@ -123,5 +154,14 @@ mod tests { let summary = runner.measure(spec).await.expect("measure"); assert_eq!(summary.samples.len(), 2); assert!(summary.median >= 0.0); + assert!(summary.p95 >= summary.median); + } + + #[test] + fn environment_records_version_and_platform() { + let values = environment(); + assert_eq!(values["codeforge_version"], env!("CARGO_PKG_VERSION")); + assert!(values.contains_key("os")); + assert!(values.contains_key("arch")); } } diff --git a/crates/codeforge-cli/src/main.rs b/crates/codeforge-cli/src/main.rs index 4143f6e..327eebe 100644 --- a/crates/codeforge-cli/src/main.rs +++ b/crates/codeforge-cli/src/main.rs @@ -3,12 +3,14 @@ use std::process::ExitCode; use anyhow::{Context, Result}; use clap::{Args, Parser, Subcommand}; -use codeforge_baseline::{BaselineStore, PrecisionRow}; -use codeforge_core::{CodeForgeEngine, ReviewOptions}; +use codeforge_baseline::{BaselineStore, PrecisionRow, ScanEvidence}; +use codeforge_core::{CodeForgeEngine, ReviewOptions, ReviewSelection}; use codeforge_fleet::{ Doctor, FleetCommand as FleetOperation, FleetConfig, FleetRunOptions, FleetRunner, }; -use codeforge_protocol::{FindingDisposition, Language, Severity, diagnostics_to_sarif}; +use codeforge_protocol::{ + Capability, FindingDisposition, Language, Severity, diagnostics_to_sarif, +}; use serde::Serialize; #[derive(Debug, Parser)] @@ -304,9 +306,12 @@ struct BaselinePruneArgs { json: bool, #[arg(long)] include_fixtures: bool, - /// Remove resolved or stale entries. Without this flag pruning is a dry run. + /// Remove resolved entries. Without this flag pruning is a dry run. #[arg(long)] apply: bool, + /// Confirm that the current full scan is complete and suitable for destructive cleanup. + #[arg(long)] + complete_scan: bool, } #[derive(Debug, Args)] @@ -952,10 +957,12 @@ async fn benchmark(args: BenchmarkArgs) -> Result<()> { } else { println!("Command: {}", snapshot.command.join(" ")); println!( - "Samples: {} median: {:.3} {} variance: {:.6}", + "Samples: {} median: {:.3} {} p95: {:.3} {} variance: {:.6}", snapshot.summary.samples.len(), snapshot.summary.median, snapshot.summary.unit, + snapshot.summary.p95, + snapshot.summary.unit, snapshot.summary.variance ); } @@ -1040,8 +1047,50 @@ async fn baseline(args: BaselineArgs) -> Result<()> { } else { engine.review(options).await? }; + let mut scan_reasons = Vec::new(); + let coverage = &report.coverage; + if coverage.selection != ReviewSelection::AllIndexedFiles { + scan_reasons.push("scan selected only changed files".to_owned()); + } + if !coverage.requested_languages.is_empty() { + scan_reasons.push("scan applied a language filter".to_owned()); + } + if coverage.selected_files != coverage.discovered_files { + scan_reasons.push(format!( + "selected {} of {} indexed files", + coverage.selected_files, coverage.discovered_files + )); + } + if coverage.submitted_files != coverage.selected_files { + scan_reasons.push(format!( + "submitted {} of {} selected files", + coverage.submitted_files, coverage.selected_files + )); + } + let missing_lint_engines = coverage + .engines + .iter() + .filter(|engine| { + !engine.available && engine.metadata.capabilities.contains(&Capability::Lint) + }) + .map(|engine| engine.metadata.id.clone()) + .collect::>(); + if !missing_lint_engines.is_empty() { + scan_reasons.push(format!( + "relevant lint engines unavailable: {}", + missing_lint_engines.join(", ") + )); + } + if !args.complete_scan { + scan_reasons.push("operator did not pass --complete-scan".to_owned()); + } + let scan = ScanEvidence { + complete: scan_reasons.is_empty(), + reasons: scan_reasons, + }; let mut store = BaselineStore::load(&args.file)?; - let report = store.prune_against(&report.diagnostics, engine.root(), args.apply)?; + let report = + store.prune_against(&report.diagnostics, engine.root(), scan, args.apply)?; if args.apply { store.save(&args.file)?; } @@ -1054,19 +1103,35 @@ async fn baseline(args: BaselineArgs) -> Result<()> { report.ambiguous ); } + if report.blocked_by_incomplete_scan { + eprintln!( + "warning: incomplete scan evidence blocks --apply: {}", + report.scan.reasons.join("; ") + ); + } println!( - "{} {} resolved/stale entries; {} retained.", + "{} {} resolved entries; {} stale entries retained; {} entries retained.", if report.applied { "Removed" } else { "Would remove" }, report.removable, + report.stale, report.retained ); - for entry in &report.entries { + for entry in &report.resolved_entries { + println!( + " resolved [{}] {} {} ({:?})", + entry.rule_id, + entry.path.display(), + entry.fingerprint, + entry.lifecycle + ); + } + for entry in &report.stale_entries { println!( - " [{}] {} {} ({:?})", + " stale-retained [{}] {} {} ({:?})", entry.rule_id, entry.path.display(), entry.fingerprint, diff --git a/crates/codeforge-core/src/lib.rs b/crates/codeforge-core/src/lib.rs index df84850..e853cd3 100644 --- a/crates/codeforge-core/src/lib.rs +++ b/crates/codeforge-core/src/lib.rs @@ -6,7 +6,7 @@ pub mod context; pub mod external; mod sandbox; -use std::collections::{BTreeSet, HashMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::sync::RwLock; use std::time::Instant; @@ -118,6 +118,16 @@ pub struct BenchmarkSnapshot { pub metric: String, pub command: Vec, pub summary: TimingSummary, + #[serde(default = "default_benchmark_warmup")] + pub warmup: usize, + #[serde(default)] + pub samples: usize, + #[serde(default)] + pub environment: BTreeMap, +} + +const fn default_benchmark_warmup() -> usize { + 1 } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -413,6 +423,9 @@ impl CodeForgeEngine { metric: command.label.clone(), command: command.command_line(), summary, + warmup: 1, + samples, + environment: codeforge_benchmark::environment(), }) } diff --git a/crates/codeforge-engines/src/builtin.rs b/crates/codeforge-engines/src/builtin.rs index 6cf73cf..62aeb48 100644 --- a/crates/codeforge-engines/src/builtin.rs +++ b/crates/codeforge-engines/src/builtin.rs @@ -553,16 +553,28 @@ fn python_rule(source: &str, node: Node<'_>) -> Option { title: "Replace bare except".to_owned(), }), }), - "comparison_operator" - if text.contains("None") && (text.contains("==") || text.contains("!=")) => - { - let (operator, replacement_operator) = if text.contains("!=") { - ("!=", " is not ") + "comparison_operator" => { + let mut cursor = node.walk(); + let children = node.children(&mut cursor).collect::>(); + let mut operand_cursor = node.walk(); + let operands = node.named_children(&mut operand_cursor).collect::>(); + if operands.len() != 2 || !operands.iter().any(|operand| operand.kind() == "none") { + return None; + } + let operator = children + .iter() + .find(|child| matches!(child.kind(), "==" | "!="))?; + let replacement_operator = if operator.kind() == "!=" { + " is not " } else { - ("==", " is ") + " is " }; - let (left, right) = text.split_once(operator)?; - let replacement = format!("{}{}{}", left.trim(), replacement_operator, right.trim()); + let replacement = format!( + "{}{}{}", + node_text(source, operands[0]).trim(), + replacement_operator, + node_text(source, operands[1]).trim() + ); Some(Finding { rule_id: "PY-CORRECTNESS-001".to_owned(), severity: Severity::Warning, @@ -800,18 +812,25 @@ fn javascript_rule(source: &str, node: Node<'_>) -> Option { fn java_rule(source: &str, node: Node<'_>) -> Option { let text = node_text(source, node); match node.kind() { - "catch_clause" if text.contains("{}") => Some(Finding { - rule_id: "JAVA-CORRECTNESS-001".to_owned(), - severity: Severity::Warning, - category: DiagnosticCategory::Correctness, - confidence: Confidence::Medium, - message: "Empty catch block swallows failures".to_owned(), - explanation: "Log, translate, or explicitly document why the exception can be ignored." - .to_owned(), - start_byte: node.start_byte(), - end_byte: node.end_byte(), - fix: None, - }), + "catch_clause" => { + let body = node.child_by_field_name("body")?; + if body.named_child_count() != 0 { + return None; + } + Some(Finding { + rule_id: "JAVA-CORRECTNESS-001".to_owned(), + severity: Severity::Warning, + category: DiagnosticCategory::Correctness, + confidence: Confidence::Medium, + message: "Empty catch block swallows failures".to_owned(), + explanation: + "Log, translate, or explicitly document why the exception can be ignored." + .to_owned(), + start_byte: node.start_byte(), + end_byte: node.end_byte(), + fix: None, + }) + } "method_invocation" if text.starts_with("System.out.print") => Some(Finding { rule_id: "JAVA-MAINTAINABILITY-001".to_owned(), severity: Severity::Hint, @@ -934,6 +953,62 @@ mod tests { assert_eq!(finding.fixes[0].edits[0].replacement, "value is None"); } + #[test] + fn python_none_rule_ignores_string_literals_and_chained_comparisons() { + for source in [ + "if value == \"None\":\n pass\n", + "if value == None == other:\n pass\n", + "if value is None:\n pass\n", + ] { + let findings = analyze_source( + "python-builtin", + Language::Python, + Path::new("example.py"), + source, + ); + assert!( + !findings + .iter() + .any(|finding| finding.rule_id == "PY-CORRECTNESS-001"), + "{source}" + ); + } + } + + #[test] + fn java_empty_catch_rule_uses_ast_body_and_ignores_documented_or_nonempty_handlers() { + let empty = "class Example { void run() { try { work(); } catch (Exception e) { } } }"; + let findings = analyze_source( + "java-builtin", + Language::Java, + Path::new("Example.java"), + empty, + ); + assert!( + findings + .iter() + .any(|finding| finding.rule_id == "JAVA-CORRECTNESS-001") + ); + + for source in [ + "class Example { void run() { try { work(); } catch (Exception e) { /* documented */ } } }", + "class Example { void run() { try { work(); } catch (Exception e) { log(e); } } }", + ] { + let findings = analyze_source( + "java-builtin", + Language::Java, + Path::new("Example.java"), + source, + ); + assert!( + !findings + .iter() + .any(|finding| finding.rule_id == "JAVA-CORRECTNESS-001"), + "{source}" + ); + } + } + #[test] fn broken_rust_is_reported_as_parse_error() { let findings = analyze_source( diff --git a/crates/codeforge-protocol/src/lib.rs b/crates/codeforge-protocol/src/lib.rs index e6b6279..687eea7 100644 --- a/crates/codeforge-protocol/src/lib.rs +++ b/crates/codeforge-protocol/src/lib.rs @@ -26,6 +26,10 @@ pub enum ProtocolError { Json(#[from] serde_json::Error), } +/// Semantic version of CodeForge's built-in rule behavior and fingerprints. +/// This is independent from the software package version. +pub const RULE_SEMANTICS_VERSION: &str = "1"; + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum Language { @@ -639,7 +643,7 @@ impl Diagnostic { codeforge_rule_id: None, source_context: SourceContext::Production, symbol: None, - rule_version: Some(env!("CARGO_PKG_VERSION").to_owned()), + rule_version: Some(RULE_SEMANTICS_VERSION.to_owned()), disposition: FindingDisposition::Unreviewed, disposition_reason: None, reviewed_at: None, @@ -963,6 +967,8 @@ pub struct TimingSummary { pub variance: f64, #[serde(default)] pub stddev: f64, + #[serde(default)] + pub p95: f64, pub min: f64, pub max: f64, } @@ -992,6 +998,7 @@ impl TimingSummary { .sum::() / count as f64 }; + let p95 = percentile(&samples, 95.0); Self { unit: unit.into(), min: samples.first().copied().unwrap_or_default(), @@ -1001,10 +1008,27 @@ impl TimingSummary { median, variance, stddev: variance.sqrt(), + p95, } } } +fn percentile(sorted_samples: &[f64], percentile: f64) -> f64 { + if sorted_samples.is_empty() { + return 0.0; + } + let bounded = percentile.clamp(0.0, 100.0) / 100.0; + let position = bounded * (sorted_samples.len() - 1) as f64; + let lower = position.floor() as usize; + let upper = position.ceil() as usize; + if lower == upper { + sorted_samples[lower] + } else { + let weight = position - lower as f64; + sorted_samples[lower] * (1.0 - weight) + sorted_samples[upper] * weight + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct BenchmarkResult { pub metric: String, @@ -2026,6 +2050,28 @@ mod tests { ); } + #[test] + fn timing_summary_reports_p95_and_keeps_legacy_payload_readable() { + let summary = TimingSummary::from_samples( + "milliseconds", + (1..=10).map(|value| value as f64).collect(), + ); + assert_eq!(summary.median, 5.5); + assert!((summary.p95 - 9.55).abs() < 1e-9); + let legacy = serde_json::json!({ + "unit": "milliseconds", + "samples": [1.0, 2.0], + "mean": 1.5, + "median": 1.5, + "variance": 0.25, + "stddev": 0.5, + "min": 1.0, + "max": 2.0 + }); + let restored: TimingSummary = serde_json::from_value(legacy).expect("legacy timing"); + assert_eq!(restored.p95, 0.0); + } + #[test] fn execution_and_finding_status_are_separate() { let repository = RepoRunSummary { diff --git a/docs/README.md b/docs/README.md index d78cc79..f74778d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,7 +1,7 @@ # Documentation Start with the [project README](../README.md) for installation and a first scan. -v1.0.0 is the current stable release; Experimental capabilities are labeled +v1.1.0 is the current stable release; Experimental capabilities are labeled explicitly and remain opt-in. | Question | Guide | diff --git a/docs/RELEASE_PREPARATION.md b/docs/RELEASE_PREPARATION.md index 958b79f..7fd0c73 100644 --- a/docs/RELEASE_PREPARATION.md +++ b/docs/RELEASE_PREPARATION.md @@ -1,62 +1,38 @@ -# v1.0.0 release evidence - -**Status: released.** [CodeForge Engine v1.0.0](https://github.com/styayur/codeforge-engine/releases/tag/v1.0.0) -was published from tag commit `51073219b6982ad2072dd2b34e90c71a71e2423f`. - -## Release governance - -- Release PR: [#27](https://github.com/styayur/codeforge-engine/pull/27), squash merged as `5107321`. -- PR and post-merge CI: Windows/Linux, Rust 1.90 MSRV, CodeForge Regression Gate, - cargo-audit, frontend audit and secret scan all passed. -- Non-publishing candidate build: [run 39284094980](https://github.com/styayur/codeforge-engine/actions/runs/39284094980). -- Publishing tag workflow: [run 39289895575](https://github.com/styayur/codeforge-engine/actions/runs/39289895575). -- The tag workflow validated tag/version metadata, reran format, Clippy and tests, - rebuilt both platforms, published normalized filenames, generated checksums, - downloaded the published assets again, and verified their bytes. - -## Platform acceptance - -### Windows x64 - -- CLI archive: version output `codeforge 1.0.0`; `scan`, JSON `review`, SARIF - export, preview-only `apply`, confirmed `apply`, `history` and `undo` passed. - The workspace was restored byte-for-byte to its pre-apply source. -- MSI: installed per-user with `ALLUSERS=2 MSIINSTALLPERUSER=1`, started with - window title `CodeForge Engine`, and uninstalled without registry or directory - residue. A default per-machine silent install requires administrator rights. -- NSIS: silent install, GUI startup, and silent uninstall passed without registry - or directory residue. -- Host: Windows 11 build 26200, x64. - -### Linux x64 - -- CLI archive: version output, review, apply and undo passed; source restoration - passed. -- Debian package: installed as `code-forge-engine 1.0.0`, desktop process started - under WSLg, and removal left no binary or installed package. -- AppImage: extracted successfully and desktop process started under WSLg. -- Host: WSL2 Ubuntu x86_64 with WSLg. Mesa emitted software-rendering warnings, - but both desktop processes remained alive during the startup window. -- No native physical Linux machine was available; this is recorded rather than - described as bare-metal acceptance. - -## Published assets - -The final release assets differ in bytes from the pre-tag candidate rebuild; the -published copies were downloaded and functionally tested after publication. - -```text -64ea02dcabae08ea7259a7317b326e3be90166d7fbbf5a04c94a2abdb511a30c CodeForge.Engine_1.0.0_amd64.AppImage -b46282dd7168528351e1028442ad7e18550c014b2b6c88bb1b90f761df1858d9 CodeForge.Engine_1.0.0_amd64.deb -8505e6f1b2a5821c56e0461372f82a07db3beaf499175dbfdcd436696e0f3c0c CodeForge.Engine_1.0.0_x64-setup.exe -0ea9a369ba85fad6cfe936aca3f8f2fe8116d21cf201d4f66da85690a613d99f CodeForge.Engine_1.0.0_x64_en-US.msi -9629a6e60d2b889dae9b528548c90b91f993c2b120a753631dd556bedd59a7b0 codeforge-cli-v1.0.0-x86_64-pc-windows-msvc.zip -5b50c57add5e9d5d1fc40091d34a8ff6cc01b1e9805a4a2b742011938d38e41c codeforge-cli-v1.0.0-x86_64-unknown-linux-gnu.tar.gz -``` - -## Surviving boundaries - -Context extraction, heuristic impact/test suggestions, local SARIF path import and -optional CodeQL execution remain Experimental. Fake-process tests do not certify a -real CodeQL installation. Name-based relations do not provide complete scope/type -resolution. Missing required tools remain execution gaps, not passing evidence. +# v1.1.0 release preparation + +**Status: release candidate, not yet published.** Version metadata is frozen at +1.1.0 and the release workflow can build candidate artifacts without publishing. + +## Completed in the candidate + +- Baseline pruning removes only resolved entries, retains stale entries, requires + `--complete-scan --apply`, and blocks ambiguous or incomplete scans. +- Rule lifecycle uses an explicit rule-semantics version rather than the package + version. +- Python `None` and Java empty-catch rules use AST evidence with positive and + negative regression tests. +- Review coverage fields expose indexed/selected/submitted counts and engine + availability in JSON and the desktop review view. +- Benchmark summaries include P95, warm-up/sample counts and environment records + including Git revision and local Rust tool versions. +- Release workflow explicitly sets the release title from the tag. + +## Required before publication + +- [ ] Required PR CI, Security and CodeForge Regression gates are green. +- [ ] Non-publishing candidate build succeeds for Windows and Linux. +- [ ] Windows candidate CLI, MSI and NSIS are smoke-tested. +- [ ] Linux candidate CLI, deb and AppImage are smoke-tested in an available + environment; limitations are recorded. +- [ ] Published assets are redownloaded and SHA-256 verified after release. + +## Compatibility + +Baseline schema remains 1. SARIF remains 2.1.0. Review coverage is additive. +Destructive baseline pruning is intentionally stricter and requires migration to +`--complete-scan --apply`. + +## Historical evidence + +The completed v1.0.0 acceptance record is retained at +[v1.0.0 evidence](releases/v1.0.0-evidence.md). diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 153c001..ec5ee71 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # Roadmap and maturity -## Stable workflows in v1.0.0 +## Stable workflows in v1.1.0 Native parsing/review, reviewed baselines, CLI and desktop preview/apply/undo, transaction history, configurable verification, SARIF export and independent @@ -10,7 +10,7 @@ existence does not make every proposed fix safe. ## Experimental capabilities On-demand context/impact and external SARIF path/CodeQL commands are included in -v1.0.0 as Experimental, opt-in paths. Impact resolution is name-based and bounded; +v1.1.0 as Experimental, opt-in paths. Impact resolution is name-based and bounded; test suggestions cannot replace a full test gate. Real CodeQL execution has not been certified. Desktop package build success alone does not establish end-to-end acceptance on every platform. @@ -20,7 +20,7 @@ acceptance on every platform. Evidence coverage metadata, baseline lifecycle improvements, local symbol/scope analysis, project verification orchestration, reproducible before/after benchmark comparison and executable architecture constraints require separate design and -test evidence. They are not silently promoted by the v1.0.0 release. +test evidence. They are not silently promoted by the v1.1.0 release. ## Future, not committed release scope diff --git a/docs/baseline.md b/docs/baseline.md index 3f04f70..07a2832 100644 --- a/docs/baseline.md +++ b/docs/baseline.md @@ -64,7 +64,7 @@ codeforge baseline accept --path ./repo --file .codeforge/baseline.json --findin codeforge baseline false-positive --path ./repo --file .codeforge/baseline.json --finding --reason "..." codeforge baseline unreview --path ./repo --file .codeforge/baseline.json --finding codeforge baseline prune --path ./repo --file .codeforge/baseline.json -codeforge baseline prune --path ./repo --file .codeforge/baseline.json --apply +codeforge baseline prune --path ./repo --file .codeforge/baseline.json --complete-scan --apply codeforge baseline migrate --from legacy.json --to .codeforge/baseline.json ``` @@ -72,7 +72,13 @@ Baseline writes are explicit. Audit, review, and CI are read-only with respect to the baseline. `prune` re-evaluates current diagnostics and is a dry run unless `--apply` is -provided. It removes only entries classified as resolved or stale. If any -structural/context match is ambiguous, `--apply` refuses the entire write and -leaves the baseline unchanged. Use `--include-fixtures` when the reviewed -baseline intentionally contains fixture findings. +provided. It removes only entries classified as `Resolved`; `Stale` entries are +reported and retained for review. `--apply` additionally requires +`--complete-scan` and a complete all-file review with no language filter and no +unavailable relevant lint engine. Ambiguous matches, partial scans, changed-file +scopes or unavailable analyzers block the entire write. Use `--include-fixtures` +when the reviewed baseline intentionally contains fixture findings. + +Rule staleness compares `rule_version` with the explicit CodeForge rule +semantics version, not the software package version. A missing diagnostic is only +treated as resolved when the scan evidence establishes complete evaluation. diff --git a/docs/benchmarking.md b/docs/benchmarking.md index 0f96576..0fbf100 100644 --- a/docs/benchmarking.md +++ b/docs/benchmarking.md @@ -64,8 +64,11 @@ process-startup P95 as if they were the same metric. Configure `.codeforge.toml` or use supported detected native benchmark commands. The runner uses a warm-up and measured samples, recording mean, median, variance, -minimum and maximum. Run `codeforge benchmark . --samples 5` only for a trusted -project. A command may modify files or access the network. +standard deviation, P95, minimum and maximum. Environment records include the +operating system, architecture, logical CPUs, CodeForge version, Git commit and +worktree state when available, plus `rustc` and `cargo` versions. Run +`codeforge benchmark . --samples 5` only for a trusted project. A command may +modify files or access the network. Preserve full measurements and regressions. Record command, hardware, software, sample count, correctness checks and limitations with every published comparison. diff --git a/docs/external-analysis.md b/docs/external-analysis.md index 17e5048..028d89f 100644 --- a/docs/external-analysis.md +++ b/docs/external-analysis.md @@ -1,6 +1,6 @@ # Optional external analysis -Experimental and included in the v1.0.0 package; never part of the default scan path. +Experimental and included in the v1.1.0 package; never part of the default scan path. `codeforge import-sarif report.sarif --root . --language rust` imports local SARIF 2.1.0 findings and code-flow paths as JSON. `diagnostic_index` associates diff --git a/docs/releases/v1.0.0-evidence.md b/docs/releases/v1.0.0-evidence.md new file mode 100644 index 0000000..958b79f --- /dev/null +++ b/docs/releases/v1.0.0-evidence.md @@ -0,0 +1,62 @@ +# v1.0.0 release evidence + +**Status: released.** [CodeForge Engine v1.0.0](https://github.com/styayur/codeforge-engine/releases/tag/v1.0.0) +was published from tag commit `51073219b6982ad2072dd2b34e90c71a71e2423f`. + +## Release governance + +- Release PR: [#27](https://github.com/styayur/codeforge-engine/pull/27), squash merged as `5107321`. +- PR and post-merge CI: Windows/Linux, Rust 1.90 MSRV, CodeForge Regression Gate, + cargo-audit, frontend audit and secret scan all passed. +- Non-publishing candidate build: [run 39284094980](https://github.com/styayur/codeforge-engine/actions/runs/39284094980). +- Publishing tag workflow: [run 39289895575](https://github.com/styayur/codeforge-engine/actions/runs/39289895575). +- The tag workflow validated tag/version metadata, reran format, Clippy and tests, + rebuilt both platforms, published normalized filenames, generated checksums, + downloaded the published assets again, and verified their bytes. + +## Platform acceptance + +### Windows x64 + +- CLI archive: version output `codeforge 1.0.0`; `scan`, JSON `review`, SARIF + export, preview-only `apply`, confirmed `apply`, `history` and `undo` passed. + The workspace was restored byte-for-byte to its pre-apply source. +- MSI: installed per-user with `ALLUSERS=2 MSIINSTALLPERUSER=1`, started with + window title `CodeForge Engine`, and uninstalled without registry or directory + residue. A default per-machine silent install requires administrator rights. +- NSIS: silent install, GUI startup, and silent uninstall passed without registry + or directory residue. +- Host: Windows 11 build 26200, x64. + +### Linux x64 + +- CLI archive: version output, review, apply and undo passed; source restoration + passed. +- Debian package: installed as `code-forge-engine 1.0.0`, desktop process started + under WSLg, and removal left no binary or installed package. +- AppImage: extracted successfully and desktop process started under WSLg. +- Host: WSL2 Ubuntu x86_64 with WSLg. Mesa emitted software-rendering warnings, + but both desktop processes remained alive during the startup window. +- No native physical Linux machine was available; this is recorded rather than + described as bare-metal acceptance. + +## Published assets + +The final release assets differ in bytes from the pre-tag candidate rebuild; the +published copies were downloaded and functionally tested after publication. + +```text +64ea02dcabae08ea7259a7317b326e3be90166d7fbbf5a04c94a2abdb511a30c CodeForge.Engine_1.0.0_amd64.AppImage +b46282dd7168528351e1028442ad7e18550c014b2b6c88bb1b90f761df1858d9 CodeForge.Engine_1.0.0_amd64.deb +8505e6f1b2a5821c56e0461372f82a07db3beaf499175dbfdcd436696e0f3c0c CodeForge.Engine_1.0.0_x64-setup.exe +0ea9a369ba85fad6cfe936aca3f8f2fe8116d21cf201d4f66da85690a613d99f CodeForge.Engine_1.0.0_x64_en-US.msi +9629a6e60d2b889dae9b528548c90b91f993c2b120a753631dd556bedd59a7b0 codeforge-cli-v1.0.0-x86_64-pc-windows-msvc.zip +5b50c57add5e9d5d1fc40091d34a8ff6cc01b1e9805a4a2b742011938d38e41c codeforge-cli-v1.0.0-x86_64-unknown-linux-gnu.tar.gz +``` + +## Surviving boundaries + +Context extraction, heuristic impact/test suggestions, local SARIF path import and +optional CodeQL execution remain Experimental. Fake-process tests do not certify a +real CodeQL installation. Name-based relations do not provide complete scope/type +resolution. Missing required tools remain execution gaps, not passing evidence. diff --git a/docs/releases/v1.1.0.md b/docs/releases/v1.1.0.md new file mode 100644 index 0000000..8c20332 --- /dev/null +++ b/docs/releases/v1.1.0.md @@ -0,0 +1,53 @@ +# CodeForge Engine v1.1.0 + +CodeForge Engine v1.1.0 is a compatibility-focused evidence and safety release. +It keeps the local, CLI-first workflow and does not promote unverified semantic +analysis or optional external tools to stable guarantees. + +## Baseline lifecycle + +- Baseline pruning now evaluates current diagnostics and removes only `Resolved` + entries. `Stale` entries are reported and retained for review. +- `baseline prune` is always a dry run unless `--apply` is supplied, and an apply + additionally requires `--complete-scan` plus complete all-file scan evidence. +- Ambiguous matches, changed-file scopes, language filters, incomplete submission + counts and unavailable relevant lint engines block destructive writes. +- Rule staleness now compares an explicit rule-semantics version rather than the + CodeForge software package version. +- The old lifecycle-only `prune()` path was removed; all pruning is evidence-based. + +## Diagnostics and evidence + +- Review reports include additive coverage metadata for selection scope, + requested languages, indexed/selected/submitted files, engine availability, + external-analysis intent and explicit limitations. +- The desktop review view shows the same coverage information. +- Python `None` comparison fixes now operate on AST operands, avoiding string + literals and unsafe chained-comparison rewrites. +- Java empty-catch detection uses the catch body AST, detects whitespace-empty + handlers and avoids documented or nonempty handlers. + +## Benchmark evidence + +- Timing summaries include P95 in addition to samples, mean, median, variance, + standard deviation, minimum and maximum. +- Benchmark environment records include CodeForge version, operating system, + architecture, logical CPUs, Git revision/worktree state and local `rustc` and + `cargo` versions when available. + +## Compatibility + +- Baseline schema remains version 1. +- SARIF output remains 2.1.0 and existing import compatibility is unchanged. +- Existing review reports without coverage metadata remain readable through an + explicit default. +- The baseline command behavior is intentionally safer: destructive pruning now + requires `--complete-scan --apply`; stale entries are never silently removed. +- Rust 1.90 remains the declared MSRV. + +## Boundaries + +Default scan/review paths do not run project benchmarks. Benchmarks measure +performance and do not prove correctness or semantic equivalence. External +analyzers remain optional; unavailable analysis is not treated as a clean result. +Context/impact, external SARIF import and CodeQL execution remain Experimental. diff --git a/docs/source-context.md b/docs/source-context.md index 0487052..d811f52 100644 --- a/docs/source-context.md +++ b/docs/source-context.md @@ -1,6 +1,6 @@ # On-demand context and impact suggestions -Experimental and included in the v1.0.0 package; never part of the default scan path. +Experimental and included in the v1.1.0 package; never part of the default scan path. ```bash codeforge context . --query verification --limit 10 --max-bytes 32768