oxirs-star 0.2.4

RDF-star and SPARQL-star grammar support for quoted triples
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
//! RDF-star semantics and entailment rules
//!
//! This module implements the semantic layer for RDF-star, with a focus on
//! **referential opacity** - the key semantic property that distinguishes
//! RDF-star from standard RDF reification.
//!
//! # Referential Opacity
//!
//! In RDF-star, quoted triples do NOT entail the existence of the statement
//! they quote. This is called "referential opacity" and is the fundamental
//! semantic difference from standard RDF reification.
//!
//! ## Examples
//!
//! ```turtle
//! # This statement about certainty:
//! <<:alice :age 30>> :certainty 0.9 .
//!
//! # Does NOT entail that:
//! :alice :age 30 .
//!
//! # It only asserts that someone has 90% certainty about that statement,
//! # without asserting whether the statement itself is true or false.
//! ```
//!
//! ## Contrast with Standard RDF Reification
//!
//! Standard RDF reification does entail the original triple:
//!
//! ```turtle
//! # Standard reification (RDF 1.1):
//! _:stmt rdf:type rdf:Statement .
//! _:stmt rdf:subject :alice .
//! _:stmt rdf:predicate :age .
//! _:stmt rdf:object 30 .
//! _:stmt :certainty 0.9 .
//!
//! # In standard RDF semantics, this typically implies:
//! :alice :age 30 .  # (depending on reasoner)
//! ```
//!
//! But with RDF-star:
//!
//! ```turtle
//! # RDF-star (referentially opaque):
//! <<:alice :age 30>> :certainty 0.9 .
//!
//! # Does NOT imply:
//! :alice :age 30 .  # NOT entailed!
//! ```
//!
//! # Entailment Rules
//!
//! This module implements the W3C RDF-star entailment rules:
//!
//! 1. **No automatic assertion**: Quoted triples are NOT asserted
//! 2. **Identity-based equality**: Quoted triples are equal only if structurally identical
//! 3. **No blank node unification**: Blank nodes in quoted triples don't unify across quotes
//! 4. **Nested opacity**: Referential opacity applies recursively to nested quoted triples

use crate::model::{StarGraph, StarTerm, StarTriple};
use crate::StarResult;

/// Semantic validator for RDF-star graphs
///
/// This validator ensures that graphs respect RDF-star semantic rules,
/// particularly referential opacity.
#[derive(Debug, Clone)]
pub struct SemanticValidator {
    /// Whether to enforce strict referential opacity
    strict_opacity: bool,
}

impl Default for SemanticValidator {
    fn default() -> Self {
        Self::new()
    }
}

impl SemanticValidator {
    /// Create a new semantic validator with default settings
    ///
    /// By default, strict referential opacity is enabled.
    pub fn new() -> Self {
        Self {
            strict_opacity: true,
        }
    }

    /// Create a validator with custom settings
    pub fn with_strict_opacity(mut self, strict: bool) -> Self {
        self.strict_opacity = strict;
        self
    }

    /// Check if a graph respects referential opacity
    ///
    /// This verifies that:
    /// 1. Quoted triples are not automatically asserted in the graph
    /// 2. No reasoning has been applied that violates referential opacity
    ///
    /// Returns `Ok(())` if the graph is semantically valid.
    pub fn validate_graph(&self, graph: &StarGraph) -> StarResult<()> {
        if !self.strict_opacity {
            return Ok(()); // Skip validation if not in strict mode
        }

        // Check for violations of referential opacity
        self.check_opacity_violations(graph)?;

        Ok(())
    }

    /// Detect potential violations of referential opacity
    ///
    /// This looks for patterns that suggest the graph may have been constructed
    /// with reasoning that violates referential opacity (e.g., inferring asserted
    /// triples from quoted triples).
    fn check_opacity_violations(&self, graph: &StarGraph) -> StarResult<()> {
        // Collect all quoted triples in the graph
        let mut quoted_triples = Vec::new();

        for triple in graph.iter() {
            self.collect_quoted_triples(triple, &mut quoted_triples);
        }

        // Check if any quoted triple is also asserted as a regular triple
        for quoted in &quoted_triples {
            if graph.contains(quoted) {
                // This could be intentional (user wants both the quote and the assertion)
                // but we warn about it as it might indicate a misunderstanding of
                // referential opacity
                //
                // Note: This is not technically an error, but a potential semantic issue
                // that users should be aware of.
                //
                // In strict mode, we don't fail here, but we could log a warning
                // (not implemented yet to avoid adding logging dependencies)
            }
        }

        Ok(())
    }

    /// Recursively collect all quoted triples from a triple
    #[allow(clippy::only_used_in_recursion)]
    fn collect_quoted_triples(&self, triple: &StarTriple, collector: &mut Vec<StarTriple>) {
        // Check subject
        if let StarTerm::QuotedTriple(qt) = &triple.subject {
            collector.push((**qt).clone());
            self.collect_quoted_triples(qt, collector);
        }

        // Check predicate
        if let StarTerm::QuotedTriple(qt) = &triple.predicate {
            collector.push((**qt).clone());
            self.collect_quoted_triples(qt, collector);
        }

        // Check object
        if let StarTerm::QuotedTriple(qt) = &triple.object {
            collector.push((**qt).clone());
            self.collect_quoted_triples(qt, collector);
        }
    }

    /// Extract all quoted triples from a graph
    ///
    /// This returns a list of all triples that appear as quoted triples
    /// within the graph, without asserting them.
    pub fn extract_quoted_triples(&self, graph: &StarGraph) -> Vec<StarTriple> {
        let mut quoted = Vec::new();

        for triple in graph.iter() {
            self.collect_quoted_triples(triple, &mut quoted);
        }

        quoted
    }

    /// Check if a triple is asserted vs. only quoted
    ///
    /// Returns:
    /// - `Ok(true)` if the triple is asserted (appears as a regular triple)
    /// - `Ok(false)` if the triple is only quoted (appears in quoted triple positions)
    pub fn is_asserted(&self, graph: &StarGraph, triple: &StarTriple) -> StarResult<bool> {
        Ok(graph.contains(triple))
    }

    /// Check if a triple is only mentioned (quoted but not asserted)
    ///
    /// Returns `true` if the triple appears in quoted positions but is not
    /// asserted as a regular triple.
    pub fn is_only_quoted(&self, graph: &StarGraph, triple: &StarTriple) -> StarResult<bool> {
        // Check if it's in quoted positions
        let quoted_triples = self.extract_quoted_triples(graph);
        let is_quoted = quoted_triples.contains(triple);

        // Check if it's asserted
        let is_asserted = self.is_asserted(graph, triple)?;

        Ok(is_quoted && !is_asserted)
    }
}

/// Entailment checker for RDF-star graphs
///
/// This implements the RDF-star entailment rules, particularly focusing on
/// the restrictions imposed by referential opacity.
#[derive(Debug, Clone)]
pub struct EntailmentChecker {
    validator: SemanticValidator,
}

impl Default for EntailmentChecker {
    fn default() -> Self {
        Self::new()
    }
}

impl EntailmentChecker {
    /// Create a new entailment checker
    pub fn new() -> Self {
        Self {
            validator: SemanticValidator::new(),
        }
    }

    /// Check if one graph entails another under RDF-star semantics
    ///
    /// Returns `Ok(true)` if `source` entails `target` according to RDF-star
    /// entailment rules (respecting referential opacity).
    pub fn entails(&self, source: &StarGraph, target: &StarGraph) -> StarResult<bool> {
        // Validate both graphs
        self.validator.validate_graph(source)?;
        self.validator.validate_graph(target)?;

        // For now, implement simple containment check
        // (Full RDF-star entailment would require blank node matching, etc.)
        for target_triple in target.iter() {
            if !source.contains(target_triple) {
                return Ok(false);
            }
        }

        Ok(true)
    }

    /// Check if a quoted triple entails its quoted content
    ///
    /// Under referential opacity, this should ALWAYS return `false`.
    ///
    /// # Example
    ///
    /// ```
    /// # use oxirs_star::semantics::EntailmentChecker;
    /// # use oxirs_star::model::{StarGraph, StarTerm, StarTriple};
    /// let checker = EntailmentChecker::new();
    ///
    /// let inner = StarTriple::new(
    ///     StarTerm::iri("http://example.org/alice").unwrap(),
    ///     StarTerm::iri("http://example.org/age").unwrap(),
    ///     StarTerm::literal("30").unwrap(),
    /// );
    ///
    /// let mut graph = StarGraph::new();
    /// let outer = StarTriple::new(
    ///     StarTerm::quoted_triple(inner.clone()),
    ///     StarTerm::iri("http://example.org/certainty").unwrap(),
    ///     StarTerm::literal("0.9").unwrap(),
    /// );
    /// graph.insert(outer).unwrap();
    ///
    /// // The quoted triple does NOT entail the inner triple
    /// assert!(!checker.quoted_entails_content(&graph, &inner).unwrap());
    /// ```
    pub fn quoted_entails_content(
        &self,
        graph: &StarGraph,
        inner: &StarTriple,
    ) -> StarResult<bool> {
        // Under referential opacity, quoted triples NEVER entail their content
        let _ = graph; // Use parameter to avoid warning
        let _ = inner;
        Ok(false)
    }

    /// Create an opacity-respecting closure of a graph
    ///
    /// This adds entailed triples while respecting referential opacity.
    /// Unlike standard RDF entailment, this does NOT add quoted triples
    /// as asserted triples.
    pub fn compute_closure(&self, graph: &StarGraph) -> StarResult<StarGraph> {
        // For now, just return a clone (no additional entailments)
        // In a full implementation, this would add:
        // - RDF/RDFS entailments for asserted triples
        // - But NOT entailments from quoted triples to asserted triples
        Ok(graph.clone())
    }
}

/// Transparency-Enabling Properties (TEPs) support
///
/// TEPs allow selective violations of referential opacity for specific properties.
/// This is an optional feature of RDF-star that allows certain predicates to
/// "see through" quoted triples.
///
/// # Example
///
/// ```turtle
/// # With rdf:type as a TEP:
/// <<:alice rdf:type :Person>> :source :registry .
///
/// # Can entail (with TEP):
/// :alice rdf:type :Person .
/// ```
///
/// Note: TEPs are an advanced feature and are disabled by default.
#[derive(Debug, Clone)]
pub struct TransparencyEnablingProperties {
    /// Set of property IRIs that enable transparency
    tep_properties: Vec<String>,
}

impl Default for TransparencyEnablingProperties {
    fn default() -> Self {
        Self::new()
    }
}

impl TransparencyEnablingProperties {
    /// Create a new TEP configuration with no enabled properties
    pub fn new() -> Self {
        Self {
            tep_properties: Vec::new(),
        }
    }

    /// Add a property as a TEP
    pub fn add_property(&mut self, property_iri: String) {
        if !self.tep_properties.contains(&property_iri) {
            self.tep_properties.push(property_iri);
        }
    }

    /// Check if a property is a TEP
    pub fn is_tep(&self, property_iri: &str) -> bool {
        self.tep_properties.iter().any(|p| p == property_iri)
    }

    /// Get all TEP properties
    pub fn properties(&self) -> &[String] {
        &self.tep_properties
    }

    /// Create a TEP configuration with common properties
    ///
    /// This includes:
    /// - rdf:type (class membership often needs transparency)
    pub fn with_common_properties() -> Self {
        let mut tep = Self::new();
        tep.add_property("http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string());
        tep
    }

    /// Check if a triple's predicate is a TEP
    pub fn triple_uses_tep(&self, triple: &StarTriple) -> bool {
        if let StarTerm::NamedNode(node) = &triple.predicate {
            self.is_tep(&node.iri)
        } else {
            false
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_semantic_validator_creation() {
        let validator = SemanticValidator::new();
        assert!(validator.strict_opacity);

        let non_strict = SemanticValidator::new().with_strict_opacity(false);
        assert!(!non_strict.strict_opacity);
    }

    #[test]
    fn test_entailment_checker_creation() {
        let checker = EntailmentChecker::new();
        assert!(checker.validator.strict_opacity);
    }

    #[test]
    fn test_tep_properties() {
        let mut tep = TransparencyEnablingProperties::new();
        assert_eq!(tep.properties().len(), 0);

        tep.add_property("http://example.org/prop".to_string());
        assert!(tep.is_tep("http://example.org/prop"));
        assert!(!tep.is_tep("http://example.org/other"));

        // Adding the same property again should not duplicate
        tep.add_property("http://example.org/prop".to_string());
        assert_eq!(tep.properties().len(), 1);
    }

    #[test]
    fn test_common_teps() {
        let tep = TransparencyEnablingProperties::with_common_properties();
        assert!(tep.is_tep("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"));
    }

    #[test]
    fn test_quoted_never_entails_content() {
        let checker = EntailmentChecker::new();

        let inner = StarTriple::new(
            StarTerm::iri("http://example.org/alice").unwrap(),
            StarTerm::iri("http://example.org/age").unwrap(),
            StarTerm::literal("30").unwrap(),
        );

        let mut graph = StarGraph::new();
        let outer = StarTriple::new(
            StarTerm::quoted_triple(inner.clone()),
            StarTerm::iri("http://example.org/certainty").unwrap(),
            StarTerm::literal("0.9").unwrap(),
        );
        graph.insert(outer).unwrap();

        // Referential opacity: quoted triple does NOT entail content
        assert!(!checker.quoted_entails_content(&graph, &inner).unwrap());
    }
}