use std::process::ExitCode;
pub use fallow_output::{
CoordinationGapFact, DiffTriage, GraphFacts, ImpactClosureFacts, PartitionFacts,
ReviewBriefSchemaVersion, ReviewBriefSubtractSections, ReviewDeltas, ReviewEffort,
ReviewUnitFact, RiskClass,
};
use fallow_types::results::AnalysisResults;
use rustc_hash::{FxHashMap, FxHashSet};
use crate::audit::AuditResult;
use crate::report::sink::outln;
pub type ReviewBriefOutput = fallow_output::StandardReviewBriefOutput;
const RISK_HIGH_FILES: usize = 20;
const RISK_HIGH_LINES: i64 = 500;
const RISK_MEDIUM_FILES: usize = 5;
const RISK_MEDIUM_LINES: i64 = 100;
const COORDINATION_GAP_NOTE: &str = "syntactic attention pointer, not a correctness proof";
#[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),
dependency_added: Vec::new(),
dependency_major_bumped: Vec::new(),
}
}
#[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,
diff_index: Option<&fallow_output::DiffIndex>,
) -> DiffTriage {
let files = result.changed_files_count;
let hunks = diff_index.map(fallow_output::DiffIndex::hunk_count);
let net_lines = diff_index.map(fallow_output::DiffIndex::net_lines);
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) -> 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();
GraphFacts {
exports_added: 0,
api_width_delta: 0,
boundaries_touched,
}
}
#[must_use]
fn build_impact_closure_facts(
result: &AuditResult,
diff_index: Option<&fallow_output::DiffIndex>,
) -> ImpactClosureFacts {
let mut facts = build_graph_impact_closure_facts(result, diff_index);
if let Some(check) = result.check.as_ref() {
facts.set_co_change_gaps(&check.co_change_gaps);
}
facts
}
fn build_graph_impact_closure_facts(
result: &AuditResult,
diff_index: Option<&fallow_output::DiffIndex>,
) -> 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();
let mut facts = ImpactClosureFacts::new(&closure.affected_not_shown, coordination_gap);
facts.search_gap = build_search_gap_facts(result, diff_index);
facts
}
fn build_search_gap_facts(
result: &AuditResult,
diff_index: Option<&fallow_output::DiffIndex>,
) -> Option<fallow_output::SearchGapFacts> {
use fallow_types::trace::SearchVisibility;
let diff_index = diff_index?;
let gaps = result.check.as_ref()?.search_gaps.as_ref()?;
let mut exports: Vec<fallow_output::ExportSearchGapFact> = gaps
.iter()
.filter(|gap| {
let key = diff_index.key_for_root_relative(&gap.file);
let (start, end) = gap.declaration_lines;
diff_index.range_overlaps_added(&key, u64::from(start), u64::from(end))
})
.map(|gap| {
let mut consumers: Vec<&fallow_engine::module_graph::ConsumerSearchGapPaths> =
gap.consumers.iter().collect();
consumers.sort_by(|a, b| {
let missed = |c: &fallow_engine::module_graph::ConsumerSearchGapPaths| {
c.check.visibility != SearchVisibility::Missed
};
missed(a)
.cmp(&missed(b))
.then_with(|| a.consumer_file.cmp(&b.consumer_file))
});
fallow_output::ExportSearchGapFact {
file: gap.file.clone(),
export_name: gap.export_name.clone(),
line: gap.line,
total: consumers.len(),
consumers: consumers
.into_iter()
.take(fallow_output::SEARCH_GAP_CONSUMER_CAP)
.map(|consumer| fallow_output::SearchGapConsumerFact {
consumer_file: consumer.consumer_file.clone(),
line: consumer.check.line,
search_visibility: consumer.check.visibility,
reason: consumer.check.reason,
search_as: consumer.check.search_as.clone(),
})
.collect(),
}
})
.collect();
if exports.is_empty() {
return None;
}
exports.sort_by(|a, b| a.file.cmp(&b.file).then_with(|| a.line.cmp(&b.line)));
Some(fallow_output::SearchGapFacts {
total: exports.iter().map(|export| export.total).sum(),
exports,
})
}
fn build_test_reach_facts(
result: &AuditResult,
diff_index: Option<&fallow_output::DiffIndex>,
) -> Option<fallow_output::TestReachFacts> {
use fallow_engine::module_graph::{ExportTestReach, TestMaskingState};
use fallow_output::{
ExportTestReachFact, TEST_REACH_EXPORT_CAP, TEST_REACH_FILE_CAP, TestReachClass,
TestReachCounts, TestReachFacts, TestReachMasking, TestReachStatus,
};
let diff_index = diff_index?;
let check = result.check.as_ref()?;
let reach = check.test_reach.as_ref()?;
let mut counts = TestReachCounts::default();
let mut rows: Vec<ExportTestReachFact> = Vec::new();
let mut total = 0usize;
for export in &reach.exports {
let key = diff_index.key_for_root_relative(&export.file);
let (start, end) = export.declaration_lines;
if !diff_index.range_overlaps_added(&key, u64::from(start), u64::from(end)) {
continue;
}
total += 1;
let class = if !reach.has_tests {
TestReachClass::NoneFile
} else if export.literal_value
&& check
.trace_provenance
.is_config_export(std::path::Path::new(&export.file), &export.export_name)
{
TestReachClass::Framework
} else {
match export.reach {
ExportTestReach::Direct => TestReachClass::Direct,
ExportTestReach::Transitive => TestReachClass::Transitive,
ExportTestReach::MockedOnly => TestReachClass::MockedOnly,
ExportTestReach::None => TestReachClass::None,
ExportTestReach::NoneFile => TestReachClass::NoneFile,
}
};
counts.add(class);
if !reach.has_tests || matches!(class, TestReachClass::Direct | TestReachClass::Framework) {
continue;
}
let change = export_change(
check.test_reach_base_exports.as_ref(),
diff_index,
&key,
export,
);
let mocked = class == TestReachClass::MockedOnly;
let tests = (mocked && !export.tests.is_empty()).then_some(&export.tests);
rows.push(ExportTestReachFact {
file: export.file.clone(),
export_name: export.export_name.clone(),
line: export.line,
reach: class,
change,
tests: tests.map(|tests| tests.iter().take(TEST_REACH_FILE_CAP).cloned().collect()),
tests_total: tests.map(Vec::len),
test_changed: tests.map(|tests| {
tests
.iter()
.any(|test| diff_index.changes_path(&diff_index.key_for_root_relative(test)))
}),
mocked_targets: mocked.then(|| {
export
.mocked_targets
.iter()
.take(TEST_REACH_FILE_CAP)
.cloned()
.collect()
}),
});
}
if total == 0 {
return None;
}
rows.sort_by(|a, b| {
a.reach
.cmp(&b.reach)
.then_with(|| a.change.cmp(&b.change))
.then_with(|| a.file.cmp(&b.file))
.then_with(|| a.line.cmp(&b.line))
.then_with(|| a.export_name.cmp(&b.export_name))
});
let exports_omitted = rows.len().saturating_sub(TEST_REACH_EXPORT_CAP);
rows.truncate(TEST_REACH_EXPORT_CAP);
Some(TestReachFacts {
status: if reach.has_tests {
TestReachStatus::Measured
} else {
TestReachStatus::NoTests
},
masking: match reach.masking {
TestMaskingState::Active => TestReachMasking::Active,
TestMaskingState::NoMocks => TestReachMasking::NoMocks,
TestMaskingState::Capped => TestReachMasking::Capped,
},
total,
counts,
exports: rows,
exports_omitted,
e2e_runner: reach.has_e2e_runner,
})
}
fn export_change(
base_exports: Option<&FxHashMap<String, FxHashSet<String>>>,
diff_index: &fallow_output::DiffIndex,
key: &str,
export: &fallow_engine::module_graph::ExportTestReachPaths,
) -> fallow_output::ExportChange {
use fallow_output::ExportChange;
if let Some(names) = base_exports.and_then(|base| base.get(&export.file)) {
return if names.contains(&export.export_name) {
ExportChange::Modified
} else {
ExportChange::New
};
}
let (start, end) = export.declaration_lines;
if (start..=end).all(|line| diff_index.line_is_added(key, u64::from(line))) {
ExportChange::New
} else {
ExportChange::Modified
}
}
#[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(),
independent_slices: partition.independent_slices.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());
let runtime_focus = build_runtime_focus(result, root);
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,
runtime: runtime_focus.as_ref(),
})
}
fn build_runtime_focus(
result: &AuditResult,
root: &std::path::Path,
) -> Option<crate::audit_focus::RuntimeFocus> {
let report = result.health.as_ref()?.report.runtime_coverage.as_ref()?;
let hot_pairs: Vec<(String, u64)> = report
.hot_paths
.iter()
.map(|hot| {
(
crate::audit::keys::relative_key_path(&hot.path, root),
hot.invocations,
)
})
.collect();
let mut safe_to_delete: FxHashSet<String> = FxHashSet::default();
let mut other_verdict: FxHashSet<String> = FxHashSet::default();
for finding in &report.findings {
let file = crate::audit::keys::relative_key_path(&finding.path, root);
if matches!(
finding.verdict,
fallow_output::RuntimeCoverageVerdict::SafeToDelete
) {
safe_to_delete.insert(file);
} else {
other_verdict.insert(file);
}
}
reconcile_runtime_focus(hot_pairs, &safe_to_delete, &other_verdict)
}
fn reconcile_runtime_focus(
hot_pairs: Vec<(String, u64)>,
safe_to_delete: &FxHashSet<String>,
other_verdict: &FxHashSet<String>,
) -> Option<crate::audit_focus::RuntimeFocus> {
use crate::audit_focus::{RuntimeFocus, RuntimeHotFile};
let mut hot_by_file: rustc_hash::FxHashMap<String, u64> = rustc_hash::FxHashMap::default();
for (file, invocations) in hot_pairs {
let entry = hot_by_file.entry(file).or_insert(0);
*entry = (*entry).max(invocations);
}
let mut hot_files: Vec<RuntimeHotFile> = hot_by_file
.into_iter()
.map(|(file, invocations)| RuntimeHotFile { file, invocations })
.collect();
hot_files.sort_by(|a, b| a.file.cmp(&b.file));
let hot_set: FxHashSet<&str> = hot_files.iter().map(|hot| hot.file.as_str()).collect();
let mut cold_files: Vec<String> = safe_to_delete
.iter()
.filter(|file| !other_verdict.contains(*file) && !hot_set.contains(file.as_str()))
.cloned()
.collect();
cold_files.sort();
if hot_files.is_empty() && cold_files.is_empty() {
return None;
}
Some(RuntimeFocus {
hot_files,
cold_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 {
build_brief_output_with_diff(result, result.diff_index.as_ref())
}
#[must_use]
pub fn build_brief_output_with_diff(
result: &AuditResult,
diff_index: Option<&fallow_output::DiffIndex>,
) -> ReviewBriefOutput {
let triage = build_triage(result, diff_index);
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,
boundaries_touched: Vec::new(),
},
|check| derive_graph_facts(&check.results),
);
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, diff_index);
let focus = build_focus_map(result, &deltas);
ReviewBriefOutput {
branching: build_branching_report(result),
test_reach: build_test_reach_facts(result, diff_index),
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(),
ownership: result.ownership.clone(),
decisions: result.decision_surface.clone().unwrap_or_default(),
}
}
fn build_branching_report(result: &AuditResult) -> Option<fallow_output::BranchingReport> {
let base = result.base_snapshot.as_ref()?;
let health = result.health.as_ref()?;
let root = health.config.root.as_path();
let accounting: rustc_hash::FxHashSet<String> = result
.changed_files
.iter()
.map(|path| crate::audit::keys::relative_key_path(path, root))
.collect();
let restrict = |source: &fallow_output::BranchingSnapshot| -> fallow_output::BranchingSnapshot {
source
.iter()
.filter(|(path, _)| accounting.contains(path.as_str()))
.map(|(path, totals)| (path.clone(), *totals))
.collect()
};
let head = crate::audit::branching_keys(&health.branching_by_file, root);
Some(fallow_output::BranchingReport::compare(
&restrict(&base.branching),
&restrict(&head),
fallow_output::DEFAULT_BRANCHING_TOLERANCE,
&|path| fallow_engine::test_paths::is_test_path_str(root, path),
))
}
fn build_brief_subtract_sections(
result: &AuditResult,
) -> Result<ReviewBriefSubtractSections, ExitCode> {
let mut obj = serde_json::Map::new();
if let Some(ref check) = result.check {
crate::audit::insert_audit_dead_code_json(&mut obj, result, check)?;
}
if let Some(ref dupes) = result.dupes {
crate::audit::insert_audit_duplication_json(&mut obj, result, dupes)?;
}
if let Some(ref health) = result.health {
crate::audit::insert_audit_health_json(&mut obj, result, health)?;
}
Ok(ReviewBriefSubtractSections {
dead_code: obj.remove("dead_code"),
duplication: obj.remove("duplication"),
complexity: obj.remove("complexity"),
})
}
pub fn build_brief_json(
result: &AuditResult,
diff_index: Option<&fallow_output::DiffIndex>,
) -> Result<serde_json::Value, ExitCode> {
let brief = build_brief_output_with_diff(result, diff_index);
let audit_header =
fallow_api::build_review_brief_header(crate::audit::audit_json_header_input(result));
let subtract = build_brief_subtract_sections(result)?;
let mut output = fallow_output::build_review_brief_json_output(brief, audit_header, subtract)
.map_err(|err| {
crate::error::emit_error(
&format!("JSON serialization error: {err}"),
2,
fallow_config::OutputFormat::Json,
)
})?;
fallow_api::attach_audit_wire_attribution(&mut output);
Ok(output)
}
fn print_brief_json(
result: &AuditResult,
diff_index: Option<&fallow_output::DiffIndex>,
json_style: crate::json_style::JsonStyle,
) -> ExitCode {
match build_brief_json(result, diff_index) {
Ok(output) => {
let Ok(output) = fallow_output::serialize_review_brief_json_output(
output,
crate::output_runtime::telemetry_analysis_run_id().as_deref(),
) else {
return ExitCode::SUCCESS;
};
crate::report::emit_report_json(&output, "audit-brief", json_style)
}
Err(_) => ExitCode::SUCCESS,
}
}
#[cfg(test)]
fn serialize_brief_json(
value: &serde_json::Value,
json_style: crate::json_style::JsonStyle,
) -> Result<String, serde_json::Error> {
json_style.serialize(value)
}
fn print_brief_human(
result: &AuditResult,
diff_index: Option<&fallow_output::DiffIndex>,
quiet: bool,
explain: bool,
show_deprioritized: bool,
) {
let brief = build_brief_output_with_diff(result, diff_index);
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_branching_human(brief.branching.as_ref());
print_partition_human(&brief.partition);
print_impact_closure_human(&brief.impact_closure);
for line in test_reach_lines(brief.test_reach.as_ref()) {
eprintln!("{line}");
}
print_focus_human(&brief.focus, show_deprioritized);
print_deltas_human(&brief.deltas);
print_weakening_human(&brief.weakening);
print_routing_human(&brief.routing);
print_ownership_human(brief.ownership.as_ref());
}
crate::audit::print_audit_findings(result, quiet, explain, false);
}
fn branching_human_lines(report: Option<&fallow_output::BranchingReport>) -> Option<Vec<String>> {
let report = report.filter(|report| report.is_reportable())?;
let mut lines =
vec![" branching: branching about level, more functions, smaller peak".to_string()];
for split in report.split_in_place.iter().take(2) {
lines.push(format!(" {}", elide_path(&split.path, 71)));
lines.push(format!(
" {} to {} branch points, {} to {} functions, peak {} to {}",
split.branch_points_before,
split.branch_points_after,
split.functions_before,
split.functions_after,
split.peak_before,
split.peak_after,
));
}
let remaining = report.split_in_place.len().saturating_sub(2);
if remaining > 0 {
lines.push(format!(
" and {remaining} more file{}",
crate::report::plural(remaining)
));
}
Some(lines)
}
fn elide_path(path: &str, budget: usize) -> String {
if path.chars().count() <= budget {
return path.to_string();
}
let tail: String = path
.chars()
.rev()
.take(budget.saturating_sub(4))
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect();
format!(".../{tail}")
}
fn print_branching_human(report: Option<&fallow_output::BranchingReport>) {
let Some(lines) = branching_human_lines(report) else {
return;
};
for line in lines {
eprintln!("{line}");
}
}
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} "));
}
if partition.independent_slices.len() >= 2 {
let slices: Vec<String> = partition
.independent_slices
.iter()
.map(|slice| {
let labeled: Vec<String> = slice.iter().map(|dir| unit_label(dir)).collect();
format!("[{}]", labeled.join(", "))
})
.collect();
eprintln!(
" independent slices: {} (no import edge between them) {}",
partition.independent_slices.len(),
slices.join(" ")
);
}
}
fn unit_label(module_dir: &str) -> String {
if module_dir.is_empty() {
"<root>".to_string()
} else {
module_dir.to_string()
}
}
fn affected_lines(closure: &ImpactClosureFacts) -> Vec<String> {
if closure.affected_count == 0 {
return Vec::new();
}
let dirs = closure.affected_by_dir.len() + closure.affected_by_dir_omitted;
let mut lines = vec![format!(
" impact closure: {} file{} affected beyond the diff{}",
closure.affected_count,
crate::report::plural(closure.affected_count),
if dirs < 2 {
String::new()
} else {
format!(" across {dirs} directories")
},
)];
let Some(heaviest) = closure.affected_by_dir.first().filter(|_| dirs > 1) else {
return lines;
};
lines.push(format!(
" heaviest {} ({} file{})",
elide_path(&unit_label(&heaviest.dir), 48),
heaviest.count,
crate::report::plural(heaviest.count),
));
let remaining = dirs - 1;
let route = if closure.affected_by_dir_omitted == 0 {
"--format json for full list".to_string()
} else {
format!(
"{} of them in --format json",
closure.affected_by_dir.len() - 1
)
};
lines.push(format!(
" and {remaining} more director{} ({route})",
if remaining == 1 { "y" } else { "ies" },
));
lines
}
const MAX_HUMAN_COORDINATION_CONSUMERS: usize = 3;
const MAX_HUMAN_GAP_SOURCES: usize = 2;
fn summarize_symbols(symbols: &[String], budget: usize) -> (String, usize) {
let mut shown = 0usize;
let mut width = 0usize;
for symbol in symbols {
let separator = usize::from(shown > 0) * 2;
let remaining = symbols.len() - shown - 1;
let suffix = if remaining == 0 {
0
} else {
format!(" +{remaining} more").chars().count()
};
let next = width + separator + symbol.chars().count();
if shown > 0 && next + suffix > budget {
break;
}
width = next;
shown += 1;
}
let shown = shown.max(1).min(symbols.len());
let joined = symbols[..shown].join(", ");
let omitted = symbols.len() - shown;
if omitted == 0 {
return (elide_symbol(&joined, budget), 0);
}
let suffix = format!(" +{omitted} more");
let head = elide_symbol(&joined, budget.saturating_sub(suffix.chars().count()));
(format!("{head}{suffix}"), omitted)
}
fn elide_symbol(text: &str, budget: usize) -> String {
if text.chars().count() <= budget {
return text.to_string();
}
let head: String = text.chars().take(budget.saturating_sub(3)).collect();
format!("{head}...")
}
fn coordination_gap_lines(gaps: &[CoordinationGapFact]) -> Vec<String> {
if gaps.is_empty() {
return Vec::new();
}
let mut at: FxHashMap<&str, usize> = FxHashMap::default();
let mut consumers: Vec<(&str, Vec<&CoordinationGapFact>)> = Vec::new();
for gap in gaps {
match at.get(gap.consumer_file.as_str()) {
Some(&index) => consumers[index].1.push(gap),
None => {
at.insert(&gap.consumer_file, consumers.len());
consumers.push((&gap.consumer_file, vec![gap]));
}
}
}
let mut lines = vec![format!(
" coordination gap: {} consumer{} outside the diff use{} exports of changed files",
consumers.len(),
crate::report::plural(consumers.len()),
if consumers.len() == 1 { "s" } else { "" },
)];
for (_, sources) in &mut consumers {
sources.sort_by(|a, b| {
b.consumed_symbols
.len()
.cmp(&a.consumed_symbols.len())
.then_with(|| a.changed_file.cmp(&b.changed_file))
});
}
consumers.sort_by(|a, b| {
symbols_taken(&b.1)
.cmp(&symbols_taken(&a.1))
.then_with(|| a.0.cmp(b.0))
});
let mut symbols_omitted = 0usize;
let mut sources_omitted = 0usize;
for (consumer, sources) in consumers.iter().take(MAX_HUMAN_COORDINATION_CONSUMERS) {
lines.push(format!(" {}", elide_path(consumer, 71)));
for gap in sources.iter().take(MAX_HUMAN_GAP_SOURCES) {
debug_assert!(
!gap.consumed_symbols.is_empty(),
"a gap exists because a symbol is consumed, so the list is never empty"
);
let (symbols, omitted) = summarize_symbols(&gap.consumed_symbols, 24);
symbols_omitted += omitted;
lines.push(format!(
" consumes {symbols} from {}",
elide_path(&gap.changed_file, 28),
));
}
let hidden = sources.len().saturating_sub(MAX_HUMAN_GAP_SOURCES);
if hidden > 0 {
sources_omitted += hidden;
lines.push(format!(
" and {hidden} more changed file{}",
crate::report::plural(hidden),
));
}
}
let remaining = consumers
.len()
.saturating_sub(MAX_HUMAN_COORDINATION_CONSUMERS);
if remaining > 0 {
lines.push(format!(
" and {remaining} more consumer{} (--format json for full list)",
crate::report::plural(remaining),
));
} else if symbols_omitted > 0 || sources_omitted > 0 {
let dropped = match (symbols_omitted > 0, sources_omitted > 0) {
(true, true) => "every consumed symbol and changed file",
(false, true) => "every changed file",
_ => "every consumed symbol",
};
lines.push(format!(" (--format json for {dropped})"));
}
lines
}
fn symbols_taken(sources: &[&CoordinationGapFact]) -> usize {
sources.iter().map(|g| g.consumed_symbols.len()).sum()
}
fn co_change_gap_lines(closure: &ImpactClosureFacts) -> Vec<String> {
let gaps = &closure.co_change_gap;
if gaps.is_empty() {
return Vec::new();
}
let total = closure.co_change_gap_count.max(gaps.len());
let mut lines = vec![format!(
" co-change gaps ({total}): files that usually change together, no import between them"
)];
for gap in gaps {
lines.push(format!(
" {} -> {} ({} shared commits, {:.0}%)",
gap.changed_file,
gap.partner_file,
gap.shared_revisions,
gap.degree * 100.0
));
}
let hidden = total - gaps.len();
if hidden > 0 {
lines.push(format!(" {hidden} more not shown"));
}
lines
}
fn print_impact_closure_human(closure: &ImpactClosureFacts) {
for line in affected_lines(closure) {
eprintln!("{line}");
}
for line in coordination_gap_lines(&closure.coordination_gap) {
eprintln!("{line}");
}
for line in co_change_gap_lines(closure) {
eprintln!("{line}");
}
for line in search_gap_lines(closure.search_gap.as_ref()) {
eprintln!("{line}");
}
}
const MAX_HUMAN_SEARCH_GAP_EXPORTS: usize = 3;
const MAX_HUMAN_SEARCH_GAP_CONSUMERS: usize = 3;
fn search_gap_lines(facts: Option<&fallow_output::SearchGapFacts>) -> Vec<String> {
use fallow_types::trace::SearchVisibility;
let Some(facts) = facts else {
return Vec::new();
};
let mut lines = vec![format!(
" search gap: {} consumer use{} of changed exports hide{} from a name search",
facts.total,
crate::report::plural(facts.total),
if facts.total == 1 { "s" } else { "" },
)];
let mut truncated = facts.exports.len() > MAX_HUMAN_SEARCH_GAP_EXPORTS;
for export in facts.exports.iter().take(MAX_HUMAN_SEARCH_GAP_EXPORTS) {
lines.push(format!(
" {} in {}:{}",
elide_symbol(&export.export_name, 30),
elide_path(&export.file, 34),
export.line,
));
for consumer in export.consumers.iter().take(MAX_HUMAN_SEARCH_GAP_CONSUMERS) {
let level = if consumer.search_visibility == SearchVisibility::Missed {
"missed"
} else {
"renamed"
};
let hint = consumer
.search_as
.as_deref()
.map(|name| format!(", search {}", elide_symbol(name, 20)))
.unwrap_or_default();
lines.push(format!(
" {level} {}{hint}",
elide_path(&consumer.consumer_file, 40),
));
}
truncated |= export.total > MAX_HUMAN_SEARCH_GAP_CONSUMERS;
}
if truncated {
lines.push(" (--format json for the full list)".to_string());
}
lines
}
const MAX_HUMAN_TEST_REACH_EXPORTS: usize = 3;
const TEST_REACH_LINE_WIDTH: usize = 80;
const TEST_REACH_ROW_INDENT: &str = " ";
const TEST_REACH_REASON_INDENT: &str = " ";
const fn test_reach_label(class: fallow_output::TestReachClass) -> &'static str {
use fallow_output::TestReachClass;
match class {
TestReachClass::MockedOnly => "mocked only",
TestReachClass::None | TestReachClass::NoneFile => "no test",
TestReachClass::Transitive => "transitive",
TestReachClass::Direct => "direct",
TestReachClass::Framework => "framework",
}
}
fn test_reach_count_phrases(counts: &fallow_output::TestReachCounts) -> Vec<String> {
[
(counts.direct, "direct"),
(counts.transitive, "transitive"),
(counts.mocked_only, "mocked only"),
(counts.none + counts.none_file, "no test"),
(counts.framework, "framework"),
]
.into_iter()
.filter(|(count, _)| *count > 0)
.map(|(count, label)| format!("{count} {label}"))
.collect()
}
fn test_reach_reason(
row: &fallow_output::ExportTestReachFact,
has_e2e_runner: bool,
) -> Option<String> {
use fallow_output::TestReachClass;
match row.reach {
TestReachClass::MockedOnly => {
let targets = row.mocked_targets.as_deref().unwrap_or_default();
let first = targets.first()?;
let more = targets.len() - 1;
let suffix = if more > 0 {
format!(" +{more}")
} else {
String::new()
};
Some(format!("tests mock {}{suffix}", elide_path(first, 40)))
}
TestReachClass::NoneFile if has_e2e_runner => {
Some("file not reached by tests; e2e tests do not import files".to_string())
}
TestReachClass::NoneFile => Some("file not reached by tests".to_string()),
_ => None,
}
}
fn test_reach_lines(facts: Option<&fallow_output::TestReachFacts>) -> Vec<String> {
use fallow_output::{TestReachMasking, TestReachStatus};
let Some(facts) = facts else {
return Vec::new();
};
let exports = format!(
"{} changed export{}",
facts.total,
crate::report::plural(facts.total)
);
if facts.status == TestReachStatus::NoTests {
return vec![format!(
" test reach: no test files in this project; {exports} {} no test",
if facts.total == 1 { "has" } else { "have" },
)];
}
let mut lines = Vec::new();
let phrases = test_reach_count_phrases(&facts.counts);
let mut current = format!(" test reach: {exports}: ");
let mut first = true;
for phrase in &phrases {
let piece = if first {
phrase.clone()
} else {
format!(", {phrase}")
};
if !first && current.chars().count() + piece.chars().count() > TEST_REACH_LINE_WIDTH {
lines.push(format!("{current},"));
current = format!("{TEST_REACH_ROW_INDENT}{phrase}");
} else {
current.push_str(&piece);
}
first = false;
}
lines.push(current);
if facts.masking == TestReachMasking::Capped {
lines.push(format!(
"{TEST_REACH_ROW_INDENT}mock detection is off: too many distinct mock sets"
));
}
let shown = || facts.exports.iter().take(MAX_HUMAN_TEST_REACH_EXPORTS);
let name_width = shown()
.map(|row| elide_symbol(&row.export_name, 24).chars().count())
.max()
.unwrap_or(0);
for row in shown() {
let location = format!("{}:{}", elide_path(&row.file, 28), row.line);
let name = elide_symbol(&row.export_name, 24);
let main = format!(
"{TEST_REACH_ROW_INDENT}{:<12} {location:<20} {name:<name_width$}",
test_reach_label(row.reach),
);
let trimmed = main.trim_end().to_string();
let fits = |line: &str, reason: &str| {
line.chars().count() + 2 + reason.chars().count() <= TEST_REACH_LINE_WIDTH
};
match test_reach_reason(row, facts.e2e_runner) {
Some(reason) if fits(&main, &reason) => lines.push(format!("{main} {reason}")),
Some(reason) if fits(&trimmed, &reason) => {
lines.push(format!("{trimmed} {reason}"));
}
Some(reason) => {
lines.push(trimmed);
lines.push(format!("{TEST_REACH_REASON_INDENT}{reason}"));
}
None => lines.push(trimmed),
}
}
if facts.exports.len() > MAX_HUMAN_TEST_REACH_EXPORTS || facts.exports_omitted > 0 {
lines.push(format!(
"{TEST_REACH_ROW_INDENT}(--format json for the full list)"
));
}
lines
}
fn wrap_prose(
text: &str,
first: usize,
rest: usize,
elide: fn(&str, usize) -> String,
) -> Vec<String> {
let mut lines: Vec<String> = Vec::new();
let mut current = String::new();
for word in text.split_whitespace() {
let budget = if lines.is_empty() { first } else { rest };
if current.is_empty() {
current = elide(word, budget);
continue;
}
if current.chars().count() + 1 + word.chars().count() <= budget {
current.push(' ');
current.push_str(word);
continue;
}
lines.push(std::mem::take(&mut current));
current = elide(word, rest);
}
if !current.is_empty() {
lines.push(current);
}
lines
}
fn focus_unit_lines(unit: &crate::audit_focus::FocusUnit) -> Vec<String> {
let mut lines = vec![format!(
" [{}] {}",
unit.label.token(),
elide_path(&unit.file, 56),
)];
for (n, chunk) in wrap_prose(&unit.reason, 72, 70, elide_path)
.into_iter()
.enumerate()
{
lines.push(if n == 0 {
format!(" {chunk}")
} else {
format!(" {chunk}")
});
}
for flag in &unit.confidence {
lines.extend(
wrap_prose(flag.message(), 60, 60, elide_path)
.into_iter()
.enumerate()
.map(|(i, chunk)| {
if i == 0 {
format!(" confidence {chunk}")
} else {
format!(" {chunk}")
}
}),
);
}
lines
}
fn focus_lines(focus: &crate::audit_focus::FocusMap, show_deprioritized: bool) -> Vec<String> {
if focus.total_units() == 0 {
return Vec::new();
}
let mut lines = Vec::new();
if !focus.review_here.is_empty() {
lines.push(format!(
" 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 {
lines.extend(focus_unit_lines(unit));
}
}
if focus.deprioritized.is_empty() {
return lines;
}
if show_deprioritized {
lines.push(format!(" de-prioritized ({}):", focus.deprioritized.len()));
for unit in &focus.deprioritized {
lines.extend(focus_unit_lines(unit));
}
} else {
lines.push(format!(
" de-prioritized: {} unit{} (run with --show-deprioritized to list)",
focus.deprioritized.len(),
crate::report::plural(focus.deprioritized.len()),
));
}
lines
}
fn print_focus_human(focus: &crate::audit_focus::FocusMap, show_deprioritized: bool) {
for line in focus_lines(focus, show_deprioritized) {
eprintln!("{line}");
}
}
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()),
);
}
if !deltas.dependency_added.is_empty() {
eprintln!(
" new third-party dependenc{}: {}",
if deltas.dependency_added.len() == 1 {
"y"
} else {
"ies"
},
dependency_key_names(&deltas.dependency_added)
);
}
if !deltas.dependency_major_bumped.is_empty() {
eprintln!(
" major version bump{}: {}",
crate::report::plural(deltas.dependency_major_bumped.len()),
dependency_key_names(&deltas.dependency_major_bumped)
);
}
}
fn dependency_key_names(keys: &[String]) -> String {
keys.iter()
.map(|key| {
let (manifest, rest) = key.split_once("::").unwrap_or(("", key));
let (name, range) = rest
.match_indices('@')
.find(|(index, _)| *index > 0)
.map_or((rest, None), |(index, _)| {
(&rest[..index], rest[index + 1..].split_once("->"))
});
let range_text = range
.map(|(from, to)| format!(" {from} to {to}"))
.unwrap_or_default();
let manifest_text = if manifest.is_empty() || manifest == "package.json" {
String::new()
} else {
format!(" ({manifest})")
};
format!("{name}{range_text}{manifest_text}")
})
.collect::<Vec<_>>()
.join(", ")
}
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(", "),
);
}
}
const OWNER_COLUMN_WIDTH: usize = 24;
const SLICE_DIRS_BUDGET: usize = 27;
fn ownership_lines(ownership: Option<&fallow_output::OwnershipFacts>) -> Vec<String> {
let Some(ownership) = ownership else {
return Vec::new();
};
if ownership.group_count == 0 {
return Vec::new();
}
let mut notes: Vec<String> = Vec::new();
if ownership.transitive_only_count > 0 {
notes.push(format!(
"{} transitive only",
ownership.transitive_only_count
));
}
if ownership.unowned_direct_count > 0 {
notes.push(format!(
"{} unowned file{}",
ownership.unowned_direct_count,
crate::report::plural(ownership.unowned_direct_count),
));
}
let notes = if notes.is_empty() {
String::new()
} else {
format!(" ({})", notes.join(", "))
};
let mut lines = vec![format!(
" ownership: {} owner group{}{notes}",
ownership.group_count,
crate::report::plural(ownership.group_count),
)];
for group in &ownership.groups {
let owner = elide_symbol(&group.owner, OWNER_COLUMN_WIDTH);
let affected = if group.affected_count > 0 {
format!(", {} affected", group.affected_count)
} else {
String::new()
};
lines.push(format!(
" {owner:<OWNER_COLUMN_WIDTH$} {} changed{affected}",
group.direct_count
));
}
if ownership.groups_omitted > 0 {
lines.push(format!(
" +{} more group{}",
ownership.groups_omitted,
crate::report::plural(ownership.groups_omitted),
));
}
for (index, slice) in ownership.slices.iter().enumerate() {
if !slice.separable {
continue;
}
lines.push(format!(
" slice {} ({}) {}",
index + 1,
slice_dirs_label(&slice.module_dirs),
slice_owner_phrase(&slice.owners),
));
}
lines
}
fn slice_dirs_label(module_dirs: &[String]) -> String {
let Some(first) = module_dirs.first() else {
return String::new();
};
let more = module_dirs.len() - 1;
let suffix = if more > 0 {
format!(" +{more} more")
} else {
String::new()
};
let budget = SLICE_DIRS_BUDGET.saturating_sub(suffix.chars().count());
format!(
"{}{suffix}",
elide_leading_segments(&unit_label(first), budget)
)
}
fn elide_leading_segments(path: &str, budget: usize) -> String {
if path.chars().count() <= budget {
return path.to_string();
}
let mut rest = path;
while let Some((_, tail)) = rest.split_once('/') {
rest = tail;
if rest.chars().count() + 4 <= budget {
return format!(".../{rest}");
}
}
elide_path(rest, budget)
}
fn slice_owner_phrase(owners: &[String]) -> String {
match owners {
[owner] if owner == crate::codeowners::UNOWNED_LABEL => "is unowned".to_string(),
_ => format!(
"has one owner: {}",
elide_symbol(&owners.join(", "), OWNER_COLUMN_WIDTH)
),
}
}
fn print_ownership_human(ownership: Option<&fallow_output::OwnershipFacts>) {
for line in ownership_lines(ownership) {
eprintln!("{line}");
}
}
const MAX_BRIEF_DECISION_MEMBERS: usize = 3;
const JSON_NAME_ROUTE: &str = " (--format json for every collapsed name)";
fn decision_surface_lines(surface: &crate::audit_decision_surface::DecisionSurface) -> Vec<String> {
if surface.decisions.is_empty() {
return vec![
"Decisions: none (no consequential structural decision in this change)".to_string(),
String::new(),
];
}
let mut lines = vec![format!("Decisions to make ({}):", surface.decisions.len())];
let mut names_collapsed = false;
for (i, decision) in surface.decisions.iter().enumerate() {
let head = format!(" {}. [{}] ", i + 1, decision.category.tag());
let head_width = head.chars().count();
let asked = fallow_output::cap_member_list(&decision.question, MAX_BRIEF_DECISION_MEMBERS);
names_collapsed |= asked != decision.question;
let question = wrap_prose(&asked, 80 - head_width, 73, elide_path);
if question.is_empty() {
lines.push(head.trim_end().to_string());
}
for (n, chunk) in question.into_iter().enumerate() {
if n == 0 {
lines.push(format!("{head}{chunk}"));
} else {
lines.push(format!(" {chunk}"));
}
}
if !decision.tradeoff.is_empty() {
for (n, chunk) in wrap_prose(&decision.tradeoff, 64, 71, elide_path)
.into_iter()
.enumerate()
{
if n == 0 {
lines.push(format!(" trade-off: {chunk}"));
} else {
lines.push(format!(" {chunk}"));
}
}
}
if !decision.expert.is_empty() {
let bus = if decision.bus_factor_one {
" (bus-factor 1)"
} else {
""
};
let reserved = bus.chars().count();
let experts = wrap_prose(
&decision.expert.join(", "),
70 - reserved,
73 - reserved,
elide_symbol,
);
for (n, chunk) in experts.iter().enumerate() {
if n == 0 {
lines.push(format!(" ask: {chunk}"));
} else {
lines.push(format!(" {chunk}"));
}
}
if !bus.is_empty()
&& !experts.is_empty()
&& let Some(last) = lines.last_mut()
{
last.push_str(bus);
}
}
}
if names_collapsed {
lines.push(JSON_NAME_ROUTE.to_string());
}
if let Some(note) = &surface.truncated {
for (n, chunk) in wrap_prose(¬e.reason, 72, 72, elide_path)
.into_iter()
.enumerate()
{
lines.push(if n == 0 {
format!(" ... {chunk}")
} else {
format!(" {chunk}")
});
}
}
lines.push(String::new());
lines
}
fn print_decision_surface_human(surface: &crate::audit_decision_surface::DecisionSurface) {
for line in decision_surface_lines(surface) {
eprintln!("{line}");
}
}
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,
diff_index: Option<&fallow_output::DiffIndex>,
quiet: bool,
explain: bool,
show_deprioritized: bool,
json_style: crate::json_style::JsonStyle,
) -> ExitCode {
use fallow_config::OutputFormat;
match result.output {
OutputFormat::Json => print_brief_json(result, diff_index, json_style),
OutputFormat::Human | OutputFormat::Compact | OutputFormat::Markdown => {
print_brief_human(result, diff_index, 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,
json_style: crate::json_style::JsonStyle,
) -> ExitCode {
use fallow_config::OutputFormat;
let surface = result.decision_surface.clone().unwrap_or_default();
match result.output {
OutputFormat::Json => {
let output = crate::audit_decision_surface::build_decision_surface_output(&surface);
match fallow_output::serialize_decision_surface_json_output(
output,
crate::output_runtime::telemetry_analysis_run_id().as_deref(),
) {
Ok(value) => {
let _ = crate::report::emit_report_json(&value, "decision-surface", json_style);
ExitCode::SUCCESS
}
Err(_) => ExitCode::SUCCESS,
}
}
_ => {
if !quiet {
print_decision_surface_human(&surface);
}
ExitCode::SUCCESS
}
}
}
#[must_use]
pub fn print_walkthrough_guide_result(
result: &AuditResult,
json_style: crate::json_style::JsonStyle,
) -> ExitCode {
let guide = crate::audit_walkthrough::build_guide_from_result(result);
if let Ok(value) = fallow_output::serialize_walkthrough_guide_json_output(
guide,
crate::output_runtime::telemetry_analysis_run_id().as_deref(),
) {
let _ = crate::report::emit_report_json(&value, "review-walkthrough-guide", json_style);
}
ExitCode::SUCCESS
}
#[must_use]
pub fn print_walkthrough_file_result(
result: &AuditResult,
path: &std::path::Path,
json_style: crate::json_style::JsonStyle,
) -> ExitCode {
let contents = std::fs::read_to_string(path).unwrap_or_else(|error| {
eprintln!(
"fallow: cannot read walkthrough file '{}': {error}",
path.display()
);
String::new()
});
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,
);
if let Ok(value) = fallow_output::serialize_walkthrough_validation_json_output(
validation,
crate::output_runtime::telemetry_analysis_run_id().as_deref(),
) {
let _ =
crate::report::emit_report_json(&value, "review-walkthrough-validation", json_style);
}
ExitCode::SUCCESS
}
#[must_use]
#[expect(
clippy::too_many_arguments,
reason = "walkthrough rendering needs its existing view state plus the JSON presentation style"
)]
pub fn print_walkthrough_human_result(
result: &AuditResult,
root: &std::path::Path,
cache_dir: &std::path::Path,
mark_viewed: &[std::path::PathBuf],
show_cleared: bool,
quiet: bool,
json_style: crate::json_style::JsonStyle,
) -> ExitCode {
use fallow_config::OutputFormat;
if matches!(result.output, OutputFormat::Json) {
return print_walkthrough_guide_result(result, json_style);
}
let guide = crate::audit_walkthrough::build_guide_from_result(result);
record_walkthrough_marks(&guide, root, cache_dir, mark_viewed);
let viewed = crate::walkthrough_state::load_viewed_state(cache_dir);
if matches!(result.output, OutputFormat::Markdown) {
let viewed_files = crate::report::walkthrough_viewed_files(&guide, &viewed);
let markdown = fallow_api::build_walkthrough_markdown(&guide, root, &viewed_files);
outln!("{markdown}");
return ExitCode::SUCCESS;
}
let render = crate::report::build_walkthrough_human(&guide, &viewed, show_cleared);
if !quiet {
for line in &render.header {
eprintln!("{line}");
}
}
for line in &render.body {
outln!("{line}");
}
if !quiet {
eprintln!("{}", render.status);
}
ExitCode::SUCCESS
}
fn record_walkthrough_marks(
guide: &crate::audit_walkthrough::WalkthroughGuide,
root: &std::path::Path,
cache_dir: &std::path::Path,
mark_viewed: &[std::path::PathBuf],
) {
if mark_viewed.is_empty() {
return;
}
let keys: Vec<String> = mark_viewed
.iter()
.map(|path| walkthrough_view_key(path, root))
.collect();
let _ = crate::walkthrough_state::mark_viewed(cache_dir, &keys, &guide.graph_snapshot_hash);
}
fn walkthrough_view_key(path: &std::path::Path, root: &std::path::Path) -> String {
let rel = path.strip_prefix(root).unwrap_or(path);
rel.to_string_lossy().replace('\\', "/")
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use fallow_config::{AuditGate, OutputFormat};
use fallow_output::REVIEW_BRIEF_SCHEMA_VERSION;
use rustc_hash::FxHashSet;
use crate::audit::{AuditAttribution, AuditResult, AuditSummary, AuditVerdict};
#[test]
fn dependency_names_preserve_scopes_and_manifest_labels() {
let keys = [
"package.json::@scope/one",
"package.json::@scope/two",
"package.json::other",
"package.json::plain",
"packages/app/package.json::@scope/workspace",
"@scope/unqualified",
]
.map(str::to_string);
assert_eq!(
dependency_key_names(&keys),
"@scope/one, @scope/two, other, plain, @scope/workspace (packages/app/package.json), @scope/unqualified"
);
}
#[test]
fn dependency_names_preserve_scoped_bumps_and_alias_ranges() {
let keys = [
"package.json::@scope/bumped@^1.0.0->^2.0.0",
"packages/app/package.json::plain@^1.0.0->^2.0.0",
"package.json::@scope/alias@npm:@other/pkg@^1.0.0->npm:@other/pkg@^2.0.0",
]
.map(str::to_string);
assert_eq!(
dependency_key_names(&keys),
"@scope/bumped ^1.0.0 to ^2.0.0, plain ^1.0.0 to ^2.0.0 (packages/app/package.json), @scope/alias npm:@other/pkg@^1.0.0 to npm:@other/pkg@^2.0.0"
);
}
fn str_set(files: &[&str]) -> FxHashSet<String> {
files.iter().map(|file| (*file).to_string()).collect()
}
#[test]
fn reconcile_runtime_focus_classifies_hot_cold_and_excludes_mixed() {
let hot_pairs = vec![
("src/hot.ts".to_string(), 120),
("src/hot.ts".to_string(), 900), ("src/both.ts".to_string(), 300), ];
let safe = str_set(&["src/cold.ts", "src/mixed.ts", "src/both.ts"]);
let other = str_set(&["src/mixed.ts"]); let focus =
super::reconcile_runtime_focus(hot_pairs, &safe, &other).expect("non-empty focus");
let hot: Vec<(&str, u64)> = focus
.hot_files
.iter()
.map(|hot| (hot.file.as_str(), hot.invocations))
.collect();
assert_eq!(hot, vec![("src/both.ts", 300), ("src/hot.ts", 900)]);
assert_eq!(focus.cold_files, vec!["src/cold.ts".to_string()]);
}
#[test]
fn reconcile_runtime_focus_is_none_when_empty() {
assert!(super::reconcile_runtime_focus(Vec::new(), &str_set(&[]), &str_set(&[])).is_none());
}
use super::*;
fn audit_result(verdict: AuditVerdict, output: OutputFormat) -> AuditResult {
AuditResult {
verdict,
summary: AuditSummary {
dead_code_issues: 0,
dead_code_has_errors: false,
dead_code_skipped: false,
complexity_skipped: false,
duplication_skipped: false,
complexity_findings: 0,
max_cyclomatic: None,
duplication_clone_groups: 0,
},
attribution: AuditAttribution {
gate: AuditGate::NewOnly,
..AuditAttribution::default()
},
dupe_demotion_diff_source: None,
base_snapshot: None,
comparison: 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(),
diff_risk: fallow_output::DiffRiskReport::default(),
routing: None,
ownership: None,
decision_surface: None,
graph_snapshot_hash: None,
change_anchors: Vec::new(),
diff_index: None,
}
}
#[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,
None,
true,
false,
false,
crate::json_style::JsonStyle::Compact,
),
ExitCode::SUCCESS
);
let json = audit_result(AuditVerdict::Fail, OutputFormat::Json);
assert_eq!(
print_brief_result(
&json,
None,
true,
false,
false,
crate::json_style::JsonStyle::Compact,
),
ExitCode::SUCCESS
);
}
#[test]
fn brief_json_validates_against_audit_brief_schema_variant() {
let result = audit_result(AuditVerdict::Fail, OutputFormat::Json);
let value = fallow_output::serialize_review_brief_json_output(
build_brief_json(&result, None).expect("brief json must build"),
crate::output_runtime::telemetry_analysis_run_id().as_deref(),
)
.expect("brief json must serialize");
assert_eq!(value["kind"], "audit-brief");
assert_eq!(value["command"], "audit-brief");
assert_eq!(value["schema_version"], REVIEW_BRIEF_SCHEMA_VERSION);
assert_eq!(value["attribution"]["styling_introduced"], 0);
assert_eq!(value["attribution"]["styling_inherited"], 0);
assert_eq!(value["attribution"]["duplication_demoted"], 0);
}
#[test]
fn brief_json_is_byte_identical_on_repeated_serialization() {
let result = audit_result(AuditVerdict::Warn, OutputFormat::Json);
let first = build_brief_json(&result, None).expect("first build");
let second = build_brief_json(&result, None).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 brief_json_serialization_honors_selected_style() {
let result = audit_result(AuditVerdict::Warn, OutputFormat::Json);
let value = build_brief_json(&result, None).expect("brief json must build");
let compact = serialize_brief_json(&value, crate::json_style::JsonStyle::Compact)
.expect("compact brief JSON must serialize");
let pretty = serialize_brief_json(&value, crate::json_style::JsonStyle::Pretty)
.expect("pretty brief JSON must serialize");
assert_eq!(compact.lines().count(), 1);
assert!(pretty.lines().count() > 1);
assert_eq!(
serde_json::from_str::<serde_json::Value>(&compact).expect("compact JSON must parse"),
serde_json::from_str::<serde_json::Value>(&pretty).expect("pretty JSON must parse")
);
}
#[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 triage_uses_supplied_diff_metrics_and_existing_risk_thresholds() {
let mut result = audit_result(AuditVerdict::Warn, OutputFormat::Json);
result.changed_files_count = 1;
let mut diff = String::from(
"diff --git a/src/a.ts b/src/a.ts\n--- a/src/a.ts\n+++ b/src/a.ts\n@@ -0,0 +1,100 @@\n",
);
for _ in 0..RISK_MEDIUM_LINES {
diff.push_str("+added\n");
}
let index = fallow_output::DiffIndex::from_unified_diff(&diff);
result.diff_index = Some(index);
let triage = build_brief_output(&result).triage;
assert_eq!(triage.hunks, Some(1));
assert_eq!(triage.net_lines, Some(RISK_MEDIUM_LINES));
assert_eq!(triage.risk_class, RiskClass::Medium);
assert_eq!(triage.review_effort, ReviewEffort::Review);
}
fn reach_export(
name: &str,
lines: (u32, u32),
) -> fallow_engine::module_graph::ExportTestReachPaths {
fallow_engine::module_graph::ExportTestReachPaths {
file: "src/a.ts".to_owned(),
export_name: name.to_owned(),
line: lines.0,
declaration_lines: lines,
literal_value: false,
reach: fallow_engine::module_graph::ExportTestReach::None,
tests: Vec::new(),
mocked_targets: Vec::new(),
}
}
#[test]
fn export_change_falls_back_to_added_lines_without_base_exports() {
use fallow_output::ExportChange;
let index = fallow_output::DiffIndex::from_unified_diff(
"diff --git a/src/a.ts b/src/a.ts\n--- a/src/a.ts\n+++ b/src/a.ts\n@@ -1,3 +1,3 @@\n line one\n-old\n+new\n line three\n",
);
let edited = reach_export("edited", (1, 3));
let one_line = reach_export("oneLine", (2, 2));
let other_file: rustc_hash::FxHashMap<String, FxHashSet<String>> =
std::iter::once(("src/b.ts".to_owned(), FxHashSet::default())).collect();
for base in [None, Some(&other_file)] {
assert_eq!(
super::export_change(base, &index, "src/a.ts", &edited),
ExportChange::Modified
);
assert_eq!(
super::export_change(base, &index, "src/a.ts", &one_line),
ExportChange::New
);
}
let with_base: rustc_hash::FxHashMap<String, FxHashSet<String>> = std::iter::once((
"src/a.ts".to_owned(),
std::iter::once("oneLine".to_owned()).collect(),
))
.collect();
assert_eq!(
super::export_change(Some(&with_base), &index, "src/a.ts", &one_line),
ExportChange::Modified
);
assert_eq!(
super::export_change(Some(&with_base), &index, "src/a.ts", &edited),
ExportChange::New
);
}
#[test]
fn no_diff_brief_keeps_optional_triage_fields_absent() {
let result = audit_result(AuditVerdict::Warn, OutputFormat::Json);
let value = serde_json::to_value(build_brief_output_with_diff(&result, None))
.expect("brief serializes");
assert!(value["triage"].get("hunks").is_none());
assert!(value["triage"].get("net_lines").is_none());
}
#[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, None).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 graph_facts_carry_no_file_list() {
let facts = derive_graph_facts(&AnalysisResults::default());
let value = serde_json::to_value(&facts).expect("graph facts serialize");
let keys: Vec<&str> = value
.as_object()
.expect("graph facts are an object")
.keys()
.map(String::as_str)
.collect();
assert_eq!(
keys,
vec!["exports_added", "api_width_delta", "boundaries_touched"]
);
}
fn spread_closure(affected: &[&str]) -> ImpactClosureFacts {
let mut paths: Vec<String> = affected.iter().map(|p| (*p).to_string()).collect();
paths.sort();
ImpactClosureFacts::new(&paths, Vec::new())
}
#[test]
fn human_impact_lines_report_the_full_count_not_the_sample() {
let mut affected: Vec<String> = (0..40).map(|i| format!("src/zone{i:03}/a.ts")).collect();
affected.extend((0..40).map(|i| format!("src/zone{i:03}/b.ts")));
affected.sort();
let closure = ImpactClosureFacts::new(&affected, Vec::new());
assert!(
closure.affected_not_shown.len() < closure.affected_count,
"the fixture must exercise the sample cap"
);
assert!(
closure.affected_by_dir.len() < 40,
"the fixture must exercise the rollup cap too"
);
let lines = affected_lines(&closure);
assert!(
lines[0].contains("80 files affected") && lines[0].contains("across 40 directories"),
"both totals survive capping, and neither stands in for the other: {lines:?}"
);
assert!(
lines[2].contains("and 39 more directories"),
"the remainder counts every directory, not just the kept rollup rows: {lines:?}"
);
}
#[test]
fn the_json_route_is_only_promised_when_the_json_holds_the_breakdown() {
let complete: Vec<String> = (0..3).map(|i| format!("src/z{i}/file.ts")).collect();
let lines = affected_lines(&ImpactClosureFacts::new(&complete, Vec::new()));
assert!(
lines[2].ends_with("(--format json for full list)"),
"an uncapped rollup really is the full list: {lines:?}"
);
let mut capped: Vec<String> = (0..40).map(|i| format!("src/z{i:03}/file.ts")).collect();
capped.sort();
let closure = ImpactClosureFacts::new(&capped, Vec::new());
assert!(closure.affected_by_dir_omitted > 0, "fixture must cap");
let lines = affected_lines(&closure);
assert!(
lines[2].ends_with("(24 of them in --format json)"),
"past the rollup cap the JSON has no full list either, so do not promise one: {lines:?}"
);
}
#[test]
fn the_repository_root_is_never_a_blank_token() {
let closure = spread_closure(&["setup.ts", "playground.ts", "src/app.ts"]);
let lines = affected_lines(&closure);
assert!(
lines[1].contains("heaviest <root> (2 files)"),
"a root-level directory must render as `<root>`, not as nothing: {lines:?}"
);
assert!(
lines[2].contains("and 1 more directory ("),
"a single remaining directory is not plural: {lines:?}"
);
}
#[test]
fn impact_closure_lines_fit_eighty_columns() {
let deep = "packages/platform/features/checkout/pricing/discounts/rules/seasonal";
let mut affected: Vec<String> = (0..40).map(|i| format!("{deep}/rule{i:03}.ts")).collect();
affected.extend((0..999).map(|i| format!("src/z{i:04}/file.ts")));
affected.sort();
for line in affected_lines(&ImpactClosureFacts::new(&affected, Vec::new())) {
assert!(
line.chars().count() <= 80,
"brief lines hold under 80 columns: {} chars in {line:?}",
line.chars().count()
);
}
}
#[test]
fn a_single_directory_reach_says_only_the_count() {
let lines = affected_lines(&spread_closure(&["src/a.ts", "src/b.ts"]));
assert_eq!(
lines,
vec![" impact closure: 2 files affected beyond the diff".to_string()],
"naming the one directory would repeat what the count said"
);
}
fn gap(consumer: &str, changed: &str, symbols: &[&str]) -> CoordinationGapFact {
CoordinationGapFact {
changed_file: changed.to_string(),
consumer_file: consumer.to_string(),
consumed_symbols: symbols.iter().map(|s| (*s).to_string()).collect(),
note: COORDINATION_GAP_NOTE.to_string(),
}
}
#[test]
fn search_gap_lines_name_the_hidden_consumers_and_fit_eighty_columns() {
use fallow_types::trace::{SearchGapReason, SearchVisibility};
assert!(search_gap_lines(None).is_empty());
let consumer =
|file: &str, visibility, search_as: &str| fallow_output::SearchGapConsumerFact {
consumer_file: file.to_string(),
line: Some(3),
search_visibility: visibility,
reason: Some(SearchGapReason::ReExportAlias),
search_as: Some(search_as.to_string()),
};
let long_dir = "packages/very-long-workspace-name/src/features/deeply/nested";
let facts = fallow_output::SearchGapFacts {
total: 5,
exports: vec![fallow_output::ExportSearchGapFact {
file: format!("{long_dir}/lib.ts"),
export_name: "formatDateForTheCurrentLocaleAndTimezone".to_string(),
line: 12,
total: 5,
consumers: vec![
consumer(&format!("{long_dir}/a.ts"), SearchVisibility::Missed, "fd"),
consumer("src/b.ts", SearchVisibility::Renamed, "fmt"),
consumer("src/c.ts", SearchVisibility::Missed, "lib"),
consumer("src/d.ts", SearchVisibility::Missed, "lib2"),
],
}],
};
let lines = search_gap_lines(Some(&facts));
assert_eq!(
lines[0],
" search gap: 5 consumer uses of changed exports hide from a name search"
);
assert!(
lines
.iter()
.any(|line| line.contains("renamed src/b.ts, search fmt"))
);
assert!(!lines.iter().any(|line| line.contains("src/d.ts")));
assert_eq!(
lines.last().map(String::as_str),
Some(" (--format json for the full list)")
);
for line in &lines {
assert!(
line.chars().count() <= 80,
"line overruns 80 columns: {line}"
);
}
}
fn reach_row(
file: &str,
name: &str,
reach: fallow_output::TestReachClass,
) -> fallow_output::ExportTestReachFact {
let mocked = reach == fallow_output::TestReachClass::MockedOnly;
fallow_output::ExportTestReachFact {
file: file.to_string(),
export_name: name.to_string(),
line: 120,
reach,
change: fallow_output::ExportChange::Modified,
tests: None,
tests_total: None,
test_changed: None,
mocked_targets: mocked.then(|| vec![file.to_string(), "src/other.ts".to_string()]),
}
}
fn reach_facts(
counts: fallow_output::TestReachCounts,
exports: Vec<fallow_output::ExportTestReachFact>,
) -> fallow_output::TestReachFacts {
fallow_output::TestReachFacts {
status: fallow_output::TestReachStatus::Measured,
masking: fallow_output::TestReachMasking::Active,
total: counts.direct
+ counts.transitive
+ counts.mocked_only
+ counts.none
+ counts.none_file
+ counts.framework,
counts,
exports,
exports_omitted: 0,
e2e_runner: false,
}
}
#[test]
fn test_reach_lines_fit_eighty_columns_and_cap_the_rows() {
use fallow_output::TestReachClass;
assert!(test_reach_lines(None).is_empty());
let long_dir = "packages/very-long-workspace-name/src/features/deeply/nested";
let counts = fallow_output::TestReachCounts {
direct: 1200,
transitive: 3400,
mocked_only: 5600,
none: 7800,
none_file: 1000,
framework: 9000,
};
let mut facts = reach_facts(
counts,
vec![
reach_row(
&format!("{long_dir}/queue.ts"),
"enqueueTheJobWithAVeryLongNameIndeed",
TestReachClass::MockedOnly,
),
reach_row(&format!("{long_dir}/lib.ts"), "none", TestReachClass::None),
reach_row(&format!("{long_dir}/ui.ts"), "ui", TestReachClass::NoneFile),
reach_row("src/t.ts", "transitive", TestReachClass::Transitive),
],
);
facts.e2e_runner = true;
let lines = test_reach_lines(Some(&facts));
assert!(lines[0].starts_with(" test reach: 28000 changed exports: 1200 direct,"));
assert!(
lines
.iter()
.any(|line| line.contains("tests mock .../") && line.ends_with(" +1"))
);
assert!(
lines
.iter()
.any(|line| line
.contains("file not reached by tests; e2e tests do not import files"))
);
assert!(!lines.iter().any(|line| line.contains("src/t.ts")));
assert_eq!(
lines.last().map(String::as_str),
Some(" (--format json for the full list)")
);
for line in &lines {
assert!(
line.chars().count() <= 80,
"line overruns 80 columns: {line}"
);
}
}
#[test]
fn test_reach_lines_align_the_reason_column() {
use fallow_output::TestReachClass;
let facts = reach_facts(
fallow_output::TestReachCounts {
mocked_only: 1,
none_file: 1,
..fallow_output::TestReachCounts::default()
},
vec![
reach_row("src/queue.ts", "enqueue", TestReachClass::MockedOnly),
reach_row("src/ui.ts", "renderPanel", TestReachClass::NoneFile),
],
);
assert_eq!(
test_reach_lines(Some(&facts)),
[
" test reach: 2 changed exports: 1 mocked only, 1 no test",
" mocked only src/queue.ts:120 enqueue tests mock src/queue.ts +1",
" no test src/ui.ts:120 renderPanel file not reached by tests",
]
);
}
#[test]
fn test_reach_lines_render_the_short_states() {
let direct_only = reach_facts(
fallow_output::TestReachCounts {
direct: 2,
framework: 1,
..fallow_output::TestReachCounts::default()
},
Vec::new(),
);
assert_eq!(
test_reach_lines(Some(&direct_only)),
[" test reach: 3 changed exports: 2 direct, 1 framework"]
);
let mut capped = reach_facts(
fallow_output::TestReachCounts {
transitive: 1,
..fallow_output::TestReachCounts::default()
},
vec![reach_row(
"src/a.ts",
"a",
fallow_output::TestReachClass::Transitive,
)],
);
capped.masking = fallow_output::TestReachMasking::Capped;
assert_eq!(
test_reach_lines(Some(&capped)),
[
" test reach: 1 changed export: 1 transitive",
" mock detection is off: too many distinct mock sets",
" transitive src/a.ts:120 a",
]
);
let mut no_tests = reach_facts(
fallow_output::TestReachCounts {
none_file: 7,
..fallow_output::TestReachCounts::default()
},
Vec::new(),
);
no_tests.status = fallow_output::TestReachStatus::NoTests;
assert_eq!(
test_reach_lines(Some(&no_tests)),
[" test reach: no test files in this project; 7 changed exports have no test"]
);
}
#[test]
fn coordination_gap_lines_fit_eighty_columns() {
let symbols: Vec<String> = (0..26)
.map(|i| format!("safeParseAsyncVariant{i:02}"))
.collect();
let symbol_refs: Vec<&str> = symbols.iter().map(String::as_str).collect();
let gaps: Vec<CoordinationGapFact> = (0..1234)
.map(|i| {
gap(
&format!(
"packages/platform/features/checkout/pricing/discounts/seasonal/regional/tiers/consumer{i:04}.ts"
),
"packages/platform/features/checkout/pricing/discounts/parse.ts",
&symbol_refs,
)
})
.collect();
let lines = coordination_gap_lines(&gaps);
assert!(
lines
.iter()
.any(|line| line.contains(".../") && line.chars().count() == 80),
"the fixture must drive the consumer line to the 80-column ceiling: {lines:?}"
);
assert_eq!(
lines[0].chars().count(),
80,
"a four-digit count drives the header to the ceiling too: {:?}",
lines[0]
);
assert!(
lines[0].contains("1234 consumers") && lines.last().is_some_and(|l| l.contains("1231")),
"four-digit counts must render on both the header and the remainder: {lines:?}"
);
for line in lines {
assert!(
line.chars().count() <= 80,
"brief lines hold under 80 columns: {} chars in {line:?}",
line.chars().count()
);
}
}
#[test]
fn one_consumer_of_two_changed_files_is_counted_and_printed_once() {
let forward = coordination_gap_lines(&[
gap("src/app.ts", "src/z-core.ts", &["parse"]),
gap("src/app.ts", "src/a-core.ts", &["render"]),
]);
assert_eq!(
forward,
vec![
" coordination gap: 1 consumer outside the diff uses exports of changed files"
.to_string(),
" src/app.ts".to_string(),
" consumes render from src/a-core.ts".to_string(),
" consumes parse from src/z-core.ts".to_string(),
],
"the header counts consumers and the rows below it agree"
);
let reversed = coordination_gap_lines(&[
gap("src/app.ts", "src/a-core.ts", &["render"]),
gap("src/app.ts", "src/z-core.ts", &["parse"]),
]);
assert_eq!(
forward, reversed,
"the order must not depend on the order the gaps arrived in"
);
}
#[test]
fn a_consumer_spanning_more_changed_files_than_fit_says_how_many_are_left() {
let consumer = "packages/platform/features/checkout/pricing/discounts/\
seasonal/regional/tiers/consumer0001.ts";
let sources: Vec<CoordinationGapFact> = (0..5)
.map(|i| gap(consumer, &format!("src/core{i}.ts"), &["parse"]))
.collect();
let lines = coordination_gap_lines(&sources);
assert_eq!(
lines.iter().filter(|l| l.contains(" consumes ")).count(),
2,
"one consumer cannot take the section over: {lines:?}"
);
assert_eq!(
lines.last().map(String::as_str),
Some(" (--format json for every changed file)"),
"the collapsed sources get a route that holds: {lines:?}"
);
assert!(
lines
.iter()
.any(|l| l == " and 3 more changed files"),
"the collapsed sources are counted, not dropped: {lines:?}"
);
assert!(
lines.iter().any(|l| l.chars().count() == 80),
"the fixture must drive the consumer line to the ceiling: {lines:?}"
);
for line in &lines {
assert!(
line.chars().count() <= 80,
"brief lines hold under 80 columns: {} chars in {line:?}",
line.chars().count()
);
}
}
#[test]
fn the_widest_changed_files_are_the_ones_spelled_out_under_a_consumer() {
let gaps = vec![
gap("src/consumer.ts", "src/a-logger.ts", &["log"]),
gap("src/consumer.ts", "src/b-const.ts", &["VERSION"]),
gap(
"src/consumer.ts",
"src/z-contract.ts",
&[
"createOrder",
"cancelOrder",
"priceCart",
"applyTax",
"quoteShipping",
"capturePayment",
"refund",
"voidAuth",
],
),
];
let lines = coordination_gap_lines(&gaps);
assert!(
lines[2].ends_with(" from src/z-contract.ts"),
"the changed file the consumer takes the most from leads: {lines:?}"
);
assert_eq!(
lines[3], " consumes log from src/a-logger.ts",
"two one-symbol files tie, and the tie breaks on path: {lines:?}"
);
assert!(
!lines.iter().any(|l| l.contains("b-const")),
"the file that loses the tie is the one that collapses: {lines:?}"
);
assert!(
lines
.iter()
.any(|l| l == " and 1 more changed file"),
"the collapsed file is counted, not dropped: {lines:?}"
);
}
#[test]
fn the_widest_consumers_are_the_ones_spelled_out() {
let mut gaps: Vec<CoordinationGapFact> = (0..5)
.map(|i| gap(&format!("src/a{i}.ts"), "src/core.ts", &["parse"]))
.collect();
gaps.push(gap(
"src/zz-barrel.ts",
"src/core.ts",
&["parse", "decode", "encode"],
));
let lines = coordination_gap_lines(&gaps);
assert!(
lines[1].contains("src/zz-barrel.ts"),
"the consumer taking the most symbols leads: {lines:?}"
);
}
#[test]
fn coordination_gaps_beyond_the_cap_are_counted_not_dropped_silently() {
let gaps: Vec<CoordinationGapFact> = (0..7)
.map(|i| gap(&format!("src/c{i}.ts"), "src/core.ts", &["parse"]))
.collect();
let lines = coordination_gap_lines(&gaps);
assert!(
lines[0].starts_with(
" coordination gap: 7 consumers outside the diff use exports of changed files"
),
"{lines:?}"
);
assert_eq!(
lines.len(),
8,
"a header, two lines per shown consumer, then the remainder: {lines:?}"
);
assert!(
lines
.last()
.expect("a remainder line")
.contains("and 4 more consumers (--format json for full list)"),
"{lines:?}"
);
}
#[test]
fn a_single_coordination_gap_reads_as_singular_and_needs_no_remainder() {
let lines = coordination_gap_lines(&[gap("src/app.ts", "src/core.ts", &["parse"])]);
assert_eq!(
lines,
vec![
" coordination gap: 1 consumer outside the diff uses exports of changed files"
.to_string(),
" src/app.ts".to_string(),
" consumes parse from src/core.ts".to_string(),
]
);
}
#[test]
fn an_over_budget_first_symbol_does_not_swallow_the_more_suffix() {
let long = "aVeryLongExportedSymbolNameIndeed";
let (text, omitted) = summarize_symbols(&[long.to_string(), "parse".to_string()], 24);
assert_eq!(omitted, 1);
assert!(text.ends_with(" +1 more"), "{text:?}");
assert!(
text.chars().count() <= 24,
"{} chars in {text:?}",
text.chars().count()
);
}
#[test]
fn an_elided_symbol_list_still_gets_a_route_without_a_remainder_line() {
let symbols: Vec<String> = (0..30).map(|i| format!("symbolNumber{i:02}")).collect();
let symbol_refs: Vec<&str> = symbols.iter().map(String::as_str).collect();
let lines = coordination_gap_lines(&[gap("src/app.ts", "src/core.ts", &symbol_refs)]);
assert!(
lines[2].contains('+'),
"the fixture must elide symbols: {lines:?}"
);
assert_eq!(
lines.last().map(String::as_str),
Some(" (--format json for every consumed symbol)"),
"a `+N more` with no remainder line still needs somewhere to go: {lines:?}"
);
}
fn focus_unit(
file: &str,
reason: &str,
label: crate::audit_focus::FocusLabel,
) -> crate::audit_focus::FocusUnit {
use crate::audit_focus::{ConfidenceFlag, FocusScore, FocusUnit};
FocusUnit {
file: file.to_string(),
score: FocusScore {
fan_io: 1,
security_taint: 0,
risk_zone: 0,
change_shape: 0,
runtime: 0,
total: 1,
},
label,
reason: reason.to_string(),
confidence: vec![ConfidenceFlag::ReExportIndirection],
}
}
fn wide_focus() -> crate::audit_focus::FocusMap {
let deep = "packages/platform/features/checkout/pricing/discounts/seasonal/regional/tiers/rules.ts";
let reason = "high fan-in (312 importers), fan-out 47, changes a contract consumed \
outside the diff, sits in a risk zone, and carries a security-tainted \
argument reachable from an untrusted source";
use crate::audit_focus::FocusLabel;
crate::audit_focus::FocusMap {
review_here: vec![focus_unit(deep, reason, FocusLabel::ReviewHere)],
deprioritized: vec![focus_unit(deep, reason, FocusLabel::NotPrioritized)],
}
}
#[test]
fn focus_lines_fit_eighty_columns() {
for show_deprioritized in [false, true] {
let lines = focus_lines(&wide_focus(), show_deprioritized);
assert!(
lines.iter().any(|line| line.contains(".../")),
"the fixture must exercise path elision: {lines:?}"
);
assert!(
lines
.iter()
.filter(|l| l.starts_with(" ") && !l.contains("confidence"))
.count()
>= 1,
"the fixture must wrap a reason onto a continuation line, and the \
`confidence` row must not stand in for one: {lines:?}"
);
if show_deprioritized {
let widest = lines
.iter()
.find(|l| l.starts_with(" [not-prioritized] "))
.expect("the expanded branch renders the de-prioritized unit");
assert_eq!(
widest.chars().count(),
78,
"the widest focus row sits at its budget: {widest:?}"
);
}
for line in lines {
assert!(
line.chars().count() <= 80,
"brief lines hold under 80 columns: {} chars in {line:?}",
line.chars().count()
);
}
}
}
#[test]
fn a_wrapped_focus_reason_keeps_every_word() {
let reason =
"high fan-in (2 importers), fan-out 4, changes a contract consumed outside the diff";
let lines = focus_lines(
&crate::audit_focus::FocusMap {
review_here: vec![focus_unit(
"src/a.ts",
reason,
crate::audit_focus::FocusLabel::ReviewHere,
)],
deprioritized: Vec::new(),
},
false,
);
let rejoined: String = lines
.iter()
.filter(|l| l.starts_with(" ") && !l.contains("confidence"))
.map(|l| l.trim())
.collect::<Vec<_>>()
.join(" ");
assert_eq!(
rejoined, reason,
"wrapping re-flows the reason, it never drops a word"
);
}
fn decision(
question: &str,
tradeoff: &str,
experts: &[&str],
) -> crate::audit_decision_surface::Decision {
use crate::audit_decision_surface::{Decision, DecisionCategory};
Decision {
signal_id: "sig".to_string(),
category: DecisionCategory::PublicApiContract,
question: question.to_string(),
anchor_file: "src/core.ts".to_string(),
anchor_line: 1,
signal_key: "key".to_string(),
previous_signal_id: None,
blast: 1,
consequence: 1,
expert: experts.iter().map(|e| (*e).to_string()).collect(),
bus_factor_one: true,
internal_consumer_count: 1,
tradeoff: tradeoff.to_string(),
}
}
#[test]
fn decision_surface_lines_fit_eighty_columns() {
let exports = (0..26)
.map(|i| format!("safeParseAsyncVariant{i:02}"))
.collect::<Vec<_>>()
.join(", ");
let question = format!(
"`packages/platform/features/checkout/pricing/parse.ts` changes exports \
({exports}) imported by 312 files outside this PR. Does this change break \
or alter what those callers expect?"
);
let surface = crate::audit_decision_surface::DecisionSurface {
decisions: vec![decision(
&question,
"312 modules outside the diff consume this contract; changing its shape \
requires coordinating them.",
&[
"a-very-long-github-handle",
"another-long-handle",
"third-handle",
],
)],
truncated: Some(crate::audit_decision_surface::TruncationNote {
collapsed: 9,
reason: "9 more structural decisions collapsed below the cap of 4".to_string(),
}),
emitted_signal_ids: vec!["sig".to_string()],
};
let lines = decision_surface_lines(&surface);
assert!(
lines.iter().filter(|l| l.starts_with(" ")).count() > 3,
"the fixture must exercise wrapping: {lines:?}"
);
assert!(
lines.iter().any(|l| l.chars().count() == 80),
"the fixture must drive a line to the ceiling: {lines:?}"
);
let ask = lines
.iter()
.find(|l| l.starts_with(" ask: "))
.expect("an ask line");
assert!(
ask.ends_with("(bus-factor 1)") || lines.iter().any(|l| l.ends_with("(bus-factor 1)")),
"the bus-factor suffix still lands: {lines:?}"
);
assert!(
!ask.contains("..."),
"an owner identity that fits must not be shortened: {ask:?}"
);
let route_at = lines
.iter()
.position(|l| l == JSON_NAME_ROUTE)
.unwrap_or_else(|| panic!("the collapsed names need a route: {lines:?}"));
let note_at = lines
.iter()
.position(|l| l.starts_with(" ... "))
.unwrap_or_else(|| panic!("the collapsed decisions need a note: {lines:?}"));
assert!(
route_at < note_at,
"the name route stays with the questions it routes: under the note it \
reads as routing the collapsed DECISIONS, and the JSON carries those \
only as ids: {lines:?}"
);
for line in lines {
assert!(
line.chars().count() <= 80,
"brief lines hold under 80 columns: {} chars in {line:?}",
line.chars().count()
);
}
}
fn zod_shaped_question() -> String {
let exports = [
"_decode",
"_decodeAsync",
"_encode",
"_encodeAsync",
"_parse",
"_parseAsync",
"_safeDecode",
"_safeDecodeAsync",
"_safeEncode",
"_safeEncodeAsync",
"_safeParse",
"_safeParseAsync",
"decode",
"decodeAsync",
"encode",
"encodeAsync",
"parse",
"parseAsync",
"safeDecode",
"safeDecodeAsync",
"safeEncode",
"safeEncodeAsync",
"validate",
"validateAsync",
]
.join(", ");
format!(
"`packages/zod/src/v4/core/parse.ts` changes exports ({exports}) imported by 6 \
files outside this PR. Does this change break or alter what those callers \
expect?"
)
}
#[test]
fn a_capped_member_list_keeps_the_path_and_the_ask() {
let question = zod_shaped_question();
let lines = decision_surface_lines(&crate::audit_decision_surface::DecisionSurface {
decisions: vec![decision(&question, "", &[])],
truncated: None,
emitted_signal_ids: vec!["sig".to_string()],
});
let rendered = lines.join(" ");
assert!(
rendered.contains("+21 more"),
"the collapsed names are counted, not dropped: {lines:?}"
);
assert!(
!rendered.contains("safeEncodeAsync"),
"a name past the cap must not still render: {lines:?}"
);
assert!(
rendered.contains("`packages/zod/src/v4/core/parse.ts`"),
"the anchor path appears nowhere else on the brief: {lines:?}"
);
assert!(
rendered.contains("Does this change break or alter what those callers expect?"),
"the ask is the judgment the brief poses and survives the cap: {lines:?}"
);
assert!(
lines
.iter()
.any(|l| l == " (--format json for every collapsed name)"),
"collapsed names get a route that holds: {lines:?}"
);
}
#[test]
fn a_zod_shaped_question_reads_in_three_lines() {
let question = zod_shaped_question();
let head = format!(
" 1. [{}] ",
crate::audit_decision_surface::DecisionCategory::PublicApiContract.tag()
);
assert!(
wrap_prose(&question, 80 - head.chars().count(), 73, elide_path).len() > 5,
"the fixture must be one the cap actually shortens: {question}"
);
let lines = decision_surface_lines(&crate::audit_decision_surface::DecisionSurface {
decisions: vec![decision(&question, "", &[])],
truncated: None,
emitted_signal_ids: vec!["sig".to_string()],
});
let route_at = lines
.iter()
.position(|l| l == JSON_NAME_ROUTE)
.unwrap_or_else(|| panic!("the collapsed names need a route: {lines:?}"));
assert_eq!(
&lines[route_at + 1..],
[String::new()],
"the route closes the section, ahead of its blank line: {lines:?}"
);
let question_lines = &lines[1..route_at];
assert_eq!(
question_lines.len(),
3,
"a widened export list costs three lines, not a screenful: {lines:?}"
);
assert!(question_lines[0].starts_with(&head), "{lines:?}");
for line in &lines {
assert!(
line.chars().count() <= 80,
"brief lines hold under 80 columns: {} chars in {line:?}",
line.chars().count()
);
}
}
#[test]
fn a_route_group_anchor_still_collapses_its_export_list() {
let question = zod_shaped_question().replace(
"packages/zod/src/v4/core/parse.ts",
"app/(marketing)/lib/api.ts",
);
let lines = decision_surface_lines(&crate::audit_decision_surface::DecisionSurface {
decisions: vec![decision(&question, "", &[])],
truncated: None,
emitted_signal_ids: vec!["sig".to_string()],
});
let rendered = lines.join(" ");
assert!(
rendered.contains("`app/(marketing)/lib/api.ts`"),
"the route group survives verbatim: {lines:?}"
);
assert!(
rendered.contains("+21 more") && !rendered.contains("safeEncodeAsync"),
"the export list is the parenthetical that collapses: {lines:?}"
);
assert!(
lines.iter().any(|l| l == JSON_NAME_ROUTE),
"a collapsed list routes to the full one: {lines:?}"
);
}
#[test]
fn an_uncapped_question_is_promised_no_route_it_does_not_need() {
let lines = decision_surface_lines(&crate::audit_decision_surface::DecisionSurface {
decisions: vec![decision(
"`src/core.ts` changes exports (parse, decode) imported by 3 files outside \
this PR. Does this change break or alter what those callers expect?",
"",
&[],
)],
truncated: None,
emitted_signal_ids: vec!["sig".to_string()],
});
assert!(
!lines.iter().any(|l| l.contains("collapsed name")),
"nothing collapsed, so nothing to route to: {lines:?}"
);
}
#[test]
fn a_decision_with_no_experts_omits_the_ask_line() {
let lines = decision_surface_lines(&crate::audit_decision_surface::DecisionSurface {
decisions: vec![decision("Widens the public surface. Intended?", "", &[])],
truncated: None,
emitted_signal_ids: vec!["sig".to_string()],
});
assert!(!lines.iter().any(|l| l.contains("ask:")), "{lines:?}");
assert!(!lines.iter().any(|l| l.contains("trade-off:")), "{lines:?}");
}
#[test]
fn an_empty_decision_surface_still_says_so() {
let lines =
decision_surface_lines(&crate::audit_decision_surface::DecisionSurface::default());
assert_eq!(
lines.first().map(String::as_str),
Some("Decisions: none (no consequential structural decision in this change)")
);
}
#[test]
fn an_unscored_focus_map_renders_nothing() {
assert!(focus_lines(&crate::audit_focus::FocusMap::default(), false).is_empty());
assert!(focus_lines(&crate::audit_focus::FocusMap::default(), true).is_empty());
}
#[test]
fn a_word_wider_than_its_line_is_shortened_not_overflowed() {
let deep =
"packages/platform/features/checkout/pricing/discounts/seasonal/regional/rules.ts";
let lines = wrap_prose(&format!("touches {deep} directly"), 40, 40, elide_path);
for line in &lines {
assert!(line.chars().count() <= 40, "{line:?}");
}
assert!(
lines.iter().any(|l| l.contains(".../")),
"a path keeps its tail: {lines:?}"
);
}
#[test]
fn an_owner_identity_is_shortened_from_the_head_not_the_tail() {
let owner = "very.long.firstname.lastname@engineering.example.com";
let lines = wrap_prose(owner, 30, 30, elide_symbol);
assert_eq!(lines.len(), 1);
assert!(
lines[0].starts_with("very.long.first") && lines[0].ends_with("..."),
"{lines:?}"
);
}
fn ownership_fixture(
groups: usize,
slices: Vec<fallow_output::OwnershipSliceFact>,
) -> fallow_output::OwnershipFacts {
let kept = groups.min(fallow_output::OWNER_GROUP_CAP);
fallow_output::OwnershipFacts {
group_count: groups,
transitive_only_count: 12,
unowned_direct_count: 123,
groups: (0..kept)
.map(|i| fallow_output::OwnerGroupFact {
owner: format!("@a-very-long-organization-name/team-{i:02}"),
direct_count: 100 + i,
affected_count: 10_000 + i,
})
.collect(),
groups_omitted: groups - kept,
slices,
}
}
fn slice(dirs: &[&str], owners: &[&str]) -> fallow_output::OwnershipSliceFact {
fallow_output::OwnershipSliceFact {
module_dirs: dirs.iter().map(|d| (*d).to_string()).collect(),
owners: owners.iter().map(|o| (*o).to_string()).collect(),
separable: owners.len() == 1,
}
}
#[test]
fn ownership_lines_fit_eighty_columns() {
let slices = (0..12)
.map(|i| {
slice(
&[
"packages/some-deeply-nested/module-directory",
"packages/other",
],
&[if i % 2 == 0 {
"@a-very-long-organization-name/design-system-team"
} else {
"@x"
}],
)
})
.collect();
let lines = ownership_lines(Some(&ownership_fixture(40, slices)));
assert!(lines.len() > 10, "{lines:?}");
for line in &lines {
assert!(
line.chars().count() <= 80,
"{} columns: {line}",
line.chars().count()
);
}
}
#[test]
fn ownership_lines_count_the_groups_beyond_the_cap() {
let lines = ownership_lines(Some(&ownership_fixture(
fallow_output::OWNER_GROUP_CAP + 3,
Vec::new(),
)));
assert_eq!(lines.last().map(String::as_str), Some(" +3 more groups"));
assert_eq!(lines.len(), 1 + fallow_output::OWNER_GROUP_CAP + 1);
}
#[test]
fn ownership_lines_name_only_the_slices_with_one_owner() {
let lines = ownership_lines(Some(&ownership_fixture(
2,
vec![
slice(&["src/app", "src/core"], &["@team/app", "@team/core"]),
slice(&["src/tools"], &["@team/tools"]),
],
)));
assert!(!lines.iter().any(|l| l.contains("slice 1 ")), "{lines:?}");
assert!(
lines.contains(&" slice 2 (src/tools) has one owner: @team/tools".to_string()),
"{lines:?}"
);
}
#[test]
fn ownership_slice_lines_keep_the_distinct_tail_of_long_directories() {
let lines = ownership_lines(Some(&ownership_fixture(
2,
vec![
slice(
&["packages/hoppscotch-selfhost-web/src/api/queries"],
&["@a"],
),
slice(
&[
"packages/hoppscotch-selfhost-web/src/platform/infra",
"packages/other",
],
&[crate::codeowners::UNOWNED_LABEL],
),
],
)));
assert!(
lines
.iter()
.any(|l| l.starts_with(" slice 1 (") && l.contains("api/queries)")),
"{lines:?}"
);
assert!(
lines
.iter()
.any(|l| l.contains("platform/infra +1 more) is unowned")),
"{lines:?}"
);
}
#[test]
fn a_slice_label_removes_whole_leading_segments() {
assert_eq!(
elide_leading_segments("packages/hoppscotch-backend/src/infra-config", 27),
".../src/infra-config"
);
assert_eq!(elide_leading_segments("src/short", 27), "src/short");
let label = elide_leading_segments("a/an-extremely-long-single-folder-name-here", 20);
assert_eq!(label.chars().count(), 20, "{label}");
}
#[test]
fn ownership_lines_are_silent_without_codeowners_or_slices() {
assert!(ownership_lines(None).is_empty());
let lines = ownership_lines(Some(&ownership_fixture(1, Vec::new())));
assert!(!lines.iter().any(|l| l.contains("slice")), "{lines:?}");
}
#[test]
fn no_gaps_prints_nothing() {
assert!(coordination_gap_lines(&[]).is_empty());
}
#[test]
fn a_symbol_list_that_fits_is_printed_whole() {
assert_eq!(
summarize_symbols(&["a".into(), "b".into()], 24),
("a, b".to_string(), 0)
);
}
#[test]
fn a_single_symbol_wider_than_the_budget_is_elided_not_dropped() {
let (one, omitted) = summarize_symbols(&["aRidiculouslyLongExportedSymbolName".into()], 24);
assert_eq!(omitted, 0);
assert_eq!(one.chars().count(), 24, "{one:?}");
assert!(
one.starts_with("aRidiculously") && one.ends_with("..."),
"{one:?}"
);
}
#[test]
fn an_empty_closure_prints_nothing() {
assert!(affected_lines(&ImpactClosureFacts::new(&[], Vec::new())).is_empty());
assert!(affected_lines(&ImpactClosureFacts::default()).is_empty());
}
#[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"));
}
fn branching_fixture(
previous: (u32, u32, u16),
current: (u32, u32, u16),
) -> fallow_output::BranchingReport {
let unit = |(branch_points, functions, peak): (u32, u32, u16)| {
std::iter::once((
"src/checkout/pricing.ts".to_string(),
fallow_types::extract::FileBranching {
branch_points,
functions,
peak_cyclomatic: peak,
cognitive: branch_points,
cognitive_nesting_weight: 0,
has_module_unit: false,
has_synthetic_units: false,
},
))
.collect::<fallow_output::BranchingSnapshot>()
};
fallow_output::BranchingReport::compare(
&unit(previous),
&unit(current),
fallow_output::DEFAULT_BRANCHING_TOLERANCE,
&|_| false,
)
}
#[test]
fn branching_lines_are_silent_without_a_split() {
assert!(branching_human_lines(None).is_none());
let flat = branching_fixture((12, 3, 5), (12, 3, 5));
assert!(
branching_human_lines(Some(&flat)).is_none(),
"set totals alone are context, matching every sibling section"
);
}
#[test]
fn branching_lines_name_the_file_that_split() {
let report = branching_fixture((39, 1, 40), (39, 8, 6));
let lines = branching_human_lines(Some(&report)).expect("a split renders");
assert_eq!(lines.len(), 3);
assert!(lines[0].contains("branching about level, more functions, smaller peak"));
assert!(
lines[1].ends_with("src/checkout/pricing.ts"),
"{}",
lines[1]
);
assert!(
lines[2].contains("39 to 39 branch points, 1 to 8 functions, peak 40 to 6"),
"{}",
lines[2]
);
}
#[test]
fn branching_lines_fit_eighty_columns() {
let long = "src/features/checkout/pricing/discounts/calculate-line-totals.ts";
let unit = |branch_points: u32, functions: u32, peak: u16| {
std::iter::once((
long.to_string(),
fallow_types::extract::FileBranching {
branch_points,
functions,
peak_cyclomatic: peak,
cognitive: branch_points,
cognitive_nesting_weight: 0,
has_module_unit: false,
has_synthetic_units: false,
},
))
.collect::<fallow_output::BranchingSnapshot>()
};
let report = fallow_output::BranchingReport::compare(
&unit(9999, 1000, 9999),
&unit(9999, 4000, 12),
fallow_output::DEFAULT_BRANCHING_TOLERANCE,
&|_| false,
);
for line in branching_human_lines(Some(&report)).expect("a split renders") {
assert!(
line.chars().count() <= 80,
"{} columns: {line}",
line.chars().count()
);
}
}
}