Skip to main content

chio_store_sqlite/receipt_store/
support.rs

1use super::*;
2
3pub(crate) fn unix_timestamp_now_i64() -> i64 {
4    SystemTime::now()
5        .duration_since(UNIX_EPOCH)
6        .map(|duration| duration.as_secs() as i64)
7        .unwrap_or(0)
8}
9
10pub(crate) fn sqlite_i64(value: u64, field: &str) -> Result<i64, ReceiptStoreError> {
11    i64::try_from(value).map_err(|_| {
12        ReceiptStoreError::Conflict(format!(
13            "{field} value {value} exceeds SQLite INTEGER range"
14        ))
15    })
16}
17
18pub(crate) fn sqlite_u64(value: i64, field: &str) -> Result<u64, ReceiptStoreError> {
19    u64::try_from(value).map_err(|_| {
20        ReceiptStoreError::Conflict(format!(
21            "{field} value {value} is outside the supported u64 range"
22        ))
23    })
24}
25
26pub(crate) fn sqlite_bool(value: bool) -> i64 {
27    if value {
28        1
29    } else {
30        0
31    }
32}
33
34pub(crate) fn ensure_chio_receipt_verified(receipt: &ChioReceipt) -> Result<(), ReceiptStoreError> {
35    ensure_chio_receipt_verified_with_context(receipt, "tool receipt", None)
36}
37
38pub(crate) fn ensure_child_receipt_verified(
39    receipt: &ChildRequestReceipt,
40) -> Result<(), ReceiptStoreError> {
41    ensure_child_receipt_verified_with_context(receipt, "child receipt", None)
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45enum ActionParameterHashPolicy {
46    Strict,
47    AllowLegacySignedMismatch,
48}
49
50fn format_receipt_context(
51    receipt_kind: &str,
52    receipt_id: Option<&str>,
53    seq: Option<u64>,
54) -> String {
55    let mut context = receipt_kind.to_string();
56    if let Some(seq) = seq {
57        context.push_str(&format!(" seq {seq}"));
58    }
59    if let Some(receipt_id) = receipt_id {
60        context.push_str(&format!(" receipt {receipt_id}"));
61    }
62    context
63}
64
65pub(crate) fn ensure_chio_receipt_verified_with_context(
66    receipt: &ChioReceipt,
67    receipt_kind: &str,
68    seq: Option<u64>,
69) -> Result<(), ReceiptStoreError> {
70    ensure_chio_receipt_verified_with_context_and_action_hash_policy(
71        receipt,
72        receipt_kind,
73        seq,
74        ActionParameterHashPolicy::Strict,
75    )
76}
77
78fn ensure_chio_receipt_verified_with_context_and_action_hash_policy(
79    receipt: &ChioReceipt,
80    receipt_kind: &str,
81    seq: Option<u64>,
82    action_hash_policy: ActionParameterHashPolicy,
83) -> Result<(), ReceiptStoreError> {
84    let context = format_receipt_context(receipt_kind, Some(receipt.id.as_str()), seq);
85    let signature_valid = receipt.verify_signature().map_err(|error| {
86        ReceiptStoreError::Conflict(format!("{context} verification failed: {error}"))
87    })?;
88    if !signature_valid {
89        return Err(ReceiptStoreError::Conflict(format!(
90            "{context} has invalid signature",
91        )));
92    }
93
94    let parameter_hash_valid = receipt.action.verify_hash().map_err(|error| {
95        ReceiptStoreError::Conflict(format!("{context} verification failed: {error}"))
96    })?;
97    if !parameter_hash_valid {
98        if action_hash_policy == ActionParameterHashPolicy::AllowLegacySignedMismatch {
99            // Older signed receipts may carry pre-canonical parameter hashes.
100            // Keep them readable, but only after the receipt signature verifies.
101            return Ok(());
102        }
103        return Err(ReceiptStoreError::Conflict(format!(
104            "{context} has mismatched action parameter hash",
105        )));
106    }
107
108    Ok(())
109}
110
111pub(crate) fn ensure_child_receipt_verified_with_context(
112    receipt: &ChildRequestReceipt,
113    receipt_kind: &str,
114    seq: Option<u64>,
115) -> Result<(), ReceiptStoreError> {
116    let context = format_receipt_context(receipt_kind, Some(receipt.id.as_str()), seq);
117    let signature_valid = receipt.verify_signature().map_err(|error| {
118        ReceiptStoreError::Conflict(format!("{context} verification failed: {error}"))
119    })?;
120    if !signature_valid {
121        return Err(ReceiptStoreError::Conflict(format!(
122            "{context} has invalid signature",
123        )));
124    }
125
126    Ok(())
127}
128
129pub(crate) fn decode_verified_chio_receipt(
130    raw_json: &str,
131    receipt_kind: &str,
132    seq: Option<u64>,
133) -> Result<ChioReceipt, ReceiptStoreError> {
134    let value: serde_json::Value = serde_json::from_str(raw_json).map_err(|error| {
135        ReceiptStoreError::Conflict(format!(
136            "{} failed to decode: {error}",
137            format_receipt_context(receipt_kind, None, seq)
138        ))
139    })?;
140    let receipt_id = value
141        .get("id")
142        .and_then(|field| field.as_str())
143        .map(str::to_string);
144    let receipt: ChioReceipt = serde_json::from_value(value).map_err(|error| {
145        ReceiptStoreError::Conflict(format!(
146            "{} failed to decode: {error}",
147            format_receipt_context(receipt_kind, receipt_id.as_deref(), seq)
148        ))
149    })?;
150    ensure_chio_receipt_verified_with_context_and_action_hash_policy(
151        &receipt,
152        receipt_kind,
153        seq,
154        ActionParameterHashPolicy::AllowLegacySignedMismatch,
155    )?;
156    Ok(receipt)
157}
158
159pub(crate) fn decode_verified_child_receipt(
160    raw_json: &str,
161    receipt_kind: &str,
162    seq: Option<u64>,
163) -> Result<ChildRequestReceipt, ReceiptStoreError> {
164    let value: serde_json::Value = serde_json::from_str(raw_json).map_err(|error| {
165        ReceiptStoreError::Conflict(format!(
166            "{} failed to decode: {error}",
167            format_receipt_context(receipt_kind, None, seq)
168        ))
169    })?;
170    let receipt_id = value
171        .get("id")
172        .and_then(|field| field.as_str())
173        .map(str::to_string);
174    let receipt: ChildRequestReceipt = serde_json::from_value(value).map_err(|error| {
175        ReceiptStoreError::Conflict(format!(
176            "{} failed to decode: {error}",
177            format_receipt_context(receipt_kind, receipt_id.as_deref(), seq)
178        ))
179    })?;
180    ensure_child_receipt_verified_with_context(&receipt, receipt_kind, seq)?;
181    Ok(receipt)
182}
183
184const CHECKPOINT_TRANSPARENCY_GUARDS_SQL: &str = r#"
185CREATE TRIGGER IF NOT EXISTS kernel_checkpoints_reject_update
186BEFORE UPDATE ON kernel_checkpoints
187BEGIN
188    SELECT RAISE(ABORT, 'kernel checkpoints are immutable');
189END;
190
191CREATE TRIGGER IF NOT EXISTS kernel_checkpoints_reject_delete
192BEFORE DELETE ON kernel_checkpoints
193BEGIN
194    SELECT RAISE(ABORT, 'kernel checkpoints are immutable');
195END;
196
197CREATE TRIGGER IF NOT EXISTS kernel_checkpoints_enforce_append_only
198BEFORE INSERT ON kernel_checkpoints
199BEGIN
200    SELECT CASE
201        WHEN NEW.checkpoint_seq < 1
202            THEN RAISE(ABORT, 'checkpoint_seq must be greater than zero')
203        WHEN NEW.batch_start_seq < 1
204            THEN RAISE(ABORT, 'batch_start_seq must be greater than zero')
205        WHEN NEW.batch_end_seq < NEW.batch_start_seq
206            THEN RAISE(ABORT, 'checkpoint batch_end_seq must be >= batch_start_seq')
207        WHEN NEW.tree_size < 1
208            THEN RAISE(ABORT, 'checkpoint tree_size must be greater than zero')
209        WHEN EXISTS (
210            SELECT 1
211            FROM kernel_checkpoints existing
212            WHERE existing.checkpoint_seq >= NEW.checkpoint_seq
213        )
214            THEN RAISE(
215                ABORT,
216                'kernel checkpoints must be appended in strictly increasing checkpoint_seq order'
217            )
218        WHEN NEW.checkpoint_seq > 1
219            AND NOT EXISTS (
220                SELECT 1
221                FROM kernel_checkpoints predecessor
222                WHERE predecessor.checkpoint_seq = NEW.checkpoint_seq - 1
223                  AND predecessor.batch_end_seq + 1 = NEW.batch_start_seq
224            )
225            THEN RAISE(ABORT, 'kernel checkpoint predecessor continuity violation')
226    END;
227END;
228"#;
229
230#[derive(Debug, Clone)]
231pub(crate) struct PersistedCheckpointRow {
232    checkpoint_seq: u64,
233    batch_start_seq: u64,
234    batch_end_seq: u64,
235    tree_size: u64,
236    merkle_root_hex: String,
237    issued_at: u64,
238    statement_json: String,
239    signature_hex: String,
240    kernel_key_hex: String,
241}
242
243pub(crate) fn checkpoint_error_to_receipt_store(
244    error: chio_kernel::checkpoint::CheckpointError,
245) -> ReceiptStoreError {
246    ReceiptStoreError::Conflict(format!("checkpoint integrity failure: {error}"))
247}
248
249pub(crate) fn ensure_checkpoint_transparency_guards(
250    connection: &Connection,
251) -> Result<(), ReceiptStoreError> {
252    connection.execute_batch(CHECKPOINT_TRANSPARENCY_GUARDS_SQL)?;
253    Ok(())
254}
255
256pub(crate) fn load_persisted_checkpoint_row(
257    connection: &Connection,
258    checkpoint_seq: u64,
259) -> Result<Option<PersistedCheckpointRow>, ReceiptStoreError> {
260    connection
261        .query_row(
262            r#"
263            SELECT checkpoint_seq, batch_start_seq, batch_end_seq, tree_size,
264                   merkle_root, issued_at, statement_json, signature, kernel_key
265            FROM kernel_checkpoints
266            WHERE checkpoint_seq = ?1
267            "#,
268            params![sqlite_i64(checkpoint_seq, "checkpoint_seq")?],
269            |row| {
270                Ok((
271                    row.get::<_, i64>(0)?,
272                    row.get::<_, i64>(1)?,
273                    row.get::<_, i64>(2)?,
274                    row.get::<_, i64>(3)?,
275                    row.get::<_, String>(4)?,
276                    row.get::<_, i64>(5)?,
277                    row.get::<_, String>(6)?,
278                    row.get::<_, String>(7)?,
279                    row.get::<_, String>(8)?,
280                ))
281            },
282        )
283        .optional()?
284        .map(
285            |(
286                checkpoint_seq,
287                batch_start_seq,
288                batch_end_seq,
289                tree_size,
290                merkle_root_hex,
291                issued_at,
292                statement_json,
293                signature_hex,
294                kernel_key_hex,
295            )| {
296                Ok(PersistedCheckpointRow {
297                    checkpoint_seq: sqlite_u64(checkpoint_seq, "checkpoint_seq")?,
298                    batch_start_seq: sqlite_u64(batch_start_seq, "batch_start_seq")?,
299                    batch_end_seq: sqlite_u64(batch_end_seq, "batch_end_seq")?,
300                    tree_size: sqlite_u64(tree_size, "tree_size")?,
301                    merkle_root_hex,
302                    issued_at: sqlite_u64(issued_at, "issued_at")?,
303                    statement_json,
304                    signature_hex,
305                    kernel_key_hex,
306                })
307            },
308        )
309        .transpose()
310}
311
312pub(crate) fn load_latest_persisted_checkpoint_row(
313    connection: &Connection,
314) -> Result<Option<PersistedCheckpointRow>, ReceiptStoreError> {
315    let latest_seq = connection
316        .query_row(
317            "SELECT checkpoint_seq FROM kernel_checkpoints ORDER BY checkpoint_seq DESC LIMIT 1",
318            [],
319            |row| row.get::<_, i64>(0),
320        )
321        .optional()?;
322    latest_seq
323        .map(|value| {
324            load_persisted_checkpoint_row(connection, sqlite_u64(value, "checkpoint_seq")?)
325        })
326        .transpose()
327        .map(|row| row.flatten())
328}
329
330pub(crate) fn load_all_persisted_checkpoint_rows(
331    connection: &Connection,
332) -> Result<Vec<PersistedCheckpointRow>, ReceiptStoreError> {
333    let mut statement = connection.prepare(
334        r#"
335        SELECT checkpoint_seq, batch_start_seq, batch_end_seq, tree_size,
336               merkle_root, issued_at, statement_json, signature, kernel_key
337        FROM kernel_checkpoints
338        ORDER BY checkpoint_seq ASC
339        "#,
340    )?;
341    let rows = statement.query_map([], |row| {
342        Ok((
343            row.get::<_, i64>(0)?,
344            row.get::<_, i64>(1)?,
345            row.get::<_, i64>(2)?,
346            row.get::<_, i64>(3)?,
347            row.get::<_, String>(4)?,
348            row.get::<_, i64>(5)?,
349            row.get::<_, String>(6)?,
350            row.get::<_, String>(7)?,
351            row.get::<_, String>(8)?,
352        ))
353    })?;
354
355    rows.map(|row| {
356        let (
357            checkpoint_seq,
358            batch_start_seq,
359            batch_end_seq,
360            tree_size,
361            merkle_root_hex,
362            issued_at,
363            statement_json,
364            signature_hex,
365            kernel_key_hex,
366        ) = row.map_err(ReceiptStoreError::from)?;
367        Ok(PersistedCheckpointRow {
368            checkpoint_seq: sqlite_u64(checkpoint_seq, "checkpoint_seq")?,
369            batch_start_seq: sqlite_u64(batch_start_seq, "batch_start_seq")?,
370            batch_end_seq: sqlite_u64(batch_end_seq, "batch_end_seq")?,
371            tree_size: sqlite_u64(tree_size, "tree_size")?,
372            merkle_root_hex,
373            issued_at: sqlite_u64(issued_at, "issued_at")?,
374            statement_json,
375            signature_hex,
376            kernel_key_hex,
377        })
378    })
379    .collect::<Result<Vec<_>, _>>()
380}
381
382pub(crate) fn parse_persisted_checkpoint_row(
383    row: PersistedCheckpointRow,
384) -> Result<KernelCheckpoint, ReceiptStoreError> {
385    let body: KernelCheckpointBody = serde_json::from_str(&row.statement_json)?;
386    let signature = Signature::from_hex(&row.signature_hex)
387        .map_err(|error| ReceiptStoreError::CryptoDecode(error.to_string()))?;
388    let checkpoint = KernelCheckpoint { body, signature };
389
390    if checkpoint.body.checkpoint_seq != row.checkpoint_seq {
391        return Err(ReceiptStoreError::Conflict(format!(
392            "checkpoint row seq {} does not match signed checkpoint_seq {}",
393            row.checkpoint_seq, checkpoint.body.checkpoint_seq
394        )));
395    }
396    if checkpoint.body.batch_start_seq != row.batch_start_seq {
397        return Err(ReceiptStoreError::Conflict(format!(
398            "checkpoint {} batch_start_seq column {} does not match signed body {}",
399            row.checkpoint_seq, row.batch_start_seq, checkpoint.body.batch_start_seq
400        )));
401    }
402    if checkpoint.body.batch_end_seq != row.batch_end_seq {
403        return Err(ReceiptStoreError::Conflict(format!(
404            "checkpoint {} batch_end_seq column {} does not match signed body {}",
405            row.checkpoint_seq, row.batch_end_seq, checkpoint.body.batch_end_seq
406        )));
407    }
408    if checkpoint.body.tree_size as u64 != row.tree_size {
409        return Err(ReceiptStoreError::Conflict(format!(
410            "checkpoint {} tree_size column {} does not match signed body {}",
411            row.checkpoint_seq, row.tree_size, checkpoint.body.tree_size
412        )));
413    }
414    if checkpoint.body.merkle_root.to_hex() != row.merkle_root_hex {
415        return Err(ReceiptStoreError::Conflict(format!(
416            "checkpoint {} merkle_root column {} does not match signed body {}",
417            row.checkpoint_seq,
418            row.merkle_root_hex,
419            checkpoint.body.merkle_root.to_hex()
420        )));
421    }
422    if checkpoint.body.issued_at != row.issued_at {
423        return Err(ReceiptStoreError::Conflict(format!(
424            "checkpoint {} issued_at column {} does not match signed body {}",
425            row.checkpoint_seq, row.issued_at, checkpoint.body.issued_at
426        )));
427    }
428    if checkpoint.signature.to_hex() != row.signature_hex {
429        return Err(ReceiptStoreError::Conflict(format!(
430            "checkpoint {} signature column does not match parsed signature",
431            row.checkpoint_seq
432        )));
433    }
434    if checkpoint.body.kernel_key.to_hex() != row.kernel_key_hex {
435        return Err(ReceiptStoreError::Conflict(format!(
436            "checkpoint {} kernel_key column {} does not match signed body {}",
437            row.checkpoint_seq,
438            row.kernel_key_hex,
439            checkpoint.body.kernel_key.to_hex()
440        )));
441    }
442
443    chio_kernel::checkpoint::validate_checkpoint(&checkpoint)
444        .map_err(checkpoint_error_to_receipt_store)?;
445
446    Ok(checkpoint)
447}
448
449pub(crate) fn verify_latest_checkpoint_integrity(
450    connection: &Connection,
451) -> Result<(), ReceiptStoreError> {
452    if load_latest_persisted_checkpoint_row(connection)?.is_none() {
453        return Ok(());
454    }
455    verify_checkpoint_chain_integrity(connection).map(|_| ())
456}
457
458pub(crate) fn verify_checkpoint_chain_integrity(
459    connection: &Connection,
460) -> Result<Option<KernelCheckpoint>, ReceiptStoreError> {
461    let rows = load_all_persisted_checkpoint_rows(connection)?;
462    let mut latest = None;
463
464    for row in rows {
465        let checkpoint = parse_persisted_checkpoint_row(row)?;
466        if let Some(predecessor) = latest.as_ref() {
467            chio_kernel::checkpoint::validate_checkpoint_predecessor(predecessor, &checkpoint)
468                .map_err(checkpoint_error_to_receipt_store)?;
469        }
470        latest = Some(checkpoint);
471    }
472
473    Ok(latest)
474}
475
476const TRANSPARENCY_PROJECTION_GUARDS_SQL: &str = r#"
477CREATE TRIGGER IF NOT EXISTS claim_receipt_log_entries_reject_update
478BEFORE UPDATE ON claim_receipt_log_entries
479BEGIN
480    SELECT RAISE(ABORT, 'claim receipt log entries are immutable');
481END;
482
483CREATE TRIGGER IF NOT EXISTS claim_receipt_log_entries_reject_delete
484BEFORE DELETE ON claim_receipt_log_entries
485BEGIN
486    SELECT RAISE(ABORT, 'claim receipt log entries are immutable');
487END;
488
489CREATE TRIGGER IF NOT EXISTS checkpoint_tree_heads_reject_update
490BEFORE UPDATE ON checkpoint_tree_heads
491BEGIN
492    SELECT RAISE(ABORT, 'checkpoint tree heads are immutable');
493END;
494
495CREATE TRIGGER IF NOT EXISTS checkpoint_tree_heads_reject_delete
496BEFORE DELETE ON checkpoint_tree_heads
497BEGIN
498    SELECT RAISE(ABORT, 'checkpoint tree heads are immutable');
499END;
500
501CREATE TRIGGER IF NOT EXISTS checkpoint_predecessor_witnesses_reject_update
502BEFORE UPDATE ON checkpoint_predecessor_witnesses
503BEGIN
504    SELECT RAISE(ABORT, 'checkpoint predecessor witnesses are immutable');
505END;
506
507CREATE TRIGGER IF NOT EXISTS checkpoint_predecessor_witnesses_reject_delete
508BEFORE DELETE ON checkpoint_predecessor_witnesses
509BEGIN
510    SELECT RAISE(ABORT, 'checkpoint predecessor witnesses are immutable');
511END;
512
513CREATE TRIGGER IF NOT EXISTS checkpoint_publication_metadata_reject_update
514BEFORE UPDATE ON checkpoint_publication_metadata
515BEGIN
516    SELECT RAISE(ABORT, 'checkpoint publication metadata is immutable');
517END;
518
519CREATE TRIGGER IF NOT EXISTS checkpoint_publication_metadata_reject_delete
520BEFORE DELETE ON checkpoint_publication_metadata
521BEGIN
522    SELECT RAISE(ABORT, 'checkpoint publication metadata is immutable');
523END;
524
525CREATE TRIGGER IF NOT EXISTS checkpoint_publication_trust_anchor_bindings_reject_update
526BEFORE UPDATE ON checkpoint_publication_trust_anchor_bindings
527BEGIN
528    SELECT RAISE(ABORT, 'checkpoint publication trust-anchor bindings are immutable');
529END;
530
531CREATE TRIGGER IF NOT EXISTS checkpoint_publication_trust_anchor_bindings_reject_delete
532BEFORE DELETE ON checkpoint_publication_trust_anchor_bindings
533BEGIN
534    SELECT RAISE(ABORT, 'checkpoint publication trust-anchor bindings are immutable');
535END;
536"#;
537
538#[derive(Debug, Clone, PartialEq, Eq)]
539struct ClaimReceiptLogProjectionRow {
540    receipt_id: String,
541    receipt_kind: String,
542    source_seq: u64,
543    timestamp: u64,
544    capability_id: Option<String>,
545    session_id: Option<String>,
546    parent_request_id: Option<String>,
547    request_id: Option<String>,
548    subject_key: Option<String>,
549    issuer_key: Option<String>,
550    tool_server: Option<String>,
551    tool_name: Option<String>,
552    raw_json: String,
553}
554
555impl ClaimReceiptLogProjectionRow {
556    fn kind_rank(&self) -> u8 {
557        match self.receipt_kind.as_str() {
558            "tool_receipt" => 0,
559            "child_receipt" => 1,
560            _ => 2,
561        }
562    }
563
564    fn matches_projection_or_legacy_enrichment(&self, expected: &Self) -> bool {
565        self.receipt_id == expected.receipt_id
566            && self.receipt_kind == expected.receipt_kind
567            && self.source_seq == expected.source_seq
568            && self.timestamp == expected.timestamp
569            && self.capability_id == expected.capability_id
570            && self.session_id == expected.session_id
571            && self.parent_request_id == expected.parent_request_id
572            && self.request_id == expected.request_id
573            && self.tool_server == expected.tool_server
574            && self.tool_name == expected.tool_name
575            && self.raw_json == expected.raw_json
576            && legacy_optional_enrichment_matches(&self.subject_key, &expected.subject_key)
577            && legacy_optional_enrichment_matches(&self.issuer_key, &expected.issuer_key)
578    }
579}
580
581fn legacy_optional_enrichment_matches(
582    existing: &Option<String>,
583    expected: &Option<String>,
584) -> bool {
585    existing == expected || (existing.is_none() && expected.is_some())
586}
587
588#[derive(Debug, Clone, PartialEq, Eq)]
589struct CheckpointTreeHeadProjectionRow {
590    checkpoint_seq: u64,
591    batch_start_seq: u64,
592    batch_end_seq: u64,
593    tree_size: u64,
594    merkle_root: String,
595    issued_at: u64,
596    kernel_key: String,
597    previous_checkpoint_sha256: Option<String>,
598    statement_json: String,
599    signature: String,
600}
601
602#[derive(Debug, Clone, PartialEq, Eq)]
603struct CheckpointPredecessorWitnessProjectionRow {
604    predecessor_checkpoint_seq: u64,
605    witness_checkpoint_seq: u64,
606    previous_checkpoint_sha256: String,
607    witnessed_at: u64,
608    witness_statement_json: String,
609}
610
611#[derive(Debug, Clone, PartialEq, Eq)]
612struct CheckpointPublicationMetadataProjectionRow {
613    checkpoint_seq: u64,
614    publication_schema: String,
615    merkle_root: String,
616    published_at: u64,
617    kernel_key: String,
618    log_tree_size: u64,
619    entry_start_seq: u64,
620    entry_end_seq: u64,
621    previous_checkpoint_sha256: Option<String>,
622}
623
624fn load_tool_claim_receipt_projection_rows(
625    connection: &Connection,
626) -> Result<Vec<ClaimReceiptLogProjectionRow>, ReceiptStoreError> {
627    let mut statement = connection.prepare(
628        r#"
629        SELECT receipt_id, seq, timestamp, capability_id, subject_key, issuer_key,
630               tool_server, tool_name, raw_json
631        FROM chio_tool_receipts
632        ORDER BY timestamp ASC, seq ASC
633        "#,
634    )?;
635    let rows = statement.query_map([], |row| {
636        Ok((
637            row.get::<_, String>(0)?,
638            row.get::<_, i64>(1)?,
639            row.get::<_, i64>(2)?,
640            row.get::<_, Option<String>>(3)?,
641            row.get::<_, Option<String>>(4)?,
642            row.get::<_, Option<String>>(5)?,
643            row.get::<_, Option<String>>(6)?,
644            row.get::<_, Option<String>>(7)?,
645            row.get::<_, String>(8)?,
646        ))
647    })?;
648    rows.map(|row| {
649        let (
650            receipt_id,
651            source_seq,
652            timestamp,
653            capability_id,
654            subject_key,
655            issuer_key,
656            tool_server,
657            tool_name,
658            raw_json,
659        ) = row.map_err(ReceiptStoreError::from)?;
660        Ok(ClaimReceiptLogProjectionRow {
661            receipt_id,
662            receipt_kind: "tool_receipt".to_string(),
663            source_seq: sqlite_u64(source_seq, "claim tool source_seq")?,
664            timestamp: sqlite_u64(timestamp, "claim tool timestamp")?,
665            capability_id,
666            session_id: None,
667            parent_request_id: None,
668            request_id: None,
669            subject_key,
670            issuer_key,
671            tool_server,
672            tool_name,
673            raw_json,
674        })
675    })
676    .collect::<Result<Vec<_>, _>>()
677}
678
679fn load_child_claim_receipt_projection_rows(
680    connection: &Connection,
681) -> Result<Vec<ClaimReceiptLogProjectionRow>, ReceiptStoreError> {
682    let mut statement = connection.prepare(
683        r#"
684        SELECT receipt_id, seq, timestamp, session_id, parent_request_id,
685               request_id, raw_json
686        FROM chio_child_receipts
687        ORDER BY timestamp ASC, seq ASC
688        "#,
689    )?;
690    let rows = statement.query_map([], |row| {
691        Ok((
692            row.get::<_, String>(0)?,
693            row.get::<_, i64>(1)?,
694            row.get::<_, i64>(2)?,
695            row.get::<_, Option<String>>(3)?,
696            row.get::<_, Option<String>>(4)?,
697            row.get::<_, Option<String>>(5)?,
698            row.get::<_, String>(6)?,
699        ))
700    })?;
701    rows.map(|row| {
702        let (
703            receipt_id,
704            source_seq,
705            timestamp,
706            session_id,
707            parent_request_id,
708            request_id,
709            raw_json,
710        ) = row?;
711        Ok(ClaimReceiptLogProjectionRow {
712            receipt_id,
713            receipt_kind: "child_receipt".to_string(),
714            source_seq: sqlite_u64(source_seq, "claim child source_seq")?,
715            timestamp: sqlite_u64(timestamp, "claim child timestamp")?,
716            capability_id: None,
717            session_id,
718            parent_request_id,
719            request_id,
720            subject_key: None,
721            issuer_key: None,
722            tool_server: None,
723            tool_name: None,
724            raw_json,
725        })
726    })
727    .collect()
728}
729
730fn load_claim_receipt_log_projection_row(
731    connection: &Connection,
732    receipt_id: &str,
733) -> Result<Option<ClaimReceiptLogProjectionRow>, ReceiptStoreError> {
734    connection
735        .query_row(
736            r#"
737            SELECT receipt_id, receipt_kind, source_seq, timestamp, capability_id,
738                   session_id, parent_request_id, request_id, subject_key, issuer_key,
739                   tool_server, tool_name, raw_json
740            FROM claim_receipt_log_entries
741            WHERE receipt_id = ?1
742            "#,
743            params![receipt_id],
744            |row| {
745                Ok((
746                    row.get::<_, String>(0)?,
747                    row.get::<_, String>(1)?,
748                    row.get::<_, i64>(2)?,
749                    row.get::<_, i64>(3)?,
750                    row.get::<_, Option<String>>(4)?,
751                    row.get::<_, Option<String>>(5)?,
752                    row.get::<_, Option<String>>(6)?,
753                    row.get::<_, Option<String>>(7)?,
754                    row.get::<_, Option<String>>(8)?,
755                    row.get::<_, Option<String>>(9)?,
756                    row.get::<_, Option<String>>(10)?,
757                    row.get::<_, Option<String>>(11)?,
758                    row.get::<_, String>(12)?,
759                ))
760            },
761        )
762        .optional()
763        .map_err(ReceiptStoreError::from)?
764        .map(
765            |(
766                receipt_id,
767                receipt_kind,
768                source_seq,
769                timestamp,
770                capability_id,
771                session_id,
772                parent_request_id,
773                request_id,
774                subject_key,
775                issuer_key,
776                tool_server,
777                tool_name,
778                raw_json,
779            )| {
780                Ok(ClaimReceiptLogProjectionRow {
781                    receipt_id,
782                    receipt_kind,
783                    source_seq: sqlite_u64(source_seq, "claim log source_seq")?,
784                    timestamp: sqlite_u64(timestamp, "claim log timestamp")?,
785                    capability_id,
786                    session_id,
787                    parent_request_id,
788                    request_id,
789                    subject_key,
790                    issuer_key,
791                    tool_server,
792                    tool_name,
793                    raw_json,
794                })
795            },
796        )
797        .transpose()
798}
799
800fn insert_claim_receipt_log_projection_row(
801    connection: &Connection,
802    row: &ClaimReceiptLogProjectionRow,
803) -> Result<(), ReceiptStoreError> {
804    connection.execute(
805        r#"
806        INSERT INTO claim_receipt_log_entries (
807            receipt_id, receipt_kind, source_seq, timestamp, capability_id,
808            session_id, parent_request_id, request_id, subject_key, issuer_key,
809            tool_server, tool_name, raw_json
810        ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)
811        "#,
812        params![
813            row.receipt_id.as_str(),
814            row.receipt_kind.as_str(),
815            sqlite_i64(row.source_seq, "claim log source_seq")?,
816            sqlite_i64(row.timestamp, "claim log timestamp")?,
817            row.capability_id.as_deref(),
818            row.session_id.as_deref(),
819            row.parent_request_id.as_deref(),
820            row.request_id.as_deref(),
821            row.subject_key.as_deref(),
822            row.issuer_key.as_deref(),
823            row.tool_server.as_deref(),
824            row.tool_name.as_deref(),
825            row.raw_json.as_str(),
826        ],
827    )?;
828    Ok(())
829}
830
831fn load_claim_receipt_log_receipt_ids(
832    connection: &Connection,
833) -> Result<BTreeSet<String>, ReceiptStoreError> {
834    let mut statement = connection
835        .prepare("SELECT receipt_id FROM claim_receipt_log_entries ORDER BY entry_seq ASC")?;
836    let rows = statement.query_map([], |row| row.get::<_, String>(0))?;
837    rows.collect::<Result<BTreeSet<_>, _>>()
838        .map_err(ReceiptStoreError::from)
839}
840
841fn canonical_bytes_from_claim_log_row(
842    receipt_kind: &str,
843    raw_json: &str,
844    entry_seq: u64,
845) -> Result<Vec<u8>, ReceiptStoreError> {
846    match receipt_kind {
847        "tool_receipt" => {
848            let receipt =
849                decode_verified_chio_receipt(raw_json, "claim-log tool receipt", Some(entry_seq))?;
850            chio_core::canonical::canonical_json_bytes(&receipt)
851                .map_err(|error| ReceiptStoreError::Canonical(error.to_string()))
852        }
853        "child_receipt" => {
854            let receipt = decode_verified_child_receipt(
855                raw_json,
856                "claim-log child receipt",
857                Some(entry_seq),
858            )?;
859            chio_core::canonical::canonical_json_bytes(&receipt)
860                .map_err(|error| ReceiptStoreError::Canonical(error.to_string()))
861        }
862        other => Err(ReceiptStoreError::Conflict(format!(
863            "unsupported claim receipt kind `{other}` in claim tree"
864        ))),
865    }
866}
867
868pub(crate) fn load_claim_tree_canonical_bytes_range(
869    connection: &Connection,
870    start_entry_seq: u64,
871    end_entry_seq: u64,
872) -> Result<Vec<(u64, Vec<u8>)>, ReceiptStoreError> {
873    let mut statement = connection.prepare(
874        r#"
875        SELECT entry_seq, receipt_kind, raw_json
876        FROM claim_receipt_log_entries
877        WHERE entry_seq >= ?1 AND entry_seq <= ?2
878        ORDER BY entry_seq ASC
879        "#,
880    )?;
881    let rows = statement.query_map(
882        params![
883            sqlite_i64(start_entry_seq, "claim tree start_entry_seq")?,
884            sqlite_i64(end_entry_seq, "claim tree end_entry_seq")?,
885        ],
886        |row| {
887            Ok((
888                row.get::<_, i64>(0)?,
889                row.get::<_, String>(1)?,
890                row.get::<_, String>(2)?,
891            ))
892        },
893    )?;
894    let mut result = Vec::new();
895    for row in rows {
896        let (entry_seq, receipt_kind, raw_json) = row?;
897        let entry_seq = sqlite_u64(entry_seq, "claim tree entry_seq")?;
898        result.push((
899            entry_seq,
900            canonical_bytes_from_claim_log_row(&receipt_kind, &raw_json, entry_seq)?,
901        ));
902    }
903    Ok(result)
904}
905
906fn load_checkpoint_tree_head_projection_row(
907    connection: &Connection,
908    checkpoint_seq: u64,
909) -> Result<Option<CheckpointTreeHeadProjectionRow>, ReceiptStoreError> {
910    connection
911        .query_row(
912            r#"
913            SELECT checkpoint_seq, batch_start_seq, batch_end_seq, tree_size, merkle_root,
914                   issued_at, kernel_key, previous_checkpoint_sha256, statement_json, signature
915            FROM checkpoint_tree_heads
916            WHERE checkpoint_seq = ?1
917            "#,
918            params![sqlite_i64(checkpoint_seq, "checkpoint_seq")?],
919            |row| {
920                Ok((
921                    row.get::<_, i64>(0)?,
922                    row.get::<_, i64>(1)?,
923                    row.get::<_, i64>(2)?,
924                    row.get::<_, i64>(3)?,
925                    row.get::<_, String>(4)?,
926                    row.get::<_, i64>(5)?,
927                    row.get::<_, String>(6)?,
928                    row.get::<_, Option<String>>(7)?,
929                    row.get::<_, String>(8)?,
930                    row.get::<_, String>(9)?,
931                ))
932            },
933        )
934        .optional()
935        .map_err(ReceiptStoreError::from)?
936        .map(
937            |(
938                checkpoint_seq,
939                batch_start_seq,
940                batch_end_seq,
941                tree_size,
942                merkle_root,
943                issued_at,
944                kernel_key,
945                previous_checkpoint_sha256,
946                statement_json,
947                signature,
948            )| {
949                Ok(CheckpointTreeHeadProjectionRow {
950                    checkpoint_seq: sqlite_u64(checkpoint_seq, "tree head checkpoint_seq")?,
951                    batch_start_seq: sqlite_u64(batch_start_seq, "tree head batch_start_seq")?,
952                    batch_end_seq: sqlite_u64(batch_end_seq, "tree head batch_end_seq")?,
953                    tree_size: sqlite_u64(tree_size, "tree head tree_size")?,
954                    merkle_root,
955                    issued_at: sqlite_u64(issued_at, "tree head issued_at")?,
956                    kernel_key,
957                    previous_checkpoint_sha256,
958                    statement_json,
959                    signature,
960                })
961            },
962        )
963        .transpose()
964}
965
966fn insert_checkpoint_tree_head_projection_row(
967    connection: &Connection,
968    row: &CheckpointTreeHeadProjectionRow,
969) -> Result<(), ReceiptStoreError> {
970    connection.execute(
971        r#"
972        INSERT INTO checkpoint_tree_heads (
973            checkpoint_seq, batch_start_seq, batch_end_seq, tree_size, merkle_root,
974            issued_at, kernel_key, previous_checkpoint_sha256, statement_json, signature
975        ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
976        "#,
977        params![
978            sqlite_i64(row.checkpoint_seq, "checkpoint_seq")?,
979            sqlite_i64(row.batch_start_seq, "batch_start_seq")?,
980            sqlite_i64(row.batch_end_seq, "batch_end_seq")?,
981            sqlite_i64(row.tree_size, "tree_size")?,
982            row.merkle_root.as_str(),
983            sqlite_i64(row.issued_at, "issued_at")?,
984            row.kernel_key.as_str(),
985            row.previous_checkpoint_sha256.as_deref(),
986            row.statement_json.as_str(),
987            row.signature.as_str(),
988        ],
989    )?;
990    Ok(())
991}
992
993fn load_checkpoint_tree_head_projection_ids(
994    connection: &Connection,
995) -> Result<BTreeSet<u64>, ReceiptStoreError> {
996    let mut statement = connection
997        .prepare("SELECT checkpoint_seq FROM checkpoint_tree_heads ORDER BY checkpoint_seq ASC")?;
998    let rows = statement.query_map([], |row| row.get::<_, i64>(0))?;
999    rows.map(|row| {
1000        let checkpoint_seq = row.map_err(ReceiptStoreError::from)?;
1001        sqlite_u64(checkpoint_seq, "checkpoint_seq")
1002    })
1003    .collect::<Result<BTreeSet<_>, _>>()
1004}
1005
1006fn load_checkpoint_predecessor_witness_projection_row(
1007    connection: &Connection,
1008    witness_checkpoint_seq: u64,
1009) -> Result<Option<CheckpointPredecessorWitnessProjectionRow>, ReceiptStoreError> {
1010    connection
1011        .query_row(
1012            r#"
1013            SELECT predecessor_checkpoint_seq, witness_checkpoint_seq,
1014                   previous_checkpoint_sha256, witnessed_at, witness_statement_json
1015            FROM checkpoint_predecessor_witnesses
1016            WHERE witness_checkpoint_seq = ?1
1017            "#,
1018            params![sqlite_i64(
1019                witness_checkpoint_seq,
1020                "witness_checkpoint_seq"
1021            )?],
1022            |row| {
1023                Ok((
1024                    row.get::<_, i64>(0)?,
1025                    row.get::<_, i64>(1)?,
1026                    row.get::<_, String>(2)?,
1027                    row.get::<_, i64>(3)?,
1028                    row.get::<_, String>(4)?,
1029                ))
1030            },
1031        )
1032        .optional()
1033        .map_err(ReceiptStoreError::from)?
1034        .map(
1035            |(
1036                predecessor_checkpoint_seq,
1037                witness_checkpoint_seq,
1038                previous_checkpoint_sha256,
1039                witnessed_at,
1040                witness_statement_json,
1041            )| {
1042                Ok(CheckpointPredecessorWitnessProjectionRow {
1043                    predecessor_checkpoint_seq: sqlite_u64(
1044                        predecessor_checkpoint_seq,
1045                        "predecessor_checkpoint_seq",
1046                    )?,
1047                    witness_checkpoint_seq: sqlite_u64(
1048                        witness_checkpoint_seq,
1049                        "witness_checkpoint_seq",
1050                    )?,
1051                    previous_checkpoint_sha256,
1052                    witnessed_at: sqlite_u64(witnessed_at, "witnessed_at")?,
1053                    witness_statement_json,
1054                })
1055            },
1056        )
1057        .transpose()
1058}
1059
1060fn insert_checkpoint_predecessor_witness_projection_row(
1061    connection: &Connection,
1062    row: &CheckpointPredecessorWitnessProjectionRow,
1063) -> Result<(), ReceiptStoreError> {
1064    connection.execute(
1065        r#"
1066        INSERT INTO checkpoint_predecessor_witnesses (
1067            predecessor_checkpoint_seq, witness_checkpoint_seq,
1068            previous_checkpoint_sha256, witnessed_at, witness_statement_json
1069        ) VALUES (?1, ?2, ?3, ?4, ?5)
1070        "#,
1071        params![
1072            sqlite_i64(row.predecessor_checkpoint_seq, "predecessor_checkpoint_seq")?,
1073            sqlite_i64(row.witness_checkpoint_seq, "witness_checkpoint_seq")?,
1074            row.previous_checkpoint_sha256.as_str(),
1075            sqlite_i64(row.witnessed_at, "witnessed_at")?,
1076            row.witness_statement_json.as_str(),
1077        ],
1078    )?;
1079    Ok(())
1080}
1081
1082fn load_checkpoint_predecessor_witness_projection_ids(
1083    connection: &Connection,
1084) -> Result<BTreeSet<u64>, ReceiptStoreError> {
1085    let mut statement = connection.prepare(
1086        "SELECT witness_checkpoint_seq FROM checkpoint_predecessor_witnesses ORDER BY witness_checkpoint_seq ASC",
1087    )?;
1088    let rows = statement.query_map([], |row| row.get::<_, i64>(0))?;
1089    rows.map(|row| {
1090        let witness_checkpoint_seq = row.map_err(ReceiptStoreError::from)?;
1091        sqlite_u64(witness_checkpoint_seq, "witness_checkpoint_seq")
1092    })
1093    .collect::<Result<BTreeSet<_>, _>>()
1094}
1095
1096fn load_checkpoint_publication_metadata_projection_row(
1097    connection: &Connection,
1098    checkpoint_seq: u64,
1099) -> Result<Option<CheckpointPublicationMetadataProjectionRow>, ReceiptStoreError> {
1100    connection
1101        .query_row(
1102            r#"
1103            SELECT checkpoint_seq, publication_schema, merkle_root, published_at,
1104                   kernel_key, log_tree_size, entry_start_seq, entry_end_seq,
1105                   previous_checkpoint_sha256
1106            FROM checkpoint_publication_metadata
1107            WHERE checkpoint_seq = ?1
1108            "#,
1109            params![sqlite_i64(checkpoint_seq, "checkpoint_seq")?],
1110            |row| {
1111                Ok((
1112                    row.get::<_, i64>(0)?,
1113                    row.get::<_, String>(1)?,
1114                    row.get::<_, String>(2)?,
1115                    row.get::<_, i64>(3)?,
1116                    row.get::<_, String>(4)?,
1117                    row.get::<_, i64>(5)?,
1118                    row.get::<_, i64>(6)?,
1119                    row.get::<_, i64>(7)?,
1120                    row.get::<_, Option<String>>(8)?,
1121                ))
1122            },
1123        )
1124        .optional()
1125        .map_err(ReceiptStoreError::from)?
1126        .map(
1127            |(
1128                checkpoint_seq,
1129                publication_schema,
1130                merkle_root,
1131                published_at,
1132                kernel_key,
1133                log_tree_size,
1134                entry_start_seq,
1135                entry_end_seq,
1136                previous_checkpoint_sha256,
1137            )| {
1138                Ok(CheckpointPublicationMetadataProjectionRow {
1139                    checkpoint_seq: sqlite_u64(
1140                        checkpoint_seq,
1141                        "checkpoint publication metadata checkpoint_seq",
1142                    )?,
1143                    publication_schema,
1144                    merkle_root,
1145                    published_at: sqlite_u64(
1146                        published_at,
1147                        "checkpoint publication metadata published_at",
1148                    )?,
1149                    kernel_key,
1150                    log_tree_size: sqlite_u64(
1151                        log_tree_size,
1152                        "checkpoint publication metadata log_tree_size",
1153                    )?,
1154                    entry_start_seq: sqlite_u64(
1155                        entry_start_seq,
1156                        "checkpoint publication metadata entry_start_seq",
1157                    )?,
1158                    entry_end_seq: sqlite_u64(
1159                        entry_end_seq,
1160                        "checkpoint publication metadata entry_end_seq",
1161                    )?,
1162                    previous_checkpoint_sha256,
1163                })
1164            },
1165        )
1166        .transpose()
1167}
1168
1169fn insert_checkpoint_publication_metadata_projection_row(
1170    connection: &Connection,
1171    row: &CheckpointPublicationMetadataProjectionRow,
1172) -> Result<(), ReceiptStoreError> {
1173    connection.execute(
1174        r#"
1175        INSERT INTO checkpoint_publication_metadata (
1176            checkpoint_seq, publication_schema, merkle_root, published_at, kernel_key,
1177            log_tree_size, entry_start_seq, entry_end_seq, previous_checkpoint_sha256
1178        ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
1179        "#,
1180        params![
1181            sqlite_i64(row.checkpoint_seq, "checkpoint_seq")?,
1182            row.publication_schema.as_str(),
1183            row.merkle_root.as_str(),
1184            sqlite_i64(row.published_at, "published_at")?,
1185            row.kernel_key.as_str(),
1186            sqlite_i64(row.log_tree_size, "log_tree_size")?,
1187            sqlite_i64(row.entry_start_seq, "entry_start_seq")?,
1188            sqlite_i64(row.entry_end_seq, "entry_end_seq")?,
1189            row.previous_checkpoint_sha256.as_deref(),
1190        ],
1191    )?;
1192    Ok(())
1193}
1194
1195fn load_checkpoint_publication_metadata_projection_ids(
1196    connection: &Connection,
1197) -> Result<BTreeSet<u64>, ReceiptStoreError> {
1198    let mut statement = connection.prepare(
1199        "SELECT checkpoint_seq FROM checkpoint_publication_metadata ORDER BY checkpoint_seq ASC",
1200    )?;
1201    let rows = statement.query_map([], |row| row.get::<_, i64>(0))?;
1202    rows.map(|row| {
1203        let checkpoint_seq = row.map_err(ReceiptStoreError::from)?;
1204        sqlite_u64(
1205            checkpoint_seq,
1206            "checkpoint publication metadata checkpoint_seq",
1207        )
1208    })
1209    .collect::<Result<BTreeSet<_>, _>>()
1210}
1211
1212pub(crate) fn ensure_transparency_projection_guards(
1213    connection: &Connection,
1214) -> Result<(), ReceiptStoreError> {
1215    connection.execute_batch(TRANSPARENCY_PROJECTION_GUARDS_SQL)?;
1216    Ok(())
1217}
1218
1219pub(crate) fn backfill_claim_receipt_log_entries(
1220    connection: &mut Connection,
1221) -> Result<(), ReceiptStoreError> {
1222    let mut expected = load_tool_claim_receipt_projection_rows(connection)?;
1223    expected.extend(load_child_claim_receipt_projection_rows(connection)?);
1224    expected.sort_by(|left, right| {
1225        (
1226            left.timestamp,
1227            left.kind_rank(),
1228            left.source_seq,
1229            left.receipt_id.as_str(),
1230        )
1231            .cmp(&(
1232                right.timestamp,
1233                right.kind_rank(),
1234                right.source_seq,
1235                right.receipt_id.as_str(),
1236            ))
1237    });
1238
1239    let existing_count = connection.query_row(
1240        "SELECT COUNT(*) FROM claim_receipt_log_entries",
1241        [],
1242        |row| row.get::<_, i64>(0),
1243    )?;
1244    let existing_count = sqlite_u64(existing_count, "claim_receipt_log_entries count")?;
1245    let expected_receipt_ids = expected
1246        .iter()
1247        .map(|row| row.receipt_id.clone())
1248        .collect::<BTreeSet<_>>();
1249
1250    let tx = connection.transaction()?;
1251    if existing_count == 0 {
1252        for row in &expected {
1253            insert_claim_receipt_log_projection_row(&tx, row)?;
1254        }
1255        tx.commit()?;
1256        return Ok(());
1257    }
1258
1259    for row in &expected {
1260        let Some(existing) = load_claim_receipt_log_projection_row(&tx, &row.receipt_id)? else {
1261            return Err(ReceiptStoreError::Conflict(format!(
1262                "claim receipt log entry `{}` is missing for persisted {} source row",
1263                row.receipt_id, row.receipt_kind
1264            )));
1265        };
1266        if !existing.matches_projection_or_legacy_enrichment(row) {
1267            return Err(ReceiptStoreError::Conflict(format!(
1268                "claim receipt log entry `{}` diverges from persisted {} source row",
1269                row.receipt_id, row.receipt_kind
1270            )));
1271        }
1272    }
1273
1274    let existing_receipt_ids = load_claim_receipt_log_receipt_ids(&tx)?;
1275    if existing_receipt_ids != expected_receipt_ids {
1276        let missing = expected_receipt_ids
1277            .difference(&existing_receipt_ids)
1278            .next()
1279            .cloned();
1280        let extra = existing_receipt_ids
1281            .difference(&expected_receipt_ids)
1282            .next()
1283            .cloned();
1284        return Err(ReceiptStoreError::Conflict(format!(
1285            "claim receipt log entry set drift detected (missing: {}, extra: {})",
1286            missing.as_deref().unwrap_or("<none>"),
1287            extra.as_deref().unwrap_or("<none>")
1288        )));
1289    }
1290
1291    tx.commit()?;
1292    Ok(())
1293}
1294
1295pub(crate) fn backfill_checkpoint_transparency_projections(
1296    connection: &mut Connection,
1297) -> Result<(), ReceiptStoreError> {
1298    let rows = load_all_persisted_checkpoint_rows(connection)?;
1299    let mut parsed_checkpoints = Vec::with_capacity(rows.len());
1300    let mut expected_heads = Vec::with_capacity(rows.len());
1301    let mut expected_witnesses = Vec::new();
1302    let mut expected_publications = Vec::with_capacity(rows.len());
1303
1304    for row in rows {
1305        let checkpoint = parse_persisted_checkpoint_row(row.clone())?;
1306        if let Some(predecessor) = parsed_checkpoints.last() {
1307            chio_kernel::checkpoint::validate_checkpoint_predecessor(predecessor, &checkpoint)
1308                .map_err(checkpoint_error_to_receipt_store)?;
1309        }
1310        let publication = chio_kernel::checkpoint::build_checkpoint_publication(&checkpoint)
1311            .map_err(checkpoint_error_to_receipt_store)?;
1312
1313        expected_heads.push(CheckpointTreeHeadProjectionRow {
1314            checkpoint_seq: row.checkpoint_seq,
1315            batch_start_seq: row.batch_start_seq,
1316            batch_end_seq: row.batch_end_seq,
1317            tree_size: row.tree_size,
1318            merkle_root: row.merkle_root_hex,
1319            issued_at: row.issued_at,
1320            kernel_key: row.kernel_key_hex,
1321            previous_checkpoint_sha256: checkpoint.body.previous_checkpoint_sha256.clone(),
1322            statement_json: row.statement_json.clone(),
1323            signature: row.signature_hex,
1324        });
1325        expected_publications.push(CheckpointPublicationMetadataProjectionRow {
1326            checkpoint_seq: publication.checkpoint_seq,
1327            publication_schema: publication.schema,
1328            merkle_root: publication.merkle_root.to_hex(),
1329            published_at: publication.published_at,
1330            kernel_key: publication.kernel_key.to_hex(),
1331            log_tree_size: publication.log_tree_size,
1332            entry_start_seq: publication.entry_start_seq,
1333            entry_end_seq: publication.entry_end_seq,
1334            previous_checkpoint_sha256: publication.previous_checkpoint_sha256,
1335        });
1336
1337        if let Some(previous_checkpoint_sha256) = checkpoint.body.previous_checkpoint_sha256.clone()
1338        {
1339            if checkpoint.body.checkpoint_seq <= 1 {
1340                return Err(ReceiptStoreError::Conflict(format!(
1341                    "checkpoint {} cannot witness a predecessor digest",
1342                    checkpoint.body.checkpoint_seq
1343                )));
1344            }
1345            expected_witnesses.push(CheckpointPredecessorWitnessProjectionRow {
1346                predecessor_checkpoint_seq: checkpoint.body.checkpoint_seq - 1,
1347                witness_checkpoint_seq: checkpoint.body.checkpoint_seq,
1348                previous_checkpoint_sha256,
1349                witnessed_at: checkpoint.body.issued_at,
1350                witness_statement_json: row.statement_json,
1351            });
1352        }
1353
1354        parsed_checkpoints.push(checkpoint);
1355    }
1356
1357    let tx = connection.transaction()?;
1358    for row in &expected_heads {
1359        match load_checkpoint_tree_head_projection_row(&tx, row.checkpoint_seq)? {
1360            Some(existing) if existing == *row => {}
1361            Some(_) => {
1362                return Err(ReceiptStoreError::Conflict(format!(
1363                    "checkpoint tree head projection for checkpoint {} diverges from persisted checkpoint row",
1364                    row.checkpoint_seq
1365                )))
1366            }
1367            None => insert_checkpoint_tree_head_projection_row(&tx, row)?,
1368        }
1369    }
1370
1371    let expected_head_ids = expected_heads
1372        .iter()
1373        .map(|row| row.checkpoint_seq)
1374        .collect::<BTreeSet<_>>();
1375    let existing_head_ids = load_checkpoint_tree_head_projection_ids(&tx)?;
1376    if existing_head_ids != expected_head_ids {
1377        let missing = expected_head_ids
1378            .difference(&existing_head_ids)
1379            .next()
1380            .copied();
1381        let extra = existing_head_ids
1382            .difference(&expected_head_ids)
1383            .next()
1384            .copied();
1385        return Err(ReceiptStoreError::Conflict(format!(
1386            "checkpoint tree head projection drift detected (missing: {}, extra: {})",
1387            missing
1388                .map(|value| value.to_string())
1389                .unwrap_or_else(|| "<none>".to_string()),
1390            extra
1391                .map(|value| value.to_string())
1392                .unwrap_or_else(|| "<none>".to_string())
1393        )));
1394    }
1395
1396    for row in &expected_witnesses {
1397        match load_checkpoint_predecessor_witness_projection_row(&tx, row.witness_checkpoint_seq)? {
1398            Some(existing) if existing == *row => {}
1399            Some(_) => {
1400                return Err(ReceiptStoreError::Conflict(format!(
1401                    "checkpoint predecessor witness projection for checkpoint {} diverges from persisted checkpoint chain",
1402                    row.witness_checkpoint_seq
1403                )))
1404            }
1405            None => insert_checkpoint_predecessor_witness_projection_row(&tx, row)?,
1406        }
1407    }
1408
1409    let expected_witness_ids = expected_witnesses
1410        .iter()
1411        .map(|row| row.witness_checkpoint_seq)
1412        .collect::<BTreeSet<_>>();
1413    let existing_witness_ids = load_checkpoint_predecessor_witness_projection_ids(&tx)?;
1414    if existing_witness_ids != expected_witness_ids {
1415        let missing = expected_witness_ids
1416            .difference(&existing_witness_ids)
1417            .next()
1418            .copied();
1419        let extra = existing_witness_ids
1420            .difference(&expected_witness_ids)
1421            .next()
1422            .copied();
1423        return Err(ReceiptStoreError::Conflict(format!(
1424            "checkpoint predecessor witness projection drift detected (missing: {}, extra: {})",
1425            missing
1426                .map(|value| value.to_string())
1427                .unwrap_or_else(|| "<none>".to_string()),
1428            extra
1429                .map(|value| value.to_string())
1430                .unwrap_or_else(|| "<none>".to_string())
1431        )));
1432    }
1433
1434    for row in &expected_publications {
1435        match load_checkpoint_publication_metadata_projection_row(&tx, row.checkpoint_seq)? {
1436            Some(existing) if existing == *row => {}
1437            Some(_) => {
1438                return Err(ReceiptStoreError::Conflict(format!(
1439                    "checkpoint publication metadata projection for checkpoint {} diverges from persisted checkpoint row",
1440                    row.checkpoint_seq
1441                )))
1442            }
1443            None => insert_checkpoint_publication_metadata_projection_row(&tx, row)?,
1444        }
1445    }
1446
1447    let expected_publication_ids = expected_publications
1448        .iter()
1449        .map(|row| row.checkpoint_seq)
1450        .collect::<BTreeSet<_>>();
1451    let existing_publication_ids = load_checkpoint_publication_metadata_projection_ids(&tx)?;
1452    if existing_publication_ids != expected_publication_ids {
1453        let missing = expected_publication_ids
1454            .difference(&existing_publication_ids)
1455            .next()
1456            .copied();
1457        let extra = existing_publication_ids
1458            .difference(&expected_publication_ids)
1459            .next()
1460            .copied();
1461        return Err(ReceiptStoreError::Conflict(format!(
1462            "checkpoint publication metadata projection drift detected (missing: {}, extra: {})",
1463            missing
1464                .map(|value| value.to_string())
1465                .unwrap_or_else(|| "<none>".to_string()),
1466            extra
1467                .map(|value| value.to_string())
1468                .unwrap_or_else(|| "<none>".to_string())
1469        )));
1470    }
1471
1472    tx.commit()?;
1473    Ok(())
1474}
1475
1476pub(crate) fn settlement_reconciliation_state_text(
1477    state: SettlementReconciliationState,
1478) -> &'static str {
1479    match state {
1480        SettlementReconciliationState::Open => "open",
1481        SettlementReconciliationState::Reconciled => "reconciled",
1482        SettlementReconciliationState::Ignored => "ignored",
1483        SettlementReconciliationState::RetryScheduled => "retry_scheduled",
1484    }
1485}
1486
1487pub(crate) fn parse_settlement_reconciliation_state(
1488    value: &str,
1489) -> Result<SettlementReconciliationState, ReceiptStoreError> {
1490    serde_json::from_str(&format!("\"{value}\"")).map_err(ReceiptStoreError::from)
1491}
1492
1493pub(crate) fn metered_billing_reconciliation_state_text(
1494    state: MeteredBillingReconciliationState,
1495) -> &'static str {
1496    match state {
1497        MeteredBillingReconciliationState::Open => "open",
1498        MeteredBillingReconciliationState::Reconciled => "reconciled",
1499        MeteredBillingReconciliationState::Ignored => "ignored",
1500        MeteredBillingReconciliationState::RetryScheduled => "retry_scheduled",
1501    }
1502}
1503
1504pub(crate) fn parse_metered_billing_reconciliation_state(
1505    value: &str,
1506) -> Result<MeteredBillingReconciliationState, ReceiptStoreError> {
1507    serde_json::from_str(&format!("\"{value}\"")).map_err(ReceiptStoreError::from)
1508}
1509
1510pub(crate) fn underwriting_decision_outcome_label(
1511    outcome: UnderwritingDecisionOutcome,
1512) -> &'static str {
1513    match outcome {
1514        UnderwritingDecisionOutcome::Approve => "approve",
1515        UnderwritingDecisionOutcome::ReduceCeiling => "reduce_ceiling",
1516        UnderwritingDecisionOutcome::StepUp => "step_up",
1517        UnderwritingDecisionOutcome::Deny => "deny",
1518    }
1519}
1520
1521pub(crate) fn underwriting_lifecycle_state_label(
1522    state: UnderwritingDecisionLifecycleState,
1523) -> &'static str {
1524    match state {
1525        UnderwritingDecisionLifecycleState::Active => "active",
1526        UnderwritingDecisionLifecycleState::Superseded => "superseded",
1527    }
1528}
1529
1530pub(crate) fn underwriting_review_state_label(
1531    state: chio_kernel::UnderwritingReviewState,
1532) -> &'static str {
1533    match state {
1534        chio_kernel::UnderwritingReviewState::Approved => "approved",
1535        chio_kernel::UnderwritingReviewState::ManualReviewRequired => "manual_review_required",
1536        chio_kernel::UnderwritingReviewState::Denied => "denied",
1537    }
1538}
1539
1540pub(crate) fn underwriting_risk_class_label(
1541    class: chio_kernel::UnderwritingRiskClass,
1542) -> &'static str {
1543    match class {
1544        chio_kernel::UnderwritingRiskClass::Baseline => "baseline",
1545        chio_kernel::UnderwritingRiskClass::Guarded => "guarded",
1546        chio_kernel::UnderwritingRiskClass::Elevated => "elevated",
1547        chio_kernel::UnderwritingRiskClass::Critical => "critical",
1548    }
1549}
1550
1551pub(crate) fn underwriting_appeal_status_label(status: UnderwritingAppealStatus) -> &'static str {
1552    match status {
1553        UnderwritingAppealStatus::Open => "open",
1554        UnderwritingAppealStatus::Accepted => "accepted",
1555        UnderwritingAppealStatus::Rejected => "rejected",
1556    }
1557}
1558
1559pub(crate) fn credit_facility_disposition_label(
1560    disposition: CreditFacilityDisposition,
1561) -> &'static str {
1562    match disposition {
1563        CreditFacilityDisposition::Grant => "grant",
1564        CreditFacilityDisposition::ManualReview => "manual_review",
1565        CreditFacilityDisposition::Deny => "deny",
1566    }
1567}
1568
1569pub(crate) fn credit_facility_lifecycle_state_label(
1570    state: CreditFacilityLifecycleState,
1571) -> &'static str {
1572    match state {
1573        CreditFacilityLifecycleState::Active => "active",
1574        CreditFacilityLifecycleState::Superseded => "superseded",
1575        CreditFacilityLifecycleState::Denied => "denied",
1576        CreditFacilityLifecycleState::Expired => "expired",
1577    }
1578}
1579
1580pub(crate) fn credit_bond_disposition_label(disposition: CreditBondDisposition) -> &'static str {
1581    match disposition {
1582        CreditBondDisposition::Lock => "lock",
1583        CreditBondDisposition::Hold => "hold",
1584        CreditBondDisposition::Release => "release",
1585        CreditBondDisposition::Impair => "impair",
1586    }
1587}
1588
1589pub(crate) fn credit_bond_lifecycle_state_label(state: CreditBondLifecycleState) -> &'static str {
1590    match state {
1591        CreditBondLifecycleState::Active => "active",
1592        CreditBondLifecycleState::Superseded => "superseded",
1593        CreditBondLifecycleState::Released => "released",
1594        CreditBondLifecycleState::Impaired => "impaired",
1595        CreditBondLifecycleState::Expired => "expired",
1596    }
1597}
1598
1599pub(crate) fn liability_provider_lifecycle_state_label(
1600    state: LiabilityProviderLifecycleState,
1601) -> &'static str {
1602    match state {
1603        LiabilityProviderLifecycleState::Active => "active",
1604        LiabilityProviderLifecycleState::Suspended => "suspended",
1605        LiabilityProviderLifecycleState::Superseded => "superseded",
1606        LiabilityProviderLifecycleState::Retired => "retired",
1607    }
1608}
1609
1610pub(crate) fn credit_loss_lifecycle_event_kind_label(
1611    kind: CreditLossLifecycleEventKind,
1612) -> &'static str {
1613    match kind {
1614        CreditLossLifecycleEventKind::Delinquency => "delinquency",
1615        CreditLossLifecycleEventKind::Recovery => "recovery",
1616        CreditLossLifecycleEventKind::ReserveRelease => "reserve_release",
1617        CreditLossLifecycleEventKind::ReserveSlash => "reserve_slash",
1618        CreditLossLifecycleEventKind::WriteOff => "write_off",
1619    }
1620}
1621
1622pub(crate) fn parse_underwriting_lifecycle_state(
1623    value: &str,
1624) -> Result<UnderwritingDecisionLifecycleState, ReceiptStoreError> {
1625    serde_json::from_str(&format!("\"{value}\"")).map_err(ReceiptStoreError::from)
1626}
1627
1628pub(crate) fn parse_credit_facility_lifecycle_state(
1629    value: &str,
1630) -> Result<CreditFacilityLifecycleState, ReceiptStoreError> {
1631    serde_json::from_str(&format!("\"{value}\"")).map_err(ReceiptStoreError::from)
1632}
1633
1634pub(crate) fn parse_credit_bond_lifecycle_state(
1635    value: &str,
1636) -> Result<CreditBondLifecycleState, ReceiptStoreError> {
1637    serde_json::from_str(&format!("\"{value}\"")).map_err(ReceiptStoreError::from)
1638}
1639
1640pub(crate) fn parse_liability_provider_lifecycle_state(
1641    value: &str,
1642) -> Result<LiabilityProviderLifecycleState, ReceiptStoreError> {
1643    serde_json::from_str(&format!("\"{value}\"")).map_err(ReceiptStoreError::from)
1644}
1645
1646pub(crate) fn liability_quote_disposition_label(
1647    disposition: &LiabilityQuoteDisposition,
1648) -> &'static str {
1649    match disposition {
1650        LiabilityQuoteDisposition::Quoted => "quoted",
1651        LiabilityQuoteDisposition::Declined => "declined",
1652    }
1653}
1654
1655pub(crate) fn liability_auto_bind_disposition_label(
1656    disposition: &LiabilityAutoBindDisposition,
1657) -> &'static str {
1658    match disposition {
1659        LiabilityAutoBindDisposition::AutoBound => "auto_bound",
1660        LiabilityAutoBindDisposition::ManualReview => "manual_review",
1661        LiabilityAutoBindDisposition::Denied => "denied",
1662    }
1663}
1664
1665pub(crate) fn query_underwriting_appeal(
1666    tx: &rusqlite::Transaction<'_>,
1667    appeal_id: &str,
1668) -> Result<Option<UnderwritingAppealRecord>, ReceiptStoreError> {
1669    let row = tx
1670        .query_row(
1671            "SELECT decision_id, requested_by, reason, status, note, created_at, updated_at,
1672                resolved_by, replacement_decision_id
1673         FROM underwriting_appeals
1674         WHERE appeal_id = ?1",
1675            params![appeal_id],
1676            |row| {
1677                Ok((
1678                    row.get::<_, String>(0)?,
1679                    row.get::<_, String>(1)?,
1680                    row.get::<_, String>(2)?,
1681                    row.get::<_, String>(3)?,
1682                    row.get::<_, Option<String>>(4)?,
1683                    row.get::<_, i64>(5)?,
1684                    row.get::<_, i64>(6)?,
1685                    row.get::<_, Option<String>>(7)?,
1686                    row.get::<_, Option<String>>(8)?,
1687                ))
1688            },
1689        )
1690        .optional()
1691        .map_err(ReceiptStoreError::from)?;
1692
1693    row.map(
1694        |(
1695            decision_id,
1696            requested_by,
1697            reason,
1698            status,
1699            note,
1700            created_at,
1701            updated_at,
1702            resolved_by,
1703            replacement_decision_id,
1704        )| {
1705            Ok(UnderwritingAppealRecord {
1706                schema: chio_kernel::UNDERWRITING_APPEAL_SCHEMA.to_string(),
1707                appeal_id: appeal_id.to_string(),
1708                decision_id,
1709                requested_by,
1710                reason,
1711                status: parse_underwriting_appeal_status(&status)?,
1712                note,
1713                created_at: created_at.max(0) as u64,
1714                updated_at: updated_at.max(0) as u64,
1715                resolved_by,
1716                replacement_decision_id,
1717            })
1718        },
1719    )
1720    .transpose()
1721}
1722
1723pub(crate) fn parse_underwriting_appeal_status(
1724    value: &str,
1725) -> Result<UnderwritingAppealStatus, ReceiptStoreError> {
1726    serde_json::from_str(&format!("\"{value}\"")).map_err(ReceiptStoreError::from)
1727}
1728
1729pub(crate) fn load_underwriting_appeal_rows(
1730    connection: &Connection,
1731) -> Result<Vec<UnderwritingAppealRecord>, ReceiptStoreError> {
1732    let mut statement = connection.prepare(
1733        "SELECT appeal_id, decision_id, requested_by, reason, status, note, created_at,
1734                updated_at, resolved_by, replacement_decision_id
1735         FROM underwriting_appeals
1736         ORDER BY updated_at DESC, appeal_id DESC",
1737    )?;
1738    let rows = statement.query_map([], |row| {
1739        Ok((
1740            row.get::<_, String>(0)?,
1741            row.get::<_, String>(1)?,
1742            row.get::<_, String>(2)?,
1743            row.get::<_, String>(3)?,
1744            row.get::<_, String>(4)?,
1745            row.get::<_, Option<String>>(5)?,
1746            row.get::<_, i64>(6)?,
1747            row.get::<_, i64>(7)?,
1748            row.get::<_, Option<String>>(8)?,
1749            row.get::<_, Option<String>>(9)?,
1750        ))
1751    })?;
1752    rows.map(|row| {
1753        let (
1754            appeal_id,
1755            decision_id,
1756            requested_by,
1757            reason,
1758            status,
1759            note,
1760            created_at,
1761            updated_at,
1762            resolved_by,
1763            replacement_decision_id,
1764        ) = row.map_err(ReceiptStoreError::from)?;
1765        Ok(UnderwritingAppealRecord {
1766            schema: chio_kernel::UNDERWRITING_APPEAL_SCHEMA.to_string(),
1767            appeal_id,
1768            decision_id,
1769            requested_by,
1770            reason,
1771            status: parse_underwriting_appeal_status(&status)?,
1772            note,
1773            created_at: created_at.max(0) as u64,
1774            updated_at: updated_at.max(0) as u64,
1775            resolved_by,
1776            replacement_decision_id,
1777        })
1778    })
1779    .collect::<Result<Vec<_>, _>>()
1780}
1781
1782pub(crate) fn underwriting_decision_matches_query(
1783    decision: &SignedUnderwritingDecision,
1784    lifecycle_state: UnderwritingDecisionLifecycleState,
1785    latest_appeal_status: Option<UnderwritingAppealStatus>,
1786    query: &UnderwritingDecisionQuery,
1787) -> bool {
1788    let filters = &decision.body.evaluation.input.filters;
1789    let decision_id_matches = query
1790        .decision_id
1791        .as_deref()
1792        .is_none_or(|decision_id| decision.body.decision_id == decision_id);
1793    let capability_matches = query
1794        .capability_id
1795        .as_deref()
1796        .is_none_or(|capability_id| filters.capability_id.as_deref() == Some(capability_id));
1797    let subject_matches = query
1798        .agent_subject
1799        .as_deref()
1800        .is_none_or(|subject| filters.agent_subject.as_deref() == Some(subject));
1801    let tool_server_matches = query
1802        .tool_server
1803        .as_deref()
1804        .is_none_or(|tool_server| filters.tool_server.as_deref() == Some(tool_server));
1805    let tool_name_matches = query
1806        .tool_name
1807        .as_deref()
1808        .is_none_or(|tool_name| filters.tool_name.as_deref() == Some(tool_name));
1809    let outcome_matches = query
1810        .outcome
1811        .is_none_or(|outcome| decision.body.evaluation.outcome == outcome);
1812    let lifecycle_matches = query
1813        .lifecycle_state
1814        .is_none_or(|state| lifecycle_state == state);
1815    let appeal_matches = query
1816        .appeal_status
1817        .is_none_or(|status| latest_appeal_status == Some(status));
1818
1819    decision_id_matches
1820        && capability_matches
1821        && subject_matches
1822        && tool_server_matches
1823        && tool_name_matches
1824        && outcome_matches
1825        && lifecycle_matches
1826        && appeal_matches
1827}
1828
1829pub(crate) fn effective_credit_facility_lifecycle_state(
1830    facility: &SignedCreditFacility,
1831    persisted: CreditFacilityLifecycleState,
1832    now: u64,
1833) -> CreditFacilityLifecycleState {
1834    if persisted == CreditFacilityLifecycleState::Active && facility.body.expires_at <= now {
1835        CreditFacilityLifecycleState::Expired
1836    } else {
1837        persisted
1838    }
1839}
1840
1841pub(crate) fn effective_credit_bond_lifecycle_state(
1842    bond: &SignedCreditBond,
1843    persisted: CreditBondLifecycleState,
1844    now: u64,
1845) -> CreditBondLifecycleState {
1846    if persisted == CreditBondLifecycleState::Active && bond.body.expires_at <= now {
1847        CreditBondLifecycleState::Expired
1848    } else {
1849        persisted
1850    }
1851}
1852
1853pub(crate) fn credit_facility_matches_query(
1854    facility: &SignedCreditFacility,
1855    lifecycle_state: CreditFacilityLifecycleState,
1856    query: &CreditFacilityListQuery,
1857) -> bool {
1858    let filters = &facility.body.report.filters;
1859    let facility_id_matches = query
1860        .facility_id
1861        .as_deref()
1862        .is_none_or(|facility_id| facility.body.facility_id == facility_id);
1863    let capability_matches = query
1864        .capability_id
1865        .as_deref()
1866        .is_none_or(|capability_id| filters.capability_id.as_deref() == Some(capability_id));
1867    let subject_matches = query
1868        .agent_subject
1869        .as_deref()
1870        .is_none_or(|subject| filters.agent_subject.as_deref() == Some(subject));
1871    let tool_server_matches = query
1872        .tool_server
1873        .as_deref()
1874        .is_none_or(|tool_server| filters.tool_server.as_deref() == Some(tool_server));
1875    let tool_name_matches = query
1876        .tool_name
1877        .as_deref()
1878        .is_none_or(|tool_name| filters.tool_name.as_deref() == Some(tool_name));
1879    let disposition_matches = query
1880        .disposition
1881        .is_none_or(|disposition| facility.body.report.disposition == disposition);
1882    let lifecycle_matches = query
1883        .lifecycle_state
1884        .is_none_or(|state| lifecycle_state == state);
1885
1886    facility_id_matches
1887        && capability_matches
1888        && subject_matches
1889        && tool_server_matches
1890        && tool_name_matches
1891        && disposition_matches
1892        && lifecycle_matches
1893}
1894
1895pub(crate) fn credit_bond_matches_query(
1896    bond: &SignedCreditBond,
1897    lifecycle_state: CreditBondLifecycleState,
1898    query: &CreditBondListQuery,
1899) -> bool {
1900    let filters = &bond.body.report.filters;
1901    let bond_id_matches = query
1902        .bond_id
1903        .as_deref()
1904        .is_none_or(|bond_id| bond.body.bond_id == bond_id);
1905    let facility_id_matches = query.facility_id.as_deref().is_none_or(|facility_id| {
1906        bond.body.report.latest_facility_id.as_deref() == Some(facility_id)
1907    });
1908    let capability_matches = query
1909        .capability_id
1910        .as_deref()
1911        .is_none_or(|capability_id| filters.capability_id.as_deref() == Some(capability_id));
1912    let subject_matches = query
1913        .agent_subject
1914        .as_deref()
1915        .is_none_or(|subject| filters.agent_subject.as_deref() == Some(subject));
1916    let tool_server_matches = query
1917        .tool_server
1918        .as_deref()
1919        .is_none_or(|tool_server| filters.tool_server.as_deref() == Some(tool_server));
1920    let tool_name_matches = query
1921        .tool_name
1922        .as_deref()
1923        .is_none_or(|tool_name| filters.tool_name.as_deref() == Some(tool_name));
1924    let disposition_matches = query
1925        .disposition
1926        .is_none_or(|disposition| bond.body.report.disposition == disposition);
1927    let lifecycle_matches = query
1928        .lifecycle_state
1929        .is_none_or(|state| lifecycle_state == state);
1930
1931    bond_id_matches
1932        && facility_id_matches
1933        && capability_matches
1934        && subject_matches
1935        && tool_server_matches
1936        && tool_name_matches
1937        && disposition_matches
1938        && lifecycle_matches
1939}
1940
1941pub(crate) fn liability_provider_matches_query(
1942    provider: &SignedLiabilityProvider,
1943    lifecycle_state: LiabilityProviderLifecycleState,
1944    query: &LiabilityProviderListQuery,
1945) -> bool {
1946    let report = &provider.body.report;
1947    let provider_id_matches = query
1948        .provider_id
1949        .as_deref()
1950        .is_none_or(|provider_id| report.provider_id == provider_id);
1951    let lifecycle_matches = query
1952        .lifecycle_state
1953        .is_none_or(|state| lifecycle_state == state);
1954    let jurisdiction_matches = query.jurisdiction.as_deref().is_none_or(|jurisdiction| {
1955        report
1956            .policies
1957            .iter()
1958            .any(|policy| policy.jurisdiction.eq_ignore_ascii_case(jurisdiction))
1959    });
1960    let coverage_matches = query.coverage_class.is_none_or(|coverage_class| {
1961        report
1962            .policies
1963            .iter()
1964            .any(|policy| policy.coverage_classes.contains(&coverage_class))
1965    });
1966    let currency_matches = query.currency.as_deref().is_none_or(|currency| {
1967        report.policies.iter().any(|policy| {
1968            policy
1969                .supported_currencies
1970                .iter()
1971                .any(|candidate| candidate.eq_ignore_ascii_case(currency))
1972        })
1973    });
1974
1975    provider_id_matches
1976        && lifecycle_matches
1977        && jurisdiction_matches
1978        && coverage_matches
1979        && currency_matches
1980}
1981
1982pub(crate) fn liability_provider_policy_matches_resolution(
1983    policy: &chio_kernel::LiabilityJurisdictionPolicy,
1984    query: &LiabilityProviderResolutionQuery,
1985) -> bool {
1986    policy
1987        .jurisdiction
1988        .eq_ignore_ascii_case(&query.jurisdiction)
1989        && policy.coverage_classes.contains(&query.coverage_class)
1990        && policy
1991            .supported_currencies
1992            .iter()
1993            .any(|currency| currency.eq_ignore_ascii_case(&query.currency))
1994}
1995
1996pub(crate) fn liability_market_workflow_matches_query(
1997    quote_request: &SignedLiabilityQuoteRequest,
1998    query: &LiabilityMarketWorkflowQuery,
1999) -> bool {
2000    let request = &quote_request.body;
2001    let quote_request_id_matches = query
2002        .quote_request_id
2003        .as_deref()
2004        .is_none_or(|quote_request_id| request.quote_request_id == quote_request_id);
2005    let provider_id_matches = query
2006        .provider_id
2007        .as_deref()
2008        .is_none_or(|provider_id| request.provider_policy.provider_id == provider_id);
2009    let subject_matches = query
2010        .agent_subject
2011        .as_deref()
2012        .is_none_or(|subject| request.risk_package.body.subject_key == subject);
2013    let jurisdiction_matches = query.jurisdiction.as_deref().is_none_or(|jurisdiction| {
2014        request
2015            .provider_policy
2016            .jurisdiction
2017            .eq_ignore_ascii_case(jurisdiction)
2018    });
2019    let coverage_matches = query
2020        .coverage_class
2021        .is_none_or(|coverage_class| request.provider_policy.coverage_class == coverage_class);
2022    let currency_matches = query.currency.as_deref().is_none_or(|currency| {
2023        request
2024            .requested_coverage_amount
2025            .currency
2026            .eq_ignore_ascii_case(currency)
2027    });
2028
2029    quote_request_id_matches
2030        && provider_id_matches
2031        && subject_matches
2032        && jurisdiction_matches
2033        && coverage_matches
2034        && currency_matches
2035}
2036
2037pub(crate) fn liability_claim_workflow_matches_query(
2038    claim: &SignedLiabilityClaimPackage,
2039    query: &LiabilityClaimWorkflowQuery,
2040) -> bool {
2041    let claim_body = &claim.body;
2042    let provider_policy = &claim_body
2043        .bound_coverage
2044        .body
2045        .placement
2046        .body
2047        .quote_response
2048        .body
2049        .quote_request
2050        .body
2051        .provider_policy;
2052    let claim_id_matches = query
2053        .claim_id
2054        .as_deref()
2055        .is_none_or(|claim_id| claim_body.claim_id == claim_id);
2056    let provider_id_matches = query
2057        .provider_id
2058        .as_deref()
2059        .is_none_or(|provider_id| provider_policy.provider_id == provider_id);
2060    let subject_matches = query.agent_subject.as_deref().is_none_or(|subject| {
2061        claim_body
2062            .bound_coverage
2063            .body
2064            .placement
2065            .body
2066            .quote_response
2067            .body
2068            .quote_request
2069            .body
2070            .risk_package
2071            .body
2072            .subject_key
2073            == subject
2074    });
2075    let jurisdiction_matches = query.jurisdiction.as_deref().is_none_or(|jurisdiction| {
2076        provider_policy
2077            .jurisdiction
2078            .eq_ignore_ascii_case(jurisdiction)
2079    });
2080    let policy_number_matches = query
2081        .policy_number
2082        .as_deref()
2083        .is_none_or(|policy_number| claim_body.bound_coverage.body.policy_number == policy_number);
2084
2085    claim_id_matches
2086        && provider_id_matches
2087        && subject_matches
2088        && jurisdiction_matches
2089        && policy_number_matches
2090}
2091
2092pub(crate) fn unix_now() -> u64 {
2093    match SystemTime::now().duration_since(UNIX_EPOCH) {
2094        Ok(duration) => duration.as_secs(),
2095        Err(_) => 0,
2096    }
2097}
2098
2099pub(crate) fn parse_settlement_status(value: &str) -> Result<SettlementStatus, ReceiptStoreError> {
2100    serde_json::from_str(&format!("\"{value}\"")).map_err(ReceiptStoreError::from)
2101}
2102
2103pub(crate) fn settlement_reconciliation_action_required(
2104    settlement_status: SettlementStatus,
2105    reconciliation_state: SettlementReconciliationState,
2106) -> bool {
2107    matches!(
2108        settlement_status,
2109        SettlementStatus::Pending | SettlementStatus::Failed
2110    ) && !matches!(
2111        reconciliation_state,
2112        SettlementReconciliationState::Reconciled | SettlementReconciliationState::Ignored
2113    )
2114}
2115
2116pub(crate) fn metered_billing_evidence_record_from_columns(
2117    adapter_kind: Option<String>,
2118    evidence_id: Option<String>,
2119    observed_units: Option<i64>,
2120    billed_cost_units: Option<i64>,
2121    billed_cost_currency: Option<String>,
2122    evidence_sha256: Option<String>,
2123    recorded_at: Option<i64>,
2124) -> Option<MeteredBillingEvidenceRecord> {
2125    let (
2126        Some(adapter_kind),
2127        Some(evidence_id),
2128        Some(observed_units),
2129        Some(billed_cost_units),
2130        Some(billed_cost_currency),
2131        Some(recorded_at),
2132    ) = (
2133        adapter_kind,
2134        evidence_id,
2135        observed_units,
2136        billed_cost_units,
2137        billed_cost_currency,
2138        recorded_at,
2139    )
2140    else {
2141        return None;
2142    };
2143
2144    Some(MeteredBillingEvidenceRecord {
2145        usage_evidence: chio_core::receipt::MeteredUsageEvidenceReceiptMetadata {
2146            evidence_kind: adapter_kind,
2147            evidence_id,
2148            observed_units: observed_units.max(0) as u64,
2149            evidence_sha256,
2150        },
2151        billed_cost: chio_core::capability::MonetaryAmount {
2152            units: billed_cost_units.max(0) as u64,
2153            currency: billed_cost_currency,
2154        },
2155        recorded_at: recorded_at.max(0) as u64,
2156    })
2157}
2158
2159pub(crate) struct MeteredBillingReconciliationAnalysis {
2160    pub(crate) evidence_missing: bool,
2161    pub(crate) exceeds_quoted_units: bool,
2162    pub(crate) exceeds_max_billed_units: bool,
2163    pub(crate) exceeds_quoted_cost: bool,
2164    pub(crate) financial_mismatch: bool,
2165    pub(crate) action_required: bool,
2166}
2167
2168pub(crate) fn analyze_metered_billing_reconciliation(
2169    metered: &chio_core::receipt::MeteredBillingReceiptMetadata,
2170    financial: Option<&FinancialReceiptMetadata>,
2171    evidence: Option<&MeteredBillingEvidenceRecord>,
2172    reconciliation_state: MeteredBillingReconciliationState,
2173) -> MeteredBillingReconciliationAnalysis {
2174    let evidence_missing = evidence.is_none();
2175    let exceeds_quoted_units = evidence
2176        .is_some_and(|record| record.usage_evidence.observed_units > metered.quote.quoted_units);
2177    let exceeds_max_billed_units = evidence.is_some_and(|record| {
2178        metered
2179            .max_billed_units
2180            .is_some_and(|max_units| record.usage_evidence.observed_units > max_units)
2181    });
2182    let exceeds_quoted_cost = evidence.is_some_and(|record| {
2183        record.billed_cost.currency != metered.quote.quoted_cost.currency
2184            || record.billed_cost.units > metered.quote.quoted_cost.units
2185    });
2186    let financial_mismatch = evidence.is_some_and(|record| {
2187        financial.is_some_and(|financial| {
2188            record.billed_cost.currency != financial.currency
2189                || record.billed_cost.units != financial.cost_charged
2190        })
2191    });
2192    let action_required = (evidence_missing
2193        || exceeds_quoted_units
2194        || exceeds_max_billed_units
2195        || exceeds_quoted_cost
2196        || financial_mismatch)
2197        && !matches!(
2198            reconciliation_state,
2199            MeteredBillingReconciliationState::Reconciled
2200                | MeteredBillingReconciliationState::Ignored
2201        );
2202
2203    MeteredBillingReconciliationAnalysis {
2204        evidence_missing,
2205        exceeds_quoted_units,
2206        exceeds_max_billed_units,
2207        exceeds_quoted_cost,
2208        financial_mismatch,
2209        action_required,
2210    }
2211}
2212
2213#[derive(Default)]
2214pub(crate) struct RootAggregate {
2215    pub(crate) receipt_count: u64,
2216    pub(crate) total_cost_charged: u64,
2217    pub(crate) total_attempted_cost: u64,
2218    pub(crate) max_delegation_depth: u64,
2219    pub(crate) leaf_subjects: BTreeSet<String>,
2220}
2221
2222#[derive(Default)]
2223pub(crate) struct LeafAggregate {
2224    pub(crate) receipt_count: u64,
2225    pub(crate) total_cost_charged: u64,
2226    pub(crate) total_attempted_cost: u64,
2227    pub(crate) max_delegation_depth: u64,
2228}
2229
2230#[derive(Default)]
2231pub(crate) struct ReceiptAttributionColumns {
2232    pub(crate) subject_key: Option<String>,
2233    pub(crate) issuer_key: Option<String>,
2234    pub(crate) grant_index: Option<u32>,
2235}
2236
2237pub(crate) fn extract_receipt_attribution(receipt: &ChioReceipt) -> ReceiptAttributionColumns {
2238    let Some(metadata) = receipt.metadata.as_ref() else {
2239        return ReceiptAttributionColumns::default();
2240    };
2241
2242    let attribution = metadata
2243        .get("attribution")
2244        .cloned()
2245        .and_then(|value| serde_json::from_value::<ReceiptAttributionMetadata>(value).ok());
2246    let grant_index = attribution
2247        .as_ref()
2248        .and_then(|value| value.grant_index)
2249        .or_else(|| {
2250            metadata
2251                .get("financial")
2252                .and_then(|value| value.get("grant_index"))
2253                .and_then(serde_json::Value::as_u64)
2254                .map(|value| value as u32)
2255        });
2256
2257    ReceiptAttributionColumns {
2258        subject_key: attribution.as_ref().map(|value| value.subject_key.clone()),
2259        issuer_key: attribution.as_ref().map(|value| value.issuer_key.clone()),
2260        grant_index,
2261    }
2262}
2263
2264pub(crate) fn extract_financial_metadata(
2265    receipt: &ChioReceipt,
2266) -> Option<FinancialReceiptMetadata> {
2267    receipt
2268        .metadata
2269        .as_ref()
2270        .and_then(|metadata| metadata.get("financial"))
2271        .cloned()
2272        .and_then(|value| serde_json::from_value::<FinancialReceiptMetadata>(value).ok())
2273}
2274
2275pub(crate) fn extract_governed_transaction_metadata(
2276    receipt: &ChioReceipt,
2277) -> Option<GovernedTransactionReceiptMetadata> {
2278    receipt
2279        .metadata
2280        .as_ref()
2281        .and_then(|metadata| metadata.get("governed_transaction"))
2282        .cloned()
2283        .and_then(|value| serde_json::from_value::<GovernedTransactionReceiptMetadata>(value).ok())
2284}
2285
2286pub(crate) fn extract_economic_authorization_metadata(
2287    receipt: &ChioReceipt,
2288) -> Option<chio_core::receipt::EconomicAuthorizationReceiptMetadata> {
2289    extract_governed_transaction_metadata(receipt)
2290        .and_then(|governed| governed.economic_authorization)
2291}
2292
2293pub(crate) fn authorization_details_from_governed_metadata(
2294    governed: &GovernedTransactionReceiptMetadata,
2295) -> Vec<GovernedAuthorizationDetail> {
2296    let mut details = vec![GovernedAuthorizationDetail {
2297        detail_type: CHIO_OAUTH_AUTHORIZATION_TOOL_DETAIL_TYPE.to_string(),
2298        locations: vec![governed.server_id.clone()],
2299        actions: vec![governed.tool_name.clone()],
2300        purpose: Some(governed.purpose.clone()),
2301        max_amount: governed.max_amount.clone(),
2302        commerce: None,
2303        metered_billing: None,
2304    }];
2305
2306    if let Some(commerce) = governed.commerce.as_ref() {
2307        details.push(GovernedAuthorizationDetail {
2308            detail_type: CHIO_OAUTH_AUTHORIZATION_COMMERCE_DETAIL_TYPE.to_string(),
2309            locations: Vec::new(),
2310            actions: Vec::new(),
2311            purpose: None,
2312            max_amount: governed.max_amount.clone(),
2313            commerce: Some(GovernedAuthorizationCommerceDetail {
2314                seller: commerce.seller.clone(),
2315                shared_payment_token_id: commerce.shared_payment_token_id.clone(),
2316            }),
2317            metered_billing: None,
2318        });
2319    }
2320
2321    if let Some(metered) = governed.metered_billing.as_ref() {
2322        details.push(GovernedAuthorizationDetail {
2323            detail_type: CHIO_OAUTH_AUTHORIZATION_METERED_BILLING_DETAIL_TYPE.to_string(),
2324            locations: Vec::new(),
2325            actions: Vec::new(),
2326            purpose: None,
2327            max_amount: None,
2328            commerce: None,
2329            metered_billing: Some(GovernedAuthorizationMeteredBillingDetail {
2330                settlement_mode: metered.settlement_mode,
2331                provider: metered.quote.provider.clone(),
2332                quote_id: metered.quote.quote_id.clone(),
2333                billing_unit: metered.quote.billing_unit.clone(),
2334                quoted_units: metered.quote.quoted_units,
2335                quoted_cost: metered.quote.quoted_cost.clone(),
2336                max_billed_units: metered.max_billed_units,
2337            }),
2338        });
2339    }
2340
2341    details
2342}
2343
2344pub(crate) fn authorization_transaction_context_from_governed_metadata(
2345    governed: &GovernedTransactionReceiptMetadata,
2346) -> GovernedAuthorizationTransactionContext {
2347    GovernedAuthorizationTransactionContext {
2348        intent_id: governed.intent_id.clone(),
2349        intent_hash: governed.intent_hash.clone(),
2350        approval_token_id: governed
2351            .approval
2352            .as_ref()
2353            .map(|value| value.token_id.clone()),
2354        approval_approved: governed.approval.as_ref().map(|value| value.approved),
2355        approver_key: governed
2356            .approval
2357            .as_ref()
2358            .map(|value| value.approver_key.clone()),
2359        runtime_assurance_tier: governed.runtime_assurance.as_ref().map(|value| value.tier),
2360        runtime_assurance_schema: governed
2361            .runtime_assurance
2362            .as_ref()
2363            .map(|value| value.schema.clone()),
2364        runtime_assurance_verifier_family: governed
2365            .runtime_assurance
2366            .as_ref()
2367            .and_then(|value| value.verifier_family),
2368        runtime_assurance_verifier: governed
2369            .runtime_assurance
2370            .as_ref()
2371            .map(|value| value.verifier.clone()),
2372        runtime_assurance_evidence_sha256: governed
2373            .runtime_assurance
2374            .as_ref()
2375            .map(|value| value.evidence_sha256.clone()),
2376        call_chain: governed.call_chain.clone(),
2377        identity_assertion: None,
2378    }
2379}
2380
2381fn delegated_call_chain_is_sender_bound(
2382    call_chain: Option<&chio_core::capability::GovernedCallChainProvenance>,
2383) -> bool {
2384    let Some(call_chain) = call_chain else {
2385        return false;
2386    };
2387    if call_chain.evidence_class == chio_core::capability::GovernedProvenanceEvidenceClass::Asserted
2388    {
2389        return false;
2390    }
2391
2392    let has_local_lineage_link = call_chain.evidence_sources.iter().any(|source| {
2393        matches!(
2394            source,
2395            chio_core::capability::GovernedCallChainEvidenceSource::SessionParentRequestLineage
2396                | chio_core::capability::GovernedCallChainEvidenceSource::LocalParentReceiptLinkage
2397                | chio_core::capability::GovernedCallChainEvidenceSource::UpstreamDelegatorProof
2398        )
2399    });
2400    let has_capability_subject_binding = call_chain.evidence_sources.iter().any(|source| {
2401        matches!(
2402            source,
2403            chio_core::capability::GovernedCallChainEvidenceSource::CapabilityDelegatorSubject
2404                | chio_core::capability::GovernedCallChainEvidenceSource::CapabilityOriginSubject
2405        )
2406    });
2407
2408    has_local_lineage_link
2409        || (call_chain.evidence_class
2410            == chio_core::capability::GovernedProvenanceEvidenceClass::Verified
2411            && has_capability_subject_binding)
2412}
2413
2414pub(crate) fn resolve_sender_constraint_subject_key(
2415    receipt_id: &str,
2416    receipt_subject_key: Option<&str>,
2417    lineage_subject_key: Option<&str>,
2418) -> Result<(String, String), ReceiptStoreError> {
2419    match (receipt_subject_key, lineage_subject_key) {
2420        (Some(receipt_key), Some(lineage_key)) => {
2421            ensure_non_empty_profile_value(receipt_id, "senderConstraint.subjectKey", receipt_key)?;
2422            ensure_non_empty_profile_value(receipt_id, "capabilitySnapshot.subjectKey", lineage_key)?;
2423            if receipt_key != lineage_key {
2424                return Err(invalid_chio_oauth_authorization_profile(
2425                    receipt_id,
2426                    format!(
2427                        "senderConstraint.subjectKey `{receipt_key}` does not match capability snapshot subject `{lineage_key}`"
2428                    ),
2429                ));
2430            }
2431            Ok((receipt_key.to_string(), "receipt_attribution".to_string()))
2432        }
2433        (Some(receipt_key), None) => {
2434            ensure_non_empty_profile_value(receipt_id, "senderConstraint.subjectKey", receipt_key)?;
2435            Ok((receipt_key.to_string(), "receipt_attribution".to_string()))
2436        }
2437        (None, Some(lineage_key)) => {
2438            ensure_non_empty_profile_value(receipt_id, "senderConstraint.subjectKey", lineage_key)?;
2439            Ok((lineage_key.to_string(), "capability_snapshot".to_string()))
2440        }
2441        (None, None) => Err(invalid_chio_oauth_authorization_profile(
2442            receipt_id,
2443            "sender-constrained profile requires a bound subjectKey from receipt attribution or capability snapshot",
2444        )),
2445    }
2446}
2447
2448pub(crate) fn resolve_sender_constraint_issuer_key(
2449    receipt_id: &str,
2450    receipt_issuer_key: Option<&str>,
2451    lineage_issuer_key: Option<&str>,
2452) -> Result<(String, String), ReceiptStoreError> {
2453    match (receipt_issuer_key, lineage_issuer_key) {
2454        (Some(receipt_key), Some(lineage_key)) => {
2455            ensure_non_empty_profile_value(receipt_id, "senderConstraint.issuerKey", receipt_key)?;
2456            ensure_non_empty_profile_value(receipt_id, "capabilitySnapshot.issuerKey", lineage_key)?;
2457            if receipt_key != lineage_key {
2458                return Err(invalid_chio_oauth_authorization_profile(
2459                    receipt_id,
2460                    format!(
2461                        "senderConstraint.issuerKey `{receipt_key}` does not match capability snapshot issuer `{lineage_key}`"
2462                    ),
2463                ));
2464            }
2465            Ok((receipt_key.to_string(), "receipt_attribution".to_string()))
2466        }
2467        (Some(receipt_key), None) => {
2468            ensure_non_empty_profile_value(receipt_id, "senderConstraint.issuerKey", receipt_key)?;
2469            Ok((receipt_key.to_string(), "receipt_attribution".to_string()))
2470        }
2471        (None, Some(lineage_key)) => {
2472            ensure_non_empty_profile_value(receipt_id, "senderConstraint.issuerKey", lineage_key)?;
2473            Ok((lineage_key.to_string(), "capability_snapshot".to_string()))
2474        }
2475        (None, None) => Err(invalid_chio_oauth_authorization_profile(
2476            receipt_id,
2477            "sender-constrained profile requires a bound issuerKey from receipt attribution or capability snapshot",
2478        )),
2479    }
2480}
2481
2482pub(crate) fn resolve_sender_constraint_grant(
2483    receipt_id: &str,
2484    tool_server: &str,
2485    tool_name: &str,
2486    grant_index: Option<u32>,
2487    grants_json: Option<&str>,
2488) -> Result<(u32, bool), ReceiptStoreError> {
2489    let grants_json = grants_json.ok_or_else(|| {
2490        invalid_chio_oauth_authorization_profile(
2491            receipt_id,
2492            "sender-constrained profile requires capability snapshot grants_json",
2493        )
2494    })?;
2495    let scope: ChioScope = serde_json::from_str(grants_json).map_err(|error| {
2496        invalid_chio_oauth_authorization_profile(
2497            receipt_id,
2498            format!("invalid capability snapshot grants_json: {error}"),
2499        )
2500    })?;
2501
2502    if let Some(index) = grant_index {
2503        let grant = scope.grants.get(index as usize).ok_or_else(|| {
2504            invalid_chio_oauth_authorization_profile(
2505                receipt_id,
2506                format!("matched grant_index `{index}` is outside the capability scope"),
2507            )
2508        })?;
2509        if grant.server_id != tool_server || grant.tool_name != tool_name {
2510            return Err(invalid_chio_oauth_authorization_profile(
2511                receipt_id,
2512                format!(
2513                    "grant_index `{index}` resolves to {}/{} instead of {tool_server}/{tool_name}",
2514                    grant.server_id, grant.tool_name
2515                ),
2516            ));
2517        }
2518        return Ok((index, grant.dpop_required == Some(true)));
2519    }
2520
2521    let mut matches = scope
2522        .grants
2523        .iter()
2524        .enumerate()
2525        .filter(|(_, grant)| grant.server_id == tool_server && grant.tool_name == tool_name);
2526    let Some((index, grant)) = matches.next() else {
2527        return Err(invalid_chio_oauth_authorization_profile(
2528            receipt_id,
2529            format!("capability snapshot does not contain a grant for {tool_server}/{tool_name}"),
2530        ));
2531    };
2532    if matches.next().is_some() {
2533        return Err(invalid_chio_oauth_authorization_profile(
2534            receipt_id,
2535            format!(
2536                "capability snapshot contains multiple grants for {tool_server}/{tool_name}; grant_index is required"
2537            ),
2538        ));
2539    }
2540    Ok((index as u32, grant.dpop_required == Some(true)))
2541}
2542
2543pub(crate) struct AuthorizationSenderConstraintArgs<'a> {
2544    pub(crate) tool_server: &'a str,
2545    pub(crate) tool_name: &'a str,
2546    pub(crate) receipt_subject_key: Option<&'a str>,
2547    pub(crate) receipt_issuer_key: Option<&'a str>,
2548    pub(crate) lineage_subject_key: Option<&'a str>,
2549    pub(crate) lineage_issuer_key: Option<&'a str>,
2550    pub(crate) grant_index: Option<u32>,
2551    pub(crate) grants_json: Option<&'a str>,
2552}
2553
2554pub(crate) fn derive_authorization_sender_constraint(
2555    receipt_id: &str,
2556    args: AuthorizationSenderConstraintArgs<'_>,
2557    transaction_context: &GovernedAuthorizationTransactionContext,
2558) -> Result<AuthorizationContextSenderConstraint, ReceiptStoreError> {
2559    let AuthorizationSenderConstraintArgs {
2560        tool_server,
2561        tool_name,
2562        receipt_subject_key,
2563        receipt_issuer_key,
2564        lineage_subject_key,
2565        lineage_issuer_key,
2566        grant_index,
2567        grants_json,
2568    } = args;
2569    let (subject_key, subject_key_source) = resolve_sender_constraint_subject_key(
2570        receipt_id,
2571        receipt_subject_key,
2572        lineage_subject_key,
2573    )?;
2574    let (issuer_key, issuer_key_source) =
2575        resolve_sender_constraint_issuer_key(receipt_id, receipt_issuer_key, lineage_issuer_key)?;
2576    let (matched_grant_index, proof_required) = resolve_sender_constraint_grant(
2577        receipt_id,
2578        tool_server,
2579        tool_name,
2580        grant_index,
2581        grants_json,
2582    )?;
2583
2584    Ok(AuthorizationContextSenderConstraint {
2585        subject_key,
2586        subject_key_source,
2587        issuer_key,
2588        issuer_key_source,
2589        matched_grant_index,
2590        proof_required,
2591        proof_type: proof_required.then(|| CHIO_OAUTH_SENDER_PROOF_CHIO_DPOP.to_string()),
2592        proof_schema: proof_required.then(|| DPOP_SCHEMA.to_string()),
2593        runtime_assurance_bound: transaction_context.runtime_assurance_tier.is_some(),
2594        delegated_call_chain_bound: delegated_call_chain_is_sender_bound(
2595            transaction_context.call_chain.as_ref(),
2596        ),
2597    })
2598}
2599
2600pub(crate) fn invalid_chio_oauth_authorization_profile(
2601    receipt_id: &str,
2602    detail: impl AsRef<str>,
2603) -> ReceiptStoreError {
2604    ReceiptStoreError::Canonical(format!(
2605        "receipt {receipt_id} violates Chio OAuth authorization profile: {}",
2606        detail.as_ref()
2607    ))
2608}
2609
2610pub(crate) fn ensure_non_empty_profile_value(
2611    receipt_id: &str,
2612    field: &str,
2613    value: &str,
2614) -> Result<(), ReceiptStoreError> {
2615    if value.trim().is_empty() {
2616        return Err(invalid_chio_oauth_authorization_profile(
2617            receipt_id,
2618            format!("{field} must not be empty"),
2619        ));
2620    }
2621    Ok(())
2622}
2623
2624pub(crate) fn validate_chio_oauth_authorization_detail(
2625    receipt_id: &str,
2626    detail: &GovernedAuthorizationDetail,
2627) -> Result<bool, ReceiptStoreError> {
2628    match detail.detail_type.as_str() {
2629        CHIO_OAUTH_AUTHORIZATION_TOOL_DETAIL_TYPE => {
2630            if detail.locations.is_empty() {
2631                return Err(invalid_chio_oauth_authorization_profile(
2632                    receipt_id,
2633                    "chio_governed_tool must include at least one location",
2634                ));
2635            }
2636            if detail.actions.is_empty() {
2637                return Err(invalid_chio_oauth_authorization_profile(
2638                    receipt_id,
2639                    "chio_governed_tool must include at least one action",
2640                ));
2641            }
2642            for location in &detail.locations {
2643                ensure_non_empty_profile_value(
2644                    receipt_id,
2645                    "authorizationDetails.locations[]",
2646                    location,
2647                )?;
2648            }
2649            for action in &detail.actions {
2650                ensure_non_empty_profile_value(
2651                    receipt_id,
2652                    "authorizationDetails.actions[]",
2653                    action,
2654                )?;
2655            }
2656            if detail.commerce.is_some() || detail.metered_billing.is_some() {
2657                return Err(invalid_chio_oauth_authorization_profile(
2658                    receipt_id,
2659                    "chio_governed_tool must not carry commerce or meteredBilling sidecars",
2660                ));
2661            }
2662            Ok(true)
2663        }
2664        CHIO_OAUTH_AUTHORIZATION_COMMERCE_DETAIL_TYPE => {
2665            let Some(commerce) = detail.commerce.as_ref() else {
2666                return Err(invalid_chio_oauth_authorization_profile(
2667                    receipt_id,
2668                    "chio_governed_commerce must include commerce detail",
2669                ));
2670            };
2671            ensure_non_empty_profile_value(
2672                receipt_id,
2673                "authorizationDetails.commerce.seller",
2674                &commerce.seller,
2675            )?;
2676            ensure_non_empty_profile_value(
2677                receipt_id,
2678                "authorizationDetails.commerce.sharedPaymentTokenId",
2679                &commerce.shared_payment_token_id,
2680            )?;
2681            if detail.metered_billing.is_some() {
2682                return Err(invalid_chio_oauth_authorization_profile(
2683                    receipt_id,
2684                    "chio_governed_commerce must not carry meteredBilling detail",
2685                ));
2686            }
2687            Ok(false)
2688        }
2689        CHIO_OAUTH_AUTHORIZATION_METERED_BILLING_DETAIL_TYPE => {
2690            let Some(metered) = detail.metered_billing.as_ref() else {
2691                return Err(invalid_chio_oauth_authorization_profile(
2692                    receipt_id,
2693                    "chio_governed_metered_billing must include meteredBilling detail",
2694                ));
2695            };
2696            ensure_non_empty_profile_value(
2697                receipt_id,
2698                "authorizationDetails.meteredBilling.provider",
2699                &metered.provider,
2700            )?;
2701            ensure_non_empty_profile_value(
2702                receipt_id,
2703                "authorizationDetails.meteredBilling.quoteId",
2704                &metered.quote_id,
2705            )?;
2706            ensure_non_empty_profile_value(
2707                receipt_id,
2708                "authorizationDetails.meteredBilling.billingUnit",
2709                &metered.billing_unit,
2710            )?;
2711            if detail.commerce.is_some() {
2712                return Err(invalid_chio_oauth_authorization_profile(
2713                    receipt_id,
2714                    "chio_governed_metered_billing must not carry commerce detail",
2715                ));
2716            }
2717            Ok(false)
2718        }
2719        unsupported => Err(invalid_chio_oauth_authorization_profile(
2720            receipt_id,
2721            format!("unsupported authorizationDetails.type `{unsupported}`"),
2722        )),
2723    }
2724}
2725
2726pub(crate) fn validate_chio_oauth_authorization_row(
2727    row: &AuthorizationContextRow,
2728) -> Result<(), ReceiptStoreError> {
2729    ensure_non_empty_profile_value(
2730        &row.receipt_id,
2731        "transactionContext.intentId",
2732        &row.transaction_context.intent_id,
2733    )?;
2734    ensure_non_empty_profile_value(
2735        &row.receipt_id,
2736        "transactionContext.intentHash",
2737        &row.transaction_context.intent_hash,
2738    )?;
2739
2740    let mut saw_tool_detail = false;
2741    for detail in &row.authorization_details {
2742        if validate_chio_oauth_authorization_detail(&row.receipt_id, detail)? {
2743            saw_tool_detail = true;
2744        }
2745    }
2746    if !saw_tool_detail {
2747        return Err(invalid_chio_oauth_authorization_profile(
2748            &row.receipt_id,
2749            "report must include one chio_governed_tool authorization detail",
2750        ));
2751    }
2752
2753    if let Some(token_id) = row.transaction_context.approval_token_id.as_deref() {
2754        ensure_non_empty_profile_value(
2755            &row.receipt_id,
2756            "transactionContext.approvalTokenId",
2757            token_id,
2758        )?;
2759        let approver_key = row
2760            .transaction_context
2761            .approver_key
2762            .as_deref()
2763            .ok_or_else(|| {
2764                invalid_chio_oauth_authorization_profile(
2765                    &row.receipt_id,
2766                    "approvalTokenId requires approverKey",
2767                )
2768            })?;
2769        ensure_non_empty_profile_value(
2770            &row.receipt_id,
2771            "transactionContext.approverKey",
2772            approver_key,
2773        )?;
2774        if row.transaction_context.approval_approved.is_none() {
2775            return Err(invalid_chio_oauth_authorization_profile(
2776                &row.receipt_id,
2777                "approvalTokenId requires approvalApproved",
2778            ));
2779        }
2780    }
2781
2782    if let Some(call_chain) = row.transaction_context.call_chain.as_ref() {
2783        ensure_non_empty_profile_value(
2784            &row.receipt_id,
2785            "transactionContext.callChain.chainId",
2786            &call_chain.chain_id,
2787        )?;
2788        ensure_non_empty_profile_value(
2789            &row.receipt_id,
2790            "transactionContext.callChain.parentRequestId",
2791            &call_chain.parent_request_id,
2792        )?;
2793        ensure_non_empty_profile_value(
2794            &row.receipt_id,
2795            "transactionContext.callChain.originSubject",
2796            &call_chain.origin_subject,
2797        )?;
2798        ensure_non_empty_profile_value(
2799            &row.receipt_id,
2800            "transactionContext.callChain.delegatorSubject",
2801            &call_chain.delegator_subject,
2802        )?;
2803        if let Some(parent_receipt_id) = call_chain.parent_receipt_id.as_deref() {
2804            ensure_non_empty_profile_value(
2805                &row.receipt_id,
2806                "transactionContext.callChain.parentReceiptId",
2807                parent_receipt_id,
2808            )?;
2809        }
2810        if row.sender_constraint.delegated_call_chain_bound
2811            && !delegated_call_chain_is_sender_bound(Some(call_chain))
2812        {
2813            return Err(invalid_chio_oauth_authorization_profile(
2814                &row.receipt_id,
2815                "senderConstraint.delegatedCallChainBound requires corroborated call-chain provenance",
2816            ));
2817        }
2818    }
2819
2820    if row.transaction_context.runtime_assurance_tier.is_some() {
2821        let runtime_assurance_schema = row
2822            .transaction_context
2823            .runtime_assurance_schema
2824            .as_deref()
2825            .ok_or_else(|| {
2826                invalid_chio_oauth_authorization_profile(
2827                    &row.receipt_id,
2828                    "runtimeAssuranceTier requires runtimeAssuranceSchema",
2829                )
2830            })?;
2831        ensure_non_empty_profile_value(
2832            &row.receipt_id,
2833            "transactionContext.runtimeAssuranceSchema",
2834            runtime_assurance_schema,
2835        )?;
2836        row.transaction_context
2837            .runtime_assurance_verifier_family
2838            .ok_or_else(|| {
2839                invalid_chio_oauth_authorization_profile(
2840                    &row.receipt_id,
2841                    "runtimeAssuranceTier requires runtimeAssuranceVerifierFamily",
2842                )
2843            })?;
2844        let runtime_assurance_verifier = row
2845            .transaction_context
2846            .runtime_assurance_verifier
2847            .as_deref()
2848            .ok_or_else(|| {
2849                invalid_chio_oauth_authorization_profile(
2850                    &row.receipt_id,
2851                    "runtimeAssuranceTier requires runtimeAssuranceVerifier",
2852                )
2853            })?;
2854        ensure_non_empty_profile_value(
2855            &row.receipt_id,
2856            "transactionContext.runtimeAssuranceVerifier",
2857            runtime_assurance_verifier,
2858        )?;
2859        let runtime_assurance_evidence_sha256 = row
2860            .transaction_context
2861            .runtime_assurance_evidence_sha256
2862            .as_deref()
2863            .ok_or_else(|| {
2864                invalid_chio_oauth_authorization_profile(
2865                    &row.receipt_id,
2866                    "runtimeAssuranceTier requires runtimeAssuranceEvidenceSha256",
2867                )
2868            })?;
2869        ensure_non_empty_profile_value(
2870            &row.receipt_id,
2871            "transactionContext.runtimeAssuranceEvidenceSha256",
2872            runtime_assurance_evidence_sha256,
2873        )?;
2874    }
2875
2876    ensure_non_empty_profile_value(
2877        &row.receipt_id,
2878        "senderConstraint.subjectKey",
2879        &row.sender_constraint.subject_key,
2880    )?;
2881    if row.subject_key.as_deref() != Some(row.sender_constraint.subject_key.as_str()) {
2882        return Err(invalid_chio_oauth_authorization_profile(
2883            &row.receipt_id,
2884            "row subjectKey must match senderConstraint.subjectKey",
2885        ));
2886    }
2887    ensure_non_empty_profile_value(
2888        &row.receipt_id,
2889        "senderConstraint.subjectKeySource",
2890        &row.sender_constraint.subject_key_source,
2891    )?;
2892    ensure_non_empty_profile_value(
2893        &row.receipt_id,
2894        "senderConstraint.issuerKey",
2895        &row.sender_constraint.issuer_key,
2896    )?;
2897    ensure_non_empty_profile_value(
2898        &row.receipt_id,
2899        "senderConstraint.issuerKeySource",
2900        &row.sender_constraint.issuer_key_source,
2901    )?;
2902    if row.sender_constraint.proof_required {
2903        let proof_type = row.sender_constraint.proof_type.as_deref().ok_or_else(|| {
2904            invalid_chio_oauth_authorization_profile(
2905                &row.receipt_id,
2906                "proofRequired requires senderConstraint.proofType",
2907            )
2908        })?;
2909        ensure_non_empty_profile_value(&row.receipt_id, "senderConstraint.proofType", proof_type)?;
2910        let proof_schema = row
2911            .sender_constraint
2912            .proof_schema
2913            .as_deref()
2914            .ok_or_else(|| {
2915                invalid_chio_oauth_authorization_profile(
2916                    &row.receipt_id,
2917                    "proofRequired requires senderConstraint.proofSchema",
2918                )
2919            })?;
2920        ensure_non_empty_profile_value(
2921            &row.receipt_id,
2922            "senderConstraint.proofSchema",
2923            proof_schema,
2924        )?;
2925    }
2926
2927    Ok(())
2928}
2929
2930pub(crate) fn chain_is_complete(
2931    capability_id: &str,
2932    chain: &[chio_kernel::CapabilitySnapshot],
2933) -> bool {
2934    if chain.is_empty() {
2935        return false;
2936    }
2937    let Some(leaf) = chain.last() else {
2938        return false;
2939    };
2940    if leaf.capability_id != capability_id {
2941        return false;
2942    }
2943    if chain
2944        .first()
2945        .and_then(|snapshot| snapshot.parent_capability_id.as_ref())
2946        .is_some()
2947    {
2948        return false;
2949    }
2950    if chain.windows(2).any(|window| {
2951        window[1].parent_capability_id.as_deref() != Some(window[0].capability_id.as_str())
2952    }) {
2953        return false;
2954    }
2955    if leaf.parent_capability_id.is_some() && chain.len() == 1 {
2956        return false;
2957    }
2958    if leaf.delegation_depth as usize != chain.len().saturating_sub(1) {
2959        return false;
2960    }
2961    true
2962}
2963
2964pub(crate) fn ratio_option(numerator: u64, denominator: u64) -> Option<f64> {
2965    if denominator == 0 {
2966        None
2967    } else {
2968        Some(numerator as f64 / denominator as f64)
2969    }
2970}
2971
2972pub(crate) fn compliance_export_scope_note(
2973    query: &OperatorReportQuery,
2974    export_query: &EvidenceExportQuery,
2975) -> Option<String> {
2976    let mut notes = Vec::new();
2977
2978    if !query.direct_evidence_export_supported() {
2979        notes.push(
2980            "tool filters narrow the operator report only; direct evidence export can scope by capability, agent, and time window".to_string(),
2981        );
2982    }
2983
2984    match export_query.child_receipt_scope() {
2985        EvidenceChildReceiptScope::TimeWindowContextOnly => notes.push(
2986            "child receipts are included only as time-window context for this export scope".to_string(),
2987        ),
2988        EvidenceChildReceiptScope::OmittedNoJoinPath => notes.push(
2989            "child receipts are omitted for this export scope because no capability/agent join exists yet".to_string(),
2990        ),
2991        EvidenceChildReceiptScope::FullQueryWindow => {}
2992    }
2993
2994    if notes.is_empty() {
2995        None
2996    } else {
2997        Some(notes.join(" "))
2998    }
2999}
3000
3001pub(crate) fn ensure_tool_receipt_attribution_columns(
3002    connection: &Connection,
3003) -> Result<(), ReceiptStoreError> {
3004    let mut statement = connection.prepare("PRAGMA table_info(chio_tool_receipts)")?;
3005    let columns = statement.query_map([], |row| row.get::<_, String>(1))?;
3006    let columns = columns.collect::<Result<Vec<_>, _>>()?;
3007
3008    if !columns.iter().any(|column| column == "subject_key") {
3009        connection.execute(
3010            "ALTER TABLE chio_tool_receipts ADD COLUMN subject_key TEXT",
3011            [],
3012        )?;
3013    }
3014    if !columns.iter().any(|column| column == "issuer_key") {
3015        connection.execute(
3016            "ALTER TABLE chio_tool_receipts ADD COLUMN issuer_key TEXT",
3017            [],
3018        )?;
3019    }
3020    if !columns.iter().any(|column| column == "grant_index") {
3021        connection.execute(
3022            "ALTER TABLE chio_tool_receipts ADD COLUMN grant_index INTEGER",
3023            [],
3024        )?;
3025    }
3026
3027    // Phase 1.5 multi-tenant receipt isolation: tenant_id column.
3028    //
3029    // Pre-multitenant receipts migrate to NULL, which the
3030    // tenant-scoped WHERE clause treats as a "public" fallback set (a
3031    // tenant A query returns its own rows AND the NULL-tagged legacy
3032    // set), so historical data remains visible under query modes that
3033    // opt into backward compatibility. Operators that need strict
3034    // isolation across the legacy set can enable
3035    // [`SqliteReceiptStore::with_strict_tenant_isolation`].
3036    //
3037    // Migration fails closed: if the column cannot be added we bail
3038    // out and the caller treats the store as unreadable, per the
3039    // kernel's fail-closed convention.
3040    if !columns.iter().any(|column| column == "tenant_id") {
3041        connection.execute(
3042            "ALTER TABLE chio_tool_receipts ADD COLUMN tenant_id TEXT",
3043            [],
3044        )?;
3045    }
3046
3047    connection.execute(
3048        "CREATE INDEX IF NOT EXISTS idx_chio_tool_receipts_subject ON chio_tool_receipts(subject_key)",
3049        [],
3050    )?;
3051    connection.execute(
3052        "CREATE INDEX IF NOT EXISTS idx_chio_tool_receipts_grant ON chio_tool_receipts(capability_id, grant_index)",
3053        [],
3054    )?;
3055    connection.execute(
3056        "CREATE INDEX IF NOT EXISTS idx_chio_tool_receipts_tenant ON chio_tool_receipts(tenant_id)",
3057        [],
3058    )?;
3059    Ok(())
3060}
3061
3062pub(crate) fn ensure_receipt_lineage_statement_columns(
3063    connection: &Connection,
3064) -> Result<(), ReceiptStoreError> {
3065    let mut statement = connection.prepare("PRAGMA table_info(receipt_lineage_statements)")?;
3066    let columns = statement.query_map([], |row| row.get::<_, String>(1))?;
3067    let columns = columns.collect::<Result<Vec<_>, _>>()?;
3068
3069    if !columns.iter().any(|column| column == "statement_id") {
3070        connection.execute(
3071            "ALTER TABLE receipt_lineage_statements ADD COLUMN statement_id TEXT",
3072            [],
3073        )?;
3074    }
3075
3076    connection.execute(
3077        r#"
3078        CREATE UNIQUE INDEX IF NOT EXISTS idx_receipt_lineage_statement_id
3079            ON receipt_lineage_statements(statement_id)
3080            WHERE statement_id IS NOT NULL
3081        "#,
3082        [],
3083    )?;
3084    connection.execute(
3085        r#"
3086        UPDATE receipt_lineage_statements
3087        SET statement_id = json_extract(raw_json, '$.id')
3088        WHERE statement_id IS NULL
3089          AND json_extract(raw_json, '$.schema') = ?1
3090        "#,
3091        params![chio_core::receipt::CHIO_RECEIPT_LINEAGE_STATEMENT_SCHEMA],
3092    )?;
3093    Ok(())
3094}
3095
3096pub(crate) fn backfill_tool_receipt_attribution_columns(
3097    connection: &Connection,
3098) -> Result<(), ReceiptStoreError> {
3099    connection.execute_batch(
3100        r#"
3101        UPDATE chio_tool_receipts
3102        SET grant_index = CAST(COALESCE(
3103            json_extract(raw_json, '$.metadata.attribution.grant_index'),
3104            json_extract(raw_json, '$.metadata.financial.grant_index')
3105        ) AS INTEGER)
3106        WHERE grant_index IS NULL
3107          AND COALESCE(
3108                json_extract(raw_json, '$.metadata.attribution.grant_index'),
3109                json_extract(raw_json, '$.metadata.financial.grant_index')
3110              ) IS NOT NULL;
3111
3112        UPDATE chio_tool_receipts
3113        SET subject_key = COALESCE(
3114            subject_key,
3115            CAST(json_extract(raw_json, '$.metadata.attribution.subject_key') AS TEXT),
3116            (SELECT cl.subject_key FROM capability_lineage cl WHERE cl.capability_id = chio_tool_receipts.capability_id)
3117        )
3118        WHERE subject_key IS NULL;
3119
3120        UPDATE chio_tool_receipts
3121        SET issuer_key = COALESCE(
3122            issuer_key,
3123            CAST(json_extract(raw_json, '$.metadata.attribution.issuer_key') AS TEXT),
3124            (SELECT cl.issuer_key FROM capability_lineage cl WHERE cl.capability_id = chio_tool_receipts.capability_id)
3125        )
3126        WHERE issuer_key IS NULL;
3127
3128        -- Phase 1.5 multi-tenant receipt isolation: hydrate tenant_id
3129        -- from the canonical receipt body. Legacy receipts (pre-1.5)
3130        -- that were stored before the field existed stay NULL, which
3131        -- means "public / visible to any tenant under the default
3132        -- compat query mode". Operators who want to purge those
3133        -- legacy rows can enable strict tenant isolation on queries.
3134        --
3135        -- The receipt body uses snake_case field names (no rename_all),
3136        -- so the JSON key is `tenant_id`, not `tenantId`.
3137        UPDATE chio_tool_receipts
3138        SET tenant_id = CAST(json_extract(raw_json, '$.tenant_id') AS TEXT)
3139        WHERE tenant_id IS NULL
3140          AND json_extract(raw_json, '$.tenant_id') IS NOT NULL;
3141        "#,
3142    )?;
3143    Ok(())
3144}
3145
3146const SESSION_ANCHOR_SOURCE_KIND: &str = "session_anchor";
3147const REQUEST_LINEAGE_SOURCE_KIND: &str = "request_lineage_record";
3148const RECEIPT_LINEAGE_SOURCE_KIND: &str = "receipt_lineage_statement";
3149const CHILD_RECEIPT_BACKFILL_SOURCE_KIND: &str = "child_receipt_backfill";
3150const GOVERNED_RECEIPT_BACKFILL_SOURCE_KIND: &str = "governed_receipt_backfill";
3151
3152fn provenance_json_sha256(value: &serde_json::Value) -> Result<String, ReceiptStoreError> {
3153    let canonical = canonical_json_bytes(value)
3154        .map_err(|error| ReceiptStoreError::Canonical(error.to_string()))?;
3155    Ok(sha256_hex(&canonical))
3156}
3157
3158fn sanitize_required_identifier(
3159    record_kind: &str,
3160    record_id: &str,
3161    field: &str,
3162    value: &str,
3163) -> Result<String, ReceiptStoreError> {
3164    let trimmed = value.trim();
3165    if trimmed.is_empty() {
3166        return Err(ReceiptStoreError::Conflict(format!(
3167            "{record_kind} `{record_id}` requires non-empty {field}"
3168        )));
3169    }
3170    Ok(trimmed.to_string())
3171}
3172
3173fn sanitize_optional_identifier(
3174    record_kind: &str,
3175    record_id: &str,
3176    field: &str,
3177    value: Option<&str>,
3178) -> Result<Option<String>, ReceiptStoreError> {
3179    value
3180        .map(|value| sanitize_required_identifier(record_kind, record_id, field, value))
3181        .transpose()
3182}
3183
3184fn merge_optional_identifier(
3185    record_kind: &str,
3186    record_id: &str,
3187    field: &str,
3188    existing: Option<String>,
3189    incoming: Option<&str>,
3190) -> Result<Option<String>, ReceiptStoreError> {
3191    let incoming = sanitize_optional_identifier(record_kind, record_id, field, incoming)?;
3192    match (existing, incoming) {
3193        (Some(existing), Some(incoming)) if existing != incoming => {
3194            Err(ReceiptStoreError::Conflict(format!(
3195                "{record_kind} `{record_id}` reuses {field} with conflicting value `{incoming}` (existing `{existing}`)"
3196            )))
3197        }
3198        (Some(existing), _) => Ok(Some(existing)),
3199        (None, incoming) => Ok(incoming),
3200    }
3201}
3202
3203fn request_lineage_exists_tx(
3204    tx: &rusqlite::Transaction<'_>,
3205    session_id: &str,
3206    request_id: &str,
3207) -> Result<bool, ReceiptStoreError> {
3208    Ok(tx
3209        .query_row(
3210            r#"
3211            SELECT 1
3212            FROM request_lineage
3213            WHERE session_id = ?1 AND request_id = ?2
3214            LIMIT 1
3215            "#,
3216            params![session_id, request_id],
3217            |_| Ok(()),
3218        )
3219        .optional()?
3220        .is_some())
3221}
3222
3223fn anchored_request_lineage_exists_tx(
3224    tx: &rusqlite::Transaction<'_>,
3225    session_id: &str,
3226    request_id: &str,
3227    session_anchor_id: &str,
3228) -> Result<bool, ReceiptStoreError> {
3229    Ok(tx
3230        .query_row(
3231            r#"
3232            SELECT 1
3233            FROM request_lineage
3234            WHERE session_id = ?1
3235              AND request_id = ?2
3236              AND session_anchor_id = ?3
3237            LIMIT 1
3238            "#,
3239            params![session_id, request_id, session_anchor_id],
3240            |_| Ok(()),
3241        )
3242        .optional()?
3243        .is_some())
3244}
3245
3246fn session_anchor_exists_tx(
3247    tx: &rusqlite::Transaction<'_>,
3248    session_id: &str,
3249    session_anchor_id: &str,
3250) -> Result<bool, ReceiptStoreError> {
3251    Ok(tx
3252        .query_row(
3253            r#"
3254            SELECT 1
3255            FROM session_anchors
3256            WHERE anchor_id = ?1
3257              AND session_id = ?2
3258            LIMIT 1
3259            "#,
3260            params![session_anchor_id, session_id],
3261            |_| Ok(()),
3262        )
3263        .optional()?
3264        .is_some())
3265}
3266
3267fn receipt_id_exists_tx(
3268    tx: &rusqlite::Transaction<'_>,
3269    receipt_id: &str,
3270) -> Result<bool, ReceiptStoreError> {
3271    Ok(tx
3272        .query_row(
3273            r#"
3274            SELECT 1
3275            FROM (
3276                SELECT receipt_id FROM chio_tool_receipts
3277                UNION ALL
3278                SELECT receipt_id FROM chio_child_receipts
3279            )
3280            WHERE receipt_id = ?1
3281            LIMIT 1
3282            "#,
3283            params![receipt_id],
3284            |_| Ok(()),
3285        )
3286        .optional()?
3287        .is_some())
3288}
3289
3290fn extract_lineage_evidence_class(statement_json: &serde_json::Value) -> Option<String> {
3291    let nested = statement_json
3292        .get("callChain")
3293        .or_else(|| statement_json.get("call_chain"));
3294    [Some(statement_json), nested]
3295        .into_iter()
3296        .flatten()
3297        .find_map(|value| {
3298            value
3299                .get("evidenceClass")
3300                .or_else(|| value.get("evidence_class"))
3301                .and_then(serde_json::Value::as_str)
3302                .map(str::to_string)
3303        })
3304}
3305
3306fn extract_lineage_evidence_sources_json(
3307    statement_json: &serde_json::Value,
3308) -> Result<Option<String>, ReceiptStoreError> {
3309    let nested = statement_json
3310        .get("callChain")
3311        .or_else(|| statement_json.get("call_chain"));
3312    for value in [Some(statement_json), nested].into_iter().flatten() {
3313        if let Some(sources) = value
3314            .get("evidenceSources")
3315            .or_else(|| value.get("evidence_sources"))
3316        {
3317            return Ok(Some(serde_json::to_string(sources)?));
3318        }
3319    }
3320    Ok(None)
3321}
3322
3323#[derive(Debug, Clone, Default)]
3324struct ReceiptLineageStatementIdentifiers {
3325    statement_id: Option<String>,
3326    child_receipt_id: Option<String>,
3327    child_request_id: Option<String>,
3328    child_session_anchor_id: Option<String>,
3329    parent_request_id: Option<String>,
3330    parent_receipt_id: Option<String>,
3331}
3332
3333fn extract_receipt_lineage_statement_identifiers(
3334    statement_json: &serde_json::Value,
3335) -> ReceiptLineageStatementIdentifiers {
3336    let schema = statement_json
3337        .get("schema")
3338        .and_then(serde_json::Value::as_str);
3339    if schema != Some(chio_core::receipt::CHIO_RECEIPT_LINEAGE_STATEMENT_SCHEMA) {
3340        return ReceiptLineageStatementIdentifiers::default();
3341    }
3342
3343    if let Ok(statement) = serde_json::from_value::<chio_core::receipt::ReceiptLineageStatement>(
3344        statement_json.clone(),
3345    ) {
3346        return ReceiptLineageStatementIdentifiers {
3347            statement_id: Some(statement.id),
3348            child_receipt_id: Some(statement.child_receipt_id),
3349            child_request_id: Some(statement.child_request_id.to_string()),
3350            child_session_anchor_id: Some(statement.child_session_anchor.session_anchor_id),
3351            parent_request_id: Some(statement.parent_request_id.to_string()),
3352            parent_receipt_id: Some(statement.parent_receipt_id),
3353        };
3354    }
3355
3356    if let Ok(statement) = serde_json::from_value::<chio_core::receipt::ReceiptLineageStatementBody>(
3357        statement_json.clone(),
3358    ) {
3359        return ReceiptLineageStatementIdentifiers {
3360            statement_id: Some(statement.id),
3361            child_receipt_id: Some(statement.child_receipt_id),
3362            child_request_id: Some(statement.child_request_id.to_string()),
3363            child_session_anchor_id: Some(statement.child_session_anchor.session_anchor_id),
3364            parent_request_id: Some(statement.parent_request_id.to_string()),
3365            parent_receipt_id: Some(statement.parent_receipt_id),
3366        };
3367    }
3368
3369    ReceiptLineageStatementIdentifiers::default()
3370}
3371
3372fn build_receipt_lineage_verification_tx(
3373    tx: &rusqlite::Transaction<'_>,
3374    receipt_id: &str,
3375    request_id: Option<&str>,
3376    session_id: Option<&str>,
3377    session_anchor_id: Option<&str>,
3378    parent_request_id: Option<&str>,
3379    parent_receipt_id: Option<&str>,
3380) -> Result<ReceiptLineageVerification, ReceiptStoreError> {
3381    let session_anchor_verified = match (session_id, session_anchor_id) {
3382        (Some(session_id), Some(session_anchor_id)) => {
3383            session_anchor_exists_tx(tx, session_id, session_anchor_id)?
3384        }
3385        _ => false,
3386    };
3387    let parent_request_verified = match (session_id, parent_request_id) {
3388        (Some(session_id), Some(parent_request_id)) => {
3389            request_lineage_exists_tx(tx, session_id, parent_request_id)?
3390        }
3391        _ => false,
3392    };
3393    let parent_receipt_verified = match parent_receipt_id {
3394        Some(parent_receipt_id) => receipt_id_exists_tx(tx, parent_receipt_id)?,
3395        None => false,
3396    };
3397    let replay_protected = match (session_id, request_id, session_anchor_id) {
3398        (Some(session_id), Some(request_id), Some(session_anchor_id))
3399            if session_anchor_verified =>
3400        {
3401            anchored_request_lineage_exists_tx(tx, session_id, request_id, session_anchor_id)?
3402        }
3403        _ => false,
3404    };
3405
3406    Ok(ReceiptLineageVerification {
3407        receipt_id: receipt_id.to_string(),
3408        request_id: request_id.map(str::to_string),
3409        session_id: session_id.map(str::to_string),
3410        session_anchor_id: session_anchor_id.map(str::to_string),
3411        session_anchor_verified,
3412        parent_request_verified,
3413        parent_receipt_verified,
3414        replay_protected,
3415    })
3416}
3417
3418fn refresh_receipt_lineage_verification_state_tx(
3419    tx: &rusqlite::Transaction<'_>,
3420    receipt_id: &str,
3421) -> Result<(), ReceiptStoreError> {
3422    let row = tx
3423        .query_row(
3424            r#"
3425            SELECT request_id, session_id, session_anchor_id, parent_request_id, parent_receipt_id
3426            FROM receipt_lineage_statements
3427            WHERE receipt_id = ?1
3428            "#,
3429            params![receipt_id],
3430            |row| {
3431                Ok((
3432                    row.get::<_, Option<String>>(0)?,
3433                    row.get::<_, Option<String>>(1)?,
3434                    row.get::<_, Option<String>>(2)?,
3435                    row.get::<_, Option<String>>(3)?,
3436                    row.get::<_, Option<String>>(4)?,
3437                ))
3438            },
3439        )
3440        .optional()?;
3441    let Some((request_id, session_id, session_anchor_id, parent_request_id, parent_receipt_id)) =
3442        row
3443    else {
3444        return Ok(());
3445    };
3446
3447    let verification = build_receipt_lineage_verification_tx(
3448        tx,
3449        receipt_id,
3450        request_id.as_deref(),
3451        session_id.as_deref(),
3452        session_anchor_id.as_deref(),
3453        parent_request_id.as_deref(),
3454        parent_receipt_id.as_deref(),
3455    )?;
3456    tx.execute(
3457        r#"
3458        UPDATE receipt_lineage_statements
3459        SET verified_session_anchor = ?2,
3460            verified_parent_request = ?3,
3461            verified_parent_receipt = ?4,
3462            replay_protected = ?5
3463        WHERE receipt_id = ?1
3464        "#,
3465        params![
3466            receipt_id,
3467            sqlite_bool(verification.session_anchor_verified),
3468            sqlite_bool(verification.parent_request_verified),
3469            sqlite_bool(verification.parent_receipt_verified),
3470            sqlite_bool(verification.replay_protected),
3471        ],
3472    )?;
3473    Ok(())
3474}
3475
3476fn refresh_receipt_lineage_rows_for_request_tx(
3477    tx: &rusqlite::Transaction<'_>,
3478    session_id: &str,
3479    request_id: &str,
3480) -> Result<(), ReceiptStoreError> {
3481    let mut statement = tx.prepare(
3482        r#"
3483        SELECT receipt_id
3484        FROM receipt_lineage_statements
3485        WHERE session_id = ?1
3486          AND (request_id = ?2 OR parent_request_id = ?2)
3487        "#,
3488    )?;
3489    let receipt_ids = statement
3490        .query_map(params![session_id, request_id], |row| {
3491            row.get::<_, String>(0)
3492        })?
3493        .collect::<Result<Vec<_>, _>>()?;
3494    drop(statement);
3495
3496    for receipt_id in receipt_ids {
3497        refresh_receipt_lineage_verification_state_tx(tx, &receipt_id)?;
3498    }
3499    Ok(())
3500}
3501
3502fn refresh_receipt_lineage_rows_for_anchor_tx(
3503    tx: &rusqlite::Transaction<'_>,
3504    session_id: &str,
3505    session_anchor_id: &str,
3506) -> Result<(), ReceiptStoreError> {
3507    let mut statement = tx.prepare(
3508        r#"
3509        SELECT receipt_id
3510        FROM receipt_lineage_statements
3511        WHERE session_id = ?1
3512          AND session_anchor_id = ?2
3513        "#,
3514    )?;
3515    let receipt_ids = statement
3516        .query_map(params![session_id, session_anchor_id], |row| {
3517            row.get::<_, String>(0)
3518        })?
3519        .collect::<Result<Vec<_>, _>>()?;
3520    drop(statement);
3521
3522    for receipt_id in receipt_ids {
3523        refresh_receipt_lineage_verification_state_tx(tx, &receipt_id)?;
3524    }
3525    Ok(())
3526}
3527
3528fn refresh_receipt_lineage_rows_for_parent_receipt_tx(
3529    tx: &rusqlite::Transaction<'_>,
3530    parent_receipt_id: &str,
3531) -> Result<(), ReceiptStoreError> {
3532    let mut statement = tx.prepare(
3533        r#"
3534        SELECT receipt_id
3535        FROM receipt_lineage_statements
3536        WHERE parent_receipt_id = ?1
3537        "#,
3538    )?;
3539    let receipt_ids = statement
3540        .query_map(params![parent_receipt_id], |row| row.get::<_, String>(0))?
3541        .collect::<Result<Vec<_>, _>>()?;
3542    drop(statement);
3543
3544    for receipt_id in receipt_ids {
3545        refresh_receipt_lineage_verification_state_tx(tx, &receipt_id)?;
3546    }
3547    Ok(())
3548}
3549
3550#[allow(clippy::too_many_arguments)]
3551fn persist_session_anchor_tx(
3552    tx: &rusqlite::Transaction<'_>,
3553    session_id: &str,
3554    anchor_id: &str,
3555    auth_context_fingerprint: &str,
3556    issued_at: u64,
3557    supersedes_anchor_id: Option<&str>,
3558    source_kind: &str,
3559    anchor_json: &serde_json::Value,
3560) -> Result<(), ReceiptStoreError> {
3561    let session_id =
3562        sanitize_required_identifier("session anchor", anchor_id, "session_id", session_id)?;
3563    let anchor_id =
3564        sanitize_required_identifier("session anchor", anchor_id, "anchor_id", anchor_id)?;
3565    let auth_context_fingerprint = sanitize_required_identifier(
3566        "session anchor",
3567        &anchor_id,
3568        "auth_context_fingerprint",
3569        auth_context_fingerprint,
3570    )?;
3571    let supersedes_anchor_id = sanitize_optional_identifier(
3572        "session anchor",
3573        &anchor_id,
3574        "supersedes_anchor_id",
3575        supersedes_anchor_id,
3576    )?;
3577    if supersedes_anchor_id.as_deref() == Some(anchor_id.as_str()) {
3578        return Err(ReceiptStoreError::Conflict(format!(
3579            "session anchor `{anchor_id}` cannot supersede itself"
3580        )));
3581    }
3582
3583    if tx
3584        .query_row(
3585            r#"
3586            SELECT anchor_id
3587            FROM session_anchors
3588            WHERE session_id = ?1
3589              AND auth_context_fingerprint = ?2
3590              AND anchor_id <> ?3
3591            LIMIT 1
3592            "#,
3593            params![&session_id, &auth_context_fingerprint, &anchor_id],
3594            |row| row.get::<_, String>(0),
3595        )
3596        .optional()?
3597        .is_some()
3598    {
3599        return Err(ReceiptStoreError::Conflict(format!(
3600            "session anchor replay detected for session `{session_id}` auth_context_fingerprint `{auth_context_fingerprint}`"
3601        )));
3602    }
3603
3604    if let Some(existing_session_id) = tx
3605        .query_row(
3606            "SELECT session_id FROM session_anchors WHERE anchor_id = ?1",
3607            params![&anchor_id],
3608            |row| row.get::<_, String>(0),
3609        )
3610        .optional()?
3611    {
3612        if existing_session_id != session_id {
3613            return Err(ReceiptStoreError::Conflict(format!(
3614                "session anchor `{anchor_id}` is already bound to session `{existing_session_id}`"
3615            )));
3616        }
3617    }
3618
3619    let raw_json = serde_json::to_string(anchor_json)?;
3620    let json_sha256 = provenance_json_sha256(anchor_json)?;
3621
3622    tx.execute(
3623        "UPDATE session_anchors SET is_current = 0 WHERE session_id = ?1 AND anchor_id <> ?2",
3624        params![&session_id, &anchor_id],
3625    )?;
3626    tx.execute(
3627        r#"
3628        INSERT INTO session_anchors (
3629            anchor_id,
3630            session_id,
3631            auth_context_fingerprint,
3632            issued_at,
3633            supersedes_anchor_id,
3634            is_current,
3635            source_kind,
3636            json_sha256,
3637            raw_json
3638        ) VALUES (?1, ?2, ?3, ?4, ?5, 1, ?6, ?7, ?8)
3639        ON CONFLICT(anchor_id) DO UPDATE SET
3640            auth_context_fingerprint = excluded.auth_context_fingerprint,
3641            issued_at = excluded.issued_at,
3642            supersedes_anchor_id = COALESCE(excluded.supersedes_anchor_id, session_anchors.supersedes_anchor_id),
3643            is_current = 1,
3644            source_kind = excluded.source_kind,
3645            json_sha256 = excluded.json_sha256,
3646            raw_json = excluded.raw_json
3647        "#,
3648        params![
3649            &anchor_id,
3650            &session_id,
3651            &auth_context_fingerprint,
3652            sqlite_i64(issued_at, "session anchor issued_at")?,
3653            supersedes_anchor_id.as_deref(),
3654            source_kind,
3655            &json_sha256,
3656            &raw_json,
3657        ],
3658    )?;
3659    refresh_receipt_lineage_rows_for_anchor_tx(tx, &session_id, &anchor_id)?;
3660    Ok(())
3661}
3662
3663#[allow(clippy::too_many_arguments)]
3664fn persist_request_lineage_tx(
3665    tx: &rusqlite::Transaction<'_>,
3666    session_id: &str,
3667    request_id: &str,
3668    parent_request_id: Option<&str>,
3669    session_anchor_id: Option<&str>,
3670    recorded_at: u64,
3671    request_fingerprint: Option<&str>,
3672    source_kind: &str,
3673    lineage_json: &serde_json::Value,
3674) -> Result<(), ReceiptStoreError> {
3675    let session_id =
3676        sanitize_required_identifier("request lineage", request_id, "session_id", session_id)?;
3677    let request_id =
3678        sanitize_required_identifier("request lineage", request_id, "request_id", request_id)?;
3679    let parent_request_id = sanitize_optional_identifier(
3680        "request lineage",
3681        &request_id,
3682        "parent_request_id",
3683        parent_request_id,
3684    )?;
3685    if parent_request_id.as_deref() == Some(request_id.as_str()) {
3686        return Err(ReceiptStoreError::Conflict(format!(
3687            "request lineage `{request_id}` cannot point at itself as parent_request_id"
3688        )));
3689    }
3690
3691    let existing = tx
3692        .query_row(
3693            r#"
3694            SELECT parent_request_id, session_anchor_id, request_fingerprint
3695            FROM request_lineage
3696            WHERE session_id = ?1 AND request_id = ?2
3697            "#,
3698            params![&session_id, &request_id],
3699            |row| {
3700                Ok((
3701                    row.get::<_, Option<String>>(0)?,
3702                    row.get::<_, Option<String>>(1)?,
3703                    row.get::<_, Option<String>>(2)?,
3704                ))
3705            },
3706        )
3707        .optional()?;
3708    let (existing_parent_request_id, existing_session_anchor_id, existing_request_fingerprint) =
3709        existing.unwrap_or((None, None, None));
3710
3711    let session_anchor_id = merge_optional_identifier(
3712        "request lineage",
3713        &request_id,
3714        "session_anchor_id",
3715        existing_session_anchor_id,
3716        session_anchor_id,
3717    )?;
3718    let parent_request_id = merge_optional_identifier(
3719        "request lineage",
3720        &request_id,
3721        "parent_request_id",
3722        existing_parent_request_id,
3723        parent_request_id.as_deref(),
3724    )?;
3725    let request_fingerprint = merge_optional_identifier(
3726        "request lineage",
3727        &request_id,
3728        "request_fingerprint",
3729        existing_request_fingerprint,
3730        request_fingerprint,
3731    )?;
3732
3733    let raw_json = serde_json::to_string(lineage_json)?;
3734    let json_sha256 = provenance_json_sha256(lineage_json)?;
3735    tx.execute(
3736        r#"
3737        INSERT INTO request_lineage (
3738            session_id,
3739            request_id,
3740            parent_request_id,
3741            session_anchor_id,
3742            recorded_at,
3743            request_fingerprint,
3744            source_kind,
3745            json_sha256,
3746            raw_json
3747        ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
3748        ON CONFLICT(session_id, request_id) DO UPDATE SET
3749            parent_request_id = excluded.parent_request_id,
3750            session_anchor_id = excluded.session_anchor_id,
3751            recorded_at = excluded.recorded_at,
3752            request_fingerprint = excluded.request_fingerprint,
3753            source_kind = excluded.source_kind,
3754            json_sha256 = excluded.json_sha256,
3755            raw_json = excluded.raw_json
3756        "#,
3757        params![
3758            &session_id,
3759            &request_id,
3760            parent_request_id.as_deref(),
3761            session_anchor_id.as_deref(),
3762            sqlite_i64(recorded_at, "request lineage recorded_at")?,
3763            request_fingerprint.as_deref(),
3764            source_kind,
3765            &json_sha256,
3766            &raw_json,
3767        ],
3768    )?;
3769    refresh_receipt_lineage_rows_for_request_tx(tx, &session_id, &request_id)?;
3770    Ok(())
3771}
3772
3773#[allow(clippy::too_many_arguments)]
3774fn persist_receipt_lineage_statement_tx(
3775    tx: &rusqlite::Transaction<'_>,
3776    child_receipt_id: &str,
3777    request_id: Option<&str>,
3778    session_id: Option<&str>,
3779    session_anchor_id: Option<&str>,
3780    parent_request_id: Option<&str>,
3781    parent_receipt_id: Option<&str>,
3782    chain_id: Option<&str>,
3783    recorded_at: u64,
3784    source_kind: &str,
3785    statement_json: &serde_json::Value,
3786) -> Result<(), ReceiptStoreError> {
3787    let child_receipt_id = sanitize_required_identifier(
3788        "receipt lineage statement",
3789        child_receipt_id,
3790        "child_receipt_id",
3791        child_receipt_id,
3792    )?;
3793    let extracted = extract_receipt_lineage_statement_identifiers(statement_json);
3794    if let Some(extracted_child_receipt_id) = extracted.child_receipt_id.as_deref() {
3795        let extracted_child_receipt_id = sanitize_required_identifier(
3796            "receipt lineage statement",
3797            &child_receipt_id,
3798            "statement.child_receipt_id",
3799            extracted_child_receipt_id,
3800        )?;
3801        if extracted_child_receipt_id != child_receipt_id {
3802            return Err(ReceiptStoreError::Conflict(format!(
3803                "receipt lineage statement `{child_receipt_id}` conflicts with signed child_receipt_id `{extracted_child_receipt_id}`"
3804            )));
3805        }
3806    }
3807
3808    let existing = tx
3809        .query_row(
3810            r#"
3811            SELECT statement_id, request_id, session_id, session_anchor_id, parent_request_id, parent_receipt_id, chain_id
3812            FROM receipt_lineage_statements
3813            WHERE receipt_id = ?1
3814            "#,
3815            params![&child_receipt_id],
3816            |row| {
3817                Ok((
3818                    row.get::<_, Option<String>>(0)?,
3819                    row.get::<_, Option<String>>(1)?,
3820                    row.get::<_, Option<String>>(2)?,
3821                    row.get::<_, Option<String>>(3)?,
3822                    row.get::<_, Option<String>>(4)?,
3823                    row.get::<_, Option<String>>(5)?,
3824                    row.get::<_, Option<String>>(6)?,
3825                ))
3826            },
3827        )
3828        .optional()?;
3829    let (
3830        existing_statement_id,
3831        existing_request_id,
3832        existing_session_id,
3833        existing_session_anchor_id,
3834        existing_parent_request_id,
3835        existing_parent_receipt_id,
3836        existing_chain_id,
3837    ) = existing.unwrap_or((None, None, None, None, None, None, None));
3838
3839    let statement_id = merge_optional_identifier(
3840        "receipt lineage statement",
3841        &child_receipt_id,
3842        "statement_id",
3843        existing_statement_id,
3844        extracted.statement_id.as_deref(),
3845    )?;
3846
3847    let request_id = merge_optional_identifier(
3848        "receipt lineage statement",
3849        &child_receipt_id,
3850        "request_id",
3851        existing_request_id,
3852        extracted.child_request_id.as_deref().or(request_id),
3853    )?;
3854    let session_id = merge_optional_identifier(
3855        "receipt lineage statement",
3856        &child_receipt_id,
3857        "session_id",
3858        existing_session_id,
3859        session_id,
3860    )?;
3861    let session_anchor_id = merge_optional_identifier(
3862        "receipt lineage statement",
3863        &child_receipt_id,
3864        "session_anchor_id",
3865        existing_session_anchor_id,
3866        extracted
3867            .child_session_anchor_id
3868            .as_deref()
3869            .or(session_anchor_id),
3870    )?;
3871    let parent_request_id = merge_optional_identifier(
3872        "receipt lineage statement",
3873        &child_receipt_id,
3874        "parent_request_id",
3875        existing_parent_request_id,
3876        extracted.parent_request_id.as_deref().or(parent_request_id),
3877    )?;
3878    let parent_receipt_id = merge_optional_identifier(
3879        "receipt lineage statement",
3880        &child_receipt_id,
3881        "parent_receipt_id",
3882        existing_parent_receipt_id,
3883        extracted.parent_receipt_id.as_deref().or(parent_receipt_id),
3884    )?;
3885    let chain_id = merge_optional_identifier(
3886        "receipt lineage statement",
3887        &child_receipt_id,
3888        "chain_id",
3889        existing_chain_id,
3890        chain_id,
3891    )?;
3892
3893    if session_anchor_id.is_some() && session_id.is_none() {
3894        return Err(ReceiptStoreError::Conflict(format!(
3895            "receipt lineage statement `{child_receipt_id}` requires session_id when session_anchor_id is present"
3896        )));
3897    }
3898    if request_id.is_some() && session_id.is_none() {
3899        return Err(ReceiptStoreError::Conflict(format!(
3900            "receipt lineage statement `{child_receipt_id}` requires session_id when request_id is present"
3901        )));
3902    }
3903    if request_id.is_some() && parent_request_id.is_some() && request_id == parent_request_id {
3904        return Err(ReceiptStoreError::Conflict(format!(
3905            "receipt lineage statement `{child_receipt_id}` cannot reuse request_id as parent_request_id"
3906        )));
3907    }
3908    if parent_receipt_id.as_deref() == Some(child_receipt_id.as_str()) {
3909        return Err(ReceiptStoreError::Conflict(format!(
3910            "receipt lineage statement `{child_receipt_id}` cannot point at itself as parent_receipt_id"
3911        )));
3912    }
3913
3914    let verification = build_receipt_lineage_verification_tx(
3915        tx,
3916        &child_receipt_id,
3917        request_id.as_deref(),
3918        session_id.as_deref(),
3919        session_anchor_id.as_deref(),
3920        parent_request_id.as_deref(),
3921        parent_receipt_id.as_deref(),
3922    )?;
3923    let evidence_class = extract_lineage_evidence_class(statement_json);
3924    let evidence_sources_json = extract_lineage_evidence_sources_json(statement_json)?;
3925    let raw_json = serde_json::to_string(statement_json)?;
3926    let json_sha256 = provenance_json_sha256(statement_json)?;
3927
3928    tx.execute(
3929        r#"
3930        INSERT INTO receipt_lineage_statements (
3931            receipt_id,
3932            statement_id,
3933            request_id,
3934            session_id,
3935            session_anchor_id,
3936            chain_id,
3937            parent_request_id,
3938            parent_receipt_id,
3939            evidence_class,
3940            evidence_sources_json,
3941            verified_session_anchor,
3942            verified_parent_request,
3943            verified_parent_receipt,
3944            replay_protected,
3945            recorded_at,
3946            source_kind,
3947            json_sha256,
3948            raw_json
3949        ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)
3950        ON CONFLICT(receipt_id) DO UPDATE SET
3951            statement_id = excluded.statement_id,
3952            request_id = excluded.request_id,
3953            session_id = excluded.session_id,
3954            session_anchor_id = excluded.session_anchor_id,
3955            chain_id = excluded.chain_id,
3956            parent_request_id = excluded.parent_request_id,
3957            parent_receipt_id = excluded.parent_receipt_id,
3958            evidence_class = excluded.evidence_class,
3959            evidence_sources_json = excluded.evidence_sources_json,
3960            verified_session_anchor = excluded.verified_session_anchor,
3961            verified_parent_request = excluded.verified_parent_request,
3962            verified_parent_receipt = excluded.verified_parent_receipt,
3963            replay_protected = excluded.replay_protected,
3964            recorded_at = excluded.recorded_at,
3965            source_kind = excluded.source_kind,
3966            json_sha256 = excluded.json_sha256,
3967            raw_json = excluded.raw_json
3968        "#,
3969        params![
3970            &child_receipt_id,
3971            statement_id.as_deref(),
3972            request_id.as_deref(),
3973            session_id.as_deref(),
3974            session_anchor_id.as_deref(),
3975            chain_id.as_deref(),
3976            parent_request_id.as_deref(),
3977            parent_receipt_id.as_deref(),
3978            evidence_class.as_deref(),
3979            evidence_sources_json.as_deref(),
3980            sqlite_bool(verification.session_anchor_verified),
3981            sqlite_bool(verification.parent_request_verified),
3982            sqlite_bool(verification.parent_receipt_verified),
3983            sqlite_bool(verification.replay_protected),
3984            sqlite_i64(recorded_at, "receipt lineage statement recorded_at")?,
3985            source_kind,
3986            &json_sha256,
3987            &raw_json,
3988        ],
3989    )?;
3990    refresh_receipt_lineage_rows_for_parent_receipt_tx(tx, &child_receipt_id)?;
3991    Ok(())
3992}
3993
3994fn ensure_receipt_lineage_statement_for_receipt_id_tx(
3995    tx: &rusqlite::Transaction<'_>,
3996    receipt_id: &str,
3997) -> Result<(), ReceiptStoreError> {
3998    if tx
3999        .query_row(
4000            "SELECT 1 FROM receipt_lineage_statements WHERE receipt_id = ?1 LIMIT 1",
4001            params![receipt_id],
4002            |_| Ok(()),
4003        )
4004        .optional()?
4005        .is_some()
4006    {
4007        refresh_receipt_lineage_verification_state_tx(tx, receipt_id)?;
4008        refresh_receipt_lineage_rows_for_parent_receipt_tx(tx, receipt_id)?;
4009        return Ok(());
4010    }
4011
4012    let row = tx
4013        .query_row(
4014            "SELECT seq, raw_json FROM chio_tool_receipts WHERE receipt_id = ?1",
4015            params![receipt_id],
4016            |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
4017        )
4018        .optional()?;
4019    let Some((seq, raw_json)) = row else {
4020        return Ok(());
4021    };
4022    let receipt =
4023        decode_verified_chio_receipt(&raw_json, "persisted tool receipt", Some(seq.max(0) as u64))?;
4024    let Some(governed) = extract_governed_transaction_metadata(&receipt) else {
4025        refresh_receipt_lineage_rows_for_parent_receipt_tx(tx, receipt_id)?;
4026        return Ok(());
4027    };
4028    let Some(call_chain) = governed.call_chain.as_ref() else {
4029        refresh_receipt_lineage_rows_for_parent_receipt_tx(tx, receipt_id)?;
4030        return Ok(());
4031    };
4032
4033    persist_receipt_lineage_statement_tx(
4034        tx,
4035        &receipt.id,
4036        None,
4037        None,
4038        None,
4039        Some(call_chain.parent_request_id.as_str()),
4040        call_chain.parent_receipt_id.as_deref(),
4041        Some(call_chain.chain_id.as_str()),
4042        receipt.timestamp,
4043        GOVERNED_RECEIPT_BACKFILL_SOURCE_KIND,
4044        &serde_json::to_value(call_chain)?,
4045    )?;
4046    Ok(())
4047}
4048
4049fn load_receipt_lineage_verification(
4050    connection: &Connection,
4051    receipt_id: &str,
4052) -> Result<Option<ReceiptLineageVerification>, ReceiptStoreError> {
4053    connection
4054        .query_row(
4055            r#"
4056            SELECT receipt_id, request_id, session_id, session_anchor_id,
4057                   verified_session_anchor, verified_parent_request,
4058                   verified_parent_receipt, replay_protected
4059            FROM receipt_lineage_statements
4060            WHERE receipt_id = ?1
4061            "#,
4062            params![receipt_id],
4063            |row| {
4064                Ok(ReceiptLineageVerification {
4065                    receipt_id: row.get::<_, String>(0)?,
4066                    request_id: row.get::<_, Option<String>>(1)?,
4067                    session_id: row.get::<_, Option<String>>(2)?,
4068                    session_anchor_id: row.get::<_, Option<String>>(3)?,
4069                    session_anchor_verified: row.get::<_, i64>(4)? != 0,
4070                    parent_request_verified: row.get::<_, i64>(5)? != 0,
4071                    parent_receipt_verified: row.get::<_, i64>(6)? != 0,
4072                    replay_protected: row.get::<_, i64>(7)? != 0,
4073                })
4074            },
4075        )
4076        .optional()
4077        .map_err(ReceiptStoreError::from)
4078}
4079
4080fn load_receipt_lineage_statement_links(
4081    connection: &Connection,
4082    receipt_id: &str,
4083) -> Result<Vec<ReceiptLineageStatementLink>, ReceiptStoreError> {
4084    let mut statement = connection.prepare(
4085        r#"
4086        SELECT statement_id,
4087               receipt_id,
4088               request_id,
4089               parent_receipt_id,
4090               parent_request_id,
4091               session_id,
4092               session_anchor_id,
4093               chain_id,
4094               recorded_at
4095        FROM receipt_lineage_statements
4096        WHERE receipt_id = ?1
4097           OR parent_receipt_id = ?1
4098        ORDER BY recorded_at ASC, receipt_id ASC
4099        "#,
4100    )?;
4101    let rows = statement
4102        .query_map(params![receipt_id], |row| {
4103            let recorded_at = row.get::<_, i64>(8)?;
4104            let recorded_at = u64::try_from(recorded_at)
4105                .map_err(|_| rusqlite::Error::IntegralValueOutOfRange(8, recorded_at))?;
4106            Ok(ReceiptLineageStatementLink {
4107                statement_id: row.get::<_, Option<String>>(0)?,
4108                child_receipt_id: row.get::<_, String>(1)?,
4109                child_request_id: row.get::<_, Option<String>>(2)?,
4110                parent_receipt_id: row.get::<_, Option<String>>(3)?,
4111                parent_request_id: row.get::<_, Option<String>>(4)?,
4112                session_id: row.get::<_, Option<String>>(5)?,
4113                session_anchor_id: row.get::<_, Option<String>>(6)?,
4114                chain_id: row.get::<_, Option<String>>(7)?,
4115                recorded_at,
4116            })
4117        })?
4118        .collect::<Result<Vec<_>, _>>()?;
4119    Ok(rows)
4120}
4121
4122pub(crate) fn backfill_provenance_lineage_tables(
4123    connection: &mut Connection,
4124) -> Result<(), ReceiptStoreError> {
4125    let tx = connection.transaction()?;
4126
4127    let child_rows = {
4128        let mut statement = tx.prepare(
4129            "SELECT seq, raw_json FROM chio_child_receipts ORDER BY timestamp ASC, seq ASC",
4130        )?;
4131        let rows = statement
4132            .query_map([], |row| {
4133                Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?))
4134            })?
4135            .collect::<Result<Vec<_>, _>>()?;
4136        rows
4137    };
4138    for (seq, raw_json) in child_rows {
4139        let receipt = decode_verified_child_receipt(
4140            &raw_json,
4141            "persisted child receipt",
4142            Some(seq.max(0) as u64),
4143        )?;
4144        persist_request_lineage_tx(
4145            &tx,
4146            receipt.session_id.as_str(),
4147            receipt.request_id.as_str(),
4148            Some(receipt.parent_request_id.as_str()),
4149            None,
4150            receipt.timestamp,
4151            None,
4152            CHILD_RECEIPT_BACKFILL_SOURCE_KIND,
4153            &serde_json::from_str::<serde_json::Value>(&raw_json)?,
4154        )?;
4155    }
4156
4157    let tool_receipt_ids = {
4158        let mut statement = tx
4159            .prepare("SELECT receipt_id FROM chio_tool_receipts ORDER BY timestamp ASC, seq ASC")?;
4160        let rows = statement
4161            .query_map([], |row| row.get::<_, String>(0))?
4162            .collect::<Result<Vec<_>, _>>()?;
4163        rows
4164    };
4165    for receipt_id in tool_receipt_ids {
4166        ensure_receipt_lineage_statement_for_receipt_id_tx(&tx, &receipt_id)?;
4167    }
4168
4169    tx.commit()?;
4170    Ok(())
4171}
4172
4173impl SqliteReceiptStore {
4174    #[allow(dead_code)]
4175    pub(crate) fn claim_tree_canonical_bytes_range(
4176        &self,
4177        start_entry_seq: u64,
4178        end_entry_seq: u64,
4179    ) -> Result<Vec<(u64, Vec<u8>)>, ReceiptStoreError> {
4180        let connection = self.connection()?;
4181        load_claim_tree_canonical_bytes_range(&connection, start_entry_seq, end_entry_seq)
4182    }
4183
4184    pub fn record_session_anchor_record(
4185        &self,
4186        session_id: &str,
4187        anchor_id: &str,
4188        auth_context_fingerprint: &str,
4189        issued_at: u64,
4190        supersedes_anchor_id: Option<&str>,
4191        anchor_json: &serde_json::Value,
4192    ) -> Result<(), ReceiptStoreError> {
4193        let mut connection = self.connection()?;
4194        let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
4195        persist_session_anchor_tx(
4196            &tx,
4197            session_id,
4198            anchor_id,
4199            auth_context_fingerprint,
4200            issued_at,
4201            supersedes_anchor_id,
4202            SESSION_ANCHOR_SOURCE_KIND,
4203            anchor_json,
4204        )?;
4205        tx.commit()?;
4206        Ok(())
4207    }
4208
4209    #[allow(clippy::too_many_arguments)]
4210    pub fn record_request_lineage_record(
4211        &self,
4212        session_id: &str,
4213        request_id: &str,
4214        parent_request_id: Option<&str>,
4215        session_anchor_id: Option<&str>,
4216        recorded_at: u64,
4217        request_fingerprint: Option<&str>,
4218        lineage_json: &serde_json::Value,
4219    ) -> Result<(), ReceiptStoreError> {
4220        let mut connection = self.connection()?;
4221        let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
4222        persist_request_lineage_tx(
4223            &tx,
4224            session_id,
4225            request_id,
4226            parent_request_id,
4227            session_anchor_id,
4228            recorded_at,
4229            request_fingerprint,
4230            REQUEST_LINEAGE_SOURCE_KIND,
4231            lineage_json,
4232        )?;
4233        tx.commit()?;
4234        Ok(())
4235    }
4236
4237    #[allow(clippy::too_many_arguments)]
4238    pub fn record_receipt_lineage_statement_record(
4239        &self,
4240        child_receipt_id: &str,
4241        request_id: Option<&str>,
4242        session_id: Option<&str>,
4243        session_anchor_id: Option<&str>,
4244        parent_request_id: Option<&str>,
4245        parent_receipt_id: Option<&str>,
4246        chain_id: Option<&str>,
4247        recorded_at: u64,
4248        statement_json: &serde_json::Value,
4249    ) -> Result<(), ReceiptStoreError> {
4250        let mut connection = self.connection()?;
4251        let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
4252        persist_receipt_lineage_statement_tx(
4253            &tx,
4254            child_receipt_id,
4255            request_id,
4256            session_id,
4257            session_anchor_id,
4258            parent_request_id,
4259            parent_receipt_id,
4260            chain_id,
4261            recorded_at,
4262            RECEIPT_LINEAGE_SOURCE_KIND,
4263            statement_json,
4264        )?;
4265        tx.commit()?;
4266        Ok(())
4267    }
4268
4269    pub fn list_receipt_lineage_statement_links(
4270        &self,
4271        receipt_id: &str,
4272    ) -> Result<Vec<ReceiptLineageStatementLink>, ReceiptStoreError> {
4273        let mut connection = self.connection()?;
4274        let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
4275        ensure_receipt_lineage_statement_for_receipt_id_tx(&tx, receipt_id)?;
4276        refresh_receipt_lineage_rows_for_parent_receipt_tx(&tx, receipt_id)?;
4277        let links = load_receipt_lineage_statement_links(&tx, receipt_id)?;
4278        tx.commit()?;
4279        Ok(links)
4280    }
4281
4282    pub fn receipt_lineage_verification(
4283        &self,
4284        receipt_id: &str,
4285    ) -> Result<Option<ReceiptLineageVerification>, ReceiptStoreError> {
4286        let mut connection = self.connection()?;
4287        let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
4288        ensure_receipt_lineage_statement_for_receipt_id_tx(&tx, receipt_id)?;
4289        let verification = load_receipt_lineage_verification(&tx, receipt_id)?;
4290        tx.commit()?;
4291        Ok(verification)
4292    }
4293
4294    pub fn append_child_receipt_record(
4295        &self,
4296        receipt: &ChildRequestReceipt,
4297    ) -> Result<(), ReceiptStoreError> {
4298        ensure_child_receipt_verified(receipt)?;
4299        let raw_json = serde_json::to_string(receipt)?;
4300        let mut connection = self.connection()?;
4301        let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
4302        tx.execute(
4303            r#"
4304            INSERT INTO chio_child_receipts (
4305                receipt_id,
4306                timestamp,
4307                session_id,
4308                parent_request_id,
4309                request_id,
4310                operation_kind,
4311                terminal_state,
4312                policy_hash,
4313                outcome_hash,
4314                raw_json
4315            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4316            ON CONFLICT(receipt_id) DO NOTHING
4317            "#,
4318            params![
4319                receipt.id,
4320                sqlite_i64(receipt.timestamp, "child receipt timestamp")?,
4321                receipt.session_id.as_str(),
4322                receipt.parent_request_id.as_str(),
4323                receipt.request_id.as_str(),
4324                receipt.operation_kind.as_str(),
4325                terminal_state_kind(&receipt.terminal_state),
4326                receipt.policy_hash,
4327                receipt.outcome_hash,
4328                &raw_json,
4329            ],
4330        )?;
4331        persist_request_lineage_tx(
4332            &tx,
4333            receipt.session_id.as_str(),
4334            receipt.request_id.as_str(),
4335            Some(receipt.parent_request_id.as_str()),
4336            None,
4337            receipt.timestamp,
4338            None,
4339            CHILD_RECEIPT_BACKFILL_SOURCE_KIND,
4340            &serde_json::from_str::<serde_json::Value>(&raw_json)?,
4341        )?;
4342        tx.commit()?;
4343        Ok(())
4344    }
4345}
4346
4347impl ReceiptStore for SqliteReceiptStore {
4348    fn append_chio_receipt(&mut self, receipt: &ChioReceipt) -> Result<(), ReceiptStoreError> {
4349        self.append_chio_receipt_returning_seq(receipt).map(|_| ())
4350    }
4351
4352    fn append_chio_receipt_returning_seq(
4353        &mut self,
4354        receipt: &ChioReceipt,
4355    ) -> Result<Option<u64>, ReceiptStoreError> {
4356        let connection = self.connection()?;
4357        ensure_checkpoint_transparency_guards(&connection)?;
4358        verify_latest_checkpoint_integrity(&connection)?;
4359        let seq = SqliteReceiptStore::append_chio_receipt_returning_seq(self, receipt)?;
4360        let mut connection = self.connection()?;
4361        let tx = connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
4362        ensure_receipt_lineage_statement_for_receipt_id_tx(&tx, &receipt.id)?;
4363        tx.commit()?;
4364        Ok(Some(seq))
4365    }
4366
4367    fn receipts_canonical_bytes_range(
4368        &self,
4369        start_seq: u64,
4370        end_seq: u64,
4371    ) -> Result<Vec<(u64, Vec<u8>)>, ReceiptStoreError> {
4372        SqliteReceiptStore::receipts_canonical_bytes_range(self, start_seq, end_seq)
4373    }
4374
4375    fn store_checkpoint(&mut self, checkpoint: &KernelCheckpoint) -> Result<(), ReceiptStoreError> {
4376        let connection = self.connection()?;
4377        ensure_checkpoint_transparency_guards(&connection)?;
4378
4379        chio_kernel::checkpoint::validate_checkpoint(checkpoint)
4380            .map_err(checkpoint_error_to_receipt_store)?;
4381        if let Some(existing) =
4382            load_persisted_checkpoint_row(&connection, checkpoint.body.checkpoint_seq)?
4383        {
4384            let existing = parse_persisted_checkpoint_row(existing)?;
4385            if existing == *checkpoint {
4386                return Ok(());
4387            }
4388            return Err(ReceiptStoreError::Conflict(format!(
4389                "checkpoint {} already exists with different content",
4390                checkpoint.body.checkpoint_seq
4391            )));
4392        }
4393
4394        match verify_checkpoint_chain_integrity(&connection)? {
4395            Some(predecessor) => {
4396                if checkpoint.body.checkpoint_seq <= predecessor.body.checkpoint_seq {
4397                    return Err(ReceiptStoreError::Conflict(format!(
4398                        "checkpoint {} must be appended after existing checkpoint {}",
4399                        checkpoint.body.checkpoint_seq, predecessor.body.checkpoint_seq
4400                    )));
4401                }
4402                chio_kernel::checkpoint::validate_checkpoint_predecessor(&predecessor, checkpoint)
4403                    .map_err(|error| {
4404                        ReceiptStoreError::Conflict(format!(
4405                            "checkpoint predecessor continuity violation: {error}"
4406                        ))
4407                    })?;
4408            }
4409            None if checkpoint.body.checkpoint_seq != 1 => {
4410                return Err(ReceiptStoreError::Conflict(format!(
4411                    "checkpoint {} cannot initialize an empty checkpoint log",
4412                    checkpoint.body.checkpoint_seq
4413                )));
4414            }
4415            None => {}
4416        }
4417
4418        let statement_json = serde_json::to_string(&checkpoint.body)?;
4419        connection.execute(
4420            r#"
4421            INSERT INTO kernel_checkpoints (
4422                checkpoint_seq, batch_start_seq, batch_end_seq, tree_size,
4423                merkle_root, issued_at, statement_json, signature, kernel_key
4424            ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
4425            "#,
4426            params![
4427                sqlite_i64(checkpoint.body.checkpoint_seq, "checkpoint_seq")?,
4428                sqlite_i64(checkpoint.body.batch_start_seq, "batch_start_seq")?,
4429                sqlite_i64(checkpoint.body.batch_end_seq, "batch_end_seq")?,
4430                sqlite_i64(checkpoint.body.tree_size as u64, "tree_size")?,
4431                checkpoint.body.merkle_root.to_hex(),
4432                sqlite_i64(checkpoint.body.issued_at, "issued_at")?,
4433                statement_json,
4434                checkpoint.signature.to_hex(),
4435                checkpoint.body.kernel_key.to_hex(),
4436            ],
4437        )?;
4438
4439        let stored = load_persisted_checkpoint_row(&connection, checkpoint.body.checkpoint_seq)?
4440            .ok_or_else(|| {
4441                ReceiptStoreError::Conflict(format!(
4442                    "checkpoint {} was not visible after persistence",
4443                    checkpoint.body.checkpoint_seq
4444                ))
4445            })?;
4446        let stored = parse_persisted_checkpoint_row(stored)?;
4447        if stored != *checkpoint {
4448            return Err(ReceiptStoreError::Conflict(format!(
4449                "checkpoint {} persisted with conflicting contents",
4450                checkpoint.body.checkpoint_seq
4451            )));
4452        }
4453
4454        Ok(())
4455    }
4456
4457    fn load_checkpoint_by_seq(
4458        &self,
4459        checkpoint_seq: u64,
4460    ) -> Result<Option<KernelCheckpoint>, ReceiptStoreError> {
4461        SqliteReceiptStore::load_checkpoint_by_seq(self, checkpoint_seq)
4462    }
4463
4464    fn supports_kernel_signed_checkpoints(&self) -> bool {
4465        true
4466    }
4467
4468    fn record_capability_snapshot(
4469        &mut self,
4470        token: &CapabilityToken,
4471        parent_capability_id: Option<&str>,
4472    ) -> Result<(), ReceiptStoreError> {
4473        SqliteReceiptStore::record_capability_snapshot(self, token, parent_capability_id).map_err(
4474            |error| match error {
4475                chio_kernel::CapabilityLineageError::ReceiptStore(error) => error,
4476                chio_kernel::CapabilityLineageError::Sqlite(error) => {
4477                    ReceiptStoreError::Sqlite(error)
4478                }
4479                chio_kernel::CapabilityLineageError::Json(error) => ReceiptStoreError::Json(error),
4480            },
4481        )
4482    }
4483
4484    fn get_capability_snapshot(
4485        &self,
4486        capability_id: &str,
4487    ) -> Result<Option<chio_kernel::CapabilitySnapshot>, ReceiptStoreError> {
4488        SqliteReceiptStore::get_lineage(self, capability_id).map_err(|error| match error {
4489            chio_kernel::CapabilityLineageError::ReceiptStore(error) => error,
4490            chio_kernel::CapabilityLineageError::Sqlite(error) => ReceiptStoreError::Sqlite(error),
4491            chio_kernel::CapabilityLineageError::Json(error) => ReceiptStoreError::Json(error),
4492        })
4493    }
4494
4495    fn get_capability_delegation_chain(
4496        &self,
4497        capability_id: &str,
4498    ) -> Result<Vec<chio_kernel::CapabilitySnapshot>, ReceiptStoreError> {
4499        SqliteReceiptStore::get_delegation_chain(self, capability_id).map_err(|error| match error {
4500            chio_kernel::CapabilityLineageError::ReceiptStore(error) => error,
4501            chio_kernel::CapabilityLineageError::Sqlite(error) => ReceiptStoreError::Sqlite(error),
4502            chio_kernel::CapabilityLineageError::Json(error) => ReceiptStoreError::Json(error),
4503        })
4504    }
4505
4506    fn record_session_anchor(
4507        &mut self,
4508        session_id: &str,
4509        anchor_id: &str,
4510        auth_context_fingerprint: &str,
4511        issued_at: u64,
4512        supersedes_anchor_id: Option<&str>,
4513        anchor_json: &serde_json::Value,
4514    ) -> Result<(), ReceiptStoreError> {
4515        self.record_session_anchor_record(
4516            session_id,
4517            anchor_id,
4518            auth_context_fingerprint,
4519            issued_at,
4520            supersedes_anchor_id,
4521            anchor_json,
4522        )
4523    }
4524
4525    fn record_request_lineage(
4526        &mut self,
4527        session_id: &str,
4528        request_id: &str,
4529        parent_request_id: Option<&str>,
4530        session_anchor_id: Option<&str>,
4531        recorded_at: u64,
4532        request_fingerprint: Option<&str>,
4533        lineage_json: &serde_json::Value,
4534    ) -> Result<(), ReceiptStoreError> {
4535        self.record_request_lineage_record(
4536            session_id,
4537            request_id,
4538            parent_request_id,
4539            session_anchor_id,
4540            recorded_at,
4541            request_fingerprint,
4542            lineage_json,
4543        )
4544    }
4545
4546    fn record_receipt_lineage_statement(
4547        &mut self,
4548        child_receipt_id: &str,
4549        request_id: Option<&str>,
4550        session_id: Option<&str>,
4551        session_anchor_id: Option<&str>,
4552        parent_request_id: Option<&str>,
4553        parent_receipt_id: Option<&str>,
4554        chain_id: Option<&str>,
4555        recorded_at: u64,
4556        statement_json: &serde_json::Value,
4557    ) -> Result<(), ReceiptStoreError> {
4558        self.record_receipt_lineage_statement_record(
4559            child_receipt_id,
4560            request_id,
4561            session_id,
4562            session_anchor_id,
4563            parent_request_id,
4564            parent_receipt_id,
4565            chain_id,
4566            recorded_at,
4567            statement_json,
4568        )
4569    }
4570
4571    fn get_receipt_lineage_verification(
4572        &self,
4573        receipt_id: &str,
4574    ) -> Result<Option<ReceiptLineageVerification>, ReceiptStoreError> {
4575        self.receipt_lineage_verification(receipt_id)
4576    }
4577
4578    fn list_receipt_lineage_statement_links(
4579        &self,
4580        receipt_id: &str,
4581    ) -> Result<Vec<ReceiptLineageStatementLink>, ReceiptStoreError> {
4582        SqliteReceiptStore::list_receipt_lineage_statement_links(self, receipt_id)
4583    }
4584
4585    fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
4586        Some(self)
4587    }
4588
4589    fn resolve_credit_bond(
4590        &self,
4591        bond_id: &str,
4592    ) -> Result<Option<CreditBondRow>, ReceiptStoreError> {
4593        self.query_credit_bonds(&CreditBondListQuery {
4594            bond_id: Some(bond_id.to_string()),
4595            facility_id: None,
4596            capability_id: None,
4597            agent_subject: None,
4598            tool_server: None,
4599            tool_name: None,
4600            disposition: None,
4601            lifecycle_state: None,
4602            limit: Some(1),
4603        })
4604        .map(|report| report.bonds.into_iter().next())
4605    }
4606
4607    fn append_child_receipt(
4608        &mut self,
4609        receipt: &ChildRequestReceipt,
4610    ) -> Result<(), ReceiptStoreError> {
4611        let connection = self.connection()?;
4612        ensure_checkpoint_transparency_guards(&connection)?;
4613        verify_latest_checkpoint_integrity(&connection)?;
4614        SqliteReceiptStore::append_child_receipt_record(self, receipt)
4615    }
4616}
4617
4618impl SqliteReceiptStore {
4619    pub fn record_checkpoint_publication_trust_anchor_binding(
4620        &mut self,
4621        checkpoint_seq: u64,
4622        binding: &chio_core::receipt::CheckpointPublicationTrustAnchorBinding,
4623    ) -> Result<(), ReceiptStoreError> {
4624        binding
4625            .validate()
4626            .map_err(|error| ReceiptStoreError::Conflict(error.to_string()))?;
4627        let checkpoint = self
4628            .load_checkpoint_by_seq(checkpoint_seq)?
4629            .ok_or_else(|| {
4630                ReceiptStoreError::NotFound(format!(
4631                    "checkpoint {} does not exist for publication binding",
4632                    checkpoint_seq
4633                ))
4634            })?;
4635        let publication = chio_kernel::checkpoint::build_trust_anchored_checkpoint_publication(
4636            &checkpoint,
4637            binding.clone(),
4638        )
4639        .map_err(checkpoint_error_to_receipt_store)?;
4640        let normalized_binding = publication.trust_anchor_binding.ok_or_else(|| {
4641            ReceiptStoreError::Conflict(format!(
4642                "checkpoint {} trust-anchor binding was not preserved during validation",
4643                checkpoint_seq
4644            ))
4645        })?;
4646
4647        let connection = self.connection()?;
4648        ensure_checkpoint_transparency_guards(&connection)?;
4649        ensure_transparency_projection_guards(&connection)?;
4650
4651        let existing = connection
4652            .query_row(
4653                r#"
4654                SELECT binding_json
4655                FROM checkpoint_publication_trust_anchor_bindings
4656                WHERE checkpoint_seq = ?1
4657                "#,
4658                params![sqlite_i64(checkpoint_seq, "checkpoint_seq")?],
4659                |row| row.get::<_, String>(0),
4660            )
4661            .optional()?;
4662        match existing {
4663            Some(binding_json) => {
4664                let existing_binding: chio_core::receipt::CheckpointPublicationTrustAnchorBinding =
4665                    serde_json::from_str(&binding_json)?;
4666                if existing_binding == normalized_binding {
4667                    return Ok(());
4668                }
4669                Err(ReceiptStoreError::Conflict(format!(
4670                    "checkpoint {} already has a different trust-anchor publication binding",
4671                    checkpoint_seq
4672                )))
4673            }
4674            None => {
4675                connection.execute(
4676                    r#"
4677                    INSERT INTO checkpoint_publication_trust_anchor_bindings (
4678                        checkpoint_seq,
4679                        binding_json
4680                    ) VALUES (?1, ?2)
4681                    "#,
4682                    params![
4683                        sqlite_i64(checkpoint_seq, "checkpoint_seq")?,
4684                        serde_json::to_string(&normalized_binding)?,
4685                    ],
4686                )?;
4687                Ok(())
4688            }
4689        }
4690    }
4691}
4692
4693pub(crate) fn decision_kind(decision: &Decision) -> &'static str {
4694    match decision {
4695        Decision::Allow => "allow",
4696        Decision::Deny { .. } => "deny",
4697        Decision::Cancelled { .. } => "cancelled",
4698        Decision::Incomplete { .. } => "incomplete",
4699    }
4700}
4701
4702pub(crate) fn terminal_state_kind(state: &OperationTerminalState) -> &'static str {
4703    match state {
4704        OperationTerminalState::Completed => "completed",
4705        OperationTerminalState::Cancelled { .. } => "cancelled",
4706        OperationTerminalState::Incomplete { .. } => "incomplete",
4707    }
4708}