Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge main into stream A: coordination meets the merge queue's decisions and the CLI. get_work gives the issue's decision records newest first and its latest integration_failure decision as last_integration_failure, a merge queue failure noted in a pull request's session is noted in the live harness sessions on it or its issue too, the CLI's coordination commands use the generated operations now that the API describes them (its pending list is empty) and show get_work's definition of done, and the session, decision and coordination events sit together in the catalog, the webhook form and the docs. | 1 | //! `why`: why a line of a file is the way it is. |
| 2 | //! | |
| 3 | //! The line's commit comes from the repos service's blame; the merged pull | |
| 4 | //! request it arrived in is the one whose head it is, or else the one whose | |
| 5 | //! stretch of the default branch's history holds it (as the site's blame | |
| 6 | //! view finds it, `apps/web/app/lib/provenance.server.ts`); then that pull | |
| 7 | //! request's issue, its decision records, and the session entries recorded | |
| 8 | //! around the commit. | |
| 9 | ||
| 10 | use g1t_contracts::decisions::{Decision, ListDecisionsArgs, Why, WhyIssue, WhyPull}; | |
| 11 | use g1t_contracts::repos::{Blame, BlameArgs, Commit, LogArgs, RepoPath}; | |
| 12 | use g1t_contracts::work::{IssueDetail, ListPullsArgs, Pull, PullStatus, SessionEntry, SessionEntryKind, State, ViewArgs}; | |
| 13 | use g1t_contracts::{FailureCode, Outcome, Viewer}; | |
| 14 | use serde_json::Value; | |
| 15 | use worker::Result; | |
| 16 | ||
| 17 | use crate::operations::Services; | |
| 18 | ||
| 19 | /// How far back the default branch is read to place a commit. | |
| 20 | const HISTORY_READ: u32 = 300; | |
| 21 | /// How many steps of a session are shown for one commit. | |
| 22 | const MAX_STEPS: usize = 8; | |
| 23 | /// Longest a step's text is shown. | |
| 24 | const MAX_STEP_CHARS: usize = 600; | |
| 25 | /// Longest an issue's description is shown. | |
| 26 | const MAX_BODY_CHARS: usize = 900; | |
| 27 | ||
| 28 | /// The commit that last changed `line`, from a blame of the file. | |
| 29 | pub(crate) fn commit_of_line(blame: &Blame, line: u32) -> Option<&Commit> { | |
| 30 | let range = blame.ranges.iter().find(|range| range.start <= line && line <= range.end)?; | |
| 31 | blame.commits.iter().find(|commit| commit.hash == range.commit) | |
| 32 | } | |
| 33 | ||
| 34 | /// The merged pull request `hash` arrived in: the one whose head it is, | |
| 35 | /// or else the one whose stretch of `history` (the default branch, newest | |
| 36 | /// first) holds it, from its head back to the commit it started from. | |
| 37 | pub(crate) fn pull_for_commit<'a>(merged: &'a [Pull], history: &[String], hash: &str) -> Option<&'a Pull> { | |
| 38 | let merged: Vec<&Pull> = merged | |
| 39 | .iter() | |
| 40 | .filter(|pull| pull.status == PullStatus::Merged && pull.head_commit.is_some()) | |
| 41 | .collect(); | |
| 42 | if let Some(direct) = merged.iter().find(|pull| pull.head_commit.as_deref() == Some(hash)) { | |
| 43 | return Some(direct); | |
| 44 | } | |
| 45 | let position = |commit: &str| history.iter().position(|known| known == commit); | |
| 46 | let at = position(hash)?; | |
| 47 | let mut best: Option<(&Pull, usize)> = None; | |
| 48 | for pull in merged { | |
| 49 | let Some(head) = pull.head_commit.as_deref().and_then(position) else { continue }; | |
| 50 | if head > at { | |
| 51 | continue; | |
| 52 | } | |
| 53 | if let Some(base) = pull.merge_base.as_deref().and_then(position) | |
| 54 | && base <= at | |
| 55 | { | |
| 56 | continue; | |
| 57 | } | |
| 58 | if best.is_none_or(|(_, best_head)| head > best_head) { | |
| 59 | best = Some((pull, head)); | |
| 60 | } | |
| 61 | } | |
| 62 | best.map(|(pull, _)| pull) | |
| 63 | } | |
| 64 | ||
| 65 | /// The steps of a session behind `hash`: what was said and done between | |
| 66 | /// the push before it and the push that carried it. Steps that touch | |
| 67 | /// `file` come first; at most [`MAX_STEPS`], each cut to | |
| 68 | /// [`MAX_STEP_CHARS`]. | |
| 69 | pub(crate) fn steps_for(session: &[SessionEntry], hash: &str, file: &str) -> Vec<SessionEntry> { | |
| 70 | // A push is noted "Pushed b1c42490059b." | |
| 71 | let pushed = |entry: &SessionEntry| -> Option<String> { | |
| 72 | if entry.kind != SessionEntryKind::Note { | |
| 73 | return None; | |
| 74 | } | |
| 75 | let rest = entry.text.strip_prefix("Pushed ")?; | |
| 76 | let short: String = rest.chars().take_while(char::is_ascii_hexdigit).collect(); | |
| 77 | (short.len() >= 7).then_some(short) | |
| 78 | }; | |
| 79 | let end = session.iter().position(|entry| { | |
| 80 | entry.commit.as_deref() == Some(hash) || pushed(entry).is_some_and(|short| hash.starts_with(&short)) | |
| 81 | }); | |
| 82 | let window: &[SessionEntry] = match end { | |
| 83 | Some(end) => { | |
| 84 | let mut start = end; | |
| 85 | while start > 0 && pushed(&session[start - 1]).is_none() { | |
| 86 | start -= 1; | |
| 87 | } | |
| 88 | &session[start..=end] | |
| 89 | } | |
| 90 | None => session, | |
| 91 | }; | |
| 92 | let name = file.rsplit('/').next().unwrap_or(file); | |
| 93 | let useful: Vec<&SessionEntry> = window | |
| 94 | .iter() | |
| 95 | .filter(|entry| { | |
| 96 | entry.kind == SessionEntryKind::Message | |
| 97 | || (entry.kind == SessionEntryKind::ToolCall && (entry.text.contains(file) || entry.text.contains(name))) | |
| 98 | }) | |
| 99 | .collect(); | |
| 100 | let touching: Vec<&SessionEntry> = useful.iter().copied().filter(|entry| entry.text.contains(name)).collect(); | |
| 101 | let mut chosen: Vec<&SessionEntry> = if touching.is_empty() { | |
| 102 | useful.clone() | |
| 103 | } else { | |
| 104 | touching.into_iter().chain(useful.iter().copied().filter(|entry| entry.kind == SessionEntryKind::Message)).collect() | |
| 105 | }; | |
| 106 | chosen.sort_by_key(|entry| entry.seq); | |
| 107 | chosen.dedup_by_key(|entry| entry.seq); | |
| 108 | let skip = chosen.len().saturating_sub(MAX_STEPS); | |
| 109 | chosen | |
| 110 | .into_iter() | |
| 111 | .skip(skip) | |
| 112 | .map(|entry| { | |
| 113 | let mut entry = entry.clone(); | |
| 114 | if entry.text.chars().count() > MAX_STEP_CHARS { | |
| 115 | entry.text = format!("{}…", entry.text.chars().take(MAX_STEP_CHARS).collect::<String>()); | |
| 116 | } | |
| 117 | entry | |
| 118 | }) | |
| 119 | .collect() | |
| 120 | } | |
| 121 | ||
| 122 | /// Answers `why`. | |
| 123 | pub(crate) async fn why( | |
| 124 | services: &Services, | |
| 125 | viewer: &Viewer, | |
| 126 | repo: RepoPath, | |
| 127 | path: String, | |
| 128 | line: u32, | |
| 129 | git_ref: Option<String>, | |
| 130 | ) -> Result<Outcome<Value>> { | |
| 131 | let blame: Outcome<Blame> = g1t_kit::call( | |
| 132 | &services.repos, | |
| 133 | "blame", | |
| 134 | &BlameArgs { | |
| 135 | path: repo.clone(), | |
| 136 | viewer: viewer.clone(), | |
| 137 | git_ref, | |
| 138 | file_path: path.clone(), | |
| 139 | }, | |
| 140 | ) | |
| 141 | .await?; | |
| 142 | let blame = match blame { | |
| 143 | Outcome::Ok(blame) => blame, | |
| 144 | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), | |
| 145 | }; | |
| 146 | let Some(commit) = commit_of_line(&blame, line).cloned() else { | |
| 147 | let lines = blame.ranges.last().map_or(0, |range| range.end); | |
| 148 | return Ok(Outcome::fail( | |
| 149 | FailureCode::Invalid, | |
| 150 | format!("{path} has {lines} lines at that ref: give a line from 1 to {lines}."), | |
| 151 | )); | |
| 152 | }; | |
| 153 | ||
| 154 | let closed: Outcome<Vec<Pull>> = g1t_kit::call( | |
| 155 | &services.work, | |
| 156 | "list_pulls", | |
| 157 | &ListPullsArgs { | |
| 158 | repo: repo.clone(), | |
| 159 | viewer: viewer.clone(), | |
| 160 | state: Some(State::Closed), | |
| 161 | label: None, | |
| 162 | milestone: None, | |
| 163 | base: None, | |
| 164 | }, | |
| 165 | ) | |
| 166 | .await?; | |
| 167 | let merged = match closed { | |
| 168 | Outcome::Ok(pulls) => pulls, | |
| 169 | Outcome::Fail(_) => Vec::new(), | |
| 170 | }; | |
| 171 | let mut pull = pull_for_commit(&merged, &[], &commit.hash).cloned(); | |
| 172 | if pull.is_none() && !merged.is_empty() { | |
| 173 | let log: Outcome<Vec<Commit>> = g1t_kit::call( | |
| 174 | &services.repos, | |
| 175 | "log", | |
| 176 | &LogArgs { | |
| 177 | path: repo.clone(), | |
| 178 | viewer: viewer.clone(), | |
| 179 | git_ref: None, | |
| 180 | limit: HISTORY_READ, | |
| 181 | }, | |
| 182 | ) | |
| 183 | .await?; | |
| 184 | if let Outcome::Ok(log) = log { | |
| 185 | let history: Vec<String> = log.into_iter().map(|commit| commit.hash).collect(); | |
| 186 | pull = pull_for_commit(&merged, &history, &commit.hash).cloned(); | |
| 187 | } | |
| 188 | } | |
| 189 | ||
| 190 | let Some(pull) = pull else { | |
| 191 | return ok(Why { path, line, commit, pull: None, issue: None, decisions: Vec::new(), sessions: Vec::new() }); | |
| 192 | }; | |
| 193 | let view = |number: u32| ViewArgs { repo: repo.clone(), number, viewer: viewer.clone(), after_seq: 0 }; | |
| 194 | // A few small reads, one after another: the API has no join helper. | |
| 195 | let issue = match pull.issue { | |
| 196 | Some(number) => match g1t_kit::call::<_, Outcome<IssueDetail>>(&services.work, "get_issue", &view(number)).await? { | |
| 197 | Outcome::Ok(detail) => Some(detail.issue), | |
| 198 | Outcome::Fail(_) => None, | |
| 199 | }, | |
| 200 | None => None, | |
| 201 | }; | |
| 202 | let decisions: Outcome<Vec<Decision>> = g1t_kit::call( | |
| 203 | &services.work, | |
| 204 | "list_decisions", | |
| 205 | &ListDecisionsArgs { repo: repo.clone(), viewer: viewer.clone(), number: pull.number }, | |
| 206 | ) | |
| 207 | .await?; | |
| 208 | let session: Outcome<Vec<SessionEntry>> = g1t_kit::call(&services.work, "read_session", &view(pull.number)).await?; | |
| 209 | let decisions = match decisions { | |
| 210 | Outcome::Ok(decisions) => decisions, | |
| 211 | Outcome::Fail(_) => Vec::new(), | |
| 212 | }; | |
| 213 | let sessions = match session { | |
| 214 | Outcome::Ok(entries) => steps_for(&entries, &commit.hash, &path), | |
| 215 | Outcome::Fail(_) => Vec::new(), | |
| 216 | }; | |
| 217 | let issue = issue.map(|issue| WhyIssue { | |
| 218 | number: issue.number, | |
| 219 | title: issue.title, | |
| 220 | state: if issue.state == State::Open { "open" } else { "closed" }.to_owned(), | |
| 221 | body: if issue.body.chars().count() > MAX_BODY_CHARS { | |
| 222 | format!("{}…", issue.body.chars().take(MAX_BODY_CHARS).collect::<String>()) | |
| 223 | } else { | |
| 224 | issue.body | |
| 225 | }, | |
| 226 | }); | |
| 227 | let pull = WhyPull { | |
| 228 | number: pull.number, | |
| 229 | title: pull.title.clone(), | |
| 230 | author: pull.owner().username.clone(), | |
| 231 | agent: pull.agent.clone(), | |
| 232 | merged_by: pull.merged_by.clone(), | |
| 233 | merged_at: pull.merged_at.clone(), | |
| 234 | }; | |
| 235 | ok(Why { path, line, commit, pull: Some(pull), issue, decisions, sessions }) | |
| 236 | } | |
| 237 | ||
| 238 | fn ok(why: Why) -> Result<Outcome<Value>> { | |
| 239 | Ok(Outcome::Ok(serde_json::to_value(why)?)) | |
| 240 | } | |
| 241 | ||
| 242 | #[cfg(test)] | |
| 243 | mod tests { | |
| 244 | use super::*; | |
| 245 | use g1t_contracts::repos::{BlameRange, Signature}; | |
| 246 | ||
| 247 | fn commit(hash: &str) -> Commit { | |
| 248 | Commit { | |
| 249 | hash: hash.into(), | |
| 250 | tree_hash: "t".into(), | |
| 251 | message: "m".into(), | |
| 252 | author: Signature { name: "Ana".into(), email: "ana@example.com".into() }, | |
| 253 | parents: Vec::new(), | |
| 254 | authored_at: "2026-10-12T00:00:00Z".into(), | |
| 255 | } | |
| 256 | } | |
| 257 | ||
| 258 | fn merged(number: u32, head: &str, base: Option<&str>) -> Pull { | |
| 259 | serde_json::from_value(serde_json::json!({ | |
| 260 | "id": format!("pr_{number}"), "repoId": "rep_1", "number": number, "issue": 9, "title": "t", "body": null, | |
| 261 | "agent": "claude-code", "runtime": "external", "status": "merged", "fork": null, "forkRepoId": null, | |
| 262 | "branch": "b", "headCommit": head, "mergeBase": base, "mergedBy": "ana", "mergedAt": "2026-10-12T00:00:00Z", | |
| 263 | "supersededBy": null, "checkStatus": null, | |
| 264 | "author": { "id": "usr_1", "username": "ana" }, | |
| 265 | "createdAt": "", "updatedAt": "" | |
| 266 | })) | |
| 267 | .unwrap() | |
| 268 | } | |
| 269 | ||
| 270 | #[test] | |
| 271 | fn a_line_is_found_in_its_range() { | |
| 272 | let blame = Blame { | |
| 273 | head: "h".into(), | |
| 274 | ranges: vec![ | |
| 275 | BlameRange { start: 1, end: 3, commit: "aaa".into() }, | |
| 276 | BlameRange { start: 4, end: 4, commit: "bbb".into() }, | |
| 277 | ], | |
| 278 | commits: vec![commit("aaa"), commit("bbb")], | |
| 279 | partial: false, | |
| 280 | }; | |
| 281 | assert_eq!(commit_of_line(&blame, 3).map(|c| c.hash.as_str()), Some("aaa")); | |
| 282 | assert_eq!(commit_of_line(&blame, 4).map(|c| c.hash.as_str()), Some("bbb")); | |
| 283 | assert!(commit_of_line(&blame, 5).is_none()); | |
| 284 | } | |
| 285 | ||
| 286 | #[test] | |
| 287 | fn a_commit_is_placed_in_the_pull_request_it_arrived_in() { | |
| 288 | // Newest first: m2 landed #12 on top of b1; m1 landed #11 on top of b0. | |
| 289 | let history: Vec<String> = ["m2", "c3", "c2", "b1", "m1", "c1", "b0"].map(String::from).to_vec(); | |
| 290 | let pulls = vec![merged(11, "m1", Some("b0")), merged(12, "m2", Some("b1"))]; | |
| 291 | assert_eq!(pull_for_commit(&pulls, &[], "m1").map(|p| p.number), Some(11)); | |
| 292 | assert_eq!(pull_for_commit(&pulls, &history, "c2").map(|p| p.number), Some(12)); | |
| 293 | assert_eq!(pull_for_commit(&pulls, &history, "c1").map(|p| p.number), Some(11)); | |
| 294 | // Before any pull request. | |
| 295 | assert!(pull_for_commit(&pulls, &history, "b0").is_none()); | |
| 296 | assert!(pull_for_commit(&pulls, &history, "zzz").is_none()); | |
| 297 | } | |
| 298 | ||
| 299 | fn entry(seq: u32, kind: SessionEntryKind, text: &str, commit: Option<&str>) -> SessionEntry { | |
| 300 | SessionEntry { seq, kind, text: text.into(), tool: None, commit: commit.map(String::from), at: String::new() } | |
| 301 | } | |
| 302 | ||
| 303 | #[test] | |
| 304 | fn the_session_steps_are_those_behind_the_commit() { | |
| 305 | use SessionEntryKind::*; | |
| 306 | let session = vec![ | |
| 307 | entry(1, Message, "Starting on the parser.", None), | |
| 308 | entry(2, Note, "Pushed 1111111aaaa.", None), | |
| 309 | entry(3, Message, "The lexer drops the last token.", None), | |
| 310 | entry(4, ToolCall, "Edit src/lexer.rs", None), | |
| 311 | entry(5, ToolCall, "Run cargo test", None), | |
| 312 | entry(6, Note, "Pushed 2222222bbbb.", None), | |
| 313 | entry(7, Message, "Now the docs.", None), | |
| 314 | ]; | |
| 315 | let steps = steps_for(&session, "2222222bbbbcccc", "src/lexer.rs"); | |
| 316 | let seqs: Vec<u32> = steps.iter().map(|step| step.seq).collect(); | |
| 317 | assert_eq!(seqs, [3, 4]); | |
| 318 | // A commit no push names: the whole session is looked through. | |
| 319 | let all = steps_for(&session, "9999999", "src/lexer.rs"); | |
| 320 | assert_eq!(all.iter().map(|s| s.seq).collect::<Vec<_>>(), [1, 3, 4, 7]); | |
| 321 | let long = vec![entry(1, Message, &"x".repeat(MAX_STEP_CHARS + 10), None)]; | |
| 322 | assert!(steps_for(&long, "h", "a.rs")[0].text.ends_with('…')); | |
| 323 | } | |
| 324 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.