1use serde::{Deserialize, Serialize};
8
9pub const EVIDENCE_PACKET_SCHEMA: &str = "tokmd.evidence-packet/v1";
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct EvidencePacketManifest {
15 pub schema: String,
16 pub tokmd_version: String,
17 pub preset: String,
18 pub base: String,
19 pub head: String,
20 pub paths: Vec<String>,
21 pub status: EvidencePacketStatus,
22 pub artifacts: EvidencePacketArtifacts,
23 #[serde(default, skip_serializing_if = "Vec::is_empty")]
24 pub review_priority: Vec<EvidencePacketReviewPriorityItem>,
25 pub warnings: Vec<String>,
26 pub errors: Vec<String>,
27 pub non_claims: Vec<String>,
28 pub reproduce: Vec<String>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub struct EvidencePacketArtifacts {
34 pub analyze_md: String,
35 pub analyze_json: String,
36 pub context_md: String,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub syntax_json: Option<String>,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44pub enum EvidencePacketStatus {
45 Complete,
46 Partial,
47 Failed,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct EvidencePacketReviewPriorityItem {
53 pub rank: u32,
54 pub path: String,
55 pub category: String,
56 pub severity: String,
57 pub score: u32,
58 pub kind: String,
59 pub reason: String,
60 pub evidence: String,
61 pub refs: Vec<String>,
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[test]
69 fn evidence_packet_status_uses_contract_spelling() {
70 assert_eq!(
71 serde_json::to_string(&EvidencePacketStatus::Complete).unwrap(),
72 "\"complete\""
73 );
74 assert_eq!(
75 serde_json::to_string(&EvidencePacketStatus::Partial).unwrap(),
76 "\"partial\""
77 );
78 assert_eq!(
79 serde_json::to_string(&EvidencePacketStatus::Failed).unwrap(),
80 "\"failed\""
81 );
82 }
83
84 #[test]
85 fn evidence_packet_manifest_roundtrips() {
86 let manifest = EvidencePacketManifest {
87 schema: EVIDENCE_PACKET_SCHEMA.to_string(),
88 tokmd_version: "1.12.0".to_string(),
89 preset: "bun-ub".to_string(),
90 base: "origin/main".to_string(),
91 head: "HEAD".to_string(),
92 paths: vec!["src/runtime/api".to_string()],
93 status: EvidencePacketStatus::Complete,
94 artifacts: EvidencePacketArtifacts {
95 analyze_md: "sensors/tokmd/analyze.md".to_string(),
96 analyze_json: "sensors/tokmd/analyze.json".to_string(),
97 context_md: "sensors/tokmd/context.md".to_string(),
98 syntax_json: Some("sensors/tokmd/syntax.json".to_string()),
99 },
100 review_priority: vec![],
101 warnings: vec![],
102 errors: vec![],
103 non_claims: vec![
104 "bun-ub packages review evidence; it does not prove UB exists or is absent"
105 .to_string(),
106 ],
107 reproduce: vec![
108 "tokmd analyze --preset bun-ub --format json --effort-base-ref origin/main --effort-head-ref HEAD --no-progress src/runtime/api > sensors/tokmd/analyze.json"
109 .to_string(),
110 ],
111 };
112
113 let json = serde_json::to_string(&manifest).unwrap();
114 let roundtrip: EvidencePacketManifest = serde_json::from_str(&json).unwrap();
115 assert_eq!(roundtrip, manifest);
116 }
117
118 #[test]
119 fn evidence_packet_manifest_defaults_missing_review_priority() {
120 let json = serde_json::json!({
121 "schema": EVIDENCE_PACKET_SCHEMA,
122 "tokmd_version": "1.12.0",
123 "preset": "bun-ub",
124 "base": "origin/main",
125 "head": "HEAD",
126 "paths": ["src/runtime/api"],
127 "status": "complete",
128 "artifacts": {
129 "analyze_md": "sensors/tokmd/analyze.md",
130 "analyze_json": "sensors/tokmd/analyze.json",
131 "context_md": "sensors/tokmd/context.md"
132 },
133 "warnings": [],
134 "errors": [],
135 "non_claims": [],
136 "reproduce": []
137 });
138
139 let manifest: EvidencePacketManifest = serde_json::from_value(json).unwrap();
140 assert!(manifest.review_priority.is_empty());
141 }
142
143 #[test]
144 fn evidence_packet_canonical_layout_matches_spec() {
145 const PACKET_DIR: &str = "sensors/tokmd";
147 const MANIFEST: &str = "sensors/tokmd/manifest.json";
148 const REQUIRED: &[&str] = &["analyze.md", "analyze.json", "context.md", "manifest.json"];
149 const OPTIONAL_SYNTAX: &str = "syntax.json";
150
151 assert_eq!(MANIFEST, "sensors/tokmd/manifest.json");
152 assert!(REQUIRED.contains(&"analyze.md"));
153 assert!(REQUIRED.contains(&"analyze.json"));
154 assert!(REQUIRED.contains(&"context.md"));
155 assert!(REQUIRED.contains(&"manifest.json"));
156 assert_eq!(OPTIONAL_SYNTAX, "syntax.json");
157 assert!(
158 MANIFEST.starts_with(PACKET_DIR),
159 "manifest path should live under the canonical packet directory"
160 );
161 }
162
163 #[test]
164 fn evidence_packet_review_priority_item_serializes() {
165 let item = EvidencePacketReviewPriorityItem {
166 rank: 1,
167 path: "src/runtime/api/MarkdownObject.rs".to_string(),
168 category: "panic_seam".to_string(),
169 severity: "high".to_string(),
170 score: 95,
171 kind: "expect_call".to_string(),
172 reason: "panic-like seam".to_string(),
173 evidence: "expect".to_string(),
174 refs: vec!["sensors/tokmd/syntax.json#/receipts/0/review_signals/1".to_string()],
175 };
176
177 let json = serde_json::to_value(&item).unwrap();
178 assert_eq!(json["rank"], 1);
179 assert_eq!(json["category"], "panic_seam");
180 assert_eq!(
181 json["refs"][0],
182 "sensors/tokmd/syntax.json#/receipts/0/review_signals/1"
183 );
184 }
185}