chio_store_sqlite/receipt_store/reports/
compliance.rs1use super::*;
4
5impl SqliteReceiptStore {
6 pub fn query_compliance_report(
7 &self,
8 query: &OperatorReportQuery,
9 ) -> Result<ComplianceReport, ReceiptStoreError> {
10 require_admin_receipt_read_context(query.read_context.as_ref(), "compliance report")?;
11 let capability_id = query.capability_id.as_deref();
12 let tool_server = query.tool_server.as_deref();
13 let tool_name = query.tool_name.as_deref();
14 let since = query.since.map(|value| value as i64);
15 let until = query.until.map(|value| value as i64);
16 let agent_subject = query.agent_subject.as_deref();
17
18 let summary_sql = r#"
19 SELECT
20 COUNT(*) AS matching_receipts,
21 COALESCE(SUM(
22 CASE
23 WHEN EXISTS(
24 SELECT 1
25 FROM kernel_checkpoints kc
26 WHERE r.seq BETWEEN kc.batch_start_seq AND kc.batch_end_seq
27 ) THEN 1
28 ELSE 0
29 END
30 ), 0) AS evidence_ready_receipts,
31 COALESCE(SUM(CASE WHEN cl.capability_id IS NOT NULL THEN 1 ELSE 0 END), 0) AS lineage_covered_receipts,
32 COALESCE(SUM(
33 CASE
34 WHEN json_extract(r.raw_json, '$.metadata.financial.settlement_status') = 'pending' THEN 1
35 ELSE 0
36 END
37 ), 0) AS pending_settlement_receipts,
38 COALESCE(SUM(
39 CASE
40 WHEN json_extract(r.raw_json, '$.metadata.financial.settlement_status') = 'failed' THEN 1
41 ELSE 0
42 END
43 ), 0) AS failed_settlement_receipts
44 FROM chio_tool_receipts r
45 LEFT JOIN capability_lineage cl ON r.capability_id = cl.capability_id
46 WHERE (?1 IS NULL OR r.capability_id = ?1)
47 AND (?2 IS NULL OR r.tool_server = ?2)
48 AND (?3 IS NULL OR r.tool_name = ?3)
49 AND (?4 IS NULL OR r.timestamp >= ?4)
50 AND (?5 IS NULL OR r.timestamp <= ?5)
51 AND (?6 IS NULL OR COALESCE(r.subject_key, cl.subject_key) = ?6)
52 "#;
53
54 let (
55 matching_receipts,
56 evidence_ready_receipts,
57 lineage_covered_receipts,
58 pending_settlement_receipts,
59 failed_settlement_receipts,
60 ) = self.connection()?.query_row(
61 summary_sql,
62 params![
63 capability_id,
64 tool_server,
65 tool_name,
66 since,
67 until,
68 agent_subject
69 ],
70 |row| {
71 Ok((
72 row.get::<_, i64>(0)?.max(0) as u64,
73 row.get::<_, i64>(1)?.max(0) as u64,
74 row.get::<_, i64>(2)?.max(0) as u64,
75 row.get::<_, i64>(3)?.max(0) as u64,
76 row.get::<_, i64>(4)?.max(0) as u64,
77 ))
78 },
79 )?;
80
81 let uncheckpointed_receipts = matching_receipts.saturating_sub(evidence_ready_receipts);
82 let lineage_gap_receipts = matching_receipts.saturating_sub(lineage_covered_receipts);
83 let export_query = query
84 .to_evidence_export_query()
85 .map_err(ReceiptStoreError::ReadBoundary)?;
86
87 Ok(ComplianceReport {
88 matching_receipts,
89 evidence_ready_receipts,
90 uncheckpointed_receipts,
91 checkpoint_coverage_rate: ratio_option(evidence_ready_receipts, matching_receipts),
92 lineage_covered_receipts,
93 lineage_gap_receipts,
94 lineage_coverage_rate: ratio_option(lineage_covered_receipts, matching_receipts),
95 pending_settlement_receipts,
96 failed_settlement_receipts,
97 direct_evidence_export_supported: query.direct_evidence_export_supported(),
98 child_receipt_scope: export_query.child_receipt_scope(),
99 proofs_complete: uncheckpointed_receipts == 0,
100 export_query: export_query.clone(),
101 export_scope_note: compliance_export_scope_note(query, &export_query),
102 })
103 }
104}