From 7a2b170f5e3500a473271e75463deb1811955989 Mon Sep 17 00:00:00 2001 From: Andrea Righi Date: Wed, 22 Jul 2026 08:41:51 +0200 Subject: [PATCH] review: Preserve valid false-positive adjudications The findings validator required models to copy each protected finding and its problem text exactly into every KEEP/DROP decision. Correct DROP decisions were discarded when a model omitted incidental metadata such as a location, causing boro to fall back to both the protected baseline and raw regular findings. Identify each KEEP/DROP decision by its baseline ID and keep the original baseline finding in boro. Require the compact schema directly, while retaining strict verdict, proof, ordering, and deterministic upstream-fix checks. If baseline proof formatting still fails, salvage an independently empty regular-candidate decision instead of resurrecting disproved candidates. Also require caller-first lifecycle analysis for NULL, publication, initialization, teardown, and lifetime findings, with regression coverage for the observed failure mode. This allows boro to reliably drop validated false positives while reducing duplicated tokens and avoiding costly retries caused by irrelevant differences in copied finding metadata. Signed-off-by: Andrea Righi --- resources/review-validation-findings.md | 30 +++-- src/api.rs | 155 ++++++++++++++++-------- src/main.rs | 88 ++++++-------- 3 files changed, 164 insertions(+), 109 deletions(-) diff --git a/resources/review-validation-findings.md b/resources/review-validation-findings.md index b5189d9..6a4a2cd 100644 --- a/resources/review-validation-findings.md +++ b/resources/review-validation-findings.md @@ -26,9 +26,7 @@ The user message gives you a JSON object of this exact shape: "baseline_false_positive_challenges": [ { "baseline_id": "fast-N", - "finding": { "problem": "", "severity": "..." }, "proof": { - "finding_claim": "", "verified_facts": [""], "contradiction": "", "conclusion": "false_positive" @@ -93,9 +91,9 @@ For each finding, decide one of: adjudicate EVERY entry, even when no specialist challenged it. Assign each entry its host identity `fast-N`, where N is its zero-based array index. Return exactly one `baseline_adjudications` record per baseline entry, in the same -order. Copy `baseline_id` and the complete finding object exactly. Every record -must contain repository-tool-verified proof: make `proof.finding_claim` equal -the finding's `problem` exactly, list concrete `verified_facts`, and explain in +order. Use the exact `baseline_id` as the sole identity. The host owns the +original finding object, so never echo or rewrite it. Every record must contain +repository-tool-verified proof: list concrete `verified_facts`, and explain in `assessment` why those facts support or disprove the complete finding. Use `verdict: "KEEP"` with `proof.conclusion: "supported"` unless the checked-out tree conclusively proves the reported failure impossible. Only then use @@ -106,6 +104,23 @@ uncertainty remains, KEEP. You MUST execute repository tools while adjudicating the baseline. If tools are unavailable, KEEP every entry and state the concrete facts available from the supplied commit material. +For NULL-dereference, publication, initialization, teardown, and lifetime +findings, validation is caller-first and lifecycle-complete. Before KEEP or DROP: + +1. Enumerate every actual entry path to the named reader, including inline + wrappers and static-branch gates in unchanged files. +2. Locate when each gate becomes true and false relative to pointer/table + publication, enable failure, reader draining, and retirement. +3. Check both forward enable ordering and reverse disable/error ordering. +4. If the finding cites a similar caller with an explicit NULL check, compare + the two callers' execution phases and gates; do not assume the check proves + identical reachability. + +A local unguarded dereference is not sufficient proof of reachability. A KEEP +assessment must name the concrete caller that crosses the unsafe lifecycle +window. A DROP assessment must name the gate or ordering that makes every named +path impossible. + DROP a regular candidate when it reports the same underlying problem as a surviving baseline finding, even if the wording differs. Do not drop a candidate merely because it shares a location, function name, or terminology with the @@ -151,7 +166,8 @@ Hard rules: - Adjudicate every `baseline_findings` entry, including entries absent from `baseline_false_positive_challenges`. Emit exactly one ordered record under `baseline_adjudications` for each entry. Omission, duplication, reordering, - or an inexact finding copy invalidates the complete response. + or an inexact baseline ID invalidates the complete response. Do not echo the + host-owned finding object. - A finding must describe a problem that remains in or is introduced by the reviewed commit. Do NOT keep a finding merely because the parent version was wrong; the final report is a review of the patch, not a @@ -197,10 +213,8 @@ Output shape (strict): "baseline_adjudications": [ { "baseline_id": "fast-N", - "finding": { "problem": "..." }, "verdict": "KEEP|DROP", "proof": { - "finding_claim": "...", "verified_facts": ["..."], "assessment": "...", "conclusion": "supported|false_positive" diff --git a/src/api.rs b/src/api.rs index ba88669..f5bbabd 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1255,9 +1255,9 @@ pub const RETRY_REMINDER_FINDINGS_VALIDATION: &str = "Your previous response was rejected because it did not match the required JSON shape. \ Return ONLY a JSON object with a top-level 'commits' array. Each commit entry must have \ 'sha' (string), 'findings' (array; possibly empty), and 'baseline_adjudications' \ -(array). Return exactly one adjudication per input baseline finding, in order, copying its \ -fast-N baseline_id and finding exactly. Each adjudication must have verdict KEEP or DROP and \ -proof with finding_claim, non-empty verified_facts, assessment, and conclusion. KEEP requires \ +(array). Return exactly one adjudication per input baseline finding, in order, using its \ +fast-N baseline_id. Do not copy the finding object; the host owns it. Each adjudication must \ +have verdict KEEP or DROP and proof with non-empty verified_facts, assessment, and conclusion. KEEP requires \ conclusion='supported'; DROP requires conclusion='false_positive'. Each finding must have 'problem', \ 'severity' (Low|Medium|High|Critical), 'severity_explanation', and 'location' \ (verbatim copy of the input finding's location). \ @@ -3304,6 +3304,38 @@ pub fn parse_validation_findings(raw: &str) -> Result { Ok(v) } +/// Salvage the independently filtered regular-candidate decision when a response cannot satisfy +/// the stricter baseline-adjudication contract. Only an empty `findings` array is safe to salvage: +/// it cannot introduce or rewrite a finding, and prevents an unrelated malformed baseline proof +/// from resurrecting regular candidates which the validator conclusively dropped. +pub fn parse_empty_validation_candidate_decisions(raw: &str) -> Result { + let v = parse_model_json_with_key(raw, "commits")?; + let commits = v + .get("commits") + .and_then(Value::as_array) + .context("expected top-level 'commits' array in validation output")?; + let mut salvaged = Vec::with_capacity(commits.len()); + for entry in commits { + let sha = entry + .get("sha") + .and_then(Value::as_str) + .context("each validation commit must have a string sha")?; + let findings = entry + .get("findings") + .and_then(Value::as_array) + .context("each validation commit must have a findings array")?; + if !findings.is_empty() { + anyhow::bail!("only empty regular-candidate decisions are safe to salvage"); + } + salvaged.push(json!({ + "sha": sha, + "findings": [], + "baseline_adjudications": [], + })); + } + Ok(json!({"commits": salvaged})) +} + fn parse_baseline_adjudications_strict(adjudications: &[Value]) -> Result<()> { const HEDGES: &[&str] = &[ "may", @@ -3322,10 +3354,10 @@ fn parse_baseline_adjudications_strict(adjudications: &[Value]) -> Result<()> { let obj = adjudication .as_object() .context("each baseline adjudication must be an object")?; - const FIELDS: &[&str] = &["baseline_id", "finding", "verdict", "proof"]; + const FIELDS: &[&str] = &["baseline_id", "verdict", "proof"]; if obj.len() != FIELDS.len() || FIELDS.iter().any(|field| !obj.contains_key(*field)) { anyhow::bail!( - "baseline adjudication must contain exactly baseline_id, finding, verdict, and proof" + "baseline adjudication must contain exactly baseline_id, verdict, and proof" ); } let id = obj["baseline_id"] @@ -3336,9 +3368,6 @@ fn parse_baseline_adjudications_strict(adjudications: &[Value]) -> Result<()> { if !ids.insert(id) { anyhow::bail!("baseline adjudication baseline_id must be unique"); } - obj["finding"] - .as_object() - .context("baseline adjudication finding must be the exact finding object")?; let verdict = obj["verdict"] .as_str() .context("baseline adjudication verdict must be KEEP or DROP")?; @@ -3348,28 +3377,19 @@ fn parse_baseline_adjudications_strict(adjudications: &[Value]) -> Result<()> { let proof = obj["proof"] .as_object() .context("baseline adjudication proof must be an object")?; - const PROOF_FIELDS: &[&str] = &[ - "finding_claim", - "verified_facts", - "assessment", - "conclusion", - ]; + const PROOF_FIELDS: &[&str] = &["verified_facts", "assessment", "conclusion"]; if proof.len() != PROOF_FIELDS.len() || PROOF_FIELDS.iter().any(|field| !proof.contains_key(*field)) { anyhow::bail!( - "baseline adjudication proof must contain exactly finding_claim, verified_facts, assessment, and conclusion" + "baseline adjudication proof must contain exactly verified_facts, assessment, and conclusion" ); } - for field in ["finding_claim", "assessment"] { - proof[field] - .as_str() - .map(str::trim) - .filter(|text| !text.is_empty()) - .with_context(|| { - format!("baseline adjudication proof.{field} must be non-empty") - })?; - } + proof["assessment"] + .as_str() + .map(str::trim) + .filter(|text| !text.is_empty()) + .context("baseline adjudication proof.assessment must be non-empty")?; let facts = proof["verified_facts"] .as_array() .filter(|facts| !facts.is_empty()) @@ -3517,13 +3537,9 @@ pub fn parse_specialist_concerns_strict(raw: &str) -> Result { let obj = challenge .as_object() .context("each baseline false-positive challenge must be an object")?; - if obj.len() != 3 - || !obj.contains_key("baseline_id") - || !obj.contains_key("finding") - || !obj.contains_key("proof") - { + if obj.len() != 2 || !obj.contains_key("baseline_id") || !obj.contains_key("proof") { anyhow::bail!( - "baseline false-positive challenge must contain exactly baseline_id, finding, and proof" + "baseline false-positive challenge must contain exactly baseline_id and proof" ); } obj["baseline_id"] @@ -3531,23 +3547,15 @@ pub fn parse_specialist_concerns_strict(raw: &str) -> Result { .map(str::trim) .filter(|s| !s.is_empty()) .context("baseline challenge baseline_id must be a non-empty string")?; - obj["finding"] - .as_object() - .context("baseline challenge finding must be the exact finding object")?; let proof = obj["proof"] .as_object() .context("baseline challenge proof must be an object")?; - const PROOF_FIELDS: &[&str] = &[ - "finding_claim", - "verified_facts", - "contradiction", - "conclusion", - ]; + const PROOF_FIELDS: &[&str] = &["verified_facts", "contradiction", "conclusion"]; if proof.len() != PROOF_FIELDS.len() || PROOF_FIELDS.iter().any(|field| !proof.contains_key(*field)) { anyhow::bail!( - "baseline challenge proof must contain exactly finding_claim, verified_facts, contradiction, and conclusion" + "baseline challenge proof must contain exactly verified_facts, contradiction, and conclusion" ); } if proof["conclusion"].as_str() != Some("false_positive") { @@ -3558,7 +3566,7 @@ pub fn parse_specialist_concerns_strict(raw: &str) -> Result { .filter(|facts| !facts.is_empty()) .context("baseline challenge verified_facts must be a non-empty array")?; let mut proof_text = String::new(); - for field in ["finding_claim", "contradiction"] { + for field in ["contradiction"] { let text = proof[field] .as_str() .map(str::trim) @@ -3778,7 +3786,7 @@ pub fn specialist_stage_user_payload_with_baseline( # boro specialist stage {stage}\n\n{instruction_body}\n\n\ # Reference excerpts for this stage\n\n{reference_addon_md}\n\n\ Return ONLY JSON (no markdown fences): \ -{{\"concerns\":[{concern_schema}],\"baseline_false_positives\":[{{\"baseline_id\":\"fast-N\",\"finding\":{{\"exact\":\"finding object copied from the protected input\"}},\"proof\":{{\"finding_claim\":\"exact problem text\",\"verified_facts\":[\"fact established with repository tools\"],\"contradiction\":\"why those facts make the complete finding impossible\",\"conclusion\":\"false_positive\"}}}}]}}. \ +{{\"concerns\":[{concern_schema}],\"baseline_false_positives\":[{{\"baseline_id\":\"fast-N\",\"proof\":{{\"verified_facts\":[\"fact established with repository tools\"],\"contradiction\":\"why those facts make the complete finding impossible\",\"conclusion\":\"false_positive\"}}}}]}}. \ Top-level key must be \"concerns\" (not \"findings\"). \ Use a short \"type\" label prefixed with \"s{stage}:\" (e.g. \"s{stage}:uaf\"). \ {proof_contract}\ @@ -3787,7 +3795,7 @@ Do not emit a concern merely because the old/removed code was buggy when the new The \"location\" field is OPTIONAL - include it only when you can anchor the concern to a specific hunk in the diff: \ \"file\" must match the diff path exactly (post-image for RIGHT, pre-image for LEFT), \"line\" is 1-based, \"line_end\" optional for a range, \"side\" is \"RIGHT\" for added/modified lines or \"LEFT\" for removed/context lines in the old file. \ Do NOT invent locations - omit when unsure. \ -The protected fast-review findings are immutable input. Never rewrite, replace, deduplicate, or return them as concerns. You may challenge one only when repository-tool evidence proves the complete finding false with no assumptions or uncertainty. A challenge must copy its baseline_id and entire finding object exactly, proof.finding_claim must equal that finding's problem text exactly, verified_facts must state the concrete facts established by repository inspection, and contradiction must demonstrate that the complete reported failure is impossible. Plausibility, missing evidence for the finding, a different interpretation, lower severity, or inability to reproduce is not proof. When any doubt remains, do not challenge it. If repository tools are unavailable, baseline_false_positives MUST be empty. \ +The protected fast-review findings are immutable host-owned input. Never rewrite, replace, deduplicate, echo, or return them as concerns. You may challenge one only when repository-tool evidence proves the complete finding false with no assumptions or uncertainty. A challenge identifies its target only by the exact baseline_id; do not copy the finding object or its problem text. verified_facts must state the concrete facts established by repository inspection, and contradiction must demonstrate that the complete reported failure is impossible. Plausibility, missing evidence for the finding, a different interpretation, lower severity, or inability to reproduce is not proof. When any doubt remains, do not challenge it. If repository tools are unavailable, baseline_false_positives MUST be empty. \ Use an empty concerns array if nothing applies to this lens and an empty baseline_false_positives array unless a fast finding is conclusively disproved." ) } @@ -4121,7 +4129,7 @@ fn validation_findings_user_payload_scaled( format!( "Per-commit findings under review (validate per the system prompt):\n\n```json\n{body}\n```\n\n\ Return ONLY a JSON object: {{\"commits\":[{{\"sha\":\"\",\"findings\":[...],\"baseline_adjudications\":[...]}}]}}. \ -Independently adjudicate every entry in \"baseline_findings\", assigning zero-based IDs fast-N in array order. Return exactly one structured, repository-tool-verified KEEP or DROP record per baseline finding, in the same order, copying its baseline_id and complete finding exactly. Set proof.conclusion to \"supported\" for KEEP and \"false_positive\" for DROP. Treat \"baseline_false_positive_challenges\" only as optional evidence. Return only surviving entries from \"findings\"; never copy baseline entries into that array. \ +Independently adjudicate every entry in \"baseline_findings\", assigning zero-based IDs fast-N in array order. Return exactly one structured, repository-tool-verified KEEP or DROP record per baseline finding, in the same order, using only its baseline_id to identify it. The host owns the complete finding object: do not echo the finding or its problem text. Set proof.conclusion to \"supported\" for KEEP and \"false_positive\" for DROP. Treat \"baseline_false_positive_challenges\" only as optional evidence. Return only surviving entries from \"findings\"; never copy baseline entries into that array. \ Preserve every kept finding's \"location\" object byte-for-byte from the input. \ When \"context_status\" reports a truncated field, do not treat absence from that field as evidence; use repository tools when the claim requires the omitted context. \ No markdown fences, no prose outside the JSON." @@ -6794,9 +6802,7 @@ index 123..456 100644 "concerns": [], "baseline_false_positives": [{ "baseline_id": "fast-0", - "finding": {"problem": "foo dereferences NULL", "severity": "High"}, "proof": { - "finding_claim": "foo dereferences NULL", "verified_facts": ["read_symbol shows every caller passes &static_foo"], "contradiction": "&static_foo is non-NULL on every reachable call", "conclusion": "false_positive" @@ -6851,10 +6857,8 @@ index 123..456 100644 fn validation_parser_accepts_only_structured_baseline_adjudications() { let drop_adjudication = json!({ "baseline_id": "fast-0", - "finding": {"problem": "foo dereferences NULL", "severity": "High"}, "verdict": "DROP", "proof": { - "finding_claim": "foo dereferences NULL", "verified_facts": ["every reachable caller passes a static object"], "assessment": "the argument is non-NULL on every reachable call", "conclusion": "false_positive" @@ -6884,10 +6888,8 @@ index 123..456 100644 "findings": [], "baseline_adjudications": [{ "baseline_id": "fast-0", - "finding": {"problem": "foo dereferences NULL"}, "verdict": "KEEP", "proof": { - "finding_claim": "foo dereferences NULL", "verified_facts": ["foo accepts pointers from external callers"], "assessment": "the reachable NULL case remains supported", "conclusion": "supported" @@ -6898,6 +6900,59 @@ index 123..456 100644 assert!(parse_validation_findings(&keep.to_string()).is_ok()); } + #[test] + fn validation_parser_rejects_obsolete_finding_echo() { + let obsolete = json!({ + "commits": [{ + "sha": "328cfa18fdcf", + "findings": [], + "baseline_adjudications": [{ + "baseline_id": "fast-0", + "finding": { + "problem": "scx_idle_notify can observe unpublished tables", + "severity": "Medium" + }, + "verdict": "DROP", + "proof": { + "verified_facts": [ + "scx_update_idle is gated on scx_enabled", + "__scx_enabled is set after table publication" + ], + "assessment": "the named reader cannot reach the pre-publication window", + "conclusion": "false_positive" + } + }] + }] + }); + + assert!(parse_validation_findings(&obsolete.to_string()).is_err()); + } + + #[test] + fn empty_candidate_decision_survives_malformed_baseline_envelope() { + let raw = json!({ + "commits": [{ + "sha": "328cfa18fdcf", + "findings": [], + "baseline_adjudications": [{"baseline_id": "fast-0"}] + }] + }); + + assert!(parse_validation_findings(&raw.to_string()).is_err()); + let salvaged = parse_empty_validation_candidate_decisions(&raw.to_string()).unwrap(); + assert_eq!(salvaged["commits"][0]["findings"], json!([])); + assert_eq!(salvaged["commits"][0]["baseline_adjudications"], json!([])); + + let unsafe_nonempty = json!({ + "commits": [{ + "sha": "328cfa18fdcf", + "findings": [{"problem": "rewritten candidate"}], + "baseline_adjudications": [] + }] + }); + assert!(parse_empty_validation_candidate_decisions(&unsafe_nonempty.to_string()).is_err()); + } + #[test] fn fast_baseline_formatter_adds_ids_without_mutating_findings() { let findings = json!([{"problem":"first"}, {"problem":"second"}]); diff --git a/src/main.rs b/src/main.rs index 946f34d..b19dbc1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1377,21 +1377,6 @@ fn validate_baseline_adjudication_coverage( { anyhow::bail!("baseline adjudications must use exact fast-N IDs in order"); } - if adjudication.get("finding") != Some(finding) { - anyhow::bail!("baseline adjudication must copy the exact finding object"); - } - let problem = finding - .get("problem") - .and_then(Value::as_str) - .context("baseline finding must have a string problem")?; - if adjudication - .get("proof") - .and_then(|proof| proof.get("finding_claim")) - .and_then(Value::as_str) - != Some(problem) - { - anyhow::bail!("baseline proof must copy the exact finding claim"); - } if finding.get("source").and_then(Value::as_str) == Some("upstream-fixes") && adjudication.get("verdict").and_then(Value::as_str) != Some("KEEP") { @@ -1567,7 +1552,7 @@ async fn run_findings_validation( ); let progress_line = progress_ui.map(|ui| ui.stage_ctx(label.clone())); let t_val = Instant::now(); - let (parsed_opt, _last_raw, summed, last_err, _attempts) = + let (parsed_opt, last_raw, summed, last_err, _attempts) = api::chat_completion_with_retry_stage_timeout_preserve_input( client, validation_cfg, @@ -1614,6 +1599,38 @@ async fn run_findings_validation( verification_failed_shas.insert(payload_owned[idx].sha.clone()); } } + // Baseline proof and regular-candidate filtering are independent decisions. If the + // response only failed the baseline envelope but returned no surviving regular + // candidates, preserve that safe negative decision instead of reviving raw additions. + if !mandatory_verification_failed { + if let Ok(salvaged) = api::parse_empty_validation_candidate_decisions(&last_raw) { + let commits = salvaged + .get("commits") + .and_then(Value::as_array) + .map(Vec::as_slice) + .unwrap_or(&[]); + let exact_batch = commits.len() == indices.len() + && commits.iter().zip(indices).all(|(entry, idx)| { + entry.get("sha").and_then(Value::as_str) + == Some(payload_owned[*idx].sha.as_str()) + }); + if exact_batch { + for entry in commits { + if let Some(sha) = entry.get("sha").and_then(Value::as_str) { + by_sha.insert(sha.to_string(), entry.clone()); + } + } + v( + vdest, + format!( + "validation batch {batch_num}/{}: salvaged empty regular-candidate decisions; unresolved baselines remain protected", + batches.len() + ), + ); + continue; + } + } + } let msg = last_err .map(|e| format!("{e:#}")) .unwrap_or_else(|| "no response".to_string()); @@ -3633,9 +3650,6 @@ fn apply_adjudicated_fast_false_positives( else { continue; }; - let Some(problem) = original.get("problem").and_then(Value::as_str) else { - continue; - }; if original.get("source").and_then(Value::as_str) == Some("upstream-fixes") { v( vd, @@ -3645,20 +3659,6 @@ fn apply_adjudicated_fast_false_positives( ); continue; } - let adjudicated_finding = adjudication.get("finding"); - let proof_claim = adjudication - .get("proof") - .and_then(|proof| proof.get("finding_claim")) - .and_then(Value::as_str); - if adjudicated_finding != Some(original) || proof_claim != Some(problem) { - v( - vd, - format!( - "strong-validator baseline confirmation {id} rejected: the complete target finding was not copied exactly" - ), - ); - continue; - } if disproved.insert(index) { v( vd, @@ -5025,7 +5025,7 @@ mod fast_cli_tests { } #[test] - fn strong_validator_confirmation_removes_only_the_exact_fast_finding() { + fn strong_validator_confirmation_removes_only_the_host_identified_fast_finding() { let fast = [ json!({"problem": "foo dereferences NULL", "severity": "High"}), json!({"problem": "bar leaks memory", "severity": "Medium"}), @@ -5037,10 +5037,8 @@ mod fast_cli_tests { let mut baseline = json!([fast[0].clone(), fast[1].clone(), upstream.clone()]); let challenge = json!({ "baseline_id": "fast-0", - "finding": fast[0].clone(), "verdict": "DROP", "proof": { - "finding_claim": "foo dereferences NULL", "verified_facts": ["every caller passes a static object"], "assessment": "the argument cannot be NULL", "conclusion": "false_positive" @@ -5071,7 +5069,7 @@ mod fast_cli_tests { } #[test] - fn baseline_coverage_requires_one_exact_ordered_adjudication_per_finding() { + fn baseline_coverage_requires_one_ordered_host_id_per_finding() { let baseline = json!([ {"problem": "first issue", "severity": "High"}, {"problem": "second issue", "severity": "Low"} @@ -5093,10 +5091,8 @@ mod fast_cli_tests { "baseline_adjudications": [ { "baseline_id": "fast-0", - "finding": baseline[0].clone(), "verdict": "KEEP", "proof": { - "finding_claim": "first issue", "verified_facts": ["fact"], "assessment": "supported", "conclusion": "supported" @@ -5104,10 +5100,8 @@ mod fast_cli_tests { }, { "baseline_id": "fast-1", - "finding": baseline[1].clone(), "verdict": "DROP", "proof": { - "finding_claim": "second issue", "verified_facts": ["fact"], "assessment": "disproved", "conclusion": "false_positive" @@ -5139,10 +5133,8 @@ mod fast_cli_tests { let mut baseline = json!([finding.clone()]); let keep = json!({ "baseline_id": "fast-0", - "finding": finding, "verdict": "KEEP", "proof": { - "finding_claim": "foo dereferences NULL", "verified_facts": ["NULL remains reachable"], "assessment": "the finding is supported", "conclusion": "supported" @@ -5161,15 +5153,13 @@ mod fast_cli_tests { } #[test] - fn baseline_adjudication_cannot_remove_a_finding_without_exact_text_match() { + fn baseline_adjudication_cannot_remove_a_finding_with_an_invalid_host_id() { let fast = vec![json!({"problem": "foo dereferences NULL", "severity": "High"})]; let mut baseline = Value::Array(fast.clone()); let challenge = json!({ - "baseline_id": "fast-0", - "finding": {"problem": "foo is probably safe", "severity": "High"}, + "baseline_id": "fast-00", "verdict": "DROP", "proof": { - "finding_claim": "foo is probably safe", "verified_facts": ["fact"], "assessment": "contradiction", "conclusion": "false_positive" @@ -5193,10 +5183,8 @@ mod fast_cli_tests { let mut baseline = Value::Array(fast.clone()); let adjudicated = json!({ "baseline_id": "fast-0", - "finding": fast[0].clone(), "verdict": "DROP", "proof": { - "finding_claim": "foo dereferences NULL", "verified_facts": ["every caller passes a static object"], "assessment": "the argument cannot be NULL", "conclusion": "false_positive" @@ -5224,10 +5212,8 @@ mod fast_cli_tests { let mut baseline = json!([upstream.clone()]); let adjudicated = json!({ "baseline_id": "fast-0", - "finding": upstream.clone(), "verdict": "DROP", "proof": { - "finding_claim": "upstream fixed a regression", "verified_facts": ["the patch exists"], "assessment": "the patch is unnecessary", "conclusion": "false_positive"