use serde_json::Value;
use std::collections::{BTreeMap, BTreeSet};
pub(crate) const MUTATION_CALIBRATION_SCHEMA_VERSION: &str = "0.1";
const STATIC_WITHOUT_RUNTIME_SAMPLE_LIMIT: usize = 50;
const AGREEMENT_SAMPLE_LIMIT: usize = 50;
#[derive(Clone, Debug, Eq, PartialEq)]
struct StaticSeamRecord {
seam_id: String,
seam_kind: String,
file: String,
line: usize,
seam_grip_class: String,
oracle_kind: String,
oracle_strength: String,
observed_values: Vec<String>,
missing_discriminators: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct MutationOutcomeRecord {
mutant_id: Option<String>,
seam_id: Option<String>,
file: Option<String>,
line: Option<usize>,
mutation_operator: String,
runtime_outcome: String,
duration: Option<String>,
test_command: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct MutationCalibrationReport {
static_seams_total: usize,
mutants_total: usize,
agreement: MutationCalibrationAgreement,
precision_notes: Vec<String>,
missed_runtime_signals: Vec<MutationCalibrationRuntimeSignal>,
static_only_findings: Vec<MutationCalibrationStaticOnlyFinding>,
matched: Vec<MutationCalibrationMatch>,
ambiguous_file_line: Vec<AmbiguousMutationCalibrationMatch>,
unmatched_mutants: Vec<MutationOutcomeRecord>,
static_without_runtime: Vec<StaticSeamRecord>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
struct MutationCalibrationAgreement {
static_gap_and_runtime_signal: usize,
static_gap_without_runtime_signal: usize,
runtime_signal_without_static_gap: usize,
static_clean_and_runtime_clean: usize,
runtime_inconclusive: usize,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct MutationCalibrationRuntimeSignal {
runtime: MutationOutcomeRecord,
static_seam: Option<StaticSeamRecord>,
reason: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct MutationCalibrationStaticOnlyFinding {
seam: StaticSeamRecord,
reason: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct MutationCalibrationMatch {
join_method: &'static str,
seam: StaticSeamRecord,
mutation: MutationOutcomeRecord,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct AmbiguousMutationCalibrationMatch {
mutation: MutationOutcomeRecord,
candidates: Vec<StaticSeamRecord>,
}
pub(crate) fn mutation_calibration_report_from_json(
repo_exposure_json: &str,
mutants_json: &str,
) -> Result<MutationCalibrationReport, String> {
let static_seams = parse_repo_exposure_static_seams(repo_exposure_json)?;
let runtime_mutants = parse_mutation_outcomes_json(mutants_json)?;
Ok(build_mutation_calibration_report(
static_seams,
runtime_mutants,
))
}
pub(crate) fn render_mutation_calibration_json(
report: &MutationCalibrationReport,
) -> Result<String, String> {
let value = serde_json::json!({
"schema_version": MUTATION_CALIBRATION_SCHEMA_VERSION,
"scope": "repo",
"status": "advisory",
"metrics": {
"static_seams_total": report.static_seams_total,
"mutants_total": report.mutants_total,
"matched_total": report.matched.len(),
"ambiguous_file_line_total": report.ambiguous_file_line.len(),
"unmatched_mutants_total": report.unmatched_mutants.len(),
"static_without_runtime_total": report.static_without_runtime.len(),
"runtime_outcome_counts": runtime_outcome_counts(report),
"join_method_counts": join_method_counts(report),
},
"agreement": mutation_calibration_agreement_json(&report.agreement),
"precision_notes": &report.precision_notes,
"missed_runtime_signals": report
.missed_runtime_signals
.iter()
.map(mutation_calibration_runtime_signal_json)
.collect::<Vec<_>>(),
"static_only_findings": report
.static_only_findings
.iter()
.map(mutation_calibration_static_only_json)
.collect::<Vec<_>>(),
"matches": report
.matched
.iter()
.map(mutation_calibration_match_json)
.collect::<Vec<_>>(),
"ambiguous_file_line_matches": report
.ambiguous_file_line
.iter()
.map(ambiguous_mutation_calibration_match_json)
.collect::<Vec<_>>(),
"unmatched_mutants": report
.unmatched_mutants
.iter()
.map(mutation_outcome_json)
.collect::<Vec<_>>(),
"static_without_runtime_sample": report
.static_without_runtime
.iter()
.take(STATIC_WITHOUT_RUNTIME_SAMPLE_LIMIT)
.map(static_seam_json)
.collect::<Vec<_>>(),
});
serde_json::to_string_pretty(&value)
.map(|mut rendered| {
rendered.push('\n');
rendered
})
.map_err(|err| format!("failed to render mutation calibration JSON: {err}"))
}
pub(crate) fn render_mutation_calibration_md(report: &MutationCalibrationReport) -> String {
let mut out = String::new();
out.push_str("# ripr mutation calibration report\n\n");
out.push_str("Status: advisory\n\n");
out.push_str(
"This report joins static seam evidence to supplied cargo-mutants runtime data. \
Runtime outcome vocabulary in this report comes from that runtime data; static \
ripr reports continue to use audit vocabulary only.\n\n",
);
out.push_str("## Summary\n\n");
out.push_str("| Metric | Count |\n| --- | ---: |\n");
out.push_str(&format!(
"| static_seams_total | {} |\n",
report.static_seams_total
));
out.push_str(&format!("| mutants_total | {} |\n", report.mutants_total));
out.push_str(&format!("| matched_total | {} |\n", report.matched.len()));
out.push_str(&format!(
"| ambiguous_file_line_total | {} |\n",
report.ambiguous_file_line.len()
));
out.push_str(&format!(
"| unmatched_mutants_total | {} |\n",
report.unmatched_mutants.len()
));
out.push_str(&format!(
"| static_without_runtime_total | {} |\n",
report.static_without_runtime.len()
));
out.push_str("\n## Static/runtime agreement\n\n");
out.push_str("| Agreement bucket | Count |\n| --- | ---: |\n");
out.push_str(&format!(
"| static_gap_and_runtime_signal | {} |\n",
report.agreement.static_gap_and_runtime_signal
));
out.push_str(&format!(
"| static_gap_without_runtime_signal | {} |\n",
report.agreement.static_gap_without_runtime_signal
));
out.push_str(&format!(
"| runtime_signal_without_static_gap | {} |\n",
report.agreement.runtime_signal_without_static_gap
));
out.push_str(&format!(
"| static_clean_and_runtime_clean | {} |\n",
report.agreement.static_clean_and_runtime_clean
));
out.push_str(&format!(
"| runtime_inconclusive | {} |\n",
report.agreement.runtime_inconclusive
));
out.push_str("\nPrecision notes:\n\n");
for note in &report.precision_notes {
out.push_str(&format!("- {}\n", md_cell(note)));
}
out.push_str("\n### Runtime signals without static gaps\n\n");
if report.missed_runtime_signals.is_empty() {
out.push_str("No imported runtime gap signals lacked a matching static gap.\n");
} else {
out.push_str("| Runtime mutant | Location | Runtime outcome | Static class | Reason |\n");
out.push_str("| --- | --- | --- | --- | --- |\n");
for record in &report.missed_runtime_signals {
let mutant = record.runtime.mutant_id.as_deref().unwrap_or("unknown");
let location = mutation_location_label(&record.runtime);
let static_class = record
.static_seam
.as_ref()
.map(|seam| seam.seam_grip_class.as_str())
.unwrap_or("unmatched");
out.push_str(&format!(
"| `{}` | {} | {} | `{}` | {} |\n",
md_cell(mutant),
md_cell(&location),
md_cell(&record.runtime.runtime_outcome),
md_cell(static_class),
md_cell(&record.reason)
));
}
}
out.push_str("\n### Static gaps without runtime signals\n\n");
if report.static_only_findings.is_empty() {
out.push_str("No static gap seams lacked a runtime gap signal in this import.\n");
} else {
out.push_str("| Seam | Class | Location | Reason |\n");
out.push_str("| --- | --- | --- | --- |\n");
for record in &report.static_only_findings {
let location = format!("{}:{}", record.seam.file, record.seam.line);
out.push_str(&format!(
"| `{}` | `{}` | {} | {} |\n",
md_cell(&record.seam.seam_id),
md_cell(&record.seam.seam_grip_class),
md_cell(&location),
md_cell(&record.reason)
));
}
}
out.push_str("\n## Runtime Outcome Counts\n\n");
out.push_str("| Runtime outcome | Count |\n| --- | ---: |\n");
let counts = runtime_outcome_counts(report);
if counts.is_empty() {
out.push_str("| none | 0 |\n");
} else {
for (outcome, count) in counts {
out.push_str(&format!("| {} | {} |\n", md_cell(&outcome), count));
}
}
out.push_str("\n## Matched Mutants\n\n");
if report.matched.is_empty() {
out.push_str("No runtime mutants matched static seams.\n");
} else {
out.push_str("| Seam | Class | Oracle | Mutation operator | Runtime outcome | Join |\n");
out.push_str("| --- | --- | --- | --- | --- | --- |\n");
for record in &report.matched {
out.push_str(&format!(
"| `{}` | `{}` | `{}`/`{}` | {} | {} | `{}` |\n",
md_cell(&record.seam.seam_id),
md_cell(&record.seam.seam_grip_class),
md_cell(&record.seam.oracle_kind),
md_cell(&record.seam.oracle_strength),
md_cell(&record.mutation.mutation_operator),
md_cell(&record.mutation.runtime_outcome),
record.join_method
));
}
}
out.push_str("\n## Ambiguous File/Line Matches\n\n");
if report.ambiguous_file_line.is_empty() {
out.push_str(
"No runtime mutants matched multiple static seams at the same file and line.\n",
);
} else {
out.push_str("| Runtime mutant | Location | Runtime outcome | Candidate seams |\n");
out.push_str("| --- | --- | --- | --- |\n");
for record in &report.ambiguous_file_line {
let mutant = record.mutation.mutant_id.as_deref().unwrap_or("unknown");
let location = mutation_location_label(&record.mutation);
let candidates = record
.candidates
.iter()
.map(|candidate| format!("`{}`", candidate.seam_id))
.collect::<Vec<_>>()
.join(", ");
out.push_str(&format!(
"| `{}` | {} | {} | {} |\n",
md_cell(mutant),
md_cell(&location),
md_cell(&record.mutation.runtime_outcome),
md_cell(&candidates)
));
}
}
out.push_str("\n## Unmatched Runtime Mutants\n\n");
if report.unmatched_mutants.is_empty() {
out.push_str("All imported runtime mutants matched a static seam.\n");
} else {
out.push_str("| Location | Mutation operator | Runtime outcome | Test command |\n");
out.push_str("| --- | --- | --- | --- |\n");
for record in &report.unmatched_mutants {
let location = mutation_location_label(record);
let command = record.test_command.as_deref().unwrap_or("unknown");
out.push_str(&format!(
"| {} | {} | {} | {} |\n",
md_cell(&location),
md_cell(&record.mutation_operator),
md_cell(&record.runtime_outcome),
md_cell(command)
));
}
}
out.push_str("\n## Static Seams Without Runtime Data\n\n");
if report.static_without_runtime.is_empty() {
out.push_str(
"Every static seam matched at least one runtime mutant in the imported data.\n",
);
} else {
out.push_str(
"Sample only; see JSON `static_without_runtime_total` for the full count.\n\n",
);
out.push_str("| Seam | Kind | Class | Location |\n");
out.push_str("| --- | --- | --- | --- |\n");
for seam in report
.static_without_runtime
.iter()
.take(STATIC_WITHOUT_RUNTIME_SAMPLE_LIMIT)
{
out.push_str(&format!(
"| `{}` | `{}` | `{}` | {}:{} |\n",
md_cell(&seam.seam_id),
md_cell(&seam.seam_kind),
md_cell(&seam.seam_grip_class),
md_cell(&seam.file),
seam.line
));
}
}
out
}
fn parse_repo_exposure_static_seams(json: &str) -> Result<Vec<StaticSeamRecord>, String> {
let value: Value = serde_json::from_str(json)
.map_err(|err| format!("failed to parse repo exposure JSON: {err}"))?;
let seams = value
.get("seams")
.and_then(Value::as_array)
.ok_or_else(|| "repo exposure JSON is missing `seams` array".to_string())?;
let mut records = Vec::new();
for seam in seams {
let seam_id = required_json_string(seam, "seam_id")?;
let seam_kind = required_json_string(seam, "kind")?;
let file = normalize_report_path(&required_json_string(seam, "file")?);
let line = required_json_usize(seam, "line")?;
let seam_grip_class = required_json_string(seam, "grip_class")?;
let (oracle_kind, oracle_strength) = strongest_related_oracle(seam);
records.push(StaticSeamRecord {
seam_id,
seam_kind,
file,
line,
seam_grip_class,
oracle_kind,
oracle_strength,
observed_values: string_array_field(seam, "observed_values"),
missing_discriminators: missing_discriminator_strings(seam),
});
}
Ok(records)
}
fn parse_mutation_outcomes_json(json: &str) -> Result<Vec<MutationOutcomeRecord>, String> {
let value: Value = serde_json::from_str(json)
.map_err(|err| format!("failed to parse cargo-mutants JSON: {err}"))?;
let mut records = Vec::new();
collect_mutation_outcome_records(&value, &mut records);
let mut records = merge_mutation_outcome_records(records);
records.sort_by(|left, right| {
left.seam_id
.cmp(&right.seam_id)
.then(left.file.cmp(&right.file))
.then(left.line.cmp(&right.line))
.then(left.mutation_operator.cmp(&right.mutation_operator))
.then(left.runtime_outcome.cmp(&right.runtime_outcome))
});
Ok(records)
}
fn collect_mutation_outcome_records(value: &Value, records: &mut Vec<MutationOutcomeRecord>) {
match value {
Value::Array(items) => {
for item in items {
collect_mutation_outcome_records(item, records);
}
}
Value::Object(object) => {
for key in [
"outcomes",
"mutants",
"results",
"mutations",
"mutation_results",
] {
if let Some(items) = object.get(key).and_then(Value::as_array) {
for item in items {
collect_mutation_outcome_records(item, records);
}
}
}
if let Some(record) = mutation_outcome_record_from_object(object) {
records.push(record);
}
}
_ => {}
}
}
fn mutation_outcome_record_from_object(
object: &serde_json::Map<String, Value>,
) -> Option<MutationOutcomeRecord> {
let mutant = nested_object(object, "mutant");
let mutation = nested_object(object, "mutation");
let location = nested_object(object, "location");
let span = nested_object(object, "span")
.or_else(|| mutant.and_then(|nested| nested_object(nested, "span")))
.or_else(|| mutation.and_then(|nested| nested_object(nested, "span")))
.or_else(|| location.and_then(|nested| nested_object(nested, "span")));
let mutant_id = string_field_any(object, &["id", "mutant_id", "mutantId"]).or_else(|| {
mutant.and_then(|nested| string_field_any(nested, &["id", "mutant_id", "mutantId"]))
});
let seam_id = string_field_any(object, &["seam_id", "seamId", "probe_id", "probeId"])
.or_else(|| {
mutant.and_then(|nested| {
string_field_any(nested, &["seam_id", "seamId", "probe_id", "probeId"])
})
})
.or_else(|| {
mutation.and_then(|nested| {
string_field_any(nested, &["seam_id", "seamId", "probe_id", "probeId"])
})
});
let file = string_field_any(
object,
&["file", "path", "source_file", "src_file", "filename"],
)
.or_else(|| {
mutant.and_then(|nested| {
string_field_any(
nested,
&["file", "path", "source_file", "src_file", "filename"],
)
})
})
.or_else(|| {
mutation.and_then(|nested| {
string_field_any(
nested,
&["file", "path", "source_file", "src_file", "filename"],
)
})
})
.or_else(|| {
location.and_then(|nested| {
string_field_any(
nested,
&[
"file",
"path",
"source_file",
"src_file",
"filename",
"file_name",
],
)
})
})
.or_else(|| {
span.and_then(|nested| {
string_field_any(
nested,
&[
"file",
"path",
"source_file",
"src_file",
"filename",
"file_name",
],
)
})
})
.map(|path| normalize_report_path(&path));
let line = usize_field_any(object, &["line", "line_start", "start_line", "startLine"])
.or_else(|| {
mutant.and_then(|nested| {
usize_field_any(nested, &["line", "line_start", "start_line", "startLine"])
})
})
.or_else(|| {
mutation.and_then(|nested| {
usize_field_any(nested, &["line", "line_start", "start_line", "startLine"])
})
})
.or_else(|| {
location.and_then(|nested| {
usize_field_any(nested, &["line", "line_start", "start_line", "startLine"])
})
})
.or_else(|| span.and_then(span_start_line));
let mutation_operator = string_field_any(
object,
&[
"operator",
"mutation_operator",
"mutator",
"mutation",
"description",
"replacement",
"name",
],
)
.or_else(|| {
mutant.and_then(|nested| {
string_field_any(
nested,
&[
"operator",
"mutation_operator",
"mutator",
"mutation",
"description",
"replacement",
"name",
],
)
})
})
.or_else(|| {
mutation.and_then(|nested| {
string_field_any(
nested,
&[
"operator",
"mutation_operator",
"mutator",
"mutation",
"description",
"replacement",
"name",
],
)
})
})
.unwrap_or_else(|| "unknown".to_string());
let runtime_outcome =
string_field_any(object, &["outcome", "status", "result", "summary", "state"])
.unwrap_or_else(|| "unknown".to_string());
let duration = string_field_any(
object,
&[
"duration_ms",
"durationMillis",
"duration",
"elapsed_ms",
"elapsed",
],
);
let test_command = string_field_any(
object,
&["test_command", "testCommand", "command", "cmd", "test_cmd"],
);
let has_identity = mutant_id.is_some() || seam_id.is_some() || file.is_some() || line.is_some();
let has_runtime_detail = runtime_outcome != "unknown"
|| mutation_operator != "unknown"
|| duration.is_some()
|| test_command.is_some();
if !has_identity || !has_runtime_detail {
return None;
}
Some(MutationOutcomeRecord {
mutant_id,
seam_id,
file,
line,
mutation_operator,
runtime_outcome,
duration,
test_command,
})
}
fn build_mutation_calibration_report(
static_seams: Vec<StaticSeamRecord>,
runtime_mutants: Vec<MutationOutcomeRecord>,
) -> MutationCalibrationReport {
let mut static_by_id: BTreeMap<String, usize> = BTreeMap::new();
let mut static_by_line: BTreeMap<(String, usize), Vec<usize>> = BTreeMap::new();
for (idx, seam) in static_seams.iter().enumerate() {
static_by_id.insert(seam.seam_id.clone(), idx);
static_by_line
.entry((normalize_report_path(&seam.file), seam.line))
.or_default()
.push(idx);
}
let mut matched_static_ids = BTreeSet::new();
let mut ambiguous_static_ids = BTreeSet::new();
let mut matched = Vec::new();
let mut ambiguous_file_line = Vec::new();
let mut unmatched_mutants = Vec::new();
for mutation in runtime_mutants {
let seam_match = mutation
.seam_id
.as_ref()
.and_then(|seam_id| static_by_id.get(seam_id).copied())
.map(|idx| ("seam_id", idx))
.or_else(|| {
let file = mutation.file.as_ref()?;
let line = mutation.line?;
let key = (normalize_report_path(file), line);
let candidates = static_by_line.get(&key)?;
(candidates.len() == 1).then_some(("file_line", candidates[0]))
});
match seam_match {
Some((join_method, idx)) => {
let seam = static_seams[idx].clone();
matched_static_ids.insert(seam.seam_id.clone());
matched.push(MutationCalibrationMatch {
join_method,
seam,
mutation,
});
}
None => {
let candidates = mutation
.file
.as_ref()
.and_then(|file| {
let line = mutation.line?;
let key = (normalize_report_path(file), line);
static_by_line.get(&key)
})
.filter(|candidates| candidates.len() > 1);
if let Some(candidates) = candidates {
let candidates = candidates
.iter()
.map(|idx| {
let seam = static_seams[*idx].clone();
ambiguous_static_ids.insert(seam.seam_id.clone());
seam
})
.collect::<Vec<_>>();
ambiguous_file_line.push(AmbiguousMutationCalibrationMatch {
mutation,
candidates,
});
} else {
unmatched_mutants.push(mutation);
}
}
}
}
let static_without_runtime = static_seams
.iter()
.filter(|seam| {
!matched_static_ids.contains(&seam.seam_id)
&& !ambiguous_static_ids.contains(&seam.seam_id)
})
.cloned()
.collect::<Vec<_>>();
let (agreement, precision_notes, missed_runtime_signals, static_only_findings) =
mutation_calibration_agreement(
&static_seams,
&matched,
&ambiguous_file_line,
&unmatched_mutants,
);
MutationCalibrationReport {
static_seams_total: static_seams.len(),
mutants_total: matched.len() + ambiguous_file_line.len() + unmatched_mutants.len(),
agreement,
precision_notes,
missed_runtime_signals,
static_only_findings,
matched,
ambiguous_file_line,
unmatched_mutants,
static_without_runtime,
}
}
fn mutation_calibration_agreement(
static_seams: &[StaticSeamRecord],
matched: &[MutationCalibrationMatch],
ambiguous_file_line: &[AmbiguousMutationCalibrationMatch],
unmatched_mutants: &[MutationOutcomeRecord],
) -> (
MutationCalibrationAgreement,
Vec<String>,
Vec<MutationCalibrationRuntimeSignal>,
Vec<MutationCalibrationStaticOnlyFinding>,
) {
let mut matches_by_seam: BTreeMap<&str, Vec<&MutationCalibrationMatch>> = BTreeMap::new();
for record in matched {
matches_by_seam
.entry(record.seam.seam_id.as_str())
.or_default()
.push(record);
}
let mut agreement = MutationCalibrationAgreement::default();
let mut missed_runtime_signals = Vec::new();
let mut static_only_findings = Vec::new();
for seam in static_seams {
let records = matches_by_seam
.get(seam.seam_id.as_str())
.map(Vec::as_slice)
.unwrap_or(&[]);
let has_runtime_gap = records
.iter()
.any(|record| runtime_gap_signal(&record.mutation.runtime_outcome));
let has_runtime_clean = records
.iter()
.any(|record| runtime_clean_signal(&record.mutation.runtime_outcome));
let has_runtime_inconclusive = records.iter().any(|record| {
!runtime_gap_signal(&record.mutation.runtime_outcome)
&& !runtime_clean_signal(&record.mutation.runtime_outcome)
});
let has_static_gap = static_gap_signal(seam);
match (has_static_gap, has_runtime_gap, has_runtime_clean) {
(true, true, _) => agreement.static_gap_and_runtime_signal += 1,
(true, false, _) => {
agreement.static_gap_without_runtime_signal += 1;
static_only_findings.push(MutationCalibrationStaticOnlyFinding {
seam: seam.clone(),
reason: static_only_reason(records),
});
}
(false, true, _) => {
agreement.runtime_signal_without_static_gap += 1;
for record in records
.iter()
.filter(|record| runtime_gap_signal(&record.mutation.runtime_outcome))
{
missed_runtime_signals.push(MutationCalibrationRuntimeSignal {
runtime: record.mutation.clone(),
static_seam: Some(seam.clone()),
reason: "runtime gap signal joined to a static-clean seam".to_string(),
});
}
}
(false, false, true) => agreement.static_clean_and_runtime_clean += 1,
(false, false, false) => {}
}
if has_runtime_inconclusive {
agreement.runtime_inconclusive += 1;
}
}
for record in unmatched_mutants
.iter()
.filter(|record| runtime_gap_signal(&record.runtime_outcome))
{
agreement.runtime_signal_without_static_gap += 1;
missed_runtime_signals.push(MutationCalibrationRuntimeSignal {
runtime: record.clone(),
static_seam: None,
reason: "runtime gap signal did not join to a static seam".to_string(),
});
}
for record in ambiguous_file_line {
if runtime_gap_signal(&record.mutation.runtime_outcome) {
agreement.runtime_inconclusive += 1;
}
}
missed_runtime_signals.truncate(AGREEMENT_SAMPLE_LIMIT);
static_only_findings.truncate(AGREEMENT_SAMPLE_LIMIT);
(
agreement,
mutation_calibration_precision_notes(),
missed_runtime_signals,
static_only_findings,
)
}
fn mutation_calibration_precision_notes() -> Vec<String> {
vec![
"runtime gap signals are imported runtime labels such as missed, survived, not_caught, or uncaught".to_string(),
"runtime clean signals are imported runtime labels such as caught or timeout".to_string(),
"static_gap_without_runtime_signal includes static gap seams with no matched runtime gap signal in this import".to_string(),
"ambiguous file/line runtime gap signals are counted as runtime_inconclusive until a seam_id or unambiguous location is available".to_string(),
]
}
fn static_only_reason(records: &[&MutationCalibrationMatch]) -> String {
if records.is_empty() {
"static gap seam has no matched runtime record in this import".to_string()
} else if records
.iter()
.any(|record| runtime_clean_signal(&record.mutation.runtime_outcome))
{
"static gap seam matched runtime data without a runtime gap signal".to_string()
} else {
"static gap seam matched only runtime-inconclusive labels".to_string()
}
}
fn static_gap_signal(seam: &StaticSeamRecord) -> bool {
!matches!(
seam.seam_grip_class.as_str(),
"strongly_gripped" | "intentional" | "suppressed"
)
}
fn runtime_gap_signal(outcome: &str) -> bool {
matches!(
normalize_runtime_label(outcome).as_str(),
"missed" | "survived" | "survive" | "not_caught" | "uncaught"
)
}
fn runtime_clean_signal(outcome: &str) -> bool {
matches!(
normalize_runtime_label(outcome).as_str(),
"caught" | "timeout" | "timed_out" | "killed"
)
}
fn mutation_calibration_agreement_json(agreement: &MutationCalibrationAgreement) -> Value {
serde_json::json!({
"static_gap_and_runtime_signal": agreement.static_gap_and_runtime_signal,
"static_gap_without_runtime_signal": agreement.static_gap_without_runtime_signal,
"runtime_signal_without_static_gap": agreement.runtime_signal_without_static_gap,
"static_clean_and_runtime_clean": agreement.static_clean_and_runtime_clean,
"runtime_inconclusive": agreement.runtime_inconclusive,
})
}
fn mutation_calibration_runtime_signal_json(record: &MutationCalibrationRuntimeSignal) -> Value {
serde_json::json!({
"runtime": mutation_outcome_json(&record.runtime),
"static": record.static_seam.as_ref().map(static_seam_json),
"reason": record.reason.as_str(),
})
}
fn mutation_calibration_static_only_json(record: &MutationCalibrationStaticOnlyFinding) -> Value {
serde_json::json!({
"static": static_seam_json(&record.seam),
"reason": record.reason.as_str(),
})
}
fn mutation_calibration_match_json(record: &MutationCalibrationMatch) -> Value {
serde_json::json!({
"join_method": record.join_method,
"static": static_seam_json(&record.seam),
"runtime": mutation_outcome_json(&record.mutation),
})
}
fn ambiguous_mutation_calibration_match_json(record: &AmbiguousMutationCalibrationMatch) -> Value {
serde_json::json!({
"runtime": mutation_outcome_json(&record.mutation),
"candidates": record
.candidates
.iter()
.map(static_seam_json)
.collect::<Vec<_>>(),
})
}
fn static_seam_json(record: &StaticSeamRecord) -> Value {
serde_json::json!({
"seam_id": record.seam_id.as_str(),
"seam_kind": record.seam_kind.as_str(),
"file": record.file.as_str(),
"line": record.line,
"seam_grip_class": record.seam_grip_class.as_str(),
"oracle_kind": record.oracle_kind.as_str(),
"oracle_strength": record.oracle_strength.as_str(),
"observed_values": &record.observed_values,
"missing_discriminators": &record.missing_discriminators,
})
}
fn mutation_outcome_json(record: &MutationOutcomeRecord) -> Value {
serde_json::json!({
"mutant_id": record.mutant_id.as_deref(),
"seam_id": record.seam_id.as_deref(),
"file": record.file.as_deref(),
"line": record.line,
"mutation_operator": record.mutation_operator.as_str(),
"runtime_outcome": record.runtime_outcome.as_str(),
"duration": record.duration.as_deref(),
"test_command": record.test_command.as_deref(),
})
}
fn merge_mutation_outcome_records(
records: Vec<MutationOutcomeRecord>,
) -> Vec<MutationOutcomeRecord> {
let mut by_id: BTreeMap<String, MutationOutcomeRecord> = BTreeMap::new();
let mut without_id = Vec::new();
for record in records {
match record.mutant_id.clone() {
Some(id) => {
if let Some(existing) = by_id.get_mut(&id) {
merge_mutation_outcome_record(existing, record);
} else {
by_id.insert(id, record);
}
}
None => without_id.push(record),
}
}
by_id.into_values().chain(without_id).collect::<Vec<_>>()
}
fn merge_mutation_outcome_record(
target: &mut MutationOutcomeRecord,
source: MutationOutcomeRecord,
) {
if target.seam_id.is_none() {
target.seam_id = source.seam_id;
}
if target.file.is_none() {
target.file = source.file;
}
if target.line.is_none() {
target.line = source.line;
}
if target.mutation_operator == "unknown" && source.mutation_operator != "unknown" {
target.mutation_operator = source.mutation_operator;
}
if target.runtime_outcome == "unknown" && source.runtime_outcome != "unknown" {
target.runtime_outcome = source.runtime_outcome;
}
if target.duration.is_none() {
target.duration = source.duration;
}
if target.test_command.is_none() {
target.test_command = source.test_command;
}
}
fn runtime_outcome_counts(report: &MutationCalibrationReport) -> BTreeMap<String, usize> {
let mut counts = BTreeMap::new();
for record in report
.matched
.iter()
.map(|matched| &matched.mutation)
.chain(
report
.ambiguous_file_line
.iter()
.map(|ambiguous| &ambiguous.mutation),
)
.chain(report.unmatched_mutants.iter())
{
let key = normalize_runtime_label(&record.runtime_outcome);
*counts.entry(key).or_insert(0) += 1;
}
counts
}
fn join_method_counts(report: &MutationCalibrationReport) -> BTreeMap<&'static str, usize> {
let mut counts = BTreeMap::new();
for record in &report.matched {
*counts.entry(record.join_method).or_insert(0) += 1;
}
counts
}
fn normalize_runtime_label(value: &str) -> String {
value
.trim()
.to_ascii_lowercase()
.chars()
.map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' })
.collect::<String>()
.trim_matches('_')
.to_string()
}
fn mutation_location_label(record: &MutationOutcomeRecord) -> String {
if let Some(seam_id) = record.seam_id.as_ref() {
return format!("seam:{seam_id}");
}
match (&record.file, record.line) {
(Some(file), Some(line)) => format!("{file}:{line}"),
(Some(file), None) => file.clone(),
(None, Some(line)) => format!("line {line}"),
(None, None) => "unknown".to_string(),
}
}
fn required_json_string(value: &Value, key: &str) -> Result<String, String> {
value
.get(key)
.and_then(json_scalar_as_string)
.ok_or_else(|| format!("repo exposure seam is missing string field `{key}`"))
}
fn required_json_usize(value: &Value, key: &str) -> Result<usize, String> {
value
.get(key)
.and_then(json_scalar_as_usize)
.ok_or_else(|| format!("repo exposure seam is missing numeric field `{key}`"))
}
fn strongest_related_oracle(seam: &Value) -> (String, String) {
let mut best_kind = "unknown".to_string();
let mut best_strength = "unknown".to_string();
let mut best_rank = 0;
if let Some(related) = seam.get("related_tests").and_then(Value::as_array) {
for test in related {
let strength = test
.get("oracle_strength")
.and_then(Value::as_str)
.unwrap_or("unknown");
let rank = oracle_strength_rank(strength);
if rank > best_rank {
best_rank = rank;
best_strength = strength.to_string();
best_kind = test
.get("oracle_kind")
.and_then(Value::as_str)
.unwrap_or("unknown")
.to_string();
}
}
}
(best_kind, best_strength)
}
fn oracle_strength_rank(strength: &str) -> u8 {
match strength {
"strong" => 5,
"medium" => 4,
"weak" => 3,
"smoke" => 2,
"none" => 1,
_ => 0,
}
}
fn string_array_field(value: &Value, key: &str) -> Vec<String> {
value
.get(key)
.and_then(Value::as_array)
.map(|items| {
items
.iter()
.filter_map(json_scalar_as_string)
.collect::<Vec<_>>()
})
.unwrap_or_default()
}
fn missing_discriminator_strings(seam: &Value) -> Vec<String> {
seam.get("missing_discriminators")
.and_then(Value::as_array)
.map(|items| {
items
.iter()
.filter_map(|item| {
if let Some(value) = json_scalar_as_string(item) {
return Some(value);
}
let value = item.get("value").and_then(json_scalar_as_string)?;
match item.get("reason").and_then(json_scalar_as_string) {
Some(reason) if !reason.is_empty() => Some(format!("{value} ({reason})")),
_ => Some(value),
}
})
.collect::<Vec<_>>()
})
.unwrap_or_default()
}
fn nested_object<'a>(
object: &'a serde_json::Map<String, Value>,
key: &str,
) -> Option<&'a serde_json::Map<String, Value>> {
object.get(key).and_then(Value::as_object)
}
fn span_start_line(span: &serde_json::Map<String, Value>) -> Option<usize> {
usize_field_any(span, &["line", "line_start", "start_line", "startLine"])
.or_else(|| {
nested_object(span, "start").and_then(|start| {
usize_field_any(start, &["line", "line_start", "start_line", "startLine"])
})
})
.or_else(|| {
nested_object(span, "start_position").and_then(|start| {
usize_field_any(start, &["line", "line_start", "start_line", "startLine"])
})
})
.or_else(|| {
nested_object(span, "lo").and_then(|start| {
usize_field_any(start, &["line", "line_start", "start_line", "startLine"])
})
})
}
fn string_field_any(object: &serde_json::Map<String, Value>, keys: &[&str]) -> Option<String> {
keys.iter()
.find_map(|key| object.get(*key).and_then(json_scalar_as_string))
.filter(|value| !value.trim().is_empty())
}
fn usize_field_any(object: &serde_json::Map<String, Value>, keys: &[&str]) -> Option<usize> {
keys.iter()
.find_map(|key| object.get(*key).and_then(json_scalar_as_usize))
}
fn json_scalar_as_string(value: &Value) -> Option<String> {
match value {
Value::String(text) => Some(text.clone()),
Value::Number(number) => Some(number.to_string()),
Value::Bool(flag) => Some(flag.to_string()),
_ => None,
}
}
fn json_scalar_as_usize(value: &Value) -> Option<usize> {
match value {
Value::Number(number) => number
.as_u64()
.and_then(|value| usize::try_from(value).ok()),
Value::String(text) => text.trim().parse::<usize>().ok(),
_ => None,
}
}
fn normalize_report_path(path: &str) -> String {
let normalized = path.replace('\\', "/");
normalized
.strip_prefix("./")
.unwrap_or(normalized.as_str())
.to_string()
}
fn md_cell(value: &str) -> String {
value.replace('|', "\\|").replace(['\r', '\n'], " ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mutation_calibration_summarizes_static_runtime_agreement() -> Result<(), String> {
let static_seams = vec![
static_seam("gap-runtime", "weakly_gripped", "src/pricing.rs", 10),
static_seam("gap-clean", "weakly_gripped", "src/pricing.rs", 20),
static_seam("clean-clean", "strongly_gripped", "src/pricing.rs", 30),
static_seam("clean-gap", "strongly_gripped", "src/pricing.rs", 40),
static_seam("gap-none", "ungripped", "src/pricing.rs", 50),
];
let runtime_mutants = vec![
runtime("m1", Some("gap-runtime"), None, None, "missed"),
runtime("m2", Some("gap-clean"), None, None, "caught"),
runtime("m3", Some("clean-clean"), None, None, "caught"),
runtime("m4", Some("clean-gap"), None, None, "missed"),
runtime("m5", None, Some("src/other.rs"), Some(99), "missed"),
];
let report = build_mutation_calibration_report(static_seams, runtime_mutants);
assert_eq!(report.agreement.static_gap_and_runtime_signal, 1);
assert_eq!(report.agreement.static_gap_without_runtime_signal, 2);
assert_eq!(report.agreement.static_clean_and_runtime_clean, 1);
assert_eq!(report.agreement.runtime_signal_without_static_gap, 2);
assert_eq!(report.missed_runtime_signals.len(), 2);
assert_eq!(report.static_only_findings.len(), 2);
let json = render_mutation_calibration_json(&report)?;
assert!(json.contains(r#""schema_version": "0.1""#));
assert!(json.contains(r#""static_gap_and_runtime_signal": 1"#));
let markdown = render_mutation_calibration_md(&report);
assert!(markdown.contains("# ripr mutation calibration report"));
assert!(markdown.contains("| static_gap_and_runtime_signal | 1 |"));
assert!(markdown.contains("Runtime signals without static gaps"));
assert!(markdown.contains("Static gaps without runtime signals"));
Ok(())
}
#[test]
fn mutation_calibration_joins_by_seam_id_then_file_line_and_keeps_ambiguous() {
let static_seams = vec![
static_seam("id-match", "weakly_gripped", "src/pricing.rs", 10),
static_seam("line-a", "weakly_gripped", "src/pricing.rs", 20),
static_seam("line-b", "ungripped", "src/pricing.rs", 30),
static_seam("ambiguous-a", "ungripped", "src/ambiguous.rs", 40),
static_seam("ambiguous-b", "ungripped", "src/ambiguous.rs", 40),
];
let runtime_mutants = vec![
runtime("m-id", Some("id-match"), None, None, "missed"),
runtime("m-line", None, Some("src/pricing.rs"), Some(20), "missed"),
runtime(
"m-ambiguous",
None,
Some("src/ambiguous.rs"),
Some(40),
"missed",
),
];
let report = build_mutation_calibration_report(static_seams, runtime_mutants);
assert_eq!(report.matched.len(), 2);
assert_eq!(report.matched[0].join_method, "seam_id");
assert_eq!(report.matched[1].join_method, "file_line");
assert_eq!(report.ambiguous_file_line.len(), 1);
assert_eq!(report.ambiguous_file_line[0].candidates.len(), 2);
assert!(report.unmatched_mutants.is_empty());
}
#[test]
fn mutation_calibration_parses_repo_exposure_and_cargo_mutants_json() -> Result<(), String> {
let repo = r#"{
"schema_version": "0.2",
"scope": "repo",
"seams": [
{
"seam_id": "seam-a",
"kind": "predicate_boundary",
"file": ".\\src\\pricing.rs",
"line": "42",
"grip_class": "weakly_gripped",
"related_tests": [
{"oracle_kind": "exact_value", "oracle_strength": "weak"}
],
"observed_values": [50, true],
"missing_discriminators": [
{"value": "threshold equality", "reason": "not observed"}
]
}
]
}"#;
let mutants = r#"{
"outcomes": [
{
"id": "m1",
"mutant": {
"seam_id": "seam-a",
"operator": "replace >= with >"
},
"outcome": "missed"
}
]
}"#;
let report = mutation_calibration_report_from_json(repo, mutants)?;
assert_eq!(report.static_seams_total, 1);
assert_eq!(report.mutants_total, 1);
assert_eq!(report.matched.len(), 1);
assert_eq!(report.matched[0].seam.file, "src/pricing.rs");
assert_eq!(
report.matched[0].mutation.mutation_operator,
"replace >= with >"
);
Ok(())
}
#[test]
fn mutation_calibration_parses_nested_runtime_locations_and_aliases() -> Result<(), String> {
let repo = r#"{
"schema_version": "0.2",
"scope": "repo",
"seams": [
{
"seam_id": "seam-nested",
"kind": "predicate_boundary",
"file": "src/pricing.rs",
"line": 42,
"grip_class": "weakly_gripped",
"related_tests": [
{"oracle_kind": "smoke", "oracle_strength": "smoke"}
],
"observed_values": [],
"missing_discriminators": [
"scalar discriminator",
{"value": "boundary value"}
]
},
{
"seam_id": "seam-location",
"kind": "predicate_boundary",
"file": "src/location.rs",
"line": 17,
"grip_class": "weakly_gripped",
"related_tests": [
{"oracle_kind": "none", "oracle_strength": "none"}
],
"observed_values": [],
"missing_discriminators": []
},
{
"seam_id": "seam-span",
"kind": "predicate_boundary",
"file": "src/span.rs",
"line": 18,
"grip_class": "weakly_gripped",
"related_tests": [
{"oracle_kind": "unknown", "oracle_strength": "custom"}
],
"observed_values": [],
"missing_discriminators": []
}
]
}"#;
let mutants = r#"[
true,
{
"mutation": {
"seamId": "seam-nested",
"path": "./src/pricing.rs",
"startLine": "42",
"replacement": "replace >= with >"
},
"status": "missed"
},
{
"location": {
"file_name": "src/location.rs",
"line_start": "17"
},
"mutator": "replace location",
"result": "caught"
},
{
"span": {
"file_name": "src/span.rs",
"start": {
"line": "18"
}
},
"operator": "replace span",
"state": "not caught"
}
]"#;
let report = mutation_calibration_report_from_json(repo, mutants)?;
assert_eq!(report.mutants_total, 3);
assert_eq!(report.matched.len(), 3);
assert!(
report
.matched
.iter()
.any(|record| record.join_method == "file_line"
&& record.seam.seam_id == "seam-location")
);
assert!(
report
.matched
.iter()
.any(|record| record.mutation.mutation_operator == "replace >= with >")
);
assert!(
report
.matched
.iter()
.any(|record| record.mutation.runtime_outcome == "not caught")
);
let json = render_mutation_calibration_json(&report)?;
assert!(json.contains("scalar discriminator"));
assert!(json.contains("boundary value"));
assert!(json.contains("not_caught"));
Ok(())
}
#[test]
fn mutation_calibration_renders_empty_ambiguous_unmatched_and_inconclusive()
-> Result<(), String> {
let empty_report = build_mutation_calibration_report(Vec::new(), Vec::new());
let empty_markdown = render_mutation_calibration_md(&empty_report);
assert!(empty_markdown.contains("| none | 0 |"));
assert!(empty_markdown.contains("No runtime mutants matched static seams."));
let static_seams = vec![
static_seam("ambiguous-a", "ungripped", "src/ambiguous.rs", 40),
static_seam("ambiguous-b", "ungripped", "src/ambiguous.rs", 40),
static_seam("inconclusive", "weakly_gripped", "src/inconclusive.rs", 50),
];
let runtime_mutants = vec![
runtime(
"m-ambiguous",
None,
Some("src/ambiguous.rs"),
Some(40),
"missed",
),
runtime(
"m-inconclusive",
Some("inconclusive"),
None,
None,
"skipped",
),
MutationOutcomeRecord {
mutant_id: Some("m-line-only".to_string()),
seam_id: None,
file: None,
line: Some(77),
mutation_operator: "replace line".to_string(),
runtime_outcome: "missed".to_string(),
duration: None,
test_command: Some("cargo test targeted".to_string()),
},
MutationOutcomeRecord {
mutant_id: Some("m-unknown".to_string()),
seam_id: None,
file: None,
line: None,
mutation_operator: "replace unknown".to_string(),
runtime_outcome: "missed".to_string(),
duration: None,
test_command: None,
},
];
let report = build_mutation_calibration_report(static_seams, runtime_mutants);
assert_eq!(report.matched.len(), 1);
assert_eq!(report.ambiguous_file_line.len(), 1);
assert_eq!(report.unmatched_mutants.len(), 2);
assert_eq!(report.agreement.runtime_inconclusive, 2);
assert!(
report
.static_only_findings
.iter()
.any(|record| record.reason
== "static gap seam matched only runtime-inconclusive labels")
);
let markdown = render_mutation_calibration_md(&report);
assert!(markdown.contains("| `m-ambiguous` | src/ambiguous.rs:40 | missed |"));
assert!(markdown.contains("| line 77 | replace line | missed | cargo test targeted |"));
assert!(markdown.contains("| unknown | replace unknown | missed | unknown |"));
let json = render_mutation_calibration_json(&report)?;
let value: Value = serde_json::from_str(&json)
.map_err(|err| format!("mutation calibration JSON should parse: {err}"))?;
assert_eq!(
value["ambiguous_file_line_matches"]
.as_array()
.map(Vec::len),
Some(1)
);
assert_eq!(value["unmatched_mutants"].as_array().map(Vec::len), Some(2));
Ok(())
}
#[test]
fn mutation_calibration_merges_mutants_and_outcomes_by_id() -> Result<(), String> {
let repo = repo_json_for("seam-a", "weakly_gripped", "src/pricing.rs", 42);
let mutants = r#"[
{"mutants": [{"id": "m1", "file": "src/pricing.rs", "line": 42, "operator": "replace"}]},
{"outcomes": [{"id": "m1", "outcome": "caught", "duration_ms": 10}]}
]"#;
let report = mutation_calibration_report_from_json(&repo, mutants)?;
assert_eq!(report.matched.len(), 1);
assert_eq!(report.matched[0].mutation.runtime_outcome, "caught");
assert_eq!(report.matched[0].mutation.duration.as_deref(), Some("10"));
Ok(())
}
#[test]
fn mutation_calibration_reports_are_advisory_and_structured() -> Result<(), String> {
let report = mutation_calibration_report_from_json(
&repo_json_for("seam-a", "weakly_gripped", "src/pricing.rs", 42),
r#"[{"id":"m1","seam_id":"seam-a","outcome":"missed","operator":"replace"}]"#,
)?;
let json = render_mutation_calibration_json(&report)?;
let value: Value = serde_json::from_str(&json)
.map_err(|err| format!("mutation calibration JSON should parse: {err}"))?;
assert_eq!(value["status"], "advisory");
assert_eq!(value["metrics"]["matched_total"], 1);
assert_eq!(
value["agreement"]["static_gap_and_runtime_signal"],
Value::from(1)
);
let markdown = render_mutation_calibration_md(&report);
assert!(markdown.contains("Status: advisory"));
assert!(markdown.contains("Runtime Outcome Counts"));
Ok(())
}
fn repo_json_for(id: &str, grip_class: &str, file: &str, line: usize) -> String {
format!(
r#"{{
"schema_version": "0.2",
"scope": "repo",
"seams": [
{{
"seam_id": "{id}",
"kind": "predicate_boundary",
"file": "{file}",
"line": {line},
"grip_class": "{grip_class}",
"related_tests": [],
"observed_values": [],
"missing_discriminators": []
}}
]
}}"#
)
}
fn static_seam(id: &str, grip_class: &str, file: &str, line: usize) -> StaticSeamRecord {
StaticSeamRecord {
seam_id: id.to_string(),
seam_kind: "predicate_boundary".to_string(),
file: file.to_string(),
line,
seam_grip_class: grip_class.to_string(),
oracle_kind: "exact_value".to_string(),
oracle_strength: "unknown".to_string(),
observed_values: Vec::new(),
missing_discriminators: Vec::new(),
}
}
fn runtime(
id: &str,
seam_id: Option<&str>,
file: Option<&str>,
line: Option<usize>,
outcome: &str,
) -> MutationOutcomeRecord {
MutationOutcomeRecord {
mutant_id: Some(id.to_string()),
seam_id: seam_id.map(str::to_string),
file: file.map(str::to_string),
line,
mutation_operator: "replace".to_string(),
runtime_outcome: outcome.to_string(),
duration: None,
test_command: None,
}
}
}