axonflow_sdk_rust/types/decisions.rs
1// Decision explainability types — implements ADR-043.
2//
3// The DecisionExplanation shape is frozen per ADR-043. Additive fields
4// may be added with `Option<>` + `serde(skip_serializing_if = "Option::is_none")`;
5// renames or removals require a major version bump.
6//
7// Cross-SDK parity:
8// Go: axonflow-sdk-go/decisions.go
9// Python: axonflow-sdk-python/axonflow/decisions.py
10// TS: axonflow-sdk-typescript/src/types/decisions.ts
11// Java: axonflow-sdk-java/src/main/java/com/getaxonflow/sdk/types/DecisionExplanation.java
12
13use chrono::{DateTime, Utc};
14use serde::{Deserialize, Serialize};
15
16/// A policy reference inside a decision explanation.
17#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
18pub struct ExplainPolicy {
19 pub policy_id: String,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub policy_name: Option<String>,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub action: Option<String>,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub risk_level: Option<String>,
26 #[serde(default)]
27 pub allow_override: bool,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub policy_description: Option<String>,
30}
31
32/// Rule-level detail inside a decision explanation.
33#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
34pub struct ExplainRule {
35 pub policy_id: String,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub rule_id: Option<String>,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub rule_text: Option<String>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub matched_on: Option<String>,
42}
43
44/// Canonical payload returned by `AxonFlowClient::explain_decision`.
45///
46/// Shape frozen per ADR-043. Field semantics:
47///
48/// * `decision_id` — the global decision identifier.
49/// * `timestamp` — when the decision was made.
50/// * `policy_matches` — every policy that contributed to the decision,
51/// with risk level and overridability.
52/// * `matched_rules` — rule-level detail (optional, populated when the
53/// upstream engine supports it).
54/// * `decision` — `"allow"` | `"deny"` | `"require_approval"`.
55/// * `reason` — human-readable reason string.
56/// * `risk_level` — aggregate risk label (`"low"` | `"medium"` | `"high"` | `"critical"`).
57/// * `override_available` — true iff at least one non-critical policy with
58/// `allow_override = true` matched.
59/// * `override_existing_id` — populated when an active override already
60/// covers this caller + policy + tool scope.
61/// * `historical_hit_count_session` — how many times the same
62/// `(policy_id, user_email)` tuple matched in a rolling 24h window.
63/// * `policy_source_link` — optional URL to the policy source.
64/// * `tool_signature` — the tool the decision was scoped to (may be empty
65/// when the decision had no tool context).
66#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
67pub struct DecisionExplanation {
68 pub decision_id: String,
69 pub timestamp: DateTime<Utc>,
70 #[serde(default)]
71 pub policy_matches: Vec<ExplainPolicy>,
72 #[serde(default, skip_serializing_if = "Vec::is_empty")]
73 pub matched_rules: Vec<ExplainRule>,
74 pub decision: String,
75 pub reason: String,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub risk_level: Option<String>,
78 #[serde(default)]
79 pub override_available: bool,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub override_existing_id: Option<String>,
82 #[serde(default)]
83 pub historical_hit_count_session: i64,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub policy_source_link: Option<String>,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub tool_signature: Option<String>,
88}
89
90/// Slim summary returned by `AxonFlowClient::list_decisions`.
91///
92/// Matches the platform `GET /api/v1/decisions` contract: 5 fields.
93/// `policy_id` and `tool_signature` are optional because pre-α1 audit rows
94/// and dynamic-only blocks may not populate them. ADR-043 §"Versioning"
95/// rules apply: additive `Option<>` fields are non-breaking.
96///
97/// Cross-SDK parity:
98/// Go: axonflow-sdk-go/decisions.go (DecisionSummary)
99/// Python: axonflow-sdk-python/axonflow/decisions.py (DecisionSummary)
100/// TS: axonflow-sdk-typescript/src/types/decisions.ts (DecisionSummary)
101/// Java: axonflow-sdk-java/src/main/java/com/getaxonflow/sdk/types/DecisionSummary.java
102#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
103pub struct DecisionSummary {
104 pub decision_id: String,
105 pub timestamp: DateTime<Utc>,
106 pub decision: String,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub policy_id: Option<String>,
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub tool_signature: Option<String>,
111}
112
113/// Optional filters for `AxonFlowClient::list_decisions`.
114///
115/// Every field is optional — leaving all `None` returns the tier-default
116/// page from the caller's tenant. `since` is RFC3339; `decision` is one of
117/// `"allow"|"deny"|"require_approval"`. `limit` is server-capped per tier;
118/// over-cap requests get a 429 with the V1 upgrade envelope.
119#[derive(Debug, Clone, Default, PartialEq)]
120pub struct ListDecisionsOptions {
121 pub since: Option<DateTime<Utc>>,
122 pub decision: Option<String>,
123 pub policy_id: Option<String>,
124 pub tool_signature: Option<String>,
125 pub limit: Option<u32>,
126}
127
128/// Pricing-tier upgrade context returned in a 429 envelope when the caller's
129/// tier limits the operation. Mirrors the platform-side
130/// `feedback_429_no_upgrade_hint_is_conversion_gap.md` contract.
131#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
132pub struct UpgradeInfo {
133 pub tier: String,
134 pub wording: String,
135 pub compare_url: String,
136 pub buy_url: String,
137}
138
139/// Parsed body of a 429 response carrying a tier-cap envelope.
140/// Surfaced via `AxonFlowError::RateLimited`.
141#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
142pub struct RateLimitEnvelope {
143 pub error: String,
144 pub limit_type: String,
145 pub tier: String,
146 pub limit: u32,
147 pub remaining: u32,
148 pub upgrade: UpgradeInfo,
149}