saya-cli 0.3.1

Database-aware AI agent for the terminal: full-screen TUI, schema discovery, and bounded read-only SQL over PostgreSQL, MySQL, SQLite, DuckDB, and Snowflake.
//! Render-owned contract view DTOs.
//!
//! These are presentation types only. The `crate::contracts::view` types carry
//! data and IDs and must not gain `serde` or any presentation concern; the
//! adapter slice (2b-2c) maps one to the other.
//!
//! `profile` is the profile *name*, never the opaque identity. The identity is a
//! hash over host, database, user and scope path; serializing it would leak
//! material about the connection. There is no field for it here, and there must
//! not be one — that is a structural guarantee enforced by the type shape, not a
//! convention to remember (see `contract_view_serialized_keys_exclude_opaque_profile_identity`).

use serde::{Deserialize, Serialize};

/// One claim of a contract, in renderable form. `value` is a short rendered form
/// of the claim payload (e.g. a column name for `default_time_column`, an alias
/// for `table_alias`). `reason` is the optional justification a directive claim
/// carries; `contracts show` renders it, `contracts list` does not.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ContractClaimView {
    pub claim_id: String,
    pub kind: String,
    pub origin: String,
    pub status: String,
    pub value: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub column: Option<String>,
    /// Why a directive claim holds, when one was stated. `None` for a claim with
    /// no reason and for every non-directive kind (description/alias). Skipped
    /// from the wire form when `None` so a no-reason claim serializes the same
    /// as before the field existed.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
}

/// A disagreement between confirmed claims of an exclusive kind on one object.
/// Names the kind and the claim IDs only — never claim text.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ContractConflictView {
    pub kind: String,
    pub claim_ids: Vec<String>,
}

/// One object's recallable contract, ready to render. `schema_state` is one of
/// `current`, `needs_review`, `stale`, `live_schema_unavailable`.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ContractView {
    pub profile: String,
    pub object: String,
    pub schema_state: String,
    pub claims: Vec<ContractClaimView>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub conflicts: Vec<ContractConflictView>,
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub truncated: bool,
}

/// One claim waiting for review — a `Candidate` or a persisted `Stale` claim —
/// in renderable form. The queue is a flat per-claim listing — not the
/// object-grouped `ContractView` shape — so it gets its own DTO rather than a
/// one-claim "contract" with a smuggled evidence count. `profile` is the
/// profile *name*, never the opaque identity, and there is no field for the
/// identity here either.
///
/// `status` distinguishes the decision a reviewer is being asked to make:
/// `candidate` (a fresh claim to confirm or reject) from `stale` (a confirmed
/// claim reconciliation marked because the schema drifted, to re-confirm or
/// forget). Without it both appear and a reviewer cannot tell which.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ContractQueueItemView {
    pub profile: String,
    pub claim_id: String,
    /// The claim's persisted status word (`candidate` or `stale`) — the decision
    /// the reviewer is being asked to make, since the queue holds both.
    pub status: String,
    pub kind: String,
    pub value: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub column: Option<String>,
    pub object: String,
    pub schema_state: String,
    pub evidence_count: usize,
}