1use super::*;
3use serde::{de::Error as _, Deserialize, Deserializer, Serialize};
4use std::collections::BTreeSet;
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7pub struct LintTotals {
8 checks: u32,
9 passed: u32,
10 findings: u32,
11 actionable_findings: u32,
12 advisory_findings: u32,
13 incomplete: u32,
14}
15impl LintTotals {
16 fn from_checks(checks: &[LintCheckResult]) -> Result<Self, LintContractError> {
17 let checks_count =
18 u32::try_from(checks.len()).map_err(|_| LintContractError::TooManyChecks)?;
19 let mut totals = Self {
20 checks: checks_count,
21 passed: 0,
22 findings: 0,
23 actionable_findings: 0,
24 advisory_findings: 0,
25 incomplete: 0,
26 };
27 for check in checks {
28 match check.outcome {
29 LintOutcome::Pass => totals.passed += 1,
30 LintOutcome::Finding => {
31 totals.findings += 1;
32 match check.gate_effect() {
33 LintGateEffect::Actionable => totals.actionable_findings += 1,
34 LintGateEffect::Advisory => totals.advisory_findings += 1,
35 }
36 }
37 LintOutcome::NotRunPrerequisite
38 | LintOutcome::InconsistentSnapshot
39 | LintOutcome::FailedToRun => totals.incomplete += 1,
40 }
41 }
42 Ok(totals)
43 }
44 pub const fn checks(&self) -> u32 {
45 self.checks
46 }
47 pub const fn findings(&self) -> u32 {
48 self.findings
49 }
50 pub const fn actionable_findings(&self) -> u32 {
51 self.actionable_findings
52 }
53 pub const fn advisory_findings(&self) -> u32 {
54 self.advisory_findings
55 }
56 pub const fn passed(&self) -> u32 {
57 self.passed
58 }
59 pub const fn incomplete(&self) -> u32 {
60 self.incomplete
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
65pub struct LintReport {
66 report_schema_version: u16,
67 check_catalog_version: u16,
68 profile: LintProfile,
69 scope: LintScope,
70 capability_context: LintCapabilityContext,
71 snapshots: LintSnapshotReceipts,
72 config_fingerprint: LintConfigFingerprint,
73 producer_receipt: LintProducerReceipt,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 agent_work: Option<LintAgentWork>,
76 checks: Vec<LintCheckResult>,
77 totals: LintTotals,
78 complete: bool,
79}
80#[derive(Deserialize)]
81struct LintReportWire {
82 report_schema_version: u16,
83 check_catalog_version: u16,
84 profile: LintProfile,
85 scope: LintScope,
86 capability_context: LintCapabilityContext,
87 snapshots: LintSnapshotReceipts,
88 config_fingerprint: LintConfigFingerprint,
89 producer_receipt: LintProducerReceipt,
90 #[serde(default)]
91 agent_work: Option<LintAgentWork>,
92 checks: Vec<LintCheckResult>,
93 totals: LintTotals,
94 complete: bool,
95}
96impl LintReport {
97 pub fn try_new(
98 scope: LintScope,
99 capability_context: LintCapabilityContext,
100 snapshots: LintSnapshotReceipts,
101 config_fingerprint: LintConfigFingerprint,
102 producer_receipt: LintProducerReceipt,
103 checks: Vec<LintCheckResult>,
104 ) -> Result<Self, LintContractError> {
105 Self::try_new_for_profile(
106 LintProfile::General,
107 scope,
108 capability_context,
109 snapshots,
110 config_fingerprint,
111 producer_receipt,
112 checks,
113 )
114 }
115
116 pub fn try_new_for_profile(
117 profile: LintProfile,
118 scope: LintScope,
119 capability_context: LintCapabilityContext,
120 snapshots: LintSnapshotReceipts,
121 config_fingerprint: LintConfigFingerprint,
122 producer_receipt: LintProducerReceipt,
123 checks: Vec<LintCheckResult>,
124 ) -> Result<Self, LintContractError> {
125 Self::try_new_for_profile_with_agent_work(
126 profile,
127 scope,
128 capability_context,
129 snapshots,
130 config_fingerprint,
131 producer_receipt,
132 checks,
133 None,
134 )
135 }
136
137 #[allow(clippy::too_many_arguments)]
138 pub fn try_new_for_profile_with_agent_work(
139 profile: LintProfile,
140 scope: LintScope,
141 capability_context: LintCapabilityContext,
142 snapshots: LintSnapshotReceipts,
143 config_fingerprint: LintConfigFingerprint,
144 producer_receipt: LintProducerReceipt,
145 mut checks: Vec<LintCheckResult>,
146 agent_work: Option<LintAgentWork>,
147 ) -> Result<Self, LintContractError> {
148 if profile == LintProfile::General && agent_work.is_some() {
149 return Err(LintContractError::InvalidAgentWork);
150 }
151 checks.sort_by(|left, right| left.check_id().cmp(right.check_id()));
152 let totals = LintTotals::from_checks(&checks)?;
153 let complete = checks.iter().all(|check| check.outcome.is_complete());
154 Ok(Self {
155 report_schema_version: LINT_REPORT_SCHEMA_VERSION,
156 check_catalog_version: LINT_CHECK_CATALOG_VERSION,
157 profile,
158 scope,
159 capability_context,
160 snapshots,
161 config_fingerprint,
162 producer_receipt,
163 agent_work,
164 checks,
165 totals,
166 complete,
167 })
168 }
169 pub const fn complete(&self) -> bool {
170 self.complete
171 }
172 pub const fn profile(&self) -> LintProfile {
173 self.profile
174 }
175 pub const fn totals(&self) -> &LintTotals {
176 &self.totals
177 }
178 pub fn checks(&self) -> &[LintCheckResult] {
179 &self.checks
180 }
181 pub fn scope(&self) -> &LintScope {
182 &self.scope
183 }
184 pub const fn capability_context(&self) -> LintCapabilityContext {
185 self.capability_context
186 }
187 pub fn snapshots(&self) -> &LintSnapshotReceipts {
188 &self.snapshots
189 }
190 pub fn config_fingerprint(&self) -> &LintConfigFingerprint {
191 &self.config_fingerprint
192 }
193 pub fn producer_receipt(&self) -> &LintProducerReceipt {
194 &self.producer_receipt
195 }
196 pub fn agent_work(&self) -> Option<&LintAgentWork> {
197 self.agent_work.as_ref()
198 }
199}
200impl<'de> Deserialize<'de> for LintReport {
201 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
202 where
203 D: Deserializer<'de>,
204 {
205 let wire = LintReportWire::deserialize(deserializer)?;
206 if wire.report_schema_version != LINT_REPORT_SCHEMA_VERSION {
207 return Err(D::Error::custom(LintContractError::UnsupportedReportSchema));
208 }
209 if wire.check_catalog_version != LINT_CHECK_CATALOG_VERSION {
210 return Err(D::Error::custom(LintContractError::UnsupportedCheckCatalog));
211 }
212 let expected_checks = match wire.profile {
213 LintProfile::General => LINT_GENERAL_CHECK_COUNT,
214 LintProfile::Deep => LINT_DEEP_CHECK_COUNT,
215 };
216 let unique_ids = wire
217 .checks
218 .iter()
219 .map(LintCheckResult::check_id)
220 .collect::<BTreeSet<_>>();
221 if wire.checks.len() != expected_checks || unique_ids.len() != wire.checks.len() {
222 return Err(D::Error::custom(LintContractError::InvalidCatalogShape));
223 }
224 if wire.checks.iter().any(|check| {
225 canonical_gate_effect(wire.profile, check.check_id()) != Some(check.gate_effect())
226 }) {
227 return Err(D::Error::custom(LintContractError::InvalidCatalogShape));
228 }
229 let report = Self::try_new_for_profile_with_agent_work(
230 wire.profile,
231 wire.scope,
232 wire.capability_context,
233 wire.snapshots,
234 wire.config_fingerprint,
235 wire.producer_receipt,
236 wire.checks,
237 wire.agent_work,
238 )
239 .map_err(D::Error::custom)?;
240 if report.totals != wire.totals {
241 return Err(D::Error::custom(LintContractError::InvalidTotals));
242 }
243 if report.complete != wire.complete {
244 return Err(D::Error::custom(LintContractError::InvalidCompleteness));
245 }
246 Ok(report)
247 }
248}
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
250pub struct LintQuery {
251 #[serde(default, skip_serializing_if = "Option::is_none")]
252 pub profile: Option<LintProfile>,
253 #[serde(default, skip_serializing_if = "Option::is_none")]
254 pub space: Option<String>,
255}
256
257impl LintQuery {
258 pub const fn new(profile: Option<LintProfile>, space: Option<String>) -> Self {
259 Self { profile, space }
260 }
261
262 pub fn applied_profile(&self) -> LintProfile {
263 self.profile.unwrap_or_default()
264 }
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
268pub struct LintRequestQuery {
269 #[serde(flatten)]
270 lint: LintQuery,
271 #[serde(default, skip_serializing_if = "is_false")]
272 external_egress: bool,
273 #[serde(default, skip_serializing_if = "is_false")]
274 agent_assist: bool,
275}
276
277impl LintRequestQuery {
278 pub const fn new(lint: LintQuery, external_egress: bool, agent_assist: bool) -> Self {
279 Self {
280 lint,
281 external_egress,
282 agent_assist,
283 }
284 }
285
286 pub const fn lint(&self) -> &LintQuery {
287 &self.lint
288 }
289
290 pub const fn external_egress(&self) -> bool {
291 self.external_egress
292 }
293
294 pub const fn agent_assist(&self) -> bool {
295 self.agent_assist
296 }
297}
298
299const fn is_false(value: &bool) -> bool {
300 !*value
301}
302
303#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
304pub struct LintErrorResponse {
305 error: String,
306}
307
308impl LintErrorResponse {
309 pub fn new(error: impl Into<String>) -> Self {
310 Self {
311 error: error.into(),
312 }
313 }
314
315 pub fn error(&self) -> &str {
316 &self.error
317 }
318}