diff --git a/CHANGELOG.md b/CHANGELOG.md index ffbaa7be..3910a3f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -132,6 +132,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New `tests/export/test_timestamp_timezones.py` and `tests/provenance/test_timestamp_timezones.py`: offset presence on every export and provenance path, PROV-O literals valid as `xsd:dateTimeStamp`, comparison against a timezone-aware instant without `TypeError`, the Oxigraph filter that dropped the naive value (with a bound inside the indeterminate window, so the test cannot pass by accident), and the document `@id` remaining a valid IRI with `+00:00` in it. 11 of the 13 fail on the parent commit - **Fixed during review** (Qodo): once new entries carry `+00:00` and stored ones do not, `ProvenanceManager.query_recorded_between` and `audit_log` compared ISO timestamps as raw strings, so they ordered by spelling rather than by instant — an inclusive naive bound naming a stored offset-bearing timestamp sorted *below* it and dropped the record, and a bound written in another offset landed wherever its digits fell (`19:45+05:30` is 14:15Z, but sorted after 14:19Z). Both now compare instants through a new `to_utc_datetime()` helper that reads a missing offset as UTC, which is what the values written before this change actually were; a bound that cannot be read as a timestamp keeps the historical string comparison rather than raising on a call that used to work - The remaining 147 naive call sites are in `context/`, `vector_store/`, `seed/` and elsewhere, where timestamps are compared against values parsed from previously stored naive strings. Converting those without a read-side migration would raise `TypeError: can't compare offset-naive and offset-aware datetimes` on existing data, so they are deliberately left for a separate change +- **`SHACLGenerator` mangles `#`-terminated namespaces into `#/`, so generated shapes target nothing** (#1082) by @changshenhan + - `__init__` normalized `base_uri` with `rstrip("/") + "/"`, which turns `http://example.org/manufacturing#` into `...manufacturing#/` — the most common RDF namespace convention. Every generated URI (`sh:targetClass`, `sh:path`, shape URIs) then landed in a different namespace than the instance data, and SHACL validation silently passed because the shapes targeted nothing + - `__init__` now preserves a namespace already ending in `/` or `#`, matching the `#`-aware normalization `generate()` already applies; `shapes_uri` inherits the fix + - New `test_hash_namespace_base_uri_is_not_mangled` in `tests/ontology/test_ontology_advanced.py` fails on the pre-fix normalization and passes with it; full ontology suite (76 tests) green - **`split`/chunking paths bypassed the centralized spaCy model cache, reloading the model on every call** (#1042, closes #998) by @Accute9, reviewed by @Sameer6305 - `semantica/split/methods.py`'s `split_by_sentences()` and `semantica/split/semantic_chunker.py`'s `SemanticChunker.__init__` each called `spacy.load()` directly instead of reusing the process-level cache added in #889/`semantic_extract/methods.py`'s `load_spacy_model()` — every call/construction re-paid the ~120ms model-load cost independently of `NERExtractor`, which already used the cache diff --git a/semantica/ontology/ontology_generator.py b/semantica/ontology/ontology_generator.py index 722b154a..73c73a5f 100644 --- a/semantica/ontology/ontology_generator.py +++ b/semantica/ontology/ontology_generator.py @@ -882,7 +882,16 @@ class SHACLGenerator: """ self.logger = get_logger("ontology_shacl") self.progress_tracker = get_progress_tracker() - self.base_uri = base_uri.rstrip("/") + "/" + # Preserve an RDF namespace that already ends in `#` (the common + # convention for vocabularies): `...manufacturing#` must not become + # `...manufacturing#/`, or every generated URI lands in the wrong + # namespace and SHACL validation silently targets nothing. Matches + # the `#`-aware normalization in `generate()`. Slash-terminated + # bases are collapsed to a single trailing `/` so redundant runs + # (`.../ns////`) cannot leak a different namespace into emitted IRIs. + self.base_uri = ( + base_uri if base_uri.endswith("#") else base_uri.rstrip("/") + "/" + ) self.shapes_uri = shapes_uri or (self.base_uri + "shapes") self.include_inherited = include_inherited self.severity = severity diff --git a/tests/ontology/test_ontology_advanced.py b/tests/ontology/test_ontology_advanced.py index dfdde003..cf77dbe0 100644 --- a/tests/ontology/test_ontology_advanced.py +++ b/tests/ontology/test_ontology_advanced.py @@ -340,6 +340,27 @@ class TestSHACLHierarchicalAndValidation(unittest.TestCase): ttl = gen.serialize(graph, format="turtle") self.assertIn("myorg.com", ttl) + # 24a — an RDF namespace ending in `#` must not gain a trailing `/`, + # or every generated URI lands in a different namespace and SHACL + # validation silently targets nothing. + def test_hash_namespace_base_uri_is_not_mangled(self): + gen = self._make_gen(base_uri="http://example.org/manufacturing#") + self.assertEqual(gen.base_uri, "http://example.org/manufacturing#") + self.assertEqual(gen.shapes_uri, "http://example.org/manufacturing#shapes") + + graph = gen.generate(self._HIER_ONTOLOGY) + ttl = gen.serialize(graph, format="turtle") + self.assertNotIn("#/", ttl) + self.assertIn("manufacturing#", ttl) + + # 24b — a `#`-terminated base is preserved verbatim, while slash runs + # are collapsed: Qodo review caught that `endswith(("/","#"))` left + # `.../ns////` intact, leaking a different namespace into emitted IRIs. + def test_slash_run_normalization_regression(self): + gen = self._make_gen(base_uri="http://example.org/ns////") + self.assertEqual(gen.base_uri, "http://example.org/ns/") + self.assertEqual(gen.shapes_uri, "http://example.org/ns/shapes") + # 25 def test_severity_warning(self): gen = self._make_gen(severity="Warning")