Skip to main content

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}