yog 0.0.71

yog: the standalone server for litany loops — the world, the balls and the conversations, behind one wire
Documentation
//! The query roster's round trips — cut from the sibling table on the seam
//! production took (`codec/query.rs`): §4.8's taxonomy is a file boundary on
//! both sides.

use crate::boundary::config::Read;
use crate::boundary::{Gesture, Query, codec::decode};

/// The query roster's values — the workspace-scoped reads, then the §11
/// inspector family beneath them.
pub(super) fn surface() -> Vec<Gesture> {
    let mut out = vec![
        Gesture::Ask(Query::Workspaces),
        Gesture::Ask(Query::Conversations {
            workspace: "ws".into(),
        }),
        Gesture::Ask(Query::Balls),
        Gesture::Ask(Query::Board),
        Gesture::Ask(Query::Attention),
        Gesture::Ask(Query::Ops { max: 32 }),
        Gesture::Ask(Query::Search {
            text: "tekeli-li".into(),
        }),
    ];
    for file in [
        None,
        Some(crate::workdiff::WorkFile {
            ball: "bl-1".into(),
            handle: None,
            path: "src/a.rs".into(),
        }),
        // The three-word form: a fan candidate's file (bl-c2bd).
        Some(crate::workdiff::WorkFile {
            ball: "bl-1".into(),
            handle: Some("at-1a2b3c4d".into()),
            path: "src/a.rs".into(),
        }),
    ] {
        out.push(Gesture::Ask(Query::WorkDiff {
            workspace: "ws".into(),
            file,
        }));
    }
    // The §3.9 projection (bl-40ab): the address is its whole envelope.
    out.push(Gesture::Ask(Query::Science {
        workspace: "ws".into(),
    }));
    // The §9 browse and roster (bl-dff8), each carrying the sphere it is asked
    // in — providers, sign-ins and lineages all live inside a workspace.
    out.push(Gesture::Ask(Query::Config(Read::Lineages {
        workspace: "ws".into(),
    })));
    out.push(Gesture::Ask(Query::Config(Read::Models {
        workspace: "ws".into(),
        provider: "acme".into(),
    })));
    // REMOTE §5's roster (bl-4e08).
    out.push(Gesture::Ask(Query::Clients {
        workspace: "ws".into(),
    }));
    // The doctor, in **both** of its states (bl-28f4): a box with no workspace
    // selected — which is the box it exists for — and one with.
    for workspace in [None, Some("ws".to_owned())] {
        out.push(Gesture::Ask(Query::Doctor { workspace }));
    }
    // The sign-in lane (REMOTE §8.3, bl-c285): the pair its act names.
    out.push(Gesture::Ask(Query::LoginTail {
        workspace: "ws".into(),
        provider: "acme".into(),
    }));
    // The §11 balls section one workspace deep (bl-b4b5), and the help pages
    // (§8.5) — two spellings whose envelopes had no round trip of their own
    // until the conformance corpus took this list (bl-32cb). The help verb is
    // optional and both states are facts: the index, and one page.
    out.push(Gesture::Ask(Query::WorkspaceBalls {
        workspace: "ws".into(),
    }));
    for verb in [None, Some("scan".to_owned())] {
        out.push(Gesture::Ask(Query::Help { verb }));
    }
    // The routing leg's two reads (bl-024b): one names a handle, the other
    // names nothing — the queue it drains is the intake's own.
    out.push(Gesture::Ask(Query::Invocations));
    out.push(Gesture::Ask(Query::Capture {
        invocation: "inv-1".into(),
    }));
    out.extend(inspector_family());
    out
}

/// The §11 inspector family (bl-6233, bl-13f9): the reads addressed at a
/// conversation rather than a workspace, so each carries both halves of the
/// address and only what no seat could supply beside them.
fn inspector_family() -> Vec<Gesture> {
    let mut out = Vec::new();
    let (workspace, agent) = ("ws".to_owned(), "c-1".to_owned());
    for query in [
        Query::Transcript {
            workspace: workspace.clone(),
            agent: agent.clone(),
        },
        Query::Follow {
            workspace: workspace.clone(),
            agent: agent.clone(),
        },
        Query::Steps {
            workspace: workspace.clone(),
            agent: agent.clone(),
        },
        Query::Rail {
            workspace: workspace.clone(),
            agent: agent.clone(),
        },
        Query::Inbox {
            workspace: workspace.clone(),
            agent: agent.clone(),
        },
        Query::Step {
            workspace: workspace.clone(),
            agent: agent.clone(),
            seq: "003".to_owned(),
        },
        Query::Agent {
            workspace: workspace.clone(),
            agent: agent.clone(),
        },
    ] {
        out.push(Gesture::Ask(query));
    }
    // The listing and one file's bytes are the same query at two depths — the
    // `work-diff` shape, so the path is optional and both sides round-trip. The
    // tree is the third selection on it (bl-44e9): live, or as of one commit.
    for path in [None, Some("src/a.rs".to_owned())] {
        for at in [None, Some("abcdef1234".to_owned())] {
            out.push(Gesture::Ask(Query::Files {
                workspace: workspace.clone(),
                agent: agent.clone(),
                path: path.clone(),
                at,
            }));
        }
    }
    // Config-frozen-at (bl-13f9): the same optional commit, at the family's
    // other tree-subject read — bare is the conversation's own tip.
    for at in [None, Some("abcdef1234".to_owned())] {
        out.push(Gesture::Ask(Query::Governing {
            workspace: workspace.clone(),
            agent: agent.clone(),
            at,
        }));
    }
    out
}

/// A conversation read names its conversation, always: half an address would
/// answer about a different chat, so the envelope refuses rather than guess.
#[test]
fn an_inspector_envelope_missing_half_its_address_is_refused() {
    for op in [
        "transcript",
        "steps",
        "step",
        "files",
        "governing",
        "rail",
        "inbox",
    ] {
        assert!(
            decode(&serde_json::json!({ "op": op, "workspace": "/ws" })).is_err(),
            "{op} without an agent"
        );
        assert!(
            decode(&serde_json::json!({ "op": op, "agent": "c-1" })).is_err(),
            "{op} without a workspace"
        );
    }
    // And a step names the step: "some step" is not a question.
    assert!(
        decode(&serde_json::json!({ "op": "step", "workspace": "/ws", "agent": "c-1" })).is_err()
    );
}

/// A roster names its row, always: the envelope refuses rather than list some
/// other provider's models (and, like every wall-scoped read, it names its
/// workspace).
#[test]
fn a_roster_envelope_without_its_provider_is_refused() {
    assert!(decode(&serde_json::json!({ "op": "models", "workspace": "/ws" })).is_err());
    assert!(decode(&serde_json::json!({ "op": "models", "provider": "acme" })).is_err());
    assert!(decode(&serde_json::json!({ "op": "lineages" })).is_err());
}

/// The work-diff's `file` is all-or-nothing: half of it is a patch read that
/// would open the wrong file, so the envelope refuses rather than guessing.
#[test]
fn a_half_named_work_file_is_refused() {
    let envelope = |file: serde_json::Value| serde_json::json!({ "op": "work-diff", "workspace": "/ws", "file": file });
    assert!(decode(&envelope(serde_json::json!({ "ball": "bl-1" }))).is_err());
    assert!(decode(&envelope(serde_json::json!({ "path": "a.rs" }))).is_err());
    assert_eq!(
        decode(&envelope(serde_json::json!("src/a.rs"))),
        Err("file: not a JSON object".to_owned())
    );
}