Group the candidate modifications into ordered semantic layers. Pure and deterministic: clusters modified paths by [`layer_key`], orders clusters foundations-first by a Kahn toposort over the module-level projection of `file_deps` (a layer that is imported reads before its importer), breaking ties — and cycles — lexicographically, then attaches the tasks, criteria, and changes that land in each l
(
change_paths: &[(String, Vec<String>)],
file_module: &BTreeMap<String, String>,
file_deps: &BTreeSet<(String, String)>,
task_facts: &[TaskFact],
)
| 868 | /// prose rationale. No repo access; every input is already pinned by the |
| 869 | /// report. |
| 870 | pub(crate) fn build_walkthrough( |
| 871 | change_paths: &[(String, Vec<String>)], |
| 872 | file_module: &BTreeMap<String, String>, |
| 873 | file_deps: &BTreeSet<(String, String)>, |
| 874 | task_facts: &[TaskFact], |
| 875 | ) -> Vec<WalkthroughLayer> { |
| 876 | // 1. Cluster: layer key → sorted modified paths; path → layer key. |
| 877 | let mut layer_files: BTreeMap<String, BTreeSet<String>> = BTreeMap::new(); |
| 878 | let mut path_layer: BTreeMap<String, String> = BTreeMap::new(); |
| 879 | for (_, paths) in change_paths { |
| 880 | for p in paths { |
| 881 | let key = layer_key(p, file_module); |
| 882 | layer_files |
| 883 | .entry(key.clone()) |
| 884 | .or_default() |
| 885 | .insert(p.clone()); |
| 886 | path_layer.insert(p.clone(), key); |
| 887 | } |
| 888 | } |
| 889 | if layer_files.is_empty() { |
| 890 | return Vec::new(); |
| 891 | } |
| 892 | |
| 893 | // 2. Order: project file → file dependencies onto layers. `a imports b` |
| 894 | // means b is more foundational, so the reading-order edge is b → a. |
| 895 | // Only edges where BOTH ends are modified files count — the walkthrough |
| 896 | // orders the change-set, not the whole codebase. |
| 897 | let mut reads_after: BTreeMap<String, BTreeSet<String>> = BTreeMap::new(); // layer → layers it builds on |
| 898 | for (from, to) in file_deps { |
| 899 | let (Some(from_layer), Some(to_layer)) = (path_layer.get(from), path_layer.get(to)) else { |
| 900 | continue; |
| 901 | }; |
| 902 | if from_layer != to_layer { |
| 903 | reads_after |
| 904 | .entry(from_layer.clone()) |
| 905 | .or_default() |
| 906 | .insert(to_layer.clone()); |
| 907 | } |
| 908 | } |
| 909 | |
| 910 | // Kahn's algorithm over the layer set, always taking the lexicographically |
| 911 | // smallest ready layer. A dependency cycle leaves layers with unresolved |
| 912 | // in-edges; they are appended in lexicographic order (deterministic, and |
| 913 | // their `depends_on` still names the relationship for the reader). |
| 914 | let mut remaining: BTreeSet<String> = layer_files.keys().cloned().collect(); |
| 915 | let mut ordered: Vec<String> = Vec::new(); |
| 916 | while !remaining.is_empty() { |
| 917 | let next = remaining |
| 918 | .iter() |
| 919 | .find(|l| { |
| 920 | reads_after |
| 921 | .get(*l) |
| 922 | .map(|deps| deps.iter().all(|d| !remaining.contains(d))) |
| 923 | .unwrap_or(true) |
| 924 | }) |
| 925 | .or_else(|| remaining.iter().next()) // cycle: break it lexicographically |
| 926 | .cloned() |
| 927 | .expect("remaining is non-empty"); |