diff --git a/README.md b/README.md index 6271533..83024d3 100644 --- a/README.md +++ b/README.md @@ -250,7 +250,7 @@ context locally, then runs the ordered stages below. | 8 | Hardware & portability | Drivers/HW: DMA, IRQ, barriers, endianness | | 9 | Comment / code consistency | Audit comments touched by the patch against the actual code | | 10 | Consolidation pass | Turns regular concerns into candidate additions | -| 11 | Strong adjudication and addition validation | Independently confirms specialist challenges and filters regular additions | +| 11 | Strong adjudication and addition validation | Independently adjudicates every baseline finding and filters regular additions | | 12 | Additive merge and LKML report | Union survivors into the baseline and render the report | `BORO_MODEL` is used for regular discovery and consolidation. In normal @@ -265,21 +265,24 @@ message, and diff. In a normal review it uses `BORO_VALIDATION_MODEL`; under `--fast` it uses `BORO_MODEL`. Regular discovery is independent and can only contribute novel findings; it cannot rewrite or replace baseline findings or their lore provenance. A specialist may separately challenge a -fast finding, but this is only a proposal. The baseline stays read-only while -all specialists run. The strong validation model then independently inspects -the repository and may confirm a proposal only by returning its complete, -exact-target, unhedged proof verbatim. The host applies only those confirmations; -validation failure, ambiguity, missing tool inspection, malformed output, or -`--validation-mode=off` preserves the baseline. Deterministic upstream-fix -findings are not challengeable. The validator also drops regular candidates -that clearly report the same underlying problem. Local merging removes exact -identities only, so distinct findings at the same location are retained. +fast finding, but this is only optional evidence and the baseline stays +read-only while all specialists run. The strong validation model then +independently adjudicates every baseline finding against the repository, +including findings no specialist challenged. It may DROP one only by returning +the complete exact finding with an unhedged structured contradiction proof +after repository-tool inspection. The host applies only exact, tool-verified +DROP decisions; validation failure, ambiguity, missing tool inspection, +malformed output, or `--validation-mode=off` preserves the finding. +Deterministic upstream-fix findings must receive `KEEP` and cannot be dropped. +The validator also drops regular candidates that clearly report the same underlying problem. +Local merging removes exact identities only, so distinct findings at the same +location are retained. `--validation-mode` changes only the post-discovery stages: -- `filter` (default): adjudicate specialist challenges, validate regular - additions, union survivors into the fast baseline, then render LKML prose. -- `findings`: perform the same adjudication and additive merge while skipping +- `filter` (default): adjudicate every baseline finding, validate regular + additions, union survivors, then render LKML prose. +- `findings`: perform the same baseline adjudication and additive merge while skipping LKML rendering. - `off`: preserve every fast finding and add raw regular candidates without validation. @@ -378,11 +381,15 @@ The option `--validation-mode` selects whether (and how) regular-stage candidate additions are validated: - `filter` (default): validates regular additions, independently adjudicates - specialist baseline challenges, unions survivors with the protected fast - baseline, then renders per-commit LKML prose. The viewer / human + every protected baseline finding with one structured, repository-tool-backed + `KEEP` or `DROP` proof per finding, unions survivors with validated regular + additions, then renders per-commit LKML prose. Incomplete, reordered, or + inexact baseline adjudication is rejected and retried; a `DROP` is applied + only when its exact proof passes host validation. The viewer / human report's Findings section shows `validated_findings`; the LKML section - shows prose built from those survivors. -- `findings`: performs the same challenge adjudication, additive validation, + shows prose built from those survivors. Commits with no survivors have no + LKML report body; the renderer does not independently add or remove findings. +- `findings`: performs the same baseline adjudication, additive validation, and merge, **skips** the per-commit LKML pass entirely (saves one LLM call per commit; the human report's LKML section is empty in this mode). `scripts/boro-json-view` auto-detects @@ -397,8 +404,8 @@ stdout instead of the human report - the same shape consumed internally, with per-commit `findings[]`, `lkml_report` (filter/off modes only), `validated_findings[]` (filter/findings modes when validation succeeded), and a `usage_summary`. In filter/findings modes, -`findings[]` is the protected fast baseline after any specialist challenges -independently confirmed by the strong validator, and +`findings[]` is the protected fast baseline after exact, tool-verified DROP +decisions from the strong validator, and `validated_findings[]` is the baseline plus accepted regular additions. Each finding may carry optional `location` and `references` fields: diff --git a/resources/prompts/libvirt/inline-template.md b/resources/prompts/libvirt/inline-template.md index 2091773..592a150 100644 --- a/resources/prompts/libvirt/inline-template.md +++ b/resources/prompts/libvirt/inline-template.md @@ -61,5 +61,5 @@ text code snippet or call chain when it helps. - Order findings from most to least serious. - Keep the tone the kind of reply a regular libvir-list reviewer would send. -If no issues remain after filtering, the report should simply state that nothing -of concern was found. +The caller skips this formatter when the validated findings set is empty. Format +every supplied finding; do not independently add or remove findings. diff --git a/resources/prompts/qemu/inline-template.md b/resources/prompts/qemu/inline-template.md index 9829cc1..7b6bb4f 100644 --- a/resources/prompts/qemu/inline-template.md +++ b/resources/prompts/qemu/inline-template.md @@ -59,5 +59,5 @@ text code snippet or call chain when it helps. - Order findings from most to least serious. - Keep the tone the kind of reply a regular qemu-devel reviewer would send. -If no issues remain after filtering, the report should simply state that nothing -of concern was found. +The caller skips this formatter when the validated findings set is empty. Format +every supplied finding; do not independently add or remove findings. diff --git a/resources/review-validation-findings.md b/resources/review-validation-findings.md index 95dadf1..b5189d9 100644 --- a/resources/review-validation-findings.md +++ b/resources/review-validation-findings.md @@ -18,7 +18,7 @@ The user message gives you a JSON object of this exact shape: "diff": "", "baseline_findings": [ { - "problem": "", + "problem": "", "severity": "Low|Medium|High|Critical", "severity_explanation": "" } @@ -89,35 +89,44 @@ For each finding, decide one of: is genuinely wrong, KEEP it - filtering is for clear false positives, not for taste. -`baseline_findings` contains the protected one-shot review. It is read-only: -never rewrite or replace a baseline finding. DROP a regular candidate when it -reports the same underlying problem as a baseline finding, even if the wording -differs. Do not drop a candidate merely because it shares a location, function -name, or terminology with the baseline; distinct failure modes at the same line -are novel findings. - -`baseline_false_positive_challenges` contains specialist proposals, not -trusted conclusions. Independently inspect the reviewed commit with repository -tools and decide whether each proof establishes with certainty that the -complete copied baseline finding is false. Confirm a challenge only when all -of its verified facts are independently established and the contradiction -makes the reported failure impossible. Missing evidence, lower severity, -plausibility, inability to reproduce, or an alternative interpretation is not -enough. If any assumption, ambiguity, or uncertainty remains, do not confirm -it. Return confirmed challenges verbatim under `confirmed_false_positives`; -never construct, rewrite, or strengthen a challenge. An empty array preserves -the baseline. You MUST execute repository tools before returning any confirmed -challenge; if tools are unavailable, confirm none. - -For every candidate finding or baseline challenge whose conclusion depends on -a function-like macro, expand the complete invocation chain token by token. +`baseline_findings` contains the protected one-shot review. Independently +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 +`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 +`verdict: "DROP"` with `proof.conclusion: "false_positive"`. Missing evidence, +lower severity, plausibility, inability to reproduce, or an alternative +interpretation is not enough to DROP. If any assumption, ambiguity, or +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. + +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 +baseline; distinct failure modes at the same line are novel findings. + +`baseline_false_positive_challenges` contains optional specialist evidence, not +trusted conclusions and not the complete set of baseline findings to inspect. +Verify each proposal independently with repository tools. When its exact proof +is correct, it may inform the corresponding adjudication. When you independently +disprove an unchallenged baseline finding, construct the same strict proof +yourself. Never copy or strengthen an unverified specialist claim. + +For every candidate or baseline finding whose conclusion depends on a +function-like macro, expand the complete invocation chain token by token. At each level, bind formal parameters to actual arguments, substitute every matching preprocessing token in the replacement list, and rescan for nested expansion. Punctuation or member-access operators do not make a matching parameter token literal. Account for stringification, token pasting, and -variadic arguments when present. KEEP a candidate, or reject a baseline -challenge, only according to the final expanded token stream rather than the -unexpanded spelling of an intermediate macro body. +variadic arguments when present. Adjudicate the finding only according to the +final expanded token stream rather than the unexpanded spelling of an +intermediate macro body. Repository-verifiable absence/linkage claims are not matters of taste. Before KEEP or TIGHTEN of a claim that a declaration, definition, export, stub, @@ -138,9 +147,11 @@ Hard rules: correspond to one in the input (by `location` and substance). - Emit only surviving entries from `findings`. Never emit entries from `baseline_findings`; the caller unions the result with the protected - baseline after applying only your confirmed challenges. -- Emit only verbatim entries from `baseline_false_positive_challenges` under - `confirmed_false_positives`. Omission is rejection and preserves the finding. + baseline after applying your exact, tool-verified adjudications. +- 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. - 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 @@ -183,15 +194,16 @@ Output shape (strict): } // ... or [] if all findings for this commit are false positives ], - "confirmed_false_positives": [ + "baseline_adjudications": [ { "baseline_id": "fast-N", "finding": { "problem": "..." }, + "verdict": "KEEP|DROP", "proof": { "finding_claim": "...", "verified_facts": ["..."], - "contradiction": "...", - "conclusion": "false_positive" + "assessment": "...", + "conclusion": "supported|false_positive" } } ] @@ -203,5 +215,5 @@ Output shape (strict): No prose outside the JSON. No markdown fences. Top-level key MUST be `commits`. Each commit entry MUST carry `sha`, `findings`, and -`confirmed_false_positives` exactly as named. If you receive zero commits with -findings or challenges, return `{"commits": []}`. +`baseline_adjudications` exactly as named. If you receive zero commits with +baseline findings, regular findings, or challenges, return `{"commits": []}`. diff --git a/src/api.rs b/src/api.rs index 2c17e41..ba88669 100644 --- a/src/api.rs +++ b/src/api.rs @@ -149,7 +149,7 @@ pub enum ToolVerification { SensitiveFindings, BaselineFalsePositiveProof, Stage7LinkageAndBaselineFalsePositiveProof, - ValidationFindingsAndBaselineChallenges, + ValidationFindingsAndBaselineAdjudications, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -258,6 +258,12 @@ conclusive checked-out-tree evidence, not reasoning from the diff or prompt alon definitions, callers, configuration, or history needed to prove the finding impossible. If the evidence \ does not contradict the complete finding with certainty, return an empty baseline_false_positives array."; +const REQUIRED_VALIDATION_BASELINE_TOOL_REMINDER: &str = "You returned baseline adjudications \ +without executing any repository tool. Independently inspect the exact definitions, callers, configuration, \ +or history needed to decide every protected baseline finding. Then return one exact KEEP or DROP record \ +per baseline entry under baseline_adjudications, with concrete verified_facts and an assessment. If a \ +finding cannot be conclusively disproved, return KEEP; never omit its adjudication record."; + #[derive(Debug)] struct RequiredToolVerificationError; @@ -313,7 +319,7 @@ fn output_requires_tool_verification(policy: ToolVerification, raw: &str) -> boo ToolVerification::Stage7LinkageAndBaselineFalsePositiveProof => { parse_model_json_with_key(raw, "concerns") } - ToolVerification::ValidationFindingsAndBaselineChallenges => { + ToolVerification::ValidationFindingsAndBaselineAdjudications => { parse_model_json_with_key(raw, "commits") } ToolVerification::Optional => return false, @@ -335,7 +341,7 @@ fn output_requires_tool_verification(policy: ToolVerification, raw: &str) -> boo }) }), ToolVerification::SensitiveFindings - | ToolVerification::ValidationFindingsAndBaselineChallenges => { + | ToolVerification::ValidationFindingsAndBaselineAdjudications => { value .get("findings") .and_then(Value::as_array) @@ -357,11 +363,11 @@ fn output_requires_tool_verification(policy: ToolVerification, raw: &str) -> boo .iter() .any(finding_requires_repository_verification) }); - let baseline_confirmation = commit - .get("confirmed_false_positives") + let baseline_adjudication = commit + .get("baseline_adjudications") .and_then(Value::as_array) - .is_some_and(|challenges| !challenges.is_empty()); - sensitive_finding || baseline_confirmation + .is_some_and(|adjudications| !adjudications.is_empty()); + sensitive_finding || baseline_adjudication }) }) } @@ -1248,7 +1254,11 @@ No markdown fences, no prose outside the JSON."; 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) and 'findings' (array; possibly empty). Each finding must have 'problem', \ +'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 \ +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). \ No markdown fences, no prose outside the JSON."; @@ -2301,15 +2311,15 @@ async fn chat_completion_inner( "required-tool verification: rejecting non-empty unverified output and asking the model to inspect the repository", ); messages.push(message); - let reminder = if matches!( - cfg.verification, + let reminder = match cfg.verification { + ToolVerification::ValidationFindingsAndBaselineAdjudications => { + REQUIRED_VALIDATION_BASELINE_TOOL_REMINDER + } ToolVerification::BaselineFalsePositiveProof - | ToolVerification::Stage7LinkageAndBaselineFalsePositiveProof - | ToolVerification::ValidationFindingsAndBaselineChallenges - ) { - REQUIRED_BASELINE_PROOF_TOOL_REMINDER - } else { - REQUIRED_TOOL_REMINDER + | ToolVerification::Stage7LinkageAndBaselineFalsePositiveProof => { + REQUIRED_BASELINE_PROOF_TOOL_REMINDER + } + _ => REQUIRED_TOOL_REMINDER, }; messages.push(json!({"role": "user", "content": reminder})); continue; @@ -3260,7 +3270,8 @@ pub fn parse_findings_json(raw: &str) -> Result { /// Parse the response from the `--validation-mode=findings` stage. /// -/// Expected shape: `{"commits": [{"sha": "...", "findings": [...]}, ...]}`. +/// Expected shape: `{"commits": [{"sha": "...", "findings": [...], +/// "baseline_adjudications": [...]}, ...]}`. /// Each finding inside is normalized through [`sanitize_finding_location`] /// so the same lenient anchor rules as [`parse_findings_json`] apply. pub fn parse_validation_findings(raw: &str) -> Result { @@ -3283,18 +3294,123 @@ pub fn parse_validation_findings(raw: &str) -> Result { for f in findings.iter_mut() { sanitize_finding_location(f); } - if let Some(confirmed) = obj.get("confirmed_false_positives") { - let wrapper = json!({ - "concerns": [], - "baseline_false_positives": confirmed, - }); - parse_specialist_concerns_strict(&wrapper.to_string()) - .context("invalid confirmed baseline false-positive proof")?; - } + let adjudications = obj + .get("baseline_adjudications") + .and_then(Value::as_array) + .context("each 'commits' entry must have a 'baseline_adjudications' array")?; + parse_baseline_adjudications_strict(adjudications) + .context("invalid baseline adjudication")?; } Ok(v) } +fn parse_baseline_adjudications_strict(adjudications: &[Value]) -> Result<()> { + const HEDGES: &[&str] = &[ + "may", + "might", + "could", + "likely", + "possibly", + "perhaps", + "appears", + "seems", + "uncertain", + "unclear", + ]; + let mut ids = std::collections::HashSet::new(); + for adjudication in adjudications { + let obj = adjudication + .as_object() + .context("each baseline adjudication must be an object")?; + const FIELDS: &[&str] = &["baseline_id", "finding", "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" + ); + } + let id = obj["baseline_id"] + .as_str() + .map(str::trim) + .filter(|id| !id.is_empty()) + .context("baseline adjudication baseline_id must be a non-empty string")?; + 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")?; + if !matches!(verdict, "KEEP" | "DROP") { + anyhow::bail!("baseline adjudication verdict must be KEEP or DROP"); + } + let proof = obj["proof"] + .as_object() + .context("baseline adjudication proof must be an object")?; + const PROOF_FIELDS: &[&str] = &[ + "finding_claim", + "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" + ); + } + 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") + })?; + } + let facts = proof["verified_facts"] + .as_array() + .filter(|facts| !facts.is_empty()) + .context("baseline adjudication verified_facts must be a non-empty array")?; + for fact in facts { + fact.as_str() + .map(str::trim) + .filter(|text| !text.is_empty()) + .context("every baseline adjudication verified fact must be a non-empty string")?; + } + let conclusion = proof["conclusion"].as_str().context( + "baseline adjudication proof.conclusion must be supported or false_positive", + )?; + let expected = if verdict == "KEEP" { + "supported" + } else { + "false_positive" + }; + if conclusion != expected { + anyhow::bail!( + "baseline adjudication verdict and proof.conclusion must agree (KEEP=supported, DROP=false_positive)" + ); + } + if verdict == "DROP" { + let mut proof_text = proof["assessment"].as_str().unwrap_or("").to_string(); + for fact in facts { + proof_text.push(' '); + proof_text.push_str(fact.as_str().unwrap_or("")); + } + let lower = proof_text.to_ascii_lowercase(); + if lower + .split(|c: char| !c.is_alphanumeric()) + .any(|word| HEDGES.contains(&word)) + { + anyhow::bail!("baseline DROP proof contains uncertainty or hedging"); + } + } + } + Ok(()) +} + /// Normalize the optional `location` object on a finding (or concern) in place. /// /// Lenient by design: a malformed `location` is silently dropped (keeping the finding/concern @@ -3915,8 +4031,12 @@ pub fn fast_lkml_report_user_payload( Turn the fast review result into the final LKML-ready email body per the rules above. \ Preserve every concrete finding, but do not invent new findings. When quoting the patch, copy \ lines verbatim. Preserve every URL from the fast review verbatim in the corresponding comment. \ +If repository inspection proves a supplied finding is a false positive, silently discard it. \ +If no valid findings remain, return exactly `{sentinel}` and nothing else. Never describe a \ +discarded finding, the proof that disproved it, or the absence of findings. \ Return only the email body text, with no JSON and no markdown code fence wrapping \ -the entire message." +the entire message.", + sentinel = LKML_NO_FINDINGS_SENTINEL, ) } @@ -3928,11 +4048,11 @@ pub struct ValidationFindingsCommit<'a> { pub commit_message: &'a str, pub reference_context: &'a str, pub diff: &'a str, - /// Protected one-shot findings. Validation uses these for semantic duplicate - /// detection and as the immutable target of specialist proof challenges. + /// Protected one-shot findings. Validation independently adjudicates every + /// entry and also uses survivors for semantic duplicate detection. pub baseline_findings: &'a Value, - /// Specialist-proposed removals. The strong validator may confirm an entry - /// verbatim, but omission or any validation failure preserves the baseline. + /// Optional specialist-proposed removals supplied as evidence. The strong + /// validator also adjudicates baseline findings absent from this array. pub baseline_challenges: &'a Value, /// Regular-stage candidate additions. Only these may appear in the output. pub findings: &'a Value, @@ -4000,8 +4120,8 @@ fn validation_findings_user_payload_scaled( .unwrap_or_else(|_| "{\"commits\":[]}".to_string()); 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\":[...],\"confirmed_false_positives\":[...]}}]}}. \ -Treat \"baseline_findings\" as immutable comparison context. Return only surviving entries from \"findings\"; never copy baseline entries into the output. Independently validate every entry in \"baseline_false_positive_challenges\" against the repository and return only conclusively proven entries, verbatim, under each commit's \"confirmed_false_positives\" array. An empty or omitted confirmation array preserves every baseline finding. \ +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. \ 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." @@ -4249,10 +4369,23 @@ Do not return markdown, severity counts, severities, locations, links, or separa ) } +/// Private protocol token used by the LKML renderer to say that repository +/// verification eliminated every supplied finding. It is consumed before +/// report data is published and must never be shown to the user. +pub const LKML_NO_FINDINGS_SENTINEL: &str = "BORO_NO_FINDINGS"; + pub const LKML_FALLBACK_TEMPLATE: &str = "Write a polite LKML inline-style reply: quote relevant context lines with `>`, \ mention each finding with severity, keep a professional tone, no markdown headings or ALL CAPS."; +/// Normalize an LKML renderer response and consume its no-findings protocol +/// token. Empty model responses are treated the same way, so neither can +/// create an empty/false-positive report block in human or JSON output. +pub fn normalize_lkml_report_response(raw: &str) -> Option { + let body = strip_json_fences(raw); + (!body.is_empty() && body != LKML_NO_FINDINGS_SENTINEL).then_some(body) +} + /// One hunk pulled verbatim from a unified diff. #[derive(Debug, Clone)] pub struct DiffHunk { @@ -4594,6 +4727,29 @@ mod tests { assert!(payload.contains("diff --git a/foo.c b/foo.c")); assert!(payload.contains("final LKML-ready email body")); assert!(payload.contains("Preserve every URL")); + assert!(payload.contains(LKML_NO_FINDINGS_SENTINEL)); + assert!(payload.contains("Never describe a discarded finding")); + } + + #[test] + fn lkml_no_findings_response_is_consumed() { + assert_eq!( + normalize_lkml_report_response(LKML_NO_FINDINGS_SENTINEL), + None + ); + assert_eq!( + normalize_lkml_report_response(&format!("```\n{LKML_NO_FINDINGS_SENTINEL}\n```")), + None + ); + assert_eq!(normalize_lkml_report_response(" \n"), None); + } + + #[test] + fn lkml_real_report_response_is_preserved() { + assert_eq!( + normalize_lkml_report_response("```\nCan this leak foo?\n```"), + Some("Can this leak foo?".to_string()) + ); } #[test] @@ -4618,6 +4774,8 @@ mod tests { "location", "references", "baseline_findings", + "adjudicate EVERY entry", + "including entries absent from", "distinct failure modes at the same line", "\"commits\"", ] { @@ -4999,7 +5157,8 @@ diff --git a/kernel/sched/topology.c b/kernel/sched/topology.c "references": [{"kind": "lore", "url": "https://lore.kernel.org/all/example/", "claim": "reported upstream"}], "location": {"file": "x.c", "line": 42, "side": "RIGHT"} } - ] + ], + "baseline_adjudications": [] } ] }"#; @@ -5022,14 +5181,15 @@ diff --git a/kernel/sched/topology.c b/kernel/sched/topology.c let raw = r#"{"commits":[{"sha":"abc123","findings":[ {"problem":"x","severity":"Low","severity_explanation":"y", "location":{"file":"x.c","line":0,"side":"RIGHT"}} - ]}]}"#; + ],"baseline_adjudications":[]}]}"#; let v = parse_validation_findings(raw).unwrap(); assert!(v["commits"][0]["findings"][0].get("location").is_none()); } #[test] fn parse_validation_findings_empty_commit_findings_array_ok() { - let raw = r#"{"commits":[{"sha":"deadbeef0000","findings":[]}]}"#; + let raw = + r#"{"commits":[{"sha":"deadbeef0000","findings":[],"baseline_adjudications":[]}]}"#; let v = parse_validation_findings(raw).unwrap(); assert_eq!(v["commits"][0]["findings"], json!([])); } @@ -5043,7 +5203,7 @@ diff --git a/kernel/sched/topology.c b/kernel/sched/topology.c #[test] fn parse_validation_findings_tolerates_markdown_fence() { - let raw = "```json\n{\"commits\":[{\"sha\":\"abc\",\"findings\":[]}]}\n```"; + let raw = "```json\n{\"commits\":[{\"sha\":\"abc\",\"findings\":[],\"baseline_adjudications\":[]}]}\n```"; let v = parse_validation_findings(raw).unwrap(); assert_eq!(v["commits"][0]["sha"], "abc"); } @@ -5066,6 +5226,12 @@ diff --git a/kernel/sched/topology.c b/kernel/sched/topology.c assert!(parse_validation_findings(raw).is_err()); } + #[test] + fn parse_validation_findings_requires_confirmation_array() { + let raw = r#"{"commits":[{"sha":"abc","findings":[]}]}"#; + assert!(parse_validation_findings(raw).is_err()); + } + #[test] fn validation_findings_user_payload_serializes_input() { let challenges = json!([]); @@ -5100,6 +5266,8 @@ diff --git a/kernel/sched/topology.c b/kernel/sched/topology.c assert!(s.contains("\"problem\": \"off-by-one\"")); // The closing instruction mentioning the strict output shape must be present. assert!(s.contains("Return ONLY a JSON object")); + assert!(s.contains("Independently adjudicate every entry")); + assert!(s.contains("one structured, repository-tool-verified KEEP or DROP record")); } #[test] @@ -5804,6 +5972,8 @@ diff --git a/kernel/sched/topology.c b/kernel/sched/topology.c assert!(s.contains("# Patch")); assert!(s.contains("# Commit (headers)")); assert!(s.contains("include every referenced URL verbatim")); + assert!(!s.contains(LKML_NO_FINDINGS_SENTINEL)); + assert!(!s.contains("discarded finding")); } const SAMPLE_PATCH: &str = "\ @@ -6668,24 +6838,25 @@ index 123..456 100644 "commits": [{ "sha": "abc", "findings": [], - "confirmed_false_positives": [{"baseline_id":"fast-0"}] + "baseline_adjudications": [{"baseline_id":"fast-0"}] }] }"#; assert!(output_requires_tool_verification( - ToolVerification::SensitiveFindings, + ToolVerification::ValidationFindingsAndBaselineAdjudications, raw )); } #[test] - fn validation_parser_accepts_only_structured_baseline_confirmations() { - let challenge = json!({ + 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"], - "contradiction": "the argument is non-NULL on every reachable call", + "assessment": "the argument is non-NULL on every reachable call", "conclusion": "false_positive" } }); @@ -6693,7 +6864,7 @@ index 123..456 100644 "commits": [{ "sha": "abc", "findings": [], - "confirmed_false_positives": [challenge] + "baseline_adjudications": [drop_adjudication] }] }); assert!(parse_validation_findings(&valid.to_string()).is_ok()); @@ -6702,10 +6873,29 @@ index 123..456 100644 "commits": [{ "sha": "abc", "findings": [], - "confirmed_false_positives": [{"baseline_id": "fast-0"}] + "baseline_adjudications": [{"baseline_id": "fast-0"}] }] }); assert!(parse_validation_findings(&malformed.to_string()).is_err()); + + let keep = json!({ + "commits": [{ + "sha": "abc", + "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" + } + }] + }] + }); + assert!(parse_validation_findings(&keep.to_string()).is_ok()); } #[test] diff --git a/src/main.rs b/src/main.rs index 5b4bd56..946f34d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1334,9 +1334,77 @@ fn without_unverified_sensitive_findings(findings: &Value) -> (Vec, usize (retained, withheld) } -/// Validate regular-pipeline additions and independently adjudicate specialist -/// challenges to the protected fast-review baseline. A fast finding is removed -/// only when the strong validator returns the exact proposed challenge after +fn commit_needs_findings_validation(baseline: &Value, additions: &[Value]) -> bool { + !additions.is_empty() + || baseline + .as_array() + .is_some_and(|findings| !findings.is_empty()) +} + +fn validate_baseline_adjudication_coverage( + parsed: &Value, + expected: &[api::ValidationFindingsCommit<'_>], +) -> anyhow::Result<()> { + let commits = parsed + .get("commits") + .and_then(Value::as_array) + .context("validation output must contain a commits array")?; + if commits.len() != expected.len() { + anyhow::bail!("validation output must contain exactly one entry per input commit"); + } + + for (entry, input) in commits.iter().zip(expected) { + if entry.get("sha").and_then(Value::as_str) != Some(input.sha) { + anyhow::bail!("validation commits must preserve input order and exact sha"); + } + let baseline = input + .baseline_findings + .as_array() + .context("baseline_findings must be an array")?; + let adjudications = entry + .get("baseline_adjudications") + .and_then(Value::as_array) + .context("validation commit must contain baseline_adjudications")?; + if adjudications.len() != baseline.len() { + anyhow::bail!( + "validation commit {} must adjudicate every baseline finding exactly once", + input.sha + ); + } + for (index, (adjudication, finding)) in adjudications.iter().zip(baseline).enumerate() { + let expected_id = format!("fast-{index}"); + if adjudication.get("baseline_id").and_then(Value::as_str) != Some(expected_id.as_str()) + { + 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") + { + anyhow::bail!("deterministic upstream-fix findings must be kept"); + } + } + } + Ok(()) +} + +/// Independently adjudicate every protected fast-review baseline finding and +/// validate regular-pipeline additions. A fast finding is removed only when +/// the strong validator returns an exact structured false-positive proof after /// repository-tool verification; every failure mode preserves the baseline. #[allow(clippy::too_many_arguments)] async fn run_findings_validation( @@ -1355,9 +1423,10 @@ async fn run_findings_validation( // Domain-neutral findings validator, plus the target's linkage/build // addendum (kernel adds Kbuild/EXPORT_SYMBOL rules; other targets none). let validation_system = crate::target::review_validation_findings(target); - // Snapshot regular-stage candidates and specialist proof challenges. - // `findings[]` remains the protected baseline unless the strong validator - // explicitly confirms one of the challenges. + // Snapshot the protected baseline, regular-stage candidates, and specialist + // proof challenges. Every commit with a baseline or regular candidate must + // reach the strong adjudicator; specialist challenges are optional evidence, + // not a prerequisite for independently disproving a baseline finding. let mut payload_owned: Vec = Vec::new(); if let Some(commits) = out["commits"].as_array() { for c in commits { @@ -1371,7 +1440,8 @@ async fn run_findings_validation( .and_then(Value::as_array) .cloned() .unwrap_or_default(); - if findings.is_empty() && baseline_challenges.is_empty() { + let baseline_findings = c.get("findings").cloned().unwrap_or_else(|| json!([])); + if !commit_needs_findings_validation(&baseline_findings, &findings) { continue; } let sha_full = c.get("sha").and_then(|s| s.as_str()).unwrap_or(""); @@ -1395,7 +1465,6 @@ async fn run_findings_validation( .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); - let baseline_findings = c.get("findings").cloned().unwrap_or_else(|| json!([])); payload_owned.push(ValidationPayloadOwned { sha: sha12, subject, @@ -1411,7 +1480,7 @@ async fn run_findings_validation( if payload_owned.is_empty() { v( vdest, - "validation skipped (no regular-stage additions or baseline challenges to validate)", + "validation skipped (no baseline or regular-stage findings to adjudicate)", ); if let Some(commits) = out["commits"].as_array_mut() { for commit in commits { @@ -1458,7 +1527,7 @@ async fn run_findings_validation( let mut stage_tot = api::CumulativeTokenUsage::default(); let tool_cfg = (!no_tools).then(|| { api::ToolLoopConfig::new(repo) - .requiring(api::ToolVerification::ValidationFindingsAndBaselineChallenges) + .requiring(api::ToolVerification::ValidationFindingsAndBaselineAdjudications) }); let mut by_sha: std::collections::HashMap = std::collections::HashMap::new(); let mut verification_failed_shas: std::collections::HashSet = @@ -1511,7 +1580,11 @@ async fn run_findings_validation( tool_cfg.as_ref(), progress_line.as_ref(), repo, - api::parse_validation_findings, + |raw| { + let parsed = api::parse_validation_findings(raw)?; + validate_baseline_adjudication_coverage(&parsed, &payload_refs)?; + Ok(parsed) + }, api::RETRY_REMINDER_FINDINGS_VALIDATION, api::STAGE_RETRY_MAX_ATTEMPTS, ) @@ -1631,25 +1704,19 @@ async fn run_findings_validation( .cloned() .unwrap_or_else(|| json!([])) }; - let proposed_challenges = c - .get("_baseline_challenges") - .and_then(Value::as_array) - .cloned() - .unwrap_or_default(); - let confirmed_challenges = if no_tools { + let baseline_adjudications = if no_tools { Vec::new() } else { validated_entry .as_ref() - .and_then(|entry| entry.get("confirmed_false_positives")) + .and_then(|entry| entry.get("baseline_adjudications")) .and_then(Value::as_array) .cloned() .unwrap_or_default() }; - let removed = apply_confirmed_fast_false_positives( + let removed = apply_adjudicated_fast_false_positives( &mut c["findings"], - &proposed_challenges, - &confirmed_challenges, + &baseline_adjudications, vdest, ); if removed > 0 { @@ -1679,8 +1746,7 @@ async fn run_findings_validation( /// section) or `--dry-run`/Ctrl-C. For each commit: /// - prefer `validated_findings[]` when present (filter mode), else fall /// back to `findings[]` (off mode or validation failed); -/// - if the array is empty, set `lkml_report = "No issues found."` -/// without an LLM call; +/// - if the array is empty, omit `lkml_report` without an LLM call; /// - else call `api::chat_completion` with the target LKML prompt to render prose /// from the chosen finding set; /// - record the LKML render under validation-model usage and add the @@ -1698,6 +1764,22 @@ async fn run_findings_validation( /// keep/drop/tighten on the structured findings. When the validation /// model is distinct from the main model (e.g. a stronger validator), the /// prose is rendered by that stronger model. +fn lkml_input_has_no_findings(chosen: &Value, fast_review: Option<&str>) -> bool { + let chosen_is_empty = chosen + .as_array() + .is_some_and(|findings| findings.is_empty()); + if !chosen_is_empty { + return false; + } + let Some(review) = fast_review else { + return true; + }; + api::parse_findings_json(review) + .ok() + .and_then(|parsed| parsed.get("findings").and_then(Value::as_array).cloned()) + .is_some_and(|findings| findings.is_empty()) +} + #[allow(clippy::too_many_arguments)] async fn render_commit_lkml_phase( client: &reqwest::Client, @@ -1744,10 +1826,11 @@ async fn render_commit_lkml_phase( .and_then(|value| value.as_str()) .filter(|text| !text.trim().is_empty()) .map(str::to_owned); - let arr_empty = fast_review.is_none() - && chosen - .as_array() - .is_some_and(|findings| findings.is_empty()); + // Normal reviews have no `_fast_review`, so an empty chosen array is + // authoritative. Fast reviews retain their raw structured response; + // parse it here so `{"findings":[]}` is also suppressed without + // accidentally hiding a non-empty or malformed fast response. + let arr_empty = lkml_input_has_no_findings(&chosen, fast_review.as_deref()); let patch = c .get("patch") .and_then(|s| s.as_str()) @@ -1766,12 +1849,7 @@ async fn render_commit_lkml_phase( join_set.spawn(async move { let _permit = sem.acquire_owned().await.expect("semaphore closed"); if arr_empty { - return ( - idx, - sha_for_task, - Some("No issues found.".to_string()), - None, - ); + return (idx, sha_for_task, None, None); } let commit_headers = git::show_commit_headers(repo.as_path(), &sha_for_task).unwrap_or_default(); @@ -1812,7 +1890,14 @@ async fn render_commit_lkml_phase( ) .await { - Ok((raw, u)) => (Some(api::strip_json_fences(&raw)), u, None), + Ok((raw, u)) => { + let body = if fast_review.is_some() { + api::normalize_lkml_report_response(&raw) + } else { + Some(api::strip_json_fences(&raw)) + }; + (body, u, None) + } Err(e) => { v(&vd, format!("LKML render failed for {sha_for_task}: {e:#}")); ( @@ -3513,22 +3598,23 @@ fn merge_novel_findings(base: &[Value], additions: &[Value]) -> Vec { merged } -fn apply_confirmed_fast_false_positives( +fn apply_adjudicated_fast_false_positives( baseline_findings: &mut Value, - proposed: &[Value], - confirmed: &[Value], + adjudications: &[Value], vd: &VerboseDest, ) -> usize { let mut disproved = std::collections::HashSet::new(); - for challenge in confirmed { - if !proposed.contains(challenge) { - v( - vd, - "strong-validator baseline confirmation rejected: it was not returned verbatim from the specialist proposals", - ); + for adjudication in adjudications { + if adjudication.get("verdict").and_then(Value::as_str) != Some("DROP") + || adjudication + .get("proof") + .and_then(|proof| proof.get("conclusion")) + .and_then(Value::as_str) + != Some("false_positive") + { continue; } - let Some(id) = challenge.get("baseline_id").and_then(Value::as_str) else { + let Some(id) = adjudication.get("baseline_id").and_then(Value::as_str) else { continue; }; let Some(index_text) = id.strip_prefix("fast-") else { @@ -3550,12 +3636,21 @@ fn apply_confirmed_fast_false_positives( let Some(problem) = original.get("problem").and_then(Value::as_str) else { continue; }; - let challenge_finding = challenge.get("finding"); - let proof_claim = challenge + if original.get("source").and_then(Value::as_str) == Some("upstream-fixes") { + v( + vd, + format!( + "strong-validator baseline confirmation {id} rejected: deterministic upstream-fix findings are not adjudicable" + ), + ); + 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 challenge_finding != Some(original) || proof_claim != Some(problem) { + if adjudicated_finding != Some(original) || proof_claim != Some(problem) { v( vd, format!( @@ -3579,8 +3674,6 @@ fn apply_confirmed_fast_false_positives( return 0; }; let before = findings.len(); - // Fast findings occupy the protected prefix. Deterministic upstream findings - // appended later are outside the challenge channel and can never be removed. let mut index = 0usize; findings.retain(|_| { let keep = !disproved.contains(&index); @@ -4854,6 +4947,35 @@ mod fast_cli_tests { assert!(payload.contains("\"references\"")); } + #[test] + fn lkml_skips_empty_normal_and_fast_review_inputs() { + let empty = json!([]); + assert!(lkml_input_has_no_findings(&empty, None)); + assert!(lkml_input_has_no_findings( + &empty, + Some(r#"{"findings":[]}"#) + )); + } + + #[test] + fn lkml_keeps_nonempty_or_unstructured_fast_review_inputs() { + let empty = json!([]); + assert!(!lkml_input_has_no_findings( + &empty, + Some( + r#"{"findings":[{"problem":"bug","severity":"High","severity_explanation":"proof"}]}"# + ) + )); + assert!(!lkml_input_has_no_findings( + &empty, + Some("unstructured review text") + )); + assert!(!lkml_input_has_no_findings( + &json!([{"problem": "bug"}]), + None + )); + } + #[test] fn exact_regular_duplicate_cannot_replace_fast_provenance() { let baseline = vec![json!({ @@ -4916,18 +5038,18 @@ mod fast_cli_tests { 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"], - "contradiction": "the argument cannot be NULL", + "assessment": "the argument cannot be NULL", "conclusion": "false_positive" } }); - let removed = apply_confirmed_fast_false_positives( + let removed = apply_adjudicated_fast_false_positives( &mut baseline, std::slice::from_ref(&challenge), - std::slice::from_ref(&challenge), &VerboseDest::new(false), ); @@ -4936,25 +5058,128 @@ mod fast_cli_tests { } #[test] - fn baseline_challenge_cannot_remove_a_finding_without_exact_text_match() { + fn baseline_only_commit_still_reaches_strong_adjudication() { + assert!(commit_needs_findings_validation( + &json!([{"problem": "baseline finding"}]), + &[], + )); + assert!(commit_needs_findings_validation( + &json!([]), + &[json!({"problem": "regular addition"})], + )); + assert!(!commit_needs_findings_validation(&json!([]), &[])); + } + + #[test] + fn baseline_coverage_requires_one_exact_ordered_adjudication_per_finding() { + let baseline = json!([ + {"problem": "first issue", "severity": "High"}, + {"problem": "second issue", "severity": "Low"} + ]); + let empty = json!([]); + let inputs = [api::ValidationFindingsCommit { + sha: "abc123def456", + subject: "subject", + commit_message: "message", + reference_context: "context", + diff: "diff", + baseline_findings: &baseline, + baseline_challenges: &empty, + findings: &empty, + }]; + let parsed = json!({"commits": [{ + "sha": "abc123def456", + "findings": [], + "baseline_adjudications": [ + { + "baseline_id": "fast-0", + "finding": baseline[0].clone(), + "verdict": "KEEP", + "proof": { + "finding_claim": "first issue", + "verified_facts": ["fact"], + "assessment": "supported", + "conclusion": "supported" + } + }, + { + "baseline_id": "fast-1", + "finding": baseline[1].clone(), + "verdict": "DROP", + "proof": { + "finding_claim": "second issue", + "verified_facts": ["fact"], + "assessment": "disproved", + "conclusion": "false_positive" + } + } + ] + }]}); + + validate_baseline_adjudication_coverage(&parsed, &inputs).unwrap(); + + let mut missing = parsed.clone(); + missing["commits"][0]["baseline_adjudications"] + .as_array_mut() + .unwrap() + .pop(); + assert!(validate_baseline_adjudication_coverage(&missing, &inputs).is_err()); + + let mut reordered = parsed; + reordered["commits"][0]["baseline_adjudications"] + .as_array_mut() + .unwrap() + .swap(0, 1); + assert!(validate_baseline_adjudication_coverage(&reordered, &inputs).is_err()); + } + + #[test] + fn keep_adjudication_never_removes_a_baseline_finding() { + let finding = json!({"problem": "foo dereferences NULL", "severity": "High"}); + 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" + } + }); + + assert_eq!( + apply_adjudicated_fast_false_positives( + &mut baseline, + &[keep], + &VerboseDest::new(false), + ), + 0 + ); + assert_eq!(baseline.as_array().unwrap().len(), 1); + } + + #[test] + fn baseline_adjudication_cannot_remove_a_finding_without_exact_text_match() { 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"}, + "verdict": "DROP", "proof": { "finding_claim": "foo is probably safe", "verified_facts": ["fact"], - "contradiction": "contradiction", + "assessment": "contradiction", "conclusion": "false_positive" } }); assert_eq!( - apply_confirmed_fast_false_positives( + apply_adjudicated_fast_false_positives( &mut baseline, std::slice::from_ref(&challenge), - std::slice::from_ref(&challenge), &VerboseDest::new(false), ), 0 @@ -4963,30 +5188,61 @@ mod fast_cli_tests { } #[test] - fn strong_validator_cannot_invent_an_unproposed_baseline_removal() { + fn strong_validator_can_independently_remove_an_unchallenged_baseline_finding() { let fast = vec![json!({"problem": "foo dereferences NULL", "severity": "High"})]; let mut baseline = Value::Array(fast.clone()); - let invented = json!({ + 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"], - "contradiction": "the argument cannot be NULL", + "assessment": "the argument cannot be NULL", + "conclusion": "false_positive" + } + }); + + assert_eq!( + apply_adjudicated_fast_false_positives( + &mut baseline, + &[adjudicated], + &VerboseDest::new(false), + ), + 1 + ); + assert_eq!(baseline, json!([])); + } + + #[test] + fn strong_validator_cannot_remove_deterministic_upstream_fix() { + let upstream = json!({ + "problem": "upstream fixed a regression", + "severity": "High", + "source": "upstream-fixes" + }); + 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" } }); assert_eq!( - apply_confirmed_fast_false_positives( + apply_adjudicated_fast_false_positives( &mut baseline, - &[], - &[invented], + &[adjudicated], &VerboseDest::new(false), ), 0 ); - assert_eq!(baseline, Value::Array(fast)); + assert_eq!(baseline, json!([upstream])); } #[test]