use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct Sigma {
pub schema: String,
#[serde(default)]
pub concepts: BTreeMap<String, Concept>,
#[serde(default)]
pub roles: BTreeMap<String, Role>,
#[serde(default)]
pub symbols: Vec<Symbol>,
#[serde(default)]
pub worlds: BTreeMap<String, World>,
#[serde(default)]
pub agents: Vec<String>,
#[serde(default)]
pub entity_types: Vec<EntityTypeDecl>,
#[serde(default)]
pub extractors: Vec<ExtractorDecl>,
#[serde(default)]
pub not_expressible: Vec<NotExpressible>,
#[serde(default)]
pub subsumes: Vec<Subsumes>,
#[serde(default)]
pub entity_type_target_class: BTreeMap<String, Option<String>>,
#[serde(default)]
pub llm_context_role_synonyms: BTreeMap<String, Vec<String>>,
#[serde(default)]
pub readers: BTreeMap<String, String>,
#[serde(default)]
pub metadata: Option<serde_yaml::Value>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct Concept {
pub doc: String,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct Role {
pub domain: String,
pub range: String,
#[serde(default)]
pub symmetric: bool,
#[serde(default)]
pub acyclic: bool,
#[serde(default)]
pub doc: String,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct Symbol {
pub name: String,
pub kind: SymbolKind,
#[serde(default)]
pub doc: String,
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SymbolKind {
Operator,
Function,
Predicate,
Constant,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct World {
pub doc: String,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct EntityTypeDecl {
pub name: String,
#[serde(default)]
pub extractor: String,
pub implemented: bool,
#[serde(default)]
pub vocabulary: Option<EntityVocabulary>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct EntityVocabulary {
pub prefix: String,
pub root_class: String,
pub version: String,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct ExtractorDecl {
pub name: String,
#[serde(default)]
pub reader: String,
pub implemented: bool,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct NotExpressible {
pub key: String,
#[serde(default)]
pub reader: String,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct Subsumes {
pub sub: String,
pub sup: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SigmaError {
Parse(String),
EntityTypeWithoutExtractor {
entity_type: String,
extractor: String,
},
KeyWithoutReader { key: String },
NotExpressibleWithoutReader { key: String },
ExtractorWithoutReader { extractor: String },
SubsumesUndeclared { concept: String },
SubsumesCycle { path: Vec<String> },
VocabularyMalformed { entity_type: String, why: String },
ReservedEntityType { entity_type: String },
TargetClassForUndeclaredType { entity_type: String },
ImplementedTypeWithoutTargetClass { entity_type: String },
}
pub const RESERVED_ENTITY_TYPE: &str = "entity";
impl fmt::Display for SigmaError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Parse(e) => write!(f, "ontology.yaml: {e}"),
Self::EntityTypeWithoutExtractor {
entity_type,
extractor,
} => {
if extractor.is_empty() {
write!(f, "entity_type `{entity_type}` names no extractor")
} else {
write!(
f,
"entity_type `{entity_type}` names extractor `{extractor}`, which extractors[] does not declare"
)
}
}
Self::KeyWithoutReader { key } => {
write!(
f,
"Σ key `{key}` has no reader — a key nothing reads is decoration"
)
}
Self::ReservedEntityType { entity_type } => write!(
f,
"entity_type `{entity_type}` is reserved — `<ONT_BASE>entity/` holds the entity classes, so its properties would collide with them"
),
Self::NotExpressibleWithoutReader { key } => {
write!(f, "not_expressible `{key}` has no reader")
}
Self::ExtractorWithoutReader { extractor } => {
write!(f, "extractor `{extractor}` has no reader")
}
Self::SubsumesUndeclared { concept } => {
write!(
f,
"subsumes names concept `{concept}`, which concepts does not declare"
)
}
Self::SubsumesCycle { path } => write!(f, "subsumes cycle {}", path.join(" -> ")),
Self::VocabularyMalformed { entity_type, why } => {
write!(
f,
"entity_type `{entity_type}` vocabulary is malformed: {why}"
)
}
Self::TargetClassForUndeclaredType { entity_type } => write!(
f,
"entity_type_target_class key `{entity_type}` names no declared entity_type"
),
Self::ImplementedTypeWithoutTargetClass { entity_type } => write!(
f,
"implemented entity_type `{entity_type}` has neither a class nor an explicit `~` in entity_type_target_class"
),
}
}
}
impl std::error::Error for SigmaError {}
fn find_cycle<'a>(
n: &'a str,
edges: &BTreeMap<&'a str, BTreeSet<&'a str>>,
path: &mut Vec<&'a str>,
done: &mut BTreeSet<&'a str>,
) -> Option<Vec<String>> {
if let Some(at) = path.iter().position(|p| *p == n) {
let mut cycle: Vec<String> = path[at..].iter().map(|s| (*s).to_string()).collect();
cycle.push(n.to_string());
return Some(cycle);
}
if done.contains(n) {
return None;
}
path.push(n);
for &m in edges.get(n).into_iter().flatten() {
if let Some(c) = find_cycle(m, edges, path, done) {
return Some(c);
}
}
path.pop();
done.insert(n);
None
}
pub const READABLE_KEYS: [&str; 12] = [
"concepts",
"roles",
"symbols",
"worlds",
"agents",
"entity_types",
"extractors",
"not_expressible",
"subsumes",
"entity_type_target_class",
"llm_context_role_synonyms",
"metadata",
];
impl Sigma {
pub fn from_yaml(yaml: &str) -> Result<Self, SigmaError> {
serde_yaml::from_str(yaml).map_err(|e| SigmaError::Parse(e.to_string()))
}
pub fn check_integrity(&self) -> Result<(), SigmaError> {
self.check_entity_types()?;
self.check_vocabularies()?;
self.check_readers()?;
self.check_not_expressible()?;
self.check_extractors()?;
self.check_subsumes()?;
self.check_target_classes()
}
fn check_subsumes(&self) -> Result<(), SigmaError> {
if let Some(c) = self
.subsumes
.iter()
.flat_map(|e| [&e.sub, &e.sup])
.find(|c| !self.concepts.contains_key(c.as_str()))
{
return Err(SigmaError::SubsumesUndeclared { concept: c.clone() });
}
let edges = self.subsumption_edges();
let mut done: BTreeSet<&str> = BTreeSet::new();
for &root in edges.keys() {
if let Some(path) = find_cycle(root, &edges, &mut Vec::new(), &mut done) {
return Err(SigmaError::SubsumesCycle { path });
}
}
Ok(())
}
fn subsumption_edges(&self) -> BTreeMap<&str, BTreeSet<&str>> {
let mut edges: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
for e in &self.subsumes {
edges
.entry(e.sub.as_str())
.or_default()
.insert(e.sup.as_str());
}
edges
}
#[must_use]
pub fn supers(&self, concept: &str) -> BTreeSet<String> {
let edges = self.subsumption_edges();
let mut out = BTreeSet::new();
let mut stack: Vec<&str> = edges
.get(concept)
.map(|s| s.iter().copied().collect())
.unwrap_or_default();
while let Some(c) = stack.pop() {
if c != concept && out.insert(c.to_string()) {
stack.extend(edges.get(c).into_iter().flatten().copied());
}
}
out
}
#[must_use]
pub fn subs(&self, concept: &str) -> BTreeSet<String> {
self.concepts
.keys()
.filter(|c| c.as_str() != concept && self.supers(c).contains(concept))
.cloned()
.collect()
}
fn check_vocabularies(&self) -> Result<(), SigmaError> {
for et in &self.entity_types {
let Some(v) = &et.vocabulary else { continue };
let bad = |why: String| SigmaError::VocabularyMalformed {
entity_type: et.name.clone(),
why,
};
if v.prefix.trim().is_empty() {
return Err(bad("empty `prefix`".into()));
}
if v.version.trim().is_empty() {
return Err(bad("empty `version`".into()));
}
if !v
.root_class
.strip_prefix("ont:")
.is_some_and(|c| self.concepts.contains_key(c))
{
return Err(bad(format!(
"root_class `{}` is not `ont:<a concept Σ declares>`",
v.root_class
)));
}
}
Ok(())
}
fn check_entity_types(&self) -> Result<(), SigmaError> {
if let Some(et) = self
.entity_types
.iter()
.find(|et| et.name == RESERVED_ENTITY_TYPE)
{
return Err(SigmaError::ReservedEntityType {
entity_type: et.name.clone(),
});
}
let declared: BTreeSet<&str> = self.extractors.iter().map(|e| e.name.as_str()).collect();
match self
.entity_types
.iter()
.find(|et| et.extractor.is_empty() || !declared.contains(et.extractor.as_str()))
{
None => Ok(()),
Some(et) => Err(SigmaError::EntityTypeWithoutExtractor {
entity_type: et.name.clone(),
extractor: et.extractor.clone(),
}),
}
}
fn check_readers(&self) -> Result<(), SigmaError> {
match self
.populated_keys()
.into_iter()
.find(|key| self.readers.get(*key).is_none_or(|r| r.trim().is_empty()))
{
None => Ok(()),
Some(key) => Err(SigmaError::KeyWithoutReader {
key: key.to_string(),
}),
}
}
fn check_not_expressible(&self) -> Result<(), SigmaError> {
match self
.not_expressible
.iter()
.find(|ne| ne.reader.trim().is_empty())
{
None => Ok(()),
Some(ne) => Err(SigmaError::NotExpressibleWithoutReader {
key: ne.key.clone(),
}),
}
}
fn check_extractors(&self) -> Result<(), SigmaError> {
match self
.extractors
.iter()
.find(|ex| ex.reader.trim().is_empty())
{
None => Ok(()),
Some(ex) => Err(SigmaError::ExtractorWithoutReader {
extractor: ex.name.clone(),
}),
}
}
fn check_target_classes(&self) -> Result<(), SigmaError> {
if let Some(key) = self
.entity_type_target_class
.keys()
.find(|k| !self.declares_entity_type(k))
{
return Err(SigmaError::TargetClassForUndeclaredType {
entity_type: key.clone(),
});
}
match self
.entity_types
.iter()
.find(|et| et.implemented && !self.entity_type_target_class.contains_key(&et.name))
{
None => Ok(()),
Some(et) => Err(SigmaError::ImplementedTypeWithoutTargetClass {
entity_type: et.name.clone(),
}),
}
}
#[must_use]
pub fn populated_keys(&self) -> BTreeSet<&'static str> {
let mut keys = BTreeSet::new();
if !self.concepts.is_empty() {
keys.insert("concepts");
}
if !self.roles.is_empty() {
keys.insert("roles");
}
if !self.symbols.is_empty() {
keys.insert("symbols");
}
if !self.worlds.is_empty() {
keys.insert("worlds");
}
if !self.agents.is_empty() {
keys.insert("agents");
}
if !self.entity_types.is_empty() {
keys.insert("entity_types");
}
if !self.extractors.is_empty() {
keys.insert("extractors");
}
if !self.not_expressible.is_empty() {
keys.insert("not_expressible");
}
if !self.subsumes.is_empty() {
keys.insert("subsumes");
}
if !self.entity_type_target_class.is_empty() {
keys.insert("entity_type_target_class");
}
if !self.llm_context_role_synonyms.is_empty() {
keys.insert("llm_context_role_synonyms");
}
if self.metadata.is_some() {
keys.insert("metadata");
}
keys
}
#[must_use]
pub fn declares_role(&self, name: &str) -> bool {
self.roles.contains_key(name)
}
#[must_use]
pub fn declares_entity_type(&self, name: &str) -> bool {
self.entity_types.iter().any(|e| e.name == name)
}
#[must_use]
pub fn declares_symbol(&self, token: &str) -> bool {
self.symbols.iter().any(|s| s.name == token)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn good() -> &'static str {
r#"
schema: ont-sigma-v1
concepts:
Contract: {doc: "a pv contract file"}
Code: {doc: "a Rust item"}
roles:
binds: {domain: Contract, range: Code, acyclic: false, doc: "a contract binds a symbol"}
symbols:
- {name: "∀", kind: operator}
- {name: isFinite, kind: predicate}
worlds:
default: {doc: "the tree as committed"}
agents: [pv]
entity_types:
- {name: pv-contract, extractor: pv_contract, implemented: false}
extractors:
- {name: pv_contract, reader: ontology/extract/pv_contract.rs, implemented: false}
not_expressible:
- {key: taste, reader: ontology/sigma.rs}
readers:
concepts: ontology/sigma.rs
roles: lint/sigma_gate.rs
symbols: lint/sigma_symbols.rs
worlds: ontology/sigma.rs
agents: ontology/sigma.rs
entity_types: lint/sigma_gate.rs
extractors: ontology/sigma.rs
not_expressible: ontology/sigma.rs
"#
}
fn with_map(map: &str) -> Result<(), SigmaError> {
let y = good()
.replace(
" - {name: pv-contract, extractor: pv_contract, implemented: false}",
" - {name: pv-contract, extractor: pv_contract, implemented: true}",
)
.replace(
"readers:\n",
&format!("{map}readers:\n entity_type_target_class: ontology/shapes.rs\n"),
);
Sigma::from_yaml(&y).expect("parses").check_integrity()
}
#[test]
fn an_implemented_type_is_covered_by_a_class_or_an_explicit_tilde() {
assert_eq!(
with_map("entity_type_target_class:\n pv-contract: ont:Contract\n"),
Ok(())
);
assert_eq!(
with_map("entity_type_target_class:\n pv-contract: ~\n"),
Ok(())
);
}
#[test]
fn an_implemented_type_the_map_does_not_mention_is_malformed_named() {
let e = with_map("").unwrap_err();
assert_eq!(
e,
SigmaError::ImplementedTypeWithoutTargetClass {
entity_type: "pv-contract".into()
}
);
assert!(e.to_string().contains("`pv-contract`"), "{e}");
}
#[test]
fn a_map_key_no_entity_type_declares_is_malformed_named() {
let e = with_map("entity_type_target_class:\n pv-contract: ont:Contract\n ghost: g:G\n")
.unwrap_err();
assert_eq!(
e,
SigmaError::TargetClassForUndeclaredType {
entity_type: "ghost".into()
}
);
}
#[test]
fn the_repo_sigma_map_covers_every_implemented_type_and_names_no_ghost() {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../contracts/ontology.yaml");
let s =
Sigma::from_yaml(&std::fs::read_to_string(path).expect("read Σ")).expect("Σ parses");
assert_eq!(s.check_integrity(), Ok(()));
}
#[test]
fn a_well_formed_sigma_parses_and_passes_integrity() {
let s = Sigma::from_yaml(good()).expect("Σ parses");
assert_eq!(s.schema, "ont-sigma-v1");
assert_eq!(s.roles["binds"].domain, "Contract");
assert!(s.check_integrity().is_ok(), "{:?}", s.check_integrity());
}
#[test]
fn sigma_carries_the_contract_schemas_own_metadata_block_and_reads_nothing_of_it() {
let y = good().replace(
"readers:\n",
"metadata:\n version: 1.0.0\n description: \"apex's entity-type registry\"\n references:\n - 'APEX-001 §5 row EV-19'\nreaders:\n metadata: schema/types.rs (the contract schema, not Σ)\n",
);
let s = Sigma::from_yaml(&y).expect("Σ with metadata parses");
assert!(s.metadata.is_some());
assert!(s.check_integrity().is_ok(), "{:?}", s.check_integrity());
let without = Sigma::from_yaml(good()).expect("Σ parses");
assert_eq!(s.concepts, without.concepts);
assert_eq!(s.entity_types, without.entity_types);
}
#[test]
fn an_unclaimed_metadata_block_is_refused_like_every_other_key() {
let y = good().replace(
"readers:\n",
"metadata:\n version: 1.0.0\n description: d\nreaders:\n",
);
let s = Sigma::from_yaml(&y).expect("Σ parses");
match s.check_integrity() {
Err(SigmaError::KeyWithoutReader { key }) => assert_eq!(key, "metadata"),
other => panic!("an unclaimed metadata: must be refused, got {other:?}"),
}
}
#[test]
fn an_entity_type_naming_no_extractor_is_malformed() {
let y = good().replace(
" - {name: pv-contract, extractor: pv_contract, implemented: false}",
" - {name: pv-contract, extractor: \"\", implemented: false}",
);
let s = Sigma::from_yaml(&y).expect("parses");
assert_eq!(
s.check_integrity(),
Err(SigmaError::EntityTypeWithoutExtractor {
entity_type: "pv-contract".into(),
extractor: String::new()
})
);
}
#[test]
fn an_entity_type_named_entity_is_reserved() {
let y = good().replace(
" - {name: pv-contract, extractor: pv_contract, implemented: false}",
" - {name: pv-contract, extractor: pv_contract, implemented: false}\n - {name: entity, extractor: pv_contract, implemented: false}",
);
let s = Sigma::from_yaml(&y).expect("parses");
let err = s.check_integrity().expect_err("`entity` is reserved");
assert_eq!(
err,
SigmaError::ReservedEntityType {
entity_type: "entity".into()
}
);
assert!(err.to_string().contains("reserved"), "{err}");
use crate::ontology::extract::pv_contract::{entity_class, entity_predicate};
assert_eq!(
entity_predicate(RESERVED_ENTITY_TYPE, "study"),
entity_class("study")
);
let ok = good().replace("{name: pv-contract,", "{name: entity-ish,");
assert_eq!(
Sigma::from_yaml(&ok).expect("parses").check_integrity(),
Ok(())
);
}
#[test]
fn an_entity_type_naming_an_undeclared_extractor_is_malformed() {
let y = good().replace("extractor: pv_contract,", "extractor: ghost,");
let s = Sigma::from_yaml(&y).expect("parses");
assert_eq!(
s.check_integrity(),
Err(SigmaError::EntityTypeWithoutExtractor {
entity_type: "pv-contract".into(),
extractor: "ghost".into()
})
);
}
#[test]
fn a_sigma_key_with_no_reader_is_malformed() {
let y = good().replace(" symbols: lint/sigma_symbols.rs\n", "");
let s = Sigma::from_yaml(&y).expect("parses");
assert_eq!(
s.check_integrity(),
Err(SigmaError::KeyWithoutReader {
key: "symbols".into()
})
);
}
#[test]
fn an_extractor_without_a_reader_is_malformed() {
let y = good().replace(
" - {name: pv_contract, reader: ontology/extract/pv_contract.rs, implemented: false}",
" - {name: pv_contract, reader: \"\", implemented: false}",
);
let s = Sigma::from_yaml(&y).expect("parses");
assert_eq!(
s.check_integrity(),
Err(SigmaError::ExtractorWithoutReader {
extractor: "pv_contract".into()
})
);
}
#[test]
fn a_not_expressible_without_a_reader_is_malformed() {
let y = good().replace(
" - {key: taste, reader: ontology/sigma.rs}",
" - {key: taste, reader: \"\"}",
);
let s = Sigma::from_yaml(&y).expect("parses");
assert_eq!(
s.check_integrity(),
Err(SigmaError::NotExpressibleWithoutReader {
key: "taste".into()
})
);
}
#[test]
fn an_empty_key_needs_no_reader() {
let y = good()
.replace("agents: [pv]", "agents: []")
.replace(" agents: ontology/sigma.rs\n", "");
let s = Sigma::from_yaml(&y).expect("parses");
assert!(!s.populated_keys().contains("agents"));
assert!(s.check_integrity().is_ok(), "{:?}", s.check_integrity());
}
#[test]
fn lookups_answer_only_for_what_sigma_declares() {
let s = Sigma::from_yaml(good()).expect("parses");
assert!(s.declares_role("binds"));
assert!(!s.declares_role("refines"), "undeclared role");
assert!(s.declares_entity_type("pv-contract"));
assert!(!s.declares_entity_type("csv"), "undeclared entity type");
assert!(s.declares_symbol("∀"));
assert!(s.declares_symbol("isFinite"));
assert!(!s.declares_symbol("⊗"), "undeclared symbol");
}
#[test]
fn an_unknown_key_is_refused_by_the_parser() {
let y = format!("{}\nsurprise: 1\n", good());
assert!(matches!(Sigma::from_yaml(&y), Err(SigmaError::Parse(_))));
}
#[test]
fn the_repo_sigma_parses_and_is_well_formed() {
let path =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../contracts/ontology.yaml");
let text = std::fs::read_to_string(&path).expect("contracts/ontology.yaml is readable");
let s = Sigma::from_yaml(&text).expect("the repo's Σ parses");
assert!(s.check_integrity().is_ok(), "{:?}", s.check_integrity());
assert!(!s.entity_types.is_empty(), "Σ declares entity types");
assert!(!s.extractors.is_empty(), "Σ declares extractors");
assert!(
s.symbols.len() >= 20,
"Σ declares the corpus's operator vocabulary, got {}",
s.symbols.len()
);
}
fn with_subsumes(edges: &[(&str, &str)]) -> Sigma {
let mut s = Sigma::from_yaml(good()).expect("Σ parses");
s.concepts.insert(
"Kernel".into(),
Concept {
doc: "a kernel contract".into(),
},
);
s.subsumes = edges
.iter()
.map(|(a, b)| Subsumes {
sub: (*a).into(),
sup: (*b).into(),
})
.collect();
s.readers
.insert("subsumes".into(), "ontology/sigma.rs".into());
s
}
#[test]
fn ont4d_supers_and_subs_are_the_strict_transitive_closure() {
let s = with_subsumes(&[("Kernel", "Code"), ("Code", "Contract")]);
assert!(s.check_integrity().is_ok(), "{:?}", s.check_integrity());
assert_eq!(
s.supers("Kernel").into_iter().collect::<Vec<_>>(),
vec!["Code", "Contract"]
);
assert!(
s.supers("Contract").is_empty(),
"the top has no strict super"
);
assert_eq!(
s.subs("Contract").into_iter().collect::<Vec<_>>(),
vec!["Code", "Kernel"]
);
assert!(
!s.supers("Code").contains("Code"),
"strict: a concept is never its own super"
);
}
#[test]
fn ont4d_a_cycle_is_refused_naming_the_closing_path() {
let s = with_subsumes(&[
("Kernel", "Code"),
("Code", "Contract"),
("Contract", "Kernel"),
]);
match s.check_integrity() {
Err(SigmaError::SubsumesCycle { path }) => {
assert_eq!(path.first(), path.last(), "the path closes: {path:?}");
assert_eq!(
path.len(),
4,
"three edges, first concept repeated: {path:?}"
);
assert!(SigmaError::SubsumesCycle { path: path.clone() }
.to_string()
.starts_with("subsumes cycle "));
}
other => panic!("a cycle is exit 3: {other:?}"),
}
assert!(matches!(
with_subsumes(&[("Code", "Code")]).check_integrity(),
Err(SigmaError::SubsumesCycle { .. })
));
let cyc = with_subsumes(&[("Kernel", "Code"), ("Code", "Kernel")]);
assert_eq!(
cyc.supers("Kernel").into_iter().collect::<Vec<_>>(),
vec!["Code"]
);
}
#[test]
fn ont4d_a_diamond_is_not_a_cycle() {
let mut s = with_subsumes(&[
("Kernel", "Code"),
("Kernel", "Contract"),
("Code", "Contract"),
]);
s.concepts
.insert("Other".into(), Concept { doc: "x".into() });
assert!(
s.check_integrity().is_ok(),
"two paths to one super is a DAG: {:?}",
s.check_integrity()
);
}
#[test]
fn ont4d_an_edge_over_an_undeclared_concept_is_refused() {
let s = with_subsumes(&[("Kernel", "Ghost")]);
assert!(matches!(
s.check_integrity(),
Err(SigmaError::SubsumesUndeclared { concept }) if concept == "Ghost"
));
}
#[test]
fn ont4d_subsumes_needs_a_reader_like_every_key() {
let mut s = with_subsumes(&[("Kernel", "Code")]);
s.readers.remove("subsumes");
assert!(
matches!(s.check_integrity(), Err(SigmaError::KeyWithoutReader { key }) if key == "subsumes")
);
}
fn with_vocabulary(root_class: &str, version: &str) -> Sigma {
let yaml = good().replace(
" - {name: pv-contract, extractor: pv_contract, implemented: false}\n",
&format!(
" - {{name: pv-contract, extractor: pv_contract, implemented: false}}\n - {{name: repo, extractor: pv_contract, implemented: false, vocabulary: {{prefix: repo, root_class: \"{root_class}\", version: \"{version}\"}}}}\n"
),
);
Sigma::from_yaml(&yaml).expect("Σ with a vocabulary parses")
}
#[test]
fn ont4f_a_vocabulary_over_a_declared_concept_is_well_formed() {
let s = with_vocabulary("ont:Code", "sha");
let v = s.entity_types[1].vocabulary.as_ref().expect("vocabulary");
assert_eq!((v.prefix.as_str(), v.version.as_str()), ("repo", "sha"));
assert!(s.check_integrity().is_ok(), "{:?}", s.check_integrity());
}
#[test]
fn ont4f_a_vocabulary_whose_root_class_the_closure_cannot_reach_is_refused() {
for bad in ["ont:Ghost", "repo:Repo", ""] {
let s = with_vocabulary(bad, "sha");
let err = s.check_integrity().expect_err(bad);
assert!(
matches!(&err, SigmaError::VocabularyMalformed { entity_type, .. } if entity_type == "repo"),
"{err}"
);
}
let err = with_vocabulary("ont:Code", " ")
.check_integrity()
.unwrap_err();
assert!(err.to_string().contains("empty `version`"), "{err}");
}
}