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}