use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::time::SystemTime;
use super::ab_learnings::{Confidence, DurableFixProposal};
use super::merge::GhError;
use super::provenance::{
resolve_tier, LocalSignatures, PermissionOracle, ProvenanceRecord, ProvenanceTier, RawIssue,
};
pub const DEFAULT_REPORT_REPO: &str = "Parslee-ai/car-releases";
const TENTATIVE_MIN_EVIDENCE: usize = 2;
pub trait IssueApi: Send + Sync {
fn list_open_issues(&self, repo: &str) -> Result<Vec<RawIssue>, GhError>;
fn create_issue(
&self,
repo: &str,
title: &str,
body: &str,
labels: &[String],
) -> Result<u64, GhError>;
}
pub fn proposal_signature(proposal: &DurableFixProposal) -> String {
let mut hasher = Sha256::new();
hasher.update(proposal.target_component.trim().to_lowercase().as_bytes());
hasher.update([0u8]);
hasher.update(proposal.pattern.trim().to_lowercase().as_bytes());
format!("{:x}", hasher.finalize())[..16].to_string()
}
pub fn signature_marker(signature: &str) -> String {
format!("<!-- car-fix-signature: {signature} -->")
}
pub fn parse_signature_marker(body: &str) -> Option<&str> {
const OPEN: &str = "<!-- car-fix-signature: ";
const CLOSE: &str = " -->";
let mut rest = body;
while let Some(open) = rest.find(OPEN) {
let after = &rest[open + OPEN.len()..];
if let Some(end) = after.find(CLOSE) {
let signature = &after[..end];
if !signature.is_empty() && signature.chars().all(|c| c.is_ascii_hexdigit()) {
return Some(signature);
}
}
rest = &rest[open + OPEN.len()..];
}
None
}
pub fn clears_reporting_bar(proposal: &DurableFixProposal) -> bool {
match proposal.confidence {
Confidence::Strong => true,
Confidence::Tentative => proposal.evidence_count >= TENTATIVE_MIN_EVIDENCE,
}
}
pub fn render_issue_body(proposal: &DurableFixProposal, signature: &str) -> String {
let mut body = String::new();
body.push_str("_Filed automatically by `car coder-ab` from a dogfooding round._\n\n");
body.push_str("## Pattern\n\n");
body.push_str(proposal.pattern.trim());
body.push_str("\n\n## Where to look\n\n");
body.push_str(&format!("- **Component**: {}\n", proposal.target_component));
body.push_str(&format!(
"- **Hint**: {} _(best-effort — verify before editing)_\n",
proposal.target_hint
));
body.push_str("\n## Proposed change\n\n");
body.push_str(proposal.proposed_change.trim());
body.push_str("\n\n## Evidence\n\n");
body.push_str(&format!(
"{} occurrence(s), confidence {:?}, kind {:?}, priority {}.\n",
proposal.evidence_count, proposal.confidence, proposal.kind, proposal.priority
));
if !proposal.evidence_tasks.is_empty() {
body.push_str("\nTasks evidencing this pattern:\n\n");
for task in &proposal.evidence_tasks {
body.push_str(&format!("- `{task}`\n"));
}
}
body.push_str(
"\n---\n\nThis is a *class* of change, not a patch — the synthesizer narrows where to \
look, it does not invent the specific fix.\n\n",
);
body.push_str(&signature_marker(signature));
body.push('\n');
body
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "outcome")]
pub enum ReportOutcome {
Filed { number: u64 },
AlreadyOpen { number: u64 },
BelowBar,
Failed { error: String },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReportRecord {
pub title: String,
pub signature: String,
#[serde(flatten)]
pub outcome: ReportOutcome,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub provenance: Vec<ProvenanceRecord>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub public_marker_carriers: Vec<u64>,
}
const MAX_MARKER_CARRIERS: usize = 5;
pub fn report_proposals(
api: &dyn IssueApi,
oracle: &dyn PermissionOracle,
repo: &str,
proposals: &[DurableFixProposal],
labels: &[String],
) -> Vec<ReportRecord> {
let local = LocalSignatures::from_proposals(proposals);
let open = api.list_open_issues(repo);
let viewer = oracle.viewer_login();
proposals
.iter()
.map(|proposal| {
let signature = proposal_signature(proposal);
let mut record = ReportRecord {
title: proposal.title.clone(),
signature: signature.clone(),
outcome: ReportOutcome::BelowBar,
provenance: Vec::new(),
public_marker_carriers: Vec::new(),
};
if !clears_reporting_bar(proposal) {
return record;
}
let rows = match (&open, &viewer) {
(Ok(rows), Ok(_)) => rows,
(Err(e), _) | (_, Err(e)) => {
record.outcome = ReportOutcome::Failed {
error: e.to_string(),
};
return record;
}
};
let already_open = resolve_marker_carriers(
rows,
&signature,
oracle,
&local,
viewer.as_deref().ok(),
&mut record,
);
if let Some(number) = already_open {
record.outcome = ReportOutcome::AlreadyOpen { number };
return record;
}
let body = render_issue_body(proposal, &signature);
record.outcome = match api.create_issue(repo, &proposal.title, &body, labels) {
Ok(number) => ReportOutcome::Filed { number },
Err(e) => ReportOutcome::Failed {
error: e.to_string(),
},
};
record
})
.collect()
}
fn resolve_marker_carriers(
rows: &[RawIssue],
signature: &str,
oracle: &dyn PermissionOracle,
local: &LocalSignatures,
viewer: Option<&str>,
record: &mut ReportRecord,
) -> Option<u64> {
let mut carriers: Vec<&RawIssue> = rows
.iter()
.filter(|row| row.carries_marker(signature))
.collect();
carriers.sort_by_key(|row| match viewer {
Some(v) if v.eq_ignore_ascii_case(row.author_login()) => 0,
_ => 1,
});
for row in carriers.into_iter().take(MAX_MARKER_CARRIERS) {
let tiered = resolve_tier((*row).clone(), oracle, local, SystemTime::now());
record.provenance.push(tiered.record().clone());
if tiered.tier() == ProvenanceTier::Public {
record.public_marker_carriers.push(tiered.number());
continue;
}
return Some(tiered.number());
}
None
}
pub fn render_report(records: &[ReportRecord], repo: &str) -> String {
if records.is_empty() {
return "issue reporting: no proposals to report".to_string();
}
let mut out = format!("issue reporting → {repo}\n");
for record in records {
let line = match &record.outcome {
ReportOutcome::Filed { number } => format!("filed #{number}"),
ReportOutcome::AlreadyOpen { number } => format!("already open as #{number}"),
ReportOutcome::BelowBar => "skipped (below reporting bar)".to_string(),
ReportOutcome::Failed { error } => format!("FAILED: {error}"),
};
out.push_str(&format!(" {} — {}\n", record.title, line));
if !record.public_marker_carriers.is_empty() {
out.push_str(&format!(
" note: signature also appears on issue(s) {} filed by accounts with no write \
access to the tracker — ignored for deduplication\n",
record
.public_marker_carriers
.iter()
.map(|n| format!("#{n}"))
.collect::<Vec<_>>()
.join(", ")
));
}
}
out
}
const SCAN_LIMIT: usize = 200;
pub struct GhIssues;
impl IssueApi for GhIssues {
fn list_open_issues(&self, repo: &str) -> Result<Vec<RawIssue>, GhError> {
let args: Vec<String> = vec![
"issue".into(),
"list".into(),
"--repo".into(),
repo.into(),
"--state".into(),
"open".into(),
"--limit".into(),
SCAN_LIMIT.to_string(),
"--json".into(),
"number,title,body,author".into(),
];
let out = super::merge::gh(std::path::Path::new("."), &args)?;
let rows: Vec<IssueRow> = serde_json::from_str(&out).map_err(|e| GhError {
message: format!("could not parse `gh issue list` output: {e}"),
stderr: String::new(),
})?;
Ok(rows
.into_iter()
.map(|row| {
let login = row.author.map(|a| a.login).unwrap_or_default();
RawIssue::new(repo, row.number, login, row.title, row.body)
})
.collect())
}
fn create_issue(
&self,
repo: &str,
title: &str,
body: &str,
labels: &[String],
) -> Result<u64, GhError> {
let mut args: Vec<String> = vec![
"issue".into(),
"create".into(),
"--repo".into(),
repo.into(),
"--title".into(),
title.into(),
"--body".into(),
body.into(),
];
for label in labels {
args.push("--label".into());
args.push(label.clone());
}
let out = super::merge::gh(std::path::Path::new("."), &args)?;
parse_issue_number(&out).ok_or_else(|| GhError {
message: "`gh issue create` succeeded but printed no issue URL".to_string(),
stderr: String::new(),
})
}
}
#[derive(Deserialize)]
struct IssueRow {
number: u64,
title: String,
body: String,
#[serde(default)]
author: Option<IssueAuthor>,
}
#[derive(Deserialize)]
struct IssueAuthor {
#[serde(default)]
login: String,
}
fn parse_issue_number(output: &str) -> Option<u64> {
output
.split_whitespace()
.last()?
.rsplit('/')
.next()?
.parse()
.ok()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::coder::ab_learnings::ProposalKind;
use crate::coder::provenance::RepoPermission;
use std::sync::Mutex;
fn proposal(
component: &str,
pattern: &str,
confidence: Confidence,
evidence: usize,
) -> DurableFixProposal {
DurableFixProposal {
title: format!("fix {component}"),
pattern: pattern.to_string(),
target_component: component.to_string(),
target_hint: "coder/native_loop.rs".to_string(),
proposed_change: "widen the guard".to_string(),
evidence_tasks: vec!["task-1".to_string()],
evidence_count: evidence,
kind: ProposalKind::HarnessAddressable,
confidence,
priority: 10,
}
}
const BOT: &str = "car-bot";
#[derive(Default)]
struct FakeIssues {
open: Vec<(u64, String, String)>,
created: Mutex<Vec<(String, String, String)>>,
fail_list: bool,
fail_create: bool,
}
struct FakeOracle {
viewer: String,
maintainers: Vec<String>,
fail_identity: bool,
}
impl Default for FakeOracle {
fn default() -> Self {
Self {
viewer: BOT.to_string(),
maintainers: Vec::new(),
fail_identity: false,
}
}
}
impl PermissionOracle for FakeOracle {
fn viewer_login(&self) -> Result<String, GhError> {
if self.fail_identity {
return Err(GhError {
message: "gh api user failed".into(),
stderr: String::new(),
});
}
Ok(self.viewer.clone())
}
fn permission(&self, _repo: &str, login: &str) -> Result<RepoPermission, GhError> {
Ok(if self.maintainers.iter().any(|m| m == login) {
RepoPermission::Write
} else {
RepoPermission::None
})
}
}
impl IssueApi for FakeIssues {
fn list_open_issues(&self, repo: &str) -> Result<Vec<RawIssue>, GhError> {
if self.fail_list {
return Err(GhError {
message: "list exploded".into(),
stderr: String::new(),
});
}
Ok(self
.open
.iter()
.map(|(number, author, body)| {
RawIssue::new(repo, *number, author, "an open issue", body)
})
.collect())
}
fn create_issue(
&self,
repo: &str,
title: &str,
body: &str,
_labels: &[String],
) -> Result<u64, GhError> {
if self.fail_create {
return Err(GhError {
message: "create exploded".into(),
stderr: String::new(),
});
}
let mut created = self.created.lock().unwrap();
created.push((repo.to_string(), title.to_string(), body.to_string()));
Ok(900 + created.len() as u64)
}
}
#[test]
fn signature_is_stable_across_calls() {
let p = proposal(
"native_loop",
"tool results never returned",
Confidence::Strong,
3,
);
assert_eq!(proposal_signature(&p), proposal_signature(&p));
}
#[test]
fn signature_ignores_volatile_fields() {
let mut a = proposal(
"native_loop",
"tool results never returned",
Confidence::Strong,
3,
);
let mut b = a.clone();
b.title = "completely different wording".into();
b.evidence_tasks = vec!["task-9".into(), "task-12".into()];
b.evidence_count = 11;
b.priority = 1;
assert_eq!(proposal_signature(&a), proposal_signature(&b));
a.target_component = "contract".into();
assert_ne!(proposal_signature(&a), proposal_signature(&b));
}
#[test]
fn separator_prevents_field_boundary_collision() {
let a = proposal("ab", "c", Confidence::Strong, 1);
let b = proposal("a", "bc", Confidence::Strong, 1);
assert_ne!(proposal_signature(&a), proposal_signature(&b));
}
#[test]
fn strong_always_clears_tentative_needs_recurrence() {
assert!(clears_reporting_bar(&proposal(
"c",
"p",
Confidence::Strong,
1
)));
assert!(!clears_reporting_bar(&proposal(
"c",
"p",
Confidence::Tentative,
1
)));
assert!(clears_reporting_bar(&proposal(
"c",
"p",
Confidence::Tentative,
2
)));
}
#[test]
fn body_carries_the_marker_that_dedupe_reads() {
let p = proposal("native_loop", "thrash", Confidence::Strong, 3);
let sig = proposal_signature(&p);
let body = render_issue_body(&p, &sig);
assert!(body.contains(&signature_marker(&sig)));
assert!(body.contains(&sig));
assert!(body.contains("native_loop"));
assert!(body.contains("widen the guard"));
}
#[test]
fn files_new_proposals_and_skips_already_open_ones() {
let already = proposal("contract", "derivation drifts", Confidence::Strong, 4);
let sig = proposal_signature(&already);
let api = FakeIssues {
open: vec![(
77,
BOT.to_string(),
format!("stale text {}", signature_marker(&sig)),
)],
..Default::default()
};
let fresh = proposal("native_loop", "thrash", Confidence::Strong, 2);
let noise = proposal("router", "maybe", Confidence::Tentative, 1);
let records = report_proposals(
&api,
&FakeOracle::default(),
"acme/releases",
&[already, fresh, noise],
&[],
);
assert_eq!(
records[0].outcome,
ReportOutcome::AlreadyOpen { number: 77 }
);
assert_eq!(records[1].outcome, ReportOutcome::Filed { number: 901 });
assert_eq!(records[2].outcome, ReportOutcome::BelowBar);
let created = api.created.lock().unwrap();
assert_eq!(created.len(), 1);
assert_eq!(created[0].0, "acme/releases");
}
#[test]
fn a_tracker_failure_is_recorded_and_does_not_abort_the_round() {
let api = FakeIssues {
fail_create: true,
..Default::default()
};
let records = report_proposals(
&api,
&FakeOracle::default(),
"acme/releases",
&[
proposal("a", "one", Confidence::Strong, 2),
proposal("b", "two", Confidence::Strong, 2),
],
&[],
);
assert_eq!(records.len(), 2);
assert!(matches!(records[0].outcome, ReportOutcome::Failed { .. }));
assert!(matches!(records[1].outcome, ReportOutcome::Failed { .. }));
}
#[test]
fn a_lookup_failure_does_not_file_blind() {
let api = FakeIssues {
fail_list: true,
..Default::default()
};
let records = report_proposals(
&api,
&FakeOracle::default(),
"acme/releases",
&[proposal("a", "one", Confidence::Strong, 2)],
&[],
);
assert!(matches!(records[0].outcome, ReportOutcome::Failed { .. }));
assert!(api.created.lock().unwrap().is_empty());
}
#[test]
fn a_stranger_wearing_our_marker_cannot_suppress_a_real_report() {
let p = proposal("contract", "derivation drifts", Confidence::Strong, 4);
let sig = proposal_signature(&p);
let api = FakeIssues {
open: vec![(
4242,
"drive-by".to_string(),
format!("unrelated rant {}", signature_marker(&sig)),
)],
..Default::default()
};
let records = report_proposals(&api, &FakeOracle::default(), "acme/releases", &[p], &[]);
assert_eq!(records[0].outcome, ReportOutcome::Filed { number: 901 });
assert_eq!(records[0].public_marker_carriers, vec![4242]);
assert_eq!(records[0].provenance.len(), 1);
assert_eq!(records[0].provenance[0].tier, ProvenanceTier::Public);
let rendered = render_report(&records, "acme/releases");
assert!(rendered.contains("#4242"));
assert!(rendered.contains("ignored for deduplication"));
}
#[test]
fn a_teammates_report_still_dedupes() {
let p = proposal("contract", "derivation drifts", Confidence::Strong, 4);
let sig = proposal_signature(&p);
let api = FakeIssues {
open: vec![(50, "a-maintainer".to_string(), signature_marker(&sig))],
..Default::default()
};
let oracle = FakeOracle {
maintainers: vec!["a-maintainer".to_string()],
..Default::default()
};
let records = report_proposals(&api, &oracle, "acme/releases", &[p], &[]);
assert_eq!(
records[0].outcome,
ReportOutcome::AlreadyOpen { number: 50 }
);
assert_eq!(records[0].provenance[0].tier, ProvenanceTier::Maintainer);
assert!(records[0].public_marker_carriers.is_empty());
assert!(api.created.lock().unwrap().is_empty());
}
#[test]
fn an_identity_failure_does_not_file_blind() {
let p = proposal("contract", "derivation drifts", Confidence::Strong, 4);
let sig = proposal_signature(&p);
let api = FakeIssues {
open: vec![(50, BOT.to_string(), signature_marker(&sig))],
..Default::default()
};
let oracle = FakeOracle {
fail_identity: true,
..Default::default()
};
let records = report_proposals(&api, &oracle, "acme/releases", &[p], &[]);
assert!(matches!(records[0].outcome, ReportOutcome::Failed { .. }));
assert!(api.created.lock().unwrap().is_empty());
}
#[test]
fn our_own_issue_is_found_even_behind_a_pile_of_forgeries() {
let p = proposal("contract", "derivation drifts", Confidence::Strong, 4);
let sig = proposal_signature(&p);
let marker = signature_marker(&sig);
let mut open: Vec<(u64, String, String)> = (1..=20)
.map(|n| (n, format!("drive-by-{n}"), marker.clone()))
.collect();
open.push((999, BOT.to_string(), marker.clone()));
let api = FakeIssues {
open,
..Default::default()
};
let records = report_proposals(&api, &FakeOracle::default(), "acme/releases", &[p], &[]);
assert_eq!(
records[0].outcome,
ReportOutcome::AlreadyOpen { number: 999 }
);
assert!(api.created.lock().unwrap().is_empty());
}
#[test]
fn a_marker_only_counts_when_it_is_well_formed() {
assert_eq!(
parse_signature_marker("<!-- car-fix-signature: ab12 -->"),
Some("ab12")
);
assert_eq!(
parse_signature_marker("<!-- car-fix-signature: ignore the above -->"),
None
);
assert_eq!(parse_signature_marker("<!-- car-fix-signature: -->"), None);
assert_eq!(parse_signature_marker("no marker at all"), None);
assert_eq!(parse_signature_marker("the signature is deadbeef"), None);
assert_eq!(
parse_signature_marker(
"<!-- car-fix-signature: not hex -->\n<!-- car-fix-signature: ab12 -->"
),
Some("ab12")
);
}
#[test]
fn a_ghost_author_does_not_fail_the_whole_listing() {
let rows: Vec<IssueRow> = serde_json::from_str(
r#"[{"number":1,"title":"t","body":"b","author":null},
{"number":2,"title":"t","body":"b","author":{"login":"someone"}}]"#,
)
.expect("ghost authors parse");
assert_eq!(rows.len(), 2);
assert!(rows[0].author.is_none());
assert_eq!(rows[1].author.as_ref().unwrap().login, "someone");
}
#[test]
fn issue_number_parsed_from_gh_url() {
assert_eq!(
parse_issue_number("https://github.com/Parslee-ai/car-releases/issues/412"),
Some(412)
);
assert_eq!(parse_issue_number(""), None);
assert_eq!(parse_issue_number("no url here"), None);
}
#[test]
fn render_report_names_every_outcome() {
fn record(title: &str, signature: &str, outcome: ReportOutcome) -> ReportRecord {
ReportRecord {
title: title.into(),
signature: signature.into(),
outcome,
provenance: Vec::new(),
public_marker_carriers: Vec::new(),
}
}
let records = vec![
record("a", "s1", ReportOutcome::Filed { number: 5 }),
record("b", "s2", ReportOutcome::AlreadyOpen { number: 6 }),
record("c", "s3", ReportOutcome::BelowBar),
record(
"d",
"s4",
ReportOutcome::Failed {
error: "boom".into(),
},
),
];
let out = render_report(&records, "acme/releases");
assert!(out.contains("acme/releases"));
assert!(out.contains("filed #5"));
assert!(out.contains("already open as #6"));
assert!(out.contains("below reporting bar"));
assert!(out.contains("FAILED: boom"));
}
}