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.
| A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait | 1 | //! Rows as they come out of D1, and their conversion to contract types. |
| 2 | ||
| 3 | use g1t_contracts::User; | |
| 4 | use g1t_contracts::repos::RepoPath; | |
| 5 | use g1t_contracts::work::{ | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 6 | AgentRef, AgentVerdict, CheckStatus, Comment, CommentKind, Issue, IssueReason, MilestoneRef, Pull, PullStatus, Runtime, SessionEntry, |
| A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait | 7 | SessionEntryKind, State, Verdict, |
| 8 | }; | |
| 9 | use serde::Deserialize; | |
| 10 | ||
| 11 | /// An author or actor as stored: g1t itself when the id is its own (a | |
| 12 | /// security update it opened, a merge its settings made), g1t's agent by | |
| 13 | /// its id (what it made or filed), a person otherwise. | |
| 14 | pub(crate) fn user(id: String, username: String) -> User { | |
| 15 | let kind = if g1t_contracts::system::is_system_id(&id) { | |
| 16 | g1t_contracts::PrincipalKind::System | |
| 17 | } else if id == g1t_contracts::identity::AGENT_ID { | |
| 18 | g1t_contracts::PrincipalKind::Agent | |
| 19 | } else { | |
| 20 | g1t_contracts::PrincipalKind::User | |
| 21 | }; | |
| 22 | User { | |
| 23 | id, | |
| 24 | username, | |
| 25 | kind, | |
| 26 | ..User::default() | |
| 27 | } | |
| 28 | } | |
| 29 | ||
| 30 | /// Who asked g1t for the work, as stored beside its author: both columns, | |
| 31 | /// or nobody. | |
| 32 | pub(crate) fn requester(id: Option<String>, username: Option<String>) -> Option<User> { | |
| 33 | Some(user(id?, username?)) | |
| 34 | } | |
| 35 | ||
| 36 | #[derive(Deserialize)] | |
| 37 | pub struct IssueRow { | |
| 38 | pub id: String, | |
| 39 | pub repo_id: String, | |
| 40 | pub number: u32, | |
| 41 | pub title: String, | |
| 42 | pub body: String, | |
| 43 | /// JSON array of label names. | |
| 44 | pub labels: String, | |
| 45 | pub state: State, | |
| 46 | pub reason: Option<IssueReason>, | |
| 47 | pub resolved_by: Option<u32>, | |
| 48 | pub author_id: String, | |
| 49 | pub author_name: String, | |
| 50 | #[serde(default)] | |
| 51 | pub requested_by_id: Option<String>, | |
| 52 | #[serde(default)] | |
| 53 | pub requested_by_name: Option<String>, | |
| 54 | pub created_at: String, | |
| 55 | pub updated_at: String, | |
| 56 | pub closed_at: Option<String>, | |
| 57 | pub pull_count: u32, | |
| 58 | pub comment_count: u32, | |
| 59 | /// JSON array of usernames. | |
| 60 | pub assignees: String, | |
| 61 | /// JSON array of issue numbers. | |
| 62 | pub blocked_by: String, | |
| 63 | /// JSON of who queued it for a g1t agent, if anyone has. | |
| 64 | pub queued_by: Option<String>, | |
| 65 | pub agent: Option<String>, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 66 | /// The number of its milestone, and that milestone's title. |
| 67 | #[serde(default)] | |
| 68 | pub milestone: Option<u32>, | |
| 69 | #[serde(default)] | |
| 70 | pub milestone_title: Option<String>, | |
| A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait | 71 | } |
| 72 | ||
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 73 | /// A milestone as an issue or pull request names it, from its number and |
| 74 | /// title as read beside it. | |
| 75 | pub(crate) fn milestone_ref(number: Option<u32>, title: Option<String>) -> Option<MilestoneRef> { | |
| 76 | Some(MilestoneRef { number: number?, title: title? }) | |
| 77 | } | |
| 78 | ||
| A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait | 79 | impl From<IssueRow> for Issue { |
| 80 | fn from(row: IssueRow) -> Self { | |
| 81 | Issue { | |
| 82 | id: row.id, | |
| 83 | repo_id: row.repo_id, | |
| 84 | number: row.number, | |
| 85 | title: row.title, | |
| 86 | body: row.body, | |
| 87 | labels: serde_json::from_str(&row.labels).unwrap_or_default(), | |
| 88 | state: row.state, | |
| 89 | reason: row.reason, | |
| 90 | resolved_by: row.resolved_by, | |
| 91 | author: user(row.author_id, row.author_name), | |
| 92 | requested_by: requester(row.requested_by_id, row.requested_by_name), | |
| 93 | created_at: row.created_at, | |
| 94 | updated_at: row.updated_at, | |
| 95 | closed_at: row.closed_at, | |
| 96 | pull_count: row.pull_count, | |
| 97 | comment_count: row.comment_count, | |
| 98 | assignees: serde_json::from_str(&row.assignees).unwrap_or_default(), | |
| 99 | blocked_by: serde_json::from_str(&row.blocked_by).unwrap_or_default(), | |
| 100 | queued: row.queued_by.is_some(), | |
| 101 | agent: row.agent, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 102 | milestone: milestone_ref(row.milestone, row.milestone_title), |
| A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait | 103 | } |
| 104 | } | |
| 105 | } | |
| 106 | ||
| 107 | /// The remembered assessment of a pull request, read beside its row. | |
| 108 | #[derive(Deserialize)] | |
| 109 | pub struct Snapshot { | |
| 110 | pub stage: Option<g1t_contracts::work::Stage>, | |
| 111 | pub stage_detail: Option<String>, | |
| 112 | #[serde(default)] | |
| 113 | pub revisions: u32, | |
| 114 | /// Whether g1t is seeing it through at all. | |
| 115 | #[serde(default)] | |
| 116 | pub managed: u32, | |
| 117 | } | |
| 118 | ||
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 119 | /// A pull request's columns, with its milestone's title beside them: what |
| 120 | /// every read of a [`PullRow`] selects. | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 121 | pub const PULL_COLUMNS: &str = "pulls.*, |
| 122 | (SELECT title FROM milestones | |
| 123 | WHERE milestones.repo_id = pulls.repo_id AND milestones.number = pulls.milestone) AS milestone_title"; | |
| A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait | 124 | |
| 125 | #[derive(Deserialize)] | |
| 126 | pub struct PullRow { | |
| 127 | pub id: String, | |
| 128 | pub repo_id: String, | |
| 129 | pub number: u32, | |
| 130 | pub issue_number: Option<u32>, | |
| 131 | pub title: String, | |
| 132 | pub body: Option<String>, | |
| 133 | pub agent: String, | |
| 134 | pub runtime: Runtime, | |
| 135 | pub status: PullStatus, | |
| 136 | pub fork_repo_id: Option<String>, | |
| 137 | pub fork_namespace: Option<String>, | |
| 138 | pub fork_name: Option<String>, | |
| 139 | pub source_branch: Option<String>, | |
| 140 | pub head_commit: Option<String>, | |
| 141 | pub merge_base: Option<String>, | |
| 142 | pub merged_by: Option<String>, | |
| 143 | pub merged_at: Option<String>, | |
| 144 | pub superseded_by: Option<u32>, | |
| 145 | pub check_status: Option<CheckStatus>, | |
| 146 | /// JSON array of changed files; `None` until first worked out. | |
| 147 | pub files: Option<String>, | |
| 148 | /// JSON arrays of usernames. | |
| 149 | pub assignees: String, | |
| 150 | pub reviewers: String, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 151 | /// JSON array of `workspace/team`. |
| 152 | #[serde(default)] | |
| 153 | pub team_reviewers: Option<String>, | |
| A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait | 154 | pub author_id: String, |
| 155 | pub author_name: String, | |
| 156 | #[serde(default)] | |
| 157 | pub requested_by_id: Option<String>, | |
| 158 | #[serde(default)] | |
| 159 | pub requested_by_name: Option<String>, | |
| 160 | pub created_at: String, | |
| 161 | pub updated_at: String, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 162 | /// JSON array of label names. |
| 163 | #[serde(default)] | |
| 164 | pub labels: Option<String>, | |
| 165 | #[serde(default)] | |
| 166 | pub milestone: Option<u32>, | |
| 167 | #[serde(default)] | |
| 168 | pub milestone_title: Option<String>, | |
| 169 | /// The branch it merges into; NULL for the default branch. | |
| 170 | #[serde(default)] | |
| 171 | pub base_branch: Option<String>, | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 172 | #[serde(default)] |
| 173 | pub head_pushed_by: Option<String>, | |
| 174 | #[serde(default)] | |
| 175 | pub head_pushed_at: Option<String>, | |
| A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait | 176 | } |
| 177 | ||
| 178 | impl From<PullRow> for Pull { | |
| 179 | fn from(row: PullRow) -> Self { | |
| 180 | Pull { | |
| 181 | id: row.id, | |
| 182 | repo_id: row.repo_id, | |
| 183 | number: row.number, | |
| 184 | issue: row.issue_number, | |
| 185 | title: row.title, | |
| 186 | body: row.body, | |
| 187 | agent: row.agent, | |
| 188 | runtime: row.runtime, | |
| 189 | status: row.status, | |
| 190 | fork: row | |
| 191 | .fork_namespace | |
| 192 | .zip(row.fork_name) | |
| 193 | .map(|(namespace, name)| RepoPath { namespace, name }), | |
| 194 | fork_repo_id: row.fork_repo_id, | |
| 195 | branch: row.source_branch, | |
| 196 | head_commit: row.head_commit, | |
| 197 | merge_base: row.merge_base, | |
| 198 | merged_by: row.merged_by, | |
| 199 | merged_at: row.merged_at, | |
| 200 | superseded_by: row.superseded_by, | |
| 201 | check_status: row.check_status, | |
| 202 | files: row | |
| 203 | .files | |
| 204 | .as_deref() | |
| 205 | .and_then(|files| serde_json::from_str(files).ok()) | |
| 206 | .unwrap_or_default(), | |
| 207 | assignees: serde_json::from_str(&row.assignees).unwrap_or_default(), | |
| 208 | reviewers: serde_json::from_str(&row.reviewers).unwrap_or_default(), | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 209 | team_reviewers: row |
| 210 | .team_reviewers | |
| 211 | .as_deref() | |
| 212 | .and_then(|teams| serde_json::from_str(teams).ok()) | |
| 213 | .unwrap_or_default(), | |
| A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait | 214 | author: user(row.author_id, row.author_name), |
| 215 | requested_by: requester(row.requested_by_id, row.requested_by_name), | |
| 216 | created_at: row.created_at, | |
| 217 | updated_at: row.updated_at, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 218 | labels: row |
| 219 | .labels | |
| 220 | .as_deref() | |
| 221 | .and_then(|labels| serde_json::from_str(labels).ok()) | |
| 222 | .unwrap_or_default(), | |
| 223 | milestone: milestone_ref(row.milestone, row.milestone_title), | |
| 224 | base: row.base_branch, | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 225 | head_pushed_by: row.head_pushed_by, |
| 226 | head_pushed_at: row.head_pushed_at, | |
| A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait | 227 | } |
| 228 | } | |
| 229 | } | |
| 230 | ||
| 231 | #[derive(Deserialize)] | |
| 232 | pub struct CommentRow { | |
| 233 | pub id: String, | |
| 234 | pub kind: CommentKind, | |
| 235 | pub author_id: String, | |
| 236 | pub author_name: String, | |
| 237 | pub body: String, | |
| 238 | pub path: Option<String>, | |
| 239 | pub line: Option<u32>, | |
| 240 | pub verdict: Option<Verdict>, | |
| 241 | pub created_at: String, | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 242 | #[serde(default)] |
| 243 | pub edited_at: Option<String>, | |
| 244 | /// The issue or pull request it is on, where the query reads it. | |
| 245 | #[serde(default)] | |
| 246 | pub number: u32, | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 247 | /// One of the workspace's agents that wrote it, as itself, and whom it |
| 248 | /// acted for (migrations/0033_agent_comments.sql). | |
| 249 | #[serde(default)] | |
| 250 | pub agent_id: Option<String>, | |
| 251 | #[serde(default)] | |
| 252 | pub agent_handle: Option<String>, | |
| 253 | #[serde(default)] | |
| 254 | pub agent_name: Option<String>, | |
| 255 | #[serde(default)] | |
| 256 | pub agent_avatar_seed: Option<String>, | |
| 257 | #[serde(default)] | |
| 258 | pub acting_for_id: Option<String>, | |
| 259 | #[serde(default)] | |
| 260 | pub acting_for_name: Option<String>, | |
| 261 | /// An agent review's verdict, kept apart from `verdict`, which every | |
| 262 | /// rule about approvals reads: so it is advisory. | |
| 263 | #[serde(default)] | |
| 264 | pub agent_verdict: Option<String>, | |
| 265 | } | |
| 266 | ||
| 267 | impl CommentRow { | |
| 268 | /// Who answers for it: whoever an agent acted for, or its author. | |
| 269 | pub fn answerable_id(&self) -> &str { | |
| 270 | self.acting_for_id.as_deref().unwrap_or(&self.author_id) | |
| 271 | } | |
| 272 | ||
| 273 | /// Whether it carries a verdict, a person's or an agent's. | |
| 274 | pub fn has_verdict(&self) -> bool { | |
| 275 | self.verdict.is_some() || self.agent_verdict.is_some() | |
| 276 | } | |
| A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait | 277 | } |
| 278 | ||
| 279 | impl From<CommentRow> for Comment { | |
| 280 | fn from(row: CommentRow) -> Self { | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 281 | let agent = match (row.agent_id, row.agent_handle) { |
| 282 | (Some(id), Some(handle)) => Some(AgentRef { | |
| 283 | display_name: row.agent_name.unwrap_or_else(|| handle.clone()), | |
| 284 | avatar_seed: row.agent_avatar_seed.unwrap_or_else(|| handle.clone()), | |
| 285 | id, | |
| 286 | handle, | |
| 287 | }), | |
| 288 | _ => None, | |
| 289 | }; | |
| 290 | let advisory_verdict = row.agent_verdict.as_deref().and_then(AgentVerdict::parse); | |
| 291 | let mut author = user(row.author_id, row.author_name); | |
| 292 | if agent.is_some() { | |
| 293 | author.kind = g1t_contracts::PrincipalKind::Agent; | |
| 294 | } | |
| A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait | 295 | Comment { |
| 296 | id: row.id, | |
| 297 | kind: row.kind, | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 298 | author, |
| A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait | 299 | body: row.body, |
| 300 | path: row.path, | |
| 301 | line: row.line, | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 302 | // A person's verdict, or for showing, an agent's advisory one. |
| 303 | verdict: row.verdict.or(advisory_verdict.and_then(AgentVerdict::verdict)), | |
| A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait | 304 | created_at: row.created_at, |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 305 | edited_at: row.edited_at, |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 306 | acting_for: agent.as_ref().and(requester(row.acting_for_id, row.acting_for_name)), |
| 307 | advisory: agent.is_some() && advisory_verdict.is_some(), | |
| 308 | agent, | |
| A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait | 309 | } |
| 310 | } | |
| 311 | } | |
| 312 | ||
| 313 | #[derive(Deserialize)] | |
| 314 | pub struct SessionRow { | |
| 315 | pub seq: u32, | |
| 316 | pub kind: SessionEntryKind, | |
| 317 | pub text: String, | |
| 318 | pub tool: Option<String>, | |
| 319 | pub commit: Option<String>, | |
| 320 | pub at: String, | |
| 321 | } | |
| 322 | ||
| 323 | impl From<SessionRow> for SessionEntry { | |
| 324 | fn from(row: SessionRow) -> Self { | |
| 325 | SessionEntry { | |
| 326 | seq: row.seq, | |
| 327 | kind: row.kind, | |
| 328 | text: row.text, | |
| 329 | tool: row.tool, | |
| 330 | commit: row.commit, | |
| 331 | at: row.at, | |
| 332 | } | |
| 333 | } | |
| 334 | } | |
| 335 | ||
| 336 | /// A pull request whose head a push moved. | |
| 337 | #[derive(Deserialize)] | |
| 338 | pub struct MovedRow { | |
| 339 | pub id: String, | |
| 340 | pub repo_id: String, | |
| 341 | pub number: u32, | |
| 342 | pub issue_number: Option<u32>, | |
| 343 | pub status: PullStatus, | |
| 344 | pub author_id: String, | |
| 345 | pub author_name: String, | |
| 346 | #[serde(default)] | |
| 347 | pub requested_by_id: Option<String>, | |
| 348 | #[serde(default)] | |
| 349 | pub requested_by_name: Option<String>, | |
| 350 | } | |
| 351 | ||
| 352 | /// A single number selected as `n`. | |
| 353 | #[derive(Deserialize)] | |
| 354 | pub struct NumberRow { | |
| 355 | pub n: u32, | |
| 356 | } | |
| 357 | ||
| 358 | /// A single string selected as `value`. | |
| 359 | #[derive(Deserialize)] | |
| 360 | pub struct ValueRow { | |
| 361 | pub value: String, | |
| 362 | } | |
| 363 | ||
| 364 | /// Rows as the database holds them, for tests of the rules that read them. | |
| 365 | #[cfg(test)] | |
| 366 | pub(crate) mod stored { | |
| 367 | use super::*; | |
| 368 | ||
| 369 | /// A pull request g1t made in a fork, read back as stored: g1t as its | |
| 370 | /// author, and `requested_by` the person who asked, if anyone did. | |
| 371 | pub(crate) fn pull(author: (&str, &str), requested_by: Option<(&str, &str)>) -> Pull { | |
| 372 | let row: PullRow = serde_json::from_value(serde_json::json!({ | |
| 373 | "id": "pr_1", "repo_id": "rep_1", "number": 14, "issue_number": 12, "title": "Fix it", | |
| 374 | "body": null, "agent": "g1t", "runtime": "hosted", "status": "open", | |
| 375 | "fork_repo_id": "rep_f", "fork_namespace": "pulls", "fork_name": "pr_1", | |
| 376 | "source_branch": null, "head_commit": null, "merge_base": null, "merged_by": null, | |
| 377 | "merged_at": null, "superseded_by": null, "check_status": null, "files": null, | |
| 378 | "assignees": "[]", "reviewers": "[]", | |
| 379 | "author_id": author.0, "author_name": author.1, | |
| 380 | "requested_by_id": requested_by.map(|(id, _)| id), | |
| 381 | "requested_by_name": requested_by.map(|(_, name)| name), | |
| 382 | "created_at": "", "updated_at": "" | |
| 383 | })) | |
| 384 | .unwrap(); | |
| 385 | row.into() | |
| 386 | } | |
| 387 | ||
| 388 | /// g1t's agent, as an author is stored. | |
| 389 | pub(crate) const G1T: (&str, &str) = (g1t_contracts::identity::AGENT_ID, "g1t"); | |
| 390 | /// The person who asked g1t for the work. | |
| 391 | pub(crate) const ASKER: (&str, &str) = ("usr_1", "syntaqx"); | |
| 392 | } | |
| 393 | ||
| 394 | #[cfg(test)] | |
| 395 | mod tests { | |
| 396 | use super::stored::{ASKER, G1T, pull}; | |
| 397 | use super::*; | |
| 398 | use g1t_contracts::PrincipalKind; | |
| 399 | ||
| 400 | #[test] | |
| 401 | fn g1t_s_pull_request_reads_back_as_g1t_s_requested_by_the_person() { | |
| 402 | let made = pull(G1T, Some(ASKER)); | |
| 403 | assert_eq!((made.author.username.as_str(), made.author.kind), ("g1t", PrincipalKind::Agent)); | |
| 404 | let asked = made.requested_by.as_ref().expect("who asked is kept"); | |
| 405 | assert_eq!((asked.id.as_str(), asked.username.as_str(), asked.kind), ("usr_1", "syntaqx", PrincipalKind::User)); | |
| 406 | assert_eq!(made.owner().id, "usr_1"); | |
| 407 | } | |
| 408 | ||
| 409 | #[test] | |
| 410 | fn anyone_else_s_reads_back_as_theirs_alone() { | |
| 411 | let own = pull(ASKER, None); | |
| 412 | assert_eq!(own.author.kind, PrincipalKind::User); | |
| 413 | assert!(own.requested_by.is_none()); | |
| 414 | assert_eq!(own.owner().id, "usr_1"); | |
| 415 | // Half a requester is nobody. | |
| 416 | assert!(requester(Some("usr_1".into()), None).is_none()); | |
| 417 | } | |
| 418 | ||
| 419 | #[test] | |
| 420 | fn g1t_s_own_work_reads_back_as_g1t_itself() { | |
| 421 | let own = pull(("g1t", "g1t"), None); | |
| 422 | assert_eq!(own.author.kind, PrincipalKind::System); | |
| 423 | assert!(own.requested_by.is_none()); | |
| 424 | } | |
| 425 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.