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],
)
| 845 | /// prose rationale. No repo access; every input is already pinned by the |
| 846 | /// report. |
| 847 | pub(crate) fn build_walkthrough( |
| 848 | change_paths: &[(String, Vec<String>)], |
| 849 | file_module: &BTreeMap<String, String>, |
| 850 | file_deps: &BTreeSet<(String, String)>, |
| 851 | task_facts: &[TaskFact], |
| 852 | ) -> Vec<WalkthroughLayer> { |
| 853 | // 1. Cluster: layer key → sorted modified paths; path → layer key. |
| 854 | let mut layer_files: BTreeMap<String, BTreeSet<String>> = BTreeMap::new(); |
| 855 | let mut path_layer: BTreeMap<String, String> = BTreeMap::new(); |
| 856 | for (_, paths) in change_paths { |
| 857 | for p in paths { |
| 858 | let key = layer_key(p, file_module); |
| 859 | layer_files |
| 860 | .entry(key.clone()) |
| 861 | .or_default() |
| 862 | .insert(p.clone()); |
| 863 | path_layer.insert(p.clone(), key); |
| 864 | } |
| 865 | } |
| 866 | if layer_files.is_empty() { |
| 867 | return Vec::new(); |
| 868 | } |
| 869 | |
| 870 | // 2. Order: project file → file dependencies onto layers. `a imports b` |
| 871 | // means b is more foundational, so the reading-order edge is b → a. |
| 872 | // Only edges where BOTH ends are modified files count — the walkthrough |
| 873 | // orders the change-set, not the whole codebase. |
| 874 | let mut reads_after: BTreeMap<String, BTreeSet<String>> = BTreeMap::new(); // layer → layers it builds on |
| 875 | for (from, to) in file_deps { |
| 876 | let (Some(from_layer), Some(to_layer)) = (path_layer.get(from), path_layer.get(to)) else { |
| 877 | continue; |
| 878 | }; |
| 879 | if from_layer != to_layer { |
| 880 | reads_after |
| 881 | .entry(from_layer.clone()) |
| 882 | .or_default() |
| 883 | .insert(to_layer.clone()); |
| 884 | } |
| 885 | } |
| 886 | |
| 887 | // Kahn's algorithm over the layer set, always taking the lexicographically |
| 888 | // smallest ready layer. A dependency cycle leaves layers with unresolved |
| 889 | // in-edges; they are appended in lexicographic order (deterministic, and |
| 890 | // their `depends_on` still names the relationship for the reader). |
| 891 | let mut remaining: BTreeSet<String> = layer_files.keys().cloned().collect(); |
| 892 | let mut ordered: Vec<String> = Vec::new(); |
| 893 | while !remaining.is_empty() { |
| 894 | let next = remaining |
| 895 | .iter() |
| 896 | .find(|l| { |
| 897 | reads_after |
| 898 | .get(*l) |
| 899 | .map(|deps| deps.iter().all(|d| !remaining.contains(d))) |
| 900 | .unwrap_or(true) |
| 901 | }) |
| 902 | .or_else(|| remaining.iter().next()) // cycle: break it lexicographically |
| 903 | .cloned() |
| 904 | .expect("remaining is non-empty"); |