Build the canonical triage report for `feature` relative to `target`.
(
repo: &Repository,
feature: &str,
target: &str,
)
| 78 | |
| 79 | /// Build the canonical triage report for `feature` relative to `target`. |
| 80 | pub fn build_report( |
| 81 | repo: &Repository, |
| 82 | feature: &str, |
| 83 | target: &str, |
| 84 | ) -> Result<TriageReport, CliError> { |
| 85 | // 1. The candidate set (T0): only-in-feature, closure additions, baggage. |
| 86 | let set = repo |
| 87 | .triage_candidate_set(feature, target) |
| 88 | .map_err(CliError::Repository)?; |
| 89 | |
| 90 | // 2. The pinned view Merkle (the materialized state the report is about). |
| 91 | let view_merkle = repo |
| 92 | .get_view_info(feature) |
| 93 | .map_err(CliError::Repository)? |
| 94 | .state_base32(); |
| 95 | |
| 96 | // Promotion scope: an unreviewed change BLOCKS promotion into a shared |
| 97 | // view, but is only a warning into a draft. Resolved once here. |
| 98 | let target_is_shared = repo |
| 99 | .get_view_info(target) |
| 100 | .map_err(CliError::Repository)? |
| 101 | .scope |
| 102 | .is_shared(); |
| 103 | |
| 104 | // 3. The change → intent join. |
| 105 | // |
| 106 | // The change's modified files come from the change itself |
| 107 | // (`change_modified_paths`) — authoritative and present WITHOUT any KG |
| 108 | // enrichment. Only the intent side of the join (TOUCHES/HAS_TASK/SATISFIES, |
| 109 | // projected on `vault sync`) is read from the KG, so a change is orphaned |
| 110 | // iff no intent's task touches one of its files — never merely because |
| 111 | // `atomic vault query enrich` has not run. |
| 112 | // |
| 113 | // intent_to_changes: KG intent id → candidate hashes that reach it. |
| 114 | // ac_to_changes: KG ac id → candidate hashes that satisfy it. |
| 115 | let mut change_reports: Vec<ChangeReport> = Vec::new(); |
| 116 | let mut intent_to_changes: BTreeMap<String, BTreeSet<String>> = BTreeMap::new(); |
| 117 | let mut ac_to_changes: BTreeMap<String, BTreeSet<String>> = BTreeMap::new(); |
| 118 | let mut reached_any_intent: HashMap<String, bool> = HashMap::new(); |
| 119 | let mut all_modified_files: HashSet<String> = HashSet::new(); |
| 120 | // Raw modified paths per candidate hash (for the actionable orphan message). |
| 121 | let mut change_raw_paths: HashMap<String, Vec<String>> = HashMap::new(); |
| 122 | // Entities defined in each candidate's modified files (parallel to |
| 123 | // `change_reports`) — the seeds for the blast-radius walk below. |
| 124 | let mut change_entities: Vec<Vec<String>> = Vec::new(); |
| 125 | // Walkthrough facts, collected in the same KG passes (no extra queries): |
| 126 | // path → module (PART_OF) and file → file dependency edges (IMPORTS/ |
| 127 | // INCLUDES). Best-effort — empty on an unenriched KG. |
| 128 | let mut file_module: BTreeMap<String, String> = BTreeMap::new(); |
| 129 | let mut file_deps: BTreeSet<(String, String)> = BTreeSet::new(); |
| 130 | // Task facts from reached intents (id, text, touches, satisfies), in |
| 131 | // intent-id order — the walkthrough's narrative inputs. |
| 132 | let mut task_facts: Vec<TaskFact> = Vec::new(); |
| 133 | |
| 134 | for full in &set.only_in_feature { |
| 135 | let hash = Hash::from_base32(full.as_bytes()); |
| 136 | |
| 137 | // Authoritative modified paths straight from the change's file_ops — |