From dd694d96e451012729733bc92a2a0f400d6ce0eb Mon Sep 17 00:00:00 2001 From: RyutoYoda Date: Mon, 28 Sep 2026 11:33:09 +0900 Subject: [PATCH 1/2] fix(ontology): carry iri and prefixes through the converters `iri` on concepts and relationships and the root `prefixes` map are defined in `ontology/ontology.json` and documented in `ontology/ontology.md`, but the converters never learned about them: a document that uses them fails to parse, and adding the fields to the DTOs alone is not enough because `model.py` has nowhere to hold them, so the values are dropped on the way through. Add the fields to `spec.py`, hold them on `Concept`, `Relationship` and `OssieOntology`, and wire both converters so a document survives parse -> convert -> dump_yaml unchanged. All three are optional, so a document that declares none of them dumps exactly as before. The `iri` string is stored as written; expanding a QName against `prefixes` is left to whichever converter emits IRIs. --- .../converter/ossie_to_spec/converter.py | 3 + .../converter/spec_to_ossie/converter.py | 3 + .../ontology/src/ossie_ontology/model.py | 24 ++++++- .../ontology/src/ossie_ontology/spec.py | 3 + .../ontology/tests/test_ossie_parser.py | 65 +++++++++++++++++++ 5 files changed, 97 insertions(+), 1 deletion(-) diff --git a/converters/ontology/src/ossie_ontology/converter/ossie_to_spec/converter.py b/converters/ontology/src/ossie_ontology/converter/ossie_to_spec/converter.py index b63bf9a47..c367285f5 100644 --- a/converters/ontology/src/ossie_ontology/converter/ossie_to_spec/converter.py +++ b/converters/ontology/src/ossie_ontology/converter/ossie_to_spec/converter.py @@ -79,6 +79,7 @@ def convert(model: OssieOntology) -> OssieSpec: requires=[f.raw_expr for f in ont.requires], ontology=_convert_ontology_concepts(ont), ontology_mappings=ontology_mappings, + prefixes=model.prefixes, ) @@ -117,6 +118,7 @@ def _convert_concept(concept: Concept, rels: list[Relationship]) -> ConceptCompo derived_by=[f.raw_expr for f in concept.derived_by], requires=[f.raw_expr for f in concept.requires], relationships=[_convert_relationship(rel) for rel in rels], + iri=concept.iri, ) @@ -135,6 +137,7 @@ def _convert_relationship(rel: Relationship) -> SpecRelationship: multiplicity=multiplicity, # type: ignore[arg-type] derived_by=[f.raw_expr for f in rel.derived_by], requires=[f.raw_expr for f in rel.requires], + iri=rel.iri, ) diff --git a/converters/ontology/src/ossie_ontology/converter/spec_to_ossie/converter.py b/converters/ontology/src/ossie_ontology/converter/spec_to_ossie/converter.py index 75992421b..776a2dcae 100644 --- a/converters/ontology/src/ossie_ontology/converter/spec_to_ossie/converter.py +++ b/converters/ontology/src/ossie_ontology/converter/spec_to_ossie/converter.py @@ -99,6 +99,7 @@ def convert(self, spec: OssieSpec) -> OssieOntology: description=spec.description, ai_context=spec.ai_context, version=spec.version, + prefixes=spec.prefixes, ) self._populate_ontology(ontology, spec) @@ -131,6 +132,7 @@ def _populate_ontology(self, ontology: OntologyComponent, spec: OssieSpec) -> No type=ConceptType.from_value(concept_spec.type), description=concept_spec.description, extends=extends, + iri=concept_spec.iri, ) ) @@ -219,6 +221,7 @@ def _convert_relationship( description=rel_spec.description, verbalizes=list(rel_spec.verbalizes) if rel_spec.verbalizes else None, multiplicity=multiplicity, + iri=rel_spec.iri, ) ontology.add_relationship(relationship) diff --git a/converters/ontology/src/ossie_ontology/model.py b/converters/ontology/src/ossie_ontology/model.py index 21c722288..9badc8837 100644 --- a/converters/ontology/src/ossie_ontology/model.py +++ b/converters/ontology/src/ossie_ontology/model.py @@ -98,6 +98,7 @@ class Concept: _derived_by: list[Formula] _requires: list[Formula] _is_component: bool + _iri: str | None def __init__( self, @@ -109,7 +110,8 @@ def __init__( identify_by: dict[str, Relationship] | None = None, derived_by: list[Formula] | None = None, requires: list[Formula] | None = None, - is_component: bool = True + is_component: bool = True, + iri: str | None = None, ): self._name = name self._type = type @@ -120,6 +122,7 @@ def __init__( self._derived_by = derived_by if derived_by else [] self._requires = requires if requires else [] self._is_component = is_component + self._iri = iri def add_require(self, require: Formula) -> None: self._requires.append(require) @@ -145,6 +148,10 @@ def type(self) -> ConceptType | None: def description(self) -> str | None: return self._description + @property + def iri(self) -> str | None: + return self._iri + @property def is_builtin(self) -> bool: return self._builtin @@ -207,6 +214,7 @@ class Relationship: _multiplicity: RelationshipMultiplicity | None _derived_by: list[Formula] _requires: list[Formula] + _iri: str | None def __init__( self, @@ -216,6 +224,7 @@ def __init__( description: str | None = None, verbalizes: list[str] | None = None, multiplicity: RelationshipMultiplicity | None = None, + iri: str | None = None, ): self._name = name self._container = container @@ -228,6 +237,7 @@ def __init__( self._verbalizations = parse_verbalizations(self, verbalizes) self._derived_by = [] self._requires = [] + self._iri = iri @property def name(self) -> str: @@ -245,6 +255,10 @@ def container(self) -> Concept: def description(self) -> str | None: return self._description + @property + def iri(self) -> str | None: + return self._iri + @property def signature(self) -> list[Concept]: return [role.player for role in self._roles] @@ -1016,6 +1030,7 @@ class OssieOntology: _ontology: OntologyComponent _ontology_mappings: list[OntologyMapping] _ontology_mapping_index: dict[str, OntologyMapping] + _prefixes: dict[str, str] def __init__( self, @@ -1024,6 +1039,7 @@ def __init__( description: str | None = None, ai_context: AiContext | None = None, version: str | None = None, + prefixes: dict[str, str] | None = None, ): self._name = name self._description = description @@ -1032,6 +1048,7 @@ def __init__( self._ontology = ontology self._ontology_mappings = [] self._ontology_mapping_index = {} + self._prefixes = dict(prefixes) if prefixes else {} @property def name(self) -> str: @@ -1063,6 +1080,11 @@ def add_ontology_mapping(self, mapping: OntologyMapping) -> None: def ontology_mappings(self) -> list[OntologyMapping]: return list(self._ontology_mappings) + @property + def prefixes(self) -> dict[str, str]: + """Namespace prefixes the document's QName `iri` values resolve against.""" + return dict(self._prefixes) + # --------------------------------------------------------------------------- # Verbalization parser (handles a list of verbalization patterns) diff --git a/converters/ontology/src/ossie_ontology/spec.py b/converters/ontology/src/ossie_ontology/spec.py index f2a59012f..fe81eaa3f 100644 --- a/converters/ontology/src/ossie_ontology/spec.py +++ b/converters/ontology/src/ossie_ontology/spec.py @@ -72,6 +72,7 @@ class Relationship(OssieObject): """ name: str description: str | None = None + iri: str | None = None roles: list[Role] = Field(default_factory=list) verbalizes: list[str] = Field(default_factory=list) multiplicity: Literal["OneToOne", "ManyToOne"] | None = None @@ -100,6 +101,7 @@ class ConceptComponent(OssieObject): concept: str type: Literal["EntityType", "ValueType"] | None = None description: str | None = None + iri: str | None = None extends: list[str] | None = None identify_by: list[str] = Field(default_factory=list) derived_by: list[str] = Field(default_factory=list) @@ -247,6 +249,7 @@ class OssieSpec(OssieObject): ai_context: AiContext | None = None ontology: list[ConceptComponent] = Field(default_factory=list) ontology_mappings: list[OntologyMapping] = Field(default_factory=list) + prefixes: dict[str, str] = Field(default_factory=dict) @classmethod def load_yaml(cls, text: str) -> OssieSpec: diff --git a/converters/ontology/tests/test_ossie_parser.py b/converters/ontology/tests/test_ossie_parser.py index d23808a2e..91b1976f2 100644 --- a/converters/ontology/tests/test_ossie_parser.py +++ b/converters/ontology/tests/test_ossie_parser.py @@ -356,3 +356,68 @@ def test_relationships_are_grouped_under_their_own_container(): "Alpha": ["a1", "a2"], "Beta": ["b1", "b2"], } + + +# ----- Global identifiers ----------------------------------------------- + +_IRI_SPEC = { + "version": "0.2.0.dev0", + "name": "Demo", + "prefixes": {"foaf": "http://xmlns.com/foaf/0.1/"}, + "ontology": [ + { + "concept": "Person", + "type": "EntityType", + "iri": "foaf:Person", + "identify_by": ["person_name"], + "relationships": [ + { + "name": "person_name", + "iri": "foaf:name", + "roles": [{"concept": "String"}], + "verbalizes": ["{Person} is identified by {String}"], + "multiplicity": "OneToOne", + } + ], + } + ], +} + + +def test_iri_and_prefixes_survive_a_round_trip(tmp_path: Path): + """A document that pins its concepts to an external vocabulary must come + back out carrying the same pins. + + `iri` and `prefixes` are what tie an Ossie ontology to RDF/OWL vocabularies, + so dropping them silently turns a mapped ontology into an unmapped one. + """ + path = tmp_path / "spec.yaml" + path.write_text(yaml.safe_dump(_IRI_SPEC)) + + model = OssieParser().parse(path) + + assert model.prefixes == {"foaf": "http://xmlns.com/foaf/0.1/"} + person = model.ontology.lookup_concept("Person") + assert person is not None + assert person.iri == "foaf:Person" + assert model.ontology.lookup_concept_relationship(person, "person_name").iri == "foaf:name" + + roundtrip_path = tmp_path / "roundtrip.yaml" + roundtrip_path.write_text(OssieToSpecConverter.convert(model).dump_yaml()) + reparsed = OssieParser().parse(roundtrip_path) + + assert reparsed.prefixes == model.prefixes + reparsed_person = reparsed.ontology.lookup_concept("Person") + assert reparsed_person.iri == "foaf:Person" + assert reparsed.ontology.lookup_concept_relationship(reparsed_person, "person_name").iri == "foaf:name" + + +def test_a_document_without_iris_dumps_neither_field(tmp_path: Path): + """Both fields are optional, so a document that declares neither must dump + exactly as it did before they existed.""" + path = _write_spec(tmp_path, [_concept("Gadget")]) + + dumped = OssieToSpecConverter.convert(OssieParser().parse(path)).dump_yaml() + + assert "iri:" not in dumped + assert "prefixes:" not in dumped From e6b18731f5646308a08b53092258454291dab476 Mon Sep 17 00:00:00 2001 From: RyutoYoda Date: Mon, 28 Sep 2026 14:28:22 +0900 Subject: [PATCH 2/2] fix(ontology): declare prefixes before the concepts that use it `prefixes` was the last field on `OssieSpec`, so a round trip put the map after `ontology`, below the QNames that resolve against it. The example in `ontology/ontology.md` declares it right after `name`. Move the field up so dumps keep that layout. Field order only; a document that declares no `prefixes` dumps byte-identical as before. --- converters/ontology/src/ossie_ontology/spec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/converters/ontology/src/ossie_ontology/spec.py b/converters/ontology/src/ossie_ontology/spec.py index fe81eaa3f..637495c96 100644 --- a/converters/ontology/src/ossie_ontology/spec.py +++ b/converters/ontology/src/ossie_ontology/spec.py @@ -245,11 +245,11 @@ class OssieSpec(OssieObject): version: str | None = None name: str description: str | None = None + prefixes: dict[str, str] = Field(default_factory=dict) requires: list[str] = Field(default_factory=list) ai_context: AiContext | None = None ontology: list[ConceptComponent] = Field(default_factory=list) ontology_mappings: list[OntologyMapping] = Field(default_factory=list) - prefixes: dict[str, str] = Field(default_factory=dict) @classmethod def load_yaml(cls, text: str) -> OssieSpec: