1use crate::knowledge::{EntityTypeRef, PredicateRef};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum Cardinality {
11 Functional,
12 MultiValued,
13}
14
15impl Cardinality {
16 pub fn as_db(&self) -> &'static str {
17 match self {
18 Self::Functional => "functional",
19 Self::MultiValued => "multi_valued",
20 }
21 }
22 pub fn parse_db(s: &str) -> Option<Self> {
23 match s {
24 "functional" => Some(Self::Functional),
25 "multi_valued" => Some(Self::MultiValued),
26 _ => None,
27 }
28 }
29}
30
31#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub enum Temporality {
34 Static,
35 Interval,
36 Point,
37}
38
39impl Temporality {
40 pub fn as_db(&self) -> &'static str {
41 match self {
42 Self::Static => "static",
43 Self::Interval => "interval",
44 Self::Point => "point",
45 }
46 }
47 pub fn parse_db(s: &str) -> Option<Self> {
48 match s {
49 "static" => Some(Self::Static),
50 "interval" => Some(Self::Interval),
51 "point" => Some(Self::Point),
52 _ => None,
53 }
54 }
55}
56
57#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
58#[serde(rename_all = "snake_case")]
59pub enum Invalidation {
60 Supersede,
61 Coexist,
62 ExplicitOnly,
63}
64
65impl Invalidation {
66 pub fn as_db(&self) -> &'static str {
67 match self {
68 Self::Supersede => "supersede",
69 Self::Coexist => "coexist",
70 Self::ExplicitOnly => "explicit_only",
71 }
72 }
73 pub fn parse_db(s: &str) -> Option<Self> {
74 match s {
75 "supersede" => Some(Self::Supersede),
76 "coexist" => Some(Self::Coexist),
77 "explicit_only" => Some(Self::ExplicitOnly),
78 _ => None,
79 }
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct EntityTypes(pub Vec<EntityTypeRef>);
88
89impl<const N: usize> From<[&str; N]> for EntityTypes {
90 fn from(types: [&str; N]) -> Self {
91 Self(types.iter().map(|s| (*s).to_string()).collect())
92 }
93}
94
95impl From<&str> for EntityTypes {
96 fn from(t: &str) -> Self {
97 Self(vec![t.to_string()])
98 }
99}
100
101impl Serialize for EntityTypes {
102 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
103 where
104 S: serde::Serializer,
105 {
106 serializer.collect_seq(self.0.iter())
107 }
108}
109
110impl<'de> Deserialize<'de> for EntityTypes {
111 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
112 where
113 D: serde::Deserializer<'de>,
114 {
115 struct EntityTypesVisitor;
116
117 impl<'de> serde::de::Visitor<'de> for EntityTypesVisitor {
118 type Value = EntityTypes;
119
120 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 f.write_str("an entity type string or an array of entity type strings")
122 }
123
124 fn visit_str<E>(self, v: &str) -> Result<EntityTypes, E>
125 where
126 E: serde::de::Error,
127 {
128 Ok(EntityTypes(vec![v.to_string()]))
129 }
130
131 fn visit_seq<A>(self, mut seq: A) -> Result<EntityTypes, A::Error>
132 where
133 A: serde::de::SeqAccess<'de>,
134 {
135 let mut out = Vec::with_capacity(seq.size_hint().unwrap_or(0));
136 while let Some(t) = seq.next_element::<String>()? {
137 out.push(t);
138 }
139 Ok(EntityTypes(out))
140 }
141 }
142
143 deserializer.deserialize_any(EntityTypesVisitor)
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
151#[serde(untagged)]
152pub enum ObjectKind {
153 Literal(LiteralType),
154 Entity(EntityTypes),
155 Enum { variants: Vec<String> },
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
159#[serde(rename_all = "snake_case")]
160pub enum LiteralType {
161 Text,
162 Date,
163 DateTime,
164 Quantity { unit: String },
165 Number,
166 Bool,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct PredicateDef {
171 pub name: PredicateRef,
172 pub object_kind: ObjectKind,
173 pub subject_types: Vec<EntityTypeRef>,
174 pub cardinality: Cardinality,
175 pub temporality: Temporality,
176 pub invalidation: Invalidation,
177 pub symmetric: bool,
178 pub inverse_of: Option<PredicateRef>,
179 pub description: String,
180 pub examples: Vec<String>,
181 pub deprecated_by: Option<PredicateRef>,
182 #[serde(default)]
187 pub profile_relevant: bool,
188 #[serde(default = "default_confidence_prior")]
193 pub confidence_prior: f32,
194}
195
196fn default_confidence_prior() -> f32 {
199 1.0
200}
201pub const CORE_V1_MAJOR: u32 = 1;
203pub const CORE_V1_MINOR: u32 = 4;
204
205static CORE_V1: std::sync::LazyLock<Vec<PredicateDef>> = std::sync::LazyLock::new(|| {
206 vec![
207 PredicateDef {
208 name: "employed_by".into(),
209 object_kind: ObjectKind::Entity("Organization".into()),
210 subject_types: vec!["Person".into()],
211 cardinality: Cardinality::Functional,
212 temporality: Temporality::Interval,
213 invalidation: Invalidation::Supersede,
214 symmetric: false,
215 inverse_of: None,
216 description: "The organization that employs this person.".into(),
217 examples: vec!["Alice is employed by Acme Corp".into()],
218 deprecated_by: None,
219 profile_relevant: true,
220 confidence_prior: 1.0,
221 },
222 PredicateDef {
223 name: "works_on".into(),
224 object_kind: ObjectKind::Entity("Project".into()),
225 subject_types: vec!["Person".into()],
226 cardinality: Cardinality::MultiValued,
227 temporality: Temporality::Interval,
228 invalidation: Invalidation::Coexist,
229 symmetric: false,
230 inverse_of: None,
231 description: "A project this person is currently working on.".into(),
232 examples: vec!["Bob works on ProjectX".into()],
233 deprecated_by: None,
234 profile_relevant: false,
235 confidence_prior: 1.0,
236 },
237 PredicateDef {
238 name: "born_in".into(),
239 object_kind: ObjectKind::Entity("Place".into()),
240 subject_types: vec!["Person".into()],
241 cardinality: Cardinality::Functional,
242 temporality: Temporality::Static,
243 invalidation: Invalidation::Supersede,
244 symmetric: false,
245 inverse_of: None,
246 description: "Where this person was born. A second value is a contradiction.".into(),
247 examples: vec!["Alice was born in Seoul".into()],
248 deprecated_by: None,
249 profile_relevant: true,
250 confidence_prior: 1.0,
251 },
252 PredicateDef {
253 name: "full_name".into(),
254 object_kind: ObjectKind::Literal(LiteralType::Text),
255 subject_types: vec!["Person".into()],
256 cardinality: Cardinality::Functional,
257 temporality: Temporality::Interval,
258 invalidation: Invalidation::Supersede,
259 symmetric: false,
260 inverse_of: None,
261 description: "The person's full legal name. A new value supersedes the old.".into(),
262 examples: vec!["Alice's full name is Alice Smith".into()],
263 deprecated_by: None,
264 profile_relevant: true,
265 confidence_prior: 1.0,
266 },
267 PredicateDef {
268 name: "died_at".into(),
269 object_kind: ObjectKind::Literal(LiteralType::DateTime),
270 subject_types: vec!["Person".into()],
271 cardinality: Cardinality::Functional,
272 temporality: Temporality::Static,
273 invalidation: Invalidation::ExplicitOnly,
274 symmetric: false,
275 inverse_of: None,
276 description: "When this person died. A second value is a contradiction.".into(),
277 examples: vec!["Alice died at 2024-03-01T00:00:00Z".into()],
278 deprecated_by: None,
279 profile_relevant: true,
280 confidence_prior: 1.0,
281 },
282 PredicateDef {
283 name: "knows".into(),
284 object_kind: ObjectKind::Entity("Person".into()),
285 subject_types: vec!["Person".into()],
286 cardinality: Cardinality::MultiValued,
287 temporality: Temporality::Interval,
288 invalidation: Invalidation::Coexist,
289 symmetric: true,
290 inverse_of: None,
291 description: "This person knows another person. Symmetric.".into(),
292 examples: vec!["Alice knows Bob".into()],
293 deprecated_by: None,
294 profile_relevant: false,
295 confidence_prior: 1.0,
296 },
297 PredicateDef {
298 name: "member_of".into(),
299 object_kind: ObjectKind::Entity("Organization".into()),
300 subject_types: vec!["Person".into()],
301 cardinality: Cardinality::MultiValued,
302 temporality: Temporality::Interval,
303 invalidation: Invalidation::Coexist,
304 symmetric: false,
305 inverse_of: None,
306 description: "Organizations this person is a member of.".into(),
307 examples: vec!["Alice is a member of the Engineering Guild".into()],
308 deprecated_by: None,
309 profile_relevant: true,
310 confidence_prior: 1.0,
311 },
312 PredicateDef {
313 name: "part_of".into(),
314 object_kind: ObjectKind::Entity(["Organization", "Project"].into()),
315 subject_types: vec!["Organization".into(), "Project".into(), "Artifact".into()],
316 cardinality: Cardinality::Functional,
317 temporality: Temporality::Static,
318 invalidation: Invalidation::Supersede,
319 symmetric: false,
320 inverse_of: None,
321 description:
322 "This organization/project/artifact is part of a parent organization or project."
323 .into(),
324 examples: vec!["Acme subsidiary is part of Acme Corp".into()],
325 deprecated_by: None,
326 profile_relevant: false,
327 confidence_prior: 1.0,
328 },
329 PredicateDef {
330 name: "located_in".into(),
331 object_kind: ObjectKind::Entity("Place".into()),
332 subject_types: vec!["Place".into(), "Artifact".into()],
333 cardinality: Cardinality::Functional,
334 temporality: Temporality::Static,
335 invalidation: Invalidation::Supersede,
336 symmetric: false,
337 inverse_of: None,
338 description: "This place or artifact is located within a place.".into(),
339 examples: vec!["Seoul is located in South Korea".into()],
340 deprecated_by: None,
341 profile_relevant: false,
342 confidence_prior: 1.0,
343 },
344 PredicateDef {
345 name: "has_skill".into(),
346 object_kind: ObjectKind::Entity("Concept".into()),
347 subject_types: vec!["Person".into()],
348 cardinality: Cardinality::MultiValued,
349 temporality: Temporality::Interval,
350 invalidation: Invalidation::Coexist,
351 symmetric: false,
352 inverse_of: None,
353 description: "A skill or competency this person has.".into(),
354 examples: vec!["Alice has skill Rust programming".into()],
355 deprecated_by: None,
356 profile_relevant: true,
357 confidence_prior: 1.0,
358 },
359 PredicateDef {
360 name: "created_by".into(),
361 object_kind: ObjectKind::Entity("Person".into()),
362 subject_types: vec!["Artifact".into(), "Document".into()],
363 cardinality: Cardinality::Functional,
364 temporality: Temporality::Static,
365 invalidation: Invalidation::Supersede,
366 symmetric: false,
367 inverse_of: Some("author_of".into()),
368 description: "Who created this artifact or document.".into(),
369 examples: vec!["The report was created by Alice".into()],
370 deprecated_by: None,
371 profile_relevant: false,
372 confidence_prior: 1.0,
373 },
374 PredicateDef {
375 name: "aliases".into(),
376 object_kind: ObjectKind::Literal(LiteralType::Text),
377 subject_types: vec![
378 "Person".into(),
379 "Organization".into(),
380 "Project".into(),
381 "Artifact".into(),
382 ],
383 cardinality: Cardinality::MultiValued,
384 temporality: Temporality::Static,
385 invalidation: Invalidation::Coexist,
386 symmetric: false,
387 inverse_of: None,
388 description: "Alternative names for this entity. Multiple values coexist.".into(),
389 examples: vec!["Alice's alias is A. Smith".into()],
390 deprecated_by: None,
391 profile_relevant: true,
392 confidence_prior: 1.0,
393 },
394 PredicateDef {
399 name: "allegedly_employed_by".into(),
400 object_kind: ObjectKind::Entity("Organization".into()),
401 subject_types: vec!["Person".into()],
402 cardinality: Cardinality::MultiValued,
403 temporality: Temporality::Interval,
404 invalidation: Invalidation::Coexist,
405 symmetric: false,
406 inverse_of: None,
407 description: "Unverified claim that an organization employs this person.".into(),
408 examples: vec!["Alice is allegedly employed by Acme Corp".into()],
409 deprecated_by: None,
410 profile_relevant: false,
411 confidence_prior: 0.3,
412 },
413 PredicateDef {
414 name: "rumored_knows".into(),
415 object_kind: ObjectKind::Entity("Person".into()),
416 subject_types: vec!["Person".into()],
417 cardinality: Cardinality::MultiValued,
418 temporality: Temporality::Interval,
419 invalidation: Invalidation::Coexist,
420 symmetric: false,
421 inverse_of: None,
422 description: "Unverified claim that this person knows another person.".into(),
423 examples: vec!["Alice is rumored to know Bob".into()],
424 deprecated_by: None,
425 profile_relevant: false,
426 confidence_prior: 0.3,
427 },
428 ]
429});
430
431pub fn core_v1() -> &'static Vec<PredicateDef> {
433 &CORE_V1
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use std::collections::HashSet;
440
441 #[test]
442 fn core_v1_covers_all_invalidation_branches() {
443 let defs = core_v1();
444 let has_func_supersede_interval = defs.iter().any(|d| {
446 d.cardinality == Cardinality::Functional
447 && d.invalidation == Invalidation::Supersede
448 && d.temporality == Temporality::Interval
449 });
450 let has_func_supersede_static = defs.iter().any(|d| {
451 d.cardinality == Cardinality::Functional
452 && d.invalidation == Invalidation::Supersede
453 && d.temporality == Temporality::Static
454 });
455 let has_multi_coexist = defs.iter().any(|d| {
456 d.cardinality == Cardinality::MultiValued && d.invalidation == Invalidation::Coexist
457 });
458 let has_explicit_only = defs
459 .iter()
460 .any(|d| d.invalidation == Invalidation::ExplicitOnly);
461 let has_symmetric = defs.iter().any(|d| d.symmetric);
462 let has_inverse = defs.iter().any(|d| d.inverse_of.is_some());
463
464 assert!(
465 has_func_supersede_interval,
466 "missing Functional/Supersede/Interval"
467 );
468 assert!(
469 has_func_supersede_static,
470 "missing Functional/Supersede/Static"
471 );
472 assert!(has_multi_coexist, "missing MultiValued/Coexist");
473 assert!(has_explicit_only, "missing ExplicitOnly");
474 assert!(has_symmetric, "missing symmetric");
475 assert!(has_inverse, "missing inverse_of");
476 }
477
478 #[test]
479 fn predicate_names_are_unique() {
480 let names: HashSet<_> = core_v1().iter().map(|d| &d.name).collect();
481 assert_eq!(names.len(), core_v1().len(), "duplicate predicate names");
482 }
483
484 #[test]
485 fn predicate_defs_serialize_roundtrip() {
486 for def in core_v1() {
487 let json = serde_json::to_string(def).expect("serialize");
488 let back: PredicateDef = serde_json::from_str(&json).expect("deserialize");
489 assert_eq!(def.name, back.name);
490 assert_eq!(def.cardinality, back.cardinality);
491 assert_eq!(def.temporality, back.temporality);
492 assert_eq!(def.invalidation, back.invalidation);
493 }
494 }
495
496 #[test]
497 fn part_of_accepts_multiple_object_types() {
498 let def = core_v1()
501 .iter()
502 .find(|d| d.name == "part_of")
503 .expect("part_of");
504 let ObjectKind::Entity(types) = &def.object_kind else {
505 panic!("part_of object must be Entity");
506 };
507 assert!(types.0.iter().any(|t| t == "Organization"));
508 assert!(types.0.iter().any(|t| t == "Project"));
509 }
510
511 #[test]
512 fn object_kind_entity_roundtrips_through_array() {
513 let def = core_v1()
514 .iter()
515 .find(|d| d.name == "part_of")
516 .expect("part_of");
517 let json = serde_json::to_string(&def.object_kind).expect("serialize");
518 assert_eq!(json, r#"["Organization","Project"]"#);
519 let back: ObjectKind = serde_json::from_str(&json).expect("deserialize");
520 assert_eq!(back, def.object_kind);
521 }
522
523 #[test]
524 fn object_kind_accepts_legacy_single_string() {
525 let k: ObjectKind = serde_json::from_str(r#""Organization""#).expect("legacy string");
528 assert_eq!(k, ObjectKind::Entity(["Organization"].into()));
529 }
530
531 #[test]
532 fn object_kind_literal_strings_stay_literal() {
533 let k: ObjectKind = serde_json::from_str(r#""text""#).expect("literal");
536 assert_eq!(k, ObjectKind::Literal(LiteralType::Text));
537 }
538
539 #[test]
540 fn object_kind_enum_uses_object_shape() {
541 let k: ObjectKind = serde_json::from_str(r#"{"variants":["yes","no"]}"#).expect("enum");
544 assert_eq!(
545 k,
546 ObjectKind::Enum {
547 variants: vec!["yes".into(), "no".into()]
548 }
549 );
550 let json = serde_json::to_string(&k).expect("serialize");
551 assert_eq!(json, r#"{"variants":["yes","no"]}"#);
552 }
553}