From 95e12e27336f38a961dc67e14799b60a4a5519af Mon Sep 17 00:00:00 2001 From: CLAUDE <355856132+HoTT-stuff@users.noreply.github.com> Date: Fri, 13 Nov 2026 14:11:54 +0000 Subject: [PATCH] fix(findings-only): never drop an incomplete or failed result Since #292, an adjudication that cannot be trusted sets the Chapel RepoResult verdict to "error" but leaves RepoResult.error empty. With --findingsOnly, a repository with zero weak points and that verdict was filtered out before reporting, so the incomplete result vanished. Chapel: hasFindings() in Protocol.chpl keeps weak points, a scan error, or any adjudication verdict other than pass; MassPanic's filter uses it. The two-repo smoke program checks every verdict case. Rust: assemblyline --findings-only had the same shape of hole, keeping only weak_point_count > 0 and so dropping repositories whose scan failed. It now keeps r.error.is_some() as well, via has_findings(), with a unit test. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0136P7ZkERqsQWMogHRpM2z8 --- chapel/smoke/two_repo_smoke.chpl | 23 +++++++++++++++++++++ chapel/src/MassPanic.chpl | 2 +- chapel/src/Protocol.chpl | 10 +++++++++ src/assemblyline.rs | 35 +++++++++++++++++++++++++++++++- 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/chapel/smoke/two_repo_smoke.chpl b/chapel/smoke/two_repo_smoke.chpl index 2d5aa8d..69f6692 100644 --- a/chapel/smoke/two_repo_smoke.chpl +++ b/chapel/smoke/two_repo_smoke.chpl @@ -116,6 +116,29 @@ proc main(): int { ok &&= assertContains(blob, "\"total_weak_points\": 5", "aggregate total_weak_points (alpha only; beta errored)"); ok &&= assertContains(blob, "\"total_critical\": 1", "aggregate total_critical (alpha only)"); + // --findingsOnly keeps exactly the results hasFindings() accepts. A clean + // scan whose adjudication was incomplete, or failed, must not vanish. + proc verdictOnly(v: string): RepoResult { + var r = makeRepoResult("repo-v", "/tmp/smoke/repo-v", 0, 0, 0, 10, "00"); + r.verdict = v; + return r; + } + proc expectFindings(r: RepoResult, want: bool, tag: string): bool { + if hasFindings(r) != want { + writeln("smoke: FAIL [", tag, "] — hasFindings is ", hasFindings(r)); + return false; + } + if verbose then writeln("smoke: ok [", tag, "]"); + return true; + } + ok &&= expectFindings(verdictOnly(""), false, "clean scan, no adjudication: dropped"); + ok &&= expectFindings(verdictOnly("pass"), false, "clean scan, verdict pass: dropped"); + ok &&= expectFindings(verdictOnly("error"), true, "incomplete adjudication (error): kept"); + ok &&= expectFindings(verdictOnly("fail"), true, "verdict fail with no weak points: kept"); + ok &&= expectFindings(verdictOnly("warn"), true, "verdict warn with no weak points: kept"); + ok &&= expectFindings(results[0], true, "weak points: kept"); + ok &&= expectFindings(results[1], true, "scan error: kept"); + try { remove(outputPath); } catch { } if ok { diff --git a/chapel/src/MassPanic.chpl b/chapel/src/MassPanic.chpl index 1fa2b19..0dae3fe 100644 --- a/chapel/src/MassPanic.chpl +++ b/chapel/src/MassPanic.chpl @@ -181,7 +181,7 @@ module MassPanic { // Always build via list to avoid Chapel array-shape mismatch on assignment. var filteredList: list(RepoResult); for r in resultsArr { - if !findingsOnly || r.weakPointCount > 0 || r.error != "" then + if !findingsOnly || hasFindings(r) then filteredList.pushBack(r); } var filteredResults = filteredList.toArray(); diff --git a/chapel/src/Protocol.chpl b/chapel/src/Protocol.chpl index 676711d..a7ded75 100644 --- a/chapel/src/Protocol.chpl +++ b/chapel/src/Protocol.chpl @@ -46,6 +46,16 @@ module Protocol { var categories: list(CategoryCount); } + // True when a result has anything to report: weak points, a scan error, + // or an adjudication verdict other than pass. `--findingsOnly` keeps + // exactly these results, so an incomplete adjudication (verdict "error") + // or a fail/warn verdict with no weak points is never silently dropped. + proc hasFindings(r: RepoResult): bool { + return r.weakPointCount > 0 + || r.error != "" + || (r.verdict != "" && r.verdict != "pass"); + } + record CategoryCount { var name: string; var count: int; diff --git a/src/assemblyline.rs b/src/assemblyline.rs index 990a869..75beacc 100644 --- a/src/assemblyline.rs +++ b/src/assemblyline.rs @@ -59,6 +59,14 @@ pub struct RepoResult { pub report: Option, } +/// True when a result has anything to report: weak points or a scan error. +/// `--findings-only` keeps exactly these, so a repository whose scan failed is +/// never silently dropped from the report. Mirrors `hasFindings` in +/// `chapel/src/Protocol.chpl` (which also keeps non-pass adjudication verdicts). +pub fn has_findings(r: &RepoResult) -> bool { + r.weak_point_count > 0 || r.error.is_some() +} + /// Complete assemblyline report #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AssemblylineReport { @@ -451,7 +459,7 @@ pub fn run_with_cache( // Apply filters if config.findings_only { - results.retain(|r| r.weak_point_count > 0); + results.retain(has_findings); } if config.min_findings > 0 { results.retain(|r| r.weak_point_count >= config.min_findings); @@ -543,6 +551,31 @@ pub fn write_report(report: &AssemblylineReport, path: &Path) -> Result<()> { mod tests { use super::*; + fn result(weak_points: usize, error: Option<&str>) -> RepoResult { + RepoResult { + repo_path: PathBuf::from("/tmp/r"), + repo_name: "r".into(), + weak_point_count: weak_points, + critical_count: 0, + high_count: 0, + total_files: 0, + total_lines: 0, + error: error.map(str::to_string), + fingerprint: None, + report: None, + } + } + + #[test] + fn findings_only_keeps_failed_scans_and_drops_clean_ones() { + assert!(!has_findings(&result(0, None)), "a clean scan is dropped"); + assert!(has_findings(&result(3, None)), "weak points are kept"); + assert!( + has_findings(&result(0, Some("assail failed"))), + "a failed scan must never vanish from a findings-only report" + ); + } + #[test] fn test_is_source_file() { assert!(is_source_file(Path::new("main.rs")));