use crate::audit_walkthrough::{DirectionUnit, StandardWalkthroughGuide};
pub const MAX_CONTRACT_MEMBERS: usize = 6;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WalkthroughAccounting {
pub changed: usize,
pub staged: usize,
pub cleared: usize,
pub excluded: usize,
}
impl WalkthroughAccounting {
#[must_use]
pub fn compute(guide: &StandardWalkthroughGuide, viewed: &[String]) -> Self {
let mut staged_visible = 0usize;
let mut collapsed = 0usize;
for file in &guide.direction.order {
if is_deprioritized(guide, file) || is_collapsed_into_cleared(file, viewed) {
collapsed += 1;
} else {
staged_visible += 1;
}
}
let deprioritized_off_spine = guide
.digest
.focus
.deprioritized
.iter()
.filter(|u| !guide.direction.order.iter().any(|f| f == &u.file))
.count();
let cleared = collapsed + deprioritized_off_spine;
let source_units = guide.digest.focus.total_units();
let changed = guide.digest.triage.files;
let excluded = changed.saturating_sub(source_units);
WalkthroughAccounting {
changed,
staged: staged_visible,
cleared,
excluded,
}
}
#[must_use]
pub fn header_total(&self) -> usize {
(self.staged + self.cleared + self.excluded).max(self.changed)
}
}
#[must_use]
pub fn clean_decision_fact(question: &str, anchor_file: &str, max_members: usize) -> String {
let stripped = strip_leading_path(question, anchor_file);
let capped = cap_member_list(&stripped, max_members);
drop_trailing_question(&capped)
}
fn strip_leading_path(question: &str, anchor_file: &str) -> String {
let prefix = format!("`{anchor_file}` ");
question
.strip_prefix(&prefix)
.map_or_else(|| question.to_string(), str::to_string)
}
fn cap_member_list(text: &str, max_members: usize) -> String {
let Some(open) = text.find('(') else {
return text.to_string();
};
let Some(rel_close) = text[open..].find(')') else {
return text.to_string();
};
let close = open + rel_close;
let inner = &text[open + 1..close];
let members: Vec<&str> = inner.split(", ").collect();
if members.len() <= max_members {
return text.to_string();
}
let shown = members[..max_members].join(", ");
let more = members.len() - max_members;
format!(
"{}({shown}, +{more} more){}",
&text[..open],
&text[close + 1..]
)
}
fn drop_trailing_question(text: &str) -> String {
let parts: Vec<&str> = text.split(". ").collect();
let mut end = parts.len();
while end > 0 && parts[end - 1].trim_end().ends_with('?') {
end -= 1;
}
if end == parts.len() || end == 0 {
return text.to_string();
}
let kept = parts[..end].join(". ");
if kept.ends_with(['.', '!', '?']) {
kept
} else {
format!("{kept}.")
}
}
#[must_use]
pub fn cap_names(names: &[String], max: usize) -> (Vec<&str>, usize) {
let shown: Vec<&str> = names.iter().take(max).map(String::as_str).collect();
let more = names.len().saturating_sub(shown.len());
(shown, more)
}
#[must_use]
fn is_collapsed_into_cleared(file: &str, viewed: &[String]) -> bool {
viewed.iter().any(|v| v == file)
}
#[must_use]
fn is_deprioritized(guide: &StandardWalkthroughGuide, file: &str) -> bool {
guide
.digest
.focus
.deprioritized
.iter()
.any(|u| u.file == file)
}
#[must_use]
fn collapses_into_cleared(guide: &StandardWalkthroughGuide, file: &str, viewed: &[String]) -> bool {
is_deprioritized(guide, file) || is_collapsed_into_cleared(file, viewed)
}
#[must_use]
pub fn visible_stage_units<'a>(
guide: &'a StandardWalkthroughGuide,
viewed: &[String],
) -> Vec<&'a DirectionUnit> {
guide
.direction
.order
.iter()
.filter(|file| !collapses_into_cleared(guide, file, viewed))
.filter_map(|file| guide.direction.units.iter().find(|u| &u.file == file))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::audit_brief::{
DiffTriage, GraphFacts, ImpactClosureFacts, PartitionFacts, ReviewBriefSchemaVersion,
ReviewDeltas, ReviewEffort, RiskClass, StandardReviewBriefOutput,
};
use crate::audit_decision_surface::DecisionSurface;
use crate::audit_focus::{FocusLabel, FocusMap, FocusScore, FocusUnit};
use crate::audit_routing::RoutingFacts;
use crate::audit_walkthrough::{
AgentSchema, DirectionUnit, INJECTION_NOTE, ReviewDirection, StandardWalkthroughGuide,
};
fn focus_unit(file: &str, label: FocusLabel) -> FocusUnit {
FocusUnit {
file: file.to_string(),
score: FocusScore::default(),
label,
reason: format!("reason for {file}"),
confidence: Vec::new(),
}
}
fn dir_unit(file: &str) -> DirectionUnit {
DirectionUnit {
file: file.to_string(),
concern_lens: "orientation".to_string(),
scoring_budget: 1,
out_of_diff: Vec::new(),
expert: Vec::new(),
}
}
fn guide_for(
review_here: &[&str],
deprioritized: &[&str],
changed_total: usize,
) -> StandardWalkthroughGuide {
let order: Vec<String> = review_here
.iter()
.chain(deprioritized.iter())
.map(|s| (*s).to_string())
.collect();
let units: Vec<DirectionUnit> = order.iter().map(|f| dir_unit(f)).collect();
let digest = StandardReviewBriefOutput {
schema_version: ReviewBriefSchemaVersion::default(),
version: "test".to_string(),
command: "audit-brief".to_string(),
triage: DiffTriage {
files: changed_total,
hunks: None,
net_lines: None,
risk_class: RiskClass::Medium,
review_effort: ReviewEffort::Review,
},
graph_facts: GraphFacts {
exports_added: 0,
api_width_delta: 0,
reachable_from: Vec::new(),
boundaries_touched: Vec::new(),
},
partition: PartitionFacts::default(),
impact_closure: ImpactClosureFacts::default(),
focus: FocusMap {
review_here: review_here
.iter()
.map(|f| focus_unit(f, FocusLabel::ReviewHere))
.collect(),
deprioritized: deprioritized
.iter()
.map(|f| focus_unit(f, FocusLabel::NotPrioritized))
.collect(),
},
deltas: ReviewDeltas::default(),
weakening: Vec::new(),
routing: RoutingFacts::default(),
decisions: DecisionSurface::default(),
};
StandardWalkthroughGuide {
schema_version: ReviewBriefSchemaVersion::default(),
version: "test".to_string(),
command: "review-walkthrough-guide".to_string(),
graph_snapshot_hash: "hash1".to_string(),
digest,
direction: ReviewDirection { order, units },
change_anchors: Vec::new(),
agent_schema: AgentSchema {
judgment_shape: "",
echo_field: "graph_snapshot_hash",
anchoring_rule: "",
},
injection_note: INJECTION_NOTE,
}
}
#[test]
fn accounting_reconciles_staged_cleared_excluded() {
let guide = guide_for(&["src/a.ts", "src/b.ts"], &["src/c.ts"], 16);
let acc = WalkthroughAccounting::compute(&guide, &[]);
assert_eq!(acc.changed, 16);
assert_eq!(acc.staged, 2, "review-here source units stay in stages");
assert_eq!(acc.cleared, 1, "de-prioritized collapses into cleared");
assert_eq!(
acc.excluded, 13,
"non-source files are excluded, not dropped"
);
assert_eq!(acc.header_total(), 16);
assert_eq!(acc.staged + acc.cleared + acc.excluded, acc.changed);
}
#[test]
fn viewed_file_moves_from_staged_to_cleared() {
let guide = guide_for(&["src/a.ts", "src/b.ts"], &[], 2);
let viewed = vec!["src/a.ts".to_string()];
let acc = WalkthroughAccounting::compute(&guide, &viewed);
assert_eq!(acc.staged, 1, "the viewed file left the stage");
assert_eq!(acc.cleared, 1, "the viewed file is counted in cleared");
assert_eq!(acc.excluded, 0);
assert_eq!(acc.staged + acc.cleared + acc.excluded, acc.changed);
}
#[test]
fn deprioritized_and_viewed_appear_in_exactly_one_place() {
let guide = guide_for(&["src/a.ts", "src/b.ts"], &["src/c.ts"], 3);
let viewed = vec!["src/a.ts".to_string()];
let visible = visible_stage_units(&guide, &viewed);
let files: Vec<&str> = visible.iter().map(|u| u.file.as_str()).collect();
assert_eq!(files, vec!["src/b.ts"]);
assert!(collapses_into_cleared(&guide, "src/a.ts", &viewed));
assert!(collapses_into_cleared(&guide, "src/c.ts", &viewed));
assert!(!collapses_into_cleared(&guide, "src/b.ts", &viewed));
}
#[test]
fn strips_leading_path_caps_members_and_drops_question() {
let q = "`src/db/schema.ts` changes exports (a, b, c, d, e, f, g, h) imported by 32 files outside this PR. Does this change break or alter what those callers expect?";
let out = clean_decision_fact(q, "src/db/schema.ts", 3);
assert!(
!out.starts_with("`src/db/schema.ts`"),
"leading path must be stripped: {out}"
);
assert!(out.contains("(a, b, c, +5 more)"), "got: {out}");
assert!(
!out.contains('?'),
"trailing question must be dropped: {out}"
);
assert!(
out.ends_with("outside this PR."),
"the observation survives, ending cleanly: {out}"
);
assert!(!out.contains('`'), "no backticks remain: {out}");
}
#[test]
fn short_member_list_is_kept_and_question_dropped() {
let q = "`src/lib/r2.ts` changes exports (getR2, getR2Text) imported by 6 files outside this PR. Does this change break or alter what those callers expect?";
let out = clean_decision_fact(q, "src/lib/r2.ts", 6);
assert_eq!(
out,
"changes exports (getR2, getR2Text) imported by 6 files outside this PR."
);
}
#[test]
fn single_member_prose_parenthetical_is_kept_question_dropped() {
let q = "`src/lib/env.ts` changes export (env) imported by 22 files outside this PR. Does this change break or alter what those callers expect?";
let out = clean_decision_fact(q, "src/lib/env.ts", 6);
assert!(out.contains("(env)"), "single member kept: {out}");
assert!(!out.contains('?'), "trailing question dropped: {out}");
assert!(out.ends_with("outside this PR."), "observation kept: {out}");
}
#[test]
fn non_anchor_path_is_kept_but_question_dropped() {
let q = "`ui` now imports `db` for the first time. Intended coupling, or should this edge not exist?";
let out = clean_decision_fact(q, "src/ui/page.ts", 6);
assert_eq!(out, "`ui` now imports `db` for the first time.");
}
#[test]
fn public_api_surface_question_drops_to_one_sentence() {
let q = "This change adds 3 exports to the public API surface. Intended as maintained contracts, or should they stay internal?";
let out = clean_decision_fact(q, "src/lib/id.ts", 6);
assert_eq!(out, "This change adds 3 exports to the public API surface.");
}
#[test]
fn cap_names_first_k_then_more() {
let names = vec![
"a".to_string(),
"b".to_string(),
"c".to_string(),
"d".to_string(),
];
let (shown, more) = cap_names(&names, 2);
assert_eq!(shown, vec!["a", "b"]);
assert_eq!(more, 2);
}
}