use std::process::ExitCode;
use fallow_types::results::AnalysisResults;
use rustc_hash::FxHashSet;
use serde::Serialize;
use crate::audit::AuditResult;
pub const REVIEW_BRIEF_SCHEMA_VERSION: u32 = 5;
const RISK_HIGH_FILES: usize = 20;
const RISK_HIGH_LINES: i64 = 500;
const RISK_MEDIUM_FILES: usize = 5;
const RISK_MEDIUM_LINES: i64 = 100;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ReviewBriefSchemaVersion(pub u32);
impl Default for ReviewBriefSchemaVersion {
fn default() -> Self {
Self(REVIEW_BRIEF_SCHEMA_VERSION)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum RiskClass {
Low,
Medium,
High,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ReviewEffort {
Glance,
Review,
DeepDive,
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct DiffTriage {
pub files: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hunks: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub net_lines: Option<i64>,
pub risk_class: RiskClass,
pub review_effort: ReviewEffort,
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct GraphFacts {
pub exports_added: usize,
pub api_width_delta: i64,
pub reachable_from: Vec<String>,
pub boundaries_touched: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ImpactClosureFacts {
pub affected_not_shown: Vec<String>,
pub coordination_gap: Vec<CoordinationGapFact>,
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct CoordinationGapFact {
pub changed_file: String,
pub consumer_file: String,
pub consumed_symbols: Vec<String>,
pub note: String,
}
const COORDINATION_GAP_NOTE: &str = "syntactic attention pointer, not a correctness proof";
#[derive(Debug, Clone, Default, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct PartitionFacts {
pub units: Vec<ReviewUnitFact>,
pub order: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ReviewUnitFact {
pub module_dir: String,
pub files: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ReviewDeltas {
pub boundary_introduced: Vec<String>,
pub cycle_introduced: Vec<String>,
pub public_api_added: Vec<String>,
}
#[must_use]
#[allow(
clippy::implicit_hasher,
reason = "callers always pass the audit FxHashSet key sets; generalizing the hasher adds noise"
)]
pub fn build_review_deltas(
head_boundary: &FxHashSet<String>,
base_boundary: &FxHashSet<String>,
head_cycles: &FxHashSet<String>,
base_cycles: &FxHashSet<String>,
head_public_api: &FxHashSet<String>,
base_public_api: &FxHashSet<String>,
) -> ReviewDeltas {
use crate::audit::review_deltas::introduced_keys;
ReviewDeltas {
boundary_introduced: introduced_keys(head_boundary, base_boundary),
cycle_introduced: introduced_keys(head_cycles, base_cycles),
public_api_added: introduced_keys(head_public_api, base_public_api),
}
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[cfg_attr(
feature = "schema",
schemars(title = "fallow audit --brief --format json")
)]
pub struct ReviewBriefOutput {
pub schema_version: ReviewBriefSchemaVersion,
pub version: String,
pub command: String,
pub triage: DiffTriage,
pub graph_facts: GraphFacts,
pub partition: PartitionFacts,
pub impact_closure: ImpactClosureFacts,
pub focus: crate::audit_focus::FocusMap,
pub deltas: ReviewDeltas,
pub weakening: Vec<crate::audit::weakening::WeakeningSignal>,
pub routing: crate::audit::routing::RoutingFacts,
pub decisions: crate::audit_decision_surface::DecisionSurface,
}
#[must_use]
pub fn classify_risk(files: usize, net_lines: Option<i64>) -> RiskClass {
let lines = net_lines.unwrap_or(0).abs();
if files >= RISK_HIGH_FILES || lines >= RISK_HIGH_LINES {
RiskClass::High
} else if files >= RISK_MEDIUM_FILES || lines >= RISK_MEDIUM_LINES {
RiskClass::Medium
} else {
RiskClass::Low
}
}
#[must_use]
pub fn review_effort_for(risk: RiskClass) -> ReviewEffort {
match risk {
RiskClass::Low => ReviewEffort::Glance,
RiskClass::Medium => ReviewEffort::Review,
RiskClass::High => ReviewEffort::DeepDive,
}
}
#[must_use]
pub fn build_triage(result: &AuditResult) -> DiffTriage {
let files = result.changed_files_count;
let hunks = None;
let net_lines = None;
let risk_class = classify_risk(files, net_lines);
DiffTriage {
files,
hunks,
net_lines,
risk_class,
review_effort: review_effort_for(risk_class),
}
}
#[must_use]
pub fn derive_graph_facts(
results: &AnalysisResults,
closure: Option<&fallow_core::graph::ImpactClosurePaths>,
) -> GraphFacts {
let mut zones: FxHashSet<String> = FxHashSet::default();
for finding in &results.boundary_violations {
zones.insert(finding.violation.from_zone.clone());
zones.insert(finding.violation.to_zone.clone());
}
let mut boundaries_touched: Vec<String> = zones.into_iter().collect();
boundaries_touched.sort();
let reachable_from = closure
.map(|c| c.affected_not_shown.clone())
.unwrap_or_default();
GraphFacts {
exports_added: 0,
api_width_delta: 0,
reachable_from,
boundaries_touched,
}
}
#[must_use]
fn build_impact_closure_facts(result: &AuditResult) -> ImpactClosureFacts {
let Some(closure) = result
.check
.as_ref()
.and_then(|c| c.impact_closure.as_ref())
else {
return ImpactClosureFacts::default();
};
let coordination_gap = closure
.coordination_gap
.iter()
.map(|gap| CoordinationGapFact {
changed_file: gap.changed_file.clone(),
consumer_file: gap.consumer_file.clone(),
consumed_symbols: gap.consumed_symbols.clone(),
note: COORDINATION_GAP_NOTE.to_string(),
})
.collect();
ImpactClosureFacts {
affected_not_shown: closure.affected_not_shown.clone(),
coordination_gap,
}
}
#[must_use]
fn build_partition_facts(result: &AuditResult) -> PartitionFacts {
let Some(partition) = result
.check
.as_ref()
.and_then(|c| c.partition_order.as_ref())
else {
return PartitionFacts::default();
};
let units = partition
.units
.iter()
.map(|unit| ReviewUnitFact {
module_dir: unit.module_dir.clone(),
files: unit.files.clone(),
})
.collect();
PartitionFacts {
units,
order: partition.order.clone(),
}
}
#[must_use]
fn build_focus_map(result: &AuditResult, deltas: &ReviewDeltas) -> crate::audit_focus::FocusMap {
use crate::audit_focus::{BoundaryZoneFile, FocusInputs, build_focus_map};
let Some(check) = result.check.as_ref() else {
return crate::audit_focus::FocusMap::default();
};
let Some(graph_facts) = check.focus_facts.as_ref() else {
return crate::audit_focus::FocusMap::default();
};
let root = &check.config.root;
let mut seen_pairs: FxHashSet<String> = FxHashSet::default();
let mut boundary_files: Vec<BoundaryZoneFile> = Vec::new();
for finding in &check.results.boundary_violations {
let key = crate::audit::review_deltas::boundary_edge_key(finding);
if !deltas.boundary_introduced.contains(&key) || !seen_pairs.insert(key) {
continue;
}
boundary_files.push(BoundaryZoneFile {
from_file: crate::audit::keys::relative_key_path(&finding.violation.from_path, root),
});
}
let coordination_changed_files: Vec<String> = check
.impact_closure
.as_ref()
.map(|c| {
let mut files: Vec<String> = c
.coordination_gap
.iter()
.map(|gap| gap.changed_file.clone())
.collect();
files.sort_unstable();
files.dedup();
files
})
.unwrap_or_default();
let taint_touched_files = taint_touched_files(result.check.as_ref());
build_focus_map(&FocusInputs {
graph_facts,
boundary_files: &boundary_files,
public_api_added: &deltas.public_api_added,
coordination_changed_files: &coordination_changed_files,
taint_touched_files: &taint_touched_files,
})
}
fn taint_touched_files(check: Option<&crate::check::CheckResult>) -> Vec<String> {
let Some(check) = check else {
return Vec::new();
};
let root = &check.config.root;
let mut touched: FxHashSet<String> = FxHashSet::default();
for finding in &check.results.security_findings {
touched.insert(crate::audit::keys::relative_key_path(&finding.path, root));
for hop in &finding.trace {
touched.insert(crate::audit::keys::relative_key_path(&hop.path, root));
}
}
let mut files: Vec<String> = touched.into_iter().collect();
files.sort();
files
}
#[must_use]
pub fn build_brief_output(result: &AuditResult) -> ReviewBriefOutput {
let triage = build_triage(result);
let closure = result
.check
.as_ref()
.and_then(|c| c.impact_closure.as_ref());
let deltas = result.review_deltas.clone().unwrap_or_default();
let mut graph_facts = result.check.as_ref().map_or_else(
|| GraphFacts {
exports_added: 0,
api_width_delta: 0,
reachable_from: Vec::new(),
boundaries_touched: Vec::new(),
},
|check| derive_graph_facts(&check.results, closure),
);
let added = deltas.public_api_added.len();
graph_facts.exports_added = added;
graph_facts.api_width_delta = i64::try_from(added).unwrap_or(i64::MAX);
let partition = build_partition_facts(result);
let impact_closure = build_impact_closure_facts(result);
let focus = build_focus_map(result, &deltas);
ReviewBriefOutput {
schema_version: ReviewBriefSchemaVersion::default(),
version: env!("CARGO_PKG_VERSION").to_string(),
command: "audit-brief".to_string(),
triage,
graph_facts,
partition,
impact_closure,
focus,
deltas,
weakening: result.weakening_signals.clone(),
routing: result.routing.clone().unwrap_or_default(),
decisions: result.decision_surface.clone().unwrap_or_default(),
}
}
fn insert_brief_triage_json(
obj: &mut serde_json::Map<String, serde_json::Value>,
brief: &ReviewBriefOutput,
) {
if let Ok(value) = serde_json::to_value(&brief.triage) {
obj.insert("triage".into(), value);
}
}
fn insert_brief_graph_facts_json(
obj: &mut serde_json::Map<String, serde_json::Value>,
brief: &ReviewBriefOutput,
) {
if let Ok(value) = serde_json::to_value(&brief.graph_facts) {
obj.insert("graph_facts".into(), value);
}
}
fn insert_brief_partition_json(
obj: &mut serde_json::Map<String, serde_json::Value>,
brief: &ReviewBriefOutput,
) {
if let Ok(value) = serde_json::to_value(&brief.partition) {
obj.insert("partition".into(), value);
}
}
fn insert_brief_impact_closure_json(
obj: &mut serde_json::Map<String, serde_json::Value>,
brief: &ReviewBriefOutput,
) {
if let Ok(value) = serde_json::to_value(&brief.impact_closure) {
obj.insert("impact_closure".into(), value);
}
}
fn insert_brief_focus_json(
obj: &mut serde_json::Map<String, serde_json::Value>,
brief: &ReviewBriefOutput,
) {
if let Ok(value) = serde_json::to_value(&brief.focus) {
obj.insert("focus".into(), value);
}
}
fn insert_brief_e3_json(
obj: &mut serde_json::Map<String, serde_json::Value>,
brief: &ReviewBriefOutput,
) {
if let Ok(value) = serde_json::to_value(&brief.deltas) {
obj.insert("deltas".into(), value);
}
if let Ok(value) = serde_json::to_value(&brief.weakening) {
obj.insert("weakening".into(), value);
}
if let Ok(value) = serde_json::to_value(&brief.routing) {
obj.insert("routing".into(), value);
}
}
fn insert_brief_decisions_json(
obj: &mut serde_json::Map<String, serde_json::Value>,
brief: &ReviewBriefOutput,
) {
if let Ok(value) = serde_json::to_value(&brief.decisions) {
obj.insert("decisions".into(), value);
}
}
fn insert_brief_subtract_json(
obj: &mut serde_json::Map<String, serde_json::Value>,
result: &AuditResult,
) -> Result<(), ExitCode> {
if let Some(ref check) = result.check {
crate::audit::insert_audit_dead_code_json(obj, result, check)?;
}
if let Some(ref dupes) = result.dupes {
crate::audit::insert_audit_duplication_json(obj, result, dupes)?;
}
if let Some(ref health) = result.health {
crate::audit::insert_audit_health_json(obj, result, health)?;
}
Ok(())
}
fn build_brief_json(result: &AuditResult) -> Result<serde_json::Value, ExitCode> {
let brief = build_brief_output(result);
let mut obj = serde_json::Map::new();
obj.insert(
"schema_version".into(),
serde_json::to_value(brief.schema_version).unwrap_or(serde_json::Value::Null),
);
obj.insert(
"version".into(),
serde_json::Value::String(brief.version.clone()),
);
obj.insert(
"command".into(),
serde_json::Value::String(brief.command.clone()),
);
crate::audit::insert_audit_json_header(&mut obj, result);
obj.insert(
"schema_version".into(),
serde_json::to_value(brief.schema_version).unwrap_or(serde_json::Value::Null),
);
obj.insert(
"command".into(),
serde_json::Value::String(brief.command.clone()),
);
insert_brief_decisions_json(&mut obj, &brief);
insert_brief_triage_json(&mut obj, &brief);
insert_brief_graph_facts_json(&mut obj, &brief);
insert_brief_partition_json(&mut obj, &brief);
insert_brief_impact_closure_json(&mut obj, &brief);
insert_brief_focus_json(&mut obj, &brief);
insert_brief_e3_json(&mut obj, &brief);
insert_brief_subtract_json(&mut obj, result)?;
Ok(serde_json::Value::Object(obj))
}
fn print_brief_json(result: &AuditResult) -> ExitCode {
match build_brief_json(result) {
Ok(mut output) => {
crate::output_envelope::apply_root_kind(&mut output, "audit-brief");
crate::output_envelope::attach_telemetry_meta(&mut output);
let _ = crate::report::emit_json(&output, "audit-brief");
ExitCode::SUCCESS
}
Err(_) => ExitCode::SUCCESS,
}
}
fn print_brief_human(result: &AuditResult, quiet: bool, explain: bool, show_deprioritized: bool) {
let brief = build_brief_output(result);
if !quiet {
eprintln!();
print_decision_surface_human(&brief.decisions);
eprintln!(
"Review brief (drill-down): {} changed file{} vs {} \u{00b7} risk {} \u{00b7} effort {}",
result.changed_files_count,
crate::report::plural(result.changed_files_count),
result.base_ref,
risk_label(brief.triage.risk_class),
effort_label(brief.triage.review_effort),
);
if !brief.graph_facts.boundaries_touched.is_empty() {
eprintln!(
" boundaries touched: {}",
brief.graph_facts.boundaries_touched.join(", ")
);
}
print_partition_human(&brief.partition);
print_impact_closure_human(&brief.impact_closure);
print_focus_human(&brief.focus, show_deprioritized);
print_deltas_human(&brief.deltas);
print_weakening_human(&brief.weakening);
print_routing_human(&brief.routing);
}
crate::audit::print_audit_findings(result, quiet, explain, false);
}
fn print_partition_human(partition: &PartitionFacts) {
if partition.units.is_empty() {
return;
}
eprintln!(
" partition: {} unit{} (by module)",
partition.units.len(),
crate::report::plural(partition.units.len()),
);
if !partition.order.is_empty() {
let labeled: Vec<String> = partition.order.iter().map(|dir| unit_label(dir)).collect();
eprintln!(" review order: {}", labeled.join(" \u{2192} "));
}
}
fn unit_label(module_dir: &str) -> String {
if module_dir.is_empty() {
"<root>".to_string()
} else {
module_dir.to_string()
}
}
fn print_impact_closure_human(closure: &ImpactClosureFacts) {
if !closure.affected_not_shown.is_empty() {
eprintln!(
" impact closure: {} file{} affected beyond the diff",
closure.affected_not_shown.len(),
crate::report::plural(closure.affected_not_shown.len()),
);
}
for gap in &closure.coordination_gap {
eprintln!(
" coordination gap: {} consumes {} from {} (not in this diff)",
gap.consumer_file,
gap.consumed_symbols.join(", "),
gap.changed_file,
);
}
}
fn print_focus_human(focus: &crate::audit_focus::FocusMap, show_deprioritized: bool) {
if focus.total_units() == 0 {
return;
}
if !focus.review_here.is_empty() {
eprintln!(
" focus: {} unit{} to review here (of {} changed)",
focus.review_here.len(),
crate::report::plural(focus.review_here.len()),
focus.total_units(),
);
for unit in &focus.review_here {
eprintln!(
" [{}] {}: {}",
unit.label.token(),
unit.file,
unit.reason
);
for flag in &unit.confidence {
eprintln!(" confidence {}", flag.message());
}
}
}
if focus.deprioritized.is_empty() {
return;
}
if show_deprioritized {
eprintln!(" de-prioritized ({}):", focus.deprioritized.len());
for unit in &focus.deprioritized {
eprintln!(
" [{}] {}: {}",
unit.label.token(),
unit.file,
unit.reason
);
for flag in &unit.confidence {
eprintln!(" confidence {}", flag.message());
}
}
} else {
eprintln!(
" de-prioritized: {} unit{} (run with --show-deprioritized to list)",
focus.deprioritized.len(),
crate::report::plural(focus.deprioritized.len()),
);
}
}
fn print_deltas_human(deltas: &ReviewDeltas) {
for edge in &deltas.boundary_introduced {
eprintln!(" new boundary edge: {edge} (not present at base)");
}
for cycle in &deltas.cycle_introduced {
eprintln!(" new circular dependency: {cycle} (not present at base)");
}
if !deltas.public_api_added.is_empty() {
eprintln!(
" public API surface widened by {} export{} (exports-aware)",
deltas.public_api_added.len(),
crate::report::plural(deltas.public_api_added.len()),
);
}
}
fn print_weakening_human(signals: &[crate::audit::weakening::WeakeningSignal]) {
if signals.is_empty() {
return;
}
eprintln!(
" weakening signals ({}, reviewer-private, advisory):",
signals.len()
);
for signal in signals {
eprintln!(
" {}: {} in {}",
weakening_label(signal.kind),
signal.evidence,
signal.file,
);
}
}
fn print_routing_human(routing: &crate::audit::routing::RoutingFacts) {
for unit in &routing.units {
if unit.expert.is_empty() {
continue;
}
let bus = if unit.bus_factor_one {
" (bus-factor 1)"
} else {
""
};
eprintln!(
" review {}: ask {}{bus}",
unit.file,
unit.expert.join(", "),
);
}
}
fn print_decision_surface_human(surface: &crate::audit_decision_surface::DecisionSurface) {
if surface.decisions.is_empty() {
eprintln!("Decisions: none (no consequential structural decision in this change)");
eprintln!();
return;
}
eprintln!("Decisions to make ({}):", surface.decisions.len());
for (i, decision) in surface.decisions.iter().enumerate() {
eprintln!(
" {}. [{}] {}",
i + 1,
decision.category.tag(),
decision.question
);
if !decision.tradeoff.is_empty() {
eprintln!(" trade-off: {}", decision.tradeoff);
}
if !decision.expert.is_empty() {
let bus = if decision.bus_factor_one {
" (bus-factor 1)"
} else {
""
};
eprintln!(" ask: {}{bus}", decision.expert.join(", "));
}
}
if let Some(note) = &surface.truncated {
eprintln!(" ... {}", note.reason);
}
eprintln!();
}
fn weakening_label(kind: crate::audit::weakening::WeakeningKind) -> &'static str {
use crate::audit::weakening::WeakeningKind;
match kind {
WeakeningKind::TestWeakened => "test weakened",
WeakeningKind::ThresholdLowered => "threshold lowered",
WeakeningKind::SuppressionAdded => "suppression added",
WeakeningKind::SecurityCheckRemoved => "security check removed",
}
}
fn risk_label(risk: RiskClass) -> &'static str {
match risk {
RiskClass::Low => "low",
RiskClass::Medium => "medium",
RiskClass::High => "high",
}
}
fn effort_label(effort: ReviewEffort) -> &'static str {
match effort {
ReviewEffort::Glance => "glance",
ReviewEffort::Review => "review",
ReviewEffort::DeepDive => "deep-dive",
}
}
#[must_use]
pub fn print_brief_result(
result: &AuditResult,
quiet: bool,
explain: bool,
show_deprioritized: bool,
) -> ExitCode {
use fallow_config::OutputFormat;
match result.output {
OutputFormat::Json => print_brief_json(result),
OutputFormat::Human | OutputFormat::Compact | OutputFormat::Markdown => {
print_brief_human(result, quiet, explain, show_deprioritized);
ExitCode::SUCCESS
}
_ => {
let _ = crate::audit::print_audit_result(result, quiet, explain);
ExitCode::SUCCESS
}
}
}
#[must_use]
pub fn print_decision_surface_result(result: &AuditResult, quiet: bool) -> ExitCode {
use fallow_config::OutputFormat;
let surface = result.decision_surface.clone().unwrap_or_default();
match result.output {
OutputFormat::Json => {
let envelope = crate::output_envelope::FallowOutput::DecisionSurface(
crate::audit_decision_surface::build_decision_surface_output(&surface),
);
match serde_json::to_value(&envelope) {
Ok(mut value) => {
crate::output_envelope::apply_root_kind(&mut value, "decision-surface");
crate::output_envelope::attach_telemetry_meta(&mut value);
let _ = crate::report::emit_json(&value, "decision-surface");
ExitCode::SUCCESS
}
Err(_) => ExitCode::SUCCESS,
}
}
_ => {
if !quiet {
print_decision_surface_human(&surface);
}
ExitCode::SUCCESS
}
}
}
#[must_use]
pub fn print_walkthrough_guide_result(result: &AuditResult) -> ExitCode {
let guide = crate::audit_walkthrough::build_guide_from_result(result);
let envelope = crate::output_envelope::FallowOutput::WalkthroughGuide(guide);
if let Ok(mut value) = serde_json::to_value(&envelope) {
crate::output_envelope::apply_root_kind(&mut value, "review-walkthrough-guide");
crate::output_envelope::attach_telemetry_meta(&mut value);
let _ = crate::report::emit_json(&value, "review-walkthrough-guide");
}
ExitCode::SUCCESS
}
#[must_use]
pub fn print_walkthrough_file_result(result: &AuditResult, path: &std::path::Path) -> ExitCode {
let contents = std::fs::read_to_string(path).unwrap_or_default();
let agent = crate::audit_walkthrough::parse_agent_walkthrough(&contents);
let surface = result.decision_surface.clone().unwrap_or_default();
let current_hash = result.graph_snapshot_hash.clone().unwrap_or_default();
let change_anchor_ids =
crate::audit_walkthrough::change_anchor_allowlist(&result.change_anchors);
let validation = crate::audit_walkthrough::validate_walkthrough(
&agent,
&surface,
&change_anchor_ids,
¤t_hash,
);
let envelope = crate::output_envelope::FallowOutput::WalkthroughValidation(validation);
if let Ok(mut value) = serde_json::to_value(&envelope) {
crate::output_envelope::apply_root_kind(&mut value, "review-walkthrough-validation");
crate::output_envelope::attach_telemetry_meta(&mut value);
let _ = crate::report::emit_json(&value, "review-walkthrough-validation");
}
ExitCode::SUCCESS
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use fallow_config::{AuditGate, OutputFormat};
use crate::audit::{AuditAttribution, AuditResult, AuditSummary, AuditVerdict};
use super::*;
fn audit_result(verdict: AuditVerdict, output: OutputFormat) -> AuditResult {
AuditResult {
verdict,
summary: AuditSummary {
dead_code_issues: 0,
dead_code_has_errors: false,
complexity_findings: 0,
max_cyclomatic: None,
duplication_clone_groups: 0,
},
attribution: AuditAttribution {
gate: AuditGate::NewOnly,
..AuditAttribution::default()
},
base_snapshot: None,
base_snapshot_skipped: false,
changed_files_count: 0,
changed_files: Vec::new(),
base_ref: "origin/main".to_string(),
base_description: None,
head_sha: None,
output,
performance: false,
check: None,
dupes: None,
health: None,
elapsed: Duration::ZERO,
review_deltas: None,
weakening_signals: Vec::new(),
routing: None,
decision_surface: None,
graph_snapshot_hash: None,
change_anchors: Vec::new(),
}
}
#[test]
fn brief_mode_always_returns_success_even_when_verdict_is_fail() {
let human = audit_result(AuditVerdict::Fail, OutputFormat::Human);
assert_eq!(
print_brief_result(&human, true, false, false),
ExitCode::SUCCESS
);
let json = audit_result(AuditVerdict::Fail, OutputFormat::Json);
assert_eq!(
print_brief_result(&json, true, false, false),
ExitCode::SUCCESS
);
}
#[test]
fn brief_json_validates_against_audit_brief_schema_variant() {
let result = audit_result(AuditVerdict::Fail, OutputFormat::Json);
let mut value = build_brief_json(&result).expect("brief json must build");
crate::output_envelope::apply_root_kind(&mut value, "audit-brief");
assert_eq!(value["kind"], "audit-brief");
assert_eq!(value["command"], "audit-brief");
assert_eq!(value["schema_version"], REVIEW_BRIEF_SCHEMA_VERSION);
}
#[test]
fn brief_json_is_byte_identical_on_repeated_serialization() {
let result = audit_result(AuditVerdict::Warn, OutputFormat::Json);
let first = build_brief_json(&result).expect("first build");
let second = build_brief_json(&result).expect("second build");
let first_str = serde_json::to_string_pretty(&first).expect("serialize first");
let second_str = serde_json::to_string_pretty(&second).expect("serialize second");
assert_eq!(first_str, second_str);
}
#[test]
fn risk_class_thresholds_are_pure_functions_of_size() {
assert_eq!(classify_risk(0, None), RiskClass::Low);
assert_eq!(classify_risk(RISK_MEDIUM_FILES, None), RiskClass::Medium);
assert_eq!(classify_risk(RISK_HIGH_FILES, None), RiskClass::High);
assert_eq!(classify_risk(1, Some(RISK_HIGH_LINES)), RiskClass::High);
assert_eq!(review_effort_for(RiskClass::High), ReviewEffort::DeepDive);
}
#[test]
fn brief_json_includes_empty_impact_closure_when_no_graph_retained() {
let result = audit_result(AuditVerdict::Warn, OutputFormat::Json);
let value = build_brief_json(&result).expect("brief json must build");
assert!(value.get("impact_closure").is_some(), "{value}");
assert_eq!(
value["impact_closure"]["affected_not_shown"],
serde_json::json!([])
);
assert_eq!(
value["impact_closure"]["coordination_gap"],
serde_json::json!([])
);
}
#[test]
fn derive_graph_facts_populates_reachable_from_from_closure() {
use fallow_core::graph::{CoordinationGapPaths, ImpactClosurePaths};
let results = AnalysisResults::default();
let closure = ImpactClosurePaths {
in_diff: vec!["src/core.ts".to_string()],
affected_not_shown: vec!["src/app.ts".to_string(), "src/mid.ts".to_string()],
coordination_gap: vec![CoordinationGapPaths {
changed_file: "src/core.ts".to_string(),
consumer_file: "src/mid.ts".to_string(),
consumed_symbols: vec!["compute".to_string()],
}],
};
let facts = derive_graph_facts(&results, Some(&closure));
assert_eq!(
facts.reachable_from,
vec!["src/app.ts".to_string(), "src/mid.ts".to_string()]
);
}
#[test]
fn coordination_gap_fact_carries_honest_scope_note() {
let gap = CoordinationGapFact {
changed_file: "src/core.ts".to_string(),
consumer_file: "src/mid.ts".to_string(),
consumed_symbols: vec!["compute".to_string()],
note: COORDINATION_GAP_NOTE.to_string(),
};
assert!(gap.note.contains("attention pointer"));
assert!(gap.note.contains("not a correctness proof"));
}
}