Skip to main content

saya_cli/render/
contract_view.rs

1//! Render-owned contract view DTOs.
2//!
3//! These are presentation types only. The `crate::contracts::view` types carry
4//! data and IDs and must not gain `serde` or any presentation concern; the
5//! adapter slice (2b-2c) maps one to the other.
6//!
7//! `profile` is the profile *name*, never the opaque identity. The identity is a
8//! hash over host, database, user and scope path; serializing it would leak
9//! material about the connection. There is no field for it here, and there must
10//! not be one — that is a structural guarantee enforced by the type shape, not a
11//! convention to remember (see `contract_view_serialized_keys_exclude_opaque_profile_identity`).
12
13use serde::{Deserialize, Serialize};
14
15/// One claim of a contract, in renderable form. `value` is a short rendered form
16/// of the claim payload (e.g. a column name for `default_time_column`, an alias
17/// for `table_alias`). `reason` is the optional justification a directive claim
18/// carries; `contracts show` renders it, `contracts list` does not.
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
20pub struct ContractClaimView {
21    pub claim_id: String,
22    pub kind: String,
23    pub origin: String,
24    pub status: String,
25    pub value: String,
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub column: Option<String>,
28    /// Why a directive claim holds, when one was stated. `None` for a claim with
29    /// no reason and for every non-directive kind (description/alias). Skipped
30    /// from the wire form when `None` so a no-reason claim serializes the same
31    /// as before the field existed.
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub reason: Option<String>,
34}
35
36/// A disagreement between confirmed claims of an exclusive kind on one object.
37/// Names the kind and the claim IDs only — never claim text.
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39pub struct ContractConflictView {
40    pub kind: String,
41    pub claim_ids: Vec<String>,
42}
43
44/// One object's recallable contract, ready to render. `schema_state` is one of
45/// `current`, `needs_review`, `stale`, `live_schema_unavailable`.
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
47pub struct ContractView {
48    pub profile: String,
49    pub object: String,
50    pub schema_state: String,
51    pub claims: Vec<ContractClaimView>,
52    #[serde(default, skip_serializing_if = "Vec::is_empty")]
53    pub conflicts: Vec<ContractConflictView>,
54    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
55    pub truncated: bool,
56}
57
58/// One claim waiting for review — a `Candidate` or a persisted `Stale` claim —
59/// in renderable form. The queue is a flat per-claim listing — not the
60/// object-grouped `ContractView` shape — so it gets its own DTO rather than a
61/// one-claim "contract" with a smuggled evidence count. `profile` is the
62/// profile *name*, never the opaque identity, and there is no field for the
63/// identity here either.
64///
65/// `status` distinguishes the decision a reviewer is being asked to make:
66/// `candidate` (a fresh claim to confirm or reject) from `stale` (a confirmed
67/// claim reconciliation marked because the schema drifted, to re-confirm or
68/// forget). Without it both appear and a reviewer cannot tell which.
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
70pub struct ContractQueueItemView {
71    pub profile: String,
72    pub claim_id: String,
73    /// The claim's persisted status word (`candidate` or `stale`) — the decision
74    /// the reviewer is being asked to make, since the queue holds both.
75    pub status: String,
76    pub kind: String,
77    pub value: String,
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub column: Option<String>,
80    pub object: String,
81    pub schema_state: String,
82    pub evidence_count: usize,
83}