jacquardSnapshot

← snapshot

4909 bytes
//! 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.",
    })))
}