//! What is waiting on a person, across every repo at once.
//!
//! This is the question the whole surface exists to answer. Everything else —
//! the log, the tree, the diff — is how you answer *a* question once you have
//! decided which one to look at. This says which one.
//!
//! Three things can wait, and they are not interchangeable:
//!
//! - An **unsettled decision** waits on an attestation. Only a person can give one; the
//! compiler proves an agent cannot. Nothing else unblocks it.
//! - A **blocked gate** waits on those decisions. It is downstream, so it is ranked below
//! them: settling the decision clears the gate, never the reverse.
//! - An **open remark** waits on somebody deciding what to do about it. It blocks
//! nothing, which is exactly why it is last and why it is easy to leave rotting.
//!
//! An **ungoverned** promotion is deliberately *not* listed as waiting. It is
//! admissible right now — nothing is blocked. It is reported separately,
//! because "you may promote this and nothing checked it" is a different
//! sentence from "somebody has to act".
use axum::Json;
use axum::extract::State;
use serde_json::{Value, json};
use crate::dto::DecisionDto;
use crate::error::ApiError;
use crate::routes::read_repos;
use crate::state::{RemarkState, SharedState};
/// `GET /api/awaiting` — the inbox, across every hosted repo.
pub(crate) async fn list(State(state): State<SharedState>) -> Result<Json<Value>, ApiError> {
let repos = read_repos(&state)?;
let mut items: Vec<Value> = Vec::new();
let mut decisions_waiting = 0usize;
let mut remarks_open = 0usize;
for (slug, entry) in repos.iter() {
for record in entry.repo.pending_decisions() {
let dto = DecisionDto::of(record, entry);
decisions_waiting += 1;
items.push(json!({
"kind": "decision",
// Highest: nothing else can clear this, and it is what the
// gate is waiting on.
"rank": 0,
"repo": slug,
"id": dto.id,
"short": dto.short,
"title": dto.title,
"at": dto.at,
"proposed_by": dto.proposed_by,
"scope": dto.scope,
"asks": "a person has to put their name to this, in their own words",
"href": format!("/repos/{slug}/decisions/{}", dto.id),
}));
}
for remark in entry.remarks.values() {
if !matches!(remark.state, RemarkState::Open) {
continue;
}
remarks_open += 1;
items.push(json!({
"kind": "remark",
// Blocks nothing, which is why it is last — and why it rots.
"rank": 2,
"repo": slug,
"id": remark.id.to_string(),
"short": remark.anchor.id.chars().take(12).collect::<String>(),
"title": remark.body,
"at": remark.at,
"remark_kind": remark.kind.label(),
"quote": remark.anchor.quote,
"asks": match remark.kind.label() {
"suggestion" => "draft it as a decision, or close it",
"question" => "answer it",
_ => "acknowledge it",
},
"href": format!("/repos/{slug}/decisions"),
}));
}
}
// Oldest first within a rank: the thing that has waited longest is the
// thing most likely to have been forgotten.
items.sort_by(|a, b| {
a["rank"]
.as_i64()
.cmp(&b["rank"].as_i64())
.then(a["at"].as_i64().cmp(&b["at"].as_i64()))
});
let repo_rows: Vec<Value> = repos
.iter()
.map(|(slug, entry)| {
let unsettled = entry.repo.pending_decisions().count();
let settled = entry.repo.settled_decisions().count();
json!({
"slug": slug,
"org": entry.org_name,
"unsettled": unsettled,
"settled": settled,
"open_remarks": entry
.remarks
.values()
.filter(|r| matches!(r.state, RemarkState::Open))
.count(),
})
})
.collect();
Ok(Json(json!({
"items": items,
"counts": {
"decisions": decisions_waiting,
"remarks": remarks_open,
"total": decisions_waiting + remarks_open,
},
"repos": repo_rows,
"honesty": "an ungoverned promotion is not listed here. Nothing is blocking it — \
it can go through right now, with nothing checked. That is a different \
sentence from `somebody has to act`, and it belongs on the gate.",
})))
}