Skip to main content

tokmd_types/
packet_siblings.rs

1//! Sibling bundle file types for `tokmd render --from-packets`.
2//!
3//! These model producer artifacts such as `manual-candidates.json` and
4//! `cards.json` that may supplement manifest `preset_inputs` when partial.
5
6use serde::{Deserialize, Serialize};
7
8/// Sibling artifact filename for manual candidate indexes.
9pub const MANUAL_CANDIDATES_FILE: &str = "manual-candidates.json";
10
11/// Sibling artifact filename for ReviewCard analyzer output.
12pub const CARDS_FILE: &str = "cards.json";
13
14/// Schema id recorded by unsafe-review `manual-candidates.json` exports.
15pub const MANUAL_CANDIDATES_SCHEMA: &str = "manual-candidates/v1";
16
17/// Parsed sibling inputs available to the packet renderer.
18#[derive(Debug, Clone, PartialEq, Eq, Default)]
19pub struct PacketSiblingInputs {
20    pub manual_candidates: Option<ManualCandidatesFile>,
21    pub cards: Option<CardsFile>,
22    pub load_notes: Vec<String>,
23}
24
25/// Bundle passed from CLI load through formatting.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct PacketRenderBundle {
28    pub manifest: super::TokmdPacketsManifest,
29    pub siblings: PacketSiblingInputs,
30}
31
32/// `manual-candidates.json` index (minimal consumer contract).
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct ManualCandidatesFile {
35    pub schema_version: String,
36    #[serde(default)]
37    pub candidates: Vec<ManualCandidateRecord>,
38}
39
40/// One manual candidate row from `manual-candidates.json`.
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
42pub struct ManualCandidateRecord {
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub id: Option<String>,
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub title: Option<String>,
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub invariant: Option<String>,
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub safe_caller: Option<String>,
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub unsafe_operation: Option<String>,
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub operation_family: Option<String>,
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub location_text: Option<String>,
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub proof_mode: Option<String>,
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub fix_boundary: Option<String>,
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub pr_aperture: Option<String>,
63    #[serde(default, skip_serializing_if = "Vec::is_empty")]
64    pub test_targets: Vec<String>,
65    #[serde(default, skip_serializing_if = "Vec::is_empty")]
66    pub do_not_touch: Vec<String>,
67}
68
69/// `cards.json` ReviewCard snapshot (minimal consumer contract).
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71pub struct CardsFile {
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub schema_version: Option<String>,
74    #[serde(default, alias = "review_cards")]
75    pub cards: Vec<ReviewCardRecord>,
76}
77
78/// One ReviewCard row from `cards.json`.
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
80pub struct ReviewCardRecord {
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub id: Option<String>,
83    #[serde(default, skip_serializing_if = "Option::is_none")]
84    pub title: Option<String>,
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub location_text: Option<String>,
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub unsafe_operation: Option<String>,
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub operation_family: Option<String>,
91}
92
93impl ManualCandidatesFile {
94    /// Returns true when the file uses the supported schema id.
95    pub fn schema_matches(&self) -> bool {
96        self.schema_version == MANUAL_CANDIDATES_SCHEMA
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn manual_candidates_roundtrip() {
106        let file = ManualCandidatesFile {
107            schema_version: MANUAL_CANDIDATES_SCHEMA.into(),
108            candidates: vec![ManualCandidateRecord {
109                id: Some("seed-42".into()),
110                title: Some("UTF-8 boundary".into()),
111                invariant: Some("buffer bounds".into()),
112                ..ManualCandidateRecord::default()
113            }],
114        };
115        let json = serde_json::to_string(&file).unwrap();
116        let back: ManualCandidatesFile = serde_json::from_str(&json).unwrap();
117        assert_eq!(back, file);
118        assert!(back.schema_matches());
119    }
120
121    #[test]
122    fn cards_roundtrip_with_review_cards_alias() {
123        let raw =
124            r#"{"schema_version":"0.2","review_cards":[{"id":"rc-17","title":"slice read"}]}"#;
125        let file: CardsFile = serde_json::from_str(raw).unwrap();
126        assert_eq!(file.cards.len(), 1);
127        assert_eq!(file.cards[0].id.as_deref(), Some("rc-17"));
128    }
129}