fix(ontology): preserve #-terminated namespaces in SHACLGenerator base_uri (#1082) (#1084)

* fix(ontology): preserve #-terminated namespaces in SHACLGenerator base_uri (#1082)

SHACLGenerator.__init__ normalized base_uri with rstrip('/') + '/', turning a #-terminated RDF namespace (e.g. http://example.org/manufacturing#) into ...#/. Every generated URI then landed in a different namespace than the instance data, so SHACL validation silently passed because the shapes targeted nothing.

__init__ now preserves a base_uri already ending in '/' or '#', matching the #-aware normalization generate() already applies. shapes_uri inherits the fix.

Adds test_hash_namespace_base_uri_is_not_mangled (fails on the old normalization), plus a CHANGELOG entry. Full ontology suite green.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(ontology): collapse slash runs, only preserve #-terminated base_uri

Qodo review caught that preserving any endswith('/') base left redundant
trailing slashes (e.g. .../ns////) intact, leaking a different namespace
into emitted IRIs. Now only '#'-terminated bases are kept verbatim; slash
runs are collapsed to a single '/', matching generate() normalization.

Adds test_slash_run_normalization_regression.

---------

Co-authored-by: changshenhan <217217832+changshenhan@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
changshenhan
2026-08-26 21:11:07 +05:00
committed by GitHub
co-authored by changshenhan Claude
parent 59af023447
commit af3308ad06
3 changed files with 35 additions and 1 deletions
+4
View File
@@ -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
+10 -1
View File
@@ -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
+21
View File
@@ -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")