Skip to main content

nibli_semantics/
dictionary.rs

1//! Predicate arities — a thin facade over the SINGLE arity source.
2//!
3//! Arity comes from `nibli_lexicon`'s committed English corpus (also the
4//! source of nibli-reason's `SignatureSource`). nibli-semantics delegates here
5//! rather than generating its own parallel arity map, so the compiler arity
6//! (`get_arity_or_default`, driving `fit_args`/`event_decompose`) and the
7//! corpus arity are the SAME value by construction and cannot diverge.
8
9/// Interface to the jbovlaste arity dictionary (delegates to `nibli_lexicon`).
10pub struct LexiconSchema;
11
12impl LexiconSchema {
13    /// Retrieves the arity of a predicate. The canonical relation names in the
14    /// IR are ENGLISH corpus names (or compound relation idents);
15    /// `nibli_lexicon::get_arity` resolves them directly. Returns None for
16    /// unknown words — gismu spellings never resolve (provenance only).
17    pub fn get_arity(word: &str) -> Option<usize> {
18        nibli_lexicon::get_arity(word)
19    }
20
21    /// Retrieves the arity, defaulting to 2 for unknown words.
22    /// Use this only when a fallback is acceptable (an unknown relation).
23    pub fn get_arity_or_default(word: &str) -> usize {
24        Self::get_arity(word).unwrap_or(2)
25    }
26
27    /// Arity policy for PROGRAMMATICALLY injected facts (the `:assert` /
28    /// WIT `assert-fact` / RDF-import seam — and the NIBLI_KR §14.1 hook
29    /// where an injectable schema registry will eventually sit): a KNOWN
30    /// relation uses its corpus arity, failing closed on over-arity
31    /// (mirroring the text path's reject — never a silent truncation); an
32    /// UNKNOWN relation trusts the caller's argument count as ground truth
33    /// (no arity-2 guess).
34    pub fn injected_arity(relation: &str, provided: usize) -> Result<usize, String> {
35        match Self::get_arity(relation) {
36            Some(a) if provided > a => Err(format!(
37                "{relation:?} has arity {a}, but {provided} arguments were supplied — \
38                 refusing to silently drop the extras"
39            )),
40            Some(a) => Ok(a),
41            None => Ok(provided),
42        }
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    // ─── get_arity tests ─────────────────────────────────────────
51
52    #[test]
53    fn test_get_arity_known_alias_goes() {
54        // goes (klama) is the canonical 5-place motion predicate
55        let arity = LexiconSchema::get_arity("goes");
56        assert!(arity.is_some());
57        assert_eq!(arity.unwrap(), 5);
58    }
59
60    #[test]
61    fn test_get_arity_known_alias_dog() {
62        // dog (gerku) is a standard 2-place class predicate
63        let arity = LexiconSchema::get_arity("dog");
64        assert!(arity.is_some());
65        assert_eq!(arity.unwrap(), 2);
66    }
67
68    #[test]
69    fn test_get_arity_known_alias_loves() {
70        // loves (prami) is a standard 2-place predicate
71        let arity = LexiconSchema::get_arity("loves");
72        assert!(arity.is_some());
73        assert_eq!(arity.unwrap(), 2);
74    }
75
76    #[test]
77    fn test_get_arity_known_alias_talks() {
78        // talks (tavla) has 4 places
79        let arity = LexiconSchema::get_arity("talks");
80        assert!(arity.is_some());
81        assert_eq!(arity.unwrap(), 4);
82    }
83
84    #[test]
85    fn test_gismu_never_resolves() {
86        // GISMU-INPUT DEATH: the raw gismu spelling is provenance metadata,
87        // never a resolvable word — English corpus names only.
88        assert_eq!(LexiconSchema::get_arity("klama"), None);
89        assert_eq!(LexiconSchema::get_arity("gerku"), None);
90    }
91
92    #[test]
93    fn test_get_arity_unknown_word_returns_none() {
94        assert_eq!(LexiconSchema::get_arity("zzzzz"), None);
95    }
96
97    #[test]
98    fn test_get_arity_empty_string_returns_none() {
99        assert_eq!(LexiconSchema::get_arity(""), None);
100    }
101
102    #[test]
103    fn test_get_arity_cmavo_not_in_dict() {
104        // cmavo are not predicates — should not be in the dictionary
105        assert_eq!(LexiconSchema::get_arity("lo"), None);
106        assert_eq!(LexiconSchema::get_arity("cu"), None);
107    }
108
109    // ─── get_arity_or_default tests ──────────────────────────────
110
111    #[test]
112    fn test_get_arity_or_default_known_alias() {
113        assert_eq!(LexiconSchema::get_arity_or_default("goes"), 5);
114    }
115
116    #[test]
117    fn test_get_arity_or_default_unknown_returns_two() {
118        assert_eq!(LexiconSchema::get_arity_or_default("xyzzy"), 2);
119    }
120
121    #[test]
122    fn test_get_arity_or_default_empty_returns_two() {
123        assert_eq!(LexiconSchema::get_arity_or_default(""), 2);
124    }
125
126    #[test]
127    fn test_get_arity_english_alias() {
128        // Since the predicate-name flip, the canonical relation is the English
129        // alias; arity must resolve through the alias map too (goes = klama, 5).
130        assert_eq!(LexiconSchema::get_arity("goes"), Some(5));
131        assert_eq!(LexiconSchema::get_arity("dog"), Some(2));
132        assert_eq!(LexiconSchema::get_arity_or_default("goes"), 5);
133    }
134
135    // ─── Dictionary coverage spot-checks ─────────────────────────
136
137    #[test]
138    fn test_various_alias_arities() {
139        // Spot-check a range of common curated aliases with different arities
140        let checks = vec![
141            ("cat", 2),      // mlatu: x1 is a cat of species x2
142            ("big", 3),      // barda: x1 is big in property x2 by standard x3
143            ("fast", 2),     // sutra: x1 is fast at x2
144            ("person", 1),   // prenu: x1 is a person
145            ("name", 3),     // cmene: x1 is a name of x2 used by x3
146            ("gives", 3),    // dunda: x1 gives x2 to x3
147            ("product", 3),  // pilji: x1 is product of x2 and x3
148            ("sum", 3),      // sumji: x1 is sum of x2 and x3
149            ("quantity", 3), // klani: x1 measures x2 on scale x3
150        ];
151        for (word, expected) in checks {
152            let actual = LexiconSchema::get_arity(word);
153            assert!(actual.is_some(), "expected {} to resolve", word);
154            assert_eq!(
155                actual.unwrap(),
156                expected,
157                "{} should have arity {}, got {}",
158                word,
159                expected,
160                actual.unwrap()
161            );
162        }
163    }
164
165    #[test]
166    fn test_unknown_word_arity_is_none() {
167        // A Lojban word-class term is not a corpus name — no resolution, no panic.
168        assert_eq!(LexiconSchema::get_arity("brivla"), None);
169    }
170
171    #[test]
172    fn test_injected_arity_policy() {
173        // Known relation: corpus arity; under-arity pads, over-arity ERRORS.
174        assert_eq!(LexiconSchema::injected_arity("product", 3), Ok(3));
175        assert_eq!(LexiconSchema::injected_arity("product", 1), Ok(3));
176        let e = LexiconSchema::injected_arity("product", 4).unwrap_err();
177        assert!(e.contains("arity 3") && e.contains("4 arguments"), "{e}");
178        // Unknown relation: the caller's count is ground truth — no arity-2 guess.
179        assert_eq!(LexiconSchema::injected_arity("zzz_unknown", 1), Ok(1));
180        assert_eq!(LexiconSchema::injected_arity("zzz_unknown", 5), Ok(5));
181    }
182
183    #[test]
184    fn test_get_arity_consistent_with_default() {
185        // For known words, both methods should agree
186        let word = "goes";
187        let arity = LexiconSchema::get_arity(word).unwrap();
188        let default_arity = LexiconSchema::get_arity_or_default(word);
189        assert_eq!(arity, default_arity);
190    }
191}