Skip to main content

tensorlogic_adapters/
metadata.rs

1//! Rich metadata system with provenance tracking, documentation, and tagging.
2//!
3//! This module provides a comprehensive metadata system for domains and predicates,
4//! including:
5//! - Provenance tracking (who created/modified, when, from where)
6//! - Version history
7//! - Long-form documentation and examples
8//! - Flexible tagging system for organization and filtering
9//! - Custom attributes for extensibility
10
11use serde::{Deserialize, Serialize};
12use std::collections::{HashMap, HashSet};
13use std::fmt;
14
15/// Rich metadata container for domains and predicates.
16///
17/// This structure captures all metadata associated with a symbol, including
18/// its provenance, documentation, tags, and custom attributes.
19#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
20pub struct Metadata {
21    /// Provenance information (who, when, where)
22    pub provenance: Option<Provenance>,
23    /// Long-form documentation
24    pub documentation: Option<Documentation>,
25    /// Tags for categorization and filtering
26    pub tags: HashSet<String>,
27    /// Custom key-value attributes
28    pub attributes: HashMap<String, String>,
29    /// Version history
30    pub version_history: Vec<VersionEntry>,
31}
32
33/// Provenance information tracking the origin and history of a symbol.
34#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
35pub struct Provenance {
36    /// Who created this symbol (user, system, or tool name)
37    pub created_by: String,
38    /// When it was created (ISO 8601 timestamp)
39    pub created_at: String,
40    /// Source file or location where it was defined
41    pub source_file: Option<String>,
42    /// Source line number
43    pub source_line: Option<usize>,
44    /// Who last modified this symbol
45    pub modified_by: Option<String>,
46    /// When it was last modified (ISO 8601 timestamp)
47    pub modified_at: Option<String>,
48    /// Derivation information (if this symbol was derived from others)
49    pub derived_from: Vec<String>,
50    /// Additional provenance notes
51    pub notes: Option<String>,
52}
53
54/// Long-form documentation with examples and usage notes.
55#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
56pub struct Documentation {
57    /// Brief one-line summary
58    pub summary: String,
59    /// Detailed description (markdown supported)
60    pub description: Option<String>,
61    /// Usage examples
62    pub examples: Vec<Example>,
63    /// Usage notes and best practices
64    pub notes: Vec<String>,
65    /// Related symbols (predicates, domains)
66    pub see_also: Vec<String>,
67}
68
69/// An example demonstrating how to use a symbol.
70#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
71pub struct Example {
72    /// Example title or description
73    pub title: String,
74    /// Example code or expression
75    pub code: String,
76    /// Expected output or result
77    pub expected_output: Option<String>,
78}
79
80/// A version history entry tracking changes to a symbol.
81#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
82pub struct VersionEntry {
83    /// Version number (e.g., "1.0.0", "2.1.3")
84    pub version: String,
85    /// Timestamp of this version (ISO 8601)
86    pub timestamp: String,
87    /// Who made this change
88    pub author: String,
89    /// Description of changes
90    pub changes: String,
91}
92
93impl Metadata {
94    /// Creates a new empty metadata container.
95    pub fn new() -> Self {
96        Metadata::default()
97    }
98
99    /// Creates metadata with provenance information.
100    pub fn with_provenance(provenance: Provenance) -> Self {
101        Metadata {
102            provenance: Some(provenance),
103            ..Default::default()
104        }
105    }
106
107    /// Adds a tag to this metadata.
108    pub fn add_tag(&mut self, tag: impl Into<String>) {
109        self.tags.insert(tag.into());
110    }
111
112    /// Removes a tag from this metadata.
113    pub fn remove_tag(&self, tag: &str) -> bool {
114        self.tags.contains(tag)
115    }
116
117    /// Checks if this metadata has a specific tag.
118    pub fn has_tag(&self, tag: &str) -> bool {
119        self.tags.contains(tag)
120    }
121
122    /// Checks if this metadata has all of the given tags.
123    pub fn has_all_tags(&self, tags: &[String]) -> bool {
124        tags.iter().all(|tag| self.tags.contains(tag))
125    }
126
127    /// Checks if this metadata has any of the given tags.
128    pub fn has_any_tag(&self, tags: &[String]) -> bool {
129        tags.iter().any(|tag| self.tags.contains(tag))
130    }
131
132    /// Sets a custom attribute.
133    pub fn set_attribute(&mut self, key: impl Into<String>, value: impl Into<String>) {
134        self.attributes.insert(key.into(), value.into());
135    }
136
137    /// Gets a custom attribute by key.
138    pub fn get_attribute(&self, key: &str) -> Option<&str> {
139        self.attributes.get(key).map(|s| s.as_str())
140    }
141
142    /// Removes a custom attribute.
143    pub fn remove_attribute(&mut self, key: &str) -> Option<String> {
144        self.attributes.remove(key)
145    }
146
147    /// Adds a version entry to the history.
148    pub fn add_version(
149        &mut self,
150        version: impl Into<String>,
151        timestamp: impl Into<String>,
152        author: impl Into<String>,
153        changes: impl Into<String>,
154    ) {
155        self.version_history.push(VersionEntry {
156            version: version.into(),
157            timestamp: timestamp.into(),
158            author: author.into(),
159            changes: changes.into(),
160        });
161    }
162
163    /// Gets the latest version from the history.
164    pub fn latest_version(&self) -> Option<&VersionEntry> {
165        self.version_history.last()
166    }
167
168    /// Sets the documentation for this symbol.
169    pub fn set_documentation(&mut self, doc: Documentation) {
170        self.documentation = Some(doc);
171    }
172
173    /// Gets the documentation summary, if available.
174    pub fn get_summary(&self) -> Option<&str> {
175        self.documentation.as_ref().map(|d| d.summary.as_str())
176    }
177}
178
179impl Provenance {
180    /// Creates a new provenance record.
181    pub fn new(created_by: impl Into<String>, created_at: impl Into<String>) -> Self {
182        Provenance {
183            created_by: created_by.into(),
184            created_at: created_at.into(),
185            source_file: None,
186            source_line: None,
187            modified_by: None,
188            modified_at: None,
189            derived_from: Vec::new(),
190            notes: None,
191        }
192    }
193
194    /// Sets the source file location.
195    pub fn with_source(mut self, file: impl Into<String>, line: Option<usize>) -> Self {
196        self.source_file = Some(file.into());
197        self.source_line = line;
198        self
199    }
200
201    /// Marks this symbol as modified.
202    pub fn mark_modified(
203        &mut self,
204        modified_by: impl Into<String>,
205        modified_at: impl Into<String>,
206    ) {
207        self.modified_by = Some(modified_by.into());
208        self.modified_at = Some(modified_at.into());
209    }
210
211    /// Adds a derivation source.
212    pub fn add_derivation(&mut self, source: impl Into<String>) {
213        self.derived_from.push(source.into());
214    }
215
216    /// Sets provenance notes.
217    pub fn with_notes(mut self, notes: impl Into<String>) -> Self {
218        self.notes = Some(notes.into());
219        self
220    }
221}
222
223impl Documentation {
224    /// Creates new documentation with a summary.
225    pub fn new(summary: impl Into<String>) -> Self {
226        Documentation {
227            summary: summary.into(),
228            description: None,
229            examples: Vec::new(),
230            notes: Vec::new(),
231            see_also: Vec::new(),
232        }
233    }
234
235    /// Sets the detailed description.
236    pub fn with_description(mut self, description: impl Into<String>) -> Self {
237        self.description = Some(description.into());
238        self
239    }
240
241    /// Adds an example.
242    pub fn add_example(&mut self, example: Example) {
243        self.examples.push(example);
244    }
245
246    /// Adds a usage note.
247    pub fn add_note(&mut self, note: impl Into<String>) {
248        self.notes.push(note.into());
249    }
250
251    /// Adds a related symbol reference.
252    pub fn add_see_also(&mut self, symbol: impl Into<String>) {
253        self.see_also.push(symbol.into());
254    }
255}
256
257impl Example {
258    /// Creates a new example.
259    pub fn new(title: impl Into<String>, code: impl Into<String>) -> Self {
260        Example {
261            title: title.into(),
262            code: code.into(),
263            expected_output: None,
264        }
265    }
266
267    /// Sets the expected output.
268    pub fn with_output(mut self, output: impl Into<String>) -> Self {
269        self.expected_output = Some(output.into());
270        self
271    }
272}
273
274impl fmt::Display for Provenance {
275    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276        writeln!(f, "Created by: {} at {}", self.created_by, self.created_at)?;
277        if let Some(ref file) = self.source_file {
278            write!(f, "Source: {}", file)?;
279            if let Some(line) = self.source_line {
280                write!(f, ":{}", line)?;
281            }
282            writeln!(f)?;
283        }
284        if let Some(ref modified_by) = self.modified_by {
285            if let Some(ref modified_at) = self.modified_at {
286                writeln!(f, "Modified by: {} at {}", modified_by, modified_at)?;
287            }
288        }
289        if !self.derived_from.is_empty() {
290            writeln!(f, "Derived from: {}", self.derived_from.join(", "))?;
291        }
292        Ok(())
293    }
294}
295
296/// A tag category for organizing tags into groups.
297#[derive(Clone, Debug, Serialize, Deserialize)]
298pub struct TagCategory {
299    /// Category name (e.g., "domain", "application", "status")
300    pub name: String,
301    /// Human-readable description
302    pub description: Option<String>,
303    /// Tags in this category
304    pub tags: HashSet<String>,
305}
306
307impl TagCategory {
308    /// Creates a new tag category.
309    pub fn new(name: impl Into<String>) -> Self {
310        TagCategory {
311            name: name.into(),
312            description: None,
313            tags: HashSet::new(),
314        }
315    }
316
317    /// Sets the category description.
318    pub fn with_description(mut self, description: impl Into<String>) -> Self {
319        self.description = Some(description.into());
320        self
321    }
322
323    /// Adds a tag to this category.
324    pub fn add_tag(&mut self, tag: impl Into<String>) {
325        self.tags.insert(tag.into());
326    }
327
328    /// Checks if a tag belongs to this category.
329    pub fn contains(&self, tag: &str) -> bool {
330        self.tags.contains(tag)
331    }
332}
333
334/// A registry of tag categories for organizing the tag namespace.
335#[derive(Clone, Debug, Default, Serialize, Deserialize)]
336pub struct TagRegistry {
337    categories: HashMap<String, TagCategory>,
338}
339
340impl TagRegistry {
341    /// Creates a new empty tag registry.
342    pub fn new() -> Self {
343        TagRegistry::default()
344    }
345
346    /// Registers a tag category.
347    pub fn register_category(&mut self, category: TagCategory) {
348        self.categories.insert(category.name.clone(), category);
349    }
350
351    /// Gets a category by name.
352    pub fn get_category(&self, name: &str) -> Option<&TagCategory> {
353        self.categories.get(name)
354    }
355
356    /// Finds which category a tag belongs to.
357    pub fn find_category_for_tag(&self, tag: &str) -> Option<&str> {
358        self.categories
359            .iter()
360            .find(|(_, cat)| cat.contains(tag))
361            .map(|(name, _)| name.as_str())
362    }
363
364    /// Creates a standard tag registry with common categories.
365    pub fn standard() -> Self {
366        let mut registry = TagRegistry::new();
367
368        let mut domain_cat =
369            TagCategory::new("domain").with_description("Tags related to problem domains");
370        domain_cat.add_tag("person");
371        domain_cat.add_tag("location");
372        domain_cat.add_tag("time");
373        domain_cat.add_tag("organization");
374        registry.register_category(domain_cat);
375
376        let mut status_cat =
377            TagCategory::new("status").with_description("Tags related to development status");
378        status_cat.add_tag("experimental");
379        status_cat.add_tag("stable");
380        status_cat.add_tag("deprecated");
381        registry.register_category(status_cat);
382
383        let mut application_cat =
384            TagCategory::new("application").with_description("Tags related to application areas");
385        application_cat.add_tag("reasoning");
386        application_cat.add_tag("learning");
387        application_cat.add_tag("planning");
388        application_cat.add_tag("inference");
389        registry.register_category(application_cat);
390
391        registry
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398
399    #[test]
400    fn test_metadata_tags() {
401        let mut meta = Metadata::new();
402        meta.add_tag("experimental");
403        meta.add_tag("reasoning");
404
405        assert!(meta.has_tag("experimental"));
406        assert!(meta.has_tag("reasoning"));
407        assert!(!meta.has_tag("stable"));
408
409        assert!(meta.has_all_tags(&["experimental".to_string(), "reasoning".to_string()]));
410        assert!(!meta.has_all_tags(&["experimental".to_string(), "stable".to_string()]));
411
412        assert!(meta.has_any_tag(&["experimental".to_string(), "stable".to_string()]));
413    }
414
415    #[test]
416    fn test_metadata_attributes() {
417        let mut meta = Metadata::new();
418        meta.set_attribute("complexity", "O(n^2)");
419        meta.set_attribute("author", "Alice");
420
421        assert_eq!(meta.get_attribute("complexity"), Some("O(n^2)"));
422        assert_eq!(meta.get_attribute("author"), Some("Alice"));
423        assert_eq!(meta.get_attribute("unknown"), None);
424
425        meta.remove_attribute("complexity");
426        assert_eq!(meta.get_attribute("complexity"), None);
427    }
428
429    #[test]
430    fn test_provenance() {
431        let prov = Provenance::new("Alice", "2025-01-15T10:30:00Z")
432            .with_source("rules.tl", Some(42))
433            .with_notes("Imported from legacy system");
434
435        assert_eq!(prov.created_by, "Alice");
436        assert_eq!(prov.created_at, "2025-01-15T10:30:00Z");
437        assert_eq!(prov.source_file, Some("rules.tl".to_string()));
438        assert_eq!(prov.source_line, Some(42));
439    }
440
441    #[test]
442    fn test_provenance_modification() {
443        let mut prov = Provenance::new("Alice", "2025-01-15T10:30:00Z");
444        prov.mark_modified("Bob", "2025-01-16T14:20:00Z");
445
446        assert_eq!(prov.modified_by, Some("Bob".to_string()));
447        assert_eq!(prov.modified_at, Some("2025-01-16T14:20:00Z".to_string()));
448    }
449
450    #[test]
451    fn test_provenance_derivation() {
452        let mut prov = Provenance::new("System", "2025-01-15T10:30:00Z");
453        prov.add_derivation("BaseRule");
454        prov.add_derivation("Optimization");
455
456        assert_eq!(prov.derived_from.len(), 2);
457        assert!(prov.derived_from.contains(&"BaseRule".to_string()));
458    }
459
460    #[test]
461    fn test_documentation() {
462        let mut doc = Documentation::new("A predicate for checking person relationships")
463            .with_description(
464                "This predicate checks if two persons have a specific relationship type",
465            );
466
467        doc.add_example(Example::new("Basic usage", "knows(alice, bob)"));
468        doc.add_note("This predicate is symmetric");
469        doc.add_see_also("friend");
470        doc.add_see_also("family");
471
472        assert_eq!(doc.summary, "A predicate for checking person relationships");
473        assert_eq!(doc.examples.len(), 1);
474        assert_eq!(doc.notes.len(), 1);
475        assert_eq!(doc.see_also.len(), 2);
476    }
477
478    #[test]
479    fn test_example() {
480        let example =
481            Example::new("Simple query", "Person(x)").with_output("[alice, bob, charlie]");
482
483        assert_eq!(example.title, "Simple query");
484        assert_eq!(example.code, "Person(x)");
485        assert_eq!(
486            example.expected_output,
487            Some("[alice, bob, charlie]".to_string())
488        );
489    }
490
491    #[test]
492    fn test_version_history() {
493        let mut meta = Metadata::new();
494        meta.add_version("1.0.0", "2025-01-15T10:00:00Z", "Alice", "Initial version");
495        meta.add_version("1.1.0", "2025-01-20T15:30:00Z", "Bob", "Added constraints");
496
497        assert_eq!(meta.version_history.len(), 2);
498
499        let latest = meta.latest_version().unwrap();
500        assert_eq!(latest.version, "1.1.0");
501        assert_eq!(latest.author, "Bob");
502    }
503
504    #[test]
505    fn test_tag_category() {
506        let mut category = TagCategory::new("domain").with_description("Problem domain tags");
507
508        category.add_tag("person");
509        category.add_tag("location");
510
511        assert_eq!(category.name, "domain");
512        assert!(category.contains("person"));
513        assert!(!category.contains("experimental"));
514    }
515
516    #[test]
517    fn test_tag_registry() {
518        let mut registry = TagRegistry::new();
519
520        let mut domain_cat = TagCategory::new("domain");
521        domain_cat.add_tag("person");
522        domain_cat.add_tag("location");
523        registry.register_category(domain_cat);
524
525        let category = registry.get_category("domain").unwrap();
526        assert!(category.contains("person"));
527
528        let found_category = registry.find_category_for_tag("person");
529        assert_eq!(found_category, Some("domain"));
530    }
531
532    #[test]
533    fn test_standard_tag_registry() {
534        let registry = TagRegistry::standard();
535
536        assert!(registry.get_category("domain").is_some());
537        assert!(registry.get_category("status").is_some());
538        assert!(registry.get_category("application").is_some());
539
540        assert_eq!(
541            registry.find_category_for_tag("experimental"),
542            Some("status")
543        );
544        assert_eq!(registry.find_category_for_tag("person"), Some("domain"));
545        assert_eq!(
546            registry.find_category_for_tag("reasoning"),
547            Some("application")
548        );
549    }
550}