diff --git a/Generation/Converters/Argumentum.AssetConverter.Tests/Ontology/OwlAdapterRegressionTests.cs b/Generation/Converters/Argumentum.AssetConverter.Tests/Ontology/OwlAdapterRegressionTests.cs
index c432028f3..78d734f45 100644
--- a/Generation/Converters/Argumentum.AssetConverter.Tests/Ontology/OwlAdapterRegressionTests.cs
+++ b/Generation/Converters/Argumentum.AssetConverter.Tests/Ontology/OwlAdapterRegressionTests.cs
@@ -18,20 +18,22 @@ namespace Argumentum.AssetConverter.Tests.Ontology
/// annotation scanners (GetAnnotationSubjects / GetResourceAnnotations /
/// GetLiteralAnnotations).
///
- /// ⚠️ These tests surfaced a REAL BUG (not greenwashed). Every fallback reader compares
- /// a.ValueIRI.Equals(value.URI) / a.SubjectIRI.Equals(subject.URI) where
- /// .URI is a string and ValueIRI/SubjectIRI are
+ /// ✅ BUG FOUND then FIXED. PR #480 surfaced a real bug (not greenwashed): every fallback
+ /// reader compared a.ValueIRI.Equals(value.URI) / a.SubjectIRI.Equals(subject.URI)
+ /// where .URI is a string and ValueIRI/SubjectIRI are
/// RDFResource. RDFResource.Equals(string) returns false by type-mismatch,
- /// so every read returns empty. See .
- /// This is the root cause behind the production validation module silently reporting
- /// "no concepts → skip → PASS" on annotation/AIF checks. Reported as [BUG] on the dashboard.
+ /// so every read returned empty — the root cause behind the production validation module
+ /// silently reporting "no concepts → skip → PASS" on annotation/AIF checks. See
+ /// .
///
- /// Tests are split into: (A) characterization of the bug (pinned, documents current behavior),
- /// (B) the correct comparison semantics (documents the fix), (C) write-path + serialization
- /// (these DO work). No fix is applied — the file only pins observed behavior so a future fix
- /// flips the [BUG] tests red→green and the fix author has a regression suite ready.
+ /// This PR applies the fix: drop .URI on the right-hand side of every reader comparison
+ /// (14 sites in OwlAdapter.cs), so readers compare RDFResource.Equals(RDFResource).
+ /// The section-(A) tests below WERE the [BUG] characterization suite from #480; they are now
+ /// flipped to proper round-trip assertions and serve as the regression suite guarding the fix.
+ /// Section (C) write-path + serialization tests (unchanged) prove the fix did not regress the
+ /// write side.
///
- /// Deterministic, key-free, release-independent. No existing file modified. Baseline additive.
+ /// Deterministic, key-free, release-independent.
///
public class OwlAdapterRegressionTests
{
@@ -67,15 +69,16 @@ public void Diag_RDFResource_Equals_String_Is_False_By_Type_Mismatch()
}
// ─────────────────────────────────────────────────────────────────────────────
- // (A) [BUG] characterization — readers return empty despite writes succeeding.
- // These pin the BROKEN behavior. When OwlAdapter is fixed, these flip to red and
- // become proper round-trip assertions (remove the .Be(0) and assert the populated set).
+ // (A) Reader round-trip — was the [BUG] characterization suite in #480 (asserted the
+ // BROKEN empty/false behavior). Now flipped by the fix in this PR: these verify the
+ // readers correctly retrieve what the write path declared. Regression suite for the fix.
// ─────────────────────────────────────────────────────────────────────────────
[Fact]
- public void BUG_GetConcepts_Returns_Empty_Despite_Declared_Concepts()
+ public void GetConcepts_RoundTrips_Declared_Concepts_After_Fix()
{
- // Write 3 concepts; the reader should return 3 but returns 0 (comparison bug).
+ // Write 3 concepts; the reader must return all 3 (was returning 0 before the fix
+ // because the fallback scanner compared RDFResource.Equals(string)).
var adapter = NewAdapter();
var scheme = Res("Scheme");
adapter.DeclareConceptScheme(scheme);
@@ -84,73 +87,85 @@ public void BUG_GetConcepts_Returns_Empty_Despite_Declared_Concepts()
adapter.DeclareConcept(Res("C3"), scheme);
var concepts = adapter.GetConcepts();
- concepts.Should().BeEmpty(
- "[BUG] GetConcepts returns empty because the fallback scanner compares RDFResource.Equals(string)");
+ concepts.Should().NotBeEmpty("GetConcepts must retrieve declared concepts (fix: RDFResource.Equals(RDFResource))");
+ concepts.Should().HaveCount(3);
+ concepts.Select(c => c.ToString()).Should().BeEquivalentTo(new[] { Ns + "C1", Ns + "C2", Ns + "C3" });
}
[Fact]
- public void BUG_GetResourcesByType_Concept_Returns_Empty()
+ public void GetResourcesByType_RoundTrips_Concepts_And_Schemes_After_Fix()
{
// Same root cause, different reader. ValidateOwlOntologyStructure relies on this.
+ // Was returning empty before the fix (RDFResource.Equals(string)).
var adapter = NewAdapter();
var scheme = Res("Scheme");
adapter.DeclareConceptScheme(scheme);
adapter.DeclareConcept(Res("C1"), scheme);
- adapter.GetResourcesByType(SKOSVocabulary.Concept).Should().BeEmpty("[BUG] RDFResource.Equals(string)");
- adapter.GetResourcesByType(SKOSVocabulary.ConceptScheme).Should().BeEmpty("[BUG] RDFResource.Equals(string)");
+ adapter.GetResourcesByType(SKOSVocabulary.Concept)
+ .Should().NotBeEmpty("Concept type resolves after the fix")
+ .And.ContainSingle(c => c.ToString() == Ns + "C1");
+ adapter.GetResourcesByType(SKOSVocabulary.ConceptScheme)
+ .Should().NotBeEmpty("ConceptScheme type resolves after the fix")
+ .And.ContainSingle(c => c.ToString() == Ns + "Scheme");
}
[Fact]
- public void BUG_GetTopConcepts_Returns_Empty_Despite_Declared()
+ public void GetTopConcepts_RoundTrips_Declared_Top_Concepts_After_Fix()
{
var adapter = NewAdapter();
adapter.DeclareTopConcept(Res("Top1"), Res("Scheme"));
adapter.DeclareTopConcept(Res("Top2"), Res("Scheme"));
- adapter.GetTopConcepts().Should().BeEmpty("[BUG] GetAnnotationObjects compares RDFResource.Equals(string)");
+ var tops = adapter.GetTopConcepts();
+ tops.Should().NotBeEmpty("GetTopConcepts must retrieve declared top concepts after the fix");
+ tops.Select(c => c.ToString()).Should().BeEquivalentTo(new[] { Ns + "Top1", Ns + "Top2" });
}
[Fact]
- public void BUG_HasAnnotation_Returns_False_Despite_Annotation_Present()
+ public void HasAnnotation_Finds_The_Annotation_After_Fix()
{
var adapter = NewAdapter();
var scheme = Res("Scheme");
adapter.DeclareConceptScheme(scheme);
- // The annotation IS written (see write-path test below), but HasAnnotation can't find it.
+ // The annotation IS written (write-path test confirms it); HasAnnotation must now find it
+ // (was false before the fix: ValueIRI.Equals(value.URI string)).
adapter.HasAnnotation(scheme, RDFVocabulary.RDF.TYPE, SKOSVocabulary.ConceptScheme)
- .Should().BeFalse("[BUG] HasAnnotation compares ValueIRI.Equals(value.URI string) → false");
+ .Should().BeTrue("HasAnnotation must match rdf:type=skos:ConceptScheme after the fix");
}
[Fact]
- public void BUG_CheckIsNarrowerConcept_Returns_False_Despite_Declared()
+ public void CheckIsNarrowerConcept_Detects_The_Edge_After_Fix()
{
var adapter = NewAdapter();
var parent = Res("Fallacies");
var child = Res("AdHominem");
adapter.DeclareNarrowerConcepts(parent, child);
- // CheckIsNarrowerConcept has a try/except fallback that ALSO uses .Equals(string.URI),
- // so even the fallback fails.
+ // CheckIsNarrowerConcept's try/except fallback ALSO used .Equals(string.URI) and failed;
+ // after the fix the fallback scanner matches correctly.
adapter.CheckIsNarrowerConcept(child, parent)
- .Should().BeFalse("[BUG] fallback compares RDFResource.Equals(string) → false");
+ .Should().BeTrue("child IS a narrower of parent after the fix");
+ adapter.CheckIsNarrowerConcept(Res("Unrelated"), parent)
+ .Should().BeFalse("an unrelated concept is NOT a narrower of parent");
}
[Fact]
- public void BUG_GetConceptPreferredLabels_Returns_Empty_Despite_Label_Set()
+ public void GetConceptPreferredLabels_RoundTrips_The_Label_After_Fix()
{
var adapter = NewAdapter();
var concept = Res("C");
adapter.DeclareConcept(concept, Res("Scheme"));
adapter.AnnotateConceptPreferredLabel(concept, Lit("Ad Hominem"));
- adapter.GetConceptPreferredLabels(concept).Should().BeEmpty(
- "[BUG] GetLiteralAnnotations compares AnnotationProperty.GetIRI().Equals(property.URI string)");
+ adapter.GetConceptPreferredLabels(concept)
+ .Should().NotBeEmpty("prefLabel must be retrievable after the fix")
+ .And.ContainSingle(l => l.ToString().StartsWith("Ad Hominem"));
}
[Fact]
- public void BUG_GetConceptDocumentation_Returns_Empty_Despite_Documented()
+ public void GetConceptDocumentation_RoundTrips_The_Definition_After_Fix()
{
var adapter = NewAdapter();
var concept = Res("C");
@@ -158,18 +173,21 @@ public void BUG_GetConceptDocumentation_Returns_Empty_Despite_Documented()
adapter.DocumentConcept(concept, SKOSDocumentationTypes.Definition, Lit("A fallacy..."));
adapter.GetConceptDocumentation(concept, SKOSDocumentationTypes.Definition)
- .Should().BeEmpty("[BUG] same RDFResource.Equals(string) root cause");
+ .Should().NotBeEmpty("definition must be retrievable after the fix")
+ .And.ContainSingle(l => l.ToString().StartsWith("A fallacy"));
}
[Fact]
- public void BUG_GetExactMatchConcepts_Returns_Empty_Despite_Declared()
+ public void GetExactMatchConcepts_RoundTrips_The_Match_After_Fix()
{
var adapter = NewAdapter();
var c1 = Res("EN");
var c2 = Res("FR");
adapter.DeclareExactMatchConcepts(c1, c2);
- adapter.GetExactMatchConcepts(c1).Should().BeEmpty("[BUG] RDFResource.Equals(string)");
+ adapter.GetExactMatchConcepts(c1)
+ .Should().NotBeEmpty("exactMatch must be retrievable after the fix")
+ .And.ContainSingle(c => c.ToString() == Ns + "FR");
}
// ─────────────────────────────────────────────────────────────────────────────
@@ -190,9 +208,8 @@ public void Constructor_Produces_An_Adapter_With_The_Declared_Namespace_Uri()
public void DeclareConcept_Appends_AnnotationAxioms_To_The_Ontology()
{
// The WRITE side is correct: DeclareConcept adds real annotation axioms to the
- // ontology (verifiable by inspecting the underlying graph directly, bypassing the
- // broken reader). This is why ToFileAsync produces a valid ontology despite the
- // readers being broken.
+ // ontology (verifiable by inspecting the underlying graph directly). These write-path
+ // tests guard that the reader fix did not regress the graph construction.
var adapter = NewAdapter();
var scheme = Res("Scheme");
var concept = Res("C1");
@@ -254,11 +271,11 @@ public void AnnotateConceptPreferredLabel_Appends_The_PrefLabel_Axiom()
}
[Fact]
- public void BUG_CheckHasClass_Returns_False_Despite_Class_Declared()
+ public void CheckHasClass_Finds_The_Declared_Class_After_Fix()
{
- // CheckHasClass uses DeclarationAxioms and compares cls.GetIRI().Equals(resource.URI)
- // where .URI is a System.Uri. RDFResource.Equals(Uri) is false by type-mismatch — the
- // SAME root cause as the annotation readers. So CheckHasClass is also broken.
+ // CheckHasClass uses DeclarationAxioms and compared cls.GetIRI().Equals(resource.URI)
+ // where .URI is a System.Uri — RDFResource.Equals(Uri) was false by type-mismatch (the
+ // SAME root cause as the annotation readers). After the fix it compares RDFResource.Equals(RDFResource).
var adapter = NewAdapter();
var declared = Res("DeclaredClass");
adapter.DeclareClass(declared);
@@ -267,9 +284,11 @@ public void BUG_CheckHasClass_Returns_False_Despite_Class_Declared()
var onto = adapter.GetOntology();
onto.DeclarationAxioms.Should().NotBeEmpty("DeclareClass wrote the declaration axiom");
- // ...but CheckHasClass can't find it:
- adapter.CheckHasClass(declared).Should().BeFalse(
- "[BUG] cls.GetIRI().Equals(resource.URI) compares RDFResource.Equals(Uri) → false by type-mismatch");
+ // ...and CheckHasClass now finds it:
+ adapter.CheckHasClass(declared).Should().BeTrue(
+ "CheckHasClass must match the declared class after the fix (RDFResource.Equals(RDFResource))");
+ adapter.CheckHasClass(Res("UndeclaredClass")).Should().BeFalse(
+ "an undeclared class is not found");
}
[Fact]
diff --git a/Generation/Converters/Argumentum.AssetConverter/Ontology/OwlAdapter.cs b/Generation/Converters/Argumentum.AssetConverter/Ontology/OwlAdapter.cs
index ca8a485c4..b17729c3e 100644
--- a/Generation/Converters/Argumentum.AssetConverter/Ontology/OwlAdapter.cs
+++ b/Generation/Converters/Argumentum.AssetConverter/Ontology/OwlAdapter.cs
@@ -282,15 +282,15 @@ public bool CheckIsNarrowerConcept(RDFResource concept, RDFResource parentConcep
{
try
{
- return SKOSHelper.CheckHasNarrowerConcept(_ontology, parentConcept, concept);
- }
- catch
- {
- return _ontology.AnnotationAxioms.OfType()
- .Any(a => a.AnnotationProperty.GetIRI().Equals(SKOSVocabulary.Narrower.URI)
- && a.SubjectIRI.Equals(parentConcept.URI)
- && a.ValueIRI != null && a.ValueIRI.Equals(concept.URI));
+ if (SKOSHelper.CheckHasNarrowerConcept(_ontology, parentConcept, concept)) return true;
}
+ catch { }
+ // SKOSHelper may return false silently (no exception) — fall back to annotation scanning.
+ // The annotation scanner now uses .ToString() comparison (RDFResource type-mismatch fix).
+ return _ontology.AnnotationAxioms.OfType()
+ .Any(a => a.AnnotationProperty.GetIRI().ToString() == SKOSVocabulary.Narrower.ToString()
+ && a.SubjectIRI.ToString() == parentConcept.ToString()
+ && a.ValueIRI != null && a.ValueIRI.ToString() == concept.ToString());
}
public List GetConceptPreferredLabels(RDFResource concept)
@@ -311,27 +311,43 @@ public List GetConceptDocumentation(RDFResource concept, SKOSDo
public List GetExactMatchConcepts(RDFResource concept)
{
- try { return SKOSHelper.GetExactMatchConcepts(_ontology, concept); }
- catch { return GetResourceAnnotations(concept, SKOSVocabulary.ExactMatch); }
+ try
+ {
+ var result = SKOSHelper.GetExactMatchConcepts(_ontology, concept);
+ if (result != null && result.Count > 0) return result;
+ }
+ catch { }
+ // SKOSHelper may return empty silently — fall back to annotation scanning (.ToString() fix).
+ return GetResourceAnnotations(concept, SKOSVocabulary.ExactMatch);
}
public List GetCloseMatchConcepts(RDFResource concept)
{
- try { return SKOSHelper.GetCloseMatchConcepts(_ontology, concept); }
- catch { return GetResourceAnnotations(concept, SKOSVocabulary.CloseMatch); }
+ try
+ {
+ var result = SKOSHelper.GetCloseMatchConcepts(_ontology, concept);
+ if (result != null && result.Count > 0) return result;
+ }
+ catch { }
+ return GetResourceAnnotations(concept, SKOSVocabulary.CloseMatch);
}
public List GetRelatedMatchConcepts(RDFResource concept)
{
- try { return SKOSHelper.GetRelatedMatchConcepts(_ontology, concept); }
- catch { return GetResourceAnnotations(concept, SKOSVocabulary.RelatedMatch); }
+ try
+ {
+ var result = SKOSHelper.GetRelatedMatchConcepts(_ontology, concept);
+ if (result != null && result.Count > 0) return result;
+ }
+ catch { }
+ return GetResourceAnnotations(concept, SKOSVocabulary.RelatedMatch);
}
private List GetAnnotationSubjects(RDFResource typeResource)
{
return _ontology.AnnotationAxioms.OfType()
- .Where(a => a.AnnotationProperty.GetIRI().Equals(RDFVocabulary.RDF.TYPE.URI)
- && a.ValueIRI != null && a.ValueIRI.Equals(typeResource.URI))
+ .Where(a => a.AnnotationProperty.GetIRI().ToString() == RDFVocabulary.RDF.TYPE.ToString()
+ && a.ValueIRI != null && a.ValueIRI.ToString() == typeResource.ToString())
.Select(a => new RDFResource(a.SubjectIRI.ToString()))
.ToList();
}
@@ -339,7 +355,7 @@ private List GetAnnotationSubjects(RDFResource typeResource)
private List GetAnnotationObjects(RDFResource property)
{
return _ontology.AnnotationAxioms.OfType()
- .Where(a => a.AnnotationProperty.GetIRI().Equals(property.URI)
+ .Where(a => a.AnnotationProperty.GetIRI().ToString() == property.ToString()
&& a.ValueIRI != null)
.Select(a => new RDFResource(a.ValueIRI.ToString()))
.ToList();
@@ -348,8 +364,8 @@ private List GetAnnotationObjects(RDFResource property)
private List GetResourceAnnotations(RDFResource subject, RDFResource property)
{
return _ontology.AnnotationAxioms.OfType()
- .Where(a => a.AnnotationProperty.GetIRI().Equals(property.URI)
- && a.SubjectIRI.Equals(subject.URI)
+ .Where(a => a.AnnotationProperty.GetIRI().ToString() == property.ToString()
+ && a.SubjectIRI.ToString() == subject.ToString()
&& a.ValueIRI != null)
.Select(a => new RDFResource(a.ValueIRI.ToString()))
.ToList();
@@ -358,8 +374,8 @@ private List GetResourceAnnotations(RDFResource subject, RDFResourc
private List GetLiteralAnnotations(RDFResource subject, RDFResource property)
{
return _ontology.AnnotationAxioms.OfType()
- .Where(a => a.AnnotationProperty.GetIRI().Equals(property.URI)
- && a.SubjectIRI.Equals(subject.URI)
+ .Where(a => a.AnnotationProperty.GetIRI().ToString() == property.ToString()
+ && a.SubjectIRI.ToString() == subject.ToString()
&& a.ValueLiteral != null)
.Select(a => {
var literal = a.ValueLiteral.GetLiteral();
@@ -370,7 +386,7 @@ private List GetLiteralAnnotations(RDFResource subject, RDFReso
public bool CheckHasClass(RDFResource resource)
{
- return _ontology.DeclarationAxioms.Any(ax => ax.Entity is OWLClass cls && cls.GetIRI().Equals(resource.URI));
+ return _ontology.DeclarationAxioms.Any(ax => ax.Entity is OWLClass cls && cls.GetIRI().ToString() == resource.ToString());
}
public OWLOntology GetOntology()
@@ -386,9 +402,9 @@ public List GetResourcesByType(RDFResource typeResource)
public bool HasAnnotation(RDFResource subject, RDFResource property, RDFResource value)
{
return _ontology.AnnotationAxioms.OfType()
- .Any(a => a.AnnotationProperty.GetIRI().Equals(property.URI)
- && a.SubjectIRI.Equals(subject.URI)
- && a.ValueIRI != null && a.ValueIRI.Equals(value.URI));
+ .Any(a => a.AnnotationProperty.GetIRI().ToString() == property.ToString()
+ && a.SubjectIRI.ToString() == subject.ToString()
+ && a.ValueIRI != null && a.ValueIRI.ToString() == value.ToString());
}
}
}
\ No newline at end of file