1use serde::{Deserialize, Serialize};
13
14use crate::layout::{Point, Rect};
15
16#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
18#[serde(transparent)]
19pub struct NodeId(String);
20
21#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
23#[serde(transparent)]
24pub struct ClaimId(String);
25
26impl NodeId {
27 pub fn new(id: impl Into<String>) -> Self {
29 Self(id.into())
30 }
31
32 pub fn as_str(&self) -> &str {
34 &self.0
35 }
36
37 pub fn is_canonical(&self) -> bool {
39 is_canonical_id(&self.0, 'N')
40 }
41}
42
43impl ClaimId {
44 pub fn new(id: impl Into<String>) -> Self {
46 Self(id.into())
47 }
48
49 pub fn as_str(&self) -> &str {
51 &self.0
52 }
53
54 pub fn is_canonical(&self) -> bool {
56 is_canonical_id(&self.0, 'C')
57 }
58}
59
60impl std::fmt::Display for NodeId {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 f.write_str(&self.0)
63 }
64}
65
66impl std::fmt::Display for ClaimId {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 f.write_str(&self.0)
69 }
70}
71
72pub(crate) fn is_canonical_id(s: &str, prefix: char) -> bool {
75 let mut chars = s.chars();
76 match chars.next() {
77 Some(c) if c == prefix => {}
78 _ => return false,
79 }
80 let rest = chars.as_str();
81 !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit())
82}
83
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86pub struct Manifest {
87 pub nodes: Vec<Node>,
89 pub links: Vec<Link>,
91 pub bindings: Vec<Binding>,
93 pub claims: Vec<Claim>,
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub bounds: Option<Rect>,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub paper: Option<PaperMeta>,
102 #[serde(default, skip_serializing_if = "Vec::is_empty")]
104 pub related_work: Vec<RelatedWork>,
105 #[serde(default, skip_serializing_if = "Vec::is_empty")]
107 pub concepts: Vec<Concept>,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub problem: Option<Problem>,
111 #[serde(default, skip_serializing_if = "Vec::is_empty")]
113 pub recipes: Vec<Recipe>,
114 #[serde(default, skip_serializing_if = "Vec::is_empty")]
116 pub exhibits: Vec<Exhibit>,
117 #[serde(default, skip_serializing_if = "Vec::is_empty")]
119 pub built_on: Vec<BuiltOn>,
120 #[serde(default, skip_serializing_if = "Vec::is_empty")]
122 pub node_exhibits: Vec<NodeExhibit>,
123}
124
125#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
131pub struct PaperMeta {
132 pub title: Option<String>,
134 pub authors: Vec<String>,
136 pub year: Option<String>,
138 pub venue: Option<String>,
140 pub doi: Option<String>,
142 #[serde(rename = "abstract")]
144 pub abstract_: Option<String>,
145 pub keywords: Vec<String>,
147}
148
149#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
151pub struct RelatedWork {
152 pub id: String,
154 pub cite: String,
156 pub doi: Option<String>,
158 pub kind: Option<String>,
161 pub what_changed: Option<String>,
163 pub why: Option<String>,
165 pub adopted: Option<String>,
167 pub claims_affected: Vec<ClaimId>,
170}
171
172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
174pub struct Concept {
175 pub term: String,
177 pub notation: Option<String>,
179 pub definition: Option<String>,
181 pub boundary: Option<String>,
183 pub related: Vec<String>,
185}
186
187#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
189pub struct Problem {
190 pub statement: Option<String>,
192 pub observations: Vec<String>,
194 pub gaps: Vec<String>,
196 pub insights: Vec<String>,
198}
199
200#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
202pub struct Recipe {
203 pub name: String,
205 pub title: Option<String>,
207 pub body: String,
209}
210
211#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
213#[serde(rename_all = "snake_case")]
214pub enum ExhibitKind {
215 Figure,
216 Table,
217 Other,
218}
219
220#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
222pub struct Exhibit {
223 pub id: String,
225 pub file: String,
227 pub kind: ExhibitKind,
229 pub source: Option<String>,
231 pub description: Option<String>,
233 pub claims: Vec<ClaimId>,
235 pub body: String,
237}
238
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
241pub struct BuiltOn {
242 pub node: NodeId,
243 pub related_work: String,
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
248pub struct NodeExhibit {
249 pub node: NodeId,
250 pub exhibit: String,
251}
252
253#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
255pub struct Node {
256 pub id: NodeId,
258 pub kind: NodeKind,
260 pub label: Option<String>,
262 pub support_level: Option<String>,
264 pub source_refs: Vec<String>,
266 pub description: Option<String>,
268 pub fields: NodeFields,
270 pub evidence_notes: Vec<String>,
272 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
277 pub isolated: bool,
278 #[serde(skip_serializing_if = "Option::is_none")]
280 pub pos: Option<Point>,
281}
282
283#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
285#[serde(rename_all = "snake_case")]
286pub enum NodeKind {
287 Question,
288 Experiment,
289 Decision,
290 DeadEnd,
291 Insight,
292 Pivot,
293 Other(String),
295}
296
297#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
299#[serde(rename_all = "snake_case")]
300pub enum NodeFields {
301 Question,
302 Experiment {
303 result: Option<String>,
304 },
305 Decision {
306 choice: Option<String>,
307 alternatives: Vec<String>,
308 rationale: Option<String>,
309 },
310 DeadEnd {
311 hypothesis: Option<String>,
312 failure_mode: Option<String>,
313 lesson: Option<String>,
314 why_failed: Option<String>,
315 },
316 Insight,
317 Pivot {
318 from: Option<String>,
319 to: Option<String>,
320 trigger: Option<String>,
321 },
322 Other,
324}
325
326#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
328pub struct Link {
329 pub from: NodeId,
330 pub to: NodeId,
331 pub kind: LinkKind,
332}
333
334#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
336#[serde(rename_all = "snake_case")]
337pub enum LinkKind {
338 Child,
340 DependsOn,
342}
343
344#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
346pub struct Binding {
347 pub node: NodeId,
348 pub claim: ClaimId,
349 pub role: BindingRole,
350}
351
352#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
357#[serde(rename_all = "snake_case")]
358#[non_exhaustive]
359pub enum BindingRole {
360 Evidence,
362}
363
364#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
366pub struct Claim {
367 pub id: ClaimId,
368 pub title: String,
369 pub statement: Option<String>,
370 pub status: Option<String>,
371 pub proof: Vec<String>,
373 pub deps: Vec<ClaimId>,
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380
381 #[test]
382 fn canonical_id_grammar() {
383 assert!(is_canonical_id("N01", 'N'));
384 assert!(is_canonical_id("N7", 'N'));
385 assert!(is_canonical_id("C123", 'C'));
386 assert!(!is_canonical_id("N", 'N')); assert!(!is_canonical_id("n01", 'N')); assert!(!is_canonical_id("C01", 'N')); assert!(!is_canonical_id("N01a", 'N')); assert!(!is_canonical_id("", 'N'));
391 }
392
393 #[test]
394 fn id_accessors_and_display() {
395 let n = NodeId::new("N01");
396 assert_eq!(n.as_str(), "N01");
397 assert_eq!(n.to_string(), "N01");
398 assert!(n.is_canonical());
399 assert!(!NodeId::new("nope").is_canonical());
400 assert!(ClaimId::new("C02").is_canonical());
401 }
402}