use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
const SCHEMA_VERSION: &str = "0.1";
const REPORT_KIND: &str = "gap_decision_ledger";
pub(crate) const DEFAULT_GAP_DECISION_LEDGER_OUT: &str =
"target/ripr/reports/gap-decision-ledger.json";
pub(crate) const DEFAULT_GAP_DECISION_LEDGER_MD_OUT: &str =
"target/ripr/reports/gap-decision-ledger.md";
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct GapDecisionLedgerInput {
pub(crate) root: String,
pub(crate) generated_at: String,
pub(crate) source_kind: GapDecisionLedgerSourceKind,
pub(crate) records_path: String,
pub(crate) records_json: Result<String, String>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum GapDecisionLedgerSourceKind {
Records,
RepoExposure,
CheckOutput,
}
impl GapDecisionLedgerSourceKind {
fn as_str(self) -> &'static str {
match self {
Self::Records => "records",
Self::RepoExposure => "repo_exposure",
Self::CheckOutput => "check_output",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct GapDecisionLedgerReport {
status: String,
root: String,
generated_at: String,
inputs: GapDecisionLedgerInputs,
summary: GapDecisionLedgerSummary,
records: Vec<GapRecord>,
warnings: Vec<String>,
limits: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
struct GapDecisionLedgerInputs {
source_kind: &'static str,
records: String,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
struct GapDecisionLedgerSummary {
records_total: usize,
repairable_total: usize,
static_limitation_total: usize,
no_action_total: usize,
missing_artifact_total: usize,
projection_pr_comment_eligible: usize,
projection_gate_candidate: usize,
projection_agent_packet_eligible: usize,
ripr_zero_target_count: usize,
ripr_plus_target_count: usize,
preview_ineligible_total: usize,
receipt_improved_total: usize,
receipt_unchanged_after_attempt_total: usize,
missing_output_contract_total: usize,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub(crate) struct GapRecord {
#[serde(default)]
pub(crate) gap_id: String,
#[serde(default)]
pub(crate) canonical_gap_id: String,
#[serde(default)]
pub(crate) kind: String,
#[serde(default)]
pub(crate) language: String,
#[serde(default)]
pub(crate) language_status: String,
#[serde(default)]
pub(crate) scope: String,
#[serde(default)]
pub(crate) evidence_class: String,
#[serde(default)]
pub(crate) gap_state: String,
#[serde(default)]
pub(crate) policy_state: String,
#[serde(default)]
pub(crate) repairability: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) repair_route: Option<GapRepairRoute>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) static_limit_kind: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) static_limit_detail: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) static_limits: Vec<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) anchor: Option<GapAnchor>,
#[serde(default)]
pub(crate) evidence_ids: Vec<String>,
#[serde(default)]
pub(crate) projection_eligibility: BTreeMap<String, ProjectionEligibility>,
#[serde(default)]
pub(crate) verification_commands: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) receipt_command: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) regeneration_commands: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) receipt: Option<GapReceipt>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) safe_gate_predicate: Option<SafeGatePredicate>,
#[serde(default)]
pub(crate) authority_boundary: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub(crate) struct GapRepairRoute {
#[serde(default)]
pub(crate) route_kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) target_file: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) target_line: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) related_test: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) assertion_shape: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) changed_behavior: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) stop_conditions: Vec<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub(crate) struct GapAnchor {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) file: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) line: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) owner: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) dedupe_fingerprint: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub(crate) struct ProjectionEligibility {
#[serde(default)]
pub(crate) eligible: bool,
#[serde(default)]
pub(crate) reason: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub(crate) struct GapReceipt {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) state: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) movement: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) path: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub(crate) struct SafeGatePredicate {
#[serde(default)]
pub(crate) policy_target_enabled: bool,
#[serde(default)]
pub(crate) suppressed: bool,
#[serde(default)]
pub(crate) waived: bool,
#[serde(default)]
pub(crate) acknowledged_only: bool,
#[serde(default)]
pub(crate) baseline_known: bool,
#[serde(default)]
pub(crate) preview_language: bool,
#[serde(default)]
pub(crate) static_unknown_only: bool,
}
pub(crate) fn build_gap_decision_ledger_report(
input: GapDecisionLedgerInput,
) -> GapDecisionLedgerReport {
let mut warnings = Vec::new();
let records = match input.records_json {
Ok(contents) => match parse_gap_decision_source(input.source_kind, &contents) {
Ok(records) => records,
Err(err) => {
warnings.push(format!("parse {} failed: {err}", input.records_path));
Vec::new()
}
},
Err(err) => {
warnings.push(err);
Vec::new()
}
};
for record in &records {
validate_record(record, &mut warnings);
}
let summary = summarize_records(&records);
let status = if records.is_empty() {
"blocked"
} else if warnings.is_empty() {
"advisory"
} else {
"advisory_with_warnings"
}
.to_string();
GapDecisionLedgerReport {
status,
root: input.root,
generated_at: input.generated_at,
inputs: GapDecisionLedgerInputs {
source_kind: input.source_kind.as_str(),
records: input.records_path,
},
summary,
records,
warnings,
limits: vec![
"Advisory static gap decisions only.".to_string(),
"Gate-decision artifacts remain the only configured pass/fail authority.".to_string(),
"This report does not rerun analysis, execute mutation tests, edit source, generate tests, call providers, publish comments, or change default CI blocking.".to_string(),
],
}
}
pub(crate) fn render_gap_decision_ledger_json(
report: &GapDecisionLedgerReport,
) -> Result<String, String> {
#[derive(Serialize)]
struct JsonReport<'a> {
schema_version: &'static str,
tool: &'static str,
kind: &'static str,
status: &'a str,
root: &'a str,
generated_at: &'a str,
inputs: &'a GapDecisionLedgerInputs,
summary: &'a GapDecisionLedgerSummary,
records: &'a [GapRecord],
warnings: &'a [String],
limits: &'a [String],
}
serde_json::to_string_pretty(&JsonReport {
schema_version: SCHEMA_VERSION,
tool: "ripr",
kind: REPORT_KIND,
status: &report.status,
root: &report.root,
generated_at: &report.generated_at,
inputs: &report.inputs,
summary: &report.summary,
records: &report.records,
warnings: &report.warnings,
limits: &report.limits,
})
.map_err(|err| format!("serialize gap decision ledger JSON failed: {err}"))
}
pub(crate) fn render_gap_decision_ledger_markdown(report: &GapDecisionLedgerReport) -> String {
let mut out = String::new();
out.push_str("# RIPR Gap Decision Ledger\n\n");
out.push_str(&format!("Status: `{}`\n\n", md_inline(&report.status)));
out.push_str(&format!("Root: `{}`\n\n", md_inline(&report.root)));
out.push_str("Authority: gate-decision artifacts own pass/fail authority. This report is advisory projection input.\n\n");
out.push_str("## Summary\n\n");
out.push_str(&format!("- Records: `{}`\n", report.summary.records_total));
out.push_str(&format!(
"- Repairable: `{}`; static limitations: `{}`; no action: `{}`; missing artifacts: `{}`\n",
report.summary.repairable_total,
report.summary.static_limitation_total,
report.summary.no_action_total,
report.summary.missing_artifact_total
));
out.push_str(&format!(
"- Projections: PR comments=`{}`, gate candidates=`{}`, agent packets=`{}`\n",
report.summary.projection_pr_comment_eligible,
report.summary.projection_gate_candidate,
report.summary.projection_agent_packet_eligible
));
out.push_str(&format!(
"- Badge targets: ripr 0=`{}`, ripr+=`{}`\n",
report.summary.ripr_zero_target_count, report.summary.ripr_plus_target_count
));
out.push_str(&format!(
"- Receipts: improved=`{}`, unchanged_after_attempt=`{}`\n",
report.summary.receipt_improved_total, report.summary.receipt_unchanged_after_attempt_total
));
out.push_str(&format!(
"- Output-contract gaps: `{}`; preview ineligible: `{}`\n\n",
report.summary.missing_output_contract_total, report.summary.preview_ineligible_total
));
if !report.warnings.is_empty() {
out.push_str("## Warnings\n\n");
for warning in &report.warnings {
out.push_str(&format!("- {}\n", md_inline(warning)));
}
out.push('\n');
}
out.push_str("## Records\n\n");
if report.records.is_empty() {
out.push_str("No gap records were supplied.\n\n");
} else {
for record in &report.records {
render_record_markdown(record, &mut out);
}
}
out.push_str("## Limits\n\n");
for limit in &report.limits {
out.push_str(&format!("- {}\n", md_inline(limit)));
}
out
}
pub(crate) fn parse_gap_records_json(contents: &str) -> Result<Vec<GapRecord>, String> {
let value: Value =
serde_json::from_str(contents).map_err(|err| format!("invalid JSON: {err}"))?;
gap_records_from_value(&value)
}
fn parse_gap_decision_source(
source_kind: GapDecisionLedgerSourceKind,
contents: &str,
) -> Result<Vec<GapRecord>, String> {
match source_kind {
GapDecisionLedgerSourceKind::Records => parse_gap_records_json(contents),
GapDecisionLedgerSourceKind::RepoExposure => gap_records_from_repo_exposure_json(contents),
GapDecisionLedgerSourceKind::CheckOutput => gap_records_from_check_output_json(contents),
}
}
fn gap_records_from_check_output_json(contents: &str) -> Result<Vec<GapRecord>, String> {
let value: Value =
serde_json::from_str(contents).map_err(|err| format!("invalid JSON: {err}"))?;
let items = value
.get("finding_alignment")
.and_then(|alignment| alignment.get("items"))
.and_then(Value::as_array)
.ok_or_else(|| {
"expected check output object with finding_alignment.items array".to_string()
})?;
let mut records = Vec::new();
for (index, item) in items.iter().enumerate() {
let Some(record) = gap_record_from_finding_alignment_item(item, index) else {
continue;
};
if record.gap_id.is_empty() {
return Err(format!(
"finding_alignment item {index} produced an empty gap_id"
));
}
records.push(record);
}
Ok(records)
}
fn gap_records_from_repo_exposure_json(contents: &str) -> Result<Vec<GapRecord>, String> {
let value: Value =
serde_json::from_str(contents).map_err(|err| format!("invalid JSON: {err}"))?;
let seams = value
.get("seams")
.and_then(Value::as_array)
.ok_or_else(|| "expected repo exposure object with seams array".to_string())?;
let mut records = Vec::new();
for (index, seam) in seams.iter().enumerate() {
let Some(record) = gap_record_from_repo_exposure_seam(seam) else {
continue;
};
if record.gap_id.is_empty() {
return Err(format!("seam {index} produced an empty gap_id"));
}
records.push(record);
}
Ok(records)
}
fn gap_record_from_repo_exposure_seam(seam: &Value) -> Option<GapRecord> {
let evidence = seam.get("evidence_record")?;
let canonical_item = evidence.get("canonical_item")?;
let gap_state = string_at(canonical_item, &["gap_state"]).unwrap_or("unknown");
let actionability = string_at(canonical_item, &["actionability"]).unwrap_or("unknown");
let seam_kind = string_at(evidence, &["seam_kind"])
.or_else(|| string_at(canonical_item, &["evidence_class"]))
.unwrap_or("unknown");
let repairability = repairability_from_evidence(gap_state, actionability);
let repair_route =
repair_route_from_evidence(evidence, canonical_item, seam_kind, repairability);
let verify_command = string_at(canonical_item, &["verify_command"])
.or_else(|| string_at(evidence, &["recommendation", "verify_command"]));
let verification_commands = verify_command
.map(|command| vec![command.to_string()])
.unwrap_or_default();
let static_limits = array_values_at(canonical_item, &["static_limits"])
.or_else(|| array_values_at(evidence, &["static_limits"]))
.unwrap_or_default();
let static_limit_kind = string_at(canonical_item, &["static_limit_kind"])
.or_else(|| string_at(evidence, &["static_limit_kind"]))
.or_else(|| {
static_limits.iter().find_map(|limit| {
string_at(limit, &["static_limit_kind"]).or_else(|| string_at(limit, &["kind"]))
})
})
.map(ToString::to_string);
let static_limit_detail = string_at(canonical_item, &["static_limit_detail"])
.or_else(|| string_at(evidence, &["static_limit_detail"]))
.or_else(|| {
static_limits.iter().find_map(|limit| {
string_at(limit, &["detail"])
.or_else(|| string_at(limit, &["reason"]))
.or_else(|| string_at(limit, &["message"]))
})
})
.map(ToString::to_string);
let receipt_command = string_at(canonical_item, &["receipt_command"])
.or_else(|| string_at(evidence, &["receipt_command"]))
.map(ToString::to_string);
let seam_id = string_at(evidence, &["seam_id"])
.or_else(|| string_at(seam, &["seam_id"]))
.unwrap_or("unknown-seam");
let canonical_gap_id = string_at(evidence, &["canonical_gap_id"])
.or_else(|| string_at(canonical_item, &["canonical_gap_id"]))
.map(ToString::to_string)
.unwrap_or_else(|| format!("gap:rust:{seam_id}"));
let gap_id = format!("gap:repo:{canonical_gap_id}");
let file = string_at(evidence, &["location", "file"])
.or_else(|| string_at(seam, &["file"]))
.map(ToString::to_string);
let line = u64_at(evidence, &["location", "line"]).or_else(|| u64_at(seam, &["line"]));
let owner = string_at(evidence, &["owner"]).map(ToString::to_string);
let anchor = GapAnchor {
file,
line,
owner,
dedupe_fingerprint: Some(canonical_gap_id.clone()),
};
let projection_eligibility = projection_eligibility_from_repo_evidence(
repairability,
repair_route.is_some(),
!verification_commands.is_empty(),
anchor.file.is_some() && anchor.line.is_some(),
gap_state,
);
Some(GapRecord {
gap_id,
canonical_gap_id,
kind: gap_kind_from_evidence(gap_state, seam_kind).to_string(),
language: "rust".to_string(),
language_status: "stable".to_string(),
scope: "repo_scoped".to_string(),
evidence_class: seam_kind.to_string(),
gap_state: gap_state.to_string(),
policy_state: if gap_state == "actionable" {
"new".to_string()
} else {
"not_policy_targeted".to_string()
},
repairability: repairability.to_string(),
repair_route,
static_limit_kind,
static_limit_detail,
static_limits,
anchor: Some(anchor),
evidence_ids: evidence_ids_from_repo_evidence(evidence, seam_id),
projection_eligibility,
verification_commands,
receipt_command,
regeneration_commands: Vec::new(),
receipt: None,
safe_gate_predicate: None,
authority_boundary: "gate_decision_artifact_only".to_string(),
})
}
fn array_values_at(value: &Value, path: &[&str]) -> Option<Vec<Value>> {
let mut cursor = value;
for segment in path {
cursor = cursor.get(*segment)?;
}
cursor.as_array().map(|values| values.to_vec())
}
fn string_at<'a>(value: &'a Value, path: &[&str]) -> Option<&'a str> {
let mut cursor = value;
for segment in path {
cursor = cursor.get(*segment)?;
}
cursor.as_str()
}
fn u64_at(value: &Value, path: &[&str]) -> Option<u64> {
let mut cursor = value;
for segment in path {
cursor = cursor.get(*segment)?;
}
cursor.as_u64()
}
fn repairability_from_evidence(gap_state: &str, actionability: &str) -> &'static str {
match gap_state {
"actionable"
if matches!(
actionability,
"add_focused_test"
| "upgrade_assertion"
| "extend_related_test"
| "add_output_observer"
) =>
{
"repairable"
}
"already_observed" | "internal_only" => "no_action",
"static_limitation" => "analyzer_limitation",
_ => "unknown",
}
}
fn gap_kind_from_evidence(gap_state: &str, seam_kind: &str) -> &'static str {
match gap_state {
"already_observed" => "NoActionAlreadyObserved",
"internal_only" => "NoActionInternal",
"static_limitation" => "StaticLimitation",
"actionable" => match seam_kind {
"presentation_text" => "MissingOutputContract",
"predicate_boundary" | "match_arm" => "MissingBoundaryAssertion",
"error_variant" => "MissingErrorDiscriminator",
"field_construction" | "return_value" => "MissingValueAssertion",
"call_presence" | "side_effect" => "MissingSideEffectObserver",
_ => "Unknown",
},
_ => "Unknown",
}
}
fn gap_record_from_finding_alignment_item(item: &Value, index: usize) -> Option<GapRecord> {
let evidence_class = string_at(item, &["evidence_class"]).unwrap_or("unknown");
if evidence_class != "presentation_text" {
return None;
}
let gap_state = string_at(item, &["gap_state"]).unwrap_or("unknown");
let actionability = string_at(item, &["actionability"]).unwrap_or("unknown");
let repairability = repairability_from_evidence(gap_state, actionability);
let canonical_gap_id = string_at(item, &["canonical_gap_id"])
.map(ToString::to_string)
.unwrap_or_else(|| format!("presentation_text::item_{index}"));
let presentation_text = item.get("presentation_text");
let raw_findings = array_values_at(item, &["raw_findings"]).unwrap_or_default();
let first_raw = raw_findings.first();
let anchor = GapAnchor {
file: first_raw.and_then(|raw| string_at(raw, &["file"]).map(ToString::to_string)),
line: first_raw.and_then(|raw| u64_at(raw, &["line"])),
owner: presentation_text
.and_then(|text| string_at(text, &["constant_name"]))
.map(ToString::to_string),
dedupe_fingerprint: Some(canonical_gap_id.clone()),
};
let static_limits = array_values_at(item, &["static_limitations"]).unwrap_or_default();
let static_limit_kind = static_limits
.iter()
.find_map(|limit| string_at(limit, &["category"]).or_else(|| string_at(limit, &["kind"])))
.map(ToString::to_string);
let static_limit_detail = static_limits
.iter()
.find_map(|limit| {
string_at(limit, &["repair_route"])
.or_else(|| string_at(limit, &["detail"]))
.or_else(|| string_at(limit, &["reason"]))
})
.map(ToString::to_string);
let repair_route = presentation_text_repair_route_from_alignment_item(item, repairability);
let verification_commands =
verification_commands_from_alignment_item(item, evidence_class, repairability);
let projection_eligibility = projection_eligibility_from_pr_evidence(
repairability,
repair_route.is_some(),
!verification_commands.is_empty(),
anchor.file.is_some() && anchor.line.is_some(),
gap_state,
);
Some(GapRecord {
gap_id: format!("gap:pr:{canonical_gap_id}"),
canonical_gap_id: canonical_gap_id.clone(),
kind: gap_kind_from_evidence(gap_state, evidence_class).to_string(),
language: "rust".to_string(),
language_status: "stable".to_string(),
scope: "pr_local".to_string(),
evidence_class: evidence_class.to_string(),
gap_state: gap_state.to_string(),
policy_state: if gap_state == "actionable" {
"new".to_string()
} else {
"not_policy_targeted".to_string()
},
repairability: repairability.to_string(),
repair_route,
static_limit_kind,
static_limit_detail,
static_limits,
anchor: Some(anchor),
evidence_ids: evidence_ids_from_alignment_item(item, &canonical_gap_id),
projection_eligibility,
verification_commands,
receipt_command: None,
regeneration_commands: Vec::new(),
receipt: None,
safe_gate_predicate: None,
authority_boundary: "gate_decision_artifact_only".to_string(),
})
}
fn presentation_text_repair_route_from_alignment_item(
item: &Value,
repairability: &str,
) -> Option<GapRepairRoute> {
if repairability != "repairable" {
return None;
}
let presentation_text = item.get("presentation_text");
Some(GapRepairRoute {
route_kind: "AddOutputGolden".to_string(),
target_file: string_at(item, &["related_test", "file"]).map(ToString::to_string),
target_line: u64_at(item, &["related_test", "line"]),
related_test: string_at(item, &["related_test", "name"]).map(ToString::to_string),
assertion_shape: presentation_text
.and_then(|text| string_at(text, &["suggested_assertion"]))
.or_else(|| string_at(item, &["recommended_repair"]))
.map(ToString::to_string),
changed_behavior: presentation_text
.and_then(|text| string_at(text, &["text_literal"]))
.or_else(|| presentation_text.and_then(|text| string_at(text, &["constant_name"])))
.map(ToString::to_string),
stop_conditions: vec![
"Stop if the changed text is not user-facing in the current product surface."
.to_string(),
"Stop if the output/golden fixture would assert unrelated formatting churn."
.to_string(),
],
})
}
fn verification_commands_from_alignment_item(
item: &Value,
evidence_class: &str,
repairability: &str,
) -> Vec<String> {
if evidence_class == "presentation_text" && repairability == "repairable" {
return vec!["cargo xtask goldens check".to_string()];
}
string_at(item, &["verify_command"])
.map(|command| vec![command.to_string()])
.unwrap_or_default()
}
fn evidence_ids_from_alignment_item(item: &Value, canonical_gap_id: &str) -> Vec<String> {
let mut ids = Vec::new();
if let Some(raw_findings) = array_values_at(item, &["raw_findings"]) {
for raw in raw_findings {
if let Some(id) = string_at(&raw, &["evidence_record_ref"])
.or_else(|| string_at(&raw, &["source_id"]))
&& !ids.iter().any(|existing| existing == id)
{
ids.push(id.to_string());
}
}
}
if ids.is_empty() {
ids.push(canonical_gap_id.to_string());
}
ids
}
fn repair_route_from_evidence(
evidence: &Value,
canonical_item: &Value,
seam_kind: &str,
repairability: &str,
) -> Option<GapRepairRoute> {
if repairability != "repairable" {
return None;
}
let route_kind = match seam_kind {
"predicate_boundary" | "match_arm" => "AddBoundaryAssertion",
"error_variant" => "AddErrorAssertion",
"field_construction" | "return_value" => "AddValueAssertion",
"call_presence" | "side_effect" => "AddSideEffectObserver",
_ => "AddValueAssertion",
};
Some(GapRepairRoute {
route_kind: route_kind.to_string(),
target_file: string_at(canonical_item, &["related_test", "file"])
.or_else(|| string_at(evidence, &["recommendation", "recommended_test", "file"]))
.map(ToString::to_string),
target_line: u64_at(canonical_item, &["related_test", "line"]),
related_test: string_at(canonical_item, &["related_test", "name"]).map(ToString::to_string),
assertion_shape: string_at(evidence, &["recommendation", "assertion_shape", "example"])
.or_else(|| string_at(canonical_item, &["recommended_repair"]))
.map(ToString::to_string),
changed_behavior: first_raw_finding_expression(evidence).map(ToString::to_string),
stop_conditions: vec![
"Stop if the related test is outside the current workspace.".to_string(),
"Stop if the suggested assertion would require changing production behavior first."
.to_string(),
],
})
}
fn first_raw_finding_expression(evidence: &Value) -> Option<&str> {
evidence
.get("raw_findings")
.and_then(Value::as_array)?
.first()
.and_then(|finding| finding.get("expression"))
.and_then(Value::as_str)
}
fn projection_eligibility_from_repo_evidence(
repairability: &str,
has_repair_route: bool,
has_verify_command: bool,
has_local_anchor: bool,
gap_state: &str,
) -> BTreeMap<String, ProjectionEligibility> {
let mut projections = BTreeMap::new();
insert_projection(
&mut projections,
"ci_summary",
true,
"repo_scoped_gap_record",
);
insert_projection(
&mut projections,
"report_packet",
true,
"all_gap_records_are_reportable",
);
insert_projection(
&mut projections,
"pr_comment",
false,
"repo_scoped_not_pr_local",
);
insert_projection(
&mut projections,
"gate_candidate",
false,
"repo_scoped_not_pr_local",
);
let repairable = repairability == "repairable" && has_repair_route && has_verify_command;
insert_projection(
&mut projections,
"agent_packet",
repairable,
if repairable {
"bounded_repair_route"
} else {
"not_repairable"
},
);
insert_projection(
&mut projections,
"lsp_diagnostic",
repairable && has_local_anchor,
if repairable && has_local_anchor {
"local_file_scope"
} else {
"not_repairable_or_missing_anchor"
},
);
insert_projection(
&mut projections,
"ripr_zero_count",
repairable && gap_state == "actionable",
if repairable && gap_state == "actionable" {
"repo_scoped_policy_targeted_rust_gap"
} else {
"not_unresolved_repairable_repo_gap"
},
);
insert_projection(
&mut projections,
"ripr_plus_count",
repairable && gap_state == "actionable",
if repairable && gap_state == "actionable" {
"repo_scoped_advisory_rust_gap"
} else {
"not_unresolved_repairable_repo_gap"
},
);
projections
}
fn projection_eligibility_from_pr_evidence(
repairability: &str,
has_repair_route: bool,
has_verify_command: bool,
has_local_anchor: bool,
gap_state: &str,
) -> BTreeMap<String, ProjectionEligibility> {
let mut projections = BTreeMap::new();
insert_projection(&mut projections, "ci_summary", true, "pr_local_gap_record");
insert_projection(
&mut projections,
"report_packet",
true,
"all_gap_records_are_reportable",
);
let repairable = repairability == "repairable" && has_repair_route && has_verify_command;
insert_projection(
&mut projections,
"pr_comment",
repairable && has_local_anchor,
if repairable && has_local_anchor {
"stable_anchor_and_repair_route"
} else {
"not_repairable_or_missing_anchor"
},
);
insert_projection(
&mut projections,
"gate_candidate",
false,
"policy_target_not_supplied",
);
insert_projection(
&mut projections,
"agent_packet",
repairable,
if repairable {
"bounded_repair_route"
} else {
"not_repairable"
},
);
insert_projection(
&mut projections,
"lsp_diagnostic",
repairable && has_local_anchor,
if repairable && has_local_anchor {
"local_file_scope"
} else {
"not_repairable_or_missing_anchor"
},
);
insert_projection(
&mut projections,
"ripr_zero_count",
false,
"pr_local_not_repo_scoped",
);
insert_projection(
&mut projections,
"ripr_plus_count",
false,
"pr_local_not_repo_scoped",
);
if gap_state != "actionable" {
insert_projection(&mut projections, "pr_comment", false, "not_actionable_gap");
}
projections
}
fn insert_projection(
projections: &mut BTreeMap<String, ProjectionEligibility>,
name: &str,
eligible: bool,
reason: &str,
) {
projections.insert(
name.to_string(),
ProjectionEligibility {
eligible,
reason: reason.to_string(),
},
);
}
fn evidence_ids_from_repo_evidence(evidence: &Value, seam_id: &str) -> Vec<String> {
let mut ids = vec![seam_id.to_string()];
if let Some(raw_findings) = evidence.get("raw_findings").and_then(Value::as_array) {
for finding in raw_findings {
if let Some(source_id) = finding.get("source_id").and_then(Value::as_str)
&& !ids.iter().any(|id| id == source_id)
{
ids.push(source_id.to_string());
}
}
}
ids
}
fn gap_records_from_value(value: &Value) -> Result<Vec<GapRecord>, String> {
if let Some(records) = value.as_array() {
return parse_record_array(records);
}
let Some(object) = value.as_object() else {
return Err("expected object or array at gap record root".to_string());
};
if let Some(records) = object.get("records").and_then(Value::as_array) {
return parse_record_array(records);
}
if let Some(records) = object.get("gap_records").and_then(Value::as_array) {
return parse_record_array(records);
}
if let Some(cases) = object.get("cases").and_then(Value::as_array) {
let mut records = Vec::new();
for case in cases {
let case_id = case.get("id").and_then(Value::as_str).unwrap_or("unknown");
let Some(record) = case.get("expected_gap_record") else {
return Err(format!("case {case_id} is missing expected_gap_record"));
};
records.push(parse_record(record).map_err(|err| format!("case {case_id}: {err}"))?);
}
return Ok(records);
}
Err("expected records, gap_records, cases, or record array".to_string())
}
fn parse_record_array(records: &[Value]) -> Result<Vec<GapRecord>, String> {
records
.iter()
.enumerate()
.map(|(index, record)| parse_record(record).map_err(|err| format!("record {index}: {err}")))
.collect()
}
fn parse_record(record: &Value) -> Result<GapRecord, String> {
serde_json::from_value(record.clone()).map_err(|err| format!("invalid GapRecord: {err}"))
}
fn summarize_records(records: &[GapRecord]) -> GapDecisionLedgerSummary {
let mut summary = GapDecisionLedgerSummary {
records_total: records.len(),
..GapDecisionLedgerSummary::default()
};
for record in records {
if record.repairability == "repairable" {
summary.repairable_total += 1;
}
if record.kind == "StaticLimitation" {
summary.static_limitation_total += 1;
}
if record.repairability == "no_action"
|| matches!(
record.kind.as_str(),
"NoActionAlreadyObserved" | "NoActionInternal"
)
{
summary.no_action_total += 1;
}
if record.scope == "artifact_missing" {
summary.missing_artifact_total += 1;
}
if projection_eligible(record, "pr_comment") {
summary.projection_pr_comment_eligible += 1;
}
if projection_eligible(record, "gate_candidate") {
summary.projection_gate_candidate += 1;
}
if projection_eligible(record, "agent_packet") {
summary.projection_agent_packet_eligible += 1;
}
if projection_eligible(record, "ripr_zero_count") {
summary.ripr_zero_target_count += 1;
}
if projection_eligible(record, "ripr_plus_count") {
summary.ripr_plus_target_count += 1;
}
if record.language_status == "preview"
&& !projection_eligible(record, "gate_candidate")
&& !projection_eligible(record, "ripr_zero_count")
&& !projection_eligible(record, "ripr_plus_count")
{
summary.preview_ineligible_total += 1;
}
if record.kind == "MissingOutputContract" {
summary.missing_output_contract_total += 1;
}
if record
.receipt
.as_ref()
.and_then(|receipt| receipt.movement.as_deref())
== Some("improved")
{
summary.receipt_improved_total += 1;
}
if record
.receipt
.as_ref()
.and_then(|receipt| receipt.movement.as_deref())
== Some("unchanged_after_attempt")
{
summary.receipt_unchanged_after_attempt_total += 1;
}
}
summary
}
fn validate_record(record: &GapRecord, warnings: &mut Vec<String>) {
if record.gap_id.trim().is_empty() {
warnings.push("gap record is missing gap_id".to_string());
}
if record.kind.trim().is_empty() {
warnings.push(format!(
"gap record {} is missing kind",
fallback_gap_id(record)
));
}
if record.repairability == "repairable" && record.repair_route.is_none() {
warnings.push(format!(
"gap record {} is repairable but missing repair_route",
fallback_gap_id(record)
));
}
if record.repairability == "repairable" && record.verification_commands.is_empty() {
warnings.push(format!(
"gap record {} is repairable but missing verification_commands",
fallback_gap_id(record)
));
}
if projection_eligible(record, "pr_comment")
&& record
.anchor
.as_ref()
.and_then(|anchor| anchor.dedupe_fingerprint.as_deref())
.is_none()
{
warnings.push(format!(
"gap record {} is PR-comment eligible but missing anchor.dedupe_fingerprint",
fallback_gap_id(record)
));
}
if projection_eligible(record, "gate_candidate") && !safe_gate_predicate_satisfied(record) {
warnings.push(format!(
"gap record {} is gate-candidate eligible but safe_gate_predicate is incomplete",
fallback_gap_id(record)
));
}
if record.language_status == "preview"
&& (projection_eligible(record, "gate_candidate")
|| projection_eligible(record, "ripr_zero_count")
|| projection_eligible(record, "ripr_plus_count"))
{
warnings.push(format!(
"gap record {} is preview evidence but eligible for gate or badge authority",
fallback_gap_id(record)
));
}
if record.scope == "artifact_missing" && record.regeneration_commands.is_empty() {
warnings.push(format!(
"gap record {} has artifact_missing scope but no regeneration_commands",
fallback_gap_id(record)
));
}
}
pub(crate) fn safe_gate_predicate_satisfied(record: &GapRecord) -> bool {
let Some(predicate) = &record.safe_gate_predicate else {
return false;
};
record.language == "rust"
&& record.language_status == "stable"
&& record.scope == "pr_local"
&& matches!(record.policy_state.as_str(), "new" | "blocked")
&& record.repairability == "repairable"
&& record.repair_route.is_some()
&& !record.verification_commands.is_empty()
&& predicate.policy_target_enabled
&& !predicate.suppressed
&& !predicate.waived
&& !predicate.acknowledged_only
&& !predicate.baseline_known
&& !predicate.preview_language
&& !predicate.static_unknown_only
}
pub(crate) fn projection_eligible(record: &GapRecord, projection: &str) -> bool {
record
.projection_eligibility
.get(projection)
.is_some_and(|projection| projection.eligible)
}
fn render_record_markdown(record: &GapRecord, out: &mut String) {
out.push_str(&format!(
"### `{}`\n\n",
md_inline(&fallback_gap_id(record))
));
out.push_str(&format!(
"- Kind: `{}`; scope: `{}`; policy: `{}`; repairability: `{}`\n",
md_inline(&record.kind),
md_inline(&record.scope),
md_inline(&record.policy_state),
md_inline(&record.repairability)
));
out.push_str(&format!(
"- Evidence: `{}` / `{}`; language: `{}` / `{}`\n",
md_inline(&record.evidence_class),
md_inline(&record.gap_state),
md_inline(&record.language),
md_inline(&record.language_status)
));
if let Some(anchor) = &record.anchor {
out.push_str(&format!(
"- Anchor: `{}`{}{}\n",
md_inline(anchor.file.as_deref().unwrap_or("unknown")),
anchor
.line
.map(|line| format!(":{line}"))
.unwrap_or_default(),
anchor
.owner
.as_ref()
.map(|owner| format!(" owner `{}`", md_inline(owner)))
.unwrap_or_default()
));
}
if let Some(route) = &record.repair_route {
out.push_str(&format!(
"- Repair: `{}`{}\n",
md_inline(&route.route_kind),
route
.target_file
.as_ref()
.map(|target| format!(" in `{}`", md_inline(target)))
.unwrap_or_default()
));
if let Some(assertion) = &route.assertion_shape {
out.push_str(&format!(
"- Assertion or observer: `{}`\n",
md_inline(assertion)
));
}
}
let eligible = eligible_projection_names(record);
if !eligible.is_empty() {
out.push_str(&format!(
"- Eligible projections: `{}`\n",
eligible.join("`, `")
));
}
if !record.verification_commands.is_empty() {
out.push_str("- Verify:\n");
for command in &record.verification_commands {
out.push_str(&format!(" - `{}`\n", md_inline(command)));
}
}
if !record.regeneration_commands.is_empty() {
out.push_str("- Regenerate:\n");
for command in &record.regeneration_commands {
out.push_str(&format!(" - `{}`\n", md_inline(command)));
}
}
if let Some(receipt) = &record.receipt {
out.push_str(&format!(
"- Receipt movement: `{}`\n",
md_inline(receipt.movement.as_deref().unwrap_or("unknown"))
));
}
out.push('\n');
}
fn eligible_projection_names(record: &GapRecord) -> Vec<String> {
record
.projection_eligibility
.iter()
.filter(|(_, projection)| projection.eligible)
.map(|(name, _)| name.clone())
.collect()
}
fn fallback_gap_id(record: &GapRecord) -> String {
if record.gap_id.trim().is_empty() {
"unknown-gap".to_string()
} else {
record.gap_id.clone()
}
}
fn md_inline(value: &str) -> String {
let mut escaped = String::with_capacity(value.len());
for character in value.chars() {
match character {
'\r' | '\n' => escaped.push(' '),
'|' => escaped.push_str("\\|"),
'`' => escaped.push('\''),
_ => escaped.push(character),
}
}
escaped
}
#[cfg(test)]
mod tests {
use super::*;
fn corpus() -> String {
include_str!("../../../../fixtures/gap-decision-ledger/corpus.json").to_string()
}
fn minimal_record() -> Value {
serde_json::json!({
"gap_id": "gap:minimal",
"canonical_gap_id": "gap:minimal",
"kind": "AlreadyObserved",
"language": "rust",
"language_status": "stable",
"scope": "repo_scoped",
"evidence_class": "already_observed",
"gap_state": "already_improved",
"policy_state": "baseline",
"repairability": "no_action",
"projection_eligibility": {
"agent_packet": {"eligible": false, "reason": "already_observed"}
},
"authority_boundary": "gate_decision_artifact_only"
})
}
fn report_from_json(value: Value) -> GapDecisionLedgerReport {
build_gap_decision_ledger_report(GapDecisionLedgerInput {
root: ".".to_string(),
generated_at: "test".to_string(),
source_kind: GapDecisionLedgerSourceKind::Records,
records_path: "records.json".to_string(),
records_json: Ok(value.to_string()),
})
}
fn report_from_check_output(value: Value) -> GapDecisionLedgerReport {
build_gap_decision_ledger_report(GapDecisionLedgerInput {
root: ".".to_string(),
generated_at: "test".to_string(),
source_kind: GapDecisionLedgerSourceKind::CheckOutput,
records_path: "check.json".to_string(),
records_json: Ok(value.to_string()),
})
}
#[test]
fn gap_decision_ledger_parses_corpus_records_and_summarizes_projection_boundaries() {
let report = build_gap_decision_ledger_report(GapDecisionLedgerInput {
root: ".".to_string(),
generated_at: "test".to_string(),
source_kind: GapDecisionLedgerSourceKind::Records,
records_path: "fixtures/gap-decision-ledger/corpus.json".to_string(),
records_json: Ok(corpus()),
});
assert_eq!(report.status, "advisory");
assert_eq!(report.summary.records_total, 18);
assert_eq!(report.summary.projection_gate_candidate, 1);
assert_eq!(report.summary.ripr_zero_target_count, 1);
assert_eq!(report.summary.preview_ineligible_total, 1);
assert_eq!(report.summary.missing_output_contract_total, 1);
assert_eq!(report.summary.receipt_improved_total, 1);
assert_eq!(report.summary.receipt_unchanged_after_attempt_total, 1);
assert!(report.warnings.is_empty());
}
#[test]
fn gap_decision_ledger_accepts_supported_record_roots() {
let raw_array = report_from_json(serde_json::json!([minimal_record()]));
assert_eq!(raw_array.status, "advisory");
assert_eq!(raw_array.summary.records_total, 1);
let records = report_from_json(serde_json::json!({"records": [minimal_record()]}));
assert_eq!(records.status, "advisory");
assert_eq!(records.summary.no_action_total, 1);
let gap_records = report_from_json(serde_json::json!({"gap_records": [minimal_record()]}));
assert_eq!(gap_records.status, "advisory");
assert_eq!(gap_records.summary.no_action_total, 1);
}
#[test]
fn gap_decision_ledger_derives_missing_output_contract_from_check_output_alignment() {
let report = report_from_check_output(serde_json::json!({
"schema_version": "0.1",
"tool": "ripr",
"finding_alignment": {
"scope": "supported_classes",
"items": [
{
"canonical_gap_id": "presentation_text::HELP_DEVICE_LABEL",
"canonical_item_kind": "gap",
"evidence_class": "presentation_text",
"gap_state": "actionable",
"actionability": "add_output_observer",
"raw_group_size": 2,
"group_reason": "declaration_and_literal_same_text_constant",
"why": "Changed text flows to CLI help output and no supported output observer is found.",
"recommended_repair": "Add or update a help-output snapshot assertion for HELP_DEVICE_LABEL.",
"related_test": null,
"verify_command": "cargo xtask evidence-quality-scorecard",
"static_limitations": [],
"confidence": {
"basis": "fixture_backed",
"notes": ["Visible unobserved presentation text is actionable only for supported sink patterns."]
},
"raw_findings": [
{
"file": "crates/ripr/src/cli/help.rs",
"line": 42,
"kind": "exposed",
"expression": "pub const HELP_DEVICE_LABEL: &str =",
"probe_kind": "field_construction",
"source_id": "help-label-decl",
"evidence_record_ref": "help-label-decl"
},
{
"file": "crates/ripr/src/cli/help.rs",
"line": 43,
"kind": "static_unknown",
"expression": "\"Device label\";",
"probe_kind": "static_unknown",
"source_id": "help-label-literal",
"evidence_record_ref": "help-label-literal"
}
],
"presentation_text": {
"constant_name": "HELP_DEVICE_LABEL",
"text_literal": "Device label",
"visibility": "user_visible",
"observer": "none",
"actionability": "add_output_observer",
"source_kind": "const_decl",
"canonical_group_reason": "declaration_and_literal_same_text_constant",
"recommended_observer": "cli_help_output",
"repair_kind": "output_observer",
"target_test_type": "help_output_snapshot",
"suggested_assertion": "Assert CLI help output includes the HELP_DEVICE_LABEL text."
}
}
]
}
}));
assert_eq!(report.status, "advisory");
assert_eq!(report.summary.records_total, 1);
assert_eq!(report.summary.repairable_total, 1);
assert_eq!(report.summary.missing_output_contract_total, 1);
assert_eq!(report.summary.projection_pr_comment_eligible, 1);
assert_eq!(report.summary.projection_gate_candidate, 0);
assert_eq!(report.summary.ripr_zero_target_count, 0);
let record = &report.records[0];
assert_eq!(record.kind, "MissingOutputContract");
assert_eq!(record.scope, "pr_local");
assert_eq!(
record
.repair_route
.as_ref()
.map(|route| route.route_kind.as_str()),
Some("AddOutputGolden")
);
assert_eq!(
record.verification_commands,
vec!["cargo xtask goldens check".to_string()]
);
assert_eq!(
record
.projection_eligibility
.get("pr_comment")
.map(|projection| projection.eligible),
Some(true)
);
assert_eq!(
record.evidence_ids,
vec![
"help-label-decl".to_string(),
"help-label-literal".to_string()
]
);
}
#[test]
fn gap_decision_ledger_renders_json_and_markdown() -> Result<(), String> {
let report = build_gap_decision_ledger_report(GapDecisionLedgerInput {
root: ".".to_string(),
generated_at: "test".to_string(),
source_kind: GapDecisionLedgerSourceKind::Records,
records_path: "fixtures/gap-decision-ledger/corpus.json".to_string(),
records_json: Ok(corpus()),
});
let json = render_gap_decision_ledger_json(&report)?;
assert!(json.contains("\"kind\": \"gap_decision_ledger\""));
assert!(json.contains("\"MissingOutputContract\""));
assert!(json.contains("\"AddOutputGolden\""));
let markdown = render_gap_decision_ledger_markdown(&report);
assert!(markdown.starts_with("# RIPR Gap Decision Ledger"));
assert!(markdown.contains("gate candidates=`1`"));
assert!(
markdown
.contains("Gate-decision artifacts remain the only configured pass/fail authority")
);
assert!(markdown.contains("AddOutputGolden"));
Ok(())
}
#[test]
fn gap_decision_ledger_renders_optional_markdown_fields_and_escapes_inline_text() {
let report = report_from_json(serde_json::json!({
"records": [
{
"gap_id": "gap:`pipe|line",
"canonical_gap_id": "gap:escaped",
"kind": "MissingArtifact",
"language": "rust",
"language_status": "stable",
"scope": "artifact_missing",
"evidence_class": "missing_artifact",
"gap_state": "missing_artifact",
"policy_state": "not_policy_targeted",
"repairability": "repairable",
"repair_route": {
"route_kind": "RegenerateArtifact",
"target_file": "target/ripr/reports/index.md",
"assertion_shape": "report contains `start|here`"
},
"anchor": {
"file": "docs|OUTPUT_SCHEMA.md",
"line": 7,
"owner": "output::`schema`",
"dedupe_fingerprint": "gap:escaped"
},
"projection_eligibility": {
"agent_packet": {"eligible": true, "reason": "repair_command_present"}
},
"verification_commands": ["cargo xtask check-output-contracts"],
"regeneration_commands": ["cargo xtask reports\nindex"],
"receipt": {"movement": "missing_receipt"}
}
]
}));
let markdown = render_gap_decision_ledger_markdown(&report);
assert!(markdown.contains("gap:'pipe\\|line"));
assert!(markdown.contains("docs\\|OUTPUT_SCHEMA.md`:7"));
assert!(markdown.contains("owner `output::'schema'`"));
assert!(markdown.contains("Assertion or observer: `report contains 'start\\|here'`"));
assert!(markdown.contains("cargo xtask reports index"));
assert!(markdown.contains("Receipt movement: `missing_receipt`"));
}
#[test]
fn gap_decision_ledger_reports_bad_or_missing_records_as_blocked() {
let report = build_gap_decision_ledger_report(GapDecisionLedgerInput {
root: ".".to_string(),
generated_at: "test".to_string(),
source_kind: GapDecisionLedgerSourceKind::Records,
records_path: "missing.json".to_string(),
records_json: Err("read missing.json failed: not found".to_string()),
});
assert_eq!(report.status, "blocked");
assert_eq!(report.summary.records_total, 0);
assert_eq!(
report.warnings,
vec!["read missing.json failed: not found".to_string()]
);
}
#[test]
fn gap_decision_ledger_reports_malformed_record_inputs() {
let invalid_json = build_gap_decision_ledger_report(GapDecisionLedgerInput {
root: ".".to_string(),
generated_at: "test".to_string(),
source_kind: GapDecisionLedgerSourceKind::Records,
records_path: "bad.json".to_string(),
records_json: Ok("{".to_string()),
});
assert_eq!(invalid_json.status, "blocked");
assert!(invalid_json.warnings[0].contains("invalid JSON"));
let wrong_root = report_from_json(serde_json::json!("not records"));
assert_eq!(wrong_root.status, "blocked");
assert!(wrong_root.warnings[0].contains("expected object or array"));
let missing_case_record = report_from_json(serde_json::json!({
"cases": [{"id": "missing"}]
}));
assert_eq!(missing_case_record.status, "blocked");
assert!(missing_case_record.warnings[0].contains("missing expected_gap_record"));
}
#[test]
fn gap_decision_ledger_warns_on_unsafe_projection() {
let record = serde_json::json!({
"records": [
{
"gap_id": "gap:bad",
"canonical_gap_id": "gap:bad",
"kind": "MissingBoundaryAssertion",
"language": "typescript",
"language_status": "preview",
"scope": "pr_local",
"evidence_class": "predicate_boundary",
"gap_state": "actionable",
"policy_state": "new",
"repairability": "repairable",
"projection_eligibility": {
"gate_candidate": {"eligible": true, "reason": "bad"},
"ripr_zero_count": {"eligible": true, "reason": "bad"}
},
"verification_commands": []
}
]
});
let report = build_gap_decision_ledger_report(GapDecisionLedgerInput {
root: ".".to_string(),
generated_at: "test".to_string(),
source_kind: GapDecisionLedgerSourceKind::Records,
records_path: "bad.json".to_string(),
records_json: Ok(record.to_string()),
});
let warnings = report.warnings.join("\n");
assert!(warnings.contains("repairable but missing repair_route"));
assert!(warnings.contains("repairable but missing verification_commands"));
assert!(warnings.contains("gate-candidate eligible but safe_gate_predicate is incomplete"));
assert!(warnings.contains("preview evidence but eligible for gate or badge authority"));
}
#[test]
fn repairability_from_evidence_covers_all_branches() {
for actionability in &[
"add_focused_test",
"upgrade_assertion",
"extend_related_test",
"add_output_observer",
] {
assert_eq!(
repairability_from_evidence("actionable", actionability),
"repairable",
"expected repairable for actionability={actionability}"
);
}
assert_eq!(
repairability_from_evidence("actionable", "some_other_kind"),
"unknown"
);
assert_eq!(
repairability_from_evidence("already_observed", "anything"),
"no_action"
);
assert_eq!(
repairability_from_evidence("internal_only", "anything"),
"no_action"
);
assert_eq!(
repairability_from_evidence("static_limitation", "anything"),
"analyzer_limitation"
);
assert_eq!(
repairability_from_evidence("unrecognized_state", "x"),
"unknown"
);
}
#[test]
fn gap_kind_from_evidence_covers_all_branches() {
assert_eq!(
gap_kind_from_evidence("already_observed", "anything"),
"NoActionAlreadyObserved"
);
assert_eq!(
gap_kind_from_evidence("internal_only", "anything"),
"NoActionInternal"
);
assert_eq!(
gap_kind_from_evidence("static_limitation", "anything"),
"StaticLimitation"
);
assert_eq!(
gap_kind_from_evidence("actionable", "presentation_text"),
"MissingOutputContract"
);
for seam_kind in &["predicate_boundary", "match_arm"] {
assert_eq!(
gap_kind_from_evidence("actionable", seam_kind),
"MissingBoundaryAssertion",
"expected MissingBoundaryAssertion for seam_kind={seam_kind}"
);
}
assert_eq!(
gap_kind_from_evidence("actionable", "error_variant"),
"MissingErrorDiscriminator"
);
for seam_kind in &["field_construction", "return_value"] {
assert_eq!(
gap_kind_from_evidence("actionable", seam_kind),
"MissingValueAssertion",
"expected MissingValueAssertion for seam_kind={seam_kind}"
);
}
for seam_kind in &["call_presence", "side_effect"] {
assert_eq!(
gap_kind_from_evidence("actionable", seam_kind),
"MissingSideEffectObserver",
"expected MissingSideEffectObserver for seam_kind={seam_kind}"
);
}
assert_eq!(
gap_kind_from_evidence("actionable", "unrecognized"),
"Unknown"
);
assert_eq!(gap_kind_from_evidence("unknown_state", "x"), "Unknown");
}
#[test]
fn md_inline_escapes_special_characters() {
assert_eq!(md_inline("plain"), "plain");
assert_eq!(md_inline("a|b"), "a\\|b");
assert_eq!(md_inline("a`b"), "a'b");
assert_eq!(md_inline("a\nb"), "a b");
assert_eq!(md_inline("a\rb"), "a b");
assert_eq!(md_inline("a`b|c\nd"), "a'b\\|c d");
assert_eq!(md_inline(""), "");
}
#[test]
fn fallback_gap_id_returns_unknown_for_blank_gap_id() {
let blank = GapRecord {
gap_id: " ".to_string(),
..GapRecord::default()
};
assert_eq!(fallback_gap_id(&blank), "unknown-gap");
let empty = GapRecord {
gap_id: String::new(),
..GapRecord::default()
};
assert_eq!(fallback_gap_id(&empty), "unknown-gap");
let with_id = GapRecord {
gap_id: "gap:real".to_string(),
..GapRecord::default()
};
assert_eq!(fallback_gap_id(&with_id), "gap:real");
}
#[test]
fn source_kind_as_str_returns_correct_values() {
assert_eq!(GapDecisionLedgerSourceKind::Records.as_str(), "records");
assert_eq!(
GapDecisionLedgerSourceKind::RepoExposure.as_str(),
"repo_exposure"
);
assert_eq!(
GapDecisionLedgerSourceKind::CheckOutput.as_str(),
"check_output"
);
}
fn make_repo_exposure_seam(
gap_state: &str,
actionability: &str,
seam_kind: &str,
) -> serde_json::Value {
serde_json::json!({
"seam_id": "seam:test",
"evidence_record": {
"seam_id": "seam:test",
"seam_kind": seam_kind,
"canonical_gap_id": "gap:rust:seam:test",
"location": {"file": "src/lib.rs", "line": 10},
"owner": "my_crate",
"raw_findings": [
{"source_id": "finding-001", "expression": "fn foo()"}
],
"recommendation": {
"assertion_shape": {"example": "assert_eq!(foo(), expected)"},
"recommended_test": {"file": "tests/lib_test.rs"},
"verify_command": "cargo test foo"
},
"canonical_item": {
"gap_state": gap_state,
"actionability": actionability,
"evidence_class": seam_kind,
"related_test": {"name": "test_foo", "file": "tests/lib_test.rs", "line": 5},
"verify_command": "cargo test"
}
}
})
}
#[test]
fn repo_exposure_json_parses_seam_to_record() -> Result<(), String> {
let payload = serde_json::json!({
"seams": [make_repo_exposure_seam("actionable", "add_focused_test", "predicate_boundary")]
});
let records = gap_records_from_repo_exposure_json(&payload.to_string())
.map_err(|e| format!("parse failed: {e}"))?;
assert_eq!(records.len(), 1);
let r = &records[0];
assert_eq!(r.repairability, "repairable");
assert_eq!(r.kind, "MissingBoundaryAssertion");
assert_eq!(r.language, "rust");
assert_eq!(r.scope, "repo_scoped");
assert_eq!(r.policy_state, "new");
let Some(route) = &r.repair_route else {
return Err("expected repair_route".to_string());
};
assert_eq!(route.route_kind, "AddBoundaryAssertion");
assert!(r.evidence_ids.contains(&"seam:test".to_string()));
assert!(r.evidence_ids.contains(&"finding-001".to_string()));
let anchor = r.anchor.as_ref().ok_or("expected anchor")?;
assert_eq!(anchor.file.as_deref(), Some("src/lib.rs"));
assert_eq!(anchor.line, Some(10));
Ok(())
}
#[test]
fn repo_exposure_json_error_paths() -> Result<(), String> {
let err_msg = gap_records_from_repo_exposure_json("not-json")
.err()
.ok_or("expected error for invalid JSON")?;
assert!(
err_msg.contains("invalid JSON"),
"unexpected error: {err_msg}"
);
let err_msg = gap_records_from_repo_exposure_json(r#"{"other": []}"#)
.err()
.ok_or("expected error for missing seams")?;
assert!(
err_msg.contains("seams array"),
"unexpected error: {err_msg}"
);
let payload = serde_json::json!({"seams": [{"seam_id": "bare"}]});
let records = gap_records_from_repo_exposure_json(&payload.to_string())
.map_err(|e| format!("unexpected error: {e}"))?;
assert!(records.is_empty());
Ok(())
}
#[test]
fn repo_exposure_json_all_seam_kinds_produce_correct_routes() -> Result<(), String> {
let cases = [
("match_arm", "AddBoundaryAssertion"),
("error_variant", "AddErrorAssertion"),
("return_value", "AddValueAssertion"),
("field_construction", "AddValueAssertion"),
("call_presence", "AddSideEffectObserver"),
("side_effect", "AddSideEffectObserver"),
("unknown_kind", "AddValueAssertion"),
];
for (seam_kind, expected_route) in &cases {
let payload = serde_json::json!({
"seams": [make_repo_exposure_seam("actionable", "add_focused_test", seam_kind)]
});
let records = gap_records_from_repo_exposure_json(&payload.to_string())
.map_err(|e| format!("parse failed for {seam_kind}: {e}"))?;
let route_kind = records
.first()
.and_then(|r| r.repair_route.as_ref())
.map(|r| r.route_kind.as_str())
.ok_or_else(|| format!("missing repair_route for {seam_kind}"))?;
assert_eq!(
route_kind, *expected_route,
"wrong route kind for seam_kind={seam_kind}"
);
}
Ok(())
}
#[test]
fn repo_exposure_seam_non_actionable_states_produce_no_repair_route() -> Result<(), String> {
for gap_state in &["already_observed", "internal_only", "static_limitation"] {
let payload = serde_json::json!({
"seams": [make_repo_exposure_seam(gap_state, "add_focused_test", "predicate_boundary")]
});
let records = gap_records_from_repo_exposure_json(&payload.to_string())
.map_err(|e| format!("parse failed for {gap_state}: {e}"))?;
let r = records.first().ok_or("expected record")?;
assert!(
r.repair_route.is_none(),
"expected no repair_route for gap_state={gap_state}"
);
assert_eq!(
r.policy_state, "not_policy_targeted",
"wrong policy_state for gap_state={gap_state}"
);
}
Ok(())
}
#[test]
fn repo_exposure_seam_static_limits_populated_from_canonical_item() -> Result<(), String> {
let payload = serde_json::json!({
"seams": [{
"evidence_record": {
"seam_id": "s1",
"seam_kind": "predicate_boundary",
"canonical_item": {
"gap_state": "static_limitation",
"actionability": "none",
"static_limit_kind": "cfg_gate",
"static_limit_detail": "hidden behind #[cfg(test)]",
"static_limits": [
{"kind": "cfg_gate", "detail": "hidden behind #[cfg(test)]"}
]
}
}
}]
});
let records = gap_records_from_repo_exposure_json(&payload.to_string())
.map_err(|e| format!("parse failed: {e}"))?;
let r = records.first().ok_or("expected record")?;
assert_eq!(r.static_limit_kind.as_deref(), Some("cfg_gate"));
assert!(
r.static_limit_detail
.as_deref()
.unwrap_or("")
.contains("cfg(test)")
);
assert!(!r.static_limits.is_empty());
Ok(())
}
#[test]
fn repo_exposure_seam_static_limits_fallback_from_evidence_limits() -> Result<(), String> {
let payload = serde_json::json!({
"seams": [{
"evidence_record": {
"seam_id": "s2",
"seam_kind": "predicate_boundary",
"static_limits": [
{"kind": "opaque_body", "reason": "body not available"}
],
"canonical_item": {
"gap_state": "static_limitation",
"actionability": "none"
}
}
}]
});
let records = gap_records_from_repo_exposure_json(&payload.to_string())
.map_err(|e| format!("parse failed: {e}"))?;
let r = records.first().ok_or("expected record")?;
assert_eq!(r.static_limit_kind.as_deref(), Some("opaque_body"));
assert_eq!(r.static_limit_detail.as_deref(), Some("body not available"));
Ok(())
}
#[test]
fn repo_exposure_seam_fallback_canonical_gap_id_when_missing() -> Result<(), String> {
let payload = serde_json::json!({
"seams": [{
"seam_id": "outer-seam",
"evidence_record": {
"canonical_item": {
"gap_state": "already_observed",
"actionability": "none"
}
}
}]
});
let records = gap_records_from_repo_exposure_json(&payload.to_string())
.map_err(|e| format!("parse failed: {e}"))?;
let r = records.first().ok_or("expected record")?;
assert_eq!(r.canonical_gap_id, "gap:rust:outer-seam");
assert_eq!(r.gap_id, "gap:repo:gap:rust:outer-seam");
Ok(())
}
#[test]
fn check_output_json_error_paths() -> Result<(), String> {
let err_msg = gap_records_from_check_output_json("bad json")
.err()
.ok_or("expected error for invalid JSON")?;
assert!(
err_msg.contains("invalid JSON"),
"unexpected error: {err_msg}"
);
let err_msg = gap_records_from_check_output_json(r#"{"other": {}}"#)
.err()
.ok_or("expected error for missing finding_alignment")?;
assert!(
err_msg.contains("finding_alignment.items array"),
"unexpected error: {err_msg}"
);
let payload = serde_json::json!({
"finding_alignment": {
"items": [
{"evidence_class": "predicate_boundary", "canonical_gap_id": "g1"}
]
}
});
let records = gap_records_from_check_output_json(&payload.to_string())
.map_err(|e| format!("unexpected error: {e}"))?;
assert!(records.is_empty());
let payload = serde_json::json!({
"finding_alignment": {
"items": [
{
"evidence_class": "presentation_text",
"gap_state": "actionable",
"actionability": "add_output_observer",
"canonical_gap_id": "pt::MY_CONST",
"raw_findings": [{"file": "src/lib.rs", "line": 1}],
"presentation_text": {
"constant_name": "MY_CONST",
"text_literal": "hello",
"suggested_assertion": "assert output includes MY_CONST"
},
"related_test": {"name": "test_help", "file": "tests/help.rs", "line": 10}
}
]
}
});
let records = gap_records_from_check_output_json(&payload.to_string())
.map_err(|e| format!("unexpected error: {e}"))?;
assert_eq!(records.len(), 1);
assert_eq!(records[0].kind, "MissingOutputContract");
Ok(())
}
#[test]
fn check_output_non_actionable_item_produces_record_without_repair_route() -> Result<(), String>
{
let payload = serde_json::json!({
"finding_alignment": {
"items": [
{
"evidence_class": "presentation_text",
"gap_state": "already_observed",
"actionability": "none",
"canonical_gap_id": "pt::OBSERVED"
}
]
}
});
let records = gap_records_from_check_output_json(&payload.to_string())
.map_err(|e| format!("parse failed: {e}"))?;
let r = records.first().ok_or("expected record")?;
assert_eq!(r.repairability, "no_action");
assert!(r.repair_route.is_none());
assert!(r.verification_commands.is_empty());
Ok(())
}
#[test]
fn check_output_item_with_static_limitations() -> Result<(), String> {
let payload = serde_json::json!({
"finding_alignment": {
"items": [
{
"evidence_class": "presentation_text",
"gap_state": "static_limitation",
"actionability": "none",
"canonical_gap_id": "pt::STATIC",
"static_limitations": [
{"category": "dynamic_text", "repair_route": "needs_runtime"}
]
}
]
}
});
let records = gap_records_from_check_output_json(&payload.to_string())
.map_err(|e| format!("parse failed: {e}"))?;
let r = records.first().ok_or("expected record")?;
assert_eq!(r.repairability, "analyzer_limitation");
assert_eq!(r.static_limit_kind.as_deref(), Some("dynamic_text"));
assert_eq!(r.static_limit_detail.as_deref(), Some("needs_runtime"));
Ok(())
}
#[test]
fn check_output_item_static_limit_kind_fallback_to_kind_field() -> Result<(), String> {
let payload = serde_json::json!({
"finding_alignment": {
"items": [
{
"evidence_class": "presentation_text",
"gap_state": "static_limitation",
"actionability": "none",
"canonical_gap_id": "pt::STATIC2",
"static_limitations": [
{"kind": "opaque_body", "reason": "body not visible"}
]
}
]
}
});
let records = gap_records_from_check_output_json(&payload.to_string())
.map_err(|e| format!("parse failed: {e}"))?;
let r = records.first().ok_or("expected record")?;
assert_eq!(r.static_limit_kind.as_deref(), Some("opaque_body"));
assert_eq!(r.static_limit_detail.as_deref(), Some("body not visible"));
Ok(())
}
#[test]
fn evidence_ids_from_alignment_item_deduplicates_and_falls_back() {
let item = serde_json::json!({
"raw_findings": [
{"evidence_record_ref": "id-1"},
{"evidence_record_ref": "id-1"},
{"source_id": "id-2", "evidence_record_ref": "id-1"},
{"source_id": "id-3"}
]
});
let ids = evidence_ids_from_alignment_item(&item, "fallback-id");
assert!(ids.contains(&"id-1".to_string()));
assert!(ids.contains(&"id-2".to_string()) || ids.contains(&"id-3".to_string()));
assert_eq!(ids.iter().filter(|id| *id == "id-1").count(), 1);
let item_no_findings = serde_json::json!({});
let ids = evidence_ids_from_alignment_item(&item_no_findings, "gap:fallback");
assert_eq!(ids, vec!["gap:fallback".to_string()]);
let item_empty_findings = serde_json::json!({"raw_findings": [{"kind": "exposed"}]});
let ids = evidence_ids_from_alignment_item(&item_empty_findings, "gap:fallback2");
assert_eq!(ids, vec!["gap:fallback2".to_string()]);
}
#[test]
fn evidence_ids_from_repo_evidence_deduplicates_source_ids() {
let evidence = serde_json::json!({
"raw_findings": [
{"source_id": "s1"},
{"source_id": "s2"},
{"source_id": "s1"}
]
});
let ids = evidence_ids_from_repo_evidence(&evidence, "seam-id");
assert_eq!(ids[0], "seam-id");
assert!(ids.contains(&"s1".to_string()));
assert!(ids.contains(&"s2".to_string()));
assert_eq!(ids.iter().filter(|id| *id == "s1").count(), 1);
let evidence_empty = serde_json::json!({});
let ids = evidence_ids_from_repo_evidence(&evidence_empty, "seam-only");
assert_eq!(ids, vec!["seam-only".to_string()]);
}
#[test]
fn first_raw_finding_expression_returns_first_expression() {
let evidence = serde_json::json!({
"raw_findings": [
{"expression": "fn foo()"},
{"expression": "fn bar()"}
]
});
assert_eq!(first_raw_finding_expression(&evidence), Some("fn foo()"));
let no_findings = serde_json::json!({});
assert_eq!(first_raw_finding_expression(&no_findings), None);
let empty = serde_json::json!({"raw_findings": []});
assert_eq!(first_raw_finding_expression(&empty), None);
let no_expr = serde_json::json!({"raw_findings": [{"kind": "exposed"}]});
assert_eq!(first_raw_finding_expression(&no_expr), None);
}
#[test]
fn repo_projection_eligibility_eligible_when_fully_repairable() {
let projections = projection_eligibility_from_repo_evidence(
"repairable",
true, true, true, "actionable",
);
assert!(projections["agent_packet"].eligible);
assert!(projections["lsp_diagnostic"].eligible);
assert!(projections["ripr_zero_count"].eligible);
assert!(projections["ripr_plus_count"].eligible);
assert!(projections["ci_summary"].eligible);
assert!(projections["report_packet"].eligible);
assert!(!projections["pr_comment"].eligible);
assert!(!projections["gate_candidate"].eligible);
}
#[test]
fn repo_projection_eligibility_ineligible_without_repair_route() {
let projections = projection_eligibility_from_repo_evidence(
"repairable",
false, true,
true,
"actionable",
);
assert!(!projections["agent_packet"].eligible);
assert!(!projections["lsp_diagnostic"].eligible);
assert!(!projections["ripr_zero_count"].eligible);
assert!(!projections["ripr_plus_count"].eligible);
}
#[test]
fn repo_projection_eligibility_lsp_ineligible_without_anchor() {
let projections = projection_eligibility_from_repo_evidence(
"repairable",
true,
true,
false, "actionable",
);
assert!(projections["agent_packet"].eligible);
assert!(!projections["lsp_diagnostic"].eligible);
}
#[test]
fn repo_projection_eligibility_ripr_counts_ineligible_when_not_actionable() {
let projections = projection_eligibility_from_repo_evidence(
"repairable",
true,
true,
true,
"already_observed",
);
assert!(projections["agent_packet"].eligible);
assert!(!projections["ripr_zero_count"].eligible);
assert!(!projections["ripr_plus_count"].eligible);
}
#[test]
fn pr_projection_eligibility_eligible_when_fully_repairable() {
let projections =
projection_eligibility_from_pr_evidence("repairable", true, true, true, "actionable");
assert!(projections["pr_comment"].eligible);
assert!(projections["agent_packet"].eligible);
assert!(projections["lsp_diagnostic"].eligible);
assert!(projections["ci_summary"].eligible);
assert!(projections["report_packet"].eligible);
assert!(!projections["gate_candidate"].eligible);
assert!(!projections["ripr_zero_count"].eligible);
assert!(!projections["ripr_plus_count"].eligible);
}
#[test]
fn pr_projection_eligibility_pr_comment_ineligible_when_not_actionable() {
let projections = projection_eligibility_from_pr_evidence(
"repairable",
true,
true,
true,
"already_observed",
);
assert!(!projections["pr_comment"].eligible);
}
#[test]
fn pr_projection_eligibility_ineligible_without_anchor() {
let projections = projection_eligibility_from_pr_evidence(
"repairable",
true,
true,
false, "actionable",
);
assert!(!projections["pr_comment"].eligible);
assert!(!projections["lsp_diagnostic"].eligible);
assert!(projections["agent_packet"].eligible);
}
#[test]
fn pr_projection_eligibility_not_repairable_disables_all_action_projections() {
let projections = projection_eligibility_from_pr_evidence(
"analyzer_limitation",
false,
false,
false,
"static_limitation",
);
assert!(!projections["pr_comment"].eligible);
assert!(!projections["agent_packet"].eligible);
assert!(!projections["lsp_diagnostic"].eligible);
}
#[test]
fn safe_gate_predicate_satisfied_requires_all_conditions() {
let good_predicate = SafeGatePredicate {
policy_target_enabled: true,
suppressed: false,
waived: false,
acknowledged_only: false,
baseline_known: false,
preview_language: false,
static_unknown_only: false,
};
let good_record = GapRecord {
gap_id: "gap:test".to_string(),
language: "rust".to_string(),
language_status: "stable".to_string(),
scope: "pr_local".to_string(),
policy_state: "new".to_string(),
repairability: "repairable".to_string(),
repair_route: Some(GapRepairRoute {
route_kind: "AddBoundaryAssertion".to_string(),
..GapRepairRoute::default()
}),
verification_commands: vec!["cargo test".to_string()],
safe_gate_predicate: Some(good_predicate.clone()),
..GapRecord::default()
};
assert!(safe_gate_predicate_satisfied(&good_record));
let no_predicate = GapRecord {
safe_gate_predicate: None,
..good_record.clone()
};
assert!(!safe_gate_predicate_satisfied(&no_predicate));
let wrong_lang = GapRecord {
language: "typescript".to_string(),
safe_gate_predicate: Some(good_predicate.clone()),
..good_record.clone()
};
assert!(!safe_gate_predicate_satisfied(&wrong_lang));
let preview = GapRecord {
language_status: "preview".to_string(),
safe_gate_predicate: Some(good_predicate.clone()),
..good_record.clone()
};
assert!(!safe_gate_predicate_satisfied(&preview));
let suppressed = GapRecord {
safe_gate_predicate: Some(SafeGatePredicate {
suppressed: true,
..good_predicate.clone()
}),
..good_record.clone()
};
assert!(!safe_gate_predicate_satisfied(&suppressed));
let not_enabled = GapRecord {
safe_gate_predicate: Some(SafeGatePredicate {
policy_target_enabled: false,
..good_predicate.clone()
}),
..good_record.clone()
};
assert!(!safe_gate_predicate_satisfied(¬_enabled));
let waived = GapRecord {
safe_gate_predicate: Some(SafeGatePredicate {
waived: true,
..good_predicate.clone()
}),
..good_record.clone()
};
assert!(!safe_gate_predicate_satisfied(&waived));
let ack_only = GapRecord {
safe_gate_predicate: Some(SafeGatePredicate {
acknowledged_only: true,
..good_predicate.clone()
}),
..good_record.clone()
};
assert!(!safe_gate_predicate_satisfied(&ack_only));
let baseline = GapRecord {
safe_gate_predicate: Some(SafeGatePredicate {
baseline_known: true,
..good_predicate.clone()
}),
..good_record.clone()
};
assert!(!safe_gate_predicate_satisfied(&baseline));
let static_only = GapRecord {
safe_gate_predicate: Some(SafeGatePredicate {
static_unknown_only: true,
..good_predicate
}),
..good_record.clone()
};
assert!(!safe_gate_predicate_satisfied(&static_only));
let wrong_scope = GapRecord {
scope: "repo_scoped".to_string(),
..good_record.clone()
};
assert!(!safe_gate_predicate_satisfied(&wrong_scope));
let blocked_policy = GapRecord {
policy_state: "blocked".to_string(),
..good_record.clone()
};
assert!(safe_gate_predicate_satisfied(&blocked_policy));
let wrong_policy = GapRecord {
policy_state: "not_policy_targeted".to_string(),
..good_record.clone()
};
assert!(!safe_gate_predicate_satisfied(&wrong_policy));
let no_route = GapRecord {
repair_route: None,
..good_record.clone()
};
assert!(!safe_gate_predicate_satisfied(&no_route));
let no_cmds = GapRecord {
verification_commands: vec![],
..good_record
};
assert!(!safe_gate_predicate_satisfied(&no_cmds));
}
#[test]
fn validate_record_warns_on_blank_gap_id() {
let record = GapRecord {
gap_id: "".to_string(),
..GapRecord::default()
};
let mut warnings = Vec::new();
validate_record(&record, &mut warnings);
assert!(warnings.iter().any(|w| w.contains("missing gap_id")));
}
#[test]
fn validate_record_warns_on_blank_kind() {
let record = GapRecord {
gap_id: "gap:test".to_string(),
kind: "".to_string(),
..GapRecord::default()
};
let mut warnings = Vec::new();
validate_record(&record, &mut warnings);
assert!(warnings.iter().any(|w| w.contains("missing kind")));
}
#[test]
fn validate_record_warns_pr_comment_eligible_without_dedupe_fingerprint() {
let mut projections = BTreeMap::new();
insert_projection(&mut projections, "pr_comment", true, "test");
let record = GapRecord {
gap_id: "gap:test".to_string(),
kind: "SomeKind".to_string(),
projection_eligibility: projections,
anchor: Some(GapAnchor {
dedupe_fingerprint: None,
..GapAnchor::default()
}),
..GapRecord::default()
};
let mut warnings = Vec::new();
validate_record(&record, &mut warnings);
assert!(
warnings
.iter()
.any(|w| w.contains("missing anchor.dedupe_fingerprint"))
);
}
#[test]
fn validate_record_warns_artifact_missing_without_regeneration_commands() {
let record = GapRecord {
gap_id: "gap:test".to_string(),
kind: "MissingArtifact".to_string(),
scope: "artifact_missing".to_string(),
regeneration_commands: vec![],
..GapRecord::default()
};
let mut warnings = Vec::new();
validate_record(&record, &mut warnings);
assert!(
warnings
.iter()
.any(|w| w.contains("no regeneration_commands"))
);
}
#[test]
fn validate_record_no_warnings_for_clean_record() {
let record = GapRecord {
gap_id: "gap:clean".to_string(),
kind: "NoActionAlreadyObserved".to_string(),
repairability: "no_action".to_string(),
..GapRecord::default()
};
let mut warnings = Vec::new();
validate_record(&record, &mut warnings);
assert!(warnings.is_empty());
}
#[test]
fn summarize_records_counts_all_categories() {
let mut projections_gate = BTreeMap::new();
insert_projection(&mut projections_gate, "gate_candidate", true, "test");
insert_projection(&mut projections_gate, "ripr_zero_count", true, "test");
insert_projection(&mut projections_gate, "ripr_plus_count", true, "test");
insert_projection(&mut projections_gate, "pr_comment", true, "test");
insert_projection(&mut projections_gate, "agent_packet", true, "test");
let records = vec![
GapRecord {
gap_id: "gap:r1".to_string(),
kind: "MissingValueAssertion".to_string(),
repairability: "repairable".to_string(),
..GapRecord::default()
},
GapRecord {
gap_id: "gap:r2".to_string(),
kind: "StaticLimitation".to_string(),
repairability: "analyzer_limitation".to_string(),
..GapRecord::default()
},
GapRecord {
gap_id: "gap:r3".to_string(),
kind: "NoActionAlreadyObserved".to_string(),
repairability: "no_action".to_string(),
..GapRecord::default()
},
GapRecord {
gap_id: "gap:r4".to_string(),
kind: "NoActionInternal".to_string(),
repairability: "no_action".to_string(),
..GapRecord::default()
},
GapRecord {
gap_id: "gap:r5".to_string(),
kind: "MissingArtifact".to_string(),
scope: "artifact_missing".to_string(),
repairability: "unknown".to_string(),
..GapRecord::default()
},
GapRecord {
gap_id: "gap:r6".to_string(),
kind: "MissingOutputContract".to_string(),
projection_eligibility: projections_gate.clone(),
repairability: "no_action".to_string(),
..GapRecord::default()
},
GapRecord {
gap_id: "gap:r7".to_string(),
kind: "MissingValueAssertion".to_string(),
language_status: "preview".to_string(),
repairability: "no_action".to_string(),
..GapRecord::default()
},
GapRecord {
gap_id: "gap:r8".to_string(),
kind: "MissingValueAssertion".to_string(),
repairability: "no_action".to_string(),
receipt: Some(GapReceipt {
movement: Some("improved".to_string()),
..GapReceipt::default()
}),
..GapRecord::default()
},
GapRecord {
gap_id: "gap:r9".to_string(),
kind: "MissingValueAssertion".to_string(),
repairability: "no_action".to_string(),
receipt: Some(GapReceipt {
movement: Some("unchanged_after_attempt".to_string()),
..GapReceipt::default()
}),
..GapRecord::default()
},
];
let summary = summarize_records(&records);
assert_eq!(summary.records_total, 9);
assert_eq!(summary.repairable_total, 1);
assert_eq!(summary.static_limitation_total, 1);
assert_eq!(summary.no_action_total, 6); assert_eq!(summary.missing_artifact_total, 1);
assert_eq!(summary.projection_gate_candidate, 1);
assert_eq!(summary.ripr_zero_target_count, 1);
assert_eq!(summary.ripr_plus_target_count, 1);
assert_eq!(summary.projection_pr_comment_eligible, 1);
assert_eq!(summary.projection_agent_packet_eligible, 1);
assert_eq!(summary.missing_output_contract_total, 1);
assert_eq!(summary.receipt_improved_total, 1);
assert_eq!(summary.receipt_unchanged_after_attempt_total, 1);
assert_eq!(summary.preview_ineligible_total, 1);
}
#[test]
fn render_record_markdown_without_optional_fields() {
let record = GapRecord {
gap_id: "gap:bare".to_string(),
kind: "NoActionAlreadyObserved".to_string(),
scope: "repo_scoped".to_string(),
policy_state: "baseline".to_string(),
repairability: "no_action".to_string(),
evidence_class: "already_observed".to_string(),
gap_state: "already_observed".to_string(),
language: "rust".to_string(),
language_status: "stable".to_string(),
anchor: None,
repair_route: None,
projection_eligibility: BTreeMap::new(),
verification_commands: vec![],
regeneration_commands: vec![],
receipt: None,
..GapRecord::default()
};
let mut out = String::new();
render_record_markdown(&record, &mut out);
assert!(out.contains("### `gap:bare`"));
assert!(!out.contains("Anchor:"));
assert!(!out.contains("Repair:"));
assert!(!out.contains("Eligible projections:"));
assert!(!out.contains("Verify:"));
assert!(!out.contains("Regenerate:"));
assert!(!out.contains("Receipt movement:"));
}
#[test]
fn render_record_markdown_with_anchor_no_line_no_owner() {
let record = GapRecord {
gap_id: "gap:anchor".to_string(),
kind: "MissingBoundaryAssertion".to_string(),
anchor: Some(GapAnchor {
file: Some("src/lib.rs".to_string()),
line: None,
owner: None,
dedupe_fingerprint: None,
}),
..GapRecord::default()
};
let mut out = String::new();
render_record_markdown(&record, &mut out);
assert!(out.contains("Anchor: `src/lib.rs`"));
assert!(!out.contains("owner"));
}
#[test]
fn render_record_markdown_repair_route_with_no_target_file_no_assertion() {
let record = GapRecord {
gap_id: "gap:nofile".to_string(),
kind: "MissingValueAssertion".to_string(),
repair_route: Some(GapRepairRoute {
route_kind: "AddValueAssertion".to_string(),
target_file: None,
assertion_shape: None,
..GapRepairRoute::default()
}),
..GapRecord::default()
};
let mut out = String::new();
render_record_markdown(&record, &mut out);
assert!(out.contains("Repair: `AddValueAssertion`"));
assert!(!out.contains("in `"));
assert!(!out.contains("Assertion or observer:"));
}
#[test]
fn eligible_projection_names_returns_only_eligible() {
let mut projections = BTreeMap::new();
insert_projection(&mut projections, "ci_summary", true, "test");
insert_projection(&mut projections, "agent_packet", false, "not_repairable");
insert_projection(&mut projections, "report_packet", true, "test");
let record = GapRecord {
projection_eligibility: projections,
..GapRecord::default()
};
let names = eligible_projection_names(&record);
assert!(names.contains(&"ci_summary".to_string()));
assert!(names.contains(&"report_packet".to_string()));
assert!(!names.contains(&"agent_packet".to_string()));
assert_eq!(names.len(), 2);
}
#[test]
fn parse_gap_records_json_invalid_json_returns_error() -> Result<(), String> {
let err_msg = parse_gap_records_json("{invalid")
.err()
.ok_or("expected error for invalid JSON")?;
assert!(
err_msg.contains("invalid JSON"),
"unexpected error: {err_msg}"
);
Ok(())
}
#[test]
fn gap_records_from_value_cases_key_parses_expected_gap_record() -> Result<(), String> {
let value = serde_json::json!({
"cases": [
{
"id": "case1",
"expected_gap_record": {
"gap_id": "gap:case1",
"kind": "MissingBoundaryAssertion",
"language": "rust",
"language_status": "stable",
"scope": "pr_local",
"repairability": "repairable",
"authority_boundary": "gate_decision_artifact_only"
}
}
]
});
let records =
parse_gap_records_json(&value.to_string()).map_err(|e| format!("failed: {e}"))?;
assert_eq!(records.len(), 1);
assert_eq!(records[0].gap_id, "gap:case1");
Ok(())
}
#[test]
fn gap_records_from_value_cases_key_missing_expected_gap_record_returns_error()
-> Result<(), String> {
let value = serde_json::json!({
"cases": [{"id": "case-bad"}]
});
let err_msg = parse_gap_records_json(&value.to_string())
.err()
.ok_or("expected error for missing expected_gap_record")?;
assert!(
err_msg.contains("missing expected_gap_record"),
"unexpected error: {err_msg}"
);
Ok(())
}
#[test]
fn gap_records_from_value_no_known_key_returns_error() -> Result<(), String> {
let value = serde_json::json!({"unknown_key": []});
let err_msg = parse_gap_records_json(&value.to_string())
.err()
.ok_or("expected error for unknown key")?;
assert!(
err_msg.contains("expected records"),
"unexpected error: {err_msg}"
);
Ok(())
}
#[test]
fn build_report_with_repo_exposure_source_kind() -> Result<(), String> {
let payload = serde_json::json!({
"seams": [
make_repo_exposure_seam("actionable", "add_focused_test", "error_variant")
]
});
let report = build_gap_decision_ledger_report(GapDecisionLedgerInput {
root: ".".to_string(),
generated_at: "test".to_string(),
source_kind: GapDecisionLedgerSourceKind::RepoExposure,
records_path: "exposure.json".to_string(),
records_json: Ok(payload.to_string()),
});
assert_eq!(report.summary.records_total, 1);
assert_eq!(report.records[0].kind, "MissingErrorDiscriminator");
Ok(())
}
#[test]
fn build_report_with_repo_exposure_parse_error_is_blocked() {
let report = build_gap_decision_ledger_report(GapDecisionLedgerInput {
root: ".".to_string(),
generated_at: "test".to_string(),
source_kind: GapDecisionLedgerSourceKind::RepoExposure,
records_path: "exposure.json".to_string(),
records_json: Ok("not-json".to_string()),
});
assert_eq!(report.status, "blocked");
assert!(report.warnings[0].contains("parse exposure.json failed"));
}
#[test]
fn build_report_with_check_output_parse_error_is_blocked() {
let report = build_gap_decision_ledger_report(GapDecisionLedgerInput {
root: ".".to_string(),
generated_at: "test".to_string(),
source_kind: GapDecisionLedgerSourceKind::CheckOutput,
records_path: "check.json".to_string(),
records_json: Ok("not-json".to_string()),
});
assert_eq!(report.status, "blocked");
assert!(report.warnings[0].contains("parse check.json failed"));
}
#[test]
fn check_output_repair_route_uses_related_test_and_suggested_assertion() -> Result<(), String> {
let payload = serde_json::json!({
"finding_alignment": {
"items": [{
"evidence_class": "presentation_text",
"gap_state": "actionable",
"actionability": "add_focused_test",
"canonical_gap_id": "pt::LABEL",
"related_test": {
"name": "test_label_output",
"file": "tests/output_test.rs",
"line": 42
},
"presentation_text": {
"constant_name": "LABEL",
"text_literal": "some label",
"suggested_assertion": "assert_eq!(output, \"some label\")"
},
"raw_findings": []
}]
}
});
let records = gap_records_from_check_output_json(&payload.to_string())
.map_err(|e| format!("parse failed: {e}"))?;
let route = records[0]
.repair_route
.as_ref()
.ok_or("expected repair_route")?;
assert_eq!(route.related_test.as_deref(), Some("test_label_output"));
assert_eq!(route.target_file.as_deref(), Some("tests/output_test.rs"));
assert_eq!(route.target_line, Some(42));
assert_eq!(
route.assertion_shape.as_deref(),
Some("assert_eq!(output, \"some label\")")
);
assert_eq!(route.changed_behavior.as_deref(), Some("some label"));
assert_eq!(route.stop_conditions.len(), 2);
Ok(())
}
#[test]
fn check_output_repair_route_changed_behavior_falls_back_to_constant_name() -> Result<(), String>
{
let payload = serde_json::json!({
"finding_alignment": {
"items": [{
"evidence_class": "presentation_text",
"gap_state": "actionable",
"actionability": "add_focused_test",
"canonical_gap_id": "pt::LABEL2",
"presentation_text": {
"constant_name": "MY_CONST"
},
"raw_findings": []
}]
}
});
let records = gap_records_from_check_output_json(&payload.to_string())
.map_err(|e| format!("parse failed: {e}"))?;
let route = records[0]
.repair_route
.as_ref()
.ok_or("expected repair_route")?;
assert_eq!(route.changed_behavior.as_deref(), Some("MY_CONST"));
Ok(())
}
#[test]
fn check_output_verify_command_fallback_when_not_presentation_text_repairable()
-> Result<(), String> {
let payload = serde_json::json!({
"finding_alignment": {
"items": [{
"evidence_class": "presentation_text",
"gap_state": "static_limitation",
"actionability": "none",
"canonical_gap_id": "pt::SL",
"verify_command": "cargo xtask check-output-contracts"
}]
}
});
let records = gap_records_from_check_output_json(&payload.to_string())
.map_err(|e| format!("parse failed: {e}"))?;
assert_eq!(
records[0].verification_commands,
vec!["cargo xtask check-output-contracts".to_string()]
);
Ok(())
}
#[test]
fn repo_repair_route_uses_assertion_example_and_recommended_repair_fallback()
-> Result<(), String> {
let seam_with_example = serde_json::json!({
"seams": [{
"evidence_record": {
"seam_id": "s-example",
"seam_kind": "predicate_boundary",
"recommendation": {
"assertion_shape": {"example": "assert!(result.is_ok())"},
"recommended_test": {"file": "tests/foo.rs"}
},
"raw_findings": [
{"expression": "fn example()"}
],
"canonical_item": {
"gap_state": "actionable",
"actionability": "add_focused_test",
"related_test": {
"name": "test_example",
"file": "tests/foo.rs",
"line": 5
}
}
}
}]
});
let records = gap_records_from_repo_exposure_json(&seam_with_example.to_string())
.map_err(|e| format!("parse failed: {e}"))?;
let route = records[0]
.repair_route
.as_ref()
.ok_or("expected repair_route")?;
assert_eq!(
route.assertion_shape.as_deref(),
Some("assert!(result.is_ok())")
);
assert_eq!(route.changed_behavior.as_deref(), Some("fn example()"));
assert_eq!(route.related_test.as_deref(), Some("test_example"));
assert_eq!(route.target_line, Some(5));
let seam_recommended = serde_json::json!({
"seams": [{
"evidence_record": {
"seam_id": "s-rec",
"seam_kind": "match_arm",
"canonical_item": {
"gap_state": "actionable",
"actionability": "add_focused_test",
"recommended_repair": "Add a branch assertion for arm."
}
}
}]
});
let records = gap_records_from_repo_exposure_json(&seam_recommended.to_string())
.map_err(|e| format!("parse failed: {e}"))?;
let route = records[0]
.repair_route
.as_ref()
.ok_or("expected repair_route for recommended")?;
assert_eq!(
route.assertion_shape.as_deref(),
Some("Add a branch assertion for arm.")
);
Ok(())
}
#[test]
fn render_gap_decision_ledger_markdown_no_records_shows_placeholder() {
let report = build_gap_decision_ledger_report(GapDecisionLedgerInput {
root: ".".to_string(),
generated_at: "test".to_string(),
source_kind: GapDecisionLedgerSourceKind::Records,
records_path: "empty.json".to_string(),
records_json: Ok("[]".to_string()),
});
let markdown = render_gap_decision_ledger_markdown(&report);
assert!(markdown.contains("No gap records were supplied."));
assert!(markdown.contains("Status: `blocked`"));
}
#[test]
fn render_gap_decision_ledger_markdown_with_warnings() {
let report = build_gap_decision_ledger_report(GapDecisionLedgerInput {
root: "/repo".to_string(),
generated_at: "test".to_string(),
source_kind: GapDecisionLedgerSourceKind::Records,
records_path: "bad.json".to_string(),
records_json: Ok("not-json".to_string()),
});
let markdown = render_gap_decision_ledger_markdown(&report);
assert!(markdown.contains("## Warnings"));
assert!(markdown.contains("invalid JSON"));
}
#[test]
fn parse_record_array_reports_index_on_invalid_record() -> Result<(), String> {
let value = serde_json::json!([
{
"gap_id": "gap:valid",
"kind": "NoActionAlreadyObserved",
"authority_boundary": "gate_decision_artifact_only"
},
"not-a-record"
]);
let msg = parse_gap_records_json(&value.to_string())
.err()
.ok_or("expected error for invalid record at index 1")?;
assert!(msg.contains("record 1"), "expected 'record 1' in: {msg}");
assert!(
msg.contains("invalid GapRecord"),
"expected 'invalid GapRecord' in: {msg}"
);
Ok(())
}
}